feat(L04): add RV32I decoder and register file card
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
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('L04 identity and task contract are stable', () => {
|
||||
assert.equal(source.card.number, '04');
|
||||
assert.equal(source.card.uuid, 'f95c41db-8abd-5623-b40d-743cc19eb469');
|
||||
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('decoder safety invariants are explicit', () => {
|
||||
const text = JSON.stringify(source);
|
||||
for (const token of ['sign extension', 'illegal', 'x0', 'VCD']) assert.match(text, new RegExp(token, 'i'));
|
||||
for (const task of Object.values(source.tasks)) assert.equal(task.assessment_criterion_ref, 'RV04.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
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
|
||||
def task01() -> None:
|
||||
instr = 0xFFC30293
|
||||
opcode = instr & 0x7F
|
||||
rd = (instr >> 7) & 0x1F
|
||||
funct3 = (instr >> 12) & 0x7
|
||||
rs1 = (instr >> 15) & 0x1F
|
||||
imm = (instr >> 20) & 0xFFF
|
||||
imm = imm - 0x1000 if imm & 0x800 else imm
|
||||
assert (opcode, rd, funct3, rs1, imm) == (0x13, 5, 0, 6, -4)
|
||||
print("PASS task01 instr=ffc30293 opcode=13 rd=5 rs1=6 funct3=0 imm=-4")
|
||||
|
||||
|
||||
def task02() -> None:
|
||||
classes = {0x13: "alu_imm", 0x33: "alu_reg", 0x03: "load", 0x23: "store", 0x63: "branch", 0x6F: "jump", 0x67: "jump"}
|
||||
assert len(classes) == 7 and 0x7F not in classes
|
||||
print("PASS task02 legal=7 jump_opcodes=6f,67 illegal_7f=1")
|
||||
|
||||
|
||||
def task03() -> None:
|
||||
regs = [0] * 32
|
||||
regs[5] = 0x12345678
|
||||
attempted_x0 = 0xFFFFFFFF
|
||||
if 0 != 0:
|
||||
regs[0] = attempted_x0
|
||||
assert regs[5] == 0x12345678 and regs[0] == 0
|
||||
print("PASS task03 x5=12345678 x0=00000000 dual_read=1")
|
||||
|
||||
|
||||
TASKS = {"task01_instruction_fields": task01, "task02_opcode_decoder": task02, "task03_register_file": 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,17 @@
|
||||
module tb;
|
||||
logic [31:0] instr;
|
||||
logic [6:0] opcode, funct7;
|
||||
logic [4:0] rd, rs1, rs2;
|
||||
logic [2:0] funct3;
|
||||
logic signed [31:0] imm_i;
|
||||
string trace_file;
|
||||
instruction_fields dut (.*);
|
||||
initial begin
|
||||
if ($value$plusargs("trace=%s", trace_file)) begin $dumpfile(trace_file); $dumpvars(0, tb); end
|
||||
instr = 32'hffc30293; #1;
|
||||
if (opcode != 7'h13 || rd != 5 || rs1 != 6 || funct3 != 0 || $signed(imm_i) != -4)
|
||||
$fatal(1, "FAIL task01 opcode=%h rd=%0d rs1=%0d imm=%0d", opcode, rd, rs1, $signed(imm_i));
|
||||
$display("PASS task01 instr=ffc30293 opcode=13 rd=5 rs1=6 funct3=0 imm=-4");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,22 @@
|
||||
module tb;
|
||||
logic [6:0] opcode;
|
||||
logic alu_imm, alu_reg, load, store, branch, jump, illegal;
|
||||
integer legal = 0;
|
||||
string trace_file;
|
||||
opcode_decoder dut (.*);
|
||||
task automatic check_opcode(input logic [6:0] value, input logic [5:0] selected);
|
||||
opcode = value; #1;
|
||||
if ({jump, branch, store, load, alu_reg, alu_imm} != selected || illegal)
|
||||
$fatal(1, "FAIL task02 opcode=%h selected=%b illegal=%b", opcode, {jump, branch, store, load, alu_reg, alu_imm}, illegal);
|
||||
legal += 1;
|
||||
endtask
|
||||
initial begin
|
||||
if ($value$plusargs("trace=%s", trace_file)) begin $dumpfile(trace_file); $dumpvars(0, tb); end
|
||||
check_opcode(7'h13, 6'b000001); check_opcode(7'h33, 6'b000010); check_opcode(7'h03, 6'b000100);
|
||||
check_opcode(7'h23, 6'b001000); check_opcode(7'h63, 6'b010000); check_opcode(7'h6f, 6'b100000); check_opcode(7'h67, 6'b100000);
|
||||
opcode = 7'h7f; #1;
|
||||
if (!illegal || {jump, branch, store, load, alu_reg, alu_imm} != 0) $fatal(1, "FAIL task02 illegal default");
|
||||
$display("PASS task02 legal=%0d jump_opcodes=6f,67 illegal_7f=1", legal);
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,21 @@
|
||||
module tb;
|
||||
logic clk = 0, rst = 1, write_enable = 0;
|
||||
logic [4:0] write_address = 0, read_address1 = 0, read_address2 = 0;
|
||||
logic [31:0] write_data = 0, read_data1, read_data2;
|
||||
string trace_file;
|
||||
register_file 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;
|
||||
write_enable = 1; write_address = 5; write_data = 32'h12345678;
|
||||
@(posedge clk); #1; write_enable = 0;
|
||||
read_address1 = 5; read_address2 = 0; #1;
|
||||
if (read_data1 != 32'h12345678 || read_data2 != 0) $fatal(1, "FAIL task03 first read");
|
||||
@(negedge clk); write_enable = 1; write_address = 0; write_data = 32'hffffffff;
|
||||
@(posedge clk); #1; write_enable = 0; read_address1 = 0; read_address2 = 5; #1;
|
||||
if (read_data1 != 0 || read_data2 != 32'h12345678) $fatal(1, "FAIL task03 x0 or dual read");
|
||||
$display("PASS task03 x5=12345678 x0=00000000 dual_read=1");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/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_instruction_fields|task02_opcode_decoder|task03_register_file) ;; *) 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"
|
||||
verilator --binary --timing --assert --trace -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" >/dev/null
|
||||
cp -f "$root/src/tasks/$task.sv" "$artifact_dir/"
|
||||
[[ "$mode" == build-only ]] && { printf 'BUILD %s binary=%s\n' "$task" "$binary"; exit 0; }
|
||||
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