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.
This commit is contained in:
maksyuki committed 2026-10-10 18:04:19 +08:00
1 parent 4db27524a6
commit 3131762b35
47 files changed
+3626 -162

No files matched your search

+12
View File
@@ -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
+1
View File
@@ -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
+19 -1
View File
@@ -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) {
+387
View File
@@ -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.
+65 -1
View File
@@ -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 |
@@ -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!",
+3
View File
@@ -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]
}
@@ -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"
@@ -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. |
@@ -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. |
+10 -7
View File
@@ -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 <tiny-core>
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 <tiny-clock>
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 <tiny-bus>
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.
+6 -5
View File
@@ -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)
@@ -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]));"
]
+18 -9
View File
@@ -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)",
+30 -29
View File
@@ -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 #(
+3
View File
@@ -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;
+8 -6
View File
@@ -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
+17
View File
@@ -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": [
+7
View File
@@ -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"
]
},
+15
View File
@@ -201,3 +201,18 @@ 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
# 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
+8 -2
View File
@@ -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
+107 -47
View File
@@ -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 (
+7
View File
@@ -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
@@ -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",
+10
View File
@@ -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,
+49 -21
View File
@@ -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),
+9 -2
View File
@@ -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:
+259
View File
@@ -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
+219
View File
@@ -0,0 +1,219 @@
// Copyright (c) 2023-2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// 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
+107
View File
@@ -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
+232
View File
@@ -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
+130
View File
@@ -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
+206
View File
@@ -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
+25
View File
@@ -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
+11
View File
@@ -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()
+7 -1
View File
@@ -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")
+255
View File
@@ -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())
+11 -1
View File
@@ -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
+67
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
ENTRY(_start)
SECTIONS {
. = 0;
.text : { *(.text*) }
/DISCARD/ : { *(.riscv.attributes) *(.comment) }
}
+282
View File
@@ -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
+58
View File
@@ -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
+659
View File
@@ -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
+2 -2
View File
@@ -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"]
+5 -4
View File
@@ -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)"),
+15 -5
View File
@@ -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"
+191
View File
@@ -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"}})