feat(L02): add FPGA bring-up and programming card
This commit is contained in:
@@ -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));});
|
||||
@@ -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
+29
@@ -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]]()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+10
@@ -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
|
||||
Executable
+9
@@ -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")
|
||||
Reference in New Issue
Block a user