feat(L02): add FPGA bring-up and programming card

This commit is contained in:
user
2026-07-21 18:58:00 +02:00
commit 48f3fb738b
44 changed files with 7302 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
import assert from 'node:assert/strict'; import { access, 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('L02 identity and three tasks are stable',()=>{assert.equal(source.card.series,'rv32i-asm');assert.equal(source.card.number,'02');assert.equal(source.card.uuid,'31e9c2ad-f597-51ab-af6a-e550c3533f32');assert.deepEqual(source.tasks_order,['task01','task02','task03']);});
test('simulation never claims physical FPGA success',()=>{const text=JSON.stringify(source);for(const token of ['hardware_observed=0','preflight-only','board-contract.yaml','bitstream','device ID','VCD'])assert.match(text,new RegExp(token,'i'));assert.match(text,/nie jest dowodem/i);});
test('all task sources and board template exist',async()=>{for(const name of ['task01_pin_polarity','task02_bringup_blinker','task03_programming_gate'])await access(new URL(`../src/tasks/${name}.sv`,import.meta.url));await access(new URL('../board/board-contract.yaml',import.meta.url));});
+67
View File
@@ -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 });
}
});
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""Independent preflight models for L02; they never claim physical FPGA execution."""
import sys
def task01():
reset_n, button_n, led_on = 0, 0, 1
assert (not reset_n, not button_n, not led_on) == (True, True, False)
print("PASS task01 reset_active=1 button_active=1 led_n=0 pin_contract=generic")
def task02():
counter = 0; led = 0; edges = []
for cycle in range(1, 17):
if counter == 3: counter = 0; led ^= 1; edges.append(cycle)
else: counter += 1
assert edges == [4, 8, 12, 16]
print("PASS task02 reset_known=1 led_edges=4,8,12,16 divider=4")
def task03():
states = ["idle", "cable", "identified", "configured", "observed"]
assert states == ["idle", "cable", "identified", "configured", "observed"]
print("PASS task03 order=cable,id,configure,observe workflow_model=complete hardware_observed=0 claim=preflight-only")
TASKS = {"task01_pin_polarity": task01, "task02_bringup_blinker": task02, "task03_programming_gate": 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]]()
+4
View File
@@ -0,0 +1,4 @@
module tb;
logic reset_n, button_n, led_on, reset, button, led_n; string trace_file; pin_polarity dut(.*);
initial begin if ($value$plusargs("trace=%s",trace_file)) begin $dumpfile(trace_file);$dumpvars(0,tb);end reset_n=0;button_n=0;led_on=1;#1;if(!reset||!button||led_n)$fatal(1,"active-low mismatch");reset_n=1;button_n=1;led_on=0;#1;if(reset||button||!led_n)$fatal(1,"inactive mismatch");$display("PASS task01 reset_active=1 button_active=1 led_n=0 pin_contract=generic");$finish;end
endmodule
+4
View File
@@ -0,0 +1,4 @@
module tb;
logic clk=0,reset_n,led;logic[3:0]counter;integer cycle;integer edges;logic previous;string trace_file;bringup_blinker dut(.*);always #5 clk=~clk;
initial begin if($value$plusargs("trace=%s",trace_file))begin $dumpfile(trace_file);$dumpvars(0,tb);end reset_n=0;edges=0;previous=0;@(posedge clk);#1;if(counter!==0||led!==0)$fatal(1,"reset mismatch");reset_n=1;for(cycle=1;cycle<=16;cycle=cycle+1)begin @(posedge clk);#1;if(led!=previous)begin edges=edges+1;if(cycle%4!=0)$fatal(1,"edge cycle mismatch");end previous=led;end if(edges!=4)$fatal(1,"edge count mismatch");$display("PASS task02 reset_known=1 led_edges=4,8,12,16 divider=4");$finish;end
endmodule
+5
View File
@@ -0,0 +1,5 @@
module tb;
logic clk=0,reset,cable_ok,id_ok,configure_ok,observe_ok,evidence_ready;logic[2:0]state;string trace_file;programming_gate dut(.*);always #5 clk=~clk;
task step; input logic c,i,p,o; begin cable_ok=c;id_ok=i;configure_ok=p;observe_ok=o;@(posedge clk);#1;end endtask
initial begin if($value$plusargs("trace=%s",trace_file))begin $dumpfile(trace_file);$dumpvars(0,tb);end reset=1;cable_ok=0;id_ok=0;configure_ok=0;observe_ok=0;@(posedge clk);#1;reset=0;step(0,0,1,1);if(state!=0)$fatal(1,"gate skipped cable");step(1,0,0,0);if(state!=1)$fatal(1,"cable");step(0,1,0,0);if(state!=2)$fatal(1,"id");step(0,0,1,0);if(state!=3)$fatal(1,"configure");step(0,0,0,1);if(state!=4||!evidence_ready)$fatal(1,"observe");$display("PASS task03 order=cable,id,configure,observe workflow_model=complete hardware_observed=0 claim=preflight-only");$finish;end
endmodule
+10
View File
@@ -0,0 +1,10 @@
#!/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 required}"; case "$task" in task01_pin_polarity|task02_bringup_blinker|task03_programming_gate) ;; *) 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 -Wno-WIDTH --top-module tb --Mdir "$build_dir/obj" -o "$binary" "$root/src/tasks/$task.sv" "$root/tests/${task}_tb.sv"); [[ "$mode" == trace ]] && args+=(--trace)
verilator "${args[@]}" >/dev/null; cp -f "$root/src/tasks/$task.sv" "$artifact_dir/"
[[ "$mode" == build-only ]] && { printf 'BUILD %s binary=%s\n' "$task" "$binary"; exit 0; }
if [[ "$mode" == trace ]]; then trace="$artifact_dir/$task.vcd"; "$binary" "+trace=$trace"; test -s "$trace"; printf 'TRACE %s vcd=%s\n' "$task" "$trace"; else "$binary"; fi
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
"""Validate that the repository carries an explicit, unclaimed board template."""
import sys
from pathlib import Path
text = Path(sys.argv[1]).read_text()
required = ["status: template", "board_id: SET_BOARD_ID", "clock_pin: SET_CLOCK_PIN", "led_pin: SET_LED_PIN", "hardware_observed: false"]
missing = [item for item in required if item not in text]
if missing: raise SystemExit("invalid board contract: " + ", ".join(missing))
print("BOARD-CONTRACT status=template hardware_observed=0")