feat(L03): add blinker and synchronous logic card
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
|
||||
const source = JSON.parse(await readFile(new URL('../json/card_source.json', import.meta.url), 'utf8'));
|
||||
|
||||
test('L03 has stable identity and three ready tasks', () => {
|
||||
assert.equal(source.card.series, 'rv32i-asm');
|
||||
assert.equal(source.card.number, '03');
|
||||
assert.equal(source.card.uuid, '99190b6d-b15a-5757-8794-edd283c40f93');
|
||||
assert.deepEqual(source.tasks_order, ['task01', 'task02', 'task03']);
|
||||
assert.deepEqual(source.front_page_scope.scope_table.rows.map(row => row.status), ['ready', 'ready', 'ready']);
|
||||
});
|
||||
|
||||
test('each task follows predict, measure and evidence contract', () => {
|
||||
const text = JSON.stringify(source);
|
||||
for (const expected of ['clock enable', 'VCD', 'wrap=1,2']) assert.match(text, new RegExp(expected, 'i'));
|
||||
for (const task of Object.values(source.tasks)) {
|
||||
assert.ok(task.criterion.length > 10);
|
||||
assert.equal(task.assessment_criterion_ref, 'RV03.KW01');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { CardStateDatabase } from '../scripts/lib/card_state_db.mjs';
|
||||
|
||||
test('card state survives closing and reopening SQLite', async () => {
|
||||
const directory = await mkdtemp(path.join(os.tmpdir(), 'stem-card-state-'));
|
||||
const file = path.join(directory, 'state.sqlite3');
|
||||
const state = {
|
||||
schema: 'stem-card-navigation.v2',
|
||||
revision: 7,
|
||||
selected_at: '2026-07-17T12:00:00.000Z',
|
||||
snapshot_ref: 'task04.alloc5.cursor'
|
||||
};
|
||||
|
||||
try {
|
||||
const first = new CardStateDatabase(file);
|
||||
first.write('navigation', state);
|
||||
first.close();
|
||||
|
||||
const second = new CardStateDatabase(file);
|
||||
assert.deepEqual(second.read('navigation'), state);
|
||||
assert.equal(second.read('missing'), null);
|
||||
second.close();
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('audit log is append-only, ordered and keeps navigation timestamps', async () => {
|
||||
const directory = await mkdtemp(path.join(os.tmpdir(), 'stem-card-audit-'));
|
||||
const file = path.join(directory, 'state.sqlite3');
|
||||
try {
|
||||
const database = new CardStateDatabase(file);
|
||||
const navigation = {
|
||||
task: { id: 'task04' },
|
||||
block: { id: 'allocator-flow' },
|
||||
phase: { id: 'alloc5' },
|
||||
step: { id: 'alloc5-commit' },
|
||||
snapshot_ref: 'task04.alloc5.commit'
|
||||
};
|
||||
const first = database.appendEvent({
|
||||
eventType: 'navigation.activate',
|
||||
actor: 'nvim',
|
||||
navigation,
|
||||
occurredAt: '2026-07-17T12:00:00.000Z',
|
||||
payload: { source: 'F2' }
|
||||
});
|
||||
const second = database.appendEvent({
|
||||
eventType: 'checkpoint.ready',
|
||||
actor: 'nvim',
|
||||
navigation,
|
||||
occurredAt: '2026-07-17T12:00:01.000Z'
|
||||
});
|
||||
assert.equal(second.id, first.id + 1);
|
||||
assert.deepEqual(
|
||||
database.listEvents({ afterId: first.id, limit: 10 }).map(event => event.event_type),
|
||||
['checkpoint.ready']
|
||||
);
|
||||
assert.equal(database.listEvents()[0].snapshot_ref, navigation.snapshot_ref);
|
||||
database.close();
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Independent golden models for L03; no RTL implementation is imported."""
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def task01() -> None:
|
||||
counter = 0
|
||||
previous = 0
|
||||
pulses = 0
|
||||
consecutive = 0
|
||||
for _ in range(16):
|
||||
enable = int(counter == 3)
|
||||
counter = 0 if enable else counter + 1
|
||||
pulses += enable
|
||||
consecutive += int(enable and previous)
|
||||
previous = enable
|
||||
assert (pulses, consecutive, counter) == (4, 0, 0)
|
||||
print("PASS task01 pulses=4 consecutive=0 final_count=0")
|
||||
|
||||
|
||||
def task02() -> None:
|
||||
sequence = []
|
||||
counter = 0
|
||||
for _ in range(16):
|
||||
counter = (counter + 1) & 0xF
|
||||
sequence.append((counter >> 2) & 1)
|
||||
edges = [index + 1 for index in range(16) if sequence[index] != (sequence[index - 1] if index else 0)]
|
||||
assert edges == [4, 8, 12, 16]
|
||||
print("PASS task02 led_edges=4,8,12,16 period=8")
|
||||
|
||||
|
||||
def task03() -> None:
|
||||
pattern = [0x1, 0x2, 0x4, 0x8, 0x8, 0x4, 0x2, 0x1]
|
||||
observed = [pattern[index & 7] for index in range(10)]
|
||||
assert observed == [1, 2, 4, 8, 8, 4, 2, 1, 1, 2]
|
||||
print("PASS task03 pattern=1,2,4,8,8,4,2,1 wrap=1,2")
|
||||
|
||||
|
||||
TASKS = {
|
||||
"task01_clock_enable": task01,
|
||||
"task02_counter_blinker": task02,
|
||||
"task03_pattern_rom": task03,
|
||||
}
|
||||
|
||||
|
||||
if len(sys.argv) != 2 or sys.argv[1] not in TASKS:
|
||||
raise SystemExit(f"usage: {sys.argv[0]} {'|'.join(TASKS)}")
|
||||
TASKS[sys.argv[1]]()
|
||||
@@ -0,0 +1,33 @@
|
||||
module tb;
|
||||
logic clk = 0;
|
||||
logic rst = 1;
|
||||
logic enable;
|
||||
logic [1:0] count;
|
||||
integer pulses = 0;
|
||||
integer consecutive = 0;
|
||||
logic previous = 0;
|
||||
string trace_file;
|
||||
|
||||
clock_enable dut (.*);
|
||||
always #1 clk = ~clk;
|
||||
|
||||
initial begin
|
||||
if ($value$plusargs("trace=%s", trace_file)) begin
|
||||
$dumpfile(trace_file);
|
||||
$dumpvars(0, tb);
|
||||
end
|
||||
repeat (2) @(posedge clk);
|
||||
@(negedge clk);
|
||||
rst = 0;
|
||||
repeat (16) begin
|
||||
@(posedge clk); #1;
|
||||
if (enable) pulses += 1;
|
||||
if (enable && previous) consecutive += 1;
|
||||
previous = enable;
|
||||
end
|
||||
if (pulses != 4 || consecutive != 0 || count != 0)
|
||||
$fatal(1, "FAIL task01 pulses=%0d consecutive=%0d count=%0d", pulses, consecutive, count);
|
||||
$display("PASS task01 pulses=4 consecutive=0 final_count=0");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,36 @@
|
||||
module tb;
|
||||
logic clk = 0;
|
||||
logic rst = 1;
|
||||
logic led;
|
||||
logic [3:0] count;
|
||||
integer edge_count = 0;
|
||||
integer expected_edges [0:3] = '{4, 8, 12, 16};
|
||||
logic previous = 0;
|
||||
string trace_file;
|
||||
|
||||
counter_blinker dut (.*);
|
||||
always #1 clk = ~clk;
|
||||
|
||||
initial begin
|
||||
if ($value$plusargs("trace=%s", trace_file)) begin
|
||||
$dumpfile(trace_file);
|
||||
$dumpvars(0, tb);
|
||||
end
|
||||
repeat (2) @(posedge clk);
|
||||
@(negedge clk);
|
||||
rst = 0;
|
||||
for (integer cycle = 1; cycle <= 16; cycle += 1) begin
|
||||
@(posedge clk); #1;
|
||||
if (led != previous) begin
|
||||
if (cycle != expected_edges[edge_count])
|
||||
$fatal(1, "FAIL task02 edge=%0d cycle=%0d", edge_count, cycle);
|
||||
edge_count += 1;
|
||||
end
|
||||
if (led != count[2]) $fatal(1, "FAIL task02 led/count mismatch");
|
||||
previous = led;
|
||||
end
|
||||
if (edge_count != 4) $fatal(1, "FAIL task02 edge_count=%0d", edge_count);
|
||||
$display("PASS task02 led_edges=4,8,12,16 period=8");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,32 @@
|
||||
module tb;
|
||||
logic clk = 0;
|
||||
logic rst = 1;
|
||||
logic step = 0;
|
||||
logic [2:0] phase;
|
||||
logic [3:0] led;
|
||||
logic [3:0] expected [0:9] = '{1, 2, 4, 8, 8, 4, 2, 1, 1, 2};
|
||||
string trace_file;
|
||||
|
||||
pattern_rom dut (.*);
|
||||
always #1 clk = ~clk;
|
||||
|
||||
initial begin
|
||||
if ($value$plusargs("trace=%s", trace_file)) begin
|
||||
$dumpfile(trace_file);
|
||||
$dumpvars(0, tb);
|
||||
end
|
||||
repeat (2) @(posedge clk);
|
||||
@(negedge clk);
|
||||
rst = 0;
|
||||
for (integer index = 0; index < 10; index += 1) begin
|
||||
if (led != expected[index])
|
||||
$fatal(1, "FAIL task03 index=%0d phase=%0d led=%0h expected=%0h", index, phase, led, expected[index]);
|
||||
step = 1;
|
||||
@(posedge clk); #1;
|
||||
step = 0;
|
||||
@(negedge clk);
|
||||
end
|
||||
$display("PASS task03 pattern=1,2,4,8,8,4,2,1 wrap=1,2");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
mode=test
|
||||
if [[ "${1:-}" == "--build-only" || "${1:-}" == "--trace" ]]; then
|
||||
mode="${1#--}"
|
||||
shift
|
||||
fi
|
||||
task="${1:?task name is required}"
|
||||
|
||||
case "$task" in
|
||||
task01_clock_enable|task02_counter_blinker|task03_pattern_rom) ;;
|
||||
*) printf 'Unknown RTL task: %s\n' "$task" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
state="${STEM_STATE_DIR:-$root/.stem/instances/local}"
|
||||
profile="${STEM_PROFILE:-hazard3-sim}"
|
||||
target="${STEM_TARGET:-hazard3-baremetal}"
|
||||
build_dir="$state/build/rtl/$task"
|
||||
artifact_dir="$root/.stem/artifacts/$profile/$target/$task"
|
||||
binary="$build_dir/sim"
|
||||
mkdir -p "$build_dir" "$artifact_dir"
|
||||
|
||||
args=(--binary --timing --assert -Wall -Wno-fatal
|
||||
-Wno-DECLFILENAME -Wno-BLKSEQ -Wno-UNUSEDSIGNAL --top-module tb
|
||||
--Mdir "$build_dir/obj" -o "$binary"
|
||||
"$root/src/tasks/$task.sv" "$root/tests/${task}_tb.sv")
|
||||
if [[ "$mode" == trace ]]; then
|
||||
args+=(--trace)
|
||||
fi
|
||||
verilator "${args[@]}" >/dev/null
|
||||
cp -f "$root/src/tasks/$task.sv" "$artifact_dir/"
|
||||
|
||||
if [[ "$mode" == build-only ]]; then
|
||||
printf 'BUILD %s binary=%s\n' "$task" "$binary"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
trace_file="$artifact_dir/$task.vcd"
|
||||
if [[ "$mode" == trace ]]; then
|
||||
"$binary" "+trace=$trace_file"
|
||||
test -s "$trace_file"
|
||||
printf 'TRACE %s vcd=%s\n' "$task" "$trace_file"
|
||||
else
|
||||
"$binary"
|
||||
fi
|
||||
Reference in New Issue
Block a user