diff --git a/.gitignore b/.gitignore index 7a5a276..fd0c606 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,7 @@ dkms.conf #Iverilog Simulation Files *.vcd -*a.out \ No newline at end of file +*a.out + +# Internal-only reference documentation (not for the public repo) +docs/internal/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..59b3190 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "third_party/taxi"] + path = third_party/taxi + url = https://github.com/fpganinja/taxi.git + shallow = true diff --git a/README.md b/README.md index 0ce2664..50136d6 100644 --- a/README.md +++ b/README.md @@ -4,71 +4,122 @@ A RISC-V SoC, built collaboratively from scratch in SystemVerilog. Join on [Discord](https://discord.gg/sQjhBvWXjF) if you're interested in the project! +**Contents:** [Current state](#current-state) · [Architecture](#architecture) · +[Verification](#verification) · [Building and simulating](#building-and-simulating) · +[Roadmap](#roadmap) · [Contributing](#contributing) · [License](#license) + --- ## Current state -**RV64I base ISA + Zicsr (CSR instructions)**, non-pipelined, single-hart, fully -verified. The core is a multi-cycle Wishbone-master FSM (fetch → execute → -memory, one instruction fully retires before the next begins -- no forwarding, -no hazards to design around yet) driving a real Wishbone bus out to two real -peripherals. +**RV64IMAC + Zicsr, full M/S/U privilege modes, real timer interrupts**, +non-pipelined, single-hart, fully verified. The core is a multi-cycle +Wishbone-master FSM (fetch → execute → memory, one instruction fully retires +before the next begins -- no forwarding, no hazards to design around yet) +driving a real Wishbone bus out through an L1 instruction/data cache pair to +a real memory-mapped peripheral set. ### Architecture - `design/decoder.sv`, `design/alu.sv`, `design/register_file.sv`, - `design/csr_file.sv` -- the datapath: instruction decode, ALU (full RV64I - arithmetic/logic/shift ops including the `*W` word-width family), a 32-entry - general-purpose register file, and the 8 machine-mode CSRs this milestone - backs (`misa`, `mvendorid`/`marchid`/`mimpid`/`mhartid`, `mscratch`, - `mcycle`, `minstret`). -- `design/core.sv` -- ties the above together as a Wishbone bus master. No - private instruction/data memory of its own; every fetch and load/store goes - out over the bus. -- `design/wb4_sram.sv`, `design/uart_tx.sv`, `design/wb_addr_decoder.sv` -- - the two real Wishbone slaves (a flat 64-bit-word memory, and a - simulation-only UART that transmits via `$write`) plus the address decoder - routing between them. -- `design/soc.sv` -- top-level integration: `core` + `wb_addr_decoder` + - `wb4_sram` + `uart_tx`, `clk`/`rst` are its only ports. - -Privilege modes, traps/interrupts, and virtual memory (Sv39) don't exist yet -- -every access currently runs unconstrained, `ECALL` is a no-op, and the CSRs -backed today are exactly the ones meaningful without that infrastructure. See -`design/csr_file.sv`'s own header for the full scope note. + `design/divider.sv`, `design/c_expand.sv` -- the datapath: instruction + decode, ALU (full RV64I arithmetic/logic/shift ops including the `*W` + word-width family plus RV64M multiply/divide), a 32-entry general-purpose + register file, a standalone multi-cycle divider, and the RV64C + compressed-instruction decompressor. +- `design/csr_file.sv` -- every CSR the current privilege/interrupt/cache/ + debug feature set needs: the M-mode base set (`misa`, `mvendorid`/ + `marchid`/`mimpid`/`mhartid`, `mscratch`, `mcycle`, `minstret`), the full + M/S trap stack (`mstatus`/`sstatus`, `mtvec`/`stvec`, `mepc`/`sepc`, + `mcause`/`scause`, `mtval`/`stval`, `medeleg`/`mideleg`), the real + interrupt CSRs (`mie`/`mip`, spliced live against the CLINT's + timer-pending signal), and the Debug-mode CSRs (`dcsr`/`dpc`/ + `dscratch0`/`dscratch1`) -- storage and access-control exist, but + nothing can legally enter Debug Mode yet (see below). +- `design/core.sv` -- ties the above together as a Wishbone bus master, + including real synchronous-trap and timer-interrupt-taking logic, atomic + memory operations (LR/SC/AMO), bus-error-to-access-fault trapping, and + FENCE.I. No private instruction/data memory of its own; every fetch and + load/store goes out over the bus (through the cache, see below). +- `design/icache.sv`, `design/dcache.sv`, `design/cache_complex.sv` -- a + direct-mapped, physically-indexed/physically-tagged, write-through L1 + instruction/data cache pair sitting between `core` and the address + decoder. FENCE.I (Zifencei) flushes the I$ for self-modifying-code + coherence; the D$ never needs an equivalent flush (write-through keeps a + store hit's cached copy and the backing SRAM in lockstep). +- `design/wb4_sram.sv`, `design/uart_tx.sv`, `design/uart_rx.sv`, + `design/clint.sv`, `design/wb_addr_decoder.sv` -- the real Wishbone + slaves (a flat 64-bit-word memory; a simulation-only UART, transmit and + receive; an `mtime`/`mtimecmp` timer driving real machine-timer + interrupts) plus the address decoder routing between them. +- `design/soc.sv` -- top-level integration: `core` + cache + address + decoder + `wb4_sram` + `uart_tx`/`uart_rx` + `clint`. `clk`/`rst` are its + only ports (no real serial pins exist yet -- the UART model transmits via + `$write` and receives via a testbench-only backdoor task). + +Sv39 virtual memory doesn't exist yet -- every address currently runs +untranslated. A hardware Debug Module (JTAG/DMI, in the spirit of the +RISC-V External Debug Support spec) is in progress: `EBREAK` is already a +real, resumable synchronous trap, UART RX is wired up as its planned +transport, and the Debug-mode CSRs (`dcsr`/`dpc`/`dscratch0`/`dscratch1`) +exist with real access control -- any access from anywhere currently +traps, since there's no Debug Mode to legally be in yet. The halt/resume +FSM and the DM/JTAG stack itself are still ahead. See `design/csr_file.sv`'s +and `design/core.sv`'s own header comments for the exact current scope. + +`verification/taxi/` also carries a standalone Wishbone-to-AXI4 bridge and +behavioral DRAM timing model, built on a vendored `taxi` AXI4 IP submodule +-- proven independently via its own Verilator-only test flow, but not yet +wired into `design/soc.sv` (that RTL is Verilator-only, since it +instantiates a SystemVerilog `interface`, which Icarus cannot parse; `soc.sv` +itself must stay 100% Icarus-compatible). ### Verification -17 testbenches (unit-level for the ALU/register file/CSR file in isolation, -integration-level driving the real Wishbone bus, one running a real -`riscv64-unknown-elf`-assembled program), all passing. Shared infrastructure -lives in `testbench/`: `check_lib.sv` (a `check()` primitive), `wb_driver.sv` -(a Wishbone bus-cycle task), `halt_wait.sv` (timeout-guarded halt waiting), -`pc_trigger_sample_monitor.sv` and `core_wb4_sram_harness.sv` (reusable -monitor/harness modules) -- pulled into new testbenches via `` `include `` -rather than hand-rolled each time. - -Real code coverage has been measured (line/branch/toggle/expression, via -Verilator) across every live design file, not just claimed. Current whole-design -coverage: 97.6% line, 99.0% branch, 100% expression -- what's left uncovered -is understood and benign (a couple of structurally-unreachable default arms, -one genuinely unused ALU op, one buffer-full guard that'd need over 256 writes -in a single test to trigger). +A large testbench suite (unit-level for every datapath/cache/peripheral +module in isolation, integration-level driving the real Wishbone bus and +the real cache hierarchy, several running real `riscv64-unknown-elf` +-assembled/toolchain-built programs), all passing, run through a +non-committed regression script (there's no single top-level build script +checked in yet -- see *Building and simulating* below). Shared +infrastructure lives in `testbench/`: `check_lib.sv` (a `check()` +primitive), `wb_driver.sv` (a Wishbone bus-cycle task), `halt_wait.sv` +(timeout-guarded halt waiting), and several reusable harness/monitor +modules -- pulled into new testbenches via `` `include `` rather than +hand-rolled each time. + +Beyond the project's own testbenches, every ISA feature is additionally +cross-checked against two independent, external references: + +- **`verification/riscv-arch-test/`** -- the official RISC-V Architecture + Test (ACT4) compliance suite, run as real self-checking ELFs through a + dedicated runner. The large majority pass; the handful that don't are + understood, documented, spec-legal config/UDB mismatches, not RTL bugs. +- **`verification/riscv-formal/`** -- formal, unbounded-cycle-count + verification via SymbiYosys/RVFI taps on `design/core.sv`, covering the + base RV64I integer pipeline, the A-extension (atomics), and the + C-extension (compressed instructions). + +Real code coverage has also been measured (line/branch/toggle/expression, +via Verilator) across the design, not just claimed -- see individual +milestone notes for current numbers, which move as new features land. --- ## Building and simulating Everything here is developed and verified against **Icarus Verilog** -(`iverilog`/`vvp`) and **Verilator**, run through WSL on Windows. There is no -single top-level build script yet -- compile the specific file set a given -testbench needs directly, e.g.: +(`iverilog`/`vvp`) and **Verilator**, run through WSL on Windows. There is +no single top-level build script checked into the repo yet -- compile the +specific file set a given testbench needs directly, e.g.: ```sh iverilog -g2012 -I design -I testbench -o /tmp/soc_tb.out \ - design/alu.sv design/decoder.sv design/register_file.sv design/csr_file.sv \ - design/core.sv design/wb4_sram.sv design/uart_tx.sv design/wb_addr_decoder.sv \ - design/soc.sv testbench/soc_tb.sv + design/decoder.sv design/alu.sv design/c_expand.sv design/csr_file.sv \ + design/divider.sv design/register_file.sv design/uart_tx.sv \ + design/uart_rx.sv design/wb4_sram.sv design/wb_addr_decoder.sv \ + design/icache.sv design/dcache.sv design/cache_complex.sv \ + design/clint.sv design/core.sv design/soc.sv testbench/soc_tb.sv cd design && vvp /tmp/soc_tb.out ``` @@ -80,17 +131,22 @@ its `$readmemh` of `../firmware/crt0.hex` resolves. A Verilator lint pass over the full SoC: ```sh -verilator --lint-only -Wall -Idesign -Itestbench --top-module soc \ - design/alu.sv design/decoder.sv design/register_file.sv design/csr_file.sv \ - design/core.sv design/wb4_sram.sv design/uart_tx.sv design/wb_addr_decoder.sv \ - design/soc.sv +verilator --lint-only -Wall -Idesign -Idesign/defaults --top-module soc \ + design/decoder.sv design/alu.sv design/c_expand.sv design/csr_file.sv \ + design/divider.sv design/register_file.sv design/uart_tx.sv \ + design/uart_rx.sv design/wb4_sram.sv design/wb_addr_decoder.sv \ + design/icache.sv design/dcache.sv design/cache_complex.sv \ + design/clint.sv design/core.sv design/soc.sv ``` (Verilator wants `-Idesign`, no space; Icarus accepts either form.) `firmware/` holds a real C toolchain build (`riscv64-unknown-elf-gcc`/`-as`/ `-ld`) producing the hex images some testbenches load -- see -`firmware/Makefile`. +`firmware/Makefile`. `verification/taxi/` (the Wishbone-to-AXI4 bridge and +DRAM model) is Verilator-only and has its own separate test runner -- +never add a `taxi`-touching testbench to the file lists above, it will not +compile under Icarus. --- @@ -100,12 +156,46 @@ RV64**IMAC** + Zicsr + U/S/M privilege + Sv39, non-pipelined and in-order, before any pipelining/OoO work starts -- deliberately, so out-of-order correctness has a trusted in-order reference to debug against. -RV64IMAC + Zicsr + full U/S/M privilege modes are done, each verified via a -4-pillar pattern (unit test, hand-assembled end-to-end core testbench, -real-toolchain encoder cross-check, real-toolchain end-to-end firmware test) -and cross-checked against the official riscv-arch-test (ACT4) compliance -suite. Sv39 virtual memory is next -- a different teammate's work, built on -top of the U/S/M privilege seams (`fetch_paddr`/`mem_paddr`, `satp`, -`mstatus.MPRV/SUM/MXR`) this core already carries specifically for that -handoff. Bus protocol stays Wishbone at the core; AXI4 is a future fabric -concern at the edge, not a core-level one. +RV64IMAC + Zicsr + full U/S/M privilege modes + real timer interrupts are +done, each verified via a multi-pillar pattern (unit test, hand-assembled +end-to-end core testbench, real-toolchain encoder cross-check, real- +toolchain end-to-end firmware test) and cross-checked against both the +official riscv-arch-test compliance suite and formal (riscv-formal) +verification. Sv39 virtual memory is next on the privilege/memory side -- +a different teammate's work, built on top of the U/S/M privilege seams +(`fetch_paddr`/`mem_paddr`, `satp`, `mstatus.MPRV/SUM/MXR`) this core +already carries specifically for that handoff. + +In parallel, a hardware Debug Module (JTAG/DMI) is being built out in +staged milestones: `EBREAK` is now a real, resumable synchronous trap, +UART RX exists as its planned transport, and the Debug-mode CSRs +(`dcsr`/`dpc`/`dscratch0`/`dscratch1`) exist with real access control; +the halt/resume FSM and the JTAG TAP/DMI/Program-Buffer stack itself are +still ahead. + +Bus protocol stays Wishbone at the core; AXI4 is a fabric-edge concern (see +the standalone bridge/DRAM model under `verification/taxi/`), not a +core-level one. + +--- + +## Contributing + +This is a collaborative, from-scratch build -- [Discord](https://discord.gg/sQjhBvWXjF) +is where design decisions get discussed and work gets coordinated before a PR +shows up, not an afterthought support channel. Come say what you're +interested in; the *Roadmap* section above and the per-module header +comments throughout `design/` are the best starting map of what's settled, +what's in flight, and what's still open. + +A few conventions worth knowing before sending a change: every new RTL +feature ships with real tests, not just a claim it works (see +*Verification* above) -- a full local regression plus a Verilator lint +pass, both clean, is the bar every prior milestone has held itself to, and +new privilege/CSR/interrupt logic additionally gets cross-checked against +riscv-arch-test and, where practical, riscv-formal. Module header comments +explain *why*, not just *what* -- keep that up when you add or change one. + +## License + +[MIT](LICENSE). diff --git a/design/cache_complex.sv b/design/cache_complex.sv index 0a09544..023bce7 100644 --- a/design/cache_complex.sv +++ b/design/cache_complex.sv @@ -58,6 +58,19 @@ module cache_complex #( output logic ack_o, output logic err_o, + /* + * Zifencei: passed straight through to icache0.flush_i UNCONDITIONALLY + * -- load-bearing to get right, not a style choice. ifetch_i is LOW + * throughout S_EXEC (FENCE.I's own commit cycle, where core.sv pulses + * icache_flush_o -- see that port's own comment), so gating this on + * ifetch_i the way cyc_i/stb_i are routed above would silently make + * FENCE.I a permanent no-op: the exact class of bug this signal exists + * to prevent, not just an edge case. D$ has no equivalent flush path + * (or need for one) -- write-through already keeps a store hit's + * cached copy and SRAM in lockstep. + */ + input logic flush_i, + // Memory-facing port -- this module is a Wishbone MASTER from // wb4_sram.sv's side. Shared by both sub-caches; see this module's // own header for why no arbitration is needed. @@ -82,7 +95,7 @@ module cache_complex #( icache #(.num_lines(num_lines), .line_words(line_words)) icache0 ( .clk(clk), .rst(rst), .addr_i(addr_i), .dat_o(ic_dat_o), .cyc_i(cyc_i && ifetch_i), .stb_i(stb_i && ifetch_i), - .ack_o(ic_ack), .err_o(ic_err), + .ack_o(ic_ack), .err_o(ic_err), .flush_i(flush_i), .mem_addr_o(ic_mem_addr), .mem_dat_i(ic_mem_dat_i), .mem_sel_o(ic_mem_sel), .mem_we_o(ic_mem_we), .mem_cyc_o(ic_mem_cyc), .mem_stb_o(ic_mem_stb), .mem_ack_i(ic_mem_ack), .mem_err_i(ic_mem_err) diff --git a/design/clint.sv b/design/clint.sv new file mode 100644 index 0000000..8495afa --- /dev/null +++ b/design/clint.sv @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +/* + * Module: clint + * + * A real, addressable Wishbone-slave peripheral, timer-only CLINT + * (mtime/mtimecmp) -- deliberately minimal for this milestone: no msip + * (single-hart, no IPI use case yet, correctly out of scope until SMP is + * a milestone). + * + * Registered, 1-wait-state ack, matching design/uart_tx.sv's own house + * style exactly -- see that file for the fuller rationale (bus + * uniformity: the CPU's wait state doesn't need to know or care which + * slave it's talking to). + * + * Register map (only addr_i[3] is decoded -- everything routed here by + * wb_addr_decoder.sv already has the right upper address bits set, + * matching uart_tx.sv's own convention): + * CLINT_BASE+0x0 mtime read-only, free-running 64-bit counter. + * CLINT_BASE+0x8 mtimecmp read/write, compared against mtime_q to + * drive mtip_o. + * + * Single 64-bit bus means one LD reads either register atomically -- no + * hi/lo split-register dance real 32-bit CLINT software needs. + */ +module clint ( + input logic clk, + input logic rst, + + /* + * Only addr_i[3] (register select) is used -- addr_i's other 31 bits + * are genuinely don't-care here, same pattern already used by + * uart_tx.sv for the same reason. dat_i and sel_i, unlike addr_i, + * are NOT wrapped the same way: every one of their bits IS + * genuinely read (dat_i via the byte-lane for-loop below, sel_i via + * sel_i[lane] gating each byte) -- wrapping them too would be a + * stale pragma making a false claim, not just a harmless no-op one. + */ + /* verilator lint_off UNUSEDSIGNAL */ + input logic [31:0] addr_i, + /* verilator lint_on UNUSEDSIGNAL */ + input logic [63:0] dat_i, + output logic [63:0] dat_o, + input logic [7:0] sel_i, + input logic we_i, + input logic cyc_i, + input logic stb_i, + output logic ack_o, + output logic err_o, + + output logic mtip_o +); + /* + * MTIME_SEL is documentation for the register map (and the natural + * counterpart to MTIMECMP_SEL below), but every actual addr_i[3] + * test in this module -- write-decode and the read-mux alike -- is + * phrased as "== MTIMECMP_SEL" with mtime as the implicit else + * branch, so only one localparam is ever read by RTL. Both + * decode points testing the SAME select value (rather than one + * testing MTIME_SEL and the other MTIMECMP_SEL) is what keeps them + * consistent with each other; MTIME_SEL itself stays unreferenced + * as a result. + */ + /* verilator lint_off UNUSEDPARAM */ + localparam MTIME_SEL = 1'b0; // addr_i[3]==0 -> mtime (CLINT_BASE+0x0) + /* verilator lint_on UNUSEDPARAM */ + localparam MTIMECMP_SEL = 1'b1; // addr_i[3]==1 -> mtimecmp (CLINT_BASE+0x8) + + logic [63:0] mtime_q; + logic [63:0] mtimecmp_q; + + always_ff @(posedge clk) begin + if (rst) mtime_q <= 64'b0; + else mtime_q <= mtime_q + 64'b1; // free-running, same precedent as csr_file.sv's mcycle_q + end + + /* + * mtimecmp resets to all-ones, NOT zero -- resetting to 0 would make + * mip.MTIP read pending immediately at power-on before software ever + * programs a real deadline. Harmless for an actual taken interrupt + * since mie.MTIE also resets to 0, but surprising for anything that + * inspects mip before arming the timer. mtime itself is read-only + * (no write path) -- matches csr_file.sv's mcycle/minstret precedent + * (deferring writability, no current need). + */ + always_ff @(posedge clk) begin + if (rst) + mtimecmp_q <= 64'hFFFF_FFFF_FFFF_FFFF; + else if (cyc_i && stb_i && we_i && (addr_i[3] == MTIMECMP_SEL)) + for (int lane = 0; lane < 8; lane++) + if (sel_i[lane]) mtimecmp_q[(8*lane)+:8] <= dat_i[(8*lane)+:8]; + end + + assign mtip_o = (mtime_q >= mtimecmp_q); + + always @(posedge clk) begin + if (rst) begin + ack_o <= 1'b0; + err_o <= 1'b0; + dat_o <= 64'b0; + end else if (cyc_i && stb_i) begin + dat_o <= (addr_i[3] == MTIMECMP_SEL) ? mtimecmp_q : mtime_q; + ack_o <= 1'b1; + err_o <= 1'b0; + end else begin + ack_o <= 1'b0; + err_o <= 1'b0; + end + end + /* + * err_o is intentionally never asserted -- no error conditions are + * defined for this minimal peripheral, same reasoning as + * uart_tx.sv's own "err_o is intentionally never asserted" comment: + * wb_addr_decoder.sv is the only thing that ever routes a request + * here. + */ + +endmodule + + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/design/clint_tb.sv b/design/clint_tb.sv new file mode 100644 index 0000000..ba32b4c --- /dev/null +++ b/design/clint_tb.sv @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: clint + * + * Drives the Wishbone port directly -- no core.sv involved, same idiom + * as design/wb4_sram_tb.sv and design/uart_tx_tb.sv. check() and + * wb_cycle() come from testbench/check_lib.sv and testbench/wb_driver.sv + * (see each for its required-signal contract) -- resolved via a bare + * filename plus -I testbench on the iverilog command line, per those + * files' own headers. + */ +module clint_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + logic [31:0] addr; + logic [63:0] dat_i, dat_o; + logic [7:0] sel; + logic ack, err, cyc, stb, we; + logic mtip; + + clint dut ( + .clk(clk), .rst(rst), + .addr_i(addr), .dat_i(dat_i), .dat_o(dat_o), .sel_i(sel), + .we_i(we), .cyc_i(cyc), .stb_i(stb), .ack_o(ack), .err_o(err), + .mtip_o(mtip) + ); + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + `include "wb_driver.sv" + + initial begin + cyc = 0; stb = 0; we = 0; addr = 0; dat_i = 0; sel = 8'h00; + @(posedge clk); #1; + rst = 0; + + /* + * -- 1. mtime free-run rate -- + * Read mtime twice, N wb_cycle() calls apart (each wb_cycle is + * itself one clock period end-to-end: negedge-set, then the next + * posedge is when the DUT registers the read and ack), and check + * the delta against whatever N wb_cycle() calls actually advance + * mtime_q by -- determined empirically below rather than assumed, + * per the milestone's own instructions. + */ + wb_cycle(32'h0, 64'h0, 8'h00, 1'b0); + begin + logic [63:0] mtime_first, mtime_second; + mtime_first = dat_o; + wb_cycle(32'h0, 64'h0, 8'h00, 1'b0); + wb_cycle(32'h0, 64'h0, 8'h00, 1'b0); + wb_cycle(32'h0, 64'h0, 8'h00, 1'b0); + mtime_second = dat_o; + // 3 wb_cycle() calls separate the two reads above; empirically + // each wb_cycle() advances mtime_q by exactly 1 (one posedge + // per call), so the delta is exactly 3. + check("mtime free-run delta == 3 over 3 wb_cycle() calls", + mtime_second - mtime_first, 64'd3); + end + + /* + * -- 2. mtimecmp resets to all-ones -- + * Already proven true by construction (rst deasserted once above, + * before any write to mtimecmp), so read it now, before this + * testbench performs its first write, below. + */ + wb_cycle(32'h8, 64'h0, 8'h00, 1'b0); + check("mtimecmp resets to all-ones", dat_o, 64'hFFFF_FFFF_FFFF_FFFF); + + /* + * -- 3. mtimecmp byte-lane writes -- + * Full-word write first to get a known non-all-ones, non-zero + * value into every lane, then a partial write (lanes 0-1 only) + * and confirm lanes 2-7 keep the prior full-word value while + * lanes 0-1 take the new bytes. + */ + wb_cycle(32'h8, 64'hDEADBEEF_CAFEF00D, 8'hFF, 1'b1); + wb_cycle(32'h8, 64'h0, 8'h00, 1'b0); + check("full 8-byte (sel_i=8'hFF) write reads back exactly", dat_o, 64'hDEADBEEF_CAFEF00D); + + wb_cycle(32'h8, 64'h0000_0000_0000_A5A5, 8'h03, 1'b1); + wb_cycle(32'h8, 64'h0, 8'h00, 1'b0); + check("byte-enable write only touches enabled lanes", dat_o, 64'hDEADBEEF_CAFEA5A5); + + /* + * -- 3b. A write to mtime's own address (0x0) is a no-op -- + * mtime has no write path at all; confirm a write attempt there + * neither corrupts mtime_q's free-run nor spuriously asserts + * err_o, rather than assuming silence means correctness. + */ + begin + logic [63:0] mtime_before_noop_write, mtime_after_noop_write; + wb_cycle(32'h0, 64'h0, 8'h00, 1'b0); + mtime_before_noop_write = dat_o; + wb_cycle(32'h0, 64'hFFFF_FFFF_FFFF_FFFF, 8'hFF, 1'b1); + check("write to mtime's address doesn't assert err_o", {63'b0, err}, 64'd0); + wb_cycle(32'h0, 64'h0, 8'h00, 1'b0); + mtime_after_noop_write = dat_o; + // The write plus the final read are 2 wb_cycle() calls between + // the two capture points (mtime_before_noop_write is captured + // AFTER the first read, so it isn't itself one of the 2), same + // 1-tick-per-call rate already established above -- confirms + // the write attempt didn't ALSO stomp mtime_q with the bogus + // all-ones write data on top of its own natural free-run + // advance. + check("write to mtime's address doesn't corrupt its free-running value", + mtime_after_noop_write - mtime_before_noop_write, 64'd2); + end + + /* + * -- 4. mtip_o comparison correctness -- + * Program mtimecmp to a near-future deadline (current mtime plus + * a small delta), confirm mtip_o is low before mtime reaches it + * and goes high once mtime >= mtimecmp, letting simulated time + * actually advance between the two checks rather than comparing + * static values. + */ + begin + logic [63:0] mtime_now, deadline; + int wait_cycles; + wb_cycle(32'h0, 64'h0, 8'h00, 1'b0); + mtime_now = dat_o; + deadline = mtime_now + 64'd5; + wb_cycle(32'h8, deadline, 8'hFF, 1'b1); + check("mtip_o low before deadline reached", {63'b0, mtip}, 64'd0); + /* + * Bounded, not `while (mtip !== 1'b1) @(posedge clk);` -- + * found by code review: an unbounded wait here means a + * genuinely broken comparator (e.g. mtip_o wired to `<=` + * instead of `>=`) would hang this testbench forever instead + * of failing cleanly, unlike every other wait in this + * project's suite (halt_wait.sv's wait_halted_or_timeout, + * wb_cycle()'s own !ack&&!err loop, which is bounded in + * practice by every real slave always eventually + * ack/err-ing). 20 cycles is generous headroom over the + * 5-cycle deadline programmed above. Also adds the #1 settle + * testbench/wb_driver.sv's own header documents by name -- + * mtip_o is combinational off mtime_q/mtimecmp_q, both + * NBA-updated, so reading it immediately upon resuming from + * @(posedge clk) risks the same one-edge-stale read class + * that file's own history already found and fixed once. + */ + wait_cycles = 0; + while (mtip !== 1'b1 && wait_cycles < 20) begin + @(posedge clk); #1; + wait_cycles = wait_cycles + 1; + end + check("mtip_o high once mtime >= mtimecmp (within 20 cycles)", {63'b0, mtip}, 64'd1); + end + + /* + * -- 4b. mtip_o comparison correctness: deadline already passed -- + * Program mtimecmp to a value already at-or-below current mtime + * and confirm mtip_o reads high immediately, not just eventually + * -- the other edge of the comparison this project's own review + * flagged as an explicitly-requested case not otherwise covered. + */ + begin + logic [63:0] mtime_now; + wb_cycle(32'h0, 64'h0, 8'h00, 1'b0); + mtime_now = dat_o; + wb_cycle(32'h8, mtime_now, 8'hFF, 1'b1); + #1; + check("mtip_o high immediately when mtimecmp is set to an already-passed deadline", + {63'b0, mtip}, 64'd1); + end + + /* + * -- 5. ack/err timing -- + * ack_o must NOT assert combinationally in the same cycle + * cyc_i/stb_i first go high -- it's a registered, 1-wait-state + * response. Drive the bus signals directly here (bypassing + * wb_cycle(), which already waits a cycle for ack) so the + * same-cycle value can actually be observed. + * + * Settle first: whatever immediately precedes this block used + * wb_cycle(), which returns with cyc/stb freshly deasserted but + * ack still HIGH from that transaction's own completion -- ack + * only clears on the NEXT edge where cyc&&stb reads false, and + * nothing above guarantees that edge has actually happened yet + * (a `#1`-only tail, with no intervening @(posedge clk), leaves + * this block's own very next `@(negedge clk)` sharing the SAME + * cycle as that still-pending clear). Explicitly waiting for + * ack==0 here removes the dependency on however many posedges + * happened to elapse in whatever code happens to run immediately + * before this block -- found by exactly this failure mode when a + * new test was inserted directly above during code review. + */ + while (ack) @(posedge clk); + @(negedge clk); + addr = 32'h0; dat_i = 64'h0; sel = 8'h00; we = 1'b0; cyc = 1'b1; stb = 1'b1; + #1; + check("ack_o not combinational same-cycle as cyc/stb", {63'b0, ack}, 64'd0); + @(posedge clk); #1; + check("ack_o asserts one cycle after cyc_i&&stb_i", {63'b0, ack}, 64'd1); + cyc = 0; stb = 0; + @(negedge clk); + + // err_o must never assert, for any address/access pattern tried + // above (already implicitly checked by wb_cycle()'s own + // !ack && !err wait loop never hanging), plus explicit checks + // across both registers and both access directions here. + wb_cycle(32'h0, 64'h0, 8'h00, 1'b0); + check("err_o never asserts (mtime read)", {63'b0, err}, 64'd0); + wb_cycle(32'h8, 64'h0, 8'h00, 1'b0); + check("err_o never asserts (mtimecmp read)", {63'b0, err}, 64'd0); + wb_cycle(32'h8, 64'hFFFF_FFFF_FFFF_FFFF, 8'hFF, 1'b1); + check("err_o never asserts (mtimecmp write)", {63'b0, err}, 64'd0); + + $display(""); + $display("clint_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("clint_tb: FAILURES PRESENT"); + $finish; + end + +endmodule + + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/design/core.sv b/design/core.sv index 2ccd478..52a3e61 100644 --- a/design/core.sv +++ b/design/core.sv @@ -80,10 +80,11 @@ * adds (the fetched instruction line, plus -- only for a dword-crossing * C instruction -- the second dword's low halfword). * - * Scope: full RV64IMAC base ISA + Zicsr + full U/S/M privilege modes. - * No Sv39, no interrupts/exceptions beyond the synchronous traps already - * implemented -- later milestones (a different teammate's work for - * Sv39). Misaligned DATA access (loads/stores) traps cleanly (see + * Scope: full RV64IMAC base ISA + Zicsr + full U/S/M privilege modes, + * plus machine-timer interrupts (CLINT mtime/mtimecmp, see i_mtip below + * and the Interrupts section near csr_file0's instantiation). No Sv39, + * no external/PLIC interrupts, no IPI/msip -- later milestones (a + * different teammate's work for Sv39). Misaligned DATA access (loads/stores) traps cleanly (see * mem_load_misaligned/mem_store_misaligned below) rather than being * handled in hardware -- actual misaligned load/store SUPPORT stays * deferred to a later milestone alongside Sv39, but silently truncating @@ -93,15 +94,16 @@ * below). Misaligned INSTRUCTION fetch, by contrast, is a real, * exercised case as of the C extension (any compressed instruction can * leave pc 2-byte- rather than 4-byte-aligned) and IS correctly - * handled, not a gap. wb_err_i is likewise not acted on -- no trap - * mechanism exists yet for it to feed into. + * handled, not a gap. wb_err_i now feeds instruction/load/store + * access-fault traps (causes 1/5/7 -- see wb_done/wb_ok, fetch_fault_q, + * mem_load_access_fault/mem_store_access_fault below). * * Input ports: * clk: Clock. - * rst: Synchronous reset (active high) -- resets pc, FSM state, and - * `halted`. Does NOT reset register/memory contents; the ISA - * doesn't require it, and real hardware doesn't guarantee it - * either (only the reset vector is architecturally defined). + * rst: Synchronous reset (active high) -- resets pc and FSM state. + * Does NOT reset register/memory contents; the ISA doesn't + * require it, and real hardware doesn't guarantee it either + * (only the reset vector is architecturally defined). * * Wishbone master port: standard CLASSIC-cycle signal names, written * from this module's (the master's) point of view -- `_o` drives the @@ -116,11 +118,31 @@ * (fetch and mem/AMO never overlap -- single-issue, non-pipelined), so a * downstream cache layer has no other way to tell which logical stream * (I$ vs D$) a given transaction belongs to. Defined off `state` directly - * (see wb_master_drive below), not gated by !wb_ack_i the way + * (see wb_master_drive below), not gated by !wb_done the way * wb_addr_o/wb_cyc_o are -- `state` only updates on the following clock * edge, so this stays stable through the exact cycle a downstream router * needs it on, unlike wb_addr_o/wb_cyc_o which combinationally collapse - * to idle the instant wb_ack_i arrives. + * to idle the instant the bus cycle terminates (ack or err). + * + * icache_flush_o: Zifencei's FENCE.I, side-band like wb_ifetch_o above -- + * pulses one cycle on FENCE.I's own retirement (assign icache_flush_o = + * commit_now && is_fence_i;), telling a downstream I$ to invalidate its + * contents. Timing is provably clean, not just probably fine: FENCE.I + * retires purely within S_EXEC (bus-idle for this single-issue, + * non-pipelined core -- it issues zero bus traffic during S_EXEC), so + * the I$ is provably CACHE_IDLE at the exact cycle this pulses -- no + * mid-refill-flush case exists to reason about. D$ never needed a + * flush path to begin with (write-through already keeps a store hit's + * cached copy and SRAM in lockstep). + * + * i_mtip: machine-timer-interrupt-pending level from an external CLINT + * (design/clint.sv's mtip_o, see soc.sv). Continuously-valid status + * level, not a pulse -- spliced into mip's bit 7 inside csr_file0 and + * consumed combinationally by this module's own Interrupts section + * (near csr_file0's instantiation, below) to decide interrupt_taken. + * ANSI default (= 1'b0) so the 16+ testbenches/harnesses that + * instantiate core directly without driving this port stay compile- + * and lint-clean (same precedent as csr_file.sv's own i_mtip default). */ module core ( input logic clk, @@ -134,10 +156,10 @@ module core ( output logic wb_cyc_o, output logic wb_stb_o, input logic wb_ack_i, - /* verilator lint_off UNUSEDSIGNAL */ input logic wb_err_i, - /* verilator lint_on UNUSEDSIGNAL */ - output logic wb_ifetch_o + output logic wb_ifetch_o, + output logic icache_flush_o, + input logic i_mtip = 1'b0 /* * RVFI (RISC-V Formal Interface) -- only present when compiled for @@ -264,21 +286,47 @@ module core ( * csr_rdata/medeleg_w already use. */ logic is_load, is_store; - logic halted; logic div_stall; logic is_amo_rmw; logic mem_load_misaligned, mem_store_misaligned; + logic mem_load_access_fault, mem_store_access_fault; + /* + * trap_taken: forward-declared here (assigned near exc_code far + * below) purely so the reservation register block above its own + * declaration site can gate on it -- same forward-reference pattern + * mem_load_misaligned/mem_store_misaligned already establish. + */ + logic trap_taken; + /* + * interrupt_taken/interrupt_to_s: forward-declared here for the same + * reason as trap_taken -- csr_file0's own i_trap_taken/i_trap_to_s + * connections need them, but their real assign lives in the new + * Interrupts section after csr_file0's instantiation (it needs + * csr_file0's own mip_w/mie_w/etc. outputs first). + * + * trap_vector: forward-declared (was previously declared+driven + * together as `wire trap_vector = ...`) because wb_master_drive's + * S_FETCH arm now needs to read it, but wb_master_drive is textually + * BEFORE trap_vector's own real assign (down near Next PC) -- same + * "declare early, drive late" split trap_val/amo_rdata_q already use. + */ + logic interrupt_taken; + logic interrupt_to_s; + logic [(`WORD_SIZE - 1):0] trap_vector; logic [(`WORD_SIZE - 1):0] amo_rdata_q; /* * amo_addr_q: WORD_SIZE-wide for the RVFI tap's rvfi_mem_addr (see its - * own assign's comment), but the real hardware only ever consumes its - * low 32 bits (wb_addr_o = amo_addr_q[31:0], this core's physical - * address space is deliberately 32-bit) -- bits[63:32] are dead outside - * `ifdef RISCV_FORMAL, same reasoning/precedent as mem_paddr above. + * own assign's comment) -- real hardware only ever consumes its low 32 + * bits for the actual bus address (wb_addr_o = amo_addr_q[31:0], this + * core's physical address space is deliberately 32-bit), same + * reasoning/precedent as mem_paddr above. Bits[63:32] are no longer + * dead outside `ifdef RISCV_FORMAL, though: trap_val's access-fault + * arm (see mem_load_access_fault/mem_store_access_fault below) reads + * this register unconditionally, since it's the only stable address + * during S_AMO_WRITE (mem_paddr is repurposed for the modify value by + * then) -- genuinely fully used in every build now. */ - /* verilator lint_off UNUSEDSIGNAL */ logic [(`WORD_SIZE - 1):0] amo_addr_q; - /* verilator lint_on UNUSEDSIGNAL */ logic [7:0] amo_sel_q; /* * amo_byte_off_q: the TRUE (unrounded) low 3 address bits, captured @@ -324,6 +372,27 @@ module core ( * without ever touching the bus. */ wire mem_phase_needed = (is_load || is_store) && !(mem_load_misaligned || mem_store_misaligned); + + /* + * wb_done/wb_ok: wb4_sram.sv (the real leaf memory) keeps ack/err + * mutually exclusive per the Wishbone B4 convention -- an out-of-range + * access sets err_o with ack_o held 0. But icache.sv/dcache.sv (fixed + * for an earlier hang bug, see their own CACHE_REFILL comments) assert + * ack_o TOGETHER WITH err_o on a downstream error -- a deliberate + * compromise made back when this file had nothing consuming wb_err_i + * at all. Through the real soc.sv topology, that means wb_ack_i can go + * high even on an errored cache response, so bare wb_ack_i is + * ambiguous: it no longer means "this succeeded". wb_done means "this + * bus cycle has terminated, one way or another" (safe for anything + * that just needs to stop waiting -- state-transition/re-issue-guard + * triggers); wb_ok means "terminated CLEANLY, no error" (needed + * wherever a decision about proceeding to the NEXT NORMAL phase is + * made -- fetch_hi_taken below, and S_MEM's AMO-read-succeeded + * decision). Never use bare wb_ack_i below this point. + */ + wire wb_done = wb_ack_i || wb_err_i; + wire wb_ok = wb_ack_i && !wb_err_i; + /* * commit_now, extended for the A extension: an AMO op's * mem_phase_needed=1 now spans TWO sequential bus transactions @@ -333,25 +402,32 @@ module core ( * is_amo_rmw is false for all of them, so the new `&& !is_amo_rmw` * term is a no-op and they still commit at S_MEM exactly as before. * - * `!halted` (2026-08-12): the PC-register comment below claims - * "state parks in S_FETCH forever [after halt]... so commit_now can - * never become true again" -- true on real hardware (wb_master_drive - * stops asserting wb_cyc_o/wb_stb_o once halted, so a well-behaved - * slave has no reason to ever ack again), but NOT actually enforced - * by this expression: wb_ack_i is just a bus input, and nothing here - * stops state from reaching S_EXEC again if something (a formal - * solver's free wb_ack_i, exploring exactly this "what if the bus - * violates protocol" case; a genuine hardware fault) asserts an - * unrequested ack post-halt. Explicit `!halted` here makes that - * comment's claim actually true rather than merely usually-true, - * closing a real (if narrow) gap a riscv-formal pc_fwd_ch0 - * counterexample found: without it, a spurious post-halt ack could + * Bus-error trapping: an S_MEM response that errors commits + * IMMEDIATELY (the `|| wb_err_i` term) regardless of is_amo_rmw -- + * an AMO whose read phase faults must trap right there, not proceed + * into S_AMO_WRITE and issue a bogus second write (see the FSM below). + * S_AMO_WRITE now commits on wb_done (ack OR err), not just wb_ack_i, + * since a faulted write phase must still retire (as a trap) rather + * than hang. mem_load_access_fault/mem_store_access_fault (declared + * near mem_load_misaligned/mem_store_misaligned below) turn these + * error-commits into the correct trap via exc_code/trap_val. + * + * `!halted` (2026-08-12, removed 2026-08-20): a prior milestone added + * an explicit `!halted` term here to close a riscv-formal pc_fwd_ch0 + * counterexample -- a spurious post-halt wb_ack_i could otherwise * produce a bogus extra rvfi_valid pulse with pc frozen but state - * still oscillating. + * still oscillating, since nothing structurally prevented state from + * reaching S_EXEC again once parked in S_FETCH forever. That whole + * scenario no longer exists as a concept now that EBREAK is a real + * trap (see is_ebreak's own arm in exc_code/trap_taken below): the + * core never parks in S_FETCH permanently anymore, so there is no + * "post-halt" state left to guard against. Confirmed, not assumed: + * pc_fwd_ch0 and pc_bwd_ch0 (the two checks that originally caught + * this counterexample) both re-run clean against this exact removal. */ - wire commit_now = !halted && ((state == S_EXEC && !mem_phase_needed && !div_stall) - || (state == S_MEM && wb_ack_i && !is_amo_rmw) - || (state == S_AMO_WRITE && wb_ack_i)); + wire commit_now = (state == S_EXEC && !mem_phase_needed && !div_stall) + || (state == S_MEM && ((wb_ok && !is_amo_rmw) || wb_err_i)) + || (state == S_AMO_WRITE && wb_done); /* * C extension: fetch_hi_needed decides, on the SAME edge S_FETCH's @@ -370,19 +446,25 @@ module core ( * aligned halfword is always fully inside its own 8-byte dword, so * a quadrant field (the halfword's own low 2 bits) of anything * other than 2'b11 rules out crossing regardless of pc[2:1]. + * + * fetch_hi_taken adds wb_ok on top of fetch_hi_needed: on a fetch + * error, wb_dat_i's bits are meaningless, so a faulted fetch must + * never chase a second, bogus fetch based on garbage -- it needs to + * fall straight through to S_EXEC and trap via fetch_fault_q instead. */ wire fetch_hi_needed = (pc[2:1] == 2'b11) && (wb_dat_i[49:48] == 2'b11); + wire fetch_hi_taken = wb_ok && fetch_hi_needed; always_ff @(posedge clk) begin if (rst) begin state <= S_FETCH; end else begin case (state) - S_FETCH: if (wb_ack_i) state <= state_t'(fetch_hi_needed ? S_FETCH_HI : S_EXEC); - S_FETCH_HI: if (wb_ack_i) state <= S_EXEC; + S_FETCH: if (wb_done) state <= state_t'(fetch_hi_taken ? S_FETCH_HI : S_EXEC); + S_FETCH_HI: if (wb_done) state <= S_EXEC; S_EXEC: state <= state_t'(mem_phase_needed ? S_MEM : (div_stall ? S_EXEC : S_FETCH)); - S_MEM: if (wb_ack_i) state <= state_t'(is_amo_rmw ? S_AMO_WRITE : S_FETCH); - S_AMO_WRITE: if (wb_ack_i) state <= S_FETCH; + S_MEM: if (wb_done) state <= state_t'((wb_ok && is_amo_rmw) ? S_AMO_WRITE : S_FETCH); + S_AMO_WRITE: if (wb_done) state <= S_FETCH; default: state <= S_FETCH; endcase end @@ -407,13 +489,30 @@ module core ( logic [63:0] instr_line_q; logic [15:0] instr_hi_q; logic crossed_q; + /* + * fetch_fault_q: captures whether THIS fetch (S_FETCH or S_FETCH_HI) + * came back as a bus error, at the exact same edge instr_line_q/ + * instr_hi_q get latched. No explicit reset needed -- mirrors those + * two registers' own established convention: always freshly written + * on the edge S_EXEC is first reached, so nothing ever consults it + * uninitialized. Consumed by `instruction` below (substitutes an + * inert placeholder so decode never runs on garbage fetched bits) and + * by exc_code/trap_val (instruction access fault, cause 1). + */ + logic fetch_fault_q; always_ff @(posedge clk) begin - if (state == S_FETCH && wb_ack_i) begin - instr_line_q <= wb_dat_i; - crossed_q <= fetch_hi_needed; + if (state == S_FETCH && wb_done) begin + instr_line_q <= wb_dat_i; + crossed_q <= fetch_hi_taken; + fetch_fault_q <= wb_err_i; + end + if (state == S_FETCH_HI && wb_done) begin + instr_hi_q <= wb_dat_i[15:0]; + fetch_fault_q <= wb_err_i; // plain overwrite, not an OR-latch: S_FETCH_HI is + // only ever reached when S_FETCH's own crossed_q + // (== wb_ok) was set, so fetch_fault_q is + // guaranteed 0 walking into S_FETCH_HI. end - if (state == S_FETCH_HI && wb_ack_i) - instr_hi_q <= wb_dat_i[15:0]; end /* @@ -468,15 +567,26 @@ module core ( wire [31:0] raw32_noncompressed = crossed_q ? {instr_hi_q, hw3} : {second_hw, first_hw}; logic [(`INSTR_SIZE - 1):0] instruction; - assign instruction = is_compressed - ? (c_expand_illegal ? 32'h00000013 /* addi x0,x0,0 -- inert placeholder - value only, never the real trap - mechanism: is_illegal_instr below - (fed by c_expand_illegal - directly) is what actually traps - a reserved compressed encoding. */ - : c_expand_out) - : raw32_noncompressed; + assign instruction = fetch_fault_q + ? 32'h00000013 /* addi x0,x0,0 -- inert placeholder for a FAULTED fetch + (instr_line_q/instr_hi_q are garbage on wb_err_i). + Same trick as the compressed-illegal placeholder + below, one more reason it's safe to reuse: this + makes every downstream classification (is_load, + is_store, is_ebreak, is_illegal_instr, ...) + harmless ADDI-shaped no-ops, so nothing can act on + the garbage bits before the real trap mechanism + (fetch_fault_q feeding exc_code/trap_taken/trap_val + directly, see below) takes over. */ + : is_compressed + ? (c_expand_illegal ? 32'h00000013 /* addi x0,x0,0 -- inert placeholder + value only, never the real trap + mechanism: is_illegal_instr below + (fed by c_expand_illegal + directly) is what actually traps + a reserved compressed encoding. */ + : c_expand_out) + : raw32_noncompressed; /* * pc_plus_len: C extension's generalization of the old fixed pc+4 @@ -716,18 +826,33 @@ module core ( * Reservation register (LR/SC). Set unconditionally on LR's own * retirement (a fresh LR always creates a new reservation, * superseding any prior one -- a plain overwrite, not a conditional - * set). Cleared on ANY store-class instruction's retirement: ordinary - * SB/SH/SW/SD (is_store), SC either way (is_sc, since is_store alone - * only catches SC's MATCHED case), or an AMO's write (is_amo_rmw) -- - * matching the spec requirement that a used reservation can't be - * reused. + * set) -- but ONLY when LR actually retires cleanly (`!trap_taken`), + * same gate reg_write/csr_we already use. Without it, a faulted LR + * (misaligned, or -- since is_lr is purely combinational/decode-based + * and doesn't care about the bus outcome -- a real access-fault via + * mem_store_access_fault, newly reachable once wb_err_i started + * feeding commit_now) would still set a "valid" reservation for a + * load that never happened, letting a later SC to that address + * spuriously report success. Found via code review, not a test + * failure -- see testbench/core_reservation_fault_tb.sv for the + * proof (a faulted LR immediately followed by an SC to the same + * address must NOT report success). Cleared on ANY store-class + * instruction's retirement: ordinary SB/SH/SW/SD (is_store), SC + * either way (is_sc, since is_store alone only catches SC's MATCHED + * case), or an AMO's write (is_amo_rmw) -- matching the spec + * requirement that a used reservation can't be reused. The clear + * arm doesn't need the same `!trap_taken` guard: a faulted store/SC/ + * AMO-write never actually wrote anything either, but invalidating + * the reservation anyway is still spec-conformant (a reservation + * surviving a faulted store attempt is not architecturally + * guaranteed) and strictly safer than leaving it valid. */ logic reservation_valid_q; logic [(`WORD_SIZE - 1):0] reservation_addr_q; always_ff @(posedge clk) begin if (rst) begin reservation_valid_q <= 1'b0; - end else if (commit_now && is_lr) begin + end else if (commit_now && is_lr && !trap_taken) begin reservation_valid_q <= 1'b1; reservation_addr_q <= amo_target_addr; end else if (commit_now && (is_store || is_sc || is_amo_rmw)) begin @@ -937,7 +1062,7 @@ module core ( `INSTR_CODE(SB), `INSTR_CODE(SH), `INSTR_CODE(SW), `INSTR_CODE(SD), `INSTR_CODE(BEQ), `INSTR_CODE(BNE), `INSTR_CODE(BLT), `INSTR_CODE(BGE), `INSTR_CODE(BLTU), `INSTR_CODE(BGEU), - `INSTR_CODE(FENCE), `INSTR_CODE(ECALL), `INSTR_CODE(EBREAK), + `INSTR_CODE(FENCE), `INSTR_CODE(FENCE_I), `INSTR_CODE(ECALL), `INSTR_CODE(EBREAK), `INSTR_CODE(MRET), `INSTR_CODE(SRET), `INSTR_CODE(WFI), `INSTR_CODE(SFENCE_VMA), `INSTR_CODE(INVALID): reg_write_ctrl = 1'b0; @@ -979,7 +1104,7 @@ module core ( 1'b0; /* - * Forward-declared here, same reason is_load/is_store/halted are + * Forward-declared here, same reason is_load/is_store are * (see the comment near the top of this file): csr_rdata is * referenced below in the operand muxes, textually before csr_file0 * -- the instance that actually drives it -- is declared (csr_file0 @@ -1012,17 +1137,28 @@ module core ( * subtest that expects exactly this trap and got a normal return * instead, since TSR was until now inert storage only). * - * Deliberately NOT covered: read-only-CSR-write attempts - * (bits[11:10]) -- csr_file.sv already silently ignores these - * (Zicsr milestone's own decision; core_zicsr_tb.sv's csrrwi-to- - * mhartid case depends on that silent-ignore) -- SFENCE.VMA from - * U-mode (spec-should-trap, but decoded as an unconditional NOP - * this milestone -- see its own comment in - * instructions_and_masks.sv) -- and TVM/TW (mstatus's other two - * "trap on privileged op" bits, same "real storage, not enforced" - * status as TSR was -- SFENCE.VMA-under-TVM and WFI-under-TW are a - * separate gap, not exercised by the S-00 failure that motivated - * this fix, and left as-is rather than speculatively fixed here). + * Read-only-CSR-write attempts (bits[11:10]=='11', a real write + * genuinely attempted -- see csr_readonly_violation below) also + * trap as illegal-instruction, per spec ("Attempts to write a + * read-only CSR... raise illegal instruction exceptions"). Found + * 2026-08-20 via a real ACT4 U-00 failure (newly exercised once + * EBREAK became a real, resumable trap -- U-00's own boot sequence + * attempts csrrw x0, cycle(0xC00), x10, a write to a genuinely + * read-only CSR): traced via sail_riscv_sim --trace-instr/--trace-reg + * against the real ELF, diffed against this core's own RVFI + * retirement trace, confirming the exact first divergence is this + * core silently no-op'ing the write instead of trapping. Previously + * csr_file.sv silently ignored these writes (a Zicsr-milestone + * decision core_zicsr_tb.sv's own csrrwi-to-mhartid case used to + * depend on -- that subtest is retired now that the real trap + * exists; see core_csr_readonly_trap_tb.sv for its replacement). + * + * Still NOT covered: SFENCE.VMA from U-mode (spec-should-trap, but + * decoded as an unconditional NOP this milestone -- see its own + * comment in instructions_and_masks.sv) and TVM/TW (mstatus's other + * two "trap on privileged op" bits, same "real storage, not + * enforced" status TSR had before its own fix -- SFENCE.VMA-under- + * TVM and WFI-under-TW remain a separate, not-yet-exercised gap). */ // Declared here (ahead of csr_file0's instantiation further down) // purely because Icarus's single-pass elaborator wants a net's @@ -1032,6 +1168,30 @@ module core ( wire mstatus_tsr_w; wire is_invalid_instr = (decoded_instruction == `INSTR_CODE(INVALID)); wire csr_priv_violation = is_csr && (imm_2[9:8] > 2'(current_priv)); + // A real write is attempted by every CSR instruction except the + // csrr{s,c}{,i} forms with a zero source (csr_write_suppress already + // captures exactly that set) -- csrrw/csrrwi always attempt a write + // regardless of rd, per spec. bits[11:10]=='11' marks a read-only CSR. + wire csr_readonly_violation = is_csr && !csr_write_suppress && (imm_2[11:10] == 2'b11); + /* + * Debug-mode CSRs (dcsr/dpc/dscratch0/dscratch1, 0x7B0-0x7B3) are a + * genuinely separate check from csr_priv_violation above, not an + * extension of it: that check is a magnitude comparison against + * current_priv (imm_2[9:8] > current_priv), and these four addresses + * encode imm_2[9:8]==2'b11 -- the same encoding as an ordinary + * M-mode-only CSR -- so M-mode code would sail straight through + * csr_priv_violation untouched. Per the RISC-V Debug spec, Debug + * CSRs must only be accessible from Debug Mode itself, never merely + * M-mode, hence a dedicated in_debug_mode gate instead of a + * privilege-level comparison. in_debug_mode is a forward reference -- + * tied 0 here until Milestone 4 builds the real halt/resume FSM this + * state belongs to; until then every access from anywhere traps, + * which is spec-correct (this core has no Debug Mode to legally be + * in yet). + */ + wire in_debug_mode = 1'b0; + wire is_debug_csr_addr = is_csr && (imm_2[11:2] == 10'h1EC); + wire debug_csr_violation = is_debug_csr_addr && !in_debug_mode; wire mret_priv_violation = is_mret && (current_priv != PRIV_M); wire sret_priv_violation = is_sret && ((current_priv == PRIV_U) || (current_priv == PRIV_S && mstatus_tsr_w)); @@ -1044,26 +1204,50 @@ module core ( * is_compressed guard here rather than trusting c_expand_illegal * alone. */ - wire is_illegal_instr = is_invalid_instr || csr_priv_violation + wire is_illegal_instr = is_invalid_instr || csr_priv_violation || csr_readonly_violation + || debug_csr_violation || mret_priv_violation || sret_priv_violation || (is_compressed && c_expand_illegal); wire is_ecall = (decoded_instruction == `INSTR_CODE(ECALL)); + /* + * Zifencei: is_fence_i drives icache_flush_o below (see that port's + * own header comment for why the timing is provably clean). Declared + * here rather than next to is_illegal_instr/is_ecall's own forward + * reference needs -- FENCE.I isn't consumed by exc_code/trap_taken at + * all, so it doesn't need the same forward-declaration treatment + * those wires do. + */ + wire is_fence_i = (decoded_instruction == `INSTR_CODE(FENCE_I)); /* Exception codes, per spec's machine-cause table (synchronous only * -- bit 63/Interrupt is always 0, no interrupt source exists yet). * 4/6 (load/store-AMO address misaligned) are standard RISC-V causes; * mem_load_misaligned/mem_store_misaligned are driven in the Memory - * section below, once mem_paddr exists. */ - wire [3:0] exc_code = is_illegal_instr ? 4'd2 : + * section below, once mem_paddr exists. 1/5/7 (instruction/load/ + * store-AMO access fault) are likewise standard causes, driven by a + * real wb_err_i response -- fetch_fault_q (instruction, cause 1) and + * mem_load_access_fault/mem_store_access_fault (cause 5/7, also + * driven in the Memory section below) are this file's classification + * of WHICH access faulted, mirroring the misaligned pair exactly. + * fetch_fault_q is checked first -- defensive, not strictly required + * (the `instruction` substitution above already makes is_illegal_instr + * etc. structurally false whenever it's set), but omitting its own + * arm would let the fault be silently swallowed as a harmless ADDI. */ + wire [3:0] exc_code = fetch_fault_q ? 4'd1 : + is_illegal_instr ? 4'd2 : + is_ebreak ? 4'd3 : (is_ecall && current_priv == PRIV_U) ? 4'd8 : (is_ecall && current_priv == PRIV_S) ? 4'd9 : (is_ecall && current_priv == PRIV_M) ? 4'd11 : mem_load_misaligned ? 4'd4 : mem_store_misaligned ? 4'd6 : + mem_load_access_fault ? 4'd5 : + mem_store_access_fault ? 4'd7 : 4'd0; // don't-care, gated by trap_taken - wire trap_taken = commit_now && (is_illegal_instr || is_ecall - || mem_load_misaligned || mem_store_misaligned); + assign trap_taken = commit_now && (fetch_fault_q || is_illegal_instr || is_ebreak || is_ecall + || mem_load_misaligned || mem_store_misaligned + || mem_load_access_fault || mem_store_access_fault); /* An M-mode trap never delegates, regardless of medeleg -- falls out * naturally here since current_priv==M forces this wire to 0. */ wire trap_to_s = trap_taken && (current_priv != PRIV_M) && medeleg_w[6'(exc_code)]; @@ -1289,6 +1473,20 @@ module core ( wire [(`WORD_SIZE - 1):0] mtvec_w, stvec_w, mepc_w, sepc_w; wire [1:0] mstatus_mpp_w; wire mstatus_spp_w; + /* + * mip_w/mie_w/mideleg_w: WORD_SIZE-wide (matching csr_file0's real + * architectural CSR width), but the Interrupts section below only + * ever reads bit 7 (MTIE/MTIP/the MTI delegation bit) off each -- + * every other bit is genuinely unused by this milestone's logic + * (they exist for a real, spec-shaped mip/mie/mideleg, not padding), + * same "wrap the genuinely-partial-usage bits" precedent + * trap_vector_base already establishes below for mtvec/stvec's own + * MODE field. + */ + /* verilator lint_off UNUSEDSIGNAL */ + wire [(`WORD_SIZE - 1):0] mip_w, mie_w, mideleg_w; + /* verilator lint_on UNUSEDSIGNAL */ + wire mstatus_mie_w, mstatus_sie_w; `ifdef RISCV_FORMAL wire [(`WORD_SIZE - 1):0] mcause_w, scause_w; wire [(`WORD_SIZE - 1):0] mepc_next_w, sepc_next_w, mcause_next_w, scause_next_w; @@ -1304,11 +1502,12 @@ module core ( .i_instr_retired(commit_now), .i_current_priv(current_priv), - .i_trap_taken(trap_taken), - .i_trap_cause(trap_cause), - .i_trap_val(trap_val), + .i_mtip(i_mtip), + .i_trap_taken(trap_taken || interrupt_taken), + .i_trap_cause(trap_taken ? trap_cause : {1'b1, 63'd7}), + .i_trap_val(trap_taken ? trap_val : `WORD_SIZE'(0)), .i_trap_pc(pc), - .i_trap_to_s(trap_to_s), + .i_trap_to_s(trap_taken ? trap_to_s : interrupt_to_s), .i_mret_taken(mret_taken), .i_sret_taken(sret_taken), @@ -1319,7 +1518,35 @@ module core ( .o_medeleg(medeleg_w), .o_mstatus_mpp(mstatus_mpp_w), .o_mstatus_spp(mstatus_spp_w), - .o_mstatus_tsr(mstatus_tsr_w) + .o_mstatus_tsr(mstatus_tsr_w), + + /* + * Milestone 5 (csr_file.sv CSR-side CLINT/interrupt plumbing) added + * these 5 outputs; Milestone 6 (this section) is their real + * consumer -- see the Interrupts section immediately following + * this instantiation, which drives mti_pending/mti_to_s/ + * mti_enabled/interrupt_taken/interrupt_to_s off exactly these + * five wires. + */ + .o_mip(mip_w), + .o_mie(mie_w), + .o_mideleg(mideleg_w), + .o_mstatus_mie(mstatus_mie_w), + .o_mstatus_sie(mstatus_sie_w), + + /* + * Milestone 3 (Debug CSRs) added dcsr/dpc storage in csr_file.sv, + * but this milestone's own core.sv work stops at trapping illegal + * access to them (see debug_csr_violation above) -- no halt/ + * resume FSM exists yet to actually consume a live dpc/dcsr + * value. Explicitly, deliberately unconnected (not omitted) so + * lint tools see this as intentional, not a forgotten + * connection -- same precedent soc.sv's own dram_* ports use; + * Milestone 4's halt/resume FSM is the real consumer. + */ + /* verilator lint_off PINCONNECTEMPTY */ + .o_dcsr(), .o_dpc() + /* verilator lint_on PINCONNECTEMPTY */ `ifdef RISCV_FORMAL , .o_mcause(mcause_w), @@ -1331,6 +1558,45 @@ module core ( `endif ); + /* --------------------------------------------------------------- * + * Interrupts (CLINT machine-timer, Milestone 6) + * --------------------------------------------------------------- */ + + // MTIE & MTIP (bit 7). mti_to_s reads mideleg_w[7] -- real, unmasked, + // software-writable storage, not hardwired 0; reads 0 in practice + // only because nothing this milestone writes it. + wire mti_pending = mie_w[7] & mip_w[7]; + wire mti_to_s = mideleg_w[7]; + wire mti_enabled = mti_to_s + ? ((current_priv == PRIV_U) ? 1'b1 : (current_priv == PRIV_S) ? mstatus_sie_w : 1'b0) + : ((current_priv != PRIV_M) ? 1'b1 : mstatus_mie_w); + wire int_pending_and_enabled = mti_pending && mti_enabled; + + logic commit_now_q; + always_ff @(posedge clk) begin + if (rst) commit_now_q <= 1'b0; + else commit_now_q <= commit_now; + end + + // interrupt_taken/interrupt_to_s: forward-declared above -- csr_file0's + // own i_trap_taken/i_trap_to_s need them before this section (which + // itself needs csr_file0's own outputs) can exist. + // + // No `!halted` guard here anymore (removed alongside halted's own + // removal below) -- there is no longer a permanent-freeze state to + // guard against post-EBREAK; a future Debug Module halt/resume + // milestone will reintroduce an analogous guard with real semantics. + assign interrupt_taken = commit_now_q && int_pending_and_enabled; + assign interrupt_to_s = interrupt_taken && mti_to_s; + + logic fetch_redirect_q; + always_ff @(posedge clk) begin + if (rst) fetch_redirect_q <= 1'b0; + else if (interrupt_taken) fetch_redirect_q <= 1'b1; + else if (state == S_FETCH && wb_done) fetch_redirect_q <= 1'b0; + end + wire fetch_from_trap_vector = interrupt_taken || fetch_redirect_q; + /* * Branch comparator: NOT routed through alu0. alu_ops.sv has no * equality/inequality op, and each branch type needs a different @@ -1438,13 +1704,49 @@ module core ( assign mem_load_misaligned = (state == S_EXEC) && mem_misaligned && is_load && !is_lr && !is_amo_rmw; assign mem_store_misaligned = (state == S_EXEC) && mem_misaligned && (is_store || is_sc || is_lr || is_amo_rmw); + /* + * mem_load_access_fault/mem_store_access_fault: same is_load/is_lr/ + * is_amo_rmw split as mem_load_misaligned/mem_store_misaligned above + * (LR is spec-classified under store/AMO, cause 7, not load, cause 5 + * -- same reasoning, not just convention-matching), but keyed off a + * real wb_err_i response during S_MEM/S_AMO_WRITE instead of a + * combinational address check during S_EXEC -- a bus error can only + * be discovered once a real bus cycle actually returns. An AMO's + * read-phase fault (S_MEM, wb_err_i, is_amo_rmw) falls into the + * store/AMO term below, same as LR -- it never reaches S_AMO_WRITE + * (see commit_now/the FSM above). An AMO's write-phase fault + * (S_AMO_WRITE, wb_err_i) is its own explicit term, since is_load/ + * is_store don't apply there. + */ + assign mem_load_access_fault = (state == S_MEM) && wb_err_i && is_load && !is_lr && !is_amo_rmw; + assign mem_store_access_fault = ((state == S_MEM) && wb_err_i && (is_store || is_sc || is_lr || is_amo_rmw)) + || ((state == S_AMO_WRITE) && wb_err_i); + + /* + * mem_access_fault_addr: the one place mem_paddr vs. amo_addr_q + * genuinely matters for trap_val below. mem_paddr is live/correct + * during S_MEM, but gets REPURPOSED to the AMO modify value the + * instant S_AMO_WRITE begins (the same hazard the AMO RVFI tap + * already works around) -- an AMO write-phase fault must use + * amo_addr_q instead, or mtval reports garbage. Named separately so + * trap_val's own chain stays a flat, single-level ternary matching + * every sibling arm, rather than growing a nested one just for this + * case. + */ + wire [(`WORD_SIZE - 1):0] mem_access_fault_addr = (state == S_AMO_WRITE) ? amo_addr_q : mem_paddr; + /* trap_val, continued from its forward declaration above: the - * misaligned-access causes report the faulting address (mem_paddr), - * per spec's mtval/stval convention. */ - assign trap_val = is_illegal_instr - ? (is_compressed ? {48'b0, first_hw} - : {{(`WORD_SIZE - `INSTR_SIZE){1'b0}}, instruction}) + * misaligned-access and access-fault causes report the faulting + * address, per spec's mtval/stval convention. fetch_fault_q (cause 1) + * uses pc directly -- csr_file0.i_trap_pc already receives pc + * unconditionally for every trap, so mepc == mtval here, which is + * both spec-correct and sidesteps needing to know whether S_FETCH or + * S_FETCH_HI was the one that actually faulted. */ + assign trap_val = fetch_fault_q ? pc + : is_illegal_instr ? (is_compressed ? {48'b0, first_hw} + : {{(`WORD_SIZE - `INSTR_SIZE){1'b0}}, instruction}) : (mem_load_misaligned || mem_store_misaligned) ? mem_paddr + : (mem_load_access_fault || mem_store_access_fault) ? mem_access_fault_addr : `WORD_SIZE'(0); /* Store data, pre-shifted into the byte lane(s) it'll land in. */ @@ -1494,7 +1796,14 @@ module core ( * sidesteps that entirely. */ always_ff @(posedge clk) begin - if (state == S_MEM && wb_ack_i && is_amo_rmw) begin + if (state == S_MEM && wb_ok && is_amo_rmw) begin + // wb_ok, not bare wb_ack_i -- belt-and-suspenders: the FSM fix + // above (commit_now/state transitions) already guarantees + // S_AMO_WRITE is never entered on an errored read, so this + // capture's value is never consumed either way on a fault, but + // keying it on wb_ack_i alone would still populate it with + // garbage on a paired-error read (icache/dcache's ack+err + // coupling) for no reason. amo_rdata_q <= load_data; /* * Full WORD_SIZE-wide, aligned the same way the load/store RVFI @@ -1545,32 +1854,42 @@ module core ( * during the settle-and-decode cycle in between. * * Both S_FETCH and S_MEM additionally gate cyc_o/stb_o with - * !wb_ack_i, NOT just `state == S_*` -- this is load-bearing, not + * !wb_done, NOT just `state == S_*` -- this is load-bearing, not * decoration. `state` only updates on the NEXT clock edge after - * wb_ack_i is observed (see the state always_ff below), so for the - * entire cycle in between -- from the moment the slave's registered - * ack_o first becomes 1 to the edge core.sv's FSM actually reacts to - * it -- cyc_o/stb_o would otherwise still read as asserted. A slave - * that simply does `if (cyc_i && stb_i) ` (both - * wb4_sram.sv and uart_tx.sv do exactly this, and correctly so -- - * nothing about the spec obligates a slave to guess whether a - * still-asserted cyc/stb is a new request or the master being slow - * to notice the old one) would then see cyc/stb still high on that - * extra cycle and serve the SAME request a second time. Found via - * this exact symptom: the UART printed "HH" for a single-byte write. - * Gating with !wb_ack_i drops cyc_o/stb_o combinationally the moment - * ack_i is observed, so the slave sees the request deasserted before - * it would ever re-fire -- standard Wishbone master practice, and - * the same root cause (not the same fix -- that one patched a - * testbench's own master-role loop) as the wb_cycle ack-timing bug - * in wb4_sram_tb.sv/uart_tx_tb.sv. + * wb_ack_i/wb_err_i is observed (see the state always_ff above), so + * for the entire cycle in between -- from the moment the slave's + * registered ack_o/err_o first becomes 1 to the edge core.sv's FSM + * actually reacts to it -- cyc_o/stb_o would otherwise still read as + * asserted. A slave that simply does `if (cyc_i && stb_i) ` (both wb4_sram.sv and uart_tx.sv do exactly this, and + * correctly so -- nothing about the spec obligates a slave to guess + * whether a still-asserted cyc/stb is a new request or the master + * being slow to notice the old one) would then see cyc/stb still + * high on that extra cycle and serve the SAME request a second time. + * Found via this exact symptom: the UART printed "HH" for a + * single-byte write. Gating with !wb_done drops cyc_o/stb_o + * combinationally the moment the cycle terminates, one way or + * another -- standard Wishbone master practice, and the same root + * cause (not the same fix -- that one patched a testbench's own + * master-role loop) as the wb_cycle ack-timing bug in + * wb4_sram_tb.sv/uart_tx_tb.sv. Using wb_done rather than bare + * wb_ack_i also matters for a genuinely unpaired error response + * (wb4_sram.sv's own convention: err_o without ack_o) -- keyed on + * ack alone, this guard would keep re-driving cyc_o/stb_o forever + * after an error the slave already terminated, the exact bug class + * bus-error trapping (see wb_done/wb_ok's own comment above) exists + * to close everywhere. */ // See this port's own header comment (module port list, above) for // why this is a plain wire off `state`, not folded into - // wb_master_drive's !wb_ack_i-gated combinational block below. + // wb_master_drive's !wb_done-gated combinational block below. assign wb_ifetch_o = (state == S_FETCH) || (state == S_FETCH_HI); + // See this port's own header comment (module port list, above) for + // the timing argument. + assign icache_flush_o = commit_now && is_fence_i; + always_comb begin: wb_master_drive wb_cyc_o = 1'b0; wb_stb_o = 1'b0; @@ -1580,28 +1899,20 @@ module core ( wb_sel_o = 8'b0; case (state) S_FETCH: begin - /* - * Once halted, never issue another fetch -- see the - * halt-latch comment below for why parking here (rather - * than, say, forcing state to hold) is sufficient to - * freeze the whole core. - */ - if (!halted && !wb_ack_i) begin + if (!wb_done) begin wb_cyc_o = 1'b1; wb_stb_o = 1'b1; - wb_addr_o = fetch_addr; + wb_addr_o = fetch_from_trap_vector ? {trap_vector[31:3], 3'b0} : fetch_addr; wb_sel_o = 8'hFF; // don't-care for a read; full line for clarity end end /* * C extension: the second dword of a crossing fetch -- - * reuses the exact same !wb_ack_i gating discipline as - * every other arm here. Never halts mid-crossing (the - * !halted check mirrors S_FETCH's own, for the same reason: - * once halted, issue no further bus traffic at all). + * reuses the exact same !wb_done gating discipline as + * every other arm here. */ S_FETCH_HI: begin - if (!halted && !wb_ack_i) begin + if (!wb_done) begin wb_cyc_o = 1'b1; wb_stb_o = 1'b1; wb_addr_o = fetch_addr_hi; @@ -1609,7 +1920,7 @@ module core ( end end S_MEM: begin - if (!wb_ack_i) begin + if (!wb_done) begin wb_cyc_o = 1'b1; wb_stb_o = 1'b1; wb_we_o = is_store; @@ -1620,11 +1931,11 @@ module core ( end /* * A extension: the write half of a read-modify-write AMO. - * Reuses the exact same !wb_ack_i gating discipline as + * Reuses the exact same !wb_done gating discipline as * S_FETCH/S_MEM above -- load-bearing here too, same reason. */ S_AMO_WRITE: begin - if (!wb_ack_i) begin + if (!wb_done) begin wb_cyc_o = 1'b1; wb_stb_o = 1'b1; wb_we_o = 1'b1; // always a write -- this state exists for exactly this @@ -1700,10 +2011,11 @@ module core ( * bits (the MODE field) are structurally never read below -- masked * off, not an oversight. */ + wire route_to_s = trap_taken ? trap_to_s : interrupt_to_s; /* verilator lint_off UNUSEDSIGNAL */ - wire [(`WORD_SIZE - 1):0] trap_vector_base = trap_to_s ? stvec_w : mtvec_w; + wire [(`WORD_SIZE - 1):0] trap_vector_base = route_to_s ? stvec_w : mtvec_w; /* verilator lint_on UNUSEDSIGNAL */ - wire [(`WORD_SIZE - 1):0] trap_vector = {trap_vector_base[63:2], 2'b00}; + assign trap_vector = {trap_vector_base[63:2], 2'b00}; // was `wire trap_vector =` -- now forward-declared assign next_pc = trap_taken ? trap_vector : mret_taken ? mepc_w : @@ -1713,37 +2025,34 @@ module core ( pc_plus_len; /* --------------------------------------------------------------- * - * PC register / halt latch + * PC register * --------------------------------------------------------------- */ /* - * EBREAK latches `halted` and freezes pc -- the only piece of - * "extra" state in this design, beyond pc/state/instr_line_q. Both - * commit only on commit_now (EBREAK is never a load/store, so it - * always retires at the end of S_EXEC). pc is deliberately excluded - * from advancing on the SAME edge halted is set (`!is_ebreak` below) - * -- otherwise pc would jump past EBREAK on the very edge that's - * supposed to freeze it. After that edge, state parks in S_FETCH - * forever (the bus-driving block above stops issuing fetches once - * halted), so commit_now can never become true again and both - * registers stay frozen with no further gating needed. + * EBREAK is a real synchronous trap now (cause 3, Breakpoint -- see + * is_ebreak's own arm in exc_code/trap_taken above), so it commits + * exactly like any other trap: pc jumps to trap_vector via next_pc's + * own top-priority arm, mepc/mcause/mstatus update for real, and + * execution resumes from whatever mtvec points at. There is no + * freeze/halt special-case here anymore. * - * Testbenches watch `halted` via a hierarchical reference (e.g. - * dut.halted) to know when a test program has finished running, - * without guessing a cycle count. + * The `halted` register (a permanent one-way EBREAK freeze latch, + * removed here 2026-08-20) used to be what every testbench polled + * hierarchically (e.g. dut.halted) to know a test program had + * finished. Since EBREAK no longer parks the core, testbenches now + * detect completion by observing the one-shot `trap_taken && + * is_ebreak` pulse directly and latching it locally (see + * testbench/halt_wait.sv's updated contract). A future Debug Module + * milestone will reintroduce real, resumable halt/resume state under + * a new name -- this is a clean removal, not a placeholder. */ - always_ff @(posedge clk) begin - if (rst) - halted <= 1'b0; - else if (commit_now && is_ebreak) - halted <= 1'b1; - end - always_ff @(posedge clk) begin if (rst) pc <= '0; - else if (commit_now && !is_ebreak) + else if (commit_now) pc <= next_pc; + else if (interrupt_taken) + pc <= trap_vector; end /* @@ -1764,6 +2073,8 @@ module core ( current_priv <= priv_t'(mstatus_mpp_w); else if (sret_taken) current_priv <= priv_t'(mstatus_spp_w ? PRIV_S : PRIV_U); + else if (interrupt_taken) + current_priv <= priv_t'(interrupt_to_s ? PRIV_S : PRIV_M); end `ifdef RISCV_FORMAL @@ -1797,6 +2108,37 @@ module core ( assign rvfi_valid = commit_now; assign rvfi_order = rvfi_order_q; + + /* + * rvfi_intr support: per riscv-formal's own spec (docs/source/rvfi.rst + * upstream), rvfi_intr must be set for the first instruction that is + * part of a trap handler, i.e. one whose rvfi_pc_rdata does not match + * the rvfi_pc_wdata of the previous (valid) retirement. Implemented + * mechanically -- compare THIS retirement's pc against the LAST + * retirement's own next_pc -- not semantically ("was the previous + * retirement a trap_taken/interrupt_taken event"), and deliberately + * so: next_pc's own mux (see its assign below) already gives + * trap_taken top priority (trap_taken ? trap_vector : ...), so a + * synchronous exception's handler-entry PC chain is ALREADY naturally + * consistent in this design -- no discontinuity to flag. A semantic + * check would needlessly over-relax an already-tight, already- + * correctly-passing property for that case. interrupt_taken, by + * contrast, bypasses next_pc entirely via its own separate PC-register + * arm (see the PC register always_ff below) -- a genuine discontinuity + * this mechanical definition catches automatically, with no need to + * enumerate which mechanisms can cause one (robust to any future + * redirect mechanism this core grows later). Purely RVFI-scoped state + * -- zero impact on the real non-formal build. + */ + logic [63:0] rvfi_prev_pc_wdata_q; + always_ff @(posedge clk) begin + if (rst) rvfi_prev_pc_wdata_q <= '0; // matches pc's own reset value, + // so the very first retirement + // after reset correctly reads + // rvfi_intr=0, no special case. + else if (commit_now) rvfi_prev_pc_wdata_q <= next_pc; + end + /* * Per the RVFI spec (docs/source/rvfi.rst upstream): "For compressed * instructions the compressed instruction word must be output on @@ -1819,8 +2161,10 @@ module core ( */ assign rvfi_insn = is_compressed ? {16'b0, first_hw} : instruction; assign rvfi_trap = trap_taken; - assign rvfi_halt = 1'b0; // no graceful-halt/interrupt model exists yet - assign rvfi_intr = 1'b0; // no interrupt controller exists yet (known gap) + assign rvfi_halt = 1'b0; // no graceful-halt model exists yet + assign rvfi_intr = commit_now && (pc != rvfi_prev_pc_wdata_q); // see + // rvfi_prev_pc_wdata_q's own comment above for the + // full derivation -- real wiring, no longer hardwired assign rvfi_mode = current_priv; // PRIV_U/S/M already match RVFI's 0/1/3 encoding assign rvfi_ixl = 2'd2; // always 64-bit -- this core never runs 32-bit mode assign rvfi_rs1_addr = read_gpr_A_sel; diff --git a/design/csr_file.sv b/design/csr_file.sv index f312cda..6ec0767 100644 --- a/design/csr_file.sv +++ b/design/csr_file.sv @@ -67,6 +67,15 @@ * i_current_priv: core.sv's current privilege level (2'b00/01/11 = * U/S/M) -- consumed only by trap-entry below, to know which of * mstatus's MPP/SPP fields records the pre-trap privilege. + * i_mtip: a continuously-valid external status level (same shape as + * i_current_priv -- not a one-shot pulse like the trap/mret/sret + * side channel below), sourced from the CLINT's mtip_o once core.sv + * wires it up (Milestone 6). Spliced combinationally into mip's bit + * 7 (see mip_effective below); defaults to 1'b0 so core.sv's + * not-yet-updated instantiation (and csr_file_random_tb.sv/ + * csr_file_priv_random_tb.sv, which don't drive it either) sees + * inert behavior -- an unconnected port floating to X would + * otherwise contaminate every mip/sip read on bit 7. * i_trap_taken/i_trap_cause/i_trap_val/i_trap_pc/i_trap_to_s: the * trap-entry side channel -- independent of i_csr_we/i_csr_addr, * same "core.sv computes WHY, this module just honors it" division @@ -80,6 +89,12 @@ * i_mret_taken/i_sret_taken: same side-channel shape, for the trap- * RETURN half of mstatus_q's four possible writers (see its own * always_ff below). + * i_debug_entry/i_debug_cause: Milestone 3's own event pulse + data, + * same shape as the trap-entry side channel above but for Debug-Mode + * entry (dcsr.cause/dcsr.prv, dpc). Defaults to 1'b0/3'b0 -- no + * Debug-Mode halt/resume FSM exists in core.sv yet (Milestone 4), so + * core.sv's own instantiation leaves these unconnected for now, + * mirroring i_mtip's identical default-and-defer precedent above. * * Output ports: * o_csr_rdata: Data read from the CSR at i_csr_addr, combinationally. @@ -90,6 +105,19 @@ * mux. o_mstatus_tsr feeds core.sv's SRET-from-S-mode illegal-instruction * check (TSR itself is still just inert storage here, same as TVM/TW -- * core.sv is what turns the bit into an actual trap). + * o_mip/o_mie/o_mideleg/o_mstatus_mie/o_mstatus_sie: control-plane exports + * for Milestone 6's interrupt-taking logic in core.sv (unused/ + * unconnected until then). o_mip drives from the derived mip_effective + * below, never raw mip_q -- same requirement as the mip/sip read-mux + * arms, so core.sv never sees a stale bit 7. o_mie/o_mideleg drive from + * plain mie_q/mideleg_q storage. o_mstatus_mie/o_mstatus_sie are + * single-bit mstatus taps, same precedent as o_mstatus_spp/o_mstatus_tsr + * above. + * o_dcsr/o_dpc: control-plane exports for Milestone 4's halt/resume FSM + * (unused/unconnected until then, same deferred-consumer precedent + * o_mip/etc. above established for Milestone 6). o_dcsr already + * overlays the WARL-fixed xdebugver field, same "never let the raw + * register leak into a read path" requirement o_mip has for mip_q. */ module csr_file ( input logic i_clk, @@ -101,6 +129,7 @@ module csr_file ( input logic i_instr_retired, // drives minstret; independent of i_csr_we/addr input logic [1:0] i_current_priv, + input logic i_mtip = 1'b0, input logic i_trap_taken, input logic [(`WORD_SIZE - 1):0] i_trap_cause, @@ -110,6 +139,9 @@ module csr_file ( input logic i_mret_taken, input logic i_sret_taken, + input logic i_debug_entry = 1'b0, + input logic [2:0] i_debug_cause = 3'b0, + output logic [(`WORD_SIZE - 1):0] o_mtvec, output logic [(`WORD_SIZE - 1):0] o_stvec, output logic [(`WORD_SIZE - 1):0] o_mepc, @@ -117,7 +149,16 @@ module csr_file ( output logic [(`WORD_SIZE - 1):0] o_medeleg, output logic [1:0] o_mstatus_mpp, output logic o_mstatus_spp, - output logic o_mstatus_tsr + output logic o_mstatus_tsr, + + output logic [(`WORD_SIZE - 1):0] o_mip, + output logic [(`WORD_SIZE - 1):0] o_mie, + output logic [(`WORD_SIZE - 1):0] o_mideleg, + output logic o_mstatus_mie, + output logic o_mstatus_sie, + + output logic [(`WORD_SIZE - 1):0] o_dcsr, + output logic [(`WORD_SIZE - 1):0] o_dpc `ifdef RISCV_FORMAL /* * mcause/scause: no real core.sv control logic needs these today (unlike @@ -183,6 +224,24 @@ module csr_file ( localparam logic [(`CSR_ADDR_SIZE - 1):0] CSR_ADDR_MTVAL = 12'h343; localparam logic [(`CSR_ADDR_SIZE - 1):0] CSR_ADDR_MIP = 12'h344; + /* + * Debug-mode CSRs (Milestone 3 of the EBREAK/JTAG staged plan). Real + * spec addresses -- confirmed free of collision with anything above. + * Only meaningfully accessible from Debug Mode; core.sv is + * responsible for trapping any access attempted outside it (see + * design/core.sv's debug_csr_violation, a check separate from + * csr_priv_violation above -- these four addresses encode + * imm_2[9:8]==2'b11, the same bit pattern as an ordinary M-mode-only + * CSR, so the existing magnitude-comparison privilege check would + * let M-mode straight through). This module itself still does no + * address-based access control, same division of responsibility + * described in the module header. + */ + localparam logic [(`CSR_ADDR_SIZE - 1):0] CSR_ADDR_DCSR = 12'h7B0; + localparam logic [(`CSR_ADDR_SIZE - 1):0] CSR_ADDR_DPC = 12'h7B1; + localparam logic [(`CSR_ADDR_SIZE - 1):0] CSR_ADDR_DSCRATCH0 = 12'h7B2; + localparam logic [(`CSR_ADDR_SIZE - 1):0] CSR_ADDR_DSCRATCH1 = 12'h7B3; + /* * Read-only CSRs with no backing storage at all -- fixed values * returned directly by the read mux below, nothing to reset. misa's @@ -351,9 +410,9 @@ module csr_file ( if (i_rst) begin mip_q <= '0; end else if (i_csr_we && (i_csr_addr == CSR_ADDR_MIP)) begin - mip_q <= i_csr_wdata; + mip_q <= i_csr_wdata & ~64'h80; end else if (i_csr_we && (i_csr_addr == CSR_ADDR_SIP)) begin - mip_q <= (mip_q & ~mideleg_q) | (i_csr_wdata & mideleg_q); + mip_q <= ((mip_q & ~mideleg_q) | (i_csr_wdata & mideleg_q)) & ~64'h80; end end @@ -540,6 +599,74 @@ module csr_file ( end end + /* + * Debug-mode CSRs (Milestone 3 of the EBREAK/JTAG staged plan). No + * Debug-Mode halt/resume FSM exists in core.sv yet (Milestone 4) -- + * i_debug_entry stays permanently 0 from core.sv's own instantiation + * until then, so dcsr_q/dpc_q never actually capture anything outside + * a testbench driving the side channel directly. Real storage is + * built now anyway, same "prove the shape before the real consumer + * exists" precedent satp_q already established for Sv39. + * + * dpc_q mirrors mepc_q's own reset -> hardware-event -> ordinary- + * write priority chain and its bit-0 WARL mask exactly (same + * instruction-alignment reasoning; i_trap_pc is reused as-is, not a + * new port -- core.sv already drives it unconditionally every + * cycle). + */ + logic [(`WORD_SIZE - 1):0] dpc_q; + always_ff @(posedge i_clk) begin + if (i_rst) dpc_q <= '0; + else if (i_debug_entry) dpc_q <= i_trap_pc; + else if (i_csr_we && (i_csr_addr == CSR_ADDR_DPC)) dpc_q <= {i_csr_wdata[(`WORD_SIZE - 1):1], 1'b0}; + end + + /* + * dcsr_q: hardware entry only ever touches cause[8:6]/prv[1:0] (per + * spec) -- everything else (ebreakm/s/u/stepie/step/etc.) persists + * across entry, ordinary-CSR-writable only, same partial-bit-range + * nonblocking-assignment idiom mstatus_q's own trap-entry arm already + * uses above for MPP/SPP. xdebugver[31:28] is WARL-fixed at 4, same + * "no storage bit, OR'd in at read-mux time" treatment as + * MSTATUS_UXL_FIXED -- the FULL field is masked out of storage on + * every software write (not just the bits set in the fixed value + * itself), so the OR at read time is always correct regardless of + * what software attempts to write there. + */ + localparam logic [(`WORD_SIZE - 1):0] DCSR_XDEBUGVER_MASK = (`WORD_SIZE'(4'hF) << 28); + localparam logic [(`WORD_SIZE - 1):0] DCSR_XDEBUGVER_FIXED = (`WORD_SIZE'(4) << 28); + + logic [(`WORD_SIZE - 1):0] dcsr_q; + always_ff @(posedge i_clk) begin + if (i_rst) begin + dcsr_q <= '0; + end else if (i_debug_entry) begin + dcsr_q[8:6] <= i_debug_cause; + dcsr_q[1:0] <= i_current_priv; + end else if (i_csr_we && (i_csr_addr == CSR_ADDR_DCSR)) begin + dcsr_q <= i_csr_wdata & ~DCSR_XDEBUGVER_MASK; + end + end + + /* dscratch0/dscratch1: plain full read/write, zero side effects -- + * same "vanilla CSR" shape as mscratch_q above, Debug-Mode program- + * buffer code's own scratch space once one exists. */ + logic [(`WORD_SIZE - 1):0] dscratch0_q, dscratch1_q; + always_ff @(posedge i_clk) begin + if (i_rst) dscratch0_q <= '0; + else if (i_csr_we && (i_csr_addr == CSR_ADDR_DSCRATCH0)) dscratch0_q <= i_csr_wdata; + end + always_ff @(posedge i_clk) begin + if (i_rst) dscratch1_q <= '0; + else if (i_csr_we && (i_csr_addr == CSR_ADDR_DSCRATCH1)) dscratch1_q <= i_csr_wdata; + end + + /* mip_effective: MTIP (bit 7) is a live combinational function of + * i_mtip, not stored state like every other mip bit. Every mip/sip + * read arm and o_mip below must go through this wire -- raw mip_q + * must never leak into a read path again. */ + wire [(`WORD_SIZE - 1):0] mip_effective = {mip_q[63:8], i_mtip, mip_q[6:0]}; + /* Control-plane outputs -- see the port-list comment above for why these exist. */ assign o_mtvec = mtvec_q; assign o_stvec = stvec_q; @@ -549,6 +676,13 @@ module csr_file ( assign o_mstatus_mpp = mstatus_q[MSTATUS_MPP_MSB:MSTATUS_MPP_LSB]; assign o_mstatus_spp = mstatus_q[MSTATUS_SPP_BIT]; assign o_mstatus_tsr = mstatus_q[MSTATUS_TSR_BIT]; + assign o_mip = mip_effective; + assign o_mie = mie_q; + assign o_mideleg = mideleg_q; + assign o_mstatus_mie = mstatus_q[MSTATUS_MIE_BIT]; + assign o_mstatus_sie = mstatus_q[MSTATUS_SIE_BIT]; + assign o_dcsr = dcsr_q | DCSR_XDEBUGVER_FIXED; + assign o_dpc = dpc_q; `ifdef RISCV_FORMAL assign o_mcause = mcause_q; assign o_scause = scause_q; @@ -581,7 +715,7 @@ module csr_file ( CSR_ADDR_SEPC: o_csr_rdata = sepc_q; CSR_ADDR_SCAUSE: o_csr_rdata = scause_q; CSR_ADDR_STVAL: o_csr_rdata = stval_q; - CSR_ADDR_SIP: o_csr_rdata = mip_q & mideleg_q; + CSR_ADDR_SIP: o_csr_rdata = mip_effective & mideleg_q; CSR_ADDR_SATP: o_csr_rdata = satp_q; /* M-mode trap-control CSRs. */ @@ -594,7 +728,13 @@ module csr_file ( CSR_ADDR_MEPC: o_csr_rdata = mepc_q; CSR_ADDR_MCAUSE: o_csr_rdata = mcause_q; CSR_ADDR_MTVAL: o_csr_rdata = mtval_q; - CSR_ADDR_MIP: o_csr_rdata = mip_q; + CSR_ADDR_MIP: o_csr_rdata = mip_effective; + + /* Debug-mode CSRs (Milestone 3). */ + CSR_ADDR_DCSR: o_csr_rdata = dcsr_q | DCSR_XDEBUGVER_FIXED; + CSR_ADDR_DPC: o_csr_rdata = dpc_q; + CSR_ADDR_DSCRATCH0: o_csr_rdata = dscratch0_q; + CSR_ADDR_DSCRATCH1: o_csr_rdata = dscratch1_q; default: o_csr_rdata = `WORD_SIZE'(0); endcase diff --git a/design/csr_file_tb.sv b/design/csr_file_tb.sv index 64ac4b1..424d718 100644 --- a/design/csr_file_tb.sv +++ b/design/csr_file_tb.sv @@ -38,6 +38,7 @@ module csr_file_tb; logic instr_retired; logic [1:0] current_priv; + logic mtip; logic trap_taken; logic [(`WORD_SIZE - 1):0] trap_cause; logic [(`WORD_SIZE - 1):0] trap_val; @@ -46,9 +47,23 @@ module csr_file_tb; logic mret_taken; logic sret_taken; + /* Milestone 3: Debug-mode CSR entry side channel. */ + logic debug_entry; + logic [2:0] debug_cause; + logic [(`WORD_SIZE - 1):0] mtvec_w, stvec_w, mepc_w, sepc_w, medeleg_w; logic [1:0] mstatus_mpp_w; logic mstatus_spp_w; + logic mstatus_tsr_w; + + /* Milestone 5: CSR-side CLINT/interrupt plumbing exports. mideleg_w is + * a NEW, DISTINCT wire from medeleg_w above -- exception delegation + * and interrupt delegation are different CSRs, not aliases. */ + logic [(`WORD_SIZE - 1):0] mip_w, mie_w, mideleg_w; + logic mstatus_mie_w, mstatus_sie_w; + + /* Milestone 3: Debug CSR control-plane exports. */ + logic [(`WORD_SIZE - 1):0] dcsr_w, dpc_w; csr_file dut ( .i_clk(clk), @@ -60,6 +75,7 @@ module csr_file_tb; .i_instr_retired(instr_retired), .i_current_priv(current_priv), + .i_mtip(mtip), .i_trap_taken(trap_taken), .i_trap_cause(trap_cause), .i_trap_val(trap_val), @@ -68,13 +84,26 @@ module csr_file_tb; .i_mret_taken(mret_taken), .i_sret_taken(sret_taken), + .i_debug_entry(debug_entry), + .i_debug_cause(debug_cause), + .o_mtvec(mtvec_w), .o_stvec(stvec_w), .o_mepc(mepc_w), .o_sepc(sepc_w), .o_medeleg(medeleg_w), .o_mstatus_mpp(mstatus_mpp_w), - .o_mstatus_spp(mstatus_spp_w) + .o_mstatus_spp(mstatus_spp_w), + .o_mstatus_tsr(mstatus_tsr_w), + + .o_mip(mip_w), + .o_mie(mie_w), + .o_mideleg(mideleg_w), + .o_mstatus_mie(mstatus_mie_w), + .o_mstatus_sie(mstatus_sie_w), + + .o_dcsr(dcsr_w), + .o_dpc(dpc_w) ); /* Local mirrors of csr_file.sv's address map -- this testbench drives @@ -117,6 +146,15 @@ module csr_file_tb; localparam logic [(`CSR_ADDR_SIZE - 1):0] CSR_ADDR_MTVAL = 12'h343; localparam logic [(`CSR_ADDR_SIZE - 1):0] CSR_ADDR_MIP = 12'h344; + /* Milestone 3: Debug-mode CSR addresses, independently transcribed. */ + localparam logic [(`CSR_ADDR_SIZE - 1):0] CSR_ADDR_DCSR = 12'h7B0; + localparam logic [(`CSR_ADDR_SIZE - 1):0] CSR_ADDR_DPC = 12'h7B1; + localparam logic [(`CSR_ADDR_SIZE - 1):0] CSR_ADDR_DSCRATCH0 = 12'h7B2; + localparam logic [(`CSR_ADDR_SIZE - 1):0] CSR_ADDR_DSCRATCH1 = 12'h7B3; + localparam int DCSR_PRV_LSB=0, DCSR_PRV_MSB=1, DCSR_CAUSE_LSB=6, DCSR_CAUSE_MSB=8; + localparam int DCSR_EBREAKM_BIT=15, DCSR_STEPIE_BIT=11; + localparam logic [(`WORD_SIZE-1):0] DCSR_XDEBUGVER_FIXED = (`WORD_SIZE'(4) << 28); + /* mstatus bit positions, independently transcribed from the spec. */ localparam int SIE_BIT=1, MIE_BIT=3, SPIE_BIT=5, MPIE_BIT=7, SPP_BIT=8; localparam int MPP_LSB=11, MPP_MSB=12; @@ -171,8 +209,9 @@ module csr_file_tb; initial begin csr_addr = 0; csr_we = 0; csr_wdata = 0; instr_retired = 0; - current_priv = 0; trap_taken = 0; trap_cause = 0; trap_val = 0; trap_pc = 0; trap_to_s = 0; + current_priv = 0; mtip = 0; trap_taken = 0; trap_cause = 0; trap_val = 0; trap_pc = 0; trap_to_s = 0; mret_taken = 0; sret_taken = 0; + debug_entry = 0; debug_cause = 0; @(posedge clk); #1; @(posedge clk); #1; @@ -367,7 +406,10 @@ module csr_file_tb; * (1,5) should clear; everything else stays 1. */ write_csr(CSR_ADDR_MIDELEG, (`WORD_SIZE'(1) << 1) | (`WORD_SIZE'(1) << 5)); + check("o_mideleg output matches mideleg_q after write", + mideleg_w, (`WORD_SIZE'(1) << 1) | (`WORD_SIZE'(1) << 5)); write_csr(CSR_ADDR_MIE, {`WORD_SIZE{1'b1}}); + check("o_mie output matches mie_q after write", mie_w, {`WORD_SIZE{1'b1}}); read_csr(CSR_ADDR_SIE, rdata); check("sie read: masked by mideleg, shows only delegated bits", rdata, {`WORD_SIZE{1'b1}} & ((`WORD_SIZE'(1) << 1) | (`WORD_SIZE'(1) << 5))); @@ -375,12 +417,18 @@ module csr_file_tb; read_csr(CSR_ADDR_MIE, rdata); check("sie write to 0: only mideleg-delegated bits (1,5) cleared in mie, rest stay 1", rdata, {`WORD_SIZE{1'b1}} & ~((`WORD_SIZE'(1) << 1) | (`WORD_SIZE'(1) << 5))); - /* sip: identical shape, spot-checked once (mip/mie share the same masked-write logic). */ + check("o_mie output matches mie_q after sie-masked write", mie_w, + {`WORD_SIZE{1'b1}} & ~((`WORD_SIZE'(1) << 1) | (`WORD_SIZE'(1) << 5))); + /* sip: identical shape, spot-checked once (mip/mie share the same masked-write logic). + * Bit 7 (MTIP) is excluded from the expected pattern: Milestone 5's write-masking + * means bit 7 never sticks from a software write to mip/sip regardless of value + * written, and mip_effective's read-mux override means it reads back i_mtip's + * current value (still 0 at this point in the sequence) rather than 1 or X. */ write_csr(CSR_ADDR_MIP, {`WORD_SIZE{1'b1}}); write_csr(CSR_ADDR_SIP, `WORD_SIZE'(0)); read_csr(CSR_ADDR_MIP, rdata); - check("sip write to 0: only mideleg-delegated bits cleared in mip, rest stay 1", - rdata, {`WORD_SIZE{1'b1}} & ~((`WORD_SIZE'(1) << 1) | (`WORD_SIZE'(1) << 5))); + check("sip write to 0: only mideleg-delegated bits cleared in mip, rest stay 1 (bit 7 excluded -- see comment)", + rdata, ({`WORD_SIZE{1'b1}} & ~((`WORD_SIZE'(1) << 1) | (`WORD_SIZE'(1) << 5))) & ~(`WORD_SIZE'(1) << 7)); write_csr(CSR_ADDR_MIDELEG, `WORD_SIZE'(0)); // clean up for later sections /* @@ -435,6 +483,8 @@ module csr_file_tb; | UXL_FIXED | SXL_FIXED); check("M-target trap: o_mepc output matches", mepc_w, 64'h0000_0000_0000_1000); check("M-target trap: o_mstatus_mpp output matches", {62'b0, mstatus_mpp_w}, `WORD_SIZE'(2'b01)); + check("M-target trap: o_mstatus_mie output tracks MIE<-0", mstatus_mie_w, 1'b0); + check("M-target trap: o_mstatus_sie output tracks SIE untouched (1)", mstatus_sie_w, 1'b1); /* * 15. Trap-entry, S-target: same shape, different values, and @@ -514,6 +564,227 @@ module csr_file_tb; | UXL_FIXED | SXL_FIXED); check("sret: o_mstatus_spp reads back U (0)", `WORD_SIZE'(mstatus_spp_w), `WORD_SIZE'(0)); + /* + * =================================================================== + * Milestone 5 additions: mip.MTIP (bit 7) is a live combinational + * function of i_mtip (mip_effective in csr_file.sv), not stored + * state like every other mip bit. These checks confirm (a) mip/sip + * reads track i_mtip with NO clock edge involved at all -- the + * signal is flipped directly and re-read immediately, never via + * write_csr/an explicit @(posedge clk) -- including the sip-view + * case (only visible once mideleg delegates bit 7), and (b) a + * direct software write of all-1s to mip does not make bit 7 + * stick on any read path. Section 20 below uses a narrow + * dut.mip_q hierarchical peek -- an exception to this file's + * black-box discipline everywhere else, taken because (b) alone + * cannot distinguish real write-arm masking from mip_effective's + * read-side override hiding an unmasked mip_q regardless; see + * section 20's own comment. + * =================================================================== + */ + + /* + * 18. mip/sip bit 7 tracks i_mtip live, with no clock edge. + * Known starting state first: mideleg=0 (bit 7 not delegated to + * S) and mip's storage cleared, so only i_mtip drives bit 7 from + * here. + */ + write_csr(CSR_ADDR_MIDELEG, `WORD_SIZE'(0)); + write_csr(CSR_ADDR_MIP, `WORD_SIZE'(0)); + + mtip = 0; + read_csr(CSR_ADDR_MIP, rdata); + check("mip bit 7 reads 0 with i_mtip=0", `WORD_SIZE'(rdata[7]), `WORD_SIZE'(0)); + + mtip = 1; // direct signal change -- no write_csr, no clock edge + read_csr(CSR_ADDR_MIP, rdata); + check("mip bit 7 reads 1 immediately when i_mtip=1, no clock edge involved", + `WORD_SIZE'(rdata[7]), `WORD_SIZE'(1)); + check("o_mip control-plane output live-tracks i_mtip the same as the CSR_ADDR_MIP read", + `WORD_SIZE'(mip_w[7]), `WORD_SIZE'(1)); + + mtip = 0; + read_csr(CSR_ADDR_MIP, rdata); + check("mip bit 7 drops back to 0 immediately when i_mtip drops, no clock edge involved", + `WORD_SIZE'(rdata[7]), `WORD_SIZE'(0)); + + /* sip view: with mideleg bit 7 still 0, sip must NOT show bit 7 + * even though i_mtip=1 -- the delegation mask, not i_mtip alone, + * gates sip's visibility of MTIP. */ + mtip = 1; + read_csr(CSR_ADDR_SIP, rdata); + check("sip bit 7 stays 0 when mideleg bit 7 is 0, even though i_mtip=1", + `WORD_SIZE'(rdata[7]), `WORD_SIZE'(0)); + + /* Delegate bit 7 to S via mideleg -- sip's view must now show it, + * still with no clock edge since i_mtip last changed. */ + write_csr(CSR_ADDR_MIDELEG, `WORD_SIZE'(1) << 7); + read_csr(CSR_ADDR_SIP, rdata); + check("sip bit 7 shows through once mideleg delegates it, i_mtip=1", + `WORD_SIZE'(rdata[7]), `WORD_SIZE'(1)); + + mtip = 0; // live change again, still no clock edge + read_csr(CSR_ADDR_SIP, rdata); + check("sip bit 7 drops to 0 live when i_mtip drops, mideleg still delegating", + `WORD_SIZE'(rdata[7]), `WORD_SIZE'(0)); + + write_csr(CSR_ADDR_MIDELEG, `WORD_SIZE'(0)); // clean up + + /* + * 19. A direct software write of all-1s to mip reads back bit 7 as + * i_mtip's current value (0), not 1. NOTE: this alone does NOT + * distinguish "the write-arm mask reached mip_q's storage" from + * "mip_effective's read-mux override hides bit 7 regardless of + * what's in storage" -- mip_effective unconditionally splices + * i_mtip into bit 7 on every read path (CSR_ADDR_MIP, CSR_ADDR_SIP, + * and o_mip alike), so mip_q[7] is structurally unobservable + * through any of them whether or not the write-arm mask exists. + * The two checks below are still worth keeping as a live-behavior + * regression guard, just not read as write-mask proof. + */ + mtip = 0; + write_csr(CSR_ADDR_MIP, {`WORD_SIZE{1'b1}}); + read_csr(CSR_ADDR_MIP, rdata); + check("mip all-1s software write: bit 7 reads back as i_mtip (0), not 1", + `WORD_SIZE'(rdata[7]), `WORD_SIZE'(0)); + check("o_mip control-plane output agrees: bit 7 reads back as i_mtip (0), not 1", + `WORD_SIZE'(mip_w[7]), `WORD_SIZE'(0)); + + /* + * 20. The write-arm masking itself (mip_q <= ... & ~64'h80, both + * the direct-mip-write arm and the sip-derived-write arm) has NO + * black-box-observable effect per the note above, so it can only + * be verified by inspecting mip_q's own storage directly -- a + * deliberate, narrow exception to this file's black-box discipline + * everywhere else, taken because code review alone already missed + * that this specific arm was untested by every check above it. + */ + write_csr(CSR_ADDR_MIDELEG, `WORD_SIZE'(0)); + write_csr(CSR_ADDR_MIP, `WORD_SIZE'(0)); + write_csr(CSR_ADDR_MIP, {`WORD_SIZE{1'b1}}); + check("direct mip write-arm masks bit 7 out of mip_q storage itself", + dut.mip_q[7], 1'b0); + + write_csr(CSR_ADDR_MIP, `WORD_SIZE'(0)); + write_csr(CSR_ADDR_MIDELEG, `WORD_SIZE'(1) << 7); + write_csr(CSR_ADDR_SIP, {`WORD_SIZE{1'b1}}); + check("sip-derived write-arm masks bit 7 out of mip_q storage itself", + dut.mip_q[7], 1'b0); + write_csr(CSR_ADDR_MIDELEG, `WORD_SIZE'(0)); // clean up + + /* + * 21. Debug-mode CSR entry (Milestone 3): drive the side channel + * directly (no core.sv involved, i_debug_entry/i_debug_cause are + * unconnected from core.sv's own instantiation this milestone -- + * this file is the sole standalone proof this logic is correct + * at all). Confirms dcsr's cause/prv fields update atomically on + * entry while every other software-set field (ebreakm/stepie + * here, standing in for the whole "everything else" class) + * persists untouched -- and that dpc captures i_trap_pc, same + * reuse-not-a-new-port precedent mepc/sepc already established + * for trap-entry. + */ + write_csr(CSR_ADDR_DCSR, (`WORD_SIZE'(1) << DCSR_EBREAKM_BIT) | (`WORD_SIZE'(1) << DCSR_STEPIE_BIT)); + read_csr(CSR_ADDR_DCSR, rdata); + check("dcsr pre-entry: ebreakm/stepie stick, xdebugver reads 4, cause/prv still 0", + rdata, + (`WORD_SIZE'(1) << DCSR_EBREAKM_BIT) | (`WORD_SIZE'(1) << DCSR_STEPIE_BIT) | DCSR_XDEBUGVER_FIXED); + + @(negedge clk); + current_priv = 2'b01; // S -- the mode debug entry is taken FROM + debug_entry = 1'b1; debug_cause = 3'd1; // 1 = ebreak, per spec + trap_pc = 64'h0000_0000_0000_2000; + @(posedge clk); #1; + debug_entry = 1'b0; + + read_csr(CSR_ADDR_DCSR, rdata); + check("dcsr post-entry: cause<-1, prv<-S(01), ebreakm/stepie/xdebugver untouched", + rdata, + (`WORD_SIZE'(1) << DCSR_EBREAKM_BIT) | (`WORD_SIZE'(1) << DCSR_STEPIE_BIT) | DCSR_XDEBUGVER_FIXED + | (`WORD_SIZE'(1) << DCSR_CAUSE_LSB) | (`WORD_SIZE'(2'b01) << DCSR_PRV_LSB)); + check("o_dcsr output agrees with the CSR readback", dcsr_w, rdata); + + read_csr(CSR_ADDR_DPC, rdata); + check("dpc captured i_trap_pc on debug entry", rdata, 64'h0000_0000_0000_2000); + check("o_dpc output agrees with the CSR readback", dpc_w, rdata); + + /* + * A second entry, with DIFFERENT cause/prv values, distinguishes + * a correct overwrite from an accidental accumulate/OR bug in + * the hardware-entry arm -- the first entry above went 0->1 + * for both fields, which an OR-accumulate bug can't be told + * apart from a plain assign. Picked so overwrite and OR- + * accumulate produce DIFFERENT results in both fields: cause + * 3'd1 (001) then 3'd2 (010) -- OR gives 011(3), assign gives + * 010(2); prv 2'b01 (S) then 2'b00 (U) -- OR leaves it stuck at + * 01(S), assign correctly drops to 00(U). + */ + @(negedge clk); + current_priv = 2'b00; // U + debug_entry = 1'b1; debug_cause = 3'd2; + trap_pc = 64'h0000_0000_0000_3000; + @(posedge clk); #1; + debug_entry = 1'b0; + + read_csr(CSR_ADDR_DCSR, rdata); + check("dcsr second entry: cause freshly overwritten to 2 (not OR-accumulated to 3)", + rdata[DCSR_CAUSE_MSB:DCSR_CAUSE_LSB], 3'd2); + check("dcsr second entry: prv freshly overwritten to U(00) (not OR-stuck at S(01))", + {62'b0, rdata[DCSR_PRV_MSB:DCSR_PRV_LSB]}, 64'd0); + check("dcsr second entry: ebreakm still set", {63'b0, rdata[DCSR_EBREAKM_BIT]}, 64'd1); + check("dcsr second entry: stepie still set", {63'b0, rdata[DCSR_STEPIE_BIT]}, 64'd1); + check("dcsr second entry: xdebugver still reads 4", + rdata & (`WORD_SIZE'(4'hF) << 28), DCSR_XDEBUGVER_FIXED); + read_csr(CSR_ADDR_DPC, rdata); + check("dpc re-captured i_trap_pc on the second entry too", rdata, 64'h0000_0000_0000_3000); + + /* + * Simultaneous i_debug_entry and a same-cycle software write to + * DCSR: the if/else-if priority chain in csr_file.sv means + * debug_entry must win outright, not merge with the write. + * Driven manually (not via write_csr(), which runs its own + * separate negedge/posedge cycle) so both land on the exact + * same clock edge. + */ + @(negedge clk); + current_priv = 2'b01; // S + debug_entry = 1'b1; debug_cause = 3'd5; + trap_pc = 64'h0000_0000_0000_4000; + csr_addr = CSR_ADDR_DCSR; csr_wdata = {`WORD_SIZE{1'b1}}; csr_we = 1'b1; + @(posedge clk); #1; + debug_entry = 1'b0; csr_we = 1'b0; + + read_csr(CSR_ADDR_DCSR, rdata); + check("simultaneous i_debug_entry + software write: debug_entry wins outright (cause<-5)", + rdata[DCSR_CAUSE_MSB:DCSR_CAUSE_LSB], 3'd5); + check("simultaneous i_debug_entry + software write: debug_entry wins outright (prv<-S(01))", + {62'b0, rdata[DCSR_PRV_MSB:DCSR_PRV_LSB]}, 64'd1); + + /* xdebugver is WARL-fixed: an all-1s software write must not stick. */ + write_csr(CSR_ADDR_DCSR, {`WORD_SIZE{1'b1}}); + read_csr(CSR_ADDR_DCSR, rdata); + check("dcsr xdebugver field ignores an all-1s software write, still reads 4", + rdata & (`WORD_SIZE'(4'hF) << 28), DCSR_XDEBUGVER_FIXED); + check("dcsr bit 27 (just below xdebugver) keeps the all-1s write -- mask isn't too wide", + {63'b0, dut.dcsr_q[27]}, 64'd1); + check("dcsr bit 32 (just above xdebugver) keeps the all-1s write -- mask isn't too wide", + {63'b0, dut.dcsr_q[32]}, 64'd1); + + /* dpc is WARL like mepc/sepc: bit 0 always reads 0. */ + write_csr(CSR_ADDR_DPC, 64'h0000_0000_0000_1001); + read_csr(CSR_ADDR_DPC, rdata); + check("dpc write is bit-0 masked (WARL), same as mepc/sepc", rdata, 64'h0000_0000_0000_1000); + + /* dscratch0/dscratch1: plain full read/write round trip. */ + write_csr(CSR_ADDR_DSCRATCH0, 64'hCAFE_F00D_0000_0001); + read_csr(CSR_ADDR_DSCRATCH0, rdata); + check("dscratch0 read/write round trip", rdata, 64'hCAFE_F00D_0000_0001); + write_csr(CSR_ADDR_DSCRATCH1, 64'hCAFE_F00D_0000_0002); + read_csr(CSR_ADDR_DSCRATCH1, rdata); + check("dscratch1 read/write round trip", rdata, 64'hCAFE_F00D_0000_0002); + read_csr(CSR_ADDR_DSCRATCH0, rdata); + check("dscratch0 unaffected by dscratch1's write", rdata, 64'hCAFE_F00D_0000_0001); + $display(""); $display("csr_file_tb: %0d passed, %0d failed", pass_count, fail_count); if (fail_count > 0) $display("csr_file_tb: FAILURES PRESENT"); diff --git a/design/decoder.sv b/design/decoder.sv index 7e48622..3f5cd69 100644 --- a/design/decoder.sv +++ b/design/decoder.sv @@ -697,6 +697,18 @@ module decoder ( end: fence_instr + /* + * Zifencei: FENCE.I takes no operands (unlike plain FENCE's I-type + * pred/succ/fm fields, which core.sv doesn't even consult) -- same + * zero-operand shape as ECALL/EBREAK/MRET/SRET/WFI, a closer + * semantic fit than reusing FENCE's I-type shape for a field set + * that would just decode to unused bits. + */ + else if (`IS_INSTR(i_instruction, FENCE_I)) begin: fence_i_instr + `OUTPUT_NONE_TYPE_INSTR(FENCE_I); + end: fence_i_instr + + else if (`IS_INSTR(i_instruction, ECALL)) begin: ecall_instr `OUTPUT_NONE_TYPE_INSTR(ECALL); end: ecall_instr diff --git a/design/defaults/instruction_codes.sv b/design/defaults/instruction_codes.sv index a2285cf..839b1f3 100644 --- a/design/defaults/instruction_codes.sv +++ b/design/defaults/instruction_codes.sv @@ -206,4 +206,12 @@ /* ------------------------------------------------------------------------- */ +/* Zifencei. */ + +`define INSTR_CODE_FENCE_I 1100010 // 98 + + +/* ------------------------------------------------------------------------- */ + + /* End of file. */ diff --git a/design/defaults/instructions_and_masks.sv b/design/defaults/instructions_and_masks.sv index b1078fb..4aa2134 100644 --- a/design/defaults/instructions_and_masks.sv +++ b/design/defaults/instructions_and_masks.sv @@ -287,6 +287,16 @@ `define INSTR_FENCE {17'b0, 3'b000, 5'b0, 7'b0001111} `define INSTR_MASK_FENCE {17'b0, 3'b111, 5'b0, 7'b0001111} +/* + * Zifencei: FENCE.I -- same don't-care shape as plain FENCE above, + * differing only in funct3 (001 vs 000). Collision-free: the only two + * defined encodings at opcode 0001111 are FENCE/FENCE.I; funct3=010-111 + * remain correctly unmatched, falling through to illegal-instruction, + * no change needed there. + */ +`define INSTR_FENCE_I {17'b0, 3'b001, 5'b0, 7'b0001111} +`define INSTR_MASK_FENCE_I {17'b0, 3'b111, 5'b0, 7'b0001111} + /* ------------------------------------------------------------------------- */ diff --git a/design/icache.sv b/design/icache.sv index f589fa3..7113bb6 100644 --- a/design/icache.sv +++ b/design/icache.sv @@ -67,6 +67,17 @@ module icache #( output logic ack_o, output logic err_o, + /* + * Zifencei: FENCE.I pulses this for one cycle on its own retirement. + * Invalidates every line -- a bare valid_q<='0, mirroring the reset + * arm exactly, since state_q/ack_o/dat_o never have anything in + * flight at the cycle this can pulse (see cache_complex.sv's own + * comment on why the flush is unconditional, and core.sv's + * icache_flush_o port comment for the FSM-timing proof that this + * module is always CACHE_IDLE when it fires). + */ + input logic flush_i, + // Memory-facing port -- this module is a Wishbone MASTER from // wb4_sram.sv's side. output logic [31:0] mem_addr_o, @@ -169,9 +180,19 @@ module icache #( * hardware has no defined memory power-up state, which is * true of data_q/tag_q here too, but valid_q isn't memory * contents, it's the cache's own correctness invariant. + * + * flush_i (Zifencei) shares this same valid_q<='0 effect but + * is handled as a separate condition just below, inside the + * else branch -- deliberately NOT folded into this rst arm, + * since flush_i must NOT also reset ack_o/err_o/dat_o/state_q + * (nothing is ever in flight to unwind when it pulses; see + * this module's flush_i port comment for the proof) the way + * a real reset correctly does. */ valid_q <= '0; end else begin + if (flush_i) + valid_q <= '0; case (state_q) CACHE_IDLE: begin ack_o <= 1'b0; diff --git a/design/icache_tb.sv b/design/icache_tb.sv index 4d2971b..97fe844 100644 --- a/design/icache_tb.sv +++ b/design/icache_tb.sv @@ -48,6 +48,7 @@ module icache_tb; logic [31:0] ic_addr; logic [63:0] ic_dat_o; logic ic_cyc, ic_stb, ic_ack, ic_err; + logic ic_flush = 1'b0; // icache <-> wb4_sram, memory-facing logic [31:0] mem_addr; @@ -58,7 +59,7 @@ module icache_tb; icache #(.num_lines(NUM_LINES), .line_words(LINE_WORDS)) dut ( .clk(clk), .rst(rst), .addr_i(ic_addr), .dat_o(ic_dat_o), .cyc_i(ic_cyc), .stb_i(ic_stb), - .ack_o(ic_ack), .err_o(ic_err), + .ack_o(ic_ack), .err_o(ic_err), .flush_i(ic_flush), .mem_addr_o(mem_addr), .mem_dat_i(mem_dat_o), .mem_sel_o(mem_sel), .mem_we_o(mem_we), .mem_cyc_o(mem_cyc), .mem_stb_o(mem_stb), .mem_ack_i(mem_ack), .mem_err_i(mem_err) @@ -177,6 +178,34 @@ module icache_tb; ic_read(32'h1000); check("errored line not installed: re-misses", 64'(mem_cyc_pulses), 64'd1); + /* + * Zifencei: flush_i. Index 0's line (address 0x00) is still + * validly cached at this point -- the errored refill just above + * targeted the SAME index (0x1000 aliases index 0 too, per this + * file's own address-breakdown comments) but aborted without + * installing anything, per icache.sv's own "leaves valid_q + * exactly as it was" guarantee, so 0x00's earlier real content is + * untouched. Confirm that first (a genuine hit, zero downstream + * traffic), THEN pulse flush_i and confirm the exact same address + * now re-misses -- the direct, white-box proof flush_i actually + * invalidates valid_q, not just that the signal toggles. + */ + mem_cyc_pulses = 0; + ic_read(32'h00); + check("pre-flush: index 0 still cached", ic_dat_o, 64'hAAAA_0000_0000_0000); + check("pre-flush: zero downstream traffic (genuine hit)", 64'(mem_cyc_pulses), 64'd0); + + @(negedge clk); + ic_flush = 1'b1; + @(posedge clk); #1; + ic_flush = 1'b0; + + mem_cyc_pulses = 0; + ic_read(32'h00); + check("post-flush: same address now re-misses", ic_dat_o, 64'hAAAA_0000_0000_0000); + check("post-flush: LINE_WORDS downstream beats (genuine miss, not a false hit)", + 64'(mem_cyc_pulses), 64'(LINE_WORDS)); + $display(""); $display("icache_tb: %0d passed, %0d failed", pass_count, fail_count); if (fail_count > 0) $display("icache_tb: FAILURES PRESENT"); diff --git a/design/soc.sv b/design/soc.sv index 26098e6..c4becc4 100644 --- a/design/soc.sv +++ b/design/soc.sv @@ -7,11 +7,21 @@ * Module: soc * * Top-level integration: core (Wishbone master) <-> wb_addr_decoder <-> - * {cache_complex -> wb4_sram, uart_tx}. Exactly the wiring already proven - * in testbench/core_wb_tb.sv (for core<->decoder<->{ram,uart}) and - * testbench/core_cache_harness.sv (for core<->cache_complex<->sram) -- - * this file adds no new logic of its own, only the connections between - * already-independently-verified pieces. + * {cache_complex -> wb4_sram, uart_tx, uart_rx, clint0}. Exactly the wiring already + * proven in testbench/core_wb_tb.sv (for core<->decoder<->{ram,uart}), + * testbench/core_cache_harness.sv (for core<->cache_complex<->sram), and + * testbench/decoder_clint_harness.sv (for decoder<->{ram,uart,clint} at + * the bus level, see that harness's own header for exactly what it does + * and doesn't cover) -- this file adds no new logic of its own, only the + * connections between already-independently-verified pieces. + * + * clint0 (design/clint.sv, Milestone 3, already independently verified) + * hangs off the decoder's third slave port exactly like uart0 hangs off + * its second -- see wb_addr_decoder.sv's own header for the 3-way + * address map this now routes. clint0's mtip_o (this milestone) wires + * directly to core0.i_mtip -- see clint_mtip below -- feeding core.sv's + * machine-timer-interrupt-taking logic (design/core.sv's own Interrupts + * section, near csr_file0's instantiation). * * cache_complex sits AFTER wb_addr_decoder, between it and wb4_sram -- * not before the decoder. This is deliberate, not incidental ordering: @@ -26,6 +36,16 @@ * hierarchy's own design notes (project memory: cache-hierarchy-plan) for * the full reasoning. * + * clint0 sits outside cache_complex for the SAME structural reason, not + * a separate one worth re-deriving: mtime free-runs every cycle and the + * CPU never writes it, so a cached read would freeze at whatever value + * it first saw, with nothing to ever invalidate it -- permanently + * breaking any `while (mtime < deadline);` poll loop. Placing clint0 + * downstream of the decoder, alongside uart0, makes it structurally + * uncacheable the same way -- not an incidental side effect of where it + * happened to get wired, and NOT something a future refactor should + * "simplify" by routing it through cache_complex alongside RAM. + * * wb4_sram is instantiated at its default num_words (4096, 32KB) -- * unlike core_wb_tb.sv's deliberately small test instance, this is the * real memory map wb_addr_decoder.sv's address split (addr_i[15]) is @@ -35,18 +55,28 @@ * choice to keep, not something wb_addr_decoder.sv or wb4_sram.sv need to * know about -- the cache is fully transparent to both. * - * KNOWN, DELIBERATE LIMITATION: no I$/D$ coherence for self-modifying - * code. This ISA has no Zifencei, so software has no instruction-level - * way to invalidate a stale I$ line after a D$ store to the same physical - * address -- a store that lands in an address I$ already has cached - * leaves that stale copy in place, with nothing to evict it, until it's - * naturally replaced by a later conflicting fetch. Accepted as a - * documented gap for this milestone (same treatment as this project's - * other deliberately-deferred items -- EBREAK's sim-only halt, FENCE - * under-implementation) rather than adding cross-cache snoop/invalidate - * wiring now. Today's firmware never self-modifies, so nothing currently - * exercises this, but it is a real, silent-corruption-class gap if that - * ever changes -- must stay visible here, not buried. + * I$/D$ coherence for self-modifying code (Zifencei, FENCE.I): closed + * 2026-08-17, no longer a gap. core0.icache_flush_o (pulses one cycle on + * FENCE.I's own retirement) wires straight to cache0.flush_i, which + * cache_complex.sv passes through unconditionally to icache0 -- software + * that stores new instruction bytes then executes FENCE.I before jumping + * to them gets a correctly-invalidated I$, matching the RISC-V spec's + * own Zifencei contract. D$ never needed an equivalent flush path -- + * write-through already keeps a store hit's cached copy and SRAM in + * lockstep. See design/core.sv's icache_flush_o port comment and + * design/icache.sv's flush_i port comment for the full timing proof. + * + * uart_rx0 (design/uart_rx.sv, Milestone 2 of the EBREAK/JTAG staged + * plan) is a new sibling to uart0, NOT a new decoder port -- the two + * share the decoder's existing single uart_* port group, split by a new + * addr_i[4] sub-decode introduced in THIS file (0 routes to uart0/TX, 1 + * routes to uart_rx0/RX). This is the first address-decode logic soc.sv + * itself has ever contained -- every other split (RAM/UART/CLINT/DRAM) + * lives one level down in wb_addr_decoder.sv, which still only ever + * sees one opaque "uart" target and has no reason to know it's now + * backed by two physical instances. See the detailed rationale right + * where that split is wired, next to the uart0/uart_rx0 instantiations + * below. * * No UART pin exists at this level (or anywhere in this design) -- see * uart_tx.sv's header for why: this milestone's UART "transmits" via @@ -63,19 +93,23 @@ module soc ( logic [7:0] wb_sel; logic wb_we, wb_cyc, wb_stb, wb_ack, wb_err; logic wb_ifetch; + logic icache_flush; + logic clint_mtip; core core0 ( .clk(clk), .rst(rst), .wb_addr_o(wb_addr), .wb_dat_o(wb_dat_m2s), .wb_dat_i(wb_dat_s2m), .wb_sel_o(wb_sel), .wb_we_o(wb_we), .wb_cyc_o(wb_cyc), .wb_stb_o(wb_stb), - .wb_ack_i(wb_ack), .wb_err_i(wb_err), .wb_ifetch_o(wb_ifetch) + .wb_ack_i(wb_ack), .wb_err_i(wb_err), .wb_ifetch_o(wb_ifetch), + .icache_flush_o(icache_flush), .i_mtip(clint_mtip) ); - logic [31:0] ram_addr, uart_addr; - logic [63:0] ram_dat_o, ram_dat_i, uart_dat_o, uart_dat_i; - logic [7:0] ram_sel, uart_sel; + logic [31:0] ram_addr, uart_addr, clint_addr; + logic [63:0] ram_dat_o, ram_dat_i, uart_dat_o, uart_dat_i, clint_dat_o, clint_dat_i; + logic [7:0] ram_sel, uart_sel, clint_sel; logic ram_we, ram_cyc, ram_stb, ram_ack, ram_err; logic uart_we, uart_cyc, uart_stb, uart_ack, uart_err; + logic clint_we, clint_cyc, clint_stb, clint_ack, clint_err; wb_addr_decoder decoder0 ( .clk(clk), .rst(rst), @@ -86,7 +120,39 @@ module soc ( .ram_stb_o(ram_stb), .ram_ack_i(ram_ack), .ram_err_i(ram_err), .uart_addr_o(uart_addr), .uart_dat_o(uart_dat_o), .uart_dat_i(uart_dat_i), .uart_sel_o(uart_sel), .uart_we_o(uart_we), .uart_cyc_o(uart_cyc), - .uart_stb_o(uart_stb), .uart_ack_i(uart_ack), .uart_err_i(uart_err) + .uart_stb_o(uart_stb), .uart_ack_i(uart_ack), .uart_err_i(uart_err), + .clint_addr_o(clint_addr), .clint_dat_o(clint_dat_o), .clint_dat_i(clint_dat_i), + .clint_sel_o(clint_sel), .clint_we_o(clint_we), .clint_cyc_o(clint_cyc), + .clint_stb_o(clint_stb), .clint_ack_i(clint_ack), .clint_err_i(clint_err), + + /* + * dram_* left explicitly, deliberately unconnected -- design/ + * wb_addr_decoder.sv's DRAM slave (verification/taxi/rtl/ + * dram_model.sv) instantiates a SystemVerilog `interface` + * internally, so it can only be built via Verilator, never + * iverilog (see verification/taxi/README.md). This file must stay + * 100% iverilog-compatible -- it's compiled by + * testbench/soc_tb.sv, testbench/soc_interrupt_tb.sv, + * testbench/soc_c_regression_tb.sv, and others -- so it can never + * instantiate dram_model.sv directly. Empty parens rather than + * omitting the lines, so lint tools see this as deliberate, not a + * forgotten connection -- same precedent core0's own + * .o_instruction_address() already uses in design/core.sv. + * Known, accepted gap: any address in 0x0001_8000-0x0001_FFFF + * routed through THIS soc.sv today gets an undefined response + * (dram_ack_i/dram_err_i float) -- acceptable only because no + * existing design/testbench firmware or testbench ever generates + * such an address (confirmed by inspection). A real consumer + * exists only under verification/taxi/rtl/decoder_dram_harness.sv; + * wiring dram_model.sv into THIS file for real would need a + * separate Verilator-only top-level, not a change here -- see + * verification/taxi/README.md's Status section. + */ + /* verilator lint_off PINCONNECTEMPTY */ + .dram_addr_o(), .dram_dat_o(), .dram_dat_i(), + .dram_sel_o(), .dram_we_o(), .dram_cyc_o(), + .dram_stb_o(), .dram_ack_i(), .dram_err_i() + /* verilator lint_on PINCONNECTEMPTY */ ); logic [31:0] mem_addr; @@ -98,7 +164,7 @@ module soc ( .clk(clk), .rst(rst), .addr_i(ram_addr), .dat_i(ram_dat_o), .dat_o(ram_dat_i), .sel_i(ram_sel), .we_i(ram_we), .ifetch_i(wb_ifetch), .cyc_i(ram_cyc), .stb_i(ram_stb), - .ack_o(ram_ack), .err_o(ram_err), + .ack_o(ram_ack), .err_o(ram_err), .flush_i(icache_flush), .mem_addr_o(mem_addr), .mem_dat_o(mem_dat_m2s), .mem_dat_i(mem_dat_s2m), .mem_sel_o(mem_sel), .mem_we_o(mem_we), .mem_cyc_o(mem_cyc), .mem_stb_o(mem_stb), .mem_ack_i(mem_ack), .mem_err_i(mem_err) @@ -110,10 +176,61 @@ module soc ( .ack_o(mem_ack), .err_o(mem_err), .cyc_i(mem_cyc), .stb_i(mem_stb), .we_i(mem_we) ); + /* + * uart_addr[4] sub-decode: soc.sv's own addr_i[4] split of the + * decoder's single uart_* port group between uart0 (TX, addr_i[4]=0) + * and uart_rx0 (RX, addr_i[4]=1) -- see design/uart_rx.sv's own + * header for the register map this produces (0x8000/0x8008 TX, + * 0x8010/0x8018 RX). + * + * uart_cyc/uart_stb are gated combinationally by uart_sel_rx (itself + * a plain combinational read of uart_addr[4], which the decoder + * holds stable for the full duration of a transaction) BEFORE they + * ever reach either instance, so exactly one of the two ever sees a + * live request on a given cycle -- the other's own `cyc_i && stb_i` + * reads false and it correctly holds its own ack_o/err_o low that + * cycle (see uart_tx.sv/uart_rx.sv's own always block: the else + * branch drives both low). Since at most one instance is ever + * asserting ack_o on any given cycle, the two local ack/err/dat_o + * triples can simply be OR'd/muxed back together below with no + * arbitration needed -- not a coincidence, a direct consequence of + * the mutually-exclusive gating above. A future refactor must not + * "simplify" this by feeding both instances the same ungated + * uart_cyc/uart_stb -- that would make both instances respond to + * every UART access, corrupting whichever one wasn't the real + * target. + */ + wire uart_sel_rx = uart_addr[4]; + + wire uart_tx_cyc = uart_cyc && !uart_sel_rx; + wire uart_tx_stb = uart_stb && !uart_sel_rx; + wire uart_rx_cyc = uart_cyc && uart_sel_rx; + wire uart_rx_stb = uart_stb && uart_sel_rx; + + logic [63:0] uart_tx_dat_o, uart_rx_dat_o; + logic uart_tx_ack, uart_tx_err, uart_rx_ack, uart_rx_err; + + assign uart_ack = uart_tx_ack | uart_rx_ack; + assign uart_err = uart_tx_err | uart_rx_err; + assign uart_dat_i = uart_tx_ack ? uart_tx_dat_o : uart_rx_dat_o; + uart_tx uart0 ( .clk(clk), .rst(rst), - .addr_i(uart_addr), .dat_i(uart_dat_o), .dat_o(uart_dat_i), .sel_i(uart_sel), - .ack_o(uart_ack), .err_o(uart_err), .cyc_i(uart_cyc), .stb_i(uart_stb), .we_i(uart_we) + .addr_i(uart_addr), .dat_i(uart_dat_o), .dat_o(uart_tx_dat_o), .sel_i(uart_sel), + .ack_o(uart_tx_ack), .err_o(uart_tx_err), .cyc_i(uart_tx_cyc), .stb_i(uart_tx_stb), .we_i(uart_we) + ); + + uart_rx uart_rx0 ( + .clk(clk), .rst(rst), + .addr_i(uart_addr), .dat_i(uart_dat_o), .dat_o(uart_rx_dat_o), .sel_i(uart_sel), + .ack_o(uart_rx_ack), .err_o(uart_rx_err), .cyc_i(uart_rx_cyc), .stb_i(uart_rx_stb), .we_i(uart_we) + ); + + clint clint0 ( + .clk(clk), .rst(rst), + .addr_i(clint_addr), .dat_i(clint_dat_o), .dat_o(clint_dat_i), .sel_i(clint_sel), + .ack_o(clint_ack), .err_o(clint_err), .cyc_i(clint_cyc), .stb_i(clint_stb), .we_i(clint_we), + .mtip_o(clint_mtip) ); endmodule diff --git a/design/uart_rx.sv b/design/uart_rx.sv new file mode 100644 index 0000000..fd4e443 --- /dev/null +++ b/design/uart_rx.sv @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +/* + * Module: uart_rx + * + * A real, addressable Wishbone-slave peripheral -- the receive-side + * sibling of uart_tx.sv, matching its house style exactly (no baud-rate + * timing, registered 1-wait-state ack, err_o never asserted). Real + * serial-line timing is out of scope for this milestone, same as it was + * for uart_tx.sv -- a byte "arrives" the instant a testbench calls the + * push_byte backdoor task below, the mirror image of uart_tx.sv's own + * tx_history capture (that one records what firmware SENT; this one + * lets a testbench inject what firmware RECEIVES). + * + * Lives in soc.sv as a sibling instance to uart0 (uart_tx), sharing the + * same decoder-facing uart_* port group -- soc.sv itself is responsible + * for splitting that one Wishbone target between the two instances via + * a new addr_i[4] sub-decode (0 routes to TX, 1 routes to RX), gating + * each instance's own cyc_i/stb_i accordingly. From this module's own + * perspective, addr_i[4] is irrelevant/already decided -- only addr_i[3] + * is decoded here, exactly matching uart_tx.sv's own "wb_addr_decoder.sv + * already checked addr_i[15], nothing else needs checking" precedent, + * generalized one level: soc.sv has already checked addr_i[4] too by the + * time cyc_i/stb_i ever reach this instance. + * + * Register map (within the RX half of the shared UART window): + * 0x8010 RX_DATA read: pops the oldest queued byte (zero-extended) + * and advances the queue, but ONLY when sel_i[0] + * is asserted (matching uart_tx.sv's own sel_i[0] + * write gate) -- a probe read that doesn't include + * byte lane 0 (e.g. a sub-word load at a nonzero + * offset within this register's own 8-byte-aligned + * bus window) leaves the queue untouched instead of + * silently discarding a real received byte. + * Reading an empty queue returns 0 without + * underflowing. write: ignored. + * 0x8018 RX_STATUS read-only. bit 0 = RX_DATA_READY (1 when the + * queue is non-empty). write: ignored. + */ +module uart_rx ( + input logic clk, + input logic rst, + + /* + * Only addr_i[3] (register select) and sel_i[0] (gates the + * destructive RX_DATA pop below, mirroring uart_tx.sv's own + * sel_i[0] write gate) are used -- addr_i[4] has already been + * decided by soc.sv before cyc_i/stb_i ever reach this instance + * (see header). dat_i is entirely unused: this peripheral never + * accepts a real write (RX_DATA/RX_STATUS are both read-only from + * firmware's perspective). + */ + /* verilator lint_off UNUSEDSIGNAL */ + input logic [31:0] addr_i, + input logic [63:0] dat_i, + input logic [7:0] sel_i, + /* verilator lint_on UNUSEDSIGNAL */ + output logic [63:0] dat_o, + + output logic ack_o, + output logic err_o, + input logic cyc_i, + input logic stb_i, + input logic we_i +); + localparam RX_DATA_SEL = 1'b0; + localparam RX_STATUS_SEL = 1'b1; + + /* + * Simulation-only inbound-byte queue, populated via the push_byte + * backdoor task below and drained by real RX_DATA bus reads -- the + * mirror image of uart_tx.sv's own tx_history: a fixed array, not a + * queue/string, to stay on constructs already proven to compile + * cleanly in this codebase's iverilog flow. Unlike tx_history + * (append-only, never popped), this genuinely needs FIFO pop + * semantics, so it carries real head/tail pointers rather than a + * single running count. + */ + localparam RX_QUEUE_DEPTH = 256; + /* verilator lint_off UNUSEDSIGNAL */ + logic [7:0] rx_queue [0:(RX_QUEUE_DEPTH - 1)]; + /* verilator lint_on UNUSEDSIGNAL */ + logic [7:0] rx_head; // next index to pop (bus read) + logic [7:0] rx_tail; // next index to push (push_byte) + /* + * One bit wider than strictly needed to COUNT (8 bits covers 0-255) + * so this can also represent the value 256 itself -- "the queue is + * full" -- without wrapping back to 0. Same convention as + * uart_tx.sv's own tx_history_count. + */ + logic [$clog2(RX_QUEUE_DEPTH):0] rx_count; + + /* + * push_byte: simulation-only backdoor a testbench calls to enqueue a + * byte as if it had just arrived over the wire. Uses blocking + * assignment deliberately -- this task runs in the caller's own + * process (a testbench initial block), not this module's clocked + * process, so there is no clock edge for a nonblocking assignment to + * defer to. Call it from a quiet point in the clock (e.g. right as + * rst drops, before the DUT's first real bus transaction, or off + * @(negedge clk) like wb_driver.sv's own wb_cycle() does) to avoid + * racing the read/pop side below, which runs on posedge clk. + */ + task push_byte(input logic [7:0] b); + if (rx_count < RX_QUEUE_DEPTH) begin + rx_queue[rx_tail] = b; + rx_tail = rx_tail + 8'b1; + rx_count = rx_count + 1'b1; + end + endtask + + always @(posedge clk) begin + if (rst) begin + ack_o <= 1'b0; + err_o <= 1'b0; + dat_o <= 64'b0; + rx_head <= 8'b0; + rx_tail <= 8'b0; + rx_count <= '0; + end else if (cyc_i && stb_i) begin + if (!we_i && (addr_i[3] == RX_DATA_SEL) && sel_i[0] && (rx_count > 0)) begin + rx_head <= rx_head + 8'b1; + rx_count <= rx_count - 1'b1; + end + dat_o <= (addr_i[3] == RX_STATUS_SEL) ? {63'b0, (rx_count > 0)} + : (rx_count > 0) ? {56'b0, rx_queue[rx_head]} + : 64'b0; + ack_o <= 1'b1; + err_o <= 1'b0; + end else begin + ack_o <= 1'b0; + err_o <= 1'b0; + end + end + /* + * err_o is intentionally never asserted -- no error conditions are + * defined for this minimal peripheral, same reasoning as + * uart_tx.sv's own identical comment: soc.sv's own addr_i[4] + * sub-decode already guarantees cyc_i/stb_i only ever reach this + * instance for a genuinely RX-addressed request. + */ + +endmodule diff --git a/design/uart_rx_tb.sv b/design/uart_rx_tb.sv new file mode 100644 index 0000000..5b6c52c --- /dev/null +++ b/design/uart_rx_tb.sv @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: uart_rx + * + * Drives the Wishbone port directly, using push_byte (the backdoor + * hierarchical task uart_rx.sv exposes) to inject bytes as if they had + * arrived over the wire, then checking the real bus-facing read path + * (RX_DATA/RX_STATUS) pops them back out in order -- the mirror image + * of uart_tx_tb.sv's own write-then-check-tx_history shape, just with + * the injection and observation points swapped (backdoor writes here, + * bus reads there; bus writes there, backdoor reads here). + */ +module uart_rx_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + logic [31:0] addr; + logic [63:0] dat_i, dat_o; + logic [7:0] sel; + logic ack, err, cyc, stb, we; + + uart_rx dut ( + .clk(clk), .rst(rst), + .addr_i(addr), .dat_i(dat_i), .dat_o(dat_o), .sel_i(sel), + .ack_o(ack), .err_o(err), .cyc_i(cyc), .stb_i(stb), .we_i(we) + ); + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + `include "wb_driver.sv" + + initial begin + cyc = 0; stb = 0; we = 0; addr = 0; dat_i = 0; sel = 8'h00; + @(posedge clk); #1; + rst = 0; + + // RX_STATUS reads not-ready before anything has been pushed. + wb_cycle(32'h8018, 64'h0, 8'h00, 1'b0); + check("RX_STATUS reads not-ready (0) when empty", dat_o, 64'h0); + + // RX_DATA reads 0 (not stale, not X) when empty. + wb_cycle(32'h8010, 64'h0, 8'h01, 1'b0); + check("RX_DATA reads 0 when empty", dat_o, 64'h0); + + // Push "ABC" via the backdoor -- the mirror image of uart_tx_tb.sv's + // wb_cycle writes, just injected directly rather than over the bus. + dut.push_byte(8'h41); // 'A' + dut.push_byte(8'h42); // 'B' + dut.push_byte(8'h43); // 'C' + + wb_cycle(32'h8018, 64'h0, 8'h00, 1'b0); + check("RX_STATUS reads ready (1) after push", dat_o, 64'h1); + + // Pop all three back out over the real bus, in order (FIFO). + wb_cycle(32'h8010, 64'h0, 8'h01, 1'b0); + check("RX_DATA pop 1 == 'A'", dat_o, 64'h41); + wb_cycle(32'h8010, 64'h0, 8'h01, 1'b0); + check("RX_DATA pop 2 == 'B'", dat_o, 64'h42); + wb_cycle(32'h8010, 64'h0, 8'h01, 1'b0); + check("RX_DATA pop 3 == 'C'", dat_o, 64'h43); + + // Drained -- back to not-ready, and a further read returns 0 + // without underflowing. + wb_cycle(32'h8018, 64'h0, 8'h00, 1'b0); + check("RX_STATUS reads not-ready (0) after drain", dat_o, 64'h0); + wb_cycle(32'h8010, 64'h0, 8'h01, 1'b0); + check("RX_DATA reads 0 after drain (no underflow)", dat_o, 64'h0); + + // A bus WRITE to RX_DATA must be a pure no-op -- doesn't pop, doesn't + // corrupt the queue for the next real read. + dut.push_byte(8'h99); + wb_cycle(32'h8010, 64'hFF, 8'h01, 1'b1); // write attempt, should be ignored + check("write to RX_DATA doesn't pop the queue", {55'b0, dut.rx_count}, 64'd1); + wb_cycle(32'h8010, 64'h0, 8'h01, 1'b0); + check("queue survives the write attempt intact", dat_o, 64'h99); + + // A bus WRITE to RX_STATUS must likewise be a pure no-op -- + // mirrors the RX_DATA write-no-op case above (RX_STATUS is + // read-only too). + dut.push_byte(8'h77); + wb_cycle(32'h8018, 64'hFF, 8'h01, 1'b1); // write attempt, should be ignored + check("write to RX_STATUS doesn't corrupt the queue", {55'b0, dut.rx_count}, 64'd1); + wb_cycle(32'h8010, 64'h0, 8'h01, 1'b0); + check("queue survives the RX_STATUS write attempt intact", dat_o, 64'h77); + + // A read whose sel_i doesn't include byte lane 0 must not pop -- + // this is the sel_i[0] gate design/uart_rx.sv's own header + // documents: an ungated pop here would silently discard a real + // received byte on e.g. a sub-word load at a nonzero offset + // within this register's own bus window. + dut.push_byte(8'hAA); + wb_cycle(32'h8010, 64'h0, 8'h02, 1'b0); // read, but sel[0]=0 (lane 1 only) + check("read without sel[0] doesn't pop the queue", {55'b0, dut.rx_count}, 64'd1); + wb_cycle(32'h8010, 64'h0, 8'h01, 1'b0); // real read, sel[0]=1 + check("byte survives the lane-mismatched probe intact", dat_o, 64'hAA); + + /* + * FIFO overflow: push RX_QUEUE_DEPTH+1 (257) bytes via the + * backdoor. This also exercises the rx_head/rx_tail wraparound + * at the 256-entry boundary as a side effect of legitimately + * filling the queue -- byte value i's low 8 bits double as its + * own expected FIFO-order payload, so the drain loop below can + * check every single entry without a separate expected-value + * table. + */ + for (int i = 0; i < 257; i++) begin + dut.push_byte(i[7:0]); + end + check("queue caps at 256 entries, 257th byte silently dropped", + {55'b0, dut.rx_count}, 64'd256); + + // Drain all 256 back out over the real bus and confirm FIFO + // order. quiet_on_pass suppresses per-byte PASS spam; any real + // mismatch still prints (check_lib.sv always displays on FAIL + // regardless of quiet_on_pass). + quiet_on_pass = 1'b1; + for (int i = 0; i < 256; i++) begin + wb_cycle(32'h8010, 64'h0, 8'h01, 1'b0); + check($sformatf("overflow drain byte %0d in FIFO order", i), dat_o, {56'b0, i[7:0]}); + end + quiet_on_pass = 1'b0; + check("queue fully drained after overflow test", {55'b0, dut.rx_count}, 64'd0); + + $display(""); + $display("uart_rx_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("uart_rx_tb: FAILURES PRESENT"); + $finish; + end + +endmodule diff --git a/design/wb_addr_decoder.sv b/design/wb_addr_decoder.sv index ceed04a..450e966 100644 --- a/design/wb_addr_decoder.sv +++ b/design/wb_addr_decoder.sv @@ -6,27 +6,74 @@ /* * Module: wb_addr_decoder * - * Routes the CPU's single Wishbone master port to one of two slaves -- - * design/wb4_sram.sv (RAM) or design/uart_tx.sv (UART) -- based on - * address. One master, two fixed slaves: hand-written rather than a + * Routes the CPU's single Wishbone master port to one of four slaves -- + * design/wb4_sram.sv (RAM), design/uart_tx.sv (UART), design/clint.sv + * (CLINT), or verification/taxi/rtl/dram_model.sv (DRAM) -- based on + * address. One master, four fixed slaves: hand-written rather than a * general N-slave interconnect, since that generality isn't needed here. * - * Address map: RAM occupies 0x0000_0000-0x0000_7FFF (wb4_sram.sv's - * default num_words=4096 x 8 bytes/word = 32KB), UART is anything with - * bit 15 set (0x8000 upward). Since RAM's size is a power of two + * Address map, decoded off a real 2-bit test ({addr_i[16], addr_i[15]}): + * 00 -> RAM 0x0000_0000-0x0000_7FFF (wb4_sram.sv's default + * num_words=4096 x 8 bytes/word = 32KB) + * 01 -> UART 0x0000_8000-0x0000_FFFF + * 10 -> CLINT 0x0001_0000-0x0001_7FFF (32KB -- narrowed from an + * earlier 64KB window that claimed both addr_i[15] values; + * see "Known, accepted gap" below for why 32KB still isn't + * tight against CLINT's own real register footprint) + * 11 -> DRAM 0x0001_8000-0x0001_FFFF (32KB, the window CLINT's own + * narrowing freed up -- see "DRAM address translation" + * below for why dram_addr_o needs special handling) + * + * The RAM/UART boundary (bit 15) is DERIVED from wb4_sram's num_words + * parameter, not independent of it -- since RAM's size is a power of two * starting at address 0, "address >= 0x8000" and "bit 15 set" are the - * same condition -- this is a single bit test, not a shortcut that skips - * a real range compare. NB: this bit position is DERIVED from wb4_sram's - * num_words parameter, not independent of it -- if that default ever - * changes, this decode bit needs revisiting too. + * same condition, a single bit test rather than a shortcut that skips a + * real range compare. NB: if wb4_sram's default num_words ever changes, + * this decode bit needs revisiting too. The CLINT/DRAM boundary (bit 16), + * by contrast, is a fresh, independently-chosen window -- nothing derives + * it from any slave's own size parameter; it was simply picked to sit + * right above the existing RAM+UART 64KB region with room to spare. + * + * The "remember which slave" problem: all four slaves are registered + * (1-wait-state-or-more) Wishbone slaves, so a response arrives one or + * more cycles after the request that triggered it -- by the time + * ack_i/dat_i actually need routing back to the CPU, addr_i may already + * reflect a *different*, newer request (or none). So which slave an + * outstanding transaction belongs to is latched at the moment the + * request is issued (target_q, a 2-bit enum -- now fully saturated with + * four real targets, no spare encoding left), not re-derived from addr_i + * when the response shows up. + * + * Known, accepted gap: CLINT's own window is only 3 words wide + * (mtime/mtimecmp at CLINT_BASE+0x0/+0x8, see clint.sv) but its decoded + * region is 32KB (bits 17-31 of addr_i are never tested at all, just + * like bits above RAM/UART/DRAM's own windows aren't). Every address in + * 0x0001_0000-0x0001_7FFF that isn't exactly +0x0 or +0x8 still routes + * to clint.sv and gets a real ack (clint.sv's own addr_i[3] mux treats + * any such address as an alias of one of its two real registers -- see + * that module's header). No corruption risk (clint.sv has no side + * effects beyond those two registers either way), just address aliasing + * -- the same class of gap wb4_sram.sv's own bounds check is the only + * thing preventing for RAM, left undocumented there too. Not worth a + * fix without a concrete need (e.g. real device-tree/OpenSBI address + * decoding) driving one. * - * The "remember which slave" problem: both slaves are registered - * (1-wait-state) Wishbone slaves, so a response arrives one cycle after - * the request that triggered it -- by the time ack_i/dat_i actually need - * routing back to the CPU, addr_i may already reflect a *different*, - * newer request (or none). So which slave an outstanding transaction - * belongs to is latched at the moment the request is issued - * (sel_uart_q), not re-derived from addr_i when the response shows up. + * DRAM address translation -- the one deliberate exception among the + * four *_addr_o broadcast assigns below: ram_addr_o/uart_addr_o/ + * clint_addr_o all pass addr_i straight through UNTRANSLATED, and that's + * only ever safe by coincidence -- RAM's window happens to sit at system + * address 0 (so "raw" and "window-local" addresses are identical), and + * uart_tx.sv/clint.sv do NO bounds check of their own at all (they rely + * entirely on THIS module's cyc/stb gating and never look at their own + * addr_i's upper bits). dram_model.sv is neither of those: it does a + * real, self-contained bounds check against a ZERO-BASED window + * (`addr_valid = (addr_i[31:ADDR_W] == '0)`, see that file). Handing it + * the raw system address (nonzero bits 15/16 for every legitimately- + * routed DRAM address) would fail that check on every single access, + * permanently and silently breaking DRAM with err_o instead of ack_o -- + * so dram_addr_o is rebased to a window-local address instead + * (`{17'b0, addr_i[14:0]}`), the one broadcast assign that isn't a bare + * passthrough. */ module wb_addr_decoder ( input logic clk, @@ -63,43 +110,102 @@ module wb_addr_decoder ( output logic uart_cyc_o, output logic uart_stb_o, input logic uart_ack_i, - input logic uart_err_i + input logic uart_err_i, + + // CLINT-facing port. + output logic [31:0] clint_addr_o, + output logic [63:0] clint_dat_o, + input logic [63:0] clint_dat_i, + output logic [7:0] clint_sel_o, + output logic clint_we_o, + output logic clint_cyc_o, + output logic clint_stb_o, + input logic clint_ack_i, + input logic clint_err_i, + + // DRAM-facing port. + output logic [31:0] dram_addr_o, + output logic [63:0] dram_dat_o, + input logic [63:0] dram_dat_i, + output logic [7:0] dram_sel_o, + output logic dram_we_o, + output logic dram_cyc_o, + output logic dram_stb_o, + input logic dram_ack_i, + input logic dram_err_i ); - wire sel_uart = addr_i[15]; + wire sel_ram = !addr_i[16] && !addr_i[15]; + wire sel_uart = !addr_i[16] && addr_i[15]; + wire sel_clint = addr_i[16] && !addr_i[15]; + wire sel_dram = addr_i[16] && addr_i[15]; /* * Gate cyc/stb per slave; broadcast everything else (addr/dat/sel/we) - * to both -- harmless, since a slave with cyc=0 ignores the rest of - * the bus regardless of what's sitting on it. + * to all four -- harmless, since a slave with cyc=0 ignores the rest + * of the bus regardless of what's sitting on it. */ - assign ram_cyc_o = cyc_i && !sel_uart; - assign ram_stb_o = stb_i && !sel_uart; - assign uart_cyc_o = cyc_i && sel_uart; - assign uart_stb_o = stb_i && sel_uart; + assign ram_cyc_o = cyc_i && sel_ram; + assign ram_stb_o = stb_i && sel_ram; + assign uart_cyc_o = cyc_i && sel_uart; + assign uart_stb_o = stb_i && sel_uart; + assign clint_cyc_o = cyc_i && sel_clint; + assign clint_stb_o = stb_i && sel_clint; + assign dram_cyc_o = cyc_i && sel_dram; + assign dram_stb_o = stb_i && sel_dram; - assign ram_addr_o = addr_i; - assign uart_addr_o = addr_i; - assign ram_dat_o = dat_i; - assign uart_dat_o = dat_i; - assign ram_sel_o = sel_i; - assign uart_sel_o = sel_i; - assign ram_we_o = we_i; - assign uart_we_o = we_i; + assign ram_addr_o = addr_i; + assign uart_addr_o = addr_i; + assign clint_addr_o = addr_i; + assign dram_addr_o = {17'b0, addr_i[14:0]}; // rebased -- see "DRAM address + // translation" in the header. + assign ram_dat_o = dat_i; + assign uart_dat_o = dat_i; + assign clint_dat_o = dat_i; + assign dram_dat_o = dat_i; + assign ram_sel_o = sel_i; + assign uart_sel_o = sel_i; + assign clint_sel_o = sel_i; + assign dram_sel_o = sel_i; + assign ram_we_o = we_i; + assign uart_we_o = we_i; + assign clint_we_o = we_i; + assign dram_we_o = we_i; /* * Latched at the cycle a request is actually issued (cyc_i && stb_i), * held until the next request overwrites it -- see the module header * for why this can't just re-check addr_i when the response arrives. + * A 2-bit enum, now fully saturated with four real targets (no spare + * encoding left, unlike the old 3-target version). */ - logic sel_uart_q; + typedef enum logic [1:0] { TARGET_RAM, TARGET_UART, TARGET_CLINT, TARGET_DRAM } target_t; + + // RAM stays the terminal `else` here (equivalent to sel_ram by + // construction, since the 4-way decode above is exhaustive) rather + // than an explicit `if (sel_ram)` arm -- matches this file's existing + // defensive-catch-all style (mirrored below in the read-mux `default:`). + target_t target; + always_comb begin + if (sel_dram) target = TARGET_DRAM; + else if (sel_clint) target = TARGET_CLINT; + else if (sel_uart) target = TARGET_UART; + else target = TARGET_RAM; + end + + target_t target_q; always_ff @(posedge clk) begin if (rst) - sel_uart_q <= 1'b0; + target_q <= TARGET_RAM; else if (cyc_i && stb_i) - sel_uart_q <= sel_uart; + target_q <= target; end - assign ack_o = sel_uart_q ? uart_ack_i : ram_ack_i; - assign err_o = sel_uart_q ? uart_err_i : ram_err_i; - assign dat_o = sel_uart_q ? uart_dat_i : ram_dat_i; + always_comb begin + case (target_q) + TARGET_UART: begin ack_o = uart_ack_i; err_o = uart_err_i; dat_o = uart_dat_i; end + TARGET_CLINT: begin ack_o = clint_ack_i; err_o = clint_err_i; dat_o = clint_dat_i; end + TARGET_DRAM: begin ack_o = dram_ack_i; err_o = dram_err_i; dat_o = dram_dat_i; end + default: begin ack_o = ram_ack_i; err_o = ram_err_i; dat_o = ram_dat_i; end + endcase + end endmodule diff --git a/design/wb_addr_decoder_clint_tb.sv b/design/wb_addr_decoder_clint_tb.sv new file mode 100644 index 0000000..d4e161e --- /dev/null +++ b/design/wb_addr_decoder_clint_tb.sv @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: wb_addr_decoder + real clint0 (bus-level integration) + * + * design/wb_addr_decoder_tb.sv proves the decoder's OWN routing/latching + * logic in isolation, using fake slaves with fixed canned responses. This + * testbench instead proves the decoder wired to REAL slaves -- especially + * a real design/clint.sv -- works correctly end-to-end through the actual + * port wiring soc.sv now uses, via testbench/decoder_clint_harness.sv + * (real wb_addr_decoder + real wb4_sram + real uart_tx + real clint, no + * core.sv). This is bus-level, not firmware/instruction-level: raw + * Wishbone cycles are driven directly at the decoder's CPU-facing port + * with testbench/wb_driver.sv's wb_cycle() task, the same way + * design/wb4_sram_tb.sv and design/wb_addr_decoder_tb.sv already do -- + * no real RISC-V instructions are assembled or run through core.sv here. + * + * mtime is a real, free-running counter in this test (not a fixed fake + * response) -- so "sane" is checked via two reads separated by real + * elapsed cycles and confirming the second is strictly greater than the + * first, rather than via an exact-value check() the way the fake-slave + * testbench can afford to. + * + * Deliberately does NOT test interrupt-taking (mtip_o has no consumer + * anywhere in this milestone's fabric -- see clint0's own instantiation + * in decoder_clint_harness.sv) -- that's Milestone 6's concern. This test + * isolates address-decode-and-wiring correctness only. + */ +module wb_addr_decoder_clint_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + logic [31:0] addr; + logic [63:0] dat_i, dat_o; + logic [7:0] sel; + logic ack, err, cyc, stb, we; + + decoder_clint_harness dut ( + .clk(clk), .rst(rst), + .addr_i(addr), .dat_i(dat_i), .dat_o(dat_o), .sel_i(sel), + .we_i(we), .cyc_i(cyc), .stb_i(stb), .ack_o(ack), .err_o(err) + ); + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + `include "wb_driver.sv" + + localparam logic [31:0] CLINT_MTIME = 32'h0001_0000; + localparam logic [31:0] CLINT_MTIMECMP = 32'h0001_0008; + + logic [63:0] mtime_first, mtime_second; + + initial begin + cyc = 0; stb = 0; we = 0; addr = 0; dat_i = 0; sel = 8'hFF; + @(posedge clk); #1; + rst = 0; + + /* + * RAM round trip through the real fabric, BEFORE any CLINT + * traffic -- baseline proof that routing to RAM still works with + * CLINT now sharing the same decoder. + */ + wb_cycle(32'h0000_0100, 64'hDEADBEEF_CAFEF00D, 8'hFF, 1'b1); + wb_cycle(32'h0000_0100, 64'h0, 8'hFF, 1'b0); + check("RAM round trip (pre-CLINT traffic)", dat_o, 64'hDEADBEEF_CAFEF00D); + + /* + * UART round trip through the real fabric -- TX_DATA write then + * TX_STATUS poll, same pattern design/uart_tx_tb.sv itself uses. + */ + wb_cycle(32'h0000_8000, 64'h48, 8'h01, 1'b1); // 'H' + check("UART: one character captured", {55'b0, dut.uart0.tx_history_count}, 64'd1); + check("UART: history[0] == 'H'", {56'b0, dut.uart0.tx_history[0]}, 64'h48); + wb_cycle(32'h0000_8008, 64'h0, 8'h00, 1'b0); + check("UART: TX_STATUS reads ready", dat_o, 64'h1); + + /* + * CLINT mtime: a real, sane free-running counter, not a fixed + * value -- two reads separated by real elapsed cycles must show + * a strict increase. + */ + wb_cycle(CLINT_MTIME, 64'h0, 8'hFF, 1'b0); + mtime_first = dat_o; + + repeat (20) @(posedge clk); + + wb_cycle(CLINT_MTIME, 64'h0, 8'hFF, 1'b0); + mtime_second = dat_o; + check("CLINT mtime: second read strictly greater than first", + {63'b0, (mtime_second > mtime_first)}, 64'd1); + + /* + * CLINT mtimecmp: SD-equivalent write of a real 64-bit deadline, + * then a read-back confirming it stuck. + */ + wb_cycle(CLINT_MTIMECMP, 64'h0000_0000_0012_3456, 8'hFF, 1'b1); + wb_cycle(CLINT_MTIMECMP, 64'h0, 8'hFF, 1'b0); + check("CLINT mtimecmp: write then read-back sticks", dat_o, 64'h0000_0000_0012_3456); + + /* + * RAM/UART routing unaffected by CLINT now sharing the decoder -- + * a plain RAM read/write round trip AFTER CLINT traffic, at a + * different address than the earlier RAM check, still works. + */ + wb_cycle(32'h0000_0200, 64'h1122_3344_5566_7788, 8'hFF, 1'b1); + wb_cycle(32'h0000_0200, 64'h0, 8'hFF, 1'b0); + check("RAM round trip (post-CLINT traffic, unaffected)", dat_o, 64'h1122_3344_5566_7788); + + wb_cycle(32'h0000_8000, 64'h69, 8'h01, 1'b1); // 'i' + check("UART: second character captured (post-CLINT traffic, unaffected)", + {55'b0, dut.uart0.tx_history_count}, 64'd2); + + $display(""); + $display("wb_addr_decoder_clint_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("wb_addr_decoder_clint_tb: FAILURES PRESENT"); + $finish; + end + +endmodule diff --git a/design/wb_addr_decoder_tb.sv b/design/wb_addr_decoder_tb.sv index 765d048..4ce60a1 100644 --- a/design/wb_addr_decoder_tb.sv +++ b/design/wb_addr_decoder_tb.sv @@ -6,12 +6,12 @@ /* * Testbench: wb_addr_decoder * - * The decoder is tested in isolation -- no real wb4_sram/uart_tx - * instances. Instead this testbench models two trivial fake slaves + * The decoder is tested in isolation -- no real wb4_sram/uart_tx/clint + * instances. Instead this testbench models three trivial fake slaves * directly (each a 1-wait-state ack with a fixed, distinct response * value), so a wrong routing/latching decision is unambiguous: RAM's - * fake value is 0xAAAA..., UART's is 0xBBBB..., and nothing else could - * produce either. + * fake value is 0xAAAA..., UART's is 0xBBBB..., CLINT's is 0xCCCC..., + * and nothing else could produce any of the three. */ module wb_addr_decoder_tb; @@ -24,12 +24,15 @@ module wb_addr_decoder_tb; logic [7:0] sel; logic ack, err, cyc, stb, we; - logic [31:0] ram_addr_o, uart_addr_o; - logic [63:0] ram_dat_o, uart_dat_o; - logic [7:0] ram_sel_o, uart_sel_o; + logic [31:0] ram_addr_o, uart_addr_o, clint_addr_o, dram_addr_o; + logic [63:0] ram_dat_o, uart_dat_o, clint_dat_o, dram_dat_o; + logic [7:0] ram_sel_o, uart_sel_o, clint_sel_o, dram_sel_o; logic ram_we_o, ram_cyc_o, ram_stb_o, uart_we_o, uart_cyc_o, uart_stb_o; - logic [63:0] ram_dat_i, uart_dat_i; - logic ram_ack_i, ram_err_i, uart_ack_i, uart_err_i; + logic clint_we_o, clint_cyc_o, clint_stb_o; + logic dram_we_o, dram_cyc_o, dram_stb_o; + logic [63:0] ram_dat_i, uart_dat_i, clint_dat_i, dram_dat_i; + logic ram_ack_i, ram_err_i, uart_ack_i, uart_err_i, clint_ack_i, clint_err_i; + logic dram_ack_i, dram_err_i; wb_addr_decoder dut ( .clk(clk), .rst(rst), @@ -40,7 +43,13 @@ module wb_addr_decoder_tb; .ram_stb_o(ram_stb_o), .ram_ack_i(ram_ack_i), .ram_err_i(ram_err_i), .uart_addr_o(uart_addr_o), .uart_dat_o(uart_dat_o), .uart_dat_i(uart_dat_i), .uart_sel_o(uart_sel_o), .uart_we_o(uart_we_o), .uart_cyc_o(uart_cyc_o), - .uart_stb_o(uart_stb_o), .uart_ack_i(uart_ack_i), .uart_err_i(uart_err_i) + .uart_stb_o(uart_stb_o), .uart_ack_i(uart_ack_i), .uart_err_i(uart_err_i), + .clint_addr_o(clint_addr_o), .clint_dat_o(clint_dat_o), .clint_dat_i(clint_dat_i), + .clint_sel_o(clint_sel_o), .clint_we_o(clint_we_o), .clint_cyc_o(clint_cyc_o), + .clint_stb_o(clint_stb_o), .clint_ack_i(clint_ack_i), .clint_err_i(clint_err_i), + .dram_addr_o(dram_addr_o), .dram_dat_o(dram_dat_o), .dram_dat_i(dram_dat_i), + .dram_sel_o(dram_sel_o), .dram_we_o(dram_we_o), .dram_cyc_o(dram_cyc_o), + .dram_stb_o(dram_stb_o), .dram_ack_i(dram_ack_i), .dram_err_i(dram_err_i) ); // Fake RAM: 1-wait-state ack, fixed distinguishable response. @@ -65,6 +74,28 @@ module wb_addr_decoder_tb; end end + // Fake CLINT: same shape, third distinguishable response. + always @(posedge clk) begin + if (rst) begin + clint_ack_i <= 0; clint_err_i <= 0; clint_dat_i <= 0; + end else if (clint_cyc_o && clint_stb_o) begin + clint_ack_i <= 1'b1; clint_dat_i <= 64'hCCCCCCCC_CCCCCCCC; + end else begin + clint_ack_i <= 1'b0; + end + end + + // Fake DRAM: same shape, fourth distinguishable response. + always @(posedge clk) begin + if (rst) begin + dram_ack_i <= 0; dram_err_i <= 0; dram_dat_i <= 0; + end else if (dram_cyc_o && dram_stb_o) begin + dram_ack_i <= 1'b1; dram_dat_i <= 64'hDDDDDDDD_DDDDDDDD; + end else begin + dram_ack_i <= 1'b0; + end + end + int pass_count = 0; int fail_count = 0; logic quiet_on_pass = 1'b0; @@ -82,6 +113,8 @@ module wb_addr_decoder_tb; #1; check("RAM address: ram_cyc_o asserted", {63'b0, ram_cyc_o}, 64'd1); check("RAM address: uart_cyc_o stays low", {63'b0, uart_cyc_o}, 64'd0); + check("RAM address: clint_cyc_o stays low", {63'b0, clint_cyc_o}, 64'd0); + check("RAM address: dram_cyc_o stays low", {63'b0, dram_cyc_o}, 64'd0); cyc = 0; stb = 0; @(negedge clk); @@ -91,6 +124,80 @@ module wb_addr_decoder_tb; #1; check("UART address: uart_cyc_o asserted", {63'b0, uart_cyc_o}, 64'd1); check("UART address: ram_cyc_o stays low", {63'b0, ram_cyc_o}, 64'd0); + check("UART address: clint_cyc_o stays low", {63'b0, clint_cyc_o}, 64'd0); + check("UART address: dram_cyc_o stays low", {63'b0, dram_cyc_o}, 64'd0); + cyc = 0; stb = 0; + @(negedge clk); + + // CLINT-range address: only clint_cyc_o should assert. + @(negedge clk); + addr = 32'h0001_0000; cyc = 1; stb = 1; + #1; + check("CLINT address: clint_cyc_o asserted", {63'b0, clint_cyc_o}, 64'd1); + check("CLINT address: ram_cyc_o stays low", {63'b0, ram_cyc_o}, 64'd0); + check("CLINT address: uart_cyc_o stays low", {63'b0, uart_cyc_o}, 64'd0); + check("CLINT address: dram_cyc_o stays low", {63'b0, dram_cyc_o}, 64'd0); + cyc = 0; stb = 0; + @(negedge clk); + + // DRAM-range address: only dram_cyc_o should assert. This address + // (addr_i[16]==1, addr_i[15]==1) used to alias into CLINT's old + // 64KB window (bit 16 alone decided CLINT, regardless of bit 15); + // under the current 4-way map, bit 15 is a real, tested select + // bit, and 0x1_8000 is DRAM's own base. + @(negedge clk); + addr = 32'h0001_8000; cyc = 1; stb = 1; + #1; + check("DRAM address (formerly CLINT's over-wide window): dram_cyc_o asserted", {63'b0, dram_cyc_o}, 64'd1); + check("DRAM address: uart_cyc_o stays low", {63'b0, uart_cyc_o}, 64'd0); + check("DRAM address: ram_cyc_o stays low", {63'b0, ram_cyc_o}, 64'd0); + check("DRAM address: clint_cyc_o stays low", {63'b0, clint_cyc_o}, 64'd0); + cyc = 0; stb = 0; + @(negedge clk); + + // Boundary edge cases: the top address of each window. The decode + // is a raw bit test, not a magnitude comparator, so these can't + // actually catch an off-by-one the way they would against a real + // range compare -- but they're cheap, and a future rewrite to a + // comparator-based decode would be exactly the kind of change + // these should catch a regression in. + @(negedge clk); + addr = 32'h0000_7FFF; cyc = 1; stb = 1; // top of RAM's window + #1; + check("RAM top-of-window address: ram_cyc_o asserted", {63'b0, ram_cyc_o}, 64'd1); + check("RAM top-of-window address: uart_cyc_o stays low", {63'b0, uart_cyc_o}, 64'd0); + check("RAM top-of-window address: clint_cyc_o stays low", {63'b0, clint_cyc_o}, 64'd0); + check("RAM top-of-window address: dram_cyc_o stays low", {63'b0, dram_cyc_o}, 64'd0); + cyc = 0; stb = 0; + @(negedge clk); + + @(negedge clk); + addr = 32'h0000_FFFF; cyc = 1; stb = 1; // top of UART's window + #1; + check("UART top-of-window address: uart_cyc_o asserted", {63'b0, uart_cyc_o}, 64'd1); + check("UART top-of-window address: ram_cyc_o stays low", {63'b0, ram_cyc_o}, 64'd0); + check("UART top-of-window address: clint_cyc_o stays low", {63'b0, clint_cyc_o}, 64'd0); + check("UART top-of-window address: dram_cyc_o stays low", {63'b0, dram_cyc_o}, 64'd0); + cyc = 0; stb = 0; + @(negedge clk); + + @(negedge clk); + addr = 32'h0001_7FFF; cyc = 1; stb = 1; // top of CLINT's narrowed window + #1; + check("CLINT top-of-window address: clint_cyc_o asserted", {63'b0, clint_cyc_o}, 64'd1); + check("CLINT top-of-window address: ram_cyc_o stays low", {63'b0, ram_cyc_o}, 64'd0); + check("CLINT top-of-window address: uart_cyc_o stays low", {63'b0, uart_cyc_o}, 64'd0); + check("CLINT top-of-window address: dram_cyc_o stays low", {63'b0, dram_cyc_o}, 64'd0); + cyc = 0; stb = 0; + @(negedge clk); + + @(negedge clk); + addr = 32'h0001_FFFF; cyc = 1; stb = 1; // top of DRAM's window + #1; + check("DRAM top-of-window address: dram_cyc_o asserted", {63'b0, dram_cyc_o}, 64'd1); + check("DRAM top-of-window address: ram_cyc_o stays low", {63'b0, ram_cyc_o}, 64'd0); + check("DRAM top-of-window address: uart_cyc_o stays low", {63'b0, uart_cyc_o}, 64'd0); + check("DRAM top-of-window address: clint_cyc_o stays low", {63'b0, clint_cyc_o}, 64'd0); cyc = 0; stb = 0; @(negedge clk); @@ -106,6 +213,62 @@ module wb_addr_decoder_tb; wb_cycle(32'h0000_0300, 64'h0, 8'hFF, 1'b0); check("response routed back to RAM (latch updates both directions)", dat_o, 64'hAAAAAAAA_AAAAAAAA); + // 3-way round trip: RAM -> UART -> CLINT -> RAM, proving the + // now-2-bit target_q latch correctly updates across all three + // targets in sequence, not just the original 2. + wb_cycle(32'h0000_0400, 64'h0, 8'hFF, 1'b0); + check("3-way: response routed from RAM", dat_o, 64'hAAAAAAAA_AAAAAAAA); + + wb_cycle(32'h0000_8010, 64'h0, 8'hFF, 1'b0); + check("3-way: response routed from UART", dat_o, 64'hBBBBBBBB_BBBBBBBB); + + wb_cycle(32'h0001_0008, 64'h0, 8'hFF, 1'b0); + check("3-way: response routed from CLINT", dat_o, 64'hCCCCCCCC_CCCCCCCC); + + wb_cycle(32'h0000_0500, 64'h0, 8'hFF, 1'b0); + check("3-way: response routed back to RAM", dat_o, 64'hAAAAAAAA_AAAAAAAA); + + // Close the loop: the sequence above never exercises RAM -> CLINT + // or CLINT -> UART directly (only CLINT -> RAM and UART -> CLINT + // were hit). All 6 ordered target-pair transitions are now covered. + wb_cycle(32'h0001_0010, 64'h0, 8'hFF, 1'b0); + check("closing the loop: response routed RAM -> CLINT", dat_o, 64'hCCCCCCCC_CCCCCCCC); + + wb_cycle(32'h0000_8018, 64'h0, 8'hFF, 1'b0); + check("closing the loop: response routed CLINT -> UART", dat_o, 64'hBBBBBBBB_BBBBBBBB); + + // DRAM is the 4th target -- extend "closing the loop" to cover + // every pair involving it. target_q's own update (line ~161: + // `else if (cyc_i && stb_i) target_q <= target;`) is a flat, + // unconditional overwrite with no branch on the prior value, so + // there's no code path that could behave differently for one + // ordered pair versus another sharing the same destination -- + // only "does this destination get correctly latched" matters, a + // property of the destination alone. The 6 pairs above (among + // RAM/UART/CLINT) already fully prove the latch mechanism for + // those three; DRAM needs exactly 6 new pairs (3 partners x 2 + // directions each), continuing as a single walk from the + // previous section's end state (UART) so every DRAM-involving + // pair is hit exactly once, giving full 12/12 ordered-pair + // coverage across the file. + wb_cycle(32'h0001_8010, 64'h0, 8'hFF, 1'b0); + check("DRAM pairs: response routed UART -> DRAM", dat_o, 64'hDDDDDDDD_DDDDDDDD); + + wb_cycle(32'h0000_0600, 64'h0, 8'hFF, 1'b0); + check("DRAM pairs: response routed DRAM -> RAM", dat_o, 64'hAAAAAAAA_AAAAAAAA); + + wb_cycle(32'h0001_8018, 64'h0, 8'hFF, 1'b0); + check("DRAM pairs: response routed RAM -> DRAM", dat_o, 64'hDDDDDDDD_DDDDDDDD); + + wb_cycle(32'h0001_0018, 64'h0, 8'hFF, 1'b0); + check("DRAM pairs: response routed DRAM -> CLINT", dat_o, 64'hCCCCCCCC_CCCCCCCC); + + wb_cycle(32'h0001_8020, 64'h0, 8'hFF, 1'b0); + check("DRAM pairs: response routed CLINT -> DRAM", dat_o, 64'hDDDDDDDD_DDDDDDDD); + + wb_cycle(32'h0000_8020, 64'h0, 8'hFF, 1'b0); + check("DRAM pairs: response routed DRAM -> UART", dat_o, 64'hBBBBBBBB_BBBBBBBB); + $display(""); $display("wb_addr_decoder_tb: %0d passed, %0d failed", pass_count, fail_count); if (fail_count > 0) $display("wb_addr_decoder_tb: FAILURES PRESENT"); diff --git a/firmware/Makefile b/firmware/Makefile index 7138cfd..0b9ca42 100644 --- a/firmware/Makefile +++ b/firmware/Makefile @@ -48,6 +48,14 @@ A_ASFLAGS = -march=rv64ima -mabi=lp64 -mno-relax C_ASFLAGS = -march=rv64imac -mabi=lp64 -mno-relax CFLAGS_C = -march=rv64imac -mabi=lp64 -mno-relax -ffreestanding -nostdlib -O0 -Wall +# interrupt_test.s uses real CSR mnemonics (csrw), mret, and plain ld/sd +# memory-access mnemonics against the CLINT MMIO window -- CSR_ASFLAGS' +# rv64i_zicsr already covers all of it (mret/etc. need no separate +# "privileged" march string either, per PRIV_ASFLAGS's own comment +# above), kept as its own named variable anyway for the same +# self-documenting reason PRIV_ASFLAGS is. +INTERRUPT_ASFLAGS = -march=rv64i_zicsr -mabi=lp64 -mno-relax + all: riscv64-unknown-elf-as $(ASFLAGS) crt0.s -o crt0.o riscv64-unknown-elf-gcc $(CFLAGS) -c hello.c -o hello.o @@ -162,3 +170,17 @@ hello_c: riscv64-unknown-elf-ld -T link.ld -o hello_c.elf crt0_c.o hello_c.o riscv64-unknown-elf-objdump -d hello_c.elf riscv64-unknown-elf-objcopy -O verilog -j .text -j .rodata -j .data --verilog-data-width=8 hello_c.elf hello_c.hex + +# Milestone 6 (interrupt-taking): a real-toolchain-encoded end-to-end +# machine-timer-interrupt test (firmware/interrupt_test.s), linked +# against the SAME crt0.o/link.ld as every other target above -- +# rebuilt here (plain -march=rv64i, unchanged) rather than assumed to +# already exist, so this target works standalone. Produces a DISTINCT +# interrupt_test.hex -- every other target's hex output is untouched +# either way. +interrupt_test: + riscv64-unknown-elf-as $(ASFLAGS) crt0.s -o crt0.o + riscv64-unknown-elf-as $(INTERRUPT_ASFLAGS) interrupt_test.s -o interrupt_test.o + riscv64-unknown-elf-ld -T link.ld -o interrupt_test.elf crt0.o interrupt_test.o + riscv64-unknown-elf-objdump -d interrupt_test.elf + riscv64-unknown-elf-objcopy -O verilog -j .text -j .rodata -j .data --verilog-data-width=8 interrupt_test.elf interrupt_test.hex diff --git a/firmware/interrupt_test.s b/firmware/interrupt_test.s new file mode 100644 index 0000000..3a15a0d --- /dev/null +++ b/firmware/interrupt_test.s @@ -0,0 +1,81 @@ + .section .text + .globl main + +# Real-toolchain-assembled end-to-end machine-timer-interrupt test +# (Milestone 6) -- the interrupt/CLINT sibling of priv_test.s/csr_test.s/ +# m_test.s, run through the REAL soc.sv topology (core+decoder+cache+ +# sram+uart+clint), not a hand-packed core-only harness -- see +# testbench/soc_interrupt_tb.sv for the loading convention (crt0.hex then +# a #1-delayed readmemh override, exactly priv_toolchain_tb.sv's own +# idiom). +# +# Control flow: +# 1. Point mtvec at m_trap_handler. +# 2. mie.MTIE = 1 (bit 7). +# 3. Program the REAL CLINT's mtimecmp (CLINT_BASE+0x8 = 0x0001_0008) +# to a real, small deadline (800 -- comfortably past this program's +# own setup instructions, given every fetch/load in this milestone's +# real soc.sv topology is a genuine Wishbone transaction, but small +# enough to keep the whole test fast). +# 4. mstatus.MIE = 1 -- arms the interrupt for real. +# 5. A bounded busy-loop polling the real CLINT's mtime (CLINT_BASE+0x0) +# against the deadline -- NOT `wfi` (a documented, deliberate no-op +# in this core, see core.sv's Scope note; a real WFI-based wait +# would simply never advance). The loop's own bound (10000 +# iterations) is a safety net only -- the real machine-timer +# interrupt is expected to preempt it long before that, at whatever +# instruction boundary mtime first reaches the deadline. +# 6. m_trap_handler: marks s1=1 (proves the real interrupt fired, +# through the real CLINT/decoder/core path, not just a synthetic +# i_mtip poke), disarms the timer (mtimecmp <- all-ones) so MRET's +# own MIE restoration doesn't immediately re-trigger the same, +# still-pending condition, then mret's back to exactly where the +# busy-loop was preempted (no mepc adjustment -- unlike a +# synchronous trap, an async interrupt handler must NOT skip past +# the interrupted instruction). +# 7. Back in the busy-loop, mtip is now deasserted -- the loop runs to +# its own natural exit (mtime already >= deadline) or its bound, +# either way falls through to `ret`, and crt0.s's own trailing +# ebreak halts the core, exactly like every other firmware image in +# this repo. +# +# s1 (marker) and s2 (loop-iteration count, left alive for post-halt +# sanity) are the only results the testbench needs -- same "keep results +# alive in callee-saved registers, confirm from outside" idiom every +# other toolchain-assembled testbench in this project already uses (no +# trap-based pass/fail signal exists for a toolchain-built program). +main: + la t0, m_trap_handler + csrw mtvec, t0 + + li t0, 128 # mie.MTIE (bit 7) + csrw mie, t0 + + li a0, 0x10008 # CLINT_BASE+0x8 (mtimecmp) + li a1, 800 # deadline + sd a1, 0(a0) + + li t0, 8 # mstatus.MIE (bit 3) + csrw mstatus, t0 + + li a0, 0x10000 # CLINT_BASE (mtime) + li s2, 0 +busy_loop: + ld t1, 0(a0) # t1 = mtime (real, uncacheable CLINT read) + bge t1, a1, busy_done # not expected to be how this loop exits -- + # the real interrupt should preempt first + addi s2, s2, 1 + li t2, 10000 + blt s2, t2, busy_loop +busy_done: + + ret + +m_trap_handler: + li s1, 1 # marker: the real timer interrupt fired + li t0, 0x10008 # CLINT_BASE+0x8 (mtimecmp) + li t1, -1 # all-ones -- disarms mtip.MTIP so MRET's + # own MIE restoration can't immediately + # re-trigger the same pending condition + sd t1, 0(t0) + mret diff --git a/testbench/act_runner_tb.sv b/testbench/act_runner_tb.sv index 88a6a58..89c6c98 100644 --- a/testbench/act_runner_tb.sv +++ b/testbench/act_runner_tb.sv @@ -27,14 +27,17 @@ * FAIL macros write a marker word (1=pass, 3=fail) to a `tohost` symbol * -- the classic riscv-tests/HTIF convention, present purely so the Sail * reference model's OWN signature-computation run terminates the same way - * it always does -- and then execute `ebreak`, this core's one real - * simulation-halt mechanism (design/core.sv: permanent `halted` latch). - * This harness waits on `halted` (same convention every other testbench - * uses) and then reads the marker word straight out of SRAM. `tohost`'s - * address moves per-test (its `.text.rvmodel` section's size varies with - * how much test code preceded it), so it's supplied via +TOHOST_ADDR - * rather than assumed -- the driver script extracts it per-ELF via - * `riscv64-unknown-elf-nm`. + * it always does -- and then execute `ebreak`. This harness polls `tohost` + * directly for a real pass/fail marker rather than waiting for an + * ebreak-based halt signal: EBREAK is a real, resumable trap now (see + * design/core.sv), and several ACT4 tests deliberately execute ebreak + * MID-TEST as part of what they're testing (verifying it traps correctly, + * then resuming) well before their own real completion -- an + * ebreak-based "first occurrence" latch would stop watching too early for + * those. `tohost`'s address moves per-test (its `.text.rvmodel` section's + * size varies with how much test code preceded it), so it's supplied via + * +TOHOST_ADDR rather than assumed -- the driver script extracts it + * per-ELF via `riscv64-unknown-elf-nm`. * * Required plusargs (all consumed via $value$plusargs, see initial block): * +HEXFILE= -- $readmemh-format hex image (objcopy -O verilog @@ -56,14 +59,17 @@ * one of these on its own line, before $finish: * ACT_RESULT: PASS * ACT_RESULT: FAIL - * ACT_RESULT: UNKNOWN (tohost=0x...) -- halted without a recognized - * pass(1)/fail(3) marker; a real - * anomaly (bad tohost address, a - * DUT bug that reaches ebreak by - * some other path) worth looking - * at by hand, never a silent pass. - * TIMEOUT: ... -- emitted by halt_wait.sv itself - * if `halted` never asserts. + * TIMEOUT: ... -- tohost never became a real + * pass(1)/fail(3) marker within + * the cycle budget; a real + * anomaly (bad tohost address, a + * genuine hang) worth looking + * at by hand, never a silent + * pass. (No separate UNKNOWN + * result anymore: the polling + * loop below only ever exits + * on a real 1/3 marker or this + * timeout.) * * NUM_WORDS is sized for the largest self-checking ELF actually observed * from `make` against quantiumv-rv64im.yaml (see that generation's log) -- @@ -105,9 +111,6 @@ module act_runner_tb; .ack_o(wb_ack), .err_o(wb_err), .cyc_i(wb_cyc), .stb_i(wb_stb), .we_i(wb_we) ); - wire halted = dut.halted; - `include "halt_wait.sv" - initial begin string hex_path; logic [31:0] tohost_addr; @@ -132,15 +135,46 @@ module act_runner_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(timeout_cycles, "core never halted -- missing ebreak in the generated ELF, or a real hang"); - - tohost_word = sram0.memory[tohost_addr[31:3]]; + /* + * Poll tohost directly rather than waiting for an ebreak-based + * "halted" latch. ACT4/HTIF's real completion convention is + * "write tohost, then ebreak" -- but many ACT4 tests (e.g. U-00's + * own deliberate U_uprivinst_cg_cp_uprivinst_ebreak subcase) + * execute ebreak MID-TEST, as part of what they're testing (it + * genuinely traps now -- see design/core.sv's own EBREAK-is-a- + * real-trap milestone -- and the test's own handler verifies that + * and resumes), long before the test's real completion. A + * first-occurrence ebreak latch stops watching right there, + * before tohost is ever written -- exactly the class of bug this + * milestone's whole testbench sweep found and fixed elsewhere + * (see e.g. core_priv_tb.sv's own comment). Polling tohost + * directly for a real pass(1)/fail(3) marker sidesteps the + * question of how many ebreaks a given test deliberately takes + * entirely, and is closer to how real ACT4/Spike/HTIF harnesses + * detect completion in the first place. + */ + fork + begin + tohost_word = 64'b0; + while (tohost_word !== 64'd1 && tohost_word !== 64'd3) begin + @(posedge clk); #1; + tohost_word = sram0.memory[tohost_addr[31:3]]; + end + end + begin + repeat (timeout_cycles) @(posedge clk); + $display("TIMEOUT: core never wrote a real tohost pass/fail marker"); + $finish; + end + join_any + #1; + + // No `else` (UNKNOWN) branch -- the polling loop above only ever + // exits on tohost_word === 1 or === 3, so this is exhaustive. if (tohost_word == 64'd1) $display("ACT_RESULT: PASS"); - else if (tohost_word == 64'd3) - $display("ACT_RESULT: FAIL"); else - $display("ACT_RESULT: UNKNOWN (tohost=0x%016h)", tohost_word); + $display("ACT_RESULT: FAIL"); $finish; end diff --git a/testbench/core_a_ext_cache_tb.sv b/testbench/core_a_ext_cache_tb.sv index 281b02a..bd0e126 100644 --- a/testbench/core_a_ext_cache_tb.sv +++ b/testbench/core_a_ext_cache_tb.sv @@ -49,7 +49,8 @@ module core_a_ext_cache_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.core0.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" /* @@ -114,12 +115,12 @@ module core_a_ext_cache_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "dut.core0.halted never went high"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "EBREAK trap never fired"); check("AMOADD.W: rd = old value", dut.core0.regfile0.gp_registers[1], 64'd100); check("AMOADD.W: memory updated (write actually reached SRAM)", dut.core0.regfile0.gp_registers[2], 64'd105); - check("core halted (ebreak reached)", {63'b0, dut.core0.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); check("AMO read phase genuinely missed and refilled (cold line, per setup)", {63'b0, (read_phase_beats > 0)}, 64'd1); diff --git a/testbench/core_a_ext_tb.sv b/testbench/core_a_ext_tb.sv index 1fe4ada..48963f9 100644 --- a/testbench/core_a_ext_tb.sv +++ b/testbench/core_a_ext_tb.sv @@ -68,7 +68,8 @@ module core_a_ext_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.core0.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" /* @@ -353,7 +354,7 @@ module core_a_ext_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "dut.core0.halted never went high"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "EBREAK trap never fired"); check("LR.W (sign-extends a 32-bit value with bit31 set)", dut.core0.regfile0.gp_registers[1], 64'hFFFFFFFF80000001); check("LR.D", dut.core0.regfile0.gp_registers[2], -64'sd16); @@ -411,7 +412,7 @@ module core_a_ext_tb; check("AMOMAXU.W: rd = old (3)", dut.core0.regfile0.gp_registers[3], 64'd3); check("AMOMAXU.D: rd = old", dut.core0.regfile0.gp_registers[8], 64'd30); - check("core halted (ebreak reached)", {63'b0, dut.core0.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); check("AMOSWAP.W genuinely visited S_AMO_WRITE as a distinct state (not skipped)", {56'b0, (amo_write_cycle_count > 8'd0) ? 8'd1 : 8'd0}, 64'd1); diff --git a/testbench/core_a_toolchain_tb.sv b/testbench/core_a_toolchain_tb.sv index 18eccc5..c286ea4 100644 --- a/testbench/core_a_toolchain_tb.sv +++ b/testbench/core_a_toolchain_tb.sv @@ -76,7 +76,8 @@ module core_a_toolchain_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.trap_taken && dut.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" initial begin @@ -86,7 +87,7 @@ module core_a_toolchain_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "dut.halted never went high -- is firmware/a_test.hex built?"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "EBREAK trap never fired -- is firmware/a_test.hex built?"); /* * Expected values, mechanically identical to the ones @@ -123,7 +124,7 @@ module core_a_toolchain_tb; check("AMOMAXU.W old (t5)", dut.regfile0.gp_registers[30], 64'd3); check("AMOMAXU.D old (t6)", dut.regfile0.gp_registers[31], 64'd30); - check("core halted (ebreak reached)", {63'b0, dut.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); $display(""); $display("core_a_toolchain_tb: %0d passed, %0d failed", pass_count, fail_count); diff --git a/testbench/core_alu_ops_tb.sv b/testbench/core_alu_ops_tb.sv index 6fce115..976e9fd 100644 --- a/testbench/core_alu_ops_tb.sv +++ b/testbench/core_alu_ops_tb.sv @@ -62,6 +62,9 @@ module core_alu_ops_tb; end endtask + logic halted = 1'b0; + always @(posedge clk) if (dut.trap_taken && dut.is_ebreak) halted <= 1'b1; + initial begin #1; // run after wb4_sram's own time-0 init (zero-fill + $readmemh) -- see core_wb_tb.sv @@ -90,10 +93,10 @@ module core_alu_ops_tb; rst = 0; fork - wait (dut.halted === 1'b1); + wait (halted === 1'b1); begin repeat (150) @(posedge clk); - $display("TIMEOUT: dut.halted never went high"); + $display("TIMEOUT: EBREAK trap never fired"); $finish; end join_any diff --git a/testbench/core_amo_write_fault_harness.sv b/testbench/core_amo_write_fault_harness.sv new file mode 100644 index 0000000..2b46b1f --- /dev/null +++ b/testbench/core_amo_write_fault_harness.sv @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +/* + * Module: core_amo_write_fault_harness + * + * Purpose-built for exactly one scenario: an AMO whose READ phase + * succeeds but whose WRITE phase (to the SAME address) faults. No real + * Wishbone slave in this project can produce that -- wb4_sram.sv's + * addr_valid check is symmetric for read vs. write at a fixed address, + * so "read succeeds, write faults" is structurally impossible to + * construct through it (or any real cache/decoder path sitting on top + * of it) alone. This mock slave sidesteps that by not being a real, + * general-purpose memory at all -- it's split into exactly two behaviors + * gated on wb_ifetch_o (the same side-band signal cache_complex.sv + * already uses for I$/D$ routing): + * - Instruction fetches: served from a real, small backing array, + * same convention as wb4_sram.sv (so a real program can actually run). + * - Any data access (S_MEM/S_AMO_WRITE, ifetch low): reads always + * succeed with a fixed known value; writes always fault. There is + * only ever one AMO under test in the testbenches that use this + * harness, so no address-matching logic is needed for the data path. + * + * Use core_wb4_sram_harness (or core_cache_harness for the cached path) + * for every other testbench -- this one exists solely to make the AMO + * write-phase fault case constructible at all. + */ +module core_amo_write_fault_harness #( + parameter NUM_WORDS = 64 +) ( + input logic clk, + input logic rst +); + + logic [31:0] wb_addr; + logic [63:0] wb_dat_m2s, wb_dat_s2m; + logic [7:0] wb_sel; + logic wb_we, wb_cyc, wb_stb, wb_ack, wb_err, wb_ifetch; + + core core0 ( + .clk(clk), .rst(rst), + .wb_addr_o(wb_addr), .wb_dat_o(wb_dat_m2s), .wb_dat_i(wb_dat_s2m), + .wb_sel_o(wb_sel), .wb_we_o(wb_we), .wb_cyc_o(wb_cyc), .wb_stb_o(wb_stb), + .wb_ack_i(wb_ack), .wb_err_i(wb_err), .wb_ifetch_o(wb_ifetch) + ); + + reg [63:0] memory [0:(NUM_WORDS - 1)]; + localparam ADDR_WIDTH = $clog2(NUM_WORDS); + wire [ADDR_WIDTH-1:0] word_addr = wb_addr[ADDR_WIDTH+2:3]; + wire addr_valid = (wb_addr[31:ADDR_WIDTH+3] == 0); + + /* + * AMO_OLD_VALUE: what a read on the data path always returns -- + * the AMO's "old value" operand, chosen (along with the caller's + * own rs2) so a mem_paddr-instead-of-amo_addr_q bug in core.sv's + * trap_val is trivially distinguishable from the correct address: + * see the testbench using this harness for the exact numbers. + */ + localparam logic [63:0] AMO_OLD_VALUE = 64'd100; + + integer init_i; + initial begin + for (init_i = 0; init_i < NUM_WORDS; init_i = init_i + 1) + memory[init_i] = '0; + end + + always @(posedge clk) begin + if (rst) begin + wb_ack <= 1'b0; + wb_err <= 1'b0; + wb_dat_s2m <= 64'b0; + end else if (wb_cyc && wb_stb) begin + if (wb_ifetch) begin + if (addr_valid) begin + wb_dat_s2m <= memory[word_addr]; + wb_ack <= 1'b1; + wb_err <= 1'b0; + end else begin + wb_ack <= 1'b0; + wb_err <= 1'b1; + end + end else begin + if (wb_we) begin + wb_ack <= 1'b0; + wb_err <= 1'b1; + end else begin + wb_dat_s2m <= AMO_OLD_VALUE; + wb_ack <= 1'b1; + wb_err <= 1'b0; + end + end + end else begin + wb_ack <= 1'b0; + wb_err <= 1'b0; + end + end + +endmodule + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/testbench/core_amo_write_fault_tb.sv b/testbench/core_amo_write_fault_tb.sv new file mode 100644 index 0000000..f6a54aa --- /dev/null +++ b/testbench/core_amo_write_fault_tb.sv @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +`include "riscv_encode.sv" + + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: core, an AMO write-phase fault (the read phase succeeds, + * the write phase to the SAME address faults), proving trap_val uses + * amo_addr_q rather than mem_paddr for this specific case. + * + * Why this needs its own harness (core_amo_write_fault_harness.sv, not + * core_wb4_sram_harness): wb4_sram.sv's address check is symmetric for + * read vs. write, so "read succeeds, write to the same address faults" + * can't be constructed through it. See that harness's own header. + * + * Why this is worth a dedicated, separate test (not folded into + * core_bus_fault_trap_tb.sv's test F, which covers the AMO READ-phase + * fault): mem_paddr(=alu_result) is live/correct during S_MEM, but gets + * REPURPOSED to hold the AMO's computed modify value the instant + * S_AMO_WRITE begins -- design/core.sv's trap_val must use amo_addr_q + * (a dedicated, stable register) instead for a write-phase fault, or + * mtval reports garbage. Test F can't exercise this at all (its fault + * fires during the read phase, before S_AMO_WRITE -- and never using + * S_AMO_WRITE's stale mem_paddr in the first place isn't proof the + * *other* branch, S_AMO_WRITE's own trap_val arm, picked the right + * source either). + * + * Operands are chosen so a mem_paddr-instead-of-amo_addr_q bug is + * trivially distinguishable from the correct result: AMOADD with old + * value 100 (core_amo_write_fault_harness.sv's fixed read-return value) + * and rs2=5 computes a modify value of 105. A buggy trap_val reading + * the repurposed mem_paddr during S_AMO_WRITE would report mtval=105; + * the correct implementation reports the real target address, 0x1000 -- + * two very different, unmistakable numbers, not an off-by-a-bit case + * that could be missed by accident. + */ +module core_amo_write_fault_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + core_amo_write_fault_harness #(.NUM_WORDS(64)) dut (.clk(clk), .rst(rst)); + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; + `include "halt_wait.sv" + + localparam int unsigned HANDLER_ADDR = 32'h40; + localparam int unsigned AMO_TARGET = 32'h1000; + + logic [31:0] main_prog[0:5]; + logic [31:0] handler_prog[0:3]; + int i; + + initial begin + #1; + + /* + * addr instr notes + * 0x00 addi x28, x0, HANDLER_ADDR + * 0x04 csrrw x0, mtvec, x28 + * 0x08 lui x5, 1 x5 = 0x1000 + * 0x0C addi x6, x0, 5 x6 = 5 (rs2 operand) + * 0x10 amoadd.w x7, x6, (x5) read succeeds (100), + * WRITE FAULTS + * 0x14 ebreak never reached directly + */ + main_prog[0] = encode_i(int'(HANDLER_ADDR), 5'd0, 3'b000, 5'd28, `OPC_OP_IMM); + main_prog[1] = encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + main_prog[2] = encode_u(20'h1, 5'd5, `OPC_LUI); + main_prog[3] = encode_i(32'sd5, 5'd0, 3'b000, 5'd6, `OPC_OP_IMM); + main_prog[4] = encode_amo(`FUNCT5_AMOADD, 1'b0, 1'b0, 5'd6, 5'd5, `FUNCT3_AMO_W, 5'd7, `OPC_AMO); + main_prog[5] = encode_i(32'sd1, 5'd0, 3'b000, 5'd0, `OPC_SYSTEM); // ebreak + + for (i = 0; i < 3; i = i + 1) + dut.memory[i] = {main_prog[2*i+1], main_prog[2*i]}; + + /* + * addr instr notes + * 0x40 csrrs x10, mcause, x0 + * 0x44 csrrs x11, mtval, x0 + * 0x48 ebreak + * 0x4C (padding, never fetched) + */ + handler_prog[0] = encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd10, `OPC_SYSTEM); + handler_prog[1] = encode_csr(`CSR_MTVAL, 5'd0, `FUNCT3_CSRRS, 5'd11, `OPC_SYSTEM); + handler_prog[2] = encode_i(32'sd1, 5'd0, 3'b000, 5'd0, `OPC_SYSTEM); // ebreak + handler_prog[3] = encode_i(32'sd0, 5'd0, 3'b000, 5'd0, `OPC_OP_IMM); // padding + + for (i = 0; i < 2; i = i + 1) + dut.memory[8+i] = {handler_prog[2*i+1], handler_prog[2*i]}; + + @(posedge clk); #1; + rst = 0; + + wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "EBREAK trap never fired"); + + check("AMO write-phase fault: mcause == 7 (store/AMO access fault)", + dut.core0.regfile0.gp_registers[10], 64'd7); + check("AMO write-phase fault: mtval == the real target address (0x1000), NOT the repurposed mem_paddr modify value (105)", + dut.core0.regfile0.gp_registers[11], 64'(AMO_TARGET)); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); + + $display(""); + $display("core_amo_write_fault_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("core_amo_write_fault_tb: FAILURES PRESENT"); + $finish; + end + +endmodule + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/testbench/core_branch_jump_tb.sv b/testbench/core_branch_jump_tb.sv index 66c2cee..b543e46 100644 --- a/testbench/core_branch_jump_tb.sv +++ b/testbench/core_branch_jump_tb.sv @@ -61,6 +61,9 @@ module core_branch_jump_tb; end endtask + logic halted = 1'b0; + always @(posedge clk) if (dut.trap_taken && dut.is_ebreak) halted <= 1'b1; + initial begin #1; // run after wb4_sram's own time-0 init (zero-fill + $readmemh) -- see core_wb_tb.sv @@ -119,10 +122,10 @@ module core_branch_jump_tb; rst = 0; fork - wait (dut.halted === 1'b1); + wait (halted === 1'b1); begin repeat (150) @(posedge clk); - $display("TIMEOUT: dut.halted never went high"); + $display("TIMEOUT: EBREAK trap never fired"); $finish; end join_any diff --git a/testbench/core_bus_fault_cache_tb.sv b/testbench/core_bus_fault_cache_tb.sv new file mode 100644 index 0000000..7f1227c --- /dev/null +++ b/testbench/core_bus_fault_cache_tb.sv @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +`include "riscv_encode.sv" + + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: core, two bus-error traps through the REAL cache path + * (core_cache_harness.sv -- core -> cache_complex -> icache0/dcache0 -> + * wb4_sram), the one harness that actually exercises the paired + * ack_o+err_o convention icache.sv/dcache.sv use on a downstream error + * (unlike wb4_sram.sv's own genuinely-unpaired convention, which + * core_bus_fault_trap_tb.sv/core_amo_write_fault_tb.sv already cover). + * + * Test J is the one that actually matters here: an AMO read-phase fault + * through the cached path is the single scenario that distinguishes a + * correct `wb_ok`-gated S_MEM -> S_AMO_WRITE decision from a buggy + * bare-`wb_ack_i` one -- through the cache, a downstream error arrives + * WITH wb_ack_i also asserted (icache.sv/dcache.sv's CACHE_REFILL error + * arm sets both together), so a buggy implementation that only checks + * wb_ack_i would incorrectly see "success" and proceed into + * S_AMO_WRITE, issuing a real, bogus second bus write. The equivalent + * test through core_wb4_sram_harness (core_bus_fault_trap_tb.sv's test + * F) cannot catch this specific bug class at all, since wb4_sram.sv + * never pairs ack and err in the first place. + * + * Both accesses target an address well beyond this harness's backing + * memory (NUM_WORDS default 4096, 32KB, valid up to 0x8000) so the + * access is guaranteed a cache miss on first touch, forcing a real + * CACHE_REFILL attempt against wb4_sram that genuinely errors. + */ +module core_bus_fault_cache_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + core_cache_harness dut (.clk(clk), .rst(rst)); + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; + `include "halt_wait.sv" + + /* + * Test J's whole point, mirroring core_bus_fault_trap_tb.sv's test F: + * an AMO read-phase fault must trap immediately, never proceeding + * into S_AMO_WRITE. Latched globally -- no AMO op in this program + * ever legitimately reaches S_AMO_WRITE either. + */ + logic amo_write_entered; + state_reached_monitor amo_write_monitor ( + .clk(clk), .i_state(dut.core0.state), + .i_state_target(dut.core0.S_AMO_WRITE), .o_reached(amo_write_entered) + ); + + localparam int unsigned HANDLER_I = 32'h40; + /* + * HANDLER_J = 0x54, NOT the word-aligned-looking 0x60 -- Handler I is + * 5 instructions (odd), so packed back-to-back with Handler J at 2 + * instructions/word starting at word 8 (0x40), Handler J's own first + * instruction lands mid-word, at 0x54 (word 10's high half), not a + * fresh word boundary. Got this wrong once already in this file + * (assumed 0x60 by eyeballing rather than deriving it from the + * packing loop) -- caught only via a PC trace showing execution + * jumping to a stale mepc, not by simulation failing cleanly. + */ + localparam int unsigned HANDLER_J = 32'h54; + localparam int unsigned RESUME_I = 32'h14; + localparam int unsigned RESUME_J = 32'h2C; + localparam int unsigned OUT_OF_RANGE = 32'h10000; + + logic [31:0] main_prog[0:12]; + logic [31:0] handler_prog[0:9]; + int i; + + initial begin + #1; + + /* + * addr instr notes + * 0x00 addi x28, x0, HANDLER_I + * 0x04 csrrw x0, mtvec, x28 + * 0x08 lui x5, 16 x5 = 0x10000 + * 0x0C addi x6, x0, 999 sentinel dest + * 0x10 ld x6, 0(x5) Test I: load fault + * 0x14 addi x2, x0, 111 resume marker I + * 0x18 addi x28, x0, HANDLER_J + * 0x1C csrrw x0, mtvec, x28 + * 0x20 lui x7, 16 x7 = 0x10000 + * 0x24 addi x8, x0, 5 rs2 operand + * 0x28 amoadd.w x9, x8, (x7) Test J: AMO read-phase fault + * 0x2C addi x3, x0, 222 resume marker J + * 0x30 ebreak + */ + main_prog[0] = encode_i(int'(HANDLER_I), 5'd0, 3'b000, 5'd28, `OPC_OP_IMM); + main_prog[1] = encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + main_prog[2] = encode_u(20'h10, 5'd5, `OPC_LUI); + main_prog[3] = encode_i(32'sd999, 5'd0, 3'b000, 5'd6, `OPC_OP_IMM); + main_prog[4] = encode_i(32'sd0, 5'd5, 3'b011, 5'd6, `OPC_LOAD); + main_prog[5] = encode_i(32'sd111, 5'd0, 3'b000, 5'd2, `OPC_OP_IMM); + main_prog[6] = encode_i(int'(HANDLER_J), 5'd0, 3'b000, 5'd28, `OPC_OP_IMM); + main_prog[7] = encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + main_prog[8] = encode_u(20'h10, 5'd7, `OPC_LUI); + main_prog[9] = encode_i(32'sd5, 5'd0, 3'b000, 5'd8, `OPC_OP_IMM); + main_prog[10] = encode_amo(`FUNCT5_AMOADD, 1'b0, 1'b0, 5'd8, 5'd7, `FUNCT3_AMO_W, 5'd9, `OPC_AMO); + main_prog[11] = encode_i(32'sd222, 5'd0, 3'b000, 5'd3, `OPC_OP_IMM); + main_prog[12] = encode_i(32'sd1, 5'd0, 3'b000, 5'd0, `OPC_SYSTEM); // ebreak + + for (i = 0; i < 6; i = i + 1) + dut.sram0.memory[i] = {main_prog[2*i+1], main_prog[2*i]}; + dut.sram0.memory[6][31:0] = main_prog[12]; // 0x30: ebreak, alone in its word's low half + + /* + * Handler I (0x40) and Handler J (0x54): capture mcause/mtval, + * mepc <- fixed resume address, mret. Same shape as + * core_misaligned_trap_tb.sv / core_bus_fault_trap_tb.sv. + * addr instr | addr instr + * 0x40 csrrs x10, mcause, x0 (h0) | 0x54 csrrs x12, mcause, x0 (h5) + * 0x44 csrrs x11, mtval, x0 (h1) | 0x58 csrrs x13, mtval, x0 (h6) + * 0x48 addi x20, x0, RESUME_I (h2) | 0x5C addi x20, x0, RESUME_J (h7) + * 0x4C csrrw x0, mepc, x20 (h3) | 0x60 csrrw x0, mepc, x20 (h8) + * 0x50 mret (h4) | 0x64 mret (h9) + */ + handler_prog[0] = encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd10, `OPC_SYSTEM); + handler_prog[1] = encode_csr(`CSR_MTVAL, 5'd0, `FUNCT3_CSRRS, 5'd11, `OPC_SYSTEM); + handler_prog[2] = encode_i(int'(RESUME_I), 5'd0, 3'b000, 5'd20, `OPC_OP_IMM); + handler_prog[3] = encode_csr(`CSR_MEPC, 5'd20, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + handler_prog[4] = `INSTR_HEX_MRET; + handler_prog[5] = encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd12, `OPC_SYSTEM); + handler_prog[6] = encode_csr(`CSR_MTVAL, 5'd0, `FUNCT3_CSRRS, 5'd13, `OPC_SYSTEM); + handler_prog[7] = encode_i(int'(RESUME_J), 5'd0, 3'b000, 5'd20, `OPC_OP_IMM); + handler_prog[8] = encode_csr(`CSR_MEPC, 5'd20, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + handler_prog[9] = `INSTR_HEX_MRET; + + // Packed 2 instructions/word starting at word 8 (0x40) -- see the + // address table just above for exactly where each one lands. + for (i = 0; i < 5; i = i + 1) + dut.sram0.memory[8+i] = {handler_prog[2*i+1], handler_prog[2*i]}; + + @(posedge clk); #1; + rst = 0; + + wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "EBREAK trap never fired"); + + // ---- Test I: plain load fault, through the real I$/D$ cache path ---- + check("I: mcause == 5 (load access fault)", dut.core0.regfile0.gp_registers[10], 64'd5); + check("I: mtval == the faulting load address", dut.core0.regfile0.gp_registers[11], 64'(OUT_OF_RANGE)); + check("I: resumed cleanly", dut.core0.regfile0.gp_registers[2], 64'd111); + + // ---- Test J: AMO read-phase fault, through the real D$ cache path ---- + check("J: mcause == 7 (store/AMO access fault)", dut.core0.regfile0.gp_registers[12], 64'd7); + check("J: mtval == the faulting AMO address", dut.core0.regfile0.gp_registers[13], 64'(OUT_OF_RANGE)); + check("J: resumed cleanly", dut.core0.regfile0.gp_registers[3], 64'd222); + check("J: S_AMO_WRITE never entered (cached, paired ack+err must not be mistaken for success)", + {63'b0, amo_write_entered}, 64'd0); + + check("EBREAK trap fired", {63'b0, halted}, 64'd1); + + $display(""); + $display("core_bus_fault_cache_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("core_bus_fault_cache_tb: FAILURES PRESENT"); + $finish; + end + +endmodule + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/testbench/core_bus_fault_trap_tb.sv b/testbench/core_bus_fault_trap_tb.sv new file mode 100644 index 0000000..d6002a7 --- /dev/null +++ b/testbench/core_bus_fault_trap_tb.sv @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +`include "riscv_encode.sv" + + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: core, six independent bus-error traps (instruction/load/ + * store/AMO access faults, causes 1/5/7) genuinely trapping through the + * real core, via core_wb4_sram_harness -- wb4_sram.sv is the one real + * Wishbone-B4-compliant slave in this project (ack/err mutually + * exclusive, an out-of-range access sets err_o with ack_o held 0), so + * this harness is what actually exercises wb_done/wb_ok's necessity -- + * see design/core.sv's own header comment on wb_done/wb_ok for why bare + * wb_ack_i is ambiguous through the CACHED path but not through this one. + * + * NUM_WORDS=64 (a power of 2, matching wb4_sram.sv's own $clog2-based + * addr_valid check exactly -- a non-power-of-2 size leaves a gap between + * addr_valid's rounded-up boundary and the real array bound, which is + * not what these tests want to exercise) -- valid range is byte + * addresses [0, 0x200). 0x1000 is the shared "out of range" target for + * every test except B, which needs a specific boundary (see its own + * comment below). + * + * All six tests use mtvec re-pointed to a dedicated handler (same + * pattern as core_misaligned_trap_tb.sv), each handler capturing + * mcause/mtval into test-specific registers, then setting mepc to a + * fixed, hardcoded resume address and mret-ing there (simpler than + * computing mepc+instruction-length, and works uniformly for every + * cause here since none of them have a "faulting instruction to skip + * past" in the same sense a synchronous ALU-instruction trap would -- + * the faulted bus cycle already prevented any real effect). + * + * Instructions are built into flat, sequential arrays (main_prog/ + * handler_prog) in natural program order, then packed into 64-bit SRAM + * words by a plain loop -- deliberately NOT hand-paired `memory[N] = + * {instrB, instrA}` literals one at a time: an earlier version of this + * file did exactly that and got a word-index off-by-one partway through + * (easy to make, since every pair requires manually tracking which half + * is high/low AND which word index you're currently on), which iverilog + * happily compiled and even partially "ran" -- caught only by a PC trace + * showing execution reaching an unintended address, not by any static + * check. Packing mechanically removes that whole class of mistake. + */ +module core_bus_fault_trap_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + core_wb4_sram_harness #(.NUM_WORDS(64)) dut (.clk(clk), .rst(rst)); + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; + `include "halt_wait.sv" + + /* + * Test F's whole point: an AMO read-phase fault must trap immediately + * from S_MEM, never proceeding into S_AMO_WRITE to issue a bogus + * second (write) bus transaction. Latched globally (not scoped to + * test F's own window) since no other AMO op in this program ever + * legitimately reaches S_AMO_WRITE either (LR is not is_amo_rmw, and + * the one real AMOADD here is the one under test) -- a true positive + * anywhere in the whole run is equally damning. + */ + logic amo_write_entered; + state_reached_monitor amo_write_monitor ( + .clk(clk), .i_state(dut.core0.state), + .i_state_target(dut.core0.S_AMO_WRITE), .o_reached(amo_write_entered) + ); + + /* Register allocation (all distinct, checked independently at the end): + * x2-x7: resume markers, one per test (A-F), prove clean resume. + * x8/x9: mcause/mtval, test A (instruction fetch fault). + * x10/x11: mcause/mtval, test B (dword-crossing fetch-hi fault). + * x12/x13: mcause/mtval, test C (plain load fault). + * x14/x15: mcause/mtval, test D (plain store fault). + * x16/x17: mcause/mtval, test E (LR.W fault). + * x18/x19: mcause/mtval, test F (AMO read-phase fault). + * x20: scratch, resume-address temp, reused by every handler. + * x21: test C's LD destination -- must stay at its sentinel + * value (901), proving the faulted load never wrote it. + * x22: test E's LR destination -- same role, sentinel 902. + * x23: test F's AMO destination -- same role, sentinel 903. + * x24: test F's AMOADD rs2 operand (5) -- irrelevant, the op + * never completes. + * x28: scratch, mtvec target, reused by every test setup. + * x29/x30: scratch, out-of-range address / store value, reused. + */ + localparam int unsigned HANDLER_A = 32'h88; + localparam int unsigned HANDLER_B = 32'h9C; + localparam int unsigned HANDLER_C = 32'hB0; + localparam int unsigned HANDLER_D = 32'hC4; + localparam int unsigned HANDLER_E = 32'hD8; + localparam int unsigned HANDLER_F = 32'hEC; + + localparam int unsigned RESUME_A = 32'h0C; + localparam int unsigned RESUME_B = 32'h1C; + localparam int unsigned RESUME_C = 32'h34; + localparam int unsigned RESUME_D = 32'h4C; + localparam int unsigned RESUME_E = 32'h64; + localparam int unsigned RESUME_F = 32'h80; + + localparam int unsigned OUT_OF_RANGE = 32'h1000; + + /* + * Test B: the crossing instruction starts at 0x1FE (pc[2:1]==2'b11, + * the last halfword slot of the dword at 0x1F8 -- the LAST valid + * dword under NUM_WORDS=64, whose own fetch (S_FETCH) succeeds), so + * its upper 16 bits would need to come from the dword at 0x200 -- + * one past the valid range, so S_FETCH_HI genuinely faults. Directly + * proves fetch_fault_q's trap_val is `pc` unconditionally (mepc== + * mtval==0x1FE, the crossing instruction's own address), not + * something that needs to know whether S_FETCH or S_FETCH_HI was + * the half that actually faulted. + */ + localparam int unsigned TEST_B_TARGET = 32'h1FE; + + logic [31:0] main_prog[0:33]; + logic [31:0] handler_prog[0:29]; + int i; + + initial begin + #1; // run after wb4_sram's own time-0 init + + /* + * ---- Main program, 34 instructions, addr 0x00-0x87 ---- + * idx addr instr notes + * 0 0x00 addi x28, x0, HANDLER_A + * 1 0x04 csrrw x0, mtvec, x28 + * 2 0x08 jal x0, OUT_OF_RANGE Test A: fetch fault + * 3 0x0C addi x2, x0, 111 resume marker A + * 4 0x10 addi x28, x0, HANDLER_B + * 5 0x14 csrrw x0, mtvec, x28 + * 6 0x18 jal x0, TEST_B_TARGET Test B: fetch-hi fault + * 7 0x1C addi x3, x0, 222 resume marker B + * 8 0x20 addi x28, x0, HANDLER_C + * 9 0x24 csrrw x0, mtvec, x28 + * 10 0x28 lui x29, 1 x29 = 0x1000 + * 11 0x2C addi x21, x0, 901 sentinel dest + * 12 0x30 ld x21, 0(x29) Test C: load fault + * 13 0x34 addi x4, x0, 333 resume marker C + * 14 0x38 addi x28, x0, HANDLER_D + * 15 0x3C csrrw x0, mtvec, x28 + * 16 0x40 lui x29, 1 + * 17 0x44 addi x30, x0, 777 value to store (irrelevant) + * 18 0x48 sd x30, 0(x29) Test D: store fault + * 19 0x4C addi x5, x0, 444 resume marker D + * 20 0x50 addi x28, x0, HANDLER_E + * 21 0x54 csrrw x0, mtvec, x28 + * 22 0x58 lui x29, 1 + * 23 0x5C addi x22, x0, 902 sentinel dest + * 24 0x60 lr.w x22, (x29) Test E: LR fault + * 25 0x64 addi x6, x0, 555 resume marker E + * 26 0x68 addi x28, x0, HANDLER_F + * 27 0x6C csrrw x0, mtvec, x28 + * 28 0x70 lui x29, 1 + * 29 0x74 addi x23, x0, 903 sentinel dest + * 30 0x78 addi x24, x0, 5 rs2 operand (irrelevant) + * 31 0x7C amoadd.w x23, x24, (x29) Test F: AMO read-phase fault + * 32 0x80 addi x7, x0, 666 resume marker F + * 33 0x84 ebreak + */ + main_prog[0] = encode_i(int'(HANDLER_A), 5'd0, 3'b000, 5'd28, `OPC_OP_IMM); + main_prog[1] = encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + main_prog[2] = encode_j(int'(OUT_OF_RANGE) - 32'h08, 5'd0, `OPC_JAL); + main_prog[3] = encode_i(32'sd111, 5'd0, 3'b000, 5'd2, `OPC_OP_IMM); + main_prog[4] = encode_i(int'(HANDLER_B), 5'd0, 3'b000, 5'd28, `OPC_OP_IMM); + main_prog[5] = encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + main_prog[6] = encode_j(int'(TEST_B_TARGET) - 32'h18, 5'd0, `OPC_JAL); + main_prog[7] = encode_i(32'sd222, 5'd0, 3'b000, 5'd3, `OPC_OP_IMM); + main_prog[8] = encode_i(int'(HANDLER_C), 5'd0, 3'b000, 5'd28, `OPC_OP_IMM); + main_prog[9] = encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + main_prog[10] = encode_u(20'h1, 5'd29, `OPC_LUI); + main_prog[11] = encode_i(32'sd901, 5'd0, 3'b000, 5'd21, `OPC_OP_IMM); + main_prog[12] = encode_i(32'sd0, 5'd29, 3'b011, 5'd21, `OPC_LOAD); + main_prog[13] = encode_i(32'sd333, 5'd0, 3'b000, 5'd4, `OPC_OP_IMM); + main_prog[14] = encode_i(int'(HANDLER_D), 5'd0, 3'b000, 5'd28, `OPC_OP_IMM); + main_prog[15] = encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + main_prog[16] = encode_u(20'h1, 5'd29, `OPC_LUI); + main_prog[17] = encode_i(32'sd777, 5'd0, 3'b000, 5'd30, `OPC_OP_IMM); + main_prog[18] = encode_s(32'sd0, 5'd30, 5'd29, 3'b011, `OPC_STORE); + main_prog[19] = encode_i(32'sd444, 5'd0, 3'b000, 5'd5, `OPC_OP_IMM); + main_prog[20] = encode_i(int'(HANDLER_E), 5'd0, 3'b000, 5'd28, `OPC_OP_IMM); + main_prog[21] = encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + main_prog[22] = encode_u(20'h1, 5'd29, `OPC_LUI); + main_prog[23] = encode_i(32'sd902, 5'd0, 3'b000, 5'd22, `OPC_OP_IMM); + main_prog[24] = encode_amo(`FUNCT5_LR, 1'b0, 1'b0, 5'd0, 5'd29, `FUNCT3_AMO_W, 5'd22, `OPC_AMO); + main_prog[25] = encode_i(32'sd555, 5'd0, 3'b000, 5'd6, `OPC_OP_IMM); + main_prog[26] = encode_i(int'(HANDLER_F), 5'd0, 3'b000, 5'd28, `OPC_OP_IMM); + main_prog[27] = encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + main_prog[28] = encode_u(20'h1, 5'd29, `OPC_LUI); + main_prog[29] = encode_i(32'sd903, 5'd0, 3'b000, 5'd23, `OPC_OP_IMM); + main_prog[30] = encode_i(32'sd5, 5'd0, 3'b000, 5'd24, `OPC_OP_IMM); + main_prog[31] = encode_amo(`FUNCT5_AMOADD, 1'b0, 1'b0, 5'd24, 5'd29, `FUNCT3_AMO_W, 5'd23, `OPC_AMO); + main_prog[32] = encode_i(32'sd666, 5'd0, 3'b000, 5'd7, `OPC_OP_IMM); + main_prog[33] = encode_i(32'sd1, 5'd0, 3'b000, 5'd0, `OPC_SYSTEM); // ebreak + + for (i = 0; i < 17; i = i + 1) + dut.sram0.memory[i] = {main_prog[2*i+1], main_prog[2*i]}; + + /* + * ---- Handlers, 5 instructions each, addr 0x88-0xFF ---- + * Every handler: csrrs mcause, csrrs mtval, addi x20 (resume + * addr), csrrw mepc x20, mret. h_idx = global index 0-29 across + * all six handlers; address = 0x88 + h_idx*4. + */ + handler_prog[0] = encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd8, `OPC_SYSTEM); // A + handler_prog[1] = encode_csr(`CSR_MTVAL, 5'd0, `FUNCT3_CSRRS, 5'd9, `OPC_SYSTEM); + handler_prog[2] = encode_i(int'(RESUME_A), 5'd0, 3'b000, 5'd20, `OPC_OP_IMM); + handler_prog[3] = encode_csr(`CSR_MEPC, 5'd20, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + handler_prog[4] = `INSTR_HEX_MRET; + handler_prog[5] = encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd10, `OPC_SYSTEM); // B + handler_prog[6] = encode_csr(`CSR_MTVAL, 5'd0, `FUNCT3_CSRRS, 5'd11, `OPC_SYSTEM); + handler_prog[7] = encode_i(int'(RESUME_B), 5'd0, 3'b000, 5'd20, `OPC_OP_IMM); + handler_prog[8] = encode_csr(`CSR_MEPC, 5'd20, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + handler_prog[9] = `INSTR_HEX_MRET; + handler_prog[10] = encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd12, `OPC_SYSTEM); // C + handler_prog[11] = encode_csr(`CSR_MTVAL, 5'd0, `FUNCT3_CSRRS, 5'd13, `OPC_SYSTEM); + handler_prog[12] = encode_i(int'(RESUME_C), 5'd0, 3'b000, 5'd20, `OPC_OP_IMM); + handler_prog[13] = encode_csr(`CSR_MEPC, 5'd20, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + handler_prog[14] = `INSTR_HEX_MRET; + handler_prog[15] = encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd14, `OPC_SYSTEM); // D + handler_prog[16] = encode_csr(`CSR_MTVAL, 5'd0, `FUNCT3_CSRRS, 5'd15, `OPC_SYSTEM); + handler_prog[17] = encode_i(int'(RESUME_D), 5'd0, 3'b000, 5'd20, `OPC_OP_IMM); + handler_prog[18] = encode_csr(`CSR_MEPC, 5'd20, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + handler_prog[19] = `INSTR_HEX_MRET; + handler_prog[20] = encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd16, `OPC_SYSTEM); // E + handler_prog[21] = encode_csr(`CSR_MTVAL, 5'd0, `FUNCT3_CSRRS, 5'd17, `OPC_SYSTEM); + handler_prog[22] = encode_i(int'(RESUME_E), 5'd0, 3'b000, 5'd20, `OPC_OP_IMM); + handler_prog[23] = encode_csr(`CSR_MEPC, 5'd20, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + handler_prog[24] = `INSTR_HEX_MRET; + handler_prog[25] = encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd18, `OPC_SYSTEM); // F + handler_prog[26] = encode_csr(`CSR_MTVAL, 5'd0, `FUNCT3_CSRRS, 5'd19, `OPC_SYSTEM); + handler_prog[27] = encode_i(int'(RESUME_F), 5'd0, 3'b000, 5'd20, `OPC_OP_IMM); + handler_prog[28] = encode_csr(`CSR_MEPC, 5'd20, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + handler_prog[29] = `INSTR_HEX_MRET; + + for (i = 0; i < 15; i = i + 1) + dut.sram0.memory[17+i] = {handler_prog[2*i+1], handler_prog[2*i]}; + + /* + * Test D's "store never landed" defense-in-depth check: a nearby + * valid word, never touched by any real instruction in this + * program. Structurally this can never actually be corrupted + * (wb4_sram.sv's own addr_valid gate keeps an out-of-range write + * from ever reaching `memory[]` at all, regardless of whether + * core.sv's fault handling is correct), but checking it directly + * still proves core.sv's own fault path doesn't accidentally + * compute or drive a DIFFERENT, valid address as a side effect. + */ + dut.sram0.memory[40] = 64'hCAFE_F00D_CAFE_F00D; + + /* + * Test B's crossing dword: hw3 (bits[63:48], the halfword at + * offset 6 -- pc[2:1]==2'b11 selects it as "first_hw") holds the + * low 16 bits of an ordinary 32-bit opcode (0x0013, ADDI's own + * low halfword -- bits[1:0]==2'b11, which is what actually + * signals "this is the start of an uncompressed instruction" to + * fetch_hi_needed/is_compressed; the fetch never reaches decode + * at all, so which real instruction this would have been is + * irrelevant). The other three halfwords are never selected at + * this pc and are left zero. + */ + dut.sram0.memory[63] = {16'h0013, 16'h0000, 16'h0000, 16'h0000}; + + @(posedge clk); #1; + rst = 0; + + wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "EBREAK trap never fired"); + + // ---- Test A: instruction fetch fault ---- + check("A: mcause == 1 (instruction access fault)", dut.core0.regfile0.gp_registers[8], 64'd1); + check("A: mtval == mepc == faulting fetch address", dut.core0.regfile0.gp_registers[9], 64'h1000); + check("A: resumed cleanly", dut.core0.regfile0.gp_registers[2], 64'd111); + + // ---- Test B: dword-crossing fetch-hi fault ---- + check("B: mcause == 1 (instruction access fault)", dut.core0.regfile0.gp_registers[10], 64'd1); + check("B: mtval == mepc == the crossing instruction's own address (not the failed S_FETCH_HI address)", + dut.core0.regfile0.gp_registers[11], 64'h1FE); + check("B: resumed cleanly", dut.core0.regfile0.gp_registers[3], 64'd222); + + // ---- Test C: plain load fault ---- + check("C: mcause == 5 (load access fault)", dut.core0.regfile0.gp_registers[12], 64'd5); + check("C: mtval == the faulting load address", dut.core0.regfile0.gp_registers[13], 64'h1000); + check("C: dest register untouched (load never happened)", dut.core0.regfile0.gp_registers[21], 64'd901); + check("C: resumed cleanly", dut.core0.regfile0.gp_registers[4], 64'd333); + + // ---- Test D: plain store fault ---- + check("D: mcause == 7 (store/AMO access fault)", dut.core0.regfile0.gp_registers[14], 64'd7); + check("D: mtval == the faulting store address", dut.core0.regfile0.gp_registers[15], 64'h1000); + check("D: resumed cleanly", dut.core0.regfile0.gp_registers[5], 64'd444); + check("D: nearby valid sentinel dword unchanged", dut.sram0.memory[40], 64'hCAFE_F00D_CAFE_F00D); + + // ---- Test E: LR.W fault -- must classify as cause 7, not 5 ---- + check("E: mcause == 7 (store/AMO access fault, NOT 5 -- LR is spec-classified as store/AMO)", + dut.core0.regfile0.gp_registers[16], 64'd7); + check("E: mtval == the faulting LR address", dut.core0.regfile0.gp_registers[17], 64'h1000); + check("E: dest register untouched (LR never happened)", dut.core0.regfile0.gp_registers[22], 64'd902); + check("E: resumed cleanly", dut.core0.regfile0.gp_registers[6], 64'd555); + + // ---- Test F: AMO read-phase fault ---- + check("F: mcause == 7 (store/AMO access fault)", dut.core0.regfile0.gp_registers[18], 64'd7); + check("F: mtval == the faulting AMO address", dut.core0.regfile0.gp_registers[19], 64'h1000); + check("F: dest register untouched (AMO never happened)", dut.core0.regfile0.gp_registers[23], 64'd903); + check("F: resumed cleanly", dut.core0.regfile0.gp_registers[7], 64'd666); + check("F: S_AMO_WRITE never entered (read-phase fault must not chase a bogus write)", + {63'b0, amo_write_entered}, 64'd0); + + check("EBREAK trap fired", {63'b0, halted}, 64'd1); + + $display(""); + $display("core_bus_fault_trap_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("core_bus_fault_trap_tb: FAILURES PRESENT"); + $finish; + end + +endmodule + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/testbench/core_c_ext_cache_tb.sv b/testbench/core_c_ext_cache_tb.sv index 9f09fd1..cfe4038 100644 --- a/testbench/core_c_ext_cache_tb.sv +++ b/testbench/core_c_ext_cache_tb.sv @@ -53,7 +53,8 @@ module core_c_ext_cache_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.core0.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" /* @@ -145,7 +146,7 @@ module core_c_ext_cache_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "dut.core0.halted never went high"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "EBREAK trap never fired"); check("C.ADDI16SP (x2)", dut.core0.regfile0.gp_registers[2], 64'd272); check("C.LI negative (x5)", dut.core0.regfile0.gp_registers[5], -64'sd5); @@ -159,7 +160,7 @@ module core_c_ext_cache_tb; check("C.AND source unchanged (x11)", dut.core0.regfile0.gp_registers[11], 64'd222); check("C.SW/C.LW round trip (x15)", dut.core0.regfile0.gp_registers[15], 64'd78); check("C.BEQZ not taken -- fell through (x16)", dut.core0.regfile0.gp_registers[16], 64'd1); - check("core halted (c.ebreak reached)", {63'b0, dut.core0.halted}, 64'd1); + check("EBREAK trap fired (c.ebreak)", {63'b0, halted}, 64'd1); check("S_FETCH_HI genuinely visited for the crossing instruction", {63'b0, (fetch_hi_visit_count > 0)}, 64'd1); diff --git a/testbench/core_c_ext_tb.sv b/testbench/core_c_ext_tb.sv index be3d93b..98a5dbf 100644 --- a/testbench/core_c_ext_tb.sv +++ b/testbench/core_c_ext_tb.sv @@ -64,7 +64,8 @@ module core_c_ext_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.core0.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" /* @@ -164,7 +165,7 @@ module core_c_ext_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "dut.core0.halted never went high"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "EBREAK trap never fired"); check("C.ADDI16SP (x2)", dut.core0.regfile0.gp_registers[2], 64'd272); check("C.LI negative (x5)", dut.core0.regfile0.gp_registers[5], -64'sd5); @@ -178,7 +179,7 @@ module core_c_ext_tb; check("C.AND source unchanged (x11)", dut.core0.regfile0.gp_registers[11], 64'd222); check("C.SW/C.LW round trip (x15)", dut.core0.regfile0.gp_registers[15], 64'd78); check("C.BEQZ not taken -- fell through (x16)", dut.core0.regfile0.gp_registers[16], 64'd1); - check("core halted (c.ebreak reached)", {63'b0, dut.core0.halted}, 64'd1); + check("EBREAK trap fired (c.ebreak)", {63'b0, halted}, 64'd1); check("S_FETCH_HI genuinely visited for the crossing instruction", {63'b0, (fetch_hi_visit_count > 0)}, 64'd1); diff --git a/testbench/core_c_illegal_trap_tb.sv b/testbench/core_c_illegal_trap_tb.sv index 5ac30c4..efde034 100644 --- a/testbench/core_c_illegal_trap_tb.sv +++ b/testbench/core_c_illegal_trap_tb.sv @@ -54,9 +54,33 @@ module core_c_illegal_trap_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.core0.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" + /* + * mcause_q/mtval_q snapshot, taken at the resume point (pc==0x0A, + * `addi x5,x0,999`, the instruction right after the fixed-up mepc + * returns) -- NOT read live at check() time. mtvec stays armed at + * M_TRAP_HANDLER for the rest of this program, including this file's + * own terminating ebreak at 0x0E: since EBREAK is a real trap now, + * that ebreak bounces right back into M_TRAP_HANDLER (mtvec still + * points there), which advances mepc by 2 and mrets into whatever + * follows -- genuinely unwritten memory, causing a cascade of further + * illegal-instruction traps that keep overwriting mcause_q/mtval_q + * long after this test's own real assertion already happened. A + * point-in-time snapshot at the resume point sidesteps all of that, + * capturing state before the terminating ebreak (and its bounce-back) + * ever fires. + */ + logic [63:0] mcause_snap, mtval_snap; + logic resumed = 1'b0; + always @(posedge clk) if (!resumed && dut.core0.commit_now && dut.core0.pc == 64'h0A) begin + resumed <= 1'b1; + mcause_snap <= dut.core0.csr_file0.mcause_q; + mtval_snap <= dut.core0.csr_file0.mtval_q; + end + initial begin #1; // run after wb4_sram's own time-0 init @@ -90,14 +114,14 @@ module core_c_illegal_trap_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "dut.core0.halted never went high"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "EBREAK trap never fired"); - check("mcause == 2 (illegal instruction)", dut.core0.csr_file0.mcause_q, 64'd2); + check("mcause == 2 (illegal instruction)", mcause_snap, 64'd2); check("mtval == the raw 16-bit C.ILLEGAL halfword, zero-extended", - dut.core0.csr_file0.mtval_q, 64'h0000); + mtval_snap, 64'h0000); check("resumed exactly past the 2-byte illegal instruction (x5)", dut.core0.regfile0.gp_registers[5], 64'd999); - check("core halted (ebreak reached)", {63'b0, dut.core0.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); $display(""); $display("core_c_illegal_trap_tb: %0d passed, %0d failed", pass_count, fail_count); diff --git a/testbench/core_c_toolchain_tb.sv b/testbench/core_c_toolchain_tb.sv index 3c4c87f..7a6f44d 100644 --- a/testbench/core_c_toolchain_tb.sv +++ b/testbench/core_c_toolchain_tb.sv @@ -85,7 +85,8 @@ module core_c_toolchain_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.trap_taken && dut.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" initial begin @@ -95,7 +96,7 @@ module core_c_toolchain_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "dut.halted never went high -- is firmware/c_test.hex built?"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "EBREAK trap never fired -- is firmware/c_test.hex built?"); /* * Expected values -- see firmware/c_test.s's inline comments for @@ -109,7 +110,7 @@ module core_c_toolchain_tb; check("loop iteration count (s4)", dut.regfile0.gp_registers[20], 64'd5); check("post-return arithmetic (s5)", dut.regfile0.gp_registers[21], 64'd93); // -7 + 100 - check("core halted (ebreak reached)", {63'b0, dut.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); $display(""); $display("core_c_toolchain_tb: %0d passed, %0d failed", pass_count, fail_count); diff --git a/testbench/core_cache_harness.sv b/testbench/core_cache_harness.sv index a5fd72c..c71deea 100644 --- a/testbench/core_cache_harness.sv +++ b/testbench/core_cache_harness.sv @@ -27,7 +27,8 @@ module core_cache_harness #( parameter LINE_WORDS = 4 ) ( input logic clk, - input logic rst + input logic rst, + input logic i_mtip = 1'b0 ); logic [31:0] wb_addr; @@ -35,12 +36,14 @@ module core_cache_harness #( logic [7:0] wb_sel; logic wb_we, wb_cyc, wb_stb, wb_ack, wb_err; logic wb_ifetch; + logic icache_flush; core core0 ( .clk(clk), .rst(rst), .wb_addr_o(wb_addr), .wb_dat_o(wb_dat_m2s), .wb_dat_i(wb_dat_s2m), .wb_sel_o(wb_sel), .wb_we_o(wb_we), .wb_cyc_o(wb_cyc), .wb_stb_o(wb_stb), - .wb_ack_i(wb_ack), .wb_err_i(wb_err), .wb_ifetch_o(wb_ifetch) + .wb_ack_i(wb_ack), .wb_err_i(wb_err), .wb_ifetch_o(wb_ifetch), + .icache_flush_o(icache_flush), .i_mtip(i_mtip) ); logic [31:0] mem_addr; @@ -52,7 +55,7 @@ module core_cache_harness #( .clk(clk), .rst(rst), .addr_i(wb_addr), .dat_i(wb_dat_m2s), .dat_o(wb_dat_s2m), .sel_i(wb_sel), .we_i(wb_we), .ifetch_i(wb_ifetch), .cyc_i(wb_cyc), .stb_i(wb_stb), - .ack_o(wb_ack), .err_o(wb_err), + .ack_o(wb_ack), .err_o(wb_err), .flush_i(icache_flush), .mem_addr_o(mem_addr), .mem_dat_o(mem_dat_m2s), .mem_dat_i(mem_dat_s2m), .mem_sel_o(mem_sel), .mem_we_o(mem_we), .mem_cyc_o(mem_cyc), .mem_stb_o(mem_stb), .mem_ack_i(mem_ack), .mem_err_i(mem_err) diff --git a/testbench/core_csr_readonly_trap_tb.sv b/testbench/core_csr_readonly_trap_tb.sv new file mode 100644 index 0000000..c482130 --- /dev/null +++ b/testbench/core_csr_readonly_trap_tb.sv @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +`include "riscv_encode.sv" + + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: core, a write-attempt to a read-only CSR genuinely trapping + * through the real core (illegal-instruction, cause 2). + * + * Closes a real gap found 2026-08-20 via ACT4's priv/U/U-00 test: once + * EBREAK became a real, resumable trap (see the EBREAK-real-trap + * milestone), U-00 ran far enough for the first time to reach its own + * boot-time `csrrw x0, cycle, a0` (a write-attempt to a genuinely + * read-only CSR) -- traced via sail_riscv_sim against the real ELF, + * diffed against this core's own RVFI retirement trace, confirming the + * exact first divergence: Sail traps (illegal-instruction), this core + * silently no-op'd the write and fell through instead. Per spec: + * "Attempts to write a read-only CSR... raise illegal instruction + * exceptions." design/core.sv's own header comment at + * csr_readonly_violation has the full derivation. + * + * This subtest used to live at the top of core_zicsr_tb.sv (a + * csrrwi-to-mhartid case expecting a silent ignore) -- retired from + * there once the real trap landed, replaced by this dedicated file, + * matching this project's "one file per concern" convention for a + * trap/exception scenario rather than folding a trap-and-resume + * sequence into core_zicsr_tb.sv's otherwise straight-line "vanilla CSR + * round trip" program. + * + * Mirrors core_c_illegal_trap_tb.sv's own M_TRAP_HANDLER pattern (read + * mepc, skip past the faulting instruction, write back, mret) and its + * point-in-time-snapshot discipline (mtvec stays armed through this + * file's own terminating ebreak, which -- now a real trap -- bounces + * back into the handler once it fires; live post-halt CSR reads are not + * safe against that, see that file's own comment for the full story). + * + * Uses mhartid (0xF14, bits[11:10]=='11') as the read-only target -- + * already this project's own established "obviously read-only, always + * 0" example (see the now-retired core_zicsr_tb.sv subtest this + * replaces). + */ +module core_csr_readonly_trap_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + core_wb4_sram_harness #(.NUM_WORDS(32)) dut (.clk(clk), .rst(rst)); + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; + `include "halt_wait.sv" + + /* + * mcause_q/mtval_q snapshot, taken at the resume point (pc==0x0C, + * the marker right after the fixed-up mepc returns) -- NOT read live + * at check() time. Same reasoning as core_c_illegal_trap_tb.sv's own + * identical snapshot: mtvec stays armed through this file's own + * terminating ebreak, which bounces back into the handler once it + * fires, corrupting mcause_q/mtval_q long after this test's real + * assertion already happened if read live. + */ + logic [63:0] mcause_snap, mtval_snap; + logic resumed = 1'b0; + always @(posedge clk) if (!resumed && dut.core0.commit_now && dut.core0.pc == 64'h0C) begin + resumed <= 1'b1; + mcause_snap <= dut.core0.csr_file0.mcause_q; + mtval_snap <= dut.core0.csr_file0.mtval_q; + end + + logic [31:0] trigger_insn; + + initial begin + #1; // run after wb4_sram's own time-0 init + + /* + * addr instr notes + * 0x00 addi x28, x0, 0x18 (24) x28 = M_TRAP_HANDLER address + * 0x04 csrrw x0, mtvec, x28 + * 0x08 csrrwi x1, mhartid, 7 the faulting write-attempt (TRIGGER) + * 0x0C addi x5, x0, 555 resume target -- reached only via the + * handler's mepc+4 fix, not by falling + * through (the faulting instr never retires) + * 0x10 csrrs x6, mhartid, x0 confirm the write never actually landed + * 0x14 ebreak + * -- M_TRAP_HANDLER (0x18) -- + * 0x18 csrrs x10, mepc, x0 + * 0x1C csrrs x11, mcause, x0 + * 0x20 csrrs x12, mtval, x0 + * 0x24 addi x10, x10, 4 +4 -- the faulting instruction was a + * real 32-bit encoding, not compressed + * 0x28 csrrw x0, mepc, x10 + * 0x2C mret + */ + trigger_insn = encode_csr(`CSR_MHARTID, 5'd7, `FUNCT3_CSRRWI, 5'd1, `OPC_SYSTEM); + + dut.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd24, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut.sram0.memory[1] = {encode_i(32'sd555, 5'd0, 3'b000, 5'd5, `OPC_OP_IMM), + trigger_insn}; + dut.sram0.memory[2] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, + encode_csr(`CSR_MHARTID, 5'd0, `FUNCT3_CSRRS, 5'd6, `OPC_SYSTEM)}; + dut.sram0.memory[3] = {encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd11, `OPC_SYSTEM), + encode_csr(`CSR_MEPC, 5'd0, `FUNCT3_CSRRS, 5'd10, `OPC_SYSTEM)}; + dut.sram0.memory[4] = {encode_i(32'sd4, 5'd10, 3'b000, 5'd10, `OPC_OP_IMM), + encode_csr(`CSR_MTVAL, 5'd0, `FUNCT3_CSRRS, 5'd12, `OPC_SYSTEM)}; + dut.sram0.memory[5] = {`INSTR_HEX_MRET, + encode_csr(`CSR_MEPC, 5'd10, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM)}; + + @(posedge clk); #1; + rst = 0; + + wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "EBREAK trap never fired"); + + check("x1 never written -- the trapping csrrwi's own rd write is suppressed", + dut.core0.regfile0.gp_registers[1], 64'd0); + check("resumed exactly past the faulting csrrwi (x5)", + dut.core0.regfile0.gp_registers[5], 64'd555); + check("mhartid write-attempt never actually landed (still reads 0)", + dut.core0.regfile0.gp_registers[6], 64'd0); + check("mcause == 2 (illegal instruction)", mcause_snap, 64'd2); + check("mtval == the faulting csrrwi's own raw 32-bit encoding", + mtval_snap, {32'b0, trigger_insn}); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); + + $display(""); + $display("core_csr_readonly_trap_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("core_csr_readonly_trap_tb: FAILURES PRESENT"); + $finish; + end + +endmodule + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/testbench/core_csr_toolchain_tb.sv b/testbench/core_csr_toolchain_tb.sv index 332a461..05b3951 100644 --- a/testbench/core_csr_toolchain_tb.sv +++ b/testbench/core_csr_toolchain_tb.sv @@ -75,6 +75,9 @@ module core_csr_toolchain_tb; end endtask + logic halted = 1'b0; + always @(posedge clk) if (dut.trap_taken && dut.is_ebreak) halted <= 1'b1; + initial begin #1; // see header comment: run after wb4_sram's own time-0 init of crt0.hex $readmemh("../firmware/csr_test.hex", sram0.memory); @@ -83,10 +86,10 @@ module core_csr_toolchain_tb; rst = 0; fork - wait (dut.halted === 1'b1); + wait (halted === 1'b1); begin repeat (300) @(posedge clk); - $display("TIMEOUT: dut.halted never went high -- is firmware/csr_test.hex built?"); + $display("TIMEOUT: EBREAK trap never fired -- is firmware/csr_test.hex built?"); $finish; end join_any @@ -117,7 +120,7 @@ module core_csr_toolchain_tb; check("mscratch_q final value (register-independent confirmation)", dut.csr_file0.mscratch_q, 64'h0000_0000_0bad_beef); - check("core halted (ebreak reached)", {63'b0, dut.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); $display(""); $display("core_csr_toolchain_tb: %0d passed, %0d failed", pass_count, fail_count); diff --git a/testbench/core_debug_csr_violation_tb.sv b/testbench/core_debug_csr_violation_tb.sv new file mode 100644 index 0000000..9a3a5f9 --- /dev/null +++ b/testbench/core_debug_csr_violation_tb.sv @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +`include "riscv_encode.sv" + + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: core, an ordinary CSR access to any Debug-mode CSR + * (dcsr/dpc/dscratch0/dscratch1) genuinely trapping as illegal- + * instruction (cause 2) outside Debug Mode. + * + * Milestone 3 of the EBREAK/JTAG staged plan: in_debug_mode is tied 0 + * in design/core.sv until Milestone 4 builds the real halt/resume FSM, + * so EVERY access to these four addresses traps right now, from any + * privilege level -- this file proves exactly that (the only testable + * half this milestone; the inside-Debug-Mode-legal half needs M4's real + * in_debug_mode). Coverage, in program order: + * - M-mode READ access to all 4 addresses traps (dcsr/dpc/dscratch0/ + * dscratch1). + * - M-mode WRITE access (csrrw, a genuine, non-suppressed write -- + * unlike the csrrs-with-rs1=x0 reads above) traps too -- + * debug_csr_violation is deliberately NOT gated on write-suppression + * the way csr_readonly_violation is. + * - Two negative controls proving debug_csr_violation doesn't + * over-fire: an ordinary CSR (mscratch) access succeeds normally, + * and accesses to the addresses immediately adjacent to the 4-CSR + * block (0x7AF below, 0x7B4 above) do NOT trap -- pins down + * is_debug_csr_addr's exact range-check boundary. + * - Privilege is then dropped M -> S (via mret) and S -> U (via sret, + * the established core_priv_tb.sv-precedented mechanism), and a + * debug-CSR access is repeated from each level. Both of these also + * exercise the DUAL-violation case for free: csr_priv_violation + * (imm_2[9:8]==2'b11, a magnitude comparison against current_priv) + * is independently true from S/U for these same addresses, so this + * is the only place in the suite where csr_priv_violation and + * debug_csr_violation are simultaneously true, proving the OR- + * composition in is_illegal_instr doesn't need them to be mutually + * exclusive. + * + * Mirrors core_csr_readonly_trap_tb.sv's own M_TRAP_HANDLER pattern + * (read mepc, skip past the faulting instruction, write back, mret) and + * its point-in-time-snapshot discipline (mtvec stays armed through this + * file's own terminating ebreak, which -- a real trap since the EBREAK + * milestone -- bounces back into the handler once it fires; live + * post-halt CSR reads are not safe against that). medeleg is never + * written (stays 0 from reset), so every trap -- regardless of which + * privilege it's taken FROM -- targets M-mode and lands in the same + * shared handler; no separate S-mode handler infrastructure is needed. + */ +module core_debug_csr_violation_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + core_wb4_sram_harness #(.NUM_WORDS(32)) dut (.clk(clk), .rst(rst)); + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; + `include "halt_wait.sv" + + /* + * mcause_q/mtval_q snapshot, taken at the resume point right after + * the LAST trap (the U-mode dpc access) resolves (pc==0x58, before + * ebreak executes) -- NOT read live at check() time. Same reasoning + * as core_csr_readonly_trap_tb.sv's own identical snapshot. + */ + logic [63:0] mcause_snap, mtval_snap; + logic resumed_last = 1'b0; + always @(posedge clk) if (!resumed_last && dut.core0.commit_now && dut.core0.pc == 64'h58) begin + resumed_last <= 1'b1; + mcause_snap <= dut.core0.csr_file0.mcause_q; + mtval_snap <= dut.core0.csr_file0.mtval_q; + end + + logic [31:0] dpc_u_insn; + + initial begin + #1; // run after wb4_sram's own time-0 init + + /* + * addr instr notes + * 0x00 addi x30, x0, 92 (0x5C) x30 = M_TRAP_HANDLER address + * 0x04 csrrw x0, mtvec, x30 + * 0x08 csrrs x1, dcsr, x0 TRAP -- M-mode read (resumes 0x0C) + * 0x0C csrrs x2, dpc, x0 TRAP -- M-mode read (resumes 0x10) + * 0x10 csrrs x3, dscratch0, x0 TRAP -- M-mode read (resumes 0x14) + * 0x14 csrrs x4, dscratch1, x0 TRAP -- M-mode read (resumes 0x18) + * 0x18 addi x6, x0, 85 (0x55) write payload + * 0x1C csrrw x0, dcsr, x6 TRAP -- M-mode WRITE, not suppressed (resumes 0x20) + * 0x20 csrrs x5, mscratch, x0 NOT a trap -- ordinary CSR, negative control + * 0x24 csrrs x7, 0x7AF, x0 NOT a trap -- one below the block, negative control + * 0x28 csrrs x8, 0x7B4, x0 NOT a trap -- one above the block, negative control + * 0x2C addi x11, x0, 2047 (0x7FF) + * 0x30 addi x11, x11, 1 x11 = 0x800 (mstatus.MPP = S, bit 11) + * 0x34 csrrw x0, mstatus, x11 + * 0x38 addi x12, x0, 68 (0x44) S-mode continuation address + * 0x3C csrrw x0, mepc, x12 + * 0x40 mret -> S-mode, pc = 0x44 + * 0x44 csrrs x13, dcsr, x0 TRAP -- S-mode read, DUAL violation (resumes 0x48) + * 0x48 addi x14, x0, 84 (0x54) U-mode continuation address + * 0x4C csrrw x0, sepc, x14 + * 0x50 sret -> U-mode, pc = 0x54 (SPP still 0/U, never set) + * 0x54 csrrs x15, dpc, x0 TRAP -- U-mode read, DUAL violation (resumes 0x58) + * 0x58 ebreak + * -- M_TRAP_HANDLER (0x5C) -- + * 0x5C csrrs x10, mepc, x0 + * 0x60 addi x10, x10, 4 +4 -- every faulting instruction here is a + * real 32-bit encoding, not compressed + * 0x64 csrrw x0, mepc, x10 + * 0x68 addi x28, x28, 1 trap_count++ + * 0x6C mret + */ + dpc_u_insn = encode_csr(12'h7B1, 5'd0, `FUNCT3_CSRRS, 5'd15, `OPC_SYSTEM); + + dut.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd30, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), // 0x04 + encode_i(32'sd92, 5'd0, 3'b000, 5'd30, `OPC_OP_IMM)}; // 0x00 + dut.sram0.memory[1] = {encode_csr(12'h7B1, 5'd0, `FUNCT3_CSRRS, 5'd2, `OPC_SYSTEM), // 0x0C: dpc + encode_csr(12'h7B0, 5'd0, `FUNCT3_CSRRS, 5'd1, `OPC_SYSTEM)}; // 0x08: dcsr + dut.sram0.memory[2] = {encode_csr(12'h7B3, 5'd0, `FUNCT3_CSRRS, 5'd4, `OPC_SYSTEM), // 0x14: dscratch1 + encode_csr(12'h7B2, 5'd0, `FUNCT3_CSRRS, 5'd3, `OPC_SYSTEM)}; // 0x10: dscratch0 + dut.sram0.memory[3] = {encode_csr(12'h7B0, 5'd6, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), // 0x1C: csrrw x0,dcsr,x6 + encode_i(32'sd85, 5'd0, 3'b000, 5'd6, `OPC_OP_IMM)}; // 0x18 + dut.sram0.memory[4] = {encode_csr(12'h7AF, 5'd0, `FUNCT3_CSRRS, 5'd7, `OPC_SYSTEM), // 0x24: 0x7AF (below) + encode_csr(`CSR_MSCRATCH, 5'd0, `FUNCT3_CSRRS, 5'd5, `OPC_SYSTEM)}; // 0x20: mscratch + dut.sram0.memory[5] = {encode_i(32'sd2047, 5'd0, 3'b000, 5'd11, `OPC_OP_IMM), // 0x2C + encode_csr(12'h7B4, 5'd0, `FUNCT3_CSRRS, 5'd8, `OPC_SYSTEM)}; // 0x28: 0x7B4 (above) + dut.sram0.memory[6] = {encode_csr(`CSR_MSTATUS, 5'd11, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), // 0x34 + encode_i(32'sd1, 5'd11, 3'b000, 5'd11, `OPC_OP_IMM)}; // 0x30 + dut.sram0.memory[7] = {encode_csr(`CSR_MEPC, 5'd12, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), // 0x3C + encode_i(32'sd68, 5'd0, 3'b000, 5'd12, `OPC_OP_IMM)}; // 0x38 + dut.sram0.memory[8] = {encode_csr(12'h7B0, 5'd0, `FUNCT3_CSRRS, 5'd13, `OPC_SYSTEM), // 0x44: dcsr, S-mode + `INSTR_HEX_MRET}; // 0x40 + dut.sram0.memory[9] = {encode_csr(`CSR_SEPC, 5'd14, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), // 0x4C + encode_i(32'sd84, 5'd0, 3'b000, 5'd14, `OPC_OP_IMM)}; // 0x48 + dut.sram0.memory[10] = {dpc_u_insn, // 0x54: dpc, U-mode + `INSTR_HEX_SRET}; // 0x50 + dut.sram0.memory[11] = {encode_csr(`CSR_MEPC, 5'd0, `FUNCT3_CSRRS, 5'd10, `OPC_SYSTEM), // 0x5C + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; // 0x58: ebreak + dut.sram0.memory[12] = {encode_csr(`CSR_MEPC, 5'd10, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), // 0x64 + encode_i(32'sd4, 5'd10, 3'b000, 5'd10, `OPC_OP_IMM)}; // 0x60 + dut.sram0.memory[13] = {`INSTR_HEX_MRET, // 0x6C + encode_i(32'sd1, 5'd28, 3'b000, 5'd28, `OPC_OP_IMM)}; // 0x68 + + @(posedge clk); #1; + rst = 0; + + wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "EBREAK trap never fired"); + + check("x1 never written -- dcsr M-mode read trapped", dut.core0.regfile0.gp_registers[1], 64'd0); + check("x2 never written -- dpc M-mode read trapped", dut.core0.regfile0.gp_registers[2], 64'd0); + check("x3 never written -- dscratch0 M-mode read trapped", dut.core0.regfile0.gp_registers[3], 64'd0); + check("x4 never written -- dscratch1 M-mode read trapped", dut.core0.regfile0.gp_registers[4], 64'd0); + check("x6 == 85 -- write payload register itself unaffected by the trapped csrrw", + dut.core0.regfile0.gp_registers[6], 64'd85); + check("x5 == 0 -- ordinary mscratch access did NOT trap (negative control)", + dut.core0.regfile0.gp_registers[5], 64'd0); + check("x7 == 0 -- 0x7AF (below the block) did NOT trap (boundary control)", + dut.core0.regfile0.gp_registers[7], 64'd0); + check("x8 == 0 -- 0x7B4 (above the block) did NOT trap (boundary control)", + dut.core0.regfile0.gp_registers[8], 64'd0); + check("x13 never written -- dcsr S-mode read trapped (dual-violation path)", + dut.core0.regfile0.gp_registers[13], 64'd0); + check("x15 never written -- dpc U-mode read trapped (dual-violation path)", + dut.core0.regfile0.gp_registers[15], 64'd0); + check("trap_count == 7 -- 4 M-reads + 1 M-write + 1 S-read + 1 U-read, nothing else", + dut.core0.regfile0.gp_registers[28], 64'd7); + check("mcause == 2 (illegal instruction) for the last (U-mode dpc) trap", + mcause_snap, 64'd2); + check("mtval == the U-mode dpc csrrs's own raw 32-bit encoding", + mtval_snap, {32'b0, dpc_u_insn}); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); + + $display(""); + $display("core_debug_csr_violation_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("core_debug_csr_violation_tb: FAILURES PRESENT"); + $finish; + end + +endmodule + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/testbench/core_ebreak_trap_tb.sv b/testbench/core_ebreak_trap_tb.sv new file mode 100644 index 0000000..0bfdb2e --- /dev/null +++ b/testbench/core_ebreak_trap_tb.sv @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +`include "riscv_encode.sv" + + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: EBREAK genuinely trapping through the real core (cause 3, + * Breakpoint) -- the direct proof for the milestone that made EBREAK a + * real spec-compliant trap instead of a permanent simulation-only halt + * (see design/core.sv's own comment at the rvfi_intr/pc-register site). + * + * Two independent scenarios, each via its own core_wb4_sram_harness + * instance, mirroring core_priv_tb.sv's own multi-phase style but kept in + * ONE file since both are squarely "EBREAK's own trap behavior," not + * separate concerns: + * + * 1. M-mode EBREAK: mtvec armed, no delegation. Proves mcause==3, mepc== + * the ebreak's own address, mtval==0 (this cause's own decision -- + * see core.sv's trap_val mux, which deliberately has no explicit + * EBREAK arm and falls through to the default 0), PC genuinely lands + * at mtvec (not a freeze), a marker inside the handler actually + * executes (real forward progress -- categorically untestable before + * this milestone), and a SECOND ebreak (the handler's own completion + * signal) is correctly observed by this testbench's new sticky-latch + * mechanism -- the first real consumer/proof of testbench/ + * halt_wait.sv's post-milestone contract (see its own header comment). + * 2. S-mode EBREAK, delegated via medeleg bit 3: proves EBREAK is a real, + * ordinary exception as far as delegation is concerned -- scause==3, + * sepc==the ebreak's own address, current_priv stays S (never routed + * to M), exercising trap_to_s's own EBREAK arm for the first time. + * + * Both handlers read mepc/mcause (or sepc/scause) into GPRs via csrr, + * THEN set a marker, THEN hit their own completion ebreak -- deliberately + * NOT read live from csr_file0.*_q at check() time. mtvec/stvec stay + * armed through each scenario's own completion ebreak, which (now a real + * trap) bounces right back into the same handler and can keep executing + * indefinitely; GPR values captured by an earlier instruction are stable + * against that regardless of how many extra bounce-back passes happen + * afterward, live CSR reads are not -- the exact bug class this + * milestone's own testbench sweep found and fixed across five other + * files (core_priv_tb.sv, core_priv_toolchain_tb.sv, core_priv_u_ecall_tb.sv, + * core_c_illegal_trap_tb.sv, soc_interrupt_tb.sv, plus core_interrupt_tb.sv's + * own Case 6/etc. redesign) -- see any of their own comments for the full + * story of why a live post-halt CSR read is no longer safe here. + */ +module core_ebreak_trap_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + + /* ----------------------------------------------------------------- * + * Scenario 1: M-mode EBREAK trap + * ----------------------------------------------------------------- */ + core_wb4_sram_harness #(.NUM_WORDS(32)) dut_m (.clk(clk), .rst(rst)); + + /* + * GPR snapshot, taken on the SAME edge as halted_m's own latch -- + * NOT read live at check() time. mtvec stays armed at HANDLER through + * the completion ebreak (0x20), which (a real trap now) bounces right + * back into HANDLER and keeps re-executing it for as long as this + * testbench's sequential wait_all block is still waiting on the OTHER + * dut_s's own completion (dut_m keeps running independently in the + * meantime) -- each extra pass re-executes csrrs x10,mepc with a NEW + * (now-wrong) mepc value, silently corrupting x10 even though + * halted_m itself correctly latched on the first, real completion + * event. Gated on !halted_m so only the first pass is ever captured. + */ + logic halted_m = 1'b0; + logic [63:0] m_x1_snap, m_x10_snap, m_x11_snap, m_x12_snap, m_x13_snap; + always @(posedge clk) if (!halted_m && dut_m.core0.trap_taken && dut_m.core0.is_ebreak + && dut_m.core0.pc == 64'h20) begin + halted_m <= 1'b1; + m_x1_snap <= dut_m.core0.regfile0.gp_registers[1]; + m_x10_snap <= dut_m.core0.regfile0.gp_registers[10]; + m_x11_snap <= dut_m.core0.regfile0.gp_registers[11]; + m_x12_snap <= dut_m.core0.regfile0.gp_registers[12]; + m_x13_snap <= dut_m.core0.regfile0.gp_registers[13]; + end + + /* ----------------------------------------------------------------- * + * Scenario 2: S-mode EBREAK, delegated via medeleg bit 3 + * ----------------------------------------------------------------- */ + core_wb4_sram_harness #(.NUM_WORDS(32)) dut_s (.clk(clk), .rst(rst)); + + // Same reasoning as dut_m's own snapshot above. + logic halted_s = 1'b0; + logic [63:0] s_x1_snap, s_x20_snap, s_x21_snap, s_x22_snap; + logic [1:0] s_priv_snap; + always @(posedge clk) if (!halted_s && dut_s.core0.trap_taken && dut_s.core0.is_ebreak + && dut_s.core0.pc == 64'h6C) begin + halted_s <= 1'b1; + s_x1_snap <= dut_s.core0.regfile0.gp_registers[1]; + s_x20_snap <= dut_s.core0.regfile0.gp_registers[20]; + s_x21_snap <= dut_s.core0.regfile0.gp_registers[21]; + s_x22_snap <= dut_s.core0.regfile0.gp_registers[22]; + s_priv_snap <= dut_s.core0.current_priv; + end + + initial begin + #1; // run after every wb4_sram sub-instance's own time-0 crt0.hex init + + /* + * ---- Scenario 1 program (M-mode), addr 0x00-0x23 ---- + * addr instr notes + * 0x00 addi x28, x0, 0x10 (16) x28 = HANDLER address + * 0x04 csrrw x0, mtvec, x28 + * 0x08 ebreak TRIGGER -- real trap now + * 0x0C addi x1, x0, 999 poison -- must NEVER run + * (EBREAK doesn't fall + * through to the next pc) + * -- HANDLER (0x10) -- + * 0x10 csrrs x10, mepc, x0 + * 0x14 csrrs x11, mcause, x0 + * 0x18 csrrs x12, mtval, x0 + * 0x1C addi x13, x0, 1 marker: real forward progress + * 0x20 ebreak completion signal (2nd ebreak) + */ + dut_m.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd16, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_m.sram0.memory[1] = {encode_i(32'sd999, 5'd0, 3'b000, 5'd1, `OPC_OP_IMM), + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; // idx 0x08: ebreak + dut_m.sram0.memory[2] = {encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd11, `OPC_SYSTEM), + encode_csr(`CSR_MEPC, 5'd0, `FUNCT3_CSRRS, 5'd10, `OPC_SYSTEM)}; + dut_m.sram0.memory[3] = {encode_i(32'sd1, 5'd0, 3'b000, 5'd13, `OPC_OP_IMM), + encode_csr(`CSR_MTVAL, 5'd0, `FUNCT3_CSRRS, 5'd12, `OPC_SYSTEM)}; + dut_m.sram0.memory[4] = {32'h0, + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; // idx 0x20: ebreak (completion) + + /* + * ---- Scenario 2 program (S-mode, delegated), addr 0x00-0x7B ---- + * addr instr notes + * 0x00 addi x28, x0, 0x78 (120) x28 = M_HANDLER addr (safety net) + * 0x04 csrrw x0, mtvec, x28 + * 0x08 addi x28, x0, 0x60 (96) x28 = S_HANDLER addr + * 0x0C csrrw x0, stvec, x28 + * 0x10 addi x28, x0, 8 medeleg bit 3 (EBREAK) + * 0x14 csrrw x0, medeleg, x28 + * 0x18 addi x28, x0, 1 + * 0x1C slli x28, x28, 11 x28 = mstatus.MPP field = S + * 0x20 csrrw x0, mstatus, x28 + * 0x24 addi x28, x0, 0x58 (88) x28 = addr of TRIGGER2 + * 0x28 csrrw x0, mepc, x28 + * 0x2C mret current_priv <- S, jump to 0x58 + * 0x58 ebreak TRIGGER2 -- delegated, real trap + * 0x5C addi x1, x0, 888 poison2 -- must NEVER run + * -- S_HANDLER (0x60) -- + * 0x60 csrrs x20, sepc, x0 + * 0x64 csrrs x21, scause, x0 + * 0x68 addi x22, x0, 1 marker2: real forward progress in S + * 0x6C ebreak completion2 signal + * -- M_HANDLER (0x78), safety net only -- reached iff delegation is broken -- + * 0x78 ebreak + */ + dut_s.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd120, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_s.sram0.memory[1] = {encode_csr(`CSR_STVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd96, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_s.sram0.memory[2] = {encode_csr(`CSR_MEDELEG, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd8, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_s.sram0.memory[3] = {encode_shift64(6'b000000, 6'd11, 5'd28, 3'b001, 5'd28, `OPC_OP_IMM), + encode_i(32'sd1, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_s.sram0.memory[4] = {encode_i(32'sd88, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM), + encode_csr(`CSR_MSTATUS, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM)}; + dut_s.sram0.memory[5] = {`INSTR_HEX_MRET, + encode_csr(`CSR_MEPC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM)}; + dut_s.sram0.memory[11] = {encode_i(32'sd888, 5'd0, 3'b000, 5'd1, `OPC_OP_IMM), + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; // idx 0x58: ebreak (TRIGGER2) + dut_s.sram0.memory[12] = {encode_csr(`CSR_SCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd21, `OPC_SYSTEM), + encode_csr(`CSR_SEPC, 5'd0, `FUNCT3_CSRRS, 5'd20, `OPC_SYSTEM)}; + dut_s.sram0.memory[13] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, // idx 0x6C: ebreak (completion2) + encode_i(32'sd1, 5'd0, 3'b000, 5'd22, `OPC_OP_IMM)}; + dut_s.sram0.memory[15] = {32'h0, + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; // idx 0x78: ebreak (M_HANDLER safety net) + + @(posedge clk); #1; + rst = 0; + + fork + begin : wait_all + wait (halted_m === 1'b1); + wait (halted_s === 1'b1); + end + begin : timeout + // 300 cycles: matches halt_wait.sv's own TIMEOUT_CYCLES_SMALL + // tier (not `include`d here -- its own wait_halted_or_timeout + // task assumes a single signal literally named `halted`, + // which doesn't fit this file's two-DUT shape). + repeat (300) @(posedge clk); + $display("TIMEOUT: not every DUT halted"); + $finish; + end + join_any + #1; + + /* ---- Scenario 1 checks (M-mode) ---- */ + check("M: mepc == the ebreak's own address (0x08)", m_x10_snap, 64'h08); + check("M: mcause == 3 (Breakpoint)", m_x11_snap, 64'd3); + check("M: mtval == 0", m_x12_snap, 64'd0); + check("M: poison marker never ran -- EBREAK does not fall through (x1==0)", m_x1_snap, 64'd0); + check("M: handler ran -- real forward progress after the trap (x13==1)", m_x13_snap, 64'd1); + check("M: second (completion) ebreak observed by the new sticky-latch contract", {63'b0, halted_m}, 64'd1); + + /* ---- Scenario 2 checks (S-mode, delegated) ---- */ + check("S: sepc == TRIGGER2's own address (0x58)", s_x20_snap, 64'h58); + check("S: scause == 3 (Breakpoint, delegated -- not mcause)", s_x21_snap, 64'd3); + check("S: poison2 marker never ran (x1==0)", s_x1_snap, 64'd0); + check("S: handler ran -- real forward progress in S-mode (x22==1)", s_x22_snap, 64'd1); + check("S: current_priv stayed S (never routed to M)", {62'b0, s_priv_snap}, 64'(2'b01)); + check("S: second (completion) ebreak observed", {63'b0, halted_s}, 64'd1); + + $display(""); + $display("core_ebreak_trap_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("core_ebreak_trap_tb: FAILURES PRESENT"); + $finish; + end + +endmodule + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/testbench/core_fence_i_tb.sv b/testbench/core_fence_i_tb.sv new file mode 100644 index 0000000..1cd0d12 --- /dev/null +++ b/testbench/core_fence_i_tb.sv @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +`include "riscv_encode.sv" + + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: core, Zifencei (FENCE.I) -- a plain retire/no-op check, and + * the real proof: a genuine self-modifying-code sequence where skipping + * FENCE.I would execute stale I$ content, closing the gap soc.sv's own + * header used to document as a known, deliberate limitation. + * + * Uses core_cache_harness.sv (the real core -> cache_complex -> icache0/ + * dcache0 -> wb4_sram path, with icache_flush_o correctly wired to + * cache0.flush_i) -- a harness without the real cache in the loop would + * prove nothing here, since the whole point is that a D$ store doesn't + * automatically invalidate a stale I$ line. + * + * Test 2's construction, in order: + * 1. `jal` to TARGET once -- TARGET's first instruction (`addi x5,x0, + * 111`) fetches into I$ for the first time (a cold miss, installing + * the OLD bytes into that line). + * 2. A D$ `sw` overwrites TARGET's first instruction with a different + * encoding (`addi x5,x0,222`) -- SRAM now holds the NEW bytes, but + * the I$ line from step 1 is untouched (write-through keeps D$'s + * OWN cached copy in lockstep with SRAM, but I$ has no idea D$ just + * wrote to an address it has cached). + * 3. `fence.i` -- must invalidate the I$ line covering TARGET. + * 4. `jal` to TARGET again -- must now be a genuine miss that re-fetches + * the NEW bytes from SRAM, executing `addi x5,x0,222`, not a stale + * hit re-serving the OLD `addi x5,x0,111` from step 1. + * x5's final value (222, not 111) is therefore the direct proof: it can + * only be 222 if the second call's fetch actually saw the new bytes. + */ +module core_fence_i_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + core_cache_harness dut (.clk(clk), .rst(rst)); + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; + `include "halt_wait.sv" + + localparam int unsigned TARGET_ADDR = 32'h100; // line-aligned (default line size 32B) + + /* + * The new instruction's raw encoding, and its LUI+ADDI split -- the + * standard "li pseudo-op" construction (ADDI's 12-bit immediate is + * sign-extended, so if its own bit 11 is set, the LUI half must be + * incremented by 1 to compensate). Computed here as plain + * SystemVerilog expressions, not hand-derived hex, so there's no + * hand-arithmetic step that could be silently wrong -- the same + * lesson this session's earlier hand-packed-word mistakes already + * paid for once. + */ + logic [31:0] new_instr; + logic [19:0] new_instr_hi20; + logic [11:0] new_instr_lo12; + + logic [31:0] main_prog[0:19]; + logic [31:0] target_prog[0:1]; + int i; + + initial begin + #1; + + new_instr = encode_i(32'sd222, 5'd0, 3'b000, 5'd5, `OPC_OP_IMM); + new_instr_lo12 = new_instr[11:0]; + new_instr_hi20 = new_instr[31:12] + (new_instr_lo12[11] ? 20'd1 : 20'd0); + + /* + * ---- Test 1: FENCE.I retire/no-op, addr 0x00-0x0B ---- + * addr instr notes + * 0x00 addi x3, x0, 999 sentinel -- FENCE.I must not touch it + * 0x04 fence.i must retire cleanly: no trap, no reg write + * 0x08 addi x4, x0, 111 resume marker -- proves clean retirement + */ + main_prog[0] = encode_i(32'sd999, 5'd0, 3'b000, 5'd3, `OPC_OP_IMM); + main_prog[1] = {17'b0, 3'b001, 5'b0, 7'b0001111}; // fence.i + main_prog[2] = encode_i(32'sd111, 5'd0, 3'b000, 5'd4, `OPC_OP_IMM); + + /* + * ---- Test 2: self-modifying code, addr 0x0C-0x3B ---- + * addr instr notes + * 0x0C jal x1, TARGET call #1 -- populates I$ with OLD bytes + * 0x10 addi x6, x0, 1 "call #1 returned" marker + * 0x14 lui x9, new_instr_hi20 x9 <- new instruction encoding, part 1 + * 0x18 addi x9, x9, new_instr_lo12 x9 <- new instruction encoding, part 2 + * 0x1C addi x10, x0, TARGET_ADDR x10 <- TARGET's address (data pointer) + * 0x20 sw x9, 0(x10) D$ STORE overwrites TARGET's first instr + * 0x24 fence.i flush I$ + * 0x28 jal x1, TARGET call #2 -- must see NEW bytes (x1 again -- + * TARGET's own jalr always returns via x1) + * 0x2C addi x11, x0, 1 "call #2 returned" marker + * 0x30 ebreak + */ + main_prog[3] = encode_j(int'(TARGET_ADDR) - 32'h0C, 5'd1, `OPC_JAL); + main_prog[4] = encode_i(32'sd1, 5'd0, 3'b000, 5'd6, `OPC_OP_IMM); + main_prog[5] = encode_u(new_instr_hi20, 5'd9, `OPC_LUI); + main_prog[6] = encode_i(int'($signed(new_instr_lo12)), 5'd9, 3'b000, 5'd9, `OPC_OP_IMM); + main_prog[7] = encode_i(int'(TARGET_ADDR), 5'd0, 3'b000, 5'd10, `OPC_OP_IMM); + main_prog[8] = encode_s(32'sd0, 5'd9, 5'd10, 3'b010, `OPC_STORE); // sw x9,0(x10) + main_prog[9] = {17'b0, 3'b001, 5'b0, 7'b0001111}; // fence.i + main_prog[10] = encode_j(int'(TARGET_ADDR) - 32'h28, 5'd1, `OPC_JAL); // x1 -- TARGET's own + // jalr always returns via x1 (hardcoded there), so BOTH calls must link into x1, not a + // distinct register per call -- using x2 here originally sent the second call back to + // the FIRST call's stale return address instead, an authoring bug caught by exactly the + // kind of PC trace this session's earlier hand-packed-word mistakes were also caught by. + main_prog[11] = encode_i(32'sd1, 5'd0, 3'b000, 5'd11, `OPC_OP_IMM); + main_prog[12] = encode_i(32'sd1, 5'd0, 3'b000, 5'd0, `OPC_SYSTEM); // ebreak + + for (i = 0; i < 6; i = i + 1) + dut.sram0.memory[i] = {main_prog[2*i+1], main_prog[2*i]}; + dut.sram0.memory[6][31:0] = main_prog[12]; // 0x30: ebreak, alone in its word's low half + + /* + * addr instr notes + * 0x100 addi x5, x0, 111 OLD instruction -- will be overwritten + * 0x104 jalr x0, x1, 0 return via the caller's own link register + */ + target_prog[0] = encode_i(32'sd111, 5'd0, 3'b000, 5'd5, `OPC_OP_IMM); + target_prog[1] = encode_i(32'sd0, 5'd1, 3'b000, 5'd0, `OPC_JALR); + + dut.sram0.memory[int'(TARGET_ADDR) >> 3] = {target_prog[1], target_prog[0]}; + + @(posedge clk); #1; + rst = 0; + + wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "EBREAK trap never fired"); + + check("FENCE.I: sentinel untouched", dut.core0.regfile0.gp_registers[3], 64'd999); + check("FENCE.I: resumed cleanly (no trap, no hang)", dut.core0.regfile0.gp_registers[4], 64'd111); + + check("self-modifying code: call #1 returned", dut.core0.regfile0.gp_registers[6], 64'd1); + check("self-modifying code: call #2 returned", dut.core0.regfile0.gp_registers[11], 64'd1); + check({"self-modifying code: second call executed the NEW instruction (222), ", + "not a stale I$ hit serving the OLD one (111) -- the actual proof FENCE.I closes the gap"}, + dut.core0.regfile0.gp_registers[5], 64'd222); + + check("EBREAK trap fired", {63'b0, halted}, 64'd1); + + $display(""); + $display("core_fence_i_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("core_fence_i_tb: FAILURES PRESENT"); + $finish; + end + +endmodule + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/testbench/core_interrupt_icache_miss_tb.sv b/testbench/core_interrupt_icache_miss_tb.sv new file mode 100644 index 0000000..65adf60 --- /dev/null +++ b/testbench/core_interrupt_icache_miss_tb.sv @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +`include "defaults/defaults.sv" +`include "riscv_encode.sv" + + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: core's interrupt-taking logic against the REAL cache fabric + * (design/icache.sv/design/dcache.sv via design/cache_complex.sv, reached + * through core_cache_harness.sv) -- the one case core_interrupt_tb.sv's + * plain wb4_sram-only harness structurally cannot exercise: + * fetch_redirect_q staying high across a genuine multi-cycle I$ refill of + * the trap-vector fetch itself. + * + * mtvec is pointed at 0x1000 -- a dword the small main-flow program never + * touches (it lives entirely under 0x20), and (since the I$ starts fully + * invalid after reset) the very first fetch of it is unconditionally a + * genuine miss, no address-aliasing games required. Loaded via LUI (`lui + * x28,1` -> x28 = 0x1000) rather than ADDI -- 0x1000 doesn't fit ADDI's + * signed 12-bit immediate (same class of bug this file's sibling + * core_interrupt_tb.sv had to route around for mstatus.MPP). + * + * Same deferred-i_mtip technique as core_interrupt_tb.sv: mie.MTIE/ + * mstatus.MIE enabled during setup (i_mtip=0, so nothing fires + * prematurely), i_mtip asserts on the TRIGGER instruction's own commit + * edge. fetch_redirect_q is sampled directly (white-box -- no + * architectural way to observe "how many cycles did the redirected + * fetch actually take") across the whole run; a genuine 4-beat refill + * (line_words=4, core_cache_harness's default) must hold it high for + * more than a couple of cycles. + * + * IMPORTANT, review-confirmed caveat: this does NOT currently prove + * fetch_redirect_q is load-bearing (mutating it away -- collapsing + * fetch_from_trap_vector to just interrupt_taken -- still passes this + * test unchanged, confirmed empirically). That's expected, not a bug: + * fetch_paddr is today a pure pc passthrough (core.sv), and pc itself is + * updated to trap_vector the cycle after interrupt_taken fires, so + * fetch_addr alone already stays correct through the whole refill with + * no help from fetch_redirect_q. fetch_redirect_q exists anyway as + * deliberate, forward-compatible scaffolding for the future Sv39 stage + * (once fetch_paddr becomes a real, possibly multi-cycle translation, + * pc alone will no longer trivially "keep up") -- see the plan's own + * FSM-timing-proof section. This test still legitimately proves + * fetch_redirect_q's own *behavior* is correct (stays high for the + * whole refill, clears at the right edge) even though it can't yet + * prove *necessity*; read the check below with that in mind. + */ +module core_interrupt_icache_miss_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + logic i_mtip = 1'b0; + + core_cache_harness #(.NUM_WORDS(4096)) dut (.clk(clk), .rst(rst), .i_mtip(i_mtip)); + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + + localparam int TIMEOUT_CYCLES = 500; + localparam logic [63:0] TRIGGER_PC = 64'h18; + localparam logic [63:0] COLD_ADDR = 64'h1000; + + always @(posedge clk) begin + if (dut.core0.commit_now && dut.core0.pc == TRIGGER_PC) i_mtip <= 1'b1; + end + + // White-box: fetch_redirect_q must stay high across a genuine + // multi-cycle refill, not just blip for one cycle. + int fetch_redirect_high_cycles = 0; + always @(posedge clk) begin + if (dut.core0.fetch_redirect_q) fetch_redirect_high_cycles <= fetch_redirect_high_cycles + 1; + end + + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; + + initial begin + #1; // run after wb4_sram's own time-0 crt0.hex init + + dut.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_u(20'd1, 5'd28, `OPC_LUI)}; // lui x28,1 -> x28=0x1000 (0x00) + dut.sram0.memory[1] = {encode_csr(`CSR_MIE, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; // addi x28,x0,128 (0x08) + dut.sram0.memory[2] = {encode_csr(`CSR_MSTATUS, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd8, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; // addi x28,x0,8 (0x10) + dut.sram0.memory[3] = {encode_i(32'sd999, 5'd0, 3'b000, 5'd2, `OPC_OP_IMM), // addi x2,x0,999 (poison, 0x1C) + encode_i(32'h77, 5'd0, 3'b000, 5'd1, `OPC_OP_IMM)}; // addi x1,x0,0x77 (TRIGGER, 0x18) + dut.sram0.memory[4] = {32'h0, + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; // ebreak (0x20, safety net) + + // Handler at COLD_ADDR (0x1000) -- word index 0x1000/8 = 512. + dut.sram0.memory[512] = {encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd11, `OPC_SYSTEM), // csrr x11,mcause (0x1004) + encode_csr(`CSR_MEPC, 5'd0, `FUNCT3_CSRRS, 5'd10, `OPC_SYSTEM)}; // csrr x10,mepc (0x1000) + dut.sram0.memory[513] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, // ebreak (0x100C) + encode_i(32'sd1, 5'd0, 3'b000, 5'd12, `OPC_OP_IMM)}; // addi x12,x0,1 (0x1008) + + @(posedge clk); #1; + rst = 0; + + fork + wait (halted === 1'b1); + begin + repeat (TIMEOUT_CYCLES) @(posedge clk); + $display("TIMEOUT: EBREAK trap never fired"); + $finish; + end + join_any + #1; + + check("poison marker never ran (x2==0)", dut.core0.regfile0.gp_registers[2], 64'd0); + check("mepc == TRIGGER+4 (0x1C)", dut.core0.regfile0.gp_registers[10], 64'h1C); + check("mcause == standard machine-timer-interrupt encoding", dut.core0.regfile0.gp_registers[11], 64'h8000_0000_0000_0007); + check("handler ran (x12==1)", dut.core0.regfile0.gp_registers[12], 64'd1); + check("current_priv == M", {62'b0, dut.core0.current_priv}, 64'(2'b11)); + // NOT a necessity proof for fetch_redirect_q -- see this file's header. Confirms + // its own behavior (stays high the whole refill) is correct, not that removing + // it would break anything today. + check("fetch_redirect_q held across a genuine multi-cycle I$ refill (>2 cycles)", {63'b0, (fetch_redirect_high_cycles > 2)}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); + + $display(""); + $display("core_interrupt_icache_miss_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("core_interrupt_icache_miss_tb: FAILURES PRESENT"); + $finish; + end + +endmodule diff --git a/testbench/core_interrupt_tb.sv b/testbench/core_interrupt_tb.sv new file mode 100644 index 0000000..4f459b1 --- /dev/null +++ b/testbench/core_interrupt_tb.sv @@ -0,0 +1,683 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +`include "defaults/defaults.sv" +`include "riscv_encode.sv" + + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: core's machine-timer-interrupt-taking logic (Milestone 6), + * hand-packed instructions via core_wb4_sram_harness -- same idiom as + * core_priv_tb.sv, extended with `i_mtip` passthrough (see that harness's + * own header for why a real port was added instead of a hierarchical + * force/release). + * + * Nine independent DUT instances, one per required case -- NOT nine + * phases of one continuous program. Every case needs its own precise + * relationship between "when mie.MTIE/mstatus.MIE become enabled" and + * "when i_mtip actually asserts", and since i_mtip is driven directly by + * this testbench (not by any instruction the DUT executes), the two are + * fully decoupled -- letting the SAME enable state serve two different + * cases (e.g. masked vs. not) would require re-arming CSRs between + * phases anyway, so nine small, independently-reasoned-about programs are + * simpler and safer to get right than one long choreographed one. + * + * Common technique used by every "trigger"-shaped case (mepc x4, S-mode + * routing): i_mtip starts at 0 (so mie/mstatus can be safely enabled + * during setup with zero risk of a premature redirect), then a small + * `always @(posedge clk)` block asserts i_mtip the instant + * `core0.commit_now && core0.pc == TRIGGER_PC` -- i.e. on the exact edge + * the chosen trigger instruction retires. Because that assignment is + * non-blocking, i_mtip reads 1 starting the very next cycle -- exactly + * the cycle interrupt_taken's own deferred check runs (see core.sv's + * Interrupts section) -- with zero risk of firing one instruction early + * (during setup) or one instruction late. Every case places a "poison" + * marker instruction at the address the redirect must preempt, and every + * case's handler reads the architectural mepc/mcause CSRs back into a + * scratch register so the check below stays black-box (reading DUT + * *architectural* state through the ISA, not just internal FSM wires) -- + * the DIV case additionally needs one small white-box monitor (see its + * own comment) since "held off during a multi-cycle stall" has no + * purely-architectural observable. + * + * mie.MTIE = bit 7 (128), mstatus.MIE = bit 3 (8), mstatus.MPIE = bit 7 + * (128 -- same numeric value as mie's MTIE bit, different CSR, no + * relation), mstatus.MPP = bits[12:11] (2048 = S), mstatus.SIE = bit 1 (2). + * + * Case 5's own mtval_q check and Case 7 (mideleg[7]=1 delegation) both + * close a real, review-confirmed gap: nothing originally exercised + * either i_trap_val's mux-to-0-on-a-pure-interrupt (core.sv's resolved + * "trap_val for a pure interrupt" design decision) or mti_to_s/ + * interrupt_to_s's =1 branch at all (mideleg was never written by any + * test), so a real bug in either -- confirmed via mutation during + * review, e.g. swapping mstatus_sie_w for mstatus_mie_w in mti_enabled's + * delegated branch -- would have shipped completely undetected. + */ +module core_interrupt_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + + localparam int TIMEOUT_CYCLES = 500; + + /* ----------------------------------------------------------------- * + * Case 1: pending but masked via mie.MTIE=0 (mstatus.MIE=1, i_mtip=1 + * throughout) -- no redirect; program runs straight to its own + * EBREAK, the "would-be handler" (at mtvec) never runs. + * ----------------------------------------------------------------- */ + logic i_mtip_mask_mie = 1'b1; + core_wb4_sram_harness #(.NUM_WORDS(64)) dut_mask_mie (.clk(clk), .rst(rst), .i_mtip(i_mtip_mask_mie)); + // Completion keyed on the program's own marker write (pc==0x10), NOT on + // its trailing ebreak: since EBREAK now really traps, that ebreak would + // bounce into mtvec (armed at 0x80, needed for the OTHER cases' real + // interrupt redirects) and corrupt/loop -- see this milestone's own + // core.sv changes and Case 6's own comment below for the full story. + // A broken mask (interrupt fires anyway) means pc==0x10 is never + // reached at all -- caught by the shared timeout below, not a hang. + logic mask_mie_halted = 1'b0; + always @(posedge clk) if (dut_mask_mie.core0.commit_now && dut_mask_mie.core0.pc == 64'h10) mask_mie_halted <= 1'b1; + + /* ----------------------------------------------------------------- * + * Case 2: pending but masked via mstatus.MIE=0 (mie.MTIE=1, i_mtip=1 + * throughout) -- same shape, opposite mask bit. + * ----------------------------------------------------------------- */ + logic i_mtip_mask_mstatus = 1'b1; + core_wb4_sram_harness #(.NUM_WORDS(64)) dut_mask_mstatus (.clk(clk), .rst(rst), .i_mtip(i_mtip_mask_mstatus)); + // Same reasoning as mask_mie_halted above -- key on the marker write + // (pc==0x10), not the trailing ebreak. + logic mask_mstatus_halted = 1'b0; + always @(posedge clk) if (dut_mask_mstatus.core0.commit_now && dut_mask_mstatus.core0.pc == 64'h10) mask_mstatus_halted <= 1'b1; + + /* ----------------------------------------------------------------- * + * Case 3: multi-cycle DIV. mie.MTIE/mstatus.MIE enabled during setup + * (i_mtip still 0, so nothing fires yet); i_mtip asserts the moment + * div_stall first goes high (genuinely mid-divide, not at its + * boundary) -- proves int_pending_and_enabled becoming true DURING + * the stall doesn't matter, since commit_now (hence commit_now_q, + * hence interrupt_taken) structurally can't fire until div_stall + * clears. div_committed/bad_early_interrupt is the one white-box + * monitor this file needs (no purely-architectural way to observe + * "interrupt_taken never asserted before this specific commit"). + * ----------------------------------------------------------------- */ + logic i_mtip_div = 1'b0; + core_wb4_sram_harness #(.NUM_WORDS(64)) dut_div (.clk(clk), .rst(rst), .i_mtip(i_mtip_div)); + // Completion keyed on the handler's own marker write (pc==0x84, `addi + // x11,1`), strictly before its own trailing ebreak at 0x88 -- see + // Case 6's own comment below for why an ebreak-based signal is wrong + // here now that EBREAK really traps (it would bounce back into mtvec + // and re-execute this same handler, corrupting mepc/mcause). + logic div_halted = 1'b0; + // Point-in-time snapshot of x10 (mepc, read by the handler's own csrr + // at pc==0x80, the instruction immediately before this marker) -- NOT + // a live read at check() time, which would race a runaway bounce-back + // loop through this same handler (see Case 6's own comment below for + // the full story). Captured on the SAME edge as the completion latch. + logic [63:0] div_x10_snap; + always @(posedge clk) if (!div_halted && dut_div.core0.commit_now && dut_div.core0.pc == 64'h84) begin + div_halted <= 1'b1; + div_x10_snap <= dut_div.core0.regfile0.gp_registers[10]; + end + always @(posedge clk) begin + if (dut_div.core0.div_stall) i_mtip_div <= 1'b1; + end + logic div_committed = 1'b0; + logic bad_early_interrupt = 1'b0; + always @(posedge clk) begin + if (dut_div.core0.commit_now && dut_div.core0.is_div_family) div_committed <= 1'b1; + if (!div_committed && dut_div.core0.interrupt_taken) bad_early_interrupt <= 1'b1; + end + + /* ----------------------------------------------------------------- * + * Case 4a: mepc correctness -- ordinary ALU op as the just-retired + * instruction. Expected mepc == TRIGGER_PC + 4 (pc_plus_len). + * ----------------------------------------------------------------- */ + logic i_mtip_mepc_alu = 1'b0; + core_wb4_sram_harness #(.NUM_WORDS(64)) dut_mepc_alu (.clk(clk), .rst(rst), .i_mtip(i_mtip_mepc_alu)); + // Keyed on the handler's own marker write (pc==0x84), not its trailing + // ebreak -- see Case 6's own comment below for why. + logic mepc_alu_halted = 1'b0; + // Point-in-time snapshot of x10 (mepc) -- see Case 6's own comment + // below for why a live read at check() time isn't safe. + logic [63:0] mepc_alu_x10_snap; + always @(posedge clk) if (!mepc_alu_halted && dut_mepc_alu.core0.commit_now && dut_mepc_alu.core0.pc == 64'h84) begin + mepc_alu_halted <= 1'b1; + mepc_alu_x10_snap <= dut_mepc_alu.core0.regfile0.gp_registers[10]; + end + always @(posedge clk) begin + if (dut_mepc_alu.core0.commit_now && dut_mepc_alu.core0.pc == 64'h18) i_mtip_mepc_alu <= 1'b1; + end + + /* ----------------------------------------------------------------- * + * Case 4b: mepc correctness -- JAL as the just-retired instruction. + * Expected mepc == the JAL's own jump target (pc_rel_target), not + * pc_plus_len. + * ----------------------------------------------------------------- */ + logic i_mtip_mepc_jal = 1'b0; + core_wb4_sram_harness #(.NUM_WORDS(64)) dut_mepc_jal (.clk(clk), .rst(rst), .i_mtip(i_mtip_mepc_jal)); + // Keyed on the handler's own marker write (pc==0x84), not its trailing + // ebreak -- see Case 6's own comment below for why. + logic mepc_jal_halted = 1'b0; + // Point-in-time snapshot of x10 (mepc) -- see Case 6's own comment + // below for why a live read at check() time isn't safe. + logic [63:0] mepc_jal_x10_snap; + always @(posedge clk) if (!mepc_jal_halted && dut_mepc_jal.core0.commit_now && dut_mepc_jal.core0.pc == 64'h84) begin + mepc_jal_halted <= 1'b1; + mepc_jal_x10_snap <= dut_mepc_jal.core0.regfile0.gp_registers[10]; + end + always @(posedge clk) begin + if (dut_mepc_jal.core0.commit_now && dut_mepc_jal.core0.pc == 64'h18) i_mtip_mepc_jal <= 1'b1; + end + + /* ----------------------------------------------------------------- * + * Case 4c: mepc correctness -- taken branch (BEQ x0,x0, always true) + * as the just-retired instruction. Same shape as JAL: mepc == + * pc_rel_target. + * ----------------------------------------------------------------- */ + logic i_mtip_mepc_branch = 1'b0; + core_wb4_sram_harness #(.NUM_WORDS(64)) dut_mepc_branch (.clk(clk), .rst(rst), .i_mtip(i_mtip_mepc_branch)); + // Keyed on the handler's own marker write (pc==0x84), not its trailing + // ebreak -- see Case 6's own comment below for why. + logic mepc_branch_halted = 1'b0; + // Point-in-time snapshot of x10 (mepc) -- see Case 6's own comment + // below for why a live read at check() time isn't safe. + logic [63:0] mepc_branch_x10_snap; + always @(posedge clk) if (!mepc_branch_halted && dut_mepc_branch.core0.commit_now && dut_mepc_branch.core0.pc == 64'h84) begin + mepc_branch_halted <= 1'b1; + mepc_branch_x10_snap <= dut_mepc_branch.core0.regfile0.gp_registers[10]; + end + always @(posedge clk) begin + if (dut_mepc_branch.core0.commit_now && dut_mepc_branch.core0.pc == 64'h18) i_mtip_mepc_branch <= 1'b1; + end + + /* ----------------------------------------------------------------- * + * Case 4d: mepc correctness -- MRET as the just-retired instruction. + * THE regression test for the systematic bug the whole one-cycle- + * deferred design exists to avoid: mstatus is pre-armed MPIE=1/MIE=0 + * (so MIE only becomes visible AS MRET's own commit restores it -- + * exactly the "every MRET would fail to let an already-pending + * interrupt preempt the very first instruction it returns to" defect + * the deferred design's own header describes), mepc is pre-armed to + * point at a poison marker, and i_mtip asserts on MRET's own commit + * edge. The poison marker at MRET's return address must NEVER + * execute (checked via its own distinctive register value staying + * 0) -- proving genuine preemption, not "eventually taken". + * ----------------------------------------------------------------- */ + logic i_mtip_mepc_mret = 1'b0; + core_wb4_sram_harness #(.NUM_WORDS(64)) dut_mepc_mret (.clk(clk), .rst(rst), .i_mtip(i_mtip_mepc_mret)); + // Keyed on the handler's own marker write (pc==0x88, `addi x12,1`), + // strictly before its own trailing ebreak at 0x8C -- see Case 6's own + // comment below for why. + logic mepc_mret_halted = 1'b0; + // Point-in-time snapshot of x10 (mepc) and x11 (mcause) -- see Case 6's + // own comment below for why a live read at check() time isn't safe. + logic [63:0] mepc_mret_x10_snap, mepc_mret_x11_snap; + always @(posedge clk) if (!mepc_mret_halted && dut_mepc_mret.core0.commit_now && dut_mepc_mret.core0.pc == 64'h88) begin + mepc_mret_halted <= 1'b1; + mepc_mret_x10_snap <= dut_mepc_mret.core0.regfile0.gp_registers[10]; + mepc_mret_x11_snap <= dut_mepc_mret.core0.regfile0.gp_registers[11]; + end + always @(posedge clk) begin + if (dut_mepc_mret.core0.commit_now && dut_mepc_mret.core0.pc == 64'h20) i_mtip_mepc_mret <= 1'b1; + end + + /* ----------------------------------------------------------------- * + * Case 5: interrupt taken from S-mode with mideleg[7]=0 (left at its + * post-reset default -- never written) routes to M (mcause, not + * scause; current_priv ends M), confirming mti_to_s's real + * (not-hardwired-0) handling on the common, undelegated path. A + * marker retires genuinely in S-mode first (mideleg bit 7 has no + * bearing on WHEN the interrupt becomes visible -- an undelegated + * higher-privilege interrupt is unconditionally enabled below M + * regardless of the lower level's own mstatus bits, see + * core.sv's mti_enabled), then i_mtip asserts on that marker's own + * commit edge, same technique as every mepc case above. + * ----------------------------------------------------------------- */ + logic i_mtip_smode = 1'b0; + core_wb4_sram_harness #(.NUM_WORDS(64)) dut_smode (.clk(clk), .rst(rst), .i_mtip(i_mtip_smode)); + // Keyed on the handler's own marker write (pc==0x84), not its trailing + // ebreak -- see Case 6's own comment below for why. + logic smode_halted = 1'b0; + // Point-in-time snapshot of x10 (mcause) -- see Case 6's own comment + // below for why a live read at check() time isn't safe. + logic [63:0] smode_x10_snap; + always @(posedge clk) if (!smode_halted && dut_smode.core0.commit_now && dut_smode.core0.pc == 64'h84) begin + smode_halted <= 1'b1; + smode_x10_snap <= dut_smode.core0.regfile0.gp_registers[10]; + end + always @(posedge clk) begin + if (dut_smode.core0.commit_now && dut_smode.core0.pc == 64'h28) i_mtip_smode <= 1'b1; + end + + /* ----------------------------------------------------------------- * + * Case 7: interrupt taken from S-mode with mideleg[7]=1 (delegated), + * mstatus.SIE=1 -- routes to S (scause/sepc, not mcause/mepc; + * current_priv stays/becomes S), exercising interrupt_to_s's =1 + * branch and mti_enabled's mti_to_s-delegated arm for the first time + * anywhere in this milestone. mstatus's S-side fields (SIE/SPIE/SPP) + * are untouched by MRET (confirmed by csr_file.sv's own mstatus_q + * writer), so SIE must be armed BEFORE the MRET that enters S-mode, + * in the same write as MPP=S (a plain CSR write fully replaces every + * writable bit, not a merge -- same discipline core_priv_tb.sv/ + * priv_test.s already established for this exact field). + * ----------------------------------------------------------------- */ + logic i_mtip_smode_deleg = 1'b0; + core_wb4_sram_harness #(.NUM_WORDS(64)) dut_smode_deleg (.clk(clk), .rst(rst), .i_mtip(i_mtip_smode_deleg)); + // Keyed on the S-mode handler's own marker write (pc==0xC8), not its + // trailing ebreak -- see Case 6's own comment below for why. + logic smode_deleg_halted = 1'b0; + // Point-in-time snapshots -- see Case 6's own comment below for why a + // live read at check() time isn't safe. EBREAK isn't delegated here + // (medeleg is never programmed in this case, only mideleg), so this + // handler's own trailing ebreak bounces to the UNWRITTEN mtvec=0x80 + // window once it fires -- snapshot everything the checks below read, + // not just the obviously-CSR-sourced values, since what exactly that + // stray fetch decodes to isn't worth relying on. + logic [63:0] smode_deleg_x13_snap, smode_deleg_x14_snap; + logic [1:0] smode_deleg_priv_snap; + logic [63:0] smode_deleg_mcauseq_snap, smode_deleg_stvalq_snap; + always @(posedge clk) if (!smode_deleg_halted && dut_smode_deleg.core0.commit_now && dut_smode_deleg.core0.pc == 64'hC8) begin + smode_deleg_halted <= 1'b1; + smode_deleg_x13_snap <= dut_smode_deleg.core0.regfile0.gp_registers[13]; + smode_deleg_x14_snap <= dut_smode_deleg.core0.regfile0.gp_registers[14]; + // x15 (the marker THIS instruction itself writes) is deliberately + // NOT snapshotted here -- it would read regfile0's pre-edge value + // (a same-cycle race, since this instruction's own write and this + // read happen on the identical posedge). Read live at check() + // time instead, same as every other case's own idempotent marker + // (x11/x12 elsewhere) -- safe since a constant write stays correct + // regardless of how many extra loop passes re-execute it. + smode_deleg_priv_snap <= dut_smode_deleg.core0.current_priv; + smode_deleg_mcauseq_snap <= dut_smode_deleg.core0.csr_file0.mcause_q; + smode_deleg_stvalq_snap <= dut_smode_deleg.core0.csr_file0.stval_q; + end + // x15 snapshot, deliberately a cycle later than the block above (keyed + // on pc==0xCC, the handler's own trailing ebreak, one instruction + // after the marker at 0xC8) -- x15's own write only lands the cycle + // AFTER it commits, so capturing it on the SAME edge as the marker's + // own commit would read the stale pre-write value (confirmed: this + // handler's own mtvec window (0x80) is never populated by this case + // -- it falls through to real crt0.hex boot-stub content left over in + // that memory, not zero -- so once the bounce-back loop starts + // executing THAT unrelated code, x15 is genuinely at risk if not + // captured before it starts). + logic smode_deleg_x15_done = 1'b0; + logic [63:0] smode_deleg_x15_snap; + always @(posedge clk) if (!smode_deleg_x15_done && dut_smode_deleg.core0.commit_now && dut_smode_deleg.core0.pc == 64'hCC) begin + smode_deleg_x15_done <= 1'b1; + smode_deleg_x15_snap <= dut_smode_deleg.core0.regfile0.gp_registers[15]; + end + always @(posedge clk) begin + if (dut_smode_deleg.core0.commit_now && dut_smode_deleg.core0.pc == 64'h3C) i_mtip_smode_deleg <= 1'b1; + end + + /* ----------------------------------------------------------------- * + * Case 6: a timer interrupt pending exactly as EBREAK's OWN trap-entry + * retires must not spuriously double-fire on the very next (deferred) + * interrupt-eligibility check. Since EBREAK is now a real trap (see + * this milestone's core.sv changes), this no longer depends on any + * EBREAK-specific guard -- the old `halted`/`!halted` mechanism this + * case originally exercised no longer exists. It falls out instead + * from the ORDINARY M-mode trap-entry mechanism: EBREAK's own + * trap-entry clears mstatus.MIE (csr_file.sv's mstatus_q writer), + * exactly like any other M-mode trap, so a still-pending interrupt + * correctly can't preempt on the immediately following cycle. This + * case proves that holds, AND that EBREAK's own trap fires for real + * (mcause==3, mepc==TRIGGER's own address) with genuine forward + * progress into a real handler -- not that nothing happens at all. + * + * i_mtip asserts on TRIGGER's own commit edge (an ebreak, same as + * every other case's own trigger technique -- just that here the + * triggering instruction IS the thing under test). + * + * EVERY completion signal in this file (not just this one) is keyed + * on a handler's own marker/CSR-readback WRITE instruction, never on + * any ebreak -- a real, milestone-wide gotcha found the hard way: + * since EBREAK is a real trap now, a "terminal" ebreak placed inside + * a handler while mtvec is still armed does NOT terminate anything -- + * it bounces straight back into that SAME handler (mtvec still + * points there), re-executing it and overwriting mepc/mcause with + * the LOOPING ebreak's own values before this testbench's fork/ + * wait_all ever gets around to checking them (the 10 DUTs run fully + * concurrently; a fast DUT keeps looping for many extra cycles while + * the fork waits on a slower one). A completion signal keyed on the + * marker write instead fires on this handler's guaranteed-first pass + * (a non-pipelined core can't have looped back yet at that point), + * capturing state before any bounce-back has a chance to corrupt it. + * dut_halted's own handler has no separate marker distinct from its + * CSR reads, so it's keyed on the last of those (csrr x11,mcause, + * pc==0x84) instead. + * ----------------------------------------------------------------- */ + logic i_mtip_halted = 1'b0; + core_wb4_sram_harness #(.NUM_WORDS(64)) dut_halted (.clk(clk), .rst(rst), .i_mtip(i_mtip_halted)); + always @(posedge clk) begin + if (dut_halted.core0.commit_now && dut_halted.core0.pc == 64'h18) i_mtip_halted <= 1'b1; + end + logic bad_interrupt_racing_ebreak = 1'b0; + always @(posedge clk) begin + if (dut_halted.core0.interrupt_taken) bad_interrupt_racing_ebreak <= 1'b1; + end + logic case6_done = 1'b0; + // x10 (mepc, written by the EARLIER csrr at 0x80) genuinely changes + // across loop passes -- see the big comment above -- so it needs a + // point-in-time snapshot, gated to fire only once (a plain, ungated + // assignment would keep re-firing and overwriting itself on every + // subsequent loop pass, ending up with the LAST value, not the + // first -- the actual bug this whole redesign chased down). x11 + // (mcause) is deliberately read LIVE at check() time instead, not + // snapshotted: it's written by THIS SAME instruction (a same-cycle + // read-before-write race against a snapshot), but every trap in this + // specific loop is the same repeating ebreak (always cause 3), so + // its value never actually changes across passes -- safe as a live + // read either way. + logic [63:0] case6_x10_snap; + always @(posedge clk) begin + if (!case6_done && dut_halted.core0.commit_now && dut_halted.core0.pc == 64'h84) begin + case6_done <= 1'b1; + case6_x10_snap <= dut_halted.core0.regfile0.gp_registers[10]; + end + end + + initial begin + #1; // run after every wb4_sram sub-instance's own time-0 crt0.hex init + + /* ---- Case 1: mask via mie.MTIE=0 ---- */ + dut_mask_mie.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mask_mie.sram0.memory[1] = {encode_csr(`CSR_MSTATUS, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd8, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mask_mie.sram0.memory[2] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, // ebreak + encode_i(32'sd111, 5'd0, 3'b000, 5'd1, `OPC_OP_IMM)}; // addi x1,x0,111 + dut_mask_mie.sram0.memory[16] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, // ebreak (0x84) + encode_i(32'sd999, 5'd0, 3'b000, 5'd2, `OPC_OP_IMM)}; // addi x2,x0,999 (poison, 0x80) + + /* ---- Case 2: mask via mstatus.MIE=0 ---- */ + dut_mask_mstatus.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mask_mstatus.sram0.memory[1] = {encode_csr(`CSR_MIE, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mask_mstatus.sram0.memory[2] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, + encode_i(32'sd222, 5'd0, 3'b000, 5'd1, `OPC_OP_IMM)}; + dut_mask_mstatus.sram0.memory[16] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, + encode_i(32'sd999, 5'd0, 3'b000, 5'd2, `OPC_OP_IMM)}; + + /* ---- Case 3: multi-cycle DIV hold-off ---- */ + dut_div.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_div.sram0.memory[1] = {encode_csr(`CSR_MIE, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_div.sram0.memory[2] = {encode_csr(`CSR_MSTATUS, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd8, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_div.sram0.memory[3] = {encode_i(32'sd7, 5'd0, 3'b000, 5'd21, `OPC_OP_IMM), // addi x21,x0,7 + encode_i(32'sd100, 5'd0, 3'b000, 5'd20, `OPC_OP_IMM)}; // addi x20,x0,100 + dut_div.sram0.memory[4] = {encode_i(32'sd999, 5'd0, 3'b000, 5'd2, `OPC_OP_IMM), // addi x2,x0,999 (poison, 0x24) + encode_r(7'b0000001, 5'd21, 5'd20, 3'b100, 5'd6, `OPC_OP)}; // div x6,x20,x21 (0x20) + dut_div.sram0.memory[5] = {32'h0, + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; // ebreak (0x28) + dut_div.sram0.memory[16] = {encode_i(32'sd1, 5'd0, 3'b000, 5'd11, `OPC_OP_IMM), // addi x11,x0,1 (0x84) + encode_csr(`CSR_MEPC, 5'd0, `FUNCT3_CSRRS, 5'd10, `OPC_SYSTEM)}; // csrr x10,mepc (0x80) + dut_div.sram0.memory[17] = {32'h0, + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; // ebreak (0x88) + + /* ---- Case 4a: mepc after an ordinary ALU op ---- */ + dut_mepc_alu.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mepc_alu.sram0.memory[1] = {encode_csr(`CSR_MIE, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mepc_alu.sram0.memory[2] = {encode_csr(`CSR_MSTATUS, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd8, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mepc_alu.sram0.memory[3] = {encode_i(32'sd999, 5'd0, 3'b000, 5'd2, `OPC_OP_IMM), // addi x2,x0,999 (poison, 0x1C) + encode_i(32'h55, 5'd0, 3'b000, 5'd1, `OPC_OP_IMM)}; // addi x1,x0,0x55 (TRIGGER, 0x18) + dut_mepc_alu.sram0.memory[4] = {32'h0, + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; // ebreak (0x20) + dut_mepc_alu.sram0.memory[16] = {encode_i(32'sd1, 5'd0, 3'b000, 5'd11, `OPC_OP_IMM), + encode_csr(`CSR_MEPC, 5'd0, `FUNCT3_CSRRS, 5'd10, `OPC_SYSTEM)}; + dut_mepc_alu.sram0.memory[17] = {32'h0, + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; + + /* ---- Case 4b: mepc after a JAL ---- */ + dut_mepc_jal.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mepc_jal.sram0.memory[1] = {encode_csr(`CSR_MIE, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mepc_jal.sram0.memory[2] = {encode_csr(`CSR_MSTATUS, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd8, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mepc_jal.sram0.memory[3] = {encode_i(32'sd888, 5'd0, 3'b000, 5'd4, `OPC_OP_IMM), // addi x4,x0,888 (decoy, 0x1C -- skipped by JAL) + encode_j(32'sd8, 5'd3, `OPC_JAL)}; // jal x3,+8 (TRIGGER, 0x18 -> target 0x20) + dut_mepc_jal.sram0.memory[4] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, // ebreak (0x24, safety net) + encode_i(32'sd999, 5'd0, 3'b000, 5'd2, `OPC_OP_IMM)}; // addi x2,x0,999 (poison, 0x20) + dut_mepc_jal.sram0.memory[16] = {encode_i(32'sd1, 5'd0, 3'b000, 5'd11, `OPC_OP_IMM), + encode_csr(`CSR_MEPC, 5'd0, `FUNCT3_CSRRS, 5'd10, `OPC_SYSTEM)}; + dut_mepc_jal.sram0.memory[17] = {32'h0, + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; + + /* ---- Case 4c: mepc after a taken branch ---- */ + dut_mepc_branch.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mepc_branch.sram0.memory[1] = {encode_csr(`CSR_MIE, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mepc_branch.sram0.memory[2] = {encode_csr(`CSR_MSTATUS, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd8, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mepc_branch.sram0.memory[3] = {encode_i(32'sd888, 5'd0, 3'b000, 5'd4, `OPC_OP_IMM), // addi x4,x0,888 (decoy, 0x1C) + encode_b(32'sd8, 5'd0, 5'd0, 3'b000, `OPC_BRANCH)}; // beq x0,x0,+8 (TRIGGER, 0x18 -> target 0x20) + dut_mepc_branch.sram0.memory[4] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, // ebreak (0x24, safety net) + encode_i(32'sd999, 5'd0, 3'b000, 5'd2, `OPC_OP_IMM)}; // addi x2,x0,999 (poison, 0x20) + dut_mepc_branch.sram0.memory[16] = {encode_i(32'sd1, 5'd0, 3'b000, 5'd11, `OPC_OP_IMM), + encode_csr(`CSR_MEPC, 5'd0, `FUNCT3_CSRRS, 5'd10, `OPC_SYSTEM)}; + dut_mepc_branch.sram0.memory[17] = {32'h0, + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; + + /* ---- Case 4d: mepc after MRET (the systematic-bug regression) ---- */ + dut_mepc_mret.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mepc_mret.sram0.memory[1] = {encode_csr(`CSR_MIE, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mepc_mret.sram0.memory[2] = {encode_csr(`CSR_MSTATUS, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), // mstatus: MPIE=1,MIE=0 + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mepc_mret.sram0.memory[3] = {encode_csr(`CSR_MEPC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), // mepc = 0x28 (poison addr) + encode_i(32'h28, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_mepc_mret.sram0.memory[4] = {encode_i(32'sd777, 5'd0, 3'b000, 5'd5, `OPC_OP_IMM), // addi x5,x0,777 (decoy, 0x24 -- skipped by MRET) + `INSTR_HEX_MRET}; // mret (TRIGGER, 0x20) + dut_mepc_mret.sram0.memory[5] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, // ebreak (0x2C, safety net) + encode_i(32'sd999, 5'd0, 3'b000, 5'd2, `OPC_OP_IMM)}; // addi x2,x0,999 (poison, 0x28) + dut_mepc_mret.sram0.memory[16] = {encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd11, `OPC_SYSTEM), // csrr x11,mcause (0x84) + encode_csr(`CSR_MEPC, 5'd0, `FUNCT3_CSRRS, 5'd10, `OPC_SYSTEM)}; // csrr x10,mepc (0x80) + dut_mepc_mret.sram0.memory[17] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, // ebreak (0x8C) + encode_i(32'sd1, 5'd0, 3'b000, 5'd12, `OPC_OP_IMM)}; // addi x12,x0,1 (0x88) + + /* ---- Case 5: S-mode routing with mideleg[7]=0 ---- + * addi's 12-bit immediate is signed (range -2048..+2047), so + * mstatus.MPP=S's own field value (2048 = 1<<11) can't be loaded + * directly the way every other constant in this file is -- same + * "li+slli" technique priv_test.s/core_priv_tb.sv already use for + * this exact field. */ + dut_smode.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_smode.sram0.memory[1] = {encode_csr(`CSR_MIE, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_smode.sram0.memory[2] = {encode_shift64(6'b000000, 6'd11, 5'd28, 3'b001, 5'd28, `OPC_OP_IMM), // slli x28,x28,11 -> x28=2048 + encode_i(32'sd1, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; // addi x28,x0,1 + dut_smode.sram0.memory[3] = {encode_i(32'h28, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM), // addi x28,x0,0x28 (S_MARKER addr) + encode_csr(`CSR_MSTATUS, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM)}; // csrw mstatus,x28 (MPP=S) + dut_smode.sram0.memory[4] = {`INSTR_HEX_MRET, // mret (0x24) + encode_csr(`CSR_MEPC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM)}; // csrw mepc,x28 (0x20) + dut_smode.sram0.memory[5] = {encode_i(32'sd999, 5'd0, 3'b000, 5'd2, `OPC_OP_IMM), // addi x2,x0,999 (poison, 0x2C) + encode_i(32'sd555, 5'd0, 3'b000, 5'd5, `OPC_OP_IMM)}; // addi x5,x0,555 (S_MARKER/TRIGGER, 0x28) + dut_smode.sram0.memory[6] = {32'h0, + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; // ebreak (0x30, safety net) + dut_smode.sram0.memory[16] = {encode_i(32'sd1, 5'd0, 3'b000, 5'd11, `OPC_OP_IMM), + encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd10, `OPC_SYSTEM)}; // csrr x10,mcause (0x80) + dut_smode.sram0.memory[17] = {32'h0, + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; // ebreak (0x88) + + /* ---- Case 7: S-mode routing with mideleg[7]=1 (delegated) ---- + * Same MPP=S technique as Case 5, plus: mideleg bit 7 set (delegate + * MTI), stvec armed (the redirect target once delegated), and + * mstatus's write combines MPP=S (2048) with SIE=1 (2) in the SAME + * write, since MRET leaves S-side fields untouched -- SIE must + * already be set before MRET enters S-mode, not after. */ + dut_smode_deleg.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_smode_deleg.sram0.memory[1] = {encode_csr(`CSR_MIE, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_smode_deleg.sram0.memory[2] = {encode_csr(`CSR_MIDELEG, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), // mideleg[7]=1 + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_smode_deleg.sram0.memory[3] = {encode_csr(`CSR_STVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), // stvec=0xC0 + encode_i(32'sd192, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_smode_deleg.sram0.memory[4] = {encode_shift64(6'b000000, 6'd11, 5'd28, 3'b001, 5'd28, `OPC_OP_IMM), // slli x28,x28,11 -> 2048 + encode_i(32'sd1, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; // addi x28,x0,1 + dut_smode_deleg.sram0.memory[5] = {encode_csr(`CSR_MSTATUS, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), // mstatus = 2050 (MPP=S|SIE=1) + encode_i(32'sd2, 5'd28, 3'b000, 5'd28, `OPC_OP_IMM)}; // addi x28,x28,2 -> 2050 + dut_smode_deleg.sram0.memory[6] = {encode_csr(`CSR_MEPC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), // mepc=0x3C (S_MARKER addr) + encode_i(32'h3C, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_smode_deleg.sram0.memory[7] = {encode_i(32'sd555, 5'd0, 3'b000, 5'd5, `OPC_OP_IMM), // addi x5,x0,555 (S_MARKER/TRIGGER, 0x3C) + `INSTR_HEX_MRET}; // mret (0x38) + dut_smode_deleg.sram0.memory[8] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, // ebreak (0x44, safety net) + encode_i(32'sd999, 5'd0, 3'b000, 5'd2, `OPC_OP_IMM)}; // addi x2,x0,999 (poison, 0x40) + dut_smode_deleg.sram0.memory[24] = {encode_csr(`CSR_SEPC, 5'd0, `FUNCT3_CSRRS, 5'd14, `OPC_SYSTEM), // csrr x14,sepc (0xC4) + encode_csr(`CSR_SCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd13, `OPC_SYSTEM)}; // csrr x13,scause (0xC0) + dut_smode_deleg.sram0.memory[25] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, // ebreak (0xCC) + encode_i(32'sd1, 5'd0, 3'b000, 5'd15, `OPC_OP_IMM)}; // addi x15,x0,1 (0xC8, handler ran) + + /* ---- Case 6: EBREAK trap racing a pending timer interrupt ---- */ + dut_halted.sram0.memory[0] = {encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_halted.sram0.memory[1] = {encode_csr(`CSR_MIE, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd128, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_halted.sram0.memory[2] = {encode_csr(`CSR_MSTATUS, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM), + encode_i(32'sd8, 5'd0, 3'b000, 5'd28, `OPC_OP_IMM)}; + dut_halted.sram0.memory[3] = {32'h0, + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; // ebreak (TRIGGER, 0x18) + dut_halted.sram0.memory[16] = {encode_csr(`CSR_MCAUSE, 5'd0, `FUNCT3_CSRRS, 5'd11, `OPC_SYSTEM), // csrr x11,mcause (0x84) + encode_csr(`CSR_MEPC, 5'd0, `FUNCT3_CSRRS, 5'd10, `OPC_SYSTEM)}; // csrr x10,mepc (0x80) + dut_halted.sram0.memory[17] = {32'h0, + {11'b0, 1'b1, 13'b0, `OPC_SYSTEM}}; // ebreak (0x88, handler completion) + + @(posedge clk); #1; + rst = 0; + + fork + begin : wait_all + wait (mask_mie_halted === 1'b1); + wait (mask_mstatus_halted === 1'b1); + wait (div_halted === 1'b1); + wait (mepc_alu_halted === 1'b1); + wait (mepc_jal_halted === 1'b1); + wait (mepc_branch_halted === 1'b1); + wait (mepc_mret_halted === 1'b1); + wait (smode_halted === 1'b1); + wait (smode_deleg_x15_done === 1'b1); // strictly later than smode_deleg_halted (0xCC vs 0xC8) -- + // waiting on it also guarantees the earlier snapshots landed + wait (case6_done === 1'b1); + end + begin : timeout + repeat (TIMEOUT_CYCLES) @(posedge clk); + $display("TIMEOUT: not every DUT halted"); + $finish; + end + join_any + #1; + + /* ---- Case 1 checks ---- + * No "poison marker never ran" check anymore -- since EBREAK + * really traps now, this program's own trailing ebreak (reached + * strictly AFTER the check below already captured state) also + * bounces into mtvec's own code unconditionally, regardless of + * whether the interrupt was masked correctly, so that check can + * no longer distinguish correct from broken masking. The real + * proof of masking here is structural: mask_mie_halted only ever + * latches (avoiding the shared timeout) if x1's own write at + * pc==0x10 was reached at all -- unreachable if the interrupt + * incorrectly fired first and redirected away before it. */ + check("case1 (mask via mie.MTIE=0): ran to its own marker, x1==111", dut_mask_mie.core0.regfile0.gp_registers[1], 64'd111); + check("case1: reached its own marker undisturbed (mask held)", {63'b0, mask_mie_halted}, 64'd1); + + /* ---- Case 2 checks (same reasoning as Case 1 above) ---- */ + check("case2 (mask via mstatus.MIE=0): ran to its own marker, x1==222", dut_mask_mstatus.core0.regfile0.gp_registers[1], 64'd222); + check("case2: reached its own marker undisturbed (mask held)", {63'b0, mask_mstatus_halted}, 64'd1); + + /* ---- Case 3 checks (DIV hold-off) ---- */ + check("case3 (DIV): div's own regfile write landed (x6==14)", dut_div.core0.regfile0.gp_registers[6], 64'd14); + check("case3: divide's commit was observed (sanity on the monitor itself)", {63'b0, div_committed}, 64'd1); + check("case3: interrupt_taken never asserted before the divide's own commit", {63'b0, bad_early_interrupt}, 64'd0); + check("case3: poison marker never ran (x2==0)", dut_div.core0.regfile0.gp_registers[2], 64'd0); + check("case3: mepc == poison addr (0x24), resumed right after DIV", div_x10_snap, 64'h24); + check("case3: handler ran (x11==1)", dut_div.core0.regfile0.gp_registers[11], 64'd1); + check("case3: EBREAK trap fired", {63'b0, div_halted}, 64'd1); + + /* ---- Case 4a checks (mepc after ordinary ALU op) ---- */ + check("case4a: poison marker never ran (x2==0)", dut_mepc_alu.core0.regfile0.gp_registers[2], 64'd0); + check("case4a: mepc == TRIGGER+4 (0x1C, ordinary pc_plus_len)", mepc_alu_x10_snap, 64'h1C); + check("case4a: handler ran (x11==1)", dut_mepc_alu.core0.regfile0.gp_registers[11], 64'd1); + check("case4a: EBREAK trap fired", {63'b0, mepc_alu_halted}, 64'd1); + + /* ---- Case 4b checks (mepc after JAL) ---- */ + check("case4b: poison marker never ran (x2==0)", dut_mepc_jal.core0.regfile0.gp_registers[2], 64'd0); + check("case4b: decoy never ran (x4==0)", dut_mepc_jal.core0.regfile0.gp_registers[4], 64'd0); + check("case4b: mepc == JAL's jump target (0x20)", mepc_jal_x10_snap, 64'h20); + check("case4b: handler ran (x11==1)", dut_mepc_jal.core0.regfile0.gp_registers[11], 64'd1); + check("case4b: EBREAK trap fired", {63'b0, mepc_jal_halted}, 64'd1); + + /* ---- Case 4c checks (mepc after taken branch) ---- */ + check("case4c: poison marker never ran (x2==0)", dut_mepc_branch.core0.regfile0.gp_registers[2], 64'd0); + check("case4c: decoy never ran (x4==0)", dut_mepc_branch.core0.regfile0.gp_registers[4], 64'd0); + check("case4c: mepc == branch target (0x20)", mepc_branch_x10_snap, 64'h20); + check("case4c: handler ran (x11==1)", dut_mepc_branch.core0.regfile0.gp_registers[11], 64'd1); + check("case4c: EBREAK trap fired", {63'b0, mepc_branch_halted}, 64'd1); + + /* ---- Case 4d checks (mepc after MRET -- the systematic-bug regression) ---- */ + check("case4d: poison marker never ran (x2==0) -- genuine preemption, not eventually-taken", dut_mepc_mret.core0.regfile0.gp_registers[2], 64'd0); + check("case4d: decoy never ran (x5==0)", dut_mepc_mret.core0.regfile0.gp_registers[5], 64'd0); + check("case4d: mepc == MRET's own return address (0x28)", mepc_mret_x10_snap, 64'h28); + check("case4d: mcause == standard machine-timer-interrupt encoding", mepc_mret_x11_snap, 64'h8000_0000_0000_0007); + check("case4d: handler ran (x12==1)", dut_mepc_mret.core0.regfile0.gp_registers[12], 64'd1); + check("case4d: current_priv == M", {62'b0, dut_mepc_mret.core0.current_priv}, 64'(2'b11)); + check("case4d: EBREAK trap fired", {63'b0, mepc_mret_halted}, 64'd1); + + /* ---- Case 5 checks (S-mode routing, mideleg[7]=0) ---- */ + check("case5: S-mode marker genuinely ran (x5==555)", dut_smode.core0.regfile0.gp_registers[5], 64'd555); + check("case5: poison marker never ran (x2==0)", dut_smode.core0.regfile0.gp_registers[2], 64'd0); + check("case5: mcause == standard machine-timer-interrupt encoding (routed to M, not S)", smode_x10_snap, 64'h8000_0000_0000_0007); + check("case5: scause_q untouched (never routed to S)", dut_smode.core0.csr_file0.scause_q, 64'd0); + check("case5: mtval_q reads 0 after a pure interrupt (i_trap_val masked, not stale trap_val)", + dut_smode.core0.csr_file0.mtval_q, 64'd0); + check("case5: M handler ran (x11==1)", dut_smode.core0.regfile0.gp_registers[11], 64'd1); + check("case5: current_priv == M", {62'b0, dut_smode.core0.current_priv}, 64'(2'b11)); + check("case5: EBREAK trap fired", {63'b0, smode_halted}, 64'd1); + + /* ---- Case 7 checks (S-mode routing, mideleg[7]=1, delegated) ---- */ + check("case7: S-mode marker genuinely ran (x5==555)", dut_smode_deleg.core0.regfile0.gp_registers[5], 64'd555); + check("case7: poison never ran (x2==0)", dut_smode_deleg.core0.regfile0.gp_registers[2], 64'd0); + check("case7: scause == standard machine-timer-interrupt encoding (routed to S, not M)", + smode_deleg_x13_snap, 64'h8000_0000_0000_0007); + check("case7: sepc == the S-mode marker's own resume address (0x40)", + smode_deleg_x14_snap, 64'h40); + check("case7: mcause_q untouched (never routed to M)", smode_deleg_mcauseq_snap, 64'd0); + check("case7: stval_q reads 0 after a pure interrupt (i_trap_val masked)", smode_deleg_stvalq_snap, 64'd0); + check("case7: S handler ran (x15==1)", smode_deleg_x15_snap, 64'd1); + check("case7: current_priv == S", {62'b0, smode_deleg_priv_snap}, 64'(2'b01)); + check("case7: EBREAK trap fired", {63'b0, smode_deleg_halted}, 64'd1); + + /* ---- Case 6 checks (EBREAK trap racing a pending interrupt) ---- */ + check("case6: EBREAK trap fired for real (mcause==3, Breakpoint)", dut_halted.core0.regfile0.gp_registers[11], 64'd3); + check("case6: mepc == EBREAK's own address (0x18)", case6_x10_snap, 64'h18); + check("case6: real forward progress -- handler ran to its own completion", {63'b0, case6_done}, 64'd1); + check("case6: pending interrupt did not spuriously fire racing EBREAK's own trap-entry", {63'b0, bad_interrupt_racing_ebreak}, 64'd0); + + $display(""); + $display("core_interrupt_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("core_interrupt_tb: FAILURES PRESENT"); + $finish; + end + +endmodule diff --git a/testbench/core_isa_coverage_gap_tb.sv b/testbench/core_isa_coverage_gap_tb.sv index 6fe8310..d71fd9f 100644 --- a/testbench/core_isa_coverage_gap_tb.sv +++ b/testbench/core_isa_coverage_gap_tb.sv @@ -101,7 +101,8 @@ module core_isa_coverage_gap_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.core0.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" initial begin @@ -234,7 +235,7 @@ module core_isa_coverage_gap_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "dut.core0.halted never went high"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "EBREAK trap never fired"); check("AND", dut.core0.regfile0.gp_registers[1], 64'h82); check("OR", dut.core0.regfile0.gp_registers[2], 64'hEE); @@ -260,7 +261,7 @@ module core_isa_coverage_gap_tb; check("SRAIW arithmetic-shifts then resigns", dut.core0.regfile0.gp_registers[21], 64'hFFFFFFFFF8000000); check("FENCE fell through (no stall/corruption)", dut.core0.regfile0.gp_registers[22], 64'd112); check("plain NOP fell through (no stall/corruption)", dut.core0.regfile0.gp_registers[23], 64'd223); - check("core halted (ebreak reached)", {63'b0, dut.core0.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); $display(""); $display("core_isa_coverage_gap_tb: %0d passed, %0d failed", pass_count, fail_count); diff --git a/testbench/core_load_store_tb.sv b/testbench/core_load_store_tb.sv index 90a7559..1d48131 100644 --- a/testbench/core_load_store_tb.sv +++ b/testbench/core_load_store_tb.sv @@ -64,6 +64,9 @@ module core_load_store_tb; end endtask + logic halted = 1'b0; + always @(posedge clk) if (dut.trap_taken && dut.is_ebreak) halted <= 1'b1; + initial begin #1; // run after wb4_sram's own time-0 init (zero-fill + $readmemh) -- see core_wb_tb.sv @@ -100,10 +103,10 @@ module core_load_store_tb; rst = 0; fork - wait (dut.halted === 1'b1); + wait (halted === 1'b1); begin repeat (150) @(posedge clk); - $display("TIMEOUT: dut.halted never went high"); + $display("TIMEOUT: EBREAK trap never fired"); $finish; end join_any diff --git a/testbench/core_m_ext_cache_tb.sv b/testbench/core_m_ext_cache_tb.sv index 7249461..c830d50 100644 --- a/testbench/core_m_ext_cache_tb.sv +++ b/testbench/core_m_ext_cache_tb.sv @@ -46,7 +46,8 @@ module core_m_ext_cache_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.core0.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" /* @@ -177,7 +178,7 @@ module core_m_ext_cache_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "dut.core0.halted never went high"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "EBREAK trap never fired"); check("MUL", dut.core0.regfile0.gp_registers[1], 64'hFFFFFFFFFFFFFFFE); check("MULH", dut.core0.regfile0.gp_registers[2], 64'hFFFFFFFFFFFFFFFF); @@ -192,7 +193,7 @@ module core_m_ext_cache_tb; check("DIVUW", dut.core0.regfile0.gp_registers[11], 64'h0000000055555555); check("REMW", dut.core0.regfile0.gp_registers[12], 64'hFFFFFFFFFFFFFFFF); check("REMUW", dut.core0.regfile0.gp_registers[13], 64'd2); - check("core halted (ebreak reached)", {63'b0, dut.core0.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); check("DIV genuinely held S_EXEC for many cycles (real multi-cycle divide, not combinational)", {56'b0, (div_exec_cycle_count_final > 8'd8) ? 8'd1 : 8'd0}, 64'd1); diff --git a/testbench/core_m_ext_tb.sv b/testbench/core_m_ext_tb.sv index a21385d..36cb079 100644 --- a/testbench/core_m_ext_tb.sv +++ b/testbench/core_m_ext_tb.sv @@ -52,7 +52,8 @@ module core_m_ext_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.core0.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" /* @@ -183,7 +184,7 @@ module core_m_ext_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "dut.core0.halted never went high"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "EBREAK trap never fired"); check("MUL", dut.core0.regfile0.gp_registers[1], 64'hFFFFFFFFFFFFFFFE); check("MULH", dut.core0.regfile0.gp_registers[2], 64'hFFFFFFFFFFFFFFFF); @@ -198,7 +199,7 @@ module core_m_ext_tb; check("DIVUW", dut.core0.regfile0.gp_registers[11], 64'h0000000055555555); check("REMW", dut.core0.regfile0.gp_registers[12], 64'hFFFFFFFFFFFFFFFF); check("REMUW", dut.core0.regfile0.gp_registers[13], 64'd2); - check("core halted (ebreak reached)", {63'b0, dut.core0.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); check("DIV genuinely held S_EXEC for many cycles (real multi-cycle divide, not combinational)", {56'b0, (div_exec_cycle_count_final > 8'd8) ? 8'd1 : 8'd0}, 64'd1); diff --git a/testbench/core_m_toolchain_tb.sv b/testbench/core_m_toolchain_tb.sv index 7020d0b..88b5636 100644 --- a/testbench/core_m_toolchain_tb.sv +++ b/testbench/core_m_toolchain_tb.sv @@ -80,7 +80,8 @@ module core_m_toolchain_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.trap_taken && dut.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" initial begin @@ -90,7 +91,7 @@ module core_m_toolchain_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "dut.halted never went high -- is firmware/m_test.hex built?"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "EBREAK trap never fired -- is firmware/m_test.hex built?"); /* * Expected values, mechanically identical to the ones @@ -118,7 +119,7 @@ module core_m_toolchain_tb; check("REMW (s0)", dut.regfile0.gp_registers[8], 64'hFFFFFFFFFFFFFFFF); check("REMUW (t3)", dut.regfile0.gp_registers[28], 64'd2); - check("core halted (ebreak reached)", {63'b0, dut.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); $display(""); $display("core_m_toolchain_tb: %0d passed, %0d failed", pass_count, fail_count); diff --git a/testbench/core_m_w_edgecases_tb.sv b/testbench/core_m_w_edgecases_tb.sv index f34a5ae..191c3e3 100644 --- a/testbench/core_m_w_edgecases_tb.sv +++ b/testbench/core_m_w_edgecases_tb.sv @@ -87,7 +87,8 @@ module core_m_w_edgecases_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.core0.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" initial begin @@ -120,13 +121,13 @@ module core_m_w_edgecases_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "dut.core0.halted never went high"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_LARGE, "EBREAK trap never fired"); check("DIVW-by-zero: -777/0 -> -1, sign-extended", dut.core0.regfile0.gp_registers[1], 64'hFFFFFFFFFFFFFFFF); check("DIVUW-by-zero: 321/0 -> 2^32-1, sign-extended", dut.core0.regfile0.gp_registers[2], 64'hFFFFFFFFFFFFFFFF); check("REMW-by-zero: -555/0 -> dividend unchanged, sign-extended", dut.core0.regfile0.gp_registers[3], 64'hFFFFFFFFFFFFFDD5); check("REMUW-by-zero: 999/0 -> dividend unchanged", dut.core0.regfile0.gp_registers[4], 64'd999); - check("core halted (ebreak reached)", {63'b0, dut.core0.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); $display(""); $display("core_m_w_edgecases_tb: %0d passed, %0d failed", pass_count, fail_count); diff --git a/testbench/core_misaligned_trap_tb.sv b/testbench/core_misaligned_trap_tb.sv index 79ade16..edfa08b 100644 --- a/testbench/core_misaligned_trap_tb.sv +++ b/testbench/core_misaligned_trap_tb.sv @@ -50,7 +50,8 @@ module core_misaligned_trap_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.core0.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" initial begin @@ -139,7 +140,7 @@ module core_misaligned_trap_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "dut.core0.halted never went high"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "EBREAK trap never fired"); check("load trap: mcause == 4 (load address misaligned)", dut.core0.regfile0.gp_registers[21], 64'd4); @@ -159,7 +160,7 @@ module core_misaligned_trap_tb; check("store trap: target dword still holds the sentinel (store never reached memory)", dut.core0.regfile0.gp_registers[8], 64'hDEAD_BEEF_DEAD_BEEF); - check("core halted (ebreak reached)", {63'b0, dut.core0.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); $display(""); $display("core_misaligned_trap_tb: %0d passed, %0d failed", pass_count, fail_count); diff --git a/testbench/core_priv_tb.sv b/testbench/core_priv_tb.sv index 9f8a4f1..bccce63 100644 --- a/testbench/core_priv_tb.sv +++ b/testbench/core_priv_tb.sv @@ -88,9 +88,35 @@ module core_priv_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.core0.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" + /* + * "Final" state snapshot (current_priv/mcause_q/mtval_q), taken at + * pc==0x88 (idx34, the x11 marker -- the last real instruction before + * this program's own terminal ebreak at 0x8C). NOT read live at + * check() time: mtvec stays armed at M_TRAP_HANDLER for the rest of + * this program, including the terminal ebreak itself -- since EBREAK + * is a real trap now, that ebreak bounces right back into + * M_TRAP_HANDLER (mepc happens to land exactly on the handler's own + * first instruction, 0x8C+4=0x90), which then walks forward through + * memory as ordinary code (each mret jumping to whatever mepc was + * just advanced to), eventually corrupting mcause_q/mtval_q/ + * current_priv long after this test's own real assertions already + * happened. A point-in-time snapshot before the terminal ebreak + * sidesteps all of that. + */ + logic [63:0] final_mcause_snap, final_mtval_snap; + logic [1:0] final_priv_snap; + logic final_state_captured = 1'b0; + always @(posedge clk) if (!final_state_captured && dut.core0.commit_now && dut.core0.pc == 64'h88) begin + final_state_captured <= 1'b1; + final_mcause_snap <= dut.core0.csr_file0.mcause_q; + final_mtval_snap <= dut.core0.csr_file0.mtval_q; + final_priv_snap <= dut.core0.current_priv; + end + /* * White-box monitors. state_t's declared order in core.sv is * {S_FETCH, S_EXEC, S_MEM} = {0, 1, 2}, hence the literal 2'd1 below @@ -262,7 +288,7 @@ module core_priv_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "dut.halted never went high"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "EBREAK trap never fired"); /* Marker registers -- structural proof control flow visited every phase, in order, and returned correctly each time. */ check("x1 (phase 1 marker -- returned from M-mode ECALL)", dut.core0.regfile0.gp_registers[1], 64'd111); @@ -307,7 +333,8 @@ module core_priv_tb; check("phase 6b: trap_taken reads 1 (illegal-instruction correctly raised instead)", {63'b0, trap_taken_sampled_phase6b}, 64'd1); /* Final state: current_priv ends at U (phase 6b's illegal SRET's own M-routed trap restored MPP=U, per current_priv at that trap's entry). */ - check("final current_priv == U", {62'b0, dut.core0.current_priv}, `WORD_SIZE'(2'b00)); + check("final state sample was captured (sanity on the monitor itself)", {63'b0, final_state_captured}, 64'd1); + check("final current_priv == U", {62'b0, final_priv_snap}, `WORD_SIZE'(2'b00)); /* * Final mcause/mtval: the LAST M-target trap overall is phase 6b's * illegal SRET-from-U (also cause 2, also undelegated since @@ -315,10 +342,10 @@ module core_priv_tb; * program order) -- confirms its own raw encoding was captured, * not phase 6a's. */ - check("final mcause == 2 (illegal instruction, from phase 6b's own M-routed trap)", dut.core0.csr_file0.mcause_q, `WORD_SIZE'(2)); - check("final mtval == the illegal SRET's own raw encoding (phase 6b)", dut.core0.csr_file0.mtval_q, `WORD_SIZE'(`INSTR_HEX_SRET)); + check("final mcause == 2 (illegal instruction, from phase 6b's own M-routed trap)", final_mcause_snap, `WORD_SIZE'(2)); + check("final mtval == the illegal SRET's own raw encoding (phase 6b)", final_mtval_snap, `WORD_SIZE'(`INSTR_HEX_SRET)); - check("core halted (ebreak reached)", {63'b0, dut.core0.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); $display(""); $display("core_priv_tb: %0d passed, %0d failed", pass_count, fail_count); diff --git a/testbench/core_priv_toolchain_tb.sv b/testbench/core_priv_toolchain_tb.sv index 91e9ded..7a53a7c 100644 --- a/testbench/core_priv_toolchain_tb.sv +++ b/testbench/core_priv_toolchain_tb.sv @@ -76,9 +76,39 @@ module core_priv_toolchain_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.trap_taken && dut.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" + /* + * mcause_q/current_priv snapshot, taken on the SAME edge the + * terminal ebreak's own trap_taken first fires -- NOT read live at + * check() time. mtvec is never repointed away from M_TRAP_HANDLER by + * priv_test.s, so crt0.s's own trailing ebreak (a real trap now) + * bounces right back into it, corrupting mcause_q/current_priv long + * after this test's own real assertions already happened (same class + * of bug found and fixed in core_priv_tb.sv/core_c_illegal_trap_tb.sv + * -- see their own comments for the full story). + * + * This capture technique needs no known instruction address (unlike + * those hand-encoded testbenches): mcause_q's own always_ff writes + * `mcause_q <= i_trap_cause` on i_trap_taken, non-blocking -- so a + * SEPARATE always block reading mcause_q on that identical edge + * (trap_taken && is_ebreak, this ebreak's own commit) sees the + * PRE-edge value, i.e. whatever mcause_q held from the last real + * event BEFORE this ebreak's own trap-entry overwrites it -- exactly + * the value this test wants, for free, regardless of where in the + * program that terminal ebreak lives. + */ + logic [63:0] final_mcause_snap; + logic [1:0] final_priv_snap; + logic final_state_captured = 1'b0; + always @(posedge clk) if (!final_state_captured && dut.trap_taken && dut.is_ebreak) begin + final_state_captured <= 1'b1; + final_mcause_snap <= dut.csr_file0.mcause_q; + final_priv_snap <= dut.current_priv; + end + initial begin #1; // see header comment: run after wb4_sram's own time-0 init of crt0.hex $readmemh("../firmware/priv_test.hex", sram0.memory); @@ -86,7 +116,7 @@ module core_priv_toolchain_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "dut.halted never went high -- is firmware/priv_test.hex built?"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "EBREAK trap never fired -- is firmware/priv_test.hex built?"); /* * Expected values, mechanically identical to the derivation @@ -114,13 +144,14 @@ module core_priv_toolchain_tb; * trap_to_s correctly routed phase 3's cause into scause_q * (already checked above as s4) and nowhere near mcause_q. */ - check("mcause_q still 11 post-halt (untouched by the later S-target trap)", dut.csr_file0.mcause_q, 64'd11); + check("final-state sample was captured (sanity on the monitor itself)", {63'b0, final_state_captured}, 64'd1); + check("mcause_q still 11 post-halt (untouched by the later S-target trap)", final_mcause_snap, 64'd11); check("scause_q final value (register-independent confirmation)", dut.csr_file0.scause_q, 64'd9); /* Final privilege level: phase 3's delegated ECALL trapped from S and sret'd back to S (SPP captured S at entry) -- never left S after phase 2's bootstrap. */ - check("final current_priv == S", {62'b0, dut.current_priv}, 64'(2'b01)); + check("final current_priv == S", {62'b0, final_priv_snap}, 64'(2'b01)); - check("core halted (ebreak reached)", {63'b0, dut.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); $display(""); $display("core_priv_toolchain_tb: %0d passed, %0d failed", pass_count, fail_count); diff --git a/testbench/core_priv_u_ecall_tb.sv b/testbench/core_priv_u_ecall_tb.sv index 70f5362..9544eae 100644 --- a/testbench/core_priv_u_ecall_tb.sv +++ b/testbench/core_priv_u_ecall_tb.sv @@ -117,9 +117,31 @@ module core_priv_u_ecall_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.core0.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" + /* + * mcause_q/current_priv snapshot, taken on the SAME edge the terminal + * ebreak's own trap_taken first fires -- NOT read live at check() + * time. mtvec stays armed at M_TRAP_HANDLER through the terminal + * ebreak (idx15), which now really traps and bounces right back into + * it, corrupting mcause_q/current_priv afterward (same class of bug + * as core_priv_tb.sv/core_priv_toolchain_tb.sv -- see their own + * comments). mcause_q's own always_ff writes non-blocking on + * i_trap_taken, so reading it on this identical edge sees the + * PRE-edge value -- whatever it held from phase 1's own trap, before + * this ebreak's own entry overwrites it. + */ + logic [63:0] final_mcause_snap; + logic [1:0] final_priv_snap; + logic final_state_captured = 1'b0; + always @(posedge clk) if (!final_state_captured && dut.core0.trap_taken && dut.core0.is_ebreak) begin + final_state_captured <= 1'b1; + final_mcause_snap <= dut.core0.csr_file0.mcause_q; + final_priv_snap <= dut.core0.current_priv; + end + /* * White-box monitors. state_t's declared order in core.sv is * {S_FETCH, S_EXEC, S_MEM} = {0, 1, 2}, hence the literal 2'd1 below @@ -214,15 +236,16 @@ module core_priv_u_ecall_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_TINY, "dut.halted never went high"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_TINY, "EBREAK trap never fired"); /* Marker registers -- structural proof control flow visited every phase, in order, and returned correctly each time. */ check("x1 (phase 1 marker -- returned from U-mode ECALL)", dut.core0.regfile0.gp_registers[1], 64'd811); check("x9 (phase 2 before-SFENCE.VMA marker)", dut.core0.regfile0.gp_registers[9], 64'd344); check("x10 (phase 2 after-SFENCE.VMA marker -- SFENCE.VMA with real operands was a true no-op)", dut.core0.regfile0.gp_registers[10], 64'd455); + check("final-state sample was captured (sanity on the monitor itself)", {63'b0, final_state_captured}, 64'd1); /* Phase 1: mcause is this program's only trap, never overwritten afterward -- safe to check directly, post-halt. */ - check("phase 1: mcause == 8 (ECALL from U-mode)", dut.core0.csr_file0.mcause_q, `WORD_SIZE'(8)); + check("phase 1: mcause == 8 (ECALL from U-mode)", final_mcause_snap, `WORD_SIZE'(8)); /* Phase 1: white-box current_priv sample at the marker's own PC, proving control genuinely returned to U (not stranded in M). */ check("phase 1 current_priv sample was captured (sanity on the monitor itself)", {63'b0, priv_sample_ecall_u_captured}, 64'd1); @@ -239,9 +262,9 @@ module core_priv_u_ecall_tb; check("x6 (SFENCE.VMA's rs2 operand, ASID-shaped) unchanged after the no-op", dut.core0.regfile0.gp_registers[6], 64'd7); /* Final state: current_priv stays U for the rest of the program (nothing after phase 1 changes it). */ - check("final current_priv == U", {62'b0, dut.core0.current_priv}, `WORD_SIZE'(2'b00)); + check("final current_priv == U", {62'b0, final_priv_snap}, `WORD_SIZE'(2'b00)); - check("core halted (ebreak reached)", {63'b0, dut.core0.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); $display(""); $display("core_priv_u_ecall_tb: %0d passed, %0d failed", pass_count, fail_count); diff --git a/testbench/core_reservation_fault_tb.sv b/testbench/core_reservation_fault_tb.sv new file mode 100644 index 0000000..b5aa6bc --- /dev/null +++ b/testbench/core_reservation_fault_tb.sv @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +`include "riscv_encode.sv" + + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: core, a faulted LR must NOT create a valid LR/SC + * reservation. + * + * Closes a real gap found by code review of the bus-error-trapping + * milestone: reservation_valid_q/reservation_addr_q (design/core.sv, + * the LR/SC reservation register) were set on `commit_now && is_lr` + * with no `!trap_taken` guard -- unlike reg_write/csr_we, which are + * both correctly gated. Since is_lr is purely combinational/decode- + * based and doesn't care whether the bus transaction actually + * succeeded, a faulted LR (bus-error or misaligned) still set a + * "valid" reservation for a load that never happened. + * + * Proof strategy: an LR.W to an out-of-range address (faults, cause 7, + * same class as core_bus_fault_trap_tb.sv's test E) immediately + * followed by an SC.W to the SAME address. If the reservation was + * spuriously set, sc_addr_match/sc_success incorrectly evaluate true, + * making mem_control treat the SC as a MATCHED store -- so it actually + * attempts a real bus write to that same out-of-range address, which + * itself then faults (cause 7 again), suppressing the SC's own + * register write entirely (trap_taken blocks reg_write) -- leaving its + * destination register at whatever sentinel it held before. With the + * fix, no reservation exists, so sc_success is false, mem_control + * treats the SC as a MISMATCH (no bus access needed at all -- pure + * S_EXEC commit), and its destination register is correctly written to + * 1 (SC failure, per spec). The two outcomes are cleanly distinguished + * by one register's final value: 1 (correct) vs. the untouched + * sentinel (bug). + * + * The trap handler resumes via mepc+4 (not a fixed resume address, + * unlike core_bus_fault_trap_tb.sv's fetch-fault cases) specifically so + * this test is robust to EITHER outcome: if the SC also faults (the bug + * case), the handler fires a second time and correctly walks forward + * past it too, so the program reaches ebreak either way -- only the + * final register check tells them apart. + */ +module core_reservation_fault_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + core_wb4_sram_harness #(.NUM_WORDS(64)) dut (.clk(clk), .rst(rst)); + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; + `include "halt_wait.sv" + + localparam int unsigned HANDLER_ADDR = 32'h40; + localparam int unsigned OUT_OF_RANGE = 32'h10000; + + logic [31:0] main_prog[0:8]; + logic [31:0] handler_prog[0:3]; + int i; + + initial begin + #1; + + /* + * addr instr notes + * 0x00 addi x28, x0, HANDLER_ADDR + * 0x04 csrrw x0, mtvec, x28 + * 0x08 lui x5, 16 x5 = 0x10000 + * 0x0C addi x6, x0, 777 sentinel, LR's rd + * 0x10 lr.w x6, (x5) FAULTS (cause 7) + * 0x14 addi x9, x0, 888 sentinel, SC's rd + * 0x18 addi x10, x0, 42 rs2 for SC (irrelevant if mismatched) + * 0x1C sc.w x9, x10, (x5) must NOT fault; x9 <- 1 + * 0x20 ebreak + */ + main_prog[0] = encode_i(int'(HANDLER_ADDR), 5'd0, 3'b000, 5'd28, `OPC_OP_IMM); + main_prog[1] = encode_csr(`CSR_MTVEC, 5'd28, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + main_prog[2] = encode_u(20'h10, 5'd5, `OPC_LUI); + main_prog[3] = encode_i(32'sd777, 5'd0, 3'b000, 5'd6, `OPC_OP_IMM); + main_prog[4] = encode_amo(`FUNCT5_LR, 1'b0, 1'b0, 5'd0, 5'd5, `FUNCT3_AMO_W, 5'd6, `OPC_AMO); + main_prog[5] = encode_i(32'sd888, 5'd0, 3'b000, 5'd9, `OPC_OP_IMM); + main_prog[6] = encode_i(32'sd42, 5'd0, 3'b000, 5'd10, `OPC_OP_IMM); + main_prog[7] = encode_amo(`FUNCT5_SC, 1'b0, 1'b0, 5'd10, 5'd5, `FUNCT3_AMO_W, 5'd9, `OPC_AMO); + main_prog[8] = encode_i(32'sd1, 5'd0, 3'b000, 5'd0, `OPC_SYSTEM); // ebreak + + for (i = 0; i < 4; i = i + 1) + dut.sram0.memory[i] = {main_prog[2*i+1], main_prog[2*i]}; + dut.sram0.memory[4][31:0] = main_prog[8]; // 0x20: ebreak, alone in its word's low half + + /* + * addr instr notes + * 0x40 csrrs x20, mepc, x0 + * 0x44 addi x20, x20, 4 skip past the faulting instruction + * 0x48 csrrw x0, mepc, x20 + * 0x4C mret + */ + handler_prog[0] = encode_csr(`CSR_MEPC, 5'd0, `FUNCT3_CSRRS, 5'd20, `OPC_SYSTEM); + handler_prog[1] = encode_i(32'sd4, 5'd20, 3'b000, 5'd20, `OPC_OP_IMM); + handler_prog[2] = encode_csr(`CSR_MEPC, 5'd20, `FUNCT3_CSRRW, 5'd0, `OPC_SYSTEM); + handler_prog[3] = `INSTR_HEX_MRET; + + for (i = 0; i < 2; i = i + 1) + dut.sram0.memory[8+i] = {handler_prog[2*i+1], handler_prog[2*i]}; + + @(posedge clk); #1; + rst = 0; + + wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "EBREAK trap never fired"); + + check("faulted LR: dest register untouched (LR never happened)", + dut.core0.regfile0.gp_registers[6], 64'd777); + check({"SC to the same address after a faulted LR correctly reports FAILURE (rd=1) -- ", + "NOT the untouched sentinel (888), which would mean the faulted LR spuriously ", + "created a valid reservation and the SC's own write then also faulted"}, + dut.core0.regfile0.gp_registers[9], 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); + + $display(""); + $display("core_reservation_fault_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("core_reservation_fault_tb: FAILURES PRESENT"); + $finish; + end + +endmodule + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/testbench/core_rv64_word_ops_tb.sv b/testbench/core_rv64_word_ops_tb.sv index 1b960cc..6a2356c 100644 --- a/testbench/core_rv64_word_ops_tb.sv +++ b/testbench/core_rv64_word_ops_tb.sv @@ -63,6 +63,9 @@ module core_rv64_word_ops_tb; end endtask + logic halted = 1'b0; + always @(posedge clk) if (dut.trap_taken && dut.is_ebreak) halted <= 1'b1; + initial begin #1; // run after wb4_sram's own time-0 init (zero-fill + $readmemh) -- see core_wb_tb.sv @@ -102,10 +105,10 @@ module core_rv64_word_ops_tb; rst = 0; fork - wait (dut.halted === 1'b1); + wait (halted === 1'b1); begin repeat (150) @(posedge clk); - $display("TIMEOUT: dut.halted never went high"); + $display("TIMEOUT: EBREAK trap never fired"); $finish; end join_any diff --git a/testbench/core_upper_imm_tb.sv b/testbench/core_upper_imm_tb.sv index efb597a..70045df 100644 --- a/testbench/core_upper_imm_tb.sv +++ b/testbench/core_upper_imm_tb.sv @@ -54,6 +54,9 @@ module core_upper_imm_tb; end endtask + logic halted = 1'b0; + always @(posedge clk) if (dut.trap_taken && dut.is_ebreak) halted <= 1'b1; + initial begin #1; // run after wb4_sram's own time-0 init (zero-fill + $readmemh) -- see core_wb_tb.sv @@ -75,10 +78,10 @@ module core_upper_imm_tb; rst = 0; fork - wait (dut.halted === 1'b1); + wait (halted === 1'b1); begin repeat (150) @(posedge clk); - $display("TIMEOUT: dut.halted never went high"); + $display("TIMEOUT: EBREAK trap never fired"); $finish; end join_any diff --git a/testbench/core_wb4_sram_harness.sv b/testbench/core_wb4_sram_harness.sv index d7cd31a..229e13f 100644 --- a/testbench/core_wb4_sram_harness.sv +++ b/testbench/core_wb4_sram_harness.sv @@ -25,7 +25,8 @@ module core_wb4_sram_harness #( parameter NUM_WORDS = 4096 ) ( input logic clk, - input logic rst + input logic rst, + input logic i_mtip = 1'b0 ); logic [31:0] wb_addr; @@ -37,7 +38,7 @@ module core_wb4_sram_harness #( .clk(clk), .rst(rst), .wb_addr_o(wb_addr), .wb_dat_o(wb_dat_m2s), .wb_dat_i(wb_dat_s2m), .wb_sel_o(wb_sel), .wb_we_o(wb_we), .wb_cyc_o(wb_cyc), .wb_stb_o(wb_stb), - .wb_ack_i(wb_ack), .wb_err_i(wb_err) + .wb_ack_i(wb_ack), .wb_err_i(wb_err), .i_mtip(i_mtip) ); wb4_sram #(.num_words(NUM_WORDS)) sram0 ( diff --git a/testbench/core_wb_tb.sv b/testbench/core_wb_tb.sv index 11da91f..8b92109 100644 --- a/testbench/core_wb_tb.sv +++ b/testbench/core_wb_tb.sv @@ -65,7 +65,8 @@ module core_wb_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.core0.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" initial begin @@ -102,7 +103,7 @@ module core_wb_tb; @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_TINY, "dut.core0.halted never went high"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_TINY, "EBREAK trap never fired"); check("x1 (addi)", dut.core0.regfile0.gp_registers[1], 64'd5); check("x2 (addi)", dut.core0.regfile0.gp_registers[2], 64'd10); @@ -115,7 +116,7 @@ module core_wb_tb; check("RAM contents at 0x100", {32'b0, dut.sram0.memory[32][31:0]}, 64'd15); check("UART received exactly one byte", {55'b0, dut.uart0.tx_history_count}, 64'd1); check("UART byte is 'H'", {56'b0, dut.uart0.tx_history[0]}, 64'h48); - check("core halted (ebreak reached)", {63'b0, dut.core0.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); $display(""); $display("core_wb_tb: %0d passed, %0d failed", pass_count, fail_count); diff --git a/testbench/core_zicsr_tb.sv b/testbench/core_zicsr_tb.sv index 30ed1f7..8a7ae50 100644 --- a/testbench/core_zicsr_tb.sv +++ b/testbench/core_zicsr_tb.sv @@ -40,17 +40,15 @@ * hand-wired version. * * Exercises, in program order: - * 1. csrrwi write-attempt to the read-only mhartid, then a csrrw read - * to confirm the write never landed. - * 2. csrrs read of misa, checked against the exact hardwired bit + * 1. csrrs read of misa, checked against the exact hardwired bit * pattern. - * 3. csrrw write + csrrs read-back on mscratch (the "vanilla CSR" round + * 2. csrrw write + csrrs read-back on mscratch (the "vanilla CSR" round * trip). - * 4. csrrs-set then csrrc-clear on mscratch, with a read-back check + * 3. csrrs-set then csrrc-clear on mscratch, with a read-back check * after each. - * 5. The rd=x0 case (csrrwi x0, mscratch, ...) still performs its write + * 4. The rd=x0 case (csrrwi x0, mscratch, ...) still performs its write * -- confirmed by a subsequent read. - * 6. That same subsequent read doubles as the rs1=x0 case: a pure read + * 5. That same subsequent read doubles as the rs1=x0 case: a pure read * with zero mutation. Register-value-only checks can't distinguish * "correctly suppressed" from "coincidentally value-preserving" * here (mscratch | 0 == mscratch either way) -- see the plan's @@ -59,23 +57,33 @@ * reads 0 during that specific instruction's S_EXEC cycle, for * genuine confidence rather than a check that would pass even if * suppression were silently broken. - * 7. The immediate-form analogue of item 6: csrrsi x15, mscratch, 0 + * 6. The immediate-form analogue of item 5: csrrsi x15, mscratch, 0 * and csrrci x16, mscratch, 0, both with uimm=0. core.sv's * write-suppress condition for these is a genuinely different * branch (imm_1 == 0, not read_gpr_A_sel == 0 -- see core.sv's * csr_write_suppress mux) than the rs1=x0 register-form case in - * item 6 exercises, so each gets its own white-box dut.core0.csr_we - * monitor at its own PC, same reasoning as item 6. - * 8. A final minstret read matching the exact known instruction count + * item 5 exercises, so each gets its own white-box dut.core0.csr_we + * monitor at its own PC, same reasoning as item 5. + * 7. A final minstret read matching the exact known instruction count * retired so far, plus a post-halt hierarchical check that * minstret's storage ends at the full program length -- including * ebreak itself, which retires (and so still increments minstret) * even though it doesn't write a register. - * 9. ebreak. + * 8. ebreak. * * Deliberately NOT re-proven here (already covered by core_wb_tb.sv): * ordinary ALU/load/store/branch datapath behavior. This test's only * job is Zicsr. + * + * A csrrwi-write-attempt-to-the-read-only-mhartid subtest used to open + * this program, expecting the write to be silently ignored -- retired + * 2026-08-20 once that became a real illegal-instruction trap (see + * design/core.sv's csr_readonly_violation and its own comment for the + * full story). Its replacement lives in its own dedicated file, + * core_csr_readonly_trap_tb.sv, matching this project's "one file per + * concern" convention for a trap/exception scenario rather than folding + * a trap-and-resume sequence into this file's otherwise straight-line + * "vanilla CSR round trip" program. */ module core_zicsr_tb; @@ -90,7 +98,8 @@ module core_zicsr_tb; logic quiet_on_pass = 1'b0; `include "check_lib.sv" - wire halted = dut.core0.halted; + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; `include "halt_wait.sv" /* @@ -102,9 +111,9 @@ module core_zicsr_tb; * declared order in core.sv is {S_FETCH, S_EXEC, S_MEM} = {0, 1, 2} * for its 2-bit enum, hence the literal 2'd1 below. */ - localparam logic [63:0] PC_RS1_X0_CHECK = 64'h34; - localparam logic [63:0] PC_CSRRSI_UIMM0_CHECK = 64'h38; - localparam logic [63:0] PC_CSRRCI_UIMM0_CHECK = 64'h3C; + localparam logic [63:0] PC_RS1_X0_CHECK = 64'h2C; + localparam logic [63:0] PC_CSRRSI_UIMM0_CHECK = 64'h30; + localparam logic [63:0] PC_CSRRCI_UIMM0_CHECK = 64'h34; logic csr_we_sampled; logic csr_we_sample_captured; @@ -135,52 +144,45 @@ module core_zicsr_tb; /* * addr idx instruction notes - * 0x00 0 csrrwi x1, mhartid, 7 write-attempt to read-only CSR; x1 = old mhartid = 0 - * 0x04 1 csrrw x2, mhartid, x0 re-read mhartid; x2 must still be 0 (write from idx0 didn't land) - * 0x08 2 csrrs x3, misa, x0 pure read; x3 = exact hardwired misa pattern - * 0x0C 3 addi x4, x0, 240 x4 = 0xF0, seed value for mscratch - * 0x10 4 csrrw x5, mscratch, x4 write x4 into mscratch; x5 = old mscratch = 0 - * 0x14 5 csrrs x6, mscratch, x0 read back; x6 = 0xF0 - * 0x18 6 addi x7, x0, 15 x7 = 0x0F, set-mask - * 0x1C 7 csrrs x8, mscratch, x7 OR mask in; x8 = old mscratch = 0xF0, mscratch becomes 0xFF - * 0x20 8 csrrs x9, mscratch, x0 read back; x9 = 0xFF - * 0x24 9 addi x10, x0, 240 x10 = 0xF0, clear-mask - * 0x28 10 csrrc x11, mscratch, x10 AND ~mask in; x11 = old mscratch = 0xFF, mscratch becomes 0x0F - * 0x2C 11 csrrs x12, mscratch, x0 read back; x12 = 0x0F - * 0x30 12 csrrwi x0, mscratch, 27 rd=x0 case: mscratch write must still land (mscratch becomes 27) - * 0x34 13 csrrs x13, mscratch, x0 rd=x0 confirm (x13 = 27) AND the rs1=x0 white-box check instruction - * 0x38 14 csrrsi x15, mscratch, 0 uimm=0 case: pure read, no mutation (x15 = 27) AND the CSRRSI white-box check instruction - * 0x3C 15 csrrci x16, mscratch, 0 uimm=0 case: pure read, no mutation (x16 = 27) AND the CSRRCI white-box check instruction - * 0x40 16 csrrs x14, minstret, x0 minstret read; x14 = count of instructions retired strictly before this one = 16 - * 0x44 17 ebreak halts; also retires, so minstret's final storage = 18 + * 0x00 0 csrrs x3, misa, x0 pure read; x3 = exact hardwired misa pattern + * 0x04 1 addi x4, x0, 240 x4 = 0xF0, seed value for mscratch + * 0x08 2 csrrw x5, mscratch, x4 write x4 into mscratch; x5 = old mscratch = 0 + * 0x0C 3 csrrs x6, mscratch, x0 read back; x6 = 0xF0 + * 0x10 4 addi x7, x0, 15 x7 = 0x0F, set-mask + * 0x14 5 csrrs x8, mscratch, x7 OR mask in; x8 = old mscratch = 0xF0, mscratch becomes 0xFF + * 0x18 6 csrrs x9, mscratch, x0 read back; x9 = 0xFF + * 0x1C 7 addi x10, x0, 240 x10 = 0xF0, clear-mask + * 0x20 8 csrrc x11, mscratch, x10 AND ~mask in; x11 = old mscratch = 0xFF, mscratch becomes 0x0F + * 0x24 9 csrrs x12, mscratch, x0 read back; x12 = 0x0F + * 0x28 10 csrrwi x0, mscratch, 27 rd=x0 case: mscratch write must still land (mscratch becomes 27) + * 0x2C 11 csrrs x13, mscratch, x0 rd=x0 confirm (x13 = 27) AND the rs1=x0 white-box check instruction + * 0x30 12 csrrsi x15, mscratch, 0 uimm=0 case: pure read, no mutation (x15 = 27) AND the CSRRSI white-box check instruction + * 0x34 13 csrrci x16, mscratch, 0 uimm=0 case: pure read, no mutation (x16 = 27) AND the CSRRCI white-box check instruction + * 0x38 14 csrrs x14, minstret, x0 minstret read; x14 = count of instructions retired strictly before this one = 14 + * 0x3C 15 ebreak halts; also retires, so minstret's final storage = 16 */ - dut.sram0.memory[0] = {encode_csr(`CSR_MHARTID, 5'd0, `FUNCT3_CSRRW, 5'd2, `OPC_SYSTEM), - encode_csr(`CSR_MHARTID, 5'd7, `FUNCT3_CSRRWI, 5'd1, `OPC_SYSTEM)}; - dut.sram0.memory[1] = {encode_i(32'sd240, 5'd0, 3'b000, 5'd4, `OPC_OP_IMM), + dut.sram0.memory[0] = {encode_i(32'sd240, 5'd0, 3'b000, 5'd4, `OPC_OP_IMM), encode_csr(`CSR_MISA, 5'd0, `FUNCT3_CSRRS, 5'd3, `OPC_SYSTEM)}; - dut.sram0.memory[2] = {encode_csr(`CSR_MSCRATCH, 5'd0, `FUNCT3_CSRRS, 5'd6, `OPC_SYSTEM), + dut.sram0.memory[1] = {encode_csr(`CSR_MSCRATCH, 5'd0, `FUNCT3_CSRRS, 5'd6, `OPC_SYSTEM), encode_csr(`CSR_MSCRATCH, 5'd4, `FUNCT3_CSRRW, 5'd5, `OPC_SYSTEM)}; - dut.sram0.memory[3] = {encode_csr(`CSR_MSCRATCH, 5'd7, `FUNCT3_CSRRS, 5'd8, `OPC_SYSTEM), + dut.sram0.memory[2] = {encode_csr(`CSR_MSCRATCH, 5'd7, `FUNCT3_CSRRS, 5'd8, `OPC_SYSTEM), encode_i(32'sd15, 5'd0, 3'b000, 5'd7, `OPC_OP_IMM)}; - dut.sram0.memory[4] = {encode_i(32'sd240, 5'd0, 3'b000, 5'd10, `OPC_OP_IMM), + dut.sram0.memory[3] = {encode_i(32'sd240, 5'd0, 3'b000, 5'd10, `OPC_OP_IMM), encode_csr(`CSR_MSCRATCH, 5'd0, `FUNCT3_CSRRS, 5'd9, `OPC_SYSTEM)}; - dut.sram0.memory[5] = {encode_csr(`CSR_MSCRATCH, 5'd0, `FUNCT3_CSRRS, 5'd12, `OPC_SYSTEM), + dut.sram0.memory[4] = {encode_csr(`CSR_MSCRATCH, 5'd0, `FUNCT3_CSRRS, 5'd12, `OPC_SYSTEM), encode_csr(`CSR_MSCRATCH, 5'd10, `FUNCT3_CSRRC, 5'd11, `OPC_SYSTEM)}; - dut.sram0.memory[6] = {encode_csr(`CSR_MSCRATCH, 5'd0, `FUNCT3_CSRRS, 5'd13, `OPC_SYSTEM), + dut.sram0.memory[5] = {encode_csr(`CSR_MSCRATCH, 5'd0, `FUNCT3_CSRRS, 5'd13, `OPC_SYSTEM), encode_csr(`CSR_MSCRATCH, 5'd27, `FUNCT3_CSRRWI, 5'd0, `OPC_SYSTEM)}; - dut.sram0.memory[7] = {encode_csr(`CSR_MSCRATCH, 5'd0, `FUNCT3_CSRRCI, 5'd16, `OPC_SYSTEM), + dut.sram0.memory[6] = {encode_csr(`CSR_MSCRATCH, 5'd0, `FUNCT3_CSRRCI, 5'd16, `OPC_SYSTEM), encode_csr(`CSR_MSCRATCH, 5'd0, `FUNCT3_CSRRSI, 5'd15, `OPC_SYSTEM)}; - dut.sram0.memory[8] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, // idx17: ebreak (0x44) - encode_csr(`CSR_MINSTRET, 5'd0, `FUNCT3_CSRRS, 5'd14, `OPC_SYSTEM)}; // idx16: csrrs x14, minstret, x0 (0x40) + dut.sram0.memory[7] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, // idx15: ebreak (0x3C) + encode_csr(`CSR_MINSTRET, 5'd0, `FUNCT3_CSRRS, 5'd14, `OPC_SYSTEM)}; // idx14: csrrs x14, minstret, x0 (0x38) @(posedge clk); #1; rst = 0; - wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "dut.halted never went high"); + wait_halted_or_timeout(`TIMEOUT_CYCLES_SMALL, "EBREAK trap never fired"); - check("x1 (csrrwi rd, old mhartid before write-attempt)", dut.core0.regfile0.gp_registers[1], 64'd0); - check("x2 (csrrw rd, mhartid after write-attempt -- confirms it didn't land)", - dut.core0.regfile0.gp_registers[2], 64'd0); check("x3 (csrrs rd, misa exact bit pattern)", dut.core0.regfile0.gp_registers[3], 64'h8000_0000_0000_0100); check("x5 (csrrw rd, old mscratch before first write)", dut.core0.regfile0.gp_registers[5], 64'd0); @@ -215,11 +217,11 @@ module core_zicsr_tb; {63'b0, csrrci_we_sampled}, 64'd0); check("x14 (minstret read -- count of instructions retired strictly before this one)", - dut.core0.regfile0.gp_registers[14], 64'd16); + dut.core0.regfile0.gp_registers[14], 64'd14); check("minstret final storage -- full program length, including ebreak's own retirement", - dut.core0.csr_file0.minstret_q, 64'd18); + dut.core0.csr_file0.minstret_q, 64'd16); - check("core halted (ebreak reached)", {63'b0, dut.core0.halted}, 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); $display(""); $display("core_zicsr_tb: %0d passed, %0d failed", pass_count, fail_count); diff --git a/testbench/decoder_clint_harness.sv b/testbench/decoder_clint_harness.sv new file mode 100644 index 0000000..a6ada56 --- /dev/null +++ b/testbench/decoder_clint_harness.sv @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +/* + * Module: decoder_clint_harness + * + * Shared "decoder + real slaves, no core" wiring: a real wb_addr_decoder + * fanned out to real wb4_sram (RAM), uart_tx (UART), and clint (CLINT) + * instances. Deliberate omission, not an oversight: this harness is for + * a bus-level test that drives raw Wishbone cycles directly at the + * decoder's own CPU-facing port (via testbench/wb_driver.sv's wb_cycle() + * task, the same way design/wb4_sram_tb.sv and design/wb_addr_decoder_tb.sv + * already do), not a firmware/instruction-level test that assembles and + * runs real RISC-V instructions through core.sv. + * + * NOT a full soc.sv fidelity claim: soc.sv actually routes its RAM leg + * through cache_complex between the decoder and wb4_sram (a 5th module), + * which this harness omits entirely -- sram0 below hangs directly off + * decoder0's ram_* ports. The UART and CLINT legs ARE wired exactly as + * soc.sv has them (both sit downstream of the decoder with no cache in + * between, same as here). So this harness's RAM-window tests only prove + * decoder<->SRAM routing, not the real decoder<->cache<->SRAM path -- + * that combination is only exercised by testbench/core_cache_harness.sv. + * + * Unlike testbench/core_wb4_sram_harness.sv (which only exposes clk/rst, + * since core0 drives the internal Wishbone bus itself), this harness has + * no bus master of its own -- so it re-exposes the decoder's entire + * CPU-facing Wishbone port as its own top-level ports, one-for-one, for + * an external testbench to drive directly. + * + * wb4_sram is instantiated at its default num_words (4096, 32KB), the + * same real memory-map size soc.sv uses -- see wb_addr_decoder.sv's own + * header comment for why the RAM/UART address split is derived from + * that size. + */ +module decoder_clint_harness ( + input logic clk, + input logic rst, + + input logic [31:0] addr_i, + input logic [63:0] dat_i, + output logic [63:0] dat_o, + input logic [7:0] sel_i, + input logic we_i, + input logic cyc_i, + input logic stb_i, + output logic ack_o, + output logic err_o +); + + logic [31:0] ram_addr, uart_addr, clint_addr; + logic [63:0] ram_dat_o, ram_dat_i, uart_dat_o, uart_dat_i, clint_dat_o, clint_dat_i; + logic [7:0] ram_sel, uart_sel, clint_sel; + logic ram_we, ram_cyc, ram_stb, ram_ack, ram_err; + logic uart_we, uart_cyc, uart_stb, uart_ack, uart_err; + logic clint_we, clint_cyc, clint_stb, clint_ack, clint_err; + + wb_addr_decoder decoder0 ( + .clk(clk), .rst(rst), + .addr_i(addr_i), .dat_i(dat_i), .dat_o(dat_o), .sel_i(sel_i), + .we_i(we_i), .cyc_i(cyc_i), .stb_i(stb_i), .ack_o(ack_o), .err_o(err_o), + .ram_addr_o(ram_addr), .ram_dat_o(ram_dat_o), .ram_dat_i(ram_dat_i), + .ram_sel_o(ram_sel), .ram_we_o(ram_we), .ram_cyc_o(ram_cyc), + .ram_stb_o(ram_stb), .ram_ack_i(ram_ack), .ram_err_i(ram_err), + .uart_addr_o(uart_addr), .uart_dat_o(uart_dat_o), .uart_dat_i(uart_dat_i), + .uart_sel_o(uart_sel), .uart_we_o(uart_we), .uart_cyc_o(uart_cyc), + .uart_stb_o(uart_stb), .uart_ack_i(uart_ack), .uart_err_i(uart_err), + .clint_addr_o(clint_addr), .clint_dat_o(clint_dat_o), .clint_dat_i(clint_dat_i), + .clint_sel_o(clint_sel), .clint_we_o(clint_we), .clint_cyc_o(clint_cyc), + .clint_stb_o(clint_stb), .clint_ack_i(clint_ack), .clint_err_i(clint_err), + + /* + * dram_* left explicitly unconnected -- same situation as + * design/soc.sv's own decoder0 instantiation: the DRAM slave + * (verification/taxi/rtl/dram_model.sv) is Verilator-only, and + * this harness is compiled via iverilog (through design/ + * wb_addr_decoder_clint_tb.sv). See design/soc.sv's own comment + * for the full explanation. + */ + /* verilator lint_off PINCONNECTEMPTY */ + .dram_addr_o(), .dram_dat_o(), .dram_dat_i(), + .dram_sel_o(), .dram_we_o(), .dram_cyc_o(), + .dram_stb_o(), .dram_ack_i(), .dram_err_i() + /* verilator lint_on PINCONNECTEMPTY */ + ); + + wb4_sram #(.num_words(4096)) sram0 ( + .clk(clk), .rst(rst), + .addr_i(ram_addr), .dat_i(ram_dat_o), .dat_o(ram_dat_i), .sel_i(ram_sel), + .ack_o(ram_ack), .err_o(ram_err), .cyc_i(ram_cyc), .stb_i(ram_stb), .we_i(ram_we) + ); + + uart_tx uart0 ( + .clk(clk), .rst(rst), + .addr_i(uart_addr), .dat_i(uart_dat_o), .dat_o(uart_dat_i), .sel_i(uart_sel), + .ack_o(uart_ack), .err_o(uart_err), .cyc_i(uart_cyc), .stb_i(uart_stb), .we_i(uart_we) + ); + + clint clint0 ( + .clk(clk), .rst(rst), + .addr_i(clint_addr), .dat_i(clint_dat_o), .dat_o(clint_dat_i), .sel_i(clint_sel), + .ack_o(clint_ack), .err_o(clint_err), .cyc_i(clint_cyc), .stb_i(clint_stb), .we_i(clint_we), + .mtip_o() + ); + +endmodule diff --git a/testbench/halt_wait.sv b/testbench/halt_wait.sv index 2580453..4dc3a0e 100644 --- a/testbench/halt_wait.sv +++ b/testbench/halt_wait.sv @@ -28,12 +28,29 @@ * Required signal contract -- the including module must declare both of * these, with these exact names, before the `include line: * clk -- the free-running clock this testbench drives. - * halted -- a wire tied to the DUT's halt signal, at whatever - * hierarchy depth that particular wiring puts it at, e.g.: - * wire halted = dut.halted; // core instantiated directly - * wire halted = dut.core0.halted; // core reached through a - * // wrapper (soc.sv, - * // core_wb4_sram_harness.sv) + * halted -- a LOCAL STICKY LATCH (not a plain alias -- see below) that + * goes high the cycle the DUT's EBREAK trap fires and then stays high. + * + * `halted` used to alias a real, permanently-latching register inside + * core.sv (`dut.halted` / `dut.core0.halted`). That register was removed + * once EBREAK became a real spec-compliant trap (jumps to mtvec, resumes + * execution -- see design/core.sv's own PC-register comment) instead of a + * permanent freeze, so there is no longer anything to alias. Testbenches + * now detect completion by observing the one-shot `trap_taken && is_ebreak` + * pulse directly (both already-existing core.sv signals, exposed the same + * hierarchical way `halted` used to be) and latching it locally, at + * whatever hierarchy depth the DUT wiring puts it at: + * logic halted = 1'b0; + * always @(posedge clk) + * if (dut.trap_taken && dut.is_ebreak) halted <= 1'b1; + * // or dut.core0.trap_taken && dut.core0.is_ebreak through a wrapper + * // (soc.sv, core_wb4_sram_harness.sv) + * + * A local latch is required (not just `wire halted = dut.trap_taken && + * dut.is_ebreak;`) because trap_taken is a ONE-CYCLE PULSE on the exact + * retiring edge, not a sticky level -- this task's own `wait(halted===1)` + * needs a signal that STAYS high once the trap has fired, the same way the + * old register did. * * Three tiered timeout budgets are provided instead of one global * constant -- one constant would be either too tight for real firmware or @@ -55,7 +72,7 @@ * ... * initial begin * ... - * wait_halted_or_timeout(`TIMEOUT_CYCLES_TINY, "dut.halted never went high"); + * wait_halted_or_timeout(`TIMEOUT_CYCLES_TINY, "EBREAK trap never fired"); * // the task already applies the post-join_any #1 settle delay * // every original call site used -- no need to repeat it here * ... diff --git a/testbench/soc_c_regression_tb.sv b/testbench/soc_c_regression_tb.sv index 595e294..b663fa5 100644 --- a/testbench/soc_c_regression_tb.sv +++ b/testbench/soc_c_regression_tb.sv @@ -54,6 +54,9 @@ module soc_c_regression_tb; end endtask + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; + initial begin #1; // run after wb4_sram's own time-0 init of crt0.hex $readmemh("../firmware/hello_c.hex", dut.sram0.memory); @@ -62,12 +65,12 @@ module soc_c_regression_tb; rst = 0; fork - wait (dut.core0.halted === 1'b1); + wait (halted === 1'b1); begin /* Same generous -O0-C-build budget as soc_tb.sv's own -- * see that file's comment on why. */ repeat (3000) @(posedge clk); - $display("TIMEOUT: dut.core0.halted never went high -- is firmware/hello_c.hex built?"); + $display("TIMEOUT: EBREAK trap never fired -- is firmware/hello_c.hex built?"); $finish; end join_any diff --git a/testbench/soc_interrupt_tb.sv b/testbench/soc_interrupt_tb.sv new file mode 100644 index 0000000..3187925 --- /dev/null +++ b/testbench/soc_interrupt_tb.sv @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: end-to-end machine-timer-interrupt test against the REAL + * soc.sv topology (core+decoder+cache+sram+uart+clint) and a REAL + * toolchain-assembled program (firmware/interrupt_test.s) -- the final + * proof for Milestone 6: everything from clint0's real free-running + * mtime, through wb_addr_decoder's real address routing, through + * csr_file0's real mip/mie/mstatus plumbing, through core0's real + * interrupt-taking FSM, actually works together as wired in soc.sv, not + * just in isolation. + * + * Follows testbench/core_priv_toolchain_tb.sv's exact loading + * convention: `soc dut (.clk(clk), .rst(rst));` (soc_tb.sv's own idiom), + * then a #1-delayed $readmemh override of dut.sram0.memory with the real + * assembled interrupt_test.hex, run AFTER wb4_sram's own time-0 + * zero-fill+crt0.hex init so this test's image wins. + * + * TIMEOUT_CYCLES: generous (5000, matching this project's own + * TIMEOUT_CYCLES_LARGE tier for real firmware) -- every fetch/load in + * this milestone's real soc.sv topology is a genuine Wishbone + * transaction (cache-mediated for RAM, uncached for CLINT/UART), and the + * program's own busy-loop polls CLINT's real, uncacheable mtime once per + * iteration until the real interrupt preempts it. + */ +module soc_interrupt_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + soc dut (.clk(clk), .rst(rst)); + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; + `include "halt_wait.sv" + + /* + * mcause_q snapshot, taken on the SAME edge the terminal ebreak's own + * trap_taken first fires -- NOT read live at check() time. mtvec + * stays armed at interrupt_test.s's own m_trap_handler through + * crt0.s's trailing ebreak, which now really traps and bounces right + * back into it, corrupting mcause_q afterward (same class of bug as + * core_priv_tb.sv/core_priv_toolchain_tb.sv/core_priv_u_ecall_tb.sv -- + * see their own comments). mcause_q's own always_ff writes + * non-blocking on i_trap_taken, so reading it on this identical edge + * sees the PRE-edge value -- the real interrupt's own cause, before + * this ebreak's own entry overwrites it. + */ + logic [63:0] final_mcause_snap; + logic final_state_captured = 1'b0; + always @(posedge clk) if (!final_state_captured && dut.core0.trap_taken && dut.core0.is_ebreak) begin + final_state_captured <= 1'b1; + final_mcause_snap <= dut.core0.csr_file0.mcause_q; + end + + localparam int TIMEOUT_CYCLES_INTERRUPT = 5000; + + initial begin + #1; // run after wb4_sram's own time-0 init (see header comment) + $readmemh("../firmware/interrupt_test.hex", dut.sram0.memory); + + @(posedge clk); #1; + rst = 0; + + wait_halted_or_timeout(TIMEOUT_CYCLES_INTERRUPT, "EBREAK trap never fired -- is firmware/interrupt_test.hex built?"); + + /* + * s1 (x9): marker set inside m_trap_handler -- proves the real + * machine-timer interrupt fired exactly once through the real + * CLINT/decoder/core path (mret's own MIE-restore doesn't + * re-trigger a second entry, since the handler disarms mtimecmp + * before returning -- if it DID re-fire, s1 would still just + * read 1 here, so the real proof of "exactly once" is the + * separate mtvec-only-ever-armed-once reasoning below, not s1's + * own value). + */ + check("s1 (handler marker): real timer interrupt fired", dut.core0.regfile0.gp_registers[9], 64'd1); + + /* + * mcause: standard machine-timer-interrupt encoding (bit 63 set, + * cause value 7) -- confirms the real interrupt_taken path (not + * some other trap) is what actually redirected control. + */ + check("final-state sample was captured (sanity on the monitor itself)", {63'b0, final_state_captured}, 64'd1); + check("mcause == standard machine-timer-interrupt encoding", final_mcause_snap, 64'h8000_0000_0000_0007); + + /* + * Execution genuinely resumed after the handler: interrupt_test.s + * has exactly one ebreak (crt0.s's own trailing one) -- reaching + * `halted` at all is only possible via busy_done's `ret` + * falling back into it, which itself is only reachable by + * genuinely resuming execution in busy_loop after the mret + * (whether the loop needed 0 more iterations or several before + * its own exit condition/bound was met). + */ + check("EBREAK trap fired (real forward progress after the handler)", {63'b0, halted}, 64'd1); + + /* Never bootstrapped away from M -- interrupt_test.s runs entirely in M-mode. */ + check("final current_priv == M", {62'b0, dut.core0.current_priv}, 64'(2'b11)); + + /* + * clint0.mtip_o should have already deasserted by the time the + * program halts (the handler explicitly disarms mtimecmp before + * mret) -- a real, black-box-adjacent confirmation that the + * disarm store actually reached the real CLINT through the real + * bus, not just that the CPU-side mip bit read correctly once. + */ + check("clint0.mtip_o deasserted (handler's disarm store landed)", {63'b0, dut.clint0.mtip_o}, 64'd0); + + $display(""); + $display("soc_interrupt_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("soc_interrupt_tb: FAILURES PRESENT"); + $finish; + end + +endmodule diff --git a/testbench/soc_tb.sv b/testbench/soc_tb.sv index 6669cb8..b927615 100644 --- a/testbench/soc_tb.sv +++ b/testbench/soc_tb.sv @@ -43,12 +43,15 @@ module soc_tb; end endtask + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; + initial begin @(posedge clk); #1; rst = 0; fork - wait (dut.core0.halted === 1'b1); + wait (halted === 1'b1); begin /* * Generous on purpose: unlike the earlier hand-written @@ -59,7 +62,7 @@ module soc_tb; * just margin. */ repeat (3000) @(posedge clk); - $display("TIMEOUT: dut.core0.halted never went high -- is firmware/crt0.hex built?"); + $display("TIMEOUT: EBREAK trap never fired -- is firmware/crt0.hex built?"); $finish; end join_any diff --git a/testbench/soc_uart_rx_tb.sv b/testbench/soc_uart_rx_tb.sv new file mode 100644 index 0000000..71127c5 --- /dev/null +++ b/testbench/soc_uart_rx_tb.sv @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +`include "defaults/defaults.sv" +`include "riscv_encode.sv" + + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: soc, UART RX end-to-end through the real decoder/soc.sv + * addr_i[4] sub-decode + * + * Unlike design/uart_rx_tb.sv (drives uart_rx's own Wishbone port + * directly, in isolation), this instantiates the real top-level + * design/soc.sv and runs real firmware through core0, proving the new + * addr_i[4] TX/RX split introduced in soc.sv (see that file's header) + * is wired correctly end-to-end -- a real load instruction to 0x8010/ + * 0x8018 must actually reach uart_rx0, not uart0, and a real store to + * 0x8000 must still reach uart0 unperturbed. Bytes are injected via + * uart_rx0's push_byte backdoor (dut.uart_rx0.push_byte), the same + * mechanism design/uart_rx_tb.sv uses, just reached one level deeper + * through soc.sv's hierarchy. + * + * Instructions are packed two-per-64-bit-word directly into the SRAM's + * memory[] array, same convention as testbench/core_wb_tb.sv (see that + * file's own header for the full addressing rationale). + */ +module soc_uart_rx_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + soc dut (.clk(clk), .rst(rst)); + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + + logic halted = 1'b0; + always @(posedge clk) if (dut.core0.trap_taken && dut.core0.is_ebreak) halted <= 1'b1; + `include "halt_wait.sv" + + initial begin + #1; // see core_wb_tb.sv's header: run after wb4_sram's own time-0 init + + /* + * addr idx instruction notes + * 0x00 0 lui x8,0x8 x8 = 0x8000 + * 0x04 1 addi x8,x8,0x10 x8 = 0x8010 (RX_DATA) + * 0x08 2 lui x9,0x8 x9 = 0x8000 + * 0x0C 3 addi x9,x9,0x18 x9 = 0x8018 (RX_STATUS) + * 0x10 4 lui x10,0x8 x10 = 0x8000 (TX_DATA) + * 0x14 5 lw x4,0(x9) x4 = RX_STATUS before drain (expect 1) + * 0x18 6 lw x1,0(x8) x1 = RX_DATA pop 1 (expect 'A') + * 0x1C 7 lw x2,0(x8) x2 = RX_DATA pop 2 (expect 'B') + * 0x20 8 lw x3,0(x8) x3 = RX_DATA pop 3 (expect 'C') + * 0x24 9 lw x5,0(x9) x5 = RX_STATUS after drain (expect 0) + * 0x28 10 addi x6,x0,72 x6 = 'H' (0x48) + * 0x2C 11 sw x6,0(x10) TX_DATA <- 'H' -- proves TX still works + * 0x30 12 lw x11,8(x10) x11 = TX_STATUS (0x8008) through the real + * soc.sv mux -- proves the uart_tx_ack-true + * arm of uart_dat_i's mux, never otherwise + * exercised by a bus-level load in this suite + * 0x34 13 ebreak + */ + dut.sram0.memory[0] = {encode_i(32'sh10, 5'd8, 3'b000, 5'd8, `OPC_OP_IMM), + encode_u(20'h8, 5'd8, `OPC_LUI)}; + dut.sram0.memory[1] = {encode_i(32'sh18, 5'd9, 3'b000, 5'd9, `OPC_OP_IMM), + encode_u(20'h8, 5'd9, `OPC_LUI)}; + dut.sram0.memory[2] = {encode_i(32'sd0, 5'd9, 3'b010, 5'd4, `OPC_LOAD), + encode_u(20'h8, 5'd10, `OPC_LUI)}; + dut.sram0.memory[3] = {encode_i(32'sd0, 5'd8, 3'b010, 5'd2, `OPC_LOAD), + encode_i(32'sd0, 5'd8, 3'b010, 5'd1, `OPC_LOAD)}; + dut.sram0.memory[4] = {encode_i(32'sd0, 5'd9, 3'b010, 5'd5, `OPC_LOAD), + encode_i(32'sd0, 5'd8, 3'b010, 5'd3, `OPC_LOAD)}; + dut.sram0.memory[5] = {encode_s(32'sd0, 5'd6, 5'd10, 3'b010, `OPC_STORE), + encode_i(32'sd72, 5'd0, 3'b000, 5'd6, `OPC_OP_IMM)}; + dut.sram0.memory[6] = {{11'b0, 1'b1, 13'b0, `OPC_SYSTEM}, // idx13: ebreak (0x34) + encode_i(32'sd8, 5'd10, 3'b010, 5'd11, `OPC_LOAD)}; // idx12: lw x11,8(x10) (0x30) + + @(posedge clk); #1; + rst = 0; + + /* + * Pushed right as reset drops -- uart_rx0's own reset clears + * rx_head/rx_tail/rx_count, so pushing any earlier would be + * silently wiped. This lands well before core0's first real bus + * transaction (the fetch of the instruction at pc=0) completes, + * so it's deterministically visible by the time the program's + * own RX_STATUS/RX_DATA reads execute. + */ + dut.uart_rx0.push_byte(8'h41); // 'A' + dut.uart_rx0.push_byte(8'h42); // 'B' + dut.uart_rx0.push_byte(8'h43); // 'C' + + wait_halted_or_timeout(`TIMEOUT_CYCLES_TINY, "EBREAK trap never fired"); + + check("x8 (RX_DATA address)", dut.core0.regfile0.gp_registers[8], 64'h8010); + check("x9 (RX_STATUS address)", dut.core0.regfile0.gp_registers[9], 64'h8018); + check("x10 (TX_DATA address)", dut.core0.regfile0.gp_registers[10], 64'h8000); + check("x4 (RX_STATUS before drain, ready)", dut.core0.regfile0.gp_registers[4], 64'd1); + check("x1 (RX_DATA pop 1, real bus route to uart_rx0)", dut.core0.regfile0.gp_registers[1], 64'h41); + check("x2 (RX_DATA pop 2)", dut.core0.regfile0.gp_registers[2], 64'h42); + check("x3 (RX_DATA pop 3)", dut.core0.regfile0.gp_registers[3], 64'h43); + check("x5 (RX_STATUS after drain, empty)", dut.core0.regfile0.gp_registers[5], 64'd0); + check("uart_rx0 queue fully drained", {55'b0, dut.uart_rx0.rx_count}, 64'd0); + check("TX still routes to uart0, unperturbed by RX traffic", + {55'b0, dut.uart0.tx_history_count}, 64'd1); + check("TX byte is 'H'", {56'b0, dut.uart0.tx_history[0]}, 64'h48); + check("x11 (TX_STATUS read through the real soc.sv mux, proves uart_tx_ack-true arm)", + dut.core0.regfile0.gp_registers[11], 64'd1); + check("EBREAK trap fired", {63'b0, halted}, 64'd1); + + $display(""); + $display("soc_uart_rx_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("soc_uart_rx_tb: FAILURES PRESENT"); + $finish; + end + +endmodule diff --git a/testbench/state_reached_monitor.sv b/testbench/state_reached_monitor.sv new file mode 100644 index 0000000..8354a4d --- /dev/null +++ b/testbench/state_reached_monitor.sv @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +/* + * Module: state_reached_monitor + * + * A real module, not `include-able -- same reasoning as + * pc_trigger_sample_monitor.sv: it's a standing concurrent process, not + * a one-shot task a caller invokes at a chosen point. + * + * Generalizes a narrower pattern than pc_trigger_sample_monitor.sv + * covers: "did the DUT ever reach this state, at any PC" -- no PC + * target, no sampled signal value, just a sticky "was it ever true" + * flag. pc_trigger_sample_monitor.sv can't express this directly (its + * i_pc_target comparison is mandatory, not optional), and generalizing + * it to support an "ignore pc" mode would be a real change to an + * existing, already-used shared module for a need it was never meant + * to cover -- a new, narrower module is the correct-sized fix. + * + * Introduced to deduplicate a hand-written `amo_write_entered` latch + * that testbench/core_bus_fault_trap_tb.sv and + * testbench/core_bus_fault_cache_tb.sv each independently wrote (both + * proving an AMO read-phase fault never lets the FSM proceed into + * S_AMO_WRITE) -- found via code review of the bus-error-trapping + * milestone: two copies of the same 5-line block is exactly the + * duplication pc_trigger_sample_monitor.sv's own header cites as the + * thing worth generalizing away from. + * + * Sampled on negedge clk, same race-free reasoning as + * pc_trigger_sample_monitor.sv: state is stable for the whole cycle + * once it settles after a synchronous DUT's posedge, so negedge is a + * safe place to look without racing the DUT's own state register. + * + * Parameters: + * STATE_WIDTH: Bit width of the state/target comparison (default = 3, + * matching core.sv's current state_t enum). + * + * Input ports: + * clk: Clock signal the trigger is sampled against (negedge). + * i_state: The DUT's current state (e.g. dut.core0.state). + * i_state_target: The state value that counts as "reached" + * (e.g. dut.core0.S_AMO_WRITE). + * + * Output ports: + * o_reached: High from the first cycle i_state == i_state_target is + * ever seen, for the rest of the simulation. Never clears. + * + * Worked example -- collapses a hand-written block like: + * logic amo_write_entered = 1'b0; + * always @(posedge clk) begin + * if (dut.core0.state == dut.core0.S_AMO_WRITE) + * amo_write_entered <= 1'b1; + * end + * into: + * logic amo_write_entered; + * state_reached_monitor amo_write_monitor ( + * .clk(clk), .i_state(dut.core0.state), + * .i_state_target(dut.core0.S_AMO_WRITE), .o_reached(amo_write_entered) + * ); + */ +module state_reached_monitor #( + parameter STATE_WIDTH = 3 +) ( + input logic clk, + input logic [(STATE_WIDTH - 1):0] i_state, + input logic [(STATE_WIDTH - 1):0] i_state_target, + output logic o_reached +); + /* + * Simulation-only zero-fill, same reasoning and same idiom as + * pc_trigger_sample_monitor.sv/register_file.sv/wb4_sram.sv: keeps + * "not reached yet" a clean, checkable 0 instead of 'X. + */ + initial begin + o_reached = 1'b0; + end + + always @(negedge clk) begin + if (i_state == i_state_target) + o_reached = 1'b1; + end +endmodule: state_reached_monitor + + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/third_party/taxi b/third_party/taxi new file mode 160000 index 0000000..c719264 --- /dev/null +++ b/third_party/taxi @@ -0,0 +1 @@ +Subproject commit c71926499b47aa7e3dd963b994bd040319d596f5 diff --git a/verification/riscv-arch-test/act_results.log b/verification/riscv-arch-test/act_results.log index 10331c4..6dc394f 100644 --- a/verification/riscv-arch-test/act_results.log +++ b/verification/riscv-arch-test/act_results.log @@ -1,11 +1,12 @@ -ExceptionsZc-00: ACT_RESULT: FAIL +ExceptionsZc-00: ACT_RESULT: PASS ExceptionsZicboS-00: ACT_RESULT: PASS ExceptionsZicboU-00: ACT_RESULT: PASS S-00: ACT_RESULT: FAIL +sv_mstatus_tvm_test: ACT_RESULT: FAIL Svbare_Smode: ACT_RESULT: PASS Svbare_Umode: ACT_RESULT: PASS Svbare_mstatus_mprv: ACT_RESULT: PASS -U-00: ACT_RESULT: UNKNOWN (tohost=0x0000000000000000) +U-00: ACT_RESULT: PASS I-add-00: ACT_RESULT: PASS I-addi-00: ACT_RESULT: PASS I-addiw-00: ACT_RESULT: PASS diff --git a/verification/riscv-arch-test/quantiumv-rv64im/quantiumv-rv64im.yaml b/verification/riscv-arch-test/quantiumv-rv64im/quantiumv-rv64im.yaml index 91622f1..a15c1bc 100644 --- a/verification/riscv-arch-test/quantiumv-rv64im/quantiumv-rv64im.yaml +++ b/verification/riscv-arch-test/quantiumv-rv64im/quantiumv-rv64im.yaml @@ -53,12 +53,25 @@ params: TRAP_ON_RESERVED_INSTRUCTION: true TRAP_ON_UNIMPLEMENTED_CSR: false # csr_file.sv: unmapped CSR reads 0, writes silently ignored, no trap (deliberate, Zicsr-milestone decision) REPORT_VA_IN_MTVAL_ON_BREAKPOINT: false - REPORT_VA_IN_MTVAL_ON_LOAD_MISALIGNED: false - REPORT_VA_IN_MTVAL_ON_STORE_AMO_MISALIGNED: false + # Confirmed true via direct trace comparison against sail_riscv_sim (2026-08-17, root-causing + # ACT4's ExceptionsZc-00 failure): core.sv's mem_load_misaligned/mem_store_misaligned trap arm + # unconditionally sets trap_val = mem_paddr (the faulting address) -- this was already true + # before this fix, just never reflected here. With this left `false`, the Sail reference model + # (driven by sail.json's matching xtval_nonzero fields) computed mtval=0 for every misaligned + # load/store, while every ACT4 self-check comparison against our core's real (nonzero) mtval + # failed -- confirmed via `--trace-reg` showing `CSR mtval (0x343) -> 0x0000000000000000` on + # Sail's side at the exact instruction where core.sv would report the real address instead. + REPORT_VA_IN_MTVAL_ON_LOAD_MISALIGNED: true + REPORT_VA_IN_MTVAL_ON_STORE_AMO_MISALIGNED: true REPORT_VA_IN_MTVAL_ON_INSTRUCTION_MISALIGNED: false - REPORT_VA_IN_MTVAL_ON_LOAD_ACCESS_FAULT: false - REPORT_VA_IN_MTVAL_ON_STORE_AMO_ACCESS_FAULT: false - REPORT_VA_IN_MTVAL_ON_INSTRUCTION_ACCESS_FAULT: false + # Bus-error trapping (2026-08-17): core.sv's mem_load_access_fault/mem_store_access_fault/ + # fetch_fault_q all report the real faulting address in trap_val (mem_paddr/amo_addr_q/pc, + # same convention as the misaligned-access causes above) -- these three flags flip to true + # for the same reason those did, so the declared config matches actual RTL behavior from the + # start rather than needing a second, separate root-cause pass later. + REPORT_VA_IN_MTVAL_ON_LOAD_ACCESS_FAULT: true + REPORT_VA_IN_MTVAL_ON_STORE_AMO_ACCESS_FAULT: true + REPORT_VA_IN_MTVAL_ON_INSTRUCTION_ACCESS_FAULT: true REPORT_VA_IN_MTVAL_ON_LOAD_PAGE_FAULT: false REPORT_VA_IN_MTVAL_ON_STORE_AMO_PAGE_FAULT: false REPORT_VA_IN_MTVAL_ON_INSTRUCTION_PAGE_FAULT: false @@ -116,12 +129,17 @@ params: SXLEN: [64] TRAP_ON_ECALL_FROM_S: true REPORT_VA_IN_STVAL_ON_BREAKPOINT: false - REPORT_VA_IN_STVAL_ON_LOAD_MISALIGNED: false - REPORT_VA_IN_STVAL_ON_STORE_AMO_MISALIGNED: false + # Same fix as REPORT_VA_IN_MTVAL_ON_LOAD/STORE_AMO_MISALIGNED above -- csr_file.sv routes the + # SAME i_trap_val wire to stval_q whenever i_trap_to_s is set (core.sv computes trap_val once, + # unconditional on trap target), so an S-mode-targeted misaligned load/store reports the real + # faulting address exactly like an M-mode one does. + REPORT_VA_IN_STVAL_ON_LOAD_MISALIGNED: true + REPORT_VA_IN_STVAL_ON_STORE_AMO_MISALIGNED: true REPORT_VA_IN_STVAL_ON_INSTRUCTION_MISALIGNED: false - REPORT_VA_IN_STVAL_ON_LOAD_ACCESS_FAULT: false - REPORT_VA_IN_STVAL_ON_STORE_AMO_ACCESS_FAULT: false - REPORT_VA_IN_STVAL_ON_INSTRUCTION_ACCESS_FAULT: false + # Same fix as REPORT_VA_IN_MTVAL_ON_*_ACCESS_FAULT above -- same shared i_trap_val wire. + REPORT_VA_IN_STVAL_ON_LOAD_ACCESS_FAULT: true + REPORT_VA_IN_STVAL_ON_STORE_AMO_ACCESS_FAULT: true + REPORT_VA_IN_STVAL_ON_INSTRUCTION_ACCESS_FAULT: true REPORT_VA_IN_STVAL_ON_LOAD_PAGE_FAULT: false REPORT_VA_IN_STVAL_ON_STORE_AMO_PAGE_FAULT: false REPORT_VA_IN_STVAL_ON_INSTRUCTION_PAGE_FAULT: false diff --git a/verification/riscv-arch-test/quantiumv-rv64im/sail.json b/verification/riscv-arch-test/quantiumv-rv64im/sail.json index db81e65..17e2219 100644 --- a/verification/riscv-arch-test/quantiumv-rv64im/sail.json +++ b/verification/riscv-arch-test/quantiumv-rv64im/sail.json @@ -17,14 +17,26 @@ "illegal_instruction": true, // core.sv: trap_val = raw instruction on illegal-instr "software_breakpoint": false, "hardware_breakpoint": false, - "load_address_misaligned": false, - "load_access_fault": false, + // true: core.sv's mem_load_misaligned/mem_store_misaligned trap arm always sets + // trap_val = mem_paddr (the faulting address), routed to mtval or stval depending on + // trap target. Confirmed root cause of ACT4's ExceptionsZc-00 failure (2026-08-17): + // with this false, sail_riscv_sim computed mtval=0 for every misaligned load/store, + // mismatching our core's real address on every single self-check comparison -- see + // quantiumv-rv64im.yaml's matching REPORT_VA_IN_MTVAL_ON_LOAD/STORE_AMO_MISALIGNED + // comment for the full trace evidence. + "load_address_misaligned": true, + // true: bus-error trapping (2026-08-17) -- core.sv's mem_load_access_fault/ + // mem_store_access_fault/fetch_fault_q all set trap_val to the real faulting address + // (mem_paddr, or amo_addr_q for an AMO write-phase fault, or pc for a fetch fault) -- + // same convention as the misaligned causes just above, flipped at the same time to + // avoid repeating the exact ExceptionsZc-00 class of config/RTL mismatch a second time. + "load_access_fault": true, "load_page_fault": false, - "samo_address_misaligned": false, - "samo_access_fault": false, + "samo_address_misaligned": true, + "samo_access_fault": true, "samo_page_fault": false, "fetch_address_misaligned": false, - "fetch_access_fault": false, + "fetch_access_fault": true, "fetch_page_fault": false, "software_check": false, "reserved_exceptions": false diff --git a/verification/riscv-arch-test/run_act_tests.sh b/verification/riscv-arch-test/run_act_tests.sh index ff6315f..745ed41 100755 --- a/verification/riscv-arch-test/run_act_tests.sh +++ b/verification/riscv-arch-test/run_act_tests.sh @@ -69,7 +69,16 @@ for elf in "${elfs[@]}"; do continue fi - out=$(vvp "$VVP_BIN" "+HEXFILE=$hexfile" "+TOHOST_ADDR=$tohost_addr" "+TIMEOUT=1000000" 2>&1) + # 5000000 (up from 1000000): sized for the old EBREAK-freezes-forever + # behavior, when every test hit its own first ebreak (if any) near- + # instantly. Now that EBREAK is a real, resumable trap, tests that + # deliberately exercise it mid-test (e.g. priv/U/U-00's own + # uprivinst_ebreak subcase) run to their genuine, much later + # completion instead -- confirmed U-00 itself needs ~1.28M cycles for + # real completion (thousands of subcases, not just the first one) -- + # 5x that gives real headroom, not just margin, matching this + # project's own "generous, not tightest-fit" timeout convention. + out=$(vvp "$VVP_BIN" "+HEXFILE=$hexfile" "+TOHOST_ADDR=$tohost_addr" "+TIMEOUT=5000000" 2>&1) result_line=$(printf '%s\n' "$out" | grep -E "^ACT_RESULT:|^TIMEOUT:" | head -1) if [ -z "$result_line" ]; then diff --git a/verification/riscv-formal/quantiumv/README.md b/verification/riscv-formal/quantiumv/README.md index 5f66c99..54d4c00 100644 --- a/verification/riscv-formal/quantiumv/README.md +++ b/verification/riscv-formal/quantiumv/README.md @@ -1,4 +1,4 @@ -# riscv-formal integration (started 2026-08-11, 56/56 isa=rv64i checks PASS; AMO 18/18 and C-extension 30/30 now added, M-extension in progress, first CSR trace ports added) +# riscv-formal integration (started 2026-08-11, 56/56 isa=rv64i checks PASS; AMO 18/18 and C-extension 30/30 now added, M-extension in progress, first CSR trace ports added, rvfi_intr wired for real 2026-08-19) Formal verification via [riscv-formal](https://github.com/YosysHQ/riscv-formal) (Yosys + SymbiYosys + a SAT/BMC solver), proving ISA correctness exhaustively @@ -687,6 +687,134 @@ here per the project owner's explicit direction rather than chasing a heavier trap-suppressing `[assume]` (which would meaningfully weaken what the check proves, not just scope it) or a from-scratch custom checker. +## Resolved: `rvfi_intr` wired for real (2026-08-19) — mechanical PC-discontinuity detection, not semantic trap-tracking + +`rvfi_intr` had been hardwired to `1'b0` since the interrupt-taking milestone +(CLINT/timer interrupts, see [[interrupt-taking-milestone]]), deferred as +"out of scope" at the time. Per upstream riscv-formal's own spec +(`docs/source/rvfi.rst`): it "must be set for the first instruction that is +part of a trap handler, i.e. an instruction that has a `rvfi_pc_rdata` that +does not match the `rvfi_pc_wdata` of the previous instruction." Confirmed +via direct grep of every check-family template in `~/riscv-formal/checks/` +that exactly two families consume it — `pc_fwd`/`pc_bwd` — both already +enabled in this project's `checks.cfg`; every other family (insn, reg, +unique, causal, cover, csrc_any, ill) has zero dependency on it. + +**Implemented mechanically, not semantically** — compare each retirement's +own `pc` against the immediately-preceding valid retirement's own `next_pc`, +not "was this preceded by `trap_taken`/`interrupt_taken`": +```systemverilog +logic [63:0] rvfi_prev_pc_wdata_q; +always_ff @(posedge clk) begin + if (rst) rvfi_prev_pc_wdata_q <= '0; + else if (commit_now) rvfi_prev_pc_wdata_q <= next_pc; +end +assign rvfi_intr = commit_now && (pc != rvfi_prev_pc_wdata_q); +``` +Why mechanical: tracing `next_pc`'s own mux shows `trap_taken` already has +top priority (`trap_taken ? trap_vector : ...`), so a synchronous +exception's handler-entry PC chain is *already* naturally consistent in +this design — no discontinuity, correctly `rvfi_intr=0`. A semantic "was I +preceded by any redirect" check would needlessly over-relax an +already-tight, already-correctly-passing property for that case. +`interrupt_taken`, by contrast, bypasses `next_pc` entirely via a separate +PC-register arm (`else if (interrupt_taken) pc <= trap_vector;`) — a +genuine discontinuity the mechanical definition catches automatically, with +no need to enumerate which mechanisms can cause one (robust to any future +redirect mechanism, e.g. whatever Sv39 eventually adds). + +**A second, non-obvious gap found during research**: `wrapper.sv`'s `core +uut (...)` instantiation never connected `i_mtip` at all — it floated to +its own ANSI default (`1'b0`), meaning `interrupt_taken` could never fire +in any generated check. Wiring `rvfi_intr` correctly in `core.sv` alone +would have been dead code from the formal model's perspective. Fixed by +adding a free, unconstrained `` `rvformal_rand_reg i_mtip `` (same +convention this file already uses for `wb_ack`/`wb_err`, matching +riscv-formal's own `cores/nerv/wrapper.sv` precedent for a free interrupt +input). + +**Verified directly, not just lint-clean**: `pc_fwd_ch0` and `pc_bwd_ch0` +(the only two families that consume `rvfi_intr`) both `Status: passed` / +`DONE (PASS, rc=0)` against the real, modified `core.sv`/`wrapper.sv`, at +the configured depth (15), via `bitwuzla`. Spot-checked `insn_add_ch0` (the +toolchain smoke test) and `ill_ch0` — both still PASS, confirming the +newly-reachable interrupt path (via the now-free `i_mtip`) doesn't +introduce spurious counterexamples in families that never touched +interrupts before. `reg_ch0` (the one check in this suite that already +needed `boolector` over `bitwuzla`, see its own "Resolved finding" above) +also still `Status: passed` with the free `i_mtip` in play — 613s (10m13s), +actually faster than the previously-documented ~18-minute (1090s) baseline +despite the larger state space, ordinary solver-time variance rather than +a sign the extra freedom made this particular check easier or harder in +any structural way. + +**Scope boundary, deliberately not attempted**: no other currently- +hardwired RVFI signal touched (`rvfi_halt` stays 0, no graceful-halt model +exists), no new check families added to `checks.cfg`, no special modeling +of `mideleg`-delegated S-mode interrupt scenarios (the free `i_mtip` input +already lets the solver explore both M- and S-target routing without extra +wiring, since `interrupt_to_s` is a pure function of already-free/derived +state). + +## WSL toolchain fully rebuilt from source (2026-08-19) — the nix-store artifacts this doc's own "Environment setup" section originally pointed at were garbage-collected + +Returning to this integration to wire `rvfi_intr` found `sby`, `bitwuzla`, +and the `yosys-slang` plugin all broken — each referenced a `/nix/store` +path that no longer existed in this WSL environment (garbage-collected +since the 56/56-PASS milestone). `yosys`, `z3`, and `boolector` (the latter +already built from source in a prior session, see its own "Resolved +finding" above) still worked. Build tools were available (git/cmake/g++/ +python3, GitHub network access) — rebuilt all three from source, entirely +in user-space (no root/sudo in this environment): + +- **`sby`**: `git clone https://github.com/YosysHQ/sby ~/sby-src && cd + ~/sby-src && make install PREFIX=/home/potato/.local` against the + existing Yosys install. Lowest-risk of the three, worked on the first + try. Now `SBY v0.68` at `~/.local/bin/sby`. +- **`yosys-slang`**: `git clone --recursive + https://github.com/povik/yosys-slang ~/yosys-slang-src`, `cmake -B build + .` (auto-detected the Yosys install via `yosys-config`), `make -C build + -j"$(nproc)"`. Produced `~/yosys-slang-src/build/slang.so`. This + environment's `checks.cfg` `[script-defines]` plugin path was updated to + point here (was a dangling nix-store path). +- **`bitwuzla`**: `git clone https://github.com/bitwuzla/bitwuzla + ~/bitwuzla-src`. Needed GMP/MPFR dev headers, not present system-wide and + not installable via `apt install` (no root) — worked around via + `apt-get download libgmp-dev libmpfr-dev` (download-only, needs no root) + + `dpkg-deb -x` extraction, then hand-patched the extracted `.pc` files' + `prefix`/`libdir`/`Cflags` (Ubuntu's multiarch header layout needs an + extra `-I.../x86_64-linux-gnu` the stock `.pc` doesn't include) and + recreated two broken relative symlinks (`libgmp.so`/`libmpfr.so`) as + absolute links to the real system runtime `.so` files. Built via Meson + (`pip3 install --user --break-system-packages meson ninja`, then + `PKG_CONFIG_PATH= python3 ./configure.py && + ninja -C build`) — auto-fetched `cadical`/`symfpu` as fallback + subprojects. Produced a working `bitwuzla --version` → + `0.9.1-dev-main@dfc8b5f`, symlinked to `~/.local/bin/bitwuzla`. + +**A real, non-obvious CLI compatibility break**: the freshly-built +`bitwuzla` HEAD dropped the `--smt2`/`-i` flags that `yosys-smtbmc`'s own +bundled `smtio.py` still hardcodes for both `boolector` and `bitwuzla` in +one shared code branch (confirmed via `bitwuzla --smt2 -i` → +`invalid option '--smt2'`). Modern `bitwuzla` reads SMT2 by default and +handles incremental multi-`(check-sat)` scripts with no flag needed at +all. `smtio.py` lives in the Yosys install (root-owned in this +environment, not editable directly) — fixed non-destructively via a +`PYTHONPATH`-shadowed patched copy: `yosys-smtbmc` appends (not prepends) +the real yosys share dir to `sys.path`, so a `PYTHONPATH`-supplied +directory is found first by Python's import mechanism. The patch splits +the shared `if self.solver in ["boolector", "bitwuzla"]:` branch into two +separate `if` blocks — `boolector` keeps `--smt2 -i` (still required, +confirmed via `boolector --help`), `bitwuzla` drops both flags entirely. +Smoke-tested end-to-end (`insn_add_ch0` PASS) before relying on it for +anything else. + +None of these rebuilt artifacts (`~/sby-src`, `~/yosys-slang-src`, +`~/bitwuzla-src`, the extracted GMP/MPFR `.deb` contents, the patched +`smtio.py` copy) are part of this git repo — see "Environment setup" below +for the full, updated build recipe for a future environment hitting the +same nix-store garbage-collection issue. + ## Next steps, roughly in order 1. Resolve or accept-as-documented-limitation the M-extension @@ -743,24 +871,72 @@ verilator --lint-only -Wall -Idesign -Itestbench --top-module soc \ design/wb4_sram.sv design/wb_addr_decoder.sv design/core.sv design/soc.sv ``` -Yosys 0.61 and z3 were already present. **`sby`** (SymbiYosys) is a -*separate* repo from Yosys (not a pip package, not bundled) — a `sby` -binary happened to already exist as a nix-store artifact at -`/nix/store/5f4l0kgczm4kn3r2rm09arby2g44fmj4-yosys-sby-0.62/bin/sby`; it -needs `PYTHONPATH` pointed at that same package's `share/yosys/python3/` -directory to actually run (its own sub-modules like `sby_cmdline.py` live -there) — a plain symlink onto `PATH` is NOT enough, needs a real wrapper -script exporting `PYTHONPATH` first (see `~/.local/bin/sby`). If that -nix-store path isn't present in a future environment: `git clone -https://github.com/YosysHQ/sby && make install` against an existing Yosys. -**`yosys-slang`** (required, see above): built plugin at -`/nix/store/07xn6zd11qvkp8h65gwycfisr3x9hk4f-yosys-slang/share/yosys/plugins/slang.so` -in this environment. **`bitwuzla`** (required solver, see above): binary at -`/nix/store/zrcmy1ak6dzhjc48nw3iyi49y34zkj3f-bitwuzla-unstable-2022-10-03/bin/bitwuzla`, -symlinked to `~/.local/bin/bitwuzla`. All three nix-store paths are -environment-specific — if they're not present in a future environment, -search `/nix/store` for the package name, or build from source -(`YosysHQ/sby`, `povik/yosys-slang`, `bitwuzla/bitwuzla` upstream repos). +Yosys 0.61 and z3 were already present. **As of 2026-08-19, none of +`sby`/`yosys-slang`/`bitwuzla` came pre-built in this environment anymore** +— an earlier session found all three as ready-made nix-store artifacts +(paths below, kept as a historical pointer only — confirmed dangling/ +garbage-collected on 2026-08-19), but a future environment may have either +situation. Check for nix-store artifacts first (`find /nix/store -iname +'*sby*' -o -iname '*slang*' -o -iname '*bitwuzla*'`); if genuinely absent, +build all three from source, entirely in user-space (no root needed for +any of this): + +```sh +# sby (SymbiYosys) -- separate repo from Yosys, not a pip package +git clone https://github.com/YosysHQ/sby ~/sby-src +cd ~/sby-src && make install PREFIX=/home/potato/.local # -> ~/.local/bin/sby + +# yosys-slang -- required SystemVerilog frontend plugin, see above +git clone --recursive https://github.com/povik/yosys-slang ~/yosys-slang-src +cd ~/yosys-slang-src && cmake -B build . && make -C build -j"$(nproc)" +# -> ~/yosys-slang-src/build/slang.so ; point checks.cfg's [script-defines] at this + +# bitwuzla -- required solver, see above. Needs GMP/MPFR dev headers; if +# `apt install libgmp-dev libmpfr-dev` isn't available (no root), extract +# them without root instead: +mkdir -p ~/build-deps && cd ~/build-deps +apt-get download libgmp-dev libmpfr-dev +for f in *.deb; do dpkg-deb -x "$f" extracted/; done +# patch extracted/usr/lib/x86_64-linux-gnu/pkgconfig/{gmp,mpfr,gmpxx}.pc: +# prefix=<...>/build-deps/extracted/usr +# libdir=<...>/build-deps/extracted/usr/lib/x86_64-linux-gnu +# gmp.pc's Cflags also needs -I${includedir}/x86_64-linux-gnu appended +# (Ubuntu multiarch header layout) +# recreate extracted/usr/lib/x86_64-linux-gnu/lib{gmp,mpfr}.so as ABSOLUTE +# symlinks to the real system runtime .so files (the extracted relative +# symlinks point at a versioned .so that dpkg-deb -x never extracted) +pip3 install --user --break-system-packages meson ninja +git clone https://github.com/bitwuzla/bitwuzla ~/bitwuzla-src +cd ~/bitwuzla-src +PKG_CONFIG_PATH=~/build-deps/extracted/usr/lib/x86_64-linux-gnu/pkgconfig \ + python3 ./configure.py +ninja -C build -j"$(nproc)" # -> ~/bitwuzla-src/build/src/main/bitwuzla +ln -sf ~/bitwuzla-src/build/src/main/bitwuzla ~/.local/bin/bitwuzla +``` + +**A modern from-source `bitwuzla` needs one more fix to actually run under +`sby`**: it dropped the `--smt2`/`-i` CLI flags `yosys-smtbmc`'s own +bundled `smtio.py` still hardcodes (confirmed: `bitwuzla --smt2 -i` → +`invalid option '--smt2'`; modern bitwuzla reads SMT2 by default, no flags +needed). `smtio.py` lives inside the Yosys install (root-owned, not +editable directly in this environment) — fix via a `PYTHONPATH`-shadowed +patched copy, since `yosys-smtbmc` *appends* (not prepends) the real yosys +share dir to `sys.path`: +```sh +mkdir -p ~/yosys-py-patch +cp "$(yosys-config --datdir)/python3/smtio.py" ~/yosys-py-patch/ +# edit ~/yosys-py-patch/smtio.py: split the shared +# `if self.solver in ["boolector", "bitwuzla"]:` branch into two +# separate `if self.solver == "boolector":` / `if self.solver == "bitwuzla":` +# blocks -- keep --smt2/-i for boolector (still required), drop both for +# bitwuzla (self.popen_vargs = [self.solver] + self.solver_opts). +export PYTHONPATH=~/yosys-py-patch # set this before every sby invocation +``` +Old nix-store paths (dangling as of 2026-08-19, kept for reference only): +`sby` at `/nix/store/5f4l0kgczm4kn3r2rm09arby2g44fmj4-yosys-sby-0.62/`, +`yosys-slang` at +`/nix/store/07xn6zd11qvkp8h65gwycfisr3x9hk4f-yosys-slang/`, `bitwuzla` at +`/nix/store/zrcmy1ak6dzhjc48nw3iyi49y34zkj3f-bitwuzla-unstable-2022-10-03/`. **`boolector`** (required only for `reg_ch0`, see its own "Resolved finding" above): unlike the three above, NOT a pre-built nix-store @@ -784,6 +960,10 @@ To (re-)generate checks and try running one, from `~/riscv-formal/cores/`: ``` mkdir -p quantiumv/design && cp /wrapper.sv /checks.cfg quantiumv/ cp /mnt/c/Users/Potato/Desktop/core/design/{alu,decoder,register_file,csr_file,divider,c_expand,core}.sv quantiumv/design/ +cp -r /mnt/c/Users/Potato/Desktop/core/design/defaults quantiumv/design/ # required -- every + # design file `includes defaults/*.sv; omitting this + # produces a real elaboration failure ("unknown macro + # or compiler directive"), not an obviously-missing-file error cd quantiumv && python3 ../../checks/genchecks.py cd checks && ulimit -v 8000000 && timeout 300 sby -f insn_add_ch0.sby ``` diff --git a/verification/riscv-formal/quantiumv/checks.cfg b/verification/riscv-formal/quantiumv/checks.cfg index 4b9c2a1..91f3260 100644 --- a/verification/riscv-formal/quantiumv/checks.cfg +++ b/verification/riscv-formal/quantiumv/checks.cfg @@ -96,7 +96,7 @@ always @* if (!reset) cover (channel[0].cnt_insns == 2); # README.md's "Environment setup" section for how to locate or rebuild it # elsewhere; update this one line to match. [script-defines] -plugin -i /nix/store/07xn6zd11qvkp8h65gwycfisr3x9hk4f-yosys-slang/share/yosys/plugins/slang.so +plugin -i /home/potato/yosys-slang-src/build/slang.so # --single-unit: without it, each file gets its OWN independent macro # namespace (slang's default) -- core.sv relies on decoder.sv's own diff --git a/verification/riscv-formal/quantiumv/wrapper.sv b/verification/riscv-formal/quantiumv/wrapper.sv index f98045b..5a0858b 100644 --- a/verification/riscv-formal/quantiumv/wrapper.sv +++ b/verification/riscv-formal/quantiumv/wrapper.sv @@ -39,6 +39,16 @@ module rvfi_wrapper ( (* keep *) wire wb_stb; (* keep *) `rvformal_rand_reg wb_ack; (* keep *) `rvformal_rand_reg wb_err; + // Free/unconstrained -- lets the solver assert the timer-interrupt-pending + // input on any cycle, same convention as wb_ack/wb_err above and matching + // riscv-formal's own cores/nerv/wrapper.sv precedent for a free interrupt- + // pending input (irq, wired the identical way). Without this, i_mtip + // floats to core.sv's own ANSI default (1'b0) since core uut's own + // instantiation would otherwise omit it entirely -- meaning + // interrupt_taken could never fire in ANY generated check, and the + // real rvfi_intr logic in core.sv would be permanently dead code from + // this formal model's perspective. + (* keep *) `rvformal_rand_reg i_mtip; core uut ( .clk(clock), @@ -53,6 +63,7 @@ module rvfi_wrapper ( .wb_stb_o(wb_stb), .wb_ack_i(wb_ack), .wb_err_i(wb_err), + .i_mtip(i_mtip), `RVFI_CONN ); diff --git a/verification/taxi/README.md b/verification/taxi/README.md new file mode 100644 index 0000000..9f3ca8e --- /dev/null +++ b/verification/taxi/README.md @@ -0,0 +1,140 @@ +# taxi (AXI4/AXI4-Lite IP) -- vendored 2026-08-18, Verilator toolchain fork + +[taxi](https://github.com/fpganinja/taxi) (Alex Forencich, successor to +`verilog-axi`/`verilog-axis`) is vendored as a git submodule at +`third_party/taxi` for real AXI4/AXI4-Lite fabric IP (crossbar, RAM slave, +register/adapter modules) -- the intended backbone for eventually testing a +real DRAM model over AXI. Vendored, not just referenced externally, unlike +this project's other external-tool integrations +(`verification/riscv-arch-test/`, `verification/riscv-formal/`) -- those are +pure verification frameworks that never ship as part of the design; taxi's +AXI IP is meant to become real, synthesizable RTL inside this SoC, so its +exact source needs to travel with the repo, not be reproduced from a +separately-documented clone step. + +**License, already accepted (see project memory `bus-protocol-decision`):** +taxi's interface header files (`taxi_axi_if.sv` etc.) are MIT; its functional +IP (`taxi_axi_ram.sv`, `taxi_axi_crossbar.sv`, etc.) is **CERN-OHL-S-2.0** +(strongly reciprocal -- distributing a product built on it obligates +releasing the complete combined design's source). This repo is MIT; the team +explicitly accepted this reciprocal posture for the whole project. + +## Why a separate toolchain, not the existing iverilog flow + +taxi's entire AXI4/AXI4-Lite product line (59 of 63 files under +`third_party/taxi/src/axi/rtl/`) is built on SystemVerilog `interface`+ +`modport` ports (`taxi_axi_if.wr_slv`, `taxi_axi_if.rd_slv`, etc.). This +project's Icarus Verilog build **cannot parse an interface-typed port +declaration at all** -- confirmed by isolating it down to a 15-line, +taxi-independent repro (a bare `interface`/`modport` port on a trivial +module), which fails identically: +``` +syntax error +Errors in port declarations. +``` +Not a partial gap -- the foundational pattern the whole library is written +around doesn't compile under iverilog, full stop. **Verilator does support +it natively**, and was already a project dependency (used for the +`--lint-only` gate on every milestone) -- confirmed via a genuine +`taxi_axi_ram` write (`0xDEADBEEF`) + read-back round trip, with real AXI +handshaking exercised (`awready`/`wready`/`bvalid`/`arready`/`rvalid`), not +just elaborated. + +**Any taxi-touching testbench MUST be built and run through +`run_taxi_tests.sh`, never added to the iverilog-based `design`/`testbench` +regression file lists** -- it will not compile there. + +## Setup + +```sh +git submodule update --init third_party/taxi +``` +(`.gitmodules` marks this submodule `shallow = true`, so a fresh +`--init` pulls a shallow, ~5MB checkout rather than taxi's full history.) + +## Running the tests + +```sh +verification/taxi/run_taxi_tests.sh +``` +Builds and runs every `*_tb.sv` under `verification/taxi/tb/` via +`verilator --binary -j 0 --timing`, matching this project's existing +`$display`-based PASS/FAIL convention (`check_lib.sv`, pulled in via +`-I testbench` same as every iverilog testbench). Reports a PASS/FAIL/ERR +summary line per testbench, same shape as +`verification/riscv-arch-test/run_act_tests.sh`. + +**The working recipe, if building one of these files by hand**: +```sh +verilator --binary -j 0 --timing -Wno-fatal --top-module +./obj_dir/V +``` +`--binary` builds+links in one step (no cocotb, no hand-written C++ testbench +wrapper needed). **`--timing` is not optional**: every testbench in this +repo uses `#N` delays pervasively (`always #5 clk = ~clk;`, settle-waits like +`#1;`), and Verilator's default cycle-based mode can't compile that style at +all without it. + +A testbench may have a companion `.f` file (see `tb/dram_model_tb.f` +for the pattern) listing exactly which source files it needs, one per line, +as **repo-root-relative paths** (e.g. `third_party/taxi/src/axi/rtl/taxi_axi_if.sv`, +`verification/taxi/rtl/dram_model.sv`) -- not taxi-rtl-relative flat +filenames. That was the original convention (matching taxi's own upstream +`.f` files' internal-dependency style) until `dram_model_tb.f` needed to +reference a project-owned RTL file living outside +`third_party/taxi/src/axi/rtl/` -- repo-root-relative paths handle both +cases uniformly. + +## Gotcha worth remembering + +`taxi_axi_ram`'s (and most other taxi AXI modules') `DATA_W` is **not** its +own module parameter -- it's a `localparam` derived from whichever +`taxi_axi_if` interface instance is connected +(`localparam DATA_W = s_axi_wr.DATA_W;` internally). Passing `.DATA_W(...)` +at instantiation is a hard elaboration error ("attempts to override... but +it is a local parameter"), not a harmless no-op. Only `ADDR_W` (and other +genuinely-independent parameters) are real instantiation parameters -- +check the target module's own parameter list before assuming a signal name +is settable. + +## Status + +`taxi_axi_ram_smoke_tb.sv` -- confirms the toolchain itself works end-to-end +against real taxi functional IP (single write + read-back round trip). + +`rtl/dram_model.sv` + `tb/dram_model_tb.sv` -- a Wishbone-slave-shaped +peripheral (same port contract as `design/wb4_sram.sv`) that bridges each WB +request to a real, single-beat AXI4 transaction against a genuine +`taxi_axi_ram` backing store, with hand-written DRAM-realism timing layered +on top: a configurable `ACCESS_LATENCY_CYCLES` extra-wait parameter and a +configurable `REFRESH_INTERVAL_CYCLES`/`REFRESH_BUSY_CYCLES` pair that +periodically blocks starting a new transaction (never interrupts one already +in flight), matching real DRAM's periodic refresh unavailability. Latency/ +refresh timing is proven via cycle-count deltas against a zero-latency/ +refresh-disabled baseline instance, not a hardcoded absolute cycle count +(hand-tracing `taxi_axi_ram`'s exact registered timing by inspection is +error-prone; the delta approach is immune to any error in the absolute base +number). + +`rtl/decoder_dram_harness.sv` + `tb/decoder_dram_tb.sv` -- `dram_model.sv` +**is** now wired into a real `design/wb_addr_decoder.sv` (a real 4-way +decode: RAM/UART/CLINT/DRAM, CLINT narrowed from 64KB to 32KB to free the +window DRAM now uses), proven end-to-end at the bus level through this +harness (real decoder + real wb4_sram/uart_tx/clint/dram_model, no +`core.sv`) -- mirrors `testbench/decoder_clint_harness.sv`'s own precedent +for CLINT's bus-wiring. Still **not** wired into `design/soc.sv` directly, +though -- and unlike CLINT, that isn't just a staging choice deferred to a +later step. `design/soc.sv` is compiled by many existing iverilog +testbenches and must stay iverilog-parseable forever; `dram_model.sv` is +Verilator-only *by construction* (the `interface`-based AXI bridge is +fundamental to its design, not incidental), so `soc.sv` can never +instantiate it directly. A future "real firmware through DRAM" step would +need either a separate Verilator-only `soc`-shaped top-level under +`verification/taxi/`, or a synthesizable/swappable DRAM-slave abstraction +in `design/` with the real backing store substituted only for the taxi +build -- not the same playbook CLINT's own later firmware milestone used. + +Links: project memory `bus-protocol-decision` (the original taxi-vs-iverilog +decision history), `verification/riscv-arch-test/`, `verification/riscv-formal/` +(this project's other external-tool integrations, different pattern -- see +the note at the top of this file for why). diff --git a/verification/taxi/rtl/decoder_dram_harness.sv b/verification/taxi/rtl/decoder_dram_harness.sv new file mode 100644 index 0000000..1edfe4a --- /dev/null +++ b/verification/taxi/rtl/decoder_dram_harness.sv @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +/* + * Module: decoder_dram_harness + * + * Shared "decoder + real slaves, no core" wiring, mirroring + * testbench/decoder_clint_harness.sv exactly, but with all FOUR real + * slaves this time (RAM/UART/CLINT/DRAM) -- Verilator-only, unlike that + * file, since dram_model.sv instantiates a SystemVerilog `interface` + * internally (drives a real taxi_axi_ram over AXI4), which iverilog + * cannot parse at all (see verification/taxi/README.md). Living under + * verification/taxi/ rather than testbench/ for exactly that reason -- + * never add this file to the iverilog-based design/testbench regression + * file lists, it will not compile there. + * + * Re-exposes the decoder's entire CPU-facing Wishbone port as its own + * top-level ports, one-for-one, for an external testbench to drive + * directly via wb_cycle() -- same shape as decoder_clint_harness.sv. + * + * NOT a full soc.sv fidelity claim, same caveat as decoder_clint_harness.sv: + * soc.sv routes its RAM leg through cache_complex (a 5th module), which + * this harness omits -- sram0 hangs directly off decoder0's ram_* ports. + * UART/CLINT/DRAM legs ARE wired exactly as soc.sv has (or, for DRAM, + * WOULD have, if soc.sv could ever instantiate it directly -- see + * design/soc.sv's own header comment for why it can't). + * + * dram0's ADDR_W(15) must exactly match wb_addr_decoder.sv's 32KB DRAM + * window (0x0001_8000-0x0001_FFFF) -- all other dram_model parameters + * are left at their real defaults (ACCESS_LATENCY_CYCLES=4, + * REFRESH_INTERVAL_CYCLES=256, REFRESH_BUSY_CYCLES=16), since this + * harness's own testbench (decoder_dram_tb.sv) is testing ROUTING + * correctness, not timing correctness (already separately proven by + * verification/taxi/tb/dram_model_tb.sv's own cycle-delta method). + */ +module decoder_dram_harness ( + input logic clk, + input logic rst, + + input logic [31:0] addr_i, + input logic [63:0] dat_i, + output logic [63:0] dat_o, + input logic [7:0] sel_i, + input logic we_i, + input logic cyc_i, + input logic stb_i, + output logic ack_o, + output logic err_o +); + + logic [31:0] ram_addr, uart_addr, clint_addr, dram_addr; + logic [63:0] ram_dat_o, ram_dat_i, uart_dat_o, uart_dat_i, clint_dat_o, clint_dat_i, dram_dat_o, dram_dat_i; + logic [7:0] ram_sel, uart_sel, clint_sel, dram_sel; + logic ram_we, ram_cyc, ram_stb, ram_ack, ram_err; + logic uart_we, uart_cyc, uart_stb, uart_ack, uart_err; + logic clint_we, clint_cyc, clint_stb, clint_ack, clint_err; + logic dram_we, dram_cyc, dram_stb, dram_ack, dram_err; + + wb_addr_decoder decoder0 ( + .clk(clk), .rst(rst), + .addr_i(addr_i), .dat_i(dat_i), .dat_o(dat_o), .sel_i(sel_i), + .we_i(we_i), .cyc_i(cyc_i), .stb_i(stb_i), .ack_o(ack_o), .err_o(err_o), + .ram_addr_o(ram_addr), .ram_dat_o(ram_dat_o), .ram_dat_i(ram_dat_i), + .ram_sel_o(ram_sel), .ram_we_o(ram_we), .ram_cyc_o(ram_cyc), + .ram_stb_o(ram_stb), .ram_ack_i(ram_ack), .ram_err_i(ram_err), + .uart_addr_o(uart_addr), .uart_dat_o(uart_dat_o), .uart_dat_i(uart_dat_i), + .uart_sel_o(uart_sel), .uart_we_o(uart_we), .uart_cyc_o(uart_cyc), + .uart_stb_o(uart_stb), .uart_ack_i(uart_ack), .uart_err_i(uart_err), + .clint_addr_o(clint_addr), .clint_dat_o(clint_dat_o), .clint_dat_i(clint_dat_i), + .clint_sel_o(clint_sel), .clint_we_o(clint_we), .clint_cyc_o(clint_cyc), + .clint_stb_o(clint_stb), .clint_ack_i(clint_ack), .clint_err_i(clint_err), + .dram_addr_o(dram_addr), .dram_dat_o(dram_dat_o), .dram_dat_i(dram_dat_i), + .dram_sel_o(dram_sel), .dram_we_o(dram_we), .dram_cyc_o(dram_cyc), + .dram_stb_o(dram_stb), .dram_ack_i(dram_ack), .dram_err_i(dram_err) + ); + + wb4_sram #(.num_words(4096)) sram0 ( + .clk(clk), .rst(rst), + .addr_i(ram_addr), .dat_i(ram_dat_o), .dat_o(ram_dat_i), .sel_i(ram_sel), + .ack_o(ram_ack), .err_o(ram_err), .cyc_i(ram_cyc), .stb_i(ram_stb), .we_i(ram_we) + ); + + uart_tx uart0 ( + .clk(clk), .rst(rst), + .addr_i(uart_addr), .dat_i(uart_dat_o), .dat_o(uart_dat_i), .sel_i(uart_sel), + .ack_o(uart_ack), .err_o(uart_err), .cyc_i(uart_cyc), .stb_i(uart_stb), .we_i(uart_we) + ); + + clint clint0 ( + .clk(clk), .rst(rst), + .addr_i(clint_addr), .dat_i(clint_dat_o), .dat_o(clint_dat_i), .sel_i(clint_sel), + .ack_o(clint_ack), .err_o(clint_err), .cyc_i(clint_cyc), .stb_i(clint_stb), .we_i(clint_we), + .mtip_o() + ); + + dram_model #(.ADDR_W(15)) dram0 ( + .clk(clk), .rst(rst), + .addr_i(dram_addr), .dat_i(dram_dat_o), .dat_o(dram_dat_i), .sel_i(dram_sel), + .ack_o(dram_ack), .err_o(dram_err), .cyc_i(dram_cyc), .stb_i(dram_stb), .we_i(dram_we) + ); + +endmodule + + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/verification/taxi/rtl/dram_model.sv b/verification/taxi/rtl/dram_model.sv new file mode 100644 index 0000000..690df0d --- /dev/null +++ b/verification/taxi/rtl/dram_model.sv @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +/* + * Module: dram_model + * + * A Wishbone-slave-shaped peripheral (same port contract as design/wb4_sram.sv + * -- clk/rst, addr_i[31:0]/dat_i[63:0]/dat_o[63:0]/sel_i[7:0], ack_o/err_o, + * cyc_i/stb_i/we_i) that bridges each WB request to a real, single-beat + * AXI4 transaction against a genuinely vendored taxi_axi_ram instance, then + * layers hand-written DRAM-realism timing on top of that otherwise-fixed- + * latency RAM: a configurable extra ACCESS_LATENCY_CYCLES wait after the AXI + * response, and a configurable REFRESH_INTERVAL_CYCLES/REFRESH_BUSY_CYCLES + * pair that periodically blocks STARTING a new transaction (matching real + * DRAM's periodic refresh unavailability) without ever interrupting one + * already in flight. + * + * This exercises real taxi functional IP as the actual backing store, not a + * reimplementation of memory-array logic -- taxi_axi_ram.sv (CERN-OHL-S-2.0, + * see verification/taxi/README.md's License section for the reciprocal + * implication already accepted for this project) is instantiated directly + * below and does the real storage/AXI-compliance work; this file's own + * job is purely the WB<->AXI4 protocol bridge and the timing model on top. + * + * Verilator-only, by the same hard constraint as everything else under + * verification/taxi/: this file instantiates a SystemVerilog `interface` + * (taxi_axi_if), which this project's Icarus Verilog build cannot parse at + * all (confirmed via an isolated, taxi-independent repro -- see this + * directory's own README.md). Deliberately NOT placed under design/, to + * avoid a future contributor sweeping it into the iverilog regression file + * lists and breaking the whole build -- build/run it only via + * verification/taxi/run_taxi_tests.sh. + * + * ACCESS_LATENCY_CYCLES/REFRESH_INTERVAL_CYCLES/REFRESH_BUSY_CYCLES defaults + * are explicitly toy-scale, sim-speed/observability numbers, not literal DDR + * datasheet timings -- nothing here models per-row/bank state, so a longer, + * more "realistic" interval would only add idle simulation cycles without + * exercising more logic. 16/256 is a ~6.25% refresh duty cycle, in the right + * ballpark of real DRAM refresh overhead without being derived from any + * specific part's timing table. + */ +module dram_model #( + parameter ADDR_W = 16, // -> 2**ADDR_W byte address space + parameter ACCESS_LATENCY_CYCLES = 4, // extra cycles after the AXI response, before ack_o + parameter REFRESH_INTERVAL_CYCLES = 256, // free-running refresh period + parameter REFRESH_BUSY_CYCLES = 16 // cycles per period that block starting a NEW transaction +) ( + input logic clk, + input logic rst, + + input logic [31:0] addr_i, + input logic [63:0] dat_i, + output logic [63:0] dat_o, + input logic [7:0] sel_i, + + output logic ack_o, + output logic err_o, + input logic cyc_i, + input logic stb_i, + input logic we_i +); + + if (REFRESH_BUSY_CYCLES >= REFRESH_INTERVAL_CYCLES) + $fatal(0, "Error: REFRESH_BUSY_CYCLES must be < REFRESH_INTERVAL_CYCLES (instance %m)"); + + /* + * Refresh timer -- free-running, independent of FSM state. Real DRAM + * refresh runs on its own physical clock, not gated by controller + * activity; only ST_IDLE's own next-state logic below ever consults + * refresh_busy, holding a NEW request off (not advancing) while a + * refresh window is active. A transaction already past ST_IDLE is never + * itself interrupted by a refresh window starting mid-flight. + */ + localparam REFRESH_CNT_W = $clog2(REFRESH_INTERVAL_CYCLES); + logic [REFRESH_CNT_W-1:0] refresh_cnt; + always_ff @(posedge clk) begin + if (rst) refresh_cnt <= '0; + else if (refresh_cnt == REFRESH_CNT_W'(REFRESH_INTERVAL_CYCLES - 1)) refresh_cnt <= '0; + else refresh_cnt <= refresh_cnt + 1'b1; + end + /* REFRESH_BUSY_CYCLES(0) is a legitimate "disable refresh" configuration + * (used by the timing-reference test instances that isolate the + * ACCESS_LATENCY_CYCLES effect alone) -- for that specific + * parameterization this comparison is constant-false by construction, + * not a mistake. */ + /* verilator lint_off UNSIGNED */ + wire refresh_busy = (refresh_cnt < REFRESH_CNT_W'(REFRESH_BUSY_CYCLES)); + /* verilator lint_on UNSIGNED */ + + // AXI4 interface to the real, vendored taxi_axi_ram backing store. + taxi_axi_if #(.DATA_W(64), .ADDR_W(ADDR_W)) axi_if (); + + /* DATA_W is deliberately NOT passed to taxi_axi_ram -- it derives that + * as a localparam from the connected taxi_axi_if instance; passing it + * explicitly is a hard elaboration error, not a no-op (see this + * directory's README.md Gotcha section). PIPELINE_OUTPUT(1'b0) + * explicitly, not just relying on its own default: keeps read and + * write completion paths the same depth, so this module's own added + * latency stays symmetric between the two directions. */ + taxi_axi_ram #(.ADDR_W(ADDR_W), .PIPELINE_OUTPUT(1'b0)) ram0 ( + .clk(clk), + .rst(rst), + .s_axi_wr(axi_if.wr_slv), + .s_axi_rd(axi_if.rd_slv) + ); + + // Fields this bridge never varies -- single-outstanding, single-beat, + // no ID tracking needed (WB itself has no ID/burst concept to translate + // from). + assign axi_if.awid = '0; + assign axi_if.awlen = '0; + assign axi_if.awsize = 3'd3; // 8 bytes = full bus width; sel_i already carries byte granularity + assign axi_if.awburst = 2'b01; + assign axi_if.awlock = '0; + assign axi_if.awcache = '0; + assign axi_if.awprot = '0; + assign axi_if.awqos = '0; + assign axi_if.awregion = '0; + assign axi_if.awuser = '0; + + assign axi_if.wlast = 1'b1; + assign axi_if.wuser = '0; + + assign axi_if.bready = 1'b1; // never issues a new request before consuming the prior response + + assign axi_if.arid = '0; + assign axi_if.arlen = '0; + assign axi_if.arsize = 3'd3; + assign axi_if.arburst = 2'b01; + assign axi_if.arlock = '0; + assign axi_if.arcache = '0; + assign axi_if.arprot = '0; + assign axi_if.arqos = '0; + assign axi_if.arregion = '0; + assign axi_if.aruser = '0; + + assign axi_if.rready = 1'b1; + + typedef enum logic [2:0] { + ST_IDLE, + ST_WR_ADDR, + ST_WR_RESP, + ST_RD_ADDR, + ST_RD_RESP, + ST_LATENCY, + ST_ACK + } state_t; + + state_t state_q, state_d; + + wire addr_valid = (addr_i[31:ADDR_W] == '0); + + // Latched at the IDLE->{WR_ADDR,RD_ADDR} edge -- the WB master (per + // wb_driver.sv's own wb_cycle convention) holds these stable until + // ack/err anyway, but latching internally keeps this module correct + // without depending on that being true of every possible caller. + logic [31:0] addr_q; + logic [63:0] wdata_q; + logic [7:0] sel_q; + always_ff @(posedge clk) begin + if (state_q == ST_IDLE && cyc_i && stb_i && addr_valid && !refresh_busy) begin + addr_q <= addr_i; + wdata_q <= dat_i; + sel_q <= sel_i; + end + end + + assign axi_if.awaddr = addr_q[ADDR_W-1:0]; // raw byte address, UNSHIFTED -- + assign axi_if.araddr = addr_q[ADDR_W-1:0]; // taxi_axi_ram does its own internal + // >>$clog2(STRB_W) shift; pre-shifting + // here would double-divide and + // misaddress everything. + assign axi_if.wdata = wdata_q; + assign axi_if.wstrb = sel_q; // exact width match (STRB_W derives to 8 from DATA_W=64) + + /* + * AW/W independence -- a real protocol-correctness point, not an + * assumption: taxi_axi_ram's write FSM accepts AW and W on DIFFERENT + * cycles in general (awready sits high in steady-state idle, but + * wready only asserts the cycle AFTER AW is accepted). Two + * independently-sticky "accepted" bits, each clearing its own + * channel's valid the cycle its own handshake completes; ST_WR_ADDR is + * only left once BOTH are done. + */ + logic aw_done_q, w_done_q; + always_ff @(posedge clk) begin + if (rst || state_q != ST_WR_ADDR) begin + aw_done_q <= 1'b0; + w_done_q <= 1'b0; + end else begin + if (axi_if.awvalid && axi_if.awready) aw_done_q <= 1'b1; + if (axi_if.wvalid && axi_if.wready) w_done_q <= 1'b1; + end + end + assign axi_if.awvalid = (state_q == ST_WR_ADDR) && !aw_done_q; + assign axi_if.wvalid = (state_q == ST_WR_ADDR) && !w_done_q; + wire aw_complete = aw_done_q || (axi_if.awvalid && axi_if.awready); + wire w_complete = w_done_q || (axi_if.wvalid && axi_if.wready); + + // Read side: a single channel, so simply holding arvalid high for the + // whole ST_RD_ADDR state (deasserting the cycle after acceptance, via + // the state transition itself) is already correct -- no sticky bit + // needed the way the write side's two-channel case requires. + assign axi_if.arvalid = (state_q == ST_RD_ADDR); + + // Captured on a completed read only -- holds its prior value on writes + // and errors, same convention design/wb4_sram.sv's own dat_o uses. + always_ff @(posedge clk) begin + if (state_q == ST_RD_RESP && axi_if.rvalid) dat_o <= axi_if.rdata; + end + + /* + * err_o derivation -- decided by direct inspection of taxi_axi_ram.sv, + * not assumed: it hardwires bresp/rresp to 2'b00 (OKAY) unconditionally, + * with no other assignment anywhere in that file -- it structurally + * cannot produce a non-OKAY response, so there is nothing to gate err_o + * on there. err_o is sourced exclusively from this WB-side address- + * range check instead (mirroring wb4_sram.sv's own out-of-range + * convention); an out-of-range request never starts an AXI transaction + * at all -- bypasses ACCESS_LATENCY_CYCLES and refresh gating entirely. + */ + logic err_pending_q; + always_ff @(posedge clk) begin + if (rst) begin + err_pending_q <= 1'b0; + end else if (state_q == ST_IDLE && cyc_i && stb_i) begin + err_pending_q <= !addr_valid; + end + end + + // Counts cycles spent in ST_LATENCY; reset whenever not in that state, + // so the first cycle there always reads 0. + logic [31:0] latency_cnt_q; + always_ff @(posedge clk) begin + if (rst || state_q != ST_LATENCY) latency_cnt_q <= '0; + else latency_cnt_q <= latency_cnt_q + 1'b1; + end + + always_ff @(posedge clk) begin + if (rst) state_q <= ST_IDLE; + else state_q <= state_d; + end + + always_comb begin + state_d = state_q; + case (state_q) + ST_IDLE: begin + if (cyc_i && stb_i) begin + if (!addr_valid) state_d = ST_ACK; + else if (!refresh_busy) state_d = we_i ? ST_WR_ADDR : ST_RD_ADDR; + // else: refresh_busy -- stay ST_IDLE, request held off + end + end + ST_WR_ADDR: if (aw_complete && w_complete) state_d = ST_WR_RESP; + ST_WR_RESP: if (axi_if.bvalid) state_d = (ACCESS_LATENCY_CYCLES == 0) ? ST_ACK : ST_LATENCY; + ST_RD_ADDR: if (axi_if.arvalid && axi_if.arready) state_d = ST_RD_RESP; + ST_RD_RESP: if (axi_if.rvalid) state_d = (ACCESS_LATENCY_CYCLES == 0) ? ST_ACK : ST_LATENCY; + ST_LATENCY: if (latency_cnt_q == 32'(ACCESS_LATENCY_CYCLES - 1)) state_d = ST_ACK; + ST_ACK: state_d = ST_IDLE; + default: state_d = ST_IDLE; + endcase + end + + assign ack_o = (state_q == ST_ACK) && !err_pending_q; + assign err_o = (state_q == ST_ACK) && err_pending_q; + +endmodule + + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/verification/taxi/run_taxi_tests.sh b/verification/taxi/run_taxi_tests.sh new file mode 100644 index 0000000..a4eec62 --- /dev/null +++ b/verification/taxi/run_taxi_tests.sh @@ -0,0 +1,88 @@ +#!/bin/bash +# Builds and runs every testbench under verification/taxi/tb/ via Verilator's +# --binary mode -- the toolchain fork this directory exists for. iverilog +# cannot compile ANY of taxi's AXI4/AXI4-Lite IP (its entire product line is +# built on SystemVerilog interface+modport ports, confirmed unparseable by +# this project's Icarus build via an isolated, taxi-independent repro) -- see +# this directory's own README.md for the full story. Never add a +# taxi-touching testbench to the iverilog-based design/testbench regression +# file lists; it will not compile there. +# +# Each testbench may have a companion .f file listing the source files +# it needs, one per line, as REPO-ROOT-RELATIVE paths (not taxi-rtl-relative +# flat filenames -- that was the original convention, changed once a .f file +# needed to reference a project-owned RTL file living outside +# third_party/taxi/src/axi/rtl/, e.g. verification/taxi/rtl/dram_model.sv). +# A testbench with no .f file is assumed to need no extra sources of its own +# (e.g. a pure toolchain smoke test). +# +# Usage (from WSL): verification/taxi/run_taxi_tests.sh +set -u + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +TAXI_AXI_RTL="$REPO_ROOT/third_party/taxi/src/axi/rtl" +TB_DIR="$REPO_ROOT/verification/taxi/tb" +OUT_DIR="/tmp/taxi_verilator_out" + +if [ ! -d "$TAXI_AXI_RTL" ]; then + echo "taxi submodule not initialized -- run:" >&2 + echo " git submodule update --init third_party/taxi" >&2 + exit 1 +fi + +mkdir -p "$OUT_DIR" +PASS=0 +FAIL=0 +ERR=0 + +shopt -s nullglob +tbs=("$TB_DIR"/*_tb.sv) +shopt -u nullglob + +if [ ${#tbs[@]} -eq 0 ]; then + echo "No testbenches found under $TB_DIR" >&2 + exit 1 +fi + +for tb in "${tbs[@]}"; do + name=$(basename "$tb" .sv) + flist="$TB_DIR/${name}.f" + log="$OUT_DIR/${name}.log" + mdir="$OUT_DIR/${name}_obj" + rm -rf "$mdir" + + sources=() + if [ -f "$flist" ]; then + while IFS= read -r f; do + [ -z "$f" ] && continue + sources+=("$REPO_ROOT/$f") + done < "$flist" + fi + + if verilator --binary -j 0 --timing -Wno-fatal -Mdir "$mdir" -I"$REPO_ROOT/testbench" \ + --top-module "$name" "${sources[@]}" "$tb" > "$log" 2>&1; then + "$mdir/V$name" >> "$log" 2>&1 + summary=$(grep -E "^${name}: [0-9]+ passed, [0-9]+ failed" "$log" | tail -1) + if [ -z "$summary" ]; then + echo "NOSUMMARY $name" + ERR=$((ERR+1)) + else + failed=$(echo "$summary" | sed -E 's/.*, ([0-9]+) failed.*/\1/') + if [ "$failed" -eq 0 ]; then + echo "PASS $name" + PASS=$((PASS+1)) + else + echo "FAIL $name ($summary)" + FAIL=$((FAIL+1)) + fi + fi + else + echo "COMPERR $name" + ERR=$((ERR+1)) + tail -20 "$log" + fi +done + +echo "" +echo "=== SUMMARY: $PASS passed, $FAIL failed, $ERR errored (compile/run) ===" +[ "$FAIL" -eq 0 ] && [ "$ERR" -eq 0 ] diff --git a/verification/taxi/tb/decoder_dram_tb.f b/verification/taxi/tb/decoder_dram_tb.f new file mode 100644 index 0000000..bc1b79b --- /dev/null +++ b/verification/taxi/tb/decoder_dram_tb.f @@ -0,0 +1,8 @@ +third_party/taxi/src/axi/rtl/taxi_axi_if.sv +third_party/taxi/src/axi/rtl/taxi_axi_ram.sv +verification/taxi/rtl/dram_model.sv +verification/taxi/rtl/decoder_dram_harness.sv +design/wb_addr_decoder.sv +design/wb4_sram.sv +design/uart_tx.sv +design/clint.sv diff --git a/verification/taxi/tb/decoder_dram_tb.sv b/verification/taxi/tb/decoder_dram_tb.sv new file mode 100644 index 0000000..69d4289 --- /dev/null +++ b/verification/taxi/tb/decoder_dram_tb.sv @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: wb_addr_decoder + real dram0 (bus-level integration) + * + * Mirrors design/wb_addr_decoder_clint_tb.sv exactly, extended to the 4th + * real slave -- via verification/taxi/rtl/decoder_dram_harness.sv (real + * wb_addr_decoder + real wb4_sram + real uart_tx + real clint + real + * dram_model, no core.sv). Bus-level, not firmware/instruction-level: raw + * Wishbone cycles driven directly at the decoder's CPU-facing port with + * wb_cycle(), same idiom as every other decoder-level testbench. + * + * dram0 is instantiated with its REAL default timing parameters + * (ACCESS_LATENCY_CYCLES=4, REFRESH_INTERVAL_CYCLES=256, + * REFRESH_BUSY_CYCLES=16, see decoder_dram_harness.sv) -- this test's job + * is routing correctness, not timing correctness (already separately + * proven by verification/taxi/tb/dram_model_tb.sv's own cycle-delta + * method). wb_cycle()'s wait loop already tolerates arbitrary wait + * states, so occasionally landing a request inside a refresh-busy window + * just costs a few extra simulated cycles, at zero correctness risk. + * + * Deliberately does NOT run real firmware through core.sv -- that's a + * separate, later step if ever wanted (and, unlike CLINT's own later + * firmware-through-soc.sv proof, cannot ever be "just wire dram_model.sv + * into soc.sv" -- see verification/taxi/README.md's Status section for + * why). This test isolates address-decode-and-wiring correctness only, + * matching wb_addr_decoder_clint_tb.sv's own explicit scoping. + */ +module decoder_dram_tb; + + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + logic [31:0] addr; + logic [63:0] dat_i, dat_o; + logic [7:0] sel; + logic ack, err, cyc, stb, we; + + decoder_dram_harness dut ( + .clk(clk), .rst(rst), + .addr_i(addr), .dat_i(dat_i), .dat_o(dat_o), .sel_i(sel), + .we_i(we), .cyc_i(cyc), .stb_i(stb), .ack_o(ack), .err_o(err) + ); + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + `include "wb_driver.sv" + + localparam logic [31:0] CLINT_MTIME = 32'h0001_0000; + localparam logic [31:0] CLINT_MTIMECMP = 32'h0001_0008; + localparam logic [31:0] CLINT_TOP = 32'h0001_7FFF; // top of CLINT's narrowed window + localparam logic [31:0] DRAM_BASE = 32'h0001_8000; + localparam logic [31:0] DRAM_SECOND = 32'h0001_8100; + + logic [63:0] mtime_first, mtime_second; + + initial begin + cyc = 0; stb = 0; we = 0; addr = 0; dat_i = 0; sel = 8'hFF; + @(posedge clk); #1; + rst = 0; + + /* RAM round trip through the real fabric, BEFORE any DRAM traffic. */ + wb_cycle(32'h0000_0100, 64'hDEADBEEF_CAFEF00D, 8'hFF, 1'b1); + wb_cycle(32'h0000_0100, 64'h0, 8'hFF, 1'b0); + check("RAM round trip (pre-DRAM traffic)", dat_o, 64'hDEADBEEF_CAFEF00D); + + /* UART round trip. */ + wb_cycle(32'h0000_8000, 64'h48, 8'h01, 1'b1); // 'H' + check("UART: one character captured", {55'b0, dut.uart0.tx_history_count}, 64'd1); + check("UART: history[0] == 'H'", {56'b0, dut.uart0.tx_history[0]}, 64'h48); + wb_cycle(32'h0000_8008, 64'h0, 8'h00, 1'b0); + check("UART: TX_STATUS reads ready", dat_o, 64'h1); + + /* CLINT mtime: real free-running counter, strict-increase check. */ + wb_cycle(CLINT_MTIME, 64'h0, 8'hFF, 1'b0); + mtime_first = dat_o; + repeat (20) @(posedge clk); + wb_cycle(CLINT_MTIME, 64'h0, 8'hFF, 1'b0); + mtime_second = dat_o; + check("CLINT mtime: second read strictly greater than first", + {63'b0, (mtime_second > mtime_first)}, 64'd1); + + /* CLINT mtimecmp write/read-back. */ + wb_cycle(CLINT_MTIMECMP, 64'h0000_0000_0012_3456, 8'hFF, 1'b1); + wb_cycle(CLINT_MTIMECMP, 64'h0, 8'hFF, 1'b0); + check("CLINT mtimecmp: write then read-back sticks", dat_o, 64'h0000_0000_0012_3456); + + /* Top of CLINT's narrowed window -- proves the decoder's new + * boundary, not just clint.sv's own permissive address aliasing + * (clint.sv would ack almost anything routed to it regardless). */ + wb_cycle(CLINT_TOP, 64'h0, 8'hFF, 1'b0); + check("CLINT top-of-narrowed-window: ack_o (routes to CLINT, not err)", {63'b0, ack}, 64'd1); + check("CLINT top-of-narrowed-window: no err_o", {63'b0, err}, 64'd0); + + /* The critical DRAM round trip -- fails loudly with err_o instead + * of a data mismatch if wb_addr_decoder.sv's dram_addr_o rebase + * is missing or wrong. */ + wb_cycle(DRAM_BASE, 64'hFEED_FACE_BEEF_CAFE, 8'hFF, 1'b1); + wb_cycle(DRAM_BASE, 64'h0, 8'hFF, 1'b0); + check("DRAM round trip (bottom of window)", dat_o, 64'hFEED_FACE_BEEF_CAFE); + check("DRAM round trip: ack_o, not err_o", {63'b0, ack}, 64'd1); + + /* Second DRAM address -- confirms the addr_i[14:0] rebase isn't + * off-by-a-shift (a shift error would alias this back onto the + * first address or onto a completely different byte). */ + wb_cycle(DRAM_SECOND, 64'h1357_9BDF_2468_ACE0, 8'hFF, 1'b1); + wb_cycle(DRAM_SECOND, 64'h0, 8'hFF, 1'b0); + check("DRAM round trip (second address)", dat_o, 64'h1357_9BDF_2468_ACE0); + wb_cycle(DRAM_BASE, 64'h0, 8'hFF, 1'b0); + check("DRAM: first address unchanged by second address's write", dat_o, 64'hFEED_FACE_BEEF_CAFE); + + /* RAM/UART/CLINT routing unaffected by DRAM now sharing the decoder. */ + wb_cycle(32'h0000_0200, 64'h1122_3344_5566_7788, 8'hFF, 1'b1); + wb_cycle(32'h0000_0200, 64'h0, 8'hFF, 1'b0); + check("RAM round trip (post-DRAM traffic, unaffected)", dat_o, 64'h1122_3344_5566_7788); + + wb_cycle(32'h0000_8000, 64'h69, 8'h01, 1'b1); // 'i' + check("UART: second character captured (post-DRAM traffic, unaffected)", + {55'b0, dut.uart0.tx_history_count}, 64'd2); + + wb_cycle(CLINT_MTIMECMP, 64'h0, 8'hFF, 1'b0); + check("CLINT mtimecmp: still reads back correctly (post-DRAM traffic, unaffected)", + dat_o, 64'h0000_0000_0012_3456); + + $display(""); + $display("decoder_dram_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("decoder_dram_tb: FAILURES PRESENT"); + $finish; + end + +endmodule + + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/verification/taxi/tb/dram_model_tb.f b/verification/taxi/tb/dram_model_tb.f new file mode 100644 index 0000000..0bc35a0 --- /dev/null +++ b/verification/taxi/tb/dram_model_tb.f @@ -0,0 +1,3 @@ +third_party/taxi/src/axi/rtl/taxi_axi_if.sv +third_party/taxi/src/axi/rtl/taxi_axi_ram.sv +verification/taxi/rtl/dram_model.sv diff --git a/verification/taxi/tb/dram_model_tb.sv b/verification/taxi/tb/dram_model_tb.sv new file mode 100644 index 0000000..a177260 --- /dev/null +++ b/verification/taxi/tb/dram_model_tb.sv @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: dram_model + * + * Correctness (on `dut`, ACCESS_LATENCY_CYCLES=2, refresh disabled) mirrors + * design/wb4_sram_tb.sv's own coverage: full-word round trip, byte-enable + * write, a second-address round trip, and an out-of-range address asserting + * err_o -- via `` `include "wb_driver.sv" ``'s wb_cycle() task, same idiom + * every WB-slave testbench in this project already uses. + * + * Latency/refresh timing is proven via CYCLE-COUNT DELTAS against a shared + * zero-latency/refresh-disabled baseline (dut_ref0), not a hardcoded + * absolute cycle count -- hand-tracing taxi_axi_ram's exact registered + * timing by inspection is error-prone (this exact derivation caught a real + * off-by-one during planning: 4 cycles first, corrected to 3 on a closer + * recheck), but the DELTA a knob induces is invariant to any error in that + * base number. Three more `dram_model` instances (dut_ref0/dut_lat5/ + * dut_refresh) are driven by a local `timed_request` task using real `ref` + * arguments -- viable here specifically because this file compiles + * exclusively through Verilator (verification/taxi/run_taxi_tests.sh), + * unlike every iverilog testbench in this project, which is stuck avoiding + * `ref` args per testbench/wb_driver.sv's own documented Icarus limitation + * ("sorry: Reference ports not supported yet."). + * + * dut_refresh's single request is issued immediately after its own reset + * deasserts, landing deterministically at refresh_cnt==0 -- the very first + * cycle of a refresh-busy window -- with no phase-alignment arithmetic + * needed (0 is unambiguous). + */ +module dram_model_tb; + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + + /* ----------------------------------------------------------------- * + * Correctness suite -- `dut`. + * ----------------------------------------------------------------- */ + logic [31:0] addr; + logic [63:0] dat_i, dat_o; + logic [7:0] sel; + logic we, cyc, stb, ack, err; + + dram_model #(.ACCESS_LATENCY_CYCLES(2), .REFRESH_BUSY_CYCLES(0)) dut ( + .clk(clk), .rst(rst), + .addr_i(addr), .dat_i(dat_i), .dat_o(dat_o), .sel_i(sel), + .ack_o(ack), .err_o(err), .cyc_i(cyc), .stb_i(stb), .we_i(we) + ); + + `include "wb_driver.sv" + + /* ----------------------------------------------------------------- * + * Timing-reference instances -- dut_ref0 (baseline), dut_lat5 (latency + * delta), dut_refresh (refresh-stall delta). Each needs its own + * independent WB signal set, since wb_driver.sv's wb_cycle() can only + * bind to one fixed name set per including module. + * ----------------------------------------------------------------- */ + logic rst_ref0 = 1; + logic [31:0] addr_ref0; logic [63:0] dat_i_ref0, dat_o_ref0; logic [7:0] sel_ref0; + logic we_ref0, cyc_ref0, stb_ref0, ack_ref0, err_ref0; + dram_model #(.ACCESS_LATENCY_CYCLES(0), .REFRESH_BUSY_CYCLES(0)) dut_ref0 ( + .clk(clk), .rst(rst_ref0), + .addr_i(addr_ref0), .dat_i(dat_i_ref0), .dat_o(dat_o_ref0), .sel_i(sel_ref0), + .ack_o(ack_ref0), .err_o(err_ref0), .cyc_i(cyc_ref0), .stb_i(stb_ref0), .we_i(we_ref0) + ); + + logic rst_lat5 = 1; + logic [31:0] addr_lat5; logic [63:0] dat_i_lat5, dat_o_lat5; logic [7:0] sel_lat5; + logic we_lat5, cyc_lat5, stb_lat5, ack_lat5, err_lat5; + dram_model #(.ACCESS_LATENCY_CYCLES(5), .REFRESH_BUSY_CYCLES(0)) dut_lat5 ( + .clk(clk), .rst(rst_lat5), + .addr_i(addr_lat5), .dat_i(dat_i_lat5), .dat_o(dat_o_lat5), .sel_i(sel_lat5), + .ack_o(ack_lat5), .err_o(err_lat5), .cyc_i(cyc_lat5), .stb_i(stb_lat5), .we_i(we_lat5) + ); + + logic rst_refresh = 1; + logic [31:0] addr_refresh; logic [63:0] dat_i_refresh, dat_o_refresh; logic [7:0] sel_refresh; + logic we_refresh, cyc_refresh, stb_refresh, ack_refresh, err_refresh; + dram_model #(.ACCESS_LATENCY_CYCLES(0), .REFRESH_INTERVAL_CYCLES(20), .REFRESH_BUSY_CYCLES(6)) dut_refresh ( + .clk(clk), .rst(rst_refresh), + .addr_i(addr_refresh), .dat_i(dat_i_refresh), .dat_o(dat_o_refresh), .sel_i(sel_refresh), + .ack_o(ack_refresh), .err_o(err_refresh), .cyc_i(cyc_refresh), .stb_i(stb_refresh), .we_i(we_refresh) + ); + + task automatic timed_request( + ref logic rst_r, + ref logic [31:0] addr_r, + ref logic [63:0] dat_i_r, + ref logic [7:0] sel_r, + ref logic we_r, + ref logic cyc_r, + ref logic stb_r, + ref logic ack_r, + ref logic err_r, + output int cyc_count + ); + rst_r = 1'b1; + cyc_r = 1'b0; stb_r = 1'b0; we_r = 1'b0; addr_r = 32'h0; dat_i_r = 64'h0; sel_r = 8'hFF; + @(posedge clk); #1; + rst_r = 1'b0; + + // Issue immediately -- lands the first ST_IDLE sample at + // refresh_cnt==0, the deterministic start of a busy window. + @(negedge clk); + addr_r = 32'h0; dat_i_r = 64'hA5A5_A5A5_A5A5_A5A5; sel_r = 8'hFF; we_r = 1'b1; + cyc_r = 1'b1; stb_r = 1'b1; + cyc_count = 0; + @(posedge clk); #1; + while (!ack_r && !err_r) begin + cyc_count = cyc_count + 1; + @(posedge clk); #1; + end + cyc_r = 1'b0; stb_r = 1'b0; + endtask + + int cyc_count_ref0, cyc_count_lat5, cyc_count_refresh; + int oor_cyc_count; + + initial begin + cyc = 0; stb = 0; we = 0; addr = 0; dat_i = 0; sel = 8'hFF; + @(posedge clk); #1; + rst = 0; + + /* ---- (a) full-word round trip ---- */ + wb_cycle(32'h0, 64'hDEADBEEF_CAFEF00D, 8'hFF, 1'b1); + wb_cycle(32'h0, 64'h0, 8'hFF, 1'b0); + check("full-word round trip", dat_o, 64'hDEADBEEF_CAFEF00D); + + /* ---- (b) byte-enable write only touches enabled lanes ---- */ + wb_cycle(32'h0, 64'h0000_0000_0000_A5A5, 8'h03, 1'b1); + wb_cycle(32'h0, 64'h0, 8'hFF, 1'b0); + check("byte-enable write only touches enabled lanes", dat_o, 64'hDEADBEEF_CAFEA5A5); + + /* ---- (c) second-address round trip ---- */ + wb_cycle(32'h8, 64'h5A5A5A5A_5A5A5A5A, 8'hFF, 1'b1); + wb_cycle(32'h8, 64'h0, 8'hFF, 1'b0); + check("second-address round trip", dat_o, 64'h5A5A5A5A_5A5A5A5A); + + /* ---- (d) out-of-range address asserts err_o, bypassing the AXI + * FSM (and therefore ACCESS_LATENCY_CYCLES(2)) entirely -- proven + * below as a comparative check against cyc_count_ref0 (a real, + * successful transaction's own cycle count on a ZERO-latency + * instance), not a bare hardcoded number: the error path must + * resolve in fewer cycles than even the fastest possible + * successful transaction, since it never touches ST_WR_ADDR/ + * ST_WR_RESP/ST_LATENCY at all. ---- */ + @(negedge clk); + addr = 32'h0001_0000; dat_i = 64'h0; sel = 8'hFF; we = 1'b0; cyc = 1; stb = 1; + oor_cyc_count = 0; + @(posedge clk); #1; + while (!ack && !err) begin + oor_cyc_count = oor_cyc_count + 1; + @(posedge clk); #1; + end + check("out-of-range: err_o asserted", {63'b0, err}, 64'd1); + check("out-of-range: ack_o stays low", {63'b0, ack}, 64'd0); + cyc = 0; stb = 0; + + /* ---- latency/refresh deltas ---- */ + timed_request(rst_ref0, addr_ref0, dat_i_ref0, sel_ref0, we_ref0, + cyc_ref0, stb_ref0, ack_ref0, err_ref0, cyc_count_ref0); + timed_request(rst_lat5, addr_lat5, dat_i_lat5, sel_lat5, we_lat5, + cyc_lat5, stb_lat5, ack_lat5, err_lat5, cyc_count_lat5); + timed_request(rst_refresh, addr_refresh, dat_i_refresh, sel_refresh, we_refresh, + cyc_refresh, stb_refresh, ack_refresh, err_refresh, cyc_count_refresh); + + check("access latency delta == ACCESS_LATENCY_CYCLES (5)", + 64'(cyc_count_lat5 - cyc_count_ref0), 64'd5); + check("refresh stall delta == REFRESH_BUSY_CYCLES (6)", + 64'(cyc_count_refresh - cyc_count_ref0), 64'd6); + check("out-of-range resolves strictly faster than any successful transaction (bypasses the AXI FSM)", + {63'b0, (oor_cyc_count < cyc_count_ref0)}, 64'd1); + $display("INFO: baseline cyc_count_ref0 = %0d, out-of-range oor_cyc_count = %0d (bring-up reference only, not separately asserted)", + cyc_count_ref0, oor_cyc_count); + + $display(""); + $display("dram_model_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("dram_model_tb: FAILURES PRESENT"); + $finish; + end +endmodule + + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */ diff --git a/verification/taxi/tb/taxi_axi_ram_smoke_tb.f b/verification/taxi/tb/taxi_axi_ram_smoke_tb.f new file mode 100644 index 0000000..3c96fbe --- /dev/null +++ b/verification/taxi/tb/taxi_axi_ram_smoke_tb.f @@ -0,0 +1,2 @@ +third_party/taxi/src/axi/rtl/taxi_axi_if.sv +third_party/taxi/src/axi/rtl/taxi_axi_ram.sv diff --git a/verification/taxi/tb/taxi_axi_ram_smoke_tb.sv b/verification/taxi/tb/taxi_axi_ram_smoke_tb.sv new file mode 100644 index 0000000..4d7a50c --- /dev/null +++ b/verification/taxi/tb/taxi_axi_ram_smoke_tb.sv @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT + +/* ------------------------------------------------------------------------- */ + + +/* + * Testbench: taxi_axi_ram_smoke_tb -- confirms this project's Verilator-based + * toolchain fork (see verification/taxi/README.md) actually simulates real + * taxi functional IP correctly, not just elaborates it. + * + * taxi's whole AXI4/AXI4-Lite product line is built on SystemVerilog + * `interface`+`modport` ports (`taxi_axi_if.wr_slv`, etc.) -- confirmed + * unparseable by this project's Icarus build, isolated down to a + * taxi-independent 15-line repro. Verilator handles it natively, so any + * taxi-touching testbench (this one included) MUST be built via + * run_taxi_tests.sh (verilator --binary --timing), never added to the + * iverilog-based design/testbench regression file lists -- it will not + * compile there. + * + * A single AXI4 write beat (address 0x10, data 0xDEADBEEF) followed by a + * single read beat from the same address, driving taxi_axi_if's signals + * directly rather than through a *_mst modport view (simplest thing that + * proves the write/read path genuinely round-trips through real handshake + * logic, not just elaborates). + */ +module taxi_axi_ram_smoke_tb; + logic clk = 0; + always #5 clk = ~clk; + logic rst = 1; + + int pass_count = 0; + int fail_count = 0; + logic quiet_on_pass = 1'b0; + `include "check_lib.sv" + + localparam DATA_W = 32; + localparam ADDR_W = 16; + localparam STRB_W = DATA_W / 8; + localparam ID_W = 8; + + taxi_axi_if #(.DATA_W(DATA_W), .ADDR_W(ADDR_W), .STRB_W(STRB_W), .ID_W(ID_W)) axi_if(); + + /* DATA_W is deliberately NOT passed here -- taxi_axi_ram derives it as a + * localparam from whichever taxi_axi_if instance is connected + * (`localparam DATA_W = s_axi_wr.DATA_W;` internally). Passing .DATA_W + * explicitly is a hard elaboration error ("attempts to override... but + * it is a local parameter"), not a harmless no-op. */ + taxi_axi_ram #(.ADDR_W(ADDR_W)) dut ( + .clk(clk), + .rst(rst), + .s_axi_wr(axi_if.wr_slv), + .s_axi_rd(axi_if.rd_slv) + ); + + initial begin + axi_if.awid = '0; axi_if.awaddr = '0; axi_if.awlen = '0; axi_if.awsize = 3'd2; + axi_if.awburst = 2'b01; axi_if.awlock = 0; axi_if.awcache = '0; axi_if.awprot = '0; + axi_if.awqos = '0; axi_if.awregion = '0; axi_if.awuser = '0; axi_if.awvalid = 0; + axi_if.wdata = '0; axi_if.wstrb = '0; axi_if.wlast = 0; axi_if.wuser = '0; axi_if.wvalid = 0; + axi_if.bready = 1; + axi_if.arid = '0; axi_if.araddr = '0; axi_if.arlen = '0; axi_if.arsize = 3'd2; + axi_if.arburst = 2'b01; axi_if.arlock = 0; axi_if.arcache = '0; axi_if.arprot = '0; + axi_if.arqos = '0; axi_if.arregion = '0; axi_if.aruser = '0; axi_if.arvalid = 0; + axi_if.rready = 1; + + @(posedge clk); #1; rst = 0; + @(posedge clk); #1; + + // Single AXI4 write beat. + axi_if.awaddr = 16'h0010; axi_if.awvalid = 1; + axi_if.wdata = 32'hDEADBEEF; axi_if.wstrb = 4'hF; axi_if.wlast = 1; axi_if.wvalid = 1; + do @(posedge clk); while (!(axi_if.awready && axi_if.awvalid)); + #1; axi_if.awvalid = 0; + do @(posedge clk); while (!(axi_if.wready && axi_if.wvalid)); + #1; axi_if.wvalid = 0; + do @(posedge clk); while (!axi_if.bvalid); + #1; + check("write response bresp == OKAY", {62'b0, axi_if.bresp}, 64'd0); + + // Single AXI4 read beat from the same address. + @(posedge clk); + axi_if.araddr = 16'h0010; axi_if.arvalid = 1; + do @(posedge clk); while (!(axi_if.arready && axi_if.arvalid)); + #1; axi_if.arvalid = 0; + do @(posedge clk); while (!axi_if.rvalid); + #1; + check("taxi_axi_ram write/read round trip", {32'b0, axi_if.rdata}, 64'hDEADBEEF); + + #20; + $display(""); + $display("taxi_axi_ram_smoke_tb: %0d passed, %0d failed", pass_count, fail_count); + if (fail_count > 0) $display("taxi_axi_ram_smoke_tb: FAILURES PRESENT"); + $finish; + end +endmodule + + +/* ------------------------------------------------------------------------- */ + + +/* End of file. */