diff --git a/docs/plans/rtl-regression/README.md b/docs/plans/rtl-regression/README.md new file mode 100644 index 0000000000..e10d253fae --- /dev/null +++ b/docs/plans/rtl-regression/README.md @@ -0,0 +1,14 @@ +# RTL Regression + +This task tracks the repo-wide SURF RTL regression rollout. + +## Files +- [plan.md](plan.md): policy, methodology, scope, and acceptance criteria. +- [progress.md](progress.md): current status, validated coverage, decisions, and log. +- [handoff.md](handoff.md): quick-resume context and next-step guidance. +- [inventory.yaml](inventory.yaml): structured module inventory scaffold. + +## Current Status +- Phase-1 implementation is active. +- Planning is user-directed rather than generated-queue driven. +- Generated graph and queue artifacts are temporary analysis outputs only. diff --git a/docs/plans/rtl-regression/handoff.md b/docs/plans/rtl-regression/handoff.md new file mode 100644 index 0000000000..cb3ff34e57 --- /dev/null +++ b/docs/plans/rtl-regression/handoff.md @@ -0,0 +1,235 @@ +# SURF RTL Regression Handoff + +## Objective +- Build a repo-wide regression system for synthesizable SURF RTL. +- Keep all executable test logic in Python. +- Use `pytest + cocotb + GHDL + ruckus`. +- Keep VHDL only for wrappers, shims, and required simulation models. + +## Chosen Constraints +- Python-only test logic +- VHDL wrappers allowed +- Whole-repo target +- Vendor-heavy modules deferred in phase 1 +- Comment new Python regression code at a tutorial level, assuming the reader may be new to cocotb +- Give each Python regression the normal SURF/SLAC file header and two distinct comment layers: a module-specific `Test methodology` block under that header and tutorial-style comments in the executable code body +- Give each checked-in cocotb-facing `*IpIntegrator.vhd` wrapper the normal SURF file banner plus section comments for shim setup, DUT instantiation, and any flattening/status wiring +- For any VHDL file created or edited during this work, run `./.venv/bin/vsg` with `vsg-linter.yml`, the same config CI uses, and use `--fix`/autofix on fixable issues before moving on +- Treat VHDL packages as transitively covered unless a behavioral function/procedure needs a dedicated wrapper +- Treat stale simulator cleanup as part of task completion: after any `pytest`, cocotb, GHDL, or similar launched verification step, sweep for leftover child processes and kill them before moving on + +## Quick Resume Snapshot +- Current frontier: the axi-first pass is complete, and `verification-2` has been refreshed against the current `origin/pre-release` tip while moving the retained RTL-regression planning files into `docs/plans/rtl-regression/`. The branch line includes the landed `protocols/ssi` and `protocols/pgp` waves from `pre-release`, the current Ethernet coverage spans `EthMacCore`, `RawEthFramer`, `UdpEngine`, `IpV4Engine`, and the pure-VHDL RoCEv2 quartet (`EthMacPrepareForICrc`, `EthMacRxCheckICrc`, `RoceResizeAndSwap`, and `RoceConfigurator`), and a broader user-directed CoaXPress pure-VHDL wave is checked in under `tests/protocols/coaxpress/`. The validated CoaXPress subset now includes the receive quartet `CoaXPressRxWordPacker`, `CoaXPressRxLaneMux`, `CoaXPressRxLane`, and `CoaXPressRxHsFsm`, the receive assembly `CoaXPressRx`, the transmit/bridge/config helpers `CoaXPressEventAckMsg`, `CoaXPressTxLsFsm`, `CoaXPressConfig`, `CoaXPressOverFiberBridgeRx`, and `CoaXPressOverFiberBridgeTx`, and the higher-level assemblies `CoaXPressTx`, `CoaXPressCore`, and `CoaXPressOverFiberBridge`. The SRP follow-up has a widened SRPv3 AXI protocol matrix in `tests/protocols/srp/test_SrpV3Axi.py` covering read/write/post/null flows, response backpressure, TDEST propagation, and representative protocol-error footers through the checked-in `SrpV3AxiWrapper`, plus the later SRPv3 core/AXI-Lite and SRPv0 coverage noted below. Task selection is now user-directed rather than queue-driven, so the planning docs must track the real done/open frontier directly. +- Current axi frontier: complete for the intended simulator-friendly pass in this branch snapshot; do not resume from the older stale `AxiResize` note. +- Current validated-open issues: + - The larger Ethernet families `GigEthCore`, `TenGigEthCore`, `XauiCore`, `XlauiCore`, and `Caui4Core` remain untouched in phase 1, while the remaining RoCEv2 gap is the mixed-language bench path for the five RTL entities that instantiate generated submodules: `EthMacCrcAxiStreamWrapperSend`, `EthMacCrcAxiStreamWrapperRecv`, `EthMacTxRoCEv2`, `EthMacRxRoCEv2`, and `RoceEngineWrapper`. + - `EthMacRxImportXlgmii` and `EthMacTxExportXlgmii` are still placeholder no-op RTL; the checked-in tests now lock down that inert contract instead of claiming functional XLGMII support. + - `CoaXPressConfig` is now active and validated through the real `CoaXPressConfig` / `SrpV3AxiLite` ingress path. The bench covers all four tagged/untagged read/write command-format quadrants, command CRC words, tag incrementing, SRPv3 response completion after a config receive acknowledgment, and timeout/nonzero-ack-status error footer behavior. The current CoaXPress receive benches also include `CoaXPressRxHsFsm` incomplete-frame new-header detection plus a dual-lane step/alignment case, multi-lane `CoaXPressRx` lane-rotation cases, and receive-lane control-ack, heartbeat, event, and stream-data CRC/`EOP` trailer validation. Receive-lane event payload words are now buffered until CRC/`EOP` validation, exported on `eventMaster`, crossed by `CoaXPressRx` into `cfgClk`, and covered at the lane and receive-assembly levels while the legacy `eventAck/eventTag` pulse remains trailer-gated. Receive-lane malformed-packet pulses now aggregate into `CoaXPressRx.rxFsmError` and the existing core `RxFsmErrorCnt` software counter. The receive side is still intentionally mixed-depth: bounded event payloads are validate-before-release, but stream-data payloads are forwarded immediately. Stream-data trailer failures now follow the SURF SSI convention: the lane emits an in-order trailer verdict marker, the receive image FSM holds only the final packed SSI EOF beat until that verdict arrives, and malformed CRC/`EOP` trailers set SSI `EOFE` on that final beat rather than retroactively dropping payload. + - The latest CXPoF bridge work is now reflected at both leaf and top level: `CoaXPressOverFiberBridgeRx` covers embedded EOP K-code reconstruction, HKP-to-payload mixing, an HKP-carried CXP EOP word, malformed lane-placement checks for `/S/`, `/Q/`, `/T/`, and `/E/`, `/Q/` ordered-set sequence policy, classified `/E/` and malformed-condition status, HKP K-code validation/classification, and `/E/` abort/recovery before and after payload, while `CoaXPressOverFiberBridge` covers 64-bit RX gearbox traversal for classified `/E/` abort/recovery, HKP-to-payload status, and lane-0 `/Q/` sequence mismatch/no-output/recovery. These status fields are now grouped in the product-facing `CxpofRxStatusType` record; the cocotb regressions use leaf/top status-wrapper entities only to flatten the record for simulator visibility. The GTH/GTY CoaXPress-over-Fiber wrappers now instantiate `CoaXPressOverFiberBridgeAxiL`, so their AXI-Lite ports expose sticky bridge status bits, last-observed sequence/HKP fields, and event counters instead of default decode-error responses. + - `tests/protocols/srp/test_SrpV3Axi.py` is now the active SRPv3 AXI regression, not just the old legacy-style posted-write/readback smoke, and it reuses the shared SRPv3 helper/model layer from `tests/protocols/srp/srp_test_utils.py`. It validates non-posted write echo/readback, posted-write no-response behavior, NULL responses, response backpressure, TDEST propagation, full-word `TKEEP`, and footer bits for version mismatch, malformed write framing, invalid alignment, invalid request size, downstream write address error, and downstream read address error. `tests/protocols/srp/test_SrpV3Core.py` now covers direct reset/idle smoke for the default `SrpV3CoreWrapper` mode plus direct 32-bit malformed-header, immediate-read-error, disabled-read/write, missing-SOF blowoff, short-write framing, and early/late read-data TLAST EOFE behavior through `CORE_DATA_BYTES_G => 4`. That narrow mode exposed and now covers two real `SrpV3Core` bugs: the response-header counter was not reset when emitting an error response from a truncated request header, and `READ_S` could miss an immediate downstream read error before any payload beat arrived. SRPv0 now has direct bridge-half coverage plus loopback coverage: `tests/protocols/srp/test_AxiLiteSrpV0.py` checks request packing and bad-response handling through `AxiLiteSrpV0Wrapper`, `tests/protocols/srp/test_SrpV0AxiLite.py` checks legacy frame parsing/status/address expansion plus downstream AXI-Lite read/write error propagation through `SrpV0AxiLiteWrapper`, and `tests/protocols/srp/test_SrpV0Loopback.py` still covers `AxiLiteSrpV0` and `SrpV0AxiLite` together through the checked-in stream loopback wrapper. `tests/protocols/srp/test_SrpV3AxiLite.py` now carries active reset/idle smoke for direct, full, and legacy-wide `DATA_BYTES_G => 32` modes, active narrow probes for the direct and full wrappers, active directed regressions for the direct and full wrappers, one active legacy-wide directed regression, and direct-wrapper `ignoreMemResp` coverage for an AXI-Lite `SLVERR` read. The duplicate wide-wrapper probe/direct cases were removed instead of kept as skipped opt-in coverage, and the old wide/narrow-only SRP wrapper files were folded into generics, leaving the default `tests/protocols/srp` run skip-free. The direct narrow `SrpV3AxiLite` issue turned out to be a bench artifact, not an RTL defect: `tests/protocols/srp/srp_test_utils.py` now holds each source beat until a sampled clock edge confirms `TREADY`, which fixes the false failure on the original `SsiFrameLimiter` bypass configuration (`SLAVE_FIFO_G => false`). The focused `tests/protocols/ssi/test_SsiFrameLimiter.py` isolation regressions remain green in both limiter modes. + - The broader flat-stream helper cleanup is now partially checked in beyond SRP. `tests/axi/utils.py` owns the shared `wait_sampled_ready()` primitive, and the flattened helper layers in `tests/protocols/ssi/ssi_test_utils.py`, `tests/protocols/srp/srp_test_utils.py`, `tests/protocols/pgp/pgp4/pgp4_test_utils.py`, `tests/protocols/coaxpress/coaxpress_test_utils.py`, `tests/ethernet/EthMacCore/ethmac_test_utils.py`, and `tests/ethernet/RawEthFramer/raw_eth_test_utils.py` now use it where appropriate instead of open-coded sampled-ready loops. Direct one-off benches that were good fits for the same cleanup now include `tests/protocols/srp/test_SrpV3Axi.py`, `tests/protocols/ssi/test_SsiResizeFifoEofe.py`, `tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py`, `tests/protocols/coaxpress/test_CoaXPressTx.py`, `tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py`, and `tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py`. + - One subtle point from that cleanup is important for future edits: `wait_sampled_ready()` returns after the accepting clock edge has already occurred. A source must therefore advance or deassert immediately after the helper returns; leaving an extra post-handshake clock in place can create duplicate transfers or malformed streams. The current checked-in cleanup already corrected the false regressions that came from that mistake. + - The current validation subset for the flat-driver cleanup is `174 passed` across `tests/protocols/srp/test_SrpV3Axi.py`, `tests/protocols/ssi/test_SsiResizeFifoEofe.py`, the active PGP4 flat-wrapper subset, `tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py`, `tests/protocols/coaxpress/test_CoaXPressTx.py`, `tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py`, and `tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py`. + - Remaining intentionally manual AXI-style cases after the static sweep are `tests/protocols/pgp/pgp4/test_Pgp4Rx.py`, the interleaved protocol-word capture helper in `tests/protocols/pgp/pgp4/pgp4_test_utils.py`, and the manual stress path in `tests/axi/axi_stream/test_AxiStreamScatterGather.py`. Those should only be refactored if a richer shared helper is added that can observe outputs while a source beat is still in flight. + - The former CoaXPress RX known-issue benches have been promoted or reclassified: repeated single-line image frames now run directly in `CoaXPressRxHsFsm` and top-level `CoaXPressRx`, four-lane short-frame rotation/recovery now runs in the normal `CoaXPressRx` sweep, and the core RX backpressure counter check now uses long lines so `RxOverflowCnt` is exercised without producing `RxFsmErrorCnt`. The intentionally heavy four-lane overflow recovery workloads are opt-in stress checks behind `RUN_STRESS_TESTS=1`, not expected-open known issues. + - For future CoaXPress work, treat the named packet classes and control bytes in `tests/protocols/coaxpress/coaxpress_test_utils.py` as shared spec anchors, not as optional local style. Those constants were normalized against the same `CXP-001-2021` / `CXPR-008-2021` references cited in `CoaXPressPkg.vhd`. Keep the packet-layer names aligned to the spec even where current RTL ports still use legacy `eventAck` naming on the receive side: `0x07` is an event packet and `0x08` is an event acknowledgment. + - The current receive benches are intentionally mixed-depth: control-ack traffic is now driven with fuller spec-shaped framing, `CoaXPressRxLane` validates control-ack, heartbeat, bounded event payload, and stream-data CRC/`EOP` trailers, lane parser errors are visible through `rxFsmError`, and `CoaXPressRxHsFsm` detects a new image header before the prior frame's declared line count completes. The event payload path now has a bounded application-facing validate-before-release contract; do not describe stream-data receive as a buffered bad-payload dropper because the RTL still forwards stream payload before the trailer is validated. For malformed stream trailers, describe the contract as SSI terminal-error marking: final image `TUSER` carries `EOFE`, and downstream SSI consumers must reject or quarantine that frame. +- Current planning discipline: + - Use manual user-directed area selection as the active source of truth for what to work on next. + - Keep `docs/plans/rtl-regression/progress.md` and this handoff file aligned with the actual validated branch frontier. + - Do not use generated graph or queue artifacts as the next-module selector. If hierarchy analysis is useful, regenerate it temporarily with `scripts/build_rtl_instantiation_graph.py` and treat the output as disposable reference material. + - Prefer parallel pytest for routine local validation, especially cocotb subsystem slices: `-n auto --dist=worksteal` is the default shape unless a single simulation needs serial logs or interactive debugging. +- Current wrapper discipline: + - Prefer the existing subsystem `ip_integrator/` shim layers over bespoke record flattening. + - Keep first-pass wrapper benches intentionally narrow and document any omitted branches explicitly. + - Use `start_lockstep_clocks()` when a DUT depends on truly shared clock edges. + - Prefer explicit short sim-build keys for generated-wrapper benches when case metadata would otherwise create fragile build paths. + - When a wrapper is checked in, write it like the surrounding repo HDL: include the SLAC/SURF banner and enough section comments that a new session can identify the shim, DUT, and flattening regions quickly. + - For the current Ethernet slice, the checked-in wrappers under `ethernet/EthMacCore/wrappers/`, `ethernet/RawEthFramer/wrappers/`, `ethernet/UdpEngine/wrappers/`, `ethernet/IpV4Engine/wrappers/`, and the new `ethernet/RoCEv2/wrappers/` leaf adapters are the expected cocotb surfaces. Keep using those subsystem-local wrappers rather than rebuilding record-packing logic in Python. +- Current cocotb-file discipline: + - New test files should start with the standard SURF/SLAC header block. + - The `Test methodology` block belongs directly under that header. + - In-body tutorial comments are still required; the methodology block does not replace them. + +## Session Learnings To Preserve +- Start with the smallest stable wrapper that exposes the DUT cleanly to cocotb. Reuse the existing subsystem `ip_integrator/` shims before inventing bespoke flattening or a new subsystem-local wrapper. +- Prefer checked-in subsystem-local wrappers for durable integration patterns, including simulator-hostile generic adapters. +- If a Python cocotb file is permanent enough to check in, do not leave it with a custom or abbreviated header. Use the standard repo header immediately, then add the methodology block and tutorial comments in the same first pass. +- If a wrapper is permanent enough to check in, do not leave it as a bare anonymous adapter. Add the standard SURF banner and short section comments immediately, not as a cleanup pass later. +- For AXI and AXI-Lite benches, the practical first-pass shape is usually: + - cocotb protocol master on the control/request side, + - cocotb RAM or simple protocol model on the generated/response side, + - lightweight monitors on accepted handshakes when timing or burst shape matters. +- Do not rely on final memory contents alone when the contract includes timing-visible behavior. Record accepted handshakes if the bench is supposed to prove spacing, burst length, sideband propagation, partial-last-beat strobes, or arbitration order. +- For `COMMON_CLK_G` style wrappers, use one shared clock coroutine via `start_lockstep_clocks()` when the RTL expects true shared edges rather than merely equal nominal periods. +- For first-pass wrapper benches, prove the externally visible stable path first and defer shakier simulator-sensitive branches explicitly in the docs instead of stretching one bench to cover everything. +- `AxiStreamDmaV2Read` needed a real RTL/runtime fix rather than a bench workaround: keep the bounded byte-count conversion fix in `axi/axi4/rtl/AxiPkg.vhd` and the direct terminal-mask generation in `axi/dma/rtl/v2/AxiStreamDmaV2Read.vhd`. The current wrapper only exposes an 8-bit `TUSER`, so the observable contract in the checked-in bench is first-user propagation plus payload/keep/id/dest and descriptor return fields. +- `tests/dsp/generic/dsp_test_utils.py` is now the shared home for DSP-specific signed helpers, rolling reference models, cocotb clock-settle timing, and generated FIR wrappers. Reuse it instead of cloning DSP arithmetic or wrapper boilerplate. +- Before writing new AXI-Lite, AXI Stream, SSI, or ethernet transaction code, search the nearest subsystem `tests/` package for an existing helper module first. Future sessions should assume that a reusable helper probably already exists and should only write new transaction plumbing after confirming the local helper layer is insufficient. +- `tests/ethernet/EthMacCore/ethmac_test_utils.py` is now the shared home for the current Ethernet MAC slice: flat EMAC beat helpers, Ethernet/IPv4/UDP packet builders, checksum reference code, MAC-config byte-order helpers, and minimum-frame padding helpers. Reuse it instead of cloning packet or sideband plumbing across `EthMacCore` benches. +- Do not replace `ethernet/RoCEv2/blue-*` with local test doubles when the target boundary is `ethernet/RoCEv2/rtl`. The intended shape is one cocotb bench per RTL entity in `ethernet/RoCEv2/rtl`, with `blue-crc`, `blue-rdma`, and `blue-lib` used as real transitive dependencies where instantiated. Under the current local toolchain only `ghdl` is available, so the pure-VHDL quartet is covered now and the remaining five mixed-language entities stay open until the runner supports real VHDL+Verilog simulation. +- RoCEv2 RTL entity matrix for the next session: + - Covered now with the current GHDL-only flow: `EthMacPrepareForICrc`, `EthMacRxCheckICrc`, `RoceResizeAndSwap`, `RoceConfigurator` + - Still required, but must use real generated dependencies: `EthMacCrcAxiStreamWrapperSend` -> `blue-crc/mkCrcRawAxiStreamCustomSend.v`, `EthMacCrcAxiStreamWrapperRecv` -> `blue-crc/mkCrcRawAxiStreamCustomRecv.v`, `EthMacTxRoCEv2` -> send CRC wrapper, `EthMacRxRoCEv2` -> recv CRC wrapper, `RoceEngineWrapper` -> `blue-rdma/mkAxisTransportLayer.v` plus `blue-lib/` +- `tests/ethernet/RawEthFramer/raw_eth_test_utils.py` now holds the shared raw-Ethernet helper pieces: flat app-side beat helpers, raw-Ethernet header/frame builders, and lookup-handshake utilities reused by the `RawEthFramer`, `RawEthFramerRx`, and `RawEthFramerTx` benches. +- `tests/ethernet/UdpEngine/udp_test_utils.py` is now the shared home for the UDP slice: legacy-address constants, pseudo-frame builders, DHCP option helpers, and the common cocotb bench setup for the `ArpIpTable`, `UdpEngine*`, and `UdpEngineWrapper*` wrappers. Reuse it instead of rebuilding IPv4/UDP helper glue in each test module. +- `tests/ethernet/IpV4Engine/ipv4_test_utils.py` is now the shared home for the IPv4 slice: packet/header builders and common cocotb bench setup for the `ArpEngine`, `IcmpEngine`, `IgmpV2Engine`, and `IpV4Engine*` wrappers. Reuse it instead of cloning IPv4 framing helpers across that directory. +- `tests/protocols/ssi/ssi_test_utils.py` is the shared home for SSI transaction work: flat SSI endpoints, beat/frame helpers, contiguous-frame send, receive/no-output utilities, and `SOF`/`EOFE`-aware assertions. Use it instead of open-coding SSI handshake loops or terminal-flag checks. +- `tests/protocols/coaxpress/coaxpress_test_utils.py` is now the shared home for the CoaXPress leaf/assembly slice: protocol constants, repeated-byte word builders, byte-splitting helpers, wide-word packing helpers, basic clock/reset helpers, and simple pulse-capture utilities for the raw-word wrappers. Reuse it instead of cloning CoaXPress symbol constants or one-cycle output capture loops across future CoaXPress benches. +- Keep future CoaXPress benches explicit about the boundary between normative spec behavior and current RTL-contract-only coverage. For top-level and over-fiber work in particular, the governing references are the CoaXPress protocol spec (`CXP-001-2021`) and the CoaXPress-over-Fiber bridge spec (`CXPR-008-2021`); use the shared helper names for packet classes and control symbols instead of sprinkling raw byte literals through new tests. +- Across the AXI slices, prefer the subsystem helper paths that already exist for register transactions, frame movement, and setup. In practice that means reusing helpers such as `tests/common/regression_utils.py`, the AXI/ethernet subsystem utility modules, and any nearby module-family helpers before inventing a one-off local transaction wrapper. +- The current `EthMacCore` slice is intentionally a checked-in-wrapper-first rollout, not a cocotb-generated-wrapper experiment. Keep new Ethernet work on that same pattern unless the simulator forces a very local generic adapter. +- The XGMII import/export loopback behavior differs from the GMII path when `phyReady` drops mid-traffic: the blocked frame is retained and drains after link recovery, padded to Ethernet's minimum frame size if it was short. The GMII path drops that blocked frame. Future import/export coverage should preserve that distinction instead of forcing one common expectation. +- The current `EthMacRxImportXlgmii.vhd` and `EthMacTxExportXlgmii.vhd` leaves are placeholders: they drive no data-path activity and never pulse the count/status outputs. Future work should treat functional XLGMII support as an RTL gap, not as a missing bench. +- `EthMacRxCsum` reliably raises `IPERR` on a bad IPv4 header checksum, but the checked-in wrapper contract does not currently require `EOFE` for that case. Keep the negative test aligned to the real observable contract rather than to a stronger assumption. +- The RX/TX shift benches need a small idle-plus-settle gap before changing runtime shift controls because the underlying `AxiStreamShift` samples those controls while idle. Preserve that guardrail if those benches are refactored or expanded. +- `EthMacRxBypass` compares the EtherType field in the flattened EMAC byte-lane order, not normal wire-order host integer order. In practice that means a wire EtherType like `0x9000` must be compared as `x"0090"` at the wrapper/DUT generic boundary, just as `0x88B5` appears as `x"B588"` in the existing wrappers. +- Do not leave stale simulation trees behind between tasks. If a verification command launched `pytest`, cocotb, GHDL, or wrapper executables, treat post-run process cleanup as mandatory before the next edit or test cycle. + +## Current Status +Planning is complete and implementation is well underway. The agreed direction is a Python-only executable regression framework with tiered `smoke` and `functional` coverage. Existing VHDL TBs are reference material only and should be rewritten in Python when migrated, unless a thin wrapper is still useful for cocotb access. + +The repo now has the initial handoff artifacts, a checked-in inventory scaffold at `docs/plans/rtl-regression/inventory.yaml`, and local bootstrap helpers in `scripts/setup_regression_env.sh` plus `.vscode/tasks.json`. The first pilot modules were `FifoAsync`, `AxiStreamFifoV2`, and `AxiLiteAsync`, and the work has since moved into a graph-guided bottom-up rollout across `base/`. + +The local machine now has `ghdl`, a working `.venv`, the Python regression packages, a repo-local `ruckus` link to `~/ruckus`, and a successful `make MODULES="$PWD" import` run. Local environment bootstrap is no longer the blocker. The first shared-helper-based pilot regression now exists in `tests/base/fifo/test_FifoAsync.py` and passes locally. + +New regressions are now being organized by subsystem under `tests/`, with shared helpers in `tests/common/`. The `FifoAsync` pilot lives in `tests/base/fifo/test_FifoAsync.py`, and `AxiStreamFifoV2` now lives in `tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py`. New work should follow that package layout instead of adding more flat files under `tests/`. + +`FifoAsync` now has a validated expanded 12-case matrix, `FifoSync` has a validated expanded 11-case matrix, `Synchronizer` and `SynchronizerVector` now each have validated 6-case matrices under `tests/base/sync/`, `RstPipeline` has a validated 4-case matrix under `tests/base/general/`, `SimpleDualPortRam` has a validated 5-case matrix under `tests/base/ram/`, `FifoOutputPipeline` has a validated 5-case matrix under `tests/base/fifo/`, and `FifoWrFsm` has a validated 4-case matrix under `tests/base/fifo/`. + +The next graph-guided 10-module follow-on is also now in place: `Crc32Parallel`, `Crc32`, `CRC32Rtl`, `RstSync`, `PwrUpRst`, `SynchronizerEdge`, `SynchronizerOneShot`, `TrueDualPortRam`, `LutRam`, and `FifoRdFsm`. The combined validation command for that batch is `./.venv/bin/python -m pytest -v tests/base/crc/test_Crc32Parallel.py tests/base/crc/test_Crc32.py tests/base/crc/test_CRC32Rtl.py tests/base/sync/test_RstSync.py tests/base/general/test_PwrUpRst.py tests/base/sync/test_SynchronizerEdge.py tests/base/sync/test_SynchronizerOneShot.py tests/base/ram/test_TrueDualPortRam.py tests/base/ram/test_LutRam.py tests/base/fifo/test_FifoRdFsm.py`, and it currently passes with `38 passed`. + +The next 15-module `base/` general/delay/sync batch is now also implemented and validated: `Arbiter`, `ClockDivider`, `Debouncer`, `Gearbox`, `Heartbeat`, `Mux`, `OneShot`, `RegisterVector`, `RstPipelineVector`, `Scrambler`, `WatchDogRst`, `SlvDelay`, `SlvFixedDelay`, `SynchronizerFifo`, and `SynchronizerOneShotCnt`. The combined validation command for that batch is `./.venv/bin/python -m pytest -n 0 -q tests/base/general/test_Arbiter.py tests/base/general/test_ClockDivider.py tests/base/general/test_Debouncer.py tests/base/general/test_Gearbox.py tests/base/general/test_Heartbeat.py tests/base/general/test_Mux.py tests/base/general/test_OneShot.py tests/base/general/test_RegisterVector.py tests/base/general/test_RstPipelineVector.py tests/base/general/test_Scrambler.py tests/base/general/test_WatchDogRst.py tests/base/delay/test_SlvDelay.py tests/base/delay/test_SlvFixedDelay.py tests/base/sync/test_SynchronizerFifo.py tests/base/sync/test_SynchronizerOneShotCnt.py`, and it currently passes with `41 passed`. + +The next 10-module wrapper/integration batch is now also implemented and validated: `DspComparator`, `Fifo`, `FifoCascade`, `FifoMux`, `AsyncGearbox`, `SynchronizerOneShotVector`, `SynchronizerOneShotCntVector`, `SyncStatusVector`, `SyncTrigPeriod`, and `SyncMinMax`. The combined validation command for that batch is `./.venv/bin/python -m pytest -n 0 -q tests/dsp/generic/test_DspComparator.py tests/base/fifo/test_Fifo.py tests/base/fifo/test_FifoCascade.py tests/base/fifo/test_FifoMux.py tests/base/general/test_AsyncGearbox.py tests/base/sync/test_SynchronizerOneShotVector.py tests/base/sync/test_SynchronizerOneShotCntVector.py tests/base/sync/test_SyncStatusVector.py tests/base/sync/test_SyncTrigPeriod.py tests/base/sync/test_SyncMinMax.py`, and it currently passes with `18 passed`. + +The remaining practical non-vendor, non-dummy `base/` modules are now also implemented and validated: `MasterRamIpIntegrator`, `SlaveRamIpIntegrator`, `DualPortRam`, `SlvDelayRam`, `SlvDelayFifo`, `SyncClockFreq`, `SyncTrigRate`, and `SyncTrigRateVector`. The combined validation command for that batch is `./.venv/bin/python -m pytest -n 0 -q tests/base/general/test_MasterRamIpIntegrator.py tests/base/general/test_SlaveRamIpIntegrator.py tests/base/ram/test_DualPortRam.py tests/base/delay/test_SlvDelayRam.py tests/base/delay/test_SlvDelayFifo.py tests/base/sync/test_SyncClockFreq.py tests/base/sync/test_SyncTrigRate.py tests/base/sync/test_SyncTrigRateVector.py`, and it currently passes with `15 passed`. + +`Crc32` now covers multiple common 32-bit polynomials instead of only the default IEEE CRC-32 polynomial. That test uses a thin wrapper at `base/crc/wrappers/Crc32PolyWrapper.vhd` because the local GHDL flow rejects direct command-line overrides of the `CRC_POLY_G : slv(31 downto 0)` generic. Pytest still defaults to `-n auto --dist=worksteal` through `pytest.ini` so parameterized regressions fan out across worker processes by default. + +The project now also has a shared helper path in `tests/common/regression_utils.py` for test scaffolding, but the wrapper policy is to keep durable cocotb-facing HDL shims checked in under subsystem-local `wrappers/` or `ip_integrator/` folders. `Heartbeat` and `Debouncer` remain useful examples of very small wrappers, but new permanent generic-adapter shims should follow the checked-in subsystem-local pattern. + +`tests/common/regression_utils.py` now also includes `start_lockstep_clocks()` for DUTs whose generics assume truly common clocks in both ports. Use that helper instead of launching two same-period clocks independently when the RTL assumes shared edge identity. + +`ethernet/EthMacCore/` now has checked-in regression coverage under `tests/ethernet/EthMacCore/` for both the original leaf slice and the deeper assembly layer. The current benches cover `EthCrc32Parallel`, `EthMacFlowCtrl`, `EthMacRxPause`, `EthMacTxPause`, `EthMacRxFilter`, `EthMacRxShift`, `EthMacTxShift`, `EthMacRxImport`, `EthMacTxExport`, `EthMacRxCsum`, `EthMacTxCsum`, `EthMacTop`, `EthMacRx`, `EthMacTx`, `EthMacRxFifo`, `EthMacTxFifo`, `EthMacRxBypass`, and `EthMacTxBypass`. The import/export benches now also explicitly encode the current placeholder `XLGMII` contract so future functional XLGMII RTL work will show up as a deliberate expectation change instead of an accidental gap. The current Ethernet wrappers live under `ethernet/EthMacCore/wrappers/` and should be treated as the stable cocotb-facing surfaces for further `EthMacCore` work. + +`ethernet/UdpEngine/` and `ethernet/IpV4Engine/` also received a thin-area cleanup pass on this branch. The `UdpEngine` top/wrapper benches now cover additional client/server routing paths beyond the earlier single happy-path smoke, while `IpV4Engine` now has a top-level protocol-TX path and deeper `IcmpEngine` negative/recovery coverage. The combined local validation command for that focused follow-up is `./.venv/bin/python -m pytest -n 0 -q tests/ethernet/EthMacCore/test_EthMacRxImport.py tests/ethernet/EthMacCore/test_EthMacTxExport.py tests/ethernet/EthMacCore/test_EthMacRxBypass.py tests/ethernet/EthMacCore/test_EthMacTxBypass.py tests/ethernet/UdpEngine/test_UdpEngine.py tests/ethernet/UdpEngine/test_UdpEngineWrapper.py tests/ethernet/IpV4Engine/test_IpV4Engine.py tests/ethernet/IpV4Engine/test_IcmpEngine.py`, and it passes locally with `14 passed`. + +`ethernet/RawEthFramer/` now also has checked-in regression coverage under `tests/ethernet/RawEthFramer/`. The earlier top-level `RawEthFramer` wrapper bench is joined by direct leaf benches for `RawEthFramerRx` and `RawEthFramerTx`, plus a `RawEthFramerPair` integration bench whose wrapper cross-connects two `RawEthFramer` instances to mirror the legacy `ethernet/RawEthFramer/tb/RawEthFramerTb.vhd` topology. The validated RX leaf bench covers lookup-gated unicast decode, short-frame trim behavior, broadcast bypass, and representative reject cases. The validated TX leaf bench now covers lookup-request exposure before forwarding, successful multi-beat unicast forwarding after lookup resolution, broadcast bypass with the observed padded wire image, and zero-MAC lookup-miss drop. The successful unicast leaf case intentionally models the nonzero lookup latency that the integrated `RawEthFramer` wrapper inserts before `ack`, so keep that timing assumption if the TX bench is expanded further. + +`ethernet/UdpEngine/` now has checked-in regression coverage under `tests/ethernet/UdpEngine/` as well. The current validated set covers `ArpIpTable`, `UdpEngineArp`, `UdpEngineDhcp`, `UdpEngineRx`, `UdpEngineTx`, `UdpEngine`, and `UdpEngineWrapper`, all backed by checked-in wrappers under `ethernet/UdpEngine/wrappers/` and the shared helper layer in `tests/ethernet/UdpEngine/udp_test_utils.py`. + +`ethernet/IpV4Engine/` now also has checked-in regression coverage under `tests/ethernet/IpV4Engine/`. The current validated set covers `ArpEngine`, `IcmpEngine`, `IgmpV2Engine`, `IpV4Engine`, `IpV4EngineDeMux`, `IpV4EngineRx`, and `IpV4EngineTx`, all backed by checked-in wrappers under `ethernet/IpV4Engine/wrappers/` and the shared helper layer in `tests/ethernet/IpV4Engine/ipv4_test_utils.py`. The `IgmpV2Engine` leaf bench covers power-up reports, general-query re-arming, and report suppression on matching inbound membership reports; it also documents the leaf-level stale pseudo-header bytes that are ignored by downstream `IpV4EngineTx` assembly. + +The wrapper coverage policy is now more explicit in practice: test the wrapper-specific behavior, not the full leaf matrix again. `Fifo` validated both inferred sync/async selection branches, `FifoCascade` validated public stage-vector mapping plus a curated output smoke, and `FifoMux` originally validated only the stable split-to-narrow path. + +The current `test-base-2` branch adds the targeted base-depth pass without new wrappers. `FifoMux` now also covers pack-to-wide conversion and partial-pack reset no-output behavior, `FifoAsync` adds burst/backpressure/reset stress plus near-full turnover under concurrent read/write pressure, `FifoSync` adds simultaneous read/write near-full behavior, `FifoCascade` adds staged pressure recovery, `SynchronizerFifo` adds explicit read-enable gaps and reset while FWFT data is prefetched with the read side paused, `Arbiter` adds starvation-rotation coverage, `WatchDogRst` adds noisy near-timeout keepalive coverage, `DualPortRam` adds same-address cross-port collision coverage, `SimpleDualPortRam` adds port-B enable hold behavior in direct and registered-output modes, `TrueDualPortRam` adds same-address dual-write collision recovery, `SlvDelayFifo` adds reset flushing of multiple pending entries, and `SlvDelayRam` adds reset-aligned runtime delay growth with output-history discard. The focused validation command is `/Users/bareese/surf/.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/base/fifo/test_FifoMux.py tests/base/fifo/test_FifoAsync.py tests/base/fifo/test_FifoSync.py tests/base/fifo/test_FifoCascade.py tests/base/sync/test_SynchronizerFifo.py tests/base/general/test_Arbiter.py tests/base/general/test_WatchDogRst.py tests/base/ram/test_DualPortRam.py tests/base/ram/test_SimpleDualPortRam.py tests/base/ram/test_TrueDualPortRam.py tests/base/delay/test_SlvDelayFifo.py`, and it passes locally with `59 passed`. A five-file refinement sanity run also passed with `/Users/bareese/surf/.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/base/fifo/test_FifoAsync.py tests/base/fifo/test_FifoMux.py tests/base/sync/test_SynchronizerFifo.py tests/base/ram/test_SimpleDualPortRam.py tests/base/delay/test_SlvDelayFifo.py` (`27 passed`), and the focused `SlvDelayRam` run passed with `/Users/bareese/surf/.venv/bin/python -m pytest -n 0 -q tests/base/delay/test_SlvDelayRam.py` (`3 passed`). + +That same wrapper-policy lesson now applies to the late `base/sync` wrappers as well. `SyncClockFreq` is stable with a checked-in subsystem wrapper, but its common-clock measurement quantizes one count above the abstract target under the current GHDL flow, so the regression checks a bounded expected range rather than an exact integer. `SyncTrigRate` is intentionally covered as a wrapper/integration bench only: it proves aligned update publication, denser-window rate growth, reset-path liveness, and strobe pulse behavior, while exact min/max pipeline semantics remain the responsibility of the dedicated `SyncMinMax` leaf test. + +At this point the practical phase-1 `base/` rollout is effectively complete. The only uncovered non-dummy `base/` module is `LutFixedDelay`, and it remains deferred because it still depends on the vendor-backed `SinglePortRamPrimitive` path. The other remaining `base/` gaps are vendor-heavy or dummy-backed variants. + +The first post-`base/` `axi/` follow-on is now in place as well. `AxiStreamPipeline` is validated under `tests/axi/axi_stream/test_AxiStreamPipeline.py` using a thin flat-port adapter at `axi/axi-stream/ip_integrator/AxiStreamPipelineIpIntegrator.vhd`, and `AxiLiteCrossbar` is validated under `tests/axi/axi_lite/test_AxiLiteCrossbar.py` using the existing `axi/axi-lite/tb/AxiLiteCrossbarTb.vhd` harness as a cocotb-facing shell. The combined validation command is `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi_stream/test_AxiStreamPipeline.py tests/axi/axi_lite/test_AxiLiteCrossbar.py`, and it currently passes with `4 passed`. + +For `AxiStreamPipeline`, treat the zero-stage case as a true combinational pass-through and the staged cases as wrapper-visible buffered paths. The stable expectation under the current wrapper is sink-handshake latency of `PIPE_STAGES_G + 2` clocks plus bounded reset flush behavior, not a naive one-to-one mapping from the user generic name. For `AxiLiteCrossbar`, the useful regression surface is region routing, decode-miss `DECERR` handling, and concurrent traffic through the existing cascaded harness topology rather than a broad generic sweep. + +`AxiStreamMux` is now validated under `tests/axi/axi_stream/test_AxiStreamMux.py` using a thin two-input adapter at `axi/axi-stream/ip_integrator/AxiStreamMuxIpIntegrator.vhd`. The module-local validation command is `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi_stream/test_AxiStreamMux.py`, and it currently passes with `3 passed`. A small follow-on sanity run across `tests/axi/axi_stream/test_AxiStreamPipeline.py`, `tests/axi/axi_stream/test_AxiStreamMux.py`, and `tests/axi/axi_lite/test_AxiLiteCrossbar.py` also passes with `7 passed`. Keep the validated subset intentionally narrow: indexed arbitration with explicit priority plus `disableSel`, routed `TDEST`/`TID` remap under backpressure, and staged asynchronous active-low reset recovery in passthrough mode. Interleave and explicit rearbitrate branches remain open for later work. Also note the mux-specific nuance from this bench: `disableSel` is applied before the separate priority-mask generation, so a disabled higher-priority source can still suppress lower-priority requesters. + +`AxiStreamDeMux` is now validated under `tests/axi/axi_stream/test_AxiStreamDeMux.py` using a thin one-input/two-output adapter at `axi/axi-stream/ip_integrator/AxiStreamDeMuxIpIntegrator.vhd`. The module-local validation command is `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi_stream/test_AxiStreamDeMux.py`, and it currently passes with `3 passed`. A small follow-on sanity run across `tests/axi/axi_stream/test_AxiStreamPipeline.py`, `tests/axi/axi_stream/test_AxiStreamMux.py`, `tests/axi/axi_stream/test_AxiStreamDeMux.py`, and `tests/axi/axi_lite/test_AxiLiteCrossbar.py` passes with `10 passed`. Keep the validated subset intentionally narrow: indexed decode to both outputs, exact-match routed decode under output backpressure, and dynamic-route table behavior including unmatched-destination drop plus staged asynchronous active-low reset flush. Wildcard-route patterns and larger fanout counts remain open for later work. + +The next five flat-queue modules are now also in place and validated. `AxiStreamResize` uses `axi/axi-stream/ip_integrator/AxiStreamResizeIpIntegrator.vhd` plus `tests/axi/axi_stream/test_AxiStreamResize.py`; `AxiLiteAsync` uses `axi/axi-lite/ip_integrator/AxiLiteAsyncIpIntegrator.vhd` plus `tests/axi/axi_lite/test_AxiLiteAsync.py`; `AxiLiteMaster` uses `axi/axi-lite/ip_integrator/AxiLiteMasterIpIntegrator.vhd` plus `tests/axi/axi_lite/test_AxiLiteMaster.py`; `AxiLiteToDrp` uses `axi/bridge/ip_integrator/AxiLiteToDrpIpIntegrator.vhd` plus `tests/axi/bridge/test_AxiLiteToDrp.py`; and `AxiDualPortRam` is validated through the existing `axi/axi-lite/ip_integrator/AxiDualPortRamIpIntegrator.vhd` wrapper plus `tests/axi/axi_lite/test_AxiDualPortRam.py`. The five-module validation command is `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi_stream/test_AxiStreamResize.py tests/axi/axi_lite/test_AxiLiteAsync.py tests/axi/axi_lite/test_AxiLiteMaster.py tests/axi/bridge/test_AxiLiteToDrp.py tests/axi/axi_lite/test_AxiDualPortRam.py`, and it currently passes with `10 passed`. A broader AXI follow-on sanity run across pipeline, mux, demux, resize, crossbar, async, master, DRP bridge, and dual-port RAM also passes with `20 passed`. + +The next 10 generated-queue AXI modules are now also in place and validated. `AxiLiteRegs`, `AxiLiteRespTimer`, `AxiLiteSlave`, `AxiLiteWriteFilter`, and `AxiVersion` are covered under `tests/axi/axi_lite/` with thin subsystem-local wrappers in `axi/axi-lite/ip_integrator/`. `AxiStreamCombiner`, `AxiStreamFlush`, `AxiStreamGearboxPack`, `AxiStreamGearboxUnpack`, and `AxiStreamSplitter` are covered under `tests/axi/axi_stream/` with thin subsystem-local wrappers in `axi/axi-stream/ip_integrator/`. The combined validation command is `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi_lite/test_AxiLiteRegs.py tests/axi/axi_lite/test_AxiLiteRespTimer.py tests/axi/axi_lite/test_AxiLiteSlave.py tests/axi/axi_lite/test_AxiLiteWriteFilter.py tests/axi/axi_lite/test_AxiVersion.py tests/axi/axi_stream/test_AxiStreamCombiner.py tests/axi/axi_stream/test_AxiStreamFlush.py tests/axi/axi_stream/test_AxiStreamGearboxPack.py tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py tests/axi/axi_stream/test_AxiStreamSplitter.py`, and it currently passes with `14 passed`. + +Keep the validated subset intentionally narrow for the two most timing-sensitive wrappers. `AxiStreamResize` covers equal-width pass-through plus curated upsize/downsize cases with sideband alignment and staged reset flush. `AxiLiteMaster` covers request/ack sequencing, staggered AXI ready/valid handshakes, propagated `SLVERR` responses, and reset return to idle. `AxiDualPortRam` covers AXI round-trips, system-port visibility, byte-masked system writes, and AXI write-disable error responses through the existing wrapper. `AxiLiteAsync` is currently validated only on the stable `COMMON_CLK_G=true` wrapper path; the asynchronous reset-crossing branches remain open for later work. `AxiLiteToDrp` is currently validated only on the stable common-clock non-arbitrated path with timeout recovery; the async arbitration branch remains open for later work. + +`AxiRateGen` is now also validated under `tests/axi/axi4/test_AxiRateGen.py` using `axi/axi4/ip_integrator/AxiRateGenIpIntegrator.vhd`. The module-local validation command is `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi4/test_AxiRateGen.py`, and it currently passes with `1 passed`. Keep the validated subset intentionally narrow here too: the stable `COMMON_CLK_G=true` path covers AXI-Lite register programming, timer-spaced generated AXI writes, and generated-read completion through a cocotb AXI RAM model, while the asynchronous AXI-Lite crossing branches remain open for later work. + +The remaining final 11 `axi/` modules from the axi-first pass are now also implemented and validated. The new checked-in benches are: +- `tests/axi/axi4/test_AxiReadEmulate.py` +- `tests/axi/axi4/test_AxiWriteEmulate.py` +- `tests/axi/axi4/test_AxiRingBuffer.py` +- `tests/axi/axi4/test_AxiMonAxiL.py` +- `tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py` +- `tests/axi/axi_stream/test_AxiStreamMonAxiL.py` +- `tests/axi/dma/test_AxiStreamDmaWrite.py` +- `tests/axi/dma/test_AxiStreamDma.py` +- `tests/axi/dma/test_AxiStreamDmaFifo.py` +- `tests/axi/dma/test_AxiStreamDmaRingRead.py` +- `tests/axi/dma/test_AxiStreamDmaRingWrite.py` + +The supporting wrappers added for that batch are: +- `axi/axi4/ip_integrator/AxiReadEmulateIpIntegrator.vhd` +- `axi/axi4/ip_integrator/AxiWriteEmulateIpIntegrator.vhd` +- `axi/axi4/ip_integrator/AxiRingBufferIpIntegrator.vhd` +- `axi/axi4/ip_integrator/AxiMonAxiLIpIntegrator.vhd` +- `axi/axi-lite/ip_integrator/AxiLiteRamSyncStatusVectorIpIntegrator.vhd` +- `axi/axi-stream/ip_integrator/AxiStreamMonAxiLIpIntegrator.vhd` +- `axi/dma/ip_integrator/AxiStreamDmaWriteIpIntegrator.vhd` +- `axi/dma/ip_integrator/AxiStreamDmaFifoIpIntegrator.vhd` +- `axi/dma/ip_integrator/AxiStreamDmaIpIntegrator.vhd` +- `axi/dma/ip_integrator/AxiStreamDmaRingReadIpIntegrator.vhd` +- `axi/dma/ip_integrator/AxiStreamDmaRingWriteIpIntegrator.vhd` + +The combined validation command for that batch is `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi4/test_AxiReadEmulate.py tests/axi/axi4/test_AxiWriteEmulate.py tests/axi/axi4/test_AxiRingBuffer.py tests/axi/axi4/test_AxiMonAxiL.py tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py tests/axi/axi_stream/test_AxiStreamMonAxiL.py tests/axi/dma/test_AxiStreamDmaWrite.py tests/axi/dma/test_AxiStreamDma.py tests/axi/dma/test_AxiStreamDmaFifo.py tests/axi/dma/test_AxiStreamDmaRingRead.py tests/axi/dma/test_AxiStreamDmaRingWrite.py`, and it passes locally with `11 passed`. + +One small RTL fix landed during that validation pass because the new `AxiStreamDmaRingWrite` test exposed a real simulation-width hazard: `axi/dma/rtl/v1/AxiStreamDmaRingWrite.vhd` now slices `dmaAck.size` back to `RAM_DATA_WIDTH_C` before incrementing `nextAddr`. Keep that change; it is what allows the checked-in narrow wrapper to simulate cleanly under GHDL. + +The earlier checked-in RTL instantiation graph and phase-1 queue artifacts have been retired because task selection is now user-directed. The generator remains available for explicit one-off hierarchy analysis, but generated output should stay temporary unless the user asks for a specific artifact. + +## Immediate Next Task +If the user keeps the focus on stream-helper cleanup rather than resuming a new subsystem, the next practical step is the remaining PGP interleaved source/capture helpers: decide whether `tests/protocols/pgp/pgp4/test_Pgp4Rx.py` and the protocol-word collector in `tests/protocols/pgp/pgp4/pgp4_test_utils.py` should stay intentionally manual or be folded into a richer shared helper that can hold a source beat through acceptance while concurrently capturing narrow output pulses. + +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 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. + +The latest CoaXPress validation after the HKP AXI-Lite consumer sweep is green for `./.venv/bin/python -m pytest -q tests/protocols/coaxpress` (`19 passed` in 560.44 seconds). The focused bridge sanity run `./.venv/bin/python -m pytest -q tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeRx.py tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py` also passed with `2 passed`. + +If the user switches back to `ethernet/RoCEv2`, the next real step is still enabling a mixed-language cocotb path for the five remaining RTL entities listed above. Keep `docs/plans/rtl-regression/progress.md` and this handoff file aligned with the real validated subset, and do not reintroduce local stand-ins for `blue-*`. + +## Read Order +1. `docs/plans/rtl-regression/handoff.md` +2. `docs/plans/rtl-regression/progress.md` only when deeper status detail is needed. +3. `docs/plans/rtl-regression/plan.md` only when policy details need to be checked. + +Before writing code in a fresh session: +1. Re-read the Python comment rules and the checked-in wrapper comment/header rules above. +2. If adding a permanent `*IpIntegrator.vhd`, include the standard SURF banner and section comments in the first edit, not as an afterthought. +3. If adding a Python regression, include the standard SURF/SLAC header, the `Test methodology` header block, and in-body tutorial comments in the first draft. +4. If creating or editing any VHDL file, run `./.venv/bin/vsg -c vsg-linter.yml ...` on that file set, use `--fix` when possible, and rerun the same lint command until it is clean. +5. After any step that launches `pytest`, cocotb, GHDL, or another simulator process, sweep for stale child processes and kill any leftovers before starting the next step. + +## Important Repo Facts +- New Python regressions should be organized under subsystem packages in `tests/` +- Shared Python regression helper lives in `tests/common/regression_utils.py` +- `tests/common/regression_utils.py` now supports both test-local extra VHDL source lists and generated test-local wrapper emission for wrapper-based cases +- `tests/common/regression_utils.py` also now provides `start_lockstep_clocks()` for `COMMON_CLK_G` style benches that require truly shared edges +- When starting a new test, check for nearby shared helper modules before writing any new transaction boilerplate. The expected search order is: `tests/common/`, then the current subsystem package, then closely related subsystem packages that already cover the same protocol family. +- For AXI-Lite benches, prefer existing helpers for repeated register reads/writes, environment decoding, and common bench setup rather than spelling out raw transactions in every file. +- For AXI Stream benches, prefer existing helpers for beat/frame packing, contiguous-frame driving, whole-frame receive, no-output checks, and handshake observation rather than writing custom ready/valid loops unless the DUT exposes a genuinely new contract. +- For SSI benches, prefer `tests/protocols/ssi/ssi_test_utils.py` for beat models, frame helpers, `EOFE`/`SOF` handling, and sink/source setup instead of duplicating SSI transaction utilities in a local test file. +- Default comment style for new cocotb tests has two parts: a wrapped four-bullet `Test methodology` header (`Sweep`, `Stimulus`, `Checks`, `Timing`) plus tutorial-style in-body comments that explain what each coroutine step is doing and why +- New cocotb tests should also use the standard SURF/SLAC file header, not a shortened local variant +- The methodology header should be module-specific and describe the real curated sweep, driven sequence, expected outputs/state changes, and timing checks; avoid generic boilerplate +- Keep methodology comment lines to a normal readable width in the source file +- Checked-in cocotb-facing `*IpIntegrator.vhd` files should also follow repo style: standard SLAC/SURF banner at the top and short section comments marking shim setup, DUT hookup, and flattening/status export logic +- For AXI Stream and AXI-Lite record ports, prefer the existing IP-integrator shim entities to flatten record interfaces for cocotb instead of hand-writing record packing in each wrapper +- If an AXI wrapper needs DUT-specific extra signals, keep the standard shim pair for the bus itself and only wire the extra signals manually +- More generally, if any module needs a VHDL shim layer to fit cleanly into the cocotb flow, that shim belongs in the nearest real subsystem `ip_integrator/` tree rather than under `tests/` +- Do not use generic `hdl/` buckets for cocotb-facing adapter layers; reserve those locations for genuinely different kinds of HDL support +- Many VHDL wrappers live under `*/tb/` +- The initial regression inventory lives in `docs/plans/rtl-regression/inventory.yaml` +- Use `./.venv/bin/python ...` for repo-local Python commands unless the virtualenv has already been activated in the current shell; do not assume a `python` shim exists on `PATH` +- If GHDL rejects a direct command-line override for a non-scalar or real generic, prefer a generated thin test-only wrapper over simulator-specific literal workarounds or another checked-in one-off HDL shim +- If a wrapper branch is unstable under the current open-source flow, keep the validated subset narrow and record the omitted branch explicitly in the docs instead of over-claiming wrapper coverage +- Use `ps -Ao pid,ppid,stat,time,command` when needed to find stale simulation children, then terminate only the leftover run trees instead of broad process classes +- `LutFixedDelay` remains intentionally deferred because it depends on `SinglePortRamPrimitive`; do not accidentally treat the now-small remaining `base/` set as phase-1 work that still needs to be forced through +- Regenerate graph and queue analysis with `./.venv/bin/python scripts/build_rtl_instantiation_graph.py` only when hierarchy analysis is useful or the user explicitly asks for it; by default the script writes to a temporary output directory, not `docs/plans/rtl-regression/` +- Local bootstrap entrypoint: `scripts/setup_regression_env.sh` +- Local `ruckus` is linked from `~/ruckus` + +## Resume Rule +If resuming implementation, update `docs/plans/rtl-regression/progress.md` first. diff --git a/docs/plans/rtl-regression/inventory.yaml b/docs/plans/rtl-regression/inventory.yaml new file mode 100644 index 0000000000..c2abd02f1f --- /dev/null +++ b/docs/plans/rtl-regression/inventory.yaml @@ -0,0 +1,700 @@ +version: 1 +last_updated: 2026-05-21 + +field_guide: + entity: "Synthesizable RTL entity name" + path: "Path to the synthesizable RTL source file" + subsystem: "High-level repo grouping used for rollout and sharding" + tier: "One of functional_python, smoke_python, wrapper_required, deferred_vendor_heavy" + status: "Implementation state for this inventory row" + priority: "Rollout priority; pilot marks the initial implementation set" + wrapper_path: "Optional VHDL wrapper/shim used for cocotb access" + python_test: "Planned or implemented Python regression entrypoint" + reference_assets: "Legacy assets worth mining for intent, not preserving as execution requirements" + notes: "Short implementation notes" + deferred_reason: "Required when tier is deferred_vendor_heavy" + +inventory_rules: + - "Every synthesizable RTL entity should eventually appear exactly once in this inventory." + - "Executable regression logic must live in Python." + - "VHDL may remain only as thin wrappers, shims, or required simulation models." + - "Legacy VHDL testbenches are reference material, not a preservation target." + - "Generic-heavy modules should prefer functional_python over smoke_python when actively implemented." + +modules: + - entity: Crc32Parallel + path: base/crc/rtl/Crc32Parallel.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/crc/test_Crc32Parallel.py + reference_assets: [] + notes: "Graph-guided CRC leaf with validated coverage for byte-width variation, registered vs direct input handling, and reset polarity/style variants against a shared Python CRC model." + deferred_reason: "" + + - entity: Crc32 + path: base/crc/rtl/Crc32.vhd + subsystem: base + tier: functional_python + status: implemented_validated_expanded + priority: phase1_low_level + wrapper_path: base/crc/wrappers/Crc32PolyWrapper.vhd + python_test: tests/base/crc/test_Crc32.py + reference_assets: [] + notes: "Graph-guided generic CRC leaf with validated coverage for IEEE CRC-32, Castagnoli, and Koopman-style 32-bit polynomial cases plus byte-width, input-register, and reset polarity/style variation. Uses a thin test-only wrapper because the local GHDL flow rejects direct command-line overrides of the 32-bit `CRC_POLY_G` `slv` generic." + deferred_reason: "" + + - entity: CRC32Rtl + path: base/crc/rtl/CRC32Rtl.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/crc/test_CRC32Rtl.py + reference_assets: [] + notes: "Legacy-compatible fixed-polynomial CRC leaf with validated baseline behavioral coverage for synchronous and asynchronous reset handling and reset polarity variation." + deferred_reason: "" + + - entity: RstSync + path: base/sync/rtl/RstSync.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/sync/test_RstSync.py + reference_assets: [] + notes: "High-fanout reset synchronizer from the graph-guided leaf set. Validated coverage includes release-delay behavior, active-high vs active-low reset polarity, asynchronous vs synchronous power-on reset handling, and bypass/no-output configurations." + deferred_reason: "" + + - entity: PwrUpRst + path: base/general/rtl/PwrUpRst.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/general/test_PwrUpRst.py + reference_assets: [] + notes: "Reusable reset-generator primitive with validated coverage for counter-based startup stretch behavior, synchronous vs asynchronous reset handling, and reset polarity variation." + deferred_reason: "" + + - entity: SynchronizerEdge + path: base/sync/rtl/SynchronizerEdge.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/sync/test_SynchronizerEdge.py + reference_assets: [] + notes: "Graph-guided edge-detect synchronizer with validated coverage for rising/falling pulse detection through different sync depths, reset polarity/style variation, and bypass handling." + deferred_reason: "" + + - entity: SynchronizerOneShot + path: base/sync/rtl/SynchronizerOneShot.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/sync/test_SynchronizerOneShot.py + reference_assets: [] + notes: "Graph-guided one-shot synchronizer with validated coverage for single-pulse capture, stretched output behavior, reset polarity/style variation, active-low output mode, and bypass operation." + deferred_reason: "" + + - entity: TrueDualPortRam + path: base/ram/inferred/TrueDualPortRam.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/ram/test_TrueDualPortRam.py + reference_assets: [] + notes: "Reusable dual-port RAM primitive with validated direct cocotb coverage for read-first, write-first, and no-change modes, registered output behavior, byte-write masking, and reset polarity/style variants." + deferred_reason: "" + + - entity: LutRam + path: base/ram/inferred/LutRam.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/ram/test_LutRam.py + reference_assets: [] + notes: "Reusable LUTRAM primitive with validated coverage for combinational and registered variants, read-first/write-first/no-change behavior, multiport access, byte-write masking, and reset polarity/style variants." + deferred_reason: "" + + - entity: FifoRdFsm + path: base/fifo/rtl/inferred/FifoRdFsm.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/fifo/test_FifoRdFsm.py + reference_assets: [] + notes: "Graph-guided FIFO read-side primitive with validated coverage for standard vs FWFT behavior, block vs distributed memory modes, count progression, visible prefetch behavior, underflow-safe idle behavior, and reset polarity/style variants." + deferred_reason: "" + + - entity: SynchronizerVector + path: base/sync/rtl/SynchronizerVector.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/sync/test_SynchronizerVector.py + reference_assets: [] + notes: "High-reuse `base/sync` vector leaf selected from the instantiation graph. Validated curated matrix covers width variation, stage-depth latency, synchronous vs asynchronous reset handling, active-high vs active-low reset polarity, output inversion, and bypass behavior." + deferred_reason: "" + + - entity: RstPipeline + path: base/general/rtl/RstPipeline.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/general/test_RstPipeline.py + reference_assets: [] + notes: "High-reuse `base/general` reset-distribution leaf selected from the instantiation graph. Validated matrix covers stage-depth latency and `INV_RST_G` behavior; timing-only and synthesis-only generics remain intentionally out of scope." + deferred_reason: "" + + - entity: SimpleDualPortRam + path: base/ram/inferred/SimpleDualPortRam.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/ram/test_SimpleDualPortRam.py + reference_assets: [] + notes: "Graph-guided shared RAM primitive with validated direct cocotb coverage for baseline port-A write/port-B read operation, optional output register behavior, byte-write masking, and synchronous/asynchronous reset polarity variants on the read side." + deferred_reason: "" + + - entity: FifoOutputPipeline + path: base/fifo/rtl/FifoOutputPipeline.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/fifo/test_FifoOutputPipeline.py + reference_assets: [] + notes: "Graph-guided FIFO leaf with validated coverage for zero-latency passthrough, pipelined ordering, backpressure holding behavior, and synchronous/asynchronous reset polarity variants using a small FWFT-style source model." + deferred_reason: "" + + - entity: FifoWrFsm + path: base/fifo/rtl/inferred/FifoWrFsm.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/fifo/test_FifoWrFsm.py + reference_assets: [] + notes: "Graph-guided FIFO write-side primitive with validated coverage for startup readiness, count/index progression, full and overflow behavior, programmable-full threshold behavior, gray-coded async mode, and reset polarity/style variants." + deferred_reason: "" + + - entity: Synchronizer + path: base/sync/rtl/Synchronizer.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/sync/test_Synchronizer.py + reference_assets: [] + notes: "High-reuse `base/sync` leaf selected from the instantiation graph. Validated curated matrix covers synchronous vs asynchronous reset handling, active-high vs active-low reset polarity, stage-depth latency, output inversion, and bypass behavior without introducing a wrapper." + deferred_reason: "" + + - entity: FifoSync + path: base/fifo/rtl/inferred/FifoSync.vhd + subsystem: base + tier: functional_python + status: implemented_validated_expanded + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/fifo/test_FifoSync.py + reference_assets: + - base/fifo/tb/FifoFwftTb.vhd + - base/fifo/tb/FwftCntTb.vhd + notes: "Bottom-up follow-on to FifoAsync. Expanded validated matrix covers FWFT vs standard mode, block vs distributed RAM, reset polarity/style variants, wider/deeper sizing, FWFT pipeline behavior, and threshold-focused cases for the synchronous implementation path." + deferred_reason: "" + + - entity: FifoAsync + path: base/fifo/rtl/inferred/FifoAsync.vhd + subsystem: base + tier: functional_python + status: implemented_validated_expanded + priority: pilot + wrapper_path: "" + python_test: tests/base/fifo/test_FifoAsync.py + reference_assets: + - base/sync/tb/SynchronizerFifoTb.vhd + notes: "Low-level async FIFO pilot. Expanded validated matrix covers FWFT vs standard mode, block vs distributed RAM, reset polarity/style variants, wider/deeper sizing, non-default sync depth, pipeline behavior, and threshold-focused cases. The matrix intentionally excludes TPD_G and INIT_G as non-behavioral here, and BYP_RAM_G because the current implementation does not exercise a separate bypass-RAM path." + deferred_reason: "" + + - entity: DspComparator + path: dsp/generic/fixed/DspComparator.vhd + subsystem: dsp + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/dsp/generic/test_DspComparator.py + reference_assets: [] + notes: "First validated `dsp/` leaf in the new cocotb flow. Coverage focuses on comparator truth behavior and pipeline/reset variation rather than a large DSP-generic sweep." + deferred_reason: "" + + - entity: DspAddSub + path: dsp/generic/fixed/DspAddSub.vhd + subsystem: dsp + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/dsp/generic/test_DspAddSub.py + reference_assets: + - dsp/generic/tb/DspAddSubTb.vhd + notes: "Validated signed add and subtract behavior with direct and pipelined output paths, including backpressure hold and reset clearing. The legacy VHDL bench only provided free-running stimulus, so the cocotb bench replaces it with explicit arithmetic and interface assertions." + deferred_reason: "" + + - entity: FirFilterTap + path: dsp/generic/fixed/FirFilterTap.vhd + subsystem: dsp + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: generated_test_local + python_test: tests/dsp/generic/test_FirFilterTap.py + reference_assets: [] + notes: "Validated signed multiply-accumulate behavior for both generic coefficient initialization and runtime coefficient updates through a generated wrapper. The bench also checks that disabled cycles hold the previous cascade output." + deferred_reason: "" + + - entity: DspPreSubMult + path: dsp/generic/fixed/DspPreSubMult.vhd + subsystem: dsp + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/dsp/generic/test_DspPreSubMult.py + reference_assets: [] + notes: "Validated signed pre-subtract multiply behavior, output hold under backpressure, and reset recovery with a direct arithmetic reference model." + deferred_reason: "" + + - entity: DspSquareDiffMult + path: dsp/generic/fixed/DspSquareDiffMult.vhd + subsystem: dsp + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/dsp/generic/test_DspSquareDiffMult.py + reference_assets: [] + notes: "Validated signed square-difference multiplication, output hold under backpressure, and reset recovery with explicit Python arithmetic checks." + deferred_reason: "" + + - entity: BoxcarIntegrator + path: dsp/generic/fixed/BoxcarIntegrator.vhd + subsystem: dsp + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/dsp/generic/test_BoxcarIntegrator.py + reference_assets: + - dsp/generic/tb/BoxcarIntegratorTb.vhd + notes: "Validated unsigned and signed modes, direct and registered output paths, `obAck` hold behavior, and `intCount` reconfiguration. The cocotb bench keeps the useful rolling-sum intent from the legacy bench but adds explicit interface assertions." + deferred_reason: "" + + - entity: BoxcarFilter + path: dsp/generic/fixed/BoxcarFilter.vhd + subsystem: dsp + tier: functional_python + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: "" + python_test: tests/dsp/generic/test_BoxcarFilter.py + reference_assets: + - dsp/generic/tb/BoxcarFilterTb.vhd + notes: "Validated the integrated boxcar filter output scaling and hold behavior with a Python rolling-average model, replacing the legacy demo-style ramp-only bench with explicit output checks." + deferred_reason: "" + + - entity: FirFilterSingleChannel + path: dsp/generic/fixed/FirFilterSingleChannel.vhd + subsystem: dsp + tier: functional_python + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: generated_test_local + python_test: tests/dsp/generic/test_FirFilterSingleChannel.py + reference_assets: + - dsp/generic/tb/FirFilterSingleChannelTb.vhd + notes: "Validated common-clock FIR data flow, sideband alignment, and AXI-Lite coefficient programming through a generated wrapper. The new bench keeps the useful coefficient-programming intent from the legacy environment but replaces its unasserted waveform stimulus with explicit filtered-output checks." + deferred_reason: "" + + - entity: FirFilterMultiChannel + path: dsp/generic/fixed/FirFilterMultiChannel.vhd + subsystem: dsp + tier: functional_python + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: generated_test_local + python_test: tests/dsp/generic/test_FirFilterMultiChannel.py + reference_assets: [] + notes: "Validated common-clock parallel multichannel FIR frame flow and AXI-Lite coefficient updates through a generated wrapper, using a per-channel Python FIR reference model and cocotbext-axi stream endpoints." + deferred_reason: "" + + - entity: Fifo + path: base/fifo/rtl/Fifo.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: "" + python_test: tests/base/fifo/test_Fifo.py + reference_assets: [] + notes: "Validated wrapper-focused coverage for the inferred backend only. The regression checks sync-vs-async branch selection, wrapper-level data flow, `INIT_G` forwarding, and the sync-mode `wr_data_count`/`rd_data_count` aliasing rule instead of replaying the full FIFO primitive matrix." + deferred_reason: "" + + - entity: FifoCascade + path: base/fifo/rtl/FifoCascade.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: "" + python_test: tests/base/fifo/test_FifoCascade.py + reference_assets: [] + notes: "Validated wrapper-specific coverage for inferred FWFT paths. The regression checks public `progFullVec`/`progEmptyVec` stage mapping plus a curated output smoke; it intentionally does not claim full multi-stage drain ordering under the current GHDL flow." + deferred_reason: "" + + - entity: FifoMux + path: base/fifo/rtl/FifoMux.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: "" + python_test: tests/base/fifo/test_FifoMux.py + reference_assets: [] + notes: "Validated wrapper-specific coverage currently targets the stable split-to-narrow path, including endian-aware slicing and reset-safe drain behavior. The pack-to-wide branch remains intentionally unclaimed in phase 1 because it did not surface a stable visible output in the current open-source flow." + deferred_reason: "" + + - entity: AsyncGearbox + path: base/general/rtl/AsyncGearbox.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: "" + python_test: tests/base/general/test_AsyncGearbox.py + reference_assets: [] + notes: "Validated asynchronous gearbox wrapper with a Python bit-level reference model, covering representative width conversion and reset behavior without trying to exhaust the full ratio space." + deferred_reason: "" + + - entity: SynchronizerOneShotVector + path: base/sync/rtl/SynchronizerOneShotVector.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/sync/test_SynchronizerOneShotVector.py + reference_assets: [] + notes: "Validated vectorized one-shot synchronizer coverage for lane independence and bypass behavior using a small curated lane matrix." + deferred_reason: "" + + - entity: SynchronizerOneShotCntVector + path: base/sync/rtl/SynchronizerOneShotCntVector.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: generated_test_local + python_test: tests/base/sync/test_SynchronizerOneShotCntVector.py + reference_assets: [] + notes: "Validated counted vector one-shot coverage using a generated test-local wrapper to flatten the array-typed output into a cocotb-friendly interface." + deferred_reason: "" + + - entity: SyncStatusVector + path: base/sync/rtl/SyncStatusVector.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: generated_test_local + python_test: tests/base/sync/test_SyncStatusVector.py + reference_assets: [] + notes: "Validated status-vector coverage for count/IRQ snapshot behavior. Uses a generated test-local wrapper for the vectorized count output and a lockstep shared-clock helper when `COMMON_CLK_G=true`." + deferred_reason: "" + + - entity: SyncTrigPeriod + path: base/sync/rtl/SyncTrigPeriod.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/sync/test_SyncTrigPeriod.py + reference_assets: [] + notes: "Validated trigger-period monitor with curated common-clock coverage for period accumulation and reset behavior." + deferred_reason: "" + + - entity: SyncMinMax + path: base/sync/rtl/SyncMinMax.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/sync/test_SyncMinMax.py + reference_assets: [] + notes: "Validated common-clock min/max snapshot behavior, including `rstStat` reseeding. Current phase-1 coverage is intentionally limited to the stable common-clock path." + deferred_reason: "" + + - entity: MasterRamIpIntegrator + path: base/general/ip_integrator/MasterRamIpIntegrator.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: "" + python_test: tests/base/general/test_MasterRamIpIntegrator.py + reference_assets: [] + notes: "Validated IP-integrator wrapper coverage for master-side write and read request forwarding, including handshake-safe addressing and shared comment-rich cocotb sequencing." + deferred_reason: "" + + - entity: SlaveRamIpIntegrator + path: base/general/ip_integrator/SlaveRamIpIntegrator.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: "" + python_test: tests/base/general/test_SlaveRamIpIntegrator.py + reference_assets: [] + notes: "Validated IP-integrator wrapper coverage for slave-side data return and write acceptance behavior using direct cocotb handshakes against the exported RAM-style ports." + deferred_reason: "" + + - entity: DualPortRam + path: base/ram/inferred/DualPortRam.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/ram/test_DualPortRam.py + reference_assets: [] + notes: "Validated inferred dual-port RAM wrapper with curated coverage for independent port readback, write-mode semantics, registered-output behavior, byte-write masking, and reset polarity variation." + deferred_reason: "" + + - entity: SlvDelayRam + path: base/delay/rtl/SlvDelayRam.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/delay/test_SlvDelayRam.py + reference_assets: [] + notes: "Validated RAM-backed delay line for configured latency, enable-hold behavior, and reset handling. The bench documents the observed GHDL-visible latency offset instead of pretending the comment-level ideal is cycle-exact." + deferred_reason: "" + + - entity: SlvDelayFifo + path: base/delay/rtl/SlvDelayFifo.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_low_level + wrapper_path: "" + python_test: tests/base/delay/test_SlvDelayFifo.py + reference_assets: [] + notes: "Validated FIFO-backed delay line with curated coverage for timestamp-ordered delayed delivery and a short-delay smoke that stays inside the stable visible behavior under the current GHDL flow." + deferred_reason: "" + + - entity: SyncClockFreq + path: base/sync/rtl/SyncClockFreq.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: generated_test_local + python_test: tests/base/sync/test_SyncClockFreq.py + reference_assets: [] + notes: "Validated frequency-monitor wrapper using a generated real-generic shim and lockstep shared-clock helper. The common-clock case is checked against a bounded expected range because the current GHDL-visible quantization lands one count above the abstract target." + deferred_reason: "" + + - entity: SyncTrigRate + path: base/sync/rtl/SyncTrigRate.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: generated_test_local + python_test: tests/base/sync/test_SyncTrigRate.py + reference_assets: [] + notes: "Validated wrapper/integration coverage for aligned update publication, denser-window rate growth, reset-path liveness, and update-strobe pulse behavior. Exact min/max pipeline semantics remain intentionally covered by the dedicated `SyncMinMax` leaf regression." + deferred_reason: "" + + - entity: SyncTrigRateVector + path: base/sync/rtl/SyncTrigRateVector.vhd + subsystem: base + tier: functional_python + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: generated_test_local + python_test: tests/base/sync/test_SyncTrigRateVector.py + reference_assets: [] + notes: "Validated vectorized trigger-rate wrapper with a generated flattening shim, per-lane rate independence checks, and shared update-strobe pulse behavior on the stable common-clock path." + deferred_reason: "" + + - entity: LutFixedDelay + path: base/delay/rtl/LutFixedDelay.vhd + subsystem: base + tier: deferred_vendor_heavy + status: deferred_phase1 + priority: phase1_deferred + wrapper_path: "" + python_test: "" + reference_assets: [] + notes: "Single remaining non-dummy `base/` gap after the phase-1 practical rollout." + deferred_reason: "Depends on `SinglePortRamPrimitive`, so the current implementation path still drags vendor-specific infrastructure back into the open-source simulation flow." + + - entity: AxiStreamFifoV2 + path: axi/axi-stream/rtl/AxiStreamFifoV2.vhd + subsystem: axi + tier: wrapper_required + status: implemented_validated_initial + priority: pilot + wrapper_path: axi/axi-stream/ip_integrator/AxiStreamFifoV2IpIntegrator.vhd + python_test: tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py + reference_assets: + - tests/test_AxiStreamFifoV2IpIntegrator.py + notes: "Generic-heavy AXI-Stream pilot. Reused the IP integrator wrapper and migrated the Python regression into the shared package layout. Current validated sweep covers width-conversion cases plus one VALID_THOLD=0 configuration." + deferred_reason: "" + + - entity: AxiStreamPipeline + path: axi/axi-stream/rtl/AxiStreamPipeline.vhd + subsystem: axi + tier: wrapper_required + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: axi/axi-stream/ip_integrator/AxiStreamPipelineIpIntegrator.vhd + python_test: tests/axi/axi_stream/test_AxiStreamPipeline.py + reference_assets: + - tests/test_AxiStreamPipelineTb.py + notes: "Validated AXI-Stream helper with a thin flat-port wrapper. The curated sweep covers zero-stage pass-through, staged pipeline ordering and sideband forwarding, backpressure stability, and synchronous/asynchronous reset handling. Staged cases are checked against the wrapper-visible latency of `PIPE_STAGES_G + 2` clocks plus bounded reset flush behavior." + deferred_reason: "" + + - entity: AxiStreamMux + path: axi/axi-stream/rtl/AxiStreamMux.vhd + subsystem: axi + tier: wrapper_required + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: axi/axi-stream/ip_integrator/AxiStreamMuxIpIntegrator.vhd + python_test: tests/axi/axi_stream/test_AxiStreamMux.py + reference_assets: + - axi/axi-stream/tb/AxiStreamMuxTb.vhd + - axi/axi-stream/tb/AxiStreamDemuxMuxTb.vhd + notes: "Validated AXI-Stream mux coverage with a thin two-input wrapper. The curated sweep covers indexed arbitration with explicit priority plus `disableSel`, routed `TDEST`/`TID` remap under backpressure, and staged asynchronous active-low reset recovery in passthrough mode. Interleave and explicit rearbitrate branches remain intentionally open for later work." + deferred_reason: "" + + - entity: AxiStreamDeMux + path: axi/axi-stream/rtl/AxiStreamDeMux.vhd + subsystem: axi + tier: wrapper_required + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: axi/axi-stream/ip_integrator/AxiStreamDeMuxIpIntegrator.vhd + python_test: tests/axi/axi_stream/test_AxiStreamDeMux.py + reference_assets: + - axi/axi-stream/tb/AxiStreamDemuxMuxTb.vhd + - tests/test_AxiStreamDemuxMuxTb.py + notes: "Validated AXI-Stream demux coverage with a thin one-input/two-output wrapper. The curated sweep covers indexed routing, exact-match routed decode under output backpressure, and dynamic-route table behavior including unmatched-destination drop and staged asynchronous active-low reset flush. Wildcard-route patterns and larger fanout counts remain intentionally open for later work." + deferred_reason: "" + + - entity: AxiLiteCrossbar + path: axi/axi-lite/rtl/AxiLiteCrossbar.vhd + subsystem: axi + tier: wrapper_required + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: axi/axi-lite/tb/AxiLiteCrossbarTb.vhd + python_test: tests/axi/axi_lite/test_AxiLiteCrossbar.py + reference_assets: + - axi/axi-lite/tb/AxiLiteCrossbarTb.vhd + notes: "Validated AXI-Lite crossbar coverage through the existing cocotb-facing harness topology. The regression checks routed read/write correctness across the local and cascaded regions, decode-miss `DECERR` responses, and concurrent traffic without region cross-coupling." + deferred_reason: "" + + - entity: AxiLiteAsync + path: axi/axi-lite/rtl/AxiLiteAsync.vhd + subsystem: axi + tier: wrapper_required + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: axi/axi-lite/ip_integrator/AxiLiteAsyncIpIntegrator.vhd + python_test: tests/axi/axi_lite/test_AxiLiteAsync.py + reference_assets: + - axi/axi-lite/tb/AxiLiteAsyncTb.vhd + notes: "Validated AXI-Lite async-wrapper coverage with a purpose-built IP-integrator adapter. The current passing subset is intentionally narrow: common-clock pass-through plus restart/recovery on the stable wrapper path. The asynchronous reset-crossing branches remain open for later work because they were not simulator-stable enough for this initial batch." + deferred_reason: "" + + - entity: AxiStreamResize + path: axi/axi-stream/rtl/AxiStreamResize.vhd + subsystem: axi + tier: wrapper_required + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: axi/axi-stream/ip_integrator/AxiStreamResizeIpIntegrator.vhd + python_test: tests/axi/axi_stream/test_AxiStreamResize.py + reference_assets: + - axi/axi-stream/tb/AxiStreamResizeTb.vhd + notes: "Validated AXI-Stream resize coverage with a thin flat-port adapter. The curated sweep covers equal-width pass-through, 2-byte to 4-byte upsize, and 4-byte to 2-byte downsize with sideband alignment checks plus staged reset flush in the resized cases." + deferred_reason: "" + + - entity: AxiLiteMaster + path: axi/axi-lite/rtl/AxiLiteMaster.vhd + subsystem: axi + tier: wrapper_required + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: axi/axi-lite/ip_integrator/AxiLiteMasterIpIntegrator.vhd + python_test: tests/axi/axi_lite/test_AxiLiteMaster.py + reference_assets: [] + notes: "Validated AXI-Lite master coverage with a thin request/ack-to-flat-port adapter and a cocotb slave model. The first bench covers write/read request sequencing, staggered AXI ready/valid handshakes, propagated `SLVERR` responses, and reset return to idle." + deferred_reason: "" + + - entity: AxiLiteToDrp + path: axi/bridge/rtl/AxiLiteToDrp.vhd + subsystem: axi + tier: wrapper_required + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: axi/bridge/ip_integrator/AxiLiteToDrpIpIntegrator.vhd + python_test: tests/axi/bridge/test_AxiLiteToDrp.py + reference_assets: [] + notes: "Validated AXI-Lite-to-DRP bridge coverage with a thin flat-port wrapper. The current passing subset is intentionally narrow: common-clock non-arbitrated write/read mapping plus timeout-driven `SLVERR` and `drpUsrRst` recovery. The async arbitration branch remains open for later work." + deferred_reason: "" + + - entity: AxiDualPortRam + path: axi/axi-lite/rtl/AxiDualPortRam.vhd + subsystem: axi + tier: wrapper_required + status: implemented_validated_initial + priority: phase1_wrapper + wrapper_path: axi/axi-lite/ip_integrator/AxiDualPortRamIpIntegrator.vhd + python_test: tests/axi/axi_lite/test_AxiDualPortRam.py + reference_assets: + - axi/axi-lite/ip_integrator/AxiDualPortRamIpIntegrator.vhd + notes: "Validated AXI dual-port RAM coverage through the existing wrapper. The curated sweep covers AXI round-trips, system-port visibility of AXI writes, byte-masked system writes, and AXI write-disable `SLVERR` behavior when wrapper error responses are enabled." + deferred_reason: "" diff --git a/docs/plans/rtl-regression/plan.md b/docs/plans/rtl-regression/plan.md new file mode 100644 index 0000000000..d9ea61cc85 --- /dev/null +++ b/docs/plans/rtl-regression/plan.md @@ -0,0 +1,153 @@ +# SURF RTL Regression Plan + +## Objective +- Build a repo-wide regression system for synthesizable SURF RTL. +- Standardize on a single executable test framework so new work stays consistent. +- Make progress resumable across many context windows without re-discovery. + +## Chosen Methodology +- Python-only executable test logic. +- Primary stack: `pytest + cocotb + GHDL + ruckus`. +- Local Python commands should use the repo virtualenv interpreter (`./.venv/bin/python`) unless the virtualenv has already been explicitly activated in that shell. +- VHDL is allowed only for thin wrappers, shims, or required simulation models. +- Existing VHDL testbenches are reference material, not execution constraints. +- New Python regression code should use tutorial-style comments by default. +- New Python cocotb test files should start with the standard SURF/SLAC header block, not an ad hoc local header. +- Every Python regression should also carry a short module-specific `Test methodology` block immediately under the SLAC header comment. +- The header methodology block should use four wrapped bullets: `Sweep`, `Stimulus`, `Checks`, and `Timing`. +- The methodology bullets must describe the actual curated parameter sweep, the actual driven input sequence, the expected outputs or state changes, and the timing/latency/pulse/backpressure behavior being checked for that specific module. +- Do not use generic placeholder methodology prose; the header should tell a reader what this specific bench is proving. +- Keep methodology comment lines at a normal source width so the block is readable in the editor instead of turning into single-line paragraphs. +- Assume the reader is not already comfortable with cocotb. +- Comment the purpose of each major step in the test flow, including clock startup, reset sequencing, trigger waits, stimulus phases, and result checks. +- Treat the header methodology block and the in-body tutorial comments as separate requirements; one does not replace the other. +- Shared helpers may stay somewhat denser, but module-level tests should still explain how the Python coroutine behavior maps onto DUT behavior. +- When a DUT generic assumes truly common clocks, drive those clocks from one shared cocotb coroutine rather than starting two same-period clocks independently. +- For Python cocotb files, the minimum first-draft structure is: + - standard SURF/SLAC file header, + - module-specific `Test methodology` block, + - tutorial-style comments in the executable body. +- Checked-in cocotb-facing VHDL wrappers should follow the in-tree SURF style too: add the standard SLAC/SURF banner at the top and include brief section comments for the major adapter regions. +- For `*IpIntegrator.vhd` wrappers, the minimum expected sectioning is usually: + - bus shim section, + - DUT instantiation section, + - output/status flattening section when present. +- Do not leave permanent checked-in wrappers as uncommented bare port maps even if the logic is thin; future sessions should be able to scan the file and identify the adapter shape immediately. +- For any VHDL file you create or edit, run the `vsg` linter with the same configuration CI uses (`./.venv/bin/vsg -c vsg-linter.yml ...`) before considering the work done. +- When `vsg` reports fixable issues, use `--fix`/autofix first, then rerun the same CI-configured lint command to confirm the file is clean. + +## Scope +- Whole repo target. +- Phase 1 focuses on simulator-friendly modules. +- Vendor-heavy modules are deferred in phase 1 unless they become practical under the open-source flow. + +## Coverage Model +- `functional_python` + - Module has a Python-authored cocotb regression. +- `smoke_python` + - Module has compile/elaborate coverage only. +- `wrapper_required` + - Module needs a retained or added VHDL wrapper to expose a cocotb-friendly interface. +- `deferred_vendor_heavy` + - Module is intentionally excluded from phase 1 executable regression. + +## Package Coverage Policy +- VHDL packages are not treated as standalone executable regression targets. +- Type/constant packages are covered transitively through the modules that compile and use them. +- Behavioral package functions and procedures should be covered through DUTs that exercise them whenever practical. +- If an important package function or procedure is not well reached transitively, add a minimal VHDL wrapper and test that wrapper from Python. +- Package-helper wrappers should be tracked separately from the main synthesizable-module inventory when they are introduced. + +## Generic And Configuration Policy +- Generic-heavy modules are Python-first by default. +- Build curated configuration matrices in Python. +- Do not use naive full Cartesian products for broad generic spaces. +- Compute expected behavior dynamically in Python from the active generics. +- If simulator limitations make direct generic overrides awkward, prefer checked-in subsystem-local VHDL wrappers over ad hoc test-local copies. +- Keep checked-in wrappers thin and declarative: expose cycle-friendly or cocotb-friendly generics, map them onto the real DUT generics, and keep them beside the subsystem RTL they adapt. +- For integration wrappers, test the wrapper-specific behavior rather than replaying the full underlying leaf matrix through the wrapper. +- If only a simulator-stable subset of a wrapper is practical in phase 1, keep that subset intentionally narrow and document the unvalidated branches explicitly in the handoff/progress docs. + +## CI And Runtime Policy +- Tier-first split. +- Separate `smoke` and `functional` regression tiers. +- Shard by subsystem only if runtime requires it. +- Prefer parallel pytest for normal local validation and subsystem slices, using + `-n auto --dist=worksteal` unless a single cocotb simulation needs serial log + ordering or interactive debugging. +- Keep room for PR-vs-nightly expansion later if runtime and coverage needs justify it. +- Treat simulator process cleanup as part of every verification step, not as optional housekeeping. +- After any command that launches `pytest`, cocotb, GHDL, or another simulation runner, check for stale child processes and kill any leftovers before moving on to the next step. +- When cleanup is needed, prefer an explicit process sweep first (for example with `ps -Ao pid,ppid,stat,time,command`) so only the stale run trees are terminated. + +## Reuse Policy +- Legacy VHDL testbenches are reference material only. +- Rewrite executable test logic in Python when migrating a module into the new regression system. +- Keep VHDL wrappers only when they make Python stimulus materially cleaner. +- Do not preserve old benches purely for historical reasons. +- Before writing new cocotb transaction code, search the nearest subsystem `tests/` package for an existing `*_test_utils.py` or equivalent shared helper module and reuse it when possible. +- Prefer extending an existing helper with one more narrowly useful utility over cloning handshake loops, packet builders, frame receivers, or register-access boilerplate into each new test file. +- For AXI-Lite work, look for existing read/write helpers, setup helpers, and protocol-master wrappers first; do not hand-code repeated register transactions if the subsystem already has a stable helper path. +- For AXI Stream work, look for existing frame/beat helpers, contiguous-send helpers, receive helpers, keep-mask helpers, and handshake monitors before writing custom ready/valid loops. +- For SSI work, prefer the existing SSI helper layer for flat endpoint setup, beat modeling, frame send/receive, no-output checks, and `EOFE`/`SOF`-aware assertions instead of rebuilding SSI transaction plumbing in each bench. +- When a wrapper is needed only to adapt simulator-hostile generics, check it into the nearest subsystem-local `wrappers/` or `ip_integrator/` folder instead of hiding it under `tests/` or a generic `hdl/` bucket. +- For SURF AXI/AxiLite record ports, prefer the existing IP-integrator shim layers (`SlaveAxiStreamIpIntegrator`, `MasterAxiStreamIpIntegrator`, `SlaveAxiLiteIpIntegrator`, `MasterAxiLiteIpIntegrator`) instead of hand-writing record-to-flat unpacking in each test wrapper. +- If a DUT has extra nonstandard side signals, compose those on top of the standard AXI shim pair rather than replacing the standard flattening pattern. +- For wrapper-style protocol benches, prefer thin subsystem wrappers plus cocotb protocol masters/RAM models, and add accepted-handshake monitoring whenever timing-visible protocol behavior is part of the contract being proven. +- More generally, if a VHDL shim layer is needed to make a module practical to drive from cocotb, place that file in the nearest real subsystem `wrappers/` or `ip_integrator/` folder beside related adapter layers. +- Do not place cocotb-facing shim/adaptor VHDL under `tests/` or generic `hdl/` buckets when it is serving the same integration role as the existing `*IpIntegrator.vhd` files. +- When a wrapper is checked in under `wrappers/` or `ip_integrator/`, treat it like production repo HDL for readability purposes: keep the standard file banner and add concise section comments instead of relying on file naming alone. +- Treat checked-in Python cocotb tests the same way: use the normal repo header/comment style in the first draft instead of leaving cleanup for later. + +## Rollout Planning Policy +- The active planning driver is now manual user-directed area selection, with `docs/plans/rtl-regression/progress.md` and `docs/plans/rtl-regression/handoff.md` tracking what is done, what is intentionally narrow, and what remains open. +- Prefer testing high-reuse leaf primitives directly before spending effort on higher-level assemblies that mostly repackage them, but choose targets from the user's current direction and the documented open frontier. +- Use any regenerated instantiation graph or queue only as temporary analysis output; do not check it into `docs/plans/rtl-regression/` or read it by default in fresh context windows. +- Keep this plan policy-oriented. Day-to-day target selection, validation status, and known gaps belong in the progress and handoff docs. + +## CoaXPress Spec Discipline +- Treat the published CoaXPress specifications as normative for future `protocols/coaxpress/` work, especially for top-level receive/transmit and over-fiber bridge benches. +- The two governing references are the CoaXPress protocol spec (`CXP-001-2021`) and the CoaXPress-over-Fiber bridge spec (`CXPR-008-2021`), matching the links already called out in `protocols/coaxpress/core/rtl/CoaXPressPkg.vhd`. +- When a CoaXPress bench encodes packet classes, control symbols, or bridge control characters, derive those values from the spec-defined names first and mirror them through shared helpers such as `tests/protocols/coaxpress/coaxpress_test_utils.py` instead of scattering raw literals. +- At the packet layer, prefer the published names even when the current RTL signal naming drifts; for example, `0x07` is an event packet and `0x08` is an event acknowledgment even though some existing RTL ports still use `eventAck` for the receive-side event indication. +- For CoaXPress image/header benches, keep the repeated-byte field encoding, header field order, endianness conversion, line-size semantics, and end-of-frame rules explicitly tied to the spec-defined rectangular image packet layout. +- For CoaXPress-over-Fiber benches, keep `/I/`, `/Q/`, `/S/`, `/T/`, and `/E/` handling, lane-0-only start/sequence semantics, and payload-vs-housekeeping start words aligned to `CXPR-008-2021`. +- If a checked-in bench intentionally validates only the current RTL contract instead of the full normative spec behavior, document that narrowed scope explicitly in the progress and handoff docs rather than implying full spec coverage. +- If a CoaXPress top-level bench has to be checked in as skipped because it exposes a likely RTL defect, keep the spec-shaped stimulus and the skip reason in-tree, and record the blocking symptom explicitly in the progress and handoff docs so the next pass resumes from the defect rather than from scratch. + +## Optional Graph Analysis +The old checked-in graph and queue artifacts have been retired from this task directory to keep fresh context small and avoid stale task selection. + +If hierarchy analysis is useful: +1. Use `./.venv/bin/python scripts/build_rtl_instantiation_graph.py`. +2. Read the generated output from the script's temporary output directory, or pass an explicit non-`docs/plans/rtl-regression` `--output-dir`. +3. Treat the graph and queue as disposable reference material only. +4. Keep the real done/open frontier in `docs/plans/rtl-regression/progress.md` and `docs/plans/rtl-regression/handoff.md`. + +## Phase Breakdown +### Phase 1 +- Create the regression inventory and artifact scaffolding. +- Establish shared Python regression helpers. +- Add smoke coverage for simulator-friendly modules. +- Add functional Python tests for the highest-value pilot modules and reusable blocks. +- Define the migration pattern for wrappers and generic-heavy modules. +- Standardize the subsystem-local checked-in wrapper pattern for real- or vector-generic leaves that need cycle-native test knobs under GHDL. + +### Phase 2 +- Deepen randomized and adversarial coverage. +- Expand curated configuration sweeps for generic-heavy modules. +- Add stronger reusable scoreboards and protocol-specific helpers. +- Revisit deferred vendor-heavy modules after phase 1 baseline stability. + +## Acceptance Criteria For Phase 1 +- The repo has a checked-in inventory and handoff system. +- New windows can recover project state by reading the handoff artifacts only. +- The Python-only regression direction is documented and stable. +- The progress and handoff artifacts stay aligned with the actual validated branch frontier instead of lagging behind completed subsystem waves. +- The smoke/functional tier split is established in the plan and progress tracking. + +## Open Questions And Deferred Decisions +- Whether PR-vs-nightly split is needed immediately or only after runtime data. +- Exact criteria for moving a vendor-heavy module out of `deferred_vendor_heavy`. +- Which user-directed subsystem slice should be taken next after the current documented frontier. +- Whether a separate tracked list of high-risk behavioral package helpers is needed once the module inventory stabilizes. diff --git a/docs/plans/rtl-regression/progress.md b/docs/plans/rtl-regression/progress.md new file mode 100644 index 0000000000..5f253566a5 --- /dev/null +++ b/docs/plans/rtl-regression/progress.md @@ -0,0 +1,365 @@ +# SURF RTL Regression Progress + +## Summary +- Current phase: Phase-1 implementation tracking; no active RTL edit is selected +- Current subsystem: manual user-directed rollout tracking and planning-file housekeeping +- Current focus module: no active RTL implementation item is selected; the current tree reflects the completed CoaXPress, SRP, base-depth, AXI, SSI/PGP, Ethernet, and DSP regression work described below, with planning files now housed under `docs/plans/rtl-regression/`. +- Last updated: 2026-05-21 + +## Current Frontier Snapshot +- Active planning rule: take the next work item from the user's manual direction, not from any generated graph or queue artifact. +- Manual planning note: + - 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 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. + - Treat stale simulator cleanup as mandatory after every launched verification command: after any `pytest`, cocotb, GHDL, or similar simulation step, sweep for leftover child processes and kill them before starting the next task. +- Known expected-open tests on this branch: + - No simulator-friendly expected-open leaf tests remain in the currently covered `ethernet/IpV4Engine` slice, and the recent `EthMacCore` / `UdpEngine` thin-area follow-up is also checked in on this branch. + - The current `EthMac*Xlgmii` import/export leaves are still placeholder no-op RTL, so the checked-in benches document that inert contract rather than claiming functional XLGMII datapath coverage. + - The remaining Ethernet work is in the larger untouched families `GigEthCore`, `TenGigEthCore`, `XauiCore`, `XlauiCore`, and `Caui4Core`, plus the five still-open RoCEv2 RTL entities that depend on generated submodules: `EthMacCrcAxiStreamWrapperSend`, `EthMacCrcAxiStreamWrapperRecv`, `EthMacTxRoCEv2`, `EthMacRxRoCEv2`, and `RoceEngineWrapper`. +- RoCEv2 RTL target matrix: + - Covered now under the current GHDL-only flow: `EthMacPrepareForICrc`, `EthMacRxCheckICrc`, `RoceResizeAndSwap`, `RoceConfigurator` + - Still required, but need real `blue-*` dependencies under a mixed-language simulator: `EthMacCrcAxiStreamWrapperSend`, `EthMacCrcAxiStreamWrapperRecv`, `EthMacTxRoCEv2`, `EthMacRxRoCEv2`, `RoceEngineWrapper` +- CoaXPress RTL target matrix: + - Covered now under the current GHDL-only flow: `CoaXPressRxWordPacker`, `CoaXPressRxLaneMux`, `CoaXPressRxLane`, `CoaXPressRxHsFsm`, `CoaXPressRx`, `CoaXPressEventAckMsg`, `CoaXPressTxLsFsm`, `CoaXPressConfig`, `CoaXPressTx`, `CoaXPressCore`, `CoaXPressOverFiberBridgeRx`, `CoaXPressOverFiberBridgeTx`, and `CoaXPressOverFiberBridge` + - `tests/protocols/coaxpress/test_CoaXPressConfig.py` is active again after the SRP helper cleanup. It now drives requests through the real `CoaXPressConfig`/`SrpV3AxiLite` ingress path and checks all four tagged/untagged read/write command-format quadrants, CRC generation, tag incrementing, SRPv3 response completion, and timeout/nonzero-ack-status error footer behavior. + - The former CoaXPress RX known-issue benches have been folded into normal coverage where they are fast enough: four-lane short-frame rotation/recovery, repeated single-line image frames in both `CoaXPressRxHsFsm` and top-level `CoaXPressRx`, and the core RX overflow-vs-FSM-error counter behavior. The heavy four-lane overflow recovery workloads are now explicit opt-in stress checks behind `RUN_STRESS_TESTS=1`. + - Spec discipline for this family: use the packet/control naming and byte values from `CXP-001-2021` and `CXPR-008-2021` via shared helpers, and keep future top-level/bridge benches explicit about whether they are proving normative spec behavior or only the currently validated RTL subset. At the packet layer, keep the published names even when current RTL signal names drift: `0x07` is an event packet and `0x08` is an event acknowledgment. + - The checked-in RX benches now use fuller spec-shaped control-ack framing on the wire, include `CoaXPressRxHsFsm` incomplete-frame new-header detection plus the dual-lane step/alignment case, include multi-lane `CoaXPressRx` lane-rotation cases, and validate receive-lane control-ack, heartbeat, event, and stream-data CRC/`EOP` trailers. `CoaXPressRxLane` now buffers bounded event payloads and releases them on `eventMaster` only after CRC/`EOP` validation, `CoaXPressRx` crosses that payload stream into `cfgClk`, and the legacy `eventAck/eventTag` pulse still arrives only after a valid event CRC/`EOP`. Receive-lane malformed-packet pulses now aggregate into `CoaXPressRx.rxFsmError` and the existing core `RxFsmErrorCnt` software counter. The stream-data path now follows the SURF SSI terminal-error convention: payload still streams as it arrives, but the lane emits an in-order trailer verdict marker, `CoaXPressRxHsFsm` holds only the final packed image EOF beat until that verdict arrives, and malformed CRC/`EOP` trailers are reported with SSI `EOFE` on that final beat rather than by retroactively dropping the already-forwarded payload. + - The CXPoF bridge benches now include the recent optional-depth guardrails from `coaxpress-tests`: leaf embedded EOP K-code reconstruction, HKP-to-payload mixing, HKP-carried CXP EOP reconstruction, broader malformed control-lane sweeps for `/S/`, `/Q/`, `/T/`, and `/E/`, plus top-level 64-bit RX coverage for `/E/` abort/recovery, HKP-to-payload gearbox traversal, and lane-0 `/Q/` status/no-output/recovery behavior. `CoaXPressOverFiberBridgeRx` now exposes `/Q/` ordered-set data plus sequence policy, classified `/E/` and malformed-condition status, and HKP K-code validation/classification through the product-facing `CxpofRxStatusType` record. The cocotb bridge benches target thin wrapper entities that flatten that record only for simulator visibility, while `CoaXPressOverFiberBridgeAxiL` makes the same status software-visible as sticky bits, last-observed fields, and counters in the GTH/GTY wrapper AXI-Lite maps. The AXI-Lite regression now sweeps the named HKP K-code classification values through the packed HKP readback register so software-visible consumers are covered beyond a single EOP sample. + - Most recent recorded CoaXPress validation after the HKP AXI-Lite consumer sweep is green for the full directory: `./.venv/bin/python -m pytest -q tests/protocols/coaxpress` (`19 passed` in 560.44 seconds). The focused bridge sanity run `./.venv/bin/python -m pytest -q tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeRx.py tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py` also passed with `2 passed`. +- SRP RTL target matrix: + - Covered now under the current GHDL-only flow: `SrpV3Axi` through the checked-in `protocols/srp/wrappers/SrpV3AxiWrapper.vhd` wrapper and `tests/protocols/srp/test_SrpV3Axi.py`. + - The current SRPv3 AXI bench now validates non-posted write echo/readback, posted-write no-response behavior followed by readback, NULL header/footer behavior, response backpressure hold, TDEST propagation, full-word `TKEEP`, and footer bits for version mismatch, malformed write framing, invalid alignment, invalid request size, and downstream address error. + - `SrpV3Core` now has a direct checked-in wrapper with reset/idle smoke coverage plus a narrow 32-bit fault-injection mode for malformed-header, immediate-read-error, disabled-read/write, missing-SOF blowoff, short-write framing, and early/late read-data TLAST EOFE behavior. `tests/protocols/srp/test_SrpV3Core.py` now uses `SrpV3CoreWrapper` in both default mode and `CORE_DATA_BYTES_G => 4` mode instead of carrying a separate narrow test file. The narrow mode exposed and now covers two core-side bugs: the response-header counter was not reset when emitting an error response from a truncated request header, and `READ_S` could miss an immediate downstream read error before any payload beat arrived. + - `AxiLiteSrpV0` and `SrpV0AxiLite` are now covered directly and together. `tests/protocols/srp/test_AxiLiteSrpV0.py` uses `protocols/srp/wrappers/AxiLiteSrpV0Wrapper.vhd` to validate AXI-Lite-to-SRPv0 request field packing, read data return, bad-response `SLVERR`, and bleed/recovery behavior. `tests/protocols/srp/test_SrpV0AxiLite.py` uses `protocols/srp/wrappers/SrpV0AxiLiteWrapper.vhd` to validate multi-word write/read frames, malformed/unsupported request status failures, `EN_32BIT_ADDR_G` address expansion, and downstream AXI-Lite read/write error propagation into the legacy fail status. `tests/protocols/srp/test_SrpV0Loopback.py` still covers both bridge halves together through the checked-in stream loopback wrapper and attached RAM. + - SRPv3 AXI-Lite status: `tests/protocols/srp/test_SrpV3AxiLite.py` now has active reset/idle coverage for the direct, full, and legacy-wide `DATA_BYTES_G => 32` modes, active narrow probes (`short_frame`, `four_beat_header`, and `single_read`) for both the direct and full wrappers, active directed regressions for the direct and full wrappers, one active legacy-wide directed regression, and direct-wrapper `ignoreMemResp` coverage for an AXI-Lite `SLVERR` read. The earlier direct-wrapper failure was not an `SrpV3AxiLite` ingress RTL defect after all: the real issue was in `tests/protocols/srp/srp_test_utils.py`, where `FlatSrpAxis.send_packed_words()` treated combinational `TREADY` as immediate acceptance instead of holding each beat until a sampled clock edge confirmed the handshake. With that helper fixed and the two `SrpV3Core` fixes in place, the direct and full `SrpV3AxiLite` paths now pass on the original `SsiFrameLimiter` bypass configuration (`SLAVE_FIFO_G => false`), and the focused standalone `tests/protocols/ssi/test_SsiFrameLimiter.py` regressions remain green in both limiter modes. The duplicate wide-wrapper probe/direct cases were removed rather than kept as skipped opt-in coverage, and the old wide/narrow-only SRP wrapper files were folded into generics, so the default `tests/protocols/srp` run is now skip-free. +- Flat AXI/SSI helper cleanup status: + - `tests/axi/utils.py` now owns the shared `wait_sampled_ready()` primitive for flattened ready/valid sources that cannot use `cocotbext.axi.AxiStreamSource` directly. + - The current flat-driver users are `tests/protocols/ssi/ssi_test_utils.py`, `tests/protocols/srp/srp_test_utils.py`, `tests/protocols/pgp/pgp4/pgp4_test_utils.py`, `tests/protocols/coaxpress/coaxpress_test_utils.py`, `tests/ethernet/EthMacCore/ethmac_test_utils.py`, and `tests/ethernet/RawEthFramer/raw_eth_test_utils.py`, plus direct one-off benches in `tests/protocols/srp/test_SrpV3Axi.py`, `tests/protocols/ssi/test_SsiResizeFifoEofe.py`, `tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py`, `tests/protocols/coaxpress/test_CoaXPressTx.py`, `tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py`, and `tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py`. + - One important cleanup correction landed during that pass: when a source now waits with `wait_sampled_ready()`, the handshake already completed on the sampled edge, so the source must advance or deassert immediately after the helper returns. The first draft left extra post-handshake clock edges in a few benches and caused false regressions; those benches were corrected before the current validation run. + - Current validation for that cleanup pass is green on `tests/protocols/srp/test_SrpV3Axi.py`, `tests/protocols/ssi/test_SsiResizeFifoEofe.py`, the PGP4 subset (`test_Pgp4Tx.py`, `test_Pgp4LiteRxLowSpeed.py`, `test_Pgp4RxCrcError.py`, `test_Pgp4RxLiteLowSpeedLane.py`, `test_Pgp4TxProtocol.py`, `test_Pgp4TxLiteProtocol.py`, `test_Pgp4TxLite.py`), `tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py`, `tests/protocols/coaxpress/test_CoaXPressTx.py`, `tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py`, and `tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py` with `174 passed`. + - Remaining intentionally manual cases after the static sweep are `tests/protocols/pgp/pgp4/test_Pgp4Rx.py`, the interleaved capture helper in `tests/protocols/pgp/pgp4/pgp4_test_utils.py`, and the manual stress path in `tests/axi/axi_stream/test_AxiStreamScatterGather.py`. Those were left alone because they interleave input acceptance with concurrent output observation or deliberately spell out handshake timing inside the test body. +- Most recent reusable bench pattern: + - Prefer the existing subsystem shims, cocotb protocol masters/RAM models, and explicit handshake monitoring when the behavior under test includes timing-visible protocol details. + - For `protocols/pgp`, keep the first pass family-oriented: shared Python helper coverage in `tests/protocols/pgp/pgp_test_utils.py`, family subpackages under `tests/protocols/pgp/pgp2b/`, `tests/protocols/pgp/pgp2fc/`, `tests/protocols/pgp/pgp3/`, and `tests/protocols/pgp/pgp4/`, thin wrapper-level loopback benches for the core lane/core surfaces, direct register benches for the AXI management blocks, and checked-in low-speed wrappers whose benches assert wrapper-visible lock/config behavior unless the serialized payload-recovery path is explicitly proven. `pgp3` stays documented and organized, but it is intentionally out of the near-term rollout plan on this branch. The reusable helper now also tolerates the trailing zero padding that appears on short odd-byte frames when the older 16-bit SSI/PGP2 wrappers return compressed-keep traffic through `cocotbext.axi`. + - For `ethernet`, keep using the checked-in wrappers under `ethernet/*/wrappers/` plus the shared helper layers in `tests/ethernet/EthMacCore/ethmac_test_utils.py`, `tests/ethernet/RawEthFramer/raw_eth_test_utils.py`, `tests/ethernet/UdpEngine/udp_test_utils.py`, and `tests/ethernet/IpV4Engine/ipv4_test_utils.py`. The practical first-pass shape is flat EMAC/app beat wrappers with explicit sideband bits, curated functional slices instead of exhaustive generic sweeps, and dedicated pair/integration wrappers only where an old VHDL bench contributes a topology worth preserving. + - For `protocols/coaxpress`, the practical first-pass shape is subsystem-local wrappers under `protocols/coaxpress/core/wrappers/` plus a small helper layer in `tests/protocols/coaxpress/coaxpress_test_utils.py`. Keep the input side raw and cycle-native where the DUT already consumes protocol words directly, and only flatten the observable AXI-stream outputs or handshaked lane arrays that cocotb actually needs to inspect. + - The checked-in CoaXPress helper now carries named spec constants for packet classes and bridge control characters. Reuse those names instead of introducing more raw `0x01`/`0x02`/`0x07`-style literals in future benches. + +## Status +| Subsystem | Inventory | Smoke | Functional | Notes | +| --- | --- | --- | --- | --- | +| Cross-cutting infrastructure | started | not started | started | Shared helper structure now lives in `tests/common/regression_utils.py`; pytest now defaults to `xdist` parallel execution via `pytest.ini`; `progress.md` and `handoff.md` are the active planning inputs, while graph/queue analysis is regenerated only as temporary reference material when needed | +| `base` | started | not started | started | Validated low-level regressions now exist for `FifoAsync`, `FifoSync`, `FifoOutputPipeline`, `FifoWrFsm`, `FifoRdFsm`, `Fifo`, `FifoCascade`, `FifoMux`, `Synchronizer`, `SynchronizerVector`, `SynchronizerEdge`, `SynchronizerOneShot`, `SynchronizerFifo`, `SynchronizerOneShotCnt`, `SynchronizerOneShotVector`, `SynchronizerOneShotCntVector`, `SyncStatusVector`, `SyncTrigPeriod`, `SyncMinMax`, `SyncClockFreq`, `SyncTrigRate`, `SyncTrigRateVector`, `RstSync`, `RstPipeline`, `RstPipelineVector`, `PwrUpRst`, `Arbiter`, `ClockDivider`, `Debouncer`, `Gearbox`, `AsyncGearbox`, `Heartbeat`, `Mux`, `OneShot`, `RegisterVector`, `WatchDogRst`, `Scrambler`, `MasterRamIpIntegrator`, `SlaveRamIpIntegrator`, `SimpleDualPortRam`, `DualPortRam`, `TrueDualPortRam`, `LutRam`, `SlvDelay`, `SlvFixedDelay`, `SlvDelayRam`, `SlvDelayFifo`, `Crc32Parallel`, `Crc32`, and `CRC32Rtl` under subsystem-organized `tests/base/` packages. The current `test-base-2` depth pass adds targeted coverage for `FifoMux` pack-to-wide conversion plus partial-pack reset, `FifoAsync` burst/backpressure/reset behavior plus near-full turnover, `FifoSync` simultaneous read/write near full, `FifoCascade` staged pressure recovery, `SynchronizerFifo` explicit read-enable gaps plus reset while prefetched, `Arbiter` starvation rotation, `WatchDogRst` noisy near-timeout keepalive behavior, `DualPortRam` same-address cross-port collision, `SimpleDualPortRam` port-B enable hold behavior in direct and registered-output modes, `TrueDualPortRam` same-address dual-write collision recovery, `SlvDelayFifo` reset flushing of multiple pending entries, and `SlvDelayRam` reset-aligned runtime delay growth with output-history discard. Remaining uncovered `base/` entities are vendor-heavy, dummy-backed, or `LutFixedDelay`, which is deferred because it depends on `SinglePortRamPrimitive`. | +| `dsp` | started | not started | started | The planned `dsp/generic/fixed` leaf batch is now validated under `tests/dsp/generic/`: `FirFilterTap`, `DspAddSub`, `DspComparator`, `DspPreSubMult`, `DspSquareDiffMult`, `BoxcarIntegrator`, `BoxcarFilter`, `FirFilterSingleChannel`, and `FirFilterMultiChannel`. Shared DSP helpers now live in `tests/dsp/generic/dsp_test_utils.py`, and the legacy VHDL benches under `dsp/generic/tb/` were treated as behavioral reference material rather than as execution constraints. | +| `axi` | started | not started | started | The axi-first pass is now complete for the simulator-friendly queue. The final locally validated batch adds `AxiReadEmulate`, `AxiRingBuffer`, `AxiWriteEmulate`, `AxiStreamDmaRingRead`, `AxiStreamDmaWrite`, `AxiLiteRamSyncStatusVector`, `AxiStreamMonAxiL`, `AxiStreamDma`, `AxiStreamDmaFifo`, `AxiStreamDmaRingWrite`, and `AxiMonAxiL`, with a combined `11 passed` validation run on 2026-03-27. Added checked-in subsystem wrappers under `axi/axi4/ip_integrator/`, `axi/axi-lite/ip_integrator/`, `axi/axi-stream/ip_integrator/`, and `axi/dma/ip_integrator/` for those benches. `AxiStreamFifoV2` now has an expanded `10 passed` wrapper regression under `tests/axi/axi_stream/` covering async and sync width conversion, metadata truncation, `VALID_THOLD` frame-ready and burst-release modes, dynamic pause-threshold behavior, `CASCADE_SIZE=2`, and the `S_HAS_TREADY=0` pause-only source-side path. `AxiResize` now passes its equal-width, `32-bit -> 64-bit`, and `64-bit -> 32-bit` wrapper regression on this branch after the read-hold RTL fix. `AxiLiteAsync`, `AxiLiteToDrp`, and `AxiRateGen` still keep intentionally narrow common-clock subsets while the more timing-sensitive async AXI-Lite crossing branches remain open. `AxiStreamCompact`, `AxiStreamFrameRateLimiter`, and `AxiStreamDmaV2WriteMux` still keep intentionally narrow first-pass subsets. `AxiStreamDmaV2Read` is now validated with a two-case wrapper regression covering both aligned and short terminal-beat reads after fixing bounded byte-count conversion in `AxiPkg` and terminal-mask generation in `AxiStreamDmaV2Read`. | +| `protocols` | started | not started | started | `protocols/line-codes` is now validated under `tests/protocols/line_codes/` with shared Python helper coverage for `LineCode8b10b`, `LineCode10b12b`, and `LineCode12b14b`, plus package-level `Code8b10b`, `Code10b12b`, and `Code12b14b` cocotb coverage. The package benches preserve explicit disparity-seed sweeps, and the 12b14b package bench also preserves its historical training/transition sequences. `protocols/packetizer/` now has standalone module regressions under `tests/protocols/packetizer/`: `AxiStreamPacketizer` checks legacy V0 header formatting, appended-tail and separate-tail packet termination, plus max-size split packet continuation; `AxiStreamDepacketizer` checks direct V0 packet-header/tail restoration for both tail encodings plus valid two-packet continuation; `AxiStreamPacketizer2` checks V2 header/payload/tail formatting, split-frame sequence behavior, partial final `TKEEP`, and interleaved-`TDEST` rearbitration state; `AxiStreamDepacketizer2` checks direct packet-header/tail stimulus restoration of payload, `TDEST`, `TID`, SOF, `TLAST`, per-byte `TUSER`, partial final `TKEEP`, and CRC-none nonzero-tail `EOFE` marking; and `AxiStreamBytePacker` now sweeps 2-to-5, 3-to-6, 4-to-8, and 5-to-7 compressed-keep width conversions while checking compacted payload/user ordering across partial beats, exact-width terminal packing, reset flush of a partial word, idle-gap preservation, and ignored output-ready behavior. These benches avoid using a loopback oracle for the individual leaf contracts. `protocols/ssi/` now has checked-in cocotb coverage for `SsiInsertSof`, `SsiIbFrameFilter`, `SsiObFrameFilter`, and `SsiFifo`, plus first-pass coverage for `SsiCmdMasterPulser`, `SsiCmdMaster`, `SsiFrameLimiter`, `SsiIncrementingTx`, `SsiAxiLiteMaster`, and a traffic-smoke `SsiDbgTap`, all backed by the shared SSI helper layer in `tests/protocols/ssi/ssi_test_utils.py` and checked-in wrappers under `protocols/ssi/wrappers/`. The SSI helper layer now uses handshake-based frame receive/capture so contiguous multi-beat traffic is observed correctly, the checked-in benches now carry the tutorial-style comment pass and shared setup cleanup, `SsiObFrameFilter` now covers the cached-last-user `VALID_THOLD_G=0` malformed/drop path plus a pipelined `PIPE_STAGES_G=2` pass-through case, and the remaining SSI gaps are optional deeper branch work rather than blockers. `protocols/pgp/` now has shared helper coverage in `tests/protocols/pgp/pgp_test_utils.py`, family-organized benches under `tests/protocols/pgp/pgp2b/`, `tests/protocols/pgp/pgp2fc/`, `tests/protocols/pgp/pgp3/`, `tests/protocols/pgp/pgp4/`, and `tests/protocols/pgp/shared/`, checked-in wrappers for the direct-wrapper cases, validated loopback benches for `Pgp2bLane`, `Pgp2fcLane`, `Pgp4Core`, `Pgp4CoreLite`, and `Pgp3Core`, direct wrapper benches for `Pgp2bAxi`, `Pgp2fcAxi`, `Pgp4AxiL`, `Pgp4TxLite`, `Pgp4Tx`, `Pgp4RxLiteLowSpeedReg`, the remaining non-vendor `pgp4/core/rtl` leaves (`Pgp4Rx`, `Pgp4RxEb`, `Pgp4RxProtocol`, `Pgp4TxLiteProtocol`, `Pgp4TxProtocol`, `Pgp4RxLiteLowSpeedLane`, and `Pgp4LiteRxLowSpeed`), and the shared VC FIFOs (`PgpRxVcFifo` and `PgpTxVcFifo`). The two shared FIFO benches now cover async link-up pass-through plus the family-specific control behavior: `PgpRxVcFifo` covers link-down blowoff and exported pause assertion under output backpressure, while `PgpTxVcFifo` covers link-down drop and mid-frame flush/truncation. The two `pgp4` low-speed receive leaves now have dedicated guardrail benches around lock/config behavior through checked-in wrappers rather than the earlier smoke-only placeholders, while `pgp2b/core/rtl` and `pgp2fc/core/rtl` both have leaf-wrapper cocotb coverage across every non-vendor entity. `pgp2fc` now also has fixed-latency fast-control coverage at three levels: `Pgp2fcTxPhy` checks deterministic FC entry timing from the quiescent training path plus active cell and empty-path contexts, `Pgp2fcTx` checks scheduler-context invariance, and `Pgp2fcLane` loopback calibrates `TX_FC_VALID -> RX_FC_VALID` latency once and proves that the same cycle delta holds while user frame traffic is active. The current `pgp2b` and `pgp2fc` leaf suite is intentionally mixed-depth: `Pgp2bTxSched`, `Pgp2bTxPhy`, `Pgp2bTx`, `CRC7Rtl`, `Pgp2fcAlignmentChecker`, `Pgp2fcTxSched`, `Pgp2fcTxPhy`, and `Pgp2fcTx` keep directed behavioral checks, while the remaining RX, cell, and alignment-control benches currently hold first-pass quiescent or guardrail assertions until later wrapper expansion exposes enough state for deeper deterministic traffic. `protocols/coaxpress/` now has a broader pure-VHDL staged rollout under `tests/protocols/coaxpress/` backed by checked-in wrappers in `protocols/coaxpress/core/wrappers/`: the receive leaves now cover offset-start packing, lane rotation, stream/config/event/heartbeat decode with control-ack, heartbeat, and event CRC/`EOP` guardrails, rectangular-header framing, bounded event payload validate-before-release, and a dual-lane `CoaXPressRxHsFsm` step/alignment case; the receive assembly now covers both the original one-lane integration path and a dual-lane `CoaXPressRx` lane-rotation case; the transmit/bridge leaf benches cover `CoaXPressEventAckMsg` serialization under backpressure, `CoaXPressTxLsFsm` idle/config/trigger cadence across both implemented low-speed rates, `CoaXPressOverFiberBridgeTx` CXPoF start/payload/terminate formatting plus partial-lane fill behavior, and `CoaXPressOverFiberBridgeRx` CXPoF start-word decode plus HKP K-code status, `/Q/` sequence policy, `/E/` cause classification, and negative lane-placement guardrails; and the higher-level assembly benches cover `CoaXPressTx` config/event-ack arbitration plus software-trigger injection across the cfg-to-tx clock crossing and `CoaXPressOverFiberBridge` top-level 32b/64b gearbox integration on both the TX and RX sides. The current CoaXPress subset remains intentionally staged: it normalizes packet/control literals onto named spec constants in `tests/protocols/coaxpress/coaxpress_test_utils.py`, marks malformed stream-data trailers with SSI `EOFE` on the final image beat, and still documents full buffered bad-payload dropping as an open spec-depth gap rather than claiming full protocol compliance. | +| `ethernet` | started | not started | started | `tests/ethernet/EthMacCore/` now covers both the original leaf slice and the deeper assembly benches for `EthMacRx`, `EthMacTx`, `EthMacRxFifo`, and `EthMacTxFifo`, plus direct `EthMacRxBypass` / `EthMacTxBypass` leaf benches and import/export sweeps that now explicitly include the current placeholder `XLGMII` contract. `tests/ethernet/RawEthFramer/` covers the `RawEthFramer` leaves/top/pair slice, including the successful multi-beat `RawEthFramerTx` unicast-forwarding path after lookup resolution. `tests/ethernet/UdpEngine/` covers `ArpIpTable`, `UdpEngineArp`, `UdpEngineDhcp`, `UdpEngineRx`, `UdpEngineTx`, `UdpEngine`, and `UdpEngineWrapper`, with the top/wrapper benches now widened across client/server routing paths. `tests/ethernet/IpV4Engine/` covers `ArpEngine`, `IcmpEngine`, `IgmpV2Engine`, `IpV4Engine`, `IpV4EngineDeMux`, `IpV4EngineRx`, and `IpV4EngineTx`, and the top/leaf benches now also include protocol-TX and deeper ICMP reject/recovery coverage. `tests/ethernet/RoCEv2/` currently covers the pure-VHDL RTL entities `EthMacPrepareForICrc`, `EthMacRxCheckICrc`, `RoceResizeAndSwap`, and `RoceConfigurator` through checked-in wrappers under `ethernet/RoCEv2/wrappers/`. The remaining RoCEv2 RTL entities that still need cocotb benches are `EthMacCrcAxiStreamWrapperSend`, `EthMacCrcAxiStreamWrapperRecv`, `EthMacTxRoCEv2`, `EthMacRxRoCEv2`, and `RoceEngineWrapper`, and those should use the real generated `blue-*` dependencies rather than local stand-ins. The explicit Ethernet caveats are now the larger untouched families such as `GigEthCore`, `TenGigEthCore`, `XauiCore`, `XlauiCore`, and `Caui4Core`, the mixed-language RoCEv2 bench gap listed above, plus the still-placeholder `EthMac*Xlgmii` RTL. | +| `devices` | not started | not started | not started | Many vendor-heavy cases | +| `xilinx` | not started | not started | not started | Many vendor-heavy cases | + +## Completed Decisions +- Use Python-only executable test logic. +- Use `pytest + cocotb + GHDL + ruckus` as the primary stack. +- Keep VHDL only for wrappers, shims, and required simulation models. +- Comment new Python regression code at a tutorial level so readers who are new to cocotb can follow the flow in-place. +- Whole repo is the long-term target. +- Phase 1 focuses on simulator-friendly modules. +- Vendor-heavy modules are deferred in phase 1. +- Generic-heavy modules are Python-first. +- Use curated configuration matrices instead of full Cartesian products. +- Keep a tier-first CI model: `smoke` and `functional`. +- Rewrite legacy VHDL TB logic in Python rather than preserving it by default. +- Keep wrappers only when they make Python interaction cleaner. +- Run the `vsg` linter with CI's `vsg-linter.yml` settings on any created or edited VHDL files, and use autofix before doing manual cleanup when possible. +- Treat VHDL packages as transitively covered unless a behavioral function/procedure needs a dedicated wrapper. +- Treat `docs/plans/rtl-regression/progress.md` and `docs/plans/rtl-regression/handoff.md` as the living planning inputs. Do not resume queue-driven planning unless the user explicitly asks for a regenerated queue. + +## Completed Work Items +- Surveyed repo structure and existing verification flow. +- Reviewed existing Python regressions and representative VHDL testbenches. +- Merged the current `pre-release` branch into `verification-2`, bringing the already-landed `protocols/ssi` and `protocols/pgp` regression waves into the same branch line as the earlier `ethernet/EthMacCore` slice. +- Refreshed `verification-2` against the current `origin/pre-release` tip on 2026-05-21 and intentionally kept the RTL-regression planning files. +- Re-added `dsp/` to the generated phase-1 queue scope so DSP work is tracked by the same bottom-up planner as the other simulator-friendly subsystems. +- Implemented `tests/dsp/generic/test_DspAddSub.py`. +- Validated `tests/dsp/generic/test_DspAddSub.py` locally with `./.venv/bin/python -m pytest -n 0 -q tests/dsp/generic/test_DspAddSub.py`. +- Added shared DSP helpers in `tests/dsp/generic/dsp_test_utils.py` for signed packing/truncation, rolling reference models, cocotb clock-settle timing, and generated FIR wrappers. +- Extended `tests/common/regression_utils.py` so generated-wrapper benches can use short explicit sim-build keys instead of overloading generic/extra-env strings into fragile build paths. +- Implemented `tests/dsp/generic/test_FirFilterTap.py`, `tests/dsp/generic/test_DspPreSubMult.py`, `tests/dsp/generic/test_DspSquareDiffMult.py`, `tests/dsp/generic/test_BoxcarIntegrator.py`, `tests/dsp/generic/test_BoxcarFilter.py`, `tests/dsp/generic/test_FirFilterSingleChannel.py`, and `tests/dsp/generic/test_FirFilterMultiChannel.py`. +- Fixed DSP RTL issues exposed by the new benches in `dsp/generic/fixed/FirFilterTap.vhd`, `dsp/generic/fixed/BoxcarIntegrator.vhd`, and `dsp/generic/fixed/FirFilterSingleChannel.vhd`. +- Validated the full planned DSP leaf batch locally with `./.venv/bin/python -m pytest -n 0 -q tests/dsp/generic` (`15 passed`). +- Compared `cocotb + pytest`, `VUnit`, and `OSVVM` for SURF. +- Chose Python-only executable regression logic. +- Defined the context-handoff artifact set. +- Created the original checked-in handoff artifacts under `docs/_meta/`; they now live under `docs/plans/rtl-regression/`. +- Created the initial regression inventory scaffold in `docs/plans/rtl-regression/inventory.yaml`. +- Selected and documented the first pilot modules: `FifoAsync`, `AxiStreamFifoV2`, and `AxiLiteAsync`. +- Added `scripts/setup_regression_env.sh` to bootstrap the local regression environment. +- Added `.vscode/tasks.json` with setup, import, and regression tasks. +- Installed local `ghdl` via Homebrew. +- Created `.venv`, installed Python regression dependencies, linked `~/ruckus`, and completed `make MODULES="$PWD" import`. +- Added shared regression helpers in `tests/regression_utils.py`. +- Implemented the first Python pilot regression in `tests/base/fifo/test_FifoAsync.py`. +- Validated `tests/base/fifo/test_FifoAsync.py` locally with `./.venv/bin/python -m pytest -v tests/base/fifo/test_FifoAsync.py`. +- Reorganized new regressions into subsystem packages under `tests/` and moved shared helpers to `tests/common/`. +- Added `tests/README.md` to document the regression layout policy. +- Added the shared Ethernet MAC helper layer in `tests/ethernet/EthMacCore/ethmac_test_utils.py`, including packet builders, checksum helpers, MAC-config byte-order handling, flat EMAC beat helpers, and Ethernet minimum-frame padding support for the import/export loopback benches. +- Added checked-in cocotb-facing wrappers under `ethernet/EthMacCore/wrappers/` for the first `EthMacCore` wave, including loopback wrappers for import/export and top-level tests plus flat wrappers for the checksum, pause, filter, and shift leaves. +- Implemented and validated the first `ethernet/EthMacCore` functional wave under `tests/ethernet/EthMacCore/`: `EthCrc32Parallel`, `EthMacFlowCtrl`, `EthMacRxPause`, `EthMacTxPause`, `EthMacRxFilter`, `EthMacRxShift`, `EthMacTxShift`, `EthMacRxImport`, `EthMacTxExport`, `EthMacRxCsum`, `EthMacTxCsum`, and `EthMacTop`. +- Expanded the first `EthMacCore` wave beyond the initial happy paths: `EthMacTop` now covers filter/backpressure/checksum/pause interactions, `EthMacRxImport` and `EthMacTxExport` now cover both GMII and XGMII plus link-not-ready recovery behavior, the RX/TX checksum benches now include negative and partial-repair cases, `EthMacRxFilter` now covers multicast/broadcast/filter-disable/multi-beat-drop behavior, the RX/TX shift benches now cover runtime shift changes and control-bit propagation, and `EthCrc32Parallel` now sweeps all byte widths `1..16`. +- Ran a quick HDL coverage spike against the local Homebrew `ghdl` build and confirmed it does not expose `--coverage` or a `coverage` subcommand. +- Migrated `AxiStreamFifoV2` into `tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py` and validated the current 10-case sweep locally. +- Expanded `tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py` into a broader 10-case `AxiStreamFifoV2` regression covering thresholded release modes, metadata truncation, multi-stage cascade buffering, and the `S_HAS_TREADY=0` pause-only path, and validated it locally with `10 passed`. +- Revalidated `tests/axi/axi4/test_AxiResize.py` after merging `verification` into `fix-axi-resize`; the equal-width, `32-bit -> 64-bit`, and `64-bit -> 32-bit` cases now all pass locally on this branch, so the stale upsize `xfail` was removed. +- Expanded `FifoAsync` into a curated 12-case matrix and validated it locally under parallel pytest execution. +- Added `pytest.ini` to default to `-n auto --dist=worksteal`, and aligned CI to rely on that default xdist configuration. +- Implemented `tests/base/fifo/test_FifoSync.py` and validated its 11-case matrix locally under parallel pytest execution. +- Added `scripts/build_rtl_instantiation_graph.py` for optional hierarchy analysis. The generated graph artifacts were later retired when planning moved to user-directed frontier tracking. +- Implemented `tests/base/sync/test_Synchronizer.py` and validated its 6-case matrix locally under parallel pytest execution. +- Implemented `tests/base/sync/test_SynchronizerVector.py` and validated its 6-case matrix locally under parallel pytest execution. +- Implemented `tests/base/general/test_RstPipeline.py` and validated its 4-case matrix locally under parallel pytest execution. +- Implemented `tests/base/ram/test_SimpleDualPortRam.py` and validated its 5-case matrix locally under parallel pytest execution. +- Implemented `tests/base/fifo/test_FifoOutputPipeline.py` and validated its 5-case matrix locally under parallel pytest execution. +- Implemented `tests/base/fifo/test_FifoWrFsm.py` and validated its 4-case matrix locally under parallel pytest execution. +- Extended `tests/common/regression_utils.py` so regressions can add test-local VHDL wrapper sources when simulator limitations make a thin shim cleaner than direct generic overrides. +- Implemented `tests/base/crc/test_Crc32Parallel.py`, `tests/base/crc/test_Crc32.py`, and `tests/base/crc/test_CRC32Rtl.py` and validated their combined 9-case CRC batch locally under parallel pytest execution. +- Implemented `tests/base/sync/test_RstSync.py`, `tests/base/sync/test_SynchronizerEdge.py`, and `tests/base/sync/test_SynchronizerOneShot.py` and validated their combined 11-case sync/reset batch locally under parallel pytest execution. +- Implemented `tests/base/general/test_PwrUpRst.py` and validated its 3-case matrix locally under parallel pytest execution. +- Implemented `tests/base/ram/test_TrueDualPortRam.py` and `tests/base/ram/test_LutRam.py` and validated their combined 9-case RAM batch locally under parallel pytest execution. +- Implemented `tests/base/fifo/test_FifoRdFsm.py` and validated its 4-case matrix locally under parallel pytest execution. +- Validated the full 10-module follow-on subset in one run with `./.venv/bin/python -m pytest -v tests/base/crc/test_Crc32Parallel.py tests/base/crc/test_Crc32.py tests/base/crc/test_CRC32Rtl.py tests/base/sync/test_RstSync.py tests/base/general/test_PwrUpRst.py tests/base/sync/test_SynchronizerEdge.py tests/base/sync/test_SynchronizerOneShot.py tests/base/ram/test_TrueDualPortRam.py tests/base/ram/test_LutRam.py tests/base/fifo/test_FifoRdFsm.py` (`38 passed`). +- Implemented `tests/base/general/test_Arbiter.py`, `tests/base/general/test_ClockDivider.py`, `tests/base/general/test_Debouncer.py`, `tests/base/general/test_Gearbox.py`, `tests/base/general/test_Heartbeat.py`, `tests/base/general/test_Mux.py`, `tests/base/general/test_OneShot.py`, `tests/base/general/test_RegisterVector.py`, `tests/base/general/test_RstPipelineVector.py`, `tests/base/general/test_Scrambler.py`, `tests/base/general/test_WatchDogRst.py`, `tests/base/delay/test_SlvDelay.py`, `tests/base/delay/test_SlvFixedDelay.py`, `tests/base/sync/test_SynchronizerFifo.py`, and `tests/base/sync/test_SynchronizerOneShotCnt.py`. +- Implemented `tests/axi/axi_stream/test_AxiStreamScatterGather.py`, `tests/axi/axi4/test_AxiMemTester.py`, `tests/axi/dma/test_AxiStreamDmaV2Desc.py`, `tests/axi/dma/test_AxiStreamDmaV2Fifo.py`, `tests/axi/axi4/test_AxiReadPathFifo.py`, `tests/axi/axi4/test_AxiWritePathFifo.py`, `tests/axi/dma/test_AxiStreamDmaV2.py`, `tests/axi/axi_stream/test_AxiStreamBatchingFifo.py`, `tests/axi/axi_stream/test_AxiStreamMon.py`, and `tests/axi/axi_stream/test_AxiStreamRingBuffer.py`, plus the supporting `*IpIntegrator.vhd` wrappers needed for the new AXI4/AXIS/DMA benches. +- Validated the full 15-module follow-on subset in one run with `./.venv/bin/python -m pytest -n 0 -q tests/base/general/test_Arbiter.py tests/base/general/test_ClockDivider.py tests/base/general/test_Debouncer.py tests/base/general/test_Gearbox.py tests/base/general/test_Heartbeat.py tests/base/general/test_Mux.py tests/base/general/test_OneShot.py tests/base/general/test_RegisterVector.py tests/base/general/test_RstPipelineVector.py tests/base/general/test_Scrambler.py tests/base/general/test_WatchDogRst.py tests/base/delay/test_SlvDelay.py tests/base/delay/test_SlvFixedDelay.py tests/base/sync/test_SynchronizerFifo.py tests/base/sync/test_SynchronizerOneShotCnt.py` (`41 passed`). +- Added a shared generated-wrapper path in `tests/common/regression_utils.py` and migrated the `Heartbeat` and `Debouncer` regressions away from checked-in one-off VHDL wrappers. +- Revalidated the generated-wrapper migration locally with `./.venv/bin/python -m pytest -n 0 -q tests/base/general/test_Heartbeat.py tests/base/general/test_Debouncer.py` (`6 passed`) and then revalidated the full 15-module batch (`41 passed`). +- Implemented `tests/dsp/generic/test_DspComparator.py`, `tests/base/fifo/test_Fifo.py`, `tests/base/fifo/test_FifoCascade.py`, `tests/base/fifo/test_FifoMux.py`, `tests/base/general/test_AsyncGearbox.py`, `tests/base/sync/test_SynchronizerOneShotVector.py`, `tests/base/sync/test_SynchronizerOneShotCntVector.py`, `tests/base/sync/test_SyncStatusVector.py`, `tests/base/sync/test_SyncTrigPeriod.py`, and `tests/base/sync/test_SyncMinMax.py`. +- Validated the combined 10-module wrapper/integration batch with `./.venv/bin/python -m pytest -n 0 -q tests/dsp/generic/test_DspComparator.py tests/base/fifo/test_Fifo.py tests/base/fifo/test_FifoCascade.py tests/base/fifo/test_FifoMux.py tests/base/general/test_AsyncGearbox.py tests/base/sync/test_SynchronizerOneShotVector.py tests/base/sync/test_SynchronizerOneShotCntVector.py tests/base/sync/test_SyncStatusVector.py tests/base/sync/test_SyncTrigPeriod.py tests/base/sync/test_SyncMinMax.py` (`18 passed`). +- Implemented `tests/base/general/test_MasterRamIpIntegrator.py`, `tests/base/general/test_SlaveRamIpIntegrator.py`, `tests/base/ram/test_DualPortRam.py`, `tests/base/delay/test_SlvDelayRam.py`, `tests/base/delay/test_SlvDelayFifo.py`, `tests/base/sync/test_SyncClockFreq.py`, `tests/base/sync/test_SyncTrigRate.py`, and `tests/base/sync/test_SyncTrigRateVector.py`. +- Validated the combined remaining non-vendor, non-dummy `base/` batch with `./.venv/bin/python -m pytest -n 0 -q tests/base/general/test_MasterRamIpIntegrator.py tests/base/general/test_SlaveRamIpIntegrator.py tests/base/ram/test_DualPortRam.py tests/base/delay/test_SlvDelayRam.py tests/base/delay/test_SlvDelayFifo.py tests/base/sync/test_SyncClockFreq.py tests/base/sync/test_SyncTrigRate.py tests/base/sync/test_SyncTrigRateVector.py` (`15 passed`). +- Implemented `tests/axi/axi_stream/test_AxiStreamPipeline.py` with a thin flat-port adapter at `axi/axi-stream/ip_integrator/AxiStreamPipelineIpIntegrator.vhd`, and validated its curated 3-case sweep locally. +- Implemented `tests/axi/axi_lite/test_AxiLiteCrossbar.py` with a cocotb-facing crossbar wrapper and validated its routed-region, decode-error, and concurrent-traffic coverage locally. +- Validated the first post-`base/` `axi/` pair with `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi_stream/test_AxiStreamPipeline.py tests/axi/axi_lite/test_AxiLiteCrossbar.py` (`4 passed`). +- Implemented `tests/axi/axi_stream/test_AxiStreamMux.py` with a thin two-input adapter at `axi/axi-stream/ip_integrator/AxiStreamMuxIpIntegrator.vhd`, and validated its curated indexed-priority, routed-remap, and asynchronous reset/recovery sweep locally (`3 passed`). +- Revalidated the small post-`base/` `axi/` follow-on set with `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi_stream/test_AxiStreamPipeline.py tests/axi/axi_stream/test_AxiStreamMux.py tests/axi/axi_lite/test_AxiLiteCrossbar.py` (`7 passed`). +- Implemented `tests/axi/axi_stream/test_AxiStreamDeMux.py` with a thin two-output adapter at `axi/axi-stream/ip_integrator/AxiStreamDeMuxIpIntegrator.vhd`, and validated its curated indexed-routing, routed-backpressure, and dynamic-route/drop/reset sweep locally (`3 passed`). +- Revalidated the current small `axi/` follow-on subset with `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi_stream/test_AxiStreamPipeline.py tests/axi/axi_stream/test_AxiStreamMux.py tests/axi/axi_stream/test_AxiStreamDeMux.py tests/axi/axi_lite/test_AxiLiteCrossbar.py` (`10 passed`). +- Replaced the hand-curated flat phase-1 list with a generated path-qualified queue emitted by `scripts/build_rtl_instantiation_graph.py`. That generated queue was useful during the earlier axi-first rollout but has since been retired from normal context. +- Implemented `tests/axi/axi_lite/test_AxiLiteRegs.py`, `tests/axi/axi_lite/test_AxiLiteRespTimer.py`, `tests/axi/axi_lite/test_AxiLiteSlave.py`, `tests/axi/axi_lite/test_AxiLiteWriteFilter.py`, `tests/axi/axi_lite/test_AxiVersion.py`, `tests/axi/axi_stream/test_AxiStreamCombiner.py`, `tests/axi/axi_stream/test_AxiStreamFlush.py`, `tests/axi/axi_stream/test_AxiStreamGearboxPack.py`, `tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py`, and `tests/axi/axi_stream/test_AxiStreamSplitter.py` with thin subsystem-local adapters under `axi/axi-lite/ip_integrator/` and `axi/axi-stream/ip_integrator/`. +- Validated the generated-queue 10-module AXI batch with `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi_lite/test_AxiLiteRegs.py tests/axi/axi_lite/test_AxiLiteRespTimer.py tests/axi/axi_lite/test_AxiLiteSlave.py tests/axi/axi_lite/test_AxiLiteWriteFilter.py tests/axi/axi_lite/test_AxiVersion.py tests/axi/axi_stream/test_AxiStreamCombiner.py tests/axi/axi_stream/test_AxiStreamFlush.py tests/axi/axi_stream/test_AxiStreamGearboxPack.py tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py tests/axi/axi_stream/test_AxiStreamSplitter.py` (`14 passed`). +- Implemented `tests/axi/axi4/test_AxiReadPathMux.py`, `tests/axi/axi4/test_AxiWritePathMux.py`, `tests/axi/axi4/test_AxiResize.py`, and `tests/axi/bridge/test_AxiToAxiLite.py` with thin subsystem-local adapters at `axi/axi4/ip_integrator/AxiReadPathMuxIpIntegrator.vhd`, `axi/axi4/ip_integrator/AxiWritePathMuxIpIntegrator.vhd`, `axi/axi4/ip_integrator/AxiResizeIpIntegrator.vhd`, and `axi/bridge/ip_integrator/AxiToAxiLiteIpIntegrator.vhd`. +- Validated the stable AXI4/bridge follow-on subset with `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi4/test_AxiReadPathMux.py tests/axi/axi4/test_AxiWritePathMux.py tests/axi/bridge/test_AxiToAxiLite.py` (`3 passed`). Historical note: at this point `tests/axi/axi4/test_AxiResize.py` still carried the restored upsize case as an expected gap; the later `AxiResize` RTL fix is now merged and the current status above counts the equal-width, upsize, and downsize cases as passing. +- Implemented `tests/axi/axi_stream/test_AxiStreamTrailerRemove.py`, `tests/axi/axi4/test_AxiRam.py`, `tests/axi/bridge/test_AxiLiteToIpBus.py`, `tests/axi/bridge/test_IpBusToAxiLite.py`, `tests/axi/dma/test_AxiStreamDmaV2Read.py`, `tests/axi/axi_stream/test_AxiStreamGearbox.py`, `tests/axi/axi_stream/test_AxiStreamTap.py`, `tests/axi/dma/test_AxiStreamDmaRead.py`, `tests/axi/dma/test_AxiStreamDmaV2Write.py`, and `tests/axi/axi_stream/test_AxiStreamTimer.py` with subsystem-local adapters under `axi/axi-stream/ip_integrator/`, `axi/bridge/ip_integrator/`, and `axi/dma/ip_integrator/`. +- Validated the stable 9-module subset of that generated-queue window with `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi4/test_AxiRam.py tests/axi/bridge/test_AxiLiteToIpBus.py tests/axi/bridge/test_IpBusToAxiLite.py tests/axi/dma/test_AxiStreamDmaRead.py tests/axi/dma/test_AxiStreamDmaV2Write.py tests/axi/axi_stream/test_AxiStreamGearbox.py tests/axi/axi_stream/test_AxiStreamTap.py tests/axi/axi_stream/test_AxiStreamTimer.py tests/axi/axi_stream/test_AxiStreamTrailerRemove.py` (`9 passed`). Historical note: at this point `tests/axi/dma/test_AxiStreamDmaV2Read.py` still failed immediately inside `AxiStreamDmaV2Read` at `31 ns`; the later bounded byte-count and terminal-mask fixes are now merged and the current status above counts the aligned and short terminal-beat cases as validated. +- Implemented `tests/axi/axi4/test_AxiRateGen.py` with a thin subsystem-local adapter at `axi/axi4/ip_integrator/AxiRateGenIpIntegrator.vhd`. +- Validated `AxiRateGen` locally with `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi4/test_AxiRateGen.py` (`1 passed`) and revalidated the nearby AXI4 subset with `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi4/test_AxiReadPathMux.py tests/axi/axi4/test_AxiWritePathMux.py tests/axi/axi4/test_AxiRam.py tests/axi/axi4/test_AxiRateGen.py` (`4 passed`). +- Refactored `protocols/line-codes` into a clean three-layer cocotb layout backed by a shared helper in `tests/protocols/line_codes/line_code_test_utils.py`: direct package benches via checked-in `Code*PkgWrapper.vhd` adapters, direct `Encoder*`/`Decoder*` entity benches, and small end-to-end integration smokes that reuse the legacy `protocols/line-codes/tb/LineCode*Tb.vhd` shells instead of duplicated wrapper HDL. +- Added permanent checked-in package adapters at `protocols/line-codes/wrappers/Code8b10bPkgWrapper.vhd`, `protocols/line-codes/wrappers/Code10b12bPkgWrapper.vhd`, and `protocols/line-codes/wrappers/Code12b14bPkgWrapper.vhd`, then removed the duplicated `protocols/line-codes/wrappers/LineCode*Wrapper.vhd` files and their matching `test_LineCode*Wrapper.py` benches. +- The direct-entity benches now cover clock-enable gating, flow-control hold behavior, malformed decoder inputs, dual-byte 8b10b lane coverage, and 12b14b debug-disparity handoff through `test_Encoder*.py` and `test_Decoder*.py`. +- The current line-code validation command is `./.venv/bin/python -m pytest -n 0 -q tests/protocols/line_codes`, and it passes locally with `23 passed`. +- The new package-surface coverage exposed a real `Code12b14bPkg` invalid-K disparity bug; `protocols/line-codes/rtl/Code12b14bPkg.vhd` now leaves `dispOut` unchanged on illegal K requests instead of tripping a GHDL bound-check failure. + +## Current In-Progress Item +- No RTL implementation item is active at this moment. The current housekeeping item is the task-plan migration from the old `docs/_meta/rtl_regression_*` paths into `docs/plans/rtl-regression/` as directed by `AGENTS.md`. +- User-directed CoaXPress RTL event-payload, bridge-status, and receive-side SSI `EOFE` work is complete for this pass. `CoaXPressRxLane` now buffers bounded event payloads and releases them on `eventMaster` only after CRC/`EOP` validation, `CoaXPressRx` crosses that event stream into `cfgClk`, and `CoaXPressOverFiberBridgeRx` now exports `/Q/` sequence policy, classified `/E/` error causes, and HKP K-code status through `CxpofRxStatusType` while preserving the existing reconstructed CXP word-stream behavior. The GTH/GTY CoaXPress-over-Fiber wrappers now terminate their AXI-Lite ports with `CoaXPressOverFiberBridgeAxiL` bridge status/counter registers instead of leaving those ports at default decode-error responses, and the AXI-Lite regression now checks all named HKP classification readbacks through that consumer surface. On the image receive path, malformed stream trailers now reach downstream SSI consumers as terminal `EOFE` on the final packed image beat. +- 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 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`. + +## Next 3 Concrete Tasks +- 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 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 +- Runtime may grow quickly once configuration-heavy modules are added without careful tiering. +- Wrapper policy must stay narrow or VHDL cruft will accumulate again. +- HDL source coverage is not immediately available with the current local `ghdl` LLVM build; it needs a separate tooling decision if we want it later. + +## Findings Worth Preserving +- For a quick resume, read this file’s `Current Frontier Snapshot`, `Current In-Progress Item`, `Next 3 Concrete Tasks`, and `Findings Worth Preserving` sections before digging through the full log. +- Existing Python regressions are generally the best reusable verification assets. +- Existing VHDL TBs contain useful behavioral intent but are inconsistent as a scalable execution framework. +- Generic-heavy modules strongly favor Python-authored tests. +- Broad repo coverage will require tiering and likely later sharding. +- The initial inventory file should remain small and explicit rather than auto-generated until the schema stabilizes. +- `AxiStreamFifoV2` already has a useful wrapper-plus-Python pattern, and the same shim-first approach works well for later AXI wrappers when the DUT-specific extra signals are kept thin. +- AXI Stream leaves with partially driven sideband fields can upset `cocotbext.axi` sinks under GHDL when those fields stay `U`; for first-pass benches, prefer either wrappers that drive those sidebands deterministically or hand-monitored payload checks when the sidebands are not part of the behavior under test. +- Real-generic overrides are still awkward under the local open-source stack; if a module only needs a small non-default real-generic operating point, prefer baking the stable value into the thin subsystem wrapper over fighting simulator-specific override syntax. +- Some wrapper benches need to stay intentionally narrow on this branch to avoid conflating simulator limitations with RTL failures. In this batch that applies to `AxiStreamCompact`, `AxiStreamFrameRateLimiter`, and `AxiStreamDmaV2WriteMux`. +- The local machine needs a reproducible one-command bootstrap path before test implementation work can move efficiently. +- The bootstrap path is now working locally with `~/ruckus` linked into the repo. +- Bare `python` should not be assumed to exist on `PATH` in this repo's shell environment; use `./.venv/bin/python` for local pytest and helper-script invocations unless the virtualenv is already activated. +- The first shared-helper-based pilot is working; start simple and grow coverage incrementally rather than front-loading every edge case. +- New regressions need to live in subsystem packages from the start; do not add more flat `tests/test_*.py` files. +- The current Homebrew `ghdl` install is sufficient for cocotb regressions but not for a simple built-in HDL coverage flow. +- The existing `AxiLiteAsyncTb.vhd` is useful as intent/reference, but it is not an appropriate long-term wrapper because it embeds clocks, memories, and transaction logic; `AxiLiteAsyncIpIntegrator.vhd` is now the cleaner cocotb-facing adapter. +- Future Python regression code should follow the user's preferred two-layer comment style: keep a module-specific `Test methodology` header block under the SLAC banner and also explain major coroutine steps, waits, stimulus phases, and checks in-place for readers who are not already comfortable with cocotb. +- Future Python regression code should also keep the standard SURF/SLAC file header itself; do not treat the methodology block as a substitute for the normal repo banner. +- The methodology block should use wrapped `Sweep`, `Stimulus`, `Checks`, and `Timing` bullets and describe the real bench behavior, not generic filler text. +- The same “write it readable on the first pass” rule applies to permanent cocotb-facing VHDL wrappers: checked-in `*IpIntegrator.vhd` files should carry the standard SURF banner and short section comments for shim setup, DUT hookup, and flattening/status wiring. +- `FifoAsync` needed a curated matrix rather than a naive Cartesian sweep: standard FIFO mode, FWFT mode, and pipelined FWFT do not share identical read/full semantics. +- VHDL packages should not become top-level test targets by default; only high-value behavioral helpers warrant dedicated wrapper tests. +- `FifoSync` benefits from the same curated-matrix approach as `FifoAsync`, but its threshold checks needed event-driven flag handling because `prog_full`/`prog_empty` timing did not line up with fixed write-count assumptions. +- Temporary instantiation graph analysis can still be useful when choosing between related modules because it exposes high-reuse leaves and duplicated coverage paths, but it should never override the user-directed frontier. +- Duplicate entity names are common in SURF due to dummy/vendor variants, so any temporary graph analysis must use path context rather than bare entity names. +- Direct cocotb tests for simple SURF leaf modules still need to account for `TPD_G` when sampling outputs after clock or reset events; sampling exactly at the nominal edge can create false negatives. +- Simple RAM tests benefit from a small startup warm-up and conservative read sampling so direct and registered output configurations share one stable helper. +- For leaf modules with combinational outputs derived from current request inputs, pulse-based tests should drop the request before sampling post-edge state or they may observe the next pending transaction instead of the one just accepted. +- The local GHDL flow rejects direct command-line overrides of a 32-bit `slv` generic in `Crc32`; when a parameterized leaf still needs expanded coverage, prefer a thin test-only wrapper over simulator-specific literal hacks. +- For repeated real-generic shim cases, generated test-local wrappers are a better default than checking in one VHDL file per module; they keep the workaround explicit without growing permanent HDL debris. +- `COMMON_CLK_G` style benches need truly shared edges, not just same-period clocks. A single cocotb coroutine that drives both clocks in lockstep is the safer default for those wrappers. +- Integration-wrapper tests should stay narrow and wrapper-specific. `Fifo` validated both inferred sync/async wrapper branches, `FifoCascade` validated public stage-vector plumbing plus a curated output smoke, and `FifoMux` is currently validated only on the stable split-to-narrow path while the packed-write branch remains deferred. +- `SyncClockFreq` is stable under the generated-wrapper approach, but the common-clock path quantizes one count above the abstract frequency target under GHDL, so the regression checks a bounded expected range instead of an over-precise exact integer. +- `SyncTrigRate` is now covered as a wrapper/integration bench: it validates aligned update publication, denser-window rate growth, reset-path liveness, and update-strobe pulse behavior. Exact min/max pipeline arithmetic remains covered by the dedicated `SyncMinMax` leaf regression rather than being re-proven through the wrapper. +- `LutFixedDelay` is the lone non-dummy `base/` entity still deferred in phase 1 because it depends on `SinglePortRamPrimitive`, which is currently only available through the vendor/dummy-backed path. +- `AxiStreamPipeline` is stable with a thin flat-port wrapper. The zero-stage case should be checked as true pass-through, while staged cases should be checked against the wrapper-visible latency of `PIPE_STAGES_G + 2` clocks and a bounded reset flush rather than an over-precise internal-stage assumption. +- `AxiStreamMux` is stable with a thin two-input adapter, but its `disableSel` handling composes with the separate priority-masking step in a non-obvious order: a disabled higher-priority source can still suppress lower-priority requesters. Disable-focused benches should either use equal priorities or mask the lower-priority source instead. +- `AxiStreamMux` interleave and explicit rearbitrate branches remain intentionally out of scope for the first wrapper bench; the validated subset is indexed arbitration plus `disableSel`, routed `TDEST`/`TID` remap under backpressure, and staged reset/recovery in passthrough mode. +- `AxiStreamDeMux` is stable with a thin one-input/two-output adapter. The first bench covers indexed decode, exact-match routed decode under output backpressure, and dynamic-route table behavior including unmatched-destination drop and staged reset flush. Broader wildcard-route patterns and larger fanout counts remain intentionally unproven in this first wrapper bench. +- `AxiLiteCrossbar` is practical under the current open-source flow with a thin cocotb-facing wrapper around the cascaded topology. The useful regression surface is routed-region correctness, decode-miss `DECERR` handling, and concurrent traffic through the cascaded topology, not a giant generic sweep. +- SURF already has reusable AXI record-flattening shims. New AXI Stream and AXI-Lite wrappers should prefer the existing IP-integrator shim layers over hand-written record packing, and only custom-wire the DUT-specific extra side signals on top. +- More generally, any VHDL shim layer added only to make a module fit cleanly into cocotb should live in the nearest real subsystem `ip_integrator/` tree, not under `tests/` and not under generic `hdl/` directories. +- If that shim layer is checked in instead of generated locally, treat it like normal repo HDL rather than disposable glue: add the standard header and enough section comments that the adapter structure is obvious during a later resume. +- Apply the same “first-draft readability” rule to checked-in cocotb tests: standard header first, methodology block second, tutorial comments in the body. +- `AxiReadPathMux` and `AxiWritePathMux` are more stable with tiny source-side pin drivers than with `cocotbext.axi` masters because the muxes rewrite IDs internally; the downstream shared-port checks can still use the library RAM models. +- `protocols/line-codes` is cleaner when split into three layers: direct package benches for exhaustive legal-space and explicit disparity-seed checks, direct `Encoder*`/`Decoder*` entity benches for clock/reset/flow-control behavior, and one small family-level integration smoke that proves the encoder-to-decoder chain still matches the legacy shell. +- For the line-code families, keep the shared Python harness generic and keep legality decisions local to each module bench. `8b10b` varies by byte-lane width, `10b12b` uses the curated `x & 28` K-symbol subset from the legacy bench, and `12b14b` also preserves its historical mixed training pattern plus explicit malformed-decode and illegal-K checks. +- For line-code package coverage, prefer checked-in subsystem wrappers only where the package surface truly needs explicit disparity seeding or direct encode/decode visibility. `Code8b10bPkg`, `Code10b12bPkg`, and `Code12b14bPkg` all follow that pattern now; the family integration layer should reuse existing `tb/LineCode*Tb.vhd` shells rather than cloning them into new wrapper HDL. +- The new package-surface tests are strong enough to expose latent package bugs. In this batch they caught an illegal-K path defect in `Code12b14bPkg` where the package could drive an out-of-range disparity sum; that path is now fixed and should stay covered. +- The current `Code12b14b` cocotb coverage preserves the legacy explicit disparity seeds plus the training/transition sequences, but it still does not carry forward the old VHDL bench's stateful run-length monitor as a passing assertion. Treat that run-length monitor as a known follow-up gap rather than silently assuming it is covered. +- `AxiToAxiLite` is practical with a thin bridge-local adapter, but mixed-width checks need to stay single-beat on the AXI side when the downstream response path is fundamentally AXI-Lite-like. +- `AxiResize` no longer has the old verification-branch upsize gap in the current tree; the equal-width, `32-bit -> 64-bit`, and `64-bit -> 32-bit` wrapper cases are counted as passing after the read-hold RTL fix. +- `AxiRateGen` is practical with the existing AXI4 and AXI-Lite IP-integrator shim pair plus a cocotb AXI RAM model, and the stable first-pass subset is the `COMMON_CLK_G=true` path with timer spacing, zero-fill writes, and generated-read completion rather than the asynchronous AXI-Lite crossing branches. +- For protocol-generator or wrapper-style benches, pair end-state checks with explicit accepted-handshake monitoring whenever the externally visible contract includes timing, burst shape, sideband propagation, or arbitration order. +- The SSI helper layer is now broad enough to justify reuse across the whole subsystem: `FlatSsiEndpoint`, `SsiBeat`, `recv_frame`, contiguous frame driving, optional `TID` handling, and no-output checks cover the current flattened SSI wrappers without each bench re-implementing handshake plumbing. +- `SsiInsertSof` is practical with a small SSI-local wrapper that exposes semantic SOF/EOFE controls instead of treating raw `TUSER` layout as the cocotb API. The currently validated subset keeps `COMMON_CLK_G=true` and FIFO-backed paths enabled; raw `TUSER_MASK_G` bit-level expectations are still a follow-up item if we want to pin down SSI user-bit indexing more aggressively. +- `SsiIbFrameFilter` is practical with the same semantic-wrapper approach, but the wrapper should use native SSI `TUSER_FIRST_LAST_C` encoding rather than a generic normal-user layout so cocotb can drive `SOF`/`EOFE` directly and the DUT sees the expected first/last byte markers. +- `SsiObFrameFilter` is practical with the same native-SSI wrapper contract. The checked-in bench now covers good-frame pass-through, missing-SOF drop, repeated-`SOF` termination, mid-frame `TDEST` termination, the cached-last-user `VALID_THOLD_G=0` `EOFE` drop path with exported drop-flag pulses, and one pipelined `PIPE_STAGES_G=2` pass-through case without relying on fixed latency assumptions. +- `SsiFifo` is practical with the same flat SSI wrapper pattern, but contiguous multi-beat checks need handshake-based observation rather than late frame collection. The earlier apparent middle-beat loss on a 3-beat `SsiFifoWrapper` probe turned out to be a receive-helper artifact; once the helper was corrected to capture accepted handshakes, both `SsiIbFrameFilter` and `SsiFifo` preserved the full frame and the checked-in `SsiFifo` regression could safely add real 3-beat pass-through assertions on the default, frame-ready, and thresholded paths plus a repeated-`SOF` termination check on the composite FIFO wrapper. +- The first reusable SSI Python helper can now absorb whole-frame receive and metadata-extension commonality, but it should still stop short of becoming a golden protocol model. Module-specific policy such as header injection, command decode rules, or filter-drop counters should remain in each bench. +- `AxiStreamDmaV2Read` is stable with a thin IP-integrator wrapper once the read-path byte-count conversions stay bounded to the burst-size window. The checked-in bench proves both aligned and short terminal-beat reads; because the wrapper exports only an 8-bit `TUSER`, the observable contract is first-user propagation rather than separate first/last byte tagging. +- The `axi/dma/rtl/v2/` benches are now intentionally split by behavior instead of repeatedly re-proving the same path through the top-level DMA stack: `AxiStreamDmaV2` and `AxiStreamDmaV2Desc` stay focused on descriptor-manager register/control surfaces, `AxiStreamDmaV2Read` owns aligned and short terminal-beat readout, `AxiStreamDmaV2Write` owns descriptor-return integrity plus burst splitting, `AxiStreamDmaV2WriteMux` owns arbitration ordering, and `AxiStreamDmaV2Fifo` owns the integrated FIFO register/count/pause-threshold surface. +- For checked-in VHDL changes, use the repo virtualenv's `vsg` with `vsg-linter.yml` so local lint matches CI, and prefer `--fix` before manual spacing/alignment cleanup. +- The current `EthMacCore` wave has a few wrapper-visible behavior details worth preserving in the docs instead of rediscovering later: the XGMII import/export loopback retains a frame presented during `phyReady=0` and drains it after link recovery with Ethernet minimum-size padding applied, while the GMII path drops it; `EthMacRxCsum` reliably asserts `IPERR` on a bad IPv4 header checksum but the checked-in wrapper contract does not require `EOFE` on that case; and the RX/TX shift benches need a small idle-plus-settle gap before changing runtime shift controls because the underlying `AxiStreamShift` samples those controls from its IDLE state. +- Verification hygiene now includes process cleanup: if a `pytest`/cocotb/GHDL step leaves stale run trees behind, kill those leftovers immediately before starting another compile or simulation command. +- For narrow AXI Stream cocotb wrappers backed by full SURF `AxiStreamMasterType` records, explicitly zero unused high `tKeep`, `tStrb`, and `tUser` bits before assigning the exposed 8-byte bus. `AXI_STREAM_MASTER_INIT_C` defaults `tKeep`/`tStrb` high, which can perturb RTL that inspects wider keep vectors internally. +- For routine local validation, prefer parallel pytest (`-n auto --dist=worksteal`) so cocotb suites finish quickly. Drop to `-n 0` only when debugging one simulation or preserving serial log ordering. + +## Log +- 2026-03-20: Agreed on Python-only executable regression logic and wrapper-only VHDL retention. +- 2026-03-20: Agreed on whole-repo scope with simulator-friendly phase 1 and vendor-heavy deferral. +- 2026-03-20: Agreed to add stable handoff artifacts under `docs/_meta/` before deeper implementation work. +- 2026-03-20: Added `docs/plans/rtl-regression/inventory.yaml` and seeded it with the first three pilot modules. +- 2026-03-20: Added local bootstrap helpers in `scripts/setup_regression_env.sh` and `.vscode/tasks.json`. +- 2026-03-20: Installed local toolchain and completed the first successful `make MODULES="$PWD" import`. +- 2026-03-20: Added `tests/regression_utils.py` and landed the first passing pilot regression for `FifoAsync`. +- 2026-03-20: Moved new regression infrastructure to `tests/common/`, relocated `FifoAsync` to `tests/base/fifo/`, and documented the subsystem-organized test layout. +- 2026-03-20: Checked local HDL coverage viability; the installed LLVM-backed `ghdl` rejects `--coverage`, so HDL coverage is deferred pending a different simulator/backend decision. +- 2026-03-20: Migrated `AxiStreamFifoV2` into `tests/axi/axi_stream/` and validated the full current 10-case sweep in 146s. +- 2026-04-02: Expanded `tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py` to cover `VALID_THOLD` release behavior, burst gating, dynamic pause control, `CASCADE_SIZE=2`, and `S_HAS_TREADY=0`, and revalidated it locally with `10 passed`. +- 2026-04-02: Merged `verification` into `fix-axi-resize`, reran `tests/axi/axi4/test_AxiResize.py`, and confirmed the previous `32-bit -> 64-bit` upsize `xfail` now passes on this branch; removed the stale `xfail`. +- 2026-04-02: Expanded the `axi/dma/rtl/v2/` regression split without broad overlap. `tests/axi/dma/test_AxiStreamDmaV2Write.py` now covers both single-frame and multi-burst writes, `tests/axi/dma/test_AxiStreamDmaV2WriteMux.py` now covers descriptor-first, simultaneous-launch, and data-first arbitration cases, and `tests/axi/dma/test_AxiStreamDmaV2Fifo.py` now covers the integrated FIFO register map plus dynamic pause-threshold behavior against the live write-buffer count. The combined validation run across `tests/axi/dma/test_AxiStreamDmaV2.py`, `tests/axi/dma/test_AxiStreamDmaV2Desc.py`, `tests/axi/dma/test_AxiStreamDmaV2Read.py`, `tests/axi/dma/test_AxiStreamDmaV2Write.py`, `tests/axi/dma/test_AxiStreamDmaV2WriteMux.py`, and `tests/axi/dma/test_AxiStreamDmaV2Fifo.py` passes locally with `9 passed`. +- 2026-04-02: Started the `protocols/line-codes` refactor by moving the family benches onto a shared helper in `tests/protocols/line_codes/line_code_test_utils.py` and by adding checked-in package-surface wrappers for `Code8b10bPkg`, `Code10b12bPkg`, and `Code12b14bPkg`. +- 2026-04-02: Compared the cocotb line-code coverage against the legacy VHDL benches under `protocols/line-codes/tb/`, preserved the legacy disparity-seed and training-pattern intent in the `Code*Pkg` Python benches, and noted the old `Code12b14bTb.vhd` run-length monitor as the only still-unported legacy assertion. +- 2026-04-02: Completed the clean-slate line-code redesign: deleted the duplicated `LineCode*Wrapper.vhd` and `test_LineCode*Wrapper.py` layer, added direct `test_Encoder*.py` and `test_Decoder*.py` benches against the real RTL entities, reused `protocols/line-codes/tb/LineCode*Tb.vhd` as the thin integration shells, fixed an illegal-K disparity bug in `protocols/line-codes/rtl/Code12b14bPkg.vhd`, and validated the full `tests/protocols/line_codes` directory locally with `23 passed`. +- 2026-04-03: Added `dsp/` back into `scripts/build_rtl_instantiation_graph.py` so temporary graph and queue analysis can track `dsp/generic/fixed` alongside the other phase-1 subsystems. +- 2026-04-03: Implemented `tests/dsp/generic/test_DspAddSub.py` as the first post-resume DSP leaf bench, replacing the old free-running `dsp/generic/tb/DspAddSubTb.vhd` stimulus with explicit signed add/sub arithmetic checks plus backpressure-hold and reset-clearing assertions. The module-local validation run passes locally with `2 passed`. +- 2026-04-03: Completed the planned `dsp/generic/fixed` cocotb leaf batch. Added new benches for `FirFilterTap`, `DspPreSubMult`, `DspSquareDiffMult`, `BoxcarIntegrator`, `BoxcarFilter`, `FirFilterSingleChannel`, and `FirFilterMultiChannel`; centralized shared DSP timing/model/wrapper helpers in `tests/dsp/generic/dsp_test_utils.py`; added explicit sim-build-key support for generated wrappers in `tests/common/regression_utils.py`; fixed RTL issues in `FirFilterTap`, `BoxcarIntegrator`, and `FirFilterSingleChannel`; and validated the full directory with `15 passed`. +- 2026-04-06: Started the manual `protocols/ssi` rollout with `SsiInsertSof`. Added the checked-in wrapper `protocols/ssi/wrappers/SsiInsertSofWrapper.vhd`, implemented `tests/protocols/ssi/test_SsiInsertSof.py`, validated the three-case FIFO-backed sweep locally with `3 passed`, and linted the wrapper cleanly with `vsg -c vsg-linter.yml -f`. +- 2026-04-06: Validated the narrow next SSI step with `SsiIbFrameFilter`. Added the checked-in native-SSI wrapper `protocols/ssi/wrappers/SsiIbFrameFilterWrapper.vhd`, implemented `tests/protocols/ssi/test_SsiIbFrameFilter.py`, validated the first-pass same-clock sweep locally with `1 passed`, linted the wrapper cleanly with `vsg --fix -c vsg-linter.yml -f`, and then consolidated shared flat SSI driver/sink timing into `tests/protocols/ssi/ssi_test_utils.py` so `test_SsiInsertSof.py` and `test_SsiIbFrameFilter.py` use the same basic endpoint helper layer. +- 2026-04-06: Added the next outbound SSI step with `SsiObFrameFilter`. Added the checked-in native-SSI wrapper `protocols/ssi/wrappers/SsiObFrameFilterWrapper.vhd`, implemented `tests/protocols/ssi/test_SsiObFrameFilter.py`, validated the narrowed default-mode regression locally with `1 passed`, revalidated the current SSI trio with `5 passed`, and linted the wrapper cleanly with `vsg --fix -c vsg-linter.yml -f`. The stable checked-in subset covers good-frame pass-through plus repeated-`SOF` termination with `PIPE_STAGES_G=0`; the cached-last-user `EOFE` branch for `VALID_THOLD_G=0` remains a follow-up item. +- 2026-04-06: Added the next composite SSI step with `SsiFifo`. Added the checked-in wrapper `protocols/ssi/wrappers/SsiFifoWrapper.vhd`, implemented `tests/protocols/ssi/test_SsiFifo.py`, validated the narrowed smoke regression locally with `1 passed`, and linted the wrapper cleanly with `vsg --fix -c vsg-linter.yml -f`. The stable checked-in subset keeps one physical clock and currently proves valid single-beat pass-through plus missing-SOF drop; multi-beat good-frame integrity, buffered occupancy behavior, and the sync-output gearbox branch remain follow-up items. +- 2026-04-06: Completed the remaining manual `protocols/ssi` leaf batch. Added checked-in wrappers `protocols/ssi/wrappers/SsiCmdMasterPulserWrapper.vhd`, `protocols/ssi/wrappers/SsiCmdMasterWrapper.vhd`, `protocols/ssi/wrappers/SsiFrameLimiterWrapper.vhd`, `protocols/ssi/wrappers/SsiIncrementingTxWrapper.vhd`, `protocols/ssi/wrappers/SsiAxiLiteMasterWrapper.vhd`, and `protocols/ssi/wrappers/SsiDbgTapWrapper.vhd`; added `tests/protocols/ssi/test_SsiCmdMasterPulser.py`, `tests/protocols/ssi/test_SsiCmdMaster.py`, `tests/protocols/ssi/test_SsiFrameLimiter.py`, `tests/protocols/ssi/test_SsiIncrementingTx.py`, `tests/protocols/ssi/test_SsiAxiLiteMaster.py`, and `tests/protocols/ssi/test_SsiDbgTap.py`; extended `tests/protocols/ssi/ssi_test_utils.py` with optional `TID` handling plus shared frame-receive helpers; validated the new six-test batch locally with `6 passed`; and kept wrapper lint clean under `vsg --fix -c vsg-linter.yml -f` plus a clean follow-up lint pass. The current scope is intentionally uneven: `SsiCmdMasterPulser`, `SsiCmdMaster`, `SsiIncrementingTx`, and `SsiAxiLiteMaster` have functional first-pass checks, `SsiFrameLimiter` is currently a narrowed single-beat/missing-SOF subset, and `SsiDbgTap` is traffic smoke only because the RTL exposes no externally visible state. +- 2026-04-07: Tightened the shared SSI helper layer in `tests/protocols/ssi/ssi_test_utils.py` so flat SSI sources wait for a real handshake edge and shared frame receivers capture accepted handshakes instead of inferring contiguous traffic from post-send snapshots. Revalidated the existing SSI slice locally with `19 passed`. A focused follow-up on `SsiIbFrameFilter` and `SsiFifo` showed the earlier apparent 3-beat middle-word loss was a bench observation bug rather than an RTL bug, after which the checked-in `SsiFifo` regression added a real contiguous 3-beat pass-through assertion and the `SsiAxiLiteMaster` / `SsiIncrementingTx` benches were updated to assert the full emitted frame shapes. +- 2026-04-07: Expanded `tests/protocols/ssi/test_SsiFifo.py` beyond the earlier single-beat subset. The checked-in FIFO bench now proves contiguous 3-beat frame preservation on the default, `VALID_THOLD_G=0`, and `VALID_THOLD_G=2` wrapper paths; exercises repeated-`SOF` malformed-frame handling across the buffered modes; and keeps the existing missing-`SOF` drop plus `SLAVE_READY_EN_G=false` overflow checks. The buffered malformed-frame result is mode-specific at the wrapper boundary: `VALID_THOLD_G=0` drops the malformed repeated-`SOF` frame outright, while `VALID_THOLD_G=2` emits the expected two-beat terminated `EOFE` frame. A follow-up backpressure pass also added explicit drain-side ready toggling on a good 5-beat frame, malformed termination under toggled ready, and thresholded release under intermittent drain stalls so SSI frame policy is now checked under nontrivial sink backpressure without duplicating the full `AxiStreamFifoV2` transport matrix. Revalidated the full touched SSI slice locally with `19 passed`. +- 2026-04-10: Landed the first manual `ethernet/EthMacCore` bench wave under `tests/ethernet/EthMacCore/`, added the required checked-in wrappers under `ethernet/EthMacCore/wrappers/`, and validated the baseline 12-module slice covering `EthCrc32Parallel`, `EthMacFlowCtrl`, `EthMacRxPause`, `EthMacTxPause`, `EthMacRxFilter`, `EthMacRxShift`, `EthMacTxShift`, `EthMacRxImport`, `EthMacTxExport`, `EthMacRxCsum`, `EthMacTxCsum`, and `EthMacTop`. +- 2026-04-10: Expanded that same `EthMacCore` slice beyond the initial happy paths. The checked-in Ethernet MAC suite now covers `EthMacTop` filter/backpressure/checksum/pause interactions, GMII and XGMII import/export plus link-not-ready recovery behavior, checksum negative cases, filter multicast/broadcast/filter-disable/multi-beat-drop behavior, shift runtime-control and control-bit propagation edges, and full byte-width `1..16` logic-path coverage for `EthCrc32Parallel`. The full `tests/ethernet/EthMacCore` directory currently passes locally with `32 passed`. +- 2026-04-17: Expanded the `ethernet/EthMacCore` slice into the deeper assembly layer. The checked-in Ethernet MAC benches now also cover `EthMacRx`, `EthMacTx`, `EthMacRxFifo`, and `EthMacTxFifo` through the existing subsystem-local wrappers under `ethernet/EthMacCore/wrappers/`. +- 2026-04-17: Added the manual `ethernet/UdpEngine` wave under `tests/ethernet/UdpEngine/`. The checked-in helper layer now lives in `tests/ethernet/UdpEngine/udp_test_utils.py`, the new checked-in wrappers live under `ethernet/UdpEngine/wrappers/`, and the locally validated benches cover `ArpIpTable`, `UdpEngineArp`, `UdpEngineDhcp`, `UdpEngineRx`, `UdpEngineTx`, `UdpEngine`, and `UdpEngineWrapper`. The current local validation command `./.venv/bin/python -m pytest -n 0 -q tests/ethernet/UdpEngine/test_ArpIpTable.py tests/ethernet/UdpEngine/test_UdpEngineArp.py tests/ethernet/UdpEngine/test_UdpEngineDhcp.py tests/ethernet/UdpEngine/test_UdpEngineRx.py tests/ethernet/UdpEngine/test_UdpEngineTx.py tests/ethernet/UdpEngine/test_UdpEngine.py tests/ethernet/UdpEngine/test_UdpEngineWrapper.py` passes with `7 passed`. +- 2026-04-17: Added the manual `ethernet/IpV4Engine` wave under `tests/ethernet/IpV4Engine/`. The checked-in helper layer now lives in `tests/ethernet/IpV4Engine/ipv4_test_utils.py`, the checked-in wrappers live under `ethernet/IpV4Engine/wrappers/`, and the validated benches cover `ArpEngine`, `IcmpEngine`, `IpV4Engine`, `IpV4EngineDeMux`, `IpV4EngineRx`, and `IpV4EngineTx`. +- 2026-04-17: Added `ethernet/IpV4Engine/wrappers/IgmpV2EngineWrapper.vhd` plus `tests/ethernet/IpV4Engine/test_IgmpV2Engine.py`, extending the IPv4 helper layer with IGMP packet builders in `tests/ethernet/IpV4Engine/ipv4_test_utils.py`. The new leaf bench covers power-up membership reports, general-query re-arming, and suppression of a pending local report when a matching inbound membership report is observed, while documenting the two leaf-boundary stale-field quirks that `IpV4EngineTx` ignores. +- 2026-04-16: Merged the current `pre-release` branch into `verification-2`, so this branch line now contains the already-landed SSI and PGP waves from PR #1391 and PR #1392 in addition to the manual Ethernet slice. +- 2026-04-16: Refreshed the planning docs to move the active frontier from “continue Ethernet next” to “refresh the stale queue/override inputs, then resume from the real merged-branch frontier.” +- 2026-04-17: Switched the planning docs from queue-driven task selection to user-directed frontier tracking. The graph and queue artifacts remain checked in for provenance, but `progress.md` and `handoff.md` are now the active done/open source of truth. +- 2026-04-17: Expanded `tests/ethernet/RawEthFramer/test_RawEthFramerTx.py` to cover the successful multi-beat unicast forwarding path after lookup resolution. The new leaf test holds the source frame through the exported request phase, models the same nonzero lookup latency the integrated `RawEthFramer` wrapper inserts before `ack`, and validates the full MAC-side wire image alongside the existing request, broadcast-bypass, and zero-MAC drop checks. +- 2026-04-17: Completed the Ethernet thin-area follow-up across `EthMacCore`, `UdpEngine`, and `IpV4Engine`. Added checked-in wrappers `ethernet/EthMacCore/wrappers/EthMacRxBypassWrapper.vhd` and `ethernet/EthMacCore/wrappers/EthMacTxBypassWrapper.vhd`, added `tests/ethernet/EthMacCore/test_EthMacRxBypass.py` and `tests/ethernet/EthMacCore/test_EthMacTxBypass.py`, widened `test_EthMacRxImport.py` / `test_EthMacTxExport.py` to cover the current placeholder `XLGMII` contract, widened `tests/ethernet/UdpEngine/test_UdpEngine.py` and `tests/ethernet/UdpEngine/test_UdpEngineWrapper.py` across additional client/server routing paths, widened `tests/ethernet/IpV4Engine/test_IpV4Engine.py` with a top-level protocol-TX path, and deepened `tests/ethernet/IpV4Engine/test_IcmpEngine.py` with truncated-request rejection, EOFE preservation, and post-reject recovery checks. The combined local validation command `./.venv/bin/python -m pytest -n 0 -q tests/ethernet/EthMacCore/test_EthMacRxImport.py tests/ethernet/EthMacCore/test_EthMacTxExport.py tests/ethernet/EthMacCore/test_EthMacRxBypass.py tests/ethernet/EthMacCore/test_EthMacTxBypass.py tests/ethernet/UdpEngine/test_UdpEngine.py tests/ethernet/UdpEngine/test_UdpEngineWrapper.py tests/ethernet/IpV4Engine/test_IpV4Engine.py tests/ethernet/IpV4Engine/test_IcmpEngine.py` passes locally with `14 passed`. +- 2026-03-20: Added an explicit project rule to comment new Python regression code where intent or runner behavior is not self-evident. +- 2026-03-20: Expanded `FifoAsync` to a validated 12-case parameter matrix and enabled default pytest xdist parallelization with `pytest.ini`. +- 2026-03-20: Added package-coverage policy: packages are covered transitively unless a behavioral helper warrants a dedicated wrapper test. +- 2026-03-20: Switched from pilot-only work to the bottom-up rollout and selected `FifoSync` as the next low-level target. +- 2026-03-20: Implemented and validated an 11-case `FifoSync` matrix under `tests/base/fifo/test_FifoSync.py`. +- 2026-03-20: Added and generated the first-pass RTL instantiation graph to guide bottom-up rollout decisions and reduce repeated test effort across the hierarchy. +- 2026-03-20: Implemented and validated a 6-case `Synchronizer` matrix under `tests/base/sync/test_Synchronizer.py` as the next graph-guided `base` leaf. +- 2026-03-20: Documented that local Python commands should use `./.venv/bin/python` unless the virtualenv is already activated, after a bare `python` invocation failed due to a missing shell shim. +- 2026-03-20: Implemented and validated the next five graph-guided `base` regressions: `SynchronizerVector`, `RstPipeline`, `SimpleDualPortRam`, `FifoOutputPipeline`, and `FifoWrFsm`. +- 2026-03-20: Updated the planning and handoff docs to preserve the user's tutorial-style cocotb comment preference for future regressions. +- 2026-03-20: Implemented and validated the next 10 graph-guided `base` regressions: `Crc32Parallel`, `Crc32`, `CRC32Rtl`, `RstSync`, `PwrUpRst`, `SynchronizerEdge`, `SynchronizerOneShot`, `TrueDualPortRam`, `LutRam`, and `FifoRdFsm`. +- 2026-03-20: Expanded `Crc32` coverage beyond the default IEEE polynomial to include Castagnoli and Koopman-style cases, using a thin test-only VHDL wrapper because local GHDL rejected direct runtime overrides of the 32-bit `CRC_POLY_G` vector generic. +- 2026-03-20: Implemented and validated the next 15 graph-guided `base` regressions: `Arbiter`, `ClockDivider`, `Debouncer`, `Gearbox`, `Heartbeat`, `Mux`, `OneShot`, `RegisterVector`, `RstPipelineVector`, `Scrambler`, `WatchDogRst`, `SlvDelay`, `SlvFixedDelay`, `SynchronizerFifo`, and `SynchronizerOneShotCnt` (`41 passed`). +- 2026-03-21: Replaced the checked-in `Heartbeat`/`Debouncer` wrapper files with a shared generated-wrapper helper in `tests/common/regression_utils.py` and revalidated both the targeted tests (`6 passed`) and the full 15-module batch (`41 passed`). +- 2026-03-21: Implemented and validated the next 10-module wrapper/integration batch: `DspComparator`, `Fifo`, `FifoCascade`, `FifoMux`, `AsyncGearbox`, `SynchronizerOneShotVector`, `SynchronizerOneShotCntVector`, `SyncStatusVector`, `SyncTrigPeriod`, and `SyncMinMax` (`18 passed`). +- 2026-03-21: Added `start_lockstep_clocks()` in `tests/common/regression_utils.py` for `COMMON_CLK_G` style benches and recorded that `FifoCascade`/`FifoMux` should keep intentionally narrow wrapper coverage under the current GHDL flow instead of forcing unstable branches. +- 2026-03-21: Implemented and validated the remaining non-vendor, non-dummy `base/` batch: `MasterRamIpIntegrator`, `SlaveRamIpIntegrator`, `DualPortRam`, `SlvDelayRam`, `SlvDelayFifo`, `SyncClockFreq`, `SyncTrigRate`, and `SyncTrigRateVector` (`15 passed`). `LutFixedDelay` remains deferred because it still depends on `SinglePortRamPrimitive`. +- 2026-03-21: Tightened the comment policy for Python regressions: header-level methodology comments and in-body tutorial comments are both required, the methodology block should use wrapped `Sweep`/`Stimulus`/`Checks`/`Timing` bullets, and the text should stay module-specific and editor-readable. +- 2026-03-21: Started the first post-`base/` simulator-friendly `axi/` follow-on with `AxiStreamPipeline` and `AxiLiteCrossbar` as the next migration targets, using the legacy flat Python benches only as intent/reference while keeping the new work under subsystem-packaged tests. +- 2026-03-21: Implemented and validated `AxiStreamPipeline` and `AxiLiteCrossbar` as the first post-`base/` `axi/` follow-on. `AxiStreamPipeline` uses a thin flat-port wrapper plus a curated pass-through/staged/reset sweep, and `AxiLiteCrossbar` uses a cocotb-facing wrapper for routed-region, decode-error, and concurrent-traffic checks (`4 passed` combined). +- 2026-03-21: Refactored the `AxiStreamPipeline` test adapter to reuse the existing `SlaveAxiStreamIpIntegrator`/`MasterAxiStreamIpIntegrator` shim pair for standard AXIS flattening, preserving only the pipeline-specific sideband wiring in the adapter (`3 passed` on the pipeline regression after the refactor). +- 2026-03-21: Moved and renamed the `AxiStreamPipeline` adapter to `axi/axi-stream/ip_integrator/AxiStreamPipelineIpIntegrator.vhd` so its path and name match the existing AXI IP-integrator adapter conventions and live with the rest of the AXI adapter layer. +- 2026-03-21: Tightened the planning rule for cocotb-facing shim placement: if a VHDL adapter is needed to fit a module into cocotb, place it in the nearest real subsystem `ip_integrator/` tree alongside the existing integration shims rather than under `tests/`. +- 2026-03-21: Collapsed the large instantiation-graph output into a reviewed flat phase-1 module build order in `plan.md` so future windows can take the next queued module directly instead of re-analyzing the JSON graph before every step. +- 2026-03-21: Resumed the flat `axi/` queue at `AxiStreamMux` and chose a dedicated IP-integrator adapter over the existing combined DeMux/Mux harness so the new cocotb bench can stay mux-specific and exercise arbitration plus `TDEST`/`TID` remap behavior directly. +- 2026-03-21: Implemented and validated `AxiStreamMux` with `axi/axi-stream/ip_integrator/AxiStreamMuxIpIntegrator.vhd` plus `tests/axi/axi_stream/test_AxiStreamMux.py`. The validated 3-case sweep covers indexed arbitration with explicit priority and `disableSel`, routed `TDEST`/`TID` remap under backpressure, and staged asynchronous active-low reset recovery (`3 passed`). +- 2026-03-21: Revalidated the current small `axi/` follow-on subset with `tests/axi/axi_stream/test_AxiStreamPipeline.py`, `tests/axi/axi_stream/test_AxiStreamMux.py`, and `tests/axi/axi_lite/test_AxiLiteCrossbar.py` in one run (`7 passed`). +- 2026-03-21: Started the next flat-queue `axi/` item, `AxiStreamDeMux`, and began evaluating whether a dedicated cocotb-facing adapter is cleaner than reusing the older combined DeMux/Mux harness for the first narrow wrapper bench. +- 2026-03-21: Implemented and validated `AxiStreamDeMux` with `axi/axi-stream/ip_integrator/AxiStreamDeMuxIpIntegrator.vhd` plus `tests/axi/axi_stream/test_AxiStreamDeMux.py`. The validated 3-case sweep covers indexed routing, exact-match routed decode under output backpressure, and dynamic-route/drop/reset behavior (`3 passed`). +- 2026-03-21: Revalidated the current small `axi/` follow-on subset with `tests/axi/axi_stream/test_AxiStreamPipeline.py`, `tests/axi/axi_stream/test_AxiStreamMux.py`, `tests/axi/axi_stream/test_AxiStreamDeMux.py`, and `tests/axi/axi_lite/test_AxiLiteCrossbar.py` in one run (`10 passed`). +- 2026-03-21: Started scoping the next five flat-queue modules after `AxiStreamDeMux`: `AxiStreamResize`, `AxiLiteAsync`, `AxiLiteMaster`, `AxiLiteToDrp`, and `AxiDualPortRam`, beginning with a wrapper/reference-asset pass to separate straightforward benches from blocks that still need adapter cleanup. +- 2026-03-21: Implemented and validated the next five flat-queue modules: `AxiStreamResize`, `AxiLiteAsync`, `AxiLiteMaster`, `AxiLiteToDrp`, and `AxiDualPortRam`. The five-module batch passes with `10 passed`, and a broader AXI follow-on sanity run across pipeline, mux, demux, resize, crossbar, async, master, DRP bridge, and dual-port RAM passes with `20 passed`. `AxiLiteAsync` and `AxiLiteToDrp` intentionally keep only the stable common-clock subsets in this first batch; the async CDC/arbitration branches remain open. +- 2026-03-21: Replaced the hand-maintained flat phase-1 list in the plan with a generated path-qualified bottom-up queue emitted by `scripts/build_rtl_instantiation_graph.py`. The initial generated queue contained `411` phase-1 modules with `0` unresolved duplicate-name phase-1 edges under the then-current filter set. +- 2026-03-21: Implemented and validated the next 10 generated-queue AXI modules: `AxiLiteRegs`, `AxiLiteRespTimer`, `AxiLiteSlave`, `AxiLiteWriteFilter`, `AxiVersion`, `AxiStreamCombiner`, `AxiStreamFlush`, `AxiStreamGearboxPack`, `AxiStreamGearboxUnpack`, and `AxiStreamSplitter`. The combined validation command across those 10 module files passes with `14 passed`. +- 2026-03-26: Implemented and validated `AxiReadPathMux`, `AxiWritePathMux`, and `AxiToAxiLite` with subsystem-local IP-integrator adapters plus new `tests/axi/axi4/` and `tests/axi/bridge/` cocotb benches. Historical context: `tests/axi/axi4/test_AxiResize.py` was also present with a restored `32-bit -> 64-bit` upsize case that was expected to fail at that time; the later `AxiResize` RTL fix is now merged and the current branch counts that case as passing. +- 2026-03-26: Implemented and validated the next 10 generated-queue AXI modules: `AxiStreamDmaV2WriteMux`, `AxiLiteMasterProxy`, `AxiLiteSequencerRam`, `AxiStreamCompact`, `AxiStreamConcat`, `AxiStreamFrameRateLimiter`, `AxiStreamPrbsFlowCtrl`, `AxiStreamRepeater`, `AxiStreamShift`, and `AxiStreamTrailerAppend`. Added subsystem-local wrappers under `axi/dma/ip_integrator/`, `axi/axi-lite/ip_integrator/`, and `axi/axi-stream/ip_integrator/`, plus new cocotb benches under `tests/axi/`. The combined validation run across those 10 module files passes with `10 passed`. `AxiStreamCompact`, `AxiStreamFrameRateLimiter`, and `AxiStreamDmaV2WriteMux` currently keep intentionally narrow first-pass checks on this branch instead of forcing the less stable simulator corners. +- 2026-03-26: Implemented the next 10 generated-queue AXI module benches: `AxiStreamTrailerRemove`, `AxiRam`, `AxiLiteToIpBus`, `IpBusToAxiLite`, `AxiStreamDmaV2Read`, `AxiStreamGearbox`, `AxiStreamTap`, `AxiStreamDmaRead`, `AxiStreamDmaV2Write`, and `AxiStreamTimer`. The stable 9-module subset passes with `9 passed`, while `AxiStreamDmaV2Read` remains an expected open failure on this branch because the new minimal aligned one-beat reproducer still aborts at `31 ns` inside the DUT with `std_logic_arith.vhdl:2014:9: ARG is too large in CONV_INTEGER`. +- 2026-04-02: Fixed the remaining `AxiStreamDmaV2Read` open issue by bounding the byte-count `conv_integer()` paths in `axi/axi4/rtl/AxiPkg.vhd`, replacing terminal-beat `tKeep`/`tStrb` generation in `axi/dma/rtl/v2/AxiStreamDmaV2Read.vhd` with direct `slv` mask construction, and expanding `tests/axi/dma/test_AxiStreamDmaV2Read.py` into aligned and short-terminal-beat cases. Local validation now passes with `./.venv/bin/python -m pytest -n 0 -q tests/axi/dma/test_AxiStreamDmaV2Read.py` (`2 passed`). +- 2026-03-26: Replaced the temporary legacy-harness `AxiStreamDmaRead` smoke with a cocotb-owned bench plus `AxiStreamDmaReadIpIntegrator.vhd`. The new test drives the DMA request directly, uses a cocotb AXI RAM model, applies output backpressure, and checks payload plus sideband fields without delegating pass/fail to the old VHDL testbench shell (`1 passed`). +- 2026-03-26: Retargeted the remaining legacy-entity holdouts in the current validated set. `AxiRam` now uses `AxiRamIpIntegrator.vhd` plus a cocotb AXI master round-trip bench, `AxiStreamGearbox` now targets `AxiStreamGearboxIpIntegrator.vhd` instead of the old `tb/` shell, and `AxiLiteCrossbar` now targets `AxiLiteCrossbarIpIntegrator.vhd` instead of `AxiLiteCrossbarTb.vhd` (`3 passed` across the retargeted tests). +- 2026-03-26: Resumed the generated queue at `AxiRateGen` and started scoping the cocotb-facing AXI4/IP-integrator pattern for the next `axi/axi4/` regression. +- 2026-03-26: Implemented and validated `AxiRateGen` with `axi/axi4/ip_integrator/AxiRateGenIpIntegrator.vhd` plus `tests/axi/axi4/test_AxiRateGen.py`. The stable common-clock subset passes with `1 passed`, and a nearby AXI4 sanity run across `AxiReadPathMux`, `AxiWritePathMux`, `AxiRam`, and `AxiRateGen` passes with `4 passed`. +- 2026-03-26: Tightened the planning docs so wrapper readability is explicit instead of implicit: permanent cocotb-facing `*IpIntegrator.vhd` files should include the standard SURF banner and brief section comments in the first edit, just like the Python benches are required to carry their methodology and tutorial comments. +- 2026-03-26: Tightened the planning docs again so the Python-side header rule is explicit too: checked-in cocotb tests should keep the standard SURF/SLAC banner in addition to the required methodology block and tutorial comments; this is now documented as a first-draft requirement rather than an implied cleanup step. +- 2026-03-26: Corrected the queue frontier after noticing the prior resume notes had jumped ahead to `IpV4Engine`. The real next unfinished non-deferred queue entry is `EthMacRxShift`, followed by `EthMacTxExportGmii`, `EthMacTxShift`, `IpV4EngineRx`, `IpV4EngineTx`, `RawEthFramer`, `UdpEngineRx`, `GLinkTxToRx`, `HtspRx`, and `HtspTx`. +- 2026-03-26: Changed the rollout policy to finish `axi/` first before returning to other subsystems. Recorded temporary `ethernet` and `protocols` subsystem deferrals in the generated queue inputs, regenerated the queue, and set the active axi frontier to `AxiResize`. +- 2026-03-27: Implemented and validated the final 11 pending `axi/` benches: `AxiReadEmulate`, `AxiRingBuffer`, `AxiWriteEmulate`, `AxiStreamDmaRingRead`, `AxiStreamDmaWrite`, `AxiLiteRamSyncStatusVector`, `AxiStreamMonAxiL`, `AxiStreamDma`, `AxiStreamDmaFifo`, `AxiStreamDmaRingWrite`, and `AxiMonAxiL`. Added the required subsystem-local `*IpIntegrator.vhd` wrappers, kept the new Python tests fully commented with the standard SURF header plus methodology/tutorial notes, and validated the final batch with `./.venv/bin/python -m pytest -n 0 -q ...` across the 11 files (`11 passed`). +- 2026-03-27: The new `AxiStreamDmaRingWrite` regression exposed a width-safety issue in the DUT pointer update under GHDL. Fixed `axi/dma/rtl/v1/AxiStreamDmaRingWrite.vhd` so `dmaAck.size` is sliced back to the local pointer width before incrementing `nextAddr`, which keeps the logic behavior unchanged for narrower address maps while making the testbench-safe wrapper configuration simulate cleanly. +- 2026-03-26: Implemented and validated the next 10 generated-queue AXI modules after the then-open `AxiResize` and `AxiStreamDmaV2Read` gaps: `AxiStreamScatterGather`, `AxiMemTester`, `AxiStreamDmaV2Desc`, `AxiStreamDmaV2Fifo`, `AxiReadPathFifo`, `AxiWritePathFifo`, `AxiStreamDmaV2`, `AxiStreamBatchingFifo`, `AxiStreamMon`, and `AxiStreamRingBuffer`. Added the required new wrapper files under `axi/axi4/ip_integrator/`, `axi/axi-stream/ip_integrator/`, and `axi/dma/ip_integrator/`, fixed `tests/common/regression_utils.py` to stringify simulator env values before dispatch, and validated the combined batch with `./.venv/bin/python -m pytest -n 0 -q tests/axi/axi_stream/test_AxiStreamScatterGather.py tests/axi/axi4/test_AxiMemTester.py tests/axi/dma/test_AxiStreamDmaV2Desc.py tests/axi/dma/test_AxiStreamDmaV2Fifo.py tests/axi/axi4/test_AxiReadPathFifo.py tests/axi/axi4/test_AxiWritePathFifo.py tests/axi/dma/test_AxiStreamDmaV2.py tests/axi/axi_stream/test_AxiStreamBatchingFifo.py tests/axi/axi_stream/test_AxiStreamMon.py tests/axi/axi_stream/test_AxiStreamRingBuffer.py` (`10 passed`). Both gaps are now superseded by later fixes recorded above. +- 2026-04-20: Started the first `ethernet/RoCEv2` phase-1 slice with the stable VHDL-only helper leaves instead of the mixed-language top wrappers. Added `ethernet/RoCEv2/wrappers/EthMacPrepareForICrcWrapper.vhd` and `ethernet/RoCEv2/wrappers/EthMacRxCheckICrcWrapper.vhd`, implemented `tests/ethernet/RoCEv2/test_EthMacPrepareForICrc.py` and `tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py`, linted both wrappers cleanly with `./.venv/bin/vsg -c vsg-linter.yml -f ...`, validated the pair with `./.venv/bin/python -m pytest -n 0 -q tests/ethernet/RoCEv2/test_EthMacPrepareForICrc.py tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py` (`2 passed`), and recorded the remaining open RoCEv2 work as the mixed-language wrapper/assembly path rather than claiming full-family coverage. +- 2026-04-20: Added the pure-VHDL RoCEv2 follow-on benches `tests/ethernet/RoCEv2/test_RoceResizeAndSwap.py` and `test_RoceConfigurator.py` plus their checked-in wrappers `ethernet/RoCEv2/wrappers/RoceResizeAndSwapIpIntegrator.vhd` and `RoceConfiguratorWrapper.vhd`, and validated the pure-VHDL RoCEv2 bench set with `./.venv/bin/python -m pytest -n 0 -q tests/ethernet/RoCEv2/test_EthMacPrepareForICrc.py tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py tests/ethernet/RoCEv2/test_RoceResizeAndSwap.py tests/ethernet/RoCEv2/test_RoceConfigurator.py` (`4 passed`). +- 2026-04-20: Removed the temporary local CRC stand-ins and the stub-backed `EthMacTxRoCEv2` / `EthMacRxRoCEv2` benches after clarifying the intended boundary: there should be one cocotb test module per entity in `ethernet/RoCEv2/rtl`, but `blue-*` must remain real transitive dependencies rather than being replaced with local test doubles. The current open RoCEv2 bench set is therefore `EthMacCrcAxiStreamWrapperSend`, `EthMacCrcAxiStreamWrapperRecv`, `EthMacTxRoCEv2`, `EthMacRxRoCEv2`, and `RoceEngineWrapper`, all of which now require a mixed-language simulation path. +- 2026-04-20: Added the next CoaXPress assembly pass with `tests/protocols/coaxpress/test_CoaXPressRx.py`, `test_CoaXPressCore.py`, and the checked-in wrappers `protocols/coaxpress/core/wrappers/CoaXPressRxWrapper.vhd` and `CoaXPressCoreWrapper.vhd`. The validated subset covered the one-lane `CoaXPressRx` receive assembly and the AXI-Lite-controlled `CoaXPressCore` TX/config assembly path. `tests/protocols/coaxpress/test_CoaXPressConfig.py` was kept in-tree as a skipped investigation bench at that point because the real SRP-driven `CoaXPressConfig` request path did not complete under the then-current `CoaXPressConfig` / `SrpV3AxiLite` integration. +- 2026-04-20: Deepened the CoaXPress receive/bridge coverage without changing RTL scope. `test_CoaXPressRxHsFsm.py` now includes a dual-lane step/alignment case, `test_CoaXPressRx.py` now includes a dual-lane lane-rotation integration case through a generalized `CoaXPressRxWrapper.vhd`, `test_CoaXPressRxLane.py` now covers alternate-success control acknowledgments plus truncated-event guardrails, `test_CoaXPressTxLsFsm.py` now covers the slower implemented low-speed-rate trigger cadence with inverted-trigger mapping, and the CXPoF bridge leaf benches now include HKP, negative lane-placement checks, and partial-lane low-speed fill behavior. The focused validation run across those six files passed locally with `8 passed`. +- 2026-05-01: Integrated the latest `coaxpress-tests` progress into the `verification-2` merge worktree while preserving the `verification-2` `docs/_meta` artifacts. The CoaXPress conflict resolution takes the updated bridge README/test coverage from `coaxpress-tests`: leaf HKP-to-payload mixing, malformed control-lane guardrails for `/S/`, `/Q/`, `/T/`, and `/E/`, and top-level 64-bit RX gearbox cases for `/E/` abort/recovery, HKP-to-payload transition, and lane-0 `/Q/` no-output/recovery. Focused validation used `./.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/protocols/coaxpress` and passed with `17 passed, 1 skipped`; the skipped bench was `CoaXPressConfig` at that point. +- 2026-05-03: Re-enabled `tests/protocols/coaxpress/test_CoaXPressConfig.py` after the SRP helper cleanup by driving the real `CoaXPressConfig` / `SrpV3AxiLite` ingress with shared SRPv3 request helpers. The active bench now covers untagged read and tagged write command serialization, command CRC generation including tag-word coverage, tag incrementing, and completed SRPv3 responses after config receive acknowledgments. Focused validation used `./.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/protocols/coaxpress` and passed with `18 passed`. +- 2026-05-03: Deepened the remaining current-RTL CoaXPress spec coverage without changing RTL. `test_CoaXPressConfig.py` now adds tagged read, untagged write, config-response timeout, and nonzero-ack-status error-footer coverage; `test_CoaXPressRxHsFsm.py` now covers a new image header before the prior frame's declared line count completes; and `test_CoaXPressRxLane.py` documented the then-current malformed-control-ack trailer limitation. This ACK limitation was superseded by the 2026-05-04 receive-lane RTL fix. Focused validation used `./.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/protocols/coaxpress` and passed with `18 passed`. +- 2026-05-04: Converted two CoaXPress receive-side partial protocol areas into enforced RTL behavior. `CoaXPressRxLane.vhd` now validates control-ack and heartbeat CRC/`EOP` trailers before pulsing `cfgMaster` or `heatbeatMaster`, while the focused receive-lane and top-level receive benches now compute real CRC words and include malformed-trailer suppression/recovery guardrails. Focused validation used `./.venv/bin/vsg --config vsg-linter.yml -f protocols/coaxpress/core/rtl/CoaXPressRxLane.vhd`, `./.venv/bin/python -m pytest -n 0 -q tests/protocols/coaxpress/test_CoaXPressRxLane.py`, and `./.venv/bin/python -m pytest -n 0 -q tests/protocols/coaxpress/test_CoaXPressRx.py`. +- 2026-05-04: Resumed CoaXPress RTL spec work with the stream-data trailer path. `CoaXPressRxLane.vhd` now holds the stream parser in CRC/`EOP` states after the declared payload count instead of accepting a new `SOP` immediately, and the receive-lane, receive-assembly, and core helpers now emit real stream CRC/trailer words. This validates stream packet framing before the next packet while preserving the existing immediate payload-forwarding contract. Focused validation used `./.venv/bin/vsg --fix -c vsg-linter.yml -f protocols/coaxpress/core/rtl/CoaXPressRxLane.vhd`, `./.venv/bin/python -m pytest -n 0 -q tests/protocols/coaxpress/test_CoaXPressRxLane.py`, `./.venv/bin/python -m pytest -n 0 -q tests/protocols/coaxpress/test_CoaXPressRx.py`, `./.venv/bin/python -m pytest -n 0 -q tests/protocols/coaxpress/test_CoaXPressCore.py`, and `./.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/protocols/coaxpress` (`18 passed`). +- 2026-05-04: Made CoaXPress receive-lane parser errors observable. `CoaXPressRxLane` now exports `rxError`, `CoaXPressRx` ORs all lane errors into `rxFsmError` alongside the existing high-speed FSM error, the lane wrapper exposes the new pulse, and `test_CoaXPressCore.py` verifies a bad stream CRC increments the existing software-visible `RxFsmErrorCnt` before a later clean frame recovers. Focused validation used `./.venv/bin/vsg --fix -c vsg-linter.yml -f protocols/coaxpress/core/rtl/CoaXPressRxLane.vhd protocols/coaxpress/core/rtl/CoaXPressRx.vhd protocols/coaxpress/core/wrappers/CoaXPressRxLaneWrapper.vhd`, `./.venv/bin/python -m pytest -n 0 -q tests/protocols/coaxpress/test_CoaXPressRxLane.py`, `./.venv/bin/python -m pytest -n 0 -q tests/protocols/coaxpress/test_CoaXPressRx.py`, `./.venv/bin/python -m pytest -n 0 -q tests/protocols/coaxpress/test_CoaXPressCore.py`, and `./.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/protocols/coaxpress` (`18 passed`). +- 2026-05-04: Collapsed the CXPoF bridge RX status scalar list into `CxpofRxStatusType`, added cocotb-only flattening wrappers for the bridge leaf/top regressions, and added `CoaXPressOverFiberBridgeAxiL` so the GTH/GTY CoaXPress-over-Fiber wrapper AXI-Lite ports expose sticky bridge status, last observed sequence/HKP fields, and event counters. Focused validation used `./.venv/bin/vsg -c vsg-linter.yml --fix protocols/coaxpress/core/rtl/CoaXPressPkg.vhd protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeRx.vhd protocols/coaxpress/core/rtl/CoaXPressOverFiberBridge.vhd protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeAxiL.vhd protocols/coaxpress/core/wrappers/CoaXPressOverFiberBridgeRxStatusWrapper.vhd protocols/coaxpress/core/wrappers/CoaXPressOverFiberBridgeStatusWrapper.vhd protocols/coaxpress/gthUs+/rtl/CoaXPressOverFiberGthUsIpWrapper.vhd protocols/coaxpress/gthUs/rtl/CoaXPressOverFiberGthUsIpWrapper.vhd protocols/coaxpress/gtyUs+/rtl/CoaXPressOverFiberGtyUsIpWrapper.vhd` and `./.venv/bin/python -m pytest -q tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeRx.py tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py` (`2 passed`). +- 2026-05-05: Implemented the robust CoaXPress receive-side SSI `EOFE` design for malformed stream trailers. `CoaXPressRxLane` now emits an in-order trailer verdict marker after stream CRC/`EOP` validation, `CoaXPressRxLaneMux` rotates lanes only after that marker, `CoaXPressRxWordPacker` now has real input/output handshakes and propagates terminal `EOFE`, and `CoaXPressRxHsFsm` holds only the final packed image EOF beat until the trailer verdict arrives. Malformed stream trailers now set SSI `EOFE` on that final beat while lane-level `rxError` remains the single source for the existing software `RxFsmErrorCnt` increment. Focused validation used `./.venv/bin/python -m pytest -q tests/protocols/coaxpress/test_CoaXPressRx.py` (`3 passed`), `./.venv/bin/python -m pytest -q tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py` (`3 passed`), `./.venv/bin/python -m pytest -q tests/protocols/coaxpress/test_CoaXPressRxLane.py tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py tests/protocols/coaxpress/test_CoaXPressRxLaneMux.py` (`4 passed`), and `./.venv/bin/python -m pytest -q tests/protocols/coaxpress/test_CoaXPressCore.py::test_CoaXPressCore` (`1 passed`). `git diff --check -- protocols/coaxpress/core/rtl protocols/coaxpress/core/wrappers tests/protocols/coaxpress` was clean. +- 2026-05-21: Moved the retained RTL-regression task notes from `docs/_meta/rtl_regression_*` into `docs/plans/rtl-regression/` as `plan.md`, `progress.md`, `handoff.md`, and `inventory.yaml`, added the task README, and updated the current progress summary to match the actual tree state after the `origin/pre-release` refresh. +- 2026-05-21: Started `protocols/packetizer` with standalone module tests rather than a packetizer/depacketizer loopback oracle. Added full-`TUSER` cocotb wrappers `protocols/packetizer/wrappers/AxiStreamPacketizer2Wrapper.vhd` and `AxiStreamDepacketizer2Wrapper.vhd`, shared helpers under `tests/protocols/packetizer/packetizer_test_utils.py`, and direct tests for `AxiStreamPacketizer2` packet formatting/splitting plus `AxiStreamDepacketizer2` packet restoration. Validation used `./.venv/bin/vsg -c vsg-linter.yml -f protocols/packetizer/wrappers/AxiStreamPacketizer2Wrapper.vhd protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd` and `./.venv/bin/python -m pytest -n 0 -q tests/protocols/packetizer/test_AxiStreamPacketizer2.py tests/protocols/packetizer/test_AxiStreamDepacketizer2.py` (`2 passed`). +- 2026-05-21: Added standalone legacy V0 packetizer/depacketizer coverage. New wrappers `protocols/packetizer/wrappers/AxiStreamPacketizerWrapper.vhd` and `AxiStreamDepacketizerWrapper.vhd` expose full per-byte user vectors; new tests `tests/protocols/packetizer/test_AxiStreamPacketizer.py` and `test_AxiStreamDepacketizer.py` cover V0 header fields plus appended-tail and separate-tail EOF/user encoding. The wrapper shims now zero unused high `tKeep`/`tStrb`/`tUser` bits before assigning narrow bus fields. Validation used `./.venv/bin/vsg -c vsg-linter.yml -f protocols/packetizer/wrappers/AxiStreamPacketizerWrapper.vhd protocols/packetizer/wrappers/AxiStreamDepacketizerWrapper.vhd protocols/packetizer/wrappers/AxiStreamPacketizer2Wrapper.vhd protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd` and `./.venv/bin/python -m pytest -n 0 -q tests/protocols/packetizer` (`4 passed`). +- 2026-05-21: Added standalone `AxiStreamBytePacker` coverage with `protocols/packetizer/wrappers/AxiStreamBytePackerWrapper.vhd` and `tests/protocols/packetizer/test_AxiStreamBytePacker.py`. The bench covers 4-byte compressed-keep input to 8-byte output packing across partial beats, exact-width terminal packing, per-byte `TUSER` propagation, and reset flushing of a buffered partial word. Validation used `./.venv/bin/vsg -c vsg-linter.yml -f protocols/packetizer/wrappers/AxiStreamBytePackerWrapper.vhd` and `./.venv/bin/python -m pytest -n 0 -q tests/protocols/packetizer` (`5 passed`). +- 2026-05-21: Expanded packetizer standalone stress coverage. Added V0 max-size split and continuation depacketization, V2 partial final `TKEEP`, V2 interleaved-`TDEST` rearbitration state, V2 CRC-none nonzero-tail `EOFE` marking, and byte-packer idle-gap/no-ready cases. Validation used `./.venv/bin/python -m pytest -n 0 -q tests/protocols/packetizer` (`5 passed`; each pytest wrapper runs multiple cocotb cases). +- 2026-05-21: Expanded packetizer parameter coverage where it had the clearest value. `AxiStreamBytePackerWrapper` now has `SLAVE_BYTES_G` and `MASTER_BYTES_G` generics, and `test_AxiStreamBytePacker.py` sweeps 2-to-5, 3-to-6, 4-to-8, and 5-to-7 compressed-keep width conversions while running the same partial/exact/reset/idle/no-ready stress cases. Validation used `./.venv/bin/vsg -c vsg-linter.yml -f protocols/packetizer/wrappers/AxiStreamBytePackerWrapper.vhd` and `./.venv/bin/python -m pytest -n 0 -q tests/protocols/packetizer` (`8 passed`). +- 2026-05-21: Expanded packetizer corner and parameter coverage. Added `recv_beats_with_backpressure()` shared helper, V0/V2 packetizer output-backpressure hold checks, V0 malformed-continuation bleed/recovery, V2 bad-version and bad-CRC-mode header error checks, V2 link-drop recovery, Packetizer2 CRC NONE/DATA/FULL sweeps, `AxiStreamPacketizer2LoopbackWrapper` plus loopback CRC-mode coverage, BytePacker zero-keep input guardrail, and BytePacker width pairs 1-to-8, 3-to-7, and 7-to-8. Validation used `./.venv/bin/vsg -c vsg-linter.yml -f protocols/packetizer/wrappers/AxiStreamPacketizer2LoopbackWrapper.vhd`, `env PYTHONPYCACHEPREFIX=/private/tmp/surf-pycache python3 -m py_compile ...`, and `./.venv/bin/python -m pytest -n 0 -q tests/protocols/packetizer` (`16 passed`). +- 2026-05-21: Added the remaining high-value packetizer edge tests from the follow-up review list. New checks cover DATA/FULL bad-CRC rejection in `AxiStreamDepacketizer2`, V2 sequence-counter wrap with `SEQ_CNT_SIZE_G=4`, `TDEST_BITS_G=0` and `TDEST_BITS_G=1` loopback edge cases, one-byte-over `maxPktBytes` packet splitting, and isolated mid-frame link-drop termination/recovery. Validation used `env PYTHONPYCACHEPREFIX=/private/tmp/surf-pycache python3 -m py_compile ...` and `./.venv/bin/python -m pytest -n 0 -q tests/protocols/packetizer` (`22 passed`). +- 2026-05-21: Refactored the packetizer cocotb helper layer after the expanded coverage pass. `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 individual packetizer tests now focus mostly on scenario setup and expected protocol fields. Validation used `env PYTHONPYCACHEPREFIX=/private/tmp/surf-pycache ./.venv/bin/python -m py_compile ...`, `./.venv/bin/python -m pytest -n 0 -q tests/protocols/packetizer` (`22 passed`), and `git diff --check`. diff --git a/protocols/coaxpress/core/rtl/CoaXPressCore.vhd b/protocols/coaxpress/core/rtl/CoaXPressCore.vhd index 204e2405c1..6007553a07 100755 --- a/protocols/coaxpress/core/rtl/CoaXPressCore.vhd +++ b/protocols/coaxpress/core/rtl/CoaXPressCore.vhd @@ -93,6 +93,7 @@ architecture mapping of CoaXPressCore is signal eventAck : sl; signal eventTag : slv(7 downto 0); + signal eventMaster : AxiStreamMasterType; signal trigAck : sl; signal txLsRateInt : sl; @@ -177,6 +178,8 @@ begin cfgClk => cfgClk, cfgRst => cfgRst, cfgRxMaster => cfgRxMaster, + eventMaster => eventMaster, + eventSlave => AXI_STREAM_SLAVE_FORCE_C, -- Event ACK Interface (cfgClk domain) eventAck => eventAck, eventTag => eventTag, diff --git a/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridge.vhd b/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridge.vhd index 718ff9e4e3..a8c99ea9a7 100644 --- a/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridge.vhd +++ b/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridge.vhd @@ -45,10 +45,11 @@ entity CoaXPressOverFiberBridge is txLsLaneEn : in slv(3 downto 0); txLsRate : in sl; -- CXP RX interface (rxClk312 domain) - rxClk312 : in sl; - rxRst312 : in sl; - rxData : out slv(31 downto 0); - rxDataK : out slv(3 downto 0)); + rxClk312 : in sl; + rxRst312 : in sl; + rxData : out slv(31 downto 0); + rxDataK : out slv(3 downto 0); + rxStatus : out CxpofRxStatusType); end entity CoaXPressOverFiberBridge; architecture mapping of CoaXPressOverFiberBridge is @@ -92,7 +93,9 @@ begin xgmiiRxc => rxc, -- CXP interface rxData => rxData, - rxDataK => rxDataK); + rxDataK => rxDataK, + -- Status Interface + rxStatus => rxStatus); GEN_TX : if (LANE0_G = true) generate diff --git a/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeAxiL.vhd b/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeAxiL.vhd new file mode 100644 index 0000000000..5d5f9cf4cf --- /dev/null +++ b/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeAxiL.vhd @@ -0,0 +1,220 @@ +------------------------------------------------------------------------------- +-- Title : CXP Over Fiber Bridge AXI-Lite Status +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: AXI-Lite status and counters for CoaXPress-over-Fiber bridge RX +------------------------------------------------------------------------------- +-- 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; +use ieee.std_logic_unsigned.all; +use ieee.std_logic_arith.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.CoaXPressPkg.all; + +entity CoaXPressOverFiberBridgeAxiL is + generic ( + TPD_G : time := 1 ns; + CNT_WIDTH_G : positive range 1 to 32 := 16); + port ( + -- Bridge RX status clock domain + rxClk : in sl; + rxRst : in sl; + rxStatus : in CxpofRxStatusType; + -- AXI-Lite Register Interface + axilClk : in sl; + axilRst : in sl; + axilReadMaster : in AxiLiteReadMasterType; + axilReadSlave : out AxiLiteReadSlaveType; + axilWriteMaster : in AxiLiteWriteMasterType; + axilWriteSlave : out AxiLiteWriteSlaveType); +end entity CoaXPressOverFiberBridgeAxiL; + +architecture rtl of CoaXPressOverFiberBridgeAxiL is + + constant STATUS_WIDTH_C : positive := 6; + + constant RX_ERROR_INDEX_C : natural := 0; + constant RX_ABORT_INDEX_C : natural := 1; + constant SEQ_VALID_INDEX_C : natural := 2; + constant SEQ_ERROR_INDEX_C : natural := 3; + constant HKP_VALID_INDEX_C : natural := 4; + constant HKP_ERROR_INDEX_C : natural := 5; + + type RegType is record + cntRst : sl; + stickyStatus : slv(STATUS_WIDTH_C-1 downto 0); + statusCnt : Slv32Array(STATUS_WIDTH_C-1 downto 0); + rxErrorCode : slv(3 downto 0); + seqData : slv(23 downto 0); + seqExpected : slv(23 downto 0); + seqErrorExpected : slv(23 downto 0); + hkpData : slv(31 downto 0); + hkpWordCount : slv(7 downto 0); + hkpKCodeMask : slv(3 downto 0); + hkpKCodeValid : sl; + hkpType : slv(3 downto 0); + axilWriteSlave : AxiLiteWriteSlaveType; + axilReadSlave : AxiLiteReadSlaveType; + end record RegType; + + constant REG_INIT_C : RegType := ( + cntRst => '1', + stickyStatus => (others => '0'), + statusCnt => (others => (others => '0')), + rxErrorCode => CXPOF_RX_ERR_NONE_C, + seqData => (others => '0'), + seqExpected => (others => '0'), + seqErrorExpected => (others => '0'), + hkpData => (others => '0'), + hkpWordCount => (others => '0'), + hkpKCodeMask => (others => '0'), + hkpKCodeValid => '0', + hkpType => CXPOF_HKP_TYPE_NONE_C, + axilWriteSlave => AXI_LITE_WRITE_SLAVE_INIT_C, + axilReadSlave => AXI_LITE_READ_SLAVE_INIT_C); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + + signal statusIn : slv(STATUS_WIDTH_C-1 downto 0); + + signal rxAxilReadMaster : AxiLiteReadMasterType; + signal rxAxilReadSlave : AxiLiteReadSlaveType; + signal rxAxilWriteMaster : AxiLiteWriteMasterType; + signal rxAxilWriteSlave : AxiLiteWriteSlaveType; + +begin + + statusIn(RX_ERROR_INDEX_C) <= rxStatus.rxError; + statusIn(RX_ABORT_INDEX_C) <= rxStatus.rxAbort; + statusIn(SEQ_VALID_INDEX_C) <= rxStatus.seqValid; + statusIn(SEQ_ERROR_INDEX_C) <= rxStatus.seqError; + statusIn(HKP_VALID_INDEX_C) <= rxStatus.hkpValid; + statusIn(HKP_ERROR_INDEX_C) <= rxStatus.hkpError; + + U_AxiLiteAsync : entity surf.AxiLiteAsync + generic map ( + TPD_G => TPD_G) + port map ( + -- Slave Interface + sAxiClk => axilClk, + sAxiClkRst => axilRst, + sAxiReadMaster => axilReadMaster, + sAxiReadSlave => axilReadSlave, + sAxiWriteMaster => axilWriteMaster, + sAxiWriteSlave => axilWriteSlave, + -- Master Interface + mAxiClk => rxClk, + mAxiClkRst => rxRst, + mAxiReadMaster => rxAxilReadMaster, + mAxiReadSlave => rxAxilReadSlave, + mAxiWriteMaster => rxAxilWriteMaster, + mAxiWriteSlave => rxAxilWriteSlave); + + comb : process (r, rxAxilReadMaster, rxAxilWriteMaster, rxRst, rxStatus, + statusIn) is + variable v : RegType; + variable axilEp : AxiLiteEndpointType; + variable hkpStatus : slv(31 downto 0); + begin + -- Latch the current value + v := r; + + -- Reset strobes + v.cntRst := '0'; + + -- Accumulate sticky status bits and event counters in the RX domain. + v.stickyStatus := r.stickyStatus or statusIn; + for i in 0 to STATUS_WIDTH_C-1 loop + if (statusIn(i) = '1') then + v.statusCnt(i)(CNT_WIDTH_G-1 downto 0) := r.statusCnt(i)(CNT_WIDTH_G-1 downto 0) + 1; + end if; + end loop; + + if (r.cntRst = '1') then + v.stickyStatus := (others => '0'); + v.statusCnt := (others => (others => '0')); + end if; + + if (rxStatus.rxError = '1') then + v.rxErrorCode := rxStatus.rxErrorCode; + end if; + + if (rxStatus.seqValid = '1') then + v.seqData := rxStatus.seqData; + v.seqExpected := rxStatus.seqExpected; + end if; + + if (rxStatus.seqError = '1') then + v.seqErrorExpected := rxStatus.seqErrorExpected; + end if; + + if (rxStatus.hkpValid = '1') or (rxStatus.hkpError = '1') then + v.hkpData := rxStatus.hkpData; + v.hkpWordCount := rxStatus.hkpWordCount; + v.hkpKCodeMask := rxStatus.hkpKCodeMask; + v.hkpKCodeValid := rxStatus.hkpKCodeValid; + v.hkpType := rxStatus.hkpType; + end if; + + hkpStatus := (others => '0'); + hkpStatus(7 downto 0) := r.hkpWordCount; + hkpStatus(11 downto 8) := r.hkpKCodeMask; + hkpStatus(12) := r.hkpKCodeValid; + hkpStatus(19 downto 16) := r.hkpType; + + ------------------------ + -- AXI-Lite Transactions + ------------------------ + axiSlaveWaitTxn(axilEp, rxAxilWriteMaster, rxAxilReadMaster, v.axilWriteSlave, v.axilReadSlave); + + axiSlaveRegisterR(axilEp, x"000", 0, r.stickyStatus); + axiSlaveRegisterR(axilEp, x"004", 0, r.rxErrorCode); + axiSlaveRegisterR(axilEp, x"008", 0, r.seqData); + axiSlaveRegisterR(axilEp, x"00C", 0, r.seqExpected); + axiSlaveRegisterR(axilEp, x"010", 0, r.seqErrorExpected); + axiSlaveRegisterR(axilEp, x"014", 0, r.hkpData); + axiSlaveRegisterR(axilEp, x"018", 0, hkpStatus); + + for i in 0 to STATUS_WIDTH_C-1 loop + axiSlaveRegisterR(axilEp, x"020"+toSlv(i*4, 12), 0, r.statusCnt(i)); + end loop; + + axiSlaveRegister(axilEp, x"03C", 0, v.cntRst); + + axiSlaveDefault(axilEp, v.axilWriteSlave, v.axilReadSlave, AXI_RESP_DECERR_C); + + -- Outputs + rxAxilReadSlave <= r.axilReadSlave; + rxAxilWriteSlave <= r.axilWriteSlave; + + -- Reset + if (rxRst = '1') then + v := REG_INIT_C; + end if; + + rin <= v; + + end process comb; + + seq : process (rxClk) is + begin + if (rising_edge(rxClk)) then + r <= rin after TPD_G; + end if; + end process seq; + +end rtl; diff --git a/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeRx.vhd b/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeRx.vhd index e647e52e1a..21e0046d15 100644 --- a/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeRx.vhd +++ b/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeRx.vhd @@ -35,7 +35,9 @@ entity CoaXPressOverFiberBridgeRx is xgmiiRxc : in slv(3 downto 0); -- Rx PHY Interface rxData : out slv(31 downto 0); - rxDataK : out slv(3 downto 0)); + rxDataK : out slv(3 downto 0); + -- Status Interface + rxStatus : out CxpofRxStatusType); end entity CoaXPressOverFiberBridgeRx; architecture rtl of CoaXPressOverFiberBridgeRx is @@ -46,17 +48,19 @@ architecture rtl of CoaXPressOverFiberBridgeRx is PAYLOAD_S); type RegType is record - errDet : sl; + status : CxpofRxStatusType; + seqLocked : sl; rxData : Slv32Array(1 downto 0); rxDataK : Slv4Array(1 downto 0); state : StateType; end record RegType; constant REG_INIT_C : RegType := ( - errDet => '0', - rxData => (others => CXP_IDLE_C), - rxDataK => (others => CXP_IDLE_K_C), - state => IDLE_S); + status => CXPOF_RX_STATUS_INIT_C, + seqLocked => '0', + rxData => (others => CXP_IDLE_C), + rxDataK => (others => CXP_IDLE_K_C), + state => IDLE_S); signal r : RegType := REG_INIT_C; signal rin : RegType; @@ -73,7 +77,15 @@ begin v := r; -- Reset strobe - v.errDet := '0'; + v.status.rxError := '0'; + v.status.rxAbort := '0'; + v.status.rxErrorCode := CXPOF_RX_ERR_NONE_C; + v.status.seqValid := '0'; + v.status.seqError := '0'; + v.status.hkpValid := '0'; + v.status.hkpEop := '0'; + v.status.hkpSof := '0'; + v.status.hkpError := '0'; -- Update shift register v.rxDataK(1) := CXP_IDLE_K_C; @@ -86,10 +98,11 @@ begin ---------------------------------------------------------------------- when IDLE_S => -- Check for SOP - if (xgmiiRxc = "0001") and (xgmiiRxd(15 downto 9) = "1000000") and (xgmiiRxd(7 downto 0) = CXPOF_START_C) then + if (xgmiiRxc = CXPOF_XGMII_LANE0_CTRL_C) and (xgmiiRxd(15 downto 9) = CXPOF_SOP_CTRL_HS_PREFIX_C) and (xgmiiRxd(7 downto 0) = CXPOF_START_C) then -- Check for HKP condition - if (xgmiiRxd(8) = '1') then + if (xgmiiRxd(8 + CXPOF_SOP_CTRL_HKP_BIT_C) = '1') then + v.status.hkpWordCount := (others => '0'); -- Next State v.state := HKP_S; else @@ -97,25 +110,26 @@ begin -- Check if data is being overwritten if (v.rxDataK(0) /= CXP_IDLE_K_C) or (v.rxData(0) /= CXP_IDLE_C) then -- Set the flag - v.errDet := '1'; + v.status.rxError := '1'; + v.status.rxErrorCode := CXPOF_RX_ERR_OVERWRITE_C; end if; -- Check for SOP if (xgmiiRxd(23 downto 16) = CXP_SOP_C(7 downto 0)) then -- Send SOP - v.rxDataK(0) := x"F"; + v.rxDataK(0) := CXP_ALL_CTRL_K_C; v.rxData(0) := CXP_SOP_C; -- Send type - v.rxDataK(1) := x"0"; + v.rxDataK(1) := CXP_ALL_DATA_K_C; v.rxData(1) := xgmiiRxd(31 downto 24) & xgmiiRxd(31 downto 24) & xgmiiRxd(31 downto 24) & xgmiiRxd(31 downto 24); -- Check for I/O ACK elsif (xgmiiRxd(23 downto 16) = CXP_IO_ACK_C(7 downto 0)) then -- Send I/O ACK inductor - v.rxDataK(1) := x"F"; + v.rxDataK(1) := CXP_ALL_CTRL_K_C; v.rxData(1) := CXP_IO_ACK_C; end if; @@ -125,17 +139,62 @@ begin end if; - elsif (xgmiiRxc /= x"F") or (xgmiiRxd /= x"07_07_07_07") then + -- Check for lane-0 sequence ordered set + elsif (xgmiiRxc = CXPOF_XGMII_LANE0_CTRL_C) and (xgmiiRxd(7 downto 0) = CXPOF_SEQ_C) then + + -- Publish the sequence data without reconstructing a CXP word. + v.status.seqValid := '1'; + v.status.seqData := xgmiiRxd(31 downto 8); + if (r.seqLocked = '1') and (xgmiiRxd(31 downto 8) /= r.status.seqExpected) then + v.status.rxError := '1'; + v.status.seqError := '1'; + v.status.seqErrorExpected := r.status.seqExpected; + v.status.rxErrorCode := CXPOF_RX_ERR_SEQ_MISMATCH_C; + end if; + v.seqLocked := '1'; + v.status.seqExpected := xgmiiRxd(31 downto 8) + 1; + + -- Check for lane-0 error ordered set while idle + elsif (xgmiiRxc = CXPOF_XGMII_LANE0_CTRL_C) and (xgmiiRxd(7 downto 0) = CXPOF_ERROR_C) then + + -- Publish an error pulse even when no packet payload is active. + v.status.rxError := '1'; + v.status.rxAbort := '1'; + v.status.rxErrorCode := CXPOF_RX_ERR_IDLE_ERROR_C; + + elsif (xgmiiRxc /= CXPOF_XGMII_ALL_CTRL_C) or (xgmiiRxd /= CXPOF_IDLE_WORD_C) then -- Set the flag - v.errDet := '1'; + v.status.rxError := '1'; + v.status.rxErrorCode := CXPOF_RX_ERR_BAD_CONTROL_C; end if; ---------------------------------------------------------------------- when HKP_S => -- Send HKP - v.rxDataK(1) := x"F"; + v.rxDataK(1) := CXP_ALL_CTRL_K_C; v.rxData(1) := xgmiiRxd; + v.status.hkpValid := '1'; + v.status.hkpData := xgmiiRxd; + v.status.hkpSof := '1'; + v.status.hkpWordCount := r.status.hkpWordCount + 1; + v.status.hkpKCodeMask := cxpKCodeMask(xgmiiRxd); + v.status.hkpType := cxpHkpType(xgmiiRxd); + if (v.status.hkpKCodeMask = CXP_ALL_CTRL_K_C) then + v.status.hkpKCodeValid := '1'; + else + v.status.hkpKCodeValid := '0'; + end if; + if (xgmiiRxc /= CXPOF_XGMII_ALL_DATA_C) then + v.status.rxError := '1'; + v.status.hkpError := '1'; + v.status.rxErrorCode := CXPOF_RX_ERR_HKP_MALFORMED_C; + elsif (v.status.hkpKCodeValid = '0') then + v.status.rxError := '1'; + v.status.hkpError := '1'; + v.status.rxErrorCode := CXPOF_RX_ERR_HKP_BAD_K_CODE_C; + end if; -- Check for EOP if (xgmiiRxd = CXP_EOP_C) then + v.status.hkpEop := '1'; -- Next State v.state := IDLE_S; else @@ -145,18 +204,27 @@ begin ---------------------------------------------------------------------- when PAYLOAD_S => -- Check for data word - if (xgmiiRxc = "0000") then + if (xgmiiRxc = CXPOF_XGMII_ALL_DATA_C) then -- Send Type - v.rxDataK(1) := x"0"; + v.rxDataK(1) := CXP_ALL_DATA_K_C; v.rxData(1) := xgmiiRxd; + -- Check for error ordered set + elsif (xgmiiRxc = CXPOF_XGMII_LANE0_CTRL_C) and (xgmiiRxd(7 downto 0) = CXPOF_ERROR_C) then + + -- Abort the active packet without synthesizing a CXP EOP. + v.status.rxError := '1'; + v.status.rxAbort := '1'; + v.status.rxErrorCode := CXPOF_RX_ERR_PAYLOAD_ABORT_C; + v.state := IDLE_S; + -- Check for EOP - elsif (xgmiiRxc = "1100") and (xgmiiRxd(31 downto 8) = x"07_FD_00") then + elsif (xgmiiRxc = CXPOF_XGMII_LANE2_3_CTRL_C) and (xgmiiRxd(31 downto 8) = CXPOF_TERM_SUFFIX_C) then -- Check for non-zero value - if (xgmiiRxd(7 downto 0) /= 0) then + if (xgmiiRxd(7 downto 0) /= CXPOF_RESERVED_BYTE_C) then -- Send EOP - v.rxDataK(1) := x"F"; + v.rxDataK(1) := CXP_ALL_CTRL_K_C; v.rxData(1) := xgmiiRxd(7 downto 0) & xgmiiRxd(7 downto 0) & xgmiiRxd(7 downto 0) & xgmiiRxd(7 downto 0); else @@ -171,7 +239,8 @@ begin -- Undefined state else -- Set the flag - v.errDet := '1'; + v.status.rxError := '1'; + v.status.rxErrorCode := CXPOF_RX_ERR_BAD_CONTROL_C; -- Next State v.state := IDLE_S; end if; @@ -181,6 +250,7 @@ begin -- Outputs rxDataK <= r.rxDataK(0); rxData <= r.rxData(0); + rxStatus <= r.status; -- Reset if (rst = '1') then diff --git a/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeTx.vhd b/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeTx.vhd index 91296002de..4e1fe2d136 100644 --- a/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeTx.vhd +++ b/protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeTx.vhd @@ -68,7 +68,7 @@ architecture rtl of CoaXPressOverFiberBridgeTx is txLsData => (others => '0'), txLsDataK => '0', xgmiiTxd => CXPOF_IDLE_WORD_C, - xgmiiTxc => x"F", + xgmiiTxc => CXPOF_XGMII_ALL_CTRL_C, state => IDLE_S); signal r : RegType := REG_INIT_C; @@ -97,7 +97,7 @@ begin ---------------------------------------------------------------------- when IDLE_S => -- Send the start word - v.xgmiiTxc := x"F"; + v.xgmiiTxc := CXPOF_XGMII_ALL_CTRL_C; v.xgmiiTxd := CXPOF_IDLE_WORD_C; -- Check for new low speed byte @@ -111,7 +111,7 @@ begin ---------------------------------------------------------------------- when LS_SOP_S => -- Set the char marker - v.xgmiiTxc := "0001"; + v.xgmiiTxc := CXPOF_XGMII_LANE0_CTRL_C; -- Lane[0] = Start[7:0] v.xgmiiTxd(0+7 downto 0+0) := CXPOF_START_C; @@ -120,28 +120,28 @@ begin v.update := '0'; -- Lane[1] = SopCtrl[7] - Packet type: "0" => Low-speed packet - v.xgmiiTxd(8+7) := '0'; + v.xgmiiTxd(8+CXPOF_SOP_CTRL_PACKET_TYPE_BIT_C) := CXPOF_SOP_CTRL_LOW_SPEED_C; -- Lane[1] = SopCtrl[6:4] - Reserved v.xgmiiTxd(8+6 downto 8+4) := "000"; -- Lane[1] = SopCtrl[3] - Update flag - v.xgmiiTxd(8+3) := r.update; + v.xgmiiTxd(8+CXPOF_SOP_CTRL_UPDATE_BIT_C) := r.update; -- Lane[1] = SopCtrl[2] - Reserved v.xgmiiTxd(8+2) := '0'; -- Lane[1] = SopCtrl[1] - Low-speed rate: When '0'=> 20.83 Mbps, When '1'=> 41.6 Mbps - v.xgmiiTxd(8+1) := r.txLsRate; + v.xgmiiTxd(8+CXPOF_SOP_CTRL_LS_RATE_BIT_C) := r.txLsRate; -- Lane[1] = SopCtrl[0] - High-speed upconnection state - v.xgmiiTxd(8+0) := '0'; + v.xgmiiTxd(8+CXPOF_SOP_CTRL_HKP_BIT_C) := '0'; -- Lane[2] = SopData0[7:0] - reserved - v.xgmiiTxd(16+7 downto 16+0) := x"00"; + v.xgmiiTxd(16+7 downto 16+0) := CXPOF_RESERVED_BYTE_C; -- Lane[3] = SopData1[7:0] - reserved - v.xgmiiTxd(24+7 downto 24+0) := x"00"; + v.xgmiiTxd(24+7 downto 24+0) := CXPOF_RESERVED_BYTE_C; -- Next State v.state := LS_PAYLOAD_S; @@ -151,7 +151,7 @@ begin v.cnt := r.cnt + 1; -- Reset the data and char bus - v.xgmiiTxc := (others => '0'); + v.xgmiiTxc := CXPOF_XGMII_ALL_DATA_C; v.xgmiiTxd := (others => '0'); -- Check for LS Stream @@ -165,9 +165,9 @@ begin -- LS CTRL if (r.txLsDataK = '0') then - v.xgmiiTxd(16*i+7 downto 16*i) := x"01"; -- data + v.xgmiiTxd(16*i+7 downto 16*i) := CXPOF_LS_CTRL_DATA_C; else - v.xgmiiTxd(16*i+7 downto 16*i) := x"02"; -- k-code + v.xgmiiTxd(16*i+7 downto 16*i) := CXPOF_LS_CTRL_K_CODE_C; end if; -- LS Char @@ -178,9 +178,9 @@ begin -- LS CTRL if (CXP_IDLE_K_C(r.idle) = '0') then - v.xgmiiTxd(16*i+7 downto 16*i) := x"01"; -- data + v.xgmiiTxd(16*i+7 downto 16*i) := CXPOF_LS_CTRL_DATA_C; else - v.xgmiiTxd(16*i+7 downto 16*i) := x"02"; -- k-code + v.xgmiiTxd(16*i+7 downto 16*i) := CXPOF_LS_CTRL_K_CODE_C; end if; -- LS Char @@ -195,13 +195,13 @@ begin v.cnt := 0; -- Set the char marker - v.xgmiiTxc := "1100"; + v.xgmiiTxc := CXPOF_XGMII_LANE2_3_CTRL_C; -- Lane[0] = Reserved - v.xgmiiTxd(7 downto 0) := x"00"; + v.xgmiiTxd(7 downto 0) := CXPOF_RESERVED_BYTE_C; -- Lane[1] = Reserved - v.xgmiiTxd(15 downto 8) := x"00"; + v.xgmiiTxd(15 downto 8) := CXPOF_RESERVED_BYTE_C; -- Lane[2] = Terminate v.xgmiiTxd(23 downto 16) := CXPOF_TERM_C; diff --git a/protocols/coaxpress/core/rtl/CoaXPressPkg.vhd b/protocols/coaxpress/core/rtl/CoaXPressPkg.vhd index 901def36d8..1c231244b2 100644 --- a/protocols/coaxpress/core/rtl/CoaXPressPkg.vhd +++ b/protocols/coaxpress/core/rtl/CoaXPressPkg.vhd @@ -34,6 +34,11 @@ package CoaXPressPkg is constant CXP_TRIG_C : slv(31 downto 0) := K_28_2_C & K_28_2_C & K_28_2_C & K_28_2_C; -- 0x5C5C5C5C constant CXP_IO_ACK_C : slv(31 downto 0) := K_28_6_C & K_28_6_C & K_28_6_C & K_28_6_C; -- 0xDCDCDCDC constant CXP_MARKER_C : slv(31 downto 0) := K_28_3_C & K_28_3_C & K_28_3_C & K_28_3_C; -- 0x7C7C7C7C + constant CXP_ALL_DATA_K_C : slv(3 downto 0) := x"0"; + constant CXP_ALL_CTRL_K_C : slv(3 downto 0) := x"F"; + + constant CXP_RX_STREAM_TUSER_BITS_C : positive := 8; + constant CXP_RX_STREAM_TRAILER_TUSER_C : natural := 4; constant CXP_TX_IDLE_C : Slv8Array(3 downto 0) := ( 0 => CXP_IDLE_C(7 downto 0), @@ -57,8 +62,131 @@ package CoaXPressPkg is constant CXPOF_IDLE_WORD_C : slv(31 downto 0) := CXPOF_IDLE_C & CXPOF_IDLE_C & CXPOF_IDLE_C & CXPOF_IDLE_C; + constant CXPOF_XGMII_ALL_DATA_C : slv(3 downto 0) := x"0"; + constant CXPOF_XGMII_ALL_CTRL_C : slv(3 downto 0) := x"F"; + constant CXPOF_XGMII_LANE0_CTRL_C : slv(3 downto 0) := "0001"; + constant CXPOF_XGMII_LANE2_3_CTRL_C : slv(3 downto 0) := "1100"; + + constant CXPOF_RESERVED_BYTE_C : slv(7 downto 0) := x"00"; + + constant CXPOF_SOP_CTRL_PACKET_TYPE_BIT_C : natural := 7; + constant CXPOF_SOP_CTRL_UPDATE_BIT_C : natural := 3; + constant CXPOF_SOP_CTRL_LS_RATE_BIT_C : natural := 1; + constant CXPOF_SOP_CTRL_HKP_BIT_C : natural := 0; + + constant CXPOF_SOP_CTRL_LOW_SPEED_C : sl := '0'; + constant CXPOF_SOP_CTRL_HIGH_SPEED_C : sl := '1'; + + constant CXPOF_SOP_CTRL_HS_PREFIX_C : slv(6 downto 0) := CXPOF_SOP_CTRL_HIGH_SPEED_C & "000000"; + + constant CXPOF_LS_CTRL_DATA_C : slv(7 downto 0) := x"01"; + constant CXPOF_LS_CTRL_K_CODE_C : slv(7 downto 0) := x"02"; + + constant CXPOF_TERM_SUFFIX_C : slv(23 downto 0) := CXPOF_IDLE_C & CXPOF_TERM_C & CXPOF_RESERVED_BYTE_C; + + constant CXPOF_RX_ERR_NONE_C : slv(3 downto 0) := x"0"; + constant CXPOF_RX_ERR_SEQ_MISMATCH_C : slv(3 downto 0) := x"1"; + constant CXPOF_RX_ERR_IDLE_ERROR_C : slv(3 downto 0) := x"2"; + constant CXPOF_RX_ERR_PAYLOAD_ABORT_C : slv(3 downto 0) := x"3"; + constant CXPOF_RX_ERR_BAD_CONTROL_C : slv(3 downto 0) := x"4"; + constant CXPOF_RX_ERR_OVERWRITE_C : slv(3 downto 0) := x"5"; + constant CXPOF_RX_ERR_HKP_MALFORMED_C : slv(3 downto 0) := x"6"; + constant CXPOF_RX_ERR_HKP_BAD_K_CODE_C : slv(3 downto 0) := x"7"; + + constant CXPOF_HKP_TYPE_NONE_C : slv(3 downto 0) := x"0"; + constant CXPOF_HKP_TYPE_K_CODE_C : slv(3 downto 0) := x"1"; + constant CXPOF_HKP_TYPE_SOP_C : slv(3 downto 0) := x"2"; + constant CXPOF_HKP_TYPE_EOP_C : slv(3 downto 0) := x"3"; + constant CXPOF_HKP_TYPE_TRIG_C : slv(3 downto 0) := x"4"; + constant CXPOF_HKP_TYPE_IO_ACK_C : slv(3 downto 0) := x"5"; + constant CXPOF_HKP_TYPE_MARKER_C : slv(3 downto 0) := x"6"; + constant CXPOF_HKP_TYPE_INVALID_C : slv(3 downto 0) := x"F"; + + type CxpofRxStatusType is record + rxError : sl; + rxAbort : sl; + rxErrorCode : slv(3 downto 0); + seqValid : sl; + seqData : slv(23 downto 0); + seqError : sl; + seqExpected : slv(23 downto 0); + seqErrorExpected : slv(23 downto 0); + hkpValid : sl; + hkpData : slv(31 downto 0); + hkpEop : sl; + hkpSof : sl; + hkpError : sl; + hkpWordCount : slv(7 downto 0); + hkpKCodeMask : slv(3 downto 0); + hkpKCodeValid : sl; + hkpType : slv(3 downto 0); + end record CxpofRxStatusType; + + constant CXPOF_RX_STATUS_INIT_C : CxpofRxStatusType := ( + rxError => '0', + rxAbort => '0', + rxErrorCode => CXPOF_RX_ERR_NONE_C, + seqValid => '0', + seqData => (others => '0'), + seqError => '0', + seqExpected => (others => '0'), + seqErrorExpected => (others => '0'), + hkpValid => '0', + hkpData => (others => '0'), + hkpEop => '0', + hkpSof => '0', + hkpError => '0', + hkpWordCount => (others => '0'), + hkpKCodeMask => (others => '0'), + hkpKCodeValid => '0', + hkpType => CXPOF_HKP_TYPE_NONE_C); + + function cxpIsKCode (data : slv(7 downto 0)) return sl; + function cxpKCodeMask (data : slv(31 downto 0)) return slv; + function cxpHkpType (data : slv(31 downto 0)) return slv; + end package CoaXPressPkg; package body CoaXPressPkg is + function cxpIsKCode (data : slv(7 downto 0)) return sl is + begin + case data is + when K_28_0_C | K_28_1_C | K_28_2_C | K_28_3_C | + K_28_4_C | K_28_5_C | K_28_6_C | K_28_7_C | + K_23_7_C | K_27_7_C | K_29_7_C | K_30_7_C => + return '1'; + when others => + return '0'; + end case; + end function cxpIsKCode; + + function cxpKCodeMask (data : slv(31 downto 0)) return slv is + variable ret : slv(3 downto 0); + begin + for i in 0 to 3 loop + ret(i) := cxpIsKCode(data((8*i)+7 downto (8*i))); + end loop; + return ret; + end function cxpKCodeMask; + + function cxpHkpType (data : slv(31 downto 0)) return slv is + begin + if (cxpKCodeMask(data) /= CXP_ALL_CTRL_K_C) then + return CXPOF_HKP_TYPE_INVALID_C; + elsif (data = CXP_SOP_C) then + return CXPOF_HKP_TYPE_SOP_C; + elsif (data = CXP_EOP_C) then + return CXPOF_HKP_TYPE_EOP_C; + elsif (data = CXP_TRIG_C) then + return CXPOF_HKP_TYPE_TRIG_C; + elsif (data = CXP_IO_ACK_C) then + return CXPOF_HKP_TYPE_IO_ACK_C; + elsif (data = CXP_MARKER_C) then + return CXPOF_HKP_TYPE_MARKER_C; + else + return CXPOF_HKP_TYPE_K_CODE_C; + end if; + end function cxpHkpType; + end package body CoaXPressPkg; diff --git a/protocols/coaxpress/core/rtl/CoaXPressRx.vhd b/protocols/coaxpress/core/rtl/CoaXPressRx.vhd index 62371849bd..c0900eebcb 100644 --- a/protocols/coaxpress/core/rtl/CoaXPressRx.vhd +++ b/protocols/coaxpress/core/rtl/CoaXPressRx.vhd @@ -43,6 +43,8 @@ entity CoaXPressRx is cfgClk : in sl; cfgRst : in sl; cfgRxMaster : out AxiStreamMasterType; + eventMaster : out AxiStreamMasterType; + eventSlave : in AxiStreamSlaveType; -- Event ACK Interface (cfgClk domain) eventAck : out sl; eventTag : out slv(7 downto 0); @@ -70,7 +72,7 @@ architecture mapping of CoaXPressRx is TDEST_BITS_C => 0, TID_BITS_C => 0, TKEEP_MODE_C => TKEEP_NORMAL_C, - TUSER_BITS_C => 1, + TUSER_BITS_C => CXP_RX_STREAM_TUSER_BITS_C, TUSER_MODE_C => TUSER_NORMAL_C); constant WIDE_AXIS_CONFIG_C : AxiStreamConfigType := ( @@ -82,9 +84,20 @@ architecture mapping of CoaXPressRx is TUSER_BITS_C => NARROW_AXIS_CONFIG_C.TUSER_BITS_C, TUSER_MODE_C => NARROW_AXIS_CONFIG_C.TUSER_MODE_C); + constant EVENT_AXIS_CONFIG_C : AxiStreamConfigType := ( + TSTRB_EN_C => false, + TDATA_BYTES_C => 4, + TDEST_BITS_C => 8, + TID_BITS_C => 0, + TKEEP_MODE_C => TKEEP_NORMAL_C, + TUSER_BITS_C => 8, + TUSER_MODE_C => TUSER_NORMAL_C); + signal ioAck : slv(NUM_LANES_G-1 downto 0); signal eventAckVec : slv(NUM_LANES_G-1 downto 0); signal eventTagVec : Slv8Array(NUM_LANES_G-1 downto 0); + signal eventMasters : AxiStreamMasterArray(NUM_LANES_G-1 downto 0); + signal rxLaneError : slv(NUM_LANES_G-1 downto 0); signal cfgMasters : AxiStreamMasterArray(NUM_LANES_G-1 downto 0); signal dataMasters : AxiStreamMasterArray(NUM_LANES_G-1 downto 0); @@ -100,18 +113,43 @@ architecture mapping of CoaXPressRx is signal fsmMaster : AxiStreamMasterType; signal hdrMaster : AxiStreamMasterType; signal hdrCtrl : AxiStreamCtrlType; + signal hsFsmError : sl; signal dataIntMaster : AxiStreamMasterType; signal dataIntSlave : AxiStreamSlaveType; signal overflowData : slv(NUM_LANES_G-1 downto 0); + signal rxFsmRstSync : slv(NUM_LANES_G-1 downto 0); + signal rxLaneRst : slv(NUM_LANES_G-1 downto 0); + signal rxPathRst : sl; + signal dataPathRst : sl; begin rxOverflow <= uOr(overflowData) or rxCtrl.overflow or hdrCtrl.overflow; + rxFsmError <= hsFsmError or uOr(rxLaneError); + rxPathRst <= rxRst(0) or rxFsmRst; + + U_DataPathRst : entity surf.RstSync + generic map ( + TPD_G => TPD_G) + port map ( + clk => dataClk, + asyncRst => dataRst or rxFsmRst, + syncRst => dataPathRst); GEN_LANE : for i in NUM_LANES_G-1 downto 0 generate + U_RxFsmRstSync : entity surf.RstSync + generic map ( + TPD_G => TPD_G) + port map ( + clk => rxClk(i), + asyncRst => rxFsmRst, + syncRst => rxFsmRstSync(i)); + + rxLaneRst(i) <= rxRst(i) or rxFsmRstSync(i); + U_Lane : entity surf.CoaXPressRxLane generic map ( TPD_G => TPD_G) @@ -123,10 +161,13 @@ begin cfgMaster => cfgMasters(i), -- Data Interface dataMaster => dataMasters(i), + -- Event payload Interface + eventMaster => eventMasters(i), -- ACK Interface ioAck => ioAck(i), eventAck => eventAckVec(i), eventTag => eventTagVec(i), + rxError => rxLaneError(i), -- RX PHY Interface rxData => rxData(i), rxDataK => rxDataK(i), @@ -146,7 +187,7 @@ begin port map ( -- Slave Port sAxisClk => rxClk(i), - sAxisRst => rxRst(i), + sAxisRst => rxLaneRst(i), sAxisMaster => dataMasters(i), sAxisCtrl => dataCtrls(i), -- Master Port @@ -188,7 +229,7 @@ begin rxRst => rxRst(0), -- Config/Status Interface rxFsmRst => rxFsmRst, - rxFsmError => rxFsmError, + rxFsmError => hsFsmError, -- Inbound Stream Interface rxMaster => rxMaster, rxSlave => rxSlave, @@ -210,14 +251,14 @@ begin SLAVE_AXI_CONFIG_G => ssiAxiStreamConfig(dataBytes => (224/8), tDestBits => 0), MASTER_AXI_CONFIG_G => AXIS_CONFIG_G) port map ( - -- Slave Port + -- Slave Port sAxisClk => rxClk(0), - sAxisRst => rxRst(0), + sAxisRst => rxPathRst, sAxisMaster => hdrMaster, sAxisCtrl => hdrCtrl, - -- Master Port + -- Master Port mAxisClk => dataClk, - mAxisRst => dataRst, + mAxisRst => dataPathRst, mAxisMaster => imageHdrMaster, mAxisSlave => imageHdrSlave); @@ -227,17 +268,17 @@ begin SLAVE_READY_EN_G => false, GEN_SYNC_FIFO_G => false, FIFO_ADDR_WIDTH_G => 9, - SLAVE_AXI_CONFIG_G => ssiAxiStreamConfig(dataBytes => (4*NUM_LANES_G), tDestBits => 0), + SLAVE_AXI_CONFIG_G => WIDE_AXIS_CONFIG_C, MASTER_AXI_CONFIG_G => AXIS_CONFIG_G) port map ( - -- INbound Interface + -- INbound Interface sAxisClk => rxClk(0), - sAxisRst => rxRst(0), + sAxisRst => rxPathRst, sAxisMaster => fsmMaster, sAxisCtrl => rxCtrl, - -- Outbound Interface + -- Outbound Interface mAxisClk => dataClk, - mAxisRst => dataRst, + mAxisRst => dataPathRst, mAxisMaster => dataIntMaster, mAxisSlave => dataIntSlave); @@ -245,6 +286,7 @@ begin generic map ( -- General Configurations TPD_G => TPD_G, + TUSER_MASK_G => (others => '0'), -- FIFO configurations COMMON_CLK_G => true, SLAVE_FIFO_G => false, @@ -253,14 +295,14 @@ begin SLAVE_AXI_CONFIG_G => AXIS_CONFIG_G, MASTER_AXI_CONFIG_G => AXIS_CONFIG_G) port map ( - -- Slave Port + -- Slave Port sAxisClk => dataClk, - sAxisRst => dataRst, + sAxisRst => dataPathRst, sAxisMaster => dataIntMaster, sAxisSlave => dataIntSlave, - -- Master Port + -- Master Port mAxisClk => dataClk, - mAxisRst => dataRst, + mAxisRst => dataPathRst, mAxisMaster => dataMaster, mAxisSlave => dataSlave); @@ -311,4 +353,27 @@ begin valid => eventAck, dout => eventTag); + U_EventPayload : entity surf.AxiStreamFifoV2 + generic map ( + -- General Configurations + TPD_G => TPD_G, + SLAVE_READY_EN_G => false, + -- FIFO configurations + MEMORY_TYPE_G => "distributed", + GEN_SYNC_FIFO_G => false, + FIFO_ADDR_WIDTH_G => 4, + -- AXI Stream Port Configurations + SLAVE_AXI_CONFIG_G => EVENT_AXIS_CONFIG_C, + MASTER_AXI_CONFIG_G => EVENT_AXIS_CONFIG_C) + port map ( + -- Slave Port + sAxisClk => rxClk(0), + sAxisRst => rxRst(0), + sAxisMaster => eventMasters(0), + -- Master Port + mAxisClk => cfgClk, + mAxisRst => cfgRst, + mAxisMaster => eventMaster, + mAxisSlave => eventSlave); + end mapping; diff --git a/protocols/coaxpress/core/rtl/CoaXPressRxHsFsm.vhd b/protocols/coaxpress/core/rtl/CoaXPressRxHsFsm.vhd index d65e729954..30cf1846d0 100644 --- a/protocols/coaxpress/core/rtl/CoaXPressRxHsFsm.vhd +++ b/protocols/coaxpress/core/rtl/CoaXPressRxHsFsm.vhd @@ -93,9 +93,24 @@ architecture rtl of CoaXPressRxHsFsm is subtype LineRemType is natural range 0 to NUM_LANES_G+1; + constant WIDE_AXIS_CONFIG_C : AxiStreamConfigType := ssiAxiStreamConfig( + dataBytes => (4*NUM_LANES_G), + tKeepMode => TKEEP_NORMAL_C, + tUserMode => TUSER_NORMAL_C, + tDestBits => 0, + tUserBits => CXP_RX_STREAM_TUSER_BITS_C); + type RegType is record endOfLine : sl; + frameEofe : sl; hdrValid : sl; + pendingValid : sl; + parseBeat : sl; + trailerMarker : sl; + trailerEofe : sl; + trailerSeen : sl; + waitTrailer : sl; + waitFrameTrailer : sl; yCnt : slv(RX_FSM_CNT_WIDTH_G-1 downto 0); dCnt : slv(RX_FSM_CNT_WIDTH_G-1 downto 0); lineRem : LineRemType; @@ -105,12 +120,23 @@ architecture rtl of CoaXPressRxHsFsm is wrd : natural range 0 to NUM_LANES_G-1; hdrMaster : AxiStreamMasterType; rxSlave : AxiStreamSlaveType; + packOut : AxiStreamSlaveType; dataMasters : AxiStreamMasterArray(1 downto 0); + dataMaster : AxiStreamMasterType; + pendingMaster : AxiStreamMasterType; state : StateType; end record RegType; constant REG_INIT_C : RegType := ( endOfLine => '0', + frameEofe => '0', hdrValid => '0', + pendingValid => '0', + parseBeat => '0', + trailerMarker => '0', + trailerEofe => '0', + trailerSeen => '0', + waitTrailer => '0', + waitFrameTrailer => '0', yCnt => (others => '0'), dCnt => (others => '0'), lineRem => 0, @@ -120,7 +146,10 @@ architecture rtl of CoaXPressRxHsFsm is wrd => 0, hdrMaster => AXI_STREAM_MASTER_INIT_C, rxSlave => AXI_STREAM_SLAVE_FORCE_C, + packOut => AXI_STREAM_SLAVE_FORCE_C, dataMasters => (others => AXI_STREAM_MASTER_INIT_C), + dataMaster => AXI_STREAM_MASTER_INIT_C, + pendingMaster => AXI_STREAM_MASTER_INIT_C, state => IDLE_S); signal r : RegType := REG_INIT_C; @@ -129,6 +158,11 @@ architecture rtl of CoaXPressRxHsFsm is signal pipeMaster : AxiStreamMasterType; signal pipeSlave : AxiStreamSlaveType; + signal packRawMaster : AxiStreamMasterType; + signal packRawSlave : AxiStreamSlaveType; + signal packMaster : AxiStreamMasterType; + signal packInSlave : AxiStreamSlaveType; + signal packOutSlave : AxiStreamSlaveType; -- attribute dont_touch : string; -- attribute dont_touch of r : signal is "TRUE"; @@ -137,7 +171,9 @@ begin packRst <= rxRst or rxFsmRst; - comb : process (pipeMaster, r, rxFsmRst, rxRst) is + packOutSlave <= rin.packOut; + + comb : process (packInSlave, packMaster, pipeMaster, r, rxFsmRst, rxRst) is variable v : RegType; variable tData : slv(31 downto 0); variable eolBeat : sl; @@ -149,7 +185,13 @@ begin v := r; -- Init Variable - tData := pipeMaster.tData(32*r.wrd+31 downto 32*r.wrd); + tData := pipeMaster.tData(32*r.wrd+31 downto 32*r.wrd); + v.trailerMarker := pipeMaster.tValid and pipeMaster.tLast and ( + pipeMaster.tUser(CXP_RX_STREAM_TRAILER_TUSER_C) or + axiStreamGetUserBit(WIDE_AXIS_CONFIG_C, pipeMaster, CXP_RX_STREAM_TRAILER_TUSER_C)); + v.trailerEofe := ssiGetUserEofe(WIDE_AXIS_CONFIG_C, pipeMaster) or pipeMaster.tUser(SSI_EOFE_C); + v.waitTrailer := r.pendingValid or (r.waitFrameTrailer and not v.trailerMarker and not r.trailerSeen) or + (packMaster.tValid and packMaster.tLast and not v.trailerMarker and not r.trailerSeen); -- Reset strobes v.dbg.errDet := '0'; @@ -169,18 +211,40 @@ begin v.hdrMaster.tUser(SSI_SOF_C) := '1'; -- single word write -- Init data stream - v.dataMasters(0).tValid := '0'; -- Reset strobe - v.dataMasters(0).tData := pipeMaster.tData; - -- Check if state is not STEP_S - if (r.state /= STEP_S) then - v.dataMasters(0).tKeep := (others => '0'); -- Reset bus + if (packInSlave.tReady = '1') then + v.dataMasters(0).tValid := '0'; -- Reset strobe + v.dataMasters(0).tData := pipeMaster.tData; + -- Check if state is not STEP_S + if (r.state /= STEP_S) then + v.dataMasters(0).tKeep := (others => '0'); -- Reset bus + end if; + else + v.dataMasters(0) := r.dataMasters(0); end if; -- Flow Control v.rxSlave.tReady := '0'; + v.packOut := AXI_STREAM_SLAVE_FORCE_C; + if (v.waitTrailer = '1') then + v.packOut.tReady := '0'; + end if; + v.parseBeat := pipeMaster.tValid and not v.trailerMarker and not v.waitTrailer and packInSlave.tReady; -- Check for valid data - if (pipeMaster.tValid = '1') then + if (pipeMaster.tValid = '1') and (v.trailerMarker = '1') then + -- Consume the lane's trailer verdict beat. Payload has + -- already streamed through; this only annotates the eventual SSI EOF. + v.rxSlave.tReady := '1'; + if (v.trailerEofe = '1') then + v.frameEofe := '1'; + end if; + if (r.waitFrameTrailer = '1') or (r.pendingValid = '1') or (r.endOfLine = '1') or + (r.dataMasters(0).tValid = '1') or (r.dataMasters(1).tValid = '1') or + ((packMaster.tValid = '1') and (packMaster.tLast = '1')) then + v.trailerSeen := '1'; + end if; + + elsif (v.parseBeat = '1') then -- State Machine case r.state is @@ -200,6 +264,10 @@ begin end if; ---------------------------------------------------------------------- when TYPE_S => + if (r.waitFrameTrailer = '0') and (r.pendingValid = '0') then + v.trailerSeen := '0'; + end if; + -- Check for "Rectangular image header indication" if (tData = x"01_01_01_01") then @@ -405,11 +473,17 @@ begin end if; - -- Shift the pipeline and convert tKEEP to count (helps with making timing in byte packer) - v.dataMasters(1) := r.dataMasters(0); + -- Shift the parser output into the word packer only when the packer can + -- accept it. This keeps the final packed EOF beat stable while the FSM + -- waits for the lane trailer verdict. + if (packInSlave.tReady = '1') then + v.dataMasters(1) := r.dataMasters(0); + else + v.dataMasters(1) := r.dataMasters(1); + end if; -- Check for end of line in the previous cycle - if (r.endOfLine = '1') then + if (packInSlave.tReady = '1') and (r.endOfLine = '1') then -- Reset flag v.endOfLine := '0'; @@ -421,6 +495,7 @@ begin if (v.yCnt = r.hdr.ySize(RX_FSM_CNT_WIDTH_G-1 downto 0)) then -- Terminate the frame v.dataMasters(1).tLast := '1'; + v.waitFrameTrailer := '1'; end if; end if; @@ -441,44 +516,46 @@ begin end if; -- Update header based on counter - case r.hdrCnt is - ---------------------------------------------------------------- - when 3 => v.hdr.steamId(7 downto 0) := tData(7 downto 0); - ---------------------------------------------------------------- - when 4 => v.hdr.sourceTag(15 downto 8) := tData(7 downto 0); - when 5 => v.hdr.sourceTag(7 downto 0) := tData(7 downto 0); - ---------------------------------------------------------------- - when 6 => v.hdr.xSize(23 downto 16) := tData(7 downto 0); - when 7 => v.hdr.xSize(15 downto 8) := tData(7 downto 0); - when 8 => v.hdr.xSize(7 downto 0) := tData(7 downto 0); - ---------------------------------------------------------------- - when 9 => v.hdr.xOffs(23 downto 16) := tData(7 downto 0); - when 10 => v.hdr.xOffs(15 downto 8) := tData(7 downto 0); - when 11 => v.hdr.xOffs(7 downto 0) := tData(7 downto 0); - ---------------------------------------------------------------- - when 12 => v.hdr.ySize(23 downto 16) := tData(7 downto 0); - when 13 => v.hdr.ySize(15 downto 8) := tData(7 downto 0); - when 14 => v.hdr.ySize(7 downto 0) := tData(7 downto 0); - ---------------------------------------------------------------- - when 15 => v.hdr.yOffs(23 downto 16) := tData(7 downto 0); - when 16 => v.hdr.yOffs(15 downto 8) := tData(7 downto 0); - when 17 => v.hdr.yOffs(7 downto 0) := tData(7 downto 0); - ---------------------------------------------------------------- - when 18 => v.hdr.dsizeL(23 downto 16) := tData(7 downto 0); - when 19 => v.hdr.dsizeL(15 downto 8) := tData(7 downto 0); - when 20 => v.hdr.dsizeL(7 downto 0) := tData(7 downto 0); - ---------------------------------------------------------------- - when 21 => v.hdr.pixelF(15 downto 8) := tData(7 downto 0); - when 22 => v.hdr.pixelF(7 downto 0) := tData(7 downto 0); - ---------------------------------------------------------------- - when 23 => v.hdr.tapG(15 downto 8) := tData(7 downto 0); - when 24 => v.hdr.tapG(7 downto 0) := tData(7 downto 0); - ---------------------------------------------------------------- - when 25 => v.hdr.flags(7 downto 0) := tData(7 downto 0); - ---------------------------------------------------------------- - when others => - null; - end case; + if (v.parseBeat = '1') then + case r.hdrCnt is + ---------------------------------------------------------------- + when 3 => v.hdr.steamId(7 downto 0) := tData(7 downto 0); + ---------------------------------------------------------------- + when 4 => v.hdr.sourceTag(15 downto 8) := tData(7 downto 0); + when 5 => v.hdr.sourceTag(7 downto 0) := tData(7 downto 0); + ---------------------------------------------------------------- + when 6 => v.hdr.xSize(23 downto 16) := tData(7 downto 0); + when 7 => v.hdr.xSize(15 downto 8) := tData(7 downto 0); + when 8 => v.hdr.xSize(7 downto 0) := tData(7 downto 0); + ---------------------------------------------------------------- + when 9 => v.hdr.xOffs(23 downto 16) := tData(7 downto 0); + when 10 => v.hdr.xOffs(15 downto 8) := tData(7 downto 0); + when 11 => v.hdr.xOffs(7 downto 0) := tData(7 downto 0); + ---------------------------------------------------------------- + when 12 => v.hdr.ySize(23 downto 16) := tData(7 downto 0); + when 13 => v.hdr.ySize(15 downto 8) := tData(7 downto 0); + when 14 => v.hdr.ySize(7 downto 0) := tData(7 downto 0); + ---------------------------------------------------------------- + when 15 => v.hdr.yOffs(23 downto 16) := tData(7 downto 0); + when 16 => v.hdr.yOffs(15 downto 8) := tData(7 downto 0); + when 17 => v.hdr.yOffs(7 downto 0) := tData(7 downto 0); + ---------------------------------------------------------------- + when 18 => v.hdr.dsizeL(23 downto 16) := tData(7 downto 0); + when 19 => v.hdr.dsizeL(15 downto 8) := tData(7 downto 0); + when 20 => v.hdr.dsizeL(7 downto 0) := tData(7 downto 0); + ---------------------------------------------------------------- + when 21 => v.hdr.pixelF(15 downto 8) := tData(7 downto 0); + when 22 => v.hdr.pixelF(7 downto 0) := tData(7 downto 0); + ---------------------------------------------------------------- + when 23 => v.hdr.tapG(15 downto 8) := tData(7 downto 0); + when 24 => v.hdr.tapG(7 downto 0) := tData(7 downto 0); + ---------------------------------------------------------------- + when 25 => v.hdr.flags(7 downto 0) := tData(7 downto 0); + ---------------------------------------------------------------- + when others => + null; + end case; + end if; -- Register a small saturated remaining-word count. This breaks the wide -- dCnt-to-wrd path when a line ends mid-beat and the next marker is held @@ -523,10 +600,42 @@ begin v.hdrMaster.tData(207 downto 192) := r.hdr.pixelF(15 downto 0); v.hdrMaster.tData(223 downto 208) := r.hdr.tapG(15 downto 0); + -- Hold only the packed SSI EOF beat until the lane trailer verdict + -- arrives. Payload still streams as soon as it is parsed. + v.dataMaster.tValid := '0'; + + if (r.pendingValid = '1') then + v.packOut.tReady := '0'; + if (v.trailerMarker = '1') or (r.trailerSeen = '1') then + v.dataMaster := r.pendingMaster; + ssiSetUserEofe(WIDE_AXIS_CONFIG_C, v.dataMaster, v.frameEofe or v.trailerEofe); + v.dataMaster.tUser(SSI_EOFE_C) := v.frameEofe or v.trailerEofe; + v.pendingMaster := AXI_STREAM_MASTER_INIT_C; + v.pendingValid := '0'; + v.frameEofe := '0'; + v.trailerSeen := '0'; + v.waitFrameTrailer := '0'; + end if; + elsif (packMaster.tValid = '1') and (packMaster.tLast = '1') and + (v.trailerMarker = '0') and (r.trailerSeen = '0') then + v.pendingMaster := packMaster; + v.pendingValid := '1'; + elsif (packMaster.tValid = '1') and (packMaster.tLast = '1') then + v.dataMaster := packMaster; + ssiSetUserEofe(WIDE_AXIS_CONFIG_C, v.dataMaster, v.frameEofe or v.trailerEofe); + v.dataMaster.tUser(SSI_EOFE_C) := v.frameEofe or v.trailerEofe; + v.frameEofe := '0'; + v.trailerSeen := '0'; + v.waitFrameTrailer := '0'; + elsif (packMaster.tValid = '1') then + v.dataMaster := packMaster; + end if; + -- Outputs - pipeSlave <= v.rxSlave; - hdrMaster <= r.hdrMaster; - rxFsmError <= r.dbg.errDet; + pipeSlave <= v.rxSlave; + hdrMaster <= r.hdrMaster; + dataMaster <= r.dataMaster; + rxFsmError <= r.dbg.errDet; -- Reset if (rxRst = '1') or (rxFsmRst = '1') then @@ -565,6 +674,20 @@ begin rxClk => rxClk, rxRst => packRst, sAxisMaster => r.dataMasters(1), - mAxisMaster => dataMaster); + sAxisSlave => packInSlave, + mAxisMaster => packRawMaster, + mAxisSlave => packRawSlave); + + U_PackPipe : entity surf.AxiStreamPipeline + generic map ( + TPD_G => TPD_G, + PIPE_STAGES_G => 1) + port map ( + axisClk => rxClk, + axisRst => packRst, + sAxisMaster => packRawMaster, + sAxisSlave => packRawSlave, + mAxisMaster => packMaster, + mAxisSlave => packOutSlave); end rtl; diff --git a/protocols/coaxpress/core/rtl/CoaXPressRxLane.vhd b/protocols/coaxpress/core/rtl/CoaXPressRxLane.vhd index 3e873bf008..f8fb0edf14 100644 --- a/protocols/coaxpress/core/rtl/CoaXPressRxLane.vhd +++ b/protocols/coaxpress/core/rtl/CoaXPressRxLane.vhd @@ -40,12 +40,15 @@ entity CoaXPressRxLane is dataMaster : out AxiStreamMasterType; -- Heartbeat Interface heatbeatMaster : out AxiStreamMasterType; + -- Event payload Interface + eventMaster : out AxiStreamMasterType; -- Image header Interface imageHdrMaster : out AxiStreamMasterType; -- ACK Interface ioAck : out sl; eventAck : out sl; eventTag : out slv(7 downto 0); + rxError : out sl; -- RX PHY Interface rxData : in slv(31 downto 0); rxDataK : in slv(3 downto 0); @@ -54,13 +57,26 @@ end entity CoaXPressRxLane; architecture rtl of CoaXPressRxLane is + constant EVENT_BUFFER_DEPTH_C : positive := 16; + constant EVENT_BUFFER_ADDR_WIDTH_C : positive := 4; + constant STREAM_AXIS_CONFIG_C : AxiStreamConfigType := ssiAxiStreamConfig( + dataBytes => 4, + tKeepMode => TKEEP_NORMAL_C, + tUserMode => TUSER_NORMAL_C, + tDestBits => 0, + tUserBits => CXP_RX_STREAM_TUSER_BITS_C); + type StateType is ( IO_ACK_S, IDLE_S, TYPE_S, CTRL_ACK_TAG_S, CTRL_ACK_S, + CTRL_ACK_CRC_S, + CTRL_ACK_EOP_S, HEARTBEAT_S, + HEARTBEAT_CRC_S, + HEARTBEAT_EOP_S, EVENT_ACK_S, EVENT_DSIZE_UPPER_S, EVENT_DSIZE_LOWER_S, @@ -71,7 +87,9 @@ architecture rtl of CoaXPressRxLane is PACKET_TAG_S, DSIZE_UPPER_S, DSIZE_LOWER_S, - STREAM_DATA_S); + STREAM_DATA_S, + STREAM_CRC_S, + STREAM_EOP_S); type RegType is record errDet : sl; @@ -80,6 +98,13 @@ architecture rtl of CoaXPressRxLane is eventAck : sl; eventTag : slv(7 downto 0); ackCnt : natural range 0 to 15; + eventId : slv(31 downto 0); + eventPending : sl; + eventReleaseCnt : natural range 0 to EVENT_BUFFER_DEPTH_C-1; + eventRamWrEn : sl; + eventRamWrAddr : slv(EVENT_BUFFER_ADDR_WIDTH_C-1 downto 0); + eventRamWrData : slv(31 downto 0); + eventReadAddr : slv(EVENT_BUFFER_ADDR_WIDTH_C-1 downto 0); -- Stream data payload streamID : slv(7 downto 0); packetTag : slv(7 downto 0); @@ -91,6 +116,7 @@ architecture rtl of CoaXPressRxLane is cfgMaster : AxiStreamMasterType; dataMaster : AxiStreamMasterType; heatbeatMaster : AxiStreamMasterType; + eventMaster : AxiStreamMasterType; -- State Types saved : StateType; state : StateType; @@ -102,6 +128,13 @@ architecture rtl of CoaXPressRxLane is eventAck => '0', eventTag => (others => '0'), ackCnt => 0, + eventId => (others => '0'), + eventPending => '0', + eventReleaseCnt => 0, + eventRamWrEn => '0', + eventRamWrAddr => (others => '0'), + eventRamWrData => (others => '0'), + eventReadAddr => (others => '0'), -- Stream data payload streamID => (others => '0'), packetTag => (others => '0'), @@ -113,6 +146,7 @@ architecture rtl of CoaXPressRxLane is cfgMaster => AXI_STREAM_MASTER_INIT_C, dataMaster => AXI_STREAM_MASTER_INIT_C, heatbeatMaster => AXI_STREAM_MASTER_INIT_C, + eventMaster => AXI_STREAM_MASTER_INIT_C, -- State Types saved => IDLE_S, state => IDLE_S); @@ -120,6 +154,8 @@ architecture rtl of CoaXPressRxLane is signal r : RegType := REG_INIT_C; signal rin : RegType; + signal eventRamRdData : slv(31 downto 0); + function cxpCrcUpdate ( crcIn : slv(31 downto 0); data : slv(31 downto 0)) @@ -148,12 +184,23 @@ architecture rtl of CoaXPressRxLane is return endianSwap(retVar); end function cxpCrcFinal; + function cxpAckIsSuccess ( + data : slv(31 downto 0)) + return boolean is + begin + return ( + (data = x"01_01_01_01") or + (data = x"04_04_04_04") or + (data = x"00_00_00_01") or + (data = x"00_00_00_04")); + end function cxpAckIsSuccess; + -- attribute dont_touch : string; -- attribute dont_touch of r : signal is "TRUE"; begin - comb : process (r, rxData, rxDataK, rxLinkUp, rxRst) is + comb : process (eventRamRdData, r, rxData, rxDataK, rxLinkUp, rxRst) is variable v : RegType; begin -- Latch the current value @@ -166,7 +213,34 @@ begin v.cfgMaster.tValid := '0'; v.dataMaster.tValid := '0'; v.dataMaster.tLast := '0'; + v.dataMaster.tKeep := (others => '0'); + v.dataMaster.tUser := (others => '0'); v.heatbeatMaster.tValid := '0'; + v.eventMaster.tValid := '0'; + v.eventMaster.tLast := '0'; + v.eventRamWrEn := '0'; + + -- Release event payload only after the packet CRC and EOP have passed. + if (r.eventPending = '1') then + v.eventMaster.tValid := '1'; + v.eventMaster.tData(31 downto 0) := eventRamRdData; + v.eventMaster.tKeep(3 downto 0) := x"F"; + v.eventMaster.tDest(7 downto 0) := r.eventTag; + v.eventMaster.tUser(31 downto 0) := r.eventId; + if (r.eventReleaseCnt = (conv_integer(r.dsize)-1)) then + v.eventMaster.tLast := '1'; + v.eventPending := '0'; + v.eventReleaseCnt := 0; + v.eventReadAddr := (others => '0'); + else + v.eventReleaseCnt := r.eventReleaseCnt + 1; + if (r.eventReleaseCnt < (conv_integer(r.dsize)-2)) then + v.eventReadAddr := r.eventReadAddr + 1; + else + v.eventReadAddr := (others => '0'); + end if; + end if; + end if; -- Check for I/O if (rxDataK = x"F") and (rxData = CXP_IO_ACK_C) then @@ -210,31 +284,54 @@ begin -- Check for "Stream data packet" if (rxData = x"01_01_01_01") then + -- Reset stream packet parser + v.dcnt := (others => '0'); + v.dsize := (others => '0'); + v.crc := x"FFFFFFFF"; -- Next State v.state := STREAM_ID_S; -- Check for "control acknowledge with no tag" elsif (rxData = x"03_03_03_03") then + -- Reset control acknowledgment parser + v.ackCnt := 0; + v.crc := x"FFFFFFFF"; -- Next State v.state := CTRL_ACK_S; -- Check for "control acknowledge with tag" elsif (rxData = x"06_06_06_06") then + -- Reset control acknowledgment parser + v.ackCnt := 0; + v.crc := x"FFFFFFFF"; -- Next State v.state := CTRL_ACK_TAG_S; -- Check for "Event packet" elsif (rxData = x"07_07_07_07") then - -- Reset event parser counters - v.ackCnt := 0; - v.dcnt := (others => '0'); - v.dsize := (others => '0'); - v.crc := x"FFFFFFFF"; - -- Next State - v.state := EVENT_ACK_S; + -- Check for pending event payload release + if (r.eventPending = '0') then + -- Reset event parser counters + v.ackCnt := 0; + v.dcnt := (others => '0'); + v.dsize := (others => '0'); + v.eventReleaseCnt := 0; + v.eventReadAddr := (others => '0'); + v.crc := x"FFFFFFFF"; + -- Next State + v.state := EVENT_ACK_S; + else + -- Set the flag + v.errDet := '1'; + -- Next State + v.state := IDLE_S; + end if; -- Check for "Heartbeat Payload" elsif (rxData = x"09_09_09_09") then + -- Reset heartbeat parser + v.ackCnt := 0; + v.crc := x"FFFFFFFF"; -- Next State v.state := HEARTBEAT_S; @@ -253,8 +350,13 @@ begin end if; ---------------------------------------------------------------------- when CTRL_ACK_TAG_S => - -- Check for non-k word - if (rxDataK = x"0") then + -- Check for repeated-byte tag word + if (rxDataK = x"0") + and (rxData(7 downto 0) = rxData(15 downto 8)) + and (rxData(7 downto 0) = rxData(23 downto 16)) + and (rxData(7 downto 0) = rxData(31 downto 24)) then + -- Include packet tag word in the CRC + v.crc := cxpCrcUpdate(r.crc, rxData); -- Next State v.state := CTRL_ACK_S; else @@ -267,41 +369,88 @@ begin when CTRL_ACK_S => -- Check for non-k word if (rxDataK = x"0") then - -- Increment the counter v.ackCnt := r.ackCnt + 1; -- "Acknowledgment code" index if (r.ackCnt = 0) then + -- Include acknowledgment code in the CRC + v.crc := cxpCrcUpdate(r.crc, rxData); -- Save the response code v.cfgMaster.tData(31 downto 0) := rxData; -- Check for Success ACK - if (rxData = x"01_01_01_01") or (rxData = x"04_04_04_04") then + if (cxpAckIsSuccess(rxData)) then -- Always send ZERO for successful ACK v.cfgMaster.tData(31 downto 0) := (others => '0'); end if; + -- "Size field" index: check if data follows + elsif (r.ackCnt = 1) then + + -- Some cameras return write ACK as code+CRC+EOP, + -- without an explicit zero-size word. + if (rxData = cxpCrcFinal(r.crc)) then + -- Next State + v.state := CTRL_ACK_EOP_S; + + -- If DSize=0 (write ACK, no data word follows), skip to CRC + elsif (rxData(31 downto 8) = 0) then + -- Include size word in the CRC + v.crc := cxpCrcUpdate(r.crc, rxData); + -- Next State + v.state := CTRL_ACK_CRC_S; + + else + -- Include size word in the CRC + v.crc := cxpCrcUpdate(r.crc, rxData); + end if; + -- "Data field" index elsif (r.ackCnt = 2) then + -- Include data word in the CRC + v.crc := cxpCrcUpdate(r.crc, rxData); -- Save the data field v.cfgMaster.tData(63 downto 32) := rxData; - -- Forward the response - v.cfgMaster.tValid := '1'; - -- Next State - v.state := IDLE_S; + v.state := CTRL_ACK_CRC_S; end if; else + -- Set the flag + v.errDet := '1'; + -- Next State + v.state := IDLE_S; + end if; + ---------------------------------------------------------------------- + when CTRL_ACK_CRC_S => + -- Check for CRC word + if (rxDataK = x"0") and (rxData = cxpCrcFinal(r.crc)) then + -- Next State + v.state := CTRL_ACK_EOP_S; + else + -- Set the flag + v.errDet := '1'; + -- Next State + v.state := IDLE_S; + end if; + ---------------------------------------------------------------------- + when CTRL_ACK_EOP_S => + -- Check for end of packet indication + if (rxDataK = x"F") and (rxData = CXP_EOP_C) then -- Forward the response v.cfgMaster.tValid := '1'; -- Next State v.state := IDLE_S; + else + -- Set the flag + v.errDet := '1'; + -- Next State + v.state := IDLE_S; end if; ---------------------------------------------------------------------- when EVENT_ACK_S => @@ -319,7 +468,13 @@ begin v.ackCnt := r.ackCnt + 1; -- Packet Tag index - if (r.ackCnt = 4) then + if (r.ackCnt < 4) then + + -- Save the event identifier bytes for payload sideband use. + v.eventId(8*r.ackCnt+7 downto 8*r.ackCnt) := rxData(7 downto 0); + + -- Packet Tag index + elsif (r.ackCnt = 4) then -- Save the packet tag v.eventTag := rxData(7 downto 0); @@ -367,8 +522,11 @@ begin -- Next State if (r.dsize(15 downto 8) = 0) and (rxData(7 downto 0) = 0) then v.state := EVENT_CRC_S; - else + elsif (r.dsize(15 downto 8) = 0) and (conv_integer(rxData(7 downto 0)) <= EVENT_BUFFER_DEPTH_C) then v.state := EVENT_PAYLOAD_S; + else + v.errDet := '1'; + v.state := IDLE_S; end if; else -- Set the flag @@ -380,7 +538,10 @@ begin when EVENT_PAYLOAD_S => -- Check for event payload word if (rxDataK = x"0") then - v.crc := cxpCrcUpdate(r.crc, rxData); + v.eventRamWrEn := '1'; + v.eventRamWrAddr := r.dcnt(EVENT_BUFFER_ADDR_WIDTH_C-1 downto 0); + v.eventRamWrData := rxData; + v.crc := cxpCrcUpdate(r.crc, rxData); -- Check the counter if (r.dcnt = (r.dsize-1)) then -- Next State @@ -415,6 +576,15 @@ begin v.eventAck := '1'; -- Next State v.state := IDLE_S; + if (r.dsize /= 0) then + v.eventPending := '1'; + v.eventReleaseCnt := 0; + if (r.dsize = 1) then + v.eventReadAddr := (others => '0'); + else + v.eventReadAddr := toSlv(1, EVENT_BUFFER_ADDR_WIDTH_C); + end if; + end if; else -- Set the flag v.errDet := '1'; @@ -423,8 +593,14 @@ begin end if; ---------------------------------------------------------------------- when HEARTBEAT_S => - -- Check for non-k word - if (rxDataK = x"0") then + -- Check for repeated-byte heartbeat payload word + if (rxDataK = x"0") + and (rxData(7 downto 0) = rxData(15 downto 8)) + and (rxData(7 downto 0) = rxData(23 downto 16)) + and (rxData(7 downto 0) = rxData(31 downto 24)) then + + -- Include heartbeat payload word in the CRC + v.crc := cxpCrcUpdate(r.crc, rxData); -- Increment the counter v.ackCnt := r.ackCnt + 1; @@ -435,12 +611,8 @@ begin -- "Acknowledgment code" index if (r.ackCnt = 11) then - -- Forward the response - v.heatbeatMaster.tValid := '1'; - v.heatbeatMaster.tLast := '1'; - -- Next State - v.state := IDLE_S; + v.state := HEARTBEAT_CRC_S; end if; @@ -451,6 +623,33 @@ begin v.state := IDLE_S; end if; ---------------------------------------------------------------------- + when HEARTBEAT_CRC_S => + -- Check for CRC word + if (rxDataK = x"0") and (rxData = cxpCrcFinal(r.crc)) then + -- Next State + v.state := HEARTBEAT_EOP_S; + else + -- Set the flag + v.errDet := '1'; + -- Next State + v.state := IDLE_S; + end if; + ---------------------------------------------------------------------- + when HEARTBEAT_EOP_S => + -- Check for end of packet indication + if (rxDataK = x"F") and (rxData = CXP_EOP_C) then + -- Forward the response + v.heatbeatMaster.tValid := '1'; + v.heatbeatMaster.tLast := '1'; + -- Next State + v.state := IDLE_S; + else + -- Set the flag + v.errDet := '1'; + -- Next State + v.state := IDLE_S; + end if; + ---------------------------------------------------------------------- when STREAM_ID_S => -- Check for non-k word if (rxDataK = x"0") @@ -459,6 +658,7 @@ begin and (rxData(7 downto 0) = rxData(31 downto 24)) then -- Save the value v.streamID := rxData(7 downto 0); + v.crc := cxpCrcUpdate(r.crc, rxData); -- Next State v.state := PACKET_TAG_S; else @@ -476,6 +676,7 @@ begin and (rxData(7 downto 0) = rxData(31 downto 24)) then -- Save the value v.packetTag := rxData(7 downto 0); + v.crc := cxpCrcUpdate(r.crc, rxData); -- Next State v.state := DSIZE_UPPER_S; else @@ -493,6 +694,7 @@ begin and (rxData(7 downto 0) = rxData(31 downto 24)) then -- Set the TDEST to the packet tag v.dsize(15 downto 8) := rxData(7 downto 0); + v.crc := cxpCrcUpdate(r.crc, rxData); -- Next State v.state := DSIZE_LOWER_S; else @@ -510,8 +712,14 @@ begin and (rxData(7 downto 0) = rxData(31 downto 24)) then -- Set the TDEST to the packet tag v.dsize(7 downto 0) := rxData(7 downto 0); + v.dcnt := (others => '0'); + v.crc := cxpCrcUpdate(r.crc, rxData); -- Next State - v.state := STREAM_DATA_S; + if (r.dsize(15 downto 8) = 0) and (rxData(7 downto 0) = 0) then + v.state := STREAM_CRC_S; + else + v.state := STREAM_DATA_S; + end if; else -- Set the flag v.errDet := '1'; @@ -523,7 +731,9 @@ begin -- Move the data v.dataMaster.tValid := '1'; v.dataMaster.tData(31 downto 0) := rxData; + v.dataMaster.tKeep(3 downto 0) := x"F"; v.dataMaster.tUser(3 downto 0) := rxDataK; + v.crc := cxpCrcUpdate(r.crc, rxData); -- Increment counter v.dbgCnt := r.dbgCnt + 1; @@ -534,12 +744,64 @@ begin v.dataMaster.tLast := '1'; -- Next State - v.state := IDLE_S; + v.state := STREAM_CRC_S; else -- Increment counter v.dcnt := r.dcnt + 1; end if; + ---------------------------------------------------------------------- + when STREAM_CRC_S => + -- Check for CRC word + if (rxDataK = x"0") and (rxData = cxpCrcFinal(r.crc)) then + -- Next State + v.state := STREAM_EOP_S; + else + -- Publish an in-order SSI-style trailer verdict marker for + -- the image FSM. The raw stream payload has already moved. + v.dataMaster.tValid := '1'; + v.dataMaster.tData(31 downto 0) := (others => '0'); + v.dataMaster.tKeep(3 downto 0) := x"F"; + v.dataMaster.tLast := '1'; + v.dataMaster.tUser(CXP_RX_STREAM_TRAILER_TUSER_C) := '1'; + axiStreamSetUserBit(STREAM_AXIS_CONFIG_C, v.dataMaster, CXP_RX_STREAM_TRAILER_TUSER_C, '1'); + ssiSetUserEofe(STREAM_AXIS_CONFIG_C, v.dataMaster, '1'); + v.dataMaster.tUser(SSI_EOFE_C) := '1'; + -- Set the flag + v.errDet := '1'; + -- Next State + v.state := IDLE_S; + end if; + ---------------------------------------------------------------------- + when STREAM_EOP_S => + -- Check for end of packet indication + if (rxDataK = x"F") and (rxData = CXP_EOP_C) then + -- Publish a clean in-order trailer verdict marker. + v.dataMaster.tValid := '1'; + v.dataMaster.tData(31 downto 0) := (others => '0'); + v.dataMaster.tKeep(3 downto 0) := x"F"; + v.dataMaster.tLast := '1'; + v.dataMaster.tUser(CXP_RX_STREAM_TRAILER_TUSER_C) := '1'; + axiStreamSetUserBit(STREAM_AXIS_CONFIG_C, v.dataMaster, CXP_RX_STREAM_TRAILER_TUSER_C, '1'); + ssiSetUserEofe(STREAM_AXIS_CONFIG_C, v.dataMaster, '0'); + v.dataMaster.tUser(SSI_EOFE_C) := '0'; + -- Next State + v.state := IDLE_S; + else + -- Publish a bad in-order trailer verdict marker. + v.dataMaster.tValid := '1'; + v.dataMaster.tData(31 downto 0) := (others => '0'); + v.dataMaster.tKeep(3 downto 0) := x"F"; + v.dataMaster.tLast := '1'; + v.dataMaster.tUser(CXP_RX_STREAM_TRAILER_TUSER_C) := '1'; + axiStreamSetUserBit(STREAM_AXIS_CONFIG_C, v.dataMaster, CXP_RX_STREAM_TRAILER_TUSER_C, '1'); + ssiSetUserEofe(STREAM_AXIS_CONFIG_C, v.dataMaster, '1'); + v.dataMaster.tUser(SSI_EOFE_C) := '1'; + -- Set the flag + v.errDet := '1'; + -- Next State + v.state := IDLE_S; + end if; ---------------------------------------------------------------------- end case; @@ -549,9 +811,11 @@ begin cfgMaster <= r.cfgMaster; dataMaster <= r.dataMaster; heatbeatMaster <= r.heatbeatMaster; + eventMaster <= r.eventMaster; ioAck <= r.ioAck; eventAck <= r.eventAck; eventTag <= r.eventTag; + rxError <= r.errDet; -- Reset if (rxRst = '1') or (rxLinkUp = '0') then @@ -570,4 +834,20 @@ begin end if; end process seq; + U_EventBuffer : entity surf.SimpleDualPortRam + generic map ( + TPD_G => TPD_G, + MEMORY_TYPE_G => "distributed", + DATA_WIDTH_G => 32, + ADDR_WIDTH_G => EVENT_BUFFER_ADDR_WIDTH_C) + port map ( + clka => rxClk, + wea => r.eventRamWrEn, + addra => r.eventRamWrAddr, + dina => r.eventRamWrData, + clkb => rxClk, + rstb => rxRst, + addrb => r.eventReadAddr, + doutb => eventRamRdData); + end rtl; diff --git a/protocols/coaxpress/core/rtl/CoaXPressRxLaneMux.vhd b/protocols/coaxpress/core/rtl/CoaXPressRxLaneMux.vhd index a9f5e9b020..0efde57af0 100644 --- a/protocols/coaxpress/core/rtl/CoaXPressRxLaneMux.vhd +++ b/protocols/coaxpress/core/rtl/CoaXPressRxLaneMux.vhd @@ -22,6 +22,8 @@ use ieee.std_logic_arith.all; library surf; use surf.StdRtlPkg.all; use surf.AxiStreamPkg.all; +use surf.SsiPkg.all; +use surf.CoaXPressPkg.all; entity CoaXPressRxLaneMux is generic ( @@ -44,21 +46,33 @@ end entity CoaXPressRxLaneMux; architecture rtl of CoaXPressRxLaneMux is + constant WIDE_AXIS_CONFIG_C : AxiStreamConfigType := ssiAxiStreamConfig( + dataBytes => (4*NUM_LANES_G), + tKeepMode => TKEEP_NORMAL_C, + tUserMode => TUSER_NORMAL_C, + tDestBits => 0, + tUserBits => CXP_RX_STREAM_TUSER_BITS_C); + type RegType is record numOfLane : slv(2 downto 0); lane : natural range 0 to NUM_LANES_G-1; + pendingTrailer : sl; + trailerMarker : sl; rxSlaves : AxiStreamSlaveArray(NUM_LANES_G-1 downto 0); pipeMaster : AxiStreamMasterType; end record RegType; constant REG_INIT_C : RegType := ( numOfLane => (others => '0'), lane => 0, + pendingTrailer => '0', + trailerMarker => '0', rxSlaves => (others => AXI_STREAM_SLAVE_FORCE_C), pipeMaster => AXI_STREAM_MASTER_INIT_C); signal r : RegType := REG_INIT_C; signal rin : RegType; + signal validVec : slv(NUM_LANES_G-1 downto 0); signal pipeSlave : AxiStreamSlaveType; -- attribute dont_touch : string; @@ -66,11 +80,16 @@ architecture rtl of CoaXPressRxLaneMux is begin - comb : process (numOfLane, pipeSlave, r, rxFsmRst, rxMasters, rxRst) is + GEN_VALID : for i in NUM_LANES_G-1 downto 0 generate + validVec(i) <= rxMasters(i).tValid when (i <= r.numOfLane) else '0'; + end generate GEN_VALID; + + comb : process (numOfLane, pipeSlave, r, rxFsmRst, rxMasters, rxRst, validVec) is variable v : RegType; begin -- Latch the current value v := r; + v.trailerMarker := '0'; -- Flow Control for i in 0 to NUM_LANES_G-1 loop @@ -89,6 +108,9 @@ begin -- Check for valid data if (rxMasters(r.lane).tValid = '1') and (v.pipeMaster.tValid = '0') then + v.trailerMarker := rxMasters(r.lane).tLast and ( + rxMasters(r.lane).tUser(CXP_RX_STREAM_TRAILER_TUSER_C) or + axiStreamGetUserBit(WIDE_AXIS_CONFIG_C, rxMasters(r.lane), CXP_RX_STREAM_TRAILER_TUSER_C)); -- Accept inbound data v.rxSlaves(r.lane).tReady := '1'; @@ -96,8 +118,16 @@ begin -- Move the outbound data v.pipeMaster := rxMasters(r.lane); - -- Check for tLast and more than 1 lane - if (rxMasters(r.lane).tLast = '1') and (NUM_LANES_G > 1) then + -- Rotate only after the lane trailer verdict marker. The preceding + -- payload TLAST still belongs to this lane until the trailer has been + -- consumed by the image FSM. + if (v.trailerMarker = '1') then + v.pendingTrailer := '0'; + elsif (rxMasters(r.lane).tLast = '1') then + v.pendingTrailer := '1'; + end if; + + if (v.trailerMarker = '1') and (NUM_LANES_G > 1) then -- Check for roll over if (r.lane = r.numOfLane) then @@ -110,6 +140,18 @@ begin end if; + -- Check for idle lane and more than 1 lane + elsif (v.pipeMaster.tValid = '0') and (NUM_LANES_G > 1) and (r.pendingTrailer = '0') and (uOr(validVec) = '1') then + + -- Check for roll over + if (r.lane = r.numOfLane) then + -- Reset counter + v.lane := 0; + else + -- Increment counter + v.lane := r.lane + 1; + end if; + end if; -- Outputs diff --git a/protocols/coaxpress/core/rtl/CoaXPressRxWordPacker.vhd b/protocols/coaxpress/core/rtl/CoaXPressRxWordPacker.vhd index 56772d7fb4..01a2f07061 100644 --- a/protocols/coaxpress/core/rtl/CoaXPressRxWordPacker.vhd +++ b/protocols/coaxpress/core/rtl/CoaXPressRxWordPacker.vhd @@ -22,6 +22,8 @@ use ieee.std_logic_unsigned.all; library surf; use surf.StdRtlPkg.all; use surf.AxiStreamPkg.all; +use surf.SsiPkg.all; +use surf.CoaXPressPkg.all; entity CoaXPressRxWordPacker is generic ( @@ -33,48 +35,73 @@ entity CoaXPressRxWordPacker is rxRst : in sl; -- Inbound frame sAxisMaster : in AxiStreamMasterType; + sAxisSlave : out AxiStreamSlaveType; -- Outbound frame - mAxisMaster : out AxiStreamMasterType); + mAxisMaster : out AxiStreamMasterType; + mAxisSlave : in AxiStreamSlaveType); end CoaXPressRxWordPacker; architecture rtl of CoaXPressRxWordPacker is + constant WIDE_AXIS_CONFIG_C : AxiStreamConfigType := ssiAxiStreamConfig( + dataBytes => (4*NUM_LANES_G), + tKeepMode => TKEEP_NORMAL_C, + tUserMode => TUSER_NORMAL_C, + tDestBits => 0, + tUserBits => CXP_RX_STREAM_TUSER_BITS_C); + type RegType is record wordCount : natural range 0 to NUM_LANES_G-1; firstWord : natural range 0 to NUM_LANES_G-1; lastWord : natural range 0 to NUM_LANES_G-1; - inMaster : AxiStreamMasterType; + beatValid : sl; + beatLast : sl; + beatData : slv(31 downto 0); + sAxisSlave : AxiStreamSlaveType; curMaster : AxiStreamMasterType; nxtMaster : AxiStreamMasterType; - outMaster : AxiStreamMasterType; end record RegType; constant REG_INIT_C : RegType := ( wordCount => 0, firstWord => 0, lastWord => 0, - inMaster => AXI_STREAM_MASTER_INIT_C, + beatValid => '0', + beatLast => '0', + beatData => (others => '0'), + sAxisSlave => AXI_STREAM_SLAVE_INIT_C, curMaster => AXI_STREAM_MASTER_INIT_C, - nxtMaster => AXI_STREAM_MASTER_INIT_C, - outMaster => AXI_STREAM_MASTER_INIT_C); + nxtMaster => AXI_STREAM_MASTER_INIT_C); signal r : RegType := REG_INIT_C; signal rin : RegType; begin - comb : process (r, rxRst, sAxisMaster) is - variable v : RegType; - variable valid : sl; - variable last : sl; - variable data : slv(31 downto 0); + sAxisSlave <= rin.sAxisSlave; + + comb : process (mAxisSlave, r, rxRst, sAxisMaster) is + variable v : RegType; begin v := r; - -- Register input - v.inMaster := sAxisMaster; + v.beatValid := '0'; + v.beatLast := '0'; + v.beatData := (others => '0'); + + -- Pop the completed output beat only when the downstream stage accepts it. + if (mAxisSlave.tReady = '1') and (r.curMaster.tValid = '1') then + v.curMaster := r.nxtMaster; + v.nxtMaster := AXI_STREAM_MASTER_INIT_C; + v.nxtMaster.tKeep := (others => '0'); + end if; + + -- The input beat can expand into at most the current partial word plus one + -- next word, so the next slot being empty is sufficient capacity. + v.sAxisSlave.tReady := not v.nxtMaster.tValid; -- Find location of last word + v.lastWord := 0; for i in 0 to NUM_LANES_G-1 loop if (sAxisMaster.tKeep(4*i) = '1') then v.lastWord := i; @@ -82,55 +109,52 @@ begin end loop; -- Find location of first word + v.firstWord := 0; for i in NUM_LANES_G-1 downto 0 loop if (sAxisMaster.tKeep(4*i) = '1') then v.firstWord := i; end if; end loop; - -- Pending output from current - if r.curMaster.tValid = '1' then - v.outMaster := r.curMaster; - v.curMaster := r.nxtMaster; - v.nxtMaster := AXI_STREAM_MASTER_INIT_C; - v.nxtMaster.tKeep := (others => '0'); - else - v.outMaster := AXI_STREAM_MASTER_INIT_C; - end if; - - -- Data is valid - if r.inMaster.tValid = '1' then + -- Data is valid and there is room to accept the whole input beat. + if (sAxisMaster.tValid = '1') and (v.sAxisSlave.tReady = '1') then -- Process each input word for i in 0 to NUM_LANES_G-1 loop - if (i >= r.firstWord) and (i <= r.lastWord) then + if (i >= v.firstWord) and (i <= v.lastWord) and (sAxisMaster.tKeep(4*i) = '1') then -- Extract values for each iteration - last := r.inMaster.tLast and toSl(i = r.lastWord); - valid := toSl(v.wordCount = NUM_LANES_G-1) or last; - data := r.inMaster.tData(i*32+31 downto i*32); + v.beatLast := sAxisMaster.tLast and toSl(i = v.lastWord); + v.beatValid := toSl(v.wordCount = NUM_LANES_G-1) or v.beatLast; + v.beatData := sAxisMaster.tData(i*32+31 downto i*32); -- Still filling current data if v.curMaster.tValid = '0' then - v.curMaster.tData(v.wordCount*32+31 downto v.wordCount*32) := data; + v.curMaster.tData(v.wordCount*32+31 downto v.wordCount*32) := v.beatData; v.curMaster.tKeep(v.wordCount*4+3 downto v.wordCount*4) := x"F"; - v.curMaster.tValid := valid; - v.curMaster.tLast := last; + v.curMaster.tValid := v.beatValid; + v.curMaster.tLast := v.beatLast; + if (v.beatLast = '1') then + ssiSetUserEofe(WIDE_AXIS_CONFIG_C, v.curMaster, ssiGetUserEofe(WIDE_AXIS_CONFIG_C, sAxisMaster)); + end if; -- Filling next data elsif v.nxtMaster.tValid = '0' then - v.nxtMaster.tData(v.wordCount*32+31 downto v.wordCount*32) := data; + v.nxtMaster.tData(v.wordCount*32+31 downto v.wordCount*32) := v.beatData; v.nxtMaster.tKeep(v.wordCount*4+3 downto v.wordCount*4) := x"F"; - v.nxtMaster.tValid := valid; - v.nxtMaster.tLast := last; + v.nxtMaster.tValid := v.beatValid; + v.nxtMaster.tLast := v.beatLast; + if (v.beatLast = '1') then + ssiSetUserEofe(WIDE_AXIS_CONFIG_C, v.nxtMaster, ssiGetUserEofe(WIDE_AXIS_CONFIG_C, sAxisMaster)); + end if; end if; - if v.wordCount = NUM_LANES_G-1 or last = '1' then + if v.wordCount = NUM_LANES_G-1 or v.beatLast = '1' then v.wordCount := 0; else v.wordCount := v.wordCount + 1; @@ -148,7 +172,7 @@ begin rin <= v; - mAxisMaster <= r.outMaster; + mAxisMaster <= r.curMaster; end process; @@ -160,4 +184,3 @@ begin end process; end rtl; - diff --git a/protocols/coaxpress/core/wrappers/CoaXPressCoreDebugWrapper.vhd b/protocols/coaxpress/core/wrappers/CoaXPressCoreDebugWrapper.vhd index 2d0d583bf7..2d097cb870 100644 --- a/protocols/coaxpress/core/wrappers/CoaXPressCoreDebugWrapper.vhd +++ b/protocols/coaxpress/core/wrappers/CoaXPressCoreDebugWrapper.vhd @@ -144,6 +144,7 @@ architecture rtl of CoaXPressCoreDebugWrapper is signal eventAck : sl; signal eventTag : slv(7 downto 0); + signal eventMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; signal trigAck : sl; signal txLsRateInt : sl; @@ -379,6 +380,8 @@ begin cfgClk => cfgClk, cfgRst => cfgRst, cfgRxMaster => cfgRxMaster, + eventMaster => eventMaster, + eventSlave => AXI_STREAM_SLAVE_FORCE_C, eventAck => eventAck, eventTag => eventTag, txClk => txClk, diff --git a/protocols/coaxpress/core/wrappers/CoaXPressOverFiberBridgeAxiLWrapper.vhd b/protocols/coaxpress/core/wrappers/CoaXPressOverFiberBridgeAxiLWrapper.vhd new file mode 100644 index 0000000000..f47954741b --- /dev/null +++ b/protocols/coaxpress/core/wrappers/CoaXPressOverFiberBridgeAxiLWrapper.vhd @@ -0,0 +1,150 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing wrapper for CoaXPressOverFiberBridgeAxiL +------------------------------------------------------------------------------- +-- 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.CoaXPressPkg.all; + +entity CoaXPressOverFiberBridgeAxiLWrapper is + port ( + -- Bridge RX status clock domain + rxClk : in sl; + rxRst : in sl; + rxError : in sl; + rxAbort : in sl; + rxErrorCode : in slv(3 downto 0); + seqValid : in sl; + seqData : in slv(23 downto 0); + seqError : in sl; + seqExpected : in slv(23 downto 0); + seqErrorExpected : in slv(23 downto 0); + hkpValid : in sl; + hkpData : in slv(31 downto 0); + hkpEop : in sl; + hkpSof : in sl; + hkpError : in sl; + hkpWordCount : in slv(7 downto 0); + hkpKCodeMask : in slv(3 downto 0); + hkpKCodeValid : in sl; + hkpType : in slv(3 downto 0); + -- AXI-Lite Register Interface + S_AXI_ACLK : in std_logic; + S_AXI_ARESETN : in std_logic; + S_AXI_AWADDR : in std_logic_vector(11 downto 0); + S_AXI_AWPROT : in std_logic_vector(2 downto 0); + S_AXI_AWVALID : in std_logic; + S_AXI_AWREADY : out std_logic; + S_AXI_WDATA : in std_logic_vector(31 downto 0); + S_AXI_WSTRB : in std_logic_vector(3 downto 0); + S_AXI_WVALID : in std_logic; + S_AXI_WREADY : out std_logic; + S_AXI_BRESP : out std_logic_vector(1 downto 0); + S_AXI_BVALID : out std_logic; + S_AXI_BREADY : in std_logic; + S_AXI_ARADDR : in std_logic_vector(11 downto 0); + S_AXI_ARPROT : in std_logic_vector(2 downto 0); + S_AXI_ARVALID : in std_logic; + S_AXI_ARREADY : out std_logic; + S_AXI_RDATA : out std_logic_vector(31 downto 0); + S_AXI_RRESP : out std_logic_vector(1 downto 0); + S_AXI_RVALID : out std_logic; + S_AXI_RREADY : in std_logic); +end entity CoaXPressOverFiberBridgeAxiLWrapper; + +architecture mapping of CoaXPressOverFiberBridgeAxiLWrapper is + + 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 rxStatus : CxpofRxStatusType := CXPOF_RX_STATUS_INIT_C; + +begin + + rxStatus.rxError <= rxError; + rxStatus.rxAbort <= rxAbort; + rxStatus.rxErrorCode <= rxErrorCode; + rxStatus.seqValid <= seqValid; + rxStatus.seqData <= seqData; + rxStatus.seqError <= seqError; + rxStatus.seqExpected <= seqExpected; + rxStatus.seqErrorExpected <= seqErrorExpected; + rxStatus.hkpValid <= hkpValid; + rxStatus.hkpData <= hkpData; + rxStatus.hkpEop <= hkpEop; + rxStatus.hkpSof <= hkpSof; + rxStatus.hkpError <= hkpError; + rxStatus.hkpWordCount <= hkpWordCount; + rxStatus.hkpKCodeMask <= hkpKCodeMask; + rxStatus.hkpKCodeValid <= hkpKCodeValid; + rxStatus.hkpType <= hkpType; + + U_ShimLayer : entity surf.SlaveAxiLiteIpIntegrator + generic map ( + INTERFACENAME => "S_AXI", + HAS_PROT => 1, + HAS_WSTRB => 1, + ADDR_WIDTH => 12) + port map ( + S_AXI_ACLK => S_AXI_ACLK, + S_AXI_ARESETN => S_AXI_ARESETN, + S_AXI_AWADDR => S_AXI_AWADDR, + S_AXI_AWPROT => S_AXI_AWPROT, + S_AXI_AWVALID => S_AXI_AWVALID, + S_AXI_AWREADY => S_AXI_AWREADY, + S_AXI_WDATA => S_AXI_WDATA, + S_AXI_WSTRB => S_AXI_WSTRB, + S_AXI_WVALID => S_AXI_WVALID, + S_AXI_WREADY => S_AXI_WREADY, + S_AXI_BRESP => S_AXI_BRESP, + S_AXI_BVALID => S_AXI_BVALID, + S_AXI_BREADY => S_AXI_BREADY, + S_AXI_ARADDR => S_AXI_ARADDR, + S_AXI_ARPROT => S_AXI_ARPROT, + S_AXI_ARVALID => S_AXI_ARVALID, + S_AXI_ARREADY => S_AXI_ARREADY, + S_AXI_RDATA => S_AXI_RDATA, + S_AXI_RRESP => S_AXI_RRESP, + S_AXI_RVALID => S_AXI_RVALID, + S_AXI_RREADY => S_AXI_RREADY, + axilClk => axilClk, + axilRst => axilRst, + axilReadMaster => axilReadMaster, + axilReadSlave => axilReadSlave, + axilWriteMaster => axilWriteMaster, + axilWriteSlave => axilWriteSlave); + + U_DUT : entity surf.CoaXPressOverFiberBridgeAxiL + generic map ( + TPD_G => 1 ns, + CNT_WIDTH_G => 16) + port map ( + rxClk => rxClk, + rxRst => rxRst, + rxStatus => rxStatus, + axilClk => axilClk, + axilRst => axilRst, + axilReadMaster => axilReadMaster, + axilReadSlave => axilReadSlave, + axilWriteMaster => axilWriteMaster, + axilWriteSlave => axilWriteSlave); + +end architecture mapping; diff --git a/protocols/coaxpress/core/wrappers/CoaXPressOverFiberBridgeRxStatusWrapper.vhd b/protocols/coaxpress/core/wrappers/CoaXPressOverFiberBridgeRxStatusWrapper.vhd new file mode 100644 index 0000000000..49b727188c --- /dev/null +++ b/protocols/coaxpress/core/wrappers/CoaXPressOverFiberBridgeRxStatusWrapper.vhd @@ -0,0 +1,93 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing status wrapper for CoaXPressOverFiberBridgeRx +------------------------------------------------------------------------------- +-- 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.CoaXPressPkg.all; + +entity CoaXPressOverFiberBridgeRxStatusWrapper is + port ( + -- Clock and Reset + clk : in sl; + rst : in sl; + -- XGMII interface + xgmiiRxd : in slv(31 downto 0); + xgmiiRxc : in slv(3 downto 0); + -- Rx PHY Interface + rxData : out slv(31 downto 0); + rxDataK : out slv(3 downto 0); + -- Flattened Status Interface + rxError : out sl; + rxAbort : out sl; + rxErrorCode : out slv(3 downto 0); + seqValid : out sl; + seqData : out slv(23 downto 0); + seqError : out sl; + seqExpected : out slv(23 downto 0); + seqErrorExpected : out slv(23 downto 0); + hkpValid : out sl; + hkpData : out slv(31 downto 0); + hkpEop : out sl; + hkpSof : out sl; + hkpError : out sl; + hkpWordCount : out slv(7 downto 0); + hkpKCodeMask : out slv(3 downto 0); + hkpKCodeValid : out sl; + hkpType : out slv(3 downto 0)); +end entity CoaXPressOverFiberBridgeRxStatusWrapper; + +architecture mapping of CoaXPressOverFiberBridgeRxStatusWrapper is + + signal rxStatus : CxpofRxStatusType := CXPOF_RX_STATUS_INIT_C; + +begin + + rxError <= rxStatus.rxError; + rxAbort <= rxStatus.rxAbort; + rxErrorCode <= rxStatus.rxErrorCode; + seqValid <= rxStatus.seqValid; + seqData <= rxStatus.seqData; + seqError <= rxStatus.seqError; + seqExpected <= rxStatus.seqExpected; + seqErrorExpected <= rxStatus.seqErrorExpected; + hkpValid <= rxStatus.hkpValid; + hkpData <= rxStatus.hkpData; + hkpEop <= rxStatus.hkpEop; + hkpSof <= rxStatus.hkpSof; + hkpError <= rxStatus.hkpError; + hkpWordCount <= rxStatus.hkpWordCount; + hkpKCodeMask <= rxStatus.hkpKCodeMask; + hkpKCodeValid <= rxStatus.hkpKCodeValid; + hkpType <= rxStatus.hkpType; + + U_DUT : entity surf.CoaXPressOverFiberBridgeRx + generic map ( + TPD_G => 1 ns) + port map ( + -- Clock and Reset + clk => clk, + rst => rst, + -- XGMII interface + xgmiiRxd => xgmiiRxd, + xgmiiRxc => xgmiiRxc, + -- Rx PHY Interface + rxData => rxData, + rxDataK => rxDataK, + -- Status Interface + rxStatus => rxStatus); + +end architecture mapping; diff --git a/protocols/coaxpress/core/wrappers/CoaXPressOverFiberBridgeStatusWrapper.vhd b/protocols/coaxpress/core/wrappers/CoaXPressOverFiberBridgeStatusWrapper.vhd new file mode 100644 index 0000000000..312bfe45b7 --- /dev/null +++ b/protocols/coaxpress/core/wrappers/CoaXPressOverFiberBridgeStatusWrapper.vhd @@ -0,0 +1,119 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing status wrapper for CoaXPressOverFiberBridge +------------------------------------------------------------------------------- +-- 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.CoaXPressPkg.all; + +entity CoaXPressOverFiberBridgeStatusWrapper is + generic ( + LANE0_G : boolean := true); + port ( + -- XGMII TX interface (txClk156 domain) + txClk156 : in sl; + xgmiiTxd : out slv(63 downto 0); + xgmiiTxc : out slv(7 downto 0); + -- XGMII RX interface (rxClk156 domain) + rxClk156 : in sl; + xgmiiRxd : in slv(63 downto 0); + xgmiiRxc : in slv(7 downto 0); + -- CXP TX interface (txClk312 domain) + txClk312 : in sl; + txRst312 : in sl; + txLsValid : in sl; + txLsData : in slv(7 downto 0); + txLsDataK : in sl; + txLsLaneEn : in slv(3 downto 0); + txLsRate : in sl; + -- CXP RX interface (rxClk312 domain) + rxClk312 : in sl; + rxRst312 : in sl; + rxData : out slv(31 downto 0); + rxDataK : out slv(3 downto 0); + -- Flattened Status Interface + rxError : out sl; + rxAbort : out sl; + rxErrorCode : out slv(3 downto 0); + seqValid : out sl; + seqData : out slv(23 downto 0); + seqError : out sl; + seqExpected : out slv(23 downto 0); + seqErrorExpected : out slv(23 downto 0); + hkpValid : out sl; + hkpData : out slv(31 downto 0); + hkpEop : out sl; + hkpSof : out sl; + hkpError : out sl; + hkpWordCount : out slv(7 downto 0); + hkpKCodeMask : out slv(3 downto 0); + hkpKCodeValid : out sl; + hkpType : out slv(3 downto 0)); +end entity CoaXPressOverFiberBridgeStatusWrapper; + +architecture mapping of CoaXPressOverFiberBridgeStatusWrapper is + + signal rxStatus : CxpofRxStatusType := CXPOF_RX_STATUS_INIT_C; + +begin + + rxError <= rxStatus.rxError; + rxAbort <= rxStatus.rxAbort; + rxErrorCode <= rxStatus.rxErrorCode; + seqValid <= rxStatus.seqValid; + seqData <= rxStatus.seqData; + seqError <= rxStatus.seqError; + seqExpected <= rxStatus.seqExpected; + seqErrorExpected <= rxStatus.seqErrorExpected; + hkpValid <= rxStatus.hkpValid; + hkpData <= rxStatus.hkpData; + hkpEop <= rxStatus.hkpEop; + hkpSof <= rxStatus.hkpSof; + hkpError <= rxStatus.hkpError; + hkpWordCount <= rxStatus.hkpWordCount; + hkpKCodeMask <= rxStatus.hkpKCodeMask; + hkpKCodeValid <= rxStatus.hkpKCodeValid; + hkpType <= rxStatus.hkpType; + + U_DUT : entity surf.CoaXPressOverFiberBridge + generic map ( + TPD_G => 1 ns, + LANE0_G => LANE0_G) + port map ( + -- XGMII TX interface (txClk156 domain) + txClk156 => txClk156, + xgmiiTxd => xgmiiTxd, + xgmiiTxc => xgmiiTxc, + -- XGMII RX interface (rxClk156 domain) + rxClk156 => rxClk156, + xgmiiRxd => xgmiiRxd, + xgmiiRxc => xgmiiRxc, + -- CXP TX interface (txClk312 domain) + txClk312 => txClk312, + txRst312 => txRst312, + txLsValid => txLsValid, + txLsData => txLsData, + txLsDataK => txLsDataK, + txLsLaneEn => txLsLaneEn, + txLsRate => txLsRate, + -- CXP RX interface (rxClk312 domain) + rxClk312 => rxClk312, + rxRst312 => rxRst312, + rxData => rxData, + rxDataK => rxDataK, + rxStatus => rxStatus); + +end architecture mapping; diff --git a/protocols/coaxpress/core/wrappers/CoaXPressRxCorePathWrapper.vhd b/protocols/coaxpress/core/wrappers/CoaXPressRxCorePathWrapper.vhd index f8b49f1a28..1f7c2100dc 100644 --- a/protocols/coaxpress/core/wrappers/CoaXPressRxCorePathWrapper.vhd +++ b/protocols/coaxpress/core/wrappers/CoaXPressRxCorePathWrapper.vhd @@ -54,6 +54,13 @@ entity CoaXPressRxCorePathWrapper is cfgTData : out slv(63 downto 0); cfgTKeep : out slv(7 downto 0); cfgTLast : out sl; + eventTValid : out sl; + eventTData : out slv(31 downto 0); + eventTKeep : out slv(3 downto 0); + eventTDest : out slv(7 downto 0); + eventTUser : out slv(31 downto 0); + eventTLast : out sl; + eventTReady : in sl; eventAck : out sl; eventTag : out slv(7 downto 0); trigAck : out sl; @@ -70,6 +77,8 @@ architecture rtl of CoaXPressRxCorePathWrapper is signal imageHdrMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; signal imageHdrSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_FORCE_C; signal cfgRxMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal eventMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal eventSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_FORCE_C; signal rxClkVec : slv(NUM_LANES_G-1 downto 0); signal rxRstVec : slv(NUM_LANES_G-1 downto 0); @@ -98,6 +107,14 @@ begin cfgTKeep <= cfgRxMaster.tKeep(7 downto 0); cfgTLast <= cfgRxMaster.tLast; + eventSlave.tReady <= eventTReady; + eventTValid <= eventMaster.tValid; + eventTData <= eventMaster.tData(31 downto 0); + eventTKeep <= eventMaster.tKeep(3 downto 0); + eventTDest <= eventMaster.tDest(7 downto 0); + eventTUser <= eventMaster.tUser(31 downto 0); + eventTLast <= eventMaster.tLast; + U_Data : entity surf.MasterAxiStreamIpIntegrator generic map ( INTERFACENAME => "M_DATA", @@ -170,6 +187,8 @@ begin cfgClk => cfgClk, cfgRst => cfgRst, cfgRxMaster => cfgRxMaster, + eventMaster => eventMaster, + eventSlave => eventSlave, eventAck => eventAck, eventTag => eventTag, txClk => txClk, diff --git a/protocols/coaxpress/core/wrappers/CoaXPressRxHsFsmWrapper.vhd b/protocols/coaxpress/core/wrappers/CoaXPressRxHsFsmWrapper.vhd index 184b114452..0ac9f64129 100644 --- a/protocols/coaxpress/core/wrappers/CoaXPressRxHsFsmWrapper.vhd +++ b/protocols/coaxpress/core/wrappers/CoaXPressRxHsFsmWrapper.vhd @@ -19,6 +19,7 @@ library surf; use surf.StdRtlPkg.all; use surf.AxiStreamPkg.all; use surf.SsiPkg.all; +use surf.CoaXPressPkg.all; entity CoaXPressRxHsFsmWrapper is generic ( @@ -31,6 +32,7 @@ entity CoaXPressRxHsFsmWrapper is sAxisTValid : in sl; sAxisTData : in slv(32*NUM_LANES_G-1 downto 0); sAxisTKeep : in slv(4*NUM_LANES_G-1 downto 0); + sAxisTUser : in slv(CXP_RX_STREAM_TUSER_BITS_C-1 downto 0); sAxisTLast : in sl; sAxisTReady : out sl; hdrTValid : out sl; @@ -54,13 +56,14 @@ architecture rtl of CoaXPressRxHsFsmWrapper is begin -- Present the flattened source beat as one wide AXI-stream record. - sAxisComb : process (sAxisTData, sAxisTKeep, sAxisTLast, sAxisTValid) is + sAxisComb : process (sAxisTData, sAxisTKeep, sAxisTLast, sAxisTUser, sAxisTValid) is variable v : AxiStreamMasterType; begin v := AXI_STREAM_MASTER_INIT_C; v.tValid := sAxisTValid; v.tData(32*NUM_LANES_G-1 downto 0) := sAxisTData; v.tKeep(4*NUM_LANES_G-1 downto 0) := sAxisTKeep; + v.tUser(CXP_RX_STREAM_TUSER_BITS_C-1 downto 0) := sAxisTUser; v.tLast := sAxisTLast; sAxisMaster <= v; end process sAxisComb; diff --git a/protocols/coaxpress/core/wrappers/CoaXPressRxLaneMuxWrapper.vhd b/protocols/coaxpress/core/wrappers/CoaXPressRxLaneMuxWrapper.vhd index 69785e1578..1a4ba721d5 100644 --- a/protocols/coaxpress/core/wrappers/CoaXPressRxLaneMuxWrapper.vhd +++ b/protocols/coaxpress/core/wrappers/CoaXPressRxLaneMuxWrapper.vhd @@ -18,6 +18,7 @@ use ieee.std_logic_1164.all; library surf; use surf.StdRtlPkg.all; use surf.AxiStreamPkg.all; +use surf.CoaXPressPkg.all; entity CoaXPressRxLaneMuxWrapper is generic ( @@ -30,6 +31,7 @@ entity CoaXPressRxLaneMuxWrapper is sAxisTValid : in slv(NUM_LANES_G-1 downto 0); sAxisTData : in slv(32*NUM_LANES_G*NUM_LANES_G-1 downto 0); sAxisTKeep : in slv(4*NUM_LANES_G*NUM_LANES_G-1 downto 0); + sAxisTUser : in slv(CXP_RX_STREAM_TUSER_BITS_C*NUM_LANES_G-1 downto 0); sAxisTLast : in slv(NUM_LANES_G-1 downto 0); sAxisTReady : out slv(NUM_LANES_G-1 downto 0); mAxisTValid : out sl; @@ -49,7 +51,7 @@ architecture rtl of CoaXPressRxLaneMuxWrapper is begin -- Rebuild the per-lane record array from the concatenated cocotb ports. - sAxisComb : process (sAxisTData, sAxisTKeep, sAxisTLast, sAxisTValid) is + sAxisComb : process (sAxisTData, sAxisTKeep, sAxisTLast, sAxisTUser, sAxisTValid) is variable masters : AxiStreamMasterArray(NUM_LANES_G-1 downto 0); begin masters := (others => AXI_STREAM_MASTER_INIT_C); @@ -59,6 +61,8 @@ begin sAxisTData(32*NUM_LANES_G*(i+1)-1 downto 32*NUM_LANES_G*i); masters(i).tKeep(4*NUM_LANES_G-1 downto 0) := sAxisTKeep(4*NUM_LANES_G*(i+1)-1 downto 4*NUM_LANES_G*i); + masters(i).tUser(CXP_RX_STREAM_TUSER_BITS_C-1 downto 0) := + sAxisTUser(CXP_RX_STREAM_TUSER_BITS_C*(i+1)-1 downto CXP_RX_STREAM_TUSER_BITS_C*i); masters(i).tLast := sAxisTLast(i); end loop; rxMasters <= masters; diff --git a/protocols/coaxpress/core/wrappers/CoaXPressRxLaneWrapper.vhd b/protocols/coaxpress/core/wrappers/CoaXPressRxLaneWrapper.vhd index 1da89a8fde..5b1badfd93 100644 --- a/protocols/coaxpress/core/wrappers/CoaXPressRxLaneWrapper.vhd +++ b/protocols/coaxpress/core/wrappers/CoaXPressRxLaneWrapper.vhd @@ -30,14 +30,21 @@ entity CoaXPressRxLaneWrapper is cfgTData : out slv(63 downto 0); dataTValid : out sl; dataTData : out slv(31 downto 0); + dataTKeep : out slv(3 downto 0); dataTUser : out slv(3 downto 0); dataTLast : out sl; heartbeatTValid : out sl; heartbeatTData : out slv(95 downto 0); heartbeatTLast : out sl; + eventTValid : out sl; + eventTData : out slv(31 downto 0); + eventTDest : out slv(7 downto 0); + eventTUser : out slv(31 downto 0); + eventTLast : out sl; ioAck : out sl; eventAck : out sl; - eventTag : out slv(7 downto 0)); + eventTag : out slv(7 downto 0); + rxError : out sl); end entity CoaXPressRxLaneWrapper; architecture rtl of CoaXPressRxLaneWrapper is @@ -45,6 +52,7 @@ architecture rtl of CoaXPressRxLaneWrapper is signal cfgMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; signal dataMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; signal heartbeatMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal eventMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; signal imageHdrMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; begin @@ -54,11 +62,17 @@ begin cfgTData <= cfgMaster.tData(63 downto 0); dataTValid <= dataMaster.tValid; dataTData <= dataMaster.tData(31 downto 0); + dataTKeep <= dataMaster.tKeep(3 downto 0); dataTUser <= dataMaster.tUser(3 downto 0); dataTLast <= dataMaster.tLast; heartbeatTValid <= heartbeatMaster.tValid; heartbeatTData <= heartbeatMaster.tData(95 downto 0); heartbeatTLast <= heartbeatMaster.tLast; + eventTValid <= eventMaster.tValid; + eventTData <= eventMaster.tData(31 downto 0); + eventTDest <= eventMaster.tDest(7 downto 0); + eventTUser <= eventMaster.tUser(31 downto 0); + eventTLast <= eventMaster.tLast; -- Instantiate the real receive-lane decoder with the flattened ports. U_DUT : entity surf.CoaXPressRxLane @@ -70,10 +84,12 @@ begin cfgMaster => cfgMaster, dataMaster => dataMaster, heatbeatMaster => heartbeatMaster, + eventMaster => eventMaster, imageHdrMaster => imageHdrMaster, ioAck => ioAck, eventAck => eventAck, eventTag => eventTag, + rxError => rxError, rxData => rxData, rxDataK => rxDataK, rxLinkUp => rxLinkUp); diff --git a/protocols/coaxpress/core/wrappers/CoaXPressRxWordPackerWrapper.vhd b/protocols/coaxpress/core/wrappers/CoaXPressRxWordPackerWrapper.vhd index 1b2d092b8c..502599fc5e 100644 --- a/protocols/coaxpress/core/wrappers/CoaXPressRxWordPackerWrapper.vhd +++ b/protocols/coaxpress/core/wrappers/CoaXPressRxWordPackerWrapper.vhd @@ -38,6 +38,7 @@ end entity CoaXPressRxWordPackerWrapper; architecture rtl of CoaXPressRxWordPackerWrapper is signal sAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal sAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; signal mAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; begin @@ -69,6 +70,8 @@ begin rxClk => rxClk, rxRst => rxRst, sAxisMaster => sAxisMaster, - mAxisMaster => mAxisMaster); + sAxisSlave => sAxisSlave, + mAxisMaster => mAxisMaster, + mAxisSlave => AXI_STREAM_SLAVE_FORCE_C); end architecture rtl; diff --git a/protocols/coaxpress/core/wrappers/CoaXPressRxWrapper.vhd b/protocols/coaxpress/core/wrappers/CoaXPressRxWrapper.vhd index 46e79717a9..1e90c9d1b0 100644 --- a/protocols/coaxpress/core/wrappers/CoaXPressRxWrapper.vhd +++ b/protocols/coaxpress/core/wrappers/CoaXPressRxWrapper.vhd @@ -54,6 +54,13 @@ entity CoaXPressRxWrapper is cfgTData : out slv(63 downto 0); cfgTKeep : out slv(7 downto 0); cfgTLast : out sl; + eventTValid : out sl; + eventTData : out slv(31 downto 0); + eventTKeep : out slv(3 downto 0); + eventTDest : out slv(7 downto 0); + eventTUser : out slv(31 downto 0); + eventTLast : out sl; + eventTReady : in sl; eventAck : out sl; eventTag : out slv(7 downto 0); trigAck : out sl; @@ -70,6 +77,8 @@ architecture rtl of CoaXPressRxWrapper is signal imageHdrMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; signal imageHdrSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_FORCE_C; signal cfgRxMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal eventMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal eventSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_FORCE_C; signal rxClkVec : slv(NUM_LANES_G-1 downto 0); signal rxRstVec : slv(NUM_LANES_G-1 downto 0); @@ -90,12 +99,13 @@ begin dataSlave.tReady <= dataTReady; imageHdrSlave.tReady <= hdrTReady; + eventSlave.tReady <= eventTReady; dataTValid <= dataMaster.tValid; dataTData <= dataMaster.tData(31 downto 0); dataTKeep <= dataMaster.tKeep(3 downto 0); dataTLast <= dataMaster.tLast; - dataTUser <= dataMaster.tUser(0 downto 0); + dataTUser(0) <= ssiGetUserEofe(AXIS_CONFIG_C, dataMaster); hdrTValid <= imageHdrMaster.tValid; hdrTData <= imageHdrMaster.tData(31 downto 0); @@ -108,6 +118,13 @@ begin cfgTKeep <= cfgRxMaster.tKeep(7 downto 0); cfgTLast <= cfgRxMaster.tLast; + eventTValid <= eventMaster.tValid; + eventTData <= eventMaster.tData(31 downto 0); + eventTKeep <= eventMaster.tKeep(3 downto 0); + eventTDest <= eventMaster.tDest(7 downto 0); + eventTUser <= eventMaster.tUser(31 downto 0); + eventTLast <= eventMaster.tLast; + U_DUT : entity surf.CoaXPressRx generic map ( TPD_G => 1 ns, @@ -124,6 +141,8 @@ begin cfgClk => cfgClk, cfgRst => cfgRst, cfgRxMaster => cfgRxMaster, + eventMaster => eventMaster, + eventSlave => eventSlave, eventAck => eventAck, eventTag => eventTag, txClk => txClk, diff --git a/protocols/coaxpress/gthUs+/rtl/CoaXPressOverFiberGthUsIpWrapper.vhd b/protocols/coaxpress/gthUs+/rtl/CoaXPressOverFiberGthUsIpWrapper.vhd index a400f7e691..958d2984ce 100755 --- a/protocols/coaxpress/gthUs+/rtl/CoaXPressOverFiberGthUsIpWrapper.vhd +++ b/protocols/coaxpress/gthUs+/rtl/CoaXPressOverFiberGthUsIpWrapper.vhd @@ -55,7 +55,7 @@ entity CoaXPressOverFiberGthUsIpWrapper is rxDispErr : out sl := '0'; rxDecErr : out sl := '0'; rxLinkUp : out sl; - -- AXI-Lite DRP Interface + -- AXI-Lite Bridge Status Interface axilClk : in sl; axilRst : in sl; axilReadMaster : in AxiLiteReadMasterType; @@ -156,6 +156,7 @@ architecture mapping of CoaXPressOverFiberGthUsIpWrapper is signal xgmiiRxc : slv(7 downto 0); signal gtResetAll : sl; + signal bridgeRxStatus : CxpofRxStatusType; begin @@ -357,6 +358,21 @@ begin rxClk312 => phyClk312, rxRst312 => phyRst312, rxData => rxData, - rxDataK => rxDataK); + rxDataK => rxDataK, + rxStatus => bridgeRxStatus); + + U_BridgeAxiL : entity surf.CoaXPressOverFiberBridgeAxiL + generic map ( + TPD_G => TPD_G) + port map ( + rxClk => phyClk312, + rxRst => phyRst312, + rxStatus => bridgeRxStatus, + axilClk => axilClk, + axilRst => axilRst, + axilReadMaster => axilReadMaster, + axilReadSlave => axilReadSlave, + axilWriteMaster => axilWriteMaster, + axilWriteSlave => axilWriteSlave); end mapping; diff --git a/protocols/coaxpress/gthUs/rtl/CoaXPressOverFiberGthUsIpWrapper.vhd b/protocols/coaxpress/gthUs/rtl/CoaXPressOverFiberGthUsIpWrapper.vhd index a400f7e691..958d2984ce 100755 --- a/protocols/coaxpress/gthUs/rtl/CoaXPressOverFiberGthUsIpWrapper.vhd +++ b/protocols/coaxpress/gthUs/rtl/CoaXPressOverFiberGthUsIpWrapper.vhd @@ -55,7 +55,7 @@ entity CoaXPressOverFiberGthUsIpWrapper is rxDispErr : out sl := '0'; rxDecErr : out sl := '0'; rxLinkUp : out sl; - -- AXI-Lite DRP Interface + -- AXI-Lite Bridge Status Interface axilClk : in sl; axilRst : in sl; axilReadMaster : in AxiLiteReadMasterType; @@ -156,6 +156,7 @@ architecture mapping of CoaXPressOverFiberGthUsIpWrapper is signal xgmiiRxc : slv(7 downto 0); signal gtResetAll : sl; + signal bridgeRxStatus : CxpofRxStatusType; begin @@ -357,6 +358,21 @@ begin rxClk312 => phyClk312, rxRst312 => phyRst312, rxData => rxData, - rxDataK => rxDataK); + rxDataK => rxDataK, + rxStatus => bridgeRxStatus); + + U_BridgeAxiL : entity surf.CoaXPressOverFiberBridgeAxiL + generic map ( + TPD_G => TPD_G) + port map ( + rxClk => phyClk312, + rxRst => phyRst312, + rxStatus => bridgeRxStatus, + axilClk => axilClk, + axilRst => axilRst, + axilReadMaster => axilReadMaster, + axilReadSlave => axilReadSlave, + axilWriteMaster => axilWriteMaster, + axilWriteSlave => axilWriteSlave); end mapping; diff --git a/protocols/coaxpress/gtyUs+/rtl/CoaXPressOverFiberGtyUsIpWrapper.vhd b/protocols/coaxpress/gtyUs+/rtl/CoaXPressOverFiberGtyUsIpWrapper.vhd index bc78349c51..25ffe225c1 100755 --- a/protocols/coaxpress/gtyUs+/rtl/CoaXPressOverFiberGtyUsIpWrapper.vhd +++ b/protocols/coaxpress/gtyUs+/rtl/CoaXPressOverFiberGtyUsIpWrapper.vhd @@ -55,7 +55,7 @@ entity CoaXPressOverFiberGtyUsIpWrapper is rxDispErr : out sl := '0'; rxDecErr : out sl := '0'; rxLinkUp : out sl; - -- AXI-Lite DRP Interface + -- AXI-Lite Bridge Status Interface axilClk : in sl; axilRst : in sl; axilReadMaster : in AxiLiteReadMasterType; @@ -156,6 +156,7 @@ architecture mapping of CoaXPressOverFiberGtyUsIpWrapper is signal xgmiiRxc : slv(7 downto 0); signal gtResetAll : sl; + signal bridgeRxStatus : CxpofRxStatusType; begin @@ -357,6 +358,21 @@ begin rxClk312 => phyClk312, rxRst312 => phyRst312, rxData => rxData, - rxDataK => rxDataK); + rxDataK => rxDataK, + rxStatus => bridgeRxStatus); + + U_BridgeAxiL : entity surf.CoaXPressOverFiberBridgeAxiL + generic map ( + TPD_G => TPD_G) + port map ( + rxClk => phyClk312, + rxRst => phyRst312, + rxStatus => bridgeRxStatus, + axilClk => axilClk, + axilRst => axilRst, + axilReadMaster => axilReadMaster, + axilReadSlave => axilReadSlave, + axilWriteMaster => axilWriteMaster, + axilWriteSlave => axilWriteSlave); end mapping; diff --git a/protocols/packetizer/wrappers/AxiStreamBytePackerWrapper.vhd b/protocols/packetizer/wrappers/AxiStreamBytePackerWrapper.vhd new file mode 100644 index 0000000000..c1c724122d --- /dev/null +++ b/protocols/packetizer/wrappers/AxiStreamBytePackerWrapper.vhd @@ -0,0 +1,126 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing wrapper for surf.AxiStreamBytePacker +------------------------------------------------------------------------------- +-- 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 AxiStreamBytePackerWrapper is + generic ( + TPD_G : time := 1 ns; + RST_ASYNC_G : boolean := false; + SLAVE_BYTES_G : positive range 1 to 8 := 4; + MASTER_BYTES_G : positive range 1 to 8 := 8); + port ( + axisClk : in sl; + axisRst : in sl; + S_AXIS_TVALID : in sl; + S_AXIS_TDATA : in slv(8*SLAVE_BYTES_G-1 downto 0); + S_AXIS_TKEEP : in slv(SLAVE_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*SLAVE_BYTES_G-1 downto 0); + S_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(8*MASTER_BYTES_G-1 downto 0); + M_AXIS_TKEEP : out slv(MASTER_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*MASTER_BYTES_G-1 downto 0); + M_AXIS_TREADY : in sl); +end entity AxiStreamBytePackerWrapper; + +architecture rtl of AxiStreamBytePackerWrapper is + + constant SLAVE_CONFIG_C : AxiStreamConfigType := ( + TSTRB_EN_C => false, + TDATA_BYTES_C => SLAVE_BYTES_G, + TDEST_BITS_C => 0, + TID_BITS_C => 0, + TKEEP_MODE_C => TKEEP_COMP_C, + TUSER_BITS_C => 8, + TUSER_MODE_C => TUSER_FIRST_LAST_C); + + constant MASTER_CONFIG_C : AxiStreamConfigType := ( + TSTRB_EN_C => false, + TDATA_BYTES_C => MASTER_BYTES_G, + TDEST_BITS_C => 0, + TID_BITS_C => 0, + TKEEP_MODE_C => TKEEP_COMP_C, + TUSER_BITS_C => 8, + TUSER_MODE_C => TUSER_FIRST_LAST_C); + + signal sAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal mAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + +begin + + assert (MASTER_BYTES_G >= SLAVE_BYTES_G) + report "AxiStreamBytePackerWrapper does not support downsizing" severity failure; + + --------------- + -- Bus shims -- + --------------- + comb : process (S_AXIS_TDATA, S_AXIS_TDEST, S_AXIS_TID, S_AXIS_TKEEP, + S_AXIS_TLAST, S_AXIS_TUSER, + S_AXIS_TVALID, mAxisMaster) is + variable vS : AxiStreamMasterType; + begin + vS := AXI_STREAM_MASTER_INIT_C; + vS.tValid := S_AXIS_TVALID; + vS.tData := (others => '0'); + vS.tData(8*SLAVE_BYTES_G-1 downto 0) := S_AXIS_TDATA; + vS.tStrb := (others => '0'); + vS.tStrb(SLAVE_BYTES_G-1 downto 0) := S_AXIS_TKEEP; + vS.tKeep := (others => '0'); + vS.tKeep(SLAVE_BYTES_G-1 downto 0) := S_AXIS_TKEEP; + vS.tLast := S_AXIS_TLAST; + vS.tDest(7 downto 0) := S_AXIS_TDEST; + vS.tId(7 downto 0) := S_AXIS_TID; + vS.tUser := (others => '0'); + vS.tUser(8*SLAVE_BYTES_G-1 downto 0) := S_AXIS_TUSER; + + sAxisMaster <= vS; + + S_AXIS_TREADY <= '1'; + M_AXIS_TVALID <= mAxisMaster.tValid; + M_AXIS_TDATA <= mAxisMaster.tData(8*MASTER_BYTES_G-1 downto 0); + M_AXIS_TKEEP <= mAxisMaster.tKeep(MASTER_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*MASTER_BYTES_G-1 downto 0); + end process comb; + + --------------------- + -- DUT instancing -- + --------------------- + U_DUT : entity surf.AxiStreamBytePacker + generic map ( + TPD_G => TPD_G, + RST_ASYNC_G => RST_ASYNC_G, + SLAVE_CONFIG_G => SLAVE_CONFIG_C, + MASTER_CONFIG_G => MASTER_CONFIG_C) + port map ( + axiClk => axisClk, + axiRst => axisRst, + sAxisMaster => sAxisMaster, + mAxisMaster => mAxisMaster); + +end architecture rtl; diff --git a/protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd b/protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd new file mode 100644 index 0000000000..29c80b1668 --- /dev/null +++ b/protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd @@ -0,0 +1,138 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing wrapper for surf.AxiStreamDepacketizer2 +------------------------------------------------------------------------------- +-- 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; +use surf.AxiStreamPacketizer2Pkg.all; + +entity AxiStreamDepacketizer2Wrapper is + generic ( + TPD_G : time := 1 ns; + RST_POLARITY_G : sl := '1'; + RST_ASYNC_G : boolean := false; + MEMORY_TYPE_G : string := "distributed"; + REG_EN_G : boolean := false; + CRC_PIPELINE_G : natural range 0 to 1 := 0; + CRC_MODE_G : string := "NONE"; + SEQ_CNT_SIZE_G : natural range 0 to 16 := 16; + TDEST_BITS_G : natural := 8; + INPUT_PIPE_STAGES_G : natural := 0; + OUTPUT_PIPE_STAGES_G : natural := 1); + port ( + axisClk : in sl; + axisRst : in sl; + linkGood : in sl; + debugOut : out slv(12 downto 0); + S_AXIS_TVALID : in sl; + S_AXIS_TDATA : in slv(63 downto 0); + S_AXIS_TKEEP : in slv(7 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(15 downto 0); + S_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(63 downto 0); + M_AXIS_TKEEP : out slv(7 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(63 downto 0); + M_AXIS_TREADY : in sl); +end entity AxiStreamDepacketizer2Wrapper; + +architecture rtl of AxiStreamDepacketizer2Wrapper is + + signal debug : Packetizer2DebugType := PACKETIZER2_DEBUG_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 + + debugOut <= debug.initDone & debug.sof & debug.eof & debug.eofe & debug.sop & + debug.eop & debug.packetError & debug.sofError & debug.seqError & + debug.versionError & debug.crcModeError & debug.eofeError & + debug.crcError; + + --------------- + -- 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(63 downto 0) := S_AXIS_TDATA; + vS.tStrb := (others => '0'); + vS.tStrb(7 downto 0) := S_AXIS_TKEEP; + vS.tKeep := (others => '0'); + vS.tKeep(7 downto 0) := S_AXIS_TKEEP; + vS.tLast := S_AXIS_TLAST; + vS.tDest(7 downto 0) := S_AXIS_TDEST; + vS.tId(7 downto 0) := S_AXIS_TID; + vS.tUser := (others => '0'); + vS.tUser(15 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(63 downto 0); + M_AXIS_TKEEP <= mAxisMaster.tKeep(7 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(63 downto 0); + end process comb; + + --------------------- + -- DUT instancing -- + --------------------- + U_DUT : entity surf.AxiStreamDepacketizer2 + generic map ( + TPD_G => TPD_G, + RST_POLARITY_G => RST_POLARITY_G, + RST_ASYNC_G => RST_ASYNC_G, + MEMORY_TYPE_G => MEMORY_TYPE_G, + REG_EN_G => REG_EN_G, + CRC_PIPELINE_G => CRC_PIPELINE_G, + CRC_MODE_G => CRC_MODE_G, + SEQ_CNT_SIZE_G => SEQ_CNT_SIZE_G, + TDEST_BITS_G => TDEST_BITS_G, + INPUT_PIPE_STAGES_G => INPUT_PIPE_STAGES_G, + OUTPUT_PIPE_STAGES_G => OUTPUT_PIPE_STAGES_G) + port map ( + axisClk => axisClk, + axisRst => axisRst, + linkGood => linkGood, + debug => debug, + sAxisMaster => sAxisMaster, + sAxisSlave => sAxisSlave, + mAxisMaster => mAxisMaster, + mAxisSlave => mAxisSlave); + +end architecture rtl; diff --git a/protocols/packetizer/wrappers/AxiStreamDepacketizerWrapper.vhd b/protocols/packetizer/wrappers/AxiStreamDepacketizerWrapper.vhd new file mode 100644 index 0000000000..87b5adbf4f --- /dev/null +++ b/protocols/packetizer/wrappers/AxiStreamDepacketizerWrapper.vhd @@ -0,0 +1,115 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing wrapper for surf.AxiStreamDepacketizer +------------------------------------------------------------------------------- +-- 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 AxiStreamDepacketizerWrapper is + generic ( + TPD_G : time := 1 ns; + RST_ASYNC_G : boolean := false; + INPUT_PIPE_STAGES_G : integer := 0; + OUTPUT_PIPE_STAGES_G : integer := 0); + port ( + axisClk : in sl; + axisRst : in sl; + restart : in sl; + S_AXIS_TVALID : in sl; + S_AXIS_TDATA : in slv(63 downto 0); + S_AXIS_TKEEP : in slv(7 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(15 downto 0); + S_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(63 downto 0); + M_AXIS_TKEEP : out slv(7 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(63 downto 0); + M_AXIS_TREADY : in sl); +end entity AxiStreamDepacketizerWrapper; + +architecture rtl of AxiStreamDepacketizerWrapper is + + 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(63 downto 0) := S_AXIS_TDATA; + vS.tStrb := (others => '0'); + vS.tStrb(7 downto 0) := S_AXIS_TKEEP; + vS.tKeep := (others => '0'); + vS.tKeep(7 downto 0) := S_AXIS_TKEEP; + vS.tLast := S_AXIS_TLAST; + vS.tDest(7 downto 0) := S_AXIS_TDEST; + vS.tId(7 downto 0) := S_AXIS_TID; + vS.tUser := (others => '0'); + vS.tUser(15 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(63 downto 0); + M_AXIS_TKEEP <= mAxisMaster.tKeep(7 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(63 downto 0); + end process comb; + + --------------------- + -- DUT instancing -- + --------------------- + U_DUT : entity surf.AxiStreamDepacketizer + generic map ( + TPD_G => TPD_G, + RST_ASYNC_G => RST_ASYNC_G, + INPUT_PIPE_STAGES_G => INPUT_PIPE_STAGES_G, + OUTPUT_PIPE_STAGES_G => OUTPUT_PIPE_STAGES_G) + port map ( + axisClk => axisClk, + axisRst => axisRst, + restart => restart, + sAxisMaster => sAxisMaster, + sAxisSlave => sAxisSlave, + mAxisMaster => mAxisMaster, + mAxisSlave => mAxisSlave); + +end architecture rtl; diff --git a/protocols/packetizer/wrappers/AxiStreamPacketizer2LoopbackWrapper.vhd b/protocols/packetizer/wrappers/AxiStreamPacketizer2LoopbackWrapper.vhd new file mode 100644 index 0000000000..5b30b3389c --- /dev/null +++ b/protocols/packetizer/wrappers/AxiStreamPacketizer2LoopbackWrapper.vhd @@ -0,0 +1,163 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing AxiStreamPacketizer2/AxiStreamDepacketizer2 loopback +------------------------------------------------------------------------------- +-- 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; +use surf.AxiStreamPacketizer2Pkg.all; + +entity AxiStreamPacketizer2LoopbackWrapper is + generic ( + TPD_G : time := 1 ns; + RST_POLARITY_G : sl := '1'; + RST_ASYNC_G : boolean := false; + MEMORY_TYPE_G : string := "distributed"; + REG_EN_G : boolean := false; + CRC_PIPELINE_G : natural range 0 to 1 := 0; + CRC_MODE_G : string := "NONE"; + MAX_PACKET_BYTES_G : positive := 64; + SEQ_CNT_SIZE_G : positive range 4 to 16 := 16; + TDEST_BITS_G : natural := 8; + INPUT_PIPE_STAGES_G : natural := 0; + OUTPUT_PIPE_STAGES_G : natural := 1); + port ( + axisClk : in sl; + axisRst : in sl; + linkGood : in sl; + maxPktBytes : in slv(bitSize(MAX_PACKET_BYTES_G)-1 downto 0) := toSlv(MAX_PACKET_BYTES_G, bitSize(MAX_PACKET_BYTES_G)); + rearbitrate : out sl; + debugOut : out slv(12 downto 0); + S_AXIS_TVALID : in sl; + S_AXIS_TDATA : in slv(63 downto 0); + S_AXIS_TKEEP : in slv(7 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(63 downto 0); + S_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(63 downto 0); + M_AXIS_TKEEP : out slv(7 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(63 downto 0); + M_AXIS_TREADY : in sl); +end entity AxiStreamPacketizer2LoopbackWrapper; + +architecture rtl of AxiStreamPacketizer2LoopbackWrapper is + + signal debug : Packetizer2DebugType := PACKETIZER2_DEBUG_INIT_C; + signal sAppAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal sAppAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + signal pktAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal pktAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + signal mAppAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal mAppAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + +begin + + debugOut <= debug.initDone & debug.sof & debug.eof & debug.eofe & debug.sop & + debug.eop & debug.packetError & debug.sofError & debug.seqError & + debug.versionError & debug.crcModeError & debug.eofeError & + debug.crcError; + + --------------- + -- 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, + mAppAxisMaster, sAppAxisSlave) is + variable vS : AxiStreamMasterType; + variable vM : AxiStreamSlaveType; + begin + vS := AXI_STREAM_MASTER_INIT_C; + vS.tValid := S_AXIS_TVALID; + vS.tData(63 downto 0) := S_AXIS_TDATA; + vS.tStrb := (others => '0'); + vS.tStrb(7 downto 0) := S_AXIS_TKEEP; + vS.tKeep := (others => '0'); + vS.tKeep(7 downto 0) := S_AXIS_TKEEP; + vS.tLast := S_AXIS_TLAST; + vS.tDest(7 downto 0) := S_AXIS_TDEST; + vS.tId(7 downto 0) := S_AXIS_TID; + vS.tUser := (others => '0'); + vS.tUser(63 downto 0) := S_AXIS_TUSER; + + vM := AXI_STREAM_SLAVE_INIT_C; + vM.tReady := M_AXIS_TREADY; + + sAppAxisMaster <= vS; + mAppAxisSlave <= vM; + + S_AXIS_TREADY <= sAppAxisSlave.tReady; + M_AXIS_TVALID <= mAppAxisMaster.tValid; + M_AXIS_TDATA <= mAppAxisMaster.tData(63 downto 0); + M_AXIS_TKEEP <= mAppAxisMaster.tKeep(7 downto 0); + M_AXIS_TLAST <= mAppAxisMaster.tLast; + M_AXIS_TDEST <= mAppAxisMaster.tDest(7 downto 0); + M_AXIS_TID <= mAppAxisMaster.tId(7 downto 0); + M_AXIS_TUSER <= mAppAxisMaster.tUser(63 downto 0); + end process comb; + + U_Packetizer : entity surf.AxiStreamPacketizer2 + generic map ( + TPD_G => TPD_G, + RST_POLARITY_G => RST_POLARITY_G, + RST_ASYNC_G => RST_ASYNC_G, + MEMORY_TYPE_G => MEMORY_TYPE_G, + REG_EN_G => REG_EN_G, + CRC_MODE_G => CRC_MODE_G, + MAX_PACKET_BYTES_G => MAX_PACKET_BYTES_G, + SEQ_CNT_SIZE_G => SEQ_CNT_SIZE_G, + TDEST_BITS_G => TDEST_BITS_G, + INPUT_PIPE_STAGES_G => INPUT_PIPE_STAGES_G, + OUTPUT_PIPE_STAGES_G => 0) + port map ( + axisClk => axisClk, + axisRst => axisRst, + rearbitrate => rearbitrate, + maxPktBytes => maxPktBytes, + sAxisMaster => sAppAxisMaster, + sAxisSlave => sAppAxisSlave, + mAxisMaster => pktAxisMaster, + mAxisSlave => pktAxisSlave); + + U_Depacketizer : entity surf.AxiStreamDepacketizer2 + generic map ( + TPD_G => TPD_G, + RST_POLARITY_G => RST_POLARITY_G, + RST_ASYNC_G => RST_ASYNC_G, + MEMORY_TYPE_G => MEMORY_TYPE_G, + REG_EN_G => REG_EN_G, + CRC_PIPELINE_G => CRC_PIPELINE_G, + CRC_MODE_G => CRC_MODE_G, + SEQ_CNT_SIZE_G => SEQ_CNT_SIZE_G, + TDEST_BITS_G => TDEST_BITS_G, + INPUT_PIPE_STAGES_G => 0, + OUTPUT_PIPE_STAGES_G => OUTPUT_PIPE_STAGES_G) + port map ( + axisClk => axisClk, + axisRst => axisRst, + linkGood => linkGood, + debug => debug, + sAxisMaster => pktAxisMaster, + sAxisSlave => pktAxisSlave, + mAxisMaster => mAppAxisMaster, + mAxisSlave => mAppAxisSlave); + +end architecture rtl; diff --git a/protocols/packetizer/wrappers/AxiStreamPacketizer2Wrapper.vhd b/protocols/packetizer/wrappers/AxiStreamPacketizer2Wrapper.vhd new file mode 100644 index 0000000000..411219ece3 --- /dev/null +++ b/protocols/packetizer/wrappers/AxiStreamPacketizer2Wrapper.vhd @@ -0,0 +1,135 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing wrapper for surf.AxiStreamPacketizer2 +------------------------------------------------------------------------------- +-- 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 AxiStreamPacketizer2Wrapper is + generic ( + TPD_G : time := 1 ns; + RST_POLARITY_G : sl := '1'; + RST_ASYNC_G : boolean := false; + MEMORY_TYPE_G : string := "distributed"; + REG_EN_G : boolean := false; + CRC_MODE_G : string := "NONE"; + MAX_PACKET_BYTES_G : positive := 64; + SEQ_CNT_SIZE_G : positive range 4 to 16 := 16; + TDEST_BITS_G : natural := 8; + OUTPUT_TDEST_G : slv(7 downto 0) := (others => '0'); + OUTPUT_TID_G : slv(7 downto 0) := (others => '0'); + INPUT_PIPE_STAGES_G : natural := 0; + OUTPUT_PIPE_STAGES_G : natural := 0); + port ( + axisClk : in sl; + axisRst : in sl; + maxPktBytes : in slv(bitSize(MAX_PACKET_BYTES_G)-1 downto 0) := toSlv(MAX_PACKET_BYTES_G, bitSize(MAX_PACKET_BYTES_G)); + rearbitrate : out sl; + S_AXIS_TVALID : in sl; + S_AXIS_TDATA : in slv(63 downto 0); + S_AXIS_TKEEP : in slv(7 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(63 downto 0); + S_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(63 downto 0); + M_AXIS_TKEEP : out slv(7 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(15 downto 0); + M_AXIS_TREADY : in sl); +end entity AxiStreamPacketizer2Wrapper; + +architecture rtl of AxiStreamPacketizer2Wrapper is + + 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(63 downto 0) := S_AXIS_TDATA; + vS.tStrb := (others => '0'); + vS.tStrb(7 downto 0) := S_AXIS_TKEEP; + vS.tKeep := (others => '0'); + vS.tKeep(7 downto 0) := S_AXIS_TKEEP; + vS.tLast := S_AXIS_TLAST; + vS.tDest(7 downto 0) := S_AXIS_TDEST; + vS.tId(7 downto 0) := S_AXIS_TID; + vS.tUser := (others => '0'); + vS.tUser(63 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(63 downto 0); + M_AXIS_TKEEP <= mAxisMaster.tKeep(7 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(15 downto 0); + end process comb; + + --------------------- + -- DUT instancing -- + --------------------- + U_DUT : entity surf.AxiStreamPacketizer2 + generic map ( + TPD_G => TPD_G, + RST_POLARITY_G => RST_POLARITY_G, + RST_ASYNC_G => RST_ASYNC_G, + MEMORY_TYPE_G => MEMORY_TYPE_G, + REG_EN_G => REG_EN_G, + CRC_MODE_G => CRC_MODE_G, + MAX_PACKET_BYTES_G => MAX_PACKET_BYTES_G, + SEQ_CNT_SIZE_G => SEQ_CNT_SIZE_G, + TDEST_BITS_G => TDEST_BITS_G, + OUTPUT_TDEST_G => OUTPUT_TDEST_G, + OUTPUT_TID_G => OUTPUT_TID_G, + INPUT_PIPE_STAGES_G => INPUT_PIPE_STAGES_G, + OUTPUT_PIPE_STAGES_G => OUTPUT_PIPE_STAGES_G) + port map ( + axisClk => axisClk, + axisRst => axisRst, + rearbitrate => rearbitrate, + maxPktBytes => maxPktBytes, + sAxisMaster => sAxisMaster, + sAxisSlave => sAxisSlave, + mAxisMaster => mAxisMaster, + mAxisSlave => mAxisSlave); + +end architecture rtl; diff --git a/protocols/packetizer/wrappers/AxiStreamPacketizerWrapper.vhd b/protocols/packetizer/wrappers/AxiStreamPacketizerWrapper.vhd new file mode 100644 index 0000000000..4a75737e42 --- /dev/null +++ b/protocols/packetizer/wrappers/AxiStreamPacketizerWrapper.vhd @@ -0,0 +1,121 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing wrapper for surf.AxiStreamPacketizer +------------------------------------------------------------------------------- +-- 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 AxiStreamPacketizerWrapper is + generic ( + TPD_G : time := 1 ns; + RST_ASYNC_G : boolean := false; + MAX_PACKET_BYTES_G : integer := 64; + MIN_TKEEP_G : slv(7 downto 0) := x"01"; + OUTPUT_SSI_G : boolean := true; + INPUT_PIPE_STAGES_G : integer := 0; + OUTPUT_PIPE_STAGES_G : integer := 0); + port ( + axisClk : in sl; + axisRst : in sl; + maxPktBytes : in slv(bitSize(MAX_PACKET_BYTES_G)-1 downto 0) := toSlv(MAX_PACKET_BYTES_G, bitSize(MAX_PACKET_BYTES_G)); + S_AXIS_TVALID : in sl; + S_AXIS_TDATA : in slv(63 downto 0); + S_AXIS_TKEEP : in slv(7 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(63 downto 0); + S_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(63 downto 0); + M_AXIS_TKEEP : out slv(7 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(15 downto 0); + M_AXIS_TREADY : in sl); +end entity AxiStreamPacketizerWrapper; + +architecture rtl of AxiStreamPacketizerWrapper is + + 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(63 downto 0) := S_AXIS_TDATA; + vS.tStrb := (others => '0'); + vS.tStrb(7 downto 0) := S_AXIS_TKEEP; + vS.tKeep := (others => '0'); + vS.tKeep(7 downto 0) := S_AXIS_TKEEP; + vS.tLast := S_AXIS_TLAST; + vS.tDest(7 downto 0) := S_AXIS_TDEST; + vS.tId(7 downto 0) := S_AXIS_TID; + vS.tUser := (others => '0'); + vS.tUser(63 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(63 downto 0); + M_AXIS_TKEEP <= mAxisMaster.tKeep(7 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(15 downto 0); + end process comb; + + --------------------- + -- DUT instancing -- + --------------------- + U_DUT : entity surf.AxiStreamPacketizer + generic map ( + TPD_G => TPD_G, + RST_ASYNC_G => RST_ASYNC_G, + MAX_PACKET_BYTES_G => MAX_PACKET_BYTES_G, + MIN_TKEEP_G => MIN_TKEEP_G, + OUTPUT_SSI_G => OUTPUT_SSI_G, + INPUT_PIPE_STAGES_G => INPUT_PIPE_STAGES_G, + OUTPUT_PIPE_STAGES_G => OUTPUT_PIPE_STAGES_G) + port map ( + axisClk => axisClk, + axisRst => axisRst, + maxPktBytes => maxPktBytes, + sAxisMaster => sAxisMaster, + sAxisSlave => sAxisSlave, + mAxisMaster => mAxisMaster, + mAxisSlave => mAxisSlave); + +end architecture rtl; diff --git a/python/surf/protocols/coaxpress/_CoaXPressOverFiberBridgeAxiL.py b/python/surf/protocols/coaxpress/_CoaXPressOverFiberBridgeAxiL.py new file mode 100644 index 0000000000..80c92ddeab --- /dev/null +++ b/python/surf/protocols/coaxpress/_CoaXPressOverFiberBridgeAxiL.py @@ -0,0 +1,177 @@ +#----------------------------------------------------------------------------- +# 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. +#----------------------------------------------------------------------------- + +import pyrogue as pr + + +class CoaXPressOverFiberBridgeAxiL(pr.Device): + def __init__(self, + statusCountBits = 16, + **kwargs): + super().__init__(**kwargs) + + rxErrorCodeEnum = { + 0x0: 'None', + 0x1: 'BadControl', + 0x2: 'Overwrite', + 0x3: 'IdleError', + 0x4: 'PayloadAbort', + 0x5: 'SeqMismatch', + 0x6: 'HkpMalformed', + 0x7: 'HkpBadKCode', + } + + hkpTypeEnum = { + 0x0: 'None', + 0x1: 'KCode', + 0x2: 'SOP', + 0x3: 'EOP', + 0x4: 'Trigger', + 0x5: 'IoAck', + 0x6: 'Marker', + 0xF: 'Invalid', + } + + statusBits = [ + ('RxError', 'Bridge RX error pulse'), + ('RxAbort', 'Bridge RX abort pulse'), + ('SeqValid', 'Received CXPoF /Q/ sequence ordered set'), + ('SeqError', 'CXPoF /Q/ sequence mismatch'), + ('HkpValid', 'Received CXPoF HKP word'), + ('HkpError', 'Malformed CXPoF HKP word'), + ] + + for bit, (name, description) in enumerate(statusBits): + self.add(pr.RemoteVariable( + name = f'{name}Sticky', + description = f'Sticky status bit: {description}', + offset = 0x000, + bitSize = 1, + bitOffset = bit, + mode = 'RO', + base = pr.Bool, + pollInterval = 1, + )) + + self.add(pr.RemoteVariable( + name = 'RxErrorCode', + description = 'Last bridge RX error cause code', + offset = 0x004, + bitSize = 4, + mode = 'RO', + enum = rxErrorCodeEnum, + pollInterval = 1, + )) + + self.add(pr.RemoteVariable( + name = 'SeqData', + description = 'Last received CXPoF /Q/ sequence value', + offset = 0x008, + bitSize = 24, + mode = 'RO', + disp = '0x{:06X}', + pollInterval = 1, + )) + + self.add(pr.RemoteVariable( + name = 'SeqExpected', + description = 'Next expected CXPoF /Q/ sequence value', + offset = 0x00C, + bitSize = 24, + mode = 'RO', + disp = '0x{:06X}', + pollInterval = 1, + )) + + self.add(pr.RemoteVariable( + name = 'SeqErrorExpected', + description = 'Expected CXPoF /Q/ sequence value captured on mismatch', + offset = 0x010, + bitSize = 24, + mode = 'RO', + disp = '0x{:06X}', + pollInterval = 1, + )) + + self.add(pr.RemoteVariable( + name = 'HkpData', + description = 'Last received CXPoF High-Speed K-Code Payload word', + offset = 0x014, + bitSize = 32, + mode = 'RO', + disp = '0x{:08X}', + pollInterval = 1, + )) + + self.add(pr.RemoteVariable( + name = 'HkpWordCount', + description = 'Word count of the last received CXPoF HKP word', + offset = 0x018, + bitSize = 8, + bitOffset = 0, + mode = 'RO', + disp = '{:d}', + pollInterval = 1, + )) + + self.add(pr.RemoteVariable( + name = 'HkpKCodeMask', + description = 'Per-byte CXP K-code mask for the last received HKP word', + offset = 0x018, + bitSize = 4, + bitOffset = 8, + mode = 'RO', + disp = '0x{:X}', + pollInterval = 1, + )) + + self.add(pr.RemoteVariable( + name = 'HkpKCodeValid', + description = 'True when every byte of the last received HKP word is a CXP K-code', + offset = 0x018, + bitSize = 1, + bitOffset = 12, + mode = 'RO', + base = pr.Bool, + pollInterval = 1, + )) + + self.add(pr.RemoteVariable( + name = 'HkpType', + description = 'Classified type of the last received HKP K-code word', + offset = 0x018, + bitSize = 4, + bitOffset = 16, + mode = 'RO', + enum = hkpTypeEnum, + pollInterval = 1, + )) + + for i, (name, description) in enumerate(statusBits): + self.add(pr.RemoteVariable( + name = f'{name}Cnt', + description = f'Counter for {description}', + offset = 0x020 + (4*i), + bitSize = statusCountBits, + mode = 'RO', + disp = '{:d}', + pollInterval = 1, + )) + + self.add(pr.RemoteCommand( + name = 'CountReset', + description = 'Reset bridge RX sticky status and counters', + offset = 0x03C, + bitSize = 1, + function = pr.BaseCommand.touchOne, + )) + + def countReset(self): + self.CountReset() diff --git a/python/surf/protocols/coaxpress/__init__.py b/python/surf/protocols/coaxpress/__init__.py index d90f51e984..e41d5c94d1 100644 --- a/python/surf/protocols/coaxpress/__init__.py +++ b/python/surf/protocols/coaxpress/__init__.py @@ -8,6 +8,7 @@ ## the terms contained in the LICENSE.txt file. ############################################################################## from surf.protocols.coaxpress._CoaXPressAxiL import * +from surf.protocols.coaxpress._CoaXPressOverFiberBridgeAxiL import * from surf.protocols.coaxpress._Bootstrap import * from surf.protocols.coaxpress._PhantomS991 import * diff --git a/tests/protocols/coaxpress/README.md b/tests/protocols/coaxpress/README.md index f67dab1496..5e9bdba0ec 100644 --- a/tests/protocols/coaxpress/README.md +++ b/tests/protocols/coaxpress/README.md @@ -49,19 +49,20 @@ intentional limitation, not as silent proof of complete spec compliance. | Test file | DUT surface | Main spec relation | Status | | --- | --- | --- | --- | -| `test_CoaXPressRxWordPacker.py` | `CoaXPressRxWordPacker` | Internal packing helper for receive-path word assembly; not a direct protocol-surface spec bench | RTL-contract | -| `test_CoaXPressRxLaneMux.py` | `CoaXPressRxLaneMux` | Internal lane arbitration and frame-boundary behavior; not a direct protocol-surface spec bench | RTL-contract | -| `test_CoaXPressRxLane.py` | `CoaXPressRxLane` | `CXP-001-2021` packet-type decode, `IO_ACK`, control acknowledgments, heartbeat prefix handling, truncated-event guardrails, stream header fields | Partial protocol | -| `test_CoaXPressRxHsFsm.py` | `CoaXPressRxHsFsm` | Rectangular image header and line marker handling from section `10.4.6.2` / `10.4.6.3`, including a dual-lane step/alignment case | Near-normative subset | -| `test_CoaXPressRx.py` | `CoaXPressRx` | One-lane control/event assembly plus dual-lane receive rotation/alignment through the lane mux and HS FSM | Partial protocol | +| `test_CoaXPressRxWordPacker.py` | `CoaXPressRxWordPacker` | Internal packing helper for receive-path word assembly, including handshake hold behavior and SSI `EOFE` propagation; not a direct protocol-surface spec bench | RTL-contract | +| `test_CoaXPressRxLaneMux.py` | `CoaXPressRxLaneMux` | Internal lane arbitration and trailer-marker-gated frame-boundary behavior; not a direct protocol-surface spec bench | RTL-contract | +| `test_CoaXPressRxLane.py` | `CoaXPressRxLane` | `CXP-001-2021` packet-type decode, `IO_ACK`, control acknowledgments, heartbeat payload/trailer handling, bounded event payload validate-before-release plus trailer-gated ACK, stream header/trailer framing, and malformed-packet `rxError` pulses | Partial protocol | +| `test_CoaXPressRxHsFsm.py` | `CoaXPressRxHsFsm` | Rectangular image header and line marker handling from section `10.4.6.2` / `10.4.6.3`, including dual-lane step/alignment, incomplete-frame new-header detection, and trailer-verdict-gated SSI `EOFE` on the final image beat | Near-normative subset | +| `test_CoaXPressRx.py` | `CoaXPressRx` | One-lane control/event-payload assembly plus multi-lane receive rotation/alignment through the lane mux and HS FSM, including malformed stream-trailer `EOFE` recovery | Partial protocol | | `test_CoaXPressEventAckMsg.py` | `CoaXPressEventAckMsg` | Event acknowledgment wire format, section `9.8.3`, Table 30 | Near-normative subset | | `test_CoaXPressTxLsFsm.py` | `CoaXPressTxLsFsm` | Low-speed idle cadence and default trigger serialization, section `9.3.1.1` / Table 15 | Partial protocol | | `test_CoaXPressTx.py` | `CoaXPressTx` | Control/event-acknowledgment arbitration and software-trigger path across the TX assembly | RTL-contract with spec packet classes | -| `test_CoaXPressConfig.py` | `CoaXPressConfig` | Control command packet formatting, CRC generation, tag handling, and SRPv3 response completion through the real `SrpV3AxiLite` ingress path, section `9.6.1.2` / `9.6.2` | Near-normative subset | +| `test_CoaXPressConfig.py` | `CoaXPressConfig` | Control command packet formatting, CRC generation, tag handling, timeout/status-error responses, and SRPv3 response completion through the real `SrpV3AxiLite` ingress path, section `9.6.1.2` / `9.6.2` | Near-normative subset | | `test_CoaXPressCore.py` | `CoaXPressCore` | AXI-Lite control of tagged config request generation plus software-visible `RxOverflowCnt` / `RxFsmErrorCnt` status behavior at the full-core boundary | RTL-contract with spec request prefix and top-level error-status checks | | `test_CoaXPressOverFiberBridgeTx.py` | `CoaXPressOverFiberBridgeTx` | CXPoF start/control/payload/terminate words, section `6.3.1` to `6.3.6` in `CXPR-008-2021` | Near-normative subset | -| `test_CoaXPressOverFiberBridgeRx.py` | `CoaXPressOverFiberBridgeRx` | CXPoF start-word decode back into CoaXPress packet and `IO_ACK` words | Partial protocol | -| `test_CoaXPressOverFiberBridge.py` | `CoaXPressOverFiberBridge` | Top-level 32b/64b gearbox integration around the bridge leaf mapping | RTL-contract with spec framing | +| `test_CoaXPressOverFiberBridgeRx.py` | `CoaXPressOverFiberBridgeRx` | CXPoF start-word decode back into CoaXPress packet and `IO_ACK` words, `/Q/` sequence tracking, classified `/E/` abort/error status, and HKP status parsing | Partial protocol | +| `test_CoaXPressOverFiberBridge.py` | `CoaXPressOverFiberBridge` | Top-level 32b/64b gearbox integration around the bridge leaf mapping and RX bridge status forwarding | RTL-contract with spec framing | +| `test_CoaXPressOverFiberBridgeAxiL.py` | `CoaXPressOverFiberBridgeAxiL` | Software-visible sticky bridge RX status, last-observed sequence/HKP fields, event counters, reset behavior, and HKP classification readback | RTL-contract status consumer | ## Spec Section Notes @@ -105,29 +106,36 @@ exposed by the current checked-in RTL. The current checked-in coverage is split: - `test_CoaXPressConfig.py` - - checks untagged read and tagged write control-command formatting for - section `9.6.1.2` and `9.6.2` + - checks all four tagged/untagged read/write control-command formatting + quadrants for section `9.6.1.2` and `9.6.2` - drives requests through the real `CoaXPressConfig` / `SrpV3AxiLite` ingress path and validates both the serialized config packet and the completed SRPv3 response + - covers config-response timeout and nonzero control-ack status mapping into + the local SRPv3 AXI-Lite error footer - `test_CoaXPressRxLane.py` and `test_CoaXPressRx.py` - now drive fuller control-ack shapes on the wire: code, size, reply data, - CRC placeholder, and `EOP` - - these benches prove the subset the current receive RTL actually consumes + CRC, and `EOP` + - prove that receive-side control acknowledgments are forwarded only after + CRC and `EOP` validation pass Important limitation: -- `CoaXPressRxLane` does not currently validate full normative acknowledgment - semantics end to end -- it consumes only the reduced subset needed by the present receive assembly +- `CoaXPressRxLane` now validates the acknowledgment packet trailer before + pulsing `cfgMaster`, and malformed acknowledgment trailers pulse `rxError`, + but it still consumes only the reduced code/size/data subset needed by the + present receive assembly rather than exposing a richer application-facing + acknowledgment parser ### Heartbeat and event traffic Heartbeat and event handling is still intentionally narrow, but the receive -event parser now checks complete packet framing before acknowledging: +parsers now check complete packet framing before producing output pulses: - `test_CoaXPressRxLane.py` - checks the current 12-byte heartbeat payload collector + - validates heartbeat CRC/`EOP` before forwarding the heartbeat word and + suppresses bad-CRC heartbeat packets - `test_CoaXPressEventAckMsg.py` - covers event acknowledgment generation on the transmit side - `test_CoaXPressRxLane.py` and `test_CoaXPressRx.py` @@ -135,8 +143,8 @@ event parser now checks complete packet framing before acknowledging: payload words, CRC, and `EOP` - `CoaXPressRxLane` now acknowledges an event only after the CRC and `EOP` pass, suppresses bad-CRC events, and recovers for a later clean event - - event payload is validated for framing/CRC but is not exported through a - receive-side payload interface + - event payload is exported through the receive-side event stream with the + packet tag and event ID preserved as stream metadata That means these benches now cover the parser/acknowledgment subset of: @@ -144,8 +152,10 @@ That means these benches now cover the parser/acknowledgment subset of: - section `9.8.2` event payload parsing - event-payload CRC/trailer handling -They still do not prove an application-facing event-payload delivery contract, -because the current RTL exposes only `eventAck` and `eventTag`. +They now prove a bounded validate-before-release event-payload delivery contract: +payload words are withheld until the trailing CRC and `EOP` pass, bad-CRC events +do not leak payload words, and oversized events are rejected instead of being +partially forwarded. ### Stream data and rectangular image traffic @@ -154,16 +164,44 @@ The image-path benches are the strongest spec-aligned receive tests today: - `test_CoaXPressRxHsFsm.py` - validates rectangular image header and line marker handling against section `10.4.6.2` and `10.4.6.3` + - detects a new image header arriving before the previously declared frame's + line count has completed - `test_CoaXPressRx.py` - validates both the original one-lane top-level receive assembly and a dual-lane lane-rotation path around the same traffic - - also carries opt-in four-lane investigation benches behind - `RUN_KNOWN_ISSUE_TESTS=1`; those are intentionally not part of the - merge-ready passing slice yet + - validates four-lane short-frame rotation, malformed-header recovery, and + repeated single-line image-frame boundaries at the top-level receive + assembly + - also carries opt-in four-lane overflow stress benches behind + `RUN_STRESS_TESTS=1` because those workloads are intentionally heavier than + the normal regression slice `test_CoaXPressRxLane.py` also exercises stream packet handling using -spec-shaped stream headers, but the emphasis there is on receive-lane state -behavior rather than on a full normative stream CRC checker. +spec-shaped stream headers and CRC/`EOP` trailers. The RTL forwards payload as +it arrives, then publishes an in-order trailer verdict marker after the CRC and +`EOP` check. The receive assembly holds only the final packed SSI image beat +until that verdict arrives. If the stream trailer is malformed, the final image +beat is released with SSI `EOFE` set in `TUSER`; if the trailer is clean, the +final beat is released as a normal SSI EOF. This is not a buffered bad-payload +drop contract: earlier payload words may already have been forwarded, and the +downstream consumer is expected to reject or quarantine the frame based on the +terminal `EOFE` bit. Bad stream trailers pulse the lane-level `rxError`, which +the receive assembly aggregates into `rxFsmError` and the core exposes through +the existing `RxFsmErrorCnt` software counter. + +### Receive event payload stream + +`CoaXPressRxLane` now exposes event payload words on an AXI-stream style +`eventMaster` interface while preserving the legacy `eventAck/eventTag` trailer +completion pulse. The lane buffers up to 16 event payload words and releases +them only after the event CRC and `EOP` trailer pass. The event payload stream +uses `TDEST[7:0]` for the packet tag and publishes the event ID bytes on +`TUSER[31:0]` at the lane boundary. The `CoaXPressRx` assembly crosses that +payload stream into the `cfgClk` domain with an `AxiStreamFifoV2`. + +This is a bounded receive-side payload contract, not an unbounded event +transport. Event payloads longer than the internal store are reported as +malformed and suppressed. ### Software-visible overflow and FSM-error status @@ -179,14 +217,14 @@ software through `CoaXPressAxiL`: checks that the top-level counter increments, then verifies the count stays stable during idle cycles and that a later clean image transaction is still accepted -- `coaxpress_core_rx_overflow_does_not_trigger_fsm_error_storm_known_issue_test` - - checked in as an opt-in skipped investigation bench - - drives sustained receive-data backpressure with repeated one-line image - frames and encodes the expected software-facing behavior: overflow should - count first, `RxFsmErrorCnt` should stay at zero, idle should not create an - error storm, and a later clean frame should still pass - - enable locally with `RUN_KNOWN_ISSUE_TESTS=1` and optionally narrow the - stress volume with `CXP_RX_OVERFLOW_STORM_FRAME_COUNT` +- `coaxpress_core_rx_overflow_does_not_trigger_fsm_error_storm_test` + - drives sustained receive-data backpressure with long image lines and encodes + the expected software-facing behavior: overflow should count, + `RxFsmErrorCnt` should stay at zero, idle should not create an error storm, + and a later clean frame should still pass + - the default workload is sized to overflow the RX data path directly; it can + be tuned with `CXP_RX_OVERFLOW_STORM_FRAME_COUNT` and + `CXP_RX_OVERFLOW_STORM_LINE_WORD_COUNT` This is intentionally a top-level software-facing check, not a replacement for the lower-level malformed-header coverage in `test_CoaXPressRxHsFsm.py`. @@ -216,34 +254,75 @@ Current checked-in coverage: - embedded EOP K-code reconstruction for stream marker and packet-end words - HKP forwarding, including a housekeeping-to-payload transition and an HKP-carried CXP EOP word + - HKP K-code semantics: all-data nGMII control-mask enforcement, + per-byte K-code validation through `hkpKCodeMask/hkpKCodeValid`, and + whole-word classification through `hkpType` + - `hkpValid/hkpData/hkpEop/hkpSof/hkpWordCount` status for HKP words that are + forwarded on the reconstructed CXP side, plus `hkpError` for malformed HKP + control masks or invalid HKP K-code bytes + - lane-0 `/Q/` sequence tracking through `seqValid/seqData/seqExpected`, with + `seqError/seqErrorExpected` on skipped sequence values while preserving + no-output behavior on the CXP word stream + - classified `/E/` status through `rxError/rxAbort/rxErrorCode` for idle and + active-packet error ordered sets - negative lane-placement checks for `/S/`, `/Q/`, `/T/`, and `/E/` - - lane-0 `/Q/` no-output guardrail, `/E/` packet abort behavior before and + - lane-0 `/Q/` no-output behavior, `/E/` packet abort behavior before and after payload, and recovery to a following valid low-speed packet - `test_CoaXPressOverFiberBridge.py` - top-level 32b/64b gearbox integration around the bridge leaves - - RX-side 64b gearbox coverage for `/E/` abort/recovery, HKP-to-payload - transition, and lane-0 `/Q/` no-output/recovery guardrails - -Still open on the bridge side: - -- normative `/Q/` sequence handling beyond the current no-output/recovery guardrails -- fuller `/E/` error semantics beyond malformed-placement and abort-and-recover guardrails -- full housekeeping protocol semantics beyond raw HKP forwarding and the current - HKP-to-payload transition check + - RX-side 64b gearbox coverage for classified `/E/` abort/recovery, + HKP-to-payload status, and lane-0 `/Q/` sequence mismatch/no-output/recovery + guardrails +- `test_CoaXPressOverFiberBridgeAxiL.py` + - AXI-Lite readback of sticky bridge RX status bits, last-observed + sequence/HKP fields, and event counters + - write-one reset coverage for sticky status and counters + - named HKP K-code classification sweep through the packed HKP status register + +The product-facing bridge status contract is the `CxpofRxStatusType` record in +`CoaXPressPkg.vhd`. The cocotb benches use thin wrapper entities to flatten +that record back to scalar ports only because GHDL/cocotb does not reliably +expose top-level VHDL record fields as child handles. Those wrappers are a test +surface, not the intended RTL integration contract. + +`CoaXPressOverFiberBridgeAxiL` makes the bridge RX status software-visible for +the GT wrapper integrations. The GTH/GTY wrapper AXI-Lite port now reports +sticky status, last observed sequence/HKP fields, and counters instead of +returning only a default decode error. The current AxiL regression also sweeps +the named HKP K-code classifications through the packed HKP readback register +so software-visible consumers are not only checked against one EOP case. The +current register map is: + +- `0x000`: sticky status bits for `rxError`, `rxAbort`, `seqValid`, + `seqError`, `hkpValid`, and `hkpError` +- `0x004`: last `rxErrorCode` +- `0x008`: last `seqData` +- `0x00C`: last `seqExpected` +- `0x010`: last `seqErrorExpected` +- `0x014`: last `hkpData` +- `0x018`: packed HKP status: `hkpWordCount[7:0]`, `hkpKCodeMask[11:8]`, + `hkpKCodeValid[12]`, and `hkpType[19:16]` +- `0x020` to `0x034`: event counters for the six sticky status bits above +- `0x03C`: write-one counter/sticky reset strobe Current RTL support limits observed while expanding the bridge tests: -- `/Q/` ordered sets are not decoded into any bridge-visible state, sequence - tracker, status output, or CXP-side indication. The current contract is only - that `/Q/` in the interpacket gap is suppressed and later valid traffic - recovers. -- `/E/` has no bridge-visible status output. When it appears during a packet, - the RX bridge aborts the active nGMII packet and returns to idle; if the start - word was already accepted, the CXP `SOP` and packet-type words may already - have been emitted, but no synthetic CXP `EOP` is generated. -- HKP handling is raw forwarding. The RX bridge does not validate HKP content - semantics or expose a separate housekeeping parser; it reconstructs K-coded - words and then returns to normal payload/EOP handling. +- `/Q/` ordered sets are not decoded into the CXP-side word stream. The current + contract initializes on the first sequence word, expects a 24-bit increment on + each later `/Q/`, pulses `seqError` on mismatch, reports the expected value, + and resynchronizes from the received value. +- `/E/` is published through `rxError`, `rxAbort`, and `rxErrorCode` status. Idle + `/E/`, active-payload `/E/`, malformed control placement, overwrite, sequence + mismatch, and malformed HKP conditions now have distinct cause codes. When + `/E/` appears during a packet, the RX bridge aborts the active nGMII packet + and returns to idle; if the start word was already accepted, the CXP `SOP` and + packet-type words may already have been emitted, but no synthetic CXP `EOP` is + generated. +- HKP handling now follows the CXPoF High-Speed K-Code Payload contract: HKP is + received with nGMII control flags clear, reconstructed on the CXP side with + K-code flags asserted, validated as K-code bytes, and classified as known CXP + K-code words where possible. HKP does not define a separate command opcode + layer in this bridge contract. ## Known Limitations @@ -252,25 +331,36 @@ compliance coverage. The most important open limits are: -- `CoaXPressRxHsFsm` still has an open bonded-receive issue on back-to-back - short four-lane image frames: later one-word tails can miss `TLAST`, which - merges or truncates adjacent frames -- the gated four-lane `CoaXPressRx` investigation benches are therefore still - opt-in only; they exist to track clean-rotation, malformed-header recovery, - and backpressure/overflow recovery once the short-tail boundary bug is fixed -- the checked-in known-issue core bench for overflow-vs-FSM-error behavior is - skipped by default until the receive-side backpressure interaction is - understood and fixed -- receive-side event payload is validated for framing/CRC before ACK, but is not - exposed through an application-facing payload interface +- the four-lane overflow recovery checks are opt-in stress benches because they + intentionally fill and drain deep receive FIFOs; enable them with + `RUN_STRESS_TESTS=1` +- receive-side event payload is validated for framing/CRC before ACK and + released through a bounded application-facing payload interface +- the receive stream-data path now validates CRC/`EOP` trailer framing before + accepting the next packet and marks malformed frames with SSI `EOFE` on the + final image beat, but it still streams payload before the trailer result is + known instead of buffering and dropping a bad frame internally - trigger coverage still does not include the broader low-speed extra modes or the full high-speed trigger matrix, though the low-speed FSM now covers active-pulse shortening through a runtime `txPulseWidth` update -- CXPoF bridge coverage still does not exhaustively cover normative `/Q/`, - `/E/`, and full housekeeping protocol semantics +- CXPoF bridge coverage now includes `/Q/` sequence mismatch policy, classified + `/E/` causes, and HKP K-code validation/classification ## Running The Slice +Latest focused validation for the current receive-side SSI `EOFE` work: + +```bash +./.venv/bin/python -m pytest -q tests/protocols/coaxpress/test_CoaXPressRx.py +./.venv/bin/python -m pytest -q tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py +./.venv/bin/python -m pytest -q \ + tests/protocols/coaxpress/test_CoaXPressRxLane.py \ + tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py \ + tests/protocols/coaxpress/test_CoaXPressRxLaneMux.py +./.venv/bin/python -m pytest -q \ + tests/protocols/coaxpress/test_CoaXPressCore.py::test_CoaXPressCore +``` + Typical local commands: ```bash diff --git a/tests/protocols/coaxpress/coaxpress_test_utils.py b/tests/protocols/coaxpress/coaxpress_test_utils.py index fc0d6ce969..2c15070222 100644 --- a/tests/protocols/coaxpress/coaxpress_test_utils.py +++ b/tests/protocols/coaxpress/coaxpress_test_utils.py @@ -23,6 +23,8 @@ CXP_IDLE = 0xB53C3CBC CXP_IDLE_K = 0x7 +CXP_ALL_DATA_K = 0x0 +CXP_ALL_CTRL_K = 0xF CXP_SOP = 0xFBFBFBFB CXP_EOP = 0xFDFDFDFD CXP_TRIG = 0x5C5C5C5C @@ -56,6 +58,30 @@ CXPOF_TERM = 0xFD CXPOF_ERROR = 0xFE +CXPOF_SOP_CTRL_LOW_SPEED = 0x00 +CXPOF_SOP_CTRL_HIGH_SPEED = 0x80 +CXPOF_SOP_CTRL_HKP = 0x01 +CXPOF_SOP_CTRL_UPDATE_BIT = 3 +CXPOF_SOP_CTRL_LS_RATE_BIT = 1 + +CXPOF_RX_ERR_NONE = 0x0 +CXPOF_RX_ERR_SEQ_MISMATCH = 0x1 +CXPOF_RX_ERR_IDLE_ERROR = 0x2 +CXPOF_RX_ERR_PAYLOAD_ABORT = 0x3 +CXPOF_RX_ERR_BAD_CONTROL = 0x4 +CXPOF_RX_ERR_OVERWRITE = 0x5 +CXPOF_RX_ERR_HKP_MALFORMED = 0x6 +CXPOF_RX_ERR_HKP_BAD_K_CODE = 0x7 + +CXPOF_HKP_TYPE_NONE = 0x0 +CXPOF_HKP_TYPE_K_CODE = 0x1 +CXPOF_HKP_TYPE_SOP = 0x2 +CXPOF_HKP_TYPE_EOP = 0x3 +CXPOF_HKP_TYPE_TRIG = 0x4 +CXPOF_HKP_TYPE_IO_ACK = 0x5 +CXPOF_HKP_TYPE_MARKER = 0x6 +CXPOF_HKP_TYPE_INVALID = 0xF + CXP_CRC32_POLY = 0x04C11DB7 diff --git a/tests/protocols/coaxpress/test_CoaXPressConfig.py b/tests/protocols/coaxpress/test_CoaXPressConfig.py index 9e5d598bf1..02005c85bd 100644 --- a/tests/protocols/coaxpress/test_CoaXPressConfig.py +++ b/tests/protocols/coaxpress/test_CoaXPressConfig.py @@ -9,8 +9,8 @@ ############################################################################## # Test methodology: -# - Sweep: Cover the two request-serialization branches that are unique to -# `CoaXPressConfig`: untagged reads and tagged writes. +# - Sweep: Cover all four config request-format quadrants plus local timeout +# and nonzero control-ack response error handling. # - Stimulus: Drive wide SRPv3 request frames into `cfgIb`, capture the emitted # CoaXPress low-speed byte stream on `cfgTx`, and feed the completion side # with one config receive acknowledgment. @@ -48,6 +48,10 @@ ) +CONFIG_READ_ERROR_FOOTER = 0x1 +CONFIG_WRITE_ERROR_FOOTER = 0x2 + + async def _drive_cfg_rx_completion(dut, value: int, *, hold_cycles: int = 8) -> None: dut.cfgRxTData.value = value dut.cfgRxTValid.value = 1 @@ -187,6 +191,138 @@ async def coaxpress_config_tagged_write_tag_increment_test(dut): ) +@cocotb.test() +async def coaxpress_config_tagged_read_and_untagged_write_request_test(dut): + # Cover the two request-format quadrants not hit by the original directed + # cases: tagged read and untagged write. + axis = await _setup_config_bench(dut, config_pkt_tag=1) + + read_request = SrpV3Request(SRP_READ, 0x12340001, 0x00000120, 4) + read_data = 0x13579BDF + read_tx_task = cocotb.start_soon( + collect_stream_bytes( + dut, + clk=dut.cfgClk, + valid_name="M_CFG_TX_TVALID", + data_name="M_CFG_TX_TDATA", + ready_name="M_CFG_TX_TREADY", + count=28, + timeout_cycles=8000, + ) + ) + read_response_task = cocotb.start_soon(axis.recv_response(timeout_time=20)) + await axis.send_words(srpv3_frame(read_request)) + + read_tx_bytes = await with_timeout(read_tx_task, 20, "us") + assert read_tx_bytes[:4] == bytes(word_to_bytes(CXP_SOP)) + assert read_tx_bytes[4:8] == bytes([0x05] * 4) + assert read_tx_bytes[8:12] == bytes([0x00] * 4) + assert read_tx_bytes[12:16] == bytes(word_to_bytes(0x04000000)) + assert read_tx_bytes[16:20] == bytes(word_to_bytes(endian_swap32(read_request.address))) + expected_read_crc = cxp_crc_word( + [0x00000000, 0x04000000, endian_swap32(read_request.address)] + ) + assert read_tx_bytes[20:24] == bytes(word_to_bytes(expected_read_crc)) + assert read_tx_bytes[-4:] == bytes(word_to_bytes(CXP_EOP)) + + await _drive_cfg_rx_completion(dut, read_data << 32) + assert_srpv3_response(await read_response_task, read_request, [read_data]) + + dut.configPktTag.value = 0 + await RisingEdge(dut.cfgClk) + await Timer(1, unit="ns") + + write_request = SrpV3Request(SRP_WRITE, 0x12340002, 0x00000124, 4) + write_data = 0x2468ACE0 + write_tx_task = cocotb.start_soon( + collect_stream_bytes( + dut, + clk=dut.cfgClk, + valid_name="M_CFG_TX_TVALID", + data_name="M_CFG_TX_TDATA", + ready_name="M_CFG_TX_TREADY", + count=28, + timeout_cycles=8000, + ) + ) + write_response_task = cocotb.start_soon(axis.recv_response(timeout_time=20)) + await axis.send_words(srpv3_frame(write_request, [write_data])) + + write_tx_bytes = await with_timeout(write_tx_task, 20, "us") + assert write_tx_bytes[:4] == bytes(word_to_bytes(CXP_SOP)) + assert write_tx_bytes[4:8] == bytes([0x02] * 4) + assert write_tx_bytes[8:12] == bytes(word_to_bytes(0x04000001)) + assert write_tx_bytes[12:16] == bytes(word_to_bytes(endian_swap32(write_request.address))) + assert write_tx_bytes[16:20] == bytes(word_to_bytes(write_data)) + expected_write_crc = cxp_crc_word( + [0x04000001, endian_swap32(write_request.address), write_data] + ) + assert write_tx_bytes[20:24] == bytes(word_to_bytes(expected_write_crc)) + assert write_tx_bytes[-4:] == bytes(word_to_bytes(CXP_EOP)) + + await _drive_cfg_rx_completion(dut, 0) + assert_srpv3_response(await write_response_task, write_request, [write_data]) + + +@cocotb.test() +async def coaxpress_config_response_error_paths_test(dut): + # The current RTL maps either a local config-response timeout or a nonzero + # control-ack status word into the local SRPv3 AXI-Lite error footer when + # `configErrResp` is asserted. + axis = await _setup_config_bench(dut, config_pkt_tag=0) + dut.configTimerSize.value = 8 + + timeout_request = SrpV3Request(SRP_READ, 0xABC00001, 0x00000200, 4) + timeout_tx_task = cocotb.start_soon( + collect_stream_bytes( + dut, + clk=dut.cfgClk, + valid_name="M_CFG_TX_TVALID", + data_name="M_CFG_TX_TDATA", + ready_name="M_CFG_TX_TREADY", + count=24, + timeout_cycles=8000, + ) + ) + timeout_response_task = cocotb.start_soon(axis.recv_response(timeout_time=20)) + await axis.send_words(srpv3_frame(timeout_request)) + await with_timeout(timeout_tx_task, 20, "us") + assert_srpv3_response( + await timeout_response_task, + timeout_request, + [], + footer_mask=CONFIG_READ_ERROR_FOOTER, + footer_value=CONFIG_READ_ERROR_FOOTER, + ) + + dut.configTimerSize.value = 4096 + status_request = SrpV3Request(SRP_WRITE, 0xABC00002, 0x00000204, 4) + status_write_data = 0xA5A55A5A + status_tx_task = cocotb.start_soon( + collect_stream_bytes( + dut, + clk=dut.cfgClk, + valid_name="M_CFG_TX_TVALID", + data_name="M_CFG_TX_TDATA", + ready_name="M_CFG_TX_TREADY", + count=28, + timeout_cycles=8000, + ) + ) + status_response_task = cocotb.start_soon(axis.recv_response(timeout_time=20)) + await axis.send_words(srpv3_frame(status_request, [status_write_data])) + await with_timeout(status_tx_task, 20, "us") + + await _drive_cfg_rx_completion(dut, 0x00000001) + assert_srpv3_response( + await status_response_task, + status_request, + [status_write_data], + footer_mask=CONFIG_WRITE_ERROR_FOOTER, + footer_value=CONFIG_WRITE_ERROR_FOOTER, + ) + + def test_CoaXPressConfig(): run_surf_vhdl_test( test_file=__file__, diff --git a/tests/protocols/coaxpress/test_CoaXPressCore.py b/tests/protocols/coaxpress/test_CoaXPressCore.py index 3770cb0b80..6454a6d784 100644 --- a/tests/protocols/coaxpress/test_CoaXPressCore.py +++ b/tests/protocols/coaxpress/test_CoaXPressCore.py @@ -38,11 +38,14 @@ CXP_IDLE, CXP_IDLE_K, CXP_MARKER, + CXP_EOP, CXP_PKT_IMAGE_HEADER, CXP_PKT_IMAGE_LINE, + CXP_PKT_STREAM_DATA, CXP_SOP, cycle, collect_stream_bytes, + cxp_crc_word, endian_swap32, find_subsequence, pack_u32_words_le, @@ -132,18 +135,34 @@ async def _read_counter(axil: AxiLiteMaster, dut, offset: int) -> int: return await axil.read_dword(offset) -async def _send_stream_packet_words(dut, payload_words: list[int], *, stream_id: int = 0x22, packet_tag: int = 0x33) -> None: +async def _send_stream_packet_words( + dut, + payload_words: list[int], + *, + stream_id: int = 0x22, + packet_tag: int = 0x33, + corrupt_crc: bool = False, +) -> None: + crc_inputs = [ + repeat_byte(stream_id), + repeat_byte(packet_tag), + repeat_byte((len(payload_words) >> 8) & 0xFF), + repeat_byte(len(payload_words) & 0xFF), + *payload_words, + ] words = [ CXP_SOP, - repeat_byte(0x01), + repeat_byte(CXP_PKT_STREAM_DATA), repeat_byte(stream_id), repeat_byte(packet_tag), repeat_byte((len(payload_words) >> 8) & 0xFF), repeat_byte(len(payload_words) & 0xFF), *payload_words, + cxp_crc_word(crc_inputs) ^ (0x00000001 if corrupt_crc else 0x00000000), + CXP_EOP, ] for word in words: - await send_rx_word(dut, data=word, data_k=0xF if word == CXP_SOP else 0x0, clk=dut.rxClk) + await send_rx_word(dut, data=word, data_k=0xF if word in (CXP_SOP, CXP_EOP) else 0x0, clk=dut.rxClk) async def _collect_core_outputs(dut, *, cycles: int) -> tuple[list[int], list[int]]: @@ -242,14 +261,16 @@ async def coaxpress_core_tagged_config_tx_path_test(dut): await send_axis_payload(dut, clk=dut.cfgClk, prefix="S_CFG_IB", payload=request_payload, width_bytes=32, tuser=0x2) tx_bytes = await with_timeout(tx_task, 20, "us") - expected_request = ( + expected_packet = ( bytes(word_to_bytes(CXP_SOP)) + bytes([0x05] * 4) + b"\x00\x00\x00\x00" + bytes(word_to_bytes(0x04000000)) + bytes(word_to_bytes(endian_swap32(addr))) + + bytes(word_to_bytes(cxp_crc_word([0x00000000, 0x04000000, endian_swap32(addr)]))) + + bytes(word_to_bytes(CXP_EOP)) ) - request_start = find_subsequence(tx_bytes, expected_request) + request_start = find_subsequence(tx_bytes, expected_packet) assert request_start is not None, tx_bytes @@ -302,17 +323,55 @@ async def coaxpress_core_rx_fsm_error_counter_and_recovery_test(dut): assert await _read_counter(axil, dut, 0x824) == first_error_count -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") -async def coaxpress_core_rx_overflow_does_not_trigger_fsm_error_storm_known_issue_test(dut): +@cocotb.test() +async def coaxpress_core_rx_lane_crc_error_counter_test(dut): + axil = await _setup_core(dut) + + assert await _read_counter(axil, dut, 0x824) == 0 + + await _send_stream_packet_words( + dut, + _header_payload(y_size=1, dsize_l=1), + stream_id=0x52, + packet_tag=0x77, + ) + await _send_stream_packet_words( + dut, + _line_payload(0x12345678), + stream_id=0x53, + packet_tag=0x78, + corrupt_crc=True, + ) + + lane_error_count = await _read_counter(axil, dut, 0x824) + assert lane_error_count > 0 + + await _collect_core_outputs(dut, cycles=32) + assert await _read_counter(axil, dut, 0x824) == lane_error_count + + await _send_image_frame( + dut, + stream_id=0x54, + packet_tag=0x79, + y_size=1, + dsize_l=1, + line_words=[0x87654321], + ) + _hdr_words, data_words = await _collect_core_outputs(dut, cycles=64) + assert 0x87654321 in data_words + assert await _read_counter(axil, dut, 0x824) == lane_error_count + + +@cocotb.test() +async def coaxpress_core_rx_overflow_does_not_trigger_fsm_error_storm_test(dut): axil = await _setup_core(dut, data_ready=0, hdr_ready=1) assert await _read_counter(axil, dut, 0x820) == 0 assert await _read_counter(axil, dut, 0x824) == 0 - frame_count = env_int("CXP_RX_OVERFLOW_STORM_FRAME_COUNT", default=96) - # Hold opaque stream metadata constant to separate a true backpressure issue - # from metadata-sensitive parsing behavior in the exploratory bench. - fixed_packet_fields = env_flag("CXP_CORE_KNOWN_ISSUE_FIXED_PACKET_FIELDS", default=False) + frame_count = env_int("CXP_RX_OVERFLOW_STORM_FRAME_COUNT", default=8) + line_word_count = env_int("CXP_RX_OVERFLOW_STORM_LINE_WORD_COUNT", default=96) + vary_packet_fields = env_flag("CXP_CORE_RX_OVERFLOW_VARY_PACKET_FIELDS", default=True) signal_counts = { "core_rx_fsm_error": 0, "core_rx_overflow": 0, @@ -365,15 +424,15 @@ async def coaxpress_core_rx_overflow_does_not_trigger_fsm_error_storm_known_issu for index in range(frame_count): phase_trace["frame_index"] = index - stream_id = 0x50 if fixed_packet_fields else (0x50 + (2 * index)) & 0xFF - packet_tag = 0x70 if fixed_packet_fields else (0x70 + (2 * index)) & 0xFF + stream_id = (0x50 + (2 * index)) & 0xFF if vary_packet_fields else 0x50 + packet_tag = (0x70 + (2 * index)) & 0xFF if vary_packet_fields else 0x70 await _send_image_frame( dut, stream_id=stream_id, packet_tag=packet_tag, y_size=1, - dsize_l=1, - line_words=[0x10000000 + index], + dsize_l=line_word_count, + line_words=[0x10000000 | (index << 12) | word_index for word_index in range(line_word_count)], ) phase_trace["label"] = "idle_quiesce" @@ -387,10 +446,6 @@ async def coaxpress_core_rx_overflow_does_not_trigger_fsm_error_storm_known_issu for task in monitor_tasks: await task - # Known issue under investigation: - # with the current 72-frame workload the realistic core path still shows no - # RX overflow. The remaining anomaly is a late lone RxFsmError pulse that - # only appears when the bench sweeps packet stream/tag fields. assert overflow_count > 0, ( f"overflow_count={overflow_count} first_error_count={first_error_count} " f"core_overflow={signal_counts['core_rx_overflow']} core_error={signal_counts['core_rx_fsm_error']} " @@ -421,6 +476,8 @@ async def coaxpress_core_rx_overflow_does_not_trigger_fsm_error_storm_known_issu released_error_count = await _read_counter(axil, dut, 0x824) assert released_error_count == first_error_count + await _collect_core_outputs(dut, cycles=env_int("CXP_RX_OVERFLOW_STORM_DRAIN_CYCLES", default=1024)) + await _send_image_frame( dut, stream_id=0xE0, diff --git a/tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py b/tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py index 75e655f89b..4ef1e069bf 100644 --- a/tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py +++ b/tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py @@ -30,13 +30,21 @@ from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import ( CXP_EOP, + CXP_ALL_CTRL_K, CXP_IDLE, CXP_IDLE_K, CXP_PKT_EVENT_ACK, CXP_SOP, CXPOF_ERROR, + CXPOF_HKP_TYPE_K_CODE, CXPOF_IDLE, + CXPOF_RX_ERR_PAYLOAD_ABORT, + CXPOF_RX_ERR_SEQ_MISMATCH, CXPOF_SEQ, + CXPOF_SOP_CTRL_HIGH_SPEED, + CXPOF_SOP_CTRL_HKP, + CXPOF_SOP_CTRL_LS_RATE_BIT, + CXPOF_SOP_CTRL_UPDATE_BIT, CXPOF_START, CXPOF_TERM, cycle, @@ -49,15 +57,15 @@ def _tx_start_word(rate: int, update: int) -> int: - return CXPOF_START | (((update << 3) | (rate << 1)) << 8) + return CXPOF_START | (((update << CXPOF_SOP_CTRL_UPDATE_BIT) | (rate << CXPOF_SOP_CTRL_LS_RATE_BIT)) << 8) def _rx_start_word(packet_byte: int) -> int: - return CXPOF_START | (0x80 << 8) | ((CXP_SOP & 0xFF) << 16) | (packet_byte << 24) + return CXPOF_START | (CXPOF_SOP_CTRL_HIGH_SPEED << 8) | ((CXP_SOP & 0xFF) << 16) | (packet_byte << 24) def _rx_hkp_start_word() -> int: - return CXPOF_START | (0x81 << 8) + return CXPOF_START | ((CXPOF_SOP_CTRL_HIGH_SPEED | CXPOF_SOP_CTRL_HKP) << 8) def _idle64() -> int: @@ -181,6 +189,15 @@ async def coaxpress_over_fiber_bridge_top_rx_error_abort_and_recovery_test(dut): await _setup_bridge(dut) rx_capture = cocotb.start_soon(_capture_rx_words(dut, cycles=64)) + abort_samples: list[tuple[int, int]] = [] + + async def capture_abort(cycles: int) -> None: + for _ in range(cycles): + await RisingEdge(dut.rxClk312) + await Timer(1, unit="ns") + if int(dut.rxAbort.value) == 1: + abort_samples.append((int(dut.rxError.value), int(dut.rxErrorCode.value))) + abort_capture = cocotb.start_soon(capture_abort(64)) # Start a valid low-speed packet, then inject `/E/` as the next 32-bit word. # The first packet must not receive a synthetic CXP EOP. @@ -201,6 +218,7 @@ async def coaxpress_over_fiber_bridge_top_rx_error_abort_and_recovery_test(dut): await _drive_rx64(dut, 0x07FD00FD | (repeat_byte(CXPOF_IDLE) << 32), 0xFC) await _drive_rx64(dut, _idle64(), 0xFF) + await abort_capture rx_observed = await rx_capture rx_expected = [ (CXP_SOP, 0xF), @@ -214,6 +232,7 @@ async def coaxpress_over_fiber_bridge_top_rx_error_abort_and_recovery_test(dut): assert find_subsequence(rx_observed, rx_expected) is not None, ( f"missing RX /E/ recovery sequence: {rx_observed}" ) + assert abort_samples == [(1, CXPOF_RX_ERR_PAYLOAD_ABORT)] @cocotb.test() @@ -221,18 +240,39 @@ async def coaxpress_over_fiber_bridge_top_rx_hkp_then_payload_mix_test(dut): await _setup_bridge(dut) rx_capture = cocotb.start_soon(_capture_rx_words(dut, cycles=48)) + hkp_samples: list[tuple[int, int, int, int, int, int, int]] = [] + + async def capture_hkp(cycles: int) -> None: + for _ in range(cycles): + await RisingEdge(dut.rxClk312) + await Timer(1, unit="ns") + if int(dut.hkpValid.value) == 1: + hkp_samples.append( + ( + int(dut.hkpData.value), + int(dut.hkpEop.value), + int(dut.hkpSof.value), + int(dut.hkpWordCount.value), + int(dut.hkpKCodeMask.value), + int(dut.hkpKCodeValid.value), + int(dut.hkpType.value), + ) + ) + hkp_capture = cocotb.start_soon(capture_hkp(48)) hkp_word = 0x9C5C3CBC - await _drive_rx64(dut, _rx_hkp_start_word() | (hkp_word << 32), 0xF1) + await _drive_rx64(dut, _rx_hkp_start_word() | (hkp_word << 32), 0x01) await _drive_rx64(dut, 0x10203040 | (0x07FD00FD << 32), 0xC0) await _drive_rx64(dut, _idle64(), 0xFF) + await hkp_capture rx_observed = await rx_capture rx_expected = [ (hkp_word, 0xF), (0x10203040, 0x0), (CXP_EOP, 0xF), ] + assert hkp_samples == [(hkp_word, 0, 1, 1, CXP_ALL_CTRL_K, 1, CXPOF_HKP_TYPE_K_CODE)] assert find_subsequence(rx_observed, rx_expected) is not None, f"missing RX HKP/data sequence: {rx_observed}" @@ -241,21 +281,50 @@ async def coaxpress_over_fiber_bridge_top_rx_sequence_no_output_recovery_test(du await _setup_bridge(dut) rx_capture = cocotb.start_soon(_capture_rx_words(dut, cycles=64)) + seq_samples: list[int] = [] + seq_errors: list[tuple[int, int, int]] = [] - # Lane-0 `/Q/` is not decoded into a CXP word by the current RX bridge. The - # top-level gearbox should preserve that no-output guardrail and allow a - # later valid low-speed packet to recover. + async def capture_seq(cycles: int) -> None: + for _ in range(cycles): + await RisingEdge(dut.rxClk312) + await Timer(1, unit="ns") + if int(dut.seqValid.value) == 1: + seq_samples.append(int(dut.seqData.value)) + if int(dut.seqError.value) == 1: + seq_errors.append( + ( + int(dut.seqData.value), + int(dut.seqErrorExpected.value), + int(dut.rxErrorCode.value), + ) + ) + seq_capture = cocotb.start_soon(capture_seq(64)) + + # Lane-0 `/Q/` is published through the status interface, not decoded into a + # CXP word. The top-level gearbox should preserve that no-output guardrail + # and allow a later valid low-speed packet to recover. await _drive_rx64( dut, (CXPOF_SEQ | (0x12 << 16) | (0x34 << 24)) | (_idle64() & 0xFFFFFFFF00000000), 0xF1, ) + await _drive_rx64( + dut, + (CXPOF_SEQ | (0x01 << 8) | (0x12 << 16) | (0x34 << 24)) | (_idle64() & 0xFFFFFFFF00000000), + 0xF1, + ) + await _drive_rx64( + dut, + (CXPOF_SEQ | (0x03 << 8) | (0x12 << 16) | (0x34 << 24)) | (_idle64() & 0xFFFFFFFF00000000), + 0xF1, + ) await _drive_rx64(dut, _idle64(), 0xFF) await _drive_rx64(dut, _rx_start_word(CXP_PKT_EVENT_ACK) | (0xA1B2C3D4 << 32), 0x01) await _drive_rx64(dut, 0x07FD00FD | (repeat_byte(CXPOF_IDLE) << 32), 0xFC) await _drive_rx64(dut, _idle64(), 0xFF) + await seq_capture rx_observed = await rx_capture rx_expected = [ (CXP_SOP, 0xF), @@ -263,19 +332,22 @@ async def coaxpress_over_fiber_bridge_top_rx_sequence_no_output_recovery_test(du (0xA1B2C3D4, 0x0), (CXP_EOP, 0xF), ] + assert seq_samples == [0x341200, 0x341201, 0x341203] + assert seq_errors == [(0x341203, 0x341202, CXPOF_RX_ERR_SEQ_MISMATCH)] assert rx_observed == rx_expected def test_CoaXPressOverFiberBridge(): run_surf_vhdl_test( test_file=__file__, - toplevel="surf.coaxpressoverfiberbridge", + toplevel="surf.coaxpressoverfiberbridgestatuswrapper", extra_vhdl_sources={ "surf": [ "protocols/coaxpress/core/rtl/CoaXPressPkg.vhd", "protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeRx.vhd", "protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeTx.vhd", "protocols/coaxpress/core/rtl/CoaXPressOverFiberBridge.vhd", + "protocols/coaxpress/core/wrappers/CoaXPressOverFiberBridgeStatusWrapper.vhd", ] }, ) diff --git a/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeAxiL.py b/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeAxiL.py new file mode 100644 index 0000000000..3f6f363d6f --- /dev/null +++ b/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeAxiL.py @@ -0,0 +1,278 @@ +############################################################################## +## 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: Exercise the software-visible CXPoF bridge RX status register file +# through the AXI-Lite clock crossing. +# - Stimulus: Pulse representative receive-domain status events for `/Q/` +# sequence tracking, sequence mismatch, named HKP K-code classifications, +# and `/E/` abort. +# - Checks: AXI-Lite reads must report sticky status, last-observed status +# fields, event counters, and write-one counter/sticky reset behavior. +# - Timing: AXI-Lite and RX clocks run asynchronously so the bench covers the +# intended `AxiLiteAsync` path, not only a common-clock register file. + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge, Timer, 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.coaxpress.coaxpress_test_utils import ( + CXP_ALL_CTRL_K, + CXP_EOP, + CXP_IO_ACK, + CXP_MARKER, + CXP_SOP, + CXP_TRIG, + CXPOF_HKP_TYPE_EOP, + CXPOF_HKP_TYPE_IO_ACK, + CXPOF_HKP_TYPE_K_CODE, + CXPOF_HKP_TYPE_MARKER, + CXPOF_HKP_TYPE_SOP, + CXPOF_HKP_TYPE_TRIG, + CXPOF_RX_ERR_PAYLOAD_ABORT, + CXPOF_RX_ERR_SEQ_MISMATCH, +) + + +STATUS_RX_ERROR = 1 << 0 +STATUS_RX_ABORT = 1 << 1 +STATUS_SEQ_VALID = 1 << 2 +STATUS_SEQ_ERROR = 1 << 3 +STATUS_HKP_VALID = 1 << 4 +STATUS_HKP_ERROR = 1 << 5 + + +async def _rx_cycle(dut, count: int = 1) -> None: + for _ in range(count): + await RisingEdge(dut.rxClk) + await Timer(1, unit="ns") + + +async def _axil_read(axil, address: int) -> int: + return await with_timeout(axil_read_u32(axil, address), 2, "us") + + +async def _axil_write(axil, address: int, value: int) -> None: + await with_timeout(axil_write_u32(axil, address, value), 2, "us") + + +def _clear_status_inputs(dut) -> None: + for name in ( + "rxError", + "rxAbort", + "rxErrorCode", + "seqValid", + "seqData", + "seqError", + "seqExpected", + "seqErrorExpected", + "hkpValid", + "hkpData", + "hkpEop", + "hkpSof", + "hkpError", + "hkpWordCount", + "hkpKCodeMask", + "hkpKCodeValid", + "hkpType", + ): + getattr(dut, name).value = 0 + + +async def _pulse_status(dut, **values: int) -> None: + _clear_status_inputs(dut) + for name, value in values.items(): + getattr(dut, name).value = value + await _rx_cycle(dut) + _clear_status_inputs(dut) + + +async def _read_until(axil, address: int, expected: int, *, mask: int = 0xFFFFFFFF) -> int: + for _ in range(64): + value = await _axil_read(axil, address) + if (value & mask) == expected: + return value + raise AssertionError(f"AXI-Lite 0x{address:03X} did not reach 0x{expected:X}") + + +async def _setup_status_axil(dut) -> AxiLiteMaster: + cocotb.start_soon(Clock(dut.rxClk, 4.0, unit="ns").start()) + cocotb.start_soon(Clock(dut.S_AXI_ACLK, 7.0, unit="ns").start()) + + dut.rxRst.setimmediatevalue(1) + dut.S_AXI_ARESETN.setimmediatevalue(0) + _clear_status_inputs(dut) + + axil = AxiLiteMaster( + AxiLiteBus.from_prefix(dut, "S_AXI"), + dut.S_AXI_ACLK, + dut.S_AXI_ARESETN, + reset_active_level=False, + ) + + await _rx_cycle(dut, 8) + dut.S_AXI_ARESETN.value = 1 + dut.rxRst.value = 0 + await _rx_cycle(dut, 8) + return axil + + +@cocotb.test() +async def coaxpress_over_fiber_bridge_axil_status_registers_test(dut): + axil = await _setup_status_axil(dut) + + assert await _axil_read(axil, 0x000) == 0 + assert await _axil_read(axil, 0x020) == 0 + + await _pulse_status( + dut, + seqValid=1, + seqData=0x341200, + seqExpected=0x341201, + ) + + await _pulse_status( + dut, + rxError=1, + rxErrorCode=CXPOF_RX_ERR_SEQ_MISMATCH, + seqValid=1, + seqData=0x341203, + seqExpected=0x341204, + seqError=1, + seqErrorExpected=0x341202, + ) + + await _pulse_status( + dut, + hkpValid=1, + hkpData=CXP_EOP, + hkpEop=1, + hkpSof=1, + hkpWordCount=1, + hkpKCodeMask=CXP_ALL_CTRL_K, + hkpKCodeValid=1, + hkpType=CXPOF_HKP_TYPE_EOP, + ) + + await _pulse_status( + dut, + rxError=1, + rxAbort=1, + rxErrorCode=CXPOF_RX_ERR_PAYLOAD_ABORT, + ) + + expected_sticky = ( + STATUS_RX_ERROR + | STATUS_RX_ABORT + | STATUS_SEQ_VALID + | STATUS_SEQ_ERROR + | STATUS_HKP_VALID + ) + assert (await _read_until(axil, 0x000, expected_sticky, mask=0x3F)) & 0x3F == expected_sticky + + assert (await _read_until(axil, 0x004, CXPOF_RX_ERR_PAYLOAD_ABORT, mask=0xF)) & 0xF == CXPOF_RX_ERR_PAYLOAD_ABORT + assert (await _read_until(axil, 0x008, 0x341203, mask=0xFFFFFF)) & 0xFFFFFF == 0x341203 + assert (await _read_until(axil, 0x00C, 0x341204, mask=0xFFFFFF)) & 0xFFFFFF == 0x341204 + assert (await _read_until(axil, 0x010, 0x341202, mask=0xFFFFFF)) & 0xFFFFFF == 0x341202 + assert await _read_until(axil, 0x014, CXP_EOP) == CXP_EOP + + hkp_status = await _read_until(axil, 0x018, 1, mask=0xFF) + assert (hkp_status >> 0) & 0xFF == 1 + assert (hkp_status >> 8) & 0xF == CXP_ALL_CTRL_K + assert (hkp_status >> 12) & 0x1 == 1 + assert (hkp_status >> 16) & 0xF == CXPOF_HKP_TYPE_EOP + + assert await _read_until(axil, 0x020, 2) == 2 + assert await _read_until(axil, 0x024, 1) == 1 + assert await _read_until(axil, 0x028, 2) == 2 + assert await _read_until(axil, 0x02C, 1) == 1 + assert await _read_until(axil, 0x030, 1) == 1 + assert await _read_until(axil, 0x034, 0) == 0 + + await _axil_write(axil, 0x03C, 1) + assert (await _read_until(axil, 0x000, 0, mask=0x3F)) & 0x3F == 0 + assert await _read_until(axil, 0x020, 0) == 0 + + await _pulse_status( + dut, + rxError=1, + rxErrorCode=0x7, + hkpValid=1, + hkpError=1, + hkpData=0x11223344, + hkpWordCount=1, + hkpKCodeMask=0, + hkpKCodeValid=0, + hkpType=CXPOF_HKP_TYPE_K_CODE, + ) + + expected_hkp_error = STATUS_RX_ERROR | STATUS_HKP_VALID | STATUS_HKP_ERROR + assert (await _read_until(axil, 0x000, expected_hkp_error, mask=0x3F)) & 0x3F == expected_hkp_error + assert await _read_until(axil, 0x030, 1) == 1 + assert await _read_until(axil, 0x034, 1) == 1 + + +@cocotb.test() +async def coaxpress_over_fiber_bridge_axil_hkp_classification_sweep_test(dut): + axil = await _setup_status_axil(dut) + + hkp_cases = [ + (CXP_SOP, CXPOF_HKP_TYPE_SOP), + (CXP_EOP, CXPOF_HKP_TYPE_EOP), + (CXP_TRIG, CXPOF_HKP_TYPE_TRIG), + (CXP_IO_ACK, CXPOF_HKP_TYPE_IO_ACK), + (CXP_MARKER, CXPOF_HKP_TYPE_MARKER), + (0x9C5C3CBC, CXPOF_HKP_TYPE_K_CODE), + ] + + for index, (word, hkp_type) in enumerate(hkp_cases, start=1): + # The bridge RX leaf classifies HKP K-code payloads; this AXI-Lite block + # is the downstream software-visible consumer. Each pulse must replace + # the last-HKP registers after the async crossing without raising the + # HKP-error sticky bit or counter. + await _pulse_status( + dut, + hkpValid=1, + hkpData=word, + hkpWordCount=index, + hkpKCodeMask=CXP_ALL_CTRL_K, + hkpKCodeValid=1, + hkpType=hkp_type, + ) + + assert await _read_until(axil, 0x014, word) == word + hkp_status = await _read_until(axil, 0x018, index, mask=0xFF) + assert (hkp_status >> 0) & 0xFF == index + assert (hkp_status >> 8) & 0xF == CXP_ALL_CTRL_K + assert (hkp_status >> 12) & 0x1 == 1 + assert (hkp_status >> 16) & 0xF == hkp_type + + assert await _read_until(axil, 0x030, index) == index + assert await _read_until(axil, 0x034, 0) == 0 + + assert (await _read_until(axil, 0x000, STATUS_HKP_VALID, mask=0x30)) & 0x30 == STATUS_HKP_VALID + + +def test_CoaXPressOverFiberBridgeAxiL(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.coaxpressoverfiberbridgeaxilwrapper", + extra_vhdl_sources={ + "surf": [ + "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", + "protocols/coaxpress/core/rtl/CoaXPressPkg.vhd", + "protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeAxiL.vhd", + "protocols/coaxpress/core/wrappers/CoaXPressOverFiberBridgeAxiLWrapper.vhd", + ] + }, + ) diff --git a/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeRx.py b/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeRx.py index e95b1a83e5..5f84097071 100644 --- a/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeRx.py +++ b/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeRx.py @@ -29,6 +29,7 @@ from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import ( CXP_EOP, + CXP_ALL_CTRL_K, CXP_IDLE, CXP_IDLE_K, CXP_IO_ACK, @@ -36,8 +37,18 @@ CXP_PKT_EVENT_ACK, CXP_SOP, CXPOF_ERROR, + CXPOF_HKP_TYPE_EOP, + CXPOF_HKP_TYPE_INVALID, + CXPOF_HKP_TYPE_K_CODE, CXPOF_IDLE, + CXPOF_RX_ERR_HKP_BAD_K_CODE, + CXPOF_RX_ERR_HKP_MALFORMED, + CXPOF_RX_ERR_IDLE_ERROR, + CXPOF_RX_ERR_PAYLOAD_ABORT, + CXPOF_RX_ERR_SEQ_MISMATCH, CXPOF_SEQ, + CXPOF_SOP_CTRL_HIGH_SPEED, + CXPOF_SOP_CTRL_HKP, CXPOF_START, CXPOF_TERM, cycle, @@ -48,7 +59,7 @@ def _cxp_start_word(packet_byte: int) -> int: - return CXPOF_START | (0x80 << 8) | ((CXP_SOP & 0xFF) << 16) | (packet_byte << 24) + return CXPOF_START | (CXPOF_SOP_CTRL_HIGH_SPEED << 8) | ((CXP_SOP & 0xFF) << 16) | (packet_byte << 24) def _control_in_lane(control_byte: int, lane: int) -> int: @@ -85,7 +96,7 @@ async def drive(rxd: int, rxc: int) -> None: await drive(0x07070707, 0xF) # Separate IO_ACK indication with no terminal payload word emitted. - dut.xgmiiRxd.value = CXPOF_START | (0x80 << 8) | ((CXP_IO_ACK & 0xFF) << 16) + dut.xgmiiRxd.value = CXPOF_START | (CXPOF_SOP_CTRL_HIGH_SPEED << 8) | ((CXP_IO_ACK & 0xFF) << 16) dut.xgmiiRxc.value = 0x1 await cycle(dut.clk, 1) sample = (int(dut.rxData.value), int(dut.rxDataK.value)) @@ -130,9 +141,9 @@ async def drive(rxd: int, rxc: int) -> None: await drive(0x07070707, 0xF) await drive(0x07070707, 0xF) - await drive(CXPOF_START | (0x81 << 8), 0x1) - await drive(0x5C5C5C5C, 0xF) - await drive(CXP_EOP, 0xF) + await drive(CXPOF_START | ((CXPOF_SOP_CTRL_HIGH_SPEED | CXPOF_SOP_CTRL_HKP) << 8), 0x1) + await drive(0x5C5C5C5C, 0x0) + await drive(0x07FD00FD, 0xC) await drive(0x07070707, 0xF) await drive(0x07070707, 0xF) @@ -144,6 +155,7 @@ async def drive(rxd: int, rxc: int) -> None: assert observed == [ (0x5C5C5C5C, 0xF), + (CXP_EOP, 0xF), (CXP_SOP, 0xF), (repeat_byte(CXP_PKT_EVENT_ACK), 0x0), (0x55667788, 0x0), @@ -153,10 +165,11 @@ async def drive(rxd: int, rxc: int) -> None: @cocotb.test() async def coaxpress_over_fiber_bridge_rx_sequence_error_and_recovery_test(dut): - # The current bridge RX does not implement a normative /Q/ ordered-set path; - # lock it down as a no-output guardrail, then prove an explicit /E/ in a - # payload aborts the packet without emitting a synthetic CXP EOP and the next - # packet still decodes cleanly. + # `/Q/` is tracked as a sequence counter: the first value initializes the + # expected sequence, a following increment is accepted, and a skipped value + # raises a sequence error while resynchronizing to later traffic. Then prove + # an explicit `/E/` in a payload reports a classified abort without emitting + # a synthetic CXP EOP and that the next packet still decodes cleanly. start_clock(dut.clk) dut.rst.setimmediatevalue(1) dut.xgmiiRxd.setimmediatevalue(0x07070707) @@ -164,16 +177,32 @@ async def coaxpress_over_fiber_bridge_rx_sequence_error_and_recovery_test(dut): await reset_dut(dut, clk_name="clk", reset_names=("rst",)) observed: list[tuple[int, int]] = [] + seq_samples: list[int] = [] + seq_errors: list[tuple[int, int]] = [] + error_codes: list[int] = [] + abort_pulses = 0 + error_pulses = 0 async def drive(rxd: int, rxc: int) -> None: + nonlocal abort_pulses, error_pulses dut.xgmiiRxd.value = rxd dut.xgmiiRxc.value = rxc await cycle(dut.clk, 1) sample = (int(dut.rxData.value), int(dut.rxDataK.value)) if sample != (CXP_IDLE, CXP_IDLE_K): observed.append(sample) + if int(dut.seqValid.value) == 1: + seq_samples.append(int(dut.seqData.value)) + if int(dut.seqError.value) == 1: + seq_errors.append((int(dut.seqData.value), int(dut.seqErrorExpected.value))) + if int(dut.rxError.value) == 1: + error_codes.append(int(dut.rxErrorCode.value)) + abort_pulses += int(dut.rxAbort.value) + error_pulses += int(dut.rxError.value) await drive(CXPOF_SEQ | (0x00 << 8) | (0x12 << 16) | (0x34 << 24), 0x1) + await drive(CXPOF_SEQ | (0x01 << 8) | (0x12 << 16) | (0x34 << 24), 0x1) + await drive(CXPOF_SEQ | (0x03 << 8) | (0x12 << 16) | (0x34 << 24), 0x1) await drive(0x07070707, 0xF) await drive(0x07070707, 0xF) @@ -189,6 +218,11 @@ async def drive(rxd: int, rxc: int) -> None: await drive(0x07070707, 0xF) await drive(0x07070707, 0xF) + assert seq_samples == [0x341200, 0x341201, 0x341203] + assert seq_errors == [(0x341203, 0x341202)] + assert abort_pulses == 1 + assert error_pulses == 2 + assert error_codes == [CXPOF_RX_ERR_SEQ_MISMATCH, CXPOF_RX_ERR_PAYLOAD_ABORT] assert observed == [ (CXP_SOP, 0xF), (repeat_byte(CXP_PKT_EVENT_ACK), 0x0), @@ -262,6 +296,7 @@ async def coaxpress_over_fiber_bridge_rx_hkp_eop_kcode_test(dut): await reset_dut(dut, clk_name="clk", reset_names=("rst",)) observed: list[tuple[int, int]] = [] + hkp_samples: list[tuple[int, int, int, int, int, int, int]] = [] async def drive(rxd: int, rxc: int) -> None: dut.xgmiiRxd.value = rxd @@ -270,8 +305,20 @@ async def drive(rxd: int, rxc: int) -> None: sample = (int(dut.rxData.value), int(dut.rxDataK.value)) if sample != (CXP_IDLE, CXP_IDLE_K): observed.append(sample) - - await drive(CXPOF_START | (0x81 << 8), 0x1) + if int(dut.hkpValid.value) == 1: + hkp_samples.append( + ( + int(dut.hkpData.value), + int(dut.hkpEop.value), + int(dut.hkpSof.value), + int(dut.hkpWordCount.value), + int(dut.hkpKCodeMask.value), + int(dut.hkpKCodeValid.value), + int(dut.hkpType.value), + ) + ) + + await drive(CXPOF_START | ((CXPOF_SOP_CTRL_HIGH_SPEED | CXPOF_SOP_CTRL_HKP) << 8), 0x1) await drive(CXP_EOP, 0x0) await drive(0x07070707, 0xF) await drive(0x07070707, 0xF) @@ -282,6 +329,7 @@ async def drive(rxd: int, rxc: int) -> None: await drive(0x07070707, 0xF) await drive(0x07070707, 0xF) + assert hkp_samples == [(CXP_EOP, 1, 1, 1, CXP_ALL_CTRL_K, 1, CXPOF_HKP_TYPE_EOP)] assert observed == [ (CXP_EOP, 0xF), (CXP_SOP, 0xF), @@ -304,14 +352,20 @@ async def coaxpress_over_fiber_bridge_rx_error_after_sop_recovery_test(dut): await reset_dut(dut, clk_name="clk", reset_names=("rst",)) observed: list[tuple[int, int]] = [] + abort_pulses = 0 + error_codes: list[int] = [] async def drive(rxd: int, rxc: int) -> None: + nonlocal abort_pulses dut.xgmiiRxd.value = rxd dut.xgmiiRxc.value = rxc await cycle(dut.clk, 1) sample = (int(dut.rxData.value), int(dut.rxDataK.value)) if sample != (CXP_IDLE, CXP_IDLE_K): observed.append(sample) + abort_pulses += int(dut.rxAbort.value) + if int(dut.rxError.value) == 1: + error_codes.append(int(dut.rxErrorCode.value)) await drive(_cxp_start_word(CXP_PKT_EVENT_ACK), 0x1) await drive(CXPOF_ERROR | (CXPOF_IDLE << 8) | (CXPOF_IDLE << 16) | (CXPOF_IDLE << 24), 0x1) @@ -324,6 +378,8 @@ async def drive(rxd: int, rxc: int) -> None: await drive(0x07070707, 0xF) await drive(0x07070707, 0xF) + assert abort_pulses == 1 + assert error_codes == [CXPOF_RX_ERR_PAYLOAD_ABORT] assert observed == [ (CXP_SOP, 0xF), (repeat_byte(CXP_PKT_EVENT_ACK), 0x0), @@ -334,11 +390,39 @@ async def drive(rxd: int, rxc: int) -> None: ] +@cocotb.test() +async def coaxpress_over_fiber_bridge_rx_idle_error_code_test(dut): + # `/E/` while idle is still an error/abort indication, but it has a distinct + # cause code from an active-packet abort. + start_clock(dut.clk) + dut.rst.setimmediatevalue(1) + dut.xgmiiRxd.setimmediatevalue(0x07070707) + dut.xgmiiRxc.setimmediatevalue(0xF) + await reset_dut(dut, clk_name="clk", reset_names=("rst",)) + + error_codes: list[int] = [] + abort_pulses = 0 + + async def drive(rxd: int, rxc: int) -> None: + nonlocal abort_pulses + dut.xgmiiRxd.value = rxd + dut.xgmiiRxc.value = rxc + await cycle(dut.clk, 1) + abort_pulses += int(dut.rxAbort.value) + if int(dut.rxError.value) == 1: + error_codes.append(int(dut.rxErrorCode.value)) + + await drive(CXPOF_ERROR | (CXPOF_IDLE << 8) | (CXPOF_IDLE << 16) | (CXPOF_IDLE << 24), 0x1) + await drive(0x07070707, 0xF) + + assert abort_pulses == 1 + assert error_codes == [CXPOF_RX_ERR_IDLE_ERROR] + + @cocotb.test() async def coaxpress_over_fiber_bridge_rx_hkp_then_payload_mix_test(dut): - # A housekeeping start word may be followed by one raw K-coded HKP word and - # then normal data/EOP handling. This locks down the current RTL contract for - # the HKP-to-payload transition without claiming full housekeeping semantics. + # A housekeeping start word may be followed by one K-code HKP word with + # nGMII control flags clear and then normal data/EOP handling. start_clock(dut.clk) dut.rst.setimmediatevalue(1) dut.xgmiiRxd.setimmediatevalue(0x07070707) @@ -346,6 +430,7 @@ async def coaxpress_over_fiber_bridge_rx_hkp_then_payload_mix_test(dut): await reset_dut(dut, clk_name="clk", reset_names=("rst",)) observed: list[tuple[int, int]] = [] + hkp_samples: list[tuple[int, int, int, int, int, int, int]] = [] async def drive(rxd: int, rxc: int) -> None: dut.xgmiiRxd.value = rxd @@ -354,15 +439,28 @@ async def drive(rxd: int, rxc: int) -> None: sample = (int(dut.rxData.value), int(dut.rxDataK.value)) if sample != (CXP_IDLE, CXP_IDLE_K): observed.append(sample) + if int(dut.hkpValid.value) == 1: + hkp_samples.append( + ( + int(dut.hkpData.value), + int(dut.hkpEop.value), + int(dut.hkpSof.value), + int(dut.hkpWordCount.value), + int(dut.hkpKCodeMask.value), + int(dut.hkpKCodeValid.value), + int(dut.hkpType.value), + ) + ) hkp_word = 0x9C5C3CBC - await drive(CXPOF_START | (0x81 << 8), 0x1) - await drive(hkp_word, 0xF) + await drive(CXPOF_START | ((CXPOF_SOP_CTRL_HIGH_SPEED | CXPOF_SOP_CTRL_HKP) << 8), 0x1) + await drive(hkp_word, 0x0) await drive(0x10203040, 0x0) await drive(0x07FD00FD, 0xC) await drive(0x07070707, 0xF) await drive(0x07070707, 0xF) + assert hkp_samples == [(hkp_word, 0, 1, 1, CXP_ALL_CTRL_K, 1, CXPOF_HKP_TYPE_K_CODE)] assert observed == [ (hkp_word, 0xF), (0x10203040, 0x0), @@ -370,6 +468,73 @@ async def drive(rxd: int, rxc: int) -> None: ] +@cocotb.test() +async def coaxpress_over_fiber_bridge_rx_hkp_malformed_status_test(dut): + # HKP words carry K-code byte values without nGMII control flags. A nonzero + # control mask is malformed even when the byte values themselves are K codes. + start_clock(dut.clk) + dut.rst.setimmediatevalue(1) + dut.xgmiiRxd.setimmediatevalue(0x07070707) + dut.xgmiiRxc.setimmediatevalue(0xF) + await reset_dut(dut, clk_name="clk", reset_names=("rst",)) + + hkp_errors: list[int] = [] + error_codes: list[int] = [] + + async def drive(rxd: int, rxc: int) -> None: + dut.xgmiiRxd.value = rxd + dut.xgmiiRxc.value = rxc + await cycle(dut.clk, 1) + if int(dut.hkpError.value) == 1: + hkp_errors.append(int(dut.hkpWordCount.value)) + if int(dut.rxError.value) == 1: + error_codes.append(int(dut.rxErrorCode.value)) + + await drive(CXPOF_START | ((CXPOF_SOP_CTRL_HIGH_SPEED | CXPOF_SOP_CTRL_HKP) << 8), 0x1) + await drive(0x5C5C3CBC, 0x5) + await drive(0x07070707, 0xF) + + assert hkp_errors == [1] + assert error_codes == [CXPOF_RX_ERR_HKP_MALFORMED] + + +@cocotb.test() +async def coaxpress_over_fiber_bridge_rx_hkp_bad_kcode_status_test(dut): + # HKP is a K-code payload, not arbitrary raw data. Non-K-code bytes are + # forwarded for observability but classified separately from bad control + # masks. + start_clock(dut.clk) + dut.rst.setimmediatevalue(1) + dut.xgmiiRxd.setimmediatevalue(0x07070707) + dut.xgmiiRxc.setimmediatevalue(0xF) + await reset_dut(dut, clk_name="clk", reset_names=("rst",)) + + hkp_samples: list[tuple[int, int, int]] = [] + error_codes: list[int] = [] + + async def drive(rxd: int, rxc: int) -> None: + dut.xgmiiRxd.value = rxd + dut.xgmiiRxc.value = rxc + await cycle(dut.clk, 1) + if int(dut.hkpValid.value) == 1: + hkp_samples.append( + ( + int(dut.hkpKCodeMask.value), + int(dut.hkpKCodeValid.value), + int(dut.hkpType.value), + ) + ) + if int(dut.rxError.value) == 1: + error_codes.append(int(dut.rxErrorCode.value)) + + await drive(CXPOF_START | ((CXPOF_SOP_CTRL_HIGH_SPEED | CXPOF_SOP_CTRL_HKP) << 8), 0x1) + await drive(0x11223344, 0x0) + await drive(0x07070707, 0xF) + + assert hkp_samples == [(0x0, 0, CXPOF_HKP_TYPE_INVALID)] + assert error_codes == [CXPOF_RX_ERR_HKP_BAD_K_CODE] + + @cocotb.test() async def coaxpress_over_fiber_bridge_rx_control_lane_guardrail_sweep_test(dut): # `/S/`, `/Q/`, `/T/`, and `/E/` are lane-sensitive XGMII control bytes. @@ -420,11 +585,12 @@ async def drive(rxd: int, rxc: int) -> None: def test_CoaXPressOverFiberBridgeRx(): run_surf_vhdl_test( test_file=__file__, - toplevel="surf.coaxpressoverfiberbridgerx", + toplevel="surf.coaxpressoverfiberbridgerxstatuswrapper", extra_vhdl_sources={ "surf": [ "protocols/coaxpress/core/rtl/CoaXPressPkg.vhd", "protocols/coaxpress/core/rtl/CoaXPressOverFiberBridgeRx.vhd", + "protocols/coaxpress/core/wrappers/CoaXPressOverFiberBridgeRxStatusWrapper.vhd", ] }, ) diff --git a/tests/protocols/coaxpress/test_CoaXPressRx.py b/tests/protocols/coaxpress/test_CoaXPressRx.py index fcef05d0e0..f2fcd74130 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRx.py +++ b/tests/protocols/coaxpress/test_CoaXPressRx.py @@ -40,6 +40,7 @@ CXP_PKT_EVENT, CXP_PKT_IMAGE_HEADER, CXP_PKT_IMAGE_LINE, + CXP_PKT_STREAM_DATA, CXP_SOP, append_snapshot_if_valid, cxp_crc_word, @@ -114,7 +115,6 @@ 0x00200010, ] - def _event_crc_words(*, event_bytes: tuple[int, int, int, int], packet_tag: int, payload_words: list[int]) -> list[int]: crc_inputs = [ *[repeat_byte(byte) for byte in event_bytes], @@ -129,6 +129,41 @@ def _event_crc_words(*, event_bytes: tuple[int, int, int, int], packet_tag: int, ] +def _control_ack_crc_words(*, ack_code: int, size_word: int, data_word: int) -> list[int]: + crc_inputs = [repeat_byte(ack_code), size_word, data_word] + return [ + *crc_inputs, + cxp_crc_word(crc_inputs), + ] + + +def _stream_packet_sequence( + *, + stream_id: int, + packet_tag: int, + payload_items: list[tuple[int, int]], +) -> list[tuple[int, int]]: + payload_words = [data for data, _data_k in payload_items] + crc_inputs = [ + repeat_byte(stream_id), + repeat_byte(packet_tag), + repeat_byte((len(payload_words) >> 8) & 0xFF), + repeat_byte(len(payload_words) & 0xFF), + *payload_words, + ] + return [ + (CXP_SOP, 0xF), + (repeat_byte(CXP_PKT_STREAM_DATA), 0x0), + (repeat_byte(stream_id), 0x0), + (repeat_byte(packet_tag), 0x0), + (repeat_byte((len(payload_words) >> 8) & 0xFF), 0x0), + (repeat_byte(len(payload_words) & 0xFF), 0x0), + *payload_items, + (cxp_crc_word(crc_inputs), 0x0), + (CXP_EOP, 0xF), + ] + + def _pack_lane_nibbles(values: list[int]) -> int: packed = 0 for index, value in enumerate(values): @@ -186,10 +221,12 @@ def _capture_outputs( event_tags: list[int], trig_ack_cycles: list[int], cycle_index: int, + event_beats: list[tuple[int, int, int, int, int]] | None = None, ) -> None: cfg_samples: list[dict[str, int]] = [] data_samples: list[dict[str, int]] = [] hdr_samples: list[dict[str, int]] = [] + event_samples: list[dict[str, int]] = [] append_snapshot_if_valid(cfg_samples, dut, valid_name="cfgTValid", field_names=("cfgTData", "cfgTKeep", "cfgTLast")) append_snapshot_if_valid( data_samples, @@ -203,6 +240,13 @@ def _capture_outputs( valid_name="hdrTValid", field_names=("hdrTData", "hdrTKeep", "hdrTLast", "hdrTUser"), ) + if event_beats is not None: + append_snapshot_if_valid( + event_samples, + dut, + valid_name="eventTValid", + field_names=("eventTData", "eventTKeep", "eventTLast", "eventTDest", "eventTUser"), + ) cfg_beats.extend((sample["cfgTData"], sample["cfgTKeep"], sample["cfgTLast"]) for sample in cfg_samples) data_beats.extend( (sample["dataTData"], sample["dataTKeep"], sample["dataTLast"], sample["dataTUser"]) for sample in data_samples @@ -210,6 +254,17 @@ def _capture_outputs( hdr_beats.extend( (sample["hdrTData"], sample["hdrTKeep"], sample["hdrTLast"], sample["hdrTUser"]) for sample in hdr_samples ) + if event_beats is not None: + event_beats.extend( + ( + sample["eventTData"], + sample["eventTKeep"], + sample["eventTLast"], + sample["eventTDest"], + sample["eventTUser"], + ) + for sample in event_samples + ) if int(dut.eventAck.value) == 1: event_tags.append(int(dut.eventTag.value)) if int(dut.trigAck.value) == 1: @@ -261,24 +316,24 @@ def _isolated_lane_frame_sequence( header_payload_words[corrupt_header_index] = corrupt_header_word return [ - (CXP_SOP, 0xF), - (repeat_byte(0x01), 0x0), - (repeat_byte(stream_id), 0x0), - (repeat_byte(packet_tag), 0x0), - (repeat_byte((len(header_payload_words) + 2) >> 8), 0x0), - (repeat_byte((len(header_payload_words) + 2) & 0xFF), 0x0), - (CXP_MARKER, 0xF), - (repeat_byte(CXP_PKT_IMAGE_HEADER), 0x0), - *[(word, 0xF) for word in header_payload_words], - (CXP_SOP, 0xF), - (repeat_byte(0x01), 0x0), - (repeat_byte((stream_id + 1) & 0xFF), 0x0), - (repeat_byte((packet_tag + 1) & 0xFF), 0x0), - (repeat_byte((len(line_words) + 2) >> 8), 0x0), - (repeat_byte((len(line_words) + 2) & 0xFF), 0x0), - (CXP_MARKER, 0xF), - (repeat_byte(CXP_PKT_IMAGE_LINE), 0x0), - *[(word, 0x0) for word in line_words], + *_stream_packet_sequence( + stream_id=stream_id, + packet_tag=packet_tag, + payload_items=[ + (CXP_MARKER, 0xF), + (repeat_byte(CXP_PKT_IMAGE_HEADER), 0x0), + *[(word, 0xF) for word in header_payload_words], + ], + ), + *_stream_packet_sequence( + stream_id=(stream_id + 1) & 0xFF, + packet_tag=(packet_tag + 1) & 0xFF, + payload_items=[ + (CXP_MARKER, 0xF), + (repeat_byte(CXP_PKT_IMAGE_LINE), 0x0), + *[(word, 0x0) for word in line_words], + ], + ), ] @@ -354,27 +409,75 @@ async def _send_one_lane_frame( line_packet_tag: int = 0x55, ) -> None: sequence = [ - (CXP_SOP, 0xF), - (repeat_byte(0x01), 0x0), - (repeat_byte(header_stream_id), 0x0), - (repeat_byte(header_packet_tag), 0x0), - (repeat_byte(0x00), 0x0), - (repeat_byte(25), 0x0), - (CXP_MARKER, 0xF), - (repeat_byte(CXP_PKT_IMAGE_HEADER), 0x0), - *[(word, 0xF) for word in SINGLE_LINE_HEADER_WORDS], - (CXP_SOP, 0xF), - (repeat_byte(0x01), 0x0), - (repeat_byte(line_stream_id), 0x0), - (repeat_byte(line_packet_tag), 0x0), - (repeat_byte(0x00), 0x0), - (repeat_byte(3), 0x0), - (CXP_MARKER, 0xF), - (repeat_byte(CXP_PKT_IMAGE_LINE), 0x0), - (line_word, 0x0), + *_stream_packet_sequence( + stream_id=header_stream_id, + packet_tag=header_packet_tag, + payload_items=[ + (CXP_MARKER, 0xF), + (repeat_byte(CXP_PKT_IMAGE_HEADER), 0x0), + *[(word, 0xF) for word in SINGLE_LINE_HEADER_WORDS], + ], + ), + *_stream_packet_sequence( + stream_id=line_stream_id, + packet_tag=line_packet_tag, + payload_items=[ + (CXP_MARKER, 0xF), + (repeat_byte(CXP_PKT_IMAGE_LINE), 0x0), + (line_word, 0x0), + ], + ), + ] + for data, data_k in sequence: + await send_rx_word(dut, data=data, data_k=data_k, clk=dut.rxClk) + + +async def _send_one_lane_frame_and_capture( + dut, + *, + line_word: int, + data_beats: list[tuple[int, int, int, int]], + hdr_beats: list[tuple[int, int, int, int]], + header_stream_id: int = 0x22, + header_packet_tag: int = 0x33, + line_stream_id: int = 0x44, + line_packet_tag: int = 0x55, + start_cycle_index: int = 0, +) -> int: + sequence = [ + *_stream_packet_sequence( + stream_id=header_stream_id, + packet_tag=header_packet_tag, + payload_items=[ + (CXP_MARKER, 0xF), + (repeat_byte(CXP_PKT_IMAGE_HEADER), 0x0), + *[(word, 0xF) for word in SINGLE_LINE_HEADER_WORDS], + ], + ), + *_stream_packet_sequence( + stream_id=line_stream_id, + packet_tag=line_packet_tag, + payload_items=[ + (CXP_MARKER, 0xF), + (repeat_byte(CXP_PKT_IMAGE_LINE), 0x0), + (line_word, 0x0), + ], + ), ] + cycle_index = start_cycle_index for data, data_k in sequence: await send_rx_word(dut, data=data, data_k=data_k, clk=dut.rxClk) + _capture_outputs( + dut, + cfg_beats=[], + data_beats=data_beats, + hdr_beats=hdr_beats, + event_tags=[], + trig_ack_cycles=[], + cycle_index=cycle_index, + ) + cycle_index += 1 + return cycle_index async def _count_signal_high_cycles(signal, clk, stop_event: Event, counts: dict[str, int], key: str) -> None: @@ -453,6 +556,7 @@ async def coaxpress_rx_one_lane_integration_test(dut): "rxNumberOfLane": 0, "dataTReady": 1, "hdrTReady": 1, + "eventTReady": 1, }, ) await reset_signals( @@ -466,16 +570,14 @@ async def coaxpress_rx_one_lane_integration_test(dut): cfg_beats: list[tuple[int, int, int]] = [] data_beats: list[tuple[int, int, int, int]] = [] hdr_beats: list[tuple[int, int, int, int]] = [] + event_beats: list[tuple[int, int, int, int, int]] = [] event_tags: list[int] = [] trig_ack_cycles: list[int] = [] sequence = [ (CXP_SOP, 0xF), (repeat_byte(CXP_PKT_CTRL_ACK_NO_TAG), 0x0), - (repeat_byte(0x00), 0x0), - (0x04000000, 0x0), - (0x01234567, 0x0), - (0xCAFEBABE, 0x0), + *[(word, 0x0) for word in _control_ack_crc_words(ack_code=0x00, size_word=0x04000000, data_word=0x01234567)], (CXP_EOP, 0xF), (CXP_SOP, 0xF), (repeat_byte(CXP_PKT_EVENT), 0x0), @@ -490,28 +592,26 @@ async def coaxpress_rx_one_lane_integration_test(dut): (CXP_EOP, 0xF), (CXP_IO_ACK, 0xF), (repeat_byte(0x01), 0x0), - (CXP_SOP, 0xF), - (repeat_byte(0x01), 0x0), - (repeat_byte(0x22), 0x0), - (repeat_byte(0x33), 0x0), - (repeat_byte(0x00), 0x0), - (repeat_byte(25), 0x0), - (CXP_MARKER, 0xF), - (repeat_byte(CXP_PKT_IMAGE_HEADER), 0x0), - *[(word, 0xF) for word in HEADER_WORDS], - (CXP_SOP, 0xF), - (repeat_byte(0x01), 0x0), - (repeat_byte(0x44), 0x0), - (repeat_byte(0x55), 0x0), - (repeat_byte(0x00), 0x0), - (repeat_byte(5), 0x0), - (CXP_MARKER, 0xF), - (repeat_byte(CXP_PKT_IMAGE_LINE), 0x0), - (0x11111111, 0x0), - (0x22222222, 0x0), - (0x33333333, 0x0), - (0xBEEFBEEF, 0x0), - (CXP_EOP, 0xF), + *_stream_packet_sequence( + stream_id=0x22, + packet_tag=0x33, + payload_items=[ + (CXP_MARKER, 0xF), + (repeat_byte(CXP_PKT_IMAGE_HEADER), 0x0), + *[(word, 0xF) for word in HEADER_WORDS], + ], + ), + *_stream_packet_sequence( + stream_id=0x44, + packet_tag=0x55, + payload_items=[ + (CXP_MARKER, 0xF), + (repeat_byte(CXP_PKT_IMAGE_LINE), 0x0), + (0x11111111, 0x0), + (0x22222222, 0x0), + (0x33333333, 0x0), + ], + ), ] for cycle_index, (data, data_k) in enumerate(sequence): @@ -524,6 +624,7 @@ async def coaxpress_rx_one_lane_integration_test(dut): event_tags=event_tags, trig_ack_cycles=trig_ack_cycles, cycle_index=cycle_index, + event_beats=event_beats, ) for cycle_index in range(40): @@ -536,9 +637,11 @@ async def coaxpress_rx_one_lane_integration_test(dut): event_tags=event_tags, trig_ack_cycles=trig_ack_cycles, cycle_index=cycle_index + len(sequence), + event_beats=event_beats, ) assert cfg_beats == [(0x0123456700000000, 0xFF, 0)] + assert event_beats == [(0x11223344, 0xF, 1, 0x5A, 0x13121110)] assert event_tags == [0x5A] assert trig_ack_cycles assert [beat[:3] for beat in hdr_beats] == [(word, 0xF, 1 if index == len(EXPECTED_HDR_WORDS) - 1 else 0) for index, word in enumerate(EXPECTED_HDR_WORDS)] @@ -550,11 +653,9 @@ async def coaxpress_rx_one_lane_integration_test(dut): ] -@cocotb.test() -async def coaxpress_rx_two_lane_mux_rotation_test(dut): - if env_int("NUM_LANES_G", default=1) != 2: - return - +async def _drive_two_lane_mux_rotation( + dut, +) -> tuple[list[tuple[int, int, int, int]], list[tuple[int, int, int, int]], list[int]]: start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -566,6 +667,7 @@ async def coaxpress_rx_two_lane_mux_rotation_test(dut): "rxNumberOfLane": 1, "dataTReady": 1, "hdrTReady": 1, + "eventTReady": 1, }, ) await reset_signals( @@ -578,6 +680,7 @@ async def coaxpress_rx_two_lane_mux_rotation_test(dut): data_beats: list[tuple[int, int, int, int]] = [] hdr_beats: list[tuple[int, int, int, int]] = [] + error_cycles: list[int] = [] async def capture(cycle_index: int) -> None: _capture_outputs( @@ -589,53 +692,55 @@ async def capture(cycle_index: int) -> None: trig_ack_cycles=[], cycle_index=cycle_index, ) - - lane0_sequence = [ - ([CXP_SOP, CXP_IDLE], [0xF, CXP_IDLE_K]), - ([repeat_byte(0x01), CXP_IDLE], [0x0, CXP_IDLE_K]), - ([repeat_byte(0x22), CXP_IDLE], [0x0, CXP_IDLE_K]), - ([repeat_byte(0x33), CXP_IDLE], [0x0, CXP_IDLE_K]), - ([repeat_byte(0x00), CXP_IDLE], [0x0, CXP_IDLE_K]), - ([repeat_byte(25), CXP_IDLE], [0x0, CXP_IDLE_K]), - ([CXP_MARKER, CXP_IDLE], [0xF, CXP_IDLE_K]), - ([repeat_byte(CXP_PKT_IMAGE_HEADER), CXP_IDLE], [0x0, CXP_IDLE_K]), - *[([word, CXP_IDLE], [0xF, CXP_IDLE_K]) for word in HEADER_WORDS], - ([CXP_SOP, CXP_IDLE], [0xF, CXP_IDLE_K]), - ([repeat_byte(0x01), CXP_IDLE], [0x0, CXP_IDLE_K]), - ([repeat_byte(0x44), CXP_IDLE], [0x0, CXP_IDLE_K]), - ([repeat_byte(0x55), CXP_IDLE], [0x0, CXP_IDLE_K]), - ([repeat_byte(0x00), CXP_IDLE], [0x0, CXP_IDLE_K]), - ([repeat_byte(5), CXP_IDLE], [0x0, CXP_IDLE_K]), - ([CXP_MARKER, CXP_IDLE], [0xF, CXP_IDLE_K]), - ([repeat_byte(CXP_PKT_IMAGE_LINE), CXP_IDLE], [0x0, CXP_IDLE_K]), - ([0x11111111, CXP_IDLE], [0x0, CXP_IDLE_K]), - ([0x22222222, CXP_IDLE], [0x0, CXP_IDLE_K]), - ([0x33333333, CXP_IDLE], [0x0, CXP_IDLE_K]), - ([CXP_EOP, CXP_IDLE], [0xF, CXP_IDLE_K]), + if int(dut.rxFsmError.value) == 1: + error_cycles.append(cycle_index) + + lane0_packets = [ + *_stream_packet_sequence( + stream_id=0x22, + packet_tag=0x33, + payload_items=[ + (CXP_MARKER, 0xF), + (repeat_byte(CXP_PKT_IMAGE_HEADER), 0x0), + *[(word, 0xF) for word in HEADER_WORDS], + ], + ), + *_stream_packet_sequence( + stream_id=0x44, + packet_tag=0x55, + payload_items=[ + (CXP_MARKER, 0xF), + (repeat_byte(CXP_PKT_IMAGE_LINE), 0x0), + (0x11111111, 0x0), + (0x22222222, 0x0), + (0x33333333, 0x0), + ], + ), ] - lane1_sequence = [ - ([CXP_IDLE, CXP_SOP], [CXP_IDLE_K, 0xF]), - ([CXP_IDLE, repeat_byte(0x01)], [CXP_IDLE_K, 0x0]), - ([CXP_IDLE, repeat_byte(0x22)], [CXP_IDLE_K, 0x0]), - ([CXP_IDLE, repeat_byte(0x33)], [CXP_IDLE_K, 0x0]), - ([CXP_IDLE, repeat_byte(0x00)], [CXP_IDLE_K, 0x0]), - ([CXP_IDLE, repeat_byte(25)], [CXP_IDLE_K, 0x0]), - ([CXP_IDLE, CXP_MARKER], [CXP_IDLE_K, 0xF]), - ([CXP_IDLE, repeat_byte(CXP_PKT_IMAGE_HEADER)], [CXP_IDLE_K, 0x0]), - *[([CXP_IDLE, word], [CXP_IDLE_K, 0xF]) for word in HEADER_WORDS], - ([CXP_IDLE, CXP_SOP], [CXP_IDLE_K, 0xF]), - ([CXP_IDLE, repeat_byte(0x01)], [CXP_IDLE_K, 0x0]), - ([CXP_IDLE, repeat_byte(0x44)], [CXP_IDLE_K, 0x0]), - ([CXP_IDLE, repeat_byte(0x55)], [CXP_IDLE_K, 0x0]), - ([CXP_IDLE, repeat_byte(0x00)], [CXP_IDLE_K, 0x0]), - ([CXP_IDLE, repeat_byte(5)], [CXP_IDLE_K, 0x0]), - ([CXP_IDLE, CXP_MARKER], [CXP_IDLE_K, 0xF]), - ([CXP_IDLE, repeat_byte(CXP_PKT_IMAGE_LINE)], [CXP_IDLE_K, 0x0]), - ([CXP_IDLE, 0x44444444], [CXP_IDLE_K, 0x0]), - ([CXP_IDLE, 0x55555555], [CXP_IDLE_K, 0x0]), - ([CXP_IDLE, 0x66666666], [CXP_IDLE_K, 0x0]), - ([CXP_IDLE, CXP_EOP], [CXP_IDLE_K, 0xF]), + lane1_packets = [ + *_stream_packet_sequence( + stream_id=0x22, + packet_tag=0x33, + payload_items=[ + (CXP_MARKER, 0xF), + (repeat_byte(CXP_PKT_IMAGE_HEADER), 0x0), + *[(word, 0xF) for word in HEADER_WORDS], + ], + ), + *_stream_packet_sequence( + stream_id=0x44, + packet_tag=0x55, + payload_items=[ + (CXP_MARKER, 0xF), + (repeat_byte(CXP_PKT_IMAGE_LINE), 0x0), + (0x44444444, 0x0), + (0x55555555, 0x0), + (0x66666666, 0x0), + ], + ), ] + lane0_sequence = [([data, CXP_IDLE], [data_k, CXP_IDLE_K]) for data, data_k in lane0_packets] + lane1_sequence = [([CXP_IDLE, data], [CXP_IDLE_K, data_k]) for data, data_k in lane1_packets] cycle_index = 0 for lane_words, lane_ks in lane0_sequence: @@ -658,19 +763,129 @@ async def capture(cycle_index: int) -> None: await capture(cycle_index) cycle_index += 1 + return hdr_beats, data_beats, error_cycles + + +@cocotb.test() +async def coaxpress_rx_two_lane_mux_rotation_test(dut): + if env_int("NUM_LANES_G", default=1) != 2: + return + + hdr_beats, data_beats, error_cycles = await _drive_two_lane_mux_rotation(dut) + + assert not error_cycles assert [beat[0] for beat in hdr_beats] == EXPECTED_HDR_WORDS * 2 - assert [beat[0] for beat in data_beats[:3]] == [0x11111111, 0x22222222, 0x33333333] - assert 0x44444444 in [beat[0] for beat in data_beats] - assert 0x55555555 in [beat[0] for beat in data_beats] - assert any(beat[2] == 1 for beat in data_beats) - - -# -# Opt-in investigation benches. These stay behind RUN_KNOWN_ISSUE_TESTS until -# the remaining 4-lane short-frame boundary issue in CoaXPressRxHsFsm is fixed. -# -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") -async def coaxpress_rx_four_lane_fsm_error_reset_recovery_known_issue_test(dut): + assert [beat[0] for beat in data_beats] == [ + 0x11111111, + 0x22222222, + 0x33333333, + 0x44444444, + 0x55555555, + 0x66666666, + ] + assert [beat[2] for beat in data_beats] == [0, 0, 1, 0, 0, 1] + + +@cocotb.test() +async def coaxpress_rx_lane_parser_error_status_recovery_test(dut): + if env_int("NUM_LANES_G", default=1) != 1: + return + + start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) + set_initial_values( + dut, + { + "rxData": 0, + "rxDataK": 0, + "rxLinkUp": 1, + "rxFsmRst": 0, + "rxNumberOfLane": 0, + "dataTReady": 1, + "hdrTReady": 1, + "eventTReady": 1, + }, + ) + await reset_signals( + dut, + clk=dut.rxClk, + reset_names=("dataRst", "cfgRst", "txRst", "rxRst"), + assert_cycles=4, + release_cycles=4, + ) + + signal_counts = {"error_pulses": 0} + stop_event = Event() + monitor_task = cocotb.start_soon(_count_signal_high_cycles(dut.rxFsmError, dut.rxClk, stop_event, signal_counts, "error_pulses")) + + bad_line_packet = _stream_packet_sequence( + stream_id=0x44, + packet_tag=0x55, + payload_items=[ + (CXP_MARKER, 0xF), + (repeat_byte(CXP_PKT_IMAGE_LINE), 0x0), + (0x11111111, 0x0), + ], + ) + bad_line_packet[-2] = (bad_line_packet[-2][0] ^ 0x00000001, bad_line_packet[-2][1]) + + bad_data_beats: list[tuple[int, int, int, int]] = [] + bad_hdr_beats: list[tuple[int, int, int, int]] = [] + cycle_index = 0 + + for data, data_k in [ + *_stream_packet_sequence( + stream_id=0x22, + packet_tag=0x33, + payload_items=[ + (CXP_MARKER, 0xF), + (repeat_byte(CXP_PKT_IMAGE_HEADER), 0x0), + *[(word, 0xF) for word in SINGLE_LINE_HEADER_WORDS], + ], + ), + *bad_line_packet, + ]: + await send_rx_word(dut, data=data, data_k=data_k, clk=dut.rxClk) + _capture_outputs( + dut, + cfg_beats=[], + data_beats=bad_data_beats, + hdr_beats=bad_hdr_beats, + event_tags=[], + trig_ack_cycles=[], + cycle_index=cycle_index, + ) + cycle_index += 1 + + await _drive_idle_and_capture( + dut, + cycles=32, + data_beats=bad_data_beats, + hdr_beats=bad_hdr_beats, + start_cycle_index=cycle_index, + ) + + data_beats: list[tuple[int, int, int, int]] = [] + hdr_beats: list[tuple[int, int, int, int]] = [] + cycle_index = await _send_one_lane_frame_and_capture( + dut, + line_word=0x22222222, + data_beats=data_beats, + hdr_beats=hdr_beats, + ) + await _drive_idle_and_capture(dut, cycles=64, data_beats=data_beats, hdr_beats=hdr_beats, start_cycle_index=cycle_index) + + stop_event.set() + await monitor_task + + assert signal_counts["error_pulses"] >= 1, signal_counts + assert bad_data_beats == [(0x11111111, 0xF, 1, 1)], bad_data_beats + assert [beat[0] for beat in data_beats] == [0x22222222], data_beats + assert [beat[2] for beat in data_beats] == [1], data_beats + assert [beat[3] for beat in data_beats] == [0], data_beats + + +@cocotb.test() +async def coaxpress_rx_four_lane_fsm_error_reset_recovery_test(dut): if env_int("NUM_LANES_G", default=1) != 4: return @@ -685,6 +900,7 @@ async def coaxpress_rx_four_lane_fsm_error_reset_recovery_known_issue_test(dut): "rxNumberOfLane": 3, "dataTReady": 1, "hdrTReady": 1, + "eventTReady": 1, }, ) await reset_signals( @@ -739,8 +955,8 @@ async def coaxpress_rx_four_lane_fsm_error_reset_recovery_known_issue_test(dut): assert observed_recovery_last == [0, 0, 1] * 4, observed_recovery_last -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") -async def coaxpress_rx_four_lane_clean_rotation_known_issue_test(dut): +@cocotb.test() +async def coaxpress_rx_four_lane_clean_rotation_test(dut): if env_int("NUM_LANES_G", default=1) != 4: return @@ -755,6 +971,7 @@ async def coaxpress_rx_four_lane_clean_rotation_known_issue_test(dut): "rxNumberOfLane": 3, "dataTReady": 1, "hdrTReady": 1, + "eventTReady": 1, }, ) await reset_signals( @@ -797,8 +1014,8 @@ async def coaxpress_rx_four_lane_clean_rotation_known_issue_test(dut): assert [beat[2] for beat in data_beats] == [0, 0, 1] * 4, data_beats -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") -async def coaxpress_rx_four_lane_fsm_error_recovery_known_issue_test(dut): +@cocotb.test() +async def coaxpress_rx_four_lane_fsm_error_recovery_test(dut): if env_int("NUM_LANES_G", default=1) != 4: return @@ -813,6 +1030,7 @@ async def coaxpress_rx_four_lane_fsm_error_recovery_known_issue_test(dut): "rxNumberOfLane": 3, "dataTReady": 1, "hdrTReady": 1, + "eventTReady": 1, }, ) await reset_signals( @@ -879,16 +1097,13 @@ async def coaxpress_rx_four_lane_fsm_error_recovery_known_issue_test(dut): assert signal_counts["error_pulses"] > 0 assert find_subsequence(observed_header_words, EXPECTED_HDR_WORDS) is not None, observed_header_words subseq_start = find_subsequence(observed_data_words, expected_recovery_words) - # Known issue under investigation: - # a malformed 4-lane header does raise rxFsmError, but the current RTL does - # not fully recover the expected post-error lane rotation and line payloads. assert subseq_start is not None, data_beats observed_recovery_last = [beat[2] for beat in data_beats[subseq_start : subseq_start + len(expected_recovery_words)]] assert observed_recovery_last == [0, 0, 1] * 4, observed_recovery_last -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") -async def coaxpress_rx_four_lane_overflow_reset_recovery_known_issue_test(dut): +@cocotb.test(skip=os.getenv("RUN_STRESS_TESTS") != "1") +async def coaxpress_rx_four_lane_overflow_reset_recovery_stress_test(dut): if env_int("NUM_LANES_G", default=1) != 4: return @@ -903,6 +1118,7 @@ async def coaxpress_rx_four_lane_overflow_reset_recovery_known_issue_test(dut): "rxNumberOfLane": 3, "dataTReady": 0, "hdrTReady": 1, + "eventTReady": 1, }, ) await reset_signals( @@ -982,8 +1198,8 @@ async def coaxpress_rx_four_lane_overflow_reset_recovery_known_issue_test(dut): assert observed_recovery_last == [0, 0, 1] * 4, (signal_counts, observed_recovery_last) -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") -async def coaxpress_rx_four_lane_overflow_recovery_known_issue_test(dut): +@cocotb.test(skip=os.getenv("RUN_STRESS_TESTS") != "1") +async def coaxpress_rx_four_lane_overflow_recovery_stress_test(dut): if env_int("NUM_LANES_G", default=1) != 4: return @@ -998,6 +1214,7 @@ async def coaxpress_rx_four_lane_overflow_recovery_known_issue_test(dut): "rxNumberOfLane": 3, "dataTReady": 0, "hdrTReady": 1, + "eventTReady": 1, }, ) await reset_signals( @@ -1066,10 +1283,6 @@ async def coaxpress_rx_four_lane_overflow_recovery_known_issue_test(dut): await task observed_data_words = [beat[0] for beat in data_beats] - # Known issue under investigation: - # with 4 bonded lanes, sustained sink backpressure can emit rxFsmError - # pulses before or alongside the expected overflow indication. The desired - # behavior is overflow-only, followed by clean post-stall recovery data. assert signal_counts["error_pulses"] == 0, signal_counts assert signal_counts["overflow_pulses"] > 0, signal_counts assert find_subsequence(observed_data_words, expected_recovery_words) is not None, observed_data_words[-64:] @@ -1079,8 +1292,8 @@ async def coaxpress_rx_four_lane_overflow_recovery_known_issue_test(dut): assert observed_recovery_last == [0, 0, 1] * 4, observed_recovery_last -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") -async def coaxpress_rx_repeated_single_line_frame_known_issue_test(dut): +@cocotb.test() +async def coaxpress_rx_repeated_single_line_frame_test(dut): if env_int("NUM_LANES_G", default=1) != 1: return @@ -1093,8 +1306,9 @@ async def coaxpress_rx_repeated_single_line_frame_known_issue_test(dut): "rxLinkUp": 1, "rxFsmRst": 0, "rxNumberOfLane": 0, - "dataTReady": env_int("CXP_RX_KNOWN_ISSUE_DATA_READY", default=0), + "dataTReady": 1, "hdrTReady": 1, + "eventTReady": 1, }, ) await reset_signals( @@ -1105,38 +1319,54 @@ async def coaxpress_rx_repeated_single_line_frame_known_issue_test(dut): release_cycles=4, ) - frame_count = env_int("CXP_RX_REPEATED_FRAME_COUNT", default=72) - vary_packet_fields = env_flag("CXP_RX_KNOWN_ISSUE_VARY_PACKET_FIELDS", default=False) + frame_count = env_int("CXP_RX_REPEATED_FRAME_COUNT", default=16) + vary_packet_fields = env_flag("CXP_RX_REPEATED_FRAME_VARY_PACKET_FIELDS", default=False) signal_counts = {"error_pulses": 0, "overflow_pulses": 0} + data_beats: list[tuple[int, int, int, int]] = [] + hdr_beats: list[tuple[int, int, int, int]] = [] stop_event = Event() monitor_tasks = [ cocotb.start_soon(_count_signal_high_cycles(dut.rxFsmError, dut.rxClk, stop_event, signal_counts, "error_pulses")), cocotb.start_soon(_count_signal_high_cycles(dut.rxOverflow, dut.rxClk, stop_event, signal_counts, "overflow_pulses")), ] + cycle_index = 0 for index in range(frame_count): header_stream_id = (0x50 + (2 * index)) & 0xFF if vary_packet_fields else 0x22 header_packet_tag = (0x70 + (2 * index)) & 0xFF if vary_packet_fields else 0x33 - await _send_one_lane_frame( + cycle_index = await _send_one_lane_frame_and_capture( dut, line_word=0xA0000000 + index, + data_beats=data_beats, + hdr_beats=hdr_beats, header_stream_id=header_stream_id, header_packet_tag=header_packet_tag, line_stream_id=(header_stream_id + 1) & 0xFF if vary_packet_fields else 0x44, line_packet_tag=(header_packet_tag + 1) & 0xFF if vary_packet_fields else 0x55, + start_cycle_index=cycle_index, ) - for _ in range(32): - await send_rx_word(dut, data=CXP_IDLE, data_k=CXP_IDLE_K, clk=dut.rxClk) + await _drive_idle_and_capture(dut, cycles=64, data_beats=data_beats, hdr_beats=hdr_beats, start_cycle_index=cycle_index) stop_event.set() for task in monitor_tasks: await task - assert signal_counts["overflow_pulses"] > 0, ( - f"overflow_pulses={signal_counts['overflow_pulses']} error_pulses={signal_counts['error_pulses']}" - ) + observed_words = [beat[0] for beat in data_beats] + expected_words = [0xA0000000 + index for index in range(frame_count)] assert signal_counts["error_pulses"] == 0, ( f"overflow_pulses={signal_counts['overflow_pulses']} error_pulses={signal_counts['error_pulses']}" ) + assert signal_counts["overflow_pulses"] == 0, signal_counts + assert observed_words == expected_words, data_beats + assert [beat[2] for beat in data_beats] == [1] * frame_count, data_beats + assert [beat[0] for beat in hdr_beats] == [ + 0x3456AA12, + 0x00000001, + 0x00000000, + 0x00000001, + 0x00000000, + 0x00000001, + 0x00200010, + ] * frame_count, hdr_beats PARAMETER_SWEEP = [ diff --git a/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py b/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py index 07d11be1c8..0adedfd93f 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py +++ b/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py @@ -21,8 +21,6 @@ # - Timing: The source holds each beat until `sAxisTReady` rises so the checks # reflect the FSM's actual per-beat acceptance rather than idealized traffic. -import os - import cocotb import pytest from cocotb.triggers import RisingEdge, Timer @@ -42,6 +40,8 @@ start_clock, ) +CXP_RX_STREAM_TRAILER_USER = 1 << 4 + def _capture_outputs(dut, *, header_beats: list[dict[str, int]], data_beats: list[dict[str, int]]) -> None: if int(dut.hdrTValid.value) == 1: @@ -62,18 +62,24 @@ def _capture_outputs(dut, *, header_beats: list[dict[str, int]], data_beats: lis ) -async def _send_handshaked_beat(dut, *, data: int, keep: int, last: int = 0) -> None: +async def _send_handshaked_beat(dut, *, data: int, keep: int, last: int = 0, user: int = 0) -> None: dut.sAxisTValid.value = 1 dut.sAxisTData.value = data dut.sAxisTKeep.value = keep + dut.sAxisTUser.value = user dut.sAxisTLast.value = last await wait_sampled_ready(dut.sAxisTReady, clk=dut.rxClk) dut.sAxisTValid.value = 0 dut.sAxisTData.value = 0 dut.sAxisTKeep.value = 0 + dut.sAxisTUser.value = 0 dut.sAxisTLast.value = 0 +async def _send_trailer_marker(dut) -> None: + await _send_handshaked_beat(dut, data=0, keep=0xF, last=1, user=CXP_RX_STREAM_TRAILER_USER) + + def _beat_data(words: list[int], *, num_lanes: int) -> int: return pack_words(words + [0] * (num_lanes - len(words))) @@ -224,6 +230,7 @@ async def coaxpress_rx_hs_fsm_header_and_lines_test(dut): dut.sAxisTValid.setimmediatevalue(0) dut.sAxisTData.setimmediatevalue(0) dut.sAxisTKeep.setimmediatevalue(0) + dut.sAxisTUser.setimmediatevalue(0) dut.sAxisTLast.setimmediatevalue(0) await reset_dut(dut, reset_names=("rxRst",)) header_beats: list[dict[str, int]] = [] @@ -237,6 +244,8 @@ async def coaxpress_rx_hs_fsm_header_and_lines_test(dut): for word in _header_words(): await _send_handshaked_beat(dut, data=word, keep=0xF) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + await _send_trailer_marker(dut) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) # Follow with two line packets whose final word should close the frame. for line_words in ([0x11111111, 0x22222222, 0x33333333], [0x44444444, 0x55555555, 0x66666666]): @@ -247,8 +256,10 @@ async def coaxpress_rx_hs_fsm_header_and_lines_test(dut): for word in line_words: await _send_handshaked_beat(dut, data=word, keep=0xF) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + await _send_trailer_marker(dut) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) - for _ in range(6): + for _ in range(12): await RisingEdge(dut.rxClk) await Timer(1, unit="ns") _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) @@ -276,6 +287,7 @@ async def coaxpress_rx_hs_fsm_malformed_header_recovery_test(dut): dut.sAxisTValid.setimmediatevalue(0) dut.sAxisTData.setimmediatevalue(0) dut.sAxisTKeep.setimmediatevalue(0) + dut.sAxisTUser.setimmediatevalue(0) dut.sAxisTLast.setimmediatevalue(0) await reset_dut(dut, reset_names=("rxRst",)) header_beats: list[dict[str, int]] = [] @@ -296,6 +308,8 @@ async def coaxpress_rx_hs_fsm_malformed_header_recovery_test(dut): ) error_seen |= int(dut.rxFsmError.value) == 1 _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + await _send_trailer_marker(dut) + error_seen |= int(dut.rxFsmError.value) == 1 await cycle(dut.rxClk, 2) error_seen |= int(dut.rxFsmError.value) == 1 assert error_seen @@ -308,6 +322,8 @@ async def coaxpress_rx_hs_fsm_malformed_header_recovery_test(dut): for word in _header_words(): await _send_handshaked_beat(dut, data=word, keep=0xF) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + await _send_trailer_marker(dut) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) await _send_handshaked_beat(dut, data=CXP_MARKER, keep=0xF) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) await _send_handshaked_beat(dut, data=repeat_byte(CXP_PKT_IMAGE_LINE), keep=0xF) @@ -318,8 +334,10 @@ async def coaxpress_rx_hs_fsm_malformed_header_recovery_test(dut): _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) await _send_handshaked_beat(dut, data=0xABCDEF02, keep=0xF) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + await _send_trailer_marker(dut) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) - for _ in range(6): + for _ in range(12): await RisingEdge(dut.rxClk) await Timer(1, unit="ns") _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) @@ -345,6 +363,7 @@ async def coaxpress_rx_hs_fsm_malformed_header_drops_following_line_test(dut): dut.sAxisTValid.setimmediatevalue(0) dut.sAxisTData.setimmediatevalue(0) dut.sAxisTKeep.setimmediatevalue(0) + dut.sAxisTUser.setimmediatevalue(0) dut.sAxisTLast.setimmediatevalue(0) await reset_dut(dut, reset_names=("rxRst",)) @@ -364,6 +383,8 @@ async def coaxpress_rx_hs_fsm_malformed_header_drops_following_line_test(dut): ) error_seen |= int(dut.rxFsmError.value) == 1 _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + await _send_trailer_marker(dut) + error_seen |= int(dut.rxFsmError.value) == 1 # A line packet arriving after a malformed header must be discarded until a # clean header has been accepted. @@ -377,6 +398,8 @@ async def coaxpress_rx_hs_fsm_malformed_header_drops_following_line_test(dut): await _send_handshaked_beat(dut, data=word, keep=0xF) error_seen |= int(dut.rxFsmError.value) == 1 _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + await _send_trailer_marker(dut) + error_seen |= int(dut.rxFsmError.value) == 1 await cycle(dut.rxClk, 2) error_seen |= int(dut.rxFsmError.value) == 1 @@ -391,6 +414,8 @@ async def coaxpress_rx_hs_fsm_malformed_header_drops_following_line_test(dut): for word in _header_words(): await _send_handshaked_beat(dut, data=word, keep=0xF) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + await _send_trailer_marker(dut) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) await _send_handshaked_beat(dut, data=CXP_MARKER, keep=0xF) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) await _send_handshaked_beat(dut, data=repeat_byte(CXP_PKT_IMAGE_LINE), keep=0xF) @@ -398,8 +423,10 @@ async def coaxpress_rx_hs_fsm_malformed_header_drops_following_line_test(dut): for word in (0xABCDEF00, 0xABCDEF01, 0xABCDEF02): await _send_handshaked_beat(dut, data=word, keep=0xF) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + await _send_trailer_marker(dut) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) - for _ in range(6): + for _ in range(12): await RisingEdge(dut.rxClk) await Timer(1, unit="ns") _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) @@ -414,6 +441,120 @@ async def coaxpress_rx_hs_fsm_malformed_header_drops_following_line_test(dut): ] +@cocotb.test() +async def coaxpress_rx_hs_fsm_new_header_before_frame_complete_test(dut): + if env_int("NUM_LANES_G", default=1) != 1: + return + + start_clock(dut.rxClk) + dut.rxRst.setimmediatevalue(1) + dut.rxFsmRst.setimmediatevalue(0) + dut.sAxisTValid.setimmediatevalue(0) + dut.sAxisTData.setimmediatevalue(0) + dut.sAxisTKeep.setimmediatevalue(0) + dut.sAxisTUser.setimmediatevalue(0) + dut.sAxisTLast.setimmediatevalue(0) + await reset_dut(dut, reset_names=("rxRst",)) + + first_header = _image_header_words_from_fields( + stream_id=0x31, + source_tag=0x1020, + x_size=1, + x_offs=0, + y_size=2, + y_offs=0, + dsize_l=1, + pixel_f=0x0010, + tap_g=0x0020, + flags=0x01, + ) + second_header = _image_header_words_from_fields( + stream_id=0x32, + source_tag=0x3040, + x_size=1, + x_offs=0, + y_size=1, + y_offs=0, + dsize_l=1, + pixel_f=0x0011, + tap_g=0x0021, + flags=0x02, + ) + expected_first_header = _expected_header_data_from_fields( + stream_id=0x31, + source_tag=0x1020, + x_size=1, + x_offs=0, + y_size=2, + y_offs=0, + dsize_l=1, + pixel_f=0x0010, + tap_g=0x0020, + flags=0x01, + ) + expected_second_header = _expected_header_data_from_fields( + stream_id=0x32, + source_tag=0x3040, + x_size=1, + x_offs=0, + y_size=1, + y_offs=0, + dsize_l=1, + pixel_f=0x0011, + tap_g=0x0021, + flags=0x02, + ) + + header_beats: list[dict[str, int]] = [] + data_beats: list[dict[str, int]] = [] + error_seen = False + + async def send_and_capture(data: int) -> None: + nonlocal error_seen + await _send_handshaked_beat(dut, data=data, keep=0xF) + error_seen |= int(dut.rxFsmError.value) == 1 + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + + await send_and_capture(CXP_MARKER) + await send_and_capture(repeat_byte(CXP_PKT_IMAGE_HEADER)) + for word in first_header: + await send_and_capture(word) + await _send_trailer_marker(dut) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + + await send_and_capture(CXP_MARKER) + await send_and_capture(repeat_byte(CXP_PKT_IMAGE_LINE)) + await send_and_capture(0x11111111) + await _send_trailer_marker(dut) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + + # Starting a new rectangular image header before the declared two-line frame + # has completed is explicitly detected by the current RTL. + await send_and_capture(CXP_MARKER) + await send_and_capture(repeat_byte(CXP_PKT_IMAGE_HEADER)) + await cycle(dut.rxClk, 1) + error_seen |= int(dut.rxFsmError.value) == 1 + for word in second_header: + await send_and_capture(word) + await _send_trailer_marker(dut) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + + for _ in range(12): + await RisingEdge(dut.rxClk) + await Timer(1, unit="ns") + error_seen |= int(dut.rxFsmError.value) == 1 + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + + assert error_seen + assert header_beats == [ + {"hdrTData": expected_first_header, "hdrTLast": 1, "hdrTSof": 1}, + {"hdrTData": expected_second_header, "hdrTLast": 1, "hdrTSof": 1}, + ] + assert data_beats == [ + {"dataTData": 0x11111111, "dataTKeep": 0xF, "dataTLast": 0}, + ] + + @cocotb.test() async def coaxpress_rx_hs_fsm_two_lane_step_alignment_test(dut): if env_int("NUM_LANES_G", default=1) != 2: @@ -425,6 +566,7 @@ async def coaxpress_rx_hs_fsm_two_lane_step_alignment_test(dut): dut.sAxisTValid.setimmediatevalue(0) dut.sAxisTData.setimmediatevalue(0) dut.sAxisTKeep.setimmediatevalue(0) + dut.sAxisTUser.setimmediatevalue(0) dut.sAxisTLast.setimmediatevalue(0) await reset_dut(dut, reset_names=("rxRst",)) @@ -470,6 +612,8 @@ async def coaxpress_rx_hs_fsm_two_lane_step_alignment_test(dut): keep=lane_keep_mask(list(range(len(words)))), ) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + await _send_trailer_marker(dut) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) await _send_handshaked_beat( dut, @@ -489,8 +633,10 @@ async def coaxpress_rx_hs_fsm_two_lane_step_alignment_test(dut): keep=lane_keep_mask([0, 1]), ) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + await _send_trailer_marker(dut) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) - for _ in range(8): + for _ in range(12): await RisingEdge(dut.rxClk) await Timer(1, unit="ns") _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) @@ -537,6 +683,7 @@ async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): dut.sAxisTValid.setimmediatevalue(0) dut.sAxisTData.setimmediatevalue(0) dut.sAxisTKeep.setimmediatevalue(0) + dut.sAxisTUser.setimmediatevalue(0) dut.sAxisTLast.setimmediatevalue(0) await reset_dut(dut, reset_names=("rxRst",)) @@ -600,6 +747,8 @@ async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): keep=lane_keep_mask([0]), ) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + await _send_trailer_marker(dut) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) await _send_handshaked_beat( dut, @@ -614,14 +763,16 @@ async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): ) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) - # Cover the merged corner case: the first line ends while the remaining - # words in the same 4-lane beat already contain the next line marker/type. + # Cover the partial-line packing corner case: the first line contributes + # only two words to the final 4-lane output beat, then its trailer verdict + # arrives before the next line completes the packed SSI frame. dut.sAxisTValid.value = 1 dut.sAxisTData.value = _beat_data( - [0x11111111, 0x22222222, CXP_MARKER, repeat_byte(CXP_PKT_IMAGE_LINE)], + [0x11111111, 0x22222222], num_lanes=4, ) - dut.sAxisTKeep.value = lane_keep_mask([0, 1, 2, 3]) + dut.sAxisTKeep.value = lane_keep_mask([0, 1]) + dut.sAxisTUser.value = 0 dut.sAxisTLast.value = 0 shared_beat_cycles = 0 while True: @@ -639,16 +790,29 @@ async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): dut.sAxisTValid.value = 0 dut.sAxisTData.value = 0 dut.sAxisTKeep.value = 0 + dut.sAxisTUser.value = 0 dut.sAxisTLast.value = 0 + await _send_trailer_marker(dut) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + + await _send_handshaked_beat( + dut, + data=_beat_data([CXP_MARKER, repeat_byte(CXP_PKT_IMAGE_LINE)], num_lanes=4), + keep=lane_keep_mask([0, 1]), + ) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + await _send_handshaked_beat( dut, data=_beat_data([0x33333333, 0x44444444], num_lanes=4), keep=lane_keep_mask([0, 1]), ) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) + await _send_trailer_marker(dut) + _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) - for _ in range(8): + for _ in range(20): await RisingEdge(dut.rxClk) await Timer(1, unit="ns") error_seen |= int(dut.rxFsmError.value) == 1 @@ -669,8 +833,8 @@ async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): ], trace -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") -async def coaxpress_rx_hs_fsm_repeated_single_line_frame_known_issue_test(dut): +@cocotb.test() +async def coaxpress_rx_hs_fsm_repeated_single_line_frame_test(dut): if env_int("NUM_LANES_G", default=1) != 1: return @@ -680,6 +844,7 @@ async def coaxpress_rx_hs_fsm_repeated_single_line_frame_known_issue_test(dut): dut.sAxisTValid.setimmediatevalue(0) dut.sAxisTData.setimmediatevalue(0) dut.sAxisTKeep.setimmediatevalue(0) + dut.sAxisTUser.setimmediatevalue(0) dut.sAxisTLast.setimmediatevalue(0) await reset_dut(dut, reset_names=("rxRst",)) @@ -694,12 +859,16 @@ async def coaxpress_rx_hs_fsm_repeated_single_line_frame_known_issue_test(dut): for word in _single_line_header_words(): await _send_handshaked_beat(dut, data=word, keep=0xF) error_pulses += int(dut.rxFsmError.value) + await _send_trailer_marker(dut) + error_pulses += int(dut.rxFsmError.value) await _send_handshaked_beat(dut, data=CXP_MARKER, keep=0xF) error_pulses += int(dut.rxFsmError.value) await _send_handshaked_beat(dut, data=repeat_byte(CXP_PKT_IMAGE_LINE), keep=0xF) error_pulses += int(dut.rxFsmError.value) await _send_handshaked_beat(dut, data=0xA0000000 + index, keep=0xF) error_pulses += int(dut.rxFsmError.value) + await _send_trailer_marker(dut) + error_pulses += int(dut.rxFsmError.value) await cycle(dut.rxClk, 8) error_pulses += sum(int(dut.rxFsmError.value) for _ in range(1)) diff --git a/tests/protocols/coaxpress/test_CoaXPressRxLane.py b/tests/protocols/coaxpress/test_CoaXPressRxLane.py index 7925977217..87313d35b4 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRxLane.py +++ b/tests/protocols/coaxpress/test_CoaXPressRxLane.py @@ -25,6 +25,8 @@ from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import ( + CXP_ACK_SUCCESS, + CXP_ACK_SUCCESS_ALT, CXP_EOP, CXP_IDLE, CXP_IDLE_K, @@ -58,6 +60,48 @@ def _event_crc_words(*, event_bytes: tuple[int, int, int, int], packet_tag: int, ] +def _control_ack_crc_words( + *, + ack_code: int, + size_word: int, + data_word: int | None = None, + packet_tag: int | None = None, +) -> list[int]: + crc_inputs = [] + if packet_tag is not None: + crc_inputs.append(repeat_byte(packet_tag)) + crc_inputs.append(repeat_byte(ack_code)) + crc_inputs.append(size_word) + if data_word is not None: + crc_inputs.append(data_word) + return [ + *crc_inputs, + cxp_crc_word(crc_inputs), + ] + + +def _heartbeat_crc_words(payload_bytes: range) -> list[int]: + crc_inputs = [repeat_byte(byte) for byte in payload_bytes] + return [ + *crc_inputs, + cxp_crc_word(crc_inputs), + ] + + +def _stream_crc_words(*, stream_id: int, packet_tag: int, payload_words: list[int]) -> list[int]: + crc_inputs = [ + repeat_byte(stream_id), + repeat_byte(packet_tag), + repeat_byte((len(payload_words) >> 8) & 0xFF), + repeat_byte(len(payload_words) & 0xFF), + *payload_words, + ] + return [ + *crc_inputs, + cxp_crc_word(crc_inputs), + ] + + @cocotb.test() async def coaxpress_rx_lane_stream_and_io_ack_test(dut): start_clock(dut.rxClk) @@ -79,7 +123,7 @@ async def drive(data: int, data_k: int) -> None: clk=dut.rxClk, capture=data_beats, valid_name="dataTValid", - field_names=("dataTData", "dataTUser", "dataTLast"), + field_names=("dataTData", "dataTKeep", "dataTUser", "dataTLast"), ) io_ack_pulses += int(dut.ioAck.value) @@ -94,18 +138,23 @@ async def drive(data: int, data_k: int) -> None: await drive(repeat_byte(0x03), 0x0) await drive(CXP_IO_ACK, 0xF) await drive(repeat_byte(0x01), 0x0) - await drive(0x11223344, 0x0) - await drive(0x55667788, 0x5) - await drive(0x99AABBCC, 0x0) - await drive(0xDEADBEEF, 0x0) + stream_payload = [0x11223344, 0x55667788, 0x99AABBCC] + await drive(stream_payload[0], 0x0) + await drive(stream_payload[1], 0x5) + await drive(stream_payload[2], 0x0) + await drive( + cxp_crc_word([repeat_byte(0x22), repeat_byte(0x33), repeat_byte(0x00), repeat_byte(0x03), *stream_payload]), + 0x0, + ) await drive(CXP_EOP, 0xF) await drive(CXP_IDLE, CXP_IDLE_K) assert io_ack_pulses == 1 assert data_beats == [ - {"dataTData": 0x11223344, "dataTUser": 0x0, "dataTLast": 0}, - {"dataTData": 0x55667788, "dataTUser": 0x5, "dataTLast": 0}, - {"dataTData": 0x99AABBCC, "dataTUser": 0x0, "dataTLast": 1}, + {"dataTData": 0x11223344, "dataTKeep": 0xF, "dataTUser": 0x0, "dataTLast": 0}, + {"dataTData": 0x55667788, "dataTKeep": 0xF, "dataTUser": 0x5, "dataTLast": 0}, + {"dataTData": 0x99AABBCC, "dataTKeep": 0xF, "dataTUser": 0x0, "dataTLast": 1}, + {"dataTData": 0x00000000, "dataTKeep": 0xF, "dataTUser": 0x0, "dataTLast": 1}, ] @@ -120,6 +169,7 @@ async def coaxpress_rx_lane_spec_prefix_control_event_and_heartbeat_test(dut): cfg_beats: list[dict[str, int]] = [] heartbeat_beats: list[dict[str, int]] = [] + event_beats: list[dict[str, int]] = [] event_pulses: list[tuple[int, int]] = [] async def drive(data: int, data_k: int, *, link_up: int = 1) -> None: @@ -139,6 +189,15 @@ async def drive(data: int, data_k: int, *, link_up: int = 1) -> None: "heartbeatTLast": int(dut.heartbeatTLast.value), } ) + if int(dut.eventTValid.value) == 1: + event_beats.append( + { + "eventTData": int(dut.eventTData.value), + "eventTDest": int(dut.eventTDest.value), + "eventTUser": int(dut.eventTUser.value), + "eventTLast": int(dut.eventTLast.value), + } + ) if int(dut.eventAck.value) == 1: event_pulses.append((int(dut.eventAck.value), int(dut.eventTag.value))) @@ -146,40 +205,47 @@ async def drive(data: int, data_k: int, *, link_up: int = 1) -> None: # code 0x00, size=4 bytes, one reply-data word, CRC, EOP. await drive(CXP_SOP, 0xF) await drive(repeat_byte(CXP_PKT_CTRL_ACK_NO_TAG), 0x0) - await drive(repeat_byte(0x00), 0x0) - await drive(0x04000000, 0x0) - await drive(0x01234567, 0x0) - await drive(0xCAFEBABE, 0x0) + for word in _control_ack_crc_words(ack_code=0x00, size_word=0x04000000, data_word=0x01234567): + await drive(word, 0x0) await drive(CXP_EOP, 0xF) # Drive one alternate-success acknowledgment code. The current RTL maps # 0x04 to the same zero-success status word as 0x01. await drive(CXP_SOP, 0xF) await drive(repeat_byte(CXP_PKT_CTRL_ACK_NO_TAG), 0x0) - await drive(repeat_byte(0x04), 0x0) - await drive(0x04000000, 0x0) - await drive(0x76543210, 0x0) - await drive(0x0BADCAFE, 0x0) + for word in _control_ack_crc_words(ack_code=0x04, size_word=0x04000000, data_word=0x76543210): + await drive(word, 0x0) + await drive(CXP_EOP, 0xF) + + # Drive a write acknowledgment (size=0, no data word). This is the format + # cameras send for register writes per CXP-001-2021 Section 9.5.2.2. + await drive(CXP_SOP, 0xF) + await drive(repeat_byte(CXP_PKT_CTRL_ACK_NO_TAG), 0x0) + for word in _control_ack_crc_words(ack_code=CXP_ACK_SUCCESS, size_word=0x00000000): + await drive(word, 0x0) await drive(CXP_EOP, 0xF) - # Drive one spec-shaped tagged read acknowledgment. The current RTL skips - # the tag word, then forwards the first reply-data word with a zeroed - # success status in the low 32 bits. + # Drive a tagged write acknowledgment (size=0, no data word). await drive(CXP_SOP, 0xF) await drive(repeat_byte(CXP_PKT_CTRL_ACK_WITH_TAG), 0x0) - await drive(repeat_byte(0x55), 0x0) - await drive(repeat_byte(0x00), 0x0) - await drive(0x04000000, 0x0) - await drive(0x89ABCDEF, 0x0) - await drive(0xFEEDBEEF, 0x0) + for word in _control_ack_crc_words(ack_code=CXP_ACK_SUCCESS_ALT, size_word=0x00000000, packet_tag=0xAA): + await drive(word, 0x0) + await drive(CXP_EOP, 0xF) + + # Drive one spec-shaped tagged read acknowledgment. The RTL includes the tag + # in the CRC, then forwards the first reply-data word with a zeroed success + # status in the low 32 bits after the trailer passes. + await drive(CXP_SOP, 0xF) + await drive(repeat_byte(CXP_PKT_CTRL_ACK_WITH_TAG), 0x0) + for word in _control_ack_crc_words(ack_code=0x00, size_word=0x04000000, data_word=0x89ABCDEF, packet_tag=0x55): + await drive(word, 0x0) await drive(CXP_EOP, 0xF) # Heartbeat first keeps the on-wire ordering consistent before the event. await drive(CXP_SOP, 0xF) await drive(repeat_byte(CXP_PKT_HEARTBEAT), 0x0) - for word in range(0x20, 0x2C): - await drive(repeat_byte(word), 0x0) - await drive(0xB6B6B6B6, 0x0) + for word in _heartbeat_crc_words(range(0x20, 0x2C)): + await drive(word, 0x0) await drive(CXP_EOP, 0xF) # Drive a fuller event packet shape. The current RTL only consumes the @@ -202,9 +268,19 @@ async def drive(data: int, data_k: int, *, link_up: int = 1) -> None: assert cfg_beats == [ {"cfgTData": (0x01234567 << 32)}, {"cfgTData": (0x76543210 << 32)}, + {"cfgTData": (0xFFFFFFFF << 32)}, + {"cfgTData": (0xFFFFFFFF << 32)}, {"cfgTData": (0x89ABCDEF << 32)}, ] assert event_pulses == [(1, 0x5A)] + assert event_beats == [ + { + "eventTData": 0x11223344, + "eventTDest": 0x5A, + "eventTUser": 0x13121110, + "eventTLast": 1, + } + ] assert heartbeat_beats == [ { "heartbeatTData": sum((word << (8 * (word - 0x20))) for word in range(0x20, 0x2C)), @@ -225,9 +301,12 @@ async def coaxpress_rx_lane_event_payload_crc_guardrail_test(dut): cfg_beats: list[dict[str, int]] = [] data_beats: list[dict[str, int]] = [] heartbeat_beats: list[dict[str, int]] = [] + event_beats: list[dict[str, int]] = [] event_tags: list[int] = [] + error_pulses = 0 async def drive(data: int, data_k: int) -> None: + nonlocal error_pulses await send_rx_word(dut, data=data, data_k=data_k, clk=dut.rxClk) if int(dut.cfgTValid.value) == 1: cfg_beats.append({"cfgTData": int(dut.cfgTData.value)}) @@ -235,17 +314,28 @@ async def drive(data: int, data_k: int) -> None: data_beats.append( { "dataTData": int(dut.dataTData.value), + "dataTKeep": int(dut.dataTKeep.value), "dataTUser": int(dut.dataTUser.value), "dataTLast": int(dut.dataTLast.value), } ) if int(dut.heartbeatTValid.value) == 1: heartbeat_beats.append({"heartbeatTData": int(dut.heartbeatTData.value)}) + if int(dut.eventTValid.value) == 1: + event_beats.append( + { + "eventTData": int(dut.eventTData.value), + "eventTDest": int(dut.eventTDest.value), + "eventTUser": int(dut.eventTUser.value), + "eventTLast": int(dut.eventTLast.value), + } + ) if int(dut.eventAck.value) == 1: event_tags.append(int(dut.eventTag.value)) + error_pulses += int(dut.rxError.value) # The receive-lane RTL validates the event payload count, CRC, and EOP before - # acknowledging the tag. The payload is intentionally not forwarded anywhere. + # acknowledging the tag and releasing the payload stream. await drive(CXP_SOP, 0xF) await drive(repeat_byte(CXP_PKT_EVENT), 0x0) for word in _event_crc_words( @@ -256,6 +346,14 @@ async def drive(data: int, data_k: int) -> None: await drive(word, 0x0) await drive(CXP_EOP, 0xF) + # A bad heartbeat CRC must also suppress the heartbeat output. + await drive(CXP_SOP, 0xF) + await drive(repeat_byte(CXP_PKT_HEARTBEAT), 0x0) + bad_heartbeat_words = _heartbeat_crc_words(range(0x30, 0x3C)) + for word in [*bad_heartbeat_words[:-1], bad_heartbeat_words[-1] ^ 0x00000001]: + await drive(word, 0x0) + await drive(CXP_EOP, 0xF) + # A bad CRC must suppress the acknowledgment and still leave the parser ready # for a later clean event. await drive(CXP_SOP, 0xF) @@ -265,6 +363,19 @@ async def drive(data: int, data_k: int) -> None: await drive(word, 0x0) await drive(CXP_EOP, 0xF) + # Oversized event payloads exceed the bounded receive-side store. The lane + # must reject them before any payload word can leak out. + await drive(CXP_SOP, 0xF) + await drive(repeat_byte(CXP_PKT_EVENT), 0x0) + for word in [ + *[repeat_byte(byte) for byte in (0xC0, 0xC1, 0xC2, 0xC3)], + repeat_byte(0x4C), + repeat_byte(0x00), + repeat_byte(0x11), + ]: + await drive(word, 0x0) + await drive(CXP_IDLE, CXP_IDLE_K) + # A later zero-payload event must still be accepted, proving the ignored # bad-CRC packet did not leave stale parser state behind. await drive(CXP_SOP, 0xF) @@ -275,9 +386,189 @@ async def drive(data: int, data_k: int) -> None: await drive(CXP_IDLE, CXP_IDLE_K) assert event_tags == [0x6D, 0x7E] + assert event_beats == [ + { + "eventTData": 0x11223344, + "eventTDest": 0x6D, + "eventTUser": 0xA3A2A1A0, + "eventTLast": 0, + }, + { + "eventTData": 0x55667788, + "eventTDest": 0x6D, + "eventTUser": 0xA3A2A1A0, + "eventTLast": 1, + }, + ] assert cfg_beats == [] assert data_beats == [] assert heartbeat_beats == [] + assert error_pulses >= 2 + + +@cocotb.test() +async def coaxpress_rx_lane_control_ack_crc_eop_guardrail_test(dut): + start_clock(dut.rxClk) + dut.rxRst.setimmediatevalue(1) + dut.rxLinkUp.setimmediatevalue(1) + dut.rxData.setimmediatevalue(CXP_IDLE) + dut.rxDataK.setimmediatevalue(CXP_IDLE_K) + await reset_dut(dut) + + cfg_beats: list[dict[str, int]] = [] + + async def drive(data: int, data_k: int) -> None: + await send_rx_word( + dut, + data=data, + data_k=data_k, + clk=dut.rxClk, + capture=cfg_beats, + valid_name="cfgTValid", + field_names=("cfgTData",), + ) + + # A bad CRC must suppress the acknowledgment and still leave the parser ready + # for a later packet. + await drive(CXP_SOP, 0xF) + await drive(repeat_byte(CXP_PKT_CTRL_ACK_NO_TAG), 0x0) + bad_crc_words = _control_ack_crc_words(ack_code=CXP_ACK_SUCCESS, size_word=0x04000000, data_word=0x12345678) + for word in [*bad_crc_words[:-1], bad_crc_words[-1] ^ 0x00000001]: + await drive(word, 0x0) + await drive(CXP_EOP, 0xF) + + # A correct CRC followed by a malformed EOP must also suppress the response. + await drive(CXP_SOP, 0xF) + await drive(repeat_byte(CXP_PKT_CTRL_ACK_NO_TAG), 0x0) + for word in _control_ack_crc_words(ack_code=CXP_ACK_SUCCESS_ALT, size_word=0x04000000, data_word=0xDEADBEEF): + await drive(word, 0x0) + await drive(0x01020304, 0x0) + + # A later clean acknowledgment must still be decoded after the malformed + # trailer words. + await drive(CXP_SOP, 0xF) + await drive(repeat_byte(CXP_PKT_CTRL_ACK_NO_TAG), 0x0) + for word in _control_ack_crc_words(ack_code=CXP_ACK_SUCCESS_ALT, size_word=0x04000000, data_word=0x87654321): + await drive(word, 0x0) + await drive(CXP_EOP, 0xF) + await drive(CXP_IDLE, CXP_IDLE_K) + + assert cfg_beats == [ + {"cfgTData": (0x87654321 << 32)}, + ] + + +@cocotb.test() +async def coaxpress_rx_lane_control_ack_success_compatibility_test(dut): + start_clock(dut.rxClk) + dut.rxRst.setimmediatevalue(1) + dut.rxLinkUp.setimmediatevalue(1) + dut.rxData.setimmediatevalue(CXP_IDLE) + dut.rxDataK.setimmediatevalue(CXP_IDLE_K) + await reset_dut(dut) + + cfg_beats: list[dict[str, int]] = [] + + async def drive(data: int, data_k: int) -> None: + await send_rx_word( + dut, + data=data, + data_k=data_k, + clk=dut.rxClk, + capture=cfg_beats, + valid_name="cfgTValid", + field_names=("cfgTData",), + ) + + # Hardware seen in the lab can return the write-success code in P0 only and + # omit the explicit zero-size word. Keep that accepted while preserving the + # stricter CRC/EOP validation added for normal control acknowledgments. + low_byte_success = 0x00000001 + await drive(CXP_SOP, 0xF) + await drive(repeat_byte(CXP_PKT_CTRL_ACK_NO_TAG), 0x0) + await drive(low_byte_success, 0x0) + await drive(cxp_crc_word([low_byte_success]), 0x0) + await drive(CXP_EOP, 0xF) + + # Also accept the alternate success code in P0 with an explicit zero-size + # write-ACK word. + low_byte_success_alt = 0x00000004 + await drive(CXP_SOP, 0xF) + await drive(repeat_byte(CXP_PKT_CTRL_ACK_NO_TAG), 0x0) + await drive(low_byte_success_alt, 0x0) + await drive(0x00000000, 0x0) + await drive(cxp_crc_word([low_byte_success_alt, 0x00000000]), 0x0) + await drive(CXP_EOP, 0xF) + await drive(CXP_IDLE, CXP_IDLE_K) + + assert cfg_beats == [ + {"cfgTData": 0xFFFFFFFF00000000}, + {"cfgTData": 0xFFFFFFFF00000000}, + ] + + +@cocotb.test() +async def coaxpress_rx_lane_stream_crc_eop_guardrail_test(dut): + start_clock(dut.rxClk) + dut.rxRst.setimmediatevalue(1) + dut.rxLinkUp.setimmediatevalue(1) + dut.rxData.setimmediatevalue(CXP_IDLE) + dut.rxDataK.setimmediatevalue(CXP_IDLE_K) + await reset_dut(dut) + + observed: list[dict[str, int]] = [] + error_pulses = 0 + + async def drive(data: int, data_k: int) -> None: + nonlocal error_pulses + await send_rx_word( + dut, + data=data, + data_k=data_k, + clk=dut.rxClk, + capture=observed, + valid_name="dataTValid", + field_names=("dataTData", "dataTKeep", "dataTUser", "dataTLast"), + ) + error_pulses += int(dut.rxError.value) + + # A new SOP where the stream CRC belongs must not be accepted as a fresh + # packet. This locks down the receive-lane fix that keeps stream packets in + # the trailer parser after the declared payload word count has been emitted. + await drive(CXP_SOP, 0xF) + await drive(repeat_byte(CXP_PKT_STREAM_DATA), 0x0) + for word in _stream_crc_words(stream_id=0x10, packet_tag=0x11, payload_words=[0x11111111])[:-1]: + await drive(word, 0x0) + await drive(CXP_SOP, 0xF) + await drive(repeat_byte(CXP_PKT_STREAM_DATA), 0x0) + await drive(repeat_byte(0x20), 0x0) + await drive(repeat_byte(0x21), 0x0) + await drive(repeat_byte(0x00), 0x0) + await drive(repeat_byte(0x01), 0x0) + await drive(0x22222222, 0x0) + await drive( + cxp_crc_word([repeat_byte(0x20), repeat_byte(0x21), repeat_byte(0x00), repeat_byte(0x01), 0x22222222]), + 0x0, + ) + await drive(CXP_EOP, 0xF) + + # A later clean stream packet must still recover after the malformed + # trailer path returned to IDLE. + clean_payload = [0x33333333] + await drive(CXP_SOP, 0xF) + await drive(repeat_byte(CXP_PKT_STREAM_DATA), 0x0) + for word in _stream_crc_words(stream_id=0x30, packet_tag=0x31, payload_words=clean_payload): + await drive(word, 0x0) + await drive(CXP_EOP, 0xF) + await drive(CXP_IDLE, CXP_IDLE_K) + + assert observed == [ + {"dataTData": 0x11111111, "dataTKeep": 0xF, "dataTUser": 0x0, "dataTLast": 1}, + {"dataTData": 0x00000000, "dataTKeep": 0xF, "dataTUser": 0x1, "dataTLast": 1}, + {"dataTData": 0x33333333, "dataTKeep": 0xF, "dataTUser": 0x0, "dataTLast": 1}, + {"dataTData": 0x00000000, "dataTKeep": 0xF, "dataTUser": 0x0, "dataTLast": 1}, + ] + assert error_pulses == 1 @cocotb.test() @@ -306,11 +597,8 @@ async def coaxpress_rx_lane_error_recovery_test(dut): for data, data_k in ( (CXP_SOP, 0xF), (repeat_byte(CXP_PKT_STREAM_DATA), 0x0), - (repeat_byte(0xAA), 0x0), - (repeat_byte(0xBB), 0x0), - (repeat_byte(0x00), 0x0), - (repeat_byte(0x01), 0x0), - (0x55667788, 0x0), + *[(word, 0x0) for word in _stream_crc_words(stream_id=0xAA, packet_tag=0xBB, payload_words=[0x55667788])], + (CXP_EOP, 0xF), (CXP_IDLE, CXP_IDLE_K), ): await send_rx_word( @@ -320,10 +608,13 @@ async def coaxpress_rx_lane_error_recovery_test(dut): clk=dut.rxClk, capture=observed, valid_name="dataTValid", - field_names=("dataTData", "dataTLast"), + field_names=("dataTData", "dataTKeep", "dataTUser", "dataTLast"), ) - assert observed == [{"dataTData": 0x55667788, "dataTLast": 1}] + assert observed == [ + {"dataTData": 0x55667788, "dataTKeep": 0xF, "dataTUser": 0x0, "dataTLast": 1}, + {"dataTData": 0x00000000, "dataTKeep": 0xF, "dataTUser": 0x0, "dataTLast": 1}, + ] def test_CoaXPressRxLane(): diff --git a/tests/protocols/coaxpress/test_CoaXPressRxLaneMux.py b/tests/protocols/coaxpress/test_CoaXPressRxLaneMux.py index bba263f324..58009ce823 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRxLaneMux.py +++ b/tests/protocols/coaxpress/test_CoaXPressRxLaneMux.py @@ -28,13 +28,17 @@ from tests.common.regression_utils import env_int, parameter_case, run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import pack_words, reset_dut, start_clock +CXP_RX_STREAM_TRAILER_USER = 1 << 4 + def _set_lane_inputs(dut, lane_beats, *, num_lanes: int) -> None: lane_width = 32 * num_lanes keep_width = 4 * num_lanes + user_width = 8 valid = 0 data = 0 keep = 0 + user = 0 last = 0 for lane, beat in enumerate(lane_beats): if beat is None: @@ -42,10 +46,12 @@ def _set_lane_inputs(dut, lane_beats, *, num_lanes: int) -> None: valid |= 1 << lane data |= beat["data"] << (lane * lane_width) keep |= beat["keep"] << (lane * keep_width) + user |= beat.get("user", 0) << (lane * user_width) last |= beat["last"] << lane dut.sAxisTValid.value = valid dut.sAxisTData.value = data dut.sAxisTKeep.value = keep + dut.sAxisTUser.value = user dut.sAxisTLast.value = last @@ -60,6 +66,7 @@ async def coaxpress_rx_lane_mux_round_robin_test(dut): dut.sAxisTValid.setimmediatevalue(0) dut.sAxisTData.setimmediatevalue(0) dut.sAxisTKeep.setimmediatevalue(0) + dut.sAxisTUser.setimmediatevalue(0) dut.sAxisTLast.setimmediatevalue(0) await reset_dut(dut, reset_names=("rxRst",)) @@ -67,9 +74,16 @@ async def coaxpress_rx_lane_mux_round_robin_test(dut): [ {"data": pack_words([0x10, 0x11, 0x12]), "keep": 0x0FFF, "last": 0}, {"data": pack_words([0x13, 0x14, 0x15]), "keep": 0x0FFF, "last": 1}, + {"data": 0, "keep": 0x0FFF, "user": CXP_RX_STREAM_TRAILER_USER, "last": 1}, + ], + [ + {"data": pack_words([0x20, 0x21, 0x22]), "keep": 0x0FFF, "last": 1}, + {"data": 0, "keep": 0x0FFF, "user": CXP_RX_STREAM_TRAILER_USER, "last": 1}, + ], + [ + {"data": pack_words([0x30, 0x31, 0x32]), "keep": 0x0FFF, "last": 1}, + {"data": 0, "keep": 0x0FFF, "user": CXP_RX_STREAM_TRAILER_USER, "last": 1}, ], - [{"data": pack_words([0x20, 0x21, 0x22]), "keep": 0x0FFF, "last": 1}], - [{"data": pack_words([0x30, 0x31, 0x32]), "keep": 0x0FFF, "last": 1}], ] observed: list[tuple[int, int, int]] = [] @@ -105,8 +119,11 @@ async def coaxpress_rx_lane_mux_round_robin_test(dut): assert observed == [ (pack_words([0x10, 0x11, 0x12]), 0x0FFF, 0), (pack_words([0x13, 0x14, 0x15]), 0x0FFF, 1), + (0, 0x0FFF, 1), (pack_words([0x20, 0x21, 0x22]), 0x0FFF, 1), + (0, 0x0FFF, 1), (pack_words([0x30, 0x31, 0x32]), 0x0FFF, 1), + (0, 0x0FFF, 1), ] @@ -122,6 +139,7 @@ def test_CoaXPressRxLaneMux(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ + "protocols/coaxpress/core/rtl/CoaXPressPkg.vhd", "protocols/coaxpress/core/rtl/CoaXPressRxLaneMux.vhd", "protocols/coaxpress/core/wrappers/CoaXPressRxLaneMuxWrapper.vhd", ] diff --git a/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py b/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py index b3e3e6d69f..301397dd55 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py +++ b/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py @@ -410,6 +410,7 @@ def test_CoaXPressRxWordPacker(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ + "protocols/coaxpress/core/rtl/CoaXPressPkg.vhd", "protocols/coaxpress/core/rtl/CoaXPressRxWordPacker.vhd", "protocols/coaxpress/core/wrappers/CoaXPressRxWordPackerWrapper.vhd", ] diff --git a/tests/protocols/packetizer/__init__.py b/tests/protocols/packetizer/__init__.py new file mode 100644 index 0000000000..b0085f1a17 --- /dev/null +++ b/tests/protocols/packetizer/__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/packetizer/packetizer_test_utils.py b/tests/protocols/packetizer/packetizer_test_utils.py new file mode 100644 index 0000000000..53c824125b --- /dev/null +++ b/tests/protocols/packetizer/packetizer_test_utils.py @@ -0,0 +1,469 @@ +############################################################################## +## 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 + +import os +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 + +PACKETIZER2_VERSION = 0x2 +PACKETIZER2_CRC_NONE = 0x0 +PACKETIZER2_CRC_DATA = 0x1 +PACKETIZER2_CRC_FULL = 0x2 +PACKETIZER0_VERSION = 0x0 +SSI_EOFE = 0 +SSI_SOF = 1 +DEBUG_INIT_DONE = 12 + +CRC_MODE_VALUES = { + "NONE": PACKETIZER2_CRC_NONE, + "DATA": PACKETIZER2_CRC_DATA, + "FULL": PACKETIZER2_CRC_FULL, +} + + +@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 = 128) -> 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_packetizer_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_packetizer_dut(dut, *, cycles: int = 4) -> None: + dut.axisRst.setimmediatevalue(1) + await cycle(dut.axisClk, cycles) + dut.axisRst.value = 0 + await cycle(dut.axisClk, 2) + + +async def wait_debug_init_done(dut, *, timeout_cycles: int = 64) -> None: + for _ in range(timeout_cycles): + if int(dut.debugOut.value) & (1 << DEBUG_INIT_DONE): + return + await RisingEdge(dut.axisClk) + await Timer(1, unit="ns") + raise AssertionError("Timed out waiting for depacketizer initDone") + + +def crc_mode_from_env(default: str = "NONE") -> int: + return CRC_MODE_VALUES[os.getenv("CRC_MODE_G", default)] + + +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 payload_to_beats( + payload: bytes, + *, + dest: int, + tid: 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) + keep = (1 << len(chunk)) - 1 + user = 0 + if is_first: + user |= first_user + if is_last: + user |= last_user << (8 * (len(chunk) - 1)) + beats.append( + AxisBeat( + data=word_from_bytes(chunk), + keep=keep, + last=int(is_last), + dest=dest, + tid=tid, + user=user, + ) + ) + return beats + + +def user_from_bytes(values: list[int]) -> int: + user = 0 + for lane, value in enumerate(values): + user |= (value & 0xFF) << (8 * lane) + return user + + +def tuser_for_lane(lane: int, value: int) -> int: + return (value & 0xFF) << (8 * lane) + + +def packetizer2_header_word(*, crc_mode: int, sof: int, tuser: int, tdest: int, tid: int, seq: int) -> int: + return ( + (PACKETIZER2_VERSION & 0xF) + | ((crc_mode & 0xF) << 4) + | ((tuser & 0xFF) << 8) + | ((tdest & 0xFF) << 16) + | ((tid & 0xFF) << 24) + | ((seq & 0xFFFF) << 32) + | ((sof & 0x1) << 63) + ) + + +def packetizer2_tail_word(*, eof: int, tuser: int, byte_count: int, crc: int = 0) -> int: + return ( + (tuser & 0xFF) + | ((eof & 0x1) << 8) + | ((byte_count & 0xF) << 16) + | ((crc & 0xFFFFFFFF) << 32) + ) + + +def packetizer0_header_word(*, frame: int, packet: int, tdest: int, tid: int, tuser: int) -> int: + return ( + (PACKETIZER0_VERSION & 0xF) + | ((frame & 0xFFF) << 4) + | ((packet & 0xFFFFFF) << 16) + | ((tdest & 0xFF) << 40) + | ((tid & 0xFF) << 48) + | ((tuser & 0xFF) << 56) + ) + + +def packetizer0_tail_byte(*, eof: int, tuser: int) -> int: + return ((eof & 0x1) << 7) | (tuser & 0x7F) + + +def packetizer2_header_beat( + *, + sof: int, + tuser: int, + dest: int, + tid: int, + seq: int, + crc_mode: int = PACKETIZER2_CRC_NONE, +) -> AxisBeat: + return AxisBeat( + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=sof, + tuser=tuser, + tdest=dest, + tid=tid, + seq=seq, + ), + keep=0xFF, + last=0, + user=0x2, + ) + + +def packetizer2_data_beat(payload: bytes) -> AxisBeat: + return AxisBeat(data=word_from_bytes(payload), keep=0xFF, last=0, user=0) + + +def packetizer2_tail_beat(*, eof: int, tuser: int, byte_count: int, crc: int = 0) -> AxisBeat: + return AxisBeat( + data=packetizer2_tail_word(eof=eof, tuser=tuser, byte_count=byte_count, crc=crc), + keep=0xFF, + last=1, + user=0, + ) + + +def packetizer0_header_beat(*, frame: int, packet: int, tuser: int, dest: int, tid: int) -> AxisBeat: + return AxisBeat( + data=packetizer0_header_word( + frame=frame, + packet=packet, + tdest=dest, + tid=tid, + tuser=tuser, + ), + keep=0xFF, + last=0, + user=0x2, + ) + + +def packetizer_data_beat(payload: bytes, *, keep: int = 0xFF, last: int = 0) -> AxisBeat: + return AxisBeat(data=word_from_bytes(payload), keep=keep, last=last, user=0) + + +def assert_packetized_beat( + beat: AxisBeat, + *, + data: int, + keep: int = 0xFF, + last: int = 0, + user: int = 0, + dest: int = 0, + tid: int = 0, +) -> None: + assert beat.data == data + assert beat.keep == keep + assert beat.last == last + assert beat.dest == dest + assert beat.tid == tid + assert beat.user == user + + +def assert_packetizer2_tail_beat( + beat: AxisBeat, + *, + eof: int, + tuser: int, + byte_count: int, + crc_mode: int = PACKETIZER2_CRC_NONE, +) -> None: + expected = packetizer2_tail_word(eof=eof, tuser=tuser, byte_count=byte_count) + assert (beat.data & 0xFFFFFFFF) == (expected & 0xFFFFFFFF) + if crc_mode == PACKETIZER2_CRC_NONE: + assert (beat.data >> 32) == 0 + else: + assert (beat.data >> 32) != 0 + assert beat.keep == 0xFF + assert beat.last == 1 + assert beat.dest == 0 + assert beat.tid == 0 + assert beat.user == 0 + + +def assert_app_beat( + beat: AxisBeat, + *, + payload: bytes, + keep: int = 0xFF, + last: int = 0, + dest: int, + tid: int, + user: int = 0, +) -> None: + assert bytes_from_word(beat.data, keep=keep) == payload + assert beat.keep == keep + assert beat.last == last + assert beat.dest == dest + assert beat.tid == tid + assert beat.user == user + + +async def assert_no_output( + endpoint: FlatAxisEndpoint, + *, + clk, + cycles: int, + drive_ready: bool = False, +) -> None: + if drive_ready: + endpoint._sig("TREADY").value = 1 + for _ in range(cycles): + await RisingEdge(clk) + await Timer(1, unit="ns") + assert int(endpoint._sig("TVALID").value) == 0 + if drive_ready: + endpoint._sig("TREADY").value = 0 + + +def byte_packer_source_beat(payload: bytes, *, last: int = 0, user_values: list[int]) -> AxisBeat: + return AxisBeat( + data=word_from_bytes(payload), + keep=(1 << len(payload)) - 1, + last=last, + user=user_from_bytes(user_values), + ) + + +def byte_packer_source_beats_from_payload( + payload: bytes, + *, + max_beat_bytes: int, + user_base: int, + first_size: int | None = None, +) -> list[AxisBeat]: + beats = [] + offset = 0 + while offset < len(payload): + if offset == 0 and first_size is not None: + size = min(first_size, max_beat_bytes, len(payload)) + else: + size = min(max_beat_bytes, len(payload) - offset) + chunk = payload[offset : offset + size] + beats.append( + byte_packer_source_beat( + chunk, + last=int(offset + size == len(payload)), + user_values=list(range(user_base + offset, user_base + offset + len(chunk))), + ) + ) + offset += size + return beats + + +async def send_unpaced_beats(endpoint: FlatAxisEndpoint, beats: list[AxisBeat], *, clk) -> None: + # `AxiStreamBytePacker` has no slave-ready output. Each driven beat is held + # for exactly one rising edge, which is the module's intended acceptance + # cadence. + for beat in beats: + endpoint.drive(beat) + await RisingEdge(clk) + await Timer(1, unit="ns") + endpoint.set_idle() + + +async def recv_valid_pulses(endpoint: FlatAxisEndpoint, count: int, *, clk) -> list[AxisBeat]: + beats = [] + while len(beats) < count: + await RisingEdge(clk) + await Timer(1, unit="ns") + if int(endpoint._sig("TVALID").value): + beats.append(endpoint.snapshot()) + return beats + + +def assert_packed_beat( + beat: AxisBeat, + *, + payload: bytes, + user_values: list[int], + last: int = 0, +) -> None: + keep = (1 << len(payload)) - 1 + assert bytes_from_word(beat.data, keep=keep) == payload + assert beat.keep == keep + assert beat.last == last + assert beat.user == user_from_bytes(user_values) + + +async def send_beats(endpoint: FlatAxisEndpoint, beats: list[AxisBeat], *, clk) -> None: + for beat in beats: + await endpoint.send(beat, clk=clk) + + +async def recv_beats(endpoint: FlatAxisEndpoint, count: int, *, clk) -> 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 recv_beats_with_backpressure( + endpoint: FlatAxisEndpoint, + count: int, + *, + clk, + hold_cycles: int = 2, +) -> list[AxisBeat]: + beats = [] + endpoint._sig("TREADY").value = 0 + for _ in range(count): + 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) + return beats diff --git a/tests/protocols/packetizer/test_AxiStreamBytePacker.py b/tests/protocols/packetizer/test_AxiStreamBytePacker.py new file mode 100644 index 0000000000..b06465041a --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamBytePacker.py @@ -0,0 +1,277 @@ +############################################################################## +## 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 `AxiStreamBytePacker` wrapper with several +# compressed-keep input/output byte-width pairs. +# - Stimulus: Drive one input beat per clock because this RTL intentionally has +# no ready handshake, using partial keeps, exact-width terminal beats, and +# reset while a partial packed word is buffered. +# - Checks: The output valid pulses must contain compacted payload bytes in +# arrival order, matching per-byte `TUSER`, compressed `TKEEP`, and `TLAST` +# only on frame-terminating words. +# - Timing: Output is sampled on clocked valid pulses rather than through a +# sink-ready handshake, matching the module's no-backpressure contract. + +import os + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + assert_no_output, + assert_packed_beat, + byte_packer_source_beat, + byte_packer_source_beats_from_payload, + recv_valid_pulses, + reset_packetizer_dut, + send_unpaced_beats, + start_packetizer_clock, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + self.slave_bytes = int(os.getenv("SLAVE_BYTES_G", "4")) + self.master_bytes = int(os.getenv("MASTER_BYTES_G", "8")) + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.M_AXIS_TREADY.setimmediatevalue(1) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + + +@cocotb.test() +async def pack_partial_beats_test(dut): + tb = TB(dut) + await tb.reset() + + # The final input beat crosses an output-word boundary: one output word + # becomes full and the remaining bytes start the next terminal output word. + payload = bytes(range(0x10, 0x10 + tb.master_bytes + 3)) + input_beats = byte_packer_source_beats_from_payload( + payload, + max_beat_bytes=tb.slave_bytes, + user_base=0xA0, + first_size=max(1, min(tb.slave_bytes - 1, 3)), + ) + + rx_task = cocotb.start_soon(recv_valid_pulses(tb.sink, 2, clk=dut.axisClk)) + await send_unpaced_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + assert_packed_beat( + rx_beats[0], + payload=payload[: tb.master_bytes], + user_values=list(range(0xA0, 0xA0 + tb.master_bytes)), + ) + assert_packed_beat( + rx_beats[1], + payload=payload[tb.master_bytes :], + user_values=list(range(0xA0 + tb.master_bytes, 0xA0 + len(payload))), + last=1, + ) + + +@cocotb.test() +async def pack_exact_width_last_test(dut): + tb = TB(dut) + await tb.reset() + + # A frame exactly as wide as the output should produce one full terminal + # word regardless of how many narrower input beats it takes to fill it. + payload = bytes(range(0x30, 0x30 + tb.master_bytes)) + input_beats = byte_packer_source_beats_from_payload( + payload, + max_beat_bytes=tb.slave_bytes, + user_base=0x10, + ) + + rx_task = cocotb.start_soon(recv_valid_pulses(tb.sink, 1, clk=dut.axisClk)) + await send_unpaced_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + assert_packed_beat( + rx_beats[0], + payload=payload, + user_values=list(range(0x10, 0x10 + len(payload))), + last=1, + ) + + +@cocotb.test() +async def reset_flushes_partial_word_test(dut): + tb = TB(dut) + await tb.reset() + + # Start a partial packed word and prove it does not leak out before reset. + partial = bytes(range(0x40, 0x40 + min(3, tb.slave_bytes, tb.master_bytes - 1))) + await send_unpaced_beats( + tb.source, + [byte_packer_source_beat(partial, user_values=list(range(0x20, 0x20 + len(partial))))], + clk=dut.axisClk, + ) + await assert_no_output(tb.sink, clk=dut.axisClk, cycles=3) + + # Reset should discard the buffered partial word. The only subsequent output + # should be the new short frame driven after reset releases. + await tb.reset() + rx_task = cocotb.start_soon(recv_valid_pulses(tb.sink, 1, clk=dut.axisClk)) + new_frame = bytes(range(0x50, 0x50 + min(2, tb.slave_bytes))) + await send_unpaced_beats( + tb.source, + [ + byte_packer_source_beat( + new_frame, + last=1, + user_values=list(range(0x30, 0x30 + len(new_frame))), + ) + ], + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 2, "us") + + assert_packed_beat( + rx_beats[0], + payload=new_frame, + user_values=list(range(0x30, 0x30 + len(new_frame))), + last=1, + ) + + +@cocotb.test() +async def idle_gap_preserves_partial_word_test(dut): + tb = TB(dut) + await tb.reset() + + # The packer should keep a partial word across idle cycles and continue + # filling it when traffic resumes. + payload = bytes(range(0x60, 0x60 + tb.master_bytes)) + first_len = max(1, min(3, tb.slave_bytes, tb.master_bytes - 1)) + first = byte_packer_source_beat( + payload[:first_len], + user_values=list(range(0x40, 0x40 + first_len)), + ) + remaining = byte_packer_source_beats_from_payload( + payload[first_len:], + max_beat_bytes=tb.slave_bytes, + user_base=0x40 + first_len, + ) + + rx_task = cocotb.start_soon(recv_valid_pulses(tb.sink, 1, clk=dut.axisClk)) + await send_unpaced_beats(tb.source, [first], clk=dut.axisClk) + await assert_no_output(tb.sink, clk=dut.axisClk, cycles=4) + await send_unpaced_beats(tb.source, remaining, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + assert_packed_beat( + rx_beats[0], + payload=payload, + user_values=list(range(0x40, 0x40 + len(payload))), + last=1, + ) + + +@cocotb.test() +async def zero_keep_beat_is_ignored_test(dut): + tb = TB(dut) + await tb.reset() + + # A valid beat with no asserted keep bits should not contribute data or + # sideband bytes to the packed output word. The following short terminal + # frame should emerge exactly as if the zero-keep beat had been idle. + payload = bytes(range(0x70, 0x70 + min(3, tb.slave_bytes))) + rx_task = cocotb.start_soon(recv_valid_pulses(tb.sink, 1, clk=dut.axisClk)) + await send_unpaced_beats( + tb.source, + [ + AxisBeat( + data=(1 << (8 * tb.slave_bytes)) - 1, + keep=0x00, + last=0, + user=(1 << (8 * tb.slave_bytes)) - 1, + ), + byte_packer_source_beat( + payload, + last=1, + user_values=list(range(0x60, 0x60 + len(payload))), + ), + ], + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 2, "us") + + assert_packed_beat( + rx_beats[0], + payload=payload, + user_values=list(range(0x60, 0x60 + len(payload))), + last=1, + ) + + +@cocotb.test() +async def output_ready_is_ignored_test(dut): + tb = TB(dut) + await tb.reset() + + # The RTL explicitly has no ready handshaking. Holding output ready low must + # not suppress the valid pulse for a complete packed output word. + dut.M_AXIS_TREADY.value = 0 + payload = bytes(range(0x80, 0x80 + tb.master_bytes)) + input_beats = byte_packer_source_beats_from_payload( + payload, + max_beat_bytes=tb.slave_bytes, + user_base=0x50, + ) + + rx_task = cocotb.start_soon(recv_valid_pulses(tb.sink, 1, clk=dut.axisClk)) + await send_unpaced_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + assert_packed_beat( + rx_beats[0], + payload=payload, + user_values=list(range(0x50, 0x50 + len(payload))), + last=1, + ) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param({"SLAVE_BYTES_G": 1, "MASTER_BYTES_G": 8}, id="comp_keep_1_to_8"), + pytest.param({"SLAVE_BYTES_G": 2, "MASTER_BYTES_G": 5}, id="comp_keep_2_to_5"), + pytest.param({"SLAVE_BYTES_G": 3, "MASTER_BYTES_G": 6}, id="comp_keep_3_to_6"), + pytest.param({"SLAVE_BYTES_G": 3, "MASTER_BYTES_G": 7}, id="comp_keep_3_to_7"), + pytest.param({"SLAVE_BYTES_G": 4, "MASTER_BYTES_G": 8}, id="comp_keep_4_to_8"), + pytest.param({"SLAVE_BYTES_G": 5, "MASTER_BYTES_G": 7}, id="comp_keep_5_to_7"), + pytest.param({"SLAVE_BYTES_G": 7, "MASTER_BYTES_G": 8}, id="comp_keep_7_to_8"), + ], +) +def test_AxiStreamBytePacker(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreambytepackerwrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamBytePackerWrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamDepacketizer.py b/tests/protocols/packetizer/test_AxiStreamDepacketizer.py new file mode 100644 index 0000000000..88b75e82fa --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamDepacketizer.py @@ -0,0 +1,233 @@ +############################################################################## +## 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 standalone legacy `AxiStreamDepacketizer` wrapper with an +# 8-byte packetized input stream. +# - Stimulus: Present hand-built V0 header and payload beats directly, +# including both tail encodings produced by the legacy packetizer. +# - Checks: The depacketized application stream must restore payload bytes, +# `TDEST`, `TID`, SOF on first-beat `TUSER`, final-byte `TUSER`, `TKEEP`, +# and `TLAST` for appended-tail and separate-tail packets. +# - Timing: The application sink is kept ready while source and sink tasks run +# concurrently, with no packetizer loopback used as the stimulus generator. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + assert_app_beat, + assert_no_output, + packetizer0_header_beat, + packetizer0_tail_byte, + packetizer_data_beat, + recv_beats, + reset_packetizer_dut, + send_beats, + start_packetizer_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_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.restart.setimmediatevalue(0) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + + +@cocotb.test() +async def depacketize_appended_tail_test(dut): + tb = TB(dut) + await tb.reset() + + # Build the packetizer-V0 stream directly: header, one full payload beat, + # and a final beat whose last byte is the EOF/user tail marker. + first = bytes(range(0x10, 0x18)) + last = bytes(range(0x18, 0x1F)) + tail = packetizer0_tail_byte(eof=1, tuser=0x41) + packet = [ + packetizer0_header_beat(frame=0, packet=0, tuser=0x20, dest=0x3, tid=0xA5), + packetizer_data_beat(first), + packetizer_data_beat(last + bytes([tail]), last=1), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 2, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + # The depacketizer restores header sideband onto the first application beat + # and sets SSI SOF in the first byte's `TUSER`. + assert_app_beat(rx_beats[0], payload=first, dest=0x3, tid=0xA5, user=0x22) + # For an appended tail, the final byte lane is stripped via `TKEEP`, and the + # tail user bits move onto the last real payload byte. + assert_app_beat( + rx_beats[1], + payload=last, + keep=0x7F, + last=1, + dest=0x3, + tid=0xA5, + user=0x41 << 48, + ) + + +@cocotb.test() +async def depacketize_separate_tail_test(dut): + tb = TB(dut) + await tb.reset() + + # This packet uses the other legal tail placement: a full final payload word + # followed by a separate one-byte EOF/user marker. + first = bytes(range(0x30, 0x38)) + last = bytes(range(0x38, 0x40)) + tail = packetizer0_tail_byte(eof=1, tuser=0x42) + packet = [ + packetizer0_header_beat(frame=0, packet=0, tuser=0x10, dest=0x2, tid=0x5A), + packetizer_data_beat(first), + packetizer_data_beat(last), + AxisBeat(data=tail, keep=0x01, last=1, user=0), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 2, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + # The first output beat proves sideband restoration, while the second proves + # that the one-byte marker was consumed without becoming payload data. + assert_app_beat(rx_beats[0], payload=first, dest=0x2, tid=0x5A, user=0x12) + assert_app_beat( + rx_beats[1], + payload=last, + last=1, + dest=0x2, + tid=0x5A, + user=0x42 << 56, + ) + + +@cocotb.test() +async def depacketize_split_sequence_test(dut): + tb = TB(dut) + await tb.reset() + + # Hand-build a valid two-packet V0 frame. The first packet terminates with + # EOF=0, so the depacketizer must retain frame state and accept packet 1 as + # a continuation without adding another SOF. + chunks = [ + bytes(range(0x60, 0x68)), + bytes(range(0x68, 0x70)), + bytes(range(0x70, 0x78)), + ] + packet = [ + packetizer0_header_beat(frame=0, packet=0, tuser=0x30, dest=0x4, tid=0x22), + packetizer_data_beat(chunks[0]), + packetizer_data_beat(chunks[1]), + AxisBeat(data=packetizer0_tail_byte(eof=0, tuser=0), keep=0x01, last=1, user=0), + packetizer0_header_beat(frame=0, packet=1, tuser=0x00, dest=0x4, tid=0x22), + packetizer_data_beat(chunks[2]), + AxisBeat(data=packetizer0_tail_byte(eof=1, tuser=0x43), keep=0x01, last=1, user=0), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 3, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert_app_beat(rx_beats[0], payload=chunks[0], dest=0x4, tid=0x22, user=0x32) + assert_app_beat(rx_beats[1], payload=chunks[1], dest=0x4, tid=0x22) + assert_app_beat( + rx_beats[2], + payload=chunks[2], + last=1, + dest=0x4, + tid=0x22, + user=0x43 << 56, + ) + + +@cocotb.test() +async def depacketize_bad_continuation_bleeds_and_recovers_test(dut): + tb = TB(dut) + await tb.reset() + + # Start a multi-packet frame, then skip packet number 1. The depacketizer + # should bleed the malformed packet instead of merging out-of-order payload + # into the frame, then return to HEADER state for the next valid frame. + chunks = [ + bytes(range(0x90, 0x98)), + bytes(range(0x98, 0xA0)), + bytes(range(0xA0, 0xA8)), + ] + first_packet = [ + packetizer0_header_beat(frame=0, packet=0, tuser=0x34, dest=0x6, tid=0x44), + packetizer_data_beat(chunks[0]), + packetizer_data_beat(chunks[1]), + AxisBeat(data=packetizer0_tail_byte(eof=0, tuser=0), keep=0x01, last=1, user=0), + ] + bad_packet = [ + packetizer0_header_beat(frame=0, packet=2, tuser=0x00, dest=0x6, tid=0x44), + packetizer_data_beat(chunks[2]), + AxisBeat(data=packetizer0_tail_byte(eof=1, tuser=0x47), keep=0x01, last=1, user=0), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 2, clk=dut.axisClk)) + await send_beats(tb.source, first_packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert_app_beat(rx_beats[0], payload=chunks[0], dest=0x6, tid=0x44, user=0x36) + assert_app_beat(rx_beats[1], payload=chunks[1], dest=0x6, tid=0x44) + + await send_beats(tb.source, bad_packet, clk=dut.axisClk) + await assert_no_output(tb.sink, clk=dut.axisClk, cycles=8, drive_ready=True) + + recovery = bytes(range(0xB0, 0xB8)) + recovery_packet = [ + packetizer0_header_beat(frame=1, packet=0, tuser=0x35, dest=0x7, tid=0x45), + packetizer_data_beat(recovery), + AxisBeat(data=packetizer0_tail_byte(eof=1, tuser=0x48), keep=0x01, last=1, user=0), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 1, clk=dut.axisClk)) + await send_beats(tb.source, recovery_packet, clk=dut.axisClk) + recovery_beat = (await with_timeout(rx_task, 4, "us"))[0] + + assert_app_beat( + recovery_beat, + payload=recovery, + last=1, + dest=0x7, + tid=0x45, + user=0x37 | (0x48 << 56), + ) + + +@pytest.mark.parametrize("parameters", [pytest.param({}, id="legacy_v0")]) +def test_AxiStreamDepacketizer(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreamdepacketizerwrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamDepacketizerWrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamDepacketizer2.py b/tests/protocols/packetizer/test_AxiStreamDepacketizer2.py new file mode 100644 index 0000000000..f8d8ab5ccb --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamDepacketizer2.py @@ -0,0 +1,285 @@ +############################################################################## +## 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 standalone `AxiStreamDepacketizer2` wrapper with CRC +# disabled and a small `TDEST_BITS_G=2` address space so startup RAM +# initialization stays short under GHDL. +# - Stimulus: Present packetizer-V2 header, payload, and tail beats directly, +# including a two-packet continuation sequence with incrementing packet +# sequence numbers. +# - Checks: The depacketized application stream must restore payload bytes, +# `TDEST`, `TID`, first-beat SOF, final-beat `TUSER`, `TKEEP`, and `TLAST` +# without relying on `AxiStreamPacketizer2` as the stimulus generator. +# - Timing: The test waits for the depacketizer `initDone` debug bit before +# traffic, then keeps the application sink ready while source and sink tasks +# run concurrently. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + PACKETIZER2_CRC_DATA, + PACKETIZER2_CRC_NONE, + DEBUG_INIT_DONE, + assert_app_beat, + cycle, + packetizer2_data_beat, + packetizer2_header_beat, + packetizer2_header_word, + packetizer2_tail_beat, + recv_beats, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, + wait_debug_init_done, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.linkGood.setimmediatevalue(1) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + await self.wait_init_done() + + async def wait_init_done(self, timeout_cycles: int = 64): + await wait_debug_init_done(self.dut, timeout_cycles=timeout_cycles) + + +def assert_error_beat(beat: AxisBeat, *, dest: int, tid: int, header_user: int) -> None: + assert beat.last == 1 + assert beat.dest == dest + assert beat.tid == tid + assert (beat.user & 0xFF) == (header_user | 0x3) + assert (beat.user >> 56) & 0x1 == 0x1 + + +@cocotb.test() +async def depacketize_single_packet_test(dut): + tb = TB(dut) + await tb.reset() + + first = bytes(range(0x10, 0x18)) + last = bytes(range(0x18, 0x20)) + packet = [ + packetizer2_header_beat(sof=1, tuser=0x20, dest=0x3, tid=0xA5, seq=0), + packetizer2_data_beat(first), + packetizer2_data_beat(last), + packetizer2_tail_beat(eof=1, tuser=0x41, byte_count=8), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 2, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + assert_app_beat(rx_beats[0], payload=first, dest=0x3, tid=0xA5, user=0x22) + assert_app_beat(rx_beats[1], payload=last, last=1, dest=0x3, tid=0xA5, user=0x41 << 56) + + +@cocotb.test() +async def depacketize_split_sequence_test(dut): + tb = TB(dut) + await tb.reset() + + chunks = [ + bytes(range(0x30, 0x38)), + bytes(range(0x38, 0x40)), + bytes(range(0x40, 0x48)), + ] + packets = [ + packetizer2_header_beat(sof=1, tuser=0x10, dest=0x2, tid=0x5A, seq=0), + packetizer2_data_beat(chunks[0]), + packetizer2_data_beat(chunks[1]), + packetizer2_tail_beat(eof=0, tuser=0, byte_count=8), + packetizer2_header_beat(sof=0, tuser=0x00, dest=0x2, tid=0x5A, seq=1), + packetizer2_data_beat(chunks[2]), + packetizer2_tail_beat(eof=1, tuser=0x43, byte_count=8), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 3, clk=dut.axisClk)) + await send_beats(tb.source, packets, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert_app_beat(rx_beats[0], payload=chunks[0], dest=0x2, tid=0x5A, user=0x12) + assert_app_beat(rx_beats[1], payload=chunks[1], dest=0x2, tid=0x5A) + assert_app_beat(rx_beats[2], payload=chunks[2], last=1, dest=0x2, tid=0x5A, user=0x43 << 56) + + +@cocotb.test() +async def depacketize_partial_last_tkeep_test(dut): + tb = TB(dut) + await tb.reset() + + first = bytes(range(0x50, 0x58)) + last = bytes(range(0x58, 0x5B)) + packet = [ + packetizer2_header_beat(sof=1, tuser=0x24, dest=0x1, tid=0xC3, seq=0), + packetizer2_data_beat(first), + packetizer2_data_beat(last), + packetizer2_tail_beat(eof=1, tuser=0x47, byte_count=3), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 2, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + assert_app_beat(rx_beats[0], payload=first, dest=0x1, tid=0xC3, user=0x26) + assert_app_beat( + rx_beats[1], + payload=last, + keep=0x07, + last=1, + dest=0x1, + tid=0xC3, + user=0x47 << 16, + ) + + +@cocotb.test() +async def depacketize_crc_none_nonzero_crc_marks_eofe_test(dut): + tb = TB(dut) + await tb.reset() + + payload = bytes(range(0x70, 0x78)) + packet = [ + packetizer2_header_beat(sof=1, tuser=0x30, dest=0x2, tid=0x55, seq=0), + packetizer2_data_beat(payload), + packetizer2_tail_beat(eof=1, tuser=0x40, byte_count=8, crc=0x1), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 1, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + # CRC mode NONE requires the CRC field to be zero. A nonzero field marks + # the frame as terminal-error while still forwarding the held payload beat. + assert_app_beat( + rx_beats[0], + payload=payload, + last=1, + dest=0x2, + tid=0x55, + user=0x32 | (0x41 << 56), + ) + + +@cocotb.test() +async def depacketize_bad_version_header_marks_eofe_test(dut): + tb = TB(dut) + await tb.reset() + + bad_header = packetizer2_header_word( + crc_mode=PACKETIZER2_CRC_NONE, + sof=1, + tuser=0x29, + tdest=0x1, + tid=0x66, + seq=0, + ) + packet = [AxisBeat(data=(bad_header & ~0xF) | 0x7, keep=0xFF, last=0, user=0x2)] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 1, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + assert_error_beat(rx_beats[0], dest=0x1, tid=0x66, header_user=0x29) + + +@cocotb.test() +async def depacketize_bad_crc_mode_header_marks_eofe_test(dut): + tb = TB(dut) + await tb.reset() + + packet = [ + packetizer2_header_beat( + sof=1, + tuser=0x2B, + dest=0x2, + tid=0x77, + seq=0, + crc_mode=PACKETIZER2_CRC_DATA, + ) + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 1, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + assert_error_beat(rx_beats[0], dest=0x2, tid=0x77, header_user=0x2B) + + +@cocotb.test() +async def depacketize_link_drop_recovers_test(dut): + tb = TB(dut) + await tb.reset() + + dut.linkGood.value = 0 + await cycle(dut.axisClk, 4) + assert (int(dut.debugOut.value) & (1 << DEBUG_INIT_DONE)) == 0 + + dut.linkGood.value = 1 + await tb.wait_init_done() + + payload = bytes(range(0x90, 0x98)) + packet = [ + packetizer2_header_beat(sof=1, tuser=0x34, dest=0x3, tid=0x88, seq=0), + packetizer2_data_beat(payload), + packetizer2_tail_beat(eof=1, tuser=0x48, byte_count=8), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 1, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + assert_app_beat( + rx_beats[0], + payload=payload, + last=1, + dest=0x3, + tid=0x88, + user=0x36 | (0x48 << 56), + ) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param( + { + "TDEST_BITS_G": 2, + }, + id="crc_none_tdest2", + ) + ], +) +def test_AxiStreamDepacketizer2(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreamdepacketizer2wrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamDepacketizer2Crc.py b/tests/protocols/packetizer/test_AxiStreamDepacketizer2Crc.py new file mode 100644 index 0000000000..493fc9a028 --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamDepacketizer2Crc.py @@ -0,0 +1,112 @@ +############################################################################## +## 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 standalone `AxiStreamDepacketizer2` wrapper with CRC +# checking enabled in DATA and FULL modes. +# - Stimulus: Drive one hand-built packet whose header advertises the active +# CRC mode but whose tail CRC field is deliberately wrong. +# - Checks: The depacketizer must forward the held payload beat, terminate the +# application frame, and mark the last byte-lane `TUSER` with SSI `EOFE`. +# - Timing: The test waits for depacketizer initialization, then uses ordinary +# AXI Stream source and sink handshakes around a short packet. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + FlatAxisEndpoint, + assert_app_beat, + crc_mode_from_env, + packetizer2_data_beat, + packetizer2_header_beat, + packetizer2_tail_beat, + recv_beats, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, + wait_debug_init_done, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.linkGood.setimmediatevalue(1) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + await self.wait_init_done() + + async def wait_init_done(self, timeout_cycles: int = 64): + await wait_debug_init_done(self.dut, timeout_cycles=timeout_cycles) + + +@cocotb.test() +async def depacketize_bad_crc_marks_eofe_test(dut): + tb = TB(dut) + await tb.reset() + + # The CRC field is intentionally not a computed CRC. In DATA and FULL modes + # that must be treated as a terminal frame error on the held payload word. + payload = bytes(range(0x20, 0x28)) + packet = [ + packetizer2_header_beat( + crc_mode=crc_mode_from_env("DATA"), + sof=1, + tuser=0x30, + dest=0x2, + tid=0x5C, + seq=0, + ), + packetizer2_data_beat(payload), + packetizer2_tail_beat(eof=1, tuser=0x42, byte_count=8, crc=0x0), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 1, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert_app_beat( + rx_beats[0], + payload=payload, + last=1, + dest=0x2, + tid=0x5C, + user=0x32 | (0x43 << 56), + ) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param({"TDEST_BITS_G": 2, "CRC_MODE_G": "DATA"}, id="crc_data_bad_crc"), + pytest.param({"TDEST_BITS_G": 2, "CRC_MODE_G": "FULL"}, id="crc_full_bad_crc"), + ], +) +def test_AxiStreamDepacketizer2Crc(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreamdepacketizer2wrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamDepacketizer2LinkDrop.py b/tests/protocols/packetizer/test_AxiStreamDepacketizer2LinkDrop.py new file mode 100644 index 0000000000..b656f79eb5 --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamDepacketizer2LinkDrop.py @@ -0,0 +1,125 @@ +############################################################################## +## 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 standalone `AxiStreamDepacketizer2` wrapper in CRC NONE +# mode with a small two-bit destination state table. +# - Stimulus: Start a packet without sending its tail, then drop `linkGood` +# while the depacketizer has active frame state. +# - Checks: The depacketizer must terminate the incomplete frame with SSI +# `EOFE` during link cleanup and accept a fresh frame after link recovery. +# - Timing: The test runs alone in this file so the link-cleanup sweep observes +# only the intentionally opened destination state. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + FlatAxisEndpoint, + assert_app_beat, + bytes_from_word, + packetizer2_data_beat, + packetizer2_header_beat, + packetizer2_tail_beat, + recv_beats, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, + wait_debug_init_done, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.linkGood.setimmediatevalue(1) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + await self.wait_init_done() + + async def wait_init_done(self, timeout_cycles: int = 64): + await wait_debug_init_done(self.dut, timeout_cycles=timeout_cycles) + + +@cocotb.test() +async def depacketize_mid_frame_link_drop_terminates_and_recovers_test(dut): + tb = TB(dut) + await tb.reset() + + payload = bytes(range(0xA0, 0xA8)) + open_packet = [ + packetizer2_header_beat(sof=1, tuser=0x36, dest=0x1, tid=0x91, seq=0), + packetizer2_data_beat(payload), + ] + + await send_beats(tb.source, open_packet, clk=dut.axisClk) + rx_task = cocotb.start_soon(recv_beats(tb.sink, 2, clk=dut.axisClk)) + dut.linkGood.value = 0 + rx_beats = await with_timeout(rx_task, 4, "us") + + assert bytes_from_word(rx_beats[0].data) == payload + assert rx_beats[0].last == 0 + assert rx_beats[0].dest == 0x1 + assert rx_beats[0].tid == 0x91 + assert rx_beats[0].user == 0x36 + assert rx_beats[1].last == 1 + assert bytes_from_word(rx_beats[1].data) == payload + assert ((rx_beats[1].user >> 56) & 0x1) == 0x1 + + dut.linkGood.value = 1 + await tb.wait_init_done() + + recovery = bytes(range(0xB0, 0xB8)) + recovery_packet = [ + packetizer2_header_beat(sof=1, tuser=0x38, dest=0x1, tid=0x92, seq=0), + packetizer2_data_beat(recovery), + packetizer2_tail_beat(eof=1, tuser=0x4A, byte_count=8), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 1, clk=dut.axisClk)) + await send_beats(tb.source, recovery_packet, clk=dut.axisClk) + recovery_beats = await with_timeout(rx_task, 3, "us") + + assert_app_beat( + recovery_beats[0], + payload=recovery, + last=1, + dest=0x1, + tid=0x92, + user=0x3A | (0x4A << 56), + ) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param({"TDEST_BITS_G": 2}, id="crc_none_tdest2"), + ], +) +def test_AxiStreamDepacketizer2LinkDrop(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreamdepacketizer2wrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamPacketizer.py b/tests/protocols/packetizer/test_AxiStreamPacketizer.py new file mode 100644 index 0000000000..0c2d05ab19 --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamPacketizer.py @@ -0,0 +1,266 @@ +############################################################################## +## 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 standalone legacy `AxiStreamPacketizer` wrapper with an +# 8-byte stream width and the default SSI packetized output mode. +# - Stimulus: Drive application AXI Stream beats directly into the packetizer, +# including `TDEST`, `TID`, first-beat `TUSER`, and final-byte `TUSER`. +# - Checks: The packetized output must contain the expected V0 header, payload, +# SSI SOF bit, and both supported EOF tail placements: appended into a +# partially-filled final word and emitted as a separate one-byte final word. +# - Timing: The sink is kept ready while a concurrent source task sends input +# beats, so the bench observes every accepted packetized beat without using +# a depacketizer loopback as an oracle. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + assert_packetized_beat, + packetizer0_header_word, + packetizer0_tail_byte, + recv_beats, + recv_beats_with_backpressure, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, + tuser_for_lane, + word_from_bytes, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.maxPktBytes.setimmediatevalue(64) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + +@cocotb.test() +async def packetize_appended_tail_test(dut): + tb = TB(dut) + await tb.reset() + + # A 15-byte frame leaves one spare byte in the final packet word, so the + # legacy packetizer should append its EOF/user tail marker into that word. + payload = bytes(range(0x10, 0x1F)) + tail = packetizer0_tail_byte(eof=1, tuser=0x41) + input_beats = [ + AxisBeat( + data=word_from_bytes(payload[0:8]), + keep=0xFF, + last=0, + dest=0x3, + tid=0xA5, + user=0x20, + ), + AxisBeat( + data=word_from_bytes(payload[8:15]), + keep=0x7F, + last=1, + dest=0x3, + tid=0xA5, + user=tuser_for_lane(7, 0x41), + ), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 3, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + # The first packet word is protocol overhead: version/frame/packet plus the + # application sideband fields copied out of the first input beat. + assert_packetized_beat( + rx_beats[0], + data=packetizer0_header_word(frame=0, packet=0, tdest=0x3, tid=0xA5, tuser=0x20), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + # The final output beat carries seven payload bytes plus the tail marker in + # byte lane 7; no depacketizer is involved in forming this expectation. + assert_packetized_beat( + rx_beats[2], + data=word_from_bytes(payload[8:15] + bytes([tail])), + last=1, + ) + + +@cocotb.test() +async def packetize_separate_tail_test(dut): + tb = TB(dut) + await tb.reset() + + # A 16-byte frame fills the last payload word completely, so the legacy + # packetizer must emit the EOF/user tail marker as its own one-byte word. + payload = bytes(range(0x30, 0x40)) + tail = packetizer0_tail_byte(eof=1, tuser=0x42) + input_beats = [ + AxisBeat( + data=word_from_bytes(payload[0:8]), + keep=0xFF, + last=0, + dest=0x2, + tid=0x5A, + user=0x10, + ), + AxisBeat( + data=word_from_bytes(payload[8:16]), + keep=0xFF, + last=1, + dest=0x2, + tid=0x5A, + user=0x42, + ), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 4, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + # The packetized sideband is only present in the header; following payload + # beats should have neutralized `TDEST`, `TID`, and `TUSER`. + assert_packetized_beat( + rx_beats[0], + data=packetizer0_header_word(frame=0, packet=0, tdest=0x2, tid=0x5A, tuser=0x10), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:16])) + # The separate tail word uses only byte lane 0 and terminates the packet. + assert_packetized_beat(rx_beats[3], data=tail, keep=0x01, last=1) + + +@cocotb.test() +async def packetize_split_frame_on_max_size_test(dut): + tb = TB(dut) + dut.maxPktBytes.value = 32 + await tb.reset() + + # With a 32-byte packet limit, the legacy packetizer can carry two 8-byte + # payload words plus header/tail overhead. A three-word frame must therefore + # become a non-EOF packet followed by an EOF continuation packet. + payload = bytes(range(0x60, 0x78)) + first_tail = packetizer0_tail_byte(eof=0, tuser=0x00) + final_tail = packetizer0_tail_byte(eof=1, tuser=0x43) + input_beats = [ + AxisBeat( + data=word_from_bytes(payload[0:8]), + keep=0xFF, + last=0, + dest=0x4, + tid=0x22, + user=0x31, + ), + AxisBeat( + data=word_from_bytes(payload[8:16]), + keep=0xFF, + last=0, + dest=0x4, + tid=0x22, + user=0, + ), + AxisBeat( + data=word_from_bytes(payload[16:24]), + keep=0xFF, + last=1, + dest=0x4, + tid=0x22, + user=0x43, + ), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 7, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + assert_packetized_beat( + rx_beats[0], + data=packetizer0_header_word(frame=0, packet=0, tdest=0x4, tid=0x22, tuser=0x31), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:16])) + assert_packetized_beat(rx_beats[3], data=first_tail, keep=0x01, last=1) + assert_packetized_beat( + rx_beats[4], + data=packetizer0_header_word(frame=0, packet=1, tdest=0x4, tid=0x22, tuser=0x43), + user=0x2, + ) + assert_packetized_beat(rx_beats[5], data=word_from_bytes(payload[16:24])) + assert_packetized_beat(rx_beats[6], data=final_tail, keep=0x01, last=1) + + +@cocotb.test() +async def packetize_output_backpressure_test(dut): + tb = TB(dut) + await tb.reset() + + # Stall every packetized output beat before accepting it. This stresses the + # legacy packetizer's output hold behavior across header, payload, and the + # separate one-byte tail marker. + payload = bytes(range(0x80, 0x90)) + tail = packetizer0_tail_byte(eof=1, tuser=0x45) + input_beats = [ + AxisBeat( + data=word_from_bytes(payload[0:8]), + keep=0xFF, + last=0, + dest=0x5, + tid=0x35, + user=0x25, + ), + AxisBeat( + data=word_from_bytes(payload[8:16]), + keep=0xFF, + last=1, + dest=0x5, + tid=0x35, + user=0x45, + ), + ] + + rx_task = cocotb.start_soon(recv_beats_with_backpressure(tb.sink, 4, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert_packetized_beat( + rx_beats[0], + data=packetizer0_header_word(frame=0, packet=0, tdest=0x5, tid=0x35, tuser=0x25), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:16])) + assert_packetized_beat(rx_beats[3], data=tail, keep=0x01, last=1) + + +@pytest.mark.parametrize("parameters", [pytest.param({}, id="legacy_v0")]) +def test_AxiStreamPacketizer(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreampacketizerwrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamPacketizerWrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamPacketizer2.py b/tests/protocols/packetizer/test_AxiStreamPacketizer2.py new file mode 100644 index 0000000000..70b8d15bf8 --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamPacketizer2.py @@ -0,0 +1,404 @@ +############################################################################## +## 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 standalone `AxiStreamPacketizer2` wrapper in CRC-disabled +# mode with an 8-byte stream width and a reduced packet-size limit for the +# split-frame case. +# - Stimulus: Drive application AXI Stream beats directly into the packetizer, +# including first/last `TUSER`, `TDEST`, `TID`, and a three-beat frame that +# must be divided into two packetizer packets. +# - Checks: The packetized output must contain the expected V2 header words, +# payload words, tail words, sequence numbers, SOF/EOF flags, and output +# sideband remapping. +# - Timing: The sink is kept ready while a concurrent source task sends input +# beats, so the bench observes every accepted packetized beat without using +# a depacketizer loopback as an oracle. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + assert_packetized_beat, + assert_packetizer2_tail_beat, + crc_mode_from_env, + packetizer2_header_word, + payload_to_beats, + recv_beats, + recv_beats_with_backpressure, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, + word_from_bytes, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.maxPktBytes.setimmediatevalue(32) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + + +@cocotb.test() +async def packetize_single_frame_test(dut): + tb = TB(dut) + await tb.reset() + + payload = bytes(range(0x10, 0x20)) + input_beats = payload_to_beats( + payload, + dest=0x3, + tid=0xA5, + first_user=0x22, + last_user=0x41, + ) + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 4, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + crc_mode = crc_mode_from_env() + + assert_packetized_beat( + rx_beats[0], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=1, + tuser=0x22, + tdest=0x3, + tid=0xA5, + seq=0, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:16])) + assert_packetizer2_tail_beat(rx_beats[3], eof=1, tuser=0x41, byte_count=8, crc_mode=crc_mode) + + +@cocotb.test() +async def packetize_split_frame_test(dut): + tb = TB(dut) + await tb.reset() + + payload = bytes(range(0x30, 0x48)) + input_beats = payload_to_beats( + payload, + dest=0x2, + tid=0x5A, + first_user=0x12, + last_user=0x43, + ) + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 7, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + crc_mode = crc_mode_from_env() + + assert_packetized_beat( + rx_beats[0], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=1, + tuser=0x12, + tdest=0x2, + tid=0x5A, + seq=0, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:16])) + assert_packetizer2_tail_beat(rx_beats[3], eof=0, tuser=0, byte_count=8, crc_mode=crc_mode) + assert_packetized_beat( + rx_beats[4], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=0, + tuser=0, + tdest=0x2, + tid=0x5A, + seq=1, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[5], data=word_from_bytes(payload[16:24])) + assert_packetizer2_tail_beat(rx_beats[6], eof=1, tuser=0x43, byte_count=8, crc_mode=crc_mode) + + +@cocotb.test() +async def packetize_partial_last_tkeep_test(dut): + tb = TB(dut) + await tb.reset() + + # A partial final input beat should still produce a full-width packetized + # tail word whose byte-count field tells the depacketizer how much of the + # previous payload word is real frame data. + payload = bytes(range(0x50, 0x5B)) + input_beats = payload_to_beats( + payload, + dest=0x1, + tid=0xC3, + first_user=0x24, + last_user=0x47, + ) + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 4, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + crc_mode = crc_mode_from_env() + + assert_packetized_beat( + rx_beats[0], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=1, + tuser=0x24, + tdest=0x1, + tid=0xC3, + seq=0, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:11])) + assert_packetizer2_tail_beat(rx_beats[3], eof=1, tuser=0x47, byte_count=3, crc_mode=crc_mode) + + +@cocotb.test() +async def packetize_one_byte_over_max_packet_boundary_test(dut): + tb = TB(dut) + await tb.reset() + + # With `maxPktBytes=32`, two 8-byte payload words plus header/tail exactly + # fill one packet. A 17-byte frame must therefore split after the first 16 + # payload bytes and carry the final byte in a continuation packet. + payload = bytes(range(0x58, 0x69)) + input_beats = payload_to_beats( + payload, + dest=0x2, + tid=0xB4, + first_user=0x26, + last_user=0x49, + ) + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 7, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + crc_mode = crc_mode_from_env() + + assert_packetized_beat( + rx_beats[0], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=1, + tuser=0x26, + tdest=0x2, + tid=0xB4, + seq=0, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:16])) + assert_packetizer2_tail_beat(rx_beats[3], eof=0, tuser=0, byte_count=8, crc_mode=crc_mode) + assert_packetized_beat( + rx_beats[4], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=0, + tuser=0x49, + tdest=0x2, + tid=0xB4, + seq=1, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[5], data=word_from_bytes(payload[16:17])) + assert_packetizer2_tail_beat(rx_beats[6], eof=1, tuser=0x49, byte_count=1, crc_mode=crc_mode) + + +@cocotb.test() +async def packetize_interleaved_tdest_state_test(dut): + tb = TB(dut) + await tb.reset() + + # Changing TDEST mid-frame forces a non-EOF tail for the active destination + # without accepting the new beat. The source helper holds the new beat until + # the packetizer rearbitrates and returns ready. + dest_a_first = bytes(range(0x70, 0x78)) + dest_b_frame = bytes(range(0x90, 0x98)) + dest_a_last = bytes(range(0x78, 0x80)) + input_beats = [ + AxisBeat( + data=word_from_bytes(dest_a_first), + keep=0xFF, + last=0, + dest=0x1, + tid=0x11, + user=0x21, + ), + AxisBeat( + data=word_from_bytes(dest_b_frame), + keep=0xFF, + last=1, + dest=0x2, + tid=0x22, + user=0x31 | (0x44 << 56), + ), + AxisBeat( + data=word_from_bytes(dest_a_last), + keep=0xFF, + last=1, + dest=0x1, + tid=0x11, + user=0x45 << 56, + ), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 9, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 4, "us") + + crc_mode = crc_mode_from_env() + + assert_packetized_beat( + rx_beats[0], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=1, + tuser=0x21, + tdest=0x1, + tid=0x11, + seq=0, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(dest_a_first)) + assert_packetizer2_tail_beat(rx_beats[2], eof=0, tuser=0, byte_count=8, crc_mode=crc_mode) + assert_packetized_beat( + rx_beats[3], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=1, + tuser=0x31, + tdest=0x2, + tid=0x22, + seq=0, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[4], data=word_from_bytes(dest_b_frame)) + assert_packetizer2_tail_beat(rx_beats[5], eof=1, tuser=0x44, byte_count=8, crc_mode=crc_mode) + assert_packetized_beat( + rx_beats[6], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=0, + tuser=0x00, + tdest=0x1, + tid=0x11, + seq=1, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[7], data=word_from_bytes(dest_a_last)) + assert_packetizer2_tail_beat(rx_beats[8], eof=1, tuser=0x45, byte_count=8, crc_mode=crc_mode) + + +@cocotb.test() +async def packetize_output_backpressure_test(dut): + tb = TB(dut) + await tb.reset() + + # Hold each packetized beat at the sink for multiple clocks before + # accepting it. The helper checks that VALID-side data, keep, last, and + # sideband remain stable while TREADY is low. + payload = bytes(range(0xA0, 0xB8)) + input_beats = payload_to_beats( + payload, + dest=0x3, + tid=0x33, + first_user=0x2A, + last_user=0x4A, + ) + + rx_task = cocotb.start_soon(recv_beats_with_backpressure(tb.sink, 7, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 5, "us") + + crc_mode = crc_mode_from_env() + + assert_packetized_beat( + rx_beats[0], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=1, + tuser=0x2A, + tdest=0x3, + tid=0x33, + seq=0, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:16])) + assert_packetizer2_tail_beat(rx_beats[3], eof=0, tuser=0, byte_count=8, crc_mode=crc_mode) + assert_packetized_beat( + rx_beats[4], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=0, + tuser=0, + tdest=0x3, + tid=0x33, + seq=1, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[5], data=word_from_bytes(payload[16:24])) + assert_packetizer2_tail_beat(rx_beats[6], eof=1, tuser=0x4A, byte_count=8, crc_mode=crc_mode) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param({}, id="crc_none"), + pytest.param({"CRC_MODE_G": "DATA"}, id="crc_data"), + pytest.param({"CRC_MODE_G": "FULL"}, id="crc_full"), + ], +) +def test_AxiStreamPacketizer2(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreampacketizer2wrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamPacketizer2Wrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamPacketizer2Loopback.py b/tests/protocols/packetizer/test_AxiStreamPacketizer2Loopback.py new file mode 100644 index 0000000000..72e4f70d5c --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamPacketizer2Loopback.py @@ -0,0 +1,124 @@ +############################################################################## +## 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 cocotb-facing loopback wrapper that connects +# `AxiStreamPacketizer2` directly to `AxiStreamDepacketizer2`, sweeping the +# NONE, DATA, and FULL CRC modes. +# - Stimulus: Drive application AXI Stream frames with partial final `TKEEP`, +# nonzero first/last `TUSER`, `TDEST`, and `TID` through a small packet-size +# limit that forces the packetizer to emit continuation packets internally. +# - Checks: The loopback output must restore payload bytes, `TKEEP`, `TLAST`, +# `TDEST`, `TID`, and first/last `TUSER`, while the sink applies backpressure. +# - Timing: The test waits for depacketizer RAM initialization before traffic, +# then uses ordinary AXI Stream ready/valid handshakes on the application +# input and output only. + +import os + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + FlatAxisEndpoint, + assert_app_beat, + payload_to_beats, + recv_beats_with_backpressure, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, + wait_debug_init_done, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.linkGood.setimmediatevalue(1) + dut.maxPktBytes.setimmediatevalue(32) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + await self.wait_init_done() + + async def wait_init_done(self, timeout_cycles: int = 64): + await wait_debug_init_done(self.dut, timeout_cycles=timeout_cycles) + + +@cocotb.test() +async def loopback_split_partial_frame_with_backpressure_test(dut): + tb = TB(dut) + await tb.reset() + + # This 19-byte frame forces an internal V2 packet split at the 32-byte + # packetized limit, then ends with a three-byte final output word. + payload = bytes(range(0x10, 0x23)) + tdest_bits = int(os.getenv("TDEST_BITS_G", "2")) + dest = 0x2 + if tdest_bits == 0: + expected_dest = 0 + dest = 0 + else: + expected_dest = (1 << min(tdest_bits, 2)) - 1 + dest = expected_dest + input_beats = payload_to_beats( + payload, + dest=dest, + tid=0x39, + first_user=0x2C, + last_user=0x4C, + ) + + rx_task = cocotb.start_soon(recv_beats_with_backpressure(tb.sink, 3, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 6, "us") + + assert_app_beat(rx_beats[0], payload=payload[0:8], dest=expected_dest, tid=0x39, user=0x2E) + assert_app_beat(rx_beats[1], payload=payload[8:16], dest=expected_dest, tid=0x39) + assert_app_beat( + rx_beats[2], + payload=payload[16:19], + keep=0x07, + last=1, + dest=expected_dest, + tid=0x39, + user=0x4C << 16, + ) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param({"TDEST_BITS_G": 2}, id="crc_none"), + pytest.param({"TDEST_BITS_G": 2, "CRC_MODE_G": "DATA"}, id="crc_data"), + pytest.param({"TDEST_BITS_G": 2, "CRC_MODE_G": "FULL"}, id="crc_full"), + pytest.param({"TDEST_BITS_G": 0}, id="tdest0"), + pytest.param({"TDEST_BITS_G": 1}, id="tdest1"), + ], +) +def test_AxiStreamPacketizer2Loopback(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreampacketizer2loopbackwrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamPacketizer2LoopbackWrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamPacketizer2SeqWrap.py b/tests/protocols/packetizer/test_AxiStreamPacketizer2SeqWrap.py new file mode 100644 index 0000000000..b326377fcf --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamPacketizer2SeqWrap.py @@ -0,0 +1,121 @@ +############################################################################## +## 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 standalone `AxiStreamPacketizer2` wrapper with +# `SEQ_CNT_SIZE_G=4`, the smallest supported sequence counter width. +# - Stimulus: Drive one long frame that requires 17 internal packetizer +# packets, forcing the packet sequence field to wrap from 15 back to 0. +# - Checks: Every packet header must carry the expected wrapped sequence value, +# SOF must appear only on the first packet, and EOF must appear only on the +# final packet tail. +# - Timing: The sink is kept ready while the source frame is sent through the +# normal ready/valid handshake. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + PACKETIZER2_CRC_NONE, + assert_packetized_beat, + assert_packetizer2_tail_beat, + packetizer2_header_word, + recv_beats, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, + word_from_bytes, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.maxPktBytes.setimmediatevalue(32) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + + +@cocotb.test() +async def packetize_sequence_counter_wrap_test(dut): + tb = TB(dut) + await tb.reset() + + # Each packet carries two full payload words at this 32-byte packet limit. + # Seventeen packets therefore require thirty-four 8-byte input beats. + input_beats = [] + for index in range(34): + payload = bytes((0x20 + index + lane) & 0xFF for lane in range(8)) + input_beats.append( + AxisBeat( + data=word_from_bytes(payload), + keep=0xFF, + last=int(index == 33), + dest=0x1, + tid=0x55, + user=(0x24 if index == 0 else 0) | ((0x46 << 56) if index == 33 else 0), + ) + ) + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 68, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 20, "us") + + for packet_index in range(17): + base = packet_index * 4 + seq = packet_index & 0xF + assert_packetized_beat( + rx_beats[base], + data=packetizer2_header_word( + crc_mode=PACKETIZER2_CRC_NONE, + sof=int(packet_index == 0), + tuser=0x24 if packet_index == 0 else 0x00, + tdest=0x1, + tid=0x55, + seq=seq, + ), + user=0x2, + ) + assert_packetizer2_tail_beat( + rx_beats[base + 3], + eof=int(packet_index == 16), + tuser=0x46 if packet_index == 16 else 0x00, + byte_count=8, + ) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param({"SEQ_CNT_SIZE_G": 4}, id="seq4_wrap"), + ], +) +def test_AxiStreamPacketizer2SeqWrap(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreampacketizer2wrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamPacketizer2Wrapper.vhd"], + }, + )