From 3131762b35e0ebfcd196395f955e7c1005bd18e7 Mon Sep 17 00:00:00 2001 From: maksyuki Date: Sat, 10 Oct 2026 18:04:19 +0800 Subject: [PATCH] feat: refactor tests for SRAM and CPU in Tiny R2 P4 flow - Updated SRAM instance paths in to reflect new hierarchy. - Enhanced to include additional source files and updated parameterized tests for SRAM configurations. - Modified to align with new SRAM structure and added a test for stale macro hierarchy. - Introduced to validate dual-port local-memory protocol and CPU debug/reset functionality, including comprehensive simulation checks. --- app/apps/ci_smoke/mcu_acceptance.S | 12 + app/apps/ci_smoke/mcu_acceptance.h | 1 + app/apps/ci_smoke/tiny.c | 20 +- docs/ip/tiny-soc-r2-local-memory.md | 387 ++++++++++ docs/ip/tiny-soc-verification.md | 66 +- .../tiny/scripts/generate_chip_config.py | 19 +- physical/smoke/sta/opensta/opensta.tcl | 3 + .../smoke/sta/opensta/tiny_r2_p4_checks.tcl | 54 ++ .../datasheets/tiny/chapter-review.md | 2 +- .../datasheets/tiny/content-review.md | 10 +- publications/datasheets/tiny/datasheet.typ | 17 +- publications/datasheets/tiny/diagrams.typ | 11 +- .../datasheets/tiny/source-contract.json | 10 +- publications/tiny_reference.py | 27 +- rtl/ip/core/mgmt_core_wrapper.sv | 59 +- rtl/ip/core/mgmt_debug_reset.sv | 3 + rtl/ip/core/mgmt_debug_wrapper.sv | 14 +- rtl/rtl_readiness.json | 17 + rtl/rtl_style_audit.json | 7 + rtl/tiny/Makefile | 17 +- rtl/tiny/README.md | 10 +- rtl/tiny/dv/retrosoc_tiny_tb.sv | 154 ++-- rtl/tiny/filelist/top.fl | 7 + rtl/tiny/integration/clock_reset_domains.json | 12 + rtl/tiny/integration/soc_topology.json | 10 + rtl/tiny/top/retrosoc_tiny.sv | 70 +- rtl/tiny/top/tiny_axi4_fabric.sv | 11 +- rtl/tiny/top/tiny_cpu_mem.sv | 259 +++++++ rtl/tiny/top/tiny_cpu_wrapper.sv | 219 ++++++ rtl/tiny/top/tiny_sram.sv | 107 +++ rtl/tiny/top/tiny_sram_axi4.sv | 232 ++++++ rtl/tiny/top/tiny_sram_axi4_demux.sv | 130 ++++ rtl/tiny/top/tiny_sram_group.sv | 206 ++++++ rtl/tiny/top/tiny_sram_port_if.sv | 25 + scripts/generate_tiny.py | 11 + scripts/tiny_ics55_platform.py | 8 +- scripts/tiny_r2_p4.py | 255 +++++++ tests/rtl/tiny_axi4_fabric_tb.sv | 12 +- tests/rtl/tiny_r2_p4_cpu.S | 67 ++ tests/rtl/tiny_r2_p4_cpu.ld | 6 + tests/rtl/tiny_r2_p4_cpu_tb.sv | 282 ++++++++ tests/rtl/tiny_r2_p4_formal.sv | 58 ++ tests/rtl/tiny_r2_p4_memory_tb.sv | 659 ++++++++++++++++++ tests/test_librelane_tiny_flow.py | 4 +- tests/test_publication_tiny.py | 9 +- tests/test_tiny_ics55_platform.py | 20 +- tests/test_tiny_r2_p4.py | 191 +++++ 47 files changed, 3627 insertions(+), 163 deletions(-) create mode 100644 docs/ip/tiny-soc-r2-local-memory.md create mode 100644 physical/smoke/sta/opensta/tiny_r2_p4_checks.tcl create mode 100644 rtl/tiny/top/tiny_cpu_mem.sv create mode 100644 rtl/tiny/top/tiny_cpu_wrapper.sv create mode 100644 rtl/tiny/top/tiny_sram.sv create mode 100644 rtl/tiny/top/tiny_sram_axi4.sv create mode 100644 rtl/tiny/top/tiny_sram_axi4_demux.sv create mode 100644 rtl/tiny/top/tiny_sram_group.sv create mode 100644 rtl/tiny/top/tiny_sram_port_if.sv create mode 100644 scripts/tiny_r2_p4.py create mode 100644 tests/rtl/tiny_r2_p4_cpu.S create mode 100644 tests/rtl/tiny_r2_p4_cpu.ld create mode 100644 tests/rtl/tiny_r2_p4_cpu_tb.sv create mode 100644 tests/rtl/tiny_r2_p4_formal.sv create mode 100644 tests/rtl/tiny_r2_p4_memory_tb.sv create mode 100644 tests/test_tiny_r2_p4.py diff --git a/app/apps/ci_smoke/mcu_acceptance.S b/app/apps/ci_smoke/mcu_acceptance.S index e48bfe8c..cd4ee824 100644 --- a/app/apps/ci_smoke/mcu_acceptance.S +++ b/app/apps/ci_smoke/mcu_acceptance.S @@ -34,3 +34,15 @@ rs_mcu_unmapped_probe: .option pop ret .size rs_mcu_unmapped_probe, .-rs_mcu_unmapped_probe + + .balign 4 + .global rs_mcu_execute_probe + .type rs_mcu_execute_probe,@function +rs_mcu_execute_probe: + mv t0, a0 + .option push + .option arch, +zifencei + fence.i + .option pop + jr t0 + .size rs_mcu_execute_probe, .-rs_mcu_execute_probe diff --git a/app/apps/ci_smoke/mcu_acceptance.h b/app/apps/ci_smoke/mcu_acceptance.h index 3f05d234..2c791b1d 100644 --- a/app/apps/ci_smoke/mcu_acceptance.h +++ b/app/apps/ci_smoke/mcu_acceptance.h @@ -6,5 +6,6 @@ uint32_t rs_mcu_compressed_probe(void); void rs_mcu_atomic_probe(uint32_t *address); void rs_mcu_unmapped_probe(void); +uint32_t rs_mcu_execute_probe(uint32_t address); #endif diff --git a/app/apps/ci_smoke/tiny.c b/app/apps/ci_smoke/tiny.c index df318bc1..4e7bf321 100644 --- a/app/apps/ci_smoke/tiny.c +++ b/app/apps/ci_smoke/tiny.c @@ -92,7 +92,25 @@ static bool rs_mcu_dma(void) { } config.kind = RS_DMA_KIND_MM_TO_STREAM; config.request = RS_DMA_REQUEST_I2S_TX; - return rs_dma_config_validate(0U, &config) != RS_OK; + if (rs_dma_config_validate(0U, &config) == RS_OK) { + return false; + } + /* DMA owns the destination until completion. FENCE.I is executed by the + * assembly helper before entering either the DMA-written or patched code. */ + rs_mcu_source[0] = UINT32_C(0x02a00513); /* addi a0,zero,42 */ + rs_mcu_source[1] = UINT32_C(0x00008067); /* jalr zero,ra,0 */ + config.kind = RS_DMA_KIND_MM_TO_MM; + config.request = RS_DMA_REQUEST_SOFTWARE; + config.byte_count = UINT32_C(8); + config.burst_beats = 1U; + if ((rs_dma_configure(RS_DMA_CHANNEL_BULK, &config) != RS_OK) || + (rs_dma_start(RS_DMA_CHANNEL_BULK) != RS_OK) || + (rs_dma_wait(RS_DMA_CHANNEL_BULK, RS_TIMEOUT_DEFAULT) != RS_OK) || + (rs_mcu_execute_probe((uint32_t)(uintptr_t)rs_mcu_destination) != UINT32_C(42))) { + return false; + } + rs_mcu_destination[0] = UINT32_C(0x02b00513); /* addi a0,zero,43 */ + return rs_mcu_execute_probe((uint32_t)(uintptr_t)rs_mcu_destination) == UINT32_C(43); } static bool rs_mcu_interrupts(void) { diff --git a/docs/ip/tiny-soc-r2-local-memory.md b/docs/ip/tiny-soc-r2-local-memory.md new file mode 100644 index 00000000..76fd0200 --- /dev/null +++ b/docs/ip/tiny-soc-r2-local-memory.md @@ -0,0 +1,387 @@ +# TINY-R2-P4 local CPU/SRAM implementation + +## Approved throughput repair (2026-10-10; validated, review pending) + +The maintainer approved the eight-file P4 throughput-repair preflight after +the read-only review identified the external frontend's per-beat bubbles. +This follow-up remains P4; it does not enable P5 target concurrency. +The initial implementation and its measurements below are retained historical +evidence for the pre-repair snapshot, not qualification of the changed RTL. +Fresh evidence is under +`build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/`. + +The group now reserves two response slots for its external client, counting +both queued completions and a macro read still in flight. A response pop may +free capacity on the same edge as the next issue. The macro's saved owner/bank +still selects the captured read value; FIFO data cannot be overwritten by +later local accesses. A same-client pending read prevents a simultaneous write +commit into the single-push response FIFO. Local I/D keep one operation each +and their original completion pipeline. + +The external frontend separates issued and retired read positions. W uses a +two-entry Common FIFO containing captured address, data and byte strobes. +Only captured legal beats enter arbitration. Invalid strobes or LAST retain +the existing error policy, including earlier committed beats and later legal +beats after an invalid strobe. B waits for all queued legal writes and their +commit responses. External transaction ownership still lasts through B/RLAST. +These are small register queues, not additional user SRAM or new macros. + +Acceptance adds consecutive 16-beat read/write handshakes at one SYS cycle per +beat after startup, without local competition or bus backpressure. Logs retain +first-beat latency, total transfer time and final-W-to-B delay. Directed checks +fill/drain queues, stop READY, interleave local service, retain old read values +across local writes, exercise buffered malformed writes and drop stale queued +responses on system reset. Induction checks response conservation/capacity in +addition to held-response stability and the existing service-count fairness. +The unchanged CPU debug/reset, discovery/counters and both real-PDK macro +fixtures remain required. + +Use the same immutable P3 flat/banked images and parameters. Each selected +Verilator image runs once; banked Icarus runs once. Compare both with their P3 +samples and the pre-repair P4 report. The repair requires burst-16 cycles below +36713/36489 respectively, truthful reporting of all other workload deltas and +identical banked results across simulators. It does not promise P3 cycle parity +or change compiler/layout settings. Existing shared Mini debug inputs are +unchanged by this repair; matching prior compatibility evidence is retained. + +### Repair results and provenance + +The campaign is complete for review. `source.json`, `report.json`, `audit.py` +and `evidence.json` in the fresh evidence root bind 1972 technical inputs, +six fresh Tiny regression simulations, three matched-image executions, +six real/behavioral SRAM fixtures, two CPU fixtures and control induction. +The combined ICS55/IHP130 input digest is +`58daf68fbb9dee199b3085a73ef07d22eb3de6d00ba875c462e85b76b6091a0d`; +the ICS55 replay input digest is +`a96a129ffc02d0d38fd2eee7ed8fb62893828db3602bbd4459e0a71ffce0c35f`. +Only the approved six technical files differ from the preceding P4 snapshot; +the other two changes are this runbook and the ledger. No commit or staging +operation is part of this campaign. + +All six SRAM runs record `read_gap_max=1 write_gap_max=1 beats=16`, with first +R at three cycles after AR and final R at cycle18; W begins one cycle after +AW, final B is at cycle20, four cycles after final W. These are standalone +uncontended protocol measurements, not end-to-end DMA or physical timing. +All six retain `local_latency_max=3`. The new queue checks cover full-response +backpressure, local writes to already captured read addresses, W contention, +buffered malformed writes, and both queued-R and accepted-but-uncommitted-W +system resets. CPU debug/patch/hart-reset and induction pass in the final suite. + +| Gate | Result under the fresh evidence root | Outcome | +| --- | --- | --- | +| Final Ruff, diff, RTL format/style/readiness | `quality-pass.{json,log}` | Pass | +| Full Pytest | `pytest-full.{json,log}` | 1659 passed, one optional `pypdf` skip; 3903.061 s | +| Tiny ICS55 PR | `regress-tiny-ics55.{json,log}` | Pass; 3396.035 s | +| Tiny IHP130 PR | `regress-tiny-ihp130.{json,log}` | Pass; 2340.150 s | +| Verilator, both immutable images once | `replay-verilator.{json,log}` | Pass; 390.698 s | +| Icarus, immutable banked image once | `replay-iverilog.{json,log}` | Pass; 5154.223 s | +| Cross-simulator report | `report-result.{json,log}`, `report.json` | Complete; all banked observations identical | +| Source/artifact audit | `evidence.json`, `audit.py` | Pass; review/phase acceptance pending | + +The initial `quality-final.{json,log}` is a retained failed development attempt: +the style gate required `s_req_accept`/`s_resp_accept` abbreviations. The names +were corrected before the final source snapshot and all long gates. The first +29 focused checks and additional queued-W-reset check also passed; final-suite +logs qualify the final technical snapshot. No failed attempt is relabeled. + +| Workload | Flat pre-repair P4 → repaired P4 | Banked pre-repair P4 → repaired P4 | +| --- | ---: | ---: | +| Empty | 69 → 69 | 66 → 66 | +| CPU | 73911 → 73911 | 73914 → 73914 | +| CPU copy | 102568 → 102568 | 86214 → 86214 | +| DMA burst1 | 91306 → 91306 | 89365 → 89365 | +| DMA burst16 | 36713 → 17897 | 36489 → 17317 | +| CPU/DMA contention | 303493 → 299881 | 299633 → 299633 | +| TCD contention | 303702 → 299878 | 299645 → 299645 | +| Chunk256 | 377705 → 361285 | 376970 → 359570 | + +Burst16 cycles decrease 51.25%/52.54%, satisfying the approved repair criterion. +They remain 8.34%/4.81% above the original P3 16520/16523 cycles. Burst1 still +retains the earlier P4 overhead versus P3; this repair does not claim universal +baseline parity. Flat local maximum is four cycles in contended windows, with +17697 eligible bank-conflict cycles; banked maximum is three with zero conflicts. +Uncontended local latency is still checked independently. Firmware checksums, +parameters and each image's compiler/layout identity remain unchanged. + +Fresh physical variants are +`build/ics55-tiny-2026-10-10-15-15-42d5beccb726/` and +`build/ihp130-tiny-2026-10-10-15-15-bff91e3529ba/`. +Both pass synthesis, netlist boot and common-SYS/32-macro binding checks. +Their `meta/metrics.json`, `sta/opensta/timing_metrics.rpt`, +`sta/opensta/p4-clock-bindings.rpt` and `sta/opensta/retrosoc_sta.log` retain +the matching observations: + +| PDK | Cells | Library area | Setup WNS / TNS (ns) | Hold WNS / TNS (ns) | +| --- | ---: | ---: | ---: | ---: | +| ICS55 | 207029 | 1805809.700799 | -36.88 / -489807.34 | 0 / 0 | +| IHP130 | 218773 | 9245382.228597 | -97.37 / -1640251.62 | 0 / 0 | + +Setup remains unclosed; library area is not die area and zero observed hold +slack violations are not physical signoff. Lint comparisons to the unchanged +baselines report 468/412 new signatures for ICS55/IHP130, zero increased and +zero resolved. Relative to pre-repair P4 there are exactly two additional +signatures: intentionally unused FIFO `cnt_o`, and `s_full`, which is consumed +by the verification assertions but unused in synthesis-oriented lint. No new +width/latch warning is attributed to the repair. Warning and metrics policy +remain observational and no baseline was edited. + +Commands below used the same timestamp, with `scripts/run_flow.py` retaining +outer command/result/log records. Standalone test artifacts use distinct +`tests-focused`, `tests-reset` and `tests-full` directories under this variant. + +```sh +source .cache/retrosoc/development/tiny-r2-p1/activate.sh +export BUILD_TIMESTAMP=2026-10-10-15-15 JOBS=3 +python3 -m pytest -q tests/test_tiny_r2_p4.py --basetemp=build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/tests-focused +python3 -m pytest -q 'tests/test_tiny_r2_p4.py::test_four_group_local_memory[BEHAV-iverilog]' --basetemp=build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/tests-reset +ruff check . +git diff --check +make rtl-format-check rtl-style-check-all rtl-readiness-check-all +python3 -m pytest -q --basetemp=build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/tests-full +python3 scripts/regress.py --root . --suite pr --soc TINY --pdk ICS55 --dry-run +python3 scripts/regress.py --root . --suite nightly --soc TINY --pdk ICS55 --dry-run +python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130 --dry-run +python3 scripts/regress.py --root . --suite nightly --soc TINY --pdk IHP130 --dry-run +python3 scripts/regress.py --root . --suite pr --soc TINY --pdk ICS55 +python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130 +make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 SIMU=VERILATOR SOC_SIM_TIME=21600 \ + TINY_P4_REFERENCE=build/ics55-tiny-performance-2026-10-09-17-10-53cc6674d532/meta/tiny-r2-p3/report.json \ + TINY_P4_CASES=perf-o2-plain-flat,perf-o2-plain-banked tiny-r2-p4-replay +make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 SIMU=IVERILOG SOC_SIM_TIME=21600 \ + TINY_P4_REFERENCE=build/ics55-tiny-performance-2026-10-09-17-10-53cc6674d532/meta/tiny-r2-p3/report.json \ + TINY_P4_CASES=perf-o2-plain-banked tiny-r2-p4-replay +make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 tiny-r2-p4-report +make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 APP=ci_smoke SYNTH=YOSYS STA=OPENSTA metrics +make CONFIG=configs/ci/ihp130-tiny.mk SOC=TINY PDK=IHP130 APP=ci_smoke SYNTH=YOSYS STA=OPENSTA metrics +PYTHONDONTWRITEBYTECODE=1 python3 build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/audit.py +``` + +Tiny PR/nightly dry-runs match; no duplicate nightly execution was needed. +The six fresh ordinary simulations all require `SIM_TEST_PASS`, including +Tiny netlist boot, together with command success and no forbidden error marker. +Mini's two prior behavioral results retain their original source attribution; +the audit verifies that changed technical inputs are confined to Tiny and its +tests. Mini synthesis/STA, the P3 compiler matrix, additional cold repetitions, +C format/policy/host reruns, PDF generation, PVT/MMMC, CDC/RDC and place/route +were not run for this repair. No C/H code or MISRA deviation changed. The +full-suite optional PDF retrieval check remains skipped for missing `pypdf`. +No new frequency, physical qualification or P5 authorization is claimed. + +## Original implementation snapshot + +This implements the approved [P4 contract](tiny-soc.md#tiny-r2-p4---dual-port-hazard3-and-four-bank-local-sram) +from `4db27524a6282ca40d76b935415b89f043d6ea83`, specification blob +`7e19de49d49efaafacccfa85671e314d53dd15a4`. The maintainer approved the preflight +and implementation. No P5 execution, dependency update, publication release or +physical-frequency qualification is implied. Historical phase evidence remains +in the [ledger](tiny-soc-verification.md). + +## Architecture and lifecycle + +Tiny uses locked `hazard3_cpu_2port`, preserving ISA, IRQ, debug, multiply and +branch-prediction settings; A remains disabled. Separate I/D AHB frontends decode +SRAM before the tagged slow-path merge. One accepted D operation spans all +targets. Write data is captured in its AHB data phase; local stores complete +after macro commit. Uncontended local completion is at most three SYS cycles. +Slow errors retain two-cycle AHB completion. Instruction origin reaches the +existing fabric fault path: only NOR boot/NSS0 may be fetched externally, and +peripheral fetches cannot trigger register reads. + +Four 32 KiB groups cover `0x30000000..0x3001ffff`, each with eight existing +4 KiB macros and I/D/external round robin advancing only on macro issue. +Per-client registers capture read responses before macro outputs change. +External W waits and R/B backpressure do not lock local macro service. +Continuously eligible requests wait for at most two other issues, conditional +on progress rather than a wall-clock bound for stopped peers. + +Each external frontend retains one transaction through B/RLAST and alternates +read/write preference. A direction-tagged demux preserves the existing external +fabric attachment. Whole-product different-target DMA read/write concurrency +remains P5; standalone frontend concurrency is not that product qualification. + +Hart reset requests debug halt, prevents new debug instruction injection, drains +accepted local/slow operations and responses, then resets the hart/frontends. +A terminal-cycle AHB overlap must still drain. SRAM/external frontends and +unrelated DMA stay active. System reset invalidates pending responses without +clearing SRAM payload. Five-edge release and Mini's default single-port reset +behavior are preserved; shared debug only adds a pending-state output. + +## ABI, physical bindings and observations + +Shared SRAM register definitions are unchanged: 128 KiB, `BANK_COUNT=32`, +`BANK_BYTES=4096`. Public counters count external AXI events, saturating sums +of simultaneous events; stall cycles use OR across groups. Local CPU traffic +is not silently added. `R2_AHB` observes the separate I/D ports. `R2_P4_LOCAL` +records local completions, latency and address-admission waits; `R2_P4_BANK` +records macro issues, eligible-client waits and conflict cycles (two or more +eligible clients in a group). These are verification-only observations. + +Macro `g,b` is `u_soc.u_sram.gen_group[g].u_group.gen_bank[b].u_ram.u_mem`, +physical index `8*g+b`. IHP130 placement coordinates stay fixed while instance +and supply hooks track both indices. CPU and all 32 macros remain directly on +SYS24; ICS55 PLL stays off and IHP130 remains no-PLL compatibility. Mapping +checks reject stale single-port/flat-bank hierarchies. STA remains observational, +with no 96/192/240 MHz or physical signoff claim. + +The Tiny publication extractor and affected draft prose/diagram follow the new +bindings. No reviewed publication SHA, delivered PDF or release status advances. + +## Acceptance and matched images + +`tests/test_tiny_r2_p4.py` uses real IHP130/ICS55 macro models and a behavioral +model on both simulators: all macro/group boundaries, byte masks, back-to-back +AHB, independent groups, held responses, W-before-AW, bursts/malformed writes, +discovery/counter semantics and reset payload retention. Deterministic readback +uses seed `0x12345678`, 4096 write/read pairs per group. The actual CPU fixture +checks mixed 16/32-bit cross-group fetch, FENCE.I, instruction faults, JTAG +patch/resume, and reset with a held slow response plus an unrelated held B. +Yosys induction proves group control fairness and retained responses with only +macro payload abstracted. Normal Tiny firmware additionally executes code +copied through the real DMA, then CPU-patched code, after completion/FENCE.I. +Its helper is private acceptance assembly, not a new SDK API. + +`tiny-r2-p4-replay` validates retained P3 images/logs, builds a fresh P4 model, +and executes each selected image once. Image source/compiler/layout and new +RTL/model identities are separate; hardware settings must match. Original P3 +same-source/three-cold-start rules are unchanged. `tiny-r2-p4-report` revalidates +artifacts and requires identical banked-image results across the two simulators. + +The reference is +`build/ics55-tiny-performance-2026-10-09-17-10-53cc6674d532/meta/tiny-r2-p3/report.json`. +Select `perf-o2-plain-flat` and `perf-o2-plain-banked` for Verilator and the banked +image for Icarus. Parameters remain 1024 words, four jobs, 4096 CPU iterations, +seed `0x12345678`, burst-1/16 DMA, TCD and chunk-256. Compare each unchanged +binary with its own historical samples, not with a different compiler/layout. +No fixed speedup is predeclared; latency/fairness requirements remain gates. + +Development evidence begins under +`build/ics55-tiny-2026-10-10-10-05-b4aa59f3e5d9/meta/tiny-r2-p4/`. +Early fixture failures and corrected runs are retained separately. The final +campaign below completes planned functional validation and measurements; human +review/phase acceptance remain pending, including the DMA throughput tradeoff. + +## Executed campaign and provenance + +The evidence root above contains `source.json`, `evidence.json`, `audit.py` and +`report.json`. The campaign snapshot contains 1972 unchanged technical inputs, +base commit `4db27524a6282ca40d76b935415b89f043d6ea83` plus the retained patch and +new-file archive, and locked tool/executable/archive identities. Its combined +ICS55/IHP130 digest is +`9fa311a5500711c99b006eacd95cdff04a74659d52b5b46640c42c49773a7f66`. +The ICS55-only replay source set has digest +`2f0d285186f84d41422d8381d09b0a01a2afc066dbd011a11ec7a8a85433ad0d`; +these are different declared input sets, not conflicting identities. + +The final audit rechecks all captured inputs, eight ordinary regression +simulations, the three matched-image executions, six standalone memory runs, +two actual-CPU runs and the group-control induction log. Ordinary firmware and +model artifacts are hashed post-run; the campaign source hashes were captured +before execution and rechecked afterward. Image/model identities and original +P3 phase attribution are retained separately. + +| Gate | Retained result under the evidence root | Outcome | +| --- | --- | --- | +| Format/style/readiness, C policy/host, Ruff/diff | `quality-final.{json,log}` | Pass; no new MISRA deviation; partial checks, not MISRA certification | +| Full Pytest | `pytest-full.{json,log}` | 1659 passed, one optional `pypdf` environment skip; 3856.630 s | +| Tiny ICS55 PR | `regress-tiny-ics55.{json,log}` | Pass; 3295.064 s | +| Tiny IHP130 PR | `regress-tiny-ihp130.{json,log}` | Pass; 2297.373 s | +| Mini ICS55 behavioral-only PR | `regress-mini-ics55.{json,log}` | Pass; 3093.379 s; no Mini physical evidence | +| P4 Verilator replay | `replay-verilator.{json,log}` | Both selected images once; 357.537 s including build/audit | +| P4 Icarus replay | `replay-iverilog.json`, `replay-iverilog.json.log` | Banked image once; 5214.736 s including build/audit | +| Matched-image aggregation | `report-result.json`, `report.log`, `report.json` | `measurement_complete`; all banked samples match across simulators | + +The six standalone memory logs report `local_latency_max=3`, with real +simultaneous group issues. Those protocol fixtures use a 10 ns verification +clock; product replays and timing checks use SAFE24. The banked product replay +also observes three-cycle maximum local latency and zero eligible bank-conflict +cycles. No physical frequency qualification follows from functional models. + +Commands were run through `scripts/run_flow.py` for retained logs/results: + +```sh +source .cache/retrosoc/development/tiny-r2-p1/activate.sh +export BUILD_TIMESTAMP=2026-10-10-10-05 +export JOBS=3 +make rtl-format-check rtl-style-check-all rtl-readiness-check-all +make sw-format-check sw-policy-check sw-host-test +ruff check . +git diff --check +python3 -m pytest -q --basetemp=build/ics55-tiny-2026-10-10-10-05-b4aa59f3e5d9/meta/tiny-r2-p4/tests-full +python3 scripts/regress.py --root . --suite pr --soc TINY --pdk ICS55 --dry-run +python3 scripts/regress.py --root . --suite nightly --soc TINY --pdk ICS55 --dry-run +python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130 --dry-run +python3 scripts/regress.py --root . --suite pr --soc MINI --pdk ICS55 --behavioral-only --dry-run +python3 scripts/regress.py --root . --suite pr --soc TINY --pdk ICS55 +python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130 +JAVA_TOOL_OPTIONS=-Dsbt.server.forcestart=true python3 scripts/regress.py \ + --root . --suite pr --soc MINI --pdk ICS55 --behavioral-only +make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 SIMU=VERILATOR SOC_SIM_TIME=21600 \ + TINY_P4_REFERENCE=build/ics55-tiny-performance-2026-10-09-17-10-53cc6674d532/meta/tiny-r2-p3/report.json \ + TINY_P4_CASES=perf-o2-plain-flat,perf-o2-plain-banked tiny-r2-p4-replay +make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 SIMU=IVERILOG SOC_SIM_TIME=21600 \ + TINY_P4_REFERENCE=build/ics55-tiny-performance-2026-10-09-17-10-53cc6674d532/meta/tiny-r2-p3/report.json \ + TINY_P4_CASES=perf-o2-plain-banked tiny-r2-p4-replay +make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 tiny-r2-p4-report +make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 APP=ci_smoke SYNTH=YOSYS STA=OPENSTA metrics +make CONFIG=configs/ci/ihp130-tiny.mk SOC=TINY PDK=IHP130 APP=ci_smoke SYNTH=YOSYS STA=OPENSTA metrics +``` + +Tiny PR and nightly expand to the same commands; nightly was not duplicated. +Each simulation requires command success, its configured terminal marker and +no forbidden-error marker. All selected ordinary targets require SIM_TEST_PASS, +including Tiny netlist boot. UART text alone is insufficient. Development +attempts are not counted as extra qualifying cold starts. + +## Measurements and remaining qualification + +Raw cycles below compare each immutable binary with its own P3 baseline. +The report also retains retired instructions, CPI inputs, empty calibration, +local waits, macro issues/conflicts and external traffic accounting. + +| Workload | Flat P3 → P4 cycles | Banked P3 → P4 cycles | +| --- | ---: | ---: | +| CPU | 73951 → 73911 | 73954 → 73914 | +| CPU copy | 127157 → 102568 | 114875 → 86214 | +| DMA burst 1 | 64577 → 91306 | 64583 → 89365 | +| DMA burst 16 | 16520 → 36713 | 16523 → 36489 | +| CPU/DMA contention | 310368 → 303493 | 310383 → 299633 | +| TCD contention | 310665 → 303702 | 310683 → 299645 | +| Chunk 256 | 373812 → 377705 | 374016 → 376970 | + +CPU-copy cycles decrease by about 19%/25%, while isolated burst-16 DMA takes +about 2.2 times the baseline cycles. The current external frontend processes +issue, completion and bus response separately for each beat; preserving local +response isolation costs streaming throughput. This regression is retained for +review, not hidden by the CPU-copy result or a changed compiler/layout. P5's +external target concurrency alone does not remove these per-beat waits. No +fixed speedup was an acceptance gate, but the tradeoff needs explicit assessment +before phase acceptance or a throughput claim. + +Actual synthesis/STA variants are +`build/ics55-tiny-2026-10-10-10-05-42d5beccb726/` and +`build/ihp130-tiny-2026-10-10-10-05-bff91e3529ba/`. +Both emit `TINY_R2_P4_BINDING_PASS` and pass netlist boot. Their raw reports are +`sta/opensta/timing_metrics.rpt`, `p4-clock-bindings.rpt`, `retrosoc_sta.log` and +`meta/metrics.json`. + +| PDK | Cells | Reported library area | Max WNS / TNS (ns) | Min WNS / TNS (ns) | +| --- | ---: | ---: | ---: | ---: | +| ICS55 | 206311 | 1803352.980799 | -40.55 / -518700.31 | -0.02 / -11.85 | +| IHP130 | 216216 | 9218095.882797 | -97.46 / -1626503.88 | -0.17 / -142.67 | + +Timing is not closed. Library area is not placed/routed die area. Lint baseline +comparisons remain non-blocking observations: Tiny ICS55 466 new signatures, +Tiny IHP130 410 new, Mini ICS55 1537 new / 118 resolved, all with zero increased. +Compared with the preceding P3 lint runs, newly observed signatures are intentional +empty named output connections: Tiny's unused master IDs, exclusive/power/fence +outputs, plus Mini's ignored `reset_pending_o`. Tiny is cacheless with committed +stores, no A/SBA and no Xh3Power, so these outputs do not add interfaces or clock +gating. Baselines and metric policy were not edited. The logs retain the known +generated-file clock skew and successful ABC Liberty fallback observations. + +The P3 eight-configuration matrix and three-cold-start campaign were not rerun. +No place/route, extracted/PVT/CDC/RDC signoff, high-rate qualification, new PDK +rollout or PDF build/visual check was performed. Publication snapshot enforcement +remains enabled; the reviewed PDF source revision was not advanced. Mini +DMA-to-WS2812 stays unsupported under option A, and product DMA/IRQ budgets +remain for a supported integration. P5 and later features are not implemented. diff --git a/docs/ip/tiny-soc-verification.md b/docs/ip/tiny-soc-verification.md index df84d24f..bcc2f0ca 100644 --- a/docs/ip/tiny-soc-verification.md +++ b/docs/ip/tiny-soc-verification.md @@ -1,5 +1,69 @@ # Tiny MCU Verification Record +## TINY-R2-P4 throughput repair; validated, review pending (2026-10-10) + +Following the read-only P4 review, the maintainer approved the bounded-buffer +repair plan for the P2 external-SRAM throughput regression. The changed Tiny +group/frontend keep local I/D single-operation semantics, reserve external +response capacity and overlap burst issuance/retirement; W is captured before +macro arbitration and B follows complete commit-response drain. Common FIFO +and register dependencies, ABI, clock/macro bindings and the pre-P5 external +attachment remain unchanged. No shared Mini debug code is changed by this fix. + +Fresh evidence belongs under +`build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/`. +The preceding P4 report below remains the pre-repair comparison, and does not +qualify these new changes. The final audit rechecks 1972 technical inputs, +six fresh regression simulations and three immutable-image executions. Full +Pytest reports 1659 passed and one optional `pypdf` skip. Both Tiny PDK PR +regressions pass, including synthesis, netlist boot and clock/macro bindings. +Streaming/backpressure/error/reset checks and response conservation/fairness +induction pass; all six SRAM fixtures retain three-cycle local completion and +one-cycle steady read/write beat spacing. Initial style failures and focused +development attempts remain separate from final qualifying results. + +Matched burst16 DMA cycles fall from 36713 to 17897 (flat) and 36489 to 17317 +(banked), reductions of 51.25% and 52.54%. All banked observations match across +Verilator and Icarus. CPU/copy/burst1 cycles remain unchanged from pre-repair +P4; burst1 overhead and 8.34%/4.81% burst16 overhead versus original P3 remain +explicit. The combined input digest is +`58daf68fbb9dee199b3085a73ef07d22eb3de6d00ba875c462e85b76b6091a0d`. +`evidence.json` binds the source, artifacts, comparison and reused unchanged +Mini behavioral evidence. All long gates ran once; Tiny nightly matched PR +and was not duplicated. Setup WNS remains negative (-36.88 ns ICS55, +-97.37 ns IHP130), with warning/metrics observations and baselines preserved. +The [runbook](tiny-soc-r2-local-memory.md) records exact commands, measurements +and unrun qualifications. Human review remains required; no P5 execution or +physical-frequency qualification is implied. + +## TINY-R2-P4 implementation and validation; review pending (2026-10-10) + +The maintainer approved P4 preflight and implementation on clean source +`4db27524a6282ca40d76b935415b89f043d6ea83`, specification blob +`7e19de49d49efaafacccfa85671e314d53dd15a4`. The [P4 runbook](tiny-soc-r2-local-memory.md) +records architecture, lifecycle, physical binding and matched-image boundaries. +P3 results retain their original source attribution; no historical phase or +physical qualification is automatically promoted. + +Development evidence is under +`build/ics55-tiny-2026-10-10-10-05-b4aa59f3e5d9/meta/tiny-r2-p4/`. +All planned functional gates completed: 1659 Pytest passes and one optional +`pypdf` skip; Tiny ICS55/IHP130 PR and Mini ICS55 behavioral compatibility pass. +Both Tiny netlists pass boot and the four-group/32-macro/common-SYS binding +checks. Local uncontended latency is three cycles; group-control induction +passes. Setup and hold timing remain negative and observational. + +`report.json` retains single-execution matched-binary results; the banked image +matches exactly across Verilator and Icarus. CPU-copy cycles improve about +19%/25% for flat/banked images, while isolated burst-16 DMA cycles increase +about 2.2 times. This external-frontend throughput regression requires review; +no blanket speedup or automatic phase acceptance is claimed. `evidence.json` +rechecks 1972 source inputs and binds the gate, image, model, log and report +artifacts. Exact commands, measurements and unrun qualifications are in the runbook. + +External fabric stays serialized until P5, which is not authorized. Default +Tiny stays ICS55 SAFE24 with PLL off; IHP130 is explicit compatibility. + ## TINY-R2-P3 implementation in progress (2026-10-08–10) The retained source-bound compiler/layout matrix is complete for commit @@ -493,7 +557,7 @@ not a prerequisite to the architectural improvements in R2-P3 through R2-P5. | TINY-R2-P1 - Reproducible Baseline, Constraints and Measurements | Reviewed source/profile/lock/tool identity; current 24 MHz functional and workload baseline; clocks, reset endpoints, constraints and timing-exception audit; counters and comparison methodology. Historical WNS is a risk reference, not a refreshed measurement. | Functional/measurement baseline accepted by explicit P2 approval on 2026-10-06; timing, warning and physical gaps retained | | TINY-R2-P2 - Reset Distribution and CPU/SRAM Clock Feasibility | Reset distribution and payload-reset semantics; locked IHP130 main-SRAM macro timing, candidate common CPU/SRAM periods and representative paths; early clock/reset feasibility with explicit gaps. No final routed/PVT pass is implied. | Implementation and evidence ready for review; all analyzed rates retain timing failures; phase acceptance pending human review | | TINY-R2-P3 - Software and DMA Scheduling | Compiler and placement A/B results, finite DMA ownership, burst/chunk scheduling and available-path correctness; approved option A retains product DMA/IRQ budget qualification for the platform that supplies the route. | Matrix retained on `3e46cc8`; subsequent two P1 HAL fixes and focused/compatibility checks complete in the worktree; human review/acceptance pending | -| TINY-R2-P4 - Dual-Port Hazard3 and Four-Bank Local SRAM | Actual separate I/D paths, four independent 32 KiB bank frontends, SYS-clocked physical main-SRAM macros, local latency/fairness, FENCE.I, debug/reset and early inventory/SDC/macro-binding updates. | Pending | +| TINY-R2-P4 - Dual-Port Hazard3 and Four-Bank Local SRAM | Actual separate I/D paths, four independent 32 KiB bank frontends, SYS-clocked physical main-SRAM macros, local latency/fairness, FENCE.I, debug/reset and early inventory/SDC/macro-binding updates. | Implementation and planned functional validation complete on `4db2752` plus retained worktree snapshot; matched-image DMA throughput regression and human phase acceptance pending review; timing remains observational | | TINY-R2-P5 - Per-Target Concurrent Fabric | Cross-target concurrency, one combined transaction per target, central-DMA read/write overlap, AW/W binding, errors, simultaneous faults/counters and accepted-transfer drain. | Pending | | TINY-R2-P6 - Shared IP and Eight-Channel DMA Integration | Legacy P7 obligations; shared PWM reporting, I2S slave and alternate-input-route prerequisites; common ABI/HAL parity; Crypto banks/lifecycle; truthful capabilities and affected Mini compatibility. | Pending | | TINY-R2-P7 - Tiny RCU and Dual-Mode Clock/Reset Integration | Legacy P8 obligations with main SRAM in SYS and XPI in MEM; atomic rate changes, all actual CDC/reset barriers, clock failures, WFI leaf gating and reserved DVP controls. | Pending | diff --git a/physical/librelane/tiny/scripts/generate_chip_config.py b/physical/librelane/tiny/scripts/generate_chip_config.py index 782be724..85a9e08b 100644 --- a/physical/librelane/tiny/scripts/generate_chip_config.py +++ b/physical/librelane/tiny/scripts/generate_chip_config.py @@ -27,7 +27,7 @@ SIDE_ORDER = ("south", "east", "north", "west") EXPECTED_SIGNAL_PADS = 52 CLOCK_PORTS = ("extclk_i_pad", "jtag_tck_i_pad") SRAM_MASTER = "RM_IHPSG13_1P_1024x32_c2_bm_bist" -SRAM_INSTANCE = "u_soc.u_sram.gen_memory.gen_bank[{index}].u_ram.u_mem" +SRAM_INSTANCE = "u_soc.u_sram.gen_group[{group}].u_group.gen_bank[{bank}].u_ram.u_mem" SRAM_COLUMNS = 8 SRAM_ORIGIN = (400, 600) # Detailed routing requires an M2 track inside each bank's 0.26 um bottom-edge @@ -95,7 +95,7 @@ def sram_instances(capacity_kib: int) -> dict[str, tuple[list[int], str]]: SRAM_ORIGIN[0] + column * SRAM_PITCH[0], SRAM_ORIGIN[1] + row * SRAM_PITCH[1], ] - instances[SRAM_INSTANCE.format(index=index)] = (location, "N") + instances[SRAM_INSTANCE.format(group=index // 8, bank=index % 8)] = (location, "N") return instances @@ -122,18 +122,9 @@ def macro_config(instances: dict[str, tuple[list[int], str]], header: str) -> di def macro_hooks(instances: dict[str, tuple[list[int], str]]) -> list[str]: hooks: list[str] = [] for instance in instances: - bracket_start = instance.find("[") - if bracket_start >= 0: - bracket_end = instance.index("]", bracket_start) - instance_pattern = ( - re.escape(instance[:bracket_start]) - + ".*" - + instance[bracket_start + 1 : bracket_end] - + ".*" - + re.escape(instance[bracket_end + 1 :]) - ) - else: - instance_pattern = re.escape(instance) + # OpenDB and synthesis may escape array brackets differently. Handle + # every index (group and bank), retaining the exact non-index hierarchy. + instance_pattern = ".*".join(re.escape(part) for part in re.split(r"[\[\]]", instance)) hooks.extend( [ f"{instance_pattern} VDD VSS VDDARRAY! VSS!", diff --git a/physical/smoke/sta/opensta/opensta.tcl b/physical/smoke/sta/opensta/opensta.tcl index 966a9e50..a8e84504 100644 --- a/physical/smoke/sta/opensta/opensta.tcl +++ b/physical/smoke/sta/opensta/opensta.tcl @@ -20,6 +20,9 @@ if {[info exists ::env(OPENSTA_TOP)] && $::env(OPENSTA_TOP) ne ""} { } link_design $top_design read_sdc $::env(OPENSTA_SDC) +if {$top_design eq "retrosoc_tiny_asic"} { + source [file join [file dirname [info script]] tiny_r2_p4_checks.tcl] +} if {[info exists ::env(OPENSTA_TINY_ICS55)] && $::env(OPENSTA_TINY_ICS55) eq "YES"} { source [file join [file dirname [info script]] tiny_ics55_checks.tcl] } diff --git a/physical/smoke/sta/opensta/tiny_r2_p4_checks.tcl b/physical/smoke/sta/opensta/tiny_r2_p4_checks.tcl new file mode 100644 index 00000000..932f5ae2 --- /dev/null +++ b/physical/smoke/sta/opensta/tiny_r2_p4_checks.tcl @@ -0,0 +1,54 @@ +# P4 structural timing prerequisites, common to both supported Tiny PDKs. +# Negative timing remains observational; missing/incorrect clock objects do not. +set p4_dir [file dirname $::env(OPENSTA_REPORT)] +set p4_ram [get_cells -hierarchical -filter {ref_name == RM_IHPSG13_1P_1024x32_c2_bm_bist}] +set p4_clock_pin A_CLK +if {[llength $p4_ram] == 0} { + set p4_ram [get_cells -hierarchical -filter {ref_name == ics55_ecos_sram_1024x32_m8}] + set p4_clock_pin CLK +} +if {[llength $p4_ram] != 32} { error "P4 requires 32 main SRAM macros" } +set p4_expected {} +for {set group 0} {$group < 4} {incr group} { + for {set bank 0} {$bank < 8} {incr bank} { + lappend p4_expected [format {u_soc.u_sram.gen_group[%d].u_group.gen_bank[%d].u_ram.u_mem} $group $bank] + } +} +set p4_seen {} +foreach cell $p4_ram { + set name [string map {/ .} [get_full_name $cell]] + foreach expected $p4_expected { + if {[string equal [string range $name end-[expr {[string length $expected]-1}] end] $expected]} { + lappend p4_seen $expected + } + } +} +if {[lsort $p4_seen] ne [lsort $p4_expected]} { error "P4 macro group hierarchy mismatch" } +set p4_source [get_pins -quiet {u_clock_buffer/clk_o}] +if {[llength $p4_source] != 1} { error "P4 SYS source missing" } +set p4_direct {} +foreach pin [get_fanout -from $p4_source -flat -pin_levels 1 -trace_arcs all] { + dict set p4_direct [get_full_name $pin] 1 +} +set p4_clock_report [open $p4_dir/p4-clock-bindings.rpt w] +foreach cell $p4_ram { + set clocks {} + foreach pin [get_pins -of_objects $cell] { + if {[string match "*/$p4_clock_pin" [get_full_name $pin]]} { lappend clocks $pin } + } + if {[llength $clocks] != 1} { error "P4 macro clock pin missing" } + set name [get_full_name [lindex $clocks 0]] + if {![dict exists $p4_direct $name]} { error "P4 SRAM clock is not direct SYS: $name" } + puts $p4_clock_report $name +} +set p4_cpu [get_pins -hierarchical -quiet {*u_hazard3_cpu_2port*/CLK *u_hazard3_cpu_2port*/CK *u_hazard3_cpu_2port*/CKN}] +set p4_front [get_pins -hierarchical -quiet {*u_cpu_mem*/CLK *u_cpu_mem*/CK *u_cpu_mem*/CKN}] +if {[llength $p4_cpu] == 0 || [llength $p4_front] == 0} { error "P4 CPU/frontend clock endpoints missing" } +foreach pin [concat $p4_cpu $p4_front] { + set name [get_full_name $pin] + if {![dict exists $p4_direct $name]} { error "P4 CPU/frontend does not share SYS: $name" } +} +puts $p4_clock_report "CPU clock endpoints: [llength $p4_cpu]" +puts $p4_clock_report "Frontend clock endpoints: [llength $p4_front]" +close $p4_clock_report +puts "TINY_R2_P4_BINDING_PASS: four groups, 32 macros, common CPU/SRAM SYS; not timing qualification" diff --git a/publications/datasheets/tiny/chapter-review.md b/publications/datasheets/tiny/chapter-review.md index 65609ab7..3ebca872 100644 --- a/publications/datasheets/tiny/chapter-review.md +++ b/publications/datasheets/tiny/chapter-review.md @@ -11,7 +11,7 @@ All **39 frozen entries / 14 IP chapters** are reviewed below. Register, address | 3 | Configuration and Scope / 2 | Covered. IHP130, 128 KiB, 24 MHz target, four DMA channels; hardware RV32IMC versus default RV32IM firmware. | [ihp130-tiny.mk](../../../configs/ci/ihp130-tiny.mk), [retrosoc_tiny.sv](../../../rtl/tiny/top/retrosoc_tiny.sv) | Use the committed profile and identity; keep deferred interfaces and physical claims excluded. | | 4 | Reading and Evidence Conventions / 2 | Covered. Product-specific organization, navigation or revision control; no additional hardware capability. | [datasheet.typ](../../../publications/datasheets/tiny/datasheet.typ), [structure-contract.json](../../../publications/datasheets/tiny/structure-contract.json) | Retain the declared scope, complete source links and independent Tiny identity when updating this section. | | 5 | System Architecture / 17 | Covered. Product-specific organization, navigation or revision control; no additional hardware capability. | [datasheet.typ](../../../publications/datasheets/tiny/datasheet.typ), [structure-contract.json](../../../publications/datasheets/tiny/structure-contract.json) | Retain the declared scope, complete source links and independent Tiny identity when updating this section. | -| 6 | Processor and Integration / 17 | Covered. One RV32IMC MCU hart, A disabled, direct AHB-Lite/AXI32 adapter and debug reset boundary. | [retrosoc_tiny.sv](../../../rtl/tiny/top/retrosoc_tiny.sv), [mgmt_core_wrapper.sv](../../../rtl/ip/core/mgmt_core_wrapper.sv) | Retain actual wrapper parameters; do not infer firmware ISA flags from the hardware C extension. | +| 6 | Processor and Integration / 17 | Working-source P4 update: dual-port RV32IMC hart, A disabled, independent local I/D and tagged slow adapter. Page number belongs to the earlier delivered PDF. | [retrosoc_tiny.sv](../../../rtl/tiny/top/retrosoc_tiny.sv), [tiny_cpu_wrapper.sv](../../../rtl/tiny/top/tiny_cpu_wrapper.sv) | Retain actual wrapper parameters; revalidate publication pages separately before delivery. | | 7 | Clock, Reset and Operating State / 18 | Covered. System and asynchronous JTAG domains; RTC/WDG share the system clock. | [clock_reset_domains.json](../../../rtl/tiny/integration/clock_reset_domains.json), [retrosoc_tiny.sv](../../../rtl/tiny/top/retrosoc_tiny.sv) | Obtain CDC/RDC and clock/power evidence separately; do not claim independent sleep timing. | | 8 | Bus Transactions, Arbitration and Errors / 19 | Covered. One globally active transaction, round-robin acceptance, saved ownership, bounded legal bursts and error/drain behavior. | [tiny_axi4_fabric.sv](../../../rtl/tiny/top/tiny_axi4_fabric.sv), [axi4_error_slave.sv](../../../rtl/ip/interconnect/axi4_error_slave.sv) | Keep local IP capability separate from end-to-end fabric admission; no unlimited liveness guarantee. | | 9 | Memory Map and Access Conventions / 21 | Covered. Twenty canonical address windows, including alias versus physical-capacity distinctions. | [memory_map.json](../../../rtl/tiny/address_map/memory_map.json) | Reconcile every source-map change; do not use unmapped holes as scratch memory. | diff --git a/publications/datasheets/tiny/content-review.md b/publications/datasheets/tiny/content-review.md index 14add1c8..1d1c4840 100644 --- a/publications/datasheets/tiny/content-review.md +++ b/publications/datasheets/tiny/content-review.md @@ -1,11 +1,17 @@ # Tiny Gen1 v0.1 DRAFT content review -The publication describes the independent IHP130 Tiny profile: one RV32IMC -Hazard3 with A disabled, RV32IM default firmware, 128 KiB SRAM, 24 MHz target, +The publication describes the independent IHP130 Tiny profile: one dual-port +RV32IMC Hazard3 with A disabled, RV32IM default firmware, 128 KiB SRAM in four +independently serviced 32 KiB groups, 24 MHz target, four DMA channels and AXI32/APB4. Rill (清溪) is the selected prototype brand. The font/palette/grid are inherited as design rules; product facts are not inherited from Mini's descriptions or qualification records. +The P4 working-source update changes CPU/local-SRAM bindings and the affected +draft architecture prose/figure. It does not advance `tiny.json.source_revision`, +rebuild or relabel a delivered PDF, or add hardware qualification. A later +publication release still requires source review, snapshot refresh and PDF checks. + | Area | Review and publication decision | Follow-up boundary | | --- | --- | --- | | CPU and startup | Publish the actual C/M extensions, disabled A, CSR-enabled firmware and Tiny guard around PSRAM setup. | Keep compiler target separate from implemented ISA; a reset vector is not a completed boot. | diff --git a/publications/datasheets/tiny/datasheet.typ b/publications/datasheets/tiny/datasheet.typ index cd9d68c6..c3749082 100644 --- a/publications/datasheets/tiny/datasheet.typ +++ b/publications/datasheets/tiny/datasheet.typ @@ -29,9 +29,9 @@ - JTAG debug with coordinated hart reset. *Memory and interconnect* - - *128 KiB SRAM*, 32 banks of 4 KiB. + - *128 KiB SRAM*, four independently serviced 32 KiB groups backed by 32 banks of 4 KiB. - Native AXI32 memory paths and one APB4 island. - - CPU and DMA share one active transaction globally. + - CPU instruction/data ports are independent locally; the compatible external attachment remains serialized until P5. - XPI NOR boot and SRAM-resident application layout. *DMA and control* @@ -103,16 +103,18 @@ The document uses the approved Mini visual language; all product facts and sourc = System Architecture == Processor and Integration -Hazard3 is hart zero. The shared wrapper selects the C and M extensions and disables A for Tiny. Its AHB-Lite bus terminates -at a direct AXI32 adapter. JTAG supports the implemented halt/resume, register and system-bus debug path; debug hart reset -waits for the adapter to become idle instead of abandoning an accepted transaction. +Hazard3 is hart zero. The locked dual-port wrapper selects the C and M extensions and disables A for Tiny. Instruction fetch +and load/store use independent AHB ports through separate local SRAM frontends; non-SRAM accesses use the tagged slow path. +JTAG supports halt/resume, register access and halted-hart abstract-command memory access, without an independent system-bus master. +Debug hart reset first halts the hart and drains accepted local I/D and slow-path responses. SRAM and unrelated DMA work stay active. The system has no application hart, MMU/Linux platform, hardware cache-coherent multicore organization or product extension slots. CPU-visible SRAM, XPI and APB routes are the implemented interconnect. The single-hart architecture does not remove the need for software/DMA buffer ownership and fences. #architecture() #source-note("rtl/tiny/top/retrosoc_tiny.sv",title:"Tiny product integration and parameters") -#source-note("rtl/ip/core/mgmt_core_wrapper.sv",title:"CPU ISA, AHB adapter and debug wrapper") +#source-note("rtl/tiny/top/tiny_cpu_wrapper.sv",title:"Dual-port CPU ISA, AHB ports and debug wrapper") +#source-note("rtl/tiny/top/tiny_sram.sv",title:"Four local SRAM groups and external attachment") == Clock, Reset and Operating State CPU, AXI, APB, RTC and watchdog share the configured 24 MHz system clock. CLINT derives a 1 MHz tick. JTAG has its own @@ -130,7 +132,8 @@ advertised as a qualified voltage/frequency operating point. #source-note("rtl/tiny/integration/clock_reset_domains.json",title:"Clock/reset inventory and debug crossings") == Bus Transactions, Arbitration and Errors -The two initiators are CPU and DMA. They share *one active read or write transaction globally*. The fabric saves source, +The two external initiators are CPU slow path and DMA. They share *one active read or write transaction globally* in this +pre-P5 attachment. CPU instruction/data local requests are independently arbitrated within each SRAM group. The fabric saves source, direction, address and attributes, forwards the request, and retains ownership until the terminal B or RLAST handshake. Read/write request arbitration is round-robin; it is not a throughput guarantee or an unrestricted liveness guarantee. diff --git a/publications/datasheets/tiny/diagrams.typ b/publications/datasheets/tiny/diagrams.typ index 4365a9ab..15b0cdf4 100644 --- a/publications/datasheets/tiny/diagrams.typ +++ b/publications/datasheets/tiny/diagrams.typ @@ -26,21 +26,22 @@ }) } #let architecture() = diagram("tiny-architecture",schematic(( - (5,4,65,19,[Hazard3 · hart 0 \ RV32IMC · A disabled]), + (5,4,65,19,[Hazard3 dual-port · hart 0 \ RV32IMC · A disabled]), (104,4,63,19,[JTAG / debug transport \ External TCK domain]), - (5,37,65,15,[AHB-Lite → AXI32 adapter]), + (5,37,65,15,[I/D decode + slow adapter]), (104,37,63,15,[Four-channel DMA \ AXI32 master]), - (30,69,112,19,[Tiny AXI32 fabric \ One active transaction globally]), - (0,110,40,22,[128 KiB SRAM \ 32 × 4 KiB]), + (30,69,112,19,[Tiny AXI32 fabric \ P5 external concurrency pending]), + (0,110,40,22,[128 KiB SRAM \ 4 × 32 KiB groups]), (44,110,40,22,[XPI read path \ Flash alias / NOR]), (88,110,40,22,[APB4 bridge \ 16 targets]), (132,110,40,22,[Error responders \ DECERR / SLVERR]), ),( ((104,13),(70,13)),((37,23),(37,37)), + ((5,44),(0,44),(0,103),(6,103),(6,110)), ((37,52),(37,61),(60,61),(60,69)),((135,52),(135,61),(112,61),(112,69)), ((45,88),(45,99),(20,99),(20,110)),((70,88),(70,103),(64,103),(64,110)), ((103,88),(103,103),(108,103),(108,110)),((127,88),(127,99),(152,99),(152,110)), -),height:134),[Tiny Gen1 integration. System logic runs from the configured 24 MHz clock; JTAG is asynchronous.]) +),height:134),[Tiny Gen1 integration. The left branch represents independent local I/D paths bypassing the external fabric. System logic uses the configured 24 MHz clock; JTAG is asynchronous.]) #let ip-diagram(entry) = { let children=entry.nodes.slice(1) diff --git a/publications/datasheets/tiny/source-contract.json b/publications/datasheets/tiny/source-contract.json index d2346185..a83343d9 100644 --- a/publications/datasheets/tiny/source-contract.json +++ b/publications/datasheets/tiny/source-contract.json @@ -98,7 +98,13 @@ "uart1": 26 }, "sources": [ - "rtl/ip/core/mgmt_core_wrapper.sv", + "rtl/tiny/top/tiny_cpu_wrapper.sv", + "rtl/tiny/top/tiny_cpu_mem.sv", + "rtl/tiny/top/tiny_sram.sv", + "rtl/tiny/top/tiny_sram_group.sv", + "rtl/tiny/top/tiny_sram_axi4.sv", + "rtl/tiny/top/tiny_sram_axi4_demux.sv", + "rtl/tiny/top/tiny_sram_port_if.sv", "rtl/ip/core/mgmt_debug_wrapper.sv", "rtl/ip/core/mgmt_debug_reset.sv", "rtl/ip/interconnect/ahbl2axi4.sv", @@ -122,7 +128,7 @@ "(s_last_addr[31:12] == s_selected_addr.addr[31:12])", "s_accept = (s_state_q == Idle) && s_selected_valid;", "Response: if (s_terminal) s_state_d = Idle;", - "s_tgt_d = s_protocol_legal ? s_decoded_target : 3'd4;", + "s_tgt_d = !s_protocol_legal ? 3'd4 : s_fetch_denied ? ((s_decoded_target == 3'd2) ? 3'd4 : 3'd3) : s_decoded_target;", "assign s_protocol_legal = !s_selected_addr.lock && (s_selected_addr.size <= 3'd2) &&\n ((s_selected_addr.addr & ((32'd1 << s_selected_addr.size) - 32'd1)) == 32'd0) &&\n ((s_selected_addr.burst == 2'b01) ||\n ((s_selected_addr.burst == 2'b00) && (s_selected_addr.len == 8'd0))) &&\n (s_selected_addr.len <= 8'd15) && !s_last_addr[32] &&\n (s_last_addr[31:12] == s_selected_addr.addr[31:12]) &&\n (decode_target(\n s_last_addr[31:0]\n ) == s_decoded_target) && ((s_decoded_target != 3'd2) || (s_selected_addr.len == 8'd0));", "assign s_terminal = (s_state_q == Response) &&\n (s_write_q ? (s_target_bvalid[s_tgt_q] && s_bready[s_owner_q]) :\n (s_target_rvalid[s_tgt_q] && s_rready[s_owner_q] && s_target_rlast[s_tgt_q]));" ] diff --git a/publications/tiny_reference.py b/publications/tiny_reference.py index 42c4a022..2b4c5732 100644 --- a/publications/tiny_reference.py +++ b/publications/tiny_reference.py @@ -26,6 +26,9 @@ TOP = "rtl/tiny/top/retrosoc_tiny.sv" RESET_TREE = "rtl/tiny/top/tiny_reset_tree.sv" SYSCTRL = "rtl/tiny/top/tiny_sysctrl.sv" ARCHINFO = "rtl/tiny/top/tiny_archinfo.sv" +CPU = "rtl/tiny/top/tiny_cpu_wrapper.sv" +CPU_MEM = "rtl/tiny/top/tiny_cpu_mem.sv" +SRAM = "rtl/tiny/top/tiny_sram.sv" FAMILIES = {"xpi", "sram", "gpio", "dma", "timer", "pwm", "rtc", "wdg", "uart", "i2c", "clint", "archinfo"} MANAGED = {"publication_media", "hazard3", "cluster_common", "cluster_archinfo", "cluster_pwm", "cluster_rtc", "cluster_wdg"} SUPPORTED_SYSCTRL = { @@ -65,21 +68,27 @@ def facts(root: Path = ROOT) -> dict: if any(profile.get(key) != value for key, value in required.items()): raise ValueError("Tiny publication profile changed; review configuration and ISA boundary") top = (root / TOP).read_text(encoding="utf-8") - ram = instance_parameters(top, "onchip_ram", "u_sram") + ram_reg = instance_parameters((root / SRAM).read_text(), "onchip_ram_reg", "u_reg") + ram = {"CapacityKiB": ram_reg.get("CapacityKiB"), + "DataWidth": 8 * ram_reg.get("DataBytes", 0), "IdWidth": 1} dma = instance_parameters(top, "apb4_dma", "u_dma") - cpu = instance_parameters(top, "mgmt_core_wrapper", "u_cpu") - if ram != {"CapacityKiB": 128, "DataWidth": 32, "IdWidth": 1, "Ics55SmallBanks": 1}: + cpu = instance_parameters(top, "tiny_cpu_wrapper", "u_cpu") + if ram != {"CapacityKiB": 128, "DataWidth": 32, "IdWidth": 1} or ram_reg.get("Present") != 1: raise ValueError("Tiny SRAM parameters changed") - # The publication remains the reviewed IHP130 compatibility profile; - # selecting ICS55's 4 KiB geometry does not alter its reported SRAM facts. - ram = {key: value for key, value in ram.items() if key != "Ics55SmallBanks"} + # P4 changes local routing, not physical geometry or the book's IHP130 + # compatibility selection. Snapshot enforcement still precedes publication. + read_topology(root / TOPOLOGY, root / MAP) + require_snippets(root, TOP, ["tiny_sram u_sram", "tiny_sram_axi4_demux u_sram_attachment", + ".ID_WIDTH(1)", ".DATA_WIDTH(32)"]) + require_snippets(root, SRAM, ["group < 4", "tiny_sram_group u_group"]) if dma != {"NumChannels": 4, "MaxBurstBeats": 16, "FifoDepth": 32, "RequestMask": 0x7F9, "EnableStreams": 0}: raise ValueError("Tiny DMA parameters changed") - if cpu != {"ExternalIrqCount": 30, "EnableAtomics": 0, "TwoCycleBusErrors": 1, "ResetSyncStages": 5}: + if cpu != {"ExternalIrqCount": 30, "EnableAtomics": 0, "ResetSyncStages": 5}: raise ValueError("Tiny CPU integration changed") require_snippets(root, RESET_TREE, [".STAGE(5)", "assign cpu_ready_rst_n_o = &leaf_rst_n_o;"]) - require_snippets(root, "rtl/ip/core/mgmt_core_wrapper.sv", [".EXTENSION_C(1)", ".EXTENSION_A(EnableAtomics)"]) - require_snippets(root, TOP, ["u_masters_axi4_if[2]", ".axi4(u_masters_axi4_if[0])", + require_snippets(root, CPU, ["hazard3_cpu_2port", ".EXTENSION_C(1)", ".EXTENSION_A(EnableAtomics)"]) + require_snippets(root, CPU_MEM, [".TwoCycleErrors(1'b1)", "ErrorFirst", "ErrorLast"]) + require_snippets(root, TOP, ["u_masters_axi4_if[2]", ".slow_axi4(u_masters_axi4_if[0])", ".axi4(u_masters_axi4_if[1])", ".cfg_apb4(u_sram_apb4_if)", "assign s_tick = s_tick_count_q == 5'd23;"]) require_snippets(root, SYSCTRL, ["(apb4.paddr[1:0] != 2'd0)", "(apb4.pstrb != 4'hf)", diff --git a/rtl/ip/core/mgmt_core_wrapper.sv b/rtl/ip/core/mgmt_core_wrapper.sv index 6afa424c..51bacb5b 100644 --- a/rtl/ip/core/mgmt_core_wrapper.sv +++ b/rtl/ip/core/mgmt_core_wrapper.sv @@ -72,36 +72,37 @@ module mgmt_core_wrapper #( .JtagIdcode (`SOC_JTAG_IDCODE), .ResetSyncStages(ResetSyncStages) ) u_mgmt_debug_wrapper ( - .clk_i (clk_i), - .rst_n_i (rst_n_i), - .bridge_idle_i (s_ahbl_idle), - .jtag_tck_i (jtag_tck_i), - .jtag_tms_i (jtag_tms_i), - .jtag_tdi_i (jtag_tdi_i), - .jtag_trst_n_i (jtag_trst_n_i), - .jtag_tdo_o (jtag_tdo_o), - .core_rst_n_o (s_core_rst_n), - .dbg_req_halt_o (s_dbg_req_halt), - .dbg_req_halt_on_reset_o (s_dbg_req_halt_on_reset), - .dbg_req_resume_o (s_dbg_req_resume), - .dbg_halted_i (s_dbg_halted), - .dbg_running_i (s_dbg_running), - .dbg_data0_rdata_o (s_dbg_data0_rdata), - .dbg_data0_wdata_i (s_dbg_data0_wdata), - .dbg_data0_wen_i (s_dbg_data0_wen), - .dbg_instr_data_o (s_dbg_instr_data), - .dbg_instr_data_vld_o (s_dbg_instr_data_vld), - .dbg_instr_data_rdy_i (s_dbg_instr_data_rdy), + .clk_i(clk_i), + .rst_n_i(rst_n_i), + .bridge_idle_i(s_ahbl_idle), + .jtag_tck_i(jtag_tck_i), + .jtag_tms_i(jtag_tms_i), + .jtag_tdi_i(jtag_tdi_i), + .jtag_trst_n_i(jtag_trst_n_i), + .jtag_tdo_o(jtag_tdo_o), + .core_rst_n_o(s_core_rst_n), + .reset_pending_o(), // Mini retains its existing reset policy. + .dbg_req_halt_o(s_dbg_req_halt), + .dbg_req_halt_on_reset_o(s_dbg_req_halt_on_reset), + .dbg_req_resume_o(s_dbg_req_resume), + .dbg_halted_i(s_dbg_halted), + .dbg_running_i(s_dbg_running), + .dbg_data0_rdata_o(s_dbg_data0_rdata), + .dbg_data0_wdata_i(s_dbg_data0_wdata), + .dbg_data0_wen_i(s_dbg_data0_wen), + .dbg_instr_data_o(s_dbg_instr_data), + .dbg_instr_data_vld_o(s_dbg_instr_data_vld), + .dbg_instr_data_rdy_i(s_dbg_instr_data_rdy), .dbg_instr_caught_exception_i(s_dbg_instr_caught_exception), - .dbg_instr_caught_ebreak_i (s_dbg_instr_caught_ebreak), - .dbg_sbus_addr_o (s_dbg_sbus_addr), - .dbg_sbus_write_o (s_dbg_sbus_write), - .dbg_sbus_size_o (s_dbg_sbus_size), - .dbg_sbus_vld_o (s_dbg_sbus_vld), - .dbg_sbus_rdy_i (s_dbg_sbus_rdy), - .dbg_sbus_err_i (s_dbg_sbus_err), - .dbg_sbus_wdata_o (s_dbg_sbus_wdata), - .dbg_sbus_rdata_i (s_dbg_sbus_rdata) + .dbg_instr_caught_ebreak_i(s_dbg_instr_caught_ebreak), + .dbg_sbus_addr_o(s_dbg_sbus_addr), + .dbg_sbus_write_o(s_dbg_sbus_write), + .dbg_sbus_size_o(s_dbg_sbus_size), + .dbg_sbus_vld_o(s_dbg_sbus_vld), + .dbg_sbus_rdy_i(s_dbg_sbus_rdy), + .dbg_sbus_err_i(s_dbg_sbus_err), + .dbg_sbus_wdata_o(s_dbg_sbus_wdata), + .dbg_sbus_rdata_i(s_dbg_sbus_rdata) ); hazard3_cpu_1port #( diff --git a/rtl/ip/core/mgmt_debug_reset.sv b/rtl/ip/core/mgmt_debug_reset.sv index 9505eabc..526d568f 100644 --- a/rtl/ip/core/mgmt_debug_reset.sv +++ b/rtl/ip/core/mgmt_debug_reset.sv @@ -19,12 +19,15 @@ module mgmt_debug_reset #( input logic reset_req_i, input logic bridge_idle_i, output logic core_rst_n_o, + output logic reset_pending_o, output logic reset_done_o ); logic s_pending_d, s_pending_q; logic s_reset_active_d, s_reset_active_q; + assign reset_pending_o = reset_req_i || s_pending_q || s_reset_active_q; + always_comb begin s_pending_d = s_pending_q; s_reset_active_d = s_reset_active_q; diff --git a/rtl/ip/core/mgmt_debug_wrapper.sv b/rtl/ip/core/mgmt_debug_wrapper.sv index 5d6bad1c..91ba858f 100644 --- a/rtl/ip/core/mgmt_debug_wrapper.sv +++ b/rtl/ip/core/mgmt_debug_wrapper.sv @@ -23,6 +23,7 @@ module mgmt_debug_wrapper #( input logic jtag_trst_n_i, output logic jtag_tdo_o, output logic core_rst_n_o, + output logic reset_pending_o, output logic dbg_req_halt_o, output logic dbg_req_halt_on_reset_o, @@ -154,12 +155,13 @@ module mgmt_debug_wrapper #( mgmt_debug_reset #( .ResetSyncStages(ResetSyncStages) ) u_mgmt_debug_reset ( - .clk_i (clk_i), - .rst_n_i (rst_n_i), - .reset_req_i (s_sys_reset_req || s_hart_reset_req), - .bridge_idle_i(bridge_idle_i), - .core_rst_n_o (core_rst_n_o), - .reset_done_o (s_reset_done) + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .reset_req_i (s_sys_reset_req || s_hart_reset_req), + .bridge_idle_i (bridge_idle_i), + .core_rst_n_o (core_rst_n_o), + .reset_pending_o(reset_pending_o), + .reset_done_o (s_reset_done) ); endmodule diff --git a/rtl/rtl_readiness.json b/rtl/rtl_readiness.json index 616992fc..2edf6e96 100644 --- a/rtl/rtl_readiness.json +++ b/rtl/rtl_readiness.json @@ -1,6 +1,23 @@ { "schema_version": 1, "targets": [ + { + "name": "tiny-r2-p4", + "paths": ["rtl/tiny/top"], + "status": "prototype", + "baseline_revision": "4db27524a6282ca40d76b935415b89f043d6ea83", + "configuration_digest": null, + "configuration_profiles": ["configs/ci/ics55-tiny.mk", "configs/ci/ihp130-tiny.mk"], + "required_evidence": [], + "synthesis_intent": { + "registers": "Common registers; independent I/D request state and per-group responses", + "reset": "five-edge release; hart halt/drain leaves SRAM and unrelated DMA active", + "clock_enable": "CPU and all 32 main-SRAM macros use direct SAFE24 SYS", + "memory": "four groups of eight tc_sram_1024x32 macros; no payload reset or extra SRAM", + "pipeline": "AHB capture, macro issue, registered response; uncontended completion within three cycles" + }, + "waivers": [] + }, { "name": "owned-rtl", "paths": [ diff --git a/rtl/rtl_style_audit.json b/rtl/rtl_style_audit.json index ad5f310d..b19702c4 100644 --- a/rtl/rtl_style_audit.json +++ b/rtl/rtl_style_audit.json @@ -437,7 +437,14 @@ "tiny_archinfo.sv", "tiny_axi42apb4.sv", "tiny_axi4_fabric.sv", + "tiny_cpu_mem.sv", + "tiny_cpu_wrapper.sv", "tiny_reset_tree.sv", + "tiny_sram.sv", + "tiny_sram_axi4.sv", + "tiny_sram_axi4_demux.sv", + "tiny_sram_group.sv", + "tiny_sram_port_if.sv", "tiny_sysctrl.sv" ] }, diff --git a/rtl/tiny/Makefile b/rtl/tiny/Makefile index d327bb2f..ab9c7035 100644 --- a/rtl/tiny/Makefile +++ b/rtl/tiny/Makefile @@ -200,4 +200,19 @@ tiny-r2-p3-matrix: | manifest tiny-r2-p3-report: $(PYTHON) $(TINY_P3_TOOL) --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) report -.PHONY: tiny-r2-p3-image tiny-r2-p3-sim tiny-r2-p3-matrix tiny-r2-p3-report \ No newline at end of file +.PHONY: tiny-r2-p3-image tiny-r2-p3-sim tiny-r2-p3-matrix tiny-r2-p3-report + +# P4 preserves old image identity and replays each selected image exactly once. +TINY_P4_REFERENCE ?= +TINY_P4_CASES ?= perf-o2-plain-flat,perf-o2-plain-banked +tiny-r2-p4-replay: | manifest + $(PYTHON) $(ROOT_PATH)/scripts/tiny_r2_p4.py --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) \ + --reference $(TINY_P4_REFERENCE) --cases $(TINY_P4_CASES) --simulator $(SIM_TOOL_NAME) \ + --vvp $(VVP) --timeout $(SOC_SIM_TIME) -- \ + make --no-print-directory -j1 -B CONFIG=$(CONFIG_PATH) BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) \ + $(foreach var,$(CONFIG_KEY_VARS),$(var)=$($(var))) SIMU=$(SIMU) SYNTH=NONE STA=NONE comp + +tiny-r2-p4-report: + $(PYTHON) $(ROOT_PATH)/scripts/tiny_r2_p4.py --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) --report + +.PHONY: tiny-r2-p4-replay tiny-r2-p4-report diff --git a/rtl/tiny/README.md b/rtl/tiny/README.md index e172e693..a31224b8 100644 --- a/rtl/tiny/README.md +++ b/rtl/tiny/README.md @@ -9,6 +9,12 @@ explicit compatibility through `configs/ci/ihp130-tiny.mk`. See the [platform runbook](../../docs/ip/tiny-ics55-platform.md) for source-bound validation; backend frequency tests are not SYS switching or timing qualification. +The [R2-P4 implementation](../../docs/ip/tiny-soc-r2-local-memory.md) connects the +official dual-port CPU to four independently serviced 32 KiB local SRAM groups. +The 32 physical macros and register ABI are unchanged. Only non-SRAM CPU work +uses the tagged slow path; the external fabric stays serialized until P5. +Source presence alone is not completed phase or physical qualification. + `top` owns product integration, `address_map`, `pin_map` and `integration` own canonical address, pad, IRQ and clock/reset inputs, `filelist` selects sources, `dv` owns product verification, and `mk` owns product configuration. Reusable @@ -43,8 +49,8 @@ final timing closure is mandatory. Software/DMA scheduling is `TINY-R2-P3`, loca `TINY-R2-P9`, system qualification `TINY-R2-P10` and complete-product physical qualification `TINY-R2-P11` (historical IHP130 title; now default ICS55). Legacy `TINY-P0` through `TINY-P12` remain in -the contract history. The committed RTL/profile still implements the initial -24 MHz/no-PLL/four-channel design; no faster CPU/SRAM or PLL timing support +the contract history. Executable profiles retain SAFE24 and four DMA channels; +ICS55's present PLL stays off. No faster CPU/SRAM or PLL timing support follows from this freeze or the `HAVE_PLL` selector alone. The R2-P2 candidate keeps that executable clock/memory configuration and adds diff --git a/rtl/tiny/dv/retrosoc_tiny_tb.sv b/rtl/tiny/dv/retrosoc_tiny_tb.sv index 8adbfac3..c90ab682 100644 --- a/rtl/tiny/dv/retrosoc_tiny_tb.sv +++ b/rtl/tiny/dv/retrosoc_tiny_tb.sv @@ -230,57 +230,117 @@ module retrosoc_tiny_tb; `undef _BASELINE_AXI end - // The baseline has one AHB port. HPROT[0] distinguishes instruction/data; - // these observations do not assert future independent local-I/D latency. - longint unsigned s_ahb_cycle = 0; - longint unsigned s_ahb_start = 0; - longint unsigned s_ahb_accept[2], s_ahb_done[2], s_ahb_latency[2]; - logic s_ahb_pending = 1'b0, s_ahb_measured = 1'b0; - logic s_ahb_data = 1'b0, s_ahb_previous_enable = 1'b0; - integer s_ahb_window = 0; - wire s_ahb_enable = u_dut.u_soc.s_perf_enable && s_baseline_enabled; - `define _BASELINE_AHB u_dut.u_soc.u_cpu.u_ahbl_if - always @(posedge s_clk) begin - s_ahb_cycle = s_ahb_cycle + 1; - if (!u_dut.u_soc.s_rst_n) begin - s_ahb_pending = 1'b0; - s_ahb_measured = 1'b0; - s_ahb_previous_enable = 1'b0; - s_ahb_window = 0; - end else if (s_baseline_enabled) begin - if (s_ahb_enable && !s_ahb_previous_enable) begin - s_ahb_window = s_ahb_window + 1; - s_ahb_measured = 1'b0; - for (int kind = 0; kind < 2; kind++) begin - s_ahb_accept[kind] = 0; - s_ahb_done[kind] = 0; - s_ahb_latency[kind] = 0; + // Verification-only per-group issue/conflict accounting. A bank conflict is + // two or more eligible macro clients in one cycle, not an AXI address stall. + for (genvar group = 0; group < 4; group++) begin : gen_bank_observer + longint unsigned s_issues[3], s_waits[3]; + longint unsigned s_conflicts = 0; + logic s_previous_enable = 0; + integer s_window = 0; + wire s_enable = u_dut.u_soc.s_perf_enable && s_baseline_enabled; + wire [2:0] s_req = u_dut.u_soc.u_sram.gen_group[group].u_group.s_req; + wire [2:0] s_grant = u_dut.u_soc.u_sram.gen_group[group].u_group.s_grant; + always @(posedge s_clk) begin + if (!u_dut.u_soc.s_rst_n) begin + s_previous_enable = 0; + s_window = 0; + end else if (s_baseline_enabled) begin + if (s_enable && !s_previous_enable) begin + s_window = s_window + 1; + s_conflicts = 0; + for (int client = 0; client < 3; client++) begin + s_issues[client] = 0; + s_waits[client] = 0; + end end + if (s_enable) begin + if ((s_req & (s_req - 3'd1)) != 3'd0) s_conflicts = s_conflicts + 1; + for (int client = 0; client < 3; client++) begin + if (s_grant[client]) s_issues[client] = s_issues[client] + 1; + if (s_req[client] && !s_grant[client]) s_waits[client] = s_waits[client] + 1; + end + end + if (!s_enable && s_previous_enable) + $display( + "R2_P4_BANK window=%0d group=%0d issues_i=%0d issues_d=%0d issues_external=%0d conflicts=%0d wait_i=%0d wait_d=%0d wait_external=%0d", + s_window, + group, + s_issues[0], + s_issues[1], + s_issues[2], + s_conflicts, + s_waits[0], + s_waits[1], + s_waits[2] + ); + s_previous_enable = s_enable; end - if (`_BASELINE_AHB.hready) begin - if (s_ahb_pending && s_ahb_measured && s_ahb_enable) begin - s_ahb_done[s_ahb_data] = s_ahb_done[s_ahb_data] + 1; - if ((s_ahb_cycle - s_ahb_start) > s_ahb_latency[s_ahb_data]) - s_ahb_latency[s_ahb_data] = s_ahb_cycle - s_ahb_start; - end - s_ahb_pending = `_BASELINE_AHB.htrans[1]; - s_ahb_measured = s_ahb_pending && s_ahb_enable; - if (s_ahb_measured) begin - s_ahb_data = `_BASELINE_AHB.hprot[0]; - s_ahb_start = s_ahb_cycle; - s_ahb_accept[s_ahb_data] = s_ahb_accept[s_ahb_data] + 1; - end - end - if (!s_ahb_enable && s_ahb_previous_enable) begin - for (int kind = 0; kind < 2; kind++) begin - $display("R2_AHB window=%0d kind=%0d accepted=%0d completed=%0d latency_max=%0d", - s_ahb_window, kind, s_ahb_accept[kind], s_ahb_done[kind], s_ahb_latency[kind]); - end - end - s_ahb_previous_enable = s_ahb_enable; end end - `undef _BASELINE_AHB + + // P4 retains the original per-kind record format, now observed on two + // independent ports. External AXI counts remain external traffic only. + for (genvar port_index = 0; port_index < 2; port_index++) begin : gen_ahb_observer + longint unsigned s_cycle = 0, s_start = 0; + longint unsigned s_accepted = 0, s_completed = 0, s_latency = 0; + longint unsigned s_local_accepted = 0, s_local_completed = 0, s_local_latency = 0; + longint unsigned s_admission_wait = 0; + logic s_pending = 0, s_measured = 0, s_local = 0, s_previous_enable = 0; + integer s_window = 0; + wire s_enable = u_dut.u_soc.s_perf_enable && s_baseline_enabled; + `define _P4_AHB u_dut.u_soc.u_cpu_ahbl_if[port_index] + always @(posedge s_clk) begin + s_cycle = s_cycle + 1; + if (!u_dut.u_soc.s_core_rst_n) begin + s_pending = 0; + s_measured = 0; + s_previous_enable = 0; + s_window = 0; + end else if (s_baseline_enabled) begin + if (s_enable && !s_previous_enable) begin + s_window = s_window + 1; + s_measured = 0; + s_accepted = 0; + s_completed = 0; + s_latency = 0; + s_local_accepted = 0; + s_local_completed = 0; + s_local_latency = 0; + s_admission_wait = 0; + end + if (s_enable && `_P4_AHB.htrans[1] && !`_P4_AHB.hready) + s_admission_wait = s_admission_wait + 1; + if (`_P4_AHB.hready) begin + if (s_pending && s_measured && s_enable) begin + s_completed = s_completed + 1; + if ((s_cycle - s_start) > s_latency) s_latency = s_cycle - s_start; + if (s_local) begin + s_local_completed = s_local_completed + 1; + if ((s_cycle - s_start) > s_local_latency) s_local_latency = s_cycle - s_start; + end + end + s_pending = `_P4_AHB.htrans[1]; + s_measured = s_pending && s_enable; + if (s_measured) begin + s_start = s_cycle; + s_accepted = s_accepted + 1; + s_local = (`_P4_AHB.haddr >= 32'h30000000) && (`_P4_AHB.haddr < 32'h30020000); + if (s_local) s_local_accepted = s_local_accepted + 1; + end + end + if (!s_enable && s_previous_enable) begin + $display("R2_AHB window=%0d kind=%0d accepted=%0d completed=%0d latency_max=%0d", + s_window, port_index, s_accepted, s_completed, s_latency); + $display( + "R2_P4_LOCAL window=%0d port=%0d accepted=%0d completed=%0d latency_max=%0d admission_wait=%0d", + s_window, port_index, s_local_accepted, s_local_completed, s_local_latency, + s_admission_wait); + end + s_previous_enable = s_enable; + end + end + `undef _P4_AHB + end `endif retrosoc_tiny_asic u_dut ( diff --git a/rtl/tiny/filelist/top.fl b/rtl/tiny/filelist/top.fl index 08b0c7ab..dd87d59b 100644 --- a/rtl/tiny/filelist/top.fl +++ b/rtl/tiny/filelist/top.fl @@ -3,5 +3,12 @@ /top/tiny_sysctrl.sv /top/tiny_archinfo.sv /top/tiny_reset_tree.sv +/top/tiny_sram_port_if.sv +/top/tiny_sram_group.sv +/top/tiny_sram_axi4.sv +/top/tiny_sram_axi4_demux.sv +/top/tiny_sram.sv +/top/tiny_cpu_mem.sv +/top/tiny_cpu_wrapper.sv /top/retrosoc_tiny.sv /top/retrosoc_tiny_asic.sv diff --git a/rtl/tiny/integration/clock_reset_domains.json b/rtl/tiny/integration/clock_reset_domains.json index e9bb824d..1c692f05 100644 --- a/rtl/tiny/integration/clock_reset_domains.json +++ b/rtl/tiny/integration/clock_reset_domains.json @@ -1,5 +1,17 @@ { "schema_version": 2, + "local_memory": { + "phase": "TINY-R2-P4", + "clock_domain": "system", + "cpu_instance": "u_soc.u_cpu.u_hazard3_cpu_2port", + "frontend_instance": "u_soc.u_cpu_mem", + "macro_instance": "u_soc.u_sram.gen_group[{group}].u_group.gen_bank[{bank}].u_ram.u_mem", + "groups": 4, + "macros_per_group": 8, + "hart_reset": "halt then drain accepted I/D and slow responses; SRAM/DMA stay active", + "main_sram_payload_reset": "none", + "local_uncontended_max_cycles": 3 + }, "inactive_macros": { "ics55_pll": { "cell": "PLL_TOP", diff --git a/rtl/tiny/integration/soc_topology.json b/rtl/tiny/integration/soc_topology.json index 004795d2..9f9795bd 100644 --- a/rtl/tiny/integration/soc_topology.json +++ b/rtl/tiny/integration/soc_topology.json @@ -1,5 +1,15 @@ { "schema_version": 1, + "cpu": {"module": "hazard3_cpu_2port", "local_ports": 2, "data_outstanding": 1}, + "main_sram": { + "groups": 4, + "group_bytes": 32768, + "macros_per_group": 8, + "macro_bytes": 4096, + "base": "0x30000000", + "macro_instance": "u_soc.u_sram.gen_group[{group}].u_group.gen_bank[{bank}].u_ram.u_mem", + "external_attachment": "serialized-pre-P5" + }, "gpio_pins": 32, "irq_vector_width": 32, "external_irq_count": 30, diff --git a/rtl/tiny/top/retrosoc_tiny.sv b/rtl/tiny/top/retrosoc_tiny.sv index 7278291b..9227531a 100644 --- a/rtl/tiny/top/retrosoc_tiny.sv +++ b/rtl/tiny/top/retrosoc_tiny.sv @@ -23,9 +23,10 @@ module retrosoc_tiny ( logic s_por_rst_n, s_rst_n; logic [16:0] s_leaf_rst_n; logic s_fabric_rst_n, s_cpu_ready_rst_n; - logic s_master_rst_n [2]; - logic s_target_rst_n [5]; - logic s_debug_halted; + logic s_master_rst_n [2]; + logic s_target_rst_n [5]; + logic s_debug_halted; + logic s_core_rst_n, s_cpu_bus_idle, s_cpu_quiesce; logic [31:0] s_irq; logic s_fault_valid, s_fault_master, s_fault_write; logic [31:0] s_fault_addr; @@ -38,6 +39,20 @@ module retrosoc_tiny ( logic [4:0] s_tick_count_d, s_tick_count_q; logic s_tick; logic s_unused_uart1_tx_stall, s_unused_uart1_rx_stall; + ahbl_if u_cpu_ahbl_if[2] ( + .hclk (clk_i), + .hresetn(s_core_rst_n) + ); + tiny_sram_port_if u_cpu_local_if[2] (); + axi4_if #( + .ADDR_WIDTH(32), + .DATA_WIDTH(32), + .ID_WIDTH (1), + .USER_WIDTH(1) + ) u_sram_axi4_if[4] ( + .aclk (clk_i), + .aresetn(s_sram_rst_n) + ); `include "tiny_apb_interfaces.svh" uart_if u_uart0_if (); uart_if u_uart1_if (); @@ -135,11 +150,10 @@ uart_if u_uart0_if (); .dat_i (s_tick_count_d), .dat_o (s_tick_count_q) ); - mgmt_core_wrapper #( - .ExternalIrqCount (30), - .EnableAtomics (1'b0), - .TwoCycleBusErrors(1'b1), - .ResetSyncStages (5) + tiny_cpu_wrapper #( + .ExternalIrqCount(30), + .EnableAtomics (1'b0), + .ResetSyncStages (5) ) u_cpu ( .clk_i (clk_i), .rst_n_i (s_cpu_ready_rst_n), @@ -150,7 +164,19 @@ uart_if u_uart0_if (); .jtag_trst_n_i (jtag_trst_n_i), .jtag_tdo_o (jtag_tdo_o), .debug_halted_o(s_debug_halted), - .axi4 (u_masters_axi4_if[0]) + .bus_idle_i (s_cpu_bus_idle), + .core_rst_n_o (s_core_rst_n), + .quiesce_o (s_cpu_quiesce), + .cpu (u_cpu_ahbl_if) + ); + tiny_cpu_mem u_cpu_mem ( + .clk_i (clk_i), + .rst_n_i (s_core_rst_n), + .quiesce_i (s_cpu_quiesce), + .idle_o (s_cpu_bus_idle), + .cpu (u_cpu_ahbl_if), + .local_ports(u_cpu_local_if), + .slow_axi4 (u_masters_axi4_if[0]) ); tiny_axi4_fabric u_fabric ( .clk_i (clk_i), @@ -167,18 +193,20 @@ uart_if u_uart0_if (); .cpu_wait_o (s_cpu_wait), .dma_wait_o (s_dma_wait) ); - onchip_ram #( - .CapacityKiB (128), - .DataWidth (32), - .IdWidth (1), - .Ics55SmallBanks(1'b1) - ) u_sram ( - .clk_i (clk_i), - .rst_n_i (s_sram_rst_n), - .mem_axi4 (u_targets_axi4_if[0]), - .cfg_apb4 (u_sram_apb4_if), - .perf_enable_i(s_perf_enable), - .perf_clear_i (s_perf_clear) + tiny_sram_axi4_demux u_sram_attachment ( + .clk_i (clk_i), + .rst_n_i (s_sram_rst_n), + .upstream(u_targets_axi4_if[0]), + .groups (u_sram_axi4_if) + ); + tiny_sram u_sram ( + .clk_i (clk_i), + .rst_n_i (s_sram_rst_n), + .local_ports (u_cpu_local_if), + .external_ports(u_sram_axi4_if), + .cfg_apb4 (u_sram_apb4_if), + .perf_enable_i (s_perf_enable), + .perf_clear_i (s_perf_clear) ); apb4_xpi u_xpi ( .clk_i (clk_i), diff --git a/rtl/tiny/top/tiny_axi4_fabric.sv b/rtl/tiny/top/tiny_axi4_fabric.sv index a4473d48..7b84e24b 100644 --- a/rtl/tiny/top/tiny_axi4_fabric.sv +++ b/rtl/tiny/top/tiny_axi4_fabric.sv @@ -56,6 +56,7 @@ module tiny_axi4_fabric ( logic [32:0] s_last_addr; logic [ 2:0] s_decoded_target; logic s_protocol_legal; + logic s_fetch_denied; logic [1:0] s_awvalid, s_arvalid, s_wvalid, s_wlast, s_bready, s_rready; logic [1:0][31:0] s_wdata; logic [1:0][ 3:0] s_wstrb; @@ -90,6 +91,11 @@ module tiny_axi4_fabric ( s_last_addr[31:0] ) == s_decoded_target) && ((s_decoded_target != 3'd2) || (s_selected_addr.len == 8'd0)); assign s_accept = (s_state_q == Idle) && s_selected_valid; + // The tagged CPU slow ingress preserves instruction origin. Reject fetches + // outside the read-idempotent NOR apertures before reaching any APB target. + assign s_fetch_denied = !s_selected[1] && !s_selected[0] && s_selected_addr.prot[2] && !( + `SOC_ADDR_IS_FLASH(s_selected_addr.addr) + || ((s_selected_addr.addr >= 32'h5000_0000) && (s_selected_addr.addr < 32'h5400_0000))); assign s_terminal = (s_state_q == Response) && (s_write_q ? (s_target_bvalid[s_tgt_q] && s_bready[s_owner_q]) : (s_target_rvalid[s_tgt_q] && s_rready[s_owner_q] && s_target_rlast[s_tgt_q])); @@ -126,8 +132,9 @@ module tiny_axi4_fabric ( if (s_accept) begin s_owner_d = s_selected[1]; s_write_d = s_selected[0]; - s_addr_d = s_selected_addr; - s_tgt_d = s_protocol_legal ? s_decoded_target : 3'd4; + s_addr_d = s_selected_addr; + s_tgt_d = !s_protocol_legal ? 3'd4 : + s_fetch_denied ? ((s_decoded_target == 3'd2) ? 3'd4 : 3'd3) : s_decoded_target; s_state_d = Addr; end Addr: diff --git a/rtl/tiny/top/tiny_cpu_mem.sv b/rtl/tiny/top/tiny_cpu_mem.sv new file mode 100644 index 00000000..bba3bdf2 --- /dev/null +++ b/rtl/tiny/top/tiny_cpu_mem.sv @@ -0,0 +1,259 @@ +// Copyright (c) 2026 Yuchi Miao +// SPDX-License-Identifier: MulanPSL-2.0 +`include "mmap_define.svh" + +// Independent one-operation I/D frontends. AHB write data belongs to the cycle +// following address acceptance. Local stores complete only after macro commit. +// Only non-SRAM work enters the tagged, non-posted slow-path merge. +module tiny_cpu_mem ( + input logic clk_i, + input logic rst_n_i, + input logic quiesce_i, + output logic idle_o, + ahbl_if.slave cpu [2], + tiny_sram_port_if.master local_ports[2], + axi4_if.master slow_axi4 +); + typedef enum logic [2:0] { + Idle, + WriteCapture, + LocalIssue, + LocalWait, + SlowIssue, + SlowWait, + ErrorFirst, + ErrorLast + } state_e; + typedef struct packed { + state_e state; + logic [31:0] addr, data; + logic [2:0] size; + logic write; + logic [3:0] strb; + } request_t; + request_t s_req_d[2], s_req_q[2]; + logic [1:0] s_terminal, s_err, s_slow_req, s_slow_grant, s_front_idle; + logic s_slow_selected, s_slow_valid, s_slow_accept; + typedef enum logic [1:0] { + SlowIdle, + SlowAddress, + SlowData, + SlowResponse + } slow_state_e; + typedef struct packed { + slow_state_e state; + logic owner, write, error; + logic [2:0] size; + logic [31:0] addr, data, rdata; + } slow_t; + slow_t s_slow_d, s_slow_q; + ahbl_if u_slow_ahbl_if ( + .hclk (clk_i), + .hresetn(rst_n_i) + ); + axi4_if #( + .ADDR_WIDTH(32), + .DATA_WIDTH(32), + .ID_WIDTH (1), + .USER_WIDTH(1) + ) u_slow_axi4_if ( + .aclk (clk_i), + .aresetn(rst_n_i) + ); + logic s_adapter_idle; + + for (genvar port_index = 0; port_index < 2; port_index++) begin : gen_port + assign s_slow_req[port_index] = s_req_q[port_index].state == SlowIssue; + assign s_front_idle[port_index] = (s_req_q[port_index].state == Idle) && + (quiesce_i || !cpu[port_index].htrans[1]); + assign s_err[port_index] = (s_req_q[port_index].state == ErrorFirst) || + (s_req_q[port_index].state == ErrorLast) || + ((s_req_q[port_index].state == SlowWait) && + (s_slow_q.state == SlowResponse) && (s_slow_q.owner == 1'(port_index)) && s_slow_q.error); + assign s_terminal[port_index] = (s_req_q[port_index].state == ErrorLast) || + ((s_req_q[port_index].state == LocalWait) && local_ports[port_index].rsp_valid) || + ((s_req_q[port_index].state == SlowWait) && + (s_slow_q.state == SlowResponse) && (s_slow_q.owner == 1'(port_index)) && !s_slow_q.error); + assign cpu[port_index].hready = + ((s_req_q[port_index].state == Idle) && !quiesce_i) || s_terminal[port_index]; + assign cpu[port_index].hresp = s_err[port_index]; + assign cpu[port_index].hrdata = (s_req_q[port_index].state == LocalWait) ? + local_ports[port_index].rdata : s_slow_q.rdata; + assign local_ports[port_index].req_valid = s_req_q[port_index].state == LocalIssue; + assign local_ports[port_index].addr = s_req_q[port_index].addr; + assign local_ports[port_index].write = s_req_q[port_index].write; + assign local_ports[port_index].wdata = s_req_q[port_index].data; + assign local_ports[port_index].wstrb = s_req_q[port_index].strb; + assign local_ports[port_index].rsp_ready = s_req_q[port_index].state == LocalWait; + + always_comb begin + s_req_d[port_index] = s_req_q[port_index]; + unique case (s_req_q[port_index].state) + WriteCapture: begin + unique case (s_req_q[port_index].size) + 3'd0: begin + s_req_d[port_index].data = {24'd0, cpu[port_index].hwdata[7:0]} << + (8 * s_req_q[port_index].addr[1:0]); + s_req_d[port_index].strb = 4'b0001 << s_req_q[port_index].addr[1:0]; + end + 3'd1: begin + s_req_d[port_index].data = {16'd0, cpu[port_index].hwdata[15:0]} << + (8 * s_req_q[port_index].addr[1:0]); + s_req_d[port_index].strb = 4'b0011 << s_req_q[port_index].addr[1:0]; + end + default: begin + s_req_d[port_index].data = cpu[port_index].hwdata; + s_req_d[port_index].strb = 4'b1111; + end + endcase + s_req_d[port_index].state = + `SOC_ADDR_IS_SRAM(s_req_q[port_index].addr) + ? LocalIssue : SlowIssue; + end + LocalIssue: if (local_ports[port_index].req_ready) s_req_d[port_index].state = LocalWait; + SlowIssue: + if (s_slow_accept && s_slow_grant[port_index]) s_req_d[port_index].state = SlowWait; + SlowWait: if (s_err[port_index]) s_req_d[port_index].state = ErrorLast; + ErrorFirst: s_req_d[port_index].state = ErrorLast; + default: begin + end + endcase + if (cpu[port_index].hready) begin + s_req_d[port_index].state = Idle; + // Capture even a terminal-cycle overlap during quiesce: that address + // was acknowledged by HREADY and cannot be silently discarded. + if (cpu[port_index].htrans[1]) begin + s_req_d[port_index].addr = cpu[port_index].haddr; + s_req_d[port_index].size = cpu[port_index].hsize; + s_req_d[port_index].write = cpu[port_index].hwrite; + s_req_d[port_index].strb = '0; + if ((cpu[port_index].htrans != 2'b10) || cpu[port_index].hmastlock || + (cpu[port_index].hsize > 3'd2) || + ((cpu[port_index].haddr & ((32'd1 << cpu[port_index].hsize) - 32'd1)) != 32'd0) || + ((port_index == 0) && (cpu[port_index].hwrite || (cpu[port_index].hsize != 3'd2)))) + s_req_d[port_index].state = ErrorFirst; + else if (cpu[port_index].hwrite) s_req_d[port_index].state = WriteCapture; + else + s_req_d[port_index].state = + `SOC_ADDR_IS_SRAM(cpu[port_index].haddr) + ? LocalIssue : SlowIssue; + end + end + end + dffr #( + .DATA_WIDTH($bits(request_t)) + ) u_request ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_req_d[port_index]), + .dat_o (s_req_q[port_index]) + ); + end + + assign idle_o = (&s_front_idle) && (s_slow_q.state == SlowIdle) && s_adapter_idle; + assign s_slow_accept = (s_slow_q.state == SlowIdle) && s_slow_valid; + round_robin_arbiter #( + .CLIENTS(2) + ) u_slow_arbiter ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .advance_i (s_slow_accept), + .request_i (s_slow_req), + .grant_o (s_slow_grant), + .selected_o(s_slow_selected), + .valid_o (s_slow_valid) + ); + always_comb begin + s_slow_d = s_slow_q; + unique case (s_slow_q.state) + SlowIdle: + if (s_slow_accept) begin + s_slow_d.owner = s_slow_selected; + s_slow_d.addr = s_req_q[s_slow_selected].addr; + s_slow_d.write = s_req_q[s_slow_selected].write; + s_slow_d.size = s_req_q[s_slow_selected].size; + // The shared AHB adapter applies lane shifting itself. + s_slow_d.data = s_req_q[s_slow_selected].data >> (8 * s_req_q[s_slow_selected].addr[1:0]); + s_slow_d.state = SlowAddress; + end + SlowAddress: if (u_slow_ahbl_if.hready) s_slow_d.state = SlowData; + SlowData: + if (u_slow_ahbl_if.hready) begin + s_slow_d.rdata = u_slow_ahbl_if.hrdata; + s_slow_d.error = u_slow_ahbl_if.hresp; + s_slow_d.state = SlowResponse; + end + SlowResponse: s_slow_d.state = SlowIdle; + default: s_slow_d.state = SlowIdle; + endcase + end + dffr #( + .DATA_WIDTH($bits(slow_t)) + ) u_slow_state ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_slow_d), + .dat_o (s_slow_q) + ); + assign u_slow_ahbl_if.haddr = s_slow_q.addr; + assign u_slow_ahbl_if.hwrite = s_slow_q.write; + assign u_slow_ahbl_if.hsize = s_slow_q.size; + assign u_slow_ahbl_if.hwdata = s_slow_q.data; + assign u_slow_ahbl_if.htrans = (s_slow_q.state == SlowAddress) ? 2'b10 : 2'b00; + assign u_slow_ahbl_if.hburst = '0; + assign u_slow_ahbl_if.hprot = {3'b001, s_slow_q.owner}; + assign u_slow_ahbl_if.hmastlock = 1'b0; + ahbl2axi4 #( + .TwoCycleErrors(1'b1) + ) u_slow_adapter ( + .ahbl (u_slow_ahbl_if), + .axi4 (u_slow_axi4_if), + .idle_o(s_adapter_idle) + ); + // Forward every channel unchanged except the saved instruction attribute. + // Tiny's fabric uses it to reject fetches before peripheral side effects. + assign slow_axi4.awid = u_slow_axi4_if.awid; + assign slow_axi4.awaddr = u_slow_axi4_if.awaddr; + assign slow_axi4.awlen = u_slow_axi4_if.awlen; + assign slow_axi4.awsize = u_slow_axi4_if.awsize; + assign slow_axi4.awburst = u_slow_axi4_if.awburst; + assign slow_axi4.awlock = u_slow_axi4_if.awlock; + assign slow_axi4.awcache = u_slow_axi4_if.awcache; + assign slow_axi4.awprot = {!s_slow_q.owner, u_slow_axi4_if.awprot[1:0]}; + assign slow_axi4.awqos = u_slow_axi4_if.awqos; + assign slow_axi4.awregion = u_slow_axi4_if.awregion; + assign slow_axi4.awuser = u_slow_axi4_if.awuser; + assign slow_axi4.awvalid = u_slow_axi4_if.awvalid; + assign slow_axi4.arid = u_slow_axi4_if.arid; + assign slow_axi4.araddr = u_slow_axi4_if.araddr; + assign slow_axi4.arlen = u_slow_axi4_if.arlen; + assign slow_axi4.arsize = u_slow_axi4_if.arsize; + assign slow_axi4.arburst = u_slow_axi4_if.arburst; + assign slow_axi4.arlock = u_slow_axi4_if.arlock; + assign slow_axi4.arcache = u_slow_axi4_if.arcache; + assign slow_axi4.arprot = {!s_slow_q.owner, u_slow_axi4_if.arprot[1:0]}; + assign slow_axi4.arqos = u_slow_axi4_if.arqos; + assign slow_axi4.arregion = u_slow_axi4_if.arregion; + assign slow_axi4.aruser = u_slow_axi4_if.aruser; + assign slow_axi4.arvalid = u_slow_axi4_if.arvalid; + assign slow_axi4.wdata = u_slow_axi4_if.wdata; + assign slow_axi4.wstrb = u_slow_axi4_if.wstrb; + assign slow_axi4.wlast = u_slow_axi4_if.wlast; + assign slow_axi4.wuser = u_slow_axi4_if.wuser; + assign slow_axi4.wvalid = u_slow_axi4_if.wvalid; + assign slow_axi4.rready = u_slow_axi4_if.rready; + assign slow_axi4.bready = u_slow_axi4_if.bready; + assign u_slow_axi4_if.awready = slow_axi4.awready; + assign u_slow_axi4_if.arready = slow_axi4.arready; + assign u_slow_axi4_if.wready = slow_axi4.wready; + assign u_slow_axi4_if.rvalid = slow_axi4.rvalid; + assign u_slow_axi4_if.rid = slow_axi4.rid; + assign u_slow_axi4_if.rdata = slow_axi4.rdata; + assign u_slow_axi4_if.rresp = slow_axi4.rresp; + assign u_slow_axi4_if.rlast = slow_axi4.rlast; + assign u_slow_axi4_if.ruser = slow_axi4.ruser; + assign u_slow_axi4_if.bvalid = slow_axi4.bvalid; + assign u_slow_axi4_if.bid = slow_axi4.bid; + assign u_slow_axi4_if.bresp = slow_axi4.bresp; + assign u_slow_axi4_if.buser = slow_axi4.buser; +endmodule diff --git a/rtl/tiny/top/tiny_cpu_wrapper.sv b/rtl/tiny/top/tiny_cpu_wrapper.sv new file mode 100644 index 00000000..530d2b8e --- /dev/null +++ b/rtl/tiny/top/tiny_cpu_wrapper.sv @@ -0,0 +1,219 @@ +// Copyright (c) 2023-2026 Yuchi Miao +// retroSoC is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +`include "mmap_define.svh" + +module tiny_cpu_wrapper #( + parameter int ExternalIrqCount = 30, + parameter bit EnableAtomics = 1'b0, + parameter int ResetSyncStages = 3 +) ( + // verilog_format: off -- preserve reviewed column alignment + input logic clk_i, + input logic rst_n_i, + input logic [ExternalIrqCount+1:0] irq_i, + input logic jtag_tck_i, + input logic jtag_tms_i, + input logic jtag_tdi_i, + input logic jtag_trst_n_i, + output logic jtag_tdo_o, + output logic debug_halted_o, + input logic bus_idle_i, + output logic core_rst_n_o, + output logic quiesce_o, + ahbl_if.master cpu[2] + // verilog_format: on +); + + logic s_pwrup_req; + logic s_reset_pending; + logic s_hart_idle; + logic s_core_rst_n; + logic s_dbg_req_halt; + logic s_dbg_req_halt_on_reset; + logic s_dbg_req_resume; + logic s_dbg_halted; + logic s_dbg_running; + logic [31:0] s_dbg_data0_rdata; + logic [31:0] s_dbg_data0_wdata; + logic s_dbg_data0_wen; + logic [31:0] s_dbg_instr_data; + logic s_dbg_instr_data_vld; + logic s_dbg_instr_data_rdy; + logic s_dbg_instr_caught_exception; + logic s_dbg_instr_caught_ebreak; + logic [31:0] s_dbg_sbus_addr; + logic s_dbg_sbus_write; + logic [ 1:0] s_dbg_sbus_size; + logic s_dbg_sbus_vld; + logic s_dbg_sbus_rdy; + logic s_dbg_sbus_err; + logic [31:0] s_dbg_sbus_wdata; + logic [31:0] s_dbg_sbus_rdata; + + assign debug_halted_o = s_dbg_halted; + assign core_rst_n_o = s_core_rst_n; + // Request a normal debug halt before draining/resetting the core. Never cut + // off an accepted AHB transfer to manufacture an idle indication. + assign quiesce_o = s_reset_pending && s_dbg_halted; + assign s_hart_idle = bus_idle_i && (!s_reset_pending || s_dbg_halted); + + mgmt_debug_wrapper #( + .JtagIdcode (`SOC_JTAG_IDCODE), + .ResetSyncStages(ResetSyncStages) + ) u_mgmt_debug_wrapper ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .bridge_idle_i (s_hart_idle), + .jtag_tck_i (jtag_tck_i), + .jtag_tms_i (jtag_tms_i), + .jtag_tdi_i (jtag_tdi_i), + .jtag_trst_n_i (jtag_trst_n_i), + .jtag_tdo_o (jtag_tdo_o), + .core_rst_n_o (s_core_rst_n), + .reset_pending_o (s_reset_pending), + .dbg_req_halt_o (s_dbg_req_halt), + .dbg_req_halt_on_reset_o (s_dbg_req_halt_on_reset), + .dbg_req_resume_o (s_dbg_req_resume), + .dbg_halted_i (s_dbg_halted), + .dbg_running_i (s_dbg_running), + .dbg_data0_rdata_o (s_dbg_data0_rdata), + .dbg_data0_wdata_i (s_dbg_data0_wdata), + .dbg_data0_wen_i (s_dbg_data0_wen), + .dbg_instr_data_o (s_dbg_instr_data), + .dbg_instr_data_vld_o (s_dbg_instr_data_vld), + .dbg_instr_data_rdy_i (s_dbg_instr_data_rdy), + .dbg_instr_caught_exception_i(s_dbg_instr_caught_exception), + .dbg_instr_caught_ebreak_i (s_dbg_instr_caught_ebreak), + .dbg_sbus_addr_o (s_dbg_sbus_addr), + .dbg_sbus_write_o (s_dbg_sbus_write), + .dbg_sbus_size_o (s_dbg_sbus_size), + .dbg_sbus_vld_o (s_dbg_sbus_vld), + .dbg_sbus_rdy_i (s_dbg_sbus_rdy), + .dbg_sbus_err_i (s_dbg_sbus_err), + .dbg_sbus_wdata_o (s_dbg_sbus_wdata), + .dbg_sbus_rdata_i (s_dbg_sbus_rdata) + ); + + hazard3_cpu_2port #( + .RESET_VECTOR (`SOC_CPU_RESET_ADDR), + .MTVEC_INIT (32'h0000_0000), + .EXTENSION_A (EnableAtomics), + .EXTENSION_C (1), + .EXTENSION_E (0), + .EXTENSION_M (1), + .EXTENSION_ZBA (1), + .EXTENSION_ZBB (1), + .EXTENSION_ZBC (1), + .EXTENSION_ZBKB (1), + .EXTENSION_ZBKX (1), + .EXTENSION_ZBS (1), + .EXTENSION_ZCB (0), + .EXTENSION_ZCLSD (0), + .EXTENSION_ZCMP (0), + .EXTENSION_ZIFENCEI (1), + .EXTENSION_ZILSD (1), + .EXTENSION_XH3BEXTM (1), + .EXTENSION_XH3IRQ (1), + .EXTENSION_XH3PMPM (0), + .EXTENSION_XH3POWER (0), + .CSR_M_MANDATORY (1), + .CSR_M_TRAP (1), + .CSR_COUNTER (1), + .U_MODE (0), + .PMP_REGIONS (0), + .PMP_GRAIN (0), + .PMP_MATCH_NAPOT (1), + .PMP_MATCH_TOR (0), + .PMP_HARDWIRED (0), + .PMP_HARDWIRED_ADDR (0), + .PMP_HARDWIRED_CFG (0), + .DEBUG_SUPPORT (1), + .BREAKPOINT_TRIGGERS(2), + .NUM_IRQS (ExternalIrqCount), + .IRQ_PRIORITY_BITS (2), + .IRQ_INPUT_BYPASS ({ExternalIrqCount{1'b0}}), + .MVENDORID_VAL (32'h0), + .MCONFIGPTR_VAL (32'h0), + .REDUCED_BYPASS (0), + .MULDIV_UNROLL (2), + .MUL_FAST (1), + .MUL_FASTER (1), + .MULH_FAST (1), + .FAST_BRANCHCMP (1), + .RESET_REGFILE (1), + .BRANCH_PREDICTOR (1), + .MTVEC_WMASK (32'hfffffffd) + ) u_hazard3_cpu_2port ( + .clk (clk_i), + .clk_always_on (clk_i), + .rst_n (s_core_rst_n), + .pwrup_req (s_pwrup_req), + .pwrup_ack (s_pwrup_req), + .clk_en (), + .unblock_out (), + .unblock_in (1'b0), + .i_haddr (cpu[0].haddr), + .i_hwrite (cpu[0].hwrite), + .i_htrans (cpu[0].htrans), + .i_hsize (cpu[0].hsize), + .i_hburst (cpu[0].hburst), + .i_hprot (cpu[0].hprot), + .i_hmastlock (cpu[0].hmastlock), + .i_hmaster (), + .i_hready (cpu[0].hready), + .i_hresp (cpu[0].hresp), + .i_hwdata (cpu[0].hwdata), + .i_hrdata (cpu[0].hrdata), + .d_haddr (cpu[1].haddr), + .d_hwrite (cpu[1].hwrite), + .d_htrans (cpu[1].htrans), + .d_hsize (cpu[1].hsize), + .d_hburst (cpu[1].hburst), + .d_hprot (cpu[1].hprot), + .d_hmastlock (cpu[1].hmastlock), + .d_hmaster (), + .d_hready (cpu[1].hready), + .d_hresp (cpu[1].hresp), + .d_hwdata (cpu[1].hwdata), + .d_hrdata (cpu[1].hrdata), + .d_hexcl (), + .d_hexokay (1'b1), + .fence_i_vld (), + .fence_d_vld (), + .fence_rdy (1'b1), + .dbg_req_halt (s_dbg_req_halt || s_reset_pending), + .dbg_req_halt_on_reset (s_dbg_req_halt_on_reset), + .dbg_req_resume (s_dbg_req_resume && !s_reset_pending), + .dbg_halted (s_dbg_halted), + .dbg_running (s_dbg_running), + .dbg_data0_rdata (s_dbg_data0_rdata), + .dbg_data0_wdata (s_dbg_data0_wdata), + .dbg_data0_wen (s_dbg_data0_wen), + .dbg_instr_data (s_dbg_instr_data), + .dbg_instr_data_vld (s_dbg_instr_data_vld && !s_reset_pending), + .dbg_instr_data_rdy (s_dbg_instr_data_rdy), + .dbg_instr_caught_exception(s_dbg_instr_caught_exception), + .dbg_instr_caught_ebreak (s_dbg_instr_caught_ebreak), + .dbg_sbus_addr (s_dbg_sbus_addr), + .dbg_sbus_write (s_dbg_sbus_write), + .dbg_sbus_size (s_dbg_sbus_size), + .dbg_sbus_vld (s_dbg_sbus_vld), + .dbg_sbus_rdy (s_dbg_sbus_rdy), + .dbg_sbus_err (s_dbg_sbus_err), + .dbg_sbus_wdata (s_dbg_sbus_wdata), + .dbg_sbus_rdata (s_dbg_sbus_rdata), + .mhartid_val ('0), + .eco_version ('0), + .irq (irq_i[ExternalIrqCount+1:2]), + .soft_irq (irq_i[0]), + .timer_irq (irq_i[1]) + ); +endmodule diff --git a/rtl/tiny/top/tiny_sram.sv b/rtl/tiny/top/tiny_sram.sv new file mode 100644 index 00000000..0d814167 --- /dev/null +++ b/rtl/tiny/top/tiny_sram.sv @@ -0,0 +1,107 @@ +// Copyright (c) 2026 Yuchi Miao +// SPDX-License-Identifier: MulanPSL-2.0 + +module tiny_sram ( + input logic clk_i, + input logic rst_n_i, + input logic perf_enable_i, + input logic perf_clear_i, + tiny_sram_port_if.slave local_ports [2], + axi4_if.slave external_ports[4], + apb4_if.slave cfg_apb4 +); + logic [3:0] s_local_ready[2], s_local_valid[2]; + logic [31:0] s_local_data[2] [4]; + logic [ 3:0] s_events [6]; + logic [31:0] s_count_d[6], s_count_q[6]; + logic [ 2:0] s_increment[6]; + logic [32:0] s_sum [6]; + + for (genvar group = 0; group < 4; group++) begin : gen_group + tiny_sram_port_if u_ports[3] (); + for (genvar client = 0; client < 2; client++) begin : gen_local + assign u_ports[client].req_valid = local_ports[client].req_valid && + (local_ports[client].addr[16:15] == 2'(group)); + assign u_ports[client].addr = local_ports[client].addr; + assign u_ports[client].write = local_ports[client].write; + assign u_ports[client].wdata = local_ports[client].wdata; + assign u_ports[client].wstrb = local_ports[client].wstrb; + assign u_ports[client].rsp_ready = local_ports[client].rsp_ready && + (local_ports[client].addr[16:15] == 2'(group)); + assign s_local_ready[client][group] = u_ports[client].req_ready; + assign s_local_valid[client][group] = u_ports[client].rsp_valid; + assign s_local_data[client][group] = u_ports[client].rdata; + end + tiny_sram_group u_group ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .ports (u_ports) + ); + tiny_sram_axi4 #( + .GroupBase(32'h3000_0000 + 32'(group) * 32'h8000) + ) u_external ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .axi4 (external_ports[group]), + .memory (u_ports[2]) + ); + assign s_events[0][group] = external_ports[group].arvalid && external_ports[group].arready; + assign s_events[1][group] = external_ports[group].awvalid && external_ports[group].awready; + assign s_events[2][group] = external_ports[group].rvalid && external_ports[group].rready; + assign s_events[3][group] = external_ports[group].wvalid && external_ports[group].wready; + assign s_events[4][group] = + (external_ports[group].arvalid && !external_ports[group].arready) || + (external_ports[group].awvalid && !external_ports[group].awready) || + (external_ports[group].wvalid && !external_ports[group].wready) || + (external_ports[group].rvalid && !external_ports[group].rready) || + (external_ports[group].bvalid && !external_ports[group].bready); + assign s_events[5][group] = + (external_ports[group].bvalid && external_ports[group].bready && + (external_ports[group].bresp != 2'b00)) || + (external_ports[group].rvalid && external_ports[group].rready && + external_ports[group].rlast && (external_ports[group].rresp != 2'b00)); + end + for (genvar client = 0; client < 2; client++) begin : gen_local_response + assign local_ports[client].req_ready = s_local_ready[client][local_ports[client].addr[16:15]]; + assign local_ports[client].rsp_valid = s_local_valid[client][local_ports[client].addr[16:15]]; + assign local_ports[client].rdata = s_local_data[client][local_ports[client].addr[16:15]]; + end + // Preserve the public external-AXI counter meanings. Four simultaneous stalls + // are one stalled cycle; request/beat events are counted individually. + for (genvar counter = 0; counter < 6; counter++) begin : gen_counter + if (counter == 4) begin : gen_cycle_count + assign s_increment[counter] = {2'b0, |s_events[counter]}; + end else begin : gen_event_count + assign s_increment[counter] = {2'b0, s_events[counter][0]} + + {2'b0, s_events[counter][1]} + {2'b0, s_events[counter][2]} + + {2'b0, s_events[counter][3]}; + end + assign s_sum[counter] = {1'b0, s_count_q[counter]} + {30'd0, s_increment[counter]}; + assign s_count_d[counter] = perf_clear_i ? 32'd0 : + perf_enable_i ? (s_sum[counter][32] ? 32'hffff_ffff : s_sum[counter][31:0]) : + s_count_q[counter]; + dffr #( + .DATA_WIDTH(32) + ) u_counter ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_count_d[counter]), + .dat_o (s_count_q[counter]) + ); + end + onchip_ram_reg #( + .Present (1'b1), + .CapacityKiB(128), + .DataBytes (4) + ) u_reg ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .apb4 (cfg_apb4), + .perf_read_requests_i (s_count_q[0]), + .perf_write_requests_i (s_count_q[1]), + .perf_read_beats_i (s_count_q[2]), + .perf_write_beats_i (s_count_q[3]), + .perf_stall_cycles_i (s_count_q[4]), + .perf_error_responses_i(s_count_q[5]) + ); +endmodule diff --git a/rtl/tiny/top/tiny_sram_axi4.sv b/rtl/tiny/top/tiny_sram_axi4.sv new file mode 100644 index 00000000..9c777a70 --- /dev/null +++ b/rtl/tiny/top/tiny_sram_axi4.sv @@ -0,0 +1,232 @@ +// Copyright (c) 2026 Yuchi Miao +// SPDX-License-Identifier: MulanPSL-2.0 + +// One external transaction per group. Address ownership lasts through the +// terminal response, but the macro is arbitrated separately for every beat. +// W may precede AW on the bus; it is backpressured until its AW is accepted. +module tiny_sram_axi4 #( + parameter logic [31:0] GroupBase = 32'h3000_0000 +) ( + input logic clk_i, + input logic rst_n_i, + axi4_if.slave axi4, + tiny_sram_port_if.master memory +); + typedef enum logic [2:0] { + Idle, + ReadStream, + ReadError, + WriteStream, + WriteResponse, + WriteDrain + } state_e; + typedef struct packed { + state_e state; + logic prefer_write; + logic id; + logic [31:0] addr; + logic [7:0] len, beat; + logic [2:0] size; + logic [1:0] burst, resp; + logic [8:0] issued, completed; + logic last; + } transaction_t; + typedef struct packed { + logic [31:0] addr, data; + logic [3:0] strb; + } write_t; + transaction_t s_transaction_d, s_transaction_q; + write_t s_write_input, s_write_output; + logic [31:0] s_next_addr; + logic [1:0] s_ar_resp, s_aw_resp; + logic [3:0] s_lane_mask; + logic s_expected_last, s_write_legal; + logic s_write_full, s_write_empty, s_write_push, s_write_pop; + logic s_req_accept, s_resp_accept, s_write_accept; + + function automatic logic [1:0] address_response(input logic [31:0] addr_i, + input logic [7:0] len_i, input logic [2:0] size_i, + input logic [1:0] burst_i, input logic lock_i); + logic [32:0] bytes_count, last_addr; + logic wrap_legal; + bytes_count = ({25'd0, len_i} + 33'd1) << size_i; + wrap_legal = (len_i == 8'd1) || (len_i == 8'd3) || (len_i == 8'd7) || (len_i == 8'd15); + last_addr = {1'b0, addr_i} + bytes_count - 33'd1; + if (burst_i == 2'b00) last_addr = {1'b0, addr_i} + (33'd1 << size_i) - 33'd1; + if (burst_i == 2'b10) + last_addr = ({1'b0, addr_i} & ~(bytes_count - 33'd1)) + bytes_count - 33'd1; + if (lock_i || (len_i > 8'd15) || (size_i > 3'd2) || + (burst_i == 2'b11) || ((burst_i == 2'b10) && !wrap_legal) || + ((addr_i & ((32'd1 << size_i) - 32'd1)) != 32'd0) || last_addr[32] || + (addr_i[31:12] != last_addr[31:12])) + return 2'b10; + if ((addr_i < GroupBase) || (last_addr[31:0] > (GroupBase + 32'h7fff))) return 2'b11; + return 2'b00; + endfunction + + assign s_ar_resp = address_response( + axi4.araddr, axi4.arlen, axi4.arsize, axi4.arburst, axi4.arlock + ); + assign s_aw_resp = address_response( + axi4.awaddr, axi4.awlen, axi4.awsize, axi4.awburst, axi4.awlock + ); + assign axi4.arready = (s_transaction_q.state == Idle) && + !(s_transaction_q.prefer_write && axi4.awvalid); + assign axi4.awready = (s_transaction_q.state == Idle) && + (s_transaction_q.prefer_write || !axi4.arvalid); + assign axi4.wready = ((s_transaction_q.state == WriteStream) && !s_transaction_q.last && + (!s_write_full || s_write_pop)) || (s_transaction_q.state == WriteDrain); + assign axi4.rvalid = (s_transaction_q.state == ReadError) || + ((s_transaction_q.state == ReadStream) && memory.rsp_valid); + assign axi4.rid = s_transaction_q.id; + assign axi4.rdata = (s_transaction_q.state == ReadStream) ? memory.rdata : 32'd0; + assign axi4.rresp = s_transaction_q.resp; + assign axi4.rlast = s_transaction_q.beat == s_transaction_q.len; + assign axi4.ruser = '0; + assign axi4.bvalid = s_transaction_q.state == WriteResponse; + assign axi4.bid = s_transaction_q.id; + assign axi4.bresp = s_transaction_q.resp; + assign axi4.buser = '0; + + // Read responses stay in the group's reserved queue until R handshakes. + // Issuance and retirement have independent positions within the same burst. + assign memory.req_valid = ((s_transaction_q.state == ReadStream) && + (s_transaction_q.issued <= {1'b0, s_transaction_q.len})) || + ((s_transaction_q.state == WriteStream) && !s_write_empty); + assign memory.addr = memory.write ? s_write_output.addr : s_transaction_q.addr; + assign memory.write = s_transaction_q.state == WriteStream; + assign memory.wdata = s_write_output.data; + assign memory.wstrb = s_write_output.strb; + assign memory.rsp_ready = ((s_transaction_q.state == ReadStream) && axi4.rready) || + (s_transaction_q.state == WriteStream); + assign s_req_accept = memory.req_valid && memory.req_ready; + assign s_resp_accept = memory.rsp_valid && memory.rsp_ready; + assign s_write_accept = axi4.wvalid && axi4.wready; + assign s_write_push = (s_transaction_q.state == WriteStream) && s_write_accept && s_write_legal; + assign s_write_pop = (s_transaction_q.state == WriteStream) && s_req_accept; + assign s_write_input = {s_transaction_q.addr, axi4.wdata, axi4.wstrb}; + // Capture W before it becomes eligible at the macro. A full queue can + // replace its head on the same edge that the arbiter commits that word. + fifo #( + .DATA_WIDTH ($bits(write_t)), + .BUFFER_DEPTH(2) + ) u_write_fifo ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .flush_i(1'b0), + .push_i (s_write_push), + .full_o (s_write_full), + .dat_i (s_write_input), + .pop_i (s_write_pop), + .empty_o(s_write_empty), + .dat_o (s_write_output), + .cnt_o () // Full/empty and accepted/completed counts cover ownership. + ); + assign s_lane_mask = 4'(((32'd1 << (32'd1 << s_transaction_q.size)) - 32'd1) + << s_transaction_q.addr[1:0]); + assign s_expected_last = s_transaction_q.beat == s_transaction_q.len; + assign s_write_legal = ((axi4.wstrb & ~s_lane_mask) == 4'd0) && (axi4.wlast == s_expected_last); + axi4_addr_gen #( + .ADDR_WIDTH(32) + ) u_address ( + .alen_i (s_transaction_q.len), + .asize_i (s_transaction_q.size), + .aburst_i(s_transaction_q.burst), + .addr_i (s_transaction_q.addr), + .addr_o (s_next_addr) + ); + + always_comb begin + s_transaction_d = s_transaction_q; + unique case (s_transaction_q.state) + Idle: begin + s_transaction_d.beat = '0; + s_transaction_d.issued = '0; + s_transaction_d.completed = '0; + s_transaction_d.last = 1'b0; + if (axi4.arvalid && axi4.arready) begin + s_transaction_d.prefer_write = 1'b1; + s_transaction_d.id = axi4.arid; + s_transaction_d.addr = axi4.araddr; + // Match the shared target's malformed-address error response length. + s_transaction_d.len = (s_ar_resp == 2'b10) ? 8'd0 : axi4.arlen; + s_transaction_d.size = axi4.arsize; + s_transaction_d.burst = axi4.arburst; + s_transaction_d.resp = s_ar_resp; + s_transaction_d.state = (s_ar_resp == 2'b00) ? ReadStream : ReadError; + end else if (axi4.awvalid && axi4.awready) begin + s_transaction_d.prefer_write = 1'b0; + s_transaction_d.id = axi4.awid; + s_transaction_d.addr = axi4.awaddr; + s_transaction_d.len = (s_aw_resp == 2'b10) ? 8'd0 : axi4.awlen; + s_transaction_d.size = axi4.awsize; + s_transaction_d.burst = axi4.awburst; + s_transaction_d.resp = s_aw_resp; + s_transaction_d.state = (s_aw_resp == 2'b00) ? WriteStream : WriteDrain; + end + end + ReadStream: begin + if (s_req_accept) begin + s_transaction_d.issued = s_transaction_q.issued + 9'd1; + s_transaction_d.addr = s_next_addr; + end + if (s_resp_accept) begin + s_transaction_d.completed = s_transaction_q.completed + 9'd1; + if (axi4.rlast) s_transaction_d.state = Idle; + else s_transaction_d.beat = s_transaction_q.beat + 8'd1; + end + end + ReadError: + if (axi4.rready) begin + if (axi4.rlast) s_transaction_d.state = Idle; + else s_transaction_d.beat = s_transaction_q.beat + 8'd1; + end + WriteStream: begin + if (s_write_accept) begin + s_transaction_d.last = s_expected_last || axi4.wlast; + s_transaction_d.beat = s_transaction_q.beat + 8'd1; + s_transaction_d.addr = s_next_addr; + if (!s_write_legal) s_transaction_d.resp = 2'b10; + end + if (s_req_accept) s_transaction_d.issued = s_transaction_q.issued + 9'd1; + if (s_resp_accept) s_transaction_d.completed = s_transaction_q.completed + 9'd1; + // Errors do not discard older queued writes. B acknowledges all + // accepted legal beats only after their macro-commit responses drain. + if (s_transaction_q.last && s_write_empty && + (s_transaction_q.issued == s_transaction_q.completed)) + s_transaction_d.state = WriteResponse; + end + WriteDrain: + if (axi4.wvalid) begin + if (s_expected_last || axi4.wlast) s_transaction_d.state = WriteResponse; + else s_transaction_d.beat = s_transaction_q.beat + 8'd1; + end + WriteResponse: if (axi4.bready) s_transaction_d.state = Idle; + default: s_transaction_d.state = Idle; + endcase + end + dffr #( + .DATA_WIDTH($bits(transaction_t)) + ) u_transaction ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_transaction_d), + .dat_o (s_transaction_q) + ); +`ifdef HAVE_SVA + assert property (@(posedge clk_i) disable iff (!rst_n_i) + s_transaction_q.completed <= s_transaction_q.issued); + assert property (@(posedge clk_i) disable iff (!rst_n_i) + axi4.bvalid |-> s_write_empty && + (s_transaction_q.completed == s_transaction_q.issued)); + assert property (@(posedge clk_i) disable iff (!rst_n_i) + axi4.rvalid && !axi4.rready |=> axi4.rvalid && + $stable( + {axi4.rdata, axi4.rid, axi4.rresp, axi4.rlast} + )); + assert property (@(posedge clk_i) disable iff (!rst_n_i) + axi4.bvalid && !axi4.bready |=> axi4.bvalid && $stable( + {axi4.bid, axi4.bresp} + )); +`endif +endmodule diff --git a/rtl/tiny/top/tiny_sram_axi4_demux.sv b/rtl/tiny/top/tiny_sram_axi4_demux.sv new file mode 100644 index 00000000..f2283861 --- /dev/null +++ b/rtl/tiny/top/tiny_sram_axi4_demux.sv @@ -0,0 +1,130 @@ +// Copyright (c) 2026 Yuchi Miao +// SPDX-License-Identifier: MulanPSL-2.0 + +// Compatibility attachment to the pre-P5 fabric. Independent direction tags +// route responses; this module does not arbitrate or lock the SRAM macros. +module tiny_sram_axi4_demux ( + input logic clk_i, + input logic rst_n_i, + axi4_if.slave upstream, + axi4_if.master groups [4] +); + logic s_read_busy_d, s_read_busy_q; + logic s_write_busy_d, s_write_busy_q; + logic s_read_terminal, s_write_terminal; + logic [1:0] s_read_group_d, s_read_group_q; + logic [1:0] s_write_group_d, s_write_group_q; + logic [3:0] s_arready, s_awready, s_wready, s_rvalid, s_bvalid, s_rlast; + logic [3:0] s_rid, s_bid, s_ruser, s_buser; + logic [1:0] s_rresp[4], s_bresp[4]; + logic [31:0] s_rdata[4]; + + assign upstream.arready = !s_read_busy_q && s_arready[upstream.araddr[16:15]]; + assign upstream.awready = !s_write_busy_q && s_awready[upstream.awaddr[16:15]]; + assign upstream.wready = s_write_busy_q && s_wready[s_write_group_q]; + assign upstream.rvalid = s_read_busy_q && s_rvalid[s_read_group_q]; + assign upstream.rdata = s_rdata[s_read_group_q]; + assign upstream.rresp = s_rresp[s_read_group_q]; + assign upstream.rlast = s_rlast[s_read_group_q]; + assign upstream.rid = s_rid[s_read_group_q]; + assign upstream.ruser = s_ruser[s_read_group_q]; + assign upstream.bvalid = s_write_busy_q && s_bvalid[s_write_group_q]; + assign upstream.bresp = s_bresp[s_write_group_q]; + assign upstream.bid = s_bid[s_write_group_q]; + assign upstream.buser = s_buser[s_write_group_q]; + assign s_read_terminal = upstream.rvalid && upstream.rready && upstream.rlast; + assign s_write_terminal = upstream.bvalid && upstream.bready; + assign s_read_busy_d = (s_read_busy_q && !s_read_terminal) || + (upstream.arvalid && upstream.arready); + assign s_write_busy_d = (s_write_busy_q && !s_write_terminal) || + (upstream.awvalid && upstream.awready); + assign s_read_group_d = upstream.araddr[16:15]; + assign s_write_group_d = upstream.awaddr[16:15]; + + dffr #( + .DATA_WIDTH(1) + ) u_read_busy ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_read_busy_d), + .dat_o (s_read_busy_q) + ); + dffr #( + .DATA_WIDTH(1) + ) u_write_busy ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_write_busy_d), + .dat_o (s_write_busy_q) + ); + dffer #( + .DATA_WIDTH(2) + ) u_read_group ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .en_i (upstream.arvalid && upstream.arready), + .dat_i (s_read_group_d), + .dat_o (s_read_group_q) + ); + dffer #( + .DATA_WIDTH(2) + ) u_write_group ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .en_i (upstream.awvalid && upstream.awready), + .dat_i (s_write_group_d), + .dat_o (s_write_group_q) + ); + + for (genvar group = 0; group < 4; group++) begin : gen_group + assign groups[group].arid = upstream.arid; + assign groups[group].araddr = upstream.araddr; + assign groups[group].arlen = upstream.arlen; + assign groups[group].arsize = upstream.arsize; + assign groups[group].arburst = upstream.arburst; + assign groups[group].arlock = upstream.arlock; + assign groups[group].arcache = upstream.arcache; + assign groups[group].arprot = upstream.arprot; + assign groups[group].arqos = upstream.arqos; + assign groups[group].arregion = upstream.arregion; + assign groups[group].aruser = upstream.aruser; + assign groups[group].awid = upstream.awid; + assign groups[group].awaddr = upstream.awaddr; + assign groups[group].awlen = upstream.awlen; + assign groups[group].awsize = upstream.awsize; + assign groups[group].awburst = upstream.awburst; + assign groups[group].awlock = upstream.awlock; + assign groups[group].awcache = upstream.awcache; + assign groups[group].awprot = upstream.awprot; + assign groups[group].awqos = upstream.awqos; + assign groups[group].awregion = upstream.awregion; + assign groups[group].awuser = upstream.awuser; + assign groups[group].wdata = upstream.wdata; + assign groups[group].wstrb = upstream.wstrb; + assign groups[group].wlast = upstream.wlast; + assign groups[group].wuser = upstream.wuser; + + assign groups[group].arvalid = upstream.arvalid && !s_read_busy_q && + (upstream.araddr[16:15] == 2'(group)); + assign groups[group].awvalid = upstream.awvalid && !s_write_busy_q && + (upstream.awaddr[16:15] == 2'(group)); + assign groups[group].wvalid = upstream.wvalid && s_write_busy_q && + (s_write_group_q == 2'(group)); + assign groups[group].rready = upstream.rready && s_read_busy_q && (s_read_group_q == 2'(group)); + assign groups[group].bready = upstream.bready && s_write_busy_q && + (s_write_group_q == 2'(group)); + assign s_arready[group] = groups[group].arready; + assign s_awready[group] = groups[group].awready; + assign s_wready[group] = groups[group].wready; + assign s_rvalid[group] = groups[group].rvalid; + assign s_bvalid[group] = groups[group].bvalid; + assign s_rlast[group] = groups[group].rlast; + assign s_rid[group] = groups[group].rid; + assign s_bid[group] = groups[group].bid; + assign s_ruser[group] = groups[group].ruser; + assign s_buser[group] = groups[group].buser; + assign s_rresp[group] = groups[group].rresp; + assign s_bresp[group] = groups[group].bresp; + assign s_rdata[group] = groups[group].rdata; + end +endmodule diff --git a/rtl/tiny/top/tiny_sram_group.sv b/rtl/tiny/top/tiny_sram_group.sv new file mode 100644 index 00000000..a1c1c357 --- /dev/null +++ b/rtl/tiny/top/tiny_sram_group.sv @@ -0,0 +1,206 @@ +// Copyright (c) 2026 Yuchi Miao +// SPDX-License-Identifier: MulanPSL-2.0 + +// One independently arbitrated 32 KiB group. Arbitration advances on the +// physical macro edge, never on an AXI address or burst reservation. Captured +// responses isolate all three clients from subsequent macro output changes. +module tiny_sram_group ( + input logic clk_i, + input logic rst_n_i, + tiny_sram_port_if.slave ports [3] +); + logic [2:0] s_req, s_grant; + logic [1:0] s_selected; + logic s_issue; + logic [1:0] s_rsp_valid_d; + logic [2:0] s_rsp_valid_q; + logic [31:0] s_rsp_data_d[2], s_rsp_data_q[3]; + logic s_read_pending_d, s_read_pending_q; + logic [1:0] s_read_owner_d, s_read_owner_q; + logic [2:0] s_read_bank_d, s_read_bank_q; + logic [2:0] s_write; + logic [31:0] s_addr[3], s_wdata[3]; + logic [ 3:0] s_wstrb [3]; + logic [31:0] s_bank_data [8]; + logic [ 7:0] s_bank_select; + + for (genvar client = 0; client < 3; client++) begin : gen_client + assign s_addr[client] = ports[client].addr; + assign s_write[client] = ports[client].write; + assign s_wdata[client] = ports[client].wdata; + assign s_wstrb[client] = ports[client].wstrb; + assign ports[client].req_ready = s_grant[client]; + assign ports[client].rsp_valid = s_rsp_valid_q[client]; + assign ports[client].rdata = s_rsp_data_q[client]; + + if (client < 2) begin : gen_local_response + assign s_req[client] = ports[client].req_valid && !s_rsp_valid_q[client] && + !(s_read_pending_q && (s_read_owner_q == 2'(client))); + always_comb begin + s_rsp_valid_d[client] = s_rsp_valid_q[client] && !ports[client].rsp_ready; + s_rsp_data_d[client] = s_rsp_data_q[client]; + if (s_read_pending_q && (s_read_owner_q == 2'(client))) begin + s_rsp_valid_d[client] = 1'b1; + s_rsp_data_d[client] = s_bank_data[s_read_bank_q]; + end + if (s_grant[client] && s_write[client]) begin + s_rsp_valid_d[client] = 1'b1; + s_rsp_data_d[client] = '0; + end + end + dffr #( + .DATA_WIDTH(1) + ) u_response_valid ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_rsp_valid_d[client]), + .dat_o (s_rsp_valid_q[client]) + ); + dffr #( + .DATA_WIDTH(32) + ) u_response_data ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_rsp_data_d[client]), + .dat_o (s_rsp_data_q[client]) + ); + end else begin : gen_external_response + logic s_empty, s_full, s_pop, s_push, s_pending; + logic [ 1:0] s_count; + logic [ 2:0] s_reserved; + logic [31:0] s_data; + assign s_pending = s_read_pending_q && (s_read_owner_q == 2'(client)); + assign s_reserved = {1'b0, s_count} + {2'b0, s_pending}; + assign s_pop = ports[client].rsp_ready && !s_empty; + // Include the macro read already in flight. A returning word must fit + // even if the consumer removes READY immediately after this issue. + // A same-client read return and write commit cannot share one FIFO push. + assign s_req[client] = ports[client].req_valid && + ((s_reserved < 3'd2) || ((s_reserved == 3'd2) && s_pop)) && + !(s_pending && s_write[client]); + assign s_push = s_pending || (s_grant[client] && s_write[client]); + assign s_data = s_pending ? s_bank_data[s_read_bank_q] : 32'd0; + assign s_rsp_valid_q[client] = !s_empty; + fifo #( + .DATA_WIDTH (32), + .BUFFER_DEPTH(2) + ) u_response_fifo ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .flush_i(1'b0), + .push_i (s_push), + .full_o (s_full), + .dat_i (s_data), + .pop_i (s_pop), + .empty_o(s_empty), + .dat_o (s_rsp_data_q[client]), + .cnt_o (s_count) + ); +`ifdef FORMAL + always_ff @(posedge clk_i) begin + if (rst_n_i) begin + assert (s_reserved <= 3'd2); + assert (!s_push || !s_full || s_pop); + assert (!(s_pending && s_grant[client] && s_write[client])); + end + end +`endif +`ifdef HAVE_SVA + assert property (@(posedge clk_i) disable iff (!rst_n_i) s_reserved <= 3'd2); + assert property (@(posedge clk_i) disable iff (!rst_n_i) !s_push || !s_full || s_pop); +`endif + end + end + + round_robin_arbiter #( + .CLIENTS(3) + ) u_arbiter ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .advance_i (s_issue), + .request_i (s_req), + .grant_o (s_grant), + .selected_o(s_selected), + .valid_o (s_issue) + ); + assign s_read_pending_d = s_issue && !s_write[s_selected]; + assign s_read_owner_d = s_selected; + assign s_read_bank_d = s_addr[s_selected][14:12]; + dffr #( + .DATA_WIDTH(1) + ) u_read_pending ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_read_pending_d), + .dat_o (s_read_pending_q) + ); + dffer #( + .DATA_WIDTH(2) + ) u_read_owner ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .en_i (s_read_pending_d), + .dat_i (s_read_owner_d), + .dat_o (s_read_owner_q) + ); + dffer #( + .DATA_WIDTH(3) + ) u_read_bank ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .en_i (s_read_pending_d), + .dat_i (s_read_bank_d), + .dat_o (s_read_bank_q) + ); + + for (genvar bank = 0; bank < 8; bank++) begin : gen_bank + assign s_bank_select[bank] = s_issue && (s_addr[s_selected][14:12] == 3'(bank)); + tc_sram_1024x32 u_ram ( + .clk_i (clk_i), + .cs_i (s_bank_select[bank]), + .addr_i(s_addr[s_selected][11:2]), + .data_i(s_wdata[s_selected]), + .mask_i(s_wstrb[s_selected]), + .wren_i(s_write[s_selected]), + .data_o(s_bank_data[bank]) + ); + end + +`ifdef FORMAL + // Service-count proof: only continuously eligible requests accumulate + // bypasses. No wall-clock liveness is assumed for a withheld response. + for (genvar client = 0; client < 3; client++) begin : gen_formal + logic [1:0] s_bypasses_q; + logic s_past_valid_q = 1'b0; + always_ff @(posedge clk_i) begin + s_past_valid_q <= 1'b1; + if (!rst_n_i || !s_req[client] || s_grant[client]) s_bypasses_q <= '0; + else if (s_issue) s_bypasses_q <= s_bypasses_q + 2'd1; + if (rst_n_i) begin + assert ($onehot0(s_grant)); + assert (s_bypasses_q <= 2'd2); + assert (!(s_req[client] && (s_bypasses_q == 2'd2) && s_issue && !s_grant[client])); + if (client < 2) + assert (!(s_rsp_valid_q[client] && s_read_pending_q && (s_read_owner_q == 2'(client)))); + end + if (s_past_valid_q && rst_n_i && $past( + rst_n_i && ports[client].rsp_valid && !ports[client].rsp_ready + )) begin + assert (ports[client].rsp_valid); + assert (ports[client].rdata == $past(ports[client].rdata)); + end + end + end +`endif +`ifdef HAVE_SVA + assert property (@(posedge clk_i) disable iff (!rst_n_i) $onehot0(s_grant)); + assert property (@(posedge clk_i) disable iff (!rst_n_i) $onehot0(s_bank_select)); + for (genvar client = 0; client < 3; client++) begin : gen_assert + assert property (@(posedge clk_i) disable iff (!rst_n_i) + ports[client].rsp_valid && !ports[client].rsp_ready |=> + ports[client].rsp_valid && $stable( + ports[client].rdata + )); + end +`endif +endmodule diff --git a/rtl/tiny/top/tiny_sram_port_if.sv b/rtl/tiny/top/tiny_sram_port_if.sv new file mode 100644 index 00000000..ff856f59 --- /dev/null +++ b/rtl/tiny/top/tiny_sram_port_if.sv @@ -0,0 +1,25 @@ +// Copyright (c) 2026 Yuchi Miao +// SPDX-License-Identifier: MulanPSL-2.0 + +// One word operation, byte strobes, one ordered response per accepted request. +// Local I/D retain their address through the response handshake (bank routing) +// and have one operation outstanding. A fixed-group external client may have +// two operations outstanding, including a reserved macro read return. It may +// advance its request address after acceptance; responses remain in order. +interface tiny_sram_port_if; + logic req_valid, req_ready; + logic [31:0] addr, wdata; + logic write; + logic [3:0] wstrb; + logic rsp_valid, rsp_ready; + logic [31:0] rdata; + + modport master( + output req_valid, addr, wdata, write, wstrb, rsp_ready, + input req_ready, rsp_valid, rdata + ); + modport slave( + input req_valid, addr, wdata, write, wstrb, rsp_ready, + output req_ready, rsp_valid, rdata + ); +endinterface diff --git a/scripts/generate_tiny.py b/scripts/generate_tiny.py index 8c359363..eef20acc 100644 --- a/scripts/generate_tiny.py +++ b/scripts/generate_tiny.py @@ -23,6 +23,17 @@ def read_topology(path: Path, memory_map: Path) -> dict: regions = {r["symbol"] for r in json.loads(memory_map.read_text())["regions"]} if document.get("schema_version") != 1 or document.get("dma_channels") != 4: raise ValueError("Tiny requires topology schema 1 and four DMA channels") + if document.get("cpu") != {"module": "hazard3_cpu_2port", "local_ports": 2, + "data_outstanding": 1}: + raise ValueError("Tiny P4 requires independent I/D and one ordered data operation") + memory = document.get("main_sram", {}) + if any(memory.get(key) != value for key, value in { + "groups": 4, "group_bytes": 32768, "macros_per_group": 8, + "macro_bytes": 4096, "base": "0x30000000", + "macro_instance": "u_soc.u_sram.gen_group[{group}].u_group.gen_bank[{bank}].u_ram.u_mem", + "external_attachment": "serialized-pre-P5", + }.items()): + raise ValueError("Tiny P4 SRAM geometry/attachment differs from its frozen contract") if document.get("irq_vector_width") != 32 or document.get("external_irq_count") != 30: raise ValueError("Tiny requires a 32-bit IRQ vector and 30 external inputs") names: set[str] = set() diff --git a/scripts/tiny_ics55_platform.py b/scripts/tiny_ics55_platform.py index 0ddba621..c96ce358 100644 --- a/scripts/tiny_ics55_platform.py +++ b/scripts/tiny_ics55_platform.py @@ -183,6 +183,12 @@ def inspect_netlist(document: dict) -> dict: raise ValueError("Tiny instantiated the unsupported 16 KiB SRAM interface stub") if len(ram) != 32 or len(pll) != 1: raise ValueError("expected 32 main SRAM macros and one PLL") + expected_ram = {f"u_soc.u_sram.gen_group[{g}].u_group.gen_bank[{n}].u_ram.u_mem" + for g in range(4) for n in range(8)} + observed_ram = {next((name for name in expected_ram + if c["path"].replace("/", ".").endswith("." + name)), "") for c in ram} + if observed_ram != expected_ram: + raise ValueError("Tiny P4 SRAM group/macro hierarchy is missing or stale") if any(c["ports"].get("CLK") != [system] for c in ram): raise ValueError("main SRAM does not share direct SYS") constants = {"0": 0, "1": 1} @@ -202,7 +208,7 @@ def inspect_netlist(document: dict) -> dict: enable = pll[0]["ports"].get("EN", []) if len(enable) != 1 or constants.get(enable[0]) != 0: raise ValueError("PLL enable is not proven constant zero") - cpu = [c for c in leaves if "u_hazard3_cpu_1port" in c["path"] + cpu = [c for c in leaves if "u_hazard3_cpu_2port" in c["path"] and ("CK" in c["ports"] or "CKN" in c["ports"])] if not cpu or any(c["ports"].get("CK", c["ports"].get("CKN")) != [system] for c in cpu): raise ValueError("CPU sequential clocks do not share direct SYS") diff --git a/scripts/tiny_r2_p4.py b/scripts/tiny_r2_p4.py new file mode 100644 index 00000000..455645e0 --- /dev/null +++ b/scripts/tiny_r2_p4.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +"""Replay immutable P3 images on P4 RTL without relabeling image provenance.""" +from __future__ import annotations + +import argparse +from pathlib import Path +import sys +import subprocess + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +from scripts import tiny_r2_baseline as b # noqa: E402 +from scripts import tiny_r2_p3 as p3 # noqa: E402 + +PHASE = "TINY-R2-P4" +HARDWARE_KEYS = ("SOC", "PDK", "HAVE_PLL", "HAVE_SRAM_IF", "HAVE_SRAM_MACRO", + "SRAM_SIZE_KIB", "PDK_BEHAV", "HAVE_HP", "EXT_CLK_HZ", "AUD_CLK_HZ", + "CLINT_TIMEBASE_HZ", "MGMT_CPU_CLK_HZ", "JTAG_IDCODE") +LOCAL_FIELDS = {"window", "port", "accepted", "completed", "latency_max", "admission_wait"} +BANK_FIELDS = {"window", "group", "issues_i", "issues_d", "issues_external", "conflicts", + "wait_i", "wait_d", "wait_external"} + + +def inputs(root: Path, variant: Path) -> dict: + result = p3.inputs(root, variant) + # Publication bindings are tested source consumers, not generated PDFs. + names = subprocess.check_output( + ["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", "publications"], + cwd=root, text=True).split("\0") + for name in names: + path = root / name + if name and path.is_file() and path.suffix in (".py", ".json", ".typ"): + result[name] = p3.sha256(path) + return result + + +def compatible_hardware(image: dict, model: dict) -> None: + a, z = (c["configuration"] for c in (image, model)) + if any(key not in a or key not in z or a[key] != z[key] for key in HARDWARE_KEYS): + raise ValueError("retained image and candidate hardware configuration differ") + + +def parse_log(log: str, flow: dict) -> dict: + samples = p3.parse_log(log, flow) + rows = [b.parse_fields(line, LOCAL_FIELDS) for line in log.splitlines() + if line.startswith("R2_P4_LOCAL ")] + rows.sort(key=lambda row: (row["window"], row["port"])) + if [(r["window"], r["port"]) for r in rows] != [(w, p) for w in range(1, 9) for p in range(2)]: + raise ValueError("missing or duplicate P4 local observations") + for row in rows: + if row["completed"] > row["accepted"] or row["accepted"] - row["completed"] > 1: + raise ValueError("P4 local request/response accounting mismatch") + samples["local"] = rows + banks = [b.parse_fields(line, BANK_FIELDS) for line in log.splitlines() + if line.startswith("R2_P4_BANK ")] + banks.sort(key=lambda row: (row["window"], row["group"])) + if [(r["window"], r["group"]) for r in banks] != [(w, g) for w in range(1, 9) for g in range(4)]: + raise ValueError("missing or duplicate P4 bank observations") + samples["banks"] = banks + return samples + + +def reference_cases(path: Path, names: list[str]) -> list[dict]: + report = b.read_json(path) + if report.get("phase") != p3.PHASE or report.get("status") != "measurement_complete": + raise ValueError("reference is not a completed P3 measurement report") + matrix = b.read_json(b.check_artifact(report["matrix"])) + if matrix.get("status") != "passed": + raise ValueError("reference matrix did not pass") + if not names or len(set(names)) != len(names): + raise ValueError("select distinct retained cases") + result = [] + for name in names: + matching = [c for c in report["cases"] if c["name"] == name] + original = [c for c in matrix["cases"] if c["name"] == name] + if len(matching) != 1 or len(original) != 1 or matching[0]["image"] != original[0]["image"]: + raise ValueError("unknown or mismatched retained case") + case = matching[0] + p3.validate_image(b.check_artifact(case["image"])) + # Recheck the original logs, not just a report's claimed numeric values. + samples = p3.validate_simulation(original[0]["simulations"]["verilator"], + case["image"], "verilator") + if samples != case["samples"]: + raise ValueError("reference report differs from its retained execution") + result.append(case) + return result + + +def replay(args) -> None: + root, variant = args.root, args.variant_root + parent = variant / "meta/tiny-r2-p4/replay" / args.simulator + directory = b.new_attempt(parent, "attempt-") + record = {"phase": PHASE, "status": "running", "repetitions": 1, "cases": []} + b.dump(parent / "latest.json", {"path": str(directory / "record.json")}) + try: + reference = b.artifact(args.reference) + cases = reference_cases(args.reference, args.cases.split(",")) + configuration = p3.configuration(variant) + for case in cases: + compatible_hardware(b.read_json(b.check_artifact(case["image"]))["configuration"], + configuration) + before = inputs(root, variant) + tools = b.tools_identity(root) + command = args.command[1:] if args.command[:1] == ["--"] else args.command + if not command: + raise ValueError("fresh model compilation command required") + p3.execute_make(root, directory, "compile-model", command) + if before != inputs(root, variant): + raise ValueError("source changed while compiling candidate model") + runtime = b.runtime_inputs(root, variant, args.simulator) + model = variant / "sim" / args.simulator / ("emu" if args.simulator == "verilator" else "simv") + record.update(reference=reference, configuration=configuration, tools=tools, + model=b.artifact(model), source_digest=b.digest_map(before), + source=b.snapshot(root, directory / "inputs", before | runtime), + compilation={s: b.artifact(directory / f"compile-model.{s}") for s in ("json", "log")}) + launch = [str(model)] + if args.simulator == "iverilog": + record["runtime_tool"] = b.vvp_identity(args.vvp, tools["iverilog"]) + launch.insert(0, record["runtime_tool"]["executable"]["path"]) + for case in cases: + image = p3.validate_image(b.check_artifact(case["image"])) + begin, end = image["layout"]["descriptor_range"] + cmd = [*launch, "+firmware=" + image["artifacts"]["tiny_p3.hex"]["path"], + "+tiny_r2_p3", f"+baseline_tcd_base={begin:x}", f"+baseline_tcd_end={end:x}", + "+max_cycles=100000000"] + label = case["name"] + flow = b.execute(root, directory, label, + ["timeout", "--foreground", f"{args.timeout}s", *cmd], quiet=True) + samples = parse_log((directory / f"{label}.log").read_text(), flow) + p3.validate_image(b.check_artifact(case["image"])) + record["cases"].append({"name": label, "image": case["image"], "samples": samples, + "baseline_samples": case["samples"], + "command": cmd, "timeout_seconds": args.timeout, + "flow": b.artifact(directory / f"{label}.json"), + "log": b.artifact(directory / f"{label}.log")}) + b.dump(directory / "record.json", record) + if before != inputs(root, variant) or runtime != b.runtime_inputs(root, variant, args.simulator): + raise ValueError("candidate inputs changed during execution") + b.check_artifact(record["model"]) + b.check_artifact(reference) + for case in cases: + p3.validate_image(b.check_artifact(case["image"])) + for tool in tools.values(): + b.check_artifact(tool["executable"]) + record["status"] = "passed" + except (OSError, ValueError) as error: + record.update(status="failed", error=str(error)) + raise + finally: + b.dump(directory / "record.json", record) + print(f"P4 {args.simulator}: one execution per image; {directory}") + + +def validate_replay(path: Path, simulator: str) -> dict: + record = b.read_json(path) + if record.get("phase") != PHASE or record.get("status") != "passed" or record.get("repetitions") != 1: + raise ValueError("not a completed single-execution P4 replay") + for field in ("model", "reference"): + b.check_artifact(record[field]) + for value in record["compilation"].values(): + b.check_artifact(value) + for field in ("inputs", "snapshot", "patch"): + b.check_artifact(record["source"][field]) + captured = b.read_json(Path(record["source"]["inputs"]["path"])) + if b.digest_map(captured) != record["source"]["input_digest"]: + raise ValueError("candidate source digest mismatch") + for tool in record["tools"].values(): + b.check_artifact(tool["executable"]) + b.check_artifact(tool["archive"]) + prefix = [record["model"]["path"]] + if simulator == "iverilog": + runtime = b.check_artifact(record["runtime_tool"]["executable"]) + compiler = Path(record["tools"]["iverilog"]["executable"]["path"]) + if runtime.resolve().parent != compiler.resolve().parent: + raise ValueError("unmatched Icarus runtime") + prefix.insert(0, str(runtime)) + reference = reference_cases(Path(record["reference"]["path"]), [c["name"] for c in record["cases"]]) + for case, original in zip(record["cases"], reference, strict=True): + if case["image"] != original["image"] or case["baseline_samples"] != original["samples"]: + raise ValueError("comparison changed the retained image or reference measurements") + image = p3.validate_image(b.check_artifact(case["image"])) + compatible_hardware(image["configuration"], record["configuration"]) + first, end = image["layout"]["descriptor_range"] + expected = [*prefix, "+firmware=" + image["artifacts"]["tiny_p3.hex"]["path"], + "+tiny_r2_p3", f"+baseline_tcd_base={first:x}", f"+baseline_tcd_end={end:x}", + "+max_cycles=100000000"] + flow = b.read_json(b.check_artifact(case["flow"])) + if case["command"] != expected or case["timeout_seconds"] <= 0 or flow["command"] != [ + "timeout", "--foreground", f"{case['timeout_seconds']}s", *expected]: + raise ValueError("model/image execution provenance mismatch") + if parse_log(b.check_artifact(case["log"]).read_text(), flow) != case["samples"]: + raise ValueError("reported candidate samples differ from the log") + return record + + +def report(args) -> None: + base = args.variant_root / "meta/tiny-r2-p4" + runs = {} + for simulator in ("verilator", "iverilog"): + pointer = b.read_json(base / "replay" / simulator / "latest.json") + path = Path(pointer["path"]) + runs[simulator] = (validate_replay(path, simulator), b.artifact(path)) + before = b.digest_map(inputs(args.root, args.variant_root)) + if any(run[0]["source_digest"] != before for run in runs.values()): + raise ValueError("candidate source changed after measurement") + cases = {c["name"]: c for c in runs["verilator"][0]["cases"]} + if set(cases) != {"perf-o2-plain-flat", "perf-o2-plain-banked"}: + raise ValueError("P4 comparison requires both approved placement images") + cross = runs["iverilog"][0]["cases"] + if len(cross) != 1 or cross[0]["name"] != "perf-o2-plain-banked": + raise ValueError("P4 requires one banked Icarus execution") + if any(cross[0][key] != cases["perf-o2-plain-banked"][key] for key in ("image", "samples")): + raise ValueError("cross-simulator image/results differ") + comparisons = [] + for name, case in cases.items(): + rows = [] + for old, new in zip(case["baseline_samples"]["cases"], case["samples"]["cases"], strict=True): + rows.append({"name": new["name"], "baseline_cycles": old["cycles"], + "candidate_cycles": new["cycles"], "baseline_instructions": old["instructions"], + "candidate_instructions": new["instructions"], + "cycle_ratio": old["cycles"] / new["cycles"]}) + comparisons.append({"name": name, "image": case["image"], "workloads": rows, + "local": case["samples"]["local"], "banks": case["samples"]["banks"]}) + b.dump(base / "report.json", {"phase": PHASE, "status": "measurement_complete", + "source_digest": before, "comparisons": comparisons, + "executions": {key: value[1] for key, value in runs.items()}, + "qualification": "matched-binary SAFE24 simulation; not phase/physical acceptance"}) + print(f"P4 matched-image report: {base / 'report.json'}") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, required=True) + parser.add_argument("--variant-root", type=Path, required=True) + parser.add_argument("--report", action="store_true") + parser.add_argument("--reference", type=Path) + parser.add_argument("--cases") + parser.add_argument("--simulator", choices=("verilator", "iverilog")) + parser.add_argument("--vvp", default="vvp") + parser.add_argument("--timeout", type=int, default=21600) + parser.add_argument("command", nargs=argparse.REMAINDER) + args = parser.parse_args() + if args.timeout <= 0: + parser.error("timeout must be positive") + if args.report: + report(args) + else: + if args.reference is None or not args.cases or not args.simulator: + parser.error("replay requires reference, cases and simulator") + replay(args) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/rtl/tiny_axi4_fabric_tb.sv b/tests/rtl/tiny_axi4_fabric_tb.sv index 58f0cff4..0914d5ff 100644 --- a/tests/rtl/tiny_axi4_fabric_tb.sv +++ b/tests/rtl/tiny_axi4_fabric_tb.sv @@ -47,6 +47,7 @@ module tiny_axi4_fabric_tb; logic [1:0][ 0:0] arlock; logic [1:0][ 0:0] arid; logic [1:0][ 0:0] arvalid; + logic [1:0][ 2:0] arprot; logic [1:0][ 0:0] rready; logic [1:0][ 0:0] awready; logic [1:0][ 0:0] arready; @@ -113,7 +114,7 @@ module tiny_axi4_fabric_tb; assign masters[m].awregion = '0; assign masters[m].awuser = '0; assign masters[m].arcache = '0; - assign masters[m].arprot = '0; + assign masters[m].arprot = arprot[m]; assign masters[m].arqos = '0; assign masters[m].arregion = '0; assign masters[m].aruser = '0; @@ -238,6 +239,7 @@ module tiny_axi4_fabric_tb; arlock = '0; arid = '0; arvalid = '0; + arprot = '0; rready = '0; completions[0] = 0; completions[1] = 0; @@ -267,6 +269,14 @@ module tiny_axi4_fabric_tb; target_requests[3]!=3 || target_requests[4]!=4 || cpu_wait==0 || dma_wait==0) $fatal(1, "accounting/fairness mismatch faults=%0d", faults); read_request(0, 32'h30000000, 1, 2, 1, 0); + arprot[0] = 3'b100; + read_request(0, 32'h10001000, 1, 2, 1, 2); + read_request(0, 32'h54000000, 1, 2, 1, 3); + if (target_requests[2] != 2 || target_requests[1] != 1) + $fatal(1, "instruction request reached a peripheral or non-NSS0 window"); + read_request(0, 32'h00000000, 1, 2, 1, 0); + read_request(0, 32'h50000000, 1, 2, 1, 0); + if (target_requests[1] != 3) $fatal(1, "NOR instruction windows not reachable"); $display("SIM_TEST_PASS Tiny fabric"); $finish; end diff --git a/tests/rtl/tiny_r2_p4_cpu.S b/tests/rtl/tiny_r2_p4_cpu.S new file mode 100644 index 00000000..ce8b62a0 --- /dev/null +++ b/tests/rtl/tiny_r2_p4_cpu.S @@ -0,0 +1,67 @@ +.section .text +.global _start +.option norvc +_start: + li sp, 0x3001f000 + la t0, trap + csrw mtvec, t0 + li s0, 0 + # A C.NOP, followed by a 32-bit ADDI crossing the group boundary, C.RET. + li s1, 0x30007ffc + li s2, 3 +1: li t0, 0x05130001 + sw t0, 0(s1) + li t0, 0x80820090 + sw t0, 4(s1) + fence.i + jalr ra, s1, 0 + li t0, 9 + bne a0, t0, fail + li t0, 0x8000 + add s1, s1, t0 + addi s2, s2, -1 + bnez s2, 1b + # Instruction-side failure returns to RA via the trap handler. + li t0, 0x10001000 + jalr ra, t0, 0 + li t0, 1 + bne s0, t0, fail + # A debugger will patch this code and release the SRAM flag while halted. + li t0, 0x30000080 + li t1, 0x00100513 + sw t1, 0(t0) + li t1, 0x00008067 + sw t1, 4(t0) + li t1, 0x3001ffe0 + sw zero, 0(t1) + fence.i + jalr ra, t0, 0 + li t0, 1 + bne a0, t0, fail + li t0, 0x1000b084 + li t1, 1 + sw t1, 0(t0) + li t0, 0x3001ffe0 +2: lw t1, 0(t0) + beqz t1, 2b + fence.i + li t0, 0x30000080 + jalr ra, t0, 0 + li t0, 2 + bne a0, t0, fail + li t0, 0x1000b084 + li t1, 0x80000001 + sw t1, 0(t0) +3: j 3b +trap: + csrr t2, mcause + li t3, 1 + bne t2, t3, fail + addi s0, s0, 1 + csrw mepc, ra + mret +fail: + li t0, 0x1000b084 + li t1, 0x80000000 + sw t1, 0(t0) +4: j 4b diff --git a/tests/rtl/tiny_r2_p4_cpu.ld b/tests/rtl/tiny_r2_p4_cpu.ld new file mode 100644 index 00000000..51fe0f35 --- /dev/null +++ b/tests/rtl/tiny_r2_p4_cpu.ld @@ -0,0 +1,6 @@ +ENTRY(_start) +SECTIONS { + . = 0; + .text : { *(.text*) } + /DISCARD/ : { *(.riscv.attributes) *(.comment) } +} diff --git a/tests/rtl/tiny_r2_p4_cpu_tb.sv b/tests/rtl/tiny_r2_p4_cpu_tb.sv new file mode 100644 index 00000000..f8bc13de --- /dev/null +++ b/tests/rtl/tiny_r2_p4_cpu_tb.sv @@ -0,0 +1,282 @@ +`timescale 1ns / 1ps +module tiny_r2_p4_cpu_tb; + logic clk_i = 0, rst_n_i = 0; + always #5 clk_i = ~clk_i; + wire s_rst_n = rst_n_i; + logic s_jtag_tck_driver = 0, s_jtag_tms_driver = 1, s_jtag_tdi_driver = 0; + wire s_jtag_tdo; + logic core_rst_n, quiesce, idle, halted; + ahbl_if cpu[2] ( + .hclk (clk_i), + .hresetn(core_rst_n) + ); + tiny_sram_port_if local_ports[2] (); + axi4_if #( + .ADDR_WIDTH(32), + .DATA_WIDTH(32), + .ID_WIDTH (1), + .USER_WIDTH(1) + ) slow ( + .aclk (clk_i), + .aresetn(rst_n_i) + ); + axi4_if #( + .ADDR_WIDTH(32), + .DATA_WIDTH(32), + .ID_WIDTH (1), + .USER_WIDTH(1) + ) ext[4] ( + .aclk (clk_i), + .aresetn(rst_n_i) + ); + apb4_if cfg ( + .pclk (clk_i), + .presetn(rst_n_i) + ); + logic [7:0] rom [0:4095]; + string firmware; + logic read_pending = 0, aw_pending = 0, write_pending = 0, hold_read = 0; + logic [31:0] read_data = 0, write_addr = 0; + logic [ 1:0] read_resp = 0; + integer starts = 0; + logic completed = 0; + logic [31:0] status; + bit ignored; + `include "tiny_jtag_smoke.svh" + +tiny_cpu_wrapper #( + .EnableAtomics (0), + .ResetSyncStages(5) + ) u_cpu ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .irq_i (32'd0), + .jtag_tck_i (s_jtag_tck_driver), + .jtag_tms_i (s_jtag_tms_driver), + .jtag_tdi_i (s_jtag_tdi_driver), + .jtag_trst_n_i (rst_n_i), + .jtag_tdo_o (s_jtag_tdo), + .debug_halted_o(halted), + .bus_idle_i (idle), + .core_rst_n_o (core_rst_n), + .quiesce_o (quiesce), + .cpu (cpu) + ); + tiny_cpu_mem u_cpu_mem ( + .clk_i (clk_i), + .rst_n_i (core_rst_n), + .quiesce_i (quiesce), + .idle_o (idle), + .cpu (cpu), + .local_ports(local_ports), + .slow_axi4 (slow) + ); + tiny_sram u_sram ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .perf_enable_i (1'b0), + .perf_clear_i (1'b0), + .local_ports (local_ports), + .external_ports(ext), + .cfg_apb4 (cfg) + ); + // Verification-only slow memory, not a product Boot ROM. It also supplies + // the terminal mailbox and instruction fault used by this CPU fixture. + assign slow.arready = !read_pending; + assign slow.rvalid = read_pending && !hold_read; + assign slow.rdata = read_data; + assign slow.rresp = read_resp; + assign slow.rid = 0; + assign slow.rlast = 1; + assign slow.ruser = 0; + assign slow.awready = !aw_pending && !write_pending; + assign slow.wready = aw_pending; + assign slow.bvalid = write_pending; + assign slow.bresp = 0; + assign slow.bid = 0; + assign slow.buser = 0; + always @(posedge clk_i) begin + if (!rst_n_i) begin + read_pending <= 0; + aw_pending <= 0; + write_pending <= 0; + end else begin + if (slow.rvalid && slow.rready) read_pending <= 0; + if (slow.arvalid && slow.arready) begin + read_pending <= 1; + if (slow.araddr < 4096) begin + read_resp <= 0; + read_data <= { + rom[slow.araddr+3], rom[slow.araddr+2], rom[slow.araddr+1], rom[slow.araddr] + }; + end else begin + read_resp <= 3; + read_data <= 0; + if (!slow.arprot[2]) $fatal(1, "unexpected slow data read %h", slow.araddr); + end + end + if (slow.awvalid && slow.awready) begin + aw_pending <= 1; + write_addr <= slow.awaddr; + end + if (slow.wvalid && slow.wready) begin + aw_pending <= 0; + write_pending <= 1; + if (write_addr != 32'h1000b084) $fatal(1, "unexpected slow write %h", write_addr); + if (slow.wdata == 1) starts <= starts + 1; + else if (slow.wdata == 32'h80000001) completed <= 1; + else $fatal(1, "SIM_TEST_FAIL CPU fixture code=%h", slow.wdata); + end + if (slow.bvalid && slow.bready) write_pending <= 0; + end + end + for (genvar g = 0; g < 4; g++) begin : gen_init + initial begin + ext[g].awid = 0; + ext[g].awaddr = 0; + ext[g].awlen = 0; + ext[g].awsize = 2; + ext[g].awburst = 1; + ext[g].awlock = 0; + ext[g].awcache = 0; + ext[g].awprot = 0; + ext[g].awqos = 0; + ext[g].awregion = 0; + ext[g].awuser = 0; + ext[g].awvalid = 0; + ext[g].wdata = 0; + ext[g].wstrb = 15; + ext[g].wlast = 1; + ext[g].wuser = 0; + ext[g].wvalid = 0; + ext[g].bready = 0; + ext[g].arid = 0; + ext[g].araddr = 0; + ext[g].arlen = 0; + ext[g].arsize = 2; + ext[g].arburst = 1; + ext[g].arlock = 0; + ext[g].arcache = 0; + ext[g].arprot = 0; + ext[g].arqos = 0; + ext[g].arregion = 0; + ext[g].aruser = 0; + ext[g].arvalid = 0; + ext[g].rready = 0; + end + end + task automatic abstract_wait; + integer polls; + logic [31:0] value; + polls = 0; + do begin + jtag_dmi(0, 7'h16, 0, value); + polls++; + end while (value[12] && polls < 128); + if (value[12] || value[10:8] != 0) $fatal(1, "abstract command status %h", value); + endtask + task automatic set_gpr(input logic [4:0] regnum, input logic [31:0] value); + logic [31:0] unused; + jtag_dmi(1, 7'h04, value, unused); + jtag_dmi(1, 7'h17, 32'h00231000 | {27'd0, regnum}, unused); + abstract_wait(); + endtask + task automatic execute_debug(input logic [31:0] instruction); + logic [31:0] unused; + jtag_dmi(1, 7'h20, instruction, unused); + jtag_dmi(1, 7'h21, 32'h00100073, unused); + jtag_dmi(1, 7'h17, 32'h00240000, unused); + abstract_wait(); + endtask + task automatic debug_store(input logic [31:0] address, input logic [31:0] value); + set_gpr(28, address); + set_gpr(29, value); + execute_debug(32'h01de2023); // sw t4,0(t3) + endtask + initial begin + #10000000; + $display( + "CPU_DIAG starts=%0d completed=%b core_reset=%b halted=%b pending=%b idle=%b quiesce=%b hold=%b read=%b", + starts, completed, core_rst_n, halted, u_cpu.s_reset_pending, idle, quiesce, hold_read, + read_pending); + $display("CPU_DIAG I addr=%h trans=%b ready=%b D addr=%h trans=%b ready=%b slowstate=%0d", + cpu[0].haddr, cpu[0].htrans, cpu[0].hready, cpu[1].haddr, cpu[1].htrans, + cpu[1].hready, u_cpu_mem.s_slow_q.state); + $fatal(1, "SIM_TEST_TIMEOUT Tiny P4 CPU"); + end + initial begin + if (!$value$plusargs("firmware=%s", firmware)) $fatal(1, "missing CPU fixture image"); + $readmemh(firmware, rom); + cfg.paddr = 0; + cfg.pprot = 0; + cfg.psel = 0; + cfg.penable = 0; + cfg.pwrite = 0; + cfg.pwdata = 0; + cfg.pstrb = 15; + repeat (6) jtag_tick(1, 0, ignored); + @(negedge clk_i); + rst_n_i = 1; + repeat (12) jtag_tick(1, 0, ignored); + jtag_idle(1); + jtag_ir(5'h11); + wait (starts == 1); + $display("P4_CPU_STAGE boot-and-boundaries"); + jtag_dmi(1, 7'h10, 1, status); + jtag_dmi(1, 7'h10, 32'h80000001, status); + wait (halted); + $display("P4_CPU_STAGE halted"); + debug_store(32'h30000080, 32'h00200513); + debug_store(32'h3001ffe0, 1); + execute_debug(32'h0000100f); // FENCE.I before debugger resume + jtag_dmi(1, 7'h10, 32'h40000001, status); + jtag_dmi(1, 7'h10, 1, status); + wait (completed); + $display("P4_CPU_STAGE patched-code-executed"); + // A different external owner retains a B response across hart reset. + @(negedge clk_i); + ext[2].awaddr = 32'h30010100; + ext[2].awvalid = 1; + do @(posedge clk_i); while (!ext[2].awready); + @(negedge clk_i); + ext[2].awvalid = 0; + ext[2].wdata = 32'hdecafbad; + ext[2].wvalid = 1; + do @(posedge clk_i); while (!ext[2].wready); + @(negedge clk_i); + ext[2].wvalid = 0; + wait (ext[2].bvalid); + @(negedge clk_i); + hold_read = 1; + wait (read_pending); + jtag_dmi(1, 7'h10, 3, status); + repeat (32) begin + @(posedge clk_i); + if (!core_rst_n || !ext[2].bvalid) $fatal(1, "hart reset discarded accepted work"); + end + @(negedge clk_i); + hold_read = 0; + wait (!core_rst_n); + $display("P4_CPU_STAGE hart-reset-drained"); + if (!ext[2].bvalid) $fatal(1, "hart reset reset external SRAM frontend"); + jtag_dmi(1, 7'h10, 1, status); + wait (core_rst_n); + wait (starts == 2); + @(negedge clk_i); + ext[2].bready = 1; + @(posedge clk_i); + @(negedge clk_i); + ext[2].bready = 0; + ext[2].araddr = 32'h30010100; + ext[2].arvalid = 1; + do @(posedge clk_i); while (!ext[2].arready); + @(negedge clk_i); + ext[2].arvalid = 0; + ext[2].rready = 1; + do @(posedge clk_i); while (!ext[2].rvalid); + if (ext[2].rdata !== 32'hdecafbad || ext[2].rresp != 0) + $fatal(1, "SRAM payload lost on hart reset"); + $display("SIM_TEST_PASS Tiny P4 CPU compressed-boundaries fence-i debug-patch hart-drain"); + $finish; + end +endmodule diff --git a/tests/rtl/tiny_r2_p4_formal.sv b/tests/rtl/tiny_r2_p4_formal.sv new file mode 100644 index 00000000..515907c1 --- /dev/null +++ b/tests/rtl/tiny_r2_p4_formal.sv @@ -0,0 +1,58 @@ +module tiny_r2_p4_formal ( + input logic clk_i, + input logic rst_n_i +); + logic s_started_q = 1'b0; + (* anyseq *) logic [2:0] s_valid, s_ready, s_write; + tiny_sram_port_if u_ports[3] (); + always_ff @(posedge clk_i) begin + s_started_q <= 1'b1; + assume (rst_n_i == s_started_q); + end + for (genvar client = 0; client < 3; client++) begin : gen_client + logic [2:0] s_outstanding_q = '0; + wire s_accept = u_ports[client].req_valid && u_ports[client].req_ready; + wire s_complete = u_ports[client].rsp_valid && u_ports[client].rsp_ready; + always_ff @(posedge clk_i) begin + if (!rst_n_i) s_outstanding_q <= '0; + else begin + case ({ + s_accept, s_complete + }) + 2'b10: s_outstanding_q <= s_outstanding_q + 3'd1; + 2'b01: s_outstanding_q <= s_outstanding_q - 3'd1; + default: begin + end + endcase + assert (s_outstanding_q <= ((client == 2) ? 3'd2 : 3'd1)); + assert (!u_ports[client].rsp_valid || (s_outstanding_q != 3'd0)); + end + end + assign u_ports[client].req_valid = s_valid[client]; + assign u_ports[client].rsp_ready = s_ready[client]; + assign u_ports[client].write = s_write[client]; + assign u_ports[client].addr = 32'h30000000 + 32'(client) * 32'h1000; + assign u_ports[client].wdata = 32'(client); + assign u_ports[client].wstrb = 4'hf; + end + tiny_sram_group u_dut ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .ports (u_ports) + ); +endmodule + +// Abstract only the macro payload: arbitration, storage reservation, response +// capture and all control logic are the production group implementation. +module tc_sram_1024x32 ( + input logic clk_i, + cs_i, + input logic [ 9:0] addr_i, + input logic [31:0] data_i, + input logic [ 3:0] mask_i, + input logic wren_i, + output logic [31:0] data_o +); + (* anyseq *) logic [31:0] s_value; + assign data_o = s_value; +endmodule diff --git a/tests/rtl/tiny_r2_p4_memory_tb.sv b/tests/rtl/tiny_r2_p4_memory_tb.sv new file mode 100644 index 00000000..984c1b12 --- /dev/null +++ b/tests/rtl/tiny_r2_p4_memory_tb.sv @@ -0,0 +1,659 @@ +`timescale 1ns / 1ps +module tiny_r2_p4_memory_tb; + logic clk_i = 0, rst_n_i = 0, quiesce = 0, idle; + logic perf_enable = 1, perf_clear = 0; + always #5 clk_i = ~clk_i; + ahbl_if cpu[2] ( + .hclk (clk_i), + .hresetn(rst_n_i) + ); + tiny_sram_port_if local_ports[2] (); + axi4_if #( + .ADDR_WIDTH(32), + .DATA_WIDTH(32), + .ID_WIDTH (1), + .USER_WIDTH(1) + ) ext[4] ( + .aclk (clk_i), + .aresetn(rst_n_i) + ); + axi4_if #( + .ADDR_WIDTH(32), + .DATA_WIDTH(32), + .ID_WIDTH (1), + .USER_WIDTH(1) + ) slow ( + .aclk (clk_i), + .aresetn(rst_n_i) + ); + apb4_if cfg ( + .pclk (clk_i), + .presetn(rst_n_i) + ); + logic [31:0] addr[2], data[2], rdata[2]; + logic [2:0] size [2]; + logic [1:0] trans[2]; + logic [1:0] wr, ready, resp; + logic [31:0] awaddr[4], wdata[4], araddr[4], erdata[4]; + logic [3:0] wstrb[4]; + logic [7:0] awlen[4], arlen[4]; + logic [2:0] awsize[4], arsize[4]; + logic [1:0] awburst[4], arburst[4], bresp[4], rresp[4]; + logic [3:0] awvalid, wvalid, wlast, bready, arvalid, rready; + logic [3:0] awready, wready, bvalid, arready, rvalid, rlast; + integer parallel_issues = 0; + integer local_latency_max = 0; + logic [31:0] observed; + logic [31:0] seed = 32'h12345678; + + tiny_cpu_mem u_cpu_mem ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .quiesce_i (quiesce), + .idle_o (idle), + .cpu (cpu), + .local_ports(local_ports), + .slow_axi4 (slow) + ); + tiny_sram u_sram ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .perf_enable_i (perf_enable), + .perf_clear_i (perf_clear), + .local_ports (local_ports), + .external_ports(ext), + .cfg_apb4 (cfg) + ); + axi4_error_slave #( + .Response(2'b11) + ) u_slow_error ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .axi4 (slow) + ); + for (genvar p = 0; p < 2; p++) begin : gen_cpu + assign cpu[p].haddr = addr[p]; + assign cpu[p].hwdata = data[p]; + assign cpu[p].hsize = size[p]; + assign cpu[p].htrans = trans[p]; + assign cpu[p].hwrite = wr[p]; + assign cpu[p].hburst = 0; + assign cpu[p].hprot = 4'(p); + assign cpu[p].hmastlock = 0; + assign ready[p] = cpu[p].hready; + assign resp[p] = cpu[p].hresp; + assign rdata[p] = cpu[p].hrdata; + end + for (genvar g = 0; g < 4; g++) begin : gen_ext + assign ext[g].awaddr = awaddr[g]; + assign ext[g].awlen = awlen[g]; + assign ext[g].awsize = awsize[g]; + assign ext[g].awburst = awburst[g]; + assign ext[g].awvalid = awvalid[g]; + assign ext[g].wdata = wdata[g]; + assign ext[g].wstrb = wstrb[g]; + assign ext[g].wlast = wlast[g]; + assign ext[g].wvalid = wvalid[g]; + assign ext[g].bready = bready[g]; + assign ext[g].araddr = araddr[g]; + assign ext[g].arlen = arlen[g]; + assign ext[g].arsize = arsize[g]; + assign ext[g].arburst = arburst[g]; + assign ext[g].arvalid = arvalid[g]; + assign ext[g].rready = rready[g]; + assign ext[g].awid = '0; + assign ext[g].awlock = '0; + assign ext[g].awcache = '0; + assign ext[g].awprot = '0; + assign ext[g].awqos = '0; + assign ext[g].awregion = '0; + assign ext[g].awuser = '0; + assign ext[g].wuser = '0; + assign ext[g].arid = '0; + assign ext[g].arlock = '0; + assign ext[g].arcache = '0; + assign ext[g].arprot = '0; + assign ext[g].arqos = '0; + assign ext[g].arregion = '0; + assign ext[g].aruser = '0; + assign awready[g] = ext[g].awready; + assign wready[g] = ext[g].wready; + assign bvalid[g] = ext[g].bvalid; + assign bresp[g] = ext[g].bresp; + assign arready[g] = ext[g].arready; + assign rvalid[g] = ext[g].rvalid; + assign rresp[g] = ext[g].rresp; + assign rlast[g] = ext[g].rlast; + assign erdata[g] = ext[g].rdata; + end + always @(posedge clk_i) begin + if ((int'(u_sram.gen_group[0].u_group.s_issue) + + int'(u_sram.gen_group[1].u_group.s_issue) + + int'(u_sram.gen_group[2].u_group.s_issue) + + int'(u_sram.gen_group[3].u_group.s_issue)) > 1) + parallel_issues++; + end + task automatic ahb(input integer p, input bit write_access, input logic [31:0] address, + input logic [2:0] width, input logic [31:0] value, input bit error_expected, + input integer max_latency, output logic [31:0] result); + time accepted; + integer error_cycles; + @(negedge clk_i); + addr[p] = address; + size[p] = width; + wr[p] = write_access; + trans[p] = 2; + do @(posedge clk_i); while (!ready[p]); + accepted = $time; + @(negedge clk_i); + trans[p] = 0; + data[p] = value; + error_cycles = 0; + do begin + @(posedge clk_i); + if (resp[p]) error_cycles++; + end while (!ready[p]); + result = rdata[p]; + if (resp[p] != error_expected || (error_expected && error_cycles != 2)) + $fatal(1, "AHB error protocol p=%0d addr=%h errors=%0d", p, address, error_cycles); + if (max_latency != 0 && (($time - accepted) / 10) > max_latency) + $fatal(1, "local latency p=%0d addr=%h cycles=%0d", p, address, ($time - accepted) / 10); + if (max_latency != 0 && (($time - accepted) / 10) > local_latency_max) + local_latency_max = integer'(($time - accepted) / 10); + @(negedge clk_i); + endtask + task automatic store(input logic [31:0] address, input logic [31:0] value, + input logic [2:0] width); + logic [31:0] unused; + ahb(1, 1, address, width, value, 0, 3, unused); + endtask + task automatic load(input integer p, input logic [31:0] address, input logic [31:0] value, + input integer max_latency); + logic [31:0] got; + ahb(p, 0, address, 2, 0, 0, max_latency, got); + if (got !== value) + $fatal(1, "local data p=%0d addr=%h got=%h expected=%h", p, address, got, value); + endtask + task automatic read_external(input integer g, input logic [31:0] address, + input logic [31:0] value, input integer hold_cycles, + input logic [1:0] expected_resp); + @(negedge clk_i); + araddr[g] = address; + arvalid[g] = 1; + do @(posedge clk_i); while (!arready[g]); + @(negedge clk_i); + arvalid[g] = 0; + while (!rvalid[g]) @(negedge clk_i); + repeat (hold_cycles) begin + @(posedge clk_i); + if (!rvalid[g] || erdata[g] !== value || rresp[g] != expected_resp || !rlast[g]) + $fatal(1, "external response changed under backpressure"); + @(negedge clk_i); + end + if (erdata[g] !== value || rresp[g] != expected_resp || !rlast[g]) + $fatal(1, "external read g=%0d got=%h expected=%h resp=%b", g, erdata[g], value, rresp[g]); + rready[g] = 1; + @(posedge clk_i); + @(negedge clk_i); + rready[g] = 0; + endtask + task automatic write_external(input integer g, input logic [31:0] address, + input logic [31:0] value, input logic [3:0] mask, + input integer delay_data); + @(negedge clk_i); + awaddr[g] = address; + awvalid[g] = 1; + do @(posedge clk_i); while (!awready[g]); + @(negedge clk_i); + awvalid[g] = 0; + repeat (delay_data) @(negedge clk_i); + wdata[g] = value; + wstrb[g] = mask; + wvalid[g] = 1; + wlast[g] = 1; + do @(posedge clk_i); while (!wready[g]); + @(negedge clk_i); + wvalid[g] = 0; + bready[g] = 1; + do @(posedge clk_i); while (!bvalid[g]); + if (bresp[g] != 0) $fatal(1, "external write"); + @(negedge clk_i); + bready[g] = 0; + endtask + task automatic config_read(input logic [11:0] offset, input logic [31:0] expected); + @(negedge clk_i); + cfg.paddr = {20'h10017, offset}; + cfg.psel = 1; + cfg.penable = 0; + @(negedge clk_i); + cfg.penable = 1; + do @(posedge clk_i); while (!cfg.pready); + if (cfg.pslverr || cfg.prdata !== expected) + $fatal(1, "SRAM ABI/perf offset=%h got=%h expected=%h", offset, cfg.prdata, expected); + @(negedge clk_i); + cfg.psel = 0; + cfg.penable = 0; + endtask + task automatic burst_and_errors; + // Sixteen captured write beats, with W asserted before AW. + @(negedge clk_i); + wdata[0] = 32'hcafe0000; + wstrb[0] = 15; + wvalid[0] = 1; + wlast[0] = 0; + repeat (3) begin + @(posedge clk_i); + if (wready[0]) $fatal(1, "W accepted without AW ownership"); + end + @(negedge clk_i); + awaddr[0] = 32'h30000fc0; + awlen[0] = 15; + awvalid[0] = 1; + do @(posedge clk_i); while (!awready[0]); + @(negedge clk_i); + awvalid[0] = 0; + for (integer beat = 0; beat < 16; beat++) begin + wdata[0] = 32'hcafe0000 + beat; + wlast[0] = (beat == 15); + do @(posedge clk_i); while (!wready[0]); + @(negedge clk_i); + end + wvalid[0] = 0; + wait (bvalid[0]); + load(0, 32'h30000ffc, 32'hcafe000f, 3); // B held while local read completes. + if (!bvalid[0] || bresp[0] != 0) $fatal(1, "B not retained"); + @(negedge clk_i); + bready[0] = 1; + @(posedge clk_i); + @(negedge clk_i); + bready[0] = 0; + awlen[0] = 0; + araddr[0] = 32'h30000fc0; + arlen[0] = 15; + arvalid[0] = 1; + do @(posedge clk_i); while (!arready[0]); + @(negedge clk_i); + arvalid[0] = 0; + for (integer beat = 0; beat < 16; beat++) begin + wait (rvalid[0]); + repeat (beat % 4) @(negedge clk_i); + if (erdata[0] !== (32'hcafe0000 + beat) || rresp[0] != 0 || rlast[0] != (beat == 15)) + $fatal(1, "burst read ordering/last at beat %0d", beat); + rready[0] = 1; + @(posedge clk_i); + @(negedge clk_i); + rready[0] = 0; + end + arlen[0] = 0; + // A malformed write cannot commit bytes. Test strobes and early/missing LAST. + for (integer bad = 0; bad < 3; bad++) begin + @(negedge clk_i); + awaddr[0] = 32'h30000fc0; + awvalid[0] = 1; + awsize[0] = (bad == 0) ? 1 : 2; + awlen[0] = (bad == 1) ? 3 : 0; + do @(posedge clk_i); while (!awready[0]); + @(negedge clk_i); + awvalid[0] = 0; + wvalid[0] = 1; + wdata[0] = 0; + wstrb[0] = (bad == 0) ? 4'h8 : 4'hf; + wlast[0] = (bad != 2); + do @(posedge clk_i); while (!wready[0]); + @(negedge clk_i); + wvalid[0] = 0; + bready[0] = 1; + do @(posedge clk_i); while (!bvalid[0]); + if (bresp[0] != 2) $fatal(1, "malformed write accepted"); + @(negedge clk_i); + bready[0] = 0; + load(0, 32'h30000fc0, 32'hcafe0000, 3); + end + awsize[0] = 2; + awlen[0] = 0; + wlast[0] = 1; + wstrb[0] = 15; + endtask + task automatic back_to_back; + // The next address is already present when the preceding write completes. + @(negedge clk_i); + addr[1] = 32'h30000100; + size[1] = 2; + wr[1] = 1; + trans[1] = 2; + do @(posedge clk_i); while (!ready[1]); + for (integer word_index = 0; word_index < 8; word_index++) begin + @(negedge clk_i); + data[1] = 32'hf00d0000 + word_index; + addr[1] = 32'h30000104 + word_index * 4; + trans[1] = (word_index == 7) ? 0 : 2; + do @(posedge clk_i); while (!ready[1]); + if (resp[1]) $fatal(1, "pipelined write error"); + end + @(negedge clk_i); + for (integer word_index = 0; word_index < 8; word_index++) + load(0, 32'h30000100 + word_index * 4, 32'hf00d0000 + word_index, 3); + endtask + task automatic streaming_burst; + time started, first_beat, previous_beat, final_write; + integer read_gap_max, write_gap_max; + read_gap_max = 0; + write_gap_max = 0; + @(negedge clk_i); + awaddr[0] = 32'h30000400; + awlen[0] = 15; + awvalid[0] = 1; + do @(posedge clk_i); while (!awready[0]); + started = $time; + @(negedge clk_i); + awvalid[0] = 0; + wvalid[0] = 1; + for (integer beat = 0; beat < 16; beat++) begin + wdata[0] = 32'hface0000 + beat; + wlast[0] = (beat == 15); + do @(posedge clk_i); while (!wready[0]); + if (beat == 0) first_beat = $time; + else begin + if (($time - previous_beat) != 10) $fatal(1, "write streaming bubble beat=%0d", beat); + write_gap_max = 1; + end + previous_beat = $time; + @(negedge clk_i); + end + final_write = previous_beat; + wvalid[0] = 0; + bready[0] = 1; + do @(posedge clk_i); while (!bvalid[0]); + if (bresp[0] != 0) $fatal(1, "streaming write response"); + $display("P4_STREAM_WRITE first=%0d total=%0d b_after_last=%0d", (first_beat - started) / 10, + ($time - started) / 10, ($time - final_write) / 10); + @(negedge clk_i); + bready[0] = 0; + awlen[0] = 0; + araddr[0] = 32'h30000400; + arlen[0] = 15; + arvalid[0] = 1; + rready[0] = 1; + do @(posedge clk_i); while (!arready[0]); + started = $time; + @(negedge clk_i); + arvalid[0] = 0; + for (integer beat = 0; beat < 16; beat++) begin + do @(posedge clk_i); while (!rvalid[0]); + if (erdata[0] !== (32'hface0000 + beat) || rresp[0] != 0 || rlast[0] != (beat == 15)) + $fatal(1, "streaming read ordering beat=%0d", beat); + if (beat == 0) first_beat = $time; + else begin + if (($time - previous_beat) != 10) $fatal(1, "read streaming bubble beat=%0d", beat); + read_gap_max = 1; + end + previous_beat = $time; + @(negedge clk_i); + end + rready[0] = 0; + arlen[0] = 0; + $display("P4_STREAM_READ first=%0d total=%0d", (first_beat - started) / 10, + (previous_beat - started) / 10); + $display("P4_STREAM read_gap_max=%0d write_gap_max=%0d beats=16", read_gap_max, write_gap_max); + endtask + + task automatic buffered_read; + // Two reserved words must survive a full queue, local writes to their + // addresses, and READY dropping again immediately after the first pop. + @(negedge clk_i); + araddr[0] = 32'h30000400; + arlen[0] = 15; + arvalid[0] = 1; + do @(posedge clk_i); while (!arready[0]); + @(negedge clk_i); + arvalid[0] = 0; + repeat (8) @(negedge clk_i); + store(32'h30000400, 32'habcd0000, 2); + store(32'h30000404, 32'habcd0001, 2); + store(32'h30000408, 32'habcd0002, 2); + for (integer beat = 0; beat < 16; beat++) begin + rready[0] = 1; + do @(posedge clk_i); while (!rvalid[0]); + if (erdata[0] !== ((beat == 2) ? 32'habcd0002 : (32'hface0000 + beat)) || + rresp[0] != 0 || rlast[0] != (beat == 15)) + $fatal(1, "reserved response lost/reordered beat=%0d", beat); + @(negedge clk_i); + rready[0] = 0; + repeat (beat % 5 + 1) @(negedge clk_i); + end + arlen[0] = 0; + endtask + + task automatic buffered_write_errors; + integer beats, stalls; + // Earlier queued beats must commit before an error B. Later legal beats + // after a bad strobe keep the existing target policy; early LAST ends W. + for (integer bad = 0; bad < 3; bad++) begin + for (integer n = 0; n < 8; n++) store(32'h30000600 + n * 4, 32'h12340000 + n, 2); + beats = (bad == 1) ? 4 : 8; + stalls = 0; + @(negedge clk_i); + awaddr[0] = 32'h30000600; + awlen[0] = 7; + awsize[0] = (bad == 0) ? 1 : 2; + awvalid[0] = 1; + do @(posedge clk_i); while (!awready[0]); + @(negedge clk_i); + awvalid[0] = 0; + fork + begin + for (integer beat = 0; beat < beats; beat++) begin + wvalid[0] = 1; + wdata[0] = 32'hbeefbeef; + wstrb[0] = (bad == 0) ? ((beat == 3) ? 4'hf : ((beat % 2) == 0 ? 4'h3 : 4'hc)) : 4'hf; + wlast[0] = (bad == 1) ? (beat == 3) : ((bad == 0) && (beat == 7)); + do begin + @(posedge clk_i); + if (!wready[0]) stalls++; + end while (!wready[0]); + @(negedge clk_i); + wvalid[0] = 0; + if (beat == 4) repeat (3) @(negedge clk_i); + end + end + begin + repeat (6) load(0, 32'h30000020, 32'h98765432, 0); + end + begin + repeat (6) load(1, 32'h30000020, 32'h98765432, 0); + end + join + wait (bvalid[0]); + if (bresp[0] != 2) $fatal(1, "buffered malformed write response"); + if (bad != 0 && stalls == 0) $fatal(1, "write buffer full path not exercised"); + for (integer n = 0; n < 8; n++) begin + if (bad == 0) begin + load(0, 32'h30000600 + n * 4, + (n == 1) ? 32'h1234beef : ((n < 4) ? 32'hbeefbeef : (32'h12340000 + n)), 3); + end else begin + load(0, 32'h30000600 + n * 4, (n < (beats - 1)) ? 32'hbeefbeef : (32'h12340000 + n), 3); + end + end + @(negedge clk_i); + bready[0] = 1; + @(posedge clk_i); + @(negedge clk_i); + bready[0] = 0; + end + awlen[0] = 0; + awsize[0] = 2; + wlast[0] = 1; + wstrb[0] = 15; + endtask + initial begin + #20000000; + $fatal(1, "SIM_TEST_TIMEOUT Tiny P4 memory"); + end + initial begin + wr = 0; + trans[0] = 0; + trans[1] = 0; + addr[0] = 0; + addr[1] = 0; + data[0] = 0; + data[1] = 0; + size[0] = 2; + size[1] = 2; + awvalid = 0; + wvalid = 0; + wlast = 0; + bready = 0; + arvalid = 0; + rready = 0; + for (integer g = 0; g < 4; g++) begin + awaddr[g] = 0; + awlen[g] = 0; + awsize[g] = 2; + awburst[g] = 1; + araddr[g] = 0; + arlen[g] = 0; + arsize[g] = 2; + arburst[g] = 1; + wdata[g] = 0; + wstrb[g] = 15; + end + cfg.paddr = 0; + cfg.pprot = 0; + cfg.psel = 0; + cfg.penable = 0; + cfg.pwrite = 0; + cfg.pwdata = 0; + cfg.pstrb = 15; + repeat (5) @(negedge clk_i); + rst_n_i = 1; + config_read(12'h00c, 131072); + config_read(12'h010, 32); + config_read(12'h014, 4096); + // First/last words of all 32 physical macros and every 32 KiB boundary. + for (integer bank = 0; bank < 32; bank++) begin + store(32'h30000000 + bank * 4096, 32'ha5000000 + bank, 2); + store(32'h30000ffc + bank * 4096, 32'h5a000000 + bank, 2); + load(0, 32'h30000000 + bank * 4096, 32'ha5000000 + bank, 3); + load(1, 32'h30000ffc + bank * 4096, 32'h5a000000 + bank, 3); + end + store(32'h30000020, 32'h11223344, 2); + store(32'h30000021, 32'haa, 0); + store(32'h30000022, 32'hbbcc, 1); + load(0, 32'h30000020, 32'hbbccaa44, 3); + for (integer lane = 0; lane < 4; lane++) begin + store(32'h30000028, 32'h11223344, 2); + store(32'h30000028 + lane, 32'haa, 0); + load(0, 32'h30000028, (32'h11223344 & ~(32'hff << (lane * 8))) | (32'haa << (lane * 8)), 3); + end + store(32'h30000028, 32'h11223344, 2); + store(32'h30000028, 32'haabb, 1); + load(0, 32'h30000028, 32'h1122aabb, 3); + back_to_back(); + fork + load(0, 32'h30000000, 32'ha5000000, 3); + load(1, 32'h30008000, 32'ha5000008, 3); + join + if (parallel_issues == 0) $fatal(1, "different groups serialized"); + fork + load(0, 32'h30000000, 32'ha5000000, 0); + load(1, 32'h30000000, 32'ha5000000, 0); + read_external(0, 32'h30000000, 32'ha5000000, 0, 0); + join + // A held R must retain old data while local writes use that same macro. + fork + read_external(0, 32'h30000020, 32'hbbccaa44, 31, 0); + begin + wait (rvalid[0]); + store(32'h30000020, 32'h98765432, 2); + load(0, 32'h30000020, 32'h98765432, 3); + end + join + // Waiting for external W does not lock the macro against CPU accesses. + fork + write_external(0, 32'h30000024, 32'hdeadbeef, 15, 31); + begin + wait (wready[0]); + load(0, 32'h30000020, 32'h98765432, 3); + end + join + load(1, 32'h30000024, 32'hdeadbeef, 3); + @(negedge clk_i); + perf_clear = 1; + @(negedge clk_i); + perf_clear = 0; + fork + read_external(0, 32'h30000000, 32'ha5000000, 7, 0); + read_external(1, 32'h30008000, 32'ha5000008, 7, 0); + read_external(2, 32'h30010000, 32'ha5000010, 7, 0); + read_external(3, 32'h30018000, 32'ha5000018, 7, 0); + join + config_read(12'h020, 4); + config_read(12'h024, 0); + config_read(12'h028, 4); + config_read(12'h030, 7); // Four parallel stalls count once per clock. + burst_and_errors(); + streaming_burst(); + buffered_read(); + buffered_write_errors(); + read_external(0, 32'h30008000, 0, 0, 3); + ahb(1, 0, 32'h70000000, 2, 0, 1, 0, observed); + ahb(1, 0, 32'h30000001, 2, 0, 1, 0, observed); + // Deterministic word/byte readback across all groups without touching a + // simulator memory array: every operation travels through the real ports. + for (integer g = 0; g < 4; g++) begin + for (integer n = 0; n < 4096; n++) begin + seed = seed ^ (seed << 13); + seed = seed ^ (seed >> 17); + seed = seed ^ (seed << 5); + store(32'h30000000 + g * 32768 + (n % 8192) * 4, seed, 2); + load(0, 32'h30000000 + g * 32768 + (n % 8192) * 4, seed, 3); + end + end + store(32'h3001fff0, 32'h76543210, 2); + @(negedge clk_i); + araddr[3] = 32'h3001fff0; + arlen[3] = 3; + arvalid[3] = 1; + do @(posedge clk_i); while (!arready[3]); + @(negedge clk_i); + arvalid[3] = 0; + wait (rvalid[3]); + repeat (4) @(negedge clk_i); + @(negedge clk_i); + rst_n_i = 0; + repeat (3) @(negedge clk_i); + if (rvalid != 0 || bvalid != 0) $fatal(1, "system reset retained a stale response"); + rst_n_i = 1; + arlen[3] = 0; + load(0, 32'h3001fff0, 32'h76543210, 3); + read_external(3, 32'h3001fff0, 32'h76543210, 0, 0); + // Reset after W acceptance, before the captured word reaches a macro. + // The cancelled word and its completion must not replay into a new session. + @(negedge clk_i); + awaddr[3] = 32'h3001fff0; + awlen[3] = 3; + awvalid[3] = 1; + do @(posedge clk_i); while (!awready[3]); + @(negedge clk_i); + awvalid[3] = 0; + wdata[3] = 32'hbad0bad0; + wlast[3] = 0; + wvalid[3] = 1; + do @(posedge clk_i); while (!wready[3]); + @(negedge clk_i); + wvalid[3] = 0; + rst_n_i = 0; + repeat (3) @(negedge clk_i); + rst_n_i = 1; + awlen[3] = 0; + wlast[3] = 1; + repeat (5) @(negedge clk_i); + if (bvalid != 0 || rvalid != 0) $fatal(1, "cancelled W response replayed"); + read_external(3, 32'h3001fff0, 32'h76543210, 0, 0); + $display("P4_BUFFER backpressure errors reset conservation PASS"); + $display("P4_MEMORY local_latency_max=%0d parallel_issue_cycles=%0d", local_latency_max, + parallel_issues); + $display("SIM_TEST_PASS Tiny P4 memory seed=12345678 random_per_group=4096"); + $finish; + end +endmodule diff --git a/tests/test_librelane_tiny_flow.py b/tests/test_librelane_tiny_flow.py index fd2da88f..600c4259 100644 --- a/tests/test_librelane_tiny_flow.py +++ b/tests/test_librelane_tiny_flow.py @@ -107,10 +107,10 @@ def test_chip_config_covers_every_onchip_sram_bank(tmp_path: Path) -> None: macro = config["MACROS"]["RM_IHPSG13_1P_1024x32_c2_bm_bist"] assert len(macro["instances"]) == 32 assert ( - "u_soc.u_sram.gen_memory.gen_bank[0].u_ram.u_mem" in macro["instances"] + "u_soc.u_sram.gen_group[0].u_group.gen_bank[0].u_ram.u_mem" in macro["instances"] ) assert ( - "u_soc.u_sram.gen_memory.gen_bank[31].u_ram.u_mem" in macro["instances"] + "u_soc.u_sram.gen_group[3].u_group.gen_bank[7].u_ram.u_mem" in macro["instances"] ) for instance in macro["instances"].values(): x, y = instance["location"] diff --git a/tests/test_publication_tiny.py b/tests/test_publication_tiny.py index 01d42098..08215d1d 100644 --- a/tests/test_publication_tiny.py +++ b/tests/test_publication_tiny.py @@ -71,7 +71,8 @@ def test_reset_update_does_not_bypass_reviewed_snapshot(monkeypatch): @pytest.fixture def source_tree(tmp_path): - sources = [tiny.TOP, tiny.RESET_TREE, tiny.ARCHINFO, tiny.SYSCTRL, "rtl/ip/core/mgmt_core_wrapper.sv", "configs/ci/ihp130-tiny.mk", + sources = [tiny.TOP, tiny.RESET_TREE, tiny.ARCHINFO, tiny.SYSCTRL, + tiny.CPU, tiny.CPU_MEM, tiny.SRAM, "configs/ci/ihp130-tiny.mk", tiny.MAP, tiny.TOPOLOGY, (tiny.BOOK / "source-contract.json").as_posix()] for relative in sources: target = tmp_path / relative @@ -81,9 +82,9 @@ def source_tree(tmp_path): @pytest.mark.parametrize("path,before,after", [ - (tiny.TOP, ".CapacityKiB(128)", ".CapacityKiB(32)"), - (tiny.TOP, ".Ics55SmallBanks(1'b1)", ".Ics55SmallBanks(1'b0)"), - (tiny.TOP, ".DataWidth (32)", ".DataWidth (64)"), + (tiny.SRAM, ".CapacityKiB(128)", ".CapacityKiB(32)"), + (tiny.SRAM, "group < 4", "group < 2"), + (tiny.SRAM, ".DataBytes(4)", ".DataBytes(8)"), (tiny.TOP, ".NumChannels (4)", ".NumChannels (8)"), (tiny.TOP, ".EnableStreams(1'b0)", ".EnableStreams(1'b1)"), (tiny.TOP, ".EnableAtomics (1'b0)", ".EnableAtomics (1'b1)"), diff --git a/tests/test_tiny_ics55_platform.py b/tests/test_tiny_ics55_platform.py index 0c441912..6aee5dd7 100644 --- a/tests/test_tiny_ics55_platform.py +++ b/tests/test_tiny_ics55_platform.py @@ -109,10 +109,11 @@ def mapped_netlist(): else "input" for k in ports}} cells = {"u_clock_buffer": cell("clock_buffer", clk_i=7, clk_o=2), "receiver": cell("P65_1233_PBMUX", PAD=1, C=7, IE="1", OE="0", CS="1"), - "u_soc.u_cpu.u_hazard3_cpu_1port.core.pc_reg": cell("DFF", CK=2), + "u_soc.u_cpu.u_hazard3_cpu_2port.core.pc_reg": cell("DFF", CK=2), "pll": cell("PLL_TOP", EN=3, CKOUT1=4, CKOUT2=5, CKTST=6), "low": cell("TIELOH7R", Z=3)} - cells.update({f"bank{i}": cell(RAM, CLK=2) for i in range(32)}) + cells.update({f"u_soc.u_sram.gen_group[{i // 8}].u_group.gen_bank[{i % 8}].u_ram.u_mem": + cell(RAM, CLK=2) for i in range(32)}) return {"modules": { "retrosoc_tiny_asic": {"ports": {"extclk_i_pad": {"direction": "inout", "bits": [1]}, "jtag_tck_i_pad": {"direction": "inout", "bits": [11]}}, @@ -201,24 +202,33 @@ def test_hierarchical_constant_output_is_resolved(mapped_netlist): "divided_ram", "wrong_cpu", "pll_sys"]) def test_unsafe_or_incomplete_mapping_is_rejected(mapped_netlist, mutation): cells = mapped_netlist["modules"]["retrosoc_tiny_asic"]["cells"] + first_ram = "u_soc.u_sram.gen_group[0].u_group.gen_bank[0].u_ram.u_mem" if mutation == "missing_pll": del cells["pll"] elif mutation == "missing_ram": - del cells["bank0"] + del cells[first_ram] elif mutation == "live_pll": cells["low"]["type"] = "TIEHIH7R" elif mutation == "unknown_enable": cells["pll"]["connections"]["EN"] = [99] elif mutation == "divided_ram": - cells["bank0"]["connections"]["CLK"] = [99] + cells[first_ram]["connections"]["CLK"] = [99] elif mutation == "wrong_cpu": - cells["u_soc.u_cpu.u_hazard3_cpu_1port.core.pc_reg"]["connections"]["CK"] = [99] + cells["u_soc.u_cpu.u_hazard3_cpu_2port.core.pc_reg"]["connections"]["CK"] = [99] else: cells["pll"]["connections"]["CKOUT1"] = [2] with pytest.raises(ValueError): inspect_netlist(mapped_netlist) +def test_stale_p4_macro_hierarchy_is_rejected(mapped_netlist): + cells = mapped_netlist["modules"]["retrosoc_tiny_asic"]["cells"] + first_ram = "u_soc.u_sram.gen_group[0].u_group.gen_bank[0].u_ram.u_mem" + cells["u_soc.u_sram.gen_memory.gen_bank[0].u_ram.u_mem"] = cells.pop(first_ram) + with pytest.raises(ValueError, match="hierarchy"): + inspect_netlist(mapped_netlist) + + def test_tiny_uses_32_actual_ics55_small_macros(tmp_path): assert shutil.which("verilator"), "required simulator unavailable" generated = tmp_path / "map" diff --git a/tests/test_tiny_r2_p4.py b/tests/test_tiny_r2_p4.py new file mode 100644 index 00000000..fd6a92cd --- /dev/null +++ b/tests/test_tiny_r2_p4.py @@ -0,0 +1,191 @@ +"""Tiny dual-port local-memory protocol and source-bound replay acceptance.""" +from __future__ import annotations + +import os +from pathlib import Path +import shutil +import subprocess + +import pytest + +from scripts.check_simulation import DEFAULT_FAILURE +from scripts.rtl.generate_memory_map import generate as generate_memory_map +from scripts import tiny_r2_p4 as p4 + +ROOT = Path(__file__).resolve().parents[1] +COMMON = ROOT / "rtl/managed/clusterip/common/rtl" +TINY = ROOT / "rtl/tiny/top" + + +def run(command, directory, label, timeout=180): + result = subprocess.run(command, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, timeout=timeout, + env={**os.environ, "CCACHE_DISABLE": "1"}) + (directory / f"{label}.log").write_text(result.stdout) + assert result.returncode == 0, result.stdout[-12000:] + if label != "sv2v": # Converted source contains literal assertion/error strings. + assert not DEFAULT_FAILURE.search(result.stdout), result.stdout[-12000:] + return result.stdout + + +@pytest.mark.parametrize("simulator", ["verilator", "iverilog"]) +@pytest.mark.parametrize("pdk", ["BEHAV", "ICS55", "IHP130"]) +def test_four_group_local_memory(tmp_path, simulator, pdk): + for tool in (simulator, "sv2v", "vvp"): + assert shutil.which(tool), f"required tool unavailable: {tool}" + generate_memory_map(ROOT / "rtl/tiny/address_map/memory_map.json", tmp_path / "map", "YES", 128) + sources = [COMMON / p for p in ( + "utils/register.sv", "utils/fifo.sv", "stream/round_robin_arbiter.sv", "interface/axi4_if.sv", + "interface/axi4_addr_gen.sv", "interface/apb4_if.sv", "interface/ahbl_if.sv")] + sources += [TINY / p for p in ( + "tiny_sram_port_if.sv", "tiny_sram_group.sv", "tiny_sram_axi4.sv", + "tiny_sram.sv", "tiny_cpu_mem.sv")] + sources += [ROOT / p for p in ( + "rtl/tech/tc_sram.sv", "rtl/ip/memory/onchip_ram_reg.sv", + "rtl/ip/interconnect/ahbl2axi4.sv", "rtl/ip/interconnect/axi4_error_slave.sv", + "tests/rtl/tiny_r2_p4_memory_tb.sv")] + defines = ["-DSV_ASSRT_DISABLE", f"-DPDK_{pdk}", "-DHAVE_SRAM_MACRO"] + vendor = [] + if pdk == "IHP130": + base = ROOT / "physical/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_sram/verilog" + vendor = [base / "RM_IHPSG13_1P_core_behavioral_bm_bist.v", + base / "RM_IHPSG13_1P_1024x32_c2_bm_bist.v"] + defines += ["-DFUNCTIONAL", "-DSYNTHESIS"] + elif pdk == "ICS55": + vendor = [ROOT / ".cache/retrosoc/pdk/ics55/sram/ics55_ecos_sram_1024x32_m8/verilog/ics55_ecos_sram_1024x32_m8_core.v"] + assert all(p.is_file() for p in vendor), "locked SRAM models unavailable" + includes = [f"-I{p}" for p in (COMMON, COMMON / "interface", tmp_path / "map/rtl", + ROOT / "rtl/ip/memory")] + top = "tiny_r2_p4_memory_tb" + output = tmp_path / "sim" + if simulator == "verilator": + run([simulator, "--binary", "--timing", "--assert", "-Wno-fatal", "-j", "3", + "-DHAVE_SVA", "--top-module", top, *defines, *includes, + *(str(p) for p in sources + vendor), "--Mdir", str(tmp_path / "obj"), "-o", str(output)], + tmp_path, "compile") + launch = [str(output)] + else: + # Keep vendor models outside sv2v, as in the executable Tiny flow. + converted = run(["sv2v", *defines, *includes, *(str(p) for p in sources)], tmp_path, "sv2v") + (tmp_path / "converted.v").write_text(converted) + run([simulator, "-g2012", "-gno-specify", "-s", top, + *defines, "-o", str(output), str(tmp_path / "converted.v"), *(str(p) for p in vendor)], + tmp_path, "compile") + launch = ["vvp", str(output)] + content = run(launch, tmp_path, "sim") + assert "SIM_TEST_PASS Tiny P4 memory" in content + assert "P4_STREAM read_gap_max=1 write_gap_max=1 beats=16" in content + assert "P4_BUFFER backpressure errors reset conservation PASS" in content + + +@pytest.mark.parametrize("simulator", ["verilator", "iverilog"]) +def test_actual_dual_port_cpu_debug_and_reset(tmp_path, simulator): + for tool in (simulator, "sv2v", "vvp", "riscv32-unknown-elf-gcc", "riscv32-unknown-elf-objcopy"): + assert shutil.which(tool), f"required tool unavailable: {tool}" + generate_memory_map(ROOT / "rtl/tiny/address_map/memory_map.json", tmp_path / "map", "YES", 128) + run(["riscv32-unknown-elf-gcc", "-march=rv32imc_zicsr_zifencei", "-mabi=ilp32", + "-nostdlib", "-Wl,--build-id=none", "-T", str(ROOT / "tests/rtl/tiny_r2_p4_cpu.ld"), + str(ROOT / "tests/rtl/tiny_r2_p4_cpu.S"), "-o", str(tmp_path / "cpu.elf")], tmp_path, "firmware") + run(["riscv32-unknown-elf-objcopy", "-O", "verilog", str(tmp_path / "cpu.elf"), + str(tmp_path / "cpu.hex")], tmp_path, "objcopy") + sources = [COMMON / p for p in ( + "utils/register.sv", "utils/fifo.sv", "stream/round_robin_arbiter.sv", "interface/axi4_if.sv", + "interface/axi4_addr_gen.sv", "interface/apb4_if.sv", "interface/ahbl_if.sv", + "clkrst/rst_sync.sv", "cdc/cdc_sync.sv")] + sources += [ROOT / "rtl/managed/hazard3/hdl" / line.removeprefix("/hazard3/") + for line in (ROOT / "rtl/tiny/filelist/core_hazard3.fl").read_text().splitlines() + if line.startswith("/hazard3/")] + sources += [TINY / p for p in ( + "tiny_sram_port_if.sv", "tiny_sram_group.sv", "tiny_sram_axi4.sv", + "tiny_sram.sv", "tiny_cpu_mem.sv", "tiny_cpu_wrapper.sv")] + sources += [ROOT / p for p in ( + "rtl/tech/tc_sram.sv", "rtl/tech/tc_clk.sv", "rtl/ip/memory/onchip_ram_reg.sv", + "rtl/ip/interconnect/ahbl2axi4.sv", "rtl/ip/core/mgmt_debug_wrapper.sv", + "rtl/ip/core/mgmt_debug_reset.sv", "tests/rtl/tiny_r2_p4_cpu_tb.sv")] + includes = [f"-I{p}" for p in (COMMON, COMMON / "interface", tmp_path / "map/rtl", + ROOT / "rtl/ip/memory", ROOT / "rtl/managed/hazard3/hdl", + ROOT / "rtl/tiny/dv")] + defines = ["-DSV_ASSRT_DISABLE", "-DPDK_BEHAV", "-DSOC_JTAG_IDCODE=32'hDEADBEEF"] + output = tmp_path / "sim" + top = "tiny_r2_p4_cpu_tb" + if simulator == "verilator": + run([simulator, "--binary", "--timing", "--assert", "-Wno-fatal", "-j", "3", + "-DHAVE_SVA", "--top-module", top, *defines, *includes, + *(str(p) for p in sources), "--Mdir", str(tmp_path / "obj"), "-o", str(output)], + tmp_path, "compile") + launch = [str(output)] + else: + converted = run(["sv2v", *defines, *includes, *(str(p) for p in sources)], tmp_path, "sv2v") + (tmp_path / "converted.v").write_text(converted) + run([simulator, "-g2012", "-s", top, "-o", str(output), str(tmp_path / "converted.v")], + tmp_path, "compile") + launch = ["vvp", str(output)] + content = run([*launch, f"+firmware={tmp_path / 'cpu.hex'}"], tmp_path, "sim") + assert "SIM_TEST_PASS Tiny P4 CPU" in content + + +def test_group_control_induction(tmp_path): + for tool in ("sv2v", "yosys"): + assert shutil.which(tool), f"required tool unavailable: {tool}" + sources = [COMMON / "utils/register.sv", COMMON / "utils/fifo.sv", + COMMON / "stream/round_robin_arbiter.sv", + TINY / "tiny_sram_port_if.sv", TINY / "tiny_sram_group.sv", + ROOT / "tests/rtl/tiny_r2_p4_formal.sv"] + converted = run(["sv2v", "-DFORMAL", "-DSV_ASSRT_DISABLE", f"-I{COMMON}", + *(str(p) for p in sources)], tmp_path, "sv2v") + (tmp_path / "formal.v").write_text(converted) + script = (f"read_verilog -formal {tmp_path / 'formal.v'}; " + "prep -top tiny_r2_p4_formal -flatten; memory_map; async2sync; dffunmap; opt; " + "sat -seq 12 -tempinduct -prove-asserts -verify -set-init-zero") + content = run(["yosys", "-p", script], tmp_path, "formal", timeout=180) + assert "Induction step proven: SUCCESS!" in content + + +def p4_log(): + from test_tiny_r2_p3 import sample_log + lines = [sample_log()] + for window in range(1, 9): + for port in range(2): + lines.append(f"R2_P4_LOCAL window={window} port={port} accepted=3 completed=3 " + "latency_max=3 admission_wait=0") + for group in range(4): + lines.append(f"R2_P4_BANK window={window} group={group} issues_i=2 issues_d=1 " + "issues_external=0 conflicts=0 wait_i=0 wait_d=0 wait_external=0") + return "\n".join(lines) + + +def test_p4_acceptance_retains_local_and_external_accounting(): + samples = p4.parse_log(p4_log(), {"status": "passed", "exit_code": 0}) + assert len(samples["local"]) == 16 and len(samples["banks"]) == 32 + assert samples["cases"][2]["checksum"] == 0xc4a58dc5 + + +@pytest.mark.parametrize("mutation", ["exit", "marker", "missing", "duplicate", "accounting"]) +def test_p4_rejects_incomplete_or_failed_evidence(mutation): + text = p4_log() + flow = {"status": "passed", "exit_code": 0} + if mutation == "exit": + flow["exit_code"] = 124 + elif mutation == "marker": + text += "\nSIM_TEST_TIMEOUT" + elif mutation == "missing": + text = "\n".join(line for line in text.splitlines() if not line.startswith("R2_P4_BANK window=8")) + elif mutation == "duplicate": + text += "\n" + next(line for line in text.splitlines() if line.startswith("R2_P4_LOCAL")) + else: + text = text.replace("accepted=3 completed=3", "accepted=3 completed=4") + with pytest.raises(ValueError): + p4.parse_log(text, flow) + + +@pytest.mark.parametrize("key", p4.HARDWARE_KEYS) +def test_p4_rejects_mismatched_hardware(key): + values = dict.fromkeys(p4.HARDWARE_KEYS, "same") + with pytest.raises(ValueError): + p4.compatible_hardware({"configuration": values}, {"configuration": values | {key: "other"}}) + + +def test_p4_preserves_independent_image_compiler_identity(): + hardware = dict.fromkeys(p4.HARDWARE_KEYS, "same") + p4.compatible_hardware({"configuration": hardware | {"SW_OPT": "O2"}}, + {"configuration": hardware | {"SW_OPT": "APP"}})