From 7feb272d9c5e280a3f19b5e94a5064732f70a02e Mon Sep 17 00:00:00 2001 From: Rob Taylor Date: Thu, 9 Jul 2026 14:50:23 +0100 Subject: [PATCH] fix(cosim): CS-gate QSPI MISO injection for shared-bus arbitration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural QSPI memories may share one SCK/SIO bus selected by distinct CS lines (same clk_gpio/d0_gpio, distinct csn_gpio). That never worked: gpu_apply_flash_din (and the CpuBackend mirror) wrote every instance's d_i to its d_in_pos unconditionally, so a deselected flash — which holds its last d_i and is often iterated last — clobbered the selected driver's MISO. Last-writer-wins silently corrupted the shared-bus reads. Gate the injection on selection: a deselected instance (prev_csn high) presents high-Z and does not drive. Fix mirrored across all backends — CpuBackend::apply_flash_din_gated (extracted as a pure, unit-testable fn) and the gpu_apply_flash_din kernels (Metal + the shared CUDA/HIP kernel_v1_impl.cuh). Gating on prev_csn (the delayed csn that produced d_i) keeps the driven value and the selection decision in phase with the setup-delay model. Independent-pin plurality (multi_mem_cosim) is unaffected — a deselected flash on its own pins was always ignored. Tests: - flash_din_tests unit tests: selected drives, deselected is high-Z, shared-bus order-independence, xmask clear-only-when-driven. - tests/qspi_shared_bus/: RTL fixture — two flashes on a shared bus, distinct CS; reads 0xA1 / 0xB2. Golden (CpuBackend, byte-identical to Metal/CUDA/HIP) + content check, wired into the all/qspi CI scopes. Verified it FAILS without the gate (fa reads 0xFF, clobbered). Also corrects a stale `len() <= 1` doc comment (real cap is MAX_QSPI_MEMS = 4) and amends ADR 0013. Co-developed-by: Claude Code v2.1.203 (claude-opus-4-8[1m]) --- .gitignore | 2 + csrc/kernel_v1.metal | 5 + csrc/kernel_v1_impl.cuh | 4 + docs/adr/0013-plural-peripheral-configs.md | 15 + scripts/ci/cosim_cpu_check.sh | 18 + src/sim/cosim/mod.rs | 132 +- src/testbench.rs | 5 +- tests/qspi_shared_bus/README.md | 40 + tests/qspi_shared_bus/check.py | 70 + .../expected/qspi_shared_bus.vcd | 394 ++++++ tests/qspi_shared_bus/fw/flashA.bin | 1 + tests/qspi_shared_bus/fw/flashB.bin | 1 + tests/qspi_shared_bus/shared_bus_dut.v | 99 ++ tests/qspi_shared_bus/shared_bus_dut_synth.gv | 1216 +++++++++++++++++ tests/qspi_shared_bus/sim_config.json | 65 + tests/qspi_shared_bus/synth.tcl | 18 + 16 files changed, 2066 insertions(+), 19 deletions(-) create mode 100644 tests/qspi_shared_bus/README.md create mode 100644 tests/qspi_shared_bus/check.py create mode 100644 tests/qspi_shared_bus/expected/qspi_shared_bus.vcd create mode 100644 tests/qspi_shared_bus/fw/flashA.bin create mode 100644 tests/qspi_shared_bus/fw/flashB.bin create mode 100644 tests/qspi_shared_bus/shared_bus_dut.v create mode 100644 tests/qspi_shared_bus/shared_bus_dut_synth.gv create mode 100644 tests/qspi_shared_bus/sim_config.json create mode 100644 tests/qspi_shared_bus/synth.tcl diff --git a/.gitignore b/.gitignore index 4d21d857..207f99d4 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,8 @@ __pycache__/ !tests/xprop_cosim/expected/*.vcd # Stage C flash cosim golden (mcu_soc; CpuBackend == Metal, #105). !tests/mcu_soc/expected/*.vcd +# Shared-bus QSPI arbitration golden (CpuBackend == Metal/CUDA/HIP). +!tests/qspi_shared_bus/expected/*.vcd *.tar.gz debug_trace.csv *_watchlist.json diff --git a/csrc/kernel_v1.metal b/csrc/kernel_v1.metal index ac986112..7ef4ab17 100644 --- a/csrc/kernel_v1.metal +++ b/csrc/kernel_v1.metal @@ -976,6 +976,11 @@ kernel void gpu_apply_flash_din( for (uint f = 0; f < params.n_flashes && f < MAX_QSPI_MEMS; f++) { constant FlashDinParams& fp = params.flashes[f]; + // Gate on selection: a deselected instance (prev_csn high) presents + // high-Z and must not drive, else on a shared SIO bus (instances sharing + // d_in_pos) it clobbers the selected driver. Mirrors + // CpuBackend::apply_flash_din_gated. + if (flash_state[f].prev_csn != 0u) continue; uchar d_i = flash_state[f].d_i; u32 xmask_off = fp.xmask_state_offset; diff --git a/csrc/kernel_v1_impl.cuh b/csrc/kernel_v1_impl.cuh index 51d745d8..499b0f95 100644 --- a/csrc/kernel_v1_impl.cuh +++ b/csrc/kernel_v1_impl.cuh @@ -1325,6 +1325,10 @@ __global__ void gpu_apply_flash_din( for (u32 f = 0; f < params->n_flashes && f < MAX_QSPI_MEMS; f++) { const FlashDinParams &fp = params->flashes[f]; + // Gate on selection: a deselected instance (prev_csn high) presents high-Z + // and must not drive, else on a shared SIO bus (instances sharing d_in_pos) + // it clobbers the selected driver. Mirrors CpuBackend::apply_flash_din_gated. + if (flash_state[f].prev_csn != 0u) continue; u8 d_i = flash_state[f].d_i; u32 xmask_off = fp.xmask_state_offset; diff --git a/docs/adr/0013-plural-peripheral-configs.md b/docs/adr/0013-plural-peripheral-configs.md index 23fa36d1..52fa408b 100644 --- a/docs/adr/0013-plural-peripheral-configs.md +++ b/docs/adr/0013-plural-peripheral-configs.md @@ -15,6 +15,21 @@ refactors; the conventions it establishes are already followed. > receives `&backend.state()[state_size..]` (`cosim/mod.rs`). The original > text below is retained for the record. +> **Amendment (2026-07-09):** Plural QSPI memories may now share one SCK/SIO +> bus, selected by distinct CS lines (config: same `clk_gpio`/`d0_gpio`, +> distinct `csn_gpio`). This requires CS-gated MISO injection: a deselected +> instance (`prev_csn` high) presents high-Z and must not drive. Previously +> `gpu_apply_flash_din` (and the CpuBackend mirror) wrote every instance's +> `d_i` to its `d_in_pos` unconditionally, so on a shared bus the +> last-iterated (typically deselected) memory clobbered the selected driver. +> The gate lives in `gpu_apply_flash_din` (Metal + the shared CUDA/HIP +> `kernel_v1_impl.cuh`) and `CpuBackend::apply_flash_din_gated`; regression: +> `tests/qspi_shared_bus/` (golden + content, `all`/`qspi` scopes) plus the +> `flash_din_tests` unit tests. Independent-pin plurality (the original Stage +> B/C design, `tests/multi_mem_cosim/`) is unaffected — a deselected flash on +> its own pins was always ignored by the design; the gate only changes the +> now-shared case. + ## Context Jacquard's cosim mode runs reactive peripheral models alongside the diff --git a/scripts/ci/cosim_cpu_check.sh b/scripts/ci/cosim_cpu_check.sh index 8cf8bc8d..495af3c9 100644 --- a/scripts/ci/cosim_cpu_check.sh +++ b/scripts/ci/cosim_cpu_check.sh @@ -150,6 +150,24 @@ if [ "$SCOPE" = all ] || [ "$SCOPE" = qspi ]; then else fail "qspi_psram_content" fi + + # qspi_shared_bus: two flashes on ONE shared SCK/SIO bus, distinct CS. The + # master reads addr 0 from each in turn; a deselected flash must present + # high-Z on the shared MISO (CS-gated din injection) or it clobbers the + # other's read. Regression for the shared-bus arbitration fix. Golden + # captured on CpuBackend, byte-identical to the Metal/CUDA/HIP kernels. + # See tests/qspi_shared_bus/README.md. + run qspi_shared_bus "$BIN" cosim tests/qspi_shared_bus/shared_bus_dut_synth.gv \ + --config tests/qspi_shared_bus/sim_config.json --top-module shared_bus_dut \ + --max-clock-edges 2000 --output-vcd "$OUT/qspi_shared_bus.vcd" || true + check qspi_shared_bus "$OUT/qspi_shared_bus.vcd" tests/qspi_shared_bus/expected/qspi_shared_bus.vcd + # Content assertion (each flash returned its own byte), independent of the diff. + total=$((total + 1)) + if python3 tests/qspi_shared_bus/check.py "$OUT/qspi_shared_bus.vcd" >/dev/null 2>&1; then + pass "qspi_shared_bus_content" + else + fail "qspi_shared_bus_content" + fi fi # --- flash fixture (mcu_soc) — `flash` scope only ------------------------ diff --git a/src/sim/cosim/mod.rs b/src/sim/cosim/mod.rs index 8df4c24f..78e8e825 100644 --- a/src/sim/cosim/mod.rs +++ b/src/sim/cosim/mod.rs @@ -4534,6 +4534,51 @@ struct CpuFlashInstance { prev_d_out: UnsafeCell, } +/// Inject one QSPI instance's MISO nibble into its `d_in` bit positions — +/// but only when the instance is **selected** (`csn` low). A deselected +/// instance (csn high) presents high-Z and must not drive: otherwise, on a +/// shared SIO bus where instances share `d_in_pos`, a deselected memory +/// clobbers the selected driver's data (the raw per-instance loop is +/// last-writer-wins). `xmask_off == 0` means xprop is disabled. +/// +/// This is the CS gate mirrored by `gpu_apply_flash_din` (Metal / CUDA / HIP), +/// which skips a flash whose `FlashState.prev_csn` is high. Kept as a free fn +/// so the shared-bus arbitration is unit-testable without a GPU or a netlist. +/// +/// `csn` is the **delayed** (`prev_csn`) chip-select, not the live output-slot +/// value: `d_i` was produced by the setup-delay dual-step using that same +/// `prev_csn`, so gating injection on it keeps the driven value and the +/// selection decision in phase. Gating on live `csn` would skew them by one +/// edge — the very skew the delay model exists to avoid. +fn apply_flash_din_gated( + state: &mut [u32], + xmask_off: usize, + d_i: u8, + csn: u32, + d_in_pos: &[u32; 4], +) { + if csn != 0 { + return; // deselected → high-Z, don't drive the (possibly shared) line + } + for i in 0..4usize { + let pos = d_in_pos[i]; + if pos == 0xFFFFFFFF { + continue; + } + let word_idx = (pos >> 5) as usize; + let bit_mask = 1u32 << (pos & 31); + if (d_i >> i) & 1 != 0 { + state[word_idx] |= bit_mask; + } else { + state[word_idx] &= !bit_mask; + } + // Driven ⇒ known: clear the X-mask so a shared MISO bit isn't X-seeded. + if xmask_off != 0 { + state[xmask_off + word_idx] &= !bit_mask; + } + } +} + struct CpuBackend { /// Full design state `[input_state | output_state]`, `2 × state_size` words. state: UnsafeCell>, @@ -5007,26 +5052,14 @@ impl CosimBackend for CpuBackend { // ── CPU apply_flash_din (mirror gpu_apply_flash_din, shader:904) ─ // Inject each memory's current MISO nibble into ITS input-state // d_in bits BEFORE simulate, clearing their X-mask (driven ⇒ known, - // #95 ph3). Every QSPI instance drives its own lanes independently. + // #95 ph3). Gated on selection (prev_csn low) so a deselected memory + // presents high-Z and can't clobber a shared SIO bus — see + // `apply_flash_din_gated`. for inst in &self.flashes { // SAFETY: sequential dispatch — no concurrent borrow. let d_i = unsafe { *inst.d_i.get() }; - for i in 0..4usize { - let pos = inst.d_in_pos[i]; - if pos == 0xFFFFFFFF { - continue; - } - let word_idx = (pos >> 5) as usize; - let bit_mask = 1u32 << (pos & 31); - if (d_i >> i) & 1 != 0 { - state[word_idx] |= bit_mask; - } else { - state[word_idx] &= !bit_mask; - } - if xmask_off != 0 { - state[xmask_off + word_idx] &= !bit_mask; - } - } + let csn = unsafe { *inst.prev_csn.get() }; + apply_flash_din_gated(state, xmask_off, d_i, csn, &inst.d_in_pos); } // ── Simulate every partition for this edge ───────────────────── @@ -5297,3 +5330,68 @@ pub use cuda::run_cosim_cuda; mod hip; #[cfg(feature = "hip")] pub use hip::run_cosim_hip; + +#[cfg(test)] +mod flash_din_tests { + use super::apply_flash_din_gated; + use crate::sim::models::read_bit; + + #[test] + fn selected_instance_drives_its_lane() { + let mut state = vec![0u32; 8]; + // Instance selected (csn=0), d_i = 0b0010 → lane 1 high. d0 at pos 33. + apply_flash_din_gated(&mut state, 0, 0b0010, 0, &[33, 34, 35, 36]); + assert_eq!(read_bit(&state, 34), 1); // lane 1 driven high + assert_eq!(read_bit(&state, 33), 0); // lane 0 low + } + + #[test] + fn deselected_instance_does_not_drive() { + let mut state = vec![0u32; 8]; + // Pre-set the shared lane high, then a DESELECTED instance (csn=1) + // with d_i=0 must NOT pull it low. + state[1] |= 1 << (34 & 31); // pos 34 = word 1, bit 2 + apply_flash_din_gated(&mut state, 0, 0b0000, 1, &[33, 34, 35, 36]); + assert_eq!(read_bit(&state, 34), 1); // untouched — high-Z + } + + #[test] + fn shared_bus_selected_wins_regardless_of_order() { + // Two instances share the SAME d_in positions (shared SIO bus). + // A is SELECTED with data (lane1=1); B is DESELECTED with stale idle + // (all lanes high). The historical bug was last-writer-wins, so assert + // both application orders leave the shared line reflecting A, not B. + let shared = [33u32, 34, 35, 36]; + + // Order 1: selected A first, then deselected B. + let mut s1 = vec![0u32; 8]; + apply_flash_din_gated(&mut s1, 0, 0b0010, 0, &shared); // A selected + apply_flash_din_gated(&mut s1, 0, 0b1111, 1, &shared); // B deselected + assert_eq!(read_bit(&s1, 34), 1); + assert_eq!(read_bit(&s1, 33), 0); + assert_eq!(read_bit(&s1, 36), 0); // B's idle-high did NOT leak onto lane 3 + + // Order 2: deselected B first, then selected A. Same result. + let mut s2 = vec![0u32; 8]; + apply_flash_din_gated(&mut s2, 0, 0b1111, 1, &shared); // B deselected + apply_flash_din_gated(&mut s2, 0, 0b0010, 0, &shared); // A selected + assert_eq!(read_bit(&s2, 34), 1); + assert_eq!(read_bit(&s2, 33), 0); + assert_eq!(read_bit(&s2, 36), 0); + } + + #[test] + fn xmask_cleared_only_when_driven() { + // xprop enabled: selected drive clears the x-mask bit; deselected leaves + // it untouched. xmask_off = 4 words in. + let mut state = vec![0u32; 8]; + state[4 + 1] |= 1 << (34 & 31); // x-mask bit set for pos 34 + apply_flash_din_gated(&mut state, 4, 0b0010, 0, &[33, 34, 35, 36]); + assert_eq!((state[4 + 1] >> (34 & 31)) & 1, 0); // cleared (driven known) + + let mut state2 = vec![0u32; 8]; + state2[4 + 1] |= 1 << (34 & 31); + apply_flash_din_gated(&mut state2, 4, 0b0010, 1, &[33, 34, 35, 36]); + assert_eq!((state2[4 + 1] >> (34 & 31)) & 1, 1); // deselected → untouched + } +} diff --git a/src/testbench.rs b/src/testbench.rs index 0dcc238b..5a39c92c 100644 --- a/src/testbench.rs +++ b/src/testbench.rs @@ -333,8 +333,9 @@ impl TestbenchConfig { /// /// Merges the legacy singular `flash` (prepended, so it is instance 0) with /// the plural `qspi_memory` list, mirroring `effective_uarts()`. This is the - /// single surface every backend consumes: the CPU backend steps every entry - /// independently; the GPU backends (Stage A) require `len() <= 1`. + /// single surface every backend consumes: every backend steps each entry + /// independently, up to `MAX_QSPI_MEMS` (4) instances. Instances may share + /// one SCK/SIO bus with distinct CS (see ADR 0013's 2026-07-09 amendment). pub fn effective_qspi_memory(&self) -> Vec { let mut out = self.qspi_memory.clone(); if let Some(ref f) = self.flash { diff --git a/tests/qspi_shared_bus/README.md b/tests/qspi_shared_bus/README.md new file mode 100644 index 00000000..02f7f80d --- /dev/null +++ b/tests/qspi_shared_bus/README.md @@ -0,0 +1,40 @@ +# qspi_shared_bus — shared SCK/SIO bus, distinct CS + +Regression for the CS-gated MISO arbitration on a **shared** QSPI bus (ADR 0013). + +Two flash instances share one SCK + SIO bus and are selected by distinct chip +selects. In `sim_config.json` both memories use the same `clk_gpio` (2) and +`d0_gpio` (5) but distinct `csn_gpio` (3 vs 4). The DUT (`shared_bus_dut.v`) is +a single SPI master that reads address 0 from flash A, then from flash B, over +the shared bus. + +The point: a **deselected** flash must present high-Z on the shared MISO. If it +drives (the pre-fix behaviour), it clobbers the selected flash's read data — +`gpu_apply_flash_din` / `CpuBackend::apply_flash_din_gated` iterate every +instance, so the last-iterated (often deselected) memory wins. The fix gates the +MISO injection on selection (`prev_csn` low). Flash A holds `0xA1`, flash B +holds `0xB2`; distinct reads prove no clobbering. + +## Pass criterion + +Byte-exact golden VCD diff (`expected/qspi_shared_bus.vcd`, captured on +CpuBackend, byte-identical to the Metal/CUDA/HIP kernels) plus a content +assertion (`check.py`): `fa == 0xA1`, `fb == 0xB2`, `all_match == 1`. + +Runs in the `all` and `qspi` scopes of `scripts/ci/cosim_cpu_check.sh`, so it +gates CpuBackend (Linux) and the CUDA/HIP/Metal GPU suites alike. + +## Regenerating + +```bash +# Netlist (system Yosys): +cd tests/qspi_shared_bus && yosys -q -s synth.tcl + +# Golden VCD (any backend — output is byte-identical across backends): +target/release/jacquard cosim tests/qspi_shared_bus/shared_bus_dut_synth.gv \ + --config tests/qspi_shared_bus/sim_config.json --top-module shared_bus_dut \ + --max-clock-edges 2000 --output-vcd tests/qspi_shared_bus/expected/qspi_shared_bus.vcd +``` + +Firmware (`fw/flashA.bin` = `0xA1…`, `fw/flashB.bin` = `0xB2…`) is 16 bytes each; +only byte 0 is read. diff --git a/tests/qspi_shared_bus/check.py b/tests/qspi_shared_bus/check.py new file mode 100644 index 00000000..76de9453 --- /dev/null +++ b/tests/qspi_shared_bus/check.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Pass criterion for the shared-bus QSPI cosim test. + +Two flashes share one SCK + SIO bus, selected by distinct CS lines. The master +reads address 0 from each in turn. Correct CS-gated MISO arbitration returns +each flash's own byte; the pre-fix bug lets the deselected flash drive the +shared MISO, corrupting the second read. + + fa == 0xA1 (flash A, selected during the first read) + fb == 0xB2 (flash B, selected during the second read) + all_match == 1, done == 1 + +Usage: check.py +""" +import re +import sys + +EXPECT = {"fa": 0xA1, "fb": 0xB2} + + +def parse_final(path): + id2name = {} + for line in open(path): + m = re.match(r"\$var \w+ \d+ (\S+) (.+?) \$end", line) + if m: + id2name[m.group(1)] = m.group(2).strip() + last = {} + for line in open(path): + line = line.rstrip("\n") + if not line: + continue + if line[0] in "01xz": + last[id2name.get(line[1:], line[1:])] = line[0] + elif line[0] == "b": + m = re.match(r"b([01xz]+) (\S+)", line) + if m: + last[id2name.get(m.group(2), m.group(2))] = m.group(1) + return last + + +def busval(last, prefix, w=8): + bits = "".join(last.get(f"{prefix}[{b}]", "x") for b in range(w - 1, -1, -1)) + return int(bits, 2) if set(bits) <= set("01") else None + + +def main(): + last = parse_final(sys.argv[1]) + ok = True + for name, exp in EXPECT.items(): + got = busval(last, name) + status = "OK" if got == exp else "MISMATCH" + if got != exp: + ok = False + gs = f"0x{got:02X}" if got is not None else "x/undriven" + print(f" {name}: got {gs}, expect 0x{exp:02X} [{status}]") + for scalar in ("done", "all_match"): + got = last.get(scalar, "?") + if got != "1": + ok = False + print(f" {scalar}: {got} [{'OK' if got == '1' else 'FAIL'}]") + if not ok: + print("FAIL: shared-bus MISO arbitration wrong " + "(deselected flash clobbered the shared line)") + sys.exit(1) + print("PASS: shared SCK/SIO + distinct CS — each flash returned its own byte") + + +if __name__ == "__main__": + main() diff --git a/tests/qspi_shared_bus/expected/qspi_shared_bus.vcd b/tests/qspi_shared_bus/expected/qspi_shared_bus.vcd new file mode 100644 index 00000000..2353d794 --- /dev/null +++ b/tests/qspi_shared_bus/expected/qspi_shared_bus.vcd @@ -0,0 +1,394 @@ +$timescale 1 ps $end +$scope module top $end +$var wire 1 ! sck $end +$var wire 1 " sio_o[3] $end +$var wire 1 # sio_o[2] $end +$var wire 1 $ sio_o[1] $end +$var wire 1 % sio_o[0] $end +$var wire 1 & cs_n_a $end +$var wire 1 ' cs_n_b $end +$var wire 1 ( fa[7] $end +$var wire 1 ) fa[6] $end +$var wire 1 * fa[5] $end +$var wire 1 + fa[4] $end +$var wire 1 , fa[3] $end +$var wire 1 - fa[2] $end +$var wire 1 . fa[1] $end +$var wire 1 / fa[0] $end +$var wire 1 0 fb[7] $end +$var wire 1 1 fb[6] $end +$var wire 1 2 fb[5] $end +$var wire 1 3 fb[4] $end +$var wire 1 4 fb[3] $end +$var wire 1 5 fb[2] $end +$var wire 1 6 fb[1] $end +$var wire 1 7 fb[0] $end +$var wire 1 8 done $end +$var wire 1 9 all_match $end +$upscope $end +$enddefinitions $end +$dumpvars +#20000 +0! +0" +0# +0$ +0% +1& +1' +0( +0) +0* +0+ +0, +0- +0. +0/ +00 +01 +02 +03 +04 +05 +06 +07 +08 +09 +#580000 +0& +#660000 +1! +#740000 +0! +#820000 +1! +#900000 +0! +#980000 +1! +#1060000 +0! +#1140000 +1! +#1220000 +0! +#1300000 +1! +#1380000 +0! +#1460000 +1! +#1540000 +0! +1% +#1620000 +1! +#1700000 +0! +#1780000 +1! +#1860000 +0! +0% +#1940000 +1! +#2020000 +0! +#2100000 +1! +#2180000 +0! +#2260000 +1! +#2340000 +0! +#2420000 +1! +#2500000 +0! +#2580000 +1! +#2660000 +0! +#2740000 +1! +#2820000 +0! +#2900000 +1! +#2980000 +0! +#3060000 +1! +#3140000 +0! +#3220000 +1! +#3300000 +0! +#3380000 +1! +#3460000 +0! +#3540000 +1! +#3620000 +0! +#3700000 +1! +#3780000 +0! +#3860000 +1! +#3940000 +0! +#4020000 +1! +#4100000 +0! +#4180000 +1! +#4260000 +0! +#4340000 +1! +#4420000 +0! +#4500000 +1! +#4580000 +0! +#4660000 +1! +#4740000 +0! +#4820000 +1! +#4900000 +0! +#4980000 +1! +#5060000 +0! +#5140000 +1! +#5220000 +0! +#5300000 +1! +#5380000 +0! +#5460000 +1! +#5540000 +0! +#5620000 +1! +#5700000 +0! +#5780000 +1! +#5860000 +0! +#5940000 +1! +#6020000 +0! +#6100000 +1! +#6180000 +0! +#6260000 +1! +#6340000 +0! +#6420000 +1! +#6500000 +0! +#6580000 +1! +#6660000 +0! +#6740000 +1! +#6820000 +0! +#6900000 +1! +1& +1( +1* +1/ +#6980000 +0! +0' +#7060000 +1! +#7140000 +0! +#7220000 +1! +#7300000 +0! +#7380000 +1! +#7460000 +0! +#7540000 +1! +#7620000 +0! +#7700000 +1! +#7780000 +0! +#7860000 +1! +#7940000 +0! +1% +#8020000 +1! +#8100000 +0! +#8180000 +1! +#8260000 +0! +0% +#8340000 +1! +#8420000 +0! +#8500000 +1! +#8580000 +0! +#8660000 +1! +#8740000 +0! +#8820000 +1! +#8900000 +0! +#8980000 +1! +#9060000 +0! +#9140000 +1! +#9220000 +0! +#9300000 +1! +#9380000 +0! +#9460000 +1! +#9540000 +0! +#9620000 +1! +#9700000 +0! +#9780000 +1! +#9860000 +0! +#9940000 +1! +#10020000 +0! +#10100000 +1! +#10180000 +0! +#10260000 +1! +#10340000 +0! +#10420000 +1! +#10500000 +0! +#10580000 +1! +#10660000 +0! +#10740000 +1! +#10820000 +0! +#10900000 +1! +#10980000 +0! +#11060000 +1! +#11140000 +0! +#11220000 +1! +#11300000 +0! +#11380000 +1! +#11460000 +0! +#11540000 +1! +#11620000 +0! +#11700000 +1! +#11780000 +0! +#11860000 +1! +#11940000 +0! +#12020000 +1! +#12100000 +0! +#12180000 +1! +#12260000 +0! +#12340000 +1! +#12420000 +0! +#12500000 +1! +#12580000 +0! +#12660000 +1! +#12740000 +0! +#12820000 +1! +#12900000 +0! +#12980000 +1! +#13060000 +0! +#13140000 +1! +#13220000 +0! +#13300000 +1! +1' +10 +12 +13 +16 +18 +19 +#13380000 +0! diff --git a/tests/qspi_shared_bus/fw/flashA.bin b/tests/qspi_shared_bus/fw/flashA.bin new file mode 100644 index 00000000..5d83b24c --- /dev/null +++ b/tests/qspi_shared_bus/fw/flashA.bin @@ -0,0 +1 @@ +¡ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ \ No newline at end of file diff --git a/tests/qspi_shared_bus/fw/flashB.bin b/tests/qspi_shared_bus/fw/flashB.bin new file mode 100644 index 00000000..2463b088 --- /dev/null +++ b/tests/qspi_shared_bus/fw/flashB.bin @@ -0,0 +1 @@ +²ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ \ No newline at end of file diff --git a/tests/qspi_shared_bus/shared_bus_dut.v b/tests/qspi_shared_bus/shared_bus_dut.v new file mode 100644 index 00000000..452d6256 --- /dev/null +++ b/tests/qspi_shared_bus/shared_bus_dut.v @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Shared-bus QSPI cosim DUT: one SPI master, TWO flash instances on a SHARED +// SCK + SIO bus, selected by distinct CS lines. Proves the CS-gated MISO +// arbitration: a deselected flash must present high-Z and not drive the shared +// MISO, otherwise it clobbers the selected flash's read data. +// +// The master performs two back-to-back single-lane READ (0x03) transactions of +// address 0: first with cs_n_a asserted (flash A), then cs_n_b (flash B). Both +// flashes share sck / sio_o (MOSI, lane 0) / sio_i (MISO, lane 1). With correct +// arbitration fa == flash A's byte (0xA1) and fb == flash B's byte (0xB2). With +// the pre-fix bug, the deselected flash drives the shared MISO and fb is wrong. +// +// SPI mode 0, MSB first; one SPI clock period = two system-clock cycles (the +// flash model steps twice per system tick). Each read is 40 SPI cycles: +// 8 command + 24 address + 8 data. + +module shared_bus_dut ( + input wire clk, + input wire rst_n, + + // Shared SPI bus (both flashes) + output wire sck, + output wire [3:0] sio_o, // MOSI on lane 0 + input wire [3:0] sio_i, // MISO on lane 1 + // Distinct chip selects + output wire cs_n_a, + output wire cs_n_b, + + // Observation + output wire [7:0] fa, // byte read from flash A + output wire [7:0] fb, // byte read from flash B + output wire done, + output wire all_match +); + localparam CMD = 8'h03; + // 40-bit frame: {cmd, 24-bit addr 0, 8 don't-care (data phase)}. + wire [39:0] frame = {CMD, 24'h000000, 8'h00}; + + reg [7:0] a_data, b_data; + reg cs_a, cs_b; + reg sck_r; + reg [3:0] mosi_r; + reg [6:0] bitidx; // 0..39 within a transaction + reg phase; // 0 = SCK low half, 1 = SCK high half + reg [1:0] txn; // 0 = read A, 1 = read B, 2 = done + reg [7:0] shreg; + + wire sel_b = (txn == 2'd1); + + always @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + cs_a <= 1'b1; cs_b <= 1'b1; + sck_r <= 1'b0; mosi_r <= 4'b0; + bitidx <= 7'd0; phase <= 1'b0; txn <= 2'd0; shreg <= 8'h00; + a_data <= 8'h00; b_data <= 8'h00; + end else if (txn != 2'd2) begin + // Select exactly the current target for the whole transaction. + cs_a <= sel_b ? 1'b1 : 1'b0; + cs_b <= sel_b ? 1'b0 : 1'b1; + if (phase == 1'b0) begin + // SCK low half: drive MOSI bit, hold SCK low. + mosi_r[0] <= frame[39 - bitidx]; + sck_r <= 1'b0; + phase <= 1'b1; + end else begin + // SCK high half: flash samples MOSI and drives MISO; capture it. + sck_r <= 1'b1; + if (bitidx >= 7'd32) + shreg <= {shreg[6:0], sio_i[1]}; // data phase: capture MISO + phase <= 1'b0; + if (bitidx == 7'd39) begin + if (sel_b) b_data <= {shreg[6:0], sio_i[1]}; + else a_data <= {shreg[6:0], sio_i[1]}; + // Deassert both, advance to the next transaction. The + // CS# high→low edge on the next target resets its byte FSM. + cs_a <= 1'b1; + cs_b <= 1'b1; + bitidx <= 7'd0; + txn <= txn + 2'd1; + end else begin + bitidx <= bitidx + 7'd1; + end + end + end else begin + cs_a <= 1'b1; cs_b <= 1'b1; sck_r <= 1'b0; + end + end + + assign sck = sck_r; + assign sio_o = mosi_r; + assign cs_n_a = cs_a; + assign cs_n_b = cs_b; + assign fa = a_data; + assign fb = b_data; + assign done = (txn == 2'd2); + // Distinct bytes prove the deselected flash did NOT clobber the shared MISO. + assign all_match = done & (fa == 8'hA1) & (fb == 8'hB2); +endmodule diff --git a/tests/qspi_shared_bus/shared_bus_dut_synth.gv b/tests/qspi_shared_bus/shared_bus_dut_synth.gv new file mode 100644 index 00000000..e7f3f520 --- /dev/null +++ b/tests/qspi_shared_bus/shared_bus_dut_synth.gv @@ -0,0 +1,1216 @@ +/* Generated by Yosys 0.66 (git sha1 7f8fdfd8d7bc08c749a2a969388d3425d4f369d5, clang++ 21.0.0 -fPIC -O3) */ + +(* top = 1 *) +(* src = "shared_bus_dut.v:18.1-99.10" *) +module shared_bus_dut(clk, rst_n, sck, sio_o, sio_i, cs_n_a, cs_n_b, fa, fb, done, all_match); + (* src = "shared_bus_dut.v:19.23-19.26" *) + input clk; + wire clk; + (* src = "shared_bus_dut.v:20.23-20.28" *) + input rst_n; + wire rst_n; + (* src = "shared_bus_dut.v:23.23-23.26" *) + output sck; + wire sck; + (* src = "shared_bus_dut.v:24.23-24.28" *) + output [3:0] sio_o; + wire [3:0] sio_o; + (* src = "shared_bus_dut.v:25.23-25.28" *) + input [3:0] sio_i; + wire [3:0] sio_i; + (* src = "shared_bus_dut.v:27.23-27.29" *) + output cs_n_a; + wire cs_n_a; + (* src = "shared_bus_dut.v:28.23-28.29" *) + output cs_n_b; + wire cs_n_b; + (* src = "shared_bus_dut.v:31.23-31.25" *) + output [7:0] fa; + wire [7:0] fa; + (* src = "shared_bus_dut.v:32.23-32.25" *) + output [7:0] fb; + wire [7:0] fb; + (* src = "shared_bus_dut.v:33.23-33.27" *) + output done; + wire done; + (* src = "shared_bus_dut.v:34.23-34.32" *) + output all_match; + wire all_match; + (* src = "shared_bus_dut.v:51.5-88.8" *) + wire _000_; + (* src = "shared_bus_dut.v:51.5-88.8" *) + wire _001_; + (* src = "shared_bus_dut.v:51.5-88.8" *) + wire _002_; + wire _003_; + wire _004_; + wire _005_; + wire _006_; + wire _007_; + wire _008_; + wire _009_; + wire _010_; + wire _011_; + wire _012_; + wire _013_; + wire _014_; + wire _015_; + wire _016_; + wire _017_; + wire _018_; + wire _019_; + wire _020_; + wire _021_; + wire _022_; + wire _023_; + wire _024_; + wire _025_; + wire _026_; + wire _027_; + wire _028_; + wire _029_; + wire _030_; + wire _031_; + wire _032_; + wire _033_; + wire _034_; + wire _035_; + wire _036_; + wire _037_; + wire _038_; + wire _039_; + wire _040_; + wire _041_; + wire _042_; + wire _043_; + wire _044_; + wire _045_; + wire _046_; + wire _047_; + wire _048_; + wire _049_; + wire _050_; + wire _051_; + wire _052_; + wire _053_; + wire _054_; + wire _055_; + wire _056_; + wire _057_; + wire _058_; + wire _059_; + wire _060_; + wire _061_; + wire _062_; + wire _063_; + wire _064_; + wire _065_; + wire _066_; + wire _067_; + wire _068_; + wire _069_; + wire _070_; + wire _071_; + wire _072_; + wire _073_; + wire _074_; + wire _075_; + wire _076_; + wire _077_; + wire _078_; + wire _079_; + wire _080_; + wire _081_; + wire _082_; + wire _083_; + wire _084_; + wire _085_; + wire _086_; + wire _087_; + wire _088_; + wire _089_; + wire _090_; + wire _091_; + wire _092_; + wire _093_; + wire _094_; + wire _095_; + wire _096_; + wire _097_; + wire _098_; + wire _099_; + wire _100_; + wire _101_; + wire _102_; + wire _103_; + wire _104_; + wire _105_; + wire _106_; + wire _107_; + wire _108_; + wire _109_; + wire _110_; + wire _111_; + wire _112_; + wire _113_; + wire _114_; + wire _115_; + wire _116_; + wire _117_; + wire _118_; + wire _119_; + wire _120_; + wire _121_; + wire _122_; + wire _123_; + wire _124_; + wire _125_; + wire _126_; + wire _127_; + wire _128_; + wire _129_; + wire _130_; + wire _131_; + wire _132_; + wire _133_; + wire _134_; + wire _135_; + wire _136_; + wire _137_; + wire _138_; + wire _139_; + wire _140_; + wire _141_; + wire _142_; + wire _143_; + (* src = "shared_bus_dut.v:44.15-44.21" *) + wire [6:0] bitidx; + (* src = "shared_bus_dut.v:45.15-45.20" *) + wire phase; + (* src = "shared_bus_dut.v:47.15-47.20" *) + (* unused_bits = "7" *) + wire [7:0] shreg; + (* src = "shared_bus_dut.v:46.15-46.18" *) + wire [1:0] txn; + INV _144_ ( + .A(txn[0]), + .Y(_126_) + ); + INV _145_ ( + .A(txn[1]), + .Y(_127_) + ); + INV _146_ ( + .A(phase), + .Y(_128_) + ); + AND2_01_0 _147_ ( + .A(txn[1]), + .B(txn[0]), + .Y(done) + ); + AND2_01_0 _148_ ( + .A(phase), + .B(done), + .Y(_002_) + ); + AND2_11_0 _149_ ( + .A(bitidx[5]), + .B(bitidx[6]), + .Y(_129_) + ); + INV _150_ ( + .A(_129_), + .Y(_130_) + ); + AND2_00_0 _151_ ( + .A(_002_), + .B(_130_), + .Y(_131_) + ); + AND2_00_0 _152_ ( + .A(shreg[5]), + .B(_131_), + .Y(_132_) + ); + AND2_01_0 _153_ ( + .A(shreg[6]), + .B(_131_), + .Y(_133_) + ); + AND2_11_1 _154_ ( + .A(_132_), + .B(_133_), + .Y(_003_) + ); + AND2_00_0 _155_ ( + .A(shreg[4]), + .B(_131_), + .Y(_134_) + ); + AND2_01_0 _156_ ( + .A(shreg[5]), + .B(_131_), + .Y(_135_) + ); + AND2_11_1 _157_ ( + .A(_134_), + .B(_135_), + .Y(_004_) + ); + AND2_00_0 _158_ ( + .A(shreg[3]), + .B(_131_), + .Y(_136_) + ); + AND2_01_0 _159_ ( + .A(shreg[4]), + .B(_131_), + .Y(_137_) + ); + AND2_11_1 _160_ ( + .A(_136_), + .B(_137_), + .Y(_005_) + ); + AND2_00_0 _161_ ( + .A(shreg[2]), + .B(_131_), + .Y(_138_) + ); + AND2_01_0 _162_ ( + .A(shreg[3]), + .B(_131_), + .Y(_139_) + ); + AND2_11_1 _163_ ( + .A(_138_), + .B(_139_), + .Y(_006_) + ); + AND2_00_0 _164_ ( + .A(shreg[1]), + .B(_131_), + .Y(_140_) + ); + AND2_01_0 _165_ ( + .A(shreg[2]), + .B(_131_), + .Y(_141_) + ); + AND2_11_1 _166_ ( + .A(_140_), + .B(_141_), + .Y(_007_) + ); + AND2_00_0 _167_ ( + .A(shreg[0]), + .B(_131_), + .Y(_142_) + ); + AND2_01_0 _168_ ( + .A(shreg[1]), + .B(_131_), + .Y(_143_) + ); + AND2_11_1 _169_ ( + .A(_142_), + .B(_143_), + .Y(_008_) + ); + AND2_00_0 _170_ ( + .A(sio_i[1]), + .B(_131_), + .Y(_037_) + ); + AND2_01_0 _171_ ( + .A(shreg[0]), + .B(_131_), + .Y(_038_) + ); + AND2_11_1 _172_ ( + .A(_037_), + .B(_038_), + .Y(_009_) + ); + AND2_00_0 _173_ ( + .A(bitidx[0]), + .B(phase), + .Y(_039_) + ); + AND2_01_0 _174_ ( + .A(_039_), + .B(done), + .Y(_040_) + ); + AND2_00_0 _175_ ( + .A(bitidx[1]), + .B(_040_), + .Y(_041_) + ); + AND2_00_0 _176_ ( + .A(bitidx[2]), + .B(_041_), + .Y(_042_) + ); + AND2_00_0 _177_ ( + .A(bitidx[3]), + .B(_042_), + .Y(_043_) + ); + AND2_00_0 _178_ ( + .A(bitidx[4]), + .B(_043_), + .Y(_044_) + ); + AND2_00_0 _179_ ( + .A(bitidx[5]), + .B(_044_), + .Y(_045_) + ); + AND2_11_0 _180_ ( + .A(bitidx[4]), + .B(bitidx[3]), + .Y(_046_) + ); + AND2_00_0 _181_ ( + .A(bitidx[2]), + .B(bitidx[1]), + .Y(_047_) + ); + AND2_00_0 _182_ ( + .A(_046_), + .B(_047_), + .Y(_048_) + ); + AND2_01_0 _183_ ( + .A(bitidx[5]), + .B(bitidx[6]), + .Y(_049_) + ); + AND2_00_0 _184_ ( + .A(_039_), + .B(_049_), + .Y(_050_) + ); + AND2_00_0 _185_ ( + .A(_048_), + .B(_050_), + .Y(_051_) + ); + INV _186_ ( + .A(_051_), + .Y(_052_) + ); + AND2_11_1 _187_ ( + .A(done), + .B(_052_), + .Y(_053_) + ); + AND2_11_1 _188_ ( + .A(bitidx[5]), + .B(_044_), + .Y(_054_) + ); + AND2_10_0 _189_ ( + .A(_045_), + .B(_054_), + .Y(_055_) + ); + AND2_00_0 _190_ ( + .A(_053_), + .B(_055_), + .Y(_010_) + ); + AND2_11_1 _191_ ( + .A(bitidx[4]), + .B(_043_), + .Y(_056_) + ); + AND2_10_0 _192_ ( + .A(_044_), + .B(_056_), + .Y(_011_) + ); + AND2_11_1 _193_ ( + .A(bitidx[3]), + .B(_042_), + .Y(_057_) + ); + AND2_10_0 _194_ ( + .A(_043_), + .B(_057_), + .Y(_058_) + ); + AND2_00_0 _195_ ( + .A(_053_), + .B(_058_), + .Y(_012_) + ); + AND2_11_1 _196_ ( + .A(bitidx[2]), + .B(_041_), + .Y(_059_) + ); + AND2_10_0 _197_ ( + .A(_042_), + .B(_059_), + .Y(_013_) + ); + AND2_11_1 _198_ ( + .A(bitidx[1]), + .B(_040_), + .Y(_060_) + ); + AND2_10_0 _199_ ( + .A(_041_), + .B(_060_), + .Y(_014_) + ); + AND2_11_1 _200_ ( + .A(bitidx[0]), + .B(_002_), + .Y(_061_) + ); + AND2_10_0 _201_ ( + .A(_040_), + .B(_061_), + .Y(_015_) + ); + AND2_01_0 _202_ ( + .A(txn[0]), + .B(txn[1]), + .Y(_062_) + ); + AND2_11_1 _203_ ( + .A(_126_), + .B(txn[1]), + .Y(_063_) + ); + AND2_01_0 _204_ ( + .A(_063_), + .B(_053_), + .Y(_064_) + ); + AND2_11_1 _205_ ( + .A(_053_), + .B(_062_), + .Y(_065_) + ); + AND2_11_1 _206_ ( + .A(done), + .B(_062_), + .Y(_066_) + ); + AND2_00_0 _207_ ( + .A(fa[6]), + .B(_065_), + .Y(_067_) + ); + AND2_00_0 _208_ ( + .A(shreg[5]), + .B(_064_), + .Y(_068_) + ); + AND2_11_1 _209_ ( + .A(_067_), + .B(_068_), + .Y(_016_) + ); + AND2_00_0 _210_ ( + .A(fa[5]), + .B(_065_), + .Y(_069_) + ); + AND2_00_0 _211_ ( + .A(shreg[4]), + .B(_064_), + .Y(_070_) + ); + AND2_11_1 _212_ ( + .A(_069_), + .B(_070_), + .Y(_017_) + ); + AND2_00_0 _213_ ( + .A(fa[4]), + .B(_065_), + .Y(_071_) + ); + AND2_00_0 _214_ ( + .A(shreg[3]), + .B(_064_), + .Y(_072_) + ); + AND2_11_1 _215_ ( + .A(_071_), + .B(_072_), + .Y(_018_) + ); + AND2_00_0 _216_ ( + .A(fa[3]), + .B(_065_), + .Y(_073_) + ); + AND2_00_0 _217_ ( + .A(shreg[2]), + .B(_064_), + .Y(_074_) + ); + AND2_11_1 _218_ ( + .A(_073_), + .B(_074_), + .Y(_019_) + ); + AND2_00_0 _219_ ( + .A(fa[2]), + .B(_065_), + .Y(_075_) + ); + AND2_00_0 _220_ ( + .A(shreg[1]), + .B(_064_), + .Y(_076_) + ); + AND2_11_1 _221_ ( + .A(_075_), + .B(_076_), + .Y(_020_) + ); + AND2_00_0 _222_ ( + .A(fa[1]), + .B(_065_), + .Y(_077_) + ); + AND2_00_0 _223_ ( + .A(shreg[0]), + .B(_064_), + .Y(_078_) + ); + AND2_11_1 _224_ ( + .A(_077_), + .B(_078_), + .Y(_021_) + ); + AND2_00_0 _225_ ( + .A(fa[0]), + .B(_065_), + .Y(_079_) + ); + AND2_00_0 _226_ ( + .A(sio_i[1]), + .B(_064_), + .Y(_080_) + ); + AND2_11_1 _227_ ( + .A(_079_), + .B(_080_), + .Y(_022_) + ); + AND2_11_1 _228_ ( + .A(_126_), + .B(_052_), + .Y(_081_) + ); + AND2_01_0 _229_ ( + .A(_127_), + .B(_081_), + .Y(_082_) + ); + AND2_01_0 _230_ ( + .A(fb[6]), + .B(_082_), + .Y(_083_) + ); + AND2_00_0 _231_ ( + .A(shreg[5]), + .B(_082_), + .Y(_084_) + ); + AND2_11_1 _232_ ( + .A(_083_), + .B(_084_), + .Y(_023_) + ); + AND2_01_0 _233_ ( + .A(fb[5]), + .B(_082_), + .Y(_085_) + ); + AND2_00_0 _234_ ( + .A(shreg[4]), + .B(_082_), + .Y(_086_) + ); + AND2_11_1 _235_ ( + .A(_085_), + .B(_086_), + .Y(_024_) + ); + AND2_01_0 _236_ ( + .A(fb[4]), + .B(_082_), + .Y(_087_) + ); + AND2_00_0 _237_ ( + .A(shreg[3]), + .B(_082_), + .Y(_088_) + ); + AND2_11_1 _238_ ( + .A(_087_), + .B(_088_), + .Y(_025_) + ); + AND2_01_0 _239_ ( + .A(fb[3]), + .B(_082_), + .Y(_089_) + ); + AND2_00_0 _240_ ( + .A(shreg[2]), + .B(_082_), + .Y(_090_) + ); + AND2_11_1 _241_ ( + .A(_089_), + .B(_090_), + .Y(_026_) + ); + AND2_01_0 _242_ ( + .A(fb[2]), + .B(_082_), + .Y(_091_) + ); + AND2_00_0 _243_ ( + .A(shreg[1]), + .B(_082_), + .Y(_092_) + ); + AND2_11_1 _244_ ( + .A(_091_), + .B(_092_), + .Y(_027_) + ); + AND2_01_0 _245_ ( + .A(fb[1]), + .B(_082_), + .Y(_093_) + ); + AND2_00_0 _246_ ( + .A(shreg[0]), + .B(_082_), + .Y(_094_) + ); + AND2_11_1 _247_ ( + .A(_093_), + .B(_094_), + .Y(_028_) + ); + AND2_01_0 _248_ ( + .A(fb[0]), + .B(_082_), + .Y(_095_) + ); + AND2_00_0 _249_ ( + .A(sio_i[1]), + .B(_082_), + .Y(_096_) + ); + AND2_11_1 _250_ ( + .A(_095_), + .B(_096_), + .Y(_029_) + ); + AND2_00_0 _251_ ( + .A(_126_), + .B(_053_), + .Y(_097_) + ); + AND2_01_0 _252_ ( + .A(_081_), + .B(_097_), + .Y(_030_) + ); + AND2_00_0 _253_ ( + .A(phase), + .B(done), + .Y(_098_) + ); + AND2_01_0 _254_ ( + .A(_128_), + .B(done), + .Y(_099_) + ); + INV _255_ ( + .A(_099_), + .Y(_100_) + ); + AND2_11_1 _256_ ( + .A(_098_), + .B(_099_), + .Y(_031_) + ); + AND2_11_1 _257_ ( + .A(bitidx[6]), + .B(_045_), + .Y(_101_) + ); + AND2_00_0 _258_ ( + .A(bitidx[6]), + .B(_045_), + .Y(_102_) + ); + AND2_01_0 _259_ ( + .A(_101_), + .B(_102_), + .Y(_032_) + ); + AND2_00_0 _260_ ( + .A(fa[7]), + .B(_065_), + .Y(_103_) + ); + AND2_00_0 _261_ ( + .A(shreg[6]), + .B(_064_), + .Y(_104_) + ); + AND2_11_1 _262_ ( + .A(_103_), + .B(_104_), + .Y(_033_) + ); + AND2_11_1 _263_ ( + .A(_051_), + .B(_063_), + .Y(_001_) + ); + AND2_01_0 _264_ ( + .A(fb[7]), + .B(_082_), + .Y(_105_) + ); + AND2_00_0 _265_ ( + .A(shreg[6]), + .B(_082_), + .Y(_106_) + ); + AND2_11_1 _266_ ( + .A(_105_), + .B(_106_), + .Y(_034_) + ); + AND2_00_0 _267_ ( + .A(txn[1]), + .B(_081_), + .Y(_107_) + ); + AND2_11_1 _268_ ( + .A(_082_), + .B(_107_), + .Y(_035_) + ); + AND2_11_1 _269_ ( + .A(_051_), + .B(_066_), + .Y(_000_) + ); + AND2_00_0 _270_ ( + .A(_129_), + .B(_048_), + .Y(_108_) + ); + AND2_11_1 _271_ ( + .A(_100_), + .B(_108_), + .Y(_109_) + ); + AND2_11_1 _272_ ( + .A(sio_o[0]), + .B(_099_), + .Y(_110_) + ); + AND2_00_0 _273_ ( + .A(_109_), + .B(_110_), + .Y(_036_) + ); + AND2_11_0 _274_ ( + .A(fb[6]), + .B(fb[2]), + .Y(_111_) + ); + AND2_11_0 _275_ ( + .A(fb[3]), + .B(fb[0]), + .Y(_112_) + ); + AND2_00_0 _276_ ( + .A(_111_), + .B(_112_), + .Y(_113_) + ); + AND2_00_0 _277_ ( + .A(done), + .B(_113_), + .Y(_114_) + ); + AND2_01_0 _278_ ( + .A(fa[5]), + .B(fa[1]), + .Y(_115_) + ); + AND2_00_0 _279_ ( + .A(fa[0]), + .B(fa[7]), + .Y(_116_) + ); + AND2_00_0 _280_ ( + .A(_115_), + .B(_116_), + .Y(_117_) + ); + AND2_11_0 _281_ ( + .A(fa[6]), + .B(fa[4]), + .Y(_118_) + ); + AND2_11_0 _282_ ( + .A(fa[2]), + .B(fa[3]), + .Y(_119_) + ); + AND2_00_0 _283_ ( + .A(_118_), + .B(_119_), + .Y(_120_) + ); + AND2_00_0 _284_ ( + .A(fb[5]), + .B(fb[4]), + .Y(_121_) + ); + AND2_00_0 _285_ ( + .A(fb[1]), + .B(fb[7]), + .Y(_122_) + ); + AND2_00_0 _286_ ( + .A(_121_), + .B(_122_), + .Y(_123_) + ); + AND2_00_0 _287_ ( + .A(_120_), + .B(_123_), + .Y(_124_) + ); + AND2_00_0 _288_ ( + .A(_117_), + .B(_124_), + .Y(_125_) + ); + AND2_00_0 _289_ ( + .A(_114_), + .B(_125_), + .Y(all_match) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _290_ ( + .CLK(clk), + .D(_030_), + .Q(txn[0]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _291_ ( + .CLK(clk), + .D(_035_), + .Q(txn[1]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _292_ ( + .CLK(clk), + .D(_029_), + .Q(fb[0]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _293_ ( + .CLK(clk), + .D(_028_), + .Q(fb[1]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _294_ ( + .CLK(clk), + .D(_027_), + .Q(fb[2]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _295_ ( + .CLK(clk), + .D(_026_), + .Q(fb[3]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _296_ ( + .CLK(clk), + .D(_025_), + .Q(fb[4]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _297_ ( + .CLK(clk), + .D(_024_), + .Q(fb[5]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _298_ ( + .CLK(clk), + .D(_023_), + .Q(fb[6]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _299_ ( + .CLK(clk), + .D(_034_), + .Q(fb[7]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _300_ ( + .CLK(clk), + .D(_036_), + .Q(sio_o[0]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _301_ ( + .CLK(clk), + .D(_022_), + .Q(fa[0]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _302_ ( + .CLK(clk), + .D(_021_), + .Q(fa[1]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _303_ ( + .CLK(clk), + .D(_020_), + .Q(fa[2]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _304_ ( + .CLK(clk), + .D(_019_), + .Q(fa[3]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _305_ ( + .CLK(clk), + .D(_018_), + .Q(fa[4]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _306_ ( + .CLK(clk), + .D(_017_), + .Q(fa[5]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _307_ ( + .CLK(clk), + .D(_016_), + .Q(fa[6]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _308_ ( + .CLK(clk), + .D(_033_), + .Q(fa[7]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _309_ ( + .CLK(clk), + .D(_015_), + .Q(bitidx[0]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _310_ ( + .CLK(clk), + .D(_014_), + .Q(bitidx[1]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _311_ ( + .CLK(clk), + .D(_013_), + .Q(bitidx[2]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _312_ ( + .CLK(clk), + .D(_012_), + .Q(bitidx[3]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _313_ ( + .CLK(clk), + .D(_011_), + .Q(bitidx[4]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _314_ ( + .CLK(clk), + .D(_010_), + .Q(bitidx[5]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _315_ ( + .CLK(clk), + .D(_032_), + .Q(bitidx[6]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _316_ ( + .CLK(clk), + .D(_031_), + .Q(phase), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _317_ ( + .CLK(clk), + .D(_009_), + .Q(shreg[0]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _318_ ( + .CLK(clk), + .D(_008_), + .Q(shreg[1]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _319_ ( + .CLK(clk), + .D(_007_), + .Q(shreg[2]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _320_ ( + .CLK(clk), + .D(_006_), + .Q(shreg[3]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _321_ ( + .CLK(clk), + .D(_005_), + .Q(shreg[4]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _322_ ( + .CLK(clk), + .D(_004_), + .Q(shreg[5]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _323_ ( + .CLK(clk), + .D(_003_), + .Q(shreg[6]), + .R(rst_n), + .S(1'h1) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _324_ ( + .CLK(clk), + .D(_000_), + .Q(cs_n_a), + .R(1'h1), + .S(rst_n) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _325_ ( + .CLK(clk), + .D(_001_), + .Q(cs_n_b), + .R(1'h1), + .S(rst_n) + ); + (* src = "shared_bus_dut.v:51.5-88.8" *) + DFFSR _326_ ( + .CLK(clk), + .D(_002_), + .Q(sck), + .R(rst_n), + .S(1'h1) + ); + assign sio_o[3:1] = 3'h0; +endmodule diff --git a/tests/qspi_shared_bus/sim_config.json b/tests/qspi_shared_bus/sim_config.json new file mode 100644 index 00000000..19b55e9d --- /dev/null +++ b/tests/qspi_shared_bus/sim_config.json @@ -0,0 +1,65 @@ +{ + "netlist_path": "tests/qspi_shared_bus/shared_bus_dut_synth.gv", + "clock_gpio": 0, + "reset_gpio": 1, + "reset_active_high": false, + "reset_cycles": 6, + "num_cycles": 300, + "clock_period_ps": 40000, + "_comment": "Two flashes on a SHARED bus: both use clk_gpio=2 and d0_gpio=5 (shared SCK + SIO), distinct csn_gpio (3 vs 4). Exercises the CS-gated MISO arbitration — a deselected flash must not drive the shared MISO.", + "qspi_memory": [ + { + "name": "fa", + "clk_gpio": 2, + "csn_gpio": 3, + "d0_gpio": 5, + "firmware": "tests/qspi_shared_bus/fw/flashA.bin", + "size_bytes": 65536 + }, + { + "name": "fb", + "clk_gpio": 2, + "csn_gpio": 4, + "d0_gpio": 5, + "firmware": "tests/qspi_shared_bus/fw/flashB.bin", + "size_bytes": 65536 + } + ], + "port_mapping": { + "inputs": { + "0": "clk", + "1": "rst_n", + "5": "sio_i[0]", + "6": "sio_i[1]", + "7": "sio_i[2]", + "8": "sio_i[3]" + }, + "outputs": { + "2": "sck", + "3": "cs_n_a", + "4": "cs_n_b", + "5": "sio_o[0]", + "6": "sio_o[1]", + "7": "sio_o[2]", + "8": "sio_o[3]", + "9": "fa[0]", + "10": "fa[1]", + "11": "fa[2]", + "12": "fa[3]", + "13": "fa[4]", + "14": "fa[5]", + "15": "fa[6]", + "16": "fa[7]", + "17": "fb[0]", + "18": "fb[1]", + "19": "fb[2]", + "20": "fb[3]", + "21": "fb[4]", + "22": "fb[5]", + "23": "fb[6]", + "24": "fb[7]", + "25": "done", + "26": "all_match" + } + } +} diff --git a/tests/qspi_shared_bus/synth.tcl b/tests/qspi_shared_bus/synth.tcl new file mode 100644 index 00000000..12923234 --- /dev/null +++ b/tests/qspi_shared_bus/synth.tcl @@ -0,0 +1,18 @@ +# Yosys synth for the shared-bus QSPI cosim DUT. +# Pure logic → AIGPDK (nomem lib): the DUT has no on-chip memory (the two +# flashes are external cosim models), so no memlib step is needed. +read_verilog shared_bus_dut.v +hierarchy -check -top shared_bus_dut +proc;; +flatten +opt_expr; opt_dff; opt_clean +techmap +opt +dfflibmap -liberty ../../aigpdk/aigpdk_nomem.lib +abc -liberty ../../aigpdk/aigpdk_nomem.lib +opt_clean -purge +techmap +abc -liberty ../../aigpdk/aigpdk_nomem.lib +opt_clean -purge +write_verilog shared_bus_dut_synth.gv +stat