From 8a83fe742d562337078b6d4ed1d504013d061706 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Thu, 21 May 2026 15:00:46 -0700 Subject: [PATCH 1/4] Create batcher test plan. --- docs/plans/batcher-regression/README.md | 10 ++ docs/plans/batcher-regression/handoff.md | 23 +++ docs/plans/batcher-regression/plan.md | 163 ++++++++++++++++++++++ docs/plans/batcher-regression/progress.md | 31 ++++ 4 files changed, 227 insertions(+) create mode 100644 docs/plans/batcher-regression/README.md create mode 100644 docs/plans/batcher-regression/handoff.md create mode 100644 docs/plans/batcher-regression/plan.md create mode 100644 docs/plans/batcher-regression/progress.md diff --git a/docs/plans/batcher-regression/README.md b/docs/plans/batcher-regression/README.md new file mode 100644 index 0000000000..2d21d5d91a --- /dev/null +++ b/docs/plans/batcher-regression/README.md @@ -0,0 +1,10 @@ +# Batcher Regression Task + +This task captures the planned RTL regression work for `protocols/batcher`. +It is a child effort of the broader `docs/plans/rtl-regression/` rollout and +uses the same Python-only cocotb methodology. + +## Files +- `plan.md`: scope, staged test strategy, acceptance criteria, and risks. +- `progress.md`: factual status log for this task. +- `handoff.md`: short resume notes for the next coding session. diff --git a/docs/plans/batcher-regression/handoff.md b/docs/plans/batcher-regression/handoff.md new file mode 100644 index 0000000000..75823535fc --- /dev/null +++ b/docs/plans/batcher-regression/handoff.md @@ -0,0 +1,23 @@ +# Batcher Regression Handoff + +## Resume Point +- Start from `docs/plans/batcher-regression/plan.md`. +- The first implementation target is `AxiStreamBatcher` leaf behavior. +- Do not start with `AxiStreamBatcherAxil` or `AxiStreamBatcherEventBuilder` + until the leaf contract is pinned down. + +## Expected File Areas +- RTL wrappers: `protocols/batcher/wrappers/` +- Tests: `tests/protocols/batcher/` +- Source RTL: `protocols/batcher/rtl/` + +## Immediate Next Action +- Confirm the plan scope with the user. +- Then continue or revise the Phase 1 standalone `AxiStreamBatcher` tests. + +## Validation Checklist +- `./.venv/bin/vsg -c vsg-linter.yml -f ` +- `PYTHONPYCACHEPREFIX=/private/tmp/surf-pycache ./.venv/bin/python -m py_compile ` +- `./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` +- Stale simulator process sweep +- `git diff --check` diff --git a/docs/plans/batcher-regression/plan.md b/docs/plans/batcher-regression/plan.md new file mode 100644 index 0000000000..89d1a6ec48 --- /dev/null +++ b/docs/plans/batcher-regression/plan.md @@ -0,0 +1,163 @@ +# Batcher Regression Plan + +## Objective +- Add focused, standalone cocotb regressions for the VHDL modules under + `protocols/batcher/`. +- Start with leaf-module behavior, then add AXI-Lite and event-builder coverage + only where it proves wrapper or integration behavior that the leaf tests do + not already prove. +- Keep executable stimulus and scoreboards in Python. +- Keep VHDL additions limited to thin cocotb-facing wrappers beside the batcher + RTL. + +## Parent Methodology +- Follow `docs/plans/rtl-regression/plan.md`. +- New Python regression files need the standard SURF header, a module-specific + `Test methodology` block, and in-body comments explaining major cocotb steps. +- Checked-in VHDL wrappers need the standard SURF banner and short section + comments for bus shims, DUT hookup, and flattened/status wiring. +- Validate edited VHDL with `./.venv/bin/vsg -c vsg-linter.yml ...`. +- Validate Python syntax with the repo virtualenv interpreter. +- After any pytest/cocotb/GHDL run, sweep for stale simulator processes. + +## Helper And Reuse Directives +- Keep shared batcher test code in `tests/protocols/batcher/batcher_test_utils.py` + or another clearly named helper in the same package. +- Do not duplicate flat AXI Stream endpoint drivers, ready/valid wait loops, + reset/clock setup, byte packing/unpacking helpers, V2 header/tail builders, + or common receive/backpressure monitors across individual test files. +- Prefer extending the batcher helper with narrow reusable utilities over adding + local helper functions to each module test. +- Keep module test files focused on scenario setup and assertions that are + specific to that module. +- Reuse existing repo helpers first where they already fit, especially + `tests/axi/utils.py` for sampled ready/valid handshakes and + `tests/common/regression_utils.py` for cocotb/GHDL launch plumbing. +- If AXI-Lite wrapper tests need repeated register transactions, add small + batcher-local register helpers or reuse existing AXI-Lite helpers rather than + spelling out raw bus operations in every test. +- Keep helper code from becoming a hidden DUT oracle: shared utilities may build + protocol bytes and perform mechanical handshakes, but module-specific policy + checks should remain visible in the tests that depend on them. + +## Module Inventory +| Module | Role | Planned Coverage | +| --- | --- | --- | +| `AxiStreamBatcher` | Leaf stream batcher for V1/V2 superframes | Direct functional regression | +| `AxiStreamBatcherAxil` | AXI-Lite register/control wrapper around the leaf batcher | Register-map and control-path regression after the leaf contract is covered | +| `AxiStreamBatcherEventBuilder` | Multi-input event-builder wrapper above the batcher | Integration regression for source selection, TDEST remap, timeout/drop behavior, and counters | + +## Phase 1: Leaf Batcher Contract +Target `protocols/batcher/rtl/AxiStreamBatcher.vhd` through a thin wrapper that +exposes flat AXI Stream ports, control generics, and runtime termination knobs. + +Planned checks: +- V2 superframe header formatting, including version, width, and sequence byte. +- V2 compacted byte stream through the `AxiStreamGearbox` path: header, payload, + and 7-byte subframe tail with no zero-padding bytes. +- Subframe tail metadata: byte count, `TDEST`, first-byte `TUSER`, and last-byte + `TUSER`. +- Termination modes: `maxSubFrames`, `maxClkGap`, `superFrameByteThreshold`, and + `forceTerm` where the EOFE bit placement can be asserted cleanly. +- Multiple subframes inside one superframe, including non-word-aligned payloads. +- Output backpressure stability while `M_AXIS_TREADY` is low. +- Reset/idleness recovery after a partial or pending superframe. +- Curated generic sweep after the default V2 case is stable: + - V2 at the default 8-byte width first. + - V1 with a power-of-two stream width if the compacted expected model remains + readable. + - Avoid broad Cartesian sweeps unless a bug or high-risk branch justifies them. + +Acceptance for Phase 1: +- One checked-in wrapper under `protocols/batcher/wrappers/` if an existing shim + is insufficient. +- Tests under `tests/protocols/batcher/`. +- Focused validation passes for the batcher test file. +- `vsg`, `py_compile`, and `git diff --check` are clean. + +## Phase 2: AXI-Lite Wrapper +Target `AxiStreamBatcherAxil` only after Phase 1 establishes the underlying +stream contract. + +Planned checks: +- Reset values and readback for: + - `superFrameByteThreshold` at `0x00` + - `maxSubFrames` at `0x04` + - `maxClkGap` at `0x08` + - idle/version status at `0x0C` +- Writes to the threshold/count/gap registers affect subsequent superframe + termination behavior. +- `softRst` at `0xFC` returns the stream path to idle and clears any pending + partial superframe. +- `blowoff` at `0xF8` accepts/drops inbound traffic without emitting malformed + output. +- `COMMON_CLOCK_G=true` first; async AXI-Lite crossing can be deferred unless the + wrapper proves stable under the local GHDL flow. + +Acceptance for Phase 2: +- AXI-Lite helper reuse from existing test utilities where practical. +- Tests prove register-visible behavior and one stream-side effect per control + register family. +- No duplicate leaf-batcher packet grammar tests unless they are necessary to + prove AXI-Lite control propagation. + +## Phase 3: Event Builder +Target `AxiStreamBatcherEventBuilder` as an integration layer, not as another +full batcher grammar test. + +Planned checks: +- Indexed mode source selection and output `TDEST` remap. +- Routed mode `TDEST_ROUTES_G` behavior for fixed and passthrough bits. +- Transition-frame handling through `TRANS_TDEST_G`. +- Bypass/drop behavior and related counters. +- Timeout behavior: stale or missing source data increments timeout-drop counters + and does not corrupt later accepted events. +- AXI-Lite readback for status/counters that are visible through the event + builder. +- Backpressure on the shared output while multiple inputs are ready. + +Acceptance for Phase 3: +- Event-builder tests use small `NUM_SLAVES_G` cases first. +- The Python expected model focuses on arbitration/remap/drop policy and reuses + leaf-batcher byte-stream helpers for the final output shape. +- Known intentionally untested branches are recorded in `progress.md`. + +## Out Of Scope +- Exhaustive generic Cartesian sweeps. +- Throughput/performance benchmarking. +- Replacing the existing RTL register map or public Python APIs. +- Vendor or mixed-language simulator work. +- Re-proving every leaf-batcher byte in higher-level wrappers when a narrower + control/integration assertion is sufficient. + +## Validation Commands +Planned focused commands: + +```bash +./.venv/bin/vsg -c vsg-linter.yml -f protocols/batcher/wrappers/*.vhd +PYTHONPYCACHEPREFIX=/private/tmp/surf-pycache ./.venv/bin/python -m py_compile tests/protocols/batcher/*.py +./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher +git diff --check +``` + +After simulator runs, sweep for stale processes with an explicit `ps`/`rg` +filter and kill only leftover run trees. + +## Risks +- V2 output uses `AxiStreamGearbox`, so expected data must model compacted bytes + rather than raw input beats. +- `forceTerm` sets SSI EOFE through `TUSER_FIRST_LAST_C`; bit placement should + be checked against SURF helpers before asserting exact raw `TUSER` bits. +- The byte threshold logic counts in word-sized internal increments; tests + should assert externally visible termination behavior, not an over-precise + internal byte accounting model. +- Event-builder scope can grow quickly; keep it to integration-specific policy + and avoid recreating a complete event-system simulation. + +## Done Criteria +- The batcher task docs identify what is covered, what is intentionally deferred, + and how to resume. +- Focused batcher regressions pass locally. +- New wrappers and tests follow the RTL regression style rules. +- `docs/plans/rtl-regression/progress.md` and `handoff.md` are updated only + after validated batcher work lands in the working tree. diff --git a/docs/plans/batcher-regression/progress.md b/docs/plans/batcher-regression/progress.md new file mode 100644 index 0000000000..76f1423353 --- /dev/null +++ b/docs/plans/batcher-regression/progress.md @@ -0,0 +1,31 @@ +# Batcher Regression Progress + +## Status +- Current phase: planning. +- Current implementation gate: do not expand batcher tests until this plan is + reviewed. +- Current target: `AxiStreamBatcher` leaf regression first, then + `AxiStreamBatcherAxil`, then `AxiStreamBatcherEventBuilder` if needed. + +## Decisions +- Use a standalone leaf-first strategy. +- Use Python/cocotb for executable stimulus and scoreboards. +- Use a thin checked-in wrapper only when the native record interface is too + awkward for direct cocotb stimulus. +- Keep high-level wrapper tests focused on register/control/integration policy + instead of re-proving the full leaf packet grammar. + +## Draft Work In This Session +- A local draft wrapper/helper/test may exist in the working tree from initial + exploration. Treat it as implementation draft material, not as accepted final + scope, until this plan is approved and any remaining planned coverage is + reviewed against it. + +## Validation +- No validation is required for the plan-only checkpoint. + +## Next Steps +1. Review and approve or adjust `plan.md`. +2. If approved, finish Phase 1 leaf-batcher coverage. +3. Run focused lint, syntax, pytest, stale-process sweep, and diff checks. +4. Update this progress file with actual validated results. From c6d6c548a53207f2ec1f33acfd90605874cc0390 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Thu, 21 May 2026 15:21:46 -0700 Subject: [PATCH 2/4] Start implementing batcher test plan. --- docs/plans/batcher-regression/handoff.md | 30 +- docs/plans/batcher-regression/progress.md | 58 +++- docs/plans/rtl-regression/handoff.md | 21 ++ docs/plans/rtl-regression/progress.md | 14 + .../wrappers/AxiStreamBatcherAxilWrapper.vhd | 209 ++++++++++++ .../wrappers/AxiStreamBatcherWrapper.vhd | 143 ++++++++ tests/protocols/batcher/__init__.py | 9 + tests/protocols/batcher/batcher_test_utils.py | 252 ++++++++++++++ .../batcher/test_AxiStreamBatcher.py | 318 ++++++++++++++++++ .../batcher/test_AxiStreamBatcherAxil.py | 279 +++++++++++++++ 10 files changed, 1309 insertions(+), 24 deletions(-) create mode 100644 protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd create mode 100644 protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd create mode 100644 tests/protocols/batcher/__init__.py create mode 100644 tests/protocols/batcher/batcher_test_utils.py create mode 100644 tests/protocols/batcher/test_AxiStreamBatcher.py create mode 100644 tests/protocols/batcher/test_AxiStreamBatcherAxil.py diff --git a/docs/plans/batcher-regression/handoff.md b/docs/plans/batcher-regression/handoff.md index 75823535fc..1967ae7256 100644 --- a/docs/plans/batcher-regression/handoff.md +++ b/docs/plans/batcher-regression/handoff.md @@ -2,9 +2,12 @@ ## Resume Point - Start from `docs/plans/batcher-regression/plan.md`. -- The first implementation target is `AxiStreamBatcher` leaf behavior. -- Do not start with `AxiStreamBatcherAxil` or `AxiStreamBatcherEventBuilder` - until the leaf contract is pinned down. +- The first implementation target, standalone `AxiStreamBatcher` V2 leaf + behavior at the default 8-byte width, now has a passing cocotb regression. +- A narrow `AxiStreamBatcherAxil` common-clock wrapper regression is also in + place for register readback and control propagation. +- Do not start broad `AxiStreamBatcherEventBuilder` coverage until the leaf and + AXI-Lite wrapper tests remain green in the current worktree. ## Expected File Areas - RTL wrappers: `protocols/batcher/wrappers/` @@ -12,12 +15,19 @@ - Source RTL: `protocols/batcher/rtl/` ## Immediate Next Action -- Confirm the plan scope with the user. -- Then continue or revise the Phase 1 standalone `AxiStreamBatcher` tests. +- If continuing Phase 1, add only focused leaf gaps such as a compact V1 case or + adverse forced-termination timing. +- If deepening Phase 2, keep it wrapper-specific: async AXI-Lite crossing, + additional blowoff timing, or soft-reset timing. Avoid duplicating leaf byte + grammar tests. +- If moving to Phase 3, start with small event-builder source-count cases and + reuse the batcher byte-stream helpers for final output shape. ## Validation Checklist -- `./.venv/bin/vsg -c vsg-linter.yml -f ` -- `PYTHONPYCACHEPREFIX=/private/tmp/surf-pycache ./.venv/bin/python -m py_compile ` -- `./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` -- Stale simulator process sweep -- `git diff --check` +- Latest completed: + - `./.venv/bin/vsg -c vsg-linter.yml -f protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd` + - `PYTHONPYCACHEPREFIX=/private/tmp/surf-pycache ./.venv/bin/python -m py_compile tests/protocols/batcher/batcher_test_utils.py tests/protocols/batcher/test_AxiStreamBatcher.py tests/protocols/batcher/test_AxiStreamBatcherAxil.py` + - `./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` (`2 passed`) + - Stale simulator process sweep, no leftover batcher `ghdl`/`pytest`/cocotb + processes observed + - `git diff --check` diff --git a/docs/plans/batcher-regression/progress.md b/docs/plans/batcher-regression/progress.md index 76f1423353..1688f1f58c 100644 --- a/docs/plans/batcher-regression/progress.md +++ b/docs/plans/batcher-regression/progress.md @@ -1,11 +1,12 @@ # Batcher Regression Progress ## Status -- Current phase: planning. -- Current implementation gate: do not expand batcher tests until this plan is - reviewed. -- Current target: `AxiStreamBatcher` leaf regression first, then - `AxiStreamBatcherAxil`, then `AxiStreamBatcherEventBuilder` if needed. +- Current phase: Phase 2 AXI-Lite wrapper implementation started. +- Current implementation gate: `AxiStreamBatcher` V2 8-byte leaf coverage and + `AxiStreamBatcherAxil` common-clock register/control coverage are validated + locally. +- Current target: keep any further `AxiStreamBatcherAxil` work register/control + specific, then move to `AxiStreamBatcherEventBuilder` if needed. ## Decisions - Use a standalone leaf-first strategy. @@ -16,16 +17,45 @@ instead of re-proving the full leaf packet grammar. ## Draft Work In This Session -- A local draft wrapper/helper/test may exist in the working tree from initial - exploration. Treat it as implementation draft material, not as accepted final - scope, until this plan is approved and any remaining planned coverage is - reviewed against it. +- Added a thin cocotb-facing wrapper at + `protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd`. +- Added a common-clock AXI-Lite wrapper at + `protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd`. +- Added shared batcher helpers in + `tests/protocols/batcher/batcher_test_utils.py`. +- Added a standalone leaf regression in + `tests/protocols/batcher/test_AxiStreamBatcher.py`. +- Added an AXI-Lite wrapper regression in + `tests/protocols/batcher/test_AxiStreamBatcherAxil.py`. +- Covered V2 compacted output for the default 8-byte width: superframe header + bytes, subframe payload/tail bytes, multiple subframes per superframe, + termination by max-subframe count, idle gap, byte threshold, forced + termination with terminal `EOFE`, output backpressure stability, and reset + recovery after a partial superframe. +- Covered `AxiStreamBatcherAxil` reset/readback for the documented register map, + control propagation for max-subframe count, byte threshold, and clock gap, + `softRst` recovery from a partial superframe, and `blowoff` accept/drop + behavior followed by normal recovery traffic. ## Validation -- No validation is required for the plan-only checkpoint. +- `./.venv/bin/vsg -c vsg-linter.yml -f protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd` + passed with zero violations. +- `PYTHONPYCACHEPREFIX=/private/tmp/surf-pycache ./.venv/bin/python -m py_compile tests/protocols/batcher/batcher_test_utils.py tests/protocols/batcher/test_AxiStreamBatcher.py tests/protocols/batcher/test_AxiStreamBatcherAxil.py` + passed. +- `./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` passed with + `2 passed`. +- Stale simulator process sweep did not show leftover `ghdl`, `pytest`, or + cocotb batcher processes. +- `git diff --check` passed for tracked changes. The new batcher files are + still untracked, so whitespace on those files was also covered by `vsg` and + `py_compile`. ## Next Steps -1. Review and approve or adjust `plan.md`. -2. If approved, finish Phase 1 leaf-batcher coverage. -3. Run focused lint, syntax, pytest, stale-process sweep, and diff checks. -4. Update this progress file with actual validated results. +1. Keep Phase 1 intentionally narrow unless a change touches the batcher leaf: + possible next leaf additions are a small V1/power-of-two-width case or more + adverse `forceTerm` timing. +2. If Phase 2 deepens, stay focused on wrapper-specific behavior such as async + AXI-Lite crossing or additional malformed/blowoff timing; do not duplicate + the full leaf byte grammar. +3. Start Phase 3 event-builder coverage with small `NUM_SLAVES_G` cases and + reuse the leaf byte-stream helpers for final output shape. diff --git a/docs/plans/rtl-regression/handoff.md b/docs/plans/rtl-regression/handoff.md index cb3ff34e57..6c10e81979 100644 --- a/docs/plans/rtl-regression/handoff.md +++ b/docs/plans/rtl-regression/handoff.md @@ -182,6 +182,27 @@ If the user keeps the focus on stream-helper cleanup rather than resuming a new If the user continues the new `protocols/packetizer` slice, the standalone-first pass now has expanded leaf coverage plus one narrow V2 loopback after the direct contracts. `AxiStreamPacketizer`, `AxiStreamDepacketizer`, `AxiStreamPacketizer2`, `AxiStreamDepacketizer2`, and `AxiStreamBytePacker` are covered directly under `tests/protocols/packetizer/`, and `AxiStreamPacketizer2LoopbackWrapper` adds CRC NONE/DATA/FULL packetizer-to-depacketizer coverage. The shared helper layer in `tests/protocols/packetizer/packetizer_test_utils.py` now owns the repeated V0/V2 packet beat builders, packetized/app-stream assertions, V2 CRC-mode env decoding, depacketizer `initDone` polling, no-output checks, and BytePacker unpaced stimulus/output-valid helpers. The packetizer/depacketizer wrappers expose full per-byte `TUSER` vectors for `TUSER_FIRST_LAST` behavior, the legacy V0 pair covers both EOF/user tail encodings, split/continuation state, output backpressure, and malformed-continuation bleed/recovery, the V2 pair covers header/payload/tail, split-frame sequencing, sequence-counter wrap at `SEQ_CNT_SIZE_G=4`, partial final `TKEEP`, interleaved-`TDEST` rearbitration, `TDEST_BITS_G=0/1/2` loopback behavior, exact and one-byte-over `maxPktBytes` splitting, output backpressure, CRC-mode packetizer behavior, DATA/FULL bad-CRC rejection, CRC-none tail-error marking, header error paths, link-drop recovery, and isolated mid-frame link-drop termination/recovery, and the byte packer covers partial-beat compaction, idle gaps, reset flush, zero-keep input beats, and no-ready behavior across 1-to-8, 2-to-5, 3-to-6, 3-to-7, 4-to-8, 5-to-7, and 7-to-8 compressed-keep width conversions. The latest focused packetizer validation is `./.venv/bin/python -m pytest -n 0 -q tests/protocols/packetizer` (`22 passed`), with `git diff --check` clean after the helper refactor. +If the user continues the new `protocols/batcher` slice, Phase 1 and a narrow +Phase 2 have started. The current worktree adds +`protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd`, +`protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd`, +`tests/protocols/batcher/batcher_test_utils.py`, and +`tests/protocols/batcher/test_AxiStreamBatcher.py` plus +`tests/protocols/batcher/test_AxiStreamBatcherAxil.py`. The validated +V2/default 8-byte leaf slice covers compacted header/payload/tail bytes, +subframe metadata, multi-subframe superframes, max-subframe/idle-gap/byte- +threshold termination, forced termination with terminal `EOFE`, output +backpressure hold, and reset recovery after a partial superframe. The validated +AXI-Lite wrapper slice covers reset/readback for the documented register map, +control propagation for max-subframe count, byte threshold, and clock gap, +`softRst` recovery from a partial superframe, and `blowoff` accept/drop behavior +followed by normal recovery. The latest focused validation is +`./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` (`2 passed`), +with clean wrapper `vsg`, Python `py_compile`, stale-process sweep, and +`git diff --check`. Possible next steps are a small V1/power-of-two leaf case, +deeper AXI-Lite async/adverse reset timing, or Phase 3 event-builder integration +coverage. + If the user keeps the focus on `protocols/srp`, the main review findings and high-value coverage additions are complete. The optional remaining SRP follow-up is deeper timeout or posted-write disabled-op permutations if a future change touches those RTL branches. The latest focused SRP validation command is `./.venv/bin/python -m pytest -n 0 -q tests/protocols/srp`, and it passed locally with `23 passed`. If the user switches back to `protocols/coaxpress`, the remaining practical work is deeper policy-level semantics on top of the bounded event payload, bridge status, and SSI `EOFE` interfaces: event-payload oversize/backpressure behavior above `CoaXPressRx`, optional software/firmware consumers of the new bridge AXI-Lite counters beyond the checked-in HKP classification readback sweep, and downstream image-path handling of terminal `EOFE`. The old skipped `CoaXPressConfig` SRP ingress investigation bench is now active. diff --git a/docs/plans/rtl-regression/progress.md b/docs/plans/rtl-regression/progress.md index 5f253566a5..c03b6a6271 100644 --- a/docs/plans/rtl-regression/progress.md +++ b/docs/plans/rtl-regression/progress.md @@ -12,6 +12,7 @@ - The axi-first pass is complete through the previously remaining final 11 `axi/` modules. - The current `verification-2` branch has been refreshed by merging the current `origin/pre-release` tip. The validated `protocols/ssi`, `protocols/pgp`, current Ethernet waves (`EthMacCore`, `RawEthFramer`, `UdpEngine`, `IpV4Engine`, and the current pure-VHDL RoCEv2 quartet), current CoaXPress status/EOFE work, SRP follow-up work, and base-depth pass are all part of the present branch snapshot. - The current packetizer pass started with standalone tests for individual VHDL modules, not a loopback-as-oracle bench. `AxiStreamPacketizer`, `AxiStreamDepacketizer`, `AxiStreamPacketizer2`, `AxiStreamDepacketizer2`, and `AxiStreamBytePacker` now have direct cocotb coverage through checked-in wrappers, and `AxiStreamPacketizer2LoopbackWrapper` adds a narrow end-to-end V2 CRC-mode loopback check after those leaf contracts are pinned down. The packetizer/depacketizer wrappers expose the full per-byte `TUSER` vectors needed by `TUSER_FIRST_LAST` semantics. The legacy V0 tests cover both tail encodings, max-size split/continuation state, output backpressure hold, malformed-continuation bleed/recovery, and normal recovery framing. The V2 tests now cover partial final `TKEEP`, split-frame sequence state, sequence-counter wrap at `SEQ_CNT_SIZE_G=4`, interleaved `TDEST` rearbitration, `TDEST_BITS_G=0/1/2` loopback behavior, exact and one-byte-over `maxPktBytes` boundary splitting, output backpressure hold, CRC NONE/DATA/FULL packetizer tail/header behavior, DATA/FULL bad-CRC rejection, CRC-none tail-error marking, bad-version and bad-CRC-mode header error paths, link-drop recovery, mid-frame link-drop termination/recovery, and V2 packetizer/depacketizer loopback across CRC modes. The byte-packer wrapper now sweeps multiple compressed-keep input/output byte-width pairs: 1-to-8, 2-to-5, 3-to-6, 3-to-7, 4-to-8, 5-to-7, and 7-to-8, including a zero-keep input-beat guardrail. + - The batcher pass has started with the same standalone-first shape. `AxiStreamBatcher` now has a thin checked-in wrapper under `protocols/batcher/wrappers/` and a V2/default-8-byte cocotb regression under `tests/protocols/batcher/`. The validated leaf slice covers compacted V2 header/payload/tail bytes through the gearbox path, subframe tail metadata, multi-subframe superframes, termination by max-subframe count, idle gap, byte threshold, forced termination with terminal `EOFE`, output backpressure hold, and reset recovery after a partial superframe. `AxiStreamBatcherAxil` now has a narrow common-clock wrapper regression for register reset/readback, control propagation, `softRst`, and `blowoff`. `AxiStreamBatcherEventBuilder` remains the next planned layer, with integration-specific assertions only. - The retained RTL-regression planning files now live under `docs/plans/rtl-regression/` instead of the old `docs/_meta/rtl_regression_*` paths. - The old checked-in graph/queue artifacts have been removed from the task planning directory; regenerate them only as temporary one-off analysis if needed. - Keep the done/open frontier in this progress file and in `docs/plans/rtl-regression/handoff.md` aligned to the actual tree. @@ -169,6 +170,14 @@ - The most recent recorded CoaXPress validation after the HKP AXI-Lite consumer sweep is green for the full `tests/protocols/coaxpress` directory with `19 passed`. - The former opt-in `CoaXPressCore` overflow/FSM-error known-issue bench is now part of the normal CoaXPress core regression with a long-line workload that actually fills the RX data FIFO. - The current `test-base-2` merge adds the base-depth follow-up pass. It expands existing tests only, with no new wrappers: `FifoMux`, `FifoAsync`, `FifoSync`, `FifoCascade`, `SynchronizerFifo`, `Arbiter`, `WatchDogRst`, `DualPortRam`, `SimpleDualPortRam`, `TrueDualPortRam`, `SlvDelayFifo`, and `SlvDelayRam` now cover selected high-value edge cases from the FIFO/CDC/RAM/delay/general base-depth list. The latest refinement adds `FifoAsync` near-full turnover under concurrent read/write pressure, explicit `FifoMux` partial-pack reset no-output checking, `SynchronizerFifo` reset while FWFT data is prefetched and read-side paused, registered-output `SimpleDualPortRam` `enb` hold behavior, multi-entry `SlvDelayFifo` reset flushing, and `SlvDelayRam` reset-aligned runtime delay growth with output-history discard. The latest local validation produced `61 passed` in the focused parallel base run before a `FifoCascade` parallel GHDL analysis race, and the failed `FifoCascade` target passed on serial rerun with `3 passed`. +- The current batcher worktree adds Phase 1/2 batcher coverage: + `protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd`, + `protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd`, + `tests/protocols/batcher/batcher_test_utils.py`, and + `tests/protocols/batcher/test_AxiStreamBatcher.py` plus + `tests/protocols/batcher/test_AxiStreamBatcherAxil.py`. Latest focused + validation is `./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` + (`2 passed`), plus clean wrapper `vsg` and Python `py_compile`. - The user-requested `protocols/srp` review fixes are complete: `test_SrpV3Axi.py` now reuses the shared SRPv3 helper/model layer, `test_SrpV3Core.py` uses decorator-based cocotb test selection, the stray SRPv3 AXI-Lite debug logging is removed, and the high-value SRP coverage additions are checked in locally. - Preserve the recent `pgp4` lesson for later PGP work: when the simulation wrapper only exposes stable lock/config surfaces, write the bench around those explicit contracts instead of claiming recovered payload coverage. - Latest focused SRP validation: `./.venv/bin/python -m pytest -n 0 -q tests/protocols/srp` passed locally with `23 passed`. @@ -177,6 +186,11 @@ - Finish the documentation relocation by keeping `docs/plans/rtl-regression/README.md`, `plan.md`, `progress.md`, `handoff.md`, and `inventory.yaml` internally consistent; staging and committing remain user-controlled. - If implementation resumes on CoaXPress, the next practical work is policy-level depth above the new status surfaces: event-payload oversize/backpressure coverage above `CoaXPressRx`, optional software/firmware consumers of the bridge AXI-Lite status counters beyond the checked-in HKP classification readback sweep, and any downstream image path that needs to enforce SSI `EOFE` frame rejection. - If implementation continues in `protocols/packetizer`, the standalone V0/V2 packetizer, depacketizer, and byte-packer leaves now have expanded direct coverage, and the V2 packetizer/depacketizer path has a narrow CRC-mode loopback. The next practical step is either deeper negative/error coverage for specific packetizer behavior or moving to another protocol leaf. +- If implementation continues in `protocols/batcher`, keep the next work leaf, + wrapper-specific, or event-builder-specific: either add a small + V1/power-of-two leaf case, deepen `AxiStreamBatcherAxil` around async + crossing or adverse reset/blowoff timing, or start event-builder coverage with + small source-count integration cases. - If implementation resumes on Ethernet/RoCEv2, the next real step is enabling a mixed-language cocotb path for `EthMacCrcAxiStreamWrapperSend`, `EthMacCrcAxiStreamWrapperRecv`, `EthMacTxRoCEv2`, `EthMacRxRoCEv2`, and `RoceEngineWrapper` against the real `blue-*` dependencies. ## Blockers And Risks diff --git a/protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd b/protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd new file mode 100644 index 0000000000..295dcc6a96 --- /dev/null +++ b/protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd @@ -0,0 +1,209 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing wrapper for surf.AxiStreamBatcherAxil +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AxiStreamPkg.all; + +entity AxiStreamBatcherAxilWrapper is + generic ( + TPD_G : time := 1 ns; + VERSION_G : positive range 1 to 2 := 2; + DATA_BYTES_G : positive range 2 to 8 := 8; + MAX_NUMBER_SUB_FRAMES_G : positive := 32; + SUPER_FRAME_BYTE_THRESHOLD_G : natural := 8192; + MAX_CLK_GAP_G : natural := 256; + INPUT_PIPE_STAGES_G : natural := 0; + OUTPUT_PIPE_STAGES_G : natural := 1; + AXIL_ADDR_WIDTH_G : positive := 12); + port ( + axisClk : in sl; + axisRst : in sl; + idle : out sl; + S_AXIS_TVALID : in sl; + S_AXIS_TDATA : in slv(8*DATA_BYTES_G-1 downto 0); + S_AXIS_TKEEP : in slv(DATA_BYTES_G-1 downto 0); + S_AXIS_TLAST : in sl; + S_AXIS_TDEST : in slv(7 downto 0); + S_AXIS_TID : in slv(7 downto 0); + S_AXIS_TUSER : in slv(8*DATA_BYTES_G-1 downto 0); + S_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(8*DATA_BYTES_G-1 downto 0); + M_AXIS_TKEEP : out slv(DATA_BYTES_G-1 downto 0); + M_AXIS_TLAST : out sl; + M_AXIS_TDEST : out slv(7 downto 0); + M_AXIS_TID : out slv(7 downto 0); + M_AXIS_TUSER : out slv(8*DATA_BYTES_G-1 downto 0); + M_AXIS_TREADY : in sl; + S_AXI_AWADDR : in slv(AXIL_ADDR_WIDTH_G-1 downto 0); + S_AXI_AWPROT : in slv(2 downto 0); + S_AXI_AWVALID : in sl; + S_AXI_AWREADY : out sl; + S_AXI_WDATA : in slv(31 downto 0); + S_AXI_WSTRB : in slv(3 downto 0); + S_AXI_WVALID : in sl; + S_AXI_WREADY : out sl; + S_AXI_BRESP : out slv(1 downto 0); + S_AXI_BVALID : out sl; + S_AXI_BREADY : in sl; + S_AXI_ARADDR : in slv(AXIL_ADDR_WIDTH_G-1 downto 0); + S_AXI_ARPROT : in slv(2 downto 0); + S_AXI_ARVALID : in sl; + S_AXI_ARREADY : out sl; + S_AXI_RDATA : out slv(31 downto 0); + S_AXI_RRESP : out slv(1 downto 0); + S_AXI_RVALID : out sl; + S_AXI_RREADY : in sl); +end entity AxiStreamBatcherAxilWrapper; + +architecture rtl of AxiStreamBatcherAxilWrapper is + + constant AXIS_CONFIG_C : AxiStreamConfigType := ( + TSTRB_EN_C => false, + TDATA_BYTES_C => DATA_BYTES_G, + TDEST_BITS_C => 8, + TID_BITS_C => 0, + TKEEP_MODE_C => TKEEP_NORMAL_C, + TUSER_BITS_C => 8, + TUSER_MODE_C => TUSER_FIRST_LAST_C); + + signal axilRstN : sl; + signal axilClk : sl; + signal axilRst : sl; + signal axilReadMaster : AxiLiteReadMasterType := AXI_LITE_READ_MASTER_INIT_C; + signal axilReadSlave : AxiLiteReadSlaveType := AXI_LITE_READ_SLAVE_INIT_C; + signal axilWriteMaster : AxiLiteWriteMasterType := AXI_LITE_WRITE_MASTER_INIT_C; + signal axilWriteSlave : AxiLiteWriteSlaveType := AXI_LITE_WRITE_SLAVE_INIT_C; + signal sAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal sAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + signal mAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal mAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + +begin + + axilRstN <= not axisRst; + + ------------------------ + -- AXI-Lite bus shim -- + ------------------------ + U_AXIL : entity surf.SlaveAxiLiteIpIntegrator + generic map ( + HAS_PROT => 1, + HAS_WSTRB => 1, + ADDR_WIDTH => AXIL_ADDR_WIDTH_G) + port map ( + S_AXI_ACLK => axisClk, -- [in] + S_AXI_ARESETN => axilRstN, -- [in] + S_AXI_AWADDR => S_AXI_AWADDR, -- [in] + S_AXI_AWPROT => S_AXI_AWPROT, -- [in] + S_AXI_AWVALID => S_AXI_AWVALID, -- [in] + S_AXI_AWREADY => S_AXI_AWREADY, -- [out] + S_AXI_WDATA => S_AXI_WDATA, -- [in] + S_AXI_WSTRB => S_AXI_WSTRB, -- [in] + S_AXI_WVALID => S_AXI_WVALID, -- [in] + S_AXI_WREADY => S_AXI_WREADY, -- [out] + S_AXI_BRESP => S_AXI_BRESP, -- [out] + S_AXI_BVALID => S_AXI_BVALID, -- [out] + S_AXI_BREADY => S_AXI_BREADY, -- [in] + S_AXI_ARADDR => S_AXI_ARADDR, -- [in] + S_AXI_ARPROT => S_AXI_ARPROT, -- [in] + S_AXI_ARVALID => S_AXI_ARVALID, -- [in] + S_AXI_ARREADY => S_AXI_ARREADY, -- [out] + S_AXI_RDATA => S_AXI_RDATA, -- [out] + S_AXI_RRESP => S_AXI_RRESP, -- [out] + S_AXI_RVALID => S_AXI_RVALID, -- [out] + S_AXI_RREADY => S_AXI_RREADY, -- [in] + axilClk => axilClk, -- [out] + axilRst => axilRst, -- [out] + axilReadMaster => axilReadMaster, -- [out] + axilReadSlave => axilReadSlave, -- [in] + axilWriteMaster => axilWriteMaster, -- [out] + axilWriteSlave => axilWriteSlave); -- [in] + + ------------------------ + -- AXI Stream shims -- + ------------------------ + comb : process (M_AXIS_TREADY, S_AXIS_TDATA, S_AXIS_TDEST, S_AXIS_TID, + S_AXIS_TKEEP, S_AXIS_TLAST, S_AXIS_TUSER, S_AXIS_TVALID, + mAxisMaster, sAxisSlave) is + variable vS : AxiStreamMasterType; + variable vM : AxiStreamSlaveType; + begin + vS := AXI_STREAM_MASTER_INIT_C; + vS.tValid := S_AXIS_TVALID; + vS.tData := (others => '0'); + vS.tData(8*DATA_BYTES_G-1 downto 0) := S_AXIS_TDATA; + vS.tStrb := (others => '0'); + vS.tStrb(DATA_BYTES_G-1 downto 0) := S_AXIS_TKEEP; + vS.tKeep := (others => '0'); + vS.tKeep(DATA_BYTES_G-1 downto 0) := S_AXIS_TKEEP; + vS.tLast := S_AXIS_TLAST; + vS.tDest := (others => '0'); + vS.tDest(7 downto 0) := S_AXIS_TDEST; + vS.tId := (others => '0'); + vS.tId(7 downto 0) := S_AXIS_TID; + vS.tUser := (others => '0'); + vS.tUser(8*DATA_BYTES_G-1 downto 0) := S_AXIS_TUSER; + + vM := AXI_STREAM_SLAVE_INIT_C; + vM.tReady := M_AXIS_TREADY; + + sAxisMaster <= vS; + mAxisSlave <= vM; + + S_AXIS_TREADY <= sAxisSlave.tReady; + M_AXIS_TVALID <= mAxisMaster.tValid; + M_AXIS_TDATA <= mAxisMaster.tData(8*DATA_BYTES_G-1 downto 0); + M_AXIS_TKEEP <= mAxisMaster.tKeep(DATA_BYTES_G-1 downto 0); + M_AXIS_TLAST <= mAxisMaster.tLast; + M_AXIS_TDEST <= mAxisMaster.tDest(7 downto 0); + M_AXIS_TID <= mAxisMaster.tId(7 downto 0); + M_AXIS_TUSER <= mAxisMaster.tUser(8*DATA_BYTES_G-1 downto 0); + end process comb; + + --------------------- + -- DUT instancing -- + --------------------- + U_DUT : entity surf.AxiStreamBatcherAxil + generic map ( + TPD_G => TPD_G, + VERSION_G => VERSION_G, + COMMON_CLOCK_G => true, + MAX_NUMBER_SUB_FRAMES_G => MAX_NUMBER_SUB_FRAMES_G, + SUPER_FRAME_BYTE_THRESHOLD_G => SUPER_FRAME_BYTE_THRESHOLD_G, + MAX_CLK_GAP_G => MAX_CLK_GAP_G, + AXIS_CONFIG_G => AXIS_CONFIG_C, + INPUT_PIPE_STAGES_G => INPUT_PIPE_STAGES_G, + OUTPUT_PIPE_STAGES_G => OUTPUT_PIPE_STAGES_G) + port map ( + axisClk => axisClk, -- [in] + axisRst => axisRst, -- [in] + idle => idle, -- [out] + sAxisMaster => sAxisMaster, -- [in] + sAxisSlave => sAxisSlave, -- [out] + mAxisMaster => mAxisMaster, -- [out] + mAxisSlave => mAxisSlave, -- [in] + axilClk => axilClk, -- [in] + axilRst => axilRst, -- [in] + axilReadMaster => axilReadMaster, -- [in] + axilReadSlave => axilReadSlave, -- [out] + axilWriteMaster => axilWriteMaster, -- [in] + axilWriteSlave => axilWriteSlave); -- [out] + +end architecture rtl; diff --git a/protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd b/protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd new file mode 100644 index 0000000000..f625290874 --- /dev/null +++ b/protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd @@ -0,0 +1,143 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing wrapper for surf.AxiStreamBatcher +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiStreamPkg.all; + +entity AxiStreamBatcherWrapper is + generic ( + TPD_G : time := 1 ns; + VERSION_G : positive range 1 to 2 := 2; + DATA_BYTES_G : positive range 2 to 8 := 8; + MAX_NUMBER_SUB_FRAMES_G : positive := 32; + SUPER_FRAME_BYTE_THRESHOLD_G : natural := 8192; + MAX_CLK_GAP_G : natural := 256; + INPUT_PIPE_STAGES_G : natural := 0; + OUTPUT_PIPE_STAGES_G : natural := 1); + port ( + axisClk : in sl; + axisRst : in sl; + forceTerm : in sl; + superFrameByteThreshold : in slv(31 downto 0); + maxSubFrames : in slv(15 downto 0); + maxClkGap : in slv(31 downto 0); + idle : out sl; + S_AXIS_TVALID : in sl; + S_AXIS_TDATA : in slv(8*DATA_BYTES_G-1 downto 0); + S_AXIS_TKEEP : in slv(DATA_BYTES_G-1 downto 0); + S_AXIS_TLAST : in sl; + S_AXIS_TDEST : in slv(7 downto 0); + S_AXIS_TID : in slv(7 downto 0); + S_AXIS_TUSER : in slv(8*DATA_BYTES_G-1 downto 0); + S_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(8*DATA_BYTES_G-1 downto 0); + M_AXIS_TKEEP : out slv(DATA_BYTES_G-1 downto 0); + M_AXIS_TLAST : out sl; + M_AXIS_TDEST : out slv(7 downto 0); + M_AXIS_TID : out slv(7 downto 0); + M_AXIS_TUSER : out slv(8*DATA_BYTES_G-1 downto 0); + M_AXIS_TREADY : in sl); +end entity AxiStreamBatcherWrapper; + +architecture rtl of AxiStreamBatcherWrapper is + + constant AXIS_CONFIG_C : AxiStreamConfigType := ( + TSTRB_EN_C => false, + TDATA_BYTES_C => DATA_BYTES_G, + TDEST_BITS_C => 8, + TID_BITS_C => 0, + TKEEP_MODE_C => TKEEP_NORMAL_C, + TUSER_BITS_C => 8, + TUSER_MODE_C => TUSER_FIRST_LAST_C); + + signal sAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal sAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + signal mAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal mAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + +begin + + --------------- + -- Bus shims -- + --------------- + comb : process (M_AXIS_TREADY, S_AXIS_TDATA, S_AXIS_TDEST, S_AXIS_TID, + S_AXIS_TKEEP, S_AXIS_TLAST, S_AXIS_TUSER, S_AXIS_TVALID, + mAxisMaster, sAxisSlave) is + variable vS : AxiStreamMasterType; + variable vM : AxiStreamSlaveType; + begin + vS := AXI_STREAM_MASTER_INIT_C; + vS.tValid := S_AXIS_TVALID; + vS.tData := (others => '0'); + vS.tData(8*DATA_BYTES_G-1 downto 0) := S_AXIS_TDATA; + vS.tStrb := (others => '0'); + vS.tStrb(DATA_BYTES_G-1 downto 0) := S_AXIS_TKEEP; + vS.tKeep := (others => '0'); + vS.tKeep(DATA_BYTES_G-1 downto 0) := S_AXIS_TKEEP; + vS.tLast := S_AXIS_TLAST; + vS.tDest := (others => '0'); + vS.tDest(7 downto 0) := S_AXIS_TDEST; + vS.tId := (others => '0'); + vS.tId(7 downto 0) := S_AXIS_TID; + vS.tUser := (others => '0'); + vS.tUser(8*DATA_BYTES_G-1 downto 0) := S_AXIS_TUSER; + + vM := AXI_STREAM_SLAVE_INIT_C; + vM.tReady := M_AXIS_TREADY; + + sAxisMaster <= vS; + mAxisSlave <= vM; + + S_AXIS_TREADY <= sAxisSlave.tReady; + M_AXIS_TVALID <= mAxisMaster.tValid; + M_AXIS_TDATA <= mAxisMaster.tData(8*DATA_BYTES_G-1 downto 0); + M_AXIS_TKEEP <= mAxisMaster.tKeep(DATA_BYTES_G-1 downto 0); + M_AXIS_TLAST <= mAxisMaster.tLast; + M_AXIS_TDEST <= mAxisMaster.tDest(7 downto 0); + M_AXIS_TID <= mAxisMaster.tId(7 downto 0); + M_AXIS_TUSER <= mAxisMaster.tUser(8*DATA_BYTES_G-1 downto 0); + end process comb; + + --------------------- + -- DUT instancing -- + --------------------- + U_DUT : entity surf.AxiStreamBatcher + generic map ( + TPD_G => TPD_G, + VERSION_G => VERSION_G, + MAX_NUMBER_SUB_FRAMES_G => MAX_NUMBER_SUB_FRAMES_G, + SUPER_FRAME_BYTE_THRESHOLD_G => SUPER_FRAME_BYTE_THRESHOLD_G, + MAX_CLK_GAP_G => MAX_CLK_GAP_G, + AXIS_CONFIG_G => AXIS_CONFIG_C, + INPUT_PIPE_STAGES_G => INPUT_PIPE_STAGES_G, + OUTPUT_PIPE_STAGES_G => OUTPUT_PIPE_STAGES_G) + port map ( + axisClk => axisClk, -- [in] + axisRst => axisRst, -- [in] + forceTerm => forceTerm, -- [in] + superFrameByteThreshold => superFrameByteThreshold, -- [in] + maxSubFrames => maxSubFrames, -- [in] + maxClkGap => maxClkGap, -- [in] + idle => idle, -- [out] + sAxisMaster => sAxisMaster, -- [in] + sAxisSlave => sAxisSlave, -- [out] + mAxisMaster => mAxisMaster, -- [out] + mAxisSlave => mAxisSlave); -- [in] + +end architecture rtl; diff --git a/tests/protocols/batcher/__init__.py b/tests/protocols/batcher/__init__.py new file mode 100644 index 0000000000..b0085f1a17 --- /dev/null +++ b/tests/protocols/batcher/__init__.py @@ -0,0 +1,9 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## diff --git a/tests/protocols/batcher/batcher_test_utils.py b/tests/protocols/batcher/batcher_test_utils.py new file mode 100644 index 0000000000..c8e065026b --- /dev/null +++ b/tests/protocols/batcher/batcher_test_utils.py @@ -0,0 +1,252 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## + +from __future__ import annotations + +from dataclasses import dataclass + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Timer + +from tests.axi.utils import wait_sampled_ready + + +@dataclass +class AxisBeat: + data: int + keep: int = 0xFF + last: int = 0 + dest: int = 0 + tid: int = 0 + user: int = 0 + + +class FlatAxisEndpoint: + def __init__(self, dut, *, prefix: str): + self.dut = dut + self.prefix = prefix + + def _sig(self, suffix: str): + return getattr(self.dut, f"{self.prefix}_{suffix}") + + def set_idle(self) -> None: + for suffix, value in ( + ("TVALID", 0), + ("TDATA", 0), + ("TKEEP", 0), + ("TLAST", 0), + ("TDEST", 0), + ("TID", 0), + ("TUSER", 0), + ): + if hasattr(self.dut, f"{self.prefix}_{suffix}"): + self._sig(suffix).value = value + + def drive(self, beat: AxisBeat) -> None: + self._sig("TVALID").value = 1 + self._sig("TDATA").value = beat.data + self._sig("TKEEP").value = beat.keep + self._sig("TLAST").value = beat.last + self._sig("TDEST").value = beat.dest + self._sig("TID").value = beat.tid + self._sig("TUSER").value = beat.user + + def snapshot(self) -> AxisBeat: + return AxisBeat( + data=int(self._sig("TDATA").value), + keep=int(self._sig("TKEEP").value), + last=int(self._sig("TLAST").value), + dest=int(self._sig("TDEST").value), + tid=int(self._sig("TID").value), + user=int(self._sig("TUSER").value), + ) + + async def send(self, beat: AxisBeat, *, clk) -> None: + self.drive(beat) + await wait_sampled_ready(self._sig("TREADY"), clk=clk) + self.set_idle() + + async def wait_valid(self, *, clk, timeout_cycles: int = 256) -> AxisBeat: + await Timer(1, unit="ns") + if int(self._sig("TVALID").value) == 1: + return self.snapshot() + for _ in range(timeout_cycles): + await FallingEdge(clk) + await Timer(1, unit="ns") + if int(self._sig("TVALID").value) == 1: + return self.snapshot() + await RisingEdge(clk) + await Timer(1, unit="ns") + if int(self._sig("TVALID").value) == 1: + return self.snapshot() + raise AssertionError(f"Timed out waiting for {self.prefix} valid") + + async def recv(self, *, clk, keep_ready: bool = False) -> AxisBeat: + self._sig("TREADY").value = 1 + beat = await self.wait_valid(clk=clk) + await RisingEdge(clk) + await Timer(1, unit="ns") + if not keep_ready: + self._sig("TREADY").value = 0 + return beat + + +def start_batcher_clock(dut, *, period_ns: float = 5.0) -> None: + cocotb.start_soon(Clock(dut.axisClk, period_ns, unit="ns").start()) + + +async def cycle(clk, count: int = 1) -> None: + for _ in range(count): + await RisingEdge(clk) + await Timer(1, unit="ns") + + +async def reset_batcher_dut(dut, *, cycles: int = 4) -> None: + dut.axisRst.setimmediatevalue(1) + await cycle(dut.axisClk, cycles) + dut.axisRst.value = 0 + await cycle(dut.axisClk, 2) + + +def word_from_bytes(data: bytes) -> int: + return int.from_bytes(data.ljust(8, b"\x00"), "little") + + +def bytes_from_word(word: int, *, keep: int = 0xFF) -> bytes: + raw = word.to_bytes(8, "little") + return bytes(raw[index] for index in range(8) if keep & (1 << index)) + + +def keep_count(keep: int) -> int: + return sum(1 for lane in range(8) if keep & (1 << lane)) + + +def user_from_lanes(values: list[int]) -> int: + user = 0 + for lane, value in enumerate(values): + user |= (value & 0xFF) << (8 * lane) + return user + + +def payload_to_beats(payload: bytes, *, dest: int, first_user: int, last_user: int) -> list[AxisBeat]: + beats = [] + for offset in range(0, len(payload), 8): + chunk = payload[offset : offset + 8] + is_first = offset == 0 + is_last = offset + 8 >= len(payload) + user_values = [0] * len(chunk) + if is_first: + user_values[0] = first_user + if is_last: + user_values[-1] = last_user + beats.append( + AxisBeat( + data=word_from_bytes(chunk), + keep=(1 << len(chunk)) - 1, + last=int(is_last), + dest=dest, + user=user_from_lanes(user_values), + ) + ) + return beats + + +def batcher_v2_header(*, seq: int = 0, data_bytes: int = 8) -> bytes: + width = (data_bytes // 2).bit_length() - 1 + return bytes([0x2 | ((width & 0xF) << 4), seq & 0xFF]) + + +def batcher_subframe_tail(*, byte_count: int, dest: int, first_user: int, last_user: int) -> bytes: + return ( + byte_count.to_bytes(4, "little") + + bytes([dest & 0xFF, first_user & 0xFF, last_user & 0xFF]) + ) + + +def expected_batched_bytes(frames: list[tuple[bytes, int, int, int]], *, seq: int = 0) -> bytes: + stream = bytearray(batcher_v2_header(seq=seq)) + for payload, dest, first_user, last_user in frames: + stream.extend(payload) + stream.extend( + batcher_subframe_tail( + byte_count=len(payload), + dest=dest, + first_user=first_user, + last_user=last_user, + ) + ) + return bytes(stream) + + +async def send_frame(endpoint: FlatAxisEndpoint, beats: list[AxisBeat], *, clk) -> None: + for beat in beats: + await endpoint.send(beat, clk=clk) + + +async def recv_until_last(endpoint: FlatAxisEndpoint, *, clk, max_beats: int = 32) -> list[AxisBeat]: + beats = [] + for _ in range(max_beats): + beat = await endpoint.recv(clk=clk, keep_ready=True) + beats.append(beat) + if beat.last: + endpoint._sig("TREADY").value = 0 + return beats + endpoint._sig("TREADY").value = 0 + raise AssertionError("Timed out waiting for terminal batcher beat") + + +async def recv_beats(endpoint: FlatAxisEndpoint, *, clk, count: int) -> list[AxisBeat]: + beats = [] + for _ in range(count): + beats.append(await endpoint.recv(clk=clk, keep_ready=True)) + endpoint._sig("TREADY").value = 0 + return beats + + +async def expect_no_valid(endpoint: FlatAxisEndpoint, *, clk, cycles: int) -> None: + endpoint._sig("TREADY").value = 1 + for _ in range(cycles): + await RisingEdge(clk) + await Timer(1, unit="ns") + assert int(endpoint._sig("TVALID").value) == 0 + endpoint._sig("TREADY").value = 0 + + +async def recv_until_last_with_backpressure( + endpoint: FlatAxisEndpoint, + *, + clk, + hold_cycles: int = 2, + max_beats: int = 32, +) -> list[AxisBeat]: + beats = [] + endpoint._sig("TREADY").value = 0 + for _ in range(max_beats): + beat = await endpoint.wait_valid(clk=clk) + for _ in range(hold_cycles): + await RisingEdge(clk) + await Timer(1, unit="ns") + assert endpoint.snapshot() == beat + endpoint._sig("TREADY").value = 1 + await RisingEdge(clk) + await Timer(1, unit="ns") + endpoint._sig("TREADY").value = 0 + beats.append(beat) + if beat.last: + return beats + raise AssertionError("Timed out waiting for terminal batcher beat") + + +def beats_to_bytes(beats: list[AxisBeat]) -> bytes: + payload = bytearray() + for beat in beats: + payload.extend(bytes_from_word(beat.data, keep=beat.keep)) + return bytes(payload) diff --git a/tests/protocols/batcher/test_AxiStreamBatcher.py b/tests/protocols/batcher/test_AxiStreamBatcher.py new file mode 100644 index 0000000000..a5f6fbcbf3 --- /dev/null +++ b/tests/protocols/batcher/test_AxiStreamBatcher.py @@ -0,0 +1,318 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Use a standalone `AxiStreamBatcher` wrapper in V2 mode with an +# 8-byte AXI Stream width so the regression checks the compacted output path +# implemented through `AxiStreamGearbox`. +# - Stimulus: Drive one or more input subframes with varied payload lengths, +# `TKEEP`, `TDEST`, first-byte `TUSER`, and last-byte `TUSER`, then terminate +# superframes by subframe count, idle gap, byte threshold, and sink +# backpressure. +# - Checks: The emitted byte stream must match the V2 superframe header, +# payload bytes, and subframe tail metadata exactly, with `TLAST` only on the +# terminal superframe beat. +# - Timing: Source and sink use ready/valid handshakes, including a held-not- +# ready sink case that asserts the DUT holds every output beat stable. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.batcher.batcher_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + beats_to_bytes, + cycle, + expected_batched_bytes, + keep_count, + payload_to_beats, + recv_beats, + recv_until_last, + recv_until_last_with_backpressure, + reset_batcher_dut, + send_frame, + start_batcher_clock, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_batcher_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.forceTerm.setimmediatevalue(0) + dut.superFrameByteThreshold.setimmediatevalue(0) + dut.maxSubFrames.setimmediatevalue(1) + dut.maxClkGap.setimmediatevalue(256) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_batcher_dut(self.dut) + + +@cocotb.test() +async def single_subframe_terminates_on_count_test(dut): + tb = TB(dut) + await tb.reset() + + # A five-byte frame exercises the V2 gearbox because the two-byte + # superframe header, payload, and seven-byte tail do not align to an + # eight-byte output boundary. + payload = bytes(range(0x10, 0x15)) + frame = (payload, 0x3, 0x22, 0x41) + input_beats = payload_to_beats( + payload, + dest=frame[1], + first_user=frame[2], + last_user=frame[3], + ) + + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frame(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes([frame]) + assert rx_beats[-1].last == 1 + await cycle(dut.axisClk, 2) + assert int(dut.idle.value) == 1 + + +@cocotb.test() +async def two_subframes_share_one_superframe_test(dut): + tb = TB(dut) + await tb.reset() + dut.maxSubFrames.value = 2 + + # Two subframes should share one superframe when the runtime subframe limit + # is raised. The second frame is deliberately not word-aligned so the tail + # metadata lands in a compacted output beat. + first = (bytes(range(0x20, 0x28)), 0x4, 0x11, 0x91) + second = (bytes(range(0x40, 0x45)), 0x7, 0x33, 0xA5) + + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + for payload, dest, first_user, last_user in (first, second): + await send_frame( + tb.source, + payload_to_beats( + payload, + dest=dest, + first_user=first_user, + last_user=last_user, + ), + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes([first, second]) + assert [beat.last for beat in rx_beats[:-1]] == [0] * (len(rx_beats) - 1) + assert rx_beats[-1].last == 1 + + +@cocotb.test() +async def idle_gap_terminates_pending_tail_test(dut): + tb = TB(dut) + await tb.reset() + dut.maxSubFrames.value = 8 + dut.maxClkGap.value = 3 + + # With no second subframe arriving, the small max-clock-gap setting must + # close the superframe after the tail has been accepted into the batcher. + payload = bytes(range(0x60, 0x6B)) + frame = (payload, 0x2, 0x44, 0xB1) + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frame( + tb.source, + payload_to_beats( + payload, + dest=frame[1], + first_user=frame[2], + last_user=frame[3], + ), + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes([frame]) + assert rx_beats[-1].last == 1 + + +@cocotb.test() +async def byte_threshold_terminates_superframe_test(dut): + tb = TB(dut) + await tb.reset() + dut.maxSubFrames.value = 8 + dut.maxClkGap.value = 0 + dut.superFrameByteThreshold.value = 24 + + # The threshold check is asserted through externally visible termination + # behavior only. The RTL floors the register value internally to its word + # accounting granularity, so the test avoids overfitting that private count. + first = (bytes(range(0x70, 0x78)), 0x1, 0x55, 0xC1) + + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frame( + tb.source, + payload_to_beats( + first[0], + dest=first[1], + first_user=first[2], + last_user=first[3], + ), + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes([first]) + assert rx_beats[-1].last == 1 + + +@cocotb.test() +async def force_term_marks_terminal_eofe_test(dut): + tb = TB(dut) + await tb.reset() + dut.maxSubFrames.value = 8 + dut.maxClkGap.value = 256 + + # Send one complete subframe, then force the enclosing superframe closed + # before the clock-gap timer or subframe count can terminate it naturally. + payload = bytes(range(0x80, 0x85)) + frame = (payload, 0x6, 0x66, 0xE2) + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frame( + tb.source, + payload_to_beats( + payload, + dest=frame[1], + first_user=frame[2], + last_user=frame[3], + ), + clk=dut.axisClk, + ) + + # `forceTerm` is sampled into the RTL and then applied from the non-header + # state, so hold it for a few clocks to avoid making the test sensitive to + # the exact state transition cycle. + await cycle(dut.axisClk, 4) + dut.forceTerm.value = 1 + await cycle(dut.axisClk, 4) + dut.forceTerm.value = 0 + + rx_beats = await with_timeout(rx_task, 4, "us") + + assert beats_to_bytes(rx_beats).startswith(expected_batched_bytes([frame])) + assert rx_beats[-1].last == 1 + terminal_lane = keep_count(rx_beats[-1].keep) - 1 + assert terminal_lane >= 0 + assert (rx_beats[-1].user >> (8 * terminal_lane)) & 0x1 == 1 + + +@cocotb.test() +async def reset_recovers_after_partial_superframe_test(dut): + tb = TB(dut) + await tb.reset() + dut.maxSubFrames.value = 8 + dut.maxClkGap.value = 256 + + # Present a non-terminal input beat and let the DUT emit at least one + # partial output beat. The following reset should discard that incomplete + # superframe state instead of contaminating the next accepted frame. + partial = AxisBeat( + data=int.from_bytes(bytes(range(0xA0, 0xA8)), "little"), + keep=0xFF, + last=0, + dest=0x2, + user=0x19, + ) + await tb.source.send(partial, clk=dut.axisClk) + partial_rx = await with_timeout(recv_beats(tb.sink, clk=dut.axisClk, count=1), 2, "us") + assert partial_rx[0].last == 0 + + await reset_batcher_dut(dut) + assert int(dut.idle.value) == 1 + dut.maxSubFrames.value = 1 + await cycle(dut.axisClk, 1) + + payload = bytes(range(0xB0, 0xB5)) + frame = (payload, 0x1, 0x22, 0xC3) + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frame( + tb.source, + payload_to_beats( + payload, + dest=frame[1], + first_user=frame[2], + last_user=frame[3], + ), + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes([frame]) + assert rx_beats[-1].last == 1 + + +@cocotb.test() +async def output_backpressure_holds_each_beat_stable_test(dut): + tb = TB(dut) + await tb.reset() + + payload = bytes(range(0x90, 0x9A)) + frame = (payload, 0x5, 0x77, 0xD4) + rx_task = cocotb.start_soon( + recv_until_last_with_backpressure(tb.sink, clk=dut.axisClk, hold_cycles=3) + ) + await send_frame( + tb.source, + payload_to_beats( + payload, + dest=frame[1], + first_user=frame[2], + last_user=frame[3], + ), + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes([frame]) + assert rx_beats[-1].last == 1 + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param( + { + "VERSION_G": 2, + "DATA_BYTES_G": 8, + "INPUT_PIPE_STAGES_G": 0, + "OUTPUT_PIPE_STAGES_G": 1, + }, + id="v2_8byte", + ), + ], +) +def test_AxiStreamBatcher(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreambatcherwrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": [ + "protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd", + ], + }, + ) diff --git a/tests/protocols/batcher/test_AxiStreamBatcherAxil.py b/tests/protocols/batcher/test_AxiStreamBatcherAxil.py new file mode 100644 index 0000000000..3c1151af38 --- /dev/null +++ b/tests/protocols/batcher/test_AxiStreamBatcherAxil.py @@ -0,0 +1,279 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Use the `AxiStreamBatcherAxil` wrapper in V2 mode with common AXI-Lite +# and stream clocks, matching the stable first control-surface target from the +# batcher regression plan. +# - Stimulus: Program the runtime threshold, max-subframe, max-clock-gap, +# `softRst`, and `blowoff` registers through a cocotb AXI-Lite master while +# driving flat AXI Stream subframes through the wrapped batcher. +# - Checks: Register reset values and readback must match the RTL/PyRogue map, +# and control writes must change stream-side termination or drop/reset behavior +# without re-proving every batcher payload byte beyond the leaf helper model. +# - Timing: AXI-Lite transactions and stream ready/valid handshakes share one +# clock, and the blowoff/reset checks include no-output windows after traffic +# is accepted. + +import cocotb +import pytest +from cocotb.triggers import with_timeout +from cocotbext.axi import AxiLiteBus, AxiLiteMaster + +from tests.axi.utils import axil_read_u32, axil_write_u32 +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.batcher.batcher_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + beats_to_bytes, + cycle, + expect_no_valid, + expected_batched_bytes, + payload_to_beats, + recv_beats, + recv_until_last, + reset_batcher_dut, + send_frame, + start_batcher_clock, +) + +SUPER_FRAME_BYTE_THRESHOLD_ADDR = 0x00 +MAX_SUB_FRAMES_ADDR = 0x04 +MAX_CLK_GAP_ADDR = 0x08 +STATUS_ADDR = 0x0C +BLOWOFF_ADDR = 0xF8 +SOFT_RST_ADDR = 0xFC + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + self.axil = AxiLiteMaster(AxiLiteBus.from_prefix(dut, "S_AXI"), dut.axisClk, dut.axisRst) + + start_batcher_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_batcher_dut(self.dut) + + async def read(self, address: int) -> int: + return await with_timeout(axil_read_u32(self.axil, address), 2, "us") + + async def write(self, address: int, value: int) -> None: + await with_timeout(axil_write_u32(self.axil, address, value), 2, "us") + + +@cocotb.test() +async def register_reset_and_readback_test(dut): + tb = TB(dut) + await tb.reset() + + # The reset values mirror `AxiStreamBatcherAxil` generics and the PyRogue + # register map. Status bit 0 is idle and bits 27:24 report VERSION_G. + assert await tb.read(SUPER_FRAME_BYTE_THRESHOLD_ADDR) == 8192 + assert await tb.read(MAX_SUB_FRAMES_ADDR) == 32 + assert await tb.read(MAX_CLK_GAP_ADDR) == 256 + assert await tb.read(STATUS_ADDR) == 0x02000001 + + # Write/readback checks keep the control register map pinned before the + # stream-side tests rely on these fields to steer termination behavior. + await tb.write(SUPER_FRAME_BYTE_THRESHOLD_ADDR, 24) + await tb.write(MAX_SUB_FRAMES_ADDR, 2) + await tb.write(MAX_CLK_GAP_ADDR, 5) + assert await tb.read(SUPER_FRAME_BYTE_THRESHOLD_ADDR) == 24 + assert await tb.read(MAX_SUB_FRAMES_ADDR) == 2 + assert await tb.read(MAX_CLK_GAP_ADDR) == 5 + + +@cocotb.test() +async def max_subframe_register_controls_termination_test(dut): + tb = TB(dut) + await tb.reset() + + # A register write to MaxSubFrames should be enough to make the wrapper + # combine two leaf subframes into one superframe. + await tb.write(SUPER_FRAME_BYTE_THRESHOLD_ADDR, 0) + await tb.write(MAX_SUB_FRAMES_ADDR, 2) + await tb.write(MAX_CLK_GAP_ADDR, 256) + + first = (bytes(range(0x10, 0x18)), 0x1, 0x21, 0x81) + second = (bytes(range(0x20, 0x25)), 0x2, 0x31, 0x91) + + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + for payload, dest, first_user, last_user in (first, second): + await send_frame( + tb.source, + payload_to_beats( + payload, + dest=dest, + first_user=first_user, + last_user=last_user, + ), + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes([first, second]) + assert rx_beats[-1].last == 1 + + +@cocotb.test() +async def threshold_and_gap_registers_control_termination_test(dut): + tb = TB(dut) + await tb.reset() + + # Use the AXI-Lite register path to exercise the two other termination + # families without duplicating all of the leaf byte grammar assertions. + await tb.write(MAX_SUB_FRAMES_ADDR, 8) + await tb.write(MAX_CLK_GAP_ADDR, 0) + await tb.write(SUPER_FRAME_BYTE_THRESHOLD_ADDR, 24) + + threshold_frame = (bytes(range(0x40, 0x48)), 0x3, 0x41, 0xA1) + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frame( + tb.source, + payload_to_beats( + threshold_frame[0], + dest=threshold_frame[1], + first_user=threshold_frame[2], + last_user=threshold_frame[3], + ), + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + assert beats_to_bytes(rx_beats) == expected_batched_bytes([threshold_frame]) + + await tb.write(SUPER_FRAME_BYTE_THRESHOLD_ADDR, 0) + await tb.write(MAX_CLK_GAP_ADDR, 3) + + gap_frame = (bytes(range(0x50, 0x55)), 0x4, 0x51, 0xB1) + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frame( + tb.source, + payload_to_beats( + gap_frame[0], + dest=gap_frame[1], + first_user=gap_frame[2], + last_user=gap_frame[3], + ), + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + assert beats_to_bytes(rx_beats) == expected_batched_bytes([gap_frame], seq=1) + + +@cocotb.test() +async def soft_reset_discards_pending_superframe_test(dut): + tb = TB(dut) + await tb.reset() + await tb.write(MAX_SUB_FRAMES_ADDR, 8) + await tb.write(MAX_CLK_GAP_ADDR, 256) + + # Start, but do not finish, a subframe. The soft reset register should + # return the stream path to idle before the next valid frame is accepted. + partial = AxisBeat( + data=int.from_bytes(bytes(range(0x60, 0x68)), "little"), + keep=0xFF, + last=0, + dest=0x5, + user=0x12, + ) + await tb.source.send(partial, clk=dut.axisClk) + partial_rx = await with_timeout(recv_beats(tb.sink, clk=dut.axisClk, count=1), 2, "us") + assert partial_rx[0].last == 0 + + await tb.write(SOFT_RST_ADDR, 1) + await cycle(dut.axisClk, 4) + assert int(dut.idle.value) == 1 + + await tb.write(MAX_SUB_FRAMES_ADDR, 1) + recovery = (bytes(range(0x70, 0x75)), 0x6, 0x61, 0xC1) + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frame( + tb.source, + payload_to_beats( + recovery[0], + dest=recovery[1], + first_user=recovery[2], + last_user=recovery[3], + ), + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + assert beats_to_bytes(rx_beats) == expected_batched_bytes([recovery]) + + +@cocotb.test() +async def blowoff_drops_accepted_input_test(dut): + tb = TB(dut) + await tb.reset() + await tb.write(MAX_SUB_FRAMES_ADDR, 1) + await tb.write(BLOWOFF_ADDR, 1) + + # Blowoff should keep accepting inbound stream beats while resetting the + # batcher path, so accepted traffic must not create a malformed output frame. + dropped = bytes(range(0x80, 0x85)) + await send_frame( + tb.source, + payload_to_beats(dropped, dest=0x7, first_user=0x71, last_user=0xD1), + clk=dut.axisClk, + ) + await expect_no_valid(tb.sink, clk=dut.axisClk, cycles=12) + + await tb.write(BLOWOFF_ADDR, 0) + await cycle(dut.axisClk, 4) + + recovery = (bytes(range(0x90, 0x95)), 0x1, 0x81, 0xE1) + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frame( + tb.source, + payload_to_beats( + recovery[0], + dest=recovery[1], + first_user=recovery[2], + last_user=recovery[3], + ), + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + assert beats_to_bytes(rx_beats) == expected_batched_bytes([recovery]) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param( + { + "VERSION_G": 2, + "DATA_BYTES_G": 8, + "INPUT_PIPE_STAGES_G": 0, + "OUTPUT_PIPE_STAGES_G": 1, + }, + id="v2_8byte_common_clock", + ), + ], +) +def test_AxiStreamBatcherAxil(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreambatcheraxilwrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": [ + "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", + "protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd", + ], + }, + ) From 8dd9b6cb2a906c0ae2487b366337fc2fad377c35 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Thu, 21 May 2026 15:54:07 -0700 Subject: [PATCH 3/4] More batcher test implementation. --- docs/plans/batcher-regression/handoff.md | 36 +- docs/plans/batcher-regression/progress.md | 48 +- docs/plans/rtl-regression/handoff.md | 24 +- docs/plans/rtl-regression/progress.md | 16 +- .../AxiStreamBatcherEventBuilderWrapper.vhd | 242 ++++++++++ tests/protocols/batcher/batcher_test_utils.py | 10 + .../test_AxiStreamBatcherEventBuilder.py | 415 ++++++++++++++++++ 7 files changed, 760 insertions(+), 31 deletions(-) create mode 100644 protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd create mode 100644 tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py diff --git a/docs/plans/batcher-regression/handoff.md b/docs/plans/batcher-regression/handoff.md index 1967ae7256..54a26d3546 100644 --- a/docs/plans/batcher-regression/handoff.md +++ b/docs/plans/batcher-regression/handoff.md @@ -6,8 +6,10 @@ behavior at the default 8-byte width, now has a passing cocotb regression. - A narrow `AxiStreamBatcherAxil` common-clock wrapper regression is also in place for register readback and control propagation. -- Do not start broad `AxiStreamBatcherEventBuilder` coverage until the leaf and - AXI-Lite wrapper tests remain green in the current worktree. +- A focused `AxiStreamBatcherEventBuilder` two-source regression is in place for + INDEXED and ROUTED integration policy. +- Keep any further event-builder work targeted; the current pass is not an + exhaustive source-count or generic matrix. ## Expected File Areas - RTL wrappers: `protocols/batcher/wrappers/` @@ -20,14 +22,34 @@ - If deepening Phase 2, keep it wrapper-specific: async AXI-Lite crossing, additional blowoff timing, or soft-reset timing. Avoid duplicating leaf byte grammar tests. -- If moving to Phase 3, start with small event-builder source-count cases and - reuse the batcher byte-stream helpers for final output shape. +- If deepening Phase 3, add only targeted event-builder integration cases such + as more source-count/generic breadth, alternate route tables, external-only + blowoff behavior, or bug-driven transition/bypass timing. + +## Current Coverage +- `AxiStreamBatcher`: compacted V2 output, subframe metadata, multi-subframe + superframes, max-subframe/idle-gap/byte-threshold termination, forced + termination with terminal `EOFE`, output backpressure, and reset recovery. +- `AxiStreamBatcherAxil`: documented register reset/readback, threshold/count/gap + control propagation, `softRst`, and `blowoff` drop/recovery. +- `AxiStreamBatcherEventBuilder`: two-source INDEXED/ROUTED source selection, + TDEST remap including fixed/passthrough routed bits, null counting without + forwarding, timeout drop for a missing source followed by a clean later event, + shared-output backpressure while both inputs contribute to an event, bypass + skip/recovery, blowoff drop/recovery, routed transition-frame preemption, and + visible counter/status readback. + +## Deferred Scope +- V1 and non-default stream-width leaf coverage. +- Async AXI-Lite crossing in `AxiStreamBatcherAxil`. +- Event-builder source-count matrices, alternate route-table shapes, and + exhaustive transition/bypass timing permutations. ## Validation Checklist - Latest completed: - - `./.venv/bin/vsg -c vsg-linter.yml -f protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd` - - `PYTHONPYCACHEPREFIX=/private/tmp/surf-pycache ./.venv/bin/python -m py_compile tests/protocols/batcher/batcher_test_utils.py tests/protocols/batcher/test_AxiStreamBatcher.py tests/protocols/batcher/test_AxiStreamBatcherAxil.py` - - `./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` (`2 passed`) + - `./.venv/bin/vsg -c vsg-linter.yml -f protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd` + - `PYTHONPYCACHEPREFIX=/private/tmp/surf-pycache ./.venv/bin/python -m py_compile tests/protocols/batcher/batcher_test_utils.py tests/protocols/batcher/test_AxiStreamBatcher.py tests/protocols/batcher/test_AxiStreamBatcherAxil.py tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py` + - `./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` (`4 passed`) - Stale simulator process sweep, no leftover batcher `ghdl`/`pytest`/cocotb processes observed - `git diff --check` diff --git a/docs/plans/batcher-regression/progress.md b/docs/plans/batcher-regression/progress.md index 1688f1f58c..8989896391 100644 --- a/docs/plans/batcher-regression/progress.md +++ b/docs/plans/batcher-regression/progress.md @@ -1,12 +1,14 @@ # Batcher Regression Progress ## Status -- Current phase: Phase 2 AXI-Lite wrapper implementation started. -- Current implementation gate: `AxiStreamBatcher` V2 8-byte leaf coverage and - `AxiStreamBatcherAxil` common-clock register/control coverage are validated - locally. -- Current target: keep any further `AxiStreamBatcherAxil` work register/control - specific, then move to `AxiStreamBatcherEventBuilder` if needed. +- Current phase: Phase 3 event-builder implementation completed for the first + focused two-source slice. +- Current implementation gate: `AxiStreamBatcher` V2 8-byte leaf coverage, + `AxiStreamBatcherAxil` common-clock register/control coverage, and + `AxiStreamBatcherEventBuilder` two-source INDEXED/ROUTED integration coverage + are validated locally. +- Current target: keep future work focused on intentionally deferred generic + breadth, async AXI-Lite behavior, or specific bug-driven edge cases. ## Decisions - Use a standalone leaf-first strategy. @@ -21,12 +23,16 @@ `protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd`. - Added a common-clock AXI-Lite wrapper at `protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd`. +- Added a two-source event-builder wrapper at + `protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd`. - Added shared batcher helpers in `tests/protocols/batcher/batcher_test_utils.py`. - Added a standalone leaf regression in `tests/protocols/batcher/test_AxiStreamBatcher.py`. - Added an AXI-Lite wrapper regression in `tests/protocols/batcher/test_AxiStreamBatcherAxil.py`. +- Added an event-builder regression in + `tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py`. - Covered V2 compacted output for the default 8-byte width: superframe header bytes, subframe payload/tail bytes, multiple subframes per superframe, termination by max-subframe count, idle gap, byte threshold, forced @@ -36,14 +42,33 @@ control propagation for max-subframe count, byte threshold, and clock gap, `softRst` recovery from a partial superframe, and `blowoff` accept/drop behavior followed by normal recovery traffic. +- Covered `AxiStreamBatcherEventBuilder` in small two-source INDEXED and ROUTED + configurations: reset/status/readback, source selection, TDEST remap including + fixed/passthrough routed bits, null source counting without forwarding, + timeout drop behavior for a missing source followed by a clean later event, + shared-output backpressure while both inputs contribute to an event, bypass + skip/recovery behavior, blowoff drop/recovery behavior, and routed + transition-frame preemption through `TRANS_TDEST_G`. +- The event-builder tests deliberately reuse the leaf byte-stream helpers for + final batcher output shape instead of duplicating the full packet grammar. + +## Deferred Scope +- Phase 1 is intentionally limited to V2 at the default 8-byte width. V1 and + non-default stream widths remain targeted follow-ups if future changes touch + those branches. +- Phase 2 is intentionally limited to `COMMON_CLOCK_G=true`. Async AXI-Lite + crossing behavior remains open. +- Phase 3 is intentionally limited to a two-source event-builder wrapper. + Broader source-count matrices, alternate route tables, and exhaustive + transition/bypass timing permutations remain out of the current pass. ## Validation -- `./.venv/bin/vsg -c vsg-linter.yml -f protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd` +- `./.venv/bin/vsg -c vsg-linter.yml -f protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd` passed with zero violations. -- `PYTHONPYCACHEPREFIX=/private/tmp/surf-pycache ./.venv/bin/python -m py_compile tests/protocols/batcher/batcher_test_utils.py tests/protocols/batcher/test_AxiStreamBatcher.py tests/protocols/batcher/test_AxiStreamBatcherAxil.py` +- `PYTHONPYCACHEPREFIX=/private/tmp/surf-pycache ./.venv/bin/python -m py_compile tests/protocols/batcher/batcher_test_utils.py tests/protocols/batcher/test_AxiStreamBatcher.py tests/protocols/batcher/test_AxiStreamBatcherAxil.py tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py` passed. - `./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` passed with - `2 passed`. + `4 passed`. - Stale simulator process sweep did not show leftover `ghdl`, `pytest`, or cocotb batcher processes. - `git diff --check` passed for tracked changes. The new batcher files are @@ -57,5 +82,6 @@ 2. If Phase 2 deepens, stay focused on wrapper-specific behavior such as async AXI-Lite crossing or additional malformed/blowoff timing; do not duplicate the full leaf byte grammar. -3. Start Phase 3 event-builder coverage with small `NUM_SLAVES_G` cases and - reuse the leaf byte-stream helpers for final output shape. +3. If Phase 3 deepens, add only targeted event-builder cases such as more + source-count/generic breadth, alternate route tables, external-only blowoff + behavior, or bug-driven transition/bypass timing. diff --git a/docs/plans/rtl-regression/handoff.md b/docs/plans/rtl-regression/handoff.md index 6c10e81979..fcc41ac7b0 100644 --- a/docs/plans/rtl-regression/handoff.md +++ b/docs/plans/rtl-regression/handoff.md @@ -182,13 +182,16 @@ If the user keeps the focus on stream-helper cleanup rather than resuming a new If the user continues the new `protocols/packetizer` slice, the standalone-first pass now has expanded leaf coverage plus one narrow V2 loopback after the direct contracts. `AxiStreamPacketizer`, `AxiStreamDepacketizer`, `AxiStreamPacketizer2`, `AxiStreamDepacketizer2`, and `AxiStreamBytePacker` are covered directly under `tests/protocols/packetizer/`, and `AxiStreamPacketizer2LoopbackWrapper` adds CRC NONE/DATA/FULL packetizer-to-depacketizer coverage. The shared helper layer in `tests/protocols/packetizer/packetizer_test_utils.py` now owns the repeated V0/V2 packet beat builders, packetized/app-stream assertions, V2 CRC-mode env decoding, depacketizer `initDone` polling, no-output checks, and BytePacker unpaced stimulus/output-valid helpers. The packetizer/depacketizer wrappers expose full per-byte `TUSER` vectors for `TUSER_FIRST_LAST` behavior, the legacy V0 pair covers both EOF/user tail encodings, split/continuation state, output backpressure, and malformed-continuation bleed/recovery, the V2 pair covers header/payload/tail, split-frame sequencing, sequence-counter wrap at `SEQ_CNT_SIZE_G=4`, partial final `TKEEP`, interleaved-`TDEST` rearbitration, `TDEST_BITS_G=0/1/2` loopback behavior, exact and one-byte-over `maxPktBytes` splitting, output backpressure, CRC-mode packetizer behavior, DATA/FULL bad-CRC rejection, CRC-none tail-error marking, header error paths, link-drop recovery, and isolated mid-frame link-drop termination/recovery, and the byte packer covers partial-beat compaction, idle gaps, reset flush, zero-keep input beats, and no-ready behavior across 1-to-8, 2-to-5, 3-to-6, 3-to-7, 4-to-8, 5-to-7, and 7-to-8 compressed-keep width conversions. The latest focused packetizer validation is `./.venv/bin/python -m pytest -n 0 -q tests/protocols/packetizer` (`22 passed`), with `git diff --check` clean after the helper refactor. -If the user continues the new `protocols/batcher` slice, Phase 1 and a narrow -Phase 2 have started. The current worktree adds +If the user continues the new `protocols/batcher` slice, Phase 1, a narrow +Phase 2, and a focused Phase 3 event-builder slice are in place. The current +worktree adds `protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd`, `protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd`, +`protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd`, `tests/protocols/batcher/batcher_test_utils.py`, and `tests/protocols/batcher/test_AxiStreamBatcher.py` plus -`tests/protocols/batcher/test_AxiStreamBatcherAxil.py`. The validated +`tests/protocols/batcher/test_AxiStreamBatcherAxil.py` plus +`tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py`. The validated V2/default 8-byte leaf slice covers compacted header/payload/tail bytes, subframe metadata, multi-subframe superframes, max-subframe/idle-gap/byte- threshold termination, forced termination with terminal `EOFE`, output @@ -196,12 +199,19 @@ backpressure hold, and reset recovery after a partial superframe. The validated AXI-Lite wrapper slice covers reset/readback for the documented register map, control propagation for max-subframe count, byte threshold, and clock gap, `softRst` recovery from a partial superframe, and `blowoff` accept/drop behavior -followed by normal recovery. The latest focused validation is -`./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` (`2 passed`), +followed by normal recovery. The validated event-builder slice covers two-source +INDEXED/ROUTED source selection, TDEST remap including fixed/passthrough routed +bits, null counting without forwarding, timeout drop for a missing source +followed by a clean later event, shared-output backpressure while both inputs +contribute to an event, bypass skip/recovery, blowoff drop/recovery, routed +transition-frame preemption, and visible counter/status readback. The latest +focused validation is +`./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` (`4 passed`), with clean wrapper `vsg`, Python `py_compile`, stale-process sweep, and `git diff --check`. Possible next steps are a small V1/power-of-two leaf case, -deeper AXI-Lite async/adverse reset timing, or Phase 3 event-builder integration -coverage. +deeper AXI-Lite async/adverse reset timing, or targeted event-builder breadth +such as more source-count/generic cases, alternate route tables, external-only +blowoff, or bug-driven transition/bypass timing. If the user keeps the focus on `protocols/srp`, the main review findings and high-value coverage additions are complete. The optional remaining SRP follow-up is deeper timeout or posted-write disabled-op permutations if a future change touches those RTL branches. The latest focused SRP validation command is `./.venv/bin/python -m pytest -n 0 -q tests/protocols/srp`, and it passed locally with `23 passed`. diff --git a/docs/plans/rtl-regression/progress.md b/docs/plans/rtl-regression/progress.md index c03b6a6271..dcf082702e 100644 --- a/docs/plans/rtl-regression/progress.md +++ b/docs/plans/rtl-regression/progress.md @@ -12,7 +12,7 @@ - The axi-first pass is complete through the previously remaining final 11 `axi/` modules. - The current `verification-2` branch has been refreshed by merging the current `origin/pre-release` tip. The validated `protocols/ssi`, `protocols/pgp`, current Ethernet waves (`EthMacCore`, `RawEthFramer`, `UdpEngine`, `IpV4Engine`, and the current pure-VHDL RoCEv2 quartet), current CoaXPress status/EOFE work, SRP follow-up work, and base-depth pass are all part of the present branch snapshot. - The current packetizer pass started with standalone tests for individual VHDL modules, not a loopback-as-oracle bench. `AxiStreamPacketizer`, `AxiStreamDepacketizer`, `AxiStreamPacketizer2`, `AxiStreamDepacketizer2`, and `AxiStreamBytePacker` now have direct cocotb coverage through checked-in wrappers, and `AxiStreamPacketizer2LoopbackWrapper` adds a narrow end-to-end V2 CRC-mode loopback check after those leaf contracts are pinned down. The packetizer/depacketizer wrappers expose the full per-byte `TUSER` vectors needed by `TUSER_FIRST_LAST` semantics. The legacy V0 tests cover both tail encodings, max-size split/continuation state, output backpressure hold, malformed-continuation bleed/recovery, and normal recovery framing. The V2 tests now cover partial final `TKEEP`, split-frame sequence state, sequence-counter wrap at `SEQ_CNT_SIZE_G=4`, interleaved `TDEST` rearbitration, `TDEST_BITS_G=0/1/2` loopback behavior, exact and one-byte-over `maxPktBytes` boundary splitting, output backpressure hold, CRC NONE/DATA/FULL packetizer tail/header behavior, DATA/FULL bad-CRC rejection, CRC-none tail-error marking, bad-version and bad-CRC-mode header error paths, link-drop recovery, mid-frame link-drop termination/recovery, and V2 packetizer/depacketizer loopback across CRC modes. The byte-packer wrapper now sweeps multiple compressed-keep input/output byte-width pairs: 1-to-8, 2-to-5, 3-to-6, 3-to-7, 4-to-8, 5-to-7, and 7-to-8, including a zero-keep input-beat guardrail. - - The batcher pass has started with the same standalone-first shape. `AxiStreamBatcher` now has a thin checked-in wrapper under `protocols/batcher/wrappers/` and a V2/default-8-byte cocotb regression under `tests/protocols/batcher/`. The validated leaf slice covers compacted V2 header/payload/tail bytes through the gearbox path, subframe tail metadata, multi-subframe superframes, termination by max-subframe count, idle gap, byte threshold, forced termination with terminal `EOFE`, output backpressure hold, and reset recovery after a partial superframe. `AxiStreamBatcherAxil` now has a narrow common-clock wrapper regression for register reset/readback, control propagation, `softRst`, and `blowoff`. `AxiStreamBatcherEventBuilder` remains the next planned layer, with integration-specific assertions only. + - The batcher pass has the same standalone-first shape. `AxiStreamBatcher` now has a thin checked-in wrapper under `protocols/batcher/wrappers/` and a V2/default-8-byte cocotb regression under `tests/protocols/batcher/`. The validated leaf slice covers compacted V2 header/payload/tail bytes through the gearbox path, subframe tail metadata, multi-subframe superframes, termination by max-subframe count, idle gap, byte threshold, forced termination with terminal `EOFE`, output backpressure hold, and reset recovery after a partial superframe. `AxiStreamBatcherAxil` has a narrow common-clock wrapper regression for register reset/readback, control propagation, `softRst`, and `blowoff`. `AxiStreamBatcherEventBuilder` now has a focused two-source INDEXED/ROUTED integration regression for source selection, TDEST remap, null counting, timeout drop/recovery, shared-output backpressure, bypass recovery, blowoff drop/recovery, transition-frame preemption, and counter/status readback. - The retained RTL-regression planning files now live under `docs/plans/rtl-regression/` instead of the old `docs/_meta/rtl_regression_*` paths. - The old checked-in graph/queue artifacts have been removed from the task planning directory; regenerate them only as temporary one-off analysis if needed. - Keep the done/open frontier in this progress file and in `docs/plans/rtl-regression/handoff.md` aligned to the actual tree. @@ -170,14 +170,17 @@ - The most recent recorded CoaXPress validation after the HKP AXI-Lite consumer sweep is green for the full `tests/protocols/coaxpress` directory with `19 passed`. - The former opt-in `CoaXPressCore` overflow/FSM-error known-issue bench is now part of the normal CoaXPress core regression with a long-line workload that actually fills the RX data FIFO. - The current `test-base-2` merge adds the base-depth follow-up pass. It expands existing tests only, with no new wrappers: `FifoMux`, `FifoAsync`, `FifoSync`, `FifoCascade`, `SynchronizerFifo`, `Arbiter`, `WatchDogRst`, `DualPortRam`, `SimpleDualPortRam`, `TrueDualPortRam`, `SlvDelayFifo`, and `SlvDelayRam` now cover selected high-value edge cases from the FIFO/CDC/RAM/delay/general base-depth list. The latest refinement adds `FifoAsync` near-full turnover under concurrent read/write pressure, explicit `FifoMux` partial-pack reset no-output checking, `SynchronizerFifo` reset while FWFT data is prefetched and read-side paused, registered-output `SimpleDualPortRam` `enb` hold behavior, multi-entry `SlvDelayFifo` reset flushing, and `SlvDelayRam` reset-aligned runtime delay growth with output-history discard. The latest local validation produced `61 passed` in the focused parallel base run before a `FifoCascade` parallel GHDL analysis race, and the failed `FifoCascade` target passed on serial rerun with `3 passed`. -- The current batcher worktree adds Phase 1/2 batcher coverage: +- The current batcher worktree adds Phase 1/2/3 batcher coverage: `protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd`, `protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd`, + `protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd`, `tests/protocols/batcher/batcher_test_utils.py`, and `tests/protocols/batcher/test_AxiStreamBatcher.py` plus - `tests/protocols/batcher/test_AxiStreamBatcherAxil.py`. Latest focused + `tests/protocols/batcher/test_AxiStreamBatcherAxil.py` plus + `tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py`. Latest focused validation is `./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` - (`2 passed`), plus clean wrapper `vsg` and Python `py_compile`. + (`4 passed`), plus clean wrapper `vsg`, Python `py_compile`, stale-process + sweep, and `git diff --check`. - The user-requested `protocols/srp` review fixes are complete: `test_SrpV3Axi.py` now reuses the shared SRPv3 helper/model layer, `test_SrpV3Core.py` uses decorator-based cocotb test selection, the stray SRPv3 AXI-Lite debug logging is removed, and the high-value SRP coverage additions are checked in locally. - Preserve the recent `pgp4` lesson for later PGP work: when the simulation wrapper only exposes stable lock/config surfaces, write the bench around those explicit contracts instead of claiming recovered payload coverage. - Latest focused SRP validation: `./.venv/bin/python -m pytest -n 0 -q tests/protocols/srp` passed locally with `23 passed`. @@ -189,8 +192,9 @@ - If implementation continues in `protocols/batcher`, keep the next work leaf, wrapper-specific, or event-builder-specific: either add a small V1/power-of-two leaf case, deepen `AxiStreamBatcherAxil` around async - crossing or adverse reset/blowoff timing, or start event-builder coverage with - small source-count integration cases. + crossing or adverse reset/blowoff timing, or deepen event-builder coverage + with targeted source-count/generic breadth, alternate route tables, + external-only blowoff, or bug-driven transition/bypass timing. - If implementation resumes on Ethernet/RoCEv2, the next real step is enabling a mixed-language cocotb path for `EthMacCrcAxiStreamWrapperSend`, `EthMacCrcAxiStreamWrapperRecv`, `EthMacTxRoCEv2`, `EthMacRxRoCEv2`, and `RoceEngineWrapper` against the real `blue-*` dependencies. ## Blockers And Risks diff --git a/protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd b/protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd new file mode 100644 index 0000000000..9c3b4c119d --- /dev/null +++ b/protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd @@ -0,0 +1,242 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing wrapper for surf.AxiStreamBatcherEventBuilder +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AxiStreamPkg.all; + +entity AxiStreamBatcherEventBuilderWrapper is + generic ( + TPD_G : time := 1 ns; + VERSION_G : positive range 1 to 2 := 2; + MODE_G : string := "INDEXED"; + DATA_BYTES_G : positive range 8 to 8 := 8; + INPUT_PIPE_STAGES_G : natural := 0; + OUTPUT_PIPE_STAGES_G : natural := 1; + AXIL_ADDR_WIDTH_G : positive := 12; + TRANS_TDEST_G : slv(7 downto 0) := x"FF"); + port ( + axisClk : in sl; + axisRst : in sl; + blowoffExt : in sl; + blowoffInt : out sl; + S0_AXIS_TVALID : in sl; + S0_AXIS_TDATA : in slv(8*DATA_BYTES_G-1 downto 0); + S0_AXIS_TKEEP : in slv(DATA_BYTES_G-1 downto 0); + S0_AXIS_TLAST : in sl; + S0_AXIS_TDEST : in slv(7 downto 0); + S0_AXIS_TID : in slv(7 downto 0); + S0_AXIS_TUSER : in slv(8*DATA_BYTES_G-1 downto 0); + S0_AXIS_TREADY : out sl; + S1_AXIS_TVALID : in sl; + S1_AXIS_TDATA : in slv(8*DATA_BYTES_G-1 downto 0); + S1_AXIS_TKEEP : in slv(DATA_BYTES_G-1 downto 0); + S1_AXIS_TLAST : in sl; + S1_AXIS_TDEST : in slv(7 downto 0); + S1_AXIS_TID : in slv(7 downto 0); + S1_AXIS_TUSER : in slv(8*DATA_BYTES_G-1 downto 0); + S1_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(8*DATA_BYTES_G-1 downto 0); + M_AXIS_TKEEP : out slv(DATA_BYTES_G-1 downto 0); + M_AXIS_TLAST : out sl; + M_AXIS_TDEST : out slv(7 downto 0); + M_AXIS_TID : out slv(7 downto 0); + M_AXIS_TUSER : out slv(8*DATA_BYTES_G-1 downto 0); + M_AXIS_TREADY : in sl; + S_AXI_AWADDR : in slv(AXIL_ADDR_WIDTH_G-1 downto 0); + S_AXI_AWPROT : in slv(2 downto 0); + S_AXI_AWVALID : in sl; + S_AXI_AWREADY : out sl; + S_AXI_WDATA : in slv(31 downto 0); + S_AXI_WSTRB : in slv(3 downto 0); + S_AXI_WVALID : in sl; + S_AXI_WREADY : out sl; + S_AXI_BRESP : out slv(1 downto 0); + S_AXI_BVALID : out sl; + S_AXI_BREADY : in sl; + S_AXI_ARADDR : in slv(AXIL_ADDR_WIDTH_G-1 downto 0); + S_AXI_ARPROT : in slv(2 downto 0); + S_AXI_ARVALID : in sl; + S_AXI_ARREADY : out sl; + S_AXI_RDATA : out slv(31 downto 0); + S_AXI_RRESP : out slv(1 downto 0); + S_AXI_RVALID : out sl; + S_AXI_RREADY : in sl); +end entity AxiStreamBatcherEventBuilderWrapper; + +architecture rtl of AxiStreamBatcherEventBuilderWrapper is + + constant NUM_SLAVES_C : positive := 2; + + constant AXIS_CONFIG_C : AxiStreamConfigType := ( + TSTRB_EN_C => false, + TDATA_BYTES_C => DATA_BYTES_G, + TDEST_BITS_C => 8, + TID_BITS_C => 0, + TKEEP_MODE_C => TKEEP_NORMAL_C, + TUSER_BITS_C => 8, + TUSER_MODE_C => TUSER_FIRST_LAST_C); + + constant TDEST_ROUTES_C : Slv8Array(NUM_SLAVES_C-1 downto 0) := ( + 0 => "--------", + 1 => "0101----"); + + signal axilRstN : sl; + signal axilClk : sl; + signal axilRst : sl; + signal axilReadMaster : AxiLiteReadMasterType := AXI_LITE_READ_MASTER_INIT_C; + signal axilReadSlave : AxiLiteReadSlaveType := AXI_LITE_READ_SLAVE_INIT_C; + signal axilWriteMaster : AxiLiteWriteMasterType := AXI_LITE_WRITE_MASTER_INIT_C; + signal axilWriteSlave : AxiLiteWriteSlaveType := AXI_LITE_WRITE_SLAVE_INIT_C; + signal sAxisMasters : AxiStreamMasterArray(NUM_SLAVES_C-1 downto 0) := (others => AXI_STREAM_MASTER_INIT_C); + signal sAxisSlaves : AxiStreamSlaveArray(NUM_SLAVES_C-1 downto 0) := (others => AXI_STREAM_SLAVE_INIT_C); + signal mAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal mAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + +begin + + axilRstN <= not axisRst; + + ------------------------ + -- AXI-Lite bus shim -- + ------------------------ + U_AXIL : entity surf.SlaveAxiLiteIpIntegrator + generic map ( + HAS_PROT => 1, + HAS_WSTRB => 1, + ADDR_WIDTH => AXIL_ADDR_WIDTH_G) + port map ( + S_AXI_ACLK => axisClk, -- [in] + S_AXI_ARESETN => axilRstN, -- [in] + S_AXI_AWADDR => S_AXI_AWADDR, -- [in] + S_AXI_AWPROT => S_AXI_AWPROT, -- [in] + S_AXI_AWVALID => S_AXI_AWVALID, -- [in] + S_AXI_AWREADY => S_AXI_AWREADY, -- [out] + S_AXI_WDATA => S_AXI_WDATA, -- [in] + S_AXI_WSTRB => S_AXI_WSTRB, -- [in] + S_AXI_WVALID => S_AXI_WVALID, -- [in] + S_AXI_WREADY => S_AXI_WREADY, -- [out] + S_AXI_BRESP => S_AXI_BRESP, -- [out] + S_AXI_BVALID => S_AXI_BVALID, -- [out] + S_AXI_BREADY => S_AXI_BREADY, -- [in] + S_AXI_ARADDR => S_AXI_ARADDR, -- [in] + S_AXI_ARPROT => S_AXI_ARPROT, -- [in] + S_AXI_ARVALID => S_AXI_ARVALID, -- [in] + S_AXI_ARREADY => S_AXI_ARREADY, -- [out] + S_AXI_RDATA => S_AXI_RDATA, -- [out] + S_AXI_RRESP => S_AXI_RRESP, -- [out] + S_AXI_RVALID => S_AXI_RVALID, -- [out] + S_AXI_RREADY => S_AXI_RREADY, -- [in] + axilClk => axilClk, -- [out] + axilRst => axilRst, -- [out] + axilReadMaster => axilReadMaster, -- [out] + axilReadSlave => axilReadSlave, -- [in] + axilWriteMaster => axilWriteMaster, -- [out] + axilWriteSlave => axilWriteSlave); -- [in] + + ------------------------ + -- AXI Stream shims -- + ------------------------ + comb : process (M_AXIS_TREADY, S0_AXIS_TDATA, S0_AXIS_TDEST, S0_AXIS_TID, + S0_AXIS_TKEEP, S0_AXIS_TLAST, S0_AXIS_TUSER, S0_AXIS_TVALID, + S1_AXIS_TDATA, S1_AXIS_TDEST, S1_AXIS_TID, S1_AXIS_TKEEP, + S1_AXIS_TLAST, S1_AXIS_TUSER, S1_AXIS_TVALID, mAxisMaster, + sAxisSlaves) is + variable vS : AxiStreamMasterArray(NUM_SLAVES_C-1 downto 0); + variable vM : AxiStreamSlaveType; + begin + vS := (others => AXI_STREAM_MASTER_INIT_C); + + vS(0).tValid := S0_AXIS_TVALID; + vS(0).tData := (others => '0'); + vS(0).tData(8*DATA_BYTES_G-1 downto 0) := S0_AXIS_TDATA; + vS(0).tStrb := (others => '0'); + vS(0).tStrb(DATA_BYTES_G-1 downto 0) := S0_AXIS_TKEEP; + vS(0).tKeep := (others => '0'); + vS(0).tKeep(DATA_BYTES_G-1 downto 0) := S0_AXIS_TKEEP; + vS(0).tLast := S0_AXIS_TLAST; + vS(0).tDest := (others => '0'); + vS(0).tDest(7 downto 0) := S0_AXIS_TDEST; + vS(0).tId := (others => '0'); + vS(0).tId(7 downto 0) := S0_AXIS_TID; + vS(0).tUser := (others => '0'); + vS(0).tUser(8*DATA_BYTES_G-1 downto 0) := S0_AXIS_TUSER; + + vS(1).tValid := S1_AXIS_TVALID; + vS(1).tData := (others => '0'); + vS(1).tData(8*DATA_BYTES_G-1 downto 0) := S1_AXIS_TDATA; + vS(1).tStrb := (others => '0'); + vS(1).tStrb(DATA_BYTES_G-1 downto 0) := S1_AXIS_TKEEP; + vS(1).tKeep := (others => '0'); + vS(1).tKeep(DATA_BYTES_G-1 downto 0) := S1_AXIS_TKEEP; + vS(1).tLast := S1_AXIS_TLAST; + vS(1).tDest := (others => '0'); + vS(1).tDest(7 downto 0) := S1_AXIS_TDEST; + vS(1).tId := (others => '0'); + vS(1).tId(7 downto 0) := S1_AXIS_TID; + vS(1).tUser := (others => '0'); + vS(1).tUser(8*DATA_BYTES_G-1 downto 0) := S1_AXIS_TUSER; + + vM := AXI_STREAM_SLAVE_INIT_C; + vM.tReady := M_AXIS_TREADY; + + sAxisMasters <= vS; + mAxisSlave <= vM; + + S0_AXIS_TREADY <= sAxisSlaves(0).tReady; + S1_AXIS_TREADY <= sAxisSlaves(1).tReady; + M_AXIS_TVALID <= mAxisMaster.tValid; + M_AXIS_TDATA <= mAxisMaster.tData(8*DATA_BYTES_G-1 downto 0); + M_AXIS_TKEEP <= mAxisMaster.tKeep(DATA_BYTES_G-1 downto 0); + M_AXIS_TLAST <= mAxisMaster.tLast; + M_AXIS_TDEST <= mAxisMaster.tDest(7 downto 0); + M_AXIS_TID <= mAxisMaster.tId(7 downto 0); + M_AXIS_TUSER <= mAxisMaster.tUser(8*DATA_BYTES_G-1 downto 0); + end process comb; + + --------------------- + -- DUT instancing -- + --------------------- + U_DUT : entity surf.AxiStreamBatcherEventBuilder + generic map ( + TPD_G => TPD_G, + VERSION_G => VERSION_G, + NUM_SLAVES_G => NUM_SLAVES_C, + MODE_G => MODE_G, + TDEST_ROUTES_G => TDEST_ROUTES_C, + TDEST_LOW_G => 0, + TRANS_TDEST_G => TRANS_TDEST_G, + AXIS_CONFIG_G => AXIS_CONFIG_C, + INPUT_PIPE_STAGES_G => INPUT_PIPE_STAGES_G, + OUTPUT_PIPE_STAGES_G => OUTPUT_PIPE_STAGES_G) + port map ( + axisClk => axisClk, -- [in] + axisRst => axisRst, -- [in] + blowoffExt => blowoffExt, -- [in] + blowoffInt => blowoffInt, -- [out] + axilReadMaster => axilReadMaster, -- [in] + axilReadSlave => axilReadSlave, -- [out] + axilWriteMaster => axilWriteMaster, -- [in] + axilWriteSlave => axilWriteSlave, -- [out] + sAxisMasters => sAxisMasters, -- [in] + sAxisSlaves => sAxisSlaves, -- [out] + mAxisMaster => mAxisMaster, -- [out] + mAxisSlave => mAxisSlave); -- [in] + +end architecture rtl; diff --git a/tests/protocols/batcher/batcher_test_utils.py b/tests/protocols/batcher/batcher_test_utils.py index c8e065026b..38b5d544eb 100644 --- a/tests/protocols/batcher/batcher_test_utils.py +++ b/tests/protocols/batcher/batcher_test_utils.py @@ -191,6 +191,16 @@ async def send_frame(endpoint: FlatAxisEndpoint, beats: list[AxisBeat], *, clk) await endpoint.send(beat, clk=clk) +async def send_frames_concurrently( + frames: list[tuple[FlatAxisEndpoint, list[AxisBeat]]], + *, + clk, +) -> None: + tasks = [cocotb.start_soon(send_frame(endpoint, beats, clk=clk)) for endpoint, beats in frames] + for task in tasks: + await task + + async def recv_until_last(endpoint: FlatAxisEndpoint, *, clk, max_beats: int = 32) -> list[AxisBeat]: beats = [] for _ in range(max_beats): diff --git a/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py b/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py new file mode 100644 index 0000000000..0e883ad35e --- /dev/null +++ b/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py @@ -0,0 +1,415 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Use a two-input `AxiStreamBatcherEventBuilder` wrapper in INDEXED and +# ROUTED modes. This keeps the event-builder policy visible without building +# an exhaustive source-count matrix. +# - Stimulus: Drive small AXI Stream frames on one or both inputs, program the +# event-builder AXI-Lite bypass/timeout controls, and use one transition TDEST +# case in routed mode. +# - Checks: Assert source-selection policy, TDEST remap, null/transition/timeout +# counters, bypass/drop behavior, and the final batcher byte stream shape +# through the shared leaf byte helpers. +# - Timing: Inputs are driven concurrently where the event builder requires all +# active sources to be present, and timeout/bypass cases verify progress when +# one source is absent or intentionally skipped. + +import os + +import cocotb +import pytest +from cocotb.triggers import with_timeout +from cocotbext.axi import AxiLiteBus, AxiLiteMaster + +from tests.axi.utils import axil_read_u32, axil_write_u32 +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.batcher.batcher_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + beats_to_bytes, + cycle, + expect_no_valid, + expected_batched_bytes, + payload_to_beats, + recv_until_last_with_backpressure, + recv_until_last, + reset_batcher_dut, + send_frame, + send_frames_concurrently, + start_batcher_clock, + word_from_bytes, +) + +DATA_CNT_BASE = 0x000 +NULL_CNT_BASE = 0x100 +TIMEOUT_DROP_CNT_BASE = 0x200 +TRANS_CNT_ADDR = 0xFC0 +TRANS_TDEST_ADDR = 0xFC4 +BYPASS_ADDR = 0xFD0 +TIMEOUT_ADDR = 0xFF0 +STATUS_ADDR = 0xFF4 +BLOWOFF_ADDR = 0xFF8 + + +class TB: + def __init__(self, dut): + self.dut = dut + self.mode = os.environ.get("MODE_G", "INDEXED").strip("'").strip('"') + self.source0 = FlatAxisEndpoint(dut, prefix="S0_AXIS") + self.source1 = FlatAxisEndpoint(dut, prefix="S1_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + self.axil = AxiLiteMaster(AxiLiteBus.from_prefix(dut, "S_AXI"), dut.axisClk, dut.axisRst) + + start_batcher_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.blowoffExt.setimmediatevalue(0) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source0.set_idle() + self.source1.set_idle() + + async def reset(self): + await reset_batcher_dut(self.dut) + + async def read(self, address: int) -> int: + return await with_timeout(axil_read_u32(self.axil, address), 2, "us") + + async def write(self, address: int, value: int) -> None: + await with_timeout(axil_write_u32(self.axil, address, value), 2, "us") + + def remapped_dest(self, index: int, original: int) -> int: + if self.mode == "ROUTED": + if index == 1: + return 0x50 | (original & 0x0F) + return original & 0xFF + return index + + +def _frame(payload: bytes, *, dest: int, first_user: int, last_user: int): + return (payload, dest, first_user, last_user) + + +def _null_beat(*, dest: int = 0) -> AxisBeat: + return AxisBeat( + data=word_from_bytes(b"\x00"), + keep=0x01, + last=1, + dest=dest, + user=0x01, + ) + + +@cocotb.test() +async def register_status_and_remap_test(dut): + tb = TB(dut) + await tb.reset() + + # Pin down the management map before relying on counters later in the file. + assert await tb.read(TRANS_TDEST_ADDR) == 0xFF + assert await tb.read(STATUS_ADDR) == 0x02000002 + assert await tb.read(BYPASS_ADDR) == 0 + assert await tb.read(TIMEOUT_ADDR) == 0 + + first = _frame(bytes(range(0x10, 0x15)), dest=0x03, first_user=0x21, last_user=0x81) + second = _frame(bytes(range(0x20, 0x25)), dest=0x07, first_user=0x31, last_user=0x91) + expected = [ + _frame(first[0], dest=tb.remapped_dest(0, first[1]), first_user=first[2], last_user=first[3]), + _frame(second[0], dest=tb.remapped_dest(1, second[1]), first_user=second[2], last_user=second[3]), + ] + + # Both channels must be valid together before the event builder can form a + # complete event, so drive the two source coroutines concurrently. + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frames_concurrently( + [ + (tb.source0, payload_to_beats(first[0], dest=first[1], first_user=first[2], last_user=first[3])), + (tb.source1, payload_to_beats(second[0], dest=second[1], first_user=second[2], last_user=second[3])), + ], + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes(expected) + assert await tb.read(DATA_CNT_BASE + 0) == 1 + assert await tb.read(DATA_CNT_BASE + 4) == 1 + + +@cocotb.test() +async def null_source_is_counted_and_not_forwarded_test(dut): + tb = TB(dut) + await tb.reset() + + data = _frame(bytes(range(0x30, 0x35)), dest=0x04, first_user=0x41, last_user=0xA1) + expected = [ + _frame(data[0], dest=tb.remapped_dest(0, data[1]), first_user=data[2], last_user=data[3]), + ] + + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frames_concurrently( + [ + (tb.source0, payload_to_beats(data[0], dest=data[1], first_user=data[2], last_user=data[3])), + (tb.source1, [_null_beat(dest=0x05)]), + ], + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes(expected) + assert await tb.read(DATA_CNT_BASE + 0) == 1 + assert await tb.read(NULL_CNT_BASE + 4) == 1 + + +@cocotb.test() +async def timeout_drops_missing_source_test(dut): + tb = TB(dut) + await tb.reset() + await tb.write(TIMEOUT_ADDR, 4) + + data = _frame(bytes(range(0x40, 0x45)), dest=0x06, first_user=0x51, last_user=0xB1) + expected = [ + _frame(data[0], dest=tb.remapped_dest(0, data[1]), first_user=data[2], last_user=data[3]), + ] + + # Only source 0 is present. The timeout register should let the builder + # emit source 0 and count source 1 as a timeout drop instead of deadlocking. + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frame( + tb.source0, + payload_to_beats(data[0], dest=data[1], first_user=data[2], last_user=data[3]), + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes(expected) + assert await tb.read(DATA_CNT_BASE + 0) == 1 + assert await tb.read(TIMEOUT_DROP_CNT_BASE + 4) == 1 + + recovery0 = _frame(bytes(range(0x48, 0x4D)), dest=0x07, first_user=0x59, last_user=0xB9) + recovery1 = _frame(bytes(range(0x58, 0x5D)), dest=0x08, first_user=0x69, last_user=0xC9) + expected = [ + _frame(recovery0[0], dest=tb.remapped_dest(0, recovery0[1]), first_user=recovery0[2], last_user=recovery0[3]), + _frame(recovery1[0], dest=tb.remapped_dest(1, recovery1[1]), first_user=recovery1[2], last_user=recovery1[3]), + ] + + # A later complete event should still be framed correctly after the timeout + # event and should advance the underlying batcher sequence normally. + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frames_concurrently( + [ + (tb.source0, payload_to_beats(recovery0[0], dest=recovery0[1], first_user=recovery0[2], last_user=recovery0[3])), + (tb.source1, payload_to_beats(recovery1[0], dest=recovery1[1], first_user=recovery1[2], last_user=recovery1[3])), + ], + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes(expected, seq=1) + assert await tb.read(DATA_CNT_BASE + 0) == 2 + assert await tb.read(DATA_CNT_BASE + 4) == 1 + assert await tb.read(TIMEOUT_DROP_CNT_BASE + 4) == 1 + + +@cocotb.test() +async def output_backpressure_holds_complete_event_test(dut): + tb = TB(dut) + await tb.reset() + + first = _frame(bytes(range(0x90, 0x95)), dest=0x0B, first_user=0x24, last_user=0x84) + second = _frame(bytes(range(0xA0, 0xA5)), dest=0x0C, first_user=0x34, last_user=0x94) + expected = [ + _frame(first[0], dest=tb.remapped_dest(0, first[1]), first_user=first[2], last_user=first[3]), + _frame(second[0], dest=tb.remapped_dest(1, second[1]), first_user=second[2], last_user=second[3]), + ] + + # Hold each output beat while both sources have contributed to the event. + # The shared helper asserts that TVALID-side fields stay stable under + # backpressure before accepting the beat. + rx_task = cocotb.start_soon( + recv_until_last_with_backpressure(tb.sink, clk=dut.axisClk, hold_cycles=3) + ) + await send_frames_concurrently( + [ + (tb.source0, payload_to_beats(first[0], dest=first[1], first_user=first[2], last_user=first[3])), + (tb.source1, payload_to_beats(second[0], dest=second[1], first_user=second[2], last_user=second[3])), + ], + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 5, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes(expected) + assert await tb.read(DATA_CNT_BASE + 0) == 1 + assert await tb.read(DATA_CNT_BASE + 4) == 1 + + +@cocotb.test() +async def blowoff_drops_inputs_and_recovers_test(dut): + tb = TB(dut) + await tb.reset() + await tb.write(BLOWOFF_ADDR, 1) + await cycle(dut.axisClk, 4) + assert int(dut.blowoffInt.value) == 1 + + dropped0 = bytes(range(0xB0, 0xB5)) + dropped1 = bytes(range(0xC0, 0xC5)) + await send_frames_concurrently( + [ + (tb.source0, payload_to_beats(dropped0, dest=0x0D, first_user=0x44, last_user=0xA4)), + (tb.source1, payload_to_beats(dropped1, dest=0x0E, first_user=0x54, last_user=0xB4)), + ], + clk=dut.axisClk, + ) + await expect_no_valid(tb.sink, clk=dut.axisClk, cycles=12) + assert await tb.read(DATA_CNT_BASE + 0) == 0 + assert await tb.read(DATA_CNT_BASE + 4) == 0 + + await tb.write(BLOWOFF_ADDR, 0) + await cycle(dut.axisClk, 4) + assert int(dut.blowoffInt.value) == 0 + + recovery0 = _frame(bytes(range(0xD0, 0xD5)), dest=0x0F, first_user=0x64, last_user=0xC4) + recovery1 = _frame(bytes(range(0xE0, 0xE5)), dest=0x10, first_user=0x74, last_user=0xD4) + expected = [ + _frame(recovery0[0], dest=tb.remapped_dest(0, recovery0[1]), first_user=recovery0[2], last_user=recovery0[3]), + _frame(recovery1[0], dest=tb.remapped_dest(1, recovery1[1]), first_user=recovery1[2], last_user=recovery1[3]), + ] + + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frames_concurrently( + [ + (tb.source0, payload_to_beats(recovery0[0], dest=recovery0[1], first_user=recovery0[2], last_user=recovery0[3])), + (tb.source1, payload_to_beats(recovery1[0], dest=recovery1[1], first_user=recovery1[2], last_user=recovery1[3])), + ], + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes(expected, seq=0) + + +@cocotb.test() +async def bypass_skips_source_and_recovers_test(dut): + tb = TB(dut) + await tb.reset() + await tb.write(BYPASS_ADDR, 0b10) + await cycle(dut.axisClk, 4) + + data = _frame(bytes(range(0x50, 0x55)), dest=0x08, first_user=0x61, last_user=0xC1) + expected = [ + _frame(data[0], dest=tb.remapped_dest(0, data[1]), first_user=data[2], last_user=data[3]), + ] + + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frame( + tb.source0, + payload_to_beats(data[0], dest=data[1], first_user=data[2], last_user=data[3]), + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes(expected) + assert await tb.read(DATA_CNT_BASE + 0) == 1 + assert await tb.read(DATA_CNT_BASE + 4) == 0 + + await tb.write(BYPASS_ADDR, 0) + await cycle(dut.axisClk, 4) + + recovery0 = _frame(bytes(range(0x60, 0x65)), dest=0x09, first_user=0x71, last_user=0xD1) + recovery1 = _frame(bytes(range(0x70, 0x75)), dest=0x0A, first_user=0x81, last_user=0xE1) + expected = [ + _frame(recovery0[0], dest=tb.remapped_dest(0, recovery0[1]), first_user=recovery0[2], last_user=recovery0[3]), + _frame(recovery1[0], dest=tb.remapped_dest(1, recovery1[1]), first_user=recovery1[2], last_user=recovery1[3]), + ] + + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frames_concurrently( + [ + (tb.source0, payload_to_beats(recovery0[0], dest=recovery0[1], first_user=recovery0[2], last_user=recovery0[3])), + (tb.source1, payload_to_beats(recovery1[0], dest=recovery1[1], first_user=recovery1[2], last_user=recovery1[3])), + ], + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert beats_to_bytes(rx_beats) == expected_batched_bytes(expected, seq=0) + + +@cocotb.test() +async def routed_transition_frame_preempts_event_test(dut): + tb = TB(dut) + await tb.reset() + if tb.mode != "ROUTED": + return + + transition = _frame(bytes(range(0x80, 0x85)), dest=0xFF, first_user=0x91, last_user=0xF1) + blocked = AxisBeat( + data=word_from_bytes(bytes(range(0x90, 0x98))), + keep=0xFF, + last=1, + dest=0x01, + user=0xA1, + ) + expected = [ + _frame(transition[0], dest=0xFF, first_user=transition[2], last_user=transition[3]), + ] + + # Hold source 1 valid to prove the transition path selects only the source + # with TRANS_TDEST_G and skips the other input for this event. + tb.source1.drive(blocked) + rx_task = cocotb.start_soon(recv_until_last(tb.sink, clk=dut.axisClk)) + await send_frame( + tb.source0, + payload_to_beats(transition[0], dest=transition[1], first_user=transition[2], last_user=transition[3]), + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 4, "us") + tb.source1.set_idle() + + assert beats_to_bytes(rx_beats) == expected_batched_bytes(expected) + assert await tb.read(TRANS_CNT_ADDR) == 1 + assert await tb.read(DATA_CNT_BASE + 0) == 0 + assert await tb.read(DATA_CNT_BASE + 4) == 0 + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param( + { + "VERSION_G": 2, + "MODE_G": "INDEXED", + "INPUT_PIPE_STAGES_G": 0, + "OUTPUT_PIPE_STAGES_G": 1, + }, + id="indexed_v2_2src", + ), + pytest.param( + { + "VERSION_G": 2, + "MODE_G": "ROUTED", + "INPUT_PIPE_STAGES_G": 0, + "OUTPUT_PIPE_STAGES_G": 1, + }, + id="routed_v2_2src", + ), + ], +) +def test_AxiStreamBatcherEventBuilder(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreambatchereventbuilderwrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": [ + "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", + "protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd", + ], + }, + ) From 10a320e97ce78b48c7e8ea4b3e0f275d28e2c767 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Thu, 21 May 2026 16:17:12 -0700 Subject: [PATCH 4/4] Enhance AxiStreamBatcher and EventBuilder with common/async clock support and improved test coverage - Updated AxiStreamBatcherAxilWrapper to support both common and independent AXI-Lite clock modes. - Enhanced AxiStreamBatcherEventBuilderWrapper to include a new route mode for TDEST remapping. - Modified tests for AxiStreamBatcherAxil to validate behavior under both clock configurations. - Expanded tests for AxiStreamBatcherEventBuilder to cover additional routing scenarios and transition TDEST configurations. - Improved documentation in handoff and progress files to reflect the latest implementation status and next steps. --- docs/plans/batcher-regression/handoff.md | 25 ++--- docs/plans/batcher-regression/progress.md | 35 +++---- docs/plans/rtl-regression/handoff.md | 22 +++-- docs/plans/rtl-regression/progress.md | 12 +-- .../wrappers/AxiStreamBatcherAxilWrapper.vhd | 13 ++- .../AxiStreamBatcherEventBuilderWrapper.vhd | 30 ++++-- .../batcher/test_AxiStreamBatcherAxil.py | 93 ++++++++++++++++--- .../test_AxiStreamBatcherEventBuilder.py | 38 ++++++-- 8 files changed, 186 insertions(+), 82 deletions(-) diff --git a/docs/plans/batcher-regression/handoff.md b/docs/plans/batcher-regression/handoff.md index 54a26d3546..c6eed8b776 100644 --- a/docs/plans/batcher-regression/handoff.md +++ b/docs/plans/batcher-regression/handoff.md @@ -4,8 +4,8 @@ - Start from `docs/plans/batcher-regression/plan.md`. - The first implementation target, standalone `AxiStreamBatcher` V2 leaf behavior at the default 8-byte width, now has a passing cocotb regression. -- A narrow `AxiStreamBatcherAxil` common-clock wrapper regression is also in - place for register readback and control propagation. +- A narrow `AxiStreamBatcherAxil` common/async-clock wrapper regression is also + in place for register readback and control propagation. - A focused `AxiStreamBatcherEventBuilder` two-source regression is in place for INDEXED and ROUTED integration policy. - Keep any further event-builder work targeted; the current pass is not an @@ -19,9 +19,9 @@ ## Immediate Next Action - If continuing Phase 1, add only focused leaf gaps such as a compact V1 case or adverse forced-termination timing. -- If deepening Phase 2, keep it wrapper-specific: async AXI-Lite crossing, - additional blowoff timing, or soft-reset timing. Avoid duplicating leaf byte - grammar tests. +- If deepening Phase 2, keep it wrapper-specific: additional blowoff timing, + soft-reset timing, or other AXI-Lite control-surface edge cases. Avoid + duplicating leaf byte grammar tests. - If deepening Phase 3, add only targeted event-builder integration cases such as more source-count/generic breadth, alternate route tables, external-only blowoff behavior, or bug-driven transition/bypass timing. @@ -31,25 +31,26 @@ superframes, max-subframe/idle-gap/byte-threshold termination, forced termination with terminal `EOFE`, output backpressure, and reset recovery. - `AxiStreamBatcherAxil`: documented register reset/readback, threshold/count/gap - control propagation, `softRst`, and `blowoff` drop/recovery. + control propagation, `softRst`, and `blowoff` drop/recovery in both common and + independent AXI-Lite clock modes. - `AxiStreamBatcherEventBuilder`: two-source INDEXED/ROUTED source selection, TDEST remap including fixed/passthrough routed bits, null counting without forwarding, timeout drop for a missing source followed by a clean later event, shared-output backpressure while both inputs contribute to an event, bypass - skip/recovery, blowoff drop/recovery, routed transition-frame preemption, and - visible counter/status readback. + skip/recovery, blowoff drop/recovery, routed transition-frame preemption, + alternate route-table remap, non-default transition TDEST, and visible + counter/status readback. ## Deferred Scope - V1 and non-default stream-width leaf coverage. -- Async AXI-Lite crossing in `AxiStreamBatcherAxil`. -- Event-builder source-count matrices, alternate route-table shapes, and - exhaustive transition/bypass timing permutations. +- Event-builder source-count matrices and exhaustive transition/bypass timing + permutations. ## Validation Checklist - Latest completed: - `./.venv/bin/vsg -c vsg-linter.yml -f protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd` - `PYTHONPYCACHEPREFIX=/private/tmp/surf-pycache ./.venv/bin/python -m py_compile tests/protocols/batcher/batcher_test_utils.py tests/protocols/batcher/test_AxiStreamBatcher.py tests/protocols/batcher/test_AxiStreamBatcherAxil.py tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py` - - `./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` (`4 passed`) + - `./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` (`6 passed`) - Stale simulator process sweep, no leftover batcher `ghdl`/`pytest`/cocotb processes observed - `git diff --check` diff --git a/docs/plans/batcher-regression/progress.md b/docs/plans/batcher-regression/progress.md index 8989896391..7af194bc8d 100644 --- a/docs/plans/batcher-regression/progress.md +++ b/docs/plans/batcher-regression/progress.md @@ -4,11 +4,11 @@ - Current phase: Phase 3 event-builder implementation completed for the first focused two-source slice. - Current implementation gate: `AxiStreamBatcher` V2 8-byte leaf coverage, - `AxiStreamBatcherAxil` common-clock register/control coverage, and - `AxiStreamBatcherEventBuilder` two-source INDEXED/ROUTED integration coverage - are validated locally. + `AxiStreamBatcherAxil` common-clock plus async AXI-Lite register/control + coverage, and `AxiStreamBatcherEventBuilder` two-source INDEXED/ROUTED + integration coverage are validated locally. - Current target: keep future work focused on intentionally deferred generic - breadth, async AXI-Lite behavior, or specific bug-driven edge cases. + breadth or specific bug-driven edge cases. ## Decisions - Use a standalone leaf-first strategy. @@ -21,7 +21,7 @@ ## Draft Work In This Session - Added a thin cocotb-facing wrapper at `protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd`. -- Added a common-clock AXI-Lite wrapper at +- Added a common/async-clock AXI-Lite wrapper at `protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd`. - Added a two-source event-builder wrapper at `protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd`. @@ -41,14 +41,17 @@ - Covered `AxiStreamBatcherAxil` reset/readback for the documented register map, control propagation for max-subframe count, byte threshold, and clock gap, `softRst` recovery from a partial superframe, and `blowoff` accept/drop - behavior followed by normal recovery traffic. + behavior followed by normal recovery traffic in both common-clock and + independent AXI-Lite clock configurations. The async readback assertions allow + the CDC bridge to settle to the expected value before checking each register. - Covered `AxiStreamBatcherEventBuilder` in small two-source INDEXED and ROUTED configurations: reset/status/readback, source selection, TDEST remap including fixed/passthrough routed bits, null source counting without forwarding, timeout drop behavior for a missing source followed by a clean later event, shared-output backpressure while both inputs contribute to an event, bypass skip/recovery behavior, blowoff drop/recovery behavior, and routed - transition-frame preemption through `TRANS_TDEST_G`. + transition-frame preemption through `TRANS_TDEST_G`. The routed sweep now + includes one alternate route-table shape and non-default transition TDEST. - The event-builder tests deliberately reuse the leaf byte-stream helpers for final batcher output shape instead of duplicating the full packet grammar. @@ -56,11 +59,9 @@ - Phase 1 is intentionally limited to V2 at the default 8-byte width. V1 and non-default stream widths remain targeted follow-ups if future changes touch those branches. -- Phase 2 is intentionally limited to `COMMON_CLOCK_G=true`. Async AXI-Lite - crossing behavior remains open. - Phase 3 is intentionally limited to a two-source event-builder wrapper. - Broader source-count matrices, alternate route tables, and exhaustive - transition/bypass timing permutations remain out of the current pass. + Broader source-count matrices and exhaustive transition/bypass timing + permutations remain out of the current pass. ## Validation - `./.venv/bin/vsg -c vsg-linter.yml -f protocols/batcher/wrappers/AxiStreamBatcherWrapper.vhd protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd` @@ -68,7 +69,7 @@ - `PYTHONPYCACHEPREFIX=/private/tmp/surf-pycache ./.venv/bin/python -m py_compile tests/protocols/batcher/batcher_test_utils.py tests/protocols/batcher/test_AxiStreamBatcher.py tests/protocols/batcher/test_AxiStreamBatcherAxil.py tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py` passed. - `./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` passed with - `4 passed`. + `6 passed`. - Stale simulator process sweep did not show leftover `ghdl`, `pytest`, or cocotb batcher processes. - `git diff --check` passed for tracked changes. The new batcher files are @@ -79,9 +80,9 @@ 1. Keep Phase 1 intentionally narrow unless a change touches the batcher leaf: possible next leaf additions are a small V1/power-of-two-width case or more adverse `forceTerm` timing. -2. If Phase 2 deepens, stay focused on wrapper-specific behavior such as async - AXI-Lite crossing or additional malformed/blowoff timing; do not duplicate - the full leaf byte grammar. +2. If Phase 2 deepens, stay focused on wrapper-specific behavior such as + additional malformed/blowoff or reset timing; do not duplicate the full leaf + byte grammar. 3. If Phase 3 deepens, add only targeted event-builder cases such as more - source-count/generic breadth, alternate route tables, external-only blowoff - behavior, or bug-driven transition/bypass timing. + source-count/generic breadth, external-only blowoff behavior, or bug-driven + transition/bypass timing. diff --git a/docs/plans/rtl-regression/handoff.md b/docs/plans/rtl-regression/handoff.md index fcc41ac7b0..ff8ffc7f6e 100644 --- a/docs/plans/rtl-regression/handoff.md +++ b/docs/plans/rtl-regression/handoff.md @@ -199,19 +199,21 @@ backpressure hold, and reset recovery after a partial superframe. The validated AXI-Lite wrapper slice covers reset/readback for the documented register map, control propagation for max-subframe count, byte threshold, and clock gap, `softRst` recovery from a partial superframe, and `blowoff` accept/drop behavior -followed by normal recovery. The validated event-builder slice covers two-source -INDEXED/ROUTED source selection, TDEST remap including fixed/passthrough routed -bits, null counting without forwarding, timeout drop for a missing source -followed by a clean later event, shared-output backpressure while both inputs -contribute to an event, bypass skip/recovery, blowoff drop/recovery, routed -transition-frame preemption, and visible counter/status readback. The latest +followed by normal recovery in both common and independent AXI-Lite clock modes. +The validated event-builder slice covers two-source INDEXED/ROUTED source +selection, TDEST remap including fixed/passthrough routed bits and one alternate +route table, non-default transition TDEST, null counting without forwarding, +timeout drop for a missing source followed by a clean later event, +shared-output backpressure while both inputs contribute to an event, bypass +skip/recovery, blowoff drop/recovery, routed transition-frame preemption, and +visible counter/status readback. The latest focused validation is -`./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` (`4 passed`), +`./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` (`6 passed`), with clean wrapper `vsg`, Python `py_compile`, stale-process sweep, and `git diff --check`. Possible next steps are a small V1/power-of-two leaf case, -deeper AXI-Lite async/adverse reset timing, or targeted event-builder breadth -such as more source-count/generic cases, alternate route tables, external-only -blowoff, or bug-driven transition/bypass timing. +deeper AXI-Lite adverse reset timing, or targeted event-builder breadth such as +more source-count/generic cases, external-only blowoff, or bug-driven +transition/bypass timing. If the user keeps the focus on `protocols/srp`, the main review findings and high-value coverage additions are complete. The optional remaining SRP follow-up is deeper timeout or posted-write disabled-op permutations if a future change touches those RTL branches. The latest focused SRP validation command is `./.venv/bin/python -m pytest -n 0 -q tests/protocols/srp`, and it passed locally with `23 passed`. diff --git a/docs/plans/rtl-regression/progress.md b/docs/plans/rtl-regression/progress.md index dcf082702e..ff50cf5c41 100644 --- a/docs/plans/rtl-regression/progress.md +++ b/docs/plans/rtl-regression/progress.md @@ -12,7 +12,7 @@ - The axi-first pass is complete through the previously remaining final 11 `axi/` modules. - The current `verification-2` branch has been refreshed by merging the current `origin/pre-release` tip. The validated `protocols/ssi`, `protocols/pgp`, current Ethernet waves (`EthMacCore`, `RawEthFramer`, `UdpEngine`, `IpV4Engine`, and the current pure-VHDL RoCEv2 quartet), current CoaXPress status/EOFE work, SRP follow-up work, and base-depth pass are all part of the present branch snapshot. - The current packetizer pass started with standalone tests for individual VHDL modules, not a loopback-as-oracle bench. `AxiStreamPacketizer`, `AxiStreamDepacketizer`, `AxiStreamPacketizer2`, `AxiStreamDepacketizer2`, and `AxiStreamBytePacker` now have direct cocotb coverage through checked-in wrappers, and `AxiStreamPacketizer2LoopbackWrapper` adds a narrow end-to-end V2 CRC-mode loopback check after those leaf contracts are pinned down. The packetizer/depacketizer wrappers expose the full per-byte `TUSER` vectors needed by `TUSER_FIRST_LAST` semantics. The legacy V0 tests cover both tail encodings, max-size split/continuation state, output backpressure hold, malformed-continuation bleed/recovery, and normal recovery framing. The V2 tests now cover partial final `TKEEP`, split-frame sequence state, sequence-counter wrap at `SEQ_CNT_SIZE_G=4`, interleaved `TDEST` rearbitration, `TDEST_BITS_G=0/1/2` loopback behavior, exact and one-byte-over `maxPktBytes` boundary splitting, output backpressure hold, CRC NONE/DATA/FULL packetizer tail/header behavior, DATA/FULL bad-CRC rejection, CRC-none tail-error marking, bad-version and bad-CRC-mode header error paths, link-drop recovery, mid-frame link-drop termination/recovery, and V2 packetizer/depacketizer loopback across CRC modes. The byte-packer wrapper now sweeps multiple compressed-keep input/output byte-width pairs: 1-to-8, 2-to-5, 3-to-6, 3-to-7, 4-to-8, 5-to-7, and 7-to-8, including a zero-keep input-beat guardrail. - - The batcher pass has the same standalone-first shape. `AxiStreamBatcher` now has a thin checked-in wrapper under `protocols/batcher/wrappers/` and a V2/default-8-byte cocotb regression under `tests/protocols/batcher/`. The validated leaf slice covers compacted V2 header/payload/tail bytes through the gearbox path, subframe tail metadata, multi-subframe superframes, termination by max-subframe count, idle gap, byte threshold, forced termination with terminal `EOFE`, output backpressure hold, and reset recovery after a partial superframe. `AxiStreamBatcherAxil` has a narrow common-clock wrapper regression for register reset/readback, control propagation, `softRst`, and `blowoff`. `AxiStreamBatcherEventBuilder` now has a focused two-source INDEXED/ROUTED integration regression for source selection, TDEST remap, null counting, timeout drop/recovery, shared-output backpressure, bypass recovery, blowoff drop/recovery, transition-frame preemption, and counter/status readback. + - The batcher pass has the same standalone-first shape. `AxiStreamBatcher` now has a thin checked-in wrapper under `protocols/batcher/wrappers/` and a V2/default-8-byte cocotb regression under `tests/protocols/batcher/`. The validated leaf slice covers compacted V2 header/payload/tail bytes through the gearbox path, subframe tail metadata, multi-subframe superframes, termination by max-subframe count, idle gap, byte threshold, forced termination with terminal `EOFE`, output backpressure hold, and reset recovery after a partial superframe. `AxiStreamBatcherAxil` has a narrow common/async-clock wrapper regression for register reset/readback, control propagation, `softRst`, and `blowoff`. `AxiStreamBatcherEventBuilder` now has a focused two-source INDEXED/ROUTED integration regression for source selection, TDEST remap including an alternate routed table, non-default transition TDEST, null counting, timeout drop/recovery, shared-output backpressure, bypass recovery, blowoff drop/recovery, transition-frame preemption, and counter/status readback. - The retained RTL-regression planning files now live under `docs/plans/rtl-regression/` instead of the old `docs/_meta/rtl_regression_*` paths. - The old checked-in graph/queue artifacts have been removed from the task planning directory; regenerate them only as temporary one-off analysis if needed. - Keep the done/open frontier in this progress file and in `docs/plans/rtl-regression/handoff.md` aligned to the actual tree. @@ -179,7 +179,7 @@ `tests/protocols/batcher/test_AxiStreamBatcherAxil.py` plus `tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py`. Latest focused validation is `./.venv/bin/python -m pytest -n 0 -q tests/protocols/batcher` - (`4 passed`), plus clean wrapper `vsg`, Python `py_compile`, stale-process + (`6 passed`), plus clean wrapper `vsg`, Python `py_compile`, stale-process sweep, and `git diff --check`. - The user-requested `protocols/srp` review fixes are complete: `test_SrpV3Axi.py` now reuses the shared SRPv3 helper/model layer, `test_SrpV3Core.py` uses decorator-based cocotb test selection, the stray SRPv3 AXI-Lite debug logging is removed, and the high-value SRP coverage additions are checked in locally. - Preserve the recent `pgp4` lesson for later PGP work: when the simulation wrapper only exposes stable lock/config surfaces, write the bench around those explicit contracts instead of claiming recovered payload coverage. @@ -191,10 +191,10 @@ - If implementation continues in `protocols/packetizer`, the standalone V0/V2 packetizer, depacketizer, and byte-packer leaves now have expanded direct coverage, and the V2 packetizer/depacketizer path has a narrow CRC-mode loopback. The next practical step is either deeper negative/error coverage for specific packetizer behavior or moving to another protocol leaf. - If implementation continues in `protocols/batcher`, keep the next work leaf, wrapper-specific, or event-builder-specific: either add a small - V1/power-of-two leaf case, deepen `AxiStreamBatcherAxil` around async - crossing or adverse reset/blowoff timing, or deepen event-builder coverage - with targeted source-count/generic breadth, alternate route tables, - external-only blowoff, or bug-driven transition/bypass timing. + V1/power-of-two leaf case, deepen `AxiStreamBatcherAxil` around adverse + reset/blowoff timing, or deepen event-builder coverage with targeted + source-count/generic breadth, external-only blowoff, or bug-driven + transition/bypass timing. - If implementation resumes on Ethernet/RoCEv2, the next real step is enabling a mixed-language cocotb path for `EthMacCrcAxiStreamWrapperSend`, `EthMacCrcAxiStreamWrapperRecv`, `EthMacTxRoCEv2`, `EthMacRxRoCEv2`, and `RoceEngineWrapper` against the real `blue-*` dependencies. ## Blockers And Risks diff --git a/protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd b/protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd index 295dcc6a96..22fc48a489 100644 --- a/protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd +++ b/protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd @@ -28,12 +28,15 @@ entity AxiStreamBatcherAxilWrapper is MAX_NUMBER_SUB_FRAMES_G : positive := 32; SUPER_FRAME_BYTE_THRESHOLD_G : natural := 8192; MAX_CLK_GAP_G : natural := 256; + COMMON_CLOCK_G : boolean := true; INPUT_PIPE_STAGES_G : natural := 0; OUTPUT_PIPE_STAGES_G : natural := 1; AXIL_ADDR_WIDTH_G : positive := 12); port ( axisClk : in sl; axisRst : in sl; + axilClkIn : in sl := '0'; + axilRstIn : in sl := '1'; idle : out sl; S_AXIS_TVALID : in sl; S_AXIS_TDATA : in slv(8*DATA_BYTES_G-1 downto 0); @@ -83,6 +86,8 @@ architecture rtl of AxiStreamBatcherAxilWrapper is TUSER_BITS_C => 8, TUSER_MODE_C => TUSER_FIRST_LAST_C); + signal axilHostClk : sl; + signal axilHostRst : sl; signal axilRstN : sl; signal axilClk : sl; signal axilRst : sl; @@ -97,7 +102,9 @@ architecture rtl of AxiStreamBatcherAxilWrapper is begin - axilRstN <= not axisRst; + axilHostClk <= axisClk when COMMON_CLOCK_G else axilClkIn; + axilHostRst <= axisRst when COMMON_CLOCK_G else axilRstIn; + axilRstN <= not axilHostRst; ------------------------ -- AXI-Lite bus shim -- @@ -108,7 +115,7 @@ begin HAS_WSTRB => 1, ADDR_WIDTH => AXIL_ADDR_WIDTH_G) port map ( - S_AXI_ACLK => axisClk, -- [in] + S_AXI_ACLK => axilHostClk, -- [in] S_AXI_ARESETN => axilRstN, -- [in] S_AXI_AWADDR => S_AXI_AWADDR, -- [in] S_AXI_AWPROT => S_AXI_AWPROT, -- [in] @@ -184,7 +191,7 @@ begin generic map ( TPD_G => TPD_G, VERSION_G => VERSION_G, - COMMON_CLOCK_G => true, + COMMON_CLOCK_G => COMMON_CLOCK_G, MAX_NUMBER_SUB_FRAMES_G => MAX_NUMBER_SUB_FRAMES_G, SUPER_FRAME_BYTE_THRESHOLD_G => SUPER_FRAME_BYTE_THRESHOLD_G, MAX_CLK_GAP_G => MAX_CLK_GAP_G, diff --git a/protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd b/protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd index 9c3b4c119d..16fd7cf896 100644 --- a/protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd +++ b/protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd @@ -22,14 +22,15 @@ use surf.AxiStreamPkg.all; entity AxiStreamBatcherEventBuilderWrapper is generic ( - TPD_G : time := 1 ns; - VERSION_G : positive range 1 to 2 := 2; - MODE_G : string := "INDEXED"; - DATA_BYTES_G : positive range 8 to 8 := 8; - INPUT_PIPE_STAGES_G : natural := 0; - OUTPUT_PIPE_STAGES_G : natural := 1; - AXIL_ADDR_WIDTH_G : positive := 12; - TRANS_TDEST_G : slv(7 downto 0) := x"FF"); + TPD_G : time := 1 ns; + VERSION_G : positive range 1 to 2 := 2; + MODE_G : string := "INDEXED"; + DATA_BYTES_G : positive range 8 to 8 := 8; + ROUTE_MODE_G : natural range 0 to 1 := 0; + INPUT_PIPE_STAGES_G : natural := 0; + OUTPUT_PIPE_STAGES_G : natural := 1; + AXIL_ADDR_WIDTH_G : positive := 12; + TRANS_TDEST_G : natural range 0 to 255 := 255); port ( axisClk : in sl; axisRst : in sl; @@ -84,6 +85,15 @@ architecture rtl of AxiStreamBatcherEventBuilderWrapper is constant NUM_SLAVES_C : positive := 2; + function route1 (mode : natural) return slv is + begin + if mode = 0 then + return "0101----"; + else + return "1010--11"; + end if; + end function route1; + constant AXIS_CONFIG_C : AxiStreamConfigType := ( TSTRB_EN_C => false, TDATA_BYTES_C => DATA_BYTES_G, @@ -95,7 +105,7 @@ architecture rtl of AxiStreamBatcherEventBuilderWrapper is constant TDEST_ROUTES_C : Slv8Array(NUM_SLAVES_C-1 downto 0) := ( 0 => "--------", - 1 => "0101----"); + 1 => route1(ROUTE_MODE_G)); signal axilRstN : sl; signal axilClk : sl; @@ -221,7 +231,7 @@ begin MODE_G => MODE_G, TDEST_ROUTES_G => TDEST_ROUTES_C, TDEST_LOW_G => 0, - TRANS_TDEST_G => TRANS_TDEST_G, + TRANS_TDEST_G => toSlv(TRANS_TDEST_G, 8), AXIS_CONFIG_G => AXIS_CONFIG_C, INPUT_PIPE_STAGES_G => INPUT_PIPE_STAGES_G, OUTPUT_PIPE_STAGES_G => OUTPUT_PIPE_STAGES_G) diff --git a/tests/protocols/batcher/test_AxiStreamBatcherAxil.py b/tests/protocols/batcher/test_AxiStreamBatcherAxil.py index 3c1151af38..ce821fc538 100644 --- a/tests/protocols/batcher/test_AxiStreamBatcherAxil.py +++ b/tests/protocols/batcher/test_AxiStreamBatcherAxil.py @@ -9,21 +9,24 @@ ############################################################################## # Test methodology: -# - Sweep: Use the `AxiStreamBatcherAxil` wrapper in V2 mode with common AXI-Lite -# and stream clocks, matching the stable first control-surface target from the -# batcher regression plan. +# - Sweep: Use the `AxiStreamBatcherAxil` wrapper in V2 mode with common and +# independent AXI-Lite/stream clocks, matching the control-surface targets +# from the batcher regression plan. # - Stimulus: Program the runtime threshold, max-subframe, max-clock-gap, # `softRst`, and `blowoff` registers through a cocotb AXI-Lite master while # driving flat AXI Stream subframes through the wrapped batcher. # - Checks: Register reset values and readback must match the RTL/PyRogue map, # and control writes must change stream-side termination or drop/reset behavior # without re-proving every batcher payload byte beyond the leaf helper model. -# - Timing: AXI-Lite transactions and stream ready/valid handshakes share one -# clock, and the blowoff/reset checks include no-output windows after traffic -# is accepted. +# - Timing: The common-clock case keeps readback strict, while the async case +# waits for the expected register value through the CDC bridge before using +# writes to steer stream-side behavior. + +import os import cocotb import pytest +from cocotb.clock import Clock from cocotb.triggers import with_timeout from cocotbext.axi import AxiLiteBus, AxiLiteMaster @@ -55,23 +58,61 @@ class TB: def __init__(self, dut): self.dut = dut + self.common_clock = os.environ.get("COMMON_CLOCK_G", "True").lower() == "true" self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") - self.axil = AxiLiteMaster(AxiLiteBus.from_prefix(dut, "S_AXI"), dut.axisClk, dut.axisRst) start_batcher_clock(dut) dut.axisRst.setimmediatevalue(1) + dut.axilClkIn.setimmediatevalue(0) + dut.axilRstIn.setimmediatevalue(1) + if self.common_clock: + self.axil_clk = dut.axisClk + self.axil_rst = dut.axisRst + else: + cocotb.start_soon(Clock(dut.axilClkIn, 7.0, unit="ns").start()) + self.axil_clk = dut.axilClkIn + self.axil_rst = dut.axilRstIn + self.axil = AxiLiteMaster(AxiLiteBus.from_prefix(dut, "S_AXI"), self.axil_clk, self.axil_rst) + dut.M_AXIS_TREADY.setimmediatevalue(0) self.source.set_idle() async def reset(self): - await reset_batcher_dut(self.dut) + if self.common_clock: + await reset_batcher_dut(self.dut) + else: + self.dut.axisRst.setimmediatevalue(1) + self.dut.axilRstIn.setimmediatevalue(1) + await cycle(self.dut.axisClk, 8) + await cycle(self.dut.axilClkIn, 8) + self.dut.axisRst.value = 0 + await cycle(self.dut.axisClk, 16) + self.dut.axilRstIn.value = 0 + await cycle(self.dut.axisClk, 64) + await cycle(self.dut.axilClkIn, 64) async def read(self, address: int) -> int: return await with_timeout(axil_read_u32(self.axil, address), 2, "us") + async def read_eventually(self, address: int, expected: int) -> None: + # The async bridge can return reset/CDC-latency responses before the + # target register response reaches the AXI-Lite side. + observed = [] + for _ in range(8): + observed.append(await self.read(address)) + if observed[-1] == expected: + return + await cycle(self.axil_clk, 4) + raise AssertionError( + f"AXI-Lite readback at 0x{address:02X} never reached " + f"0x{expected:08X}; observed {observed}" + ) + async def write(self, address: int, value: int) -> None: await with_timeout(axil_write_u32(self.axil, address, value), 2, "us") + if not self.common_clock: + await cycle(self.dut.axisClk, 16) @cocotb.test() @@ -81,19 +122,30 @@ async def register_reset_and_readback_test(dut): # The reset values mirror `AxiStreamBatcherAxil` generics and the PyRogue # register map. Status bit 0 is idle and bits 27:24 report VERSION_G. - assert await tb.read(SUPER_FRAME_BYTE_THRESHOLD_ADDR) == 8192 - assert await tb.read(MAX_SUB_FRAMES_ADDR) == 32 - assert await tb.read(MAX_CLK_GAP_ADDR) == 256 - assert await tb.read(STATUS_ADDR) == 0x02000001 + if tb.common_clock: + assert await tb.read(SUPER_FRAME_BYTE_THRESHOLD_ADDR) == 8192 + assert await tb.read(MAX_SUB_FRAMES_ADDR) == 32 + assert await tb.read(MAX_CLK_GAP_ADDR) == 256 + assert await tb.read(STATUS_ADDR) == 0x02000001 + else: + await tb.read_eventually(SUPER_FRAME_BYTE_THRESHOLD_ADDR, 8192) + await tb.read_eventually(MAX_SUB_FRAMES_ADDR, 32) + await tb.read_eventually(MAX_CLK_GAP_ADDR, 256) + await tb.read_eventually(STATUS_ADDR, 0x02000001) # Write/readback checks keep the control register map pinned before the # stream-side tests rely on these fields to steer termination behavior. await tb.write(SUPER_FRAME_BYTE_THRESHOLD_ADDR, 24) await tb.write(MAX_SUB_FRAMES_ADDR, 2) await tb.write(MAX_CLK_GAP_ADDR, 5) - assert await tb.read(SUPER_FRAME_BYTE_THRESHOLD_ADDR) == 24 - assert await tb.read(MAX_SUB_FRAMES_ADDR) == 2 - assert await tb.read(MAX_CLK_GAP_ADDR) == 5 + if tb.common_clock: + assert await tb.read(SUPER_FRAME_BYTE_THRESHOLD_ADDR) == 24 + assert await tb.read(MAX_SUB_FRAMES_ADDR) == 2 + assert await tb.read(MAX_CLK_GAP_ADDR) == 5 + else: + await tb.read_eventually(SUPER_FRAME_BYTE_THRESHOLD_ADDR, 24) + await tb.read_eventually(MAX_SUB_FRAMES_ADDR, 2) + await tb.read_eventually(MAX_CLK_GAP_ADDR, 5) @cocotb.test() @@ -257,11 +309,22 @@ async def blowoff_drops_accepted_input_test(dut): { "VERSION_G": 2, "DATA_BYTES_G": 8, + "COMMON_CLOCK_G": True, "INPUT_PIPE_STAGES_G": 0, "OUTPUT_PIPE_STAGES_G": 1, }, id="v2_8byte_common_clock", ), + pytest.param( + { + "VERSION_G": 2, + "DATA_BYTES_G": 8, + "COMMON_CLOCK_G": False, + "INPUT_PIPE_STAGES_G": 0, + "OUTPUT_PIPE_STAGES_G": 1, + }, + id="v2_8byte_async_axil", + ), ], ) def test_AxiStreamBatcherAxil(parameters): diff --git a/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py b/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py index 0e883ad35e..c61e742a84 100644 --- a/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py +++ b/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py @@ -9,12 +9,13 @@ ############################################################################## # Test methodology: -# - Sweep: Use a two-input `AxiStreamBatcherEventBuilder` wrapper in INDEXED and -# ROUTED modes. This keeps the event-builder policy visible without building -# an exhaustive source-count matrix. +# - Sweep: Use a two-input `AxiStreamBatcherEventBuilder` wrapper in INDEXED, +# ROUTED, and one alternate routed table/transition-TDEST configuration. This +# keeps event-builder policy visible without building an exhaustive +# source-count matrix. # - Stimulus: Drive small AXI Stream frames on one or both inputs, program the -# event-builder AXI-Lite bypass/timeout controls, and use one transition TDEST -# case in routed mode. +# event-builder AXI-Lite bypass/timeout controls, and use transition TDEST +# cases in routed modes. # - Checks: Assert source-selection policy, TDEST remap, null/transition/timeout # counters, bypass/drop behavior, and the final batcher byte stream shape # through the shared leaf byte helpers. @@ -63,6 +64,8 @@ class TB: def __init__(self, dut): self.dut = dut self.mode = os.environ.get("MODE_G", "INDEXED").strip("'").strip('"') + self.route_mode = int(os.environ.get("ROUTE_MODE_G", "0")) + self.trans_tdest = int(os.environ.get("TRANS_TDEST_G", "255")) self.source0 = FlatAxisEndpoint(dut, prefix="S0_AXIS") self.source1 = FlatAxisEndpoint(dut, prefix="S1_AXIS") self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") @@ -87,7 +90,9 @@ async def write(self, address: int, value: int) -> None: def remapped_dest(self, index: int, original: int) -> int: if self.mode == "ROUTED": if index == 1: - return 0x50 | (original & 0x0F) + if self.route_mode == 0: + return 0x50 | (original & 0x0F) + return 0xA0 | (original & 0x0C) | 0x03 return original & 0xFF return index @@ -112,7 +117,7 @@ async def register_status_and_remap_test(dut): await tb.reset() # Pin down the management map before relying on counters later in the file. - assert await tb.read(TRANS_TDEST_ADDR) == 0xFF + assert await tb.read(TRANS_TDEST_ADDR) == tb.trans_tdest assert await tb.read(STATUS_ADDR) == 0x02000002 assert await tb.read(BYPASS_ADDR) == 0 assert await tb.read(TIMEOUT_ADDR) == 0 @@ -347,7 +352,7 @@ async def routed_transition_frame_preempts_event_test(dut): if tb.mode != "ROUTED": return - transition = _frame(bytes(range(0x80, 0x85)), dest=0xFF, first_user=0x91, last_user=0xF1) + transition = _frame(bytes(range(0x80, 0x85)), dest=tb.trans_tdest, first_user=0x91, last_user=0xF1) blocked = AxisBeat( data=word_from_bytes(bytes(range(0x90, 0x98))), keep=0xFF, @@ -356,7 +361,7 @@ async def routed_transition_frame_preempts_event_test(dut): user=0xA1, ) expected = [ - _frame(transition[0], dest=0xFF, first_user=transition[2], last_user=transition[3]), + _frame(transition[0], dest=tb.trans_tdest, first_user=transition[2], last_user=transition[3]), ] # Hold source 1 valid to prove the transition path selects only the source @@ -384,6 +389,8 @@ async def routed_transition_frame_preempts_event_test(dut): { "VERSION_G": 2, "MODE_G": "INDEXED", + "ROUTE_MODE_G": 0, + "TRANS_TDEST_G": 255, "INPUT_PIPE_STAGES_G": 0, "OUTPUT_PIPE_STAGES_G": 1, }, @@ -393,11 +400,24 @@ async def routed_transition_frame_preempts_event_test(dut): { "VERSION_G": 2, "MODE_G": "ROUTED", + "ROUTE_MODE_G": 0, + "TRANS_TDEST_G": 255, "INPUT_PIPE_STAGES_G": 0, "OUTPUT_PIPE_STAGES_G": 1, }, id="routed_v2_2src", ), + pytest.param( + { + "VERSION_G": 2, + "MODE_G": "ROUTED", + "ROUTE_MODE_G": 1, + "TRANS_TDEST_G": 165, + "INPUT_PIPE_STAGES_G": 0, + "OUTPUT_PIPE_STAGES_G": 1, + }, + id="routed_alt_route_trans", + ), ], ) def test_AxiStreamBatcherEventBuilder(parameters):