From 9964b7443e86e34faf7720cbca762c6eb34576c0 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Mon, 31 Aug 2026 13:38:39 -0400 Subject: [PATCH 01/12] fmc: streamline the FMC target and run it at 100 MHz on cosmo Analysis showed the SP has been driving FMC_CLK at 50 MHz (CLKDIV=3) against FPGA constraints signed off at 66.67 MHz, and the target FSM was spending 7-8 fmc_clk cycles per 32-bit write plus a dead cycle before every read beat. This change reworks the interface for line rate while keeping the architecture (NWAIT-paced two-beat 32-bit accesses, async FIFO CDC into the 125 MHz AXI fabric) intact. RTL (stm32h7_fmc_target): - All FMC inputs are captured in dedicated per-pin registers before the FSM sees them; the FSM runs one cycle behind the bus, which the NWAIT pacing absorbs. On cosmo the capture registers are clocked by a later-phased (+135deg) sibling of the deskewed FSM clock, which is what gives the input pins setup margin at 10 ns; grapefruit ties both clock ports to the same clock for identical cycle behavior. - Streamlined FSM: writes take 6 fmc_clk cycles of bus occupancy (was 7-8) and read beats stream back-to-back (extra_beat_setup restores the old cadence if a board can't close single-cycle data-out). - Hardening: fmc_state now resets; an NWAIT timeout (generic, default 4096 cycles) converts a wedged AXI responder into poisoned reads / swallowed writes plus a sticky timeout_count instead of a hung SP bus, with stale read data discarded before any live read; the tristate control is re-derived from live NOE/NE every cycle and a sticky contention counter trips if we ever drive during an SP-owned phase; aborted writes complete with zero filler so the AXI side can't wedge; AWVALID and WVALID are now raised together. - The tristate control is registered in OBUFT T polarity (data_out_hiz, one IOB-attributed flop per pin): an active-high enable inferred a LUT1 inverter that blocked IOB packing and cost ~1.5 ns. cosmo clocking: new fmc_pll (clk_wiz, BUFG-in-feedback deskew, +45deg FSM/output phase, +135deg capture phase, OVERRIDE_MMCM to force M=12/D=1/O=12 so the 600-1200 MHz VCO locks at either a 50 or 100 MHz input -- one bitstream serves both hubris CLKDIV settings), an fmc_clk_monitor that holds the MMCM in reset across SP clock stops and retries lock on frequency change, and reset_fmc now gates on MMCM lock with async assert (no FMC clock edges exist while unlocked). Constraints: cosmo at 10 ns with a setup-2 multicycle establishing the edge relationship into the phase-shifted clocks, corrected min input delays (the old formulas omitted the SP's half-period of hold, telling Vivado data could change at the capture edge), multicycle data-out exceptions deleted, and IOB packing on the output/enable/nwait flops; grapefruit stays at 15 ns with the same structure. Both boards close (cosmo WNS +0.116/WHS +0.023, grapefruit +0.159/+0.031). Verification: the model now implements the WAITCFG=1/DATLAT=0 pacing contract, blocking writes via wait_until_idle (the 150 ns hack is gone), ES0491 dummy read cycles, configurable inter-transaction gaps, and one-shot mid-transaction aborts; the harness runs real fmc_clk/aclk ratios with a bus-contention tripwire, and the suite grew from 3 tests to 18 covering back-to-back traffic, FIFO backpressure, slow and wedged responders, timeout recovery, aborts, reset, and randomized soaks at 50/66.67/100 MHz. tools/fmc_sweep/fmc_sweep.py drives the hardware bring-up: a phased frequency sweep over the fmc-demo-server UDP protocol (reads of a constant register first at each step, so a timing miss is data-only, never contention), with CLKDIV control through humility. The hubris side (CLKDIV flips, fmc-demo-server timing-setter fixes) lands separately; the FPGA is NWAIT-paced, so this bitstream works unchanged at 50 MHz until hubris moves. --- .gitignore | 2 +- cos_soak_100.json | 53 ++ hdl/ip/vhd/fmc_if/docs/fmc_if.adoc | 100 ++- .../fmc_if/docs/fmc_verification_plan.adoc | 203 +++++++ hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model.vhd | 210 +++++-- .../fmc_if/model/stm32h7_fmc_model_pkg.vhd | 65 ++ .../vhd/fmc_if/model/stm32h7_fmc_sim_pkg.vhd | 50 +- hdl/ip/vhd/fmc_if/sims/fmc_tb.vhd | 338 ++++++++++- hdl/ip/vhd/fmc_if/sims/fmc_th.vhd | 67 ++- hdl/ip/vhd/fmc_if/stm32h7_fmc_target.vhd | 533 +++++++++++----- .../cosmo_seq/black_box_entities/fmc_pll.vhd | 23 + .../board_support/board_support_top.vhd | 44 +- .../board_support/fmc_clk_monitor.vhd | 124 ++++ .../cosmo_seq/board_support/reset_sync.vhd | 9 +- hdl/projects/cosmo_seq/cosmo_seq_top.vhd | 21 +- hdl/projects/cosmo_seq/cosmo_timing.xdc | 66 +- .../cosmo_seq/xilinx_ip_gen/fmc_pll_ip.tcl | 58 ++ hdl/projects/grapefruit/grapefruit_timing.xdc | 12 +- hdl/projects/grapefruit/grapefruit_top.vhd | 15 +- tools/fmc_sweep/fmc_sweep.py | 567 ++++++++++++++++++ tools/speeker/udp_if.py | 81 ++- 21 files changed, 2322 insertions(+), 319 deletions(-) create mode 100644 cos_soak_100.json create mode 100644 hdl/ip/vhd/fmc_if/docs/fmc_verification_plan.adoc create mode 100644 hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model_pkg.vhd create mode 100644 hdl/projects/cosmo_seq/black_box_entities/fmc_pll.vhd create mode 100644 hdl/projects/cosmo_seq/board_support/fmc_clk_monitor.vhd create mode 100644 hdl/projects/cosmo_seq/xilinx_ip_gen/fmc_pll_ip.tcl create mode 100755 tools/fmc_sweep/fmc_sweep.py diff --git a/.gitignore b/.gitignore index 840b033d..ae418798 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,4 @@ vivado*.str **/*.svg.bkp # cobble local env -BUILD.vars \ No newline at end of file +BUILD.vars diff --git a/cos_soak_100.json b/cos_soak_100.json new file mode 100644 index 00000000..6d91eb9b --- /dev/null +++ b/cos_soak_100.json @@ -0,0 +1,53 @@ +{ + "seed": 478, + "steps": [ + { + "divisor": 2, + "freq_mhz": 100.0, + "ok": true, + "udp_timeouts_retried": 9, + "phases": { + "A_const_reads": { + "ok": true, + "values": [ + "0x000001de" + ] + }, + "B_scratch": { + "ok": true, + "patterns": 68, + "mismatches": [] + }, + "C_stress": { + "ok": true, + "write_burst": 1024, + "read_words": 1024, + "mismatches": [] + }, + "D_soak": { + "ok": true, + "ops": 14969449, + "mismatches": [] + }, + "E_throughput": { + "ok": true, + "wire_write_MBps": 1.228, + "wire_read_MBps": 1.003, + "single_read_us": 226.7, + "fmc_read_MBps": 11.41, + "fmc_read_ns_per_word": 351, + "fmc_read_fixed_ns_per_word": 309, + "fmc_write_MBps": 50.17, + "fmc_write_ns_per_word": 80, + "note": "fmc_* numbers are bus line rate (delta-timed block ops); wire_* include per-word network cost and are insensitive to the FMC clock" + } + }, + "btr1": "0x0010ffff", + "clkdiv_verified": true, + "sp_ticks_before": 63172, + "sp_ticks_after": 3664254, + "sp_rebooted": false + } + ], + "restored_baseline_ok": true +} \ No newline at end of file diff --git a/hdl/ip/vhd/fmc_if/docs/fmc_if.adoc b/hdl/ip/vhd/fmc_if/docs/fmc_if.adoc index fd1a1f2c..a195778d 100644 --- a/hdl/ip/vhd/fmc_if/docs/fmc_if.adoc +++ b/hdl/ip/vhd/fmc_if/docs/fmc_if.adoc @@ -2,8 +2,8 @@ :toc: left :numbered: :icons: font -:revision: 1.0 -:revdate: 2024-04-24 +:revision: 2.0 +:revdate: 2026-09-10 = FMC Target interface @@ -37,6 +37,83 @@ The AXI state machine monitors the transaction FIFO (and the WDATA FIFO in the case of writes) and wiggles the AXI signals appropriately to issue the read/write transactions. +=== Pacing contract + +The wait line is held asserted by default and released under FSM control. +After the SP samples the wait line released at rising edge N, its current +data beat is valid on the bus at edge N+1, and each further release-sampled +edge advances one beat; on reads the SP samples data on the same edge it +samples the released wait. This cadence was proven on hardware; the FSM +and the simulation model both encode it. + +All FMC inputs pass through dedicated per-pin capture registers (clocked by +fmc_capture_clk, a later-phased MMCM sibling on boards that have one), so +the FSM runs one cycle behind the bus; NWAIT pacing absorbs the delay. +A 32-bit write occupies 6 fmc_clk cycles of bus time (address capture, +dispatch, two arm cycles, two back-to-back beats); the AXI write is posted +and completes behind the SP's back. A 32-bit read stalls through the +CDC/AXI round trip and then streams its two beats on consecutive cycles. +The `extra_beat_setup` generic re-inserts a dead cycle between read beats +for a board that cannot close single-cycle data-out timing (the constraints +must then carry matching setup-2 multicycle exceptions). + +=== Clocking, or how 100 MHz timing closes + +The wait line is sampled by the SP on every rising edge, so it is a +single-cycle output path with no multicycle tricks available: the FPGA has +period - tsu(NWAIT) - trace, about 6 ns at 10 ns, to get from its clock pin +through a flop and back out. A plain IBUF+BUFG spends 3.5-6.5 ns of that on +clock insertion alone, so at 100 MHz the internal FMC clock instead comes +from an MMCM in phase-alignment mode (BUFG in the feedback path), which +nulls the insertion delay and leaves the whole budget for the flop and +output buffer. The output/enable/nwait flops are additionally packed into +the IOBs, which is why data_out_hiz is one registered bit per pin in OBUFT +T polarity. + +Inputs get the opposite treatment. The SP launches on its falling edge, so +data arrives late in the cycle and the valid window at the pin is only a +few nanoseconds wide; worse, STA charges the full corner spread of the MMCM +clock network against a virtual clock with no pessimism removal. Rather +than fight that, the MMCM produces a second output for the capture +registers, phased later than the FSM clock (+135 deg vs +45 deg, see +fmc_pll_ip.tcl), which parks the sampling instant comfortably inside the +data-valid window. The capture registers are the only logic on that clock; +the FSM consumes their outputs on its own clock a cycle later, which is +where the "FSM runs one cycle behind the bus" above comes from. The +45 deg +FSM/output phase is itself pinned by output hold on the NWAIT/data pins +(earlier violates hold), so the two phases are not adjustable +independently of the analysis in cosmo_timing.xdc. + +Both phases are specified in degrees, and the MMCM implements phase as +VCO-referenced taps, so they are true fractions of the period: at 50 MHz +the same bitstream places every edge proportionally later and every margin +only grows. The constraints are therefore written once at 10 ns as a +strict superset of all slower rates. The MMCM's VCO multiplier (M=12) is +chosen so the VCO stays in range from a 50 MHz input through a 100 MHz +one, letting one bitstream lock at any SP CLKDIV setting; a clock-stop +monitor in the fabric holds the MMCM in reset while the SP's clock is +absent and relocks it on return. A board without a clock-capable pin for +the FMC clock (grapefruit) skips the MMCM, ties fmc_capture_clk to +fmc_clk, and is bounded by the plain-BUFG budget to 66.67 MHz. + +=== Robustness + +* NWAIT timeout: a transaction that would stall the SP longer than + `timeout_cycles` (a wedged AXI responder, full FIFOs that never drain, or + a transaction start the FSM never observed -- chip select pinned with no + address latch seen, e.g. a miscaptured NADV) is abandoned: reads return + 0xFFFF poison beats, writes are swallowed, and the sticky `timeout_count` + output increments. Data from a timed-out read that later arrives is + discarded before any live read is served. Without the missed-start case + the SP would hang on its bus with nothing to free it, which from software + looks like a silent death followed by a watchdog reboot. +* Contention tripwire: the tristate control is re-derived from the live NOE/NE + pins every cycle it could drive, and `contention_count` increments if the + enable is ever up while the SP owns the bus (NADV or NWE low). +* A mid-transaction chip-select abort cleans up within a few cycles; a write + whose transaction was already queued is completed with zero filler so the + AXI side never wedges waiting on write data. + == Sim Env The simulation environment has a model of the STM32's FMC peripheral with some @@ -50,18 +127,19 @@ testbench to properly interop with our AXI-Lite controller interface. == Current status -This has been simulated against FMC models for single 32-bit read/writes. No -bursting is supported currently. Only 32bit access with no consecutive access is -implemented. - +Simulated against the FMC model for single 32-bit read/writes at 50, 66.67, +and 100 MHz fmc_clk against a 125 MHz AXI clock, including back-to-back +traffic, FIFO-full backpressure, slow and wedged AXI responders, timeout +recovery, mid-transaction aborts, reset between transactions, and a +randomized soak (`buck2 run //hdl/ip/vhd/fmc_if:fmc_tb`). No bursting beyond +the two beats of a 32-bit access is supported; that is a deliberate scope +decision. The model implements the WAITCFG=1/DATLAT=0 pacing contract, +blocking writes via wait_until_idle, the ES0491 dummy read cycles, and +one-shot abort injection. == Open Actions -* Support multi-word transactions on FMC interface - -* Support byte-enables FMC - AXI for <32bit access - -* Clean up FMC model, especially around writes and figuring out blocking +* Support byte-enables FMC - AXI for <32bit access == Future Investigation/Next steps diff --git a/hdl/ip/vhd/fmc_if/docs/fmc_verification_plan.adoc b/hdl/ip/vhd/fmc_if/docs/fmc_verification_plan.adoc new file mode 100644 index 00000000..69dca327 --- /dev/null +++ b/hdl/ip/vhd/fmc_if/docs/fmc_verification_plan.adoc @@ -0,0 +1,203 @@ +:showtitle: +:toc: left +:numbered: +:icons: font +:revdate: 2026-09-10 + += FMC speed-up verification plan + +Verification for the FMC line-rate work: streaming FSM + input capture stage, +cosmo at 100 MHz behind the fmc_pll MMCM, grapefruit at 66.67 MHz. Each phase +gates the next; a failure stops the progression at a known-good configuration. +Companion pieces: `fmc_if.adoc` (design + pacing contract), +`tools/fmc_sweep/fmc_sweep.py` (the sweep driver), and the hubris revs +(demo-server fixes, then the gated CLKDIV flip). + +Throughout: FMC_CLK divisor N means FMC_CLK = 200 MHz / N, so divisor 4 = +50 MHz (shipping), 3 = 66.67 MHz, 2 = 100 MHz. + +== Phase 0: desk checks (no hardware) + +. Simulation regression: `buck2 run //hdl/ip/vhd/fmc_if:fmc_tb` -- all 19 + tests, which cover single and back-to-back reads/writes, varied addresses, + FIFO backpressure, slow and wedged AXI responders, NWAIT timeout recovery + (including a missed transaction start), mid-transaction aborts, reset + between transactions, and randomized soaks, exercised at 50/66.67/100 MHz + fmc_clk against the 125 MHz AXI clock. + The harness fails any test on bus contention (model and DUT driving in the + same phase). +. Timing closure: build both bitstreams and read the post-route summaries -- + the build does NOT fail on timing, the reports are the authority: ++ +---- +buck2 build //hdl/projects/cosmo_seq:cosmo_seq //hdl/projects/grapefruit:grapefruit +grep -A6 "Design Timing Summary" buck-out/.../cosmo_seq_route_timing.rpt +grep -A6 "Design Timing Summary" buck-out/.../grapefruit_route_timing.rpt +---- ++ +Required: WNS and WHS both positive on both boards, zero failing endpoints, +and the Inter Clock Table must show nonzero endpoint counts for every FMC +class (fmc_virt_clk -> capture clock, capture -> FSM clock, FSM clock -> +fmc_virt_clk). A class with zero endpoints means a constraint silently +stopped matching (renamed cell or clock) and the "pass" is vacuous. +Margins land near +0.1 setup / +0.02 hold on cosmo at 10 ns and somewhat +looser on grapefruit at 15 ns, moving a few tens of picoseconds per rebuild; +cosmo's output hold is the phase-limiting margin and the one to watch. +. Datasheet re-verification (once, on paper): confirm against DS12110 that + the XDC's SP timing numbers (td(CLKL-NExL)=1, td(CLKL-AV)=2.5, + td(CLKL-ADV)=3, tsu(NWAIT-CLKH)=3, th(CLKH-NWAIT)=2) match the synchronous + multiplexed PSRAM tables at the relevant conditions, and that 100 MHz + FMC_CLK is within spec at VOS1. The constraints inherited these values + from the 66.67 MHz era and have not been re-read against rev-current + silicon docs. +. hubris demo-server rev: land before any sweep. Runtime timing control + does not exist without it -- the task previously lacked the FMC controller + register region and faulted on every setter call (reported by hiffy as + success), and the setters themselves clobbered sibling BTR1 fields via + `btr1.write()`. It also adds the BTR1/BCR1 readback ops the sweep uses to + verify each divisor change and the block-transfer ops behind the phase-E + line-rate numbers. + +== Phase 1: grapefruit at 50 MHz (new RTL, old frequency) + +Goal: prove the streamlined FSM + capture stage is a drop-in at the +shipping frequency, before any clock change. The FPGA is NWAIT-paced, so no +behavioral difference is expected. + +. Build the archive with the new bitstream + (`buck2 run //tools/fpga_releaser:cli -- --fpga grapefruit --hubris ` + or the usual release flow) with hubris still at CLKDIV=3: the + demo-server rev included, the CLKDIV flip not included. +. Flash, confirm hubris boots and the FPGA loads (spartan7-loader token + checks pass, grapefruit_seq comes up). +. Baseline functional pass: ++ +---- +./tools/fmc_sweep/fmc_sweep.py --ip --interface \ + --archive --json gf_baseline.json +---- ++ +This runs phases A-E at divisor 4 only: constant-register reads with gaps, +scratch write/readback (info scratchpad at +0x10; fpga_checksum at +0xC is +off-limits -- hubris uses it to decide whether to reprogram the FPGA), +back-to-back write bursts into the scratchpad verified by ordering/last +value, double-pass read sweeps of the post-code buffer (an external capture +mem: readable, but AXI writes to it are dropped, so bulk +write-verification is the simulation suite's job, not the bench's), a +500-op seeded soak, and a throughput measurement. All must pass. +. Record the phase-E numbers as the throughput baseline. +. Leave a longer soak running with `--soak-seconds 3600` (the soak is + otherwise op-counted, and 500 UDP round trips finish in seconds). + Acceptance: zero mismatches, no UDP timeouts. + +== Phase 2: grapefruit sweep to 66.67 MHz + +. `--sweep 4,3` first. Phase ordering inside each step is deliberate: + constant reads come first because the SP never drives the bus during read + data phases, so a timing miss at a new frequency shows up as wrong data, + never as a driver fight. Do not reorder. +. If divisor 3 passes: rerun with `--sweep 4,3,2`. Divisor 2 (100 MHz) is + expected to FAIL on grapefruit -- its clock enters on non-clock-capable + F17 with ~6 ns of detour insertion delay and no MMCM. The point is to + record where the cliff is and confirm the tool's failure handling: + it must stop at the first failing phase, restore divisor 4, and re-verify + phase A (`restored_baseline_ok: true` in the JSON). +. Acceptance for 66.67 MHz: full A-E pass plus a >= 1 hour soak + (`--soak-seconds 3600`) at divisor 3, + and phase-E throughput ~1.33x the baseline batched numbers. + +== Phase 3: cosmo at 50 MHz (MMCM bitstream, old frequency) + +Goal: prove the MMCM clocking infrastructure -- dual-frequency lock, the +clock-stop monitor, reset gating -- before touching the frequency. + +. Flash the new archive (CLKDIV still 3). First FMC access happening at all + proves the MMCM locked at a 50 MHz input (the VCO sits at its 600 MHz + minimum here -- this IS the risky corner of the dual-frequency config, so + a boot-loop or dead FMC points straight at lock failure; see the rollback + note below). +. Run the full fmc_sweep at divisor 4; record baseline JSON. +. Regenerate the ILA against the new netlist (the old fmc_ila.tcl net names + are stale: the clock is now `clk_fmc_fmc_pll`, the enable regs are + `data_out_hiz_int_reg[*]`, and `mark_debug` already tags fmc_state, + axi_state, txn, timeouts, contentions). Probe at minimum: fmc_state, + NWAIT, CS/ADV/WE/OE IBUFs, DA in/out, timeouts, contentions, and the + monitor's mmcm_reset/locked. +. With the ILA, verify on live traffic: + * NWAIT cadence matches the model: one release-sample per beat, two + consecutive released samples per 32-bit access, wait re-asserted between + transactions. + * ES0491 dummy read cycles appear after bursts and provoke no state + change. + * `timeouts == 0` and `contentions == 0` after every soak. Nonzero + contention is a stop-ship: it means the pacing contract disagrees with + real silicon somewhere. +. Robustness loops (the monitor/reset machinery): + * SP warm reset under active traffic, >= 50 iterations scripted. Each + time: FMC clock stops, monitor must hold the MMCM in reset, relock on + clock return, and peek/poke must work immediately after the SP is back. + No FPGA reprogram in this loop -- the bitstream rides through. + * FPGA reprogram (SP-driven, SP idle on FMC), >= 10 iterations, traffic + resumes cleanly each time. + * Full power cycle x5. + * Acceptance: zero hangs (an SP stuck in an FMC access = the timeout + machinery or reset gating failed), counters clean afterward. + +== Phase 4: cosmo sweep to 100 MHz + +. `--sweep 4,3,2` with the lab-image demo-server. All phases must pass at + every step; there is no acceptable-failure divisor on cosmo. +. One-hour soak (`--soak-seconds 3600`) at divisor 2, counters zero. +. MMCM phase margin sweep: rebuild bitstream variants with the phases + moved +/-18.75 deg = 5 MMCM taps = ~0.52 ns (the phase grid is + 45/CLKOUT_DIVIDE = 3.75 deg per tap; edit `fmc_pll_ip.tcl`: FSM/output + phase 26.25/45/63.75, capture phase 116.25/135/153.75 -- move them + together first, then independently if a failure needs isolating). Run + the divisor-2 soak on each variant. Acceptance: the shipped phases pass + with both neighbors also passing, demonstrating >= ~0.5 ns of real + margin beyond STA. If a neighbor fails, the shipped point is marginal: + investigate before un-gating, don't ship on one passing point. +. Re-verify the SP warm-reset loop at divisor 2 (relock now returns to a + 1200 MHz VCO -- the other edge of the range). + +== Phase 5: end-to-end throughput + +. fmc_sweep phase E reports two families of numbers. `fmc_*` values come + from delta-timed server-side block ops (demo-server network ops 17-19: + checksum block reads and a fixed-address fill), where the round trip and + per-packet costs cancel and what remains is bus accesses -- these are the + line-rate measurements and must scale with the divisor (~1.33x going + 4 -> 3, ~2x going 4 -> 2). `wire_*` values come from plain batched + peek/poke: ~9 network bytes per word swamps the ~0.4 us bus + access, so they are expected NOT to move with the divisor -- they are a + network/health reference, not a bus measurement. An `fmc_*` number that + fails to scale with the divisor means the divisor did not actually + change (or the block ops hit an error, which the checksum cross-checks + catch). +. Timed cosmo-hf host-flash operations (the hot path: one status poll + one + data read per u32) before/after -- same image content, hubris timestamps + or a host-side stopwatch around a full read-back. Expected ~1.6-1.8x at + 100 MHz; writes limited by flash program time, so use read-back as the + metric. +. The phase-E `fmc_*` table across divisors goes in the PR description. + +== Phase 6: un-gate the hubris CLKDIV rev + +Per board, only after that board's phases pass: land the hubris rev that +raises the boot-time divider (cosmo CLKDIV=1, grapefruit CLKDIV=2) in the +same archive as its validated bitstream -- the archive carries both, so +image and clock setting move atomically and no mixed-field state exists. + +Rollback at any point is the one-line CLKDIV revert: every bitstream in this +series is NWAIT-paced and runs at 50 MHz unchanged. The single caveat is a +cosmo MMCM that fails to lock at one input frequency (dual-VCO corner): that +is a bitstream-level rollback, which is why Phase 3 proves the 50 MHz lock +corner before anything else depends on it. + +== Open observability item + +`timeout_count` / `contention_count` are ports (left open at the tops) and +mark_debug signals. Wiring them into a spare info/debug register would make +the Phase 3/4 counter checks possible from software instead of the ILA and +would let fmc_sweep gate every step on them; worth doing before the Phase 4 +soaks. diff --git a/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model.vhd b/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model.vhd index ad1f166b..62cc83c1 100644 --- a/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model.vhd +++ b/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model.vhd @@ -2,11 +2,29 @@ -- License, v. 2.0. If a copy of the MPL was not distributed with this -- file, You can obtain one at https://mozilla.org/MPL/2.0/. ---! FMC controller model based on ST's RM0433 rev8 ---! figures 115 and 116 for simulation of the ---! FPGA's target interface ---! Oxide's internal doc mirror link: ---! https://drive.google.com/file/d/1wPaZAHS3-0HdMkXOC8tvGYgOPOrM0qRQ/view?usp=drive_link +-- FMC controller model based on ST's RM0433 rev8 +-- figures 115 and 116 for simulation of the +-- FPGA's target interface +-- Oxide's internal doc mirror link: +-- https://drive.google.com/file/d/1wPaZAHS3-0HdMkXOC8tvGYgOPOrM0qRQ/view?usp=drive_link +-- +-- Timing semantics modeled here (WAITCFG=1, DATLAT=0, the configuration +-- hubris programs): NWAIT is sampled on rising clock edges once the address +-- phase is over. For writes, each NWAIT-released rising edge advances one +-- data beat onto the bus at the following falling edge, so the beat is +-- captured by the target on the rising edge after the release was sampled. +-- For reads, data is sampled on the same rising edge where NWAIT is seen +-- released. This matches the cadence proven on hardware against the +-- pre-streaming target FSM; if hardware ILA captures ever disagree with +-- this contract, fix it here first. +-- +-- ES0491 (dummy read cycles): after every burst read the controller +-- performs two dummy read accesses with the chip still selected; the model +-- reproduces them so the target's idle-return is exercised under them. +-- +-- Also handled on the bus actor: wait_until_idle (making writes blockable), +-- and the control messages in stm32h7_fmc_model_pkg (inter-transaction gap, +-- one-shot mid-transaction abort). library ieee; use ieee.std_logic_1164.all; @@ -16,6 +34,9 @@ library vunit_lib; context vunit_lib.vunit_context; context vunit_lib.com_context; use vunit_lib.bus_master_pkg.all; +use vunit_lib.sync_pkg.all; + +use work.stm32h7_fmc_model_pkg.all; entity stm32h7_fmc_model is generic ( @@ -26,7 +47,6 @@ entity stm32h7_fmc_model is a : out std_logic_vector(address_length(bus_handle) - 1 downto 16); ad : inout std_logic_vector(data_length(bus_handle) - 1 downto 0); ne : out std_logic_vector(3 downto 0); - -- todo missing byte enables? noe : out std_logic; nwe : out std_logic; nl : out std_logic; @@ -36,18 +56,10 @@ end entity; architecture model of stm32h7_fmc_model is - type txn_type is (read_txn, write_txn); - signal delayed_wait : std_logic := '0'; + type txn_type is (read_txn, write_txn); begin - -- wait_delay : process (clk) - -- begin - -- if rising_edge(clk) then - -- delayed_wait <= not nwait; - -- end if; - -- end process; - bfm: process variable request_msg : msg_t; variable reply_msg : msg_t; @@ -56,6 +68,13 @@ begin variable addr : std_logic_vector(address_length(bus_handle) - 1 downto 0); variable data : std_logic_vector(data_length(bus_handle) - 1 downto 0); variable rem_data_cnt : integer; + variable beats_done : natural; + variable aborted : boolean; + -- extra idle cycles between transactions; 0 = back-to-back + variable gap_cycles : natural := 0; + -- one-shot: abort the next transaction after this many beats + -- (negative = disarmed) + variable abort_beats : integer := -1; procedure bus_idle is begin @@ -96,57 +115,122 @@ begin end; begin bus_idle; - nl <= '1'; - receive(net, BUS_HANDLE.p_actor, request_msg); - msg_type := message_type(request_msg); - -- All bus transactions begin with the FMC_CLK - -- low - wait until falling_edge(clk); - if msg_type = bus_burst_write_msg then - -- Figure 116 - -- activate address, chipsel, write, and latch - transaction_start(WRITE_TXN); - wait until falling_edge(clk); - wait until falling_edge(clk); - -- on next falling edge of clock, apply wdata - while rem_data_cnt > 0 loop - wait on clk; - -- on every rising edge that wait isn't asserted, - -- we've done a transfer, so get the data, dec the counter, - -- apply to bus - if falling_edge(clk) and nwait = '1' then - data := pop_std_ulogic_vector(request_msg); - rem_data_cnt := rem_data_cnt - 1; - ad <= data; + loop + receive(net, BUS_HANDLE.p_actor, request_msg); + msg_type := message_type(request_msg); + if msg_type = set_txn_gap_msg then + gap_cycles := pop_integer(request_msg); + elsif msg_type = abort_next_msg then + abort_beats := pop_integer(request_msg); + elsif msg_type = wait_until_idle_msg then + -- Messages are handled in order, so reaching this one means + -- every previously requested bus cycle has completed; this is + -- what makes fmc_write32 blockable. + handle_wait_until_idle(net, msg_type, request_msg); + elsif msg_type = bus_burst_write_msg then + -- Figure 116: all bus transactions begin with FMC_CLK low + wait until falling_edge(clk); + beats_done := 0; + aborted := false; + transaction_start(WRITE_TXN); + -- NWAIT sampling starts on the first rising edge after the + -- address phase. The abort check sits after the edge wait so + -- an already-applied beat is held through its capture edge + -- before the bus deasserts. + while rem_data_cnt > 0 loop + wait until rising_edge(clk); + if abort_beats >= 0 and beats_done = abort_beats then + aborted := true; + abort_beats := -1; + exit; + end if; + if nwait = '1' then + wait until falling_edge(clk); + data := pop_std_ulogic_vector(request_msg); + rem_data_cnt := rem_data_cnt - 1; + ad <= data; + beats_done := beats_done + 1; + end if; + end loop; + if aborted then + -- drain the un-sent beats so the message queue stays + -- consistent + while rem_data_cnt > 0 loop + data := pop_std_ulogic_vector(request_msg); + rem_data_cnt := rem_data_cnt - 1; + end loop; + bus_idle; + -- The target takes a few cycles to notice the deselect + -- (its view of the bus is one capture-register cycle + -- behind) and clean up; a real SP cannot restart within + -- one cycle of an abort either, so give it room before + -- the next transaction. + for i in 1 to 4 loop + wait until falling_edge(clk); + end loop; + else + -- hold the final beat through its capture edge + wait until falling_edge(clk); + bus_idle; end if; - end loop; - wait until falling_edge(clk); - elsif msg_type = bus_burst_read_msg then - reply_msg := new_msg; - -- Figure 115 - -- activate address, chipsel, and latch - transaction_start(READ_TXN); - push_integer(reply_msg, rem_data_cnt); - -- on next falling edge of clock, data could be on the bus - wait until falling_edge(clk); - wait until rising_edge(clk); - while rem_data_cnt > 0 loop - wait on clk; - if rising_edge(clk) and nwait = '1' then - -- sample data, dec remaining data - push_std_ulogic_vector(reply_msg, ad); - rem_data_cnt := rem_data_cnt - 1; + elsif msg_type = bus_burst_read_msg then + wait until falling_edge(clk); + beats_done := 0; + aborted := false; + reply_msg := new_msg; + -- Figure 115 + transaction_start(READ_TXN); + push_integer(reply_msg, rem_data_cnt); + -- data cannot be valid before the edge after the address + -- phase completes + wait until falling_edge(clk); + while rem_data_cnt > 0 loop + wait until rising_edge(clk); + if abort_beats >= 0 and beats_done = abort_beats then + aborted := true; + abort_beats := -1; + exit; + end if; + if nwait = '1' then + -- sample data on the same edge the released wait is + -- sampled + push_std_ulogic_vector(reply_msg, ad); + rem_data_cnt := rem_data_cnt - 1; + beats_done := beats_done + 1; + end if; + end loop; + if aborted then + -- fill the reply so burst_read_bus completes; the values + -- are meaningless by construction (note: must carry a + -- downto range to match the reader's slice) + data := (others => '0'); + while rem_data_cnt > 0 loop + push_std_ulogic_vector(reply_msg, data); + rem_data_cnt := rem_data_cnt - 1; + end loop; + else + -- ES0491: two dummy read cycles, chip still selected + wait until rising_edge(clk); + wait until rising_edge(clk); end if; + reply(net, request_msg, reply_msg); + wait until falling_edge(clk); + bus_idle; + if aborted then + -- as for writes: let the target finish its abort cleanup + for i in 1 to 4 loop + wait until falling_edge(clk); + end loop; + end if; + else + -- This shouldn't happen but will provide + -- proper error reporting if it does + unexpected_msg_type(msg_type); + end if; + for i in 1 to gap_cycles loop + wait until falling_edge(clk); end loop; - -- data out - -- tbd waits - reply(net, request_msg, reply_msg); - wait on clk; - else - -- This shouldn't happen but will provide - -- proper error reporting if it does - unexpected_msg_type(msg_type); - end if; + end loop; end process; end model; diff --git a/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model_pkg.vhd b/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model_pkg.vhd new file mode 100644 index 00000000..8710d6a6 --- /dev/null +++ b/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model_pkg.vhd @@ -0,0 +1,65 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +-- Control messages for the STM32H7 FMC controller model. These ride the +-- same actor as the VUnit bus-master traffic so they stay ordered with the +-- transactions they configure. + +library ieee; +use ieee.std_logic_1164.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; +use vunit_lib.bus_master_pkg.all; + +package stm32h7_fmc_model_pkg is + + constant set_txn_gap_msg : msg_type_t := new_msg_type("fmc_set_txn_gap"); + constant abort_next_msg : msg_type_t := new_msg_type("fmc_abort_next"); + + -- Insert this many extra idle fmc_clk cycles between subsequent bus + -- transactions (0 = back-to-back, the default). + procedure fmc_set_txn_gap ( + signal net : inout network_t; + constant bus_handle : bus_master_t; + constant cycles : natural + ); + + -- Arm a one-shot mid-transaction abort: the next transaction deasserts + -- chip select after `after_beats` data beats have transferred + -- (0 = abort right after the address phase). + procedure fmc_abort_next ( + signal net : inout network_t; + constant bus_handle : bus_master_t; + constant after_beats : natural + ); + +end package; + +package body stm32h7_fmc_model_pkg is + + procedure fmc_set_txn_gap ( + signal net : inout network_t; + constant bus_handle : bus_master_t; + constant cycles : natural + ) is + variable request_msg : msg_t := new_msg(set_txn_gap_msg); + begin + push_integer(request_msg, cycles); + send(net, bus_handle.p_actor, request_msg); + end; + + procedure fmc_abort_next ( + signal net : inout network_t; + constant bus_handle : bus_master_t; + constant after_beats : natural + ) is + variable request_msg : msg_t := new_msg(abort_next_msg); + begin + push_integer(request_msg, after_beats); + send(net, bus_handle.p_actor, request_msg); + end; + +end package body; diff --git a/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_sim_pkg.vhd b/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_sim_pkg.vhd index 1b7a6f63..068566f7 100644 --- a/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_sim_pkg.vhd +++ b/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_sim_pkg.vhd @@ -2,8 +2,7 @@ -- License, v. 2.0. If a copy of the MPL was not distributed with this -- file, You can obtain one at https://mozilla.org/MPL/2.0/. ---! Bus master model based on ST's RM0433 ---! figures 115 and 116 +-- Testbench-facing helpers for driving the STM32H7 FMC controller model. library ieee; use ieee.std_logic_1164.all; @@ -13,6 +12,7 @@ library vunit_lib; context vunit_lib.vunit_context; context vunit_lib.com_context; use vunit_lib.bus_master_pkg.all; +use vunit_lib.sync_pkg.all; package stm32h7_fmc_sim_pkg is @@ -24,12 +24,28 @@ package stm32h7_fmc_sim_pkg is variable data : inout std_logic_vector ); + -- Blocks until the bus cycle has completed on the FMC pins. Note the + -- posted write may still be crossing into the AXI domain when this + -- returns; a subsequent fmc_read32 orders behind it, or wait for the + -- CDC/AXI latency before checking memory directly. procedure fmc_write32 ( signal net : inout network_t; constant address : std_logic_vector; variable data : inout std_logic_vector ); + -- Fire-and-forget variant, for queuing back-to-back traffic. + procedure fmc_write32_nb ( + signal net : inout network_t; + constant address : std_logic_vector; + variable data : inout std_logic_vector + ); + + -- Blocks until every previously queued transaction's bus cycle is done. + procedure fmc_wait_idle ( + signal net : inout network_t + ); + end package; package body stm32h7_fmc_sim_pkg is @@ -41,32 +57,46 @@ package body stm32h7_fmc_sim_pkg is ) is variable queue : queue_t; - constant butst_length : integer := 2; + constant burst_length : integer := 2; begin queue := new_queue; - burst_read_bus(net, SP_BUS_HANDLE, address, BUTST_LENGTH, queue); + burst_read_bus(net, SP_BUS_HANDLE, address, BURST_LENGTH, queue); data(15 downto 0) := pop_std_ulogic_vector(queue); data(31 downto 16) := pop_std_ulogic_vector(queue); end; - procedure fmc_write32 ( + procedure fmc_write32_nb ( signal net : inout network_t; constant address : std_logic_vector; variable data : inout std_logic_vector ) is variable queue : queue_t; - constant butst_length : integer := 2; + constant burst_length : integer := 2; begin queue := new_queue; push_std_ulogic_vector(queue, data(15 downto 0)); push_std_ulogic_vector(queue, data(31 downto 16)); - burst_write_bus(net, SP_BUS_HANDLE, address, BUTST_LENGTH, queue); - -- A bit of a hack until I figure out how to make this blocking - -- See https://github.com/VUnit/vunit/issues/1012 - wait for 150 ns; + burst_write_bus(net, SP_BUS_HANDLE, address, BURST_LENGTH, queue); + end; + + procedure fmc_write32 ( + signal net : inout network_t; + constant address : std_logic_vector; + variable data : inout std_logic_vector + ) is + begin + fmc_write32_nb(net, address, data); + fmc_wait_idle(net); + end; + + procedure fmc_wait_idle ( + signal net : inout network_t + ) is + begin + wait_until_idle(net, SP_BUS_HANDLE.p_actor); end; end package body; diff --git a/hdl/ip/vhd/fmc_if/sims/fmc_tb.vhd b/hdl/ip/vhd/fmc_if/sims/fmc_tb.vhd index 53758baa..2120d410 100644 --- a/hdl/ip/vhd/fmc_if/sims/fmc_tb.vhd +++ b/hdl/ip/vhd/fmc_if/sims/fmc_tb.vhd @@ -12,6 +12,7 @@ library vunit_lib; context vunit_lib.vunit_context; context vunit_lib.vc_context; use work.stm32h7_fmc_sim_pkg.all; +use work.stm32h7_fmc_model_pkg.all; use work.fmc_tb_pkg.all; entity fmc_tb is @@ -30,15 +31,101 @@ begin bench: process -- Note: External names are broken in GHDL llvm backends https://github.com/ghdl/ghdl/issues/2610 -- So this sim only works in other simulators, like nvc - -- reset_a uses the absolute path form (starting with a '.') and - -- reset_b uses the relative path form of external naming for example purposes. alias reset is << signal th.reset : std_logic >>; + alias fmc_half_period is << signal th.fmc_half_period : time >>; + alias timeout_count is << signal th.timeout_count : std_logic_vector(7 downto 0) >>; + alias ne_pins is << signal th.ne : std_logic_vector(3 downto 0) >>; + alias nwait_pin is << signal th.nwait : std_logic >>; + alias contention_count is << signal th.contention_count : std_logic_vector(7 downto 0) >>; variable address : std_logic_vector(25 downto 0) := (others => '0'); variable data : std_logic_vector(31 downto 0) := (others => '0'); variable expected_data : std_logic_vector(31 downto 0) := (others => '0'); variable buf : buffer_t; variable buf2 : buffer_t; + variable rand_state : unsigned(31 downto 0) := x"1234_5678"; + variable addr_nat : natural; + + -- deterministic LCG so the soak needs no external randomization + -- packages and reproduces exactly + impure function rand32 return unsigned is + begin + rand_state := resize(rand_state * 1664525, 32) + 1013904223; + return rand_state; + end; + + -- backing pattern for pre-filled read memory + function rd_pattern ( + addr : natural + ) return std_logic_vector is + begin + return std_logic_vector(resize(to_unsigned(addr, 32) * 1664525, 32) xor x"A5A5_A5A5"); + end; + + function to_addr ( + addr : natural + ) return std_logic_vector is + begin + return std_logic_vector(to_unsigned(addr, 26)); + end; + + procedure basic_write_read_pair is + begin + buf := allocate(wmemory, 4096); + buf2 := allocate(rmemory, 4096); + data := X"DEAD_BEEF"; + set_expected_word(wmemory, 16#40#, data); + fmc_write32(net, to_addr(16#40#), data); + expected_data := X"CAFE_F00D"; + write_word(rmemory, 16#80#, expected_data); + fmc_read32(net, to_addr(16#80#), data); + check_equal(data, expected_data, "Read data did not match expected"); + wait for 2 us; -- let the posted write land before checking memory + check_expected_was_written(buf); + end; + + procedure soak ( + constant num_ops : natural + ) is + constant rd_words : natural := 1024; + constant wr_base : natural := 16#10000#; + variable wr_index : natural := 0; + variable do_read : boolean; + variable r : unsigned(31 downto 0); + begin + buf := allocate(wmemory, 16#20000#); + buf2 := allocate(rmemory, 4 * rd_words); + for i in 0 to rd_words - 1 loop + write_word(rmemory, i * 4, rd_pattern(i * 4)); + end loop; + for i in 1 to num_ops loop + -- occasionally change the inter-transaction gap + r := rand32; + if r(2 downto 0) = "000" then + fmc_set_txn_gap(net, sp_bus_handle, to_integer(r(5 downto 3))); + end if; + r := rand32; + do_read := r(0) = '1'; + if do_read then + addr_nat := to_integer(r(11 downto 2)) * 4; + fmc_read32(net, to_addr(addr_nat), data); + check_equal(data, rd_pattern(addr_nat), + "soak read mismatch at addr " & to_string(addr_nat)); + else + -- distinct write addresses so in-flight writes can never + -- race a later expectation on the same word + addr_nat := wr_base + wr_index * 4; + wr_index := wr_index + 1; + data := std_logic_vector(rand32); + set_expected_word(wmemory, addr_nat, data); + fmc_write32_nb(net, to_addr(addr_nat), data); + end if; + end loop; + fmc_set_txn_gap(net, sp_bus_handle, 0); + fmc_wait_idle(net); + wait for 2 us; + check_expected_was_written(buf); + end; begin -- Always the first thing in the process, set up things for the VUnit test runner test_runner_setup(runner, runner_cfg); @@ -59,11 +146,13 @@ begin set_expected_word(wmemory, to_integer(address), data); -- Do the FMC -> AXI write transaction fmc_write32(net, address, data); + wait for 2 us; -- posted write crosses the CDC behind us check_expected_was_written(buf); -- Do a second transaction data := X"ADEADBAD"; set_expected_word(wmemory, to_integer(address), data); fmc_write32(net, address, data); + wait for 2 us; check_expected_was_written(buf); elsif run("basic_fmc_read_test") then buf := allocate(rmemory, 4 * 2, alignment => 32); @@ -78,11 +167,11 @@ begin -- Now do the FMC transaction, and check that returned data matches fmc_read32(net, address, data); check_equal(data, expected_data, "Read data did not match exptected"); - -- -- Do a second transaction - -- expected_data := X"ADEADBAD"; - -- write_word(rmemory, base_address(buf), expected_data); - -- fmc_read32(net, address, data); - -- check_equal(data, expected_data, "Read data did not match exptected"); + -- Do a second transaction back-to-back + expected_data := X"ADEADBAD"; + write_word(rmemory, base_address(buf), expected_data); + fmc_read32(net, address, data); + check_equal(data, expected_data, "2nd read data did not match exptected"); elsif run("basic_fmc_read_after_write") then data := X"DEADBEEF"; -- Set up the buffer used by the AXI write target @@ -94,25 +183,252 @@ begin set_expected_word(wmemory, to_integer(address), data); -- Do the FMC -> AXI write transaction fmc_write32(net, address, data); - check_expected_was_written(buf); + -- the posted write must land before its expectation is + -- replaced with the second value + wait for 2 us; -- Do a second transaction expected_data := X"ADEADBAD"; set_expected_word(wmemory, to_integer(address), expected_data); fmc_write32(net, address, expected_data); - check_expected_was_written(buf); write_word(rmemory, base_address(buf2), expected_data); set_permissions(rmemory, base_address(buf2), read_only); + -- The read orders behind both writes in the transaction FIFO, + -- so it also acts as the write-completion flush fmc_read32(net, address, data); check_equal(data, expected_data, "Read data did not match exptected"); + check_expected_was_written(buf); + elsif run("varied_address_write_read") then + -- walk a one over the address bits; 512 KB keeps the VUnit + -- memory model inside nvc's heap while still covering the + -- non-muxed a(18:16) upper-address path + buf := allocate(wmemory, 2 ** 19); + buf2 := allocate(rmemory, 2 ** 19); + for bit in 2 to 18 loop + addr_nat := 2 ** bit; + data := rd_pattern(addr_nat); + set_expected_word(wmemory, addr_nat, data); + fmc_write32_nb(net, to_addr(addr_nat), data); + write_word(rmemory, addr_nat, not data); + fmc_read32(net, to_addr(addr_nat), expected_data); + check_equal(expected_data, not data, + "read mismatch at address bit " & to_string(bit)); + end loop; + wait for 2 us; + check_expected_was_written(buf); + elsif run("back_to_back_writes") then + -- more writes than the transaction FIFO holds, with a slowed + -- AXI responder, so the dispatch-stall backpressure engages + buf := allocate(wmemory, 4096); + set_response_latency(net, axi_write_target, 500 ns); + for i in 0 to 31 loop + data := std_logic_vector(rand32); + set_expected_word(wmemory, i * 4, data); + fmc_write32_nb(net, to_addr(i * 4), data); + end loop; + fmc_wait_idle(net); + wait for 25 us; + check_expected_was_written(buf); + check_equal(unsigned(timeout_count), 0, + "backpressure must stall, not time out, at this latency"); + elsif run("back_to_back_reads") then + buf := allocate(rmemory, 4096); + for i in 0 to 31 loop + write_word(rmemory, i * 4, rd_pattern(i * 4)); + end loop; + for i in 0 to 31 loop + fmc_read32(net, to_addr(i * 4), data); + check_equal(data, rd_pattern(i * 4), + "b2b read mismatch at word " & to_string(i)); + end loop; + elsif run("slow_read_responder") then + -- long AXI read latency, but below the wait timeout: the SP + -- just stalls and then gets correct data + buf := allocate(rmemory, 64); + expected_data := X"0BAD_CAFE"; + write_word(rmemory, 16, expected_data); + set_response_latency(net, axi_read_target, 2 us); + fmc_read32(net, to_addr(16), data); + check_equal(data, expected_data, "slow read returned wrong data"); + check_equal(unsigned(timeout_count), 0, "no timeout expected"); + elsif run("read_timeout_recovery") then + -- AXI read latency far beyond the wait timeout: the SP gets + -- poison instead of a hung bus, and the stale response is + -- discarded before the next read + buf := allocate(rmemory, 64); + write_word(rmemory, 0, X"1111_2222"); + write_word(rmemory, 4, X"3333_4444"); + set_response_latency(net, axi_read_target, 50 us); + fmc_read32(net, to_addr(0), data); + check_equal(data, std_logic_vector'(X"FFFF_FFFF"), + "timed-out read must return poison"); + check_equal(unsigned(timeout_count), 1, "expected one timeout"); + -- let the stale response land and drain + wait for 60 us; + set_response_latency(net, axi_read_target, 0 ns); + fmc_read32(net, to_addr(4), data); + check_equal(data, std_logic_vector'(X"3333_4444"), + "post-timeout read must return fresh data"); + check_equal(unsigned(timeout_count), 1, "no further timeouts expected"); + elsif run("write_backpressure_timeout") then + -- wedge the write responder so the FIFOs fill and dispatch + -- stalls past the timeout: the overflowing writes are + -- swallowed rather than hanging the SP + buf := allocate(wmemory, 4096); + set_response_latency(net, axi_write_target, 20 us); + for i in 0 to 15 loop + data := std_logic_vector(rand32); + set_expected_word(wmemory, i * 4, data); + fmc_write32_nb(net, to_addr(i * 4), data); + end loop; + -- these overflow the queue; some will be swallowed by the + -- timeout, so no expectations are set on them + for i in 16 to 19 loop + data := std_logic_vector(rand32); + fmc_write32_nb(net, to_addr(i * 4), data); + end loop; + fmc_wait_idle(net); + check_true(unsigned(timeout_count) > 0, + "expected at least one swallowed write"); + set_response_latency(net, axi_write_target, 0 ns); + wait for 500 us; -- drain the wedged queue + check_expected_was_written(buf); + -- interface must still be alive + data := X"600D_600D"; + set_expected_word(wmemory, 16#100#, data); + fmc_write32(net, to_addr(16#100#), data); + wait for 2 us; + check_expected_was_written(buf); + elsif run("cs_abort_read_recovery") then + buf := allocate(rmemory, 64); + write_word(rmemory, 0, X"AAAA_BBBB"); + write_word(rmemory, 4, X"CCCC_DDDD"); + -- abort right after the address phase: the read is already in + -- flight and its data must be silently discarded + fmc_abort_next(net, sp_bus_handle, 0); + fmc_read32(net, to_addr(0), data); -- returned data is undefined + fmc_read32(net, to_addr(4), data); + check_equal(data, std_logic_vector'(X"CCCC_DDDD"), + "read after phase-0 abort must be clean"); + -- abort after one beat: the DUT finishes the word on its own + fmc_abort_next(net, sp_bus_handle, 1); + fmc_read32(net, to_addr(0), data); -- returned data is undefined + fmc_read32(net, to_addr(4), data); + check_equal(data, std_logic_vector'(X"CCCC_DDDD"), + "read after one-beat abort must be clean"); + check_equal(unsigned(timeout_count), 0, "aborts are not timeouts"); + elsif run("cs_abort_write_recovery") then + buf := allocate(wmemory, 64); + -- aborted writes complete with zero filler on the AXI side by + -- design; no expectations on them + data := X"1234_5678"; + fmc_abort_next(net, sp_bus_handle, 0); + fmc_write32(net, to_addr(0), data); + fmc_abort_next(net, sp_bus_handle, 1); + fmc_write32(net, to_addr(4), data); + -- interface must still work + data := X"8765_4321"; + set_expected_word(wmemory, 8, data); + fmc_write32(net, to_addr(8), data); + wait for 2 us; + check_expected_was_written(buf); + check_equal(unsigned(timeout_count), 0, "aborts are not timeouts"); + elsif run("reset_between_transactions") then + buf := allocate(wmemory, 64); + buf2 := allocate(rmemory, 64); + data := X"BEF0_4E00"; + set_expected_word(wmemory, 0, data); + fmc_write32(net, to_addr(0), data); + wait for 2 us; + check_expected_was_written(buf); + -- yank chip_reset between transactions and confirm the + -- interface comes back + reset <= force '1'; + wait for 300 ns; + reset <= release; + wait for 500 ns; + data := X"5EC0_4D00"; + set_expected_word(wmemory, 4, data); + fmc_write32(net, to_addr(4), data); + write_word(rmemory, 8, X"1357_2468"); + fmc_read32(net, to_addr(8), data); + check_equal(data, std_logic_vector'(X"1357_2468"), + "read after reset must work"); + wait for 2 us; + check_expected_was_written(buf); + elsif run("missed_start_timeout") then + -- Pin chip select with no address latch, emulating the FSM + -- having miscaptured a transaction start: the SP would be + -- stalled on its bus, and only the idle timeout can free it. + ne_pins <= force "1110"; + wait until nwait_pin = '1' for 30 us; + check_equal(nwait_pin, '1', + "wait must release after a missed-start timeout"); + wait for 100 ns; -- let the counter output settle + check_equal(unsigned(timeout_count), 1, "expected one timeout"); + ne_pins <= release; + wait for 2 us; + -- interface must still be alive + buf := allocate(wmemory, 64); + buf2 := allocate(rmemory, 64); + data := X"0DDB_A115"; + set_expected_word(wmemory, 0, data); + fmc_write32(net, to_addr(0), data); + write_word(rmemory, 4, X"BEA7_ED00"); + fmc_read32(net, to_addr(4), data); + check_equal(data, std_logic_vector'(X"BEA7_ED00"), + "read after missed-start recovery must work"); + wait for 2 us; + check_expected_was_written(buf); + check_equal(unsigned(timeout_count), 1, + "no further timeouts expected"); + elsif run("random_soak") then + soak(60); + elsif run("basic_write_read_66mhz") then + fmc_half_period <= force 7.5 ns; + wait for 100 ns; + basic_write_read_pair; + elsif run("basic_write_read_100mhz") then + fmc_half_period <= force 5 ns; + wait for 100 ns; + basic_write_read_pair; + elsif run("back_to_back_100mhz") then + fmc_half_period <= force 5 ns; + wait for 100 ns; + buf := allocate(wmemory, 4096); + buf2 := allocate(rmemory, 4096); + for i in 0 to 31 loop + data := std_logic_vector(rand32); + set_expected_word(wmemory, i * 4, data); + fmc_write32_nb(net, to_addr(i * 4), data); + end loop; + for i in 0 to 15 loop + write_word(rmemory, i * 4, rd_pattern(i * 4)); + fmc_read32(net, to_addr(i * 4), data); + check_equal(data, rd_pattern(i * 4), + "100mhz b2b read mismatch at word " & to_string(i)); + end loop; + fmc_wait_idle(net); + wait for 2 us; + check_expected_was_written(buf); + elsif run("random_soak_66mhz") then + fmc_half_period <= force 7.5 ns; + wait for 100 ns; + soak(60); + elsif run("random_soak_100mhz") then + fmc_half_period <= force 5 ns; + wait for 100 ns; + soak(60); end if; end loop; + -- every test finishes with a quiet bus and zero observed contention + check_equal(unsigned(contention_count), 0, + "DUT contention counter must be zero"); wait for 2 us; test_runner_cleanup(runner); wait; end process; - -- -- Example total test timeout dog - test_runner_watchdog(runner, 1 ms); + test_runner_watchdog(runner, 5 ms); end tb; diff --git a/hdl/ip/vhd/fmc_if/sims/fmc_th.vhd b/hdl/ip/vhd/fmc_if/sims/fmc_th.vhd index 6be6ddd8..c4d92737 100644 --- a/hdl/ip/vhd/fmc_if/sims/fmc_th.vhd +++ b/hdl/ip/vhd/fmc_if/sims/fmc_th.vhd @@ -19,8 +19,14 @@ end entity; architecture th of fmc_th is - signal clk : std_logic := '0'; - signal reset : std_logic := '1'; + -- fmc_clk and aclk are deliberately unrelated so the real CDC gets + -- exercised. fmc_half_period defaults to 50 MHz (the shipped SP CLKDIV) + -- and is forced by the testbench to 7.5 ns / 5 ns for the 66.67 and + -- 100 MHz ratio runs. + signal fmc_half_period : time := 10 ns; + signal fmc_clk : std_logic := '0'; + signal aclk : std_logic := '0'; + signal reset : std_logic := '1'; signal a : std_logic_vector(25 downto 16); signal ad : std_logic_vector(15 downto 0); @@ -30,31 +36,36 @@ architecture th of fmc_th is signal nl : std_logic; signal nwait : std_logic := '1'; - signal rdata : std_logic_vector(31 downto 0); - signal arid : std_logic_vector(3 downto 0) := std_logic_vector(to_unsigned(0, 4)); signal bid : std_logic_vector(3 downto 0); signal awid : std_logic_vector(3 downto 0) := std_logic_vector(to_unsigned(0, 4)); signal rid : std_logic_vector(3 downto 0); - signal data_out_tris : std_logic_vector(15 downto 0); - signal data_out_tris_en : std_logic; + signal data_out_tris : std_logic_vector(15 downto 0); + signal data_out_tris_hiz : std_logic_vector(15 downto 0); + + signal timeout_count : std_logic_vector(7 downto 0); + signal contention_count : std_logic_vector(7 downto 0); signal axi_if : axil_t; begin - -- set up a fastish, clock for the sim - -- env and release reset after a bit of time - clk <= not clk after 4 ns; + aclk <= not aclk after 4 ns; reset <= '0' after 200 ns; + fmc_clk_gen: process + begin + wait for fmc_half_period; + fmc_clk <= not fmc_clk; + end process; + -- sim infrastructure from VUnit axi_read_sim_infra: entity vunit_lib.axi_read_slave generic map ( axi_slave => axi_read_target ) port map ( - aclk => clk, + aclk => aclk, arvalid => axi_if.read_address.valid, arready => axi_if.read_address.ready, @@ -77,7 +88,7 @@ begin axi_slave => axi_write_target ) port map ( - aclk => clk, + aclk => aclk, awvalid => axi_if.write_address.valid, awready => axi_if.write_address.ready, awid => awid, @@ -103,7 +114,7 @@ begin bus_handle => SP_BUS_HANDLE ) port map ( - clk => clk, + clk => fmc_clk, a => a, ad => ad, ne => ne, @@ -113,26 +124,46 @@ begin nwait => nwait ); - ad <= data_out_tris when data_out_tris_en = '1' else (others => 'Z'); + ad <= (others => 'Z') when data_out_tris_hiz(0) = '1' else data_out_tris; + + -- Contention tripwire: the model owns the bus during the address phase + -- (NADV low) and write data beats (NWE low); the DUT driving then means + -- the two sides disagree about the transaction phase. Every test fails + -- fast on this instead of silently resolving the fight. + contention_check: process(fmc_clk) + begin + if rising_edge(fmc_clk) then + assert not (data_out_tris_hiz(0) = '0' and (nl = '0' or nwe = '0')) + report "BUS CONTENTION: DUT driving during SP address/write phase" + severity failure; + end if; + end process; dut: entity work.stm32h7_fmc_target + generic map ( + -- short enough to keep timeout tests quick, long enough that the + -- slow-responder (non-timeout) tests stay under it + timeout_cycles => 512 + ) port map ( -- Interface to the STM32H7's FMC periph - --! Write full flag, sync to write clock domain chip_reset => reset, - fmc_clk => clk, + fmc_clk => fmc_clk, + fmc_capture_clk => fmc_clk, a => a(24 downto 16), addr_data_in => ad, data_out => data_out_tris, - data_out_en => data_out_tris_en, + data_out_hiz => data_out_tris_hiz, ne => ne, - -- todo missing byte enables? noe => noe, nwe => nwe, nl => nl, nwait => nwait, + + timeout_count => timeout_count, + contention_count => contention_count, -- FPGA interface - aclk => clk, + aclk => aclk, aresetn => not reset, axi_if => axi_if diff --git a/hdl/ip/vhd/fmc_if/stm32h7_fmc_target.vhd b/hdl/ip/vhd/fmc_if/stm32h7_fmc_target.vhd index 90a42be0..0fc255f9 100644 --- a/hdl/ip/vhd/fmc_if/stm32h7_fmc_target.vhd +++ b/hdl/ip/vhd/fmc_if/stm32h7_fmc_target.vhd @@ -2,10 +2,28 @@ -- License, v. 2.0. If a copy of the MPL was not distributed with this -- file, You can obtain one at https://mozilla.org/MPL/2.0/. ---! This block provides an FMC target interface from the STM32H7's ---! local bus, crosses clock domains into the FPGA's core logic ---! clock domain, and issues AXI transactions. ---! figures 115 and 116 +-- This block provides an FMC target interface from the STM32H7's +-- local bus (synchronous multiplexed PSRAM mode, RM0433 figures 115/116), +-- crosses clock domains into the FPGA's core logic clock domain, and +-- issues AXI transactions. +-- +-- Pacing contract with the SP, proven on hardware and also encoded in the +-- simulation model: the wait line is held asserted +-- by default and released under FSM control; after the SP samples the wait +-- line released at rising edge N, its current data beat is valid on the +-- bus at edge N+1, and each further release-sampled edge advances one +-- beat. Reads therefore present data one full cycle before the release is +-- sampled, and the SP samples read data on the same edge it samples the +-- released wait. +-- +-- All FMC inputs are captured in dedicated per-pin registers clocked by +-- fmc_capture_clk before the FSM sees them, so the FSM runs one cycle +-- behind the bus. The interface is entirely NWAIT-paced, so that delay is +-- absorbed by the pacing (one extra stall cycle per transaction). On +-- boards with the FMC MMCM the capture clock is a later-phased sibling of +-- fmc_clk, which is what gives the input pins setup margin at 10 ns; +-- boards without an MMCM tie both clock ports to the same clock and get +-- identical cycle behavior. -- ES0491 FMC Errata: -- Dummy read cycles inserted when reading synchronous memories @@ -15,6 +33,8 @@ -- The extra data values read are not used by the FMC and there is no functional failure. -- Workaround -- None +-- (The dummy cycles land while this FSM is back in idle with the wait line +-- asserted and NADV high, so they cannot start a new transaction.) library ieee; use ieee.std_logic_1164.all; @@ -24,27 +44,51 @@ use work.stm32h7_fmc_target_pkg.all; use work.axil26x32_pkg.all; entity stm32h7_fmc_target is + generic ( + -- Bound on how long the SP's bus may be stalled by the wait line, in + -- fmc_clk cycles. An AHB stall is unrecoverable on the SP side, so a + -- wedged AXI responder is converted into poisoned read data (0xFFFF + -- beats) or a swallowed write, plus a bump of timeout_count, instead + -- of a hung SP. + timeout_cycles : positive := 4096; + -- Re-inserts the dead cycle between read beats (the pre-streaming + -- cadence), restoring a 2-cycle data-out window for a board that + -- cannot close single-cycle output timing. The data-out multicycle + -- constraints must match this setting. + extra_beat_setup : boolean := false + ); port ( -- Interface to the STM32H7's FMC periph chip_reset : in std_logic; - --! fmc_clk from STM32's clock generator + -- fmc_clk from STM32's clock generator (deskewed/phase-shifted on + -- boards with the FMC MMCM); clocks the FSM and all outputs fmc_clk : in std_logic; - --! non-multiplexed upper address bits from STM32 + -- clock for the input capture registers only. Tie to fmc_clk when + -- there is no MMCM; with one, the later-phased sibling output. + fmc_capture_clk : in std_logic; + -- non-multiplexed upper address bits from STM32 a : in std_logic_vector(24 downto 16); - --! multiplexed lower address bits/databits to/from STM32 + -- multiplexed lower address bits/databits to/from STM32 addr_data_in : in std_logic_vector(15 downto 0); data_out : out std_logic_vector(15 downto 0); - data_out_en : out std_logic; - --! active-low chip selects + -- Tristate control in OBUFT T polarity: '1' releases the pin, '0' + -- drives it. One bit per pin, all driven identically, registered with + -- no logic between flop and T input -- an active-high enable infers + -- an inverter LUT there, which adds ~1.5 ns and blocks IOB packing. + data_out_hiz : out std_logic_vector(15 downto 0); + -- active-low chip selects ne : in std_logic_vector(3 downto 0); - --! active-low output enable + -- active-low output enable noe : in std_logic; - --! active-low write enable + -- active-low write enable nwe : in std_logic; - --! active-low address latch for address phase + -- active-low address latch for address phase nl : in std_logic; - --! active-low pipelined wait to STM32, asserted 1 cycle before stall + -- active-low wait to STM32 nwait : out std_logic; + -- Saturating diagnostic counters, sticky until chip_reset. + timeout_count : out std_logic_vector(7 downto 0); + contention_count : out std_logic_vector(7 downto 0); -- FPGA interface aclk : in std_logic; aresetn : in std_logic; @@ -55,22 +99,37 @@ end entity; architecture rtl of stm32h7_fmc_target is + -- Driven-'1' bits pass, everything else ('0', and any undriven + -- 'Z'/'X'/'U' from a released bus) becomes '0'. Lets the final write + -- beat be captured unconditionally: qualifying it on chip select would + -- race NE's end-of-burst deassert, which is rising-edge timed and lands + -- within nanoseconds of the capture instant, differently per board. + function drive_or_zero ( + v : std_logic_vector + ) return std_logic_vector is + variable r : std_logic_vector(v'range); + begin + for i in v'range loop + r(i) := '1' when v(i) = '1' else '0'; + end loop; + return r; + end; + attribute mark_debug : string; type fmc_state_type is ( idle, - addr_delay, - addr_delay1, - read_setup, - read_word0_setup_delay, - read_word0, - read_word1_setup_delay, - read_word1, - write_setup, - write_wait_delay, - write_word0, - write_word1, - timeout_cleanup + dispatch, + rd_wait_data, + rd_arm, + rd_beat0, + rd_rearm, + rd_beat1, + wr_arm, + wr_arm2, + wr_beat0, + wr_beat1, + timeout_drain ); type axi_state_type is ( @@ -86,7 +145,6 @@ architecture rtl of stm32h7_fmc_target is attribute mark_debug of axi_state : signal is "TRUE"; signal txn : txn_type; attribute mark_debug of txn : signal is "TRUE"; - signal axi_fifo_rd_path_rdata : std_logic_vector(31 downto 0); signal axi_fifo_rd_path_rd_ack : std_logic; @@ -105,7 +163,50 @@ architecture rtl of stm32h7_fmc_target is attribute mark_debug of axi_addr : signal is "TRUE"; signal axi_fifo_wr_path_wdata : std_logic_vector(31 downto 0); signal axi_fifo_wr_path_write : std_logic; - signal txn_stored : boolean; + signal axi_fifo_wr_path_wfull : std_logic; + + -- One physical flop per pin, so each tristate T can pack into its + -- IOB; synthesis must not merge the identical registers. + signal data_out_hiz_int : std_logic_vector(15 downto 0); + -- IOB rather than keep: both prevent register merging, but keep maps + -- to a dont_touch that also blocks the pad packing it was meant to + -- enable. + attribute IOB : string; + attribute IOB of data_out_hiz_int : signal is "TRUE"; + -- fmc_clk cycles spent in a state that stalls the SP + signal timeout_cntr : natural range 0 to timeout_cycles; + signal timeouts : unsigned(7 downto 0); + attribute mark_debug of timeouts : signal is "TRUE"; + -- Both sides driving the muxed bus at once means the two ends disagree + -- about where in a transaction we are; the count is a tripwire for the + -- testbench and for ILA debug on hardware. + signal contentions : unsigned(7 downto 0); + attribute mark_debug of contentions : signal is "TRUE"; + -- Reads abandoned by timeout still complete on the AXI side eventually; + -- this many arriving read words belong to nobody and must be discarded + -- before data is served to a live read. + signal drop_pending : unsigned(4 downto 0); + + -- Per-pin input capture registers, clocked by fmc_capture_clk. IOB so + -- they land in the pad's ILOGIC flop: the input timing budget is pin -> + -- flop with nothing in between. + -- Deliberately unreset (initial values cover the pre-clock sim window): + -- they track the live bus whenever the clock runs, so by the time the + -- FSM leaves reset they already hold real pin state -- and a reset pin + -- on them would create cross-phase reset recovery paths with only the + -- inter-phase spacing to deassert in. + signal ne_q : std_logic_vector(3 downto 0) := (others => '1'); + signal noe_q : std_logic := '1'; + signal nwe_q : std_logic := '1'; + signal nl_q : std_logic := '1'; + signal a_q : std_logic_vector(24 downto 16) := (others => '0'); + signal ad_q : std_logic_vector(15 downto 0) := (others => '0'); + attribute IOB of ne_q : signal is "TRUE"; + attribute IOB of noe_q : signal is "TRUE"; + attribute IOB of nwe_q : signal is "TRUE"; + attribute IOB of nl_q : signal is "TRUE"; + attribute IOB of a_q : signal is "TRUE"; + attribute IOB of ad_q : signal is "TRUE"; alias awready is axi_if.write_address.ready; alias wready is axi_if.write_data.ready; @@ -123,8 +224,6 @@ architecture rtl of stm32h7_fmc_target is signal araddr : std_logic_vector(25 downto 0); signal rready : std_logic; - signal int_toggle : std_logic; - begin axi_if.write_address.valid <= awvalid; @@ -137,162 +236,281 @@ begin axi_if.read_address.addr <= araddr; axi_if.read_data.ready <= rready; + data_out_hiz <= data_out_hiz_int; + timeout_count <= std_logic_vector(timeouts); + contention_count <= std_logic_vector(contentions); + + -- Input capture stage: the only logic on fmc_capture_clk. + input_capture: process(fmc_capture_clk) + begin + if rising_edge(fmc_capture_clk) then + ne_q <= ne; + noe_q <= noe; + nwe_q <= nwe; + nl_q <= nl; + a_q <= a; + ad_q <= addr_data_in; + end if; + end process; + -- State machine dealing with fmc interface fmc_if_sm: process(fmc_clk, chip_reset) variable chip_selected : boolean; + -- The tristate control is re-derived from the live control pins + -- every cycle it could be driving, so a bus desync (SP deasserting + -- OE/CS under us) releases the bus within one cycle instead of + -- holding a fight until the FSM notices. hiz_v is in T polarity: + -- '1' = release. + variable hiz_v : std_logic; begin if chip_reset then - data_out <= (others => '0'); - data_out_en <= '0'; -- release bus - nwait <= '0'; - txn <= ('0', (others => '0')); + fmc_state <= idle; + data_out <= (others => '0'); + data_out_hiz_int <= (others => '1'); -- release bus + nwait <= '0'; + txn <= ('0', (others => '0')); axi_fifo_wr_path_wdata <= (others => '0'); axi_fifo_rd_path_rd_ack <= '0'; axi_fifo_txn_path_write <= '0'; axi_fifo_wr_path_write <= '0'; - txn_stored <= false; - int_toggle <= '0'; + timeout_cntr <= 0; + timeouts <= (others => '0'); + contentions <= (others => '0'); + drop_pending <= (others => '0'); elsif rising_edge(fmc_clk) then -- some variable naming for more legibility - chip_selected := ne(0) = '0'; + chip_selected := ne_q(0) = '0'; + hiz_v := noe_q or ne_q(0); -- single-cycle flags, unconditionally cleared axi_fifo_rd_path_rd_ack <= '0'; axi_fifo_txn_path_write <= '0'; - axi_fifo_wr_path_write <= '0'; + axi_fifo_wr_path_write <= '0'; + + -- The timeout counter runs in every situation that can stall the + -- SP indefinitely. That includes sitting in idle with chip + -- select asserted: a miscaptured NADV means we never saw the + -- transaction start, the SP is stalled on its bus waiting for a + -- wait release that will never come, and nothing but this + -- counter can free it. (Normal idle-with-CS dwell -- the address + -- cycle itself, ES0491 dummy reads -- lasts a handful of cycles, + -- nowhere near the timeout.) + if fmc_state = dispatch or fmc_state = rd_wait_data + or (fmc_state = idle and chip_selected) then + if timeout_cntr /= timeout_cycles then + timeout_cntr <= timeout_cntr + 1; + end if; + else + timeout_cntr <= 0; + end if; + + -- Contention tripwire: the SP drives the muxed bus during the + -- address phase (NADV low) and during write data beats (NWE low); + -- our enable being up in either is a protocol desync. + if data_out_hiz_int(0) = '0' and (nl_q = '0' or nwe_q = '0') then + if contentions /= x"FF" then + contentions <= contentions + 1; + end if; + end if; + case fmc_state is when idle => - nwait <= '0'; - data_out_en <= '0'; -- release bus - -- Look for a starting transition - -- ( chip sel and address latch) - if chip_selected and nl = '0' then + nwait <= '0'; + data_out_hiz_int <= (others => '1'); -- release bus + if drop_pending /= 0 and axi_fifo_rd_path_rempty = '0' + and axi_fifo_rd_path_rd_ack = '0' then + axi_fifo_rd_path_rd_ack <= '1'; + drop_pending <= drop_pending - 1; + elsif timeout_cntr = timeout_cycles then + -- chip select has been pinned with no transaction + -- start observed: we missed one. Free the SP's bus + -- and swallow whatever beats it clocks out. + if timeouts /= x"FF" then + timeouts <= timeouts + 1; + end if; + data_out <= x"FFFF"; + nwait <= '1'; + fmc_state <= timeout_drain; + elsif chip_selected and nl_q = '0' then -- Bus outputs right-shifted so we shift left here to -- recover byte addrs - txn.addr <= unsigned(a & addr_data_in & "0"); - txn.read_not_write <= nwe; - - fmc_state <= addr_delay; - end if; - when addr_delay => - -- We get here after latching the address - -- We'll delay for an additional cycle - -- so that the next cycle will be checking - fmc_state <= addr_delay1; - when addr_delay1 => - -- We need to immediately stall the bus at this point if we have a full txn fifo - -- other stall conditions will be checked in the read/write setup phase - -- since the conditions differ - if axi_fifo_txn_path_wfull then - nwait <= '0'; - end if; - if txn.read_not_write = '1' then - fmc_state <= read_setup; - -- For reads, we unconditionally wait here since we have to - -- do an axi transaction to even fetch the first data to return - -- which takes more than 1 cycle :) - nwait <= '0'; - else - fmc_state <= write_setup; + txn.addr <= unsigned(a_q & ad_q & "0"); + txn.read_not_write <= nwe_q; + fmc_state <= dispatch; end if; - when read_setup => - -- TODO: need a wait timeout mech here, we're potentially stalling - -- the SP's bus here! - - -- We need to issue this transaction 1x to the txn fifo + when dispatch => + -- Queue the transaction. Stalling here (the wait line is + -- still asserted from idle) is the back-pressure path + -- when the AXI side has fallen behind. if not chip_selected then fmc_state <= idle; - else - -- We're going to be doing a read here we must be waited already - -- We need to immediately stall the bus at this point if we have a full txn fifo - -- not that transactions are processed in order so any writes pending - -- will necessarily happen first. This is important since the writes could - -- have side-effects that affect the reads - if not txn_stored and axi_fifo_txn_path_wfull = '0' then - -- Store the transaction, set the stored flag so we don't - -- do it again while we wait - axi_fifo_txn_path_write <= '1'; - txn_stored <= true; + elsif timeout_cntr = timeout_cycles then + -- Nothing queued yet, so nothing is in flight: free + -- the SP and swallow/poison its beats. + if timeouts /= x"FF" then + timeouts <= timeouts + 1; end if; - -- Wait is held here until we've done the AXI transaction - -- to fetch the data and have the data back in the fifo - if not axi_fifo_rd_path_rempty then - -- Register the data - -- apply the data to the bus - -- take away the wait - data_out <= axi_fifo_rd_path_rdata(15 downto 0); - -- noe should always be active here, but this provides a safety net - -- in case there is a bus-desync of some kind, we don't want to cross-drive with - -- the sp - data_out_en <= '1' and (not noe); - fmc_state <= read_word0_setup_delay; + data_out <= x"FFFF"; + nwait <= '1'; + fmc_state <= timeout_drain; + elsif axi_fifo_txn_path_wfull = '0' and + (txn.read_not_write = '1' or axi_fifo_wr_path_wfull = '0') then + axi_fifo_txn_path_write <= '1'; + if txn.read_not_write then + -- Reads keep the SP waited: the data has to + -- round-trip the AXI side before anything can be + -- returned. + fmc_state <= rd_wait_data; + else + nwait <= '1'; + fmc_state <= wr_arm; end if; end if; - when read_word0_setup_delay => - nwait <= '1'; - fmc_state <= read_word0; - when read_word0 => - nwait <= '0'; - txn_stored <= false; - data_out <= axi_fifo_rd_path_rdata(31 downto 16); - if not chip_selected then + when rd_wait_data => + if axi_fifo_rd_path_rd_ack = '1' then + -- rempty/rdata are one cycle behind a pop; deciding + -- anything off them now would serve the word that was + -- just discarded + null; + elsif drop_pending /= 0 then + -- Stale words from earlier timed-out reads arrive + -- ahead of ours; discard them first. + if axi_fifo_rd_path_rempty = '0' then + axi_fifo_rd_path_rd_ack <= '1'; + drop_pending <= drop_pending - 1; + end if; + elsif not chip_selected then + -- The read was queued on entry to this state, so an + -- SP abort here still leaves it in flight and its + -- data must be discarded when it lands. + if drop_pending /= "11111" then + drop_pending <= drop_pending + 1; + end if; fmc_state <= idle; - data_out_en <= '0'; -- release bus - axi_fifo_rd_path_rd_ack <= '1'; -- clear the read word - else -- not done, move to next word - fmc_state <= read_word1_setup_delay; + elsif timeout_cntr = timeout_cycles then + -- The read is in flight; whenever its data lands it + -- belongs to nobody. + if drop_pending /= "11111" then + drop_pending <= drop_pending + 1; + end if; + if timeouts /= x"FF" then + timeouts <= timeouts + 1; + end if; + data_out <= x"FFFF"; + data_out_hiz_int <= (others => hiz_v); + nwait <= '1'; + fmc_state <= timeout_drain; + elsif axi_fifo_rd_path_rempty = '0' then + -- Present the first beat a full cycle before the wait + -- release can be sampled. + data_out <= axi_fifo_rd_path_rdata(15 downto 0); + data_out_hiz_int <= (others => hiz_v); + fmc_state <= rd_arm; end if; - when read_word1_setup_delay => - nwait <= '1'; - fmc_state <= read_word1; - when read_word1 => - -- TODO: if we want to allow shorter transactions - -- we'd need to do the right thing here, which would - -- be termingating early, and doing the read anyway. - -- we can only do 32bit wide reads on the AXI side - -- so care should be excersized by the user if there - -- are read side-effects on the addresses next to this - -- read since we'd be doing a 32bit axi read in this case - -- and dropping the latter part on the floor since it wasn't - -- requested. - - -- normal case: pop the rdata fifo since we're done with it + when rd_arm => + if not chip_selected then + axi_fifo_rd_path_rd_ack <= '1'; + data_out_hiz_int <= (others => '1'); + fmc_state <= idle; + else + nwait <= '1'; + data_out_hiz_int <= (others => hiz_v); + fmc_state <= rd_beat0; + end if; + when rd_beat0 => + -- SP samples word0 at this edge. + if not chip_selected then + -- Shortened read: the full 32-bit AXI read already + -- happened, so any read side effects have occurred; + -- all we can do is clean up. + axi_fifo_rd_path_rd_ack <= '1'; + data_out_hiz_int <= (others => '1'); + nwait <= '0'; + fmc_state <= idle; + else + data_out <= axi_fifo_rd_path_rdata(31 downto 16); + data_out_hiz_int <= (others => hiz_v); + if extra_beat_setup then + nwait <= '0'; + fmc_state <= rd_rearm; + else + -- wait stays released; word1 is sampled on the + -- very next edge (this word0->word1 transition is + -- the one single-cycle data-out path). + fmc_state <= rd_beat1; + end if; + end if; + when rd_rearm => + -- extra_beat_setup only: one dead cycle re-opens the + -- 2-cycle data-out window of the pre-streaming cadence. + nwait <= '1'; + data_out_hiz_int <= (others => hiz_v); + fmc_state <= rd_beat1; + when rd_beat1 => + -- SP samples word1 at this edge; done with the word. axi_fifo_rd_path_rd_ack <= '1'; - fmc_state <= idle; nwait <= '0'; - data_out_en <= '0'; -- release bus - when write_setup => + data_out_hiz_int <= (others => '1'); -- release bus + fmc_state <= idle; + when wr_arm => + -- The SP samples the released wait at this edge and + -- launches beat0 on the following falling edge. if not chip_selected then - fmc_state <= idle; + -- The transaction is already queued; complete it with + -- zeros rather than leaving the AXI side wedged + -- waiting for write data that will never come. + axi_fifo_wr_path_wdata <= (others => '0'); + axi_fifo_wr_path_write <= '1'; + nwait <= '0'; + fmc_state <= idle; else - if not txn_stored and axi_fifo_txn_path_wfull = '0' then - -- Store the transaction, set the stored flag so we don't - -- do it again while we wait - axi_fifo_txn_path_write <= '1'; - txn_stored <= true; - nwait <= '1'; - -- no waits needed until the fifo fills up - fmc_state <= write_wait_delay; - end if; + fmc_state <= wr_arm2; + end if; + when wr_arm2 => + -- beat0 is on the bus this cycle; it lands in ad_q for + -- wr_beat0 to consume on the next edge + if not chip_selected then + axi_fifo_wr_path_wdata <= (others => '0'); + axi_fifo_wr_path_write <= '1'; + nwait <= '0'; + fmc_state <= idle; + else + fmc_state <= wr_beat0; + end if; + when wr_beat0 => + if not chip_selected then + axi_fifo_wr_path_wdata <= (others => '0'); + axi_fifo_wr_path_write <= '1'; + nwait <= '0'; + fmc_state <= idle; + else + axi_fifo_wr_path_wdata(15 downto 0) <= ad_q; + fmc_state <= wr_beat1; end if; - when write_wait_delay => - fmc_state <= write_word0; - when write_word0 => - axi_fifo_wr_path_wdata(15 downto 0) <= addr_data_in; - txn_stored <= false; - fmc_state <= write_word1; - when write_word1 => - axi_fifo_wr_path_wdata(31 downto 16) <= addr_data_in; + when wr_beat1 => + -- Unconditional: reaching this state means the SP + -- committed the burst, and its data-hold contract covers + -- this capture even as NE deasserts. An aborted burst + -- leaves the bus released, which drive_or_zero turns + -- into zero filler rather than 'Z'/'X' in the AXI data. + axi_fifo_wr_path_wdata(31 downto 16) <= drive_or_zero(ad_q); axi_fifo_wr_path_write <= '1'; - nwait <= '0'; + nwait <= '0'; fmc_state <= idle; - -- We need to immediately stall the bus at this point if we have a full txn fifo - -- other stall conditions will be checked in the write setup phase - -- since the conditions differ - if axi_fifo_txn_path_wfull then + when timeout_drain => + -- Wait is released and poison is on the bus for read + -- phases; let the SP clock through its remaining beats + -- without capturing anything, then return to idle once + -- it deselects. Drive purely off the live OE/CS pins: + -- during write beats NOE is high so this never drives + -- against the SP, and the recorded transaction type may + -- be stale when the drain was entered from idle. + data_out_hiz_int <= (others => hiz_v); + if not chip_selected then + nwait <= '0'; + data_out_hiz_int <= (others => '1'); + fmc_state <= idle; end if; - when timeout_cleanup => - null; end case; end if; end process; @@ -362,7 +580,7 @@ begin reset => chip_reset, write_en => axi_fifo_wr_path_write, wdata => axi_fifo_wr_path_wdata, - wfull => open, + wfull => axi_fifo_wr_path_wfull, wusedwds => open, -- Read interface rclk => aclk, @@ -414,15 +632,16 @@ begin arvalid <= '0'; end if; when axi_write_init => - awvalid <= '1'; + -- AW and W are raised together, and only once the write + -- data has crossed the FIFO: raising AW alone lets a + -- downstream decoder start a write it then has to stall + -- on, and this block's own W data pops on the handshake. if not axi_fifo_wr_path_rempty then + awvalid <= '1'; wvalid <= '1'; + bready <= '1'; axi_state <= axi_write_wait; end if; - if awready and awvalid then - awvalid <= '0'; - end if; - bready <= '1'; when axi_write_wait => if awready and awvalid then awvalid <= '0'; @@ -432,7 +651,7 @@ begin end if; if bvalid then axi_state <= idle; - bready <= '0'; + bready <= '0'; end if; end case; end if; diff --git a/hdl/projects/cosmo_seq/black_box_entities/fmc_pll.vhd b/hdl/projects/cosmo_seq/black_box_entities/fmc_pll.vhd new file mode 100644 index 00000000..032aebe3 --- /dev/null +++ b/hdl/projects/cosmo_seq/black_box_entities/fmc_pll.vhd @@ -0,0 +1,23 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +-- A no synth, no sim, black entity to make analysis happy. +-- Generated by xilinx_ip_gen/fmc_pll_ip.tcl (clk_wiz in phase-alignment +-- mode on the SP's FMC clock). + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.numeric_std_unsigned.all; + +entity fmc_pll is + port ( + clk_fmc_in : in std_logic; + clk_fmc : out std_logic; + clk_fmc_capture : out std_logic; + reset : in std_logic; + locked : out std_logic +); + +end entity; diff --git a/hdl/projects/cosmo_seq/board_support/board_support_top.vhd b/hdl/projects/cosmo_seq/board_support/board_support_top.vhd index 0e13a17f..c865b0fa 100644 --- a/hdl/projects/cosmo_seq/board_support/board_support_top.vhd +++ b/hdl/projects/cosmo_seq/board_support/board_support_top.vhd @@ -21,6 +21,11 @@ entity board_support is reset_125m : out std_logic; clk_200m : out std_logic; reset_200m : out std_logic; + -- deskewed/phase-shifted FMC clock from the FMC MMCM; everything in + -- the FMC domain must use this, not the raw pin + fmc_clk_buf : out std_logic; + -- later-phased sibling for the FMC input capture registers only + fmc_capture_clk_buf : out std_logic; reset_fmc : out std_logic; -- misc board signals fpga1_status_led : out std_logic; @@ -38,6 +43,10 @@ architecture rtl of board_support is signal sp_system_reset_syncd : std_logic; signal pll_locked_async : std_logic; signal led_counter : unsigned(27 downto 0); + signal fmc_clk_g : std_logic; + signal fmc_capture_clk_g : std_logic; + signal fmc_mmcm_locked : std_logic; + signal fmc_mmcm_reset : std_logic; begin @@ -76,15 +85,46 @@ begin ); - -- Reset synchronizer into the clock domains + -- MMCM on the SP's (continuous) FMC clock: BUFG-in-feedback deskew plus + -- a small phase shift, which is what closes the single-cycle FMC pin + -- timing at 10 ns. See xilinx_ip_gen/fmc_pll_ip.tcl for the VCO and + -- phase reasoning. + fmc_pll_inst: entity work.fmc_pll + port map( + clk_fmc_in => sp_fmc_clk, + clk_fmc => fmc_clk_g, + clk_fmc_capture => fmc_capture_clk_g, + reset => fmc_mmcm_reset, + locked => fmc_mmcm_locked + ); + + -- Hold the MMCM in reset while the SP's clock is stopped (SP reset or + -- reconfiguration) and retry the lock if the input frequency changes. + fmc_clk_monitor_inst: entity work.fmc_clk_monitor + port map( + clk => clk_125m, + reset => reset_125m, + fmc_clk_raw => sp_fmc_clk, + mmcm_locked => fmc_mmcm_locked, + mmcm_reset => fmc_mmcm_reset + ); + + fmc_clk_buf <= fmc_clk_g; + fmc_capture_clk_buf <= fmc_capture_clk_g; + + -- Reset synchronizer into the clock domains. The FMC branch is clocked + -- by the MMCM output and additionally gated on MMCM lock: while + -- unlocked there are no FMC-domain clock edges and the async assert is + -- what keeps the FMC target's bus drive released. reset_sync_inst: entity work.reset_sync port map( pll_locked_async => pll_locked_async, + aux_locked_async => fmc_mmcm_locked, clk_125m => clk_125m, reset_125m => reset_125m, clk_200m => clk_200m, reset_200m => reset_200m, - sp_fmc_clk => sp_fmc_clk, + sp_fmc_clk => fmc_clk_g, reset_fmc_clk => reset_fmc ); diff --git a/hdl/projects/cosmo_seq/board_support/fmc_clk_monitor.vhd b/hdl/projects/cosmo_seq/board_support/fmc_clk_monitor.vhd new file mode 100644 index 00000000..5a99b062 --- /dev/null +++ b/hdl/projects/cosmo_seq/board_support/fmc_clk_monitor.vhd @@ -0,0 +1,124 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +-- Supervises the MMCM that sits on the SP's continuous FMC clock. +-- +-- The SP's clock stops whenever the SP resets or reconfigures, and an +-- MMCME2 whose input clock went away is not guaranteed to relock on its +-- own when the clock returns -- AMD's guidance is to assert its reset +-- across a clock interruption. This block watches the raw clock pin as +-- ordinary data in the always-running 125 MHz domain and holds the MMCM +-- in reset while the clock is absent, releasing it (and letting it +-- relock) once the clock is back. While the MMCM is unlocked the FMC +-- domain has no clock and reset_fmc is held asserted (see reset_sync), +-- so the wait line sits asserted and the SP's first post-reset access +-- simply stalls until lock -- nothing needs to sequence against this. +-- +-- The transition count is aliased (a 100 MHz toggle sampled at 125 MHz) +-- so it is a presence detector, not a frequency measurement: an absent +-- clock counts zero transitions, any live SP rate counts hundreds per +-- window. Frequency changes (the CLKDIV flip) drop the MMCM's own LOCKED +-- while the clock keeps toggling; the relock timer covers that case by +-- pulsing reset if LOCKED stays low too long with a clock present. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +entity fmc_clk_monitor is + generic ( + -- observation window, in clk cycles (256 @ 125 MHz ~= 2 us) + window_cycles : positive := 256; + -- fewer transitions than this in a window means "clock absent" + min_transitions : positive := 16; + -- windows of LOCKED low with a live clock before a relock retry + -- (128 windows ~= 260 us, comfortably past the ~100 us max lock time) + relock_windows : positive := 128; + -- windows of live clock required before releasing mmcm_reset + settle_windows : positive := 4 + ); + port ( + clk : in std_logic; + reset : in std_logic; + -- the FMC clock pin, sampled as data + fmc_clk_raw : in std_logic; + -- LOCKED from the MMCM, async + mmcm_locked : in std_logic; + mmcm_reset : out std_logic + ); +end entity; + +architecture rtl of fmc_clk_monitor is + + signal raw_meta : std_logic_vector(1 downto 0); + signal raw_prev : std_logic; + signal locked_meta : std_logic_vector(1 downto 0); + + signal window_cntr : natural range 0 to window_cycles - 1; + signal transition_cntr : natural range 0 to window_cycles; + signal clk_present : boolean; + + signal settle_cntr : natural range 0 to settle_windows; + signal unlock_cntr : natural range 0 to relock_windows; + +begin + + monitor: process(clk, reset) + begin + if reset then + raw_meta <= (others => '0'); + raw_prev <= '0'; + locked_meta <= (others => '0'); + window_cntr <= 0; + transition_cntr <= 0; + clk_present <= false; + settle_cntr <= 0; + unlock_cntr <= 0; + mmcm_reset <= '1'; + elsif rising_edge(clk) then + raw_meta <= raw_meta(0) & fmc_clk_raw; + raw_prev <= raw_meta(1); + locked_meta <= locked_meta(0) & mmcm_locked; + + if window_cntr = window_cycles - 1 then + window_cntr <= 0; + clk_present <= transition_cntr >= min_transitions; + transition_cntr <= 0; + + if transition_cntr < min_transitions then + -- clock gone: hold the MMCM in reset and start over + mmcm_reset <= '1'; + settle_cntr <= 0; + unlock_cntr <= 0; + elsif settle_cntr /= settle_windows then + -- clock is back; give it a few clean windows before + -- releasing the MMCM + settle_cntr <= settle_cntr + 1; + unlock_cntr <= 0; + elsif mmcm_reset = '1' then + mmcm_reset <= '0'; + elsif locked_meta(1) = '0' then + -- clock alive but no lock (e.g. the input frequency + -- changed): retry after a generous wait + if unlock_cntr = relock_windows then + mmcm_reset <= '1'; + settle_cntr <= 0; + unlock_cntr <= 0; + else + unlock_cntr <= unlock_cntr + 1; + end if; + else + unlock_cntr <= 0; + end if; + else + window_cntr <= window_cntr + 1; + if (raw_meta(1) xor raw_prev) = '1' + and transition_cntr /= window_cycles then + transition_cntr <= transition_cntr + 1; + end if; + end if; + end if; + end process; + +end architecture; diff --git a/hdl/projects/cosmo_seq/board_support/reset_sync.vhd b/hdl/projects/cosmo_seq/board_support/reset_sync.vhd index d1d244da..043f9c29 100644 --- a/hdl/projects/cosmo_seq/board_support/reset_sync.vhd +++ b/hdl/projects/cosmo_seq/board_support/reset_sync.vhd @@ -10,6 +10,13 @@ use ieee.numeric_std.all; entity reset_sync is port ( pll_locked_async : in std_logic; + -- Additional lock qualifier for the FMC domain (the FMC MMCM on + -- cosmo). Defaults to "locked" so boards without one (grapefruit) + -- are unaffected. Asserting reset on lock loss happens + -- asynchronously in the bridge, which matters: when this MMCM + -- unlocks the FMC domain has no clock edges, and the async assert + -- is what still clears the FMC target's bus drive. + aux_locked_async : in std_logic := '1'; clk_125m : in std_logic; reset_125m : out std_logic; @@ -50,7 +57,7 @@ begin ) port map( clk => sp_fmc_clk, - reset_async => pll_locked_async, + reset_async => pll_locked_async and aux_locked_async, reset_sync => reset_fmc_clk ); diff --git a/hdl/projects/cosmo_seq/cosmo_seq_top.vhd b/hdl/projects/cosmo_seq/cosmo_seq_top.vhd index 94b8fbb4..42240d3b 100644 --- a/hdl/projects/cosmo_seq/cosmo_seq_top.vhd +++ b/hdl/projects/cosmo_seq/cosmo_seq_top.vhd @@ -335,6 +335,10 @@ architecture rtl of cosmo_seq_top is signal v1p2_nic_enet_a0hp_pg : std_logic; signal v1p1_nic_enet_a0hp_pg : std_logic; alias fmc_clk : std_logic is fmc_sp_to_fpga1_clk; + -- deskewed/phase-shifted FMC clock from the MMCM in board_support; the + -- FMC domain runs on this, never on the raw pin + signal fmc_clk_buf : std_logic; + signal fmc_capture_clk_buf : std_logic; constant INFO_RESP_IDX : integer := 0; constant SPINOR_RESP_IDX: integer := 1; constant SEQ_RESP_IDX: integer := 2; @@ -364,7 +368,7 @@ architecture rtl of cosmo_seq_top is signal responders_8b : axil8x32_pkg.axil_array_t(config_array'range); signal responders_15b : axil15x32_pkg.axil_array_t(config_array'range); signal fmc_internal_data_out : std_logic_vector(15 downto 0); - signal fmc_data_out_enable: std_logic; + signal fmc_data_out_hiz: std_logic_vector(15 downto 0); signal spinor_io_o : std_logic_vector(3 downto 0); signal spinor_io_oe : std_logic_vector(3 downto 0); @@ -470,25 +474,32 @@ begin stm32h7_fmc_target_inst: entity work.stm32h7_fmc_target port map( chip_reset => reset_fmc, - fmc_clk => fmc_clk, + fmc_clk => fmc_clk_buf, + fmc_capture_clk => fmc_capture_clk_buf, a(24 downto 20) => "00000", a(19 downto 16) => fmc_sp_to_fpga1_a(19 downto 16), --a(23 downto 16) => fmc_sp_to_fpga1_a, addr_data_in => fmc_sp_to_fpga1_da, data_out => fmc_internal_data_out, - data_out_en => fmc_data_out_enable, + data_out_hiz => fmc_data_out_hiz, ne(3 downto 1) => "111", ne(0) => fmc_sp_to_fpga1_cs_l, noe => fmc_sp_to_fpga1_oe_l, nwe => fmc_sp_to_fpga1_we_l, nl => fmc_sp_to_fpga1_adv_l, nwait => fmc_sp_to_fpga1_wait_l, + timeout_count => open, + contention_count => open, aclk => clk_125m, aresetn => not reset_125m, axi_if => fmc_axi_if ); -- tristate control for the FMC data bus - fmc_sp_to_fpga1_da <= fmc_internal_data_out when fmc_data_out_enable = '1' else (others => 'Z'); + -- per-bit tristate, hiz already in OBUFT T polarity so each pin's T + -- flop packs into its IOB with no inverter in between + fmc_da_tris: for i in fmc_sp_to_fpga1_da'range generate + fmc_sp_to_fpga1_da(i) <= 'Z' when fmc_data_out_hiz(i) = '1' else fmc_internal_data_out(i); + end generate; -- Axi decode/interconnect axil_interconnect_inst: entity work.axil_interconnect @@ -510,6 +521,8 @@ begin port map( board_50mhz_clk => clk_50mhz_fpga1_1, sp_fmc_clk => fmc_clk, + fmc_clk_buf => fmc_clk_buf, + fmc_capture_clk_buf => fmc_capture_clk_buf, sp_system_reset_l => sp_to_fpga1_system_reset_l, clk_125m => clk_125m, reset_125m => reset_125m, diff --git a/hdl/projects/cosmo_seq/cosmo_timing.xdc b/hdl/projects/cosmo_seq/cosmo_timing.xdc index 8dae48f1..04dd36d1 100644 --- a/hdl/projects/cosmo_seq/cosmo_timing.xdc +++ b/hdl/projects/cosmo_seq/cosmo_timing.xdc @@ -1,20 +1,27 @@ create_clock -add -name sys_clk_pin -period 20.000 -waveform {0 10.000} [get_ports { clk_50mhz_fpga1_1 }]; -create_clock -add -name fmc_clk_pin -period 15.000 -waveform {0 7.500} [get_ports { fmc_sp_to_fpga1_clk }]; +create_clock -add -name fmc_clk_pin -period 10.000 -waveform {0 5.000} [get_ports { fmc_sp_to_fpga1_clk }]; # # FMC interface constraints # Create a virtual clock, to represent the source clock of the FMC interface -create_clock -name fmc_virt_clk -period 15.000; +create_clock -name fmc_virt_clk -period 10.000; -set_clock_groups -asynchronous -group {fmc_clk_pin fmc_virt_clk} -group {clk_125m_cosmo_pll} -group {clk_200m_cosmo_pll} +# The FMC MMCM's output clock is derived from fmc_clk_pin and must stay in +# the synchronous group with it and the virtual clock. +set_clock_groups -asynchronous -group [get_clocks -include_generated_clocks {fmc_clk_pin fmc_virt_clk}] -group {clk_125m_cosmo_pll} -group {clk_200m_cosmo_pll} # ####################### # FMC Interface # ####################### -# SP output a continuous clock here. -# The FMC interface is clocked at 66.67MHz, which is a 15ns period. +# SP outputs a continuous clock here. +# The FMC interface is constrained at 100MHz (10ns period), the CLKDIV=1 +# target rate; the same analysis is a strict superset of 50 and 66.67 MHz +# operation. The internal FMC domain clock comes from an MMCM in +# phase-alignment mode (see xilinx_ip_gen/fmc_pll_ip.tcl), so the flops +# see the pin clock plus the deliberate +45deg phase shift and STA +# accounts for it via the generated clock. # FPGA's input delays have to be low enough that they don't run into the uncertainty region due to any possible skew. # skew_bre is the shortest trace delay vs the clock, and skew_are is the longest trace delay vs the clock. # On cosmo, clock trace is 60.787 rev1, 53.026mm rev2 . @@ -57,17 +64,21 @@ set min_wait_delay 0.3635 # Source sync so external_clk_delay is 0. # Setup time is 1ns, and we include the 1/2 period due to SP shifting the data out on the falling edge. -set sp_output_half_period 7.5 +set sp_output_half_period 5.0 set sp_0_hold 0 set sp_clk_delay 0 -# We have our 1/2 period of 7.5 ns due to SP outputting on falling edges, plus the td in the datasheet +# We have our 1/2 period of 5 ns due to SP outputting on falling edges, plus the td in the datasheet set td_clkl_nehl 1 set nl_output_delay [expr {$sp_output_half_period + $td_clkl_nehl}] set max_nl [expr {$sp_clk_delay + $nl_output_delay + $max_data_delay - $min_clock_delay}] -# latest clock, earliest data. We assume a hold time of 0 for the SP. +# latest clock, earliest data. We assume a hold time of 0 for the SP, but the SP +# holds its outputs until the *next falling edge*, so the earliest change is a +# half period after the capture edge. Without that term Vivado assumes the data +# can change at the capture edge itself, which manufactures a phantom +# input-hold requirement of nearly a half period. # min external: fastest data, slowest clock -set min_nl [expr {$sp_clk_delay + $sp_0_hold + $min_data_delay - $max_clock_delay}] +set min_nl [expr {$sp_clk_delay + $sp_output_half_period + $sp_0_hold + $min_data_delay - $max_clock_delay}] # Apply to all of these pins with similar or better timing relationships. set_input_delay -clock fmc_virt_clk -max $max_nl [get_ports fmc_sp_to_fpga1_cs_l] @@ -85,8 +96,8 @@ set_input_delay -clock fmc_virt_clk -min $min_nl [get_ports fmc_sp_to_fpga1_bl_l set td_clkl_av 2.5 set a_output_delay [expr {$sp_output_half_period + $td_clkl_av}] set max_a [expr {$sp_clk_delay + $a_output_delay + $max_data_delay - $min_clock_delay}] -# Still 0 hold on these pins. -set min_a [expr {$sp_clk_delay + $sp_0_hold + $min_data_delay - $max_clock_delay}] +# Still 0 hold on these pins, held to the next falling edge as above. +set min_a [expr {$sp_clk_delay + $sp_output_half_period + $sp_0_hold + $min_data_delay - $max_clock_delay}] set_input_delay -clock fmc_virt_clk -max $max_a [get_ports fmc_sp_to_fpga1_a[*]] set_input_delay -clock fmc_virt_clk -min $min_a [get_ports fmc_sp_to_fpga1_a[*]] @@ -94,11 +105,23 @@ set_input_delay -clock fmc_virt_clk -min $min_a [get_ports fmc_sp_to_fpga1_a[*]] set td_clkl_adv 3 set ad_output_delay [expr {$sp_output_half_period + $td_clkl_adv}] set max_ad [expr {$sp_clk_delay + $ad_output_delay + $max_data_delay - $min_clock_delay}] -# Still 0 hold on these pins. -set min_ad [expr {$sp_clk_delay + $sp_0_hold + $min_data_delay - $max_clock_delay}] +# Still 0 hold on these pins, held to the next falling edge as above. +set min_ad [expr {$sp_clk_delay + $sp_output_half_period + $sp_0_hold + $min_data_delay - $max_clock_delay}] set_input_delay -clock fmc_virt_clk -max $max_ad [get_ports fmc_sp_to_fpga1_da[*]] set_input_delay -clock fmc_virt_clk -min $min_ad [get_ports fmc_sp_to_fpga1_da[*]] +# The MMCM's +45deg phase shift puts the internal capture edge at 1.25 ns, +# and STA's default edge relationship then times input paths from the +# virtual-clock launch at 0 to that 1.25 ns edge -- a nonsense 1.25 ns +# requirement. The intended capture edge is the *next* shifted edge at +# 11.25 ns, which is exactly what setup-2 selects. The default hold +# relationship (one cycle before the setup edge, back at 1.25 ns) is the +# right check and passes with the half-period of real SP hold, so no -hold +# adjustment. Output paths need nothing: launch at 1.25 ns against the SP's +# capture at 10 ns is already the correct single-cycle relationship. +set_multicycle_path 2 -setup -from [get_clocks fmc_virt_clk] -to [get_clocks *fmc_pll*] +set_multicycle_path 2 -setup -from [get_clocks fmc_clk_pin] -to [get_clocks *fmc_pll*] + #### END Of inputs # ################# @@ -129,12 +152,17 @@ set_output_delay -clock fmc_virt_clk -max $max_da [get_ports fmc_sp_to_fpga1_da[ set_output_delay -clock fmc_virt_clk -min $min_da [get_ports fmc_sp_to_fpga1_da[*]] -# assuming wait_l works, we have multiple cycles to get the data out. This is likely needed due to the tri-state stuff here -# and it has trouble meeting timing without the additional cycles. The fpga design compensates for this with wait_l. -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out*/C}] -to [get_ports {fmc_sp_to_fpga1_da[*]}] -setup 2 -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out*/C}] -to [get_ports {fmc_sp_to_fpga1_da[*]}] -hold 1 -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out_en_reg*/C}] -to [get_ports {fmc_sp_to_fpga1_da[*]}] -setup 2 -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out_en_reg*/C}] -to [get_ports {fmc_sp_to_fpga1_da[*]}] -hold 1 +# The streaming FSM presents read beats on consecutive cycles, so the +# word0->word1 transition is a true single-cycle path and no multicycle +# exception applies to the data pins. Single-cycle closes because the output +# and tristate flops pack into the IOBs: data_out_reg and data_out_hiz_int_reg +# are one flop per pin by construction, and nwait, a single-cycle path the SP +# samples every rising edge, gets the same treatment. If a board cannot close +# this way, set the extra_beat_setup generic and add setup-2/hold-1 exceptions +# on the data/tristate paths to match. +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/data_out_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/data_out_hiz_int_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/nwait_reg}] # End FMC diff --git a/hdl/projects/cosmo_seq/xilinx_ip_gen/fmc_pll_ip.tcl b/hdl/projects/cosmo_seq/xilinx_ip_gen/fmc_pll_ip.tcl new file mode 100644 index 00000000..c1a81044 --- /dev/null +++ b/hdl/projects/cosmo_seq/xilinx_ip_gen/fmc_pll_ip.tcl @@ -0,0 +1,58 @@ +# MMCM on the SP's FMC clock, in phase-alignment (deskew) mode: the feedback +# path through a BUFG zeroes out the clock insertion delay so the FMC-domain +# flops launch and capture aligned to the clock at the pin (plus the phase +# shift below). This is what closes the single-cycle NWAIT and data-out paths +# at 10 ns; a plain IBUF+BUFG eats 3.5-6.5 ns of insertion delay across PVT +# and cannot. +# +# VCO choice (M=12, D=1, O=12): VCO = 12 x f_in, so 600 MHz at a 50 MHz input +# and 1200 MHz at 100 MHz -- exactly the -1 speed grade MMCM limits per +# DS189, and the only M that spans both. One bitstream therefore locks at +# either SP CLKDIV setting, which is what lets this bitstream ship before the +# hubris CLKDIV flip. If a Vivado DRC ever rejects the edge-of-range VCO, +# drop the dual-frequency property and coordinate the rollout as a single +# archive bump instead (the hubris archive carries the bitstream, so the pair +# still moves atomically). +# +# Two output phases: +# - clk_fmc (+45 deg = +1.25 ns at 10 ns) clocks the FSM and the output +# flops. Its phase is hold-limited on the NWAIT/data pins (going earlier +# breaks output hold), so it cannot be pushed later for input margin. +# - clk_fmc_capture (+135 deg = +3.75 ns) clocks only the dedicated input +# capture registers. The late phase is what gives the input paths setup +# margin against STA's uncredited clock-network corner spread; input hold +# still has a half period of real SP hold behind it. The FSM consumes the +# captured values a cycle later, which the NWAIT pacing absorbs. +# Sweep either phase in the lab under the fmc_sweep soak to confirm real +# margin exceeds STA margin; the grid is 45/CLKOUT_DIVIDE = 3.75 deg per +# MMCM tap, so +/-5 taps = +/-18.75 deg = ~0.52 ns. Phases are fractions of +# the period, so every hold-side margin only grows at 50 MHz operation. +# +# OVERRIDE_MMCM is required: without it clk_wiz silently recalculates M +# (it picked M=11, whose 550 MHz VCO at a 50 MHz input cannot lock, killing +# the dual-frequency property). +create_ip -name clk_wiz -vendor xilinx.com -library ip -version 6.0 -module_name fmc_pll +set_property -dict [list \ + CONFIG.Component_Name {fmc_pll} \ + CONFIG.PRIM_IN_FREQ {100.000} \ + CONFIG.PRIMARY_PORT {clk_fmc_in} \ + CONFIG.CLK_OUT1_PORT {clk_fmc} \ + CONFIG.CLKOUT1_REQUESTED_OUT_FREQ {100.000} \ + CONFIG.CLKOUT1_REQUESTED_PHASE {45.000} \ + CONFIG.CLKOUT2_USED {true} \ + CONFIG.CLK_OUT2_PORT {clk_fmc_capture} \ + CONFIG.CLKOUT2_REQUESTED_OUT_FREQ {100.000} \ + CONFIG.CLKOUT2_REQUESTED_PHASE {135.000} \ + CONFIG.USE_PHASE_ALIGNMENT {true} \ + CONFIG.JITTER_SEL {Min_O_Jitter} \ + CONFIG.OVERRIDE_MMCM {true} \ + CONFIG.MMCM_DIVCLK_DIVIDE {1} \ + CONFIG.MMCM_CLKFBOUT_MULT_F {12.000} \ + CONFIG.MMCM_CLKIN1_PERIOD {10.000} \ + CONFIG.MMCM_CLKOUT0_DIVIDE_F {12.000} \ + CONFIG.MMCM_CLKOUT0_PHASE {45.000} \ + CONFIG.MMCM_CLKOUT1_DIVIDE {12} \ + CONFIG.MMCM_CLKOUT1_PHASE {135.000} \ + CONFIG.NUM_OUT_CLKS {2} \ +] [get_ips fmc_pll] +synth_ip [get_ips fmc_pll] diff --git a/hdl/projects/grapefruit/grapefruit_timing.xdc b/hdl/projects/grapefruit/grapefruit_timing.xdc index 395863df..66ba0401 100644 --- a/hdl/projects/grapefruit/grapefruit_timing.xdc +++ b/hdl/projects/grapefruit/grapefruit_timing.xdc @@ -64,10 +64,14 @@ set_output_delay -clock fmc_virt_clk -min 1.414 [get_ports fmc_sp_to_fpga_wait_l set_output_delay -clock fmc_virt_clk -max 3.851 [get_ports fmc_sp_to_fpga_da[*]] set_output_delay -clock fmc_virt_clk -min 0.830 [get_ports fmc_sp_to_fpga_da[*]] -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out*/C}] -to [get_ports {fmc_sp_to_fpga_da[*]}] -setup 2 -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out*/C}] -to [get_ports {fmc_sp_to_fpga_da[*]}] -hold 1 -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out_en_reg*/C}] -to [get_ports {fmc_sp_to_fpga_da[*]}] -setup 2 -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out_en_reg*/C}] -to [get_ports {fmc_sp_to_fpga_da[*]}] -hold 1 +# The streaming FSM presents read beats on consecutive cycles, so the +# word0->word1 transition is a true single-cycle path with no multicycle +# exception; IOB packing of the per-pin output, tristate, and nwait flops is +# what closes single-cycle at 15 ns. See cosmo_timing.xdc for the derivation; +# extra_beat_setup on the target is the escape hatch if this fails to close. +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/data_out_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/data_out_hiz_int_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/nwait_reg}] # ####################### # SPI NOR flash interface (Winbond W25Q01JV) diff --git a/hdl/projects/grapefruit/grapefruit_top.vhd b/hdl/projects/grapefruit/grapefruit_top.vhd index fd6dd60d..d1bb6204 100644 --- a/hdl/projects/grapefruit/grapefruit_top.vhd +++ b/hdl/projects/grapefruit/grapefruit_top.vhd @@ -197,7 +197,7 @@ architecture rtl of grapefruit_top is signal reset_200m : std_logic; signal reset_fmc: std_logic; signal fmc_internal_data_out : std_logic_vector(15 downto 0); - signal fmc_data_out_enable: std_logic; + signal fmc_data_out_hiz: std_logic_vector(15 downto 0); signal fmc_axi_if : axil26x32_pkg.axil_t; @@ -307,17 +307,22 @@ begin port map( chip_reset => reset_fmc, fmc_clk => fmc_sp_to_fpga_clk, + -- no MMCM here (F17 is not clock-capable); the capture stage still + -- exists, it just runs on the same clock + fmc_capture_clk => fmc_sp_to_fpga_clk, a(24 downto 20) => "00000", a(19 downto 16) => fmc_sp_to_fpga_a, addr_data_in => fmc_sp_to_fpga_da, data_out => fmc_internal_data_out, - data_out_en => fmc_data_out_enable, + data_out_hiz => fmc_data_out_hiz, ne(3 downto 1) => "111", ne(0) => fmc_sp_to_fpga_cs1_l, noe => fmc_sp_to_fpga_oe_l, nwe => fmc_sp_to_fpga_we_l, nl => fmc_sp_to_fpga_adv_l, nwait => fmc_sp_to_fpga_wait_l, + timeout_count => open, + contention_count => open, aclk => clk_125m, aresetn => not reset_125m, axi_if => fmc_axi_if @@ -337,7 +342,11 @@ begin ); -- tristate control for the FMC data bus - fmc_sp_to_fpga_da <= fmc_internal_data_out when fmc_data_out_enable = '1' else (others => 'Z'); + -- per-bit tristate, hiz already in OBUFT T polarity so each pin's T + -- flop packs into its IOB with no inverter in between + fmc_da_tris: for i in fmc_sp_to_fpga_da'range generate + fmc_sp_to_fpga_da(i) <= 'Z' when fmc_data_out_hiz(i) = '1' else fmc_internal_data_out(i); + end generate; resize_axil(fabric_responders(0), responders_8b(0)); info_regs: entity work.info generic map( diff --git a/tools/fmc_sweep/fmc_sweep.py b/tools/fmc_sweep/fmc_sweep.py new file mode 100755 index 00000000..0143939d --- /dev/null +++ b/tools/fmc_sweep/fmc_sweep.py @@ -0,0 +1,567 @@ +#!/usr/bin/env python3 +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +"""FMC interface frequency sweep and verification driver. + +Drives the FPGA's FMC target through hubris' fmc-demo-server: bulk traffic +uses the UDP peek/poke protocol (tools/speeker/udp_if.py), and the FMC_CLK +frequency is changed between phases through humility hiffy calls to the +FmcDemo timing Idol operations, and each change is verified by reading +BTR1 back. + +The phases are ordered so that a timing miss at a new frequency shows up as +bad data before it can show up as bus contention: reads of a known-constant +register first (the SP never drives the bus during read data phases, so a +miscapture is data-corruption-only), then scratch write/readback, then +batched back-to-back stress, then a seeded random soak, then a throughput +measurement. + +Usage +----- + # sanity run at the current frequency + ./tools/fmc_sweep/fmc_sweep.py --ip fe80::0c1d:beff:fe3f:0001 --interface eno1 + + # sweep 50 -> 66.67 -> 100 MHz (CLKDIV divisor values, i.e. kernel/N) + ./tools/fmc_sweep/fmc_sweep.py --ip ... --interface eno1 \ + --sweep 4,3,2 --archive /path/to/build-grapefruit.zip + + # frequency control done by hand (or by re-flashing), just verify + ./tools/fmc_sweep/fmc_sweep.py --ip ... --interface eno1 --no-timing-control + +Notes +----- +* --sweep takes *divisor* values (FMC_CLK = 200 MHz / N), so 4 = 50 MHz, + 3 = 66.67 MHz, 2 = 100 MHz. The Idol op takes the same divisor and + subtracts one for the register field itself. +* Neither board exposes a bulk R/W RAM over FMC, so bulk traffic is split: + back-to-back *reads* sweep a read-only region (default: the eSPI post-code + buffer at FPGA offset 0x8100, an external capture mem -- AXI writes to it + are silently dropped, which is exactly why it cannot be used for write + verification), and back-to-back *writes* hammer a single R/W scribble + register (default: the info block scratchpad at offset 0x10). Write + verification is last-value-plus-ordering here; per-word write verification + is the simulation suite's job. Do NOT point --scratch-addr at + fpga_checksum (offset 0xC): hubris uses it to decide whether the FPGA + needs reprogramming. +* On any phase failure the sweep restores the baseline divisor and + re-verifies phase A so the board is left usable. +""" + +import argparse +import json +import os +import random +import re +import subprocess +import sys +import time + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from speeker.udp_if import Request, UDPMem # noqa: E402 + +FPGA_WINDOW = 0xC0000000 +KERNEL_CLK_MHZ = 200.0 +# ops per packet: each peek_adv4 is 1 byte, each poke_adv4 is 5 bytes, plus +# the 2-byte header and 6-byte address op; 128 words stays well inside the +# 1500-byte cap in both directions (a read reply is 4 bytes per word). +BATCH_WORDS = 128 + + +class TimingCtl: + """FMC_CLK control through humility hiffy FmcDemo calls.""" + + def __init__(self, humility, archive, dry_run=False, verbose=False): + self.cmd_base = [humility] + if archive: + self.cmd_base += ["-a", archive] + self.dry_run = dry_run + self.verbose = verbose + + def _run(self, call, args): + cmd = list(self.cmd_base) + ["hiffy", "-c", call] + for key, value in args: + cmd += ["-a", "%s=%s" % (key, value)] + if self.verbose or self.dry_run: + print(" $ " + " ".join(cmd)) + if self.dry_run: + return + out = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + # humility exits 0 even when the served op returns Err, including + # "" from a task that faulted and restarted, so the + # output text has to be checked too. + if out.returncode != 0 or "Err(" in out.stdout: + raise SystemExit( + "humility hiffy failed: %s\n%s%s" + % (" ".join(cmd), out.stdout, out.stderr) + ) + + def set_divisor(self, divisor): + self._run("FmcDemo.set_clock_divider", [("n", str(divisor))]) + + def get_btr1(self): + """FMC_BTR1 readback via the get_btr1 Idol op, or None on older + firmware / dry runs. CLKDIV is bits [23:20].""" + if self.dry_run: + return None + cmd = list(self.cmd_base) + ["hiffy", "-c", "FmcDemo.get_btr1"] + try: + out = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + except (subprocess.TimeoutExpired, FileNotFoundError): + return None + if out.returncode != 0 or "Err(" in out.stdout: + return None + # parse only the success-reply form "FmcDemo.get_btr1() => "; + # anything looser can match numbers inside an Err message + m = re.search(r"=>\s*(0x[0-9a-fA-F]+|\d+)\s*$", out.stdout.strip()) + return int(m.group(1), 0) if m else None + + def verify_divisor(self, divisor): + """Returns (btr1, ok_or_None): ok compares the CLKDIV field against + what set_divisor should have programmed (divisor - 1); None when the + firmware has no readback op.""" + btr1 = self.get_btr1() + if btr1 is None: + return None, None + clkdiv = (btr1 >> 20) & 0xF + return btr1, clkdiv == divisor - 1 + + def system_time(self): + """Kernel tick count from `humility tasks`, or None if it could not + be read. Ticks reset on an SP reboot, which is how a hang-then- + watchdog event is told apart from a dropped packet: the tick clock + going backwards across a step means the SP restarted under us.""" + if self.dry_run: + return None + cmd = list(self.cmd_base) + ["tasks"] + try: + out = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + except (subprocess.TimeoutExpired, FileNotFoundError): + return None + if out.returncode != 0: + return None + m = re.search(r"system time = (\d+)", out.stdout) + return int(m.group(1)) if m else None + + +def freq_mhz(divisor): + return KERNEL_CLK_MHZ / divisor + + +def batched_read(mem, base, count): + """Read count 32-bit words starting at base using peek_adv4 batches.""" + words = [] + for chunk_at in range(0, count, BATCH_WORDS): + n = min(BATCH_WORDS, count - chunk_at) + req = Request() + req.set_address(base + 4 * chunk_at) + req.add_read32_advances(n) + resp = mem.execute_prebuilt_request(req) + words += [r.payload for r in resp.expected_responses] + return words + + +def phase_a_const_reads(mem, args, golden): + """Repeated single reads of a known-constant register, with gaps.""" + seen = set() + for i in range(args.const_iters): + seen.add(mem.read32(args.const_addr)) + time.sleep(0.001) + ok = len(seen) == 1 and (golden is None or seen == {golden}) + return ok, {"values": sorted("0x%08x" % v for v in seen)} + + +def phase_b_scratch(mem, args): + """Single write/readback of walking and random patterns.""" + addr = args.scratch_addr + patterns = ( + [1 << b for b in range(32)] + + [0xFFFFFFFF ^ (1 << b) for b in range(32)] + + [0x00000000, 0xFFFFFFFF, 0xA5A5A5A5, 0x5A5A5A5A] + ) + bad = [] + for pat in patterns: + mem.write32(addr, pat) + got = mem.read32(addr) + if got != pat: + bad.append({"wrote": "0x%08x" % pat, "read": "0x%08x" % got}) + return not bad, {"patterns": len(patterns), "mismatches": bad[:8]} + + +def scratch_write_burst(mem, addr, values): + """Back-to-back non-advancing writes to one register per packet batch.""" + for chunk_at in range(0, len(values), BATCH_WORDS): + chunk = values[chunk_at : chunk_at + BATCH_WORDS] + req = Request() + req.set_address(addr) + req.add_write32s(chunk) + mem.execute_prebuilt_request(req) + + +def phase_c_stress(mem, args, rng): + """Back-to-back stress: a write burst into the scratch register (posted + writes queue back-to-back; the last value read back proves ordering and + that nothing was dropped or hung), then two read sweeps of the read-only + stress region compared against each other (content is arbitrary but must + be stable).""" + values = [rng.getrandbits(32) for _ in range(args.stress_words)] + scratch_write_burst(mem, args.scratch_addr, values) + got = mem.read32(args.scratch_addr) + bad = [] + if got != values[-1]: + bad.append({"scratch_last": "0x%08x" % values[-1], + "read": "0x%08x" % got}) + pass1 = batched_read(mem, args.stress_base, args.stress_words) + pass2 = batched_read(mem, args.stress_base, args.stress_words) + for i, (x, y) in enumerate(zip(pass1, pass2)): + if x != y and len(bad) < 8: + bad.append({"index": i, "pass1": "0x%08x" % x, "pass2": "0x%08x" % y}) + return not bad, {"write_burst": len(values), "read_words": args.stress_words, + "mismatches": bad} + + +def phase_d_soak(mem, args, rng): + """Seeded random mix of scratch writes/readbacks, constant reads, and + read-region bursts, scoreboarding the scratch register and the golden + constant. Runs for --soak-ops operations or --soak-seconds, whichever + lasts longer (each op is one UDP round trip, so op counts alone finish + in seconds).""" + snapshot = batched_read(mem, args.stress_base, args.stress_words) + golden = mem.read32(args.const_addr) + last_scratch = None + bad = [] + ops = 0 + deadline = time.monotonic() + args.soak_seconds + while ops < args.soak_ops or time.monotonic() < deadline: + ops += 1 + op = rng.randrange(4) + if op == 0: + last_scratch = rng.getrandbits(32) + mem.write32(args.scratch_addr, last_scratch) + elif op == 1 and last_scratch is not None: + got = mem.read32(args.scratch_addr) + if got != last_scratch and len(bad) < 8: + bad.append({"scratch_expect": "0x%08x" % last_scratch, + "read": "0x%08x" % got}) + elif op == 2: + got = mem.read32(args.const_addr) + if got != golden and len(bad) < 8: + bad.append({"const_expect": "0x%08x" % golden, + "read": "0x%08x" % got}) + else: + index = rng.randrange(args.stress_words - 16) + got = batched_read(mem, args.stress_base + 4 * index, 16) + if got != snapshot[index : index + 16] and len(bad) < 8: + bad.append({"region_index": index}) + if bad and len(bad) >= 8: + break # no point soaking further on a badly broken link + return not bad, {"ops": ops, "mismatches": bad} + + +def _timed_block_op(mem, build_request, small, large, repeats=5): + """Median duration delta between a `large`-count and a `small`-count + server-side block op: the round trip, stack, and per-packet costs cancel, + leaving (large - small) bus accesses. Returns (seconds_per_word, checksum + of the last large op).""" + def run(count): + req = build_request(count) + t0 = time.monotonic() + resp = mem.execute_prebuilt_request(req) + dt = time.monotonic() - t0 + payload = resp.expected_responses[0].payload if resp.expected_responses else None + return dt, payload + deltas = [] + checksum = None + for _ in range(repeats): + t_small, _ = run(small) + t_large, checksum = run(large) + deltas.append(t_large - t_small) + deltas.sort() + return deltas[len(deltas) // 2] / (large - small), checksum + + +def phase_e_throughput(mem, args, rng): + """FMC line rate via the server-side block ops (delta-timed so network, + stack, and per-packet costs cancel), with the wire-level batched numbers + kept for reference. Falls back to wire-level only against firmware + without ops 17-19.""" + results = {} + ok = True + + # wire-level reference numbers (dominated by per-word network bytes) + values = [rng.getrandbits(32) for _ in range(args.stress_words)] + t0 = time.monotonic() + scratch_write_burst(mem, args.scratch_addr, values) + t_write = time.monotonic() - t0 + ok = mem.read32(args.scratch_addr) == values[-1] + t0 = time.monotonic() + snapshot = batched_read(mem, args.stress_base, args.stress_words) + t_read = time.monotonic() - t0 + nbytes = 4 * args.stress_words + results["wire_write_MBps"] = round(nbytes / t_write / 1e6, 3) + results["wire_read_MBps"] = round(nbytes / t_read / 1e6, 3) + + n = 200 + t0 = time.monotonic() + for _ in range(n): + mem.read32(args.const_addr) + results["single_read_us"] = round((time.monotonic() - t0) / n * 1e6, 1) + + # bus-level numbers via ops 17-19 + try: + golden = mem.read32(args.const_addr) + retried_before = mem.timeouts_retried + + def rd_req(count): + req = Request() + req.set_address(args.stress_base) + req.add_peek_block_checksum(count, advance=True) + return req + + spw, checksum = _timed_block_op(mem, rd_req, 64, args.stress_words) + results["fmc_read_MBps"] = round(4 / spw / 1e6, 2) + results["fmc_read_ns_per_word"] = round(spw * 1e9) + expect = sum(snapshot) & 0xFFFFFFFF + if checksum != expect: + ok = False + results["read_checksum_mismatch"] = { + "got": "0x%08x" % checksum, "expect": "0x%08x" % expect} + + def rdf_req(count): + req = Request() + req.set_address(args.const_addr) + req.add_peek_block_checksum(count, advance=False) + return req + + spw, checksum = _timed_block_op(mem, rdf_req, 64, 8192) + results["fmc_read_fixed_ns_per_word"] = round(spw * 1e9) + if checksum != (golden * 8192) & 0xFFFFFFFF: + ok = False + results["fixed_checksum_mismatch"] = "0x%08x" % checksum + + fill_value = rng.getrandbits(32) + + def wr_req(count): + req = Request() + req.set_address(args.scratch_addr) + req.add_poke_block_fill(count, fill_value) + return req + + spw, _ = _timed_block_op(mem, wr_req, 64, 8192) + results["fmc_write_MBps"] = round(4 / spw / 1e6, 2) + results["fmc_write_ns_per_word"] = round(spw * 1e9) + if mem.read32(args.scratch_addr) != fill_value: + ok = False + results["fill_readback_mismatch"] = True + if mem.timeouts_retried != retried_before: + # a resend during a timed op makes that sample garbage + results["retried_during_timing"] = True + except Exception as exc: + results["block_ops"] = "unsupported or failed: %r" % exc + + results["note"] = ("fmc_* numbers are bus line rate (delta-timed block " + "ops); wire_* include per-word network cost and are " + "insensitive to the FMC clock") + return ok, results + + +PHASES = [ + ("A_const_reads", phase_a_const_reads), + ("B_scratch", phase_b_scratch), + ("C_stress", phase_c_stress), + ("D_soak", phase_d_soak), + ("E_throughput", phase_e_throughput), +] + + +def run_phases(mem, args, rng, golden): + results = {} + all_ok = True + for name, fn in PHASES: + if name == "A_const_reads": + ok, detail = fn(mem, args, golden) + elif name == "B_scratch": + ok, detail = fn(mem, args) + else: + ok, detail = fn(mem, args, rng) + results[name] = {"ok": ok, **detail} + status = "ok" if ok else "FAIL" + print(" %-16s %s %s" % (name, status, json.dumps(detail)[:120])) + if not ok: + all_ok = False + break # keep the electrical exposure of later phases off a bad link + return all_ok, results + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--ip", required=True, help="SP link-local IPv6 address") + ap.add_argument("--interface", required=True, help="host network interface") + ap.add_argument("--port", type=int, default=11114) + ap.add_argument("--sweep", default=None, + help="comma-separated FMC_CLK divisors to sweep, e.g. 4,3,2") + ap.add_argument("--baseline-divisor", type=int, default=4, + help="known-good divisor to calibrate at and restore to") + ap.add_argument("--no-timing-control", action="store_true", + help="never call humility; frequency is managed externally") + ap.add_argument("--humility", default="humility") + ap.add_argument("--archive", default=None, help="hubris archive for humility") + ap.add_argument("--const-addr", type=lambda x: int(x, 0), + default=FPGA_WINDOW, + help="address of a read-only constant register") + ap.add_argument("--const-iters", type=int, default=50) + ap.add_argument("--scratch-addr", type=lambda x: int(x, 0), + default=FPGA_WINDOW + 0x10, + help="address of a harmless 32-bit r/w scratch register " + "(default: info block scratchpad; never point this " + "at fpga_checksum)") + ap.add_argument("--stress-base", type=lambda x: int(x, 0), + default=FPGA_WINDOW + 0x8100, + help="base of a readable region for bulk read traffic " + "(default: eSPI post-code buffer; reads only, its " + "content just has to be stable during the run)") + ap.add_argument("--stress-words", type=int, default=1024) + ap.add_argument("--soak-ops", type=int, default=500, + help="minimum soak operations (each is one UDP round trip)") + ap.add_argument("--soak-seconds", type=int, default=0, + help="minimum soak duration; the soak runs until BOTH " + "this and --soak-ops are satisfied. Applies to every " + "divisor in a sweep, so budget accordingly.") + ap.add_argument("--udp-retries", type=int, default=3, + help="resend attempts after a UDP timeout before a step " + "fails (0 = old fail-on-first-loss behavior)") + ap.add_argument("--seed", type=int, default=0x1DE) + ap.add_argument("--json", default=None, help="write results here") + ap.add_argument("--dry-run", action="store_true", + help="print humility commands without running them") + args = ap.parse_args() + + # retries make a dropped frame on a flaky link (USB NICs especially) a + # logged statistic instead of a dead hour-long soak; three consecutive + # losses still fails the step, which is the "target actually gone" case + mem = UDPMem(args.ip, args.interface, target_port=args.port, + retries=args.udp_retries) + # The archive enables observation (BTR1 readback, SP uptime/reboot + # detection) even when frequency control is off; --no-timing-control + # only means "never change the divisor" -- the mode for soaking a board + # at its kernel-configured boot frequency. + ctl = TimingCtl(args.humility, args.archive, + dry_run=args.dry_run) if args.archive else None + control = ctl is not None and not args.no_timing_control + + report = {"seed": args.seed, "steps": []} + + # Calibrate the constant register at the known-good divisor. + if control: + ctl.set_divisor(args.baseline_divisor) + time.sleep(0.1) + golden = mem.read32(args.const_addr) + print("golden const read @%s: 0x%08x" % (hex(args.const_addr), golden)) + + if args.sweep: + if not control: + raise SystemExit("--sweep requires timing control " + "(an --archive, without --no-timing-control)") + divisors = [int(d) for d in args.sweep.split(",")] + elif control: + divisors = [args.baseline_divisor] + else: + # frequency untouched: label the step with the divisor the SP + # actually booted with, when readable + btr1 = ctl.get_btr1() if ctl else None + divisors = [((btr1 >> 20) & 0xF) + 1 if btr1 is not None + else args.baseline_divisor] + if btr1 is not None: + print("boot BTR1 = 0x%08x -> divisor %d (%.2f MHz)" + % (btr1, divisors[0], freq_mhz(divisors[0]))) + overall_ok = True + for divisor in divisors: + print("== divisor %d (FMC_CLK %.2f MHz) ==" % (divisor, freq_mhz(divisor))) + if control: + ctl.set_divisor(divisor) + time.sleep(0.1) + rng = random.Random(args.seed) + btr1, div_ok = ctl.verify_divisor(divisor) if ctl else (None, None) + if btr1 is not None: + print(" BTR1 = 0x%08x (CLKDIV field %d, %s)" % + (btr1, (btr1 >> 20) & 0xF, + "matches" if div_ok else "DOES NOT MATCH requested divisor")) + if div_ok is False and not control: + # informational only: we did not request this divisor + div_ok = None + if div_ok is False: + # the programmed divider never landed: nothing this step would + # measure is at the requested frequency, so fail before phases + report["steps"].append({ + "divisor": divisor, + "freq_mhz": freq_mhz(divisor), + "ok": False, + "btr1": "0x%08x" % btr1, + "clkdiv_verified": False, + }) + overall_ok = False + print(" divisor %d FAILED (CLKDIV readback mismatch); " + "restoring baseline" % divisor) + break + ticks_before = ctl.system_time() if ctl else None + try: + ok, results = run_phases(mem, args, rng, golden) + except Exception as exc: # timeouts etc. count as a hard step failure + ok, results = False, {"exception": repr(exc)} + print(" step raised: %r" % exc) + step = { + "divisor": divisor, + "freq_mhz": freq_mhz(divisor), + "ok": ok, + "udp_timeouts_retried": mem.timeouts_retried, + "phases": results, + } + if btr1 is not None: + step["btr1"] = "0x%08x" % btr1 + step["clkdiv_verified"] = div_ok + if ctl: + ticks_after = ctl.system_time() + step["sp_ticks_before"] = ticks_before + step["sp_ticks_after"] = ticks_after + if ticks_before is not None and ticks_after is not None: + # ticks are milliseconds since boot and only ever increase + # while the SP stays up + step["sp_rebooted"] = ticks_after < ticks_before + if step["sp_rebooted"]: + ok = False + step["ok"] = False + print(" SP REBOOTED during this step (ticks %d -> %d):" + " it hung and was reset, this was not packet loss" + % (ticks_before, ticks_after)) + elif ticks_after is None: + step["sp_unresponsive"] = True + print(" could not read SP system time after the step " + "(SP hung or humility unavailable)") + report["steps"].append(step) + if not ok: + overall_ok = False + print(" divisor %d FAILED; restoring baseline" % divisor) + break + + if control: + # also covers the reboot case: a restarted SP booted back at its + # kernel-configured divisor, and this re-asserts the sweep baseline + ctl.set_divisor(args.baseline_divisor) + time.sleep(0.1) + ok, _ = phase_a_const_reads(mem, args, golden) + report["restored_baseline_ok"] = ok + print("baseline restore check: %s" % ("ok" if ok else "FAIL")) + overall_ok = overall_ok and ok + + if args.json: + with open(args.json, "w") as f: + json.dump(report, f, indent=2) + print("results written to", args.json) + + sys.exit(0 if overall_ok else 1) + + +if __name__ == "__main__": + main() diff --git a/tools/speeker/udp_if.py b/tools/speeker/udp_if.py index ad1158fe..4390bb6b 100644 --- a/tools/speeker/udp_if.py +++ b/tools/speeker/udp_if.py @@ -37,6 +37,11 @@ class Request: poke_adv2_arg = 14 # write 2 byte to addr, increment internal addr by 2 poke_adv4_arg = 15 # write 4 byte to addr, increment internal addr by 3 poke_adv8_arg = 16 # write 8 byte to addr, increment internal addr by 4 + # server-side block ops: N bus accesses per op with a constant-size + # response, so request duration is dominated by the FMC accesses + peek_block_checksum_arg = 17 # u16 count; advancing; returns u32 sum + peek_block_checksum_fixed_arg = 18 # u16 count; same address; returns u32 sum + poke_block_fill_arg = 19 # u16 count + u32 value; same address def __init__(self): # Build a bytearray to represent the packet we're going to send @@ -99,6 +104,24 @@ def add_write32s(self, values: list) -> None: else: raise Exception(f"Invalid type {type(value)} for value") + def add_peek_block_checksum(self, count, advance=True) -> None: + """Server-side read of `count` 32-bit words returning one u32 + wrapping-sum checksum; `advance` selects walking memory vs + re-reading one address.""" + op = self.peek_block_checksum_arg if advance else self.peek_block_checksum_fixed_arg + self.bytes += op.to_bytes(1, byteorder='little') + self.bytes += count.to_bytes(2, byteorder='little') + self.response.add_expected_peek(ResponsePeek(self.cur_addr, 4)) + if advance: + self.cur_addr += 4 * count + + def add_poke_block_fill(self, count, value) -> None: + """Server-side write of `value` to the current address `count` + times (non-advancing).""" + self.bytes += self.poke_block_fill_arg.to_bytes(1, byteorder='little') + self.bytes += count.to_bytes(2, byteorder='little') + self.bytes += value.to_bytes(4, byteorder='little') + def add_write32_advances(self, values: list) -> None: for value in values: self.bytes += self.poke_adv4_arg.to_bytes(1, byteorder='little') @@ -157,9 +180,18 @@ class UDPMem: Note that the SP is IPv6 only and runs on a link-local address so specification of the pc's output interface is required. """ - def __init__(self, target_ip, ifname, target_port=11114, timeout=2): + def __init__(self, target_ip, ifname, target_port=11114, timeout=2, retries=0): self.debug = False self.timeout = timeout + # Additional attempts after a receive timeout. Requests are + # idempotent (peeks re-read, pokes re-write the same value), so a + # resend after a dropped frame is safe; the socket is drained before + # each send so a late-arriving response can never be matched to a + # newer request. + self.retries = retries + # count of timed-out attempts that were subsequently retried; lets a + # caller report link flakiness instead of dying on it + self.timeouts_retried = 0 # Basic UDP IPv6 socket setup self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) # Build the target address using getaddrinfo and the interface name @@ -199,18 +231,37 @@ def _get_resp_from_request(self, request: Request) -> Response: request.response.process_bytes(resp_bytes) return request.response - def _send_get_reply_handshake(self, request: Request) -> bytes: - # Send the request out the wire - if self.debug: - print(f"Sending request: {request.hex()}") - self.sock.sendto(bytes(request), self.target_addr) - # try rx up to mtu size for timeout time and return - # response or exception on timeout - self.sock.settimeout(self.timeout) + def _drain(self) -> None: + """Discard any stale datagrams (late responses from a timed-out + attempt) so request/response pairing stays in lockstep.""" + self.sock.setblocking(False) try: - resp = self.sock.recv(1500) - except socket.timeout: - raise Exception("Timeout- no response back from target") - if self.debug: - print(f"Got response: {resp.hex()}") - return resp \ No newline at end of file + while True: + self.sock.recv(1500) + except (BlockingIOError, OSError): + pass + finally: + self.sock.setblocking(True) + + def _send_get_reply_handshake(self, request: Request) -> bytes: + for attempt in range(1 + self.retries): + self._drain() + if self.debug: + print(f"Sending request: {request.hex()}") + self.sock.sendto(bytes(request), self.target_addr) + # try rx up to mtu size for timeout time and return + # response or exception on timeout + self.sock.settimeout(self.timeout) + try: + resp = self.sock.recv(1500) + except socket.timeout: + if attempt < self.retries: + self.timeouts_retried += 1 + continue + raise Exception( + "Timeout- no response back from target" + + (f" after {1 + self.retries} attempts" if self.retries else "") + ) + if self.debug: + print(f"Got response: {resp.hex()}") + return resp \ No newline at end of file From 63b827e54841dee6d575e49ad7eca9c6aabc4dad Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Fri, 28 Aug 2026 11:30:58 -0400 Subject: [PATCH 02/12] cosmo_seq: split the board-agnostic SP5 sequencing core out of sequencer The A1/A0 state machine and the rail records it works on are not cosmo-specific -- any SP5 board sequences the same group A/B/C rails through the same machine. They were nonetheless welded to cosmo's NIC: sequencer_io_pkg held both halves, and the sequencer target globbed every file in the directory, so a sibling board could not depend on the shared part without also pulling in cosmo's T6 and colliding with its own. Carve sp5_power_pkg out of sequencer_io_pkg -- the generic rail records, the SP5 rail groups, the SP5 pin record -- and give a1_a0_seq and it a sequencer_core target. sequencer_io_pkg keeps the cosmo-specific half and depends on the new package. No files move and no logic changes. Do the same for the sim side: rail_model, cascade_rail_model and sp5_model only ever spoke sp5_power_pkg's records, so drop their vestigial sequencer_io_pkg use clauses and expose them as sequencer_core_sims. sp5_seq_tb passes unchanged, 21 of 21. --- hdl/projects/cosmo_seq/cosmo_seq_top.vhd | 1 + .../cosmo_seq/debug_module/debug_header.vhd | 1 + .../debug_module/debug_module_top.vhd | 1 + hdl/projects/cosmo_seq/sequencer/BUCK | 49 ++++- .../cosmo_seq/sequencer/a1_a0_seq.vhd | 2 +- hdl/projects/cosmo_seq/sequencer/nic_seq.vhd | 1 + hdl/projects/cosmo_seq/sequencer/seq_sync.vhd | 1 + .../cosmo_seq/sequencer/sequencer_io_pkg.vhd | 165 +--------------- .../cosmo_seq/sequencer/sequencer_regs.vhd | 1 + .../sequencer/sims/cascade_rail_model.vhd | 2 +- .../cosmo_seq/sequencer/sims/nic_model.vhd | 1 + .../cosmo_seq/sequencer/sims/rail_model.vhd | 2 +- .../sequencer/sims/sp5_seq_sim_th.vhd | 1 + .../cosmo_seq/sequencer/sims/sp5_sim.vhd | 2 +- .../cosmo_seq/sequencer/sp5_power_pkg.vhd | 178 ++++++++++++++++++ .../cosmo_seq/sequencer/sp5_sequencer.vhd | 1 + 16 files changed, 241 insertions(+), 168 deletions(-) create mode 100644 hdl/projects/cosmo_seq/sequencer/sp5_power_pkg.vhd diff --git a/hdl/projects/cosmo_seq/cosmo_seq_top.vhd b/hdl/projects/cosmo_seq/cosmo_seq_top.vhd index 42240d3b..58550779 100644 --- a/hdl/projects/cosmo_seq/cosmo_seq_top.vhd +++ b/hdl/projects/cosmo_seq/cosmo_seq_top.vhd @@ -21,6 +21,7 @@ use work.axi_st8_pkg; use work.time_pkg.all; use work.tristate_if_pkg.all; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.sp5_uart_subsystem_pkg.all; diff --git a/hdl/projects/cosmo_seq/debug_module/debug_header.vhd b/hdl/projects/cosmo_seq/debug_module/debug_header.vhd index c1f00005..c35647ce 100644 --- a/hdl/projects/cosmo_seq/debug_module/debug_header.vhd +++ b/hdl/projects/cosmo_seq/debug_module/debug_header.vhd @@ -8,6 +8,7 @@ use ieee.std_logic_1164.all; use ieee.numeric_std.all; use work.debug_regs_pkg.all; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; entity debug_header is diff --git a/hdl/projects/cosmo_seq/debug_module/debug_module_top.vhd b/hdl/projects/cosmo_seq/debug_module/debug_module_top.vhd index d5aeaa1a..c44c0533 100644 --- a/hdl/projects/cosmo_seq/debug_module/debug_module_top.vhd +++ b/hdl/projects/cosmo_seq/debug_module/debug_module_top.vhd @@ -12,6 +12,7 @@ use work.axil8x32_pkg.all; use work.debug_regs_pkg.all; use work.sp5_uart_subsystem_pkg.all; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; entity debug_module_top is diff --git a/hdl/projects/cosmo_seq/sequencer/BUCK b/hdl/projects/cosmo_seq/sequencer/BUCK index b8c64721..e4f00764 100644 --- a/hdl/projects/cosmo_seq/sequencer/BUCK +++ b/hdl/projects/cosmo_seq/sequencer/BUCK @@ -1,4 +1,4 @@ -load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim") +load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim", "sim_only_model") load("//tools:rdl.bzl", "rdl_file") rdl_file( @@ -12,10 +12,29 @@ rdl_file( visibility = ["PUBLIC"], ) +# The SP5-side sequencing core: the generic rail records, the SP5 rail groups, +# and the A1/A0 state machine that drives them. None of this is cosmo-specific, +# so sibling SP5 boards depend on this target and supply their own NIC half. +# It still carries sequencer_regs_rdl for the A1/A0 state enums -- there is one +# A1/A0 state machine, so there should be one encoding of its states. +vhdl_unit( + name = "sequencer_core", + srcs = [ + "sp5_power_pkg.vhd", + "a1_a0_seq.vhd", + ], + deps = [ + ":sequencer_regs_rdl", + ], + visibility = ["PUBLIC"], + standard = "2019", +) + vhdl_unit( name = "sequencer", - srcs = glob(["*.vhd"]), + srcs = glob(["*.vhd"], exclude = ["sp5_power_pkg.vhd", "a1_a0_seq.vhd"]), deps = [ + ":sequencer_core", ":sequencer_regs_rdl", "//hdl/ip/vhd/synchronizers:meta_sync", "//hdl/ip/vhd/irq:irq_block", @@ -25,10 +44,32 @@ vhdl_unit( standard = "2019", ) +# The rail models and the SP5 model only speak sp5_power_pkg's records, so they +# are reusable by any SP5 board's sequencer testbench, not just cosmo's. +sim_only_model( + name = "sequencer_core_sims", + srcs = [ + "sims/rail_model_msg_pkg.vhd", + "sims/rail_model.vhd", + "sims/cascade_rail_model.vhd", + "sims/sp5_sim.vhd", + ], + deps = [":sequencer_core"], + visibility = ["PUBLIC"], + standard = "2019", +) + vunit_sim( name = "sp5_seq_tb", - srcs = glob(["sims/**/*.vhd"]), - deps = [":sequencer", + srcs = glob(["sims/**/*.vhd"], exclude = [ + "sims/rail_model_msg_pkg.vhd", + "sims/rail_model.vhd", + "sims/cascade_rail_model.vhd", + "sims/sp5_sim.vhd", + ]), + deps = [ + ":sequencer", + ":sequencer_core_sims", ], visibility = ['PUBLIC'], standard = "2019", diff --git a/hdl/projects/cosmo_seq/sequencer/a1_a0_seq.vhd b/hdl/projects/cosmo_seq/sequencer/a1_a0_seq.vhd index cd646b09..9f593438 100644 --- a/hdl/projects/cosmo_seq/sequencer/a1_a0_seq.vhd +++ b/hdl/projects/cosmo_seq/sequencer/a1_a0_seq.vhd @@ -6,7 +6,7 @@ library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; -use work.sequencer_io_pkg.all; +use work.sp5_power_pkg.all; use work.sequencer_regs_pkg.all; -- This is the A1/A0 sequencer for the SP5 cosmo sled diff --git a/hdl/projects/cosmo_seq/sequencer/nic_seq.vhd b/hdl/projects/cosmo_seq/sequencer/nic_seq.vhd index 1ee181db..d05031ec 100644 --- a/hdl/projects/cosmo_seq/sequencer/nic_seq.vhd +++ b/hdl/projects/cosmo_seq/sequencer/nic_seq.vhd @@ -6,6 +6,7 @@ library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.sequencer_regs_pkg.all; diff --git a/hdl/projects/cosmo_seq/sequencer/seq_sync.vhd b/hdl/projects/cosmo_seq/sequencer/seq_sync.vhd index 9c860141..56d9adb3 100644 --- a/hdl/projects/cosmo_seq/sequencer/seq_sync.vhd +++ b/hdl/projects/cosmo_seq/sequencer/seq_sync.vhd @@ -6,6 +6,7 @@ library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.sequencer_regs_pkg.all; diff --git a/hdl/projects/cosmo_seq/sequencer/sequencer_io_pkg.vhd b/hdl/projects/cosmo_seq/sequencer/sequencer_io_pkg.vhd index 345c7afe..a8e35f63 100644 --- a/hdl/projects/cosmo_seq/sequencer/sequencer_io_pkg.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sequencer_io_pkg.vhd @@ -6,62 +6,11 @@ library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; -package sequencer_io_pkg is +use work.sp5_power_pkg.all; - -- Generic record type for the common enable/pg feedback. - -- we'll treat this as both active high internally so - -- the outer-most blocks need to invert if required. - type power_rail_t is record - enable : std_logic; - pg : std_logic; - end record; - -- FPGA's view of the world as the controller and recipient of - -- the feedback - view power_rail_at_fpga of power_rail_t is - enable : out; - pg : in; - end view; - alias power_rail_at_reg is power_rail_at_fpga'converse; - -- a cascade power rail only has a PG, the enable came from some - -- other supply - type cascade_power_rail_t is record - pg : std_logic; - end record; - view cascade_power_rail_at_fpga of cascade_power_rail_t is - pg : in; - end view; - alias cascade_power_rail_at_reg is cascade_power_rail_at_fpga'converse; - - -- Sequencing-related SP5 control/feedback pins - type sp5_seq_pins_t is record - thermtrip_l : std_logic; - reset_l : std_logic; - pwr_ok : std_logic; - slp_s3_l : std_logic; - slp_s5_l : std_logic; - rsmrst_l : std_logic; - pwr_btn_l : std_logic; - pwr_good : std_logic; - pwrgd_out : std_logic; - smerr_l : std_logic; - is_cosmo : std_logic; -- uses SP5_TO_FPGA1_DEBUG1 high at power up to indicate cosmo - end record; - -- FPGA's view of the world as the controller and recipient of - -- the feedback - view sp5_seq_at_fpga of sp5_seq_pins_t is - thermtrip_l : in; - reset_l : in; - pwr_ok : in; - slp_s3_l : in; - slp_s5_l : in; - smerr_l : in; - pwrgd_out : in; - rsmrst_l : out; - pwr_btn_l : out; - pwr_good : out; - is_cosmo : out; - end view; - alias sp5_seq_at_sp5 is sp5_seq_at_fpga'converse; +-- Cosmo-specific sequencer I/O types. The generic rail records and the SP5 +-- rail groups they build on live in sp5_power_pkg. +package sequencer_io_pkg is type seq_power_alert_pins_t is record smbus_fan_central_hsc_to_fpga1_alert_l : std_logic; @@ -148,82 +97,6 @@ package sequencer_io_pkg is end view; alias t6_debug_dbg is t6_debug_seq_ss'converse; - type early_power_t is record - fan_central_hsc_pg : std_logic; - fan_east_hsc_pg : std_logic; - fan_fail : std_logic; - fan_west_hsc_pg : std_logic; - fan_central_hsc_disable : std_logic; - fan_east_hsc_disable : std_logic; - fan_west_hsc_disable : std_logic; - end record; - view early_power_at_fpga of early_power_t is - fan_central_hsc_pg : in; - fan_east_hsc_pg : in; - fan_west_hsc_pg : in; - fan_fail : in; - fan_central_hsc_disable : out; - fan_east_hsc_disable : out; - fan_west_hsc_disable : out; - end view; - alias early_power_on_board is early_power_at_fpga'converse; - - -- DDR 12V bulk power rail - type ddr_bulk_power_t is record - abcdef_hsc : power_rail_t; - ghijkl_hsc : power_rail_t; - end record; - function is_power_good(power_group: ddr_bulk_power_t) return boolean; - -- FPGA's view of the world as the controller and recipient of - -- the feedback - view ddr_bulk_power_at_fpga of ddr_bulk_power_t is - abcdef_hsc : view power_rail_at_fpga; - ghijkl_hsc : view power_rail_at_fpga; - end view; - alias ddr_bulk_at_reg is ddr_bulk_power_at_fpga'converse; - - -- SP5 group a rails - --A (G3/S5): VDDBT_RTC_G, VDD_18_S5,VDD_33_S5, VDDIO_AUDIO (shared as 1V8) - type group_a_power_t is record - pwr_v1p5_rtc : power_rail_t; - v3p3_sp5_a1 : power_rail_t; - v1p8_sp5_a1 : power_rail_t; - end record; - function is_power_good(power_group: group_a_power_t) return boolean; - view group_a_power_at_fpga of group_a_power_t is - pwr_v1p5_rtc : view power_rail_at_fpga; - v3p3_sp5_a1 : view power_rail_at_fpga; - v1p8_sp5_a1 : view power_rail_at_fpga; - end view; - alias group_a_power_at_reg is group_a_power_at_fpga'converse; - - -- B (S3): VDD_11_S3 - type group_b_power_t is record - v1p1_sp5 : power_rail_t; - end record; - function is_power_good(power_group: group_b_power_t) return boolean; - view group_b_power_at_fpga of group_b_power_t is - v1p1_sp5 : view power_rail_at_fpga; - end view; - alias group_b_power_at_reg is group_b_power_at_fpga'converse; - - - -- C (S0): VDDIO, VDDCR_SOC,VDDCR_CPU0, VDDCR_CPU1 - type group_c_power_t is record - vddio_sp5_a0 : power_rail_t; - vddcr_cpu1 : power_rail_t; - vddcr_cpu0 : power_rail_t; - vddcr_soc : power_rail_t; - end record; - function is_power_good(power_group: group_c_power_t) return boolean; - view group_c_power_at_fpga of group_c_power_t is - vddio_sp5_a0 : view power_rail_at_fpga; - vddcr_cpu1 : view power_rail_at_fpga; - vddcr_cpu0 : view power_rail_at_fpga; - vddcr_soc : view power_rail_at_fpga; - end view; - alias group_c_power_at_reg is group_c_power_at_fpga'converse; - -- effectively one enable fires all of this due to the -- hardware design. type nic_power_t is record @@ -253,36 +126,8 @@ package sequencer_io_pkg is end package; - package body sequencer_io_pkg is - -- Functions for DDR bulk - function is_power_good(power_group: ddr_bulk_power_t) return boolean is - begin - return (power_group.abcdef_hsc.pg and power_group.ghijkl_hsc.pg) = '1'; - end function; - - -- Functions for groupA - function is_power_good(power_group: group_a_power_t) return boolean is - begin - return (power_group.pwr_v1p5_rtc.pg and power_group.v3p3_sp5_a1.pg and power_group.v1p8_sp5_a1.pg) = '1'; - end function; - - -- Functions for groupB - function is_power_good(power_group: group_b_power_t) return boolean is - begin - return power_group.v1p1_sp5.pg = '1'; - end function; - - -- Functions for groupc - function is_power_good(power_group: group_c_power_t) return boolean is - begin - return (power_group.vddio_sp5_a0.pg and - power_group.vddcr_cpu1.pg and - power_group.vddcr_cpu0.pg and - power_group.vddcr_soc.pg - ) = '1'; - end function; function is_power_good(power_group: nic_power_t) return boolean is begin return ( @@ -298,4 +143,4 @@ package body sequencer_io_pkg is ) = '1'; end function; -end package body; \ No newline at end of file +end package body; diff --git a/hdl/projects/cosmo_seq/sequencer/sequencer_regs.vhd b/hdl/projects/cosmo_seq/sequencer/sequencer_regs.vhd index 49841d51..5ba1359f 100644 --- a/hdl/projects/cosmo_seq/sequencer/sequencer_regs.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sequencer_regs.vhd @@ -10,6 +10,7 @@ use ieee.numeric_std_unsigned.all; use work.axil_common_pkg.all; use work.axil8x32_pkg; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.sequencer_regs_pkg.all; diff --git a/hdl/projects/cosmo_seq/sequencer/sims/cascade_rail_model.vhd b/hdl/projects/cosmo_seq/sequencer/sims/cascade_rail_model.vhd index c0cdb038..4046ad6c 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/cascade_rail_model.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/cascade_rail_model.vhd @@ -6,7 +6,7 @@ library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; -use work.sequencer_io_pkg.all; +use work.sp5_power_pkg.all; entity cascade_rail_model is port ( diff --git a/hdl/projects/cosmo_seq/sequencer/sims/nic_model.vhd b/hdl/projects/cosmo_seq/sequencer/sims/nic_model.vhd index 68a091c8..986331b5 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/nic_model.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/nic_model.vhd @@ -13,6 +13,7 @@ library vunit_lib; context vunit_lib.vunit_context; context vunit_lib.com_context; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.nic_model_msg_pkg.all; use work.nic_model_msg_pkg.disable_rail_msg; diff --git a/hdl/projects/cosmo_seq/sequencer/sims/rail_model.vhd b/hdl/projects/cosmo_seq/sequencer/sims/rail_model.vhd index f1e3863a..7add7fe3 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/rail_model.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/rail_model.vhd @@ -10,7 +10,7 @@ library vunit_lib; context vunit_lib.vunit_context; context vunit_lib.com_context; -use work.sequencer_io_pkg.all; +use work.sp5_power_pkg.all; use work.rail_model_msg_pkg.all; entity rail_model is diff --git a/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_th.vhd b/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_th.vhd index 0634d42e..b576d472 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_th.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_th.vhd @@ -12,6 +12,7 @@ library vunit_lib; context vunit_lib.vc_context; use work.axil8x32_pkg; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.sp5_seq_sim_pkg.all; diff --git a/hdl/projects/cosmo_seq/sequencer/sims/sp5_sim.vhd b/hdl/projects/cosmo_seq/sequencer/sims/sp5_sim.vhd index 0d7fe324..83fd87b6 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/sp5_sim.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/sp5_sim.vhd @@ -6,7 +6,7 @@ library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; -use work.sequencer_io_pkg.all; +use work.sp5_power_pkg.all; entity sp5_model is port ( diff --git a/hdl/projects/cosmo_seq/sequencer/sp5_power_pkg.vhd b/hdl/projects/cosmo_seq/sequencer/sp5_power_pkg.vhd new file mode 100644 index 00000000..1b0aafc1 --- /dev/null +++ b/hdl/projects/cosmo_seq/sequencer/sp5_power_pkg.vhd @@ -0,0 +1,178 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +-- The board-agnostic half of the sequencer's I/O types: the generic rail +-- records and the SP5-side rail groups and pins. Every SP5 board carries these +-- unchanged, so they live apart from sequencer_io_pkg's cosmo-specific records +-- to let a sibling design reuse a1_a0_seq without dragging in cosmo's NIC. +package sp5_power_pkg is + + -- Generic record type for the common enable/pg feedback. + -- we'll treat this as both active high internally so + -- the outer-most blocks need to invert if required. + type power_rail_t is record + enable : std_logic; + pg : std_logic; + end record; + -- FPGA's view of the world as the controller and recipient of + -- the feedback + view power_rail_at_fpga of power_rail_t is + enable : out; + pg : in; + end view; + alias power_rail_at_reg is power_rail_at_fpga'converse; + -- a cascade power rail only has a PG, the enable came from some + -- other supply + type cascade_power_rail_t is record + pg : std_logic; + end record; + view cascade_power_rail_at_fpga of cascade_power_rail_t is + pg : in; + end view; + alias cascade_power_rail_at_reg is cascade_power_rail_at_fpga'converse; + + -- Sequencing-related SP5 control/feedback pins + type sp5_seq_pins_t is record + thermtrip_l : std_logic; + reset_l : std_logic; + pwr_ok : std_logic; + slp_s3_l : std_logic; + slp_s5_l : std_logic; + rsmrst_l : std_logic; + pwr_btn_l : std_logic; + pwr_good : std_logic; + pwrgd_out : std_logic; + smerr_l : std_logic; + is_cosmo : std_logic; -- uses SP5_TO_FPGA1_DEBUG1 high at power up to indicate cosmo + end record; + -- FPGA's view of the world as the controller and recipient of + -- the feedback + view sp5_seq_at_fpga of sp5_seq_pins_t is + thermtrip_l : in; + reset_l : in; + pwr_ok : in; + slp_s3_l : in; + slp_s5_l : in; + smerr_l : in; + pwrgd_out : in; + rsmrst_l : out; + pwr_btn_l : out; + pwr_good : out; + is_cosmo : out; + end view; + alias sp5_seq_at_sp5 is sp5_seq_at_fpga'converse; + + type early_power_t is record + fan_central_hsc_pg : std_logic; + fan_east_hsc_pg : std_logic; + fan_fail : std_logic; + fan_west_hsc_pg : std_logic; + fan_central_hsc_disable : std_logic; + fan_east_hsc_disable : std_logic; + fan_west_hsc_disable : std_logic; + end record; + view early_power_at_fpga of early_power_t is + fan_central_hsc_pg : in; + fan_east_hsc_pg : in; + fan_west_hsc_pg : in; + fan_fail : in; + fan_central_hsc_disable : out; + fan_east_hsc_disable : out; + fan_west_hsc_disable : out; + end view; + alias early_power_on_board is early_power_at_fpga'converse; + + -- DDR 12V bulk power rail + type ddr_bulk_power_t is record + abcdef_hsc : power_rail_t; + ghijkl_hsc : power_rail_t; + end record; + function is_power_good(power_group: ddr_bulk_power_t) return boolean; + -- FPGA's view of the world as the controller and recipient of + -- the feedback + view ddr_bulk_power_at_fpga of ddr_bulk_power_t is + abcdef_hsc : view power_rail_at_fpga; + ghijkl_hsc : view power_rail_at_fpga; + end view; + alias ddr_bulk_at_reg is ddr_bulk_power_at_fpga'converse; + + -- SP5 group a rails + --A (G3/S5): VDDBT_RTC_G, VDD_18_S5,VDD_33_S5, VDDIO_AUDIO (shared as 1V8) + type group_a_power_t is record + pwr_v1p5_rtc : power_rail_t; + v3p3_sp5_a1 : power_rail_t; + v1p8_sp5_a1 : power_rail_t; + end record; + function is_power_good(power_group: group_a_power_t) return boolean; + view group_a_power_at_fpga of group_a_power_t is + pwr_v1p5_rtc : view power_rail_at_fpga; + v3p3_sp5_a1 : view power_rail_at_fpga; + v1p8_sp5_a1 : view power_rail_at_fpga; + end view; + alias group_a_power_at_reg is group_a_power_at_fpga'converse; + + -- B (S3): VDD_11_S3 + type group_b_power_t is record + v1p1_sp5 : power_rail_t; + end record; + function is_power_good(power_group: group_b_power_t) return boolean; + view group_b_power_at_fpga of group_b_power_t is + v1p1_sp5 : view power_rail_at_fpga; + end view; + alias group_b_power_at_reg is group_b_power_at_fpga'converse; + + + -- C (S0): VDDIO, VDDCR_SOC,VDDCR_CPU0, VDDCR_CPU1 + type group_c_power_t is record + vddio_sp5_a0 : power_rail_t; + vddcr_cpu1 : power_rail_t; + vddcr_cpu0 : power_rail_t; + vddcr_soc : power_rail_t; + end record; + function is_power_good(power_group: group_c_power_t) return boolean; + view group_c_power_at_fpga of group_c_power_t is + vddio_sp5_a0 : view power_rail_at_fpga; + vddcr_cpu1 : view power_rail_at_fpga; + vddcr_cpu0 : view power_rail_at_fpga; + vddcr_soc : view power_rail_at_fpga; + end view; + alias group_c_power_at_reg is group_c_power_at_fpga'converse; + +end package; + +package body sp5_power_pkg is + + -- Functions for DDR bulk + function is_power_good(power_group: ddr_bulk_power_t) return boolean is + begin + return (power_group.abcdef_hsc.pg and power_group.ghijkl_hsc.pg) = '1'; + end function; + + -- Functions for groupA + function is_power_good(power_group: group_a_power_t) return boolean is + begin + return (power_group.pwr_v1p5_rtc.pg and power_group.v3p3_sp5_a1.pg and power_group.v1p8_sp5_a1.pg) = '1'; + end function; + + -- Functions for groupB + function is_power_good(power_group: group_b_power_t) return boolean is + begin + return power_group.v1p1_sp5.pg = '1'; + end function; + + -- Functions for groupc + function is_power_good(power_group: group_c_power_t) return boolean is + begin + return (power_group.vddio_sp5_a0.pg and + power_group.vddcr_cpu1.pg and + power_group.vddcr_cpu0.pg and + power_group.vddcr_soc.pg + ) = '1'; + end function; + +end package body; diff --git a/hdl/projects/cosmo_seq/sequencer/sp5_sequencer.vhd b/hdl/projects/cosmo_seq/sequencer/sp5_sequencer.vhd index ce540f71..cac7b964 100644 --- a/hdl/projects/cosmo_seq/sequencer/sp5_sequencer.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sp5_sequencer.vhd @@ -8,6 +8,7 @@ use ieee.numeric_std.all; use work.axil8x32_pkg; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.sequencer_regs_pkg.all; From 2c592d15ca848a6b2c4bf6d981d6e5d3717c57f2 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Fri, 11 Sep 2026 15:44:54 -0400 Subject: [PATCH 03/12] cosmo_seq: expose fmc_clk_monitor as its own target --- hdl/projects/cosmo_seq/BUCK | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/hdl/projects/cosmo_seq/BUCK b/hdl/projects/cosmo_seq/BUCK index c58fbd34..243fa97d 100644 --- a/hdl/projects/cosmo_seq/BUCK +++ b/hdl/projects/cosmo_seq/BUCK @@ -35,11 +35,24 @@ vhdl_unit( visibility = ["PUBLIC"], ) +# Shared with metro_seq, which carries its own board_support but the same +# FMC clock arrangement. +vhdl_unit( + name = "fmc_clk_monitor", + srcs = ["board_support/fmc_clk_monitor.vhd"], + visibility = ["PUBLIC"], + standard = "2019", +) + vhdl_unit( name = "board_support", - srcs = glob(["board_support/*.vhd"], exclude = ["board_support/reset_sync.vhd"]), + srcs = glob(["board_support/*.vhd"], exclude = [ + "board_support/reset_sync.vhd", + "board_support/fmc_clk_monitor.vhd", + ]), deps = [ ":reset_sync", + ":fmc_clk_monitor", ":cosmo_black_boxes", "//hdl/ip/vhd/info:info", ], From 5882db0b1619b05add830fa3326c434b6619b0b7 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Fri, 11 Sep 2026 15:40:31 -0400 Subject: [PATCH 04/12] sp5_hotplug: fix the T6 power-enable guard; add an optional second NIC slot on bank 4 The T6 slot's power enable was gated on bank 0's direction bit -- M.2 A's -- rather than its own bank 2's, so whether the SP5 could power the NIC depended on how it had configured a different slot. Fix that before cloning the block. Metro's Versal presents two PCIe endpoints to the SP5, and the second one needs a hotplug slot of its own. PCA9506 bank 4 has always been wired into the expander but referenced by nothing; its registers reset inert, so giving it a slot costs no register-map change. The new slot is T6-shaped -- PERST a copy of PWR_EN, no oneshot -- behind a generic that cosmo leaves off, with the new ports defaulted so cosmo's port map is untouched. Both NIC slots also gain a presence input, defaulted to present, so metro can report the Versal's real PRSNT_L pins where cosmo tied the T6's low. --- hdl/ip/vhd/i2c/io_expanders/PCA9506ish/BUCK | 17 +- .../cosmo_seq/sp5_hotplug_subsystem/BUCK | 15 +- .../sims/sp5_hotplug_tb.vhd | 149 ++++++++++++++++++ .../sims/sp5_hotplug_th.vhd | 143 +++++++++++++++++ .../sp5_hotplug_subsystem.vhd | 43 ++++- 5 files changed, 358 insertions(+), 9 deletions(-) create mode 100644 hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sims/sp5_hotplug_tb.vhd create mode 100644 hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sims/sp5_hotplug_th.vhd diff --git a/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/BUCK b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/BUCK index 3baf8cb4..21778d07 100644 --- a/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/BUCK +++ b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/BUCK @@ -1,4 +1,4 @@ -load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim") +load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim", "sim_only_model") load("//tools:rdl.bzl", "rdl_file") rdl_file( @@ -19,11 +19,24 @@ vhdl_unit( visibility = ['PUBLIC'] ) +# The register-access helpers are useful to any testbench that talks to a +# PCA9506 over I2C, not just this one, so they are a target of their own. +sim_only_model( + name = "pca9506_sim_pkg", + srcs = ["sims/i2c_pca9506ish_sim_pkg.vhd"], + deps = [ + ":pca9506_regs_rdl", + "//hdl/ip/vhd/vunit_components:i2c_controller_vc", + ], + visibility = ['PUBLIC'], +) + vunit_sim( name = "i2c_pca9506ish_tb", - srcs = glob(["sims/**/*.vhd"]), + srcs = glob(["sims/**/*.vhd"], exclude = ["sims/i2c_pca9506ish_sim_pkg.vhd"]), deps = [ ":pca9506_top", + ":pca9506_sim_pkg", "//hdl/ip/vhd/vunit_components:i2c_controller_vc", "//hdl/ip/vhd/i2c/target:i2c_phy_consolidator", "//hdl/ip/vhd/axi_blocks:axilite_if_2k19", diff --git a/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/BUCK b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/BUCK index 140245e8..711b514c 100644 --- a/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/BUCK +++ b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/BUCK @@ -16,10 +16,23 @@ vhdl_unit( ], deps = [ ":perst_oneshot", + "//hdl/ip/vhd/common:time_pkg", "//hdl/ip/vhd/synchronizers:meta_sync", "//hdl/ip/vhd/axi_blocks:axist_if_2k19_pkg", "//hdl/ip/vhd/i2c/io_expanders/PCA9506ish:pca9506_top", ], standard = "2019", visibility = ["PUBLIC"], -) \ No newline at end of file +) +vunit_sim( + name = "sp5_hotplug_tb", + srcs = glob(["sims/*.vhd"]), + deps = [ + ":sp5_hotplug_subsystem", + "//hdl/ip/vhd/i2c/io_expanders/PCA9506ish:pca9506_sim_pkg", + "//hdl/ip/vhd/vunit_components:i2c_controller_vc", + "//hdl/ip/vhd/axi_blocks:axilite_if_2k19", + ], + visibility = ["PUBLIC"], + standard = "2019", +) diff --git a/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sims/sp5_hotplug_tb.vhd b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sims/sp5_hotplug_tb.vhd new file mode 100644 index 00000000..937393fb --- /dev/null +++ b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sims/sp5_hotplug_tb.vhd @@ -0,0 +1,149 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.numeric_std_unsigned.all; + +library vunit_lib; + context vunit_lib.com_context; + context vunit_lib.vunit_context; + context vunit_lib.vc_context; + +use work.pca9506_regs_pkg.all; +use work.i2c_pca9506ish_sim_pkg.all; + +-- Slot-level behaviour of the two NIC hotplug slots, as the SP5 sees them +-- through the emulated PCA9506: bank 2 (the T6 slot cosmo has always had) and +-- bank 4 (the second slot metro adds). The expander itself is covered by +-- i2c_pca9506ish_tb; this is about the wiring from its bits to the slot pins. +entity sp5_hotplug_tb is + generic ( + runner_cfg : string + ); +end entity; + +architecture tb of sp5_hotplug_tb is + constant hp_addr : std_logic_vector(6 downto 0) := b"0100_010"; + -- AMD Mode A: bit 4 of each bank is PWR_EN_L, an output from the SP5. + constant ALL_INPUTS : std_logic_vector(7 downto 0) := x"FF"; + constant PWR_EN_AS_OUTPUT : std_logic_vector(7 downto 0) := x"EF"; + constant PWR_EN_ASSERTED : std_logic_vector(7 downto 0) := x"00"; + constant PWR_EN_DEASSERTED : std_logic_vector(7 downto 0) := x"10"; +begin + + th: entity work.sp5_hotplug_th; + + bench: process + alias reset is << signal th.reset : std_logic >>; + alias t6_power_en is << signal th.t6_power_en : std_logic >>; + alias t6_perst_l is << signal th.t6_perst_l : std_logic >>; + alias t6_faulted is << signal th.t6_faulted : std_logic >>; + alias t6_prsnt_l is << signal th.t6_prsnt_l : std_logic >>; + alias nic2_power_en is << signal th.nic2_power_en : std_logic >>; + alias nic2_perst_l is << signal th.nic2_perst_l : std_logic >>; + alias nic2_faulted is << signal th.nic2_faulted : std_logic >>; + alias nic2_prsnt_l is << signal th.nic2_prsnt_l : std_logic >>; + variable ack_status : boolean; + constant rx_queue : queue_t := new_queue; + constant ack_queue : queue_t := new_queue; + variable ip : std_logic_vector(7 downto 0); + + -- Drive a bank's PWR_EN_L through the expander: configure bit 4 as an + -- output, then write it. + procedure set_pwr_en(bank : natural; asserted : boolean) is + begin + single_write_pca9506_reg(net, hp_addr, I2C_IOC0_OFFSET + bank, PWR_EN_AS_OUTPUT, ack_status); + if asserted then + single_write_pca9506_reg(net, hp_addr, I2C_OP0_OFFSET + bank, PWR_EN_ASSERTED, ack_status); + else + single_write_pca9506_reg(net, hp_addr, I2C_OP0_OFFSET + bank, PWR_EN_DEASSERTED, ack_status); + end if; + wait for 200 ns; + end procedure; + + procedure read_ip(bank : natural; variable value : out std_logic_vector(7 downto 0)) is + begin + read_pca9506_reg(net, hp_addr, I2C_IP0_OFFSET + bank, 1, rx_queue, ack_queue); + flush(ack_queue); + value := to_std_logic_vector(pop_byte(rx_queue), 8); + end procedure; + begin + test_runner_setup(runner, runner_cfg); + wait until reset = '0'; + wait for 500 ns; + + while test_suite loop + if run("nic_slot_power_enable_follows_expander") then + check_equal(t6_power_en, '0', "NIC slot must be off out of reset"); + set_pwr_en(2, true); + check_equal(t6_power_en, '1', "PWR_EN_L low should enable the NIC slot"); + check_equal(t6_perst_l, '1', "PERST follows the power enable on this slot"); + set_pwr_en(2, false); + check_equal(t6_power_en, '0', "PWR_EN_L high should disable the NIC slot"); + check_equal(t6_perst_l, '0', "PERST follows the power enable on this slot"); + + elsif run("nic2_slot_power_enable_follows_expander") then + check_equal(nic2_power_en, '0', "second NIC slot must be off out of reset"); + set_pwr_en(4, true); + check_equal(nic2_power_en, '1', "PWR_EN_L low should enable the second NIC slot"); + check_equal(nic2_perst_l, '1', "PERST follows the power enable on this slot"); + set_pwr_en(4, false); + check_equal(nic2_power_en, '0', "PWR_EN_L high should disable the second NIC slot"); + check_equal(nic2_perst_l, '0', "PERST follows the power enable on this slot"); + + elsif run("nic_slots_are_independent") then + set_pwr_en(2, true); + check_equal(nic2_power_en, '0', "enabling bank 2 must not enable bank 4"); + set_pwr_en(4, true); + check_equal(t6_power_en, '1', "bank 2 stays enabled when bank 4 is enabled"); + set_pwr_en(2, false); + check_equal(nic2_power_en, '1', "disabling bank 2 must not disable bank 4"); + + elsif run("power_enable_ignored_until_bank_is_configured") then + -- The guard used to look at bank 0's direction bit, so + -- configuring M.2 A as an output would have let a still-input + -- bank 2 turn the NIC on. Configure everything *but* bank 2 and + -- bank 4 as outputs and write their OP bits low: neither NIC + -- slot may move. + for bank in 0 to 3 loop + if bank /= 2 then + single_write_pca9506_reg(net, hp_addr, I2C_IOC0_OFFSET + bank, PWR_EN_AS_OUTPUT, ack_status); + end if; + end loop; + single_write_pca9506_reg(net, hp_addr, I2C_OP0_OFFSET + 2, PWR_EN_ASSERTED, ack_status); + single_write_pca9506_reg(net, hp_addr, I2C_OP0_OFFSET + 4, PWR_EN_ASSERTED, ack_status); + wait for 200 ns; + check_equal(t6_power_en, '0', "bank 2 still an input: NIC slot must stay off"); + check_equal(nic2_power_en, '0', "bank 4 still an input: second NIC slot must stay off"); + + elsif run("nic_slots_report_presence_and_fault") then + -- Both present, no fault: PRSNT_L low, PWRFLT_L high. + read_ip(2, ip); + check_equal(ip(0), '0', "bank 2 PRSNT_L should follow the presence input"); + check_equal(ip(1), '1', "bank 2 PWRFLT_L should be deasserted with no fault"); + read_ip(4, ip); + check_equal(ip(0), '0', "bank 4 PRSNT_L should follow the presence input"); + check_equal(ip(1), '1', "bank 4 PWRFLT_L should be deasserted with no fault"); + + t6_prsnt_l <= '1'; + nic2_faulted <= '1'; + wait for 200 ns; + read_ip(2, ip); + check_equal(ip(0), '1', "bank 2 PRSNT_L should deassert when the NIC is absent"); + read_ip(4, ip); + check_equal(ip(1), '0', "bank 4 PWRFLT_L should assert on a fault"); + t6_prsnt_l <= '0'; + nic2_faulted <= '0'; + end if; + end loop; + + wait for 2 us; + test_runner_cleanup(runner); + wait; + end process; + + test_runner_watchdog(runner, 10 ms); +end tb; diff --git a/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sims/sp5_hotplug_th.vhd b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sims/sp5_hotplug_th.vhd new file mode 100644 index 00000000..c95a0c6b --- /dev/null +++ b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sims/sp5_hotplug_th.vhd @@ -0,0 +1,143 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; + context vunit_lib.vc_context; + +use work.axil8x32_pkg; +use work.i2c_ctrl_vc_pkg.all; +use work.i2c_pca9506ish_sim_pkg.all; + +-- One hotplug subsystem with the second NIC slot enabled, driven over I2C the +-- way the SP5 drives it. The slot pins are exposed as signals for the tb to +-- read and poke via external names. +entity sp5_hotplug_th is +end entity; + +architecture th of sp5_hotplug_th is + + signal clk : std_logic := '0'; + signal reset : std_logic := '1'; + + signal i2c_bus_scl : std_logic; + signal i2c_bus_sda : std_logic; + signal scl_o, scl_oe, sda_o, sda_oe : std_logic; + signal axi_if : axil8x32_pkg.axil_t; + + signal int_n : std_logic; + signal a0_ok : std_logic := '1'; + + -- NIC slot, bank 2 + signal t6_power_en : std_logic; + signal t6_perst_l : std_logic; + signal t6_faulted : std_logic := '0'; + signal t6_prsnt_l : std_logic := '0'; + -- Second NIC slot, bank 4 + signal nic2_power_en : std_logic; + signal nic2_perst_l : std_logic; + signal nic2_faulted : std_logic := '0'; + signal nic2_prsnt_l : std_logic := '0'; + + -- The M.2 and backplane slots are not under test; park their inputs. + signal m2a_hsc_en, m2a_perst_l, pcie_clk_buff_m2a_oe_l : std_logic; + signal m2b_hsc_en, m2b_perst_l, pcie_clk_buff_m2b_oe_l : std_logic; + signal pcie_aux_rsw_perst_l, pcie_clk_buff_rsw_oe_l : std_logic; + +begin + + clk <= not clk after 4 ns; + reset <= '0' after 200 ns; + + axi_lite_master_inst: entity vunit_lib.axi_lite_master + generic map ( + bus_handle => bus_handle + ) + port map ( + aclk => clk, + arready => axi_if.read_address.ready, + arvalid => axi_if.read_address.valid, + araddr => axi_if.read_address.addr, + rready => axi_if.read_data.ready, + rvalid => axi_if.read_data.valid, + rdata => axi_if.read_data.data, + rresp => axi_if.read_data.resp, + awready => axi_if.write_address.ready, + awvalid => axi_if.write_address.valid, + awaddr => axi_if.write_address.addr, + wready => axi_if.write_data.ready, + wvalid => axi_if.write_data.valid, + wdata => axi_if.write_data.data, + wstrb => axi_if.write_data.strb, + bvalid => axi_if.write_response.valid, + bready => axi_if.write_response.ready, + bresp => axi_if.write_response.resp + ); + + i2c_controller_vc_inst: entity work.i2c_controller_vc + generic map( + i2c_ctrl_vc => i2c_ctrl_vc + ) + port map( + scl => i2c_bus_scl, + sda => i2c_bus_sda + ); + + -- Open-drain resolution for the one target on the bus + i2c_bus_scl <= scl_o when scl_oe = '1' else 'H'; + i2c_bus_sda <= sda_o when sda_oe = '1' else 'H'; + + dut: entity work.sp5_hotplug_subsystem + generic map( + -- Keep the PERST oneshots short so the M.2 slots settle in sim time + PERST_US_ONESHOT => 10, + NS_PER_CLK => 8, + NIC2_SLOT_ENABLED => true + ) + port map( + clk => clk, + reset => reset, + sp5_i2c_sda => i2c_bus_sda, + sp5_i2c_sda_o => sda_o, + sp5_i2c_sda_oe => sda_oe, + sp5_i2c_scl => i2c_bus_scl, + sp5_i2c_scl_o => scl_o, + sp5_i2c_scl_oe => scl_oe, + int_n => int_n, + a0_ok => a0_ok, + axi_if => axi_if, + allow_backplane_pcie_clk => '0', + m2a_pedet => '0', + m2a_prsnt_l => '1', + m2a_hsc_en => m2a_hsc_en, + m2a_perst_l => m2a_perst_l, + pcie_clk_buff_m2a_oe_l => pcie_clk_buff_m2a_oe_l, + m2a_pwr_fault_l => '1', + m2b_pedet => '0', + m2b_prsnt_l => '1', + m2b_hsc_en => m2b_hsc_en, + m2b_perst_l => m2b_perst_l, + pcie_clk_buff_m2b_oe_l => pcie_clk_buff_m2b_oe_l, + m2b_pwr_fault_l => '1', + t6_power_en => t6_power_en, + t6_perst_l => t6_perst_l, + t6_faulted => t6_faulted, + t6_prsnt_l => t6_prsnt_l, + nic2_power_en => nic2_power_en, + nic2_perst_l => nic2_perst_l, + nic2_faulted => nic2_faulted, + nic2_prsnt_l => nic2_prsnt_l, + pcie_aux_rsw_perst_l => pcie_aux_rsw_perst_l, + pcie_aux_rsw_prsnt_buff_l => '1', + pcie_aux_rsw_pwrflt_buff_l => '1', + pcie_clk_buff_rsw_oe_l => pcie_clk_buff_rsw_oe_l, + rsw_sp5_pcie_attached_buff_l => '1' + ); + +end th; diff --git a/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sp5_hotplug_subsystem.vhd b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sp5_hotplug_subsystem.vhd index 8c4c4a92..8454baf0 100644 --- a/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sp5_hotplug_subsystem.vhd +++ b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sp5_hotplug_subsystem.vhd @@ -15,7 +15,12 @@ use work.time_pkg.all; -- for calc_ms and calc_us entity sp5_hotplug_subsystem is generic( PERST_US_ONESHOT : integer := 100000; -- 100ms for Tpvperl, this is used in the oneshot - NS_PER_CLK : integer := 8 + NS_PER_CLK : integer := 8; + -- Metro's NIC presents a second PCIe endpoint to the SP5 that needs a + -- hotplug slot of its own. It goes on PCA9506 bank 4, which has always + -- been wired into the expander and referenced by nothing. Cosmo leaves + -- this off; the bank then stays exactly as inert as it is today. + NIC2_SLOT_ENABLED : boolean := false ); port( clk : in std_logic; @@ -50,10 +55,19 @@ entity sp5_hotplug_subsystem is pcie_clk_buff_m2b_oe_l : out std_logic; m2b_pwr_fault_l : in std_logic; - -- T6 things + -- NIC slot on bank 2. The T6 on cosmo, the Versal's channel A on metro. + -- Presence defaults to asserted, which is the tie cosmo has always had. t6_power_en : out std_logic; t6_perst_l : out std_logic; t6_faulted : in std_logic; + t6_prsnt_l : in std_logic := '0'; + + -- Second NIC slot on bank 4, only when NIC2_SLOT_ENABLED. Same shape as + -- the T6 slot: PERST is a copy of the power enable, no oneshot. + nic2_power_en : out std_logic; + nic2_perst_l : out std_logic; + nic2_faulted : in std_logic := '0'; + nic2_prsnt_l : in std_logic := '0'; -- Sidecar things pcie_aux_rsw_perst_l : out std_logic; @@ -175,14 +189,31 @@ begin perst_l => m2b_perst_l ); - -- T6 - t6_power_en <= not io_o(2)(4) when io_oe(0)(4) else '0'; - io(2)(3) <= '1'; -- PEDET for T6 + -- NIC slot, bank 2 (the T6 on cosmo). The power enable only counts once the + -- SP5 has configured this bank's bit 4 as an output; until then the pull + -- default is "off". + t6_power_en <= not io_o(2)(4) when io_oe(2)(4) else '0'; + io(2)(3) <= '1'; -- emils: nothing to report for a soldered-down NIC io(2)(1) <= not t6_faulted; io(2)(2) <= '1'; -- attnsw_l - io(2)(0) <= '0'; -- PRSNT_L for T6 + io(2)(0) <= t6_prsnt_l; t6_perst_l <= t6_power_en; + -- Second NIC slot, bank 4. Only metro populates this; with the generic off + -- the bank reads back its zero initialiser and the outputs stay parked, so + -- an SP5 that pokes bank 4 on cosmo sees what it always has. + nic2_slot: if NIC2_SLOT_ENABLED generate + nic2_power_en <= not io_o(4)(4) when io_oe(4)(4) else '0'; + io(4)(3) <= '1'; -- emils + io(4)(1) <= not nic2_faulted; + io(4)(2) <= '1'; -- attnsw_l + io(4)(0) <= nic2_prsnt_l; + nic2_perst_l <= nic2_power_en; + else generate + nic2_power_en <= '0'; + nic2_perst_l <= '0'; + end generate; + -- Backplane connected switch pcie_aux_power_en <= not io_o(3)(4) when io_oe(3)(4) else '0'; pcie_perst_oneshot: entity work.perst_oneshot From f1d24ca3ec7e38412dc0fb17ad42e174c021f242 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Fri, 11 Sep 2026 15:47:59 -0400 Subject: [PATCH 05/12] hash_engine: own the flash client FIFOs; NUM_FLASHES generic --- hdl/ip/vhd/hash_engine/hash_engine_regs.rdl | 3 + hdl/ip/vhd/hash_engine/hash_engine_top.vhd | 106 +++++++- hdl/ip/vhd/hash_engine/hash_feeder.vhd | 20 +- .../vhd/hash_engine/sims/hash_engine_tb.vhd | 20 ++ .../vhd/hash_engine/sims/hash_engine_th.vhd | 89 ++----- .../vhd/hash_engine/sims/hash_spi_nor_tb.vhd | 44 +++- .../vhd/hash_engine/sims/hash_spi_nor_th.vhd | 235 ++++++++---------- hdl/projects/cosmo_seq/BUCK | 1 + hdl/projects/cosmo_seq/cosmo_seq_top.vhd | 31 ++- .../cosmo_seq/sp5_espi_flash_subsystem/BUCK | 1 - .../sp5_espi_flash_subsystem.vhd | 82 +----- hdl/projects/grapefruit/BUCK | 14 +- hdl/projects/grapefruit/grapefruit_top.vhd | 73 +----- .../grapefruit/sims/grapefruit_top_tb.vhd | 33 +++ 14 files changed, 398 insertions(+), 354 deletions(-) create mode 100644 hdl/projects/grapefruit/sims/grapefruit_top_tb.vhd diff --git a/hdl/ip/vhd/hash_engine/hash_engine_regs.rdl b/hdl/ip/vhd/hash_engine/hash_engine_regs.rdl index ac09dbe2..24c8cf89 100644 --- a/hdl/ip/vhd/hash_engine/hash_engine_regs.rdl +++ b/hdl/ip/vhd/hash_engine/hash_engine_regs.rdl @@ -40,6 +40,9 @@ addrmap hash_engine_regs { enum source_select { LOCAL_REG = 4'h0 {desc = "Bytes come from processor writes to WDATA";}; HOST_QSPI = 4'h1 {desc = "Bytes are fetched from the host QSPI flash starting at FLASH_ADDR";}; + AUX_QSPI = 4'h2 {desc = "Bytes are fetched from the second QSPI flash starting at + FLASH_ADDR. Only designs with two flashes have one; elsewhere a start with + this source is refused as a configuration error.";}; }; field { diff --git a/hdl/ip/vhd/hash_engine/hash_engine_top.vhd b/hdl/ip/vhd/hash_engine/hash_engine_top.vhd index a6b7ff13..ca07c10d 100644 --- a/hdl/ip/vhd/hash_engine/hash_engine_top.vhd +++ b/hdl/ip/vhd/hash_engine/hash_engine_top.vhd @@ -28,6 +28,12 @@ use work.keccak_pkg.all; -- reset. This block never asks for it to be flushed: an abandoned read is dealt -- with by consuming the bytes still owed, see hash_feeder. entity hash_engine_top is + generic ( + -- How many spi_nor flash clients hang off this engine. CONFIG.source + -- picks between them for a run; AUX_QSPI is a configuration error + -- when there is only one. + NUM_FLASHES : natural range 1 to 2 := 1 + ); port ( clk : in std_logic; reset : in std_logic; @@ -35,14 +41,20 @@ entity hash_engine_top is -- Axilite interface axi_if : view axil_target; - -- Flash read command FIFO: word 0 is a byte address, word 1 a byte count - cmd_fifo_wdata : out std_logic_vector(31 downto 0); - cmd_fifo_write : out std_logic; - - -- Flash read response FIFO, showahead so rdack is a read acknowledge - rsp_fifo_rdata : in std_logic_vector(7 downto 0); - rsp_fifo_rdack : out std_logic; - rsp_fifo_rempty : in std_logic + -- The spi_nor side of the engine's own command and response FIFOs, one + -- pair per flash. These match spi_nor_top's hash client port shape: + -- the flash pops commands (word 0 a byte address, word 1 a byte count) + -- and pushes response bytes. Only the flash selected for the run in + -- flight ever sees a non-empty command FIFO, so the others sit idle. + flash_cmd_rdata : out std_logic_vector(31 downto 0); + flash_cmd_rdack : in std_logic_vector(NUM_FLASHES - 1 downto 0); + flash_cmd_rempty : out std_logic_vector(NUM_FLASHES - 1 downto 0); + flash_rsp_wdata : in std_logic_vector(NUM_FLASHES * 8 - 1 downto 0); + flash_rsp_write : in std_logic_vector(NUM_FLASHES - 1 downto 0); + -- Backpressure for clients that honour it. spi_nor_top does not (its + -- raw_flash_txn_mgr paces itself off the SPI link), but a behavioural + -- responder in simulation can push a byte a cycle and needs it. + flash_rsp_wfull : out std_logic_vector(NUM_FLASHES - 1 downto 0) ); end entity; @@ -77,6 +89,20 @@ architecture rtl of hash_engine_top is signal digest : digest_t; signal digest_valid : std_logic; + -- Feeder side of the flash client FIFOs + signal cmd_fifo_wdata : std_logic_vector(31 downto 0); + signal cmd_fifo_write : std_logic; + signal cmd_fifo_rdack : std_logic; + signal cmd_fifo_rempty : std_logic; + signal rsp_fifo_wdata : std_logic_vector(7 downto 0); + signal rsp_fifo_write : std_logic; + signal rsp_fifo_rdata : std_logic_vector(7 downto 0); + signal rsp_fifo_rdack : std_logic; + signal rsp_fifo_rempty : std_logic; + signal rsp_fifo_wfull : std_logic; + -- Which flash the run in flight is reading, latched by the feeder at start + signal flash_sel : natural range 0 to NUM_FLASHES - 1; + begin hash_engine_regs_inst: entity work.hash_engine_regs @@ -123,6 +149,9 @@ begin ); hash_feeder_inst: entity work.hash_feeder + generic map ( + NUM_FLASHES => NUM_FLASHES + ) port map ( clk => clk, reset => reset, @@ -144,6 +173,7 @@ begin sw_fifo_rdack => sw_fifo_rdack, sw_fifo_rempty => sw_fifo_rempty, sw_fifo_clear => sw_clear, + flash_sel => flash_sel, cmd_fifo_wdata => cmd_fifo_wdata, cmd_fifo_write => cmd_fifo_write, rsp_fifo_rdata => rsp_fifo_rdata, @@ -151,6 +181,66 @@ begin rsp_fifo_rempty => rsp_fifo_rempty ); + -- Flash client FIFOs. One pair serves every flash: the selected flash is + -- the only one shown a non-empty command FIFO and the only one whose + -- response writes are taken, so the FIFOs never see two clients at once. + -- flash_sel holds still for the whole run, which is what lets this be a + -- plain mux rather than an arbiter. + cmd_fifo: entity work.dcfifo_xpm + generic map ( + fifo_write_depth => 256, + data_width => 32, + showahead_mode => true + ) + port map ( + wclk => clk, + reset => reset, + write_en => cmd_fifo_write, + wdata => cmd_fifo_wdata, + wfull => open, + wusedwds => open, + rclk => clk, + rdata => flash_cmd_rdata, + rdreq => cmd_fifo_rdack, + rempty => cmd_fifo_rempty, + rusedwds => open + ); + + rsp_fifo: entity work.dcfifo_xpm + generic map ( + fifo_write_depth => 256, + data_width => 8, + showahead_mode => true + ) + port map ( + wclk => clk, + reset => reset, + write_en => rsp_fifo_write, + wdata => rsp_fifo_wdata, + wfull => rsp_fifo_wfull, + wusedwds => open, + rclk => clk, + rdata => rsp_fifo_rdata, + rdreq => rsp_fifo_rdack, + rempty => rsp_fifo_rempty, + rusedwds => open + ); + + flash_mux: process(all) + begin + cmd_fifo_rdack <= flash_cmd_rdack(flash_sel); + rsp_fifo_wdata <= flash_rsp_wdata(flash_sel * 8 + 7 downto flash_sel * 8); + rsp_fifo_write <= flash_rsp_write(flash_sel); + flash_rsp_wfull <= (others => rsp_fifo_wfull); + for i in 0 to NUM_FLASHES - 1 loop + if i = flash_sel then + flash_cmd_rempty(i) <= cmd_fifo_rempty; + else + flash_cmd_rempty(i) <= '1'; + end if; + end loop; + end process; + -- Also report full while the FIFO is being flushed at the tail of a run, so a -- processor that polls before writing cannot push bytes into a FIFO that is in -- reset. The flush only happens when a run ends, never as one starts, which is diff --git a/hdl/ip/vhd/hash_engine/hash_feeder.vhd b/hdl/ip/vhd/hash_engine/hash_feeder.vhd index 2407f70a..565f98b0 100644 --- a/hdl/ip/vhd/hash_engine/hash_feeder.vhd +++ b/hdl/ip/vhd/hash_engine/hash_feeder.vhd @@ -34,6 +34,9 @@ use work.hash_engine_regs_pkg.all; -- removes the race entirely. It also means data written before the first start is -- kept, so pre-loading works. entity hash_feeder is + generic ( + NUM_FLASHES : natural range 1 to 2 := 1 + ); port ( clk : in std_logic; reset : in std_logic; @@ -77,6 +80,9 @@ entity hash_feeder is -- hangs waiting for them. sw_fifo_clear : out std_logic; + -- Which flash the run in flight reads from, held for the whole run + flash_sel : out natural range 0 to NUM_FLASHES - 1; + -- Flash command FIFO: word 0 is the byte address, word 1 the byte count cmd_fifo_wdata : out std_logic_vector(31 downto 0); cmd_fifo_write : out std_logic; @@ -108,6 +114,7 @@ architecture rtl of hash_feeder is fed : unsigned(31 downto 0); addr : std_logic_vector(31 downto 0); src_qspi : std_logic; + flash_sel : natural range 0 to NUM_FLASHES - 1; clear_cnt : natural range 0 to CLEAR_CYCLES; -- Set when the flush should be followed by a new run rather than idling restart : std_logic; @@ -132,6 +139,7 @@ architecture rtl of hash_feeder is fed => (others => '0'), addr => (others => '0'), src_qspi => '0', + flash_sel => 0, clear_cnt => 0, restart => '0', finished => '0', @@ -185,6 +193,7 @@ begin done <= r.done; aborted <= r.aborted; cfg_err <= r.cfg_err; + flash_sel <= r.flash_sel; bytes_fed <= std_logic_vector(r.fed); sha3_init <= r.init; @@ -205,11 +214,13 @@ begin stop_run := false; -- A start is refused outright if the configuration cannot produce a - -- message: the core has no way to express a zero length one, and a prepend - -- longer than the message is simply nonsense. + -- message: the core has no way to express a zero length one, a prepend + -- longer than the message is simply nonsense, and the second flash can + -- only be asked for on a design that has one. accepted := start_strobe = '1' and unsigned(msg_length.count) /= 0 and - unsigned(prepend.count) <= unsigned(msg_length.count); + unsigned(prepend.count) <= unsigned(msg_length.count) and + not (cfg.source = AUX_QSPI and NUM_FLASHES = 1); if start_strobe = '1' and not accepted then v.cfg_err := '1'; @@ -222,7 +233,8 @@ begin v.prepend_cnt := unsigned(prepend.count); v.flash_req := unsigned(msg_length.count) - unsigned(prepend.count); v.addr := flash_addr.addr; - v.src_qspi := '1' when cfg.source = HOST_QSPI else '0'; + v.src_qspi := '1' when cfg.source = HOST_QSPI or cfg.source = AUX_QSPI else '0'; + v.flash_sel := 1 when cfg.source = AUX_QSPI and NUM_FLASHES > 1 else 0; v.cfg_err := '0'; v.aborted := '0'; v.done := '0'; diff --git a/hdl/ip/vhd/hash_engine/sims/hash_engine_tb.vhd b/hdl/ip/vhd/hash_engine/sims/hash_engine_tb.vhd index cc57506e..86ee1df4 100644 --- a/hdl/ip/vhd/hash_engine/sims/hash_engine_tb.vhd +++ b/hdl/ip/vhd/hash_engine/sims/hash_engine_tb.vhd @@ -336,6 +336,26 @@ begin -- and the engine still works afterwards run_local(0, 32, "hash after a rejected start"); + elsif run("reject_aux_on_single_flash") then + -- This bench builds the engine with the default NUM_FLASHES of + -- one, so asking for the second flash is a configuration error + -- like any other, and the engine must not go busy on it. + write_reg(net, CONFIG_OFFSET, pack(config_type'(source => AUX_QSPI))); + write_reg(net, PREPEND_OFFSET, To_StdLogicVector(0, 32)); + write_reg(net, LENGTH_OFFSET, To_StdLogicVector(64, 32)); + write_reg(net, FLASH_ADDR_OFFSET, To_StdLogicVector(0, 32)); + write_reg(net, CONTROL_OFFSET, START_CMD); + wait for 2 us; + + read_reg(net, STATUS_OFFSET, status); + check_equal((status and STATUS_CFG_ERR_MASK) /= (status'range => '0'), true, + "AUX_QSPI on a single-flash engine should set cfg_err"); + check_equal((status and STATUS_BUSY_MASK) = (status'range => '0'), true, + "should never go busy"); + + -- and the engine still works afterwards + run_local(0, 32, "hash after a rejected aux start"); + elsif run("reject_prepend_gt_length") then configure(CFG_LOCAL, 100, 50, 0); write_reg(net, CONTROL_OFFSET, START_CMD); diff --git a/hdl/ip/vhd/hash_engine/sims/hash_engine_th.vhd b/hdl/ip/vhd/hash_engine/sims/hash_engine_th.vhd index e24c20fc..178136f5 100644 --- a/hdl/ip/vhd/hash_engine/sims/hash_engine_th.vhd +++ b/hdl/ip/vhd/hash_engine/sims/hash_engine_th.vhd @@ -14,10 +14,9 @@ library vunit_lib; use work.axil8x32_pkg; use work.hash_engine_sim_pkg.all; --- The command and response FIFOs are real dcfifo_xpm instances here, not --- behavioural stand-ins, because an integrating design owns them exactly like the --- eSPI subsystem does. Neither is reset by the DUT: the engine resynchronises the --- response channel by draining it, not by flushing. +-- The engine owns its command and response FIFOs, so this harness only supplies +-- what sits on their far side: a behavioural flash responder where spi_nor_top +-- would be. hash_spi_nor_tb is the one that goes through the real controller. entity hash_engine_th is end entity; @@ -28,18 +27,13 @@ architecture th of hash_engine_th is signal axi_bus : axil8x32_pkg.axil_t; - signal cmd_fifo_wdata : std_logic_vector(31 downto 0); - signal cmd_fifo_write : std_logic; signal cmd_fifo_rdata : std_logic_vector(31 downto 0); - signal cmd_fifo_rdack : std_logic; - signal cmd_fifo_empty : std_logic; + signal cmd_fifo_rdack : std_logic_vector(0 downto 0); + signal cmd_fifo_empty : std_logic_vector(0 downto 0); signal rsp_fifo_wdata : std_logic_vector(7 downto 0); - signal rsp_fifo_write : std_logic; - signal rsp_fifo_wfull : std_logic; - signal rsp_fifo_rdata : std_logic_vector(7 downto 0); - signal rsp_fifo_rdack : std_logic; - signal rsp_fifo_empty : std_logic; + signal rsp_fifo_write : std_logic_vector(0 downto 0); + signal rsp_fifo_wfull : std_logic_vector(0 downto 0); begin @@ -74,66 +68,29 @@ begin dut: entity work.hash_engine_top port map ( - clk => clk, - reset => reset, - axi_if => axi_bus, - cmd_fifo_wdata => cmd_fifo_wdata, - cmd_fifo_write => cmd_fifo_write, - rsp_fifo_rdata => rsp_fifo_rdata, - rsp_fifo_rdack => rsp_fifo_rdack, - rsp_fifo_rempty => rsp_fifo_empty - ); - - cmd_fifo: entity work.dcfifo_xpm - generic map ( - fifo_write_depth => 256, - data_width => 32, - showahead_mode => true - ) - port map ( - wclk => clk, - reset => reset, - write_en => cmd_fifo_write, - wdata => cmd_fifo_wdata, - wfull => open, - wusedwds => open, - rclk => clk, - rdata => cmd_fifo_rdata, - rdreq => cmd_fifo_rdack, - rempty => cmd_fifo_empty, - rusedwds => open - ); - - rsp_fifo: entity work.dcfifo_xpm - generic map ( - fifo_write_depth => 256, - data_width => 8, - showahead_mode => true - ) - port map ( - wclk => clk, - reset => reset, - write_en => rsp_fifo_write, - wdata => rsp_fifo_wdata, - wfull => rsp_fifo_wfull, - wusedwds => open, - rclk => clk, - rdata => rsp_fifo_rdata, - rdreq => rsp_fifo_rdack, - rempty => rsp_fifo_empty, - rusedwds => open + clk => clk, + reset => reset, + axi_if => axi_bus, + flash_cmd_rdata => cmd_fifo_rdata, + flash_cmd_rdack => cmd_fifo_rdack, + flash_cmd_rempty => cmd_fifo_empty, + flash_rsp_wdata => rsp_fifo_wdata, + flash_rsp_write => rsp_fifo_write, + flash_rsp_wfull => rsp_fifo_wfull ); + -- The engine owns the command and response FIFOs now; the responder sits + -- directly on their far ends, where spi_nor_top would in a real design. fake_flash: entity work.fake_flash_responder port map ( clk => clk, reset => reset, cmd_rdata => cmd_fifo_rdata, - cmd_rdack => cmd_fifo_rdack, - cmd_rempty => cmd_fifo_empty, - rsp_wdata => rsp_fifo_wdata, - rsp_write => rsp_fifo_write, - rsp_wfull => rsp_fifo_wfull + cmd_rdack => cmd_fifo_rdack(0), + cmd_rempty => cmd_fifo_empty(0), + rsp_wdata => rsp_fifo_wdata(7 downto 0), + rsp_write => rsp_fifo_write(0), + rsp_wfull => rsp_fifo_wfull(0) ); end th; diff --git a/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_tb.vhd b/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_tb.vhd index a11d1c85..562ad754 100644 --- a/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_tb.vhd +++ b/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_tb.vhd @@ -38,6 +38,8 @@ architecture tb of hash_spi_nor_tb is pack(control_type'(abort => '0', start => '1')); constant CFG_QSPI : std_logic_vector(31 downto 0) := pack(config_type'(source => HOST_QSPI)); + constant CFG_AUX : std_logic_vector(31 downto 0) := + pack(config_type'(source => AUX_QSPI)); begin @@ -46,7 +48,11 @@ begin bench: process alias reset is << signal th.reset : std_logic >>; - constant flash_actor : actor_t := find("spi_nor_target"); + constant flash_actor : actor_t := find("spi_nor_target0"); + -- The auxiliary flash is never filled, so it reads as erased 0xFF + -- everywhere. That is what tells a hash of it apart from the same + -- range on the host flash. + constant aux_actor : actor_t := find("spi_nor_target1"); variable status : std_logic_vector(31 downto 0); variable rdata : std_logic_vector(31 downto 0); @@ -65,11 +71,12 @@ begin end function; -- The message the engine should end up hashing: the 0xFF run, then the - -- flash from base_addr on. + -- flash from base_addr on. On the aux flash every byte is erased. impure function expected_msg ( prepend : natural; nbytes : natural; - base_addr : natural + base_addr : natural; + aux : boolean := false ) return queue_t is variable q : queue_t := new_queue; begin @@ -78,7 +85,11 @@ begin end loop; for i in 0 to nbytes - 1 loop - push_byte(q, to_integer(unsigned(flash_content(base_addr + i)))); + if aux then + push_byte(q, 16#FF#); + else + push_byte(q, to_integer(unsigned(flash_content(base_addr + i)))); + end if; end loop; return q; @@ -88,15 +99,20 @@ begin prepend : natural; nbytes : natural; base_addr : natural; - name : string + name : string; + aux : boolean := false ) is variable e : digest_t; variable s : std_logic_vector(31 downto 0); variable d : std_logic_vector(255 downto 0); begin - e := sha3_256_digest(expected_msg(prepend, nbytes, base_addr)); + e := sha3_256_digest(expected_msg(prepend, nbytes, base_addr, aux)); - write_reg(net, CONFIG_OFFSET, CFG_QSPI); + if aux then + write_reg(net, CONFIG_OFFSET, CFG_AUX); + else + write_reg(net, CONFIG_OFFSET, CFG_QSPI); + end if; write_reg(net, PREPEND_OFFSET, To_StdLogicVector(prepend, 32)); write_reg(net, LENGTH_OFFSET, To_StdLogicVector(prepend + nbytes, 32)); write_reg(net, FLASH_ADDR_OFFSET, To_StdLogicVector(base_addr, 32)); @@ -172,6 +188,20 @@ begin run_hash(0, 128, 16#1000#, "first fetch"); run_hash(0, 300, 16#3000#, "second fetch, crossing a chunk boundary"); run_hash(0, 64, 16#5000#, "third fetch"); + + elsif run("aux_flash_source") then + -- Same range on the other flash. The aux part is erased, so a + -- fetch that quietly went to the host flash would come back + -- with the pattern and fail the digest. + run_hash(0, 600, 16#1000#, "600 bytes from the aux flash", aux => true); + + elsif run("host_after_aux") then + -- The selection is latched per run; make sure it releases the + -- aux flash and the host flash's channel is still in step after + -- a run that never touched it. + run_hash(0, 300, 16#2000#, "aux first", aux => true); + run_hash(0, 300, 16#2000#, "then host"); + run_hash(0, 700, 16#1234#, "then aux again, unaligned", aux => true); end if; end loop; diff --git a/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_th.vhd b/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_th.vhd index e8827b89..94f8783c 100644 --- a/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_th.vhd +++ b/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_th.vhd @@ -38,35 +38,35 @@ end entity; architecture th of hash_spi_nor_th is + -- Two flashes, indexed the way the engine's flash ports are: 0 is the + -- host's, 1 the auxiliary one CONFIG.source = AUX_QSPI selects. + constant NUM_FLASHES : natural := 2; + signal clk : std_logic := '0'; signal reset : std_logic := '1'; signal axi_bus : axil8x32_pkg.axil_t; - signal spinor_axi : axil8x32_pkg.axil_t; + type spinor_axi_t is array (0 to NUM_FLASHES - 1) of axil8x32_pkg.axil_t; + signal spinor_axi : spinor_axi_t; - signal cmd_fifo_wdata : std_logic_vector(31 downto 0); - signal cmd_fifo_write : std_logic; signal cmd_fifo_rdata : std_logic_vector(31 downto 0); - signal cmd_fifo_rdack : std_logic; - signal cmd_fifo_empty : std_logic; - - signal rsp_fifo_wdata : std_logic_vector(7 downto 0); - signal rsp_fifo_write : std_logic; - signal rsp_fifo_rdata : std_logic_vector(7 downto 0); - signal rsp_fifo_rdack : std_logic; - signal rsp_fifo_empty : std_logic; - - signal cs_n : std_logic; - signal sclk : std_logic; - signal io : std_logic_vector(3 downto 0); - signal io_o : std_logic_vector(3 downto 0); - signal io_oe : std_logic_vector(3 downto 0); - - signal flash_o : std_logic_vector(3 downto 0); - signal flash_oe : std_logic_vector(3 downto 0); - signal io_flash : std_logic_vector(3 downto 0); - signal sclk_flash : std_logic; - signal csn_flash : std_logic; + signal cmd_fifo_rdack : std_logic_vector(NUM_FLASHES - 1 downto 0); + signal cmd_fifo_empty : std_logic_vector(NUM_FLASHES - 1 downto 0); + signal rsp_fifo_wdata : std_logic_vector(NUM_FLASHES * 8 - 1 downto 0); + signal rsp_fifo_write : std_logic_vector(NUM_FLASHES - 1 downto 0); + + type lanes_t is array (0 to NUM_FLASHES - 1) of std_logic_vector(3 downto 0); + signal cs_n : std_logic_vector(NUM_FLASHES - 1 downto 0); + signal sclk : std_logic_vector(NUM_FLASHES - 1 downto 0); + signal io : lanes_t; + signal io_o : lanes_t; + signal io_oe : lanes_t; + + signal flash_o : lanes_t; + signal flash_oe : lanes_t; + signal io_flash : lanes_t; + signal sclk_flash : std_logic_vector(NUM_FLASHES - 1 downto 0); + signal csn_flash : std_logic_vector(NUM_FLASHES - 1 downto 0); begin @@ -99,120 +99,91 @@ begin ); dut: entity work.hash_engine_top - port map ( - clk => clk, - reset => reset, - axi_if => axi_bus, - cmd_fifo_wdata => cmd_fifo_wdata, - cmd_fifo_write => cmd_fifo_write, - rsp_fifo_rdata => rsp_fifo_rdata, - rsp_fifo_rdack => rsp_fifo_rdack, - rsp_fifo_rempty => rsp_fifo_empty - ); - - cmd_fifo: entity work.dcfifo_xpm - generic map ( - fifo_write_depth => 256, - data_width => 32, - showahead_mode => true - ) - port map ( - wclk => clk, - reset => reset, - write_en => cmd_fifo_write, - wdata => cmd_fifo_wdata, - wfull => open, - wusedwds => open, - rclk => clk, - rdata => cmd_fifo_rdata, - rdreq => cmd_fifo_rdack, - rempty => cmd_fifo_empty, - rusedwds => open - ); - - rsp_fifo: entity work.dcfifo_xpm generic map ( - fifo_write_depth => 256, - data_width => 8, - showahead_mode => true + NUM_FLASHES => NUM_FLASHES ) port map ( - wclk => clk, - reset => reset, - write_en => rsp_fifo_write, - wdata => rsp_fifo_wdata, - wfull => open, - wusedwds => open, - rclk => clk, - rdata => rsp_fifo_rdata, - rdreq => rsp_fifo_rdack, - rempty => rsp_fifo_empty, - rusedwds => open + clk => clk, + reset => reset, + axi_if => axi_bus, + flash_cmd_rdata => cmd_fifo_rdata, + flash_cmd_rdack => cmd_fifo_rdack, + flash_cmd_rempty => cmd_fifo_empty, + flash_rsp_wdata => rsp_fifo_wdata, + flash_rsp_write => rsp_fifo_write, + flash_rsp_wfull => open ); - -- The SPI controller's own register interface is not exercised here, so park - -- its initiator side idle. sp5_owns_flash stays at its reset value of zero, - -- which means the hubris register path is nominally selected and the hash - -- client has to win the engine on its own. - spinor_axi.read_address.valid <= '0'; - spinor_axi.read_address.addr <= (others => '0'); - spinor_axi.read_data.ready <= '0'; - spinor_axi.write_address.valid <= '0'; - spinor_axi.write_address.addr <= (others => '0'); - spinor_axi.write_data.valid <= '0'; - spinor_axi.write_data.data <= (others => '0'); - spinor_axi.write_data.strb <= (others => '0'); - spinor_axi.write_response.ready <= '0'; - - spi_nor: entity work.spi_nor_top - port map ( - clk => clk, - reset => reset, - axi_if => spinor_axi, - cs_n => cs_n, - sclk => sclk, - io => io, - io_o => io_o, - io_oe => io_oe, - sp5_owns_flash => open, - espi_cmd_fifo_rdata => (others => '0'), - espi_cmd_fifo_rdack => open, - espi_cmd_fifo_rempty => '1', - espi_data_fifo_wdata => open, - espi_data_fifo_write => open, - hash_cmd_fifo_rdata => cmd_fifo_rdata, - hash_cmd_fifo_rdack => cmd_fifo_rdack, - hash_cmd_fifo_rempty => cmd_fifo_empty, - hash_data_fifo_wdata => rsp_fifo_wdata, - hash_data_fifo_write => rsp_fifo_write - ); - - -- Everything the part sees is delayed by out_delay; everything the DUT - -- captures is delayed again by in_delay coming back. - sclk_flash <= sclk after out_delay; - csn_flash <= cs_n after out_delay; - - flash: entity work.spi_nor_target_vc - generic map ( - actor_name => "spi_nor_target" - ) - port map ( - cs_n => csn_flash, - sclk => sclk_flash, - io => io_flash, - io_o => flash_o, - io_oe => flash_oe - ); - - -- Both ends contribute to the resolved bus at the part, plus a weak pull-up - -- for the board's. If both drive a lane the resolution goes to 'X', which the - -- controller shifts in and the digest check then catches. - bus_gen: for i in io_flash'range generate - io_flash(i) <= io_o(i) after out_delay when io_oe(i) = '1' else 'Z' after out_delay; - io_flash(i) <= flash_o(i) when flash_oe(i) = '1' else 'Z'; - io_flash(i) <= 'H'; + -- One spi_nor_top and one modelled part per flash, each hung off its own + -- pair of the engine's flash ports. The parts are told apart by actor + -- name; which of them a run reads is the whole point of the aux tests. + flashes: for f in 0 to NUM_FLASHES - 1 generate + constant actor : string := "spi_nor_target" & integer'image(f); + begin + -- The SPI controller's own register interface is not exercised here, + -- so park its initiator side idle. sp5_owns_flash stays at its reset + -- value of zero, which means the hubris register path is nominally + -- selected and the hash client has to win the engine on its own. + spinor_axi(f).read_address.valid <= '0'; + spinor_axi(f).read_address.addr <= (others => '0'); + spinor_axi(f).read_data.ready <= '0'; + spinor_axi(f).write_address.valid <= '0'; + spinor_axi(f).write_address.addr <= (others => '0'); + spinor_axi(f).write_data.valid <= '0'; + spinor_axi(f).write_data.data <= (others => '0'); + spinor_axi(f).write_data.strb <= (others => '0'); + spinor_axi(f).write_response.ready <= '0'; + + spi_nor: entity work.spi_nor_top + port map ( + clk => clk, + reset => reset, + axi_if => spinor_axi(f), + cs_n => cs_n(f), + sclk => sclk(f), + io => io(f), + io_o => io_o(f), + io_oe => io_oe(f), + sp5_owns_flash => open, + espi_cmd_fifo_rdata => (others => '0'), + espi_cmd_fifo_rdack => open, + espi_cmd_fifo_rempty => '1', + espi_data_fifo_wdata => open, + espi_data_fifo_write => open, + hash_cmd_fifo_rdata => cmd_fifo_rdata, + hash_cmd_fifo_rdack => cmd_fifo_rdack(f), + hash_cmd_fifo_rempty => cmd_fifo_empty(f), + hash_data_fifo_wdata => rsp_fifo_wdata(f * 8 + 7 downto f * 8), + hash_data_fifo_write => rsp_fifo_write(f) + ); + + -- Everything the part sees is delayed by out_delay; everything the DUT + -- captures is delayed again by in_delay coming back. + sclk_flash(f) <= sclk(f) after out_delay; + csn_flash(f) <= cs_n(f) after out_delay; + + flash: entity work.spi_nor_target_vc + generic map ( + actor_name => actor + ) + port map ( + cs_n => csn_flash(f), + sclk => sclk_flash(f), + io => io_flash(f), + io_o => flash_o(f), + io_oe => flash_oe(f) + ); + + -- Both ends contribute to the resolved bus at the part, plus a weak + -- pull-up for the board's. If both drive a lane the resolution goes to + -- 'X', which the controller shifts in and the digest check then catches. + bus_gen: for i in 0 to 3 generate + io_flash(f)(i) <= io_o(f)(i) after out_delay when io_oe(f)(i) = '1' else 'Z' after out_delay; + io_flash(f)(i) <= flash_o(f)(i) when flash_oe(f)(i) = '1' else 'Z'; + io_flash(f)(i) <= 'H'; + end generate; + + io(f) <= io_flash(f) after in_delay; end generate; - io <= io_flash after in_delay; - end th; diff --git a/hdl/projects/cosmo_seq/BUCK b/hdl/projects/cosmo_seq/BUCK index 243fa97d..b6bd6e05 100644 --- a/hdl/projects/cosmo_seq/BUCK +++ b/hdl/projects/cosmo_seq/BUCK @@ -70,6 +70,7 @@ vhdl_unit( "//hdl/ip/vhd/axi_blocks:axil_interconnect", "//hdl/ip/vhd/common:tristate_if_pkg", "//hdl/ip/vhd/fmc_if:stm32h7_fmc_target", + "//hdl/ip/vhd/hash_engine:hash_engine_top", "//hdl/projects/cosmo_seq/sp5_espi_flash_subsystem:sp5_espi_flash_subsystem", "//hdl/projects/cosmo_seq/sp5_uart_subsystem:sp5_uart_subsystem", "//hdl/projects/cosmo_seq/sequencer:sequencer", diff --git a/hdl/projects/cosmo_seq/cosmo_seq_top.vhd b/hdl/projects/cosmo_seq/cosmo_seq_top.vhd index 58550779..e0225c43 100644 --- a/hdl/projects/cosmo_seq/cosmo_seq_top.vhd +++ b/hdl/projects/cosmo_seq/cosmo_seq_top.vhd @@ -375,6 +375,13 @@ architecture rtl of cosmo_seq_top is signal spinor_io_oe : std_logic_vector(3 downto 0); signal espi_io_o : std_logic_vector(3 downto 0); signal espi_io_oe : std_logic_vector(3 downto 0); + -- hash engine <-> spi_nor flash client port(s) + constant HASH_NUM_FLASHES : natural := 1; + signal hash_flash_cmd_rdata : std_logic_vector(31 downto 0); + signal hash_flash_cmd_rdack : std_logic_vector(HASH_NUM_FLASHES - 1 downto 0); + signal hash_flash_cmd_rempty : std_logic_vector(HASH_NUM_FLASHES - 1 downto 0); + signal hash_flash_rsp_wdata : std_logic_vector(HASH_NUM_FLASHES * 8 - 1 downto 0); + signal hash_flash_rsp_write : std_logic_vector(HASH_NUM_FLASHES - 1 downto 0); signal ipcc_uart_from_espi_axi_st : axi_st8_pkg.axi_st_t; signal ipcc_uart_to_espi_axi_st : axi_st8_pkg.axi_st_t; @@ -564,7 +571,29 @@ begin spi_nor_dat => spi_fpga1_to_flash_dat, spi_nor_dat_o => spinor_io_o, spi_nor_dat_oe => spinor_io_oe, - hash_axi_if => responders_8b(HASH_RESP_IDX) + hash_cmd_fifo_rdata => hash_flash_cmd_rdata, + hash_cmd_fifo_rdack => hash_flash_cmd_rdack(0), + hash_cmd_fifo_rempty => hash_flash_cmd_rempty(0), + hash_data_fifo_wdata => hash_flash_rsp_wdata(7 downto 0), + hash_data_fifo_write => hash_flash_rsp_write(0) + ); + + -- SHA3 hashing engine. It reads flash through spi_nor_top's second client + -- port and owns the FIFOs on that path; it sits here rather than inside the + -- eSPI wrapper so one engine can serve more than one flash. + hash_engine_inst: entity work.hash_engine_top + generic map( + NUM_FLASHES => HASH_NUM_FLASHES + ) + port map( + clk => clk_125m, + reset => reset_125m, + axi_if => responders_8b(HASH_RESP_IDX), + flash_cmd_rdata => hash_flash_cmd_rdata, + flash_cmd_rdack => hash_flash_cmd_rdack, + flash_cmd_rempty => hash_flash_cmd_rempty, + flash_rsp_wdata => hash_flash_rsp_wdata, + flash_rsp_write => hash_flash_rsp_write ); --Tristates for spi-nor flash pins and espi spi_nor_espi_tris:process(all) diff --git a/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/BUCK b/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/BUCK index 0027219d..64f15e8d 100644 --- a/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/BUCK +++ b/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/BUCK @@ -11,7 +11,6 @@ vhdl_unit( "//hdl/ip/vhd/axi_blocks:axist_if_2k19_pkg", "//hdl/ip/vhd/axi_blocks:axil_interconnect", "//hdl/ip/vhd/spi_nor_controller:spi_nor_top", - "//hdl/ip/vhd/hash_engine:hash_engine_top", "//hdl/ip/vhd/fifos:dcfifo_xpm", ], standard = "2019", diff --git a/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd b/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd index 572059d0..809d017d 100644 --- a/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd +++ b/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd @@ -36,11 +36,15 @@ entity sp5_espi_flash_subsystem is spi_nor_dat_o : out std_logic_vector(3 downto 0); spi_nor_dat_oe : out std_logic_vector(3 downto 0); - -- SHA3 hashing engine. It lives here rather than at the top level because - -- it reads the flash through spi_nor_top's second client port, so it needs - -- the same command/response FIFO pattern the eSPI flash channel uses. - hash_axi_if : view axil8x32_pkg.axil_target; - + -- spi_nor_top's second flash client port, for the hashing engine. The + -- engine sits at the project top rather than in here so that one engine + -- can be shared between several of these wrappers; these are the + -- engine's own FIFO endpoints, in spi_nor_top's port shape. + hash_cmd_fifo_rdata : in std_logic_vector(31 downto 0); + hash_cmd_fifo_rdack : out std_logic; + hash_cmd_fifo_rempty : in std_logic; + hash_data_fifo_wdata : out std_logic_vector(7 downto 0); + hash_data_fifo_write : out std_logic ); end entity; @@ -59,21 +63,6 @@ architecture rtl of sp5_espi_flash_subsystem is signal fifo_reset : std_logic; signal rst_cnts : integer range 0 to 5 := 5; - -- Hashing engine <-> spi_nor_top, the same shape as the eSPI pair above. - -- Deliberately not tied to fifo_reset: that is flushed on every eSPI reset, - -- which happens at the start of every boot and has nothing to do with a hash - -- the SP may have in flight. The engine resynchronises its own channel by - -- draining it, so a global reset is the only thing that needs to clear these. - signal hash_cmd_fifo_wdata : std_logic_vector(31 downto 0); - signal hash_cmd_fifo_write : std_logic; - signal hash_cmd_fifo_rdata : std_logic_vector(31 downto 0); - signal hash_cmd_fifo_rdack : std_logic; - signal hash_cmd_fifo_rempty : std_logic; - signal hash_data_fifo_wdata : std_logic_vector(7 downto 0); - signal hash_data_fifo_write : std_logic; - signal hash_rsp_fifo_rdata : std_logic_vector(7 downto 0); - signal hash_rsp_fifo_rdack : std_logic; - signal hash_rsp_fifo_rempty : std_logic; begin @@ -139,59 +128,6 @@ begin rusedwds => open ); - -- Hashing engine -> SPI NOR FIFO - hash_spinor_cmd_fifo: entity work.dcfifo_xpm - generic map( - fifo_write_depth => 256, - data_width => 32, - showahead_mode => true - ) - port map( - wclk => clk_125m, - reset => reset_125m, - write_en => hash_cmd_fifo_write, - wdata => hash_cmd_fifo_wdata, - wfull => open, - wusedwds => open, - rclk => clk_125m, - rdata => hash_cmd_fifo_rdata, - rdreq => hash_cmd_fifo_rdack, - rempty => hash_cmd_fifo_rempty, - rusedwds => open - ); - -- SPI NOR -> hashing engine FIFO - hash_spinor_data_fifo: entity work.dcfifo_xpm - generic map( - fifo_write_depth => 256, - data_width => 8, - showahead_mode => true - ) - port map( - wclk => clk_125m, - reset => reset_125m, - write_en => hash_data_fifo_write, - wdata => hash_data_fifo_wdata, - wfull => open, - wusedwds => open, - rclk => clk_125m, - rdata => hash_rsp_fifo_rdata, - rdreq => hash_rsp_fifo_rdack, - rempty => hash_rsp_fifo_rempty, - rusedwds => open - ); - - hash_engine_inst: entity work.hash_engine_top - port map( - clk => clk_125m, - reset => reset_125m, - axi_if => hash_axi_if, - cmd_fifo_wdata => hash_cmd_fifo_wdata, - cmd_fifo_write => hash_cmd_fifo_write, - rsp_fifo_rdata => hash_rsp_fifo_rdata, - rsp_fifo_rdack => hash_rsp_fifo_rdack, - rsp_fifo_rempty => hash_rsp_fifo_rempty - ); - -- eSPI block -- Only the link layer runs at 200MHz, the remaining -- logic runs at 125MHz so all the interfaces are synchronous diff --git a/hdl/projects/grapefruit/BUCK b/hdl/projects/grapefruit/BUCK index ff6f595b..a5872eb6 100644 --- a/hdl/projects/grapefruit/BUCK +++ b/hdl/projects/grapefruit/BUCK @@ -1,4 +1,4 @@ -load("//tools:hdl.bzl", "vhdl_unit", "black_box") +load("//tools:hdl.bzl", "vhdl_unit", "black_box", "vunit_sim") load("//tools:rdl.bzl", "rdl_file") load("//tools:vivado.bzl", "vivado_bitstream") @@ -102,4 +102,14 @@ vivado_bitstream( python_library( name = 'drivers', srcs = glob(["integration/drivers/*.py"]), -) \ No newline at end of file +) + +# Analysis-only: vhdl_unit collects sources but does not run a compiler, so +# without this nothing type-checks grapefruit_top until Vivado synthesis. +vunit_sim( + name = "grapefruit_top_tb", + srcs = glob(["sims/*.vhd"]), + deps = [":grapefruit_top"], + visibility = ["PUBLIC"], + standard = "2019", +) diff --git a/hdl/projects/grapefruit/grapefruit_top.vhd b/hdl/projects/grapefruit/grapefruit_top.vhd index d1bb6204..b6801f18 100644 --- a/hdl/projects/grapefruit/grapefruit_top.vhd +++ b/hdl/projects/grapefruit/grapefruit_top.vhd @@ -214,19 +214,13 @@ architecture rtl of grapefruit_top is signal fabric_responders : axil32x32_pkg.axil_array_t(config_array'range); signal responders_8b : axil8x32_pkg.axil_array_t(config_array'range); signal responders_15b : axil15x32_pkg.axil_array_t(config_array'range); - -- Hashing engine <-> spi_nor_top, the same shape as the eSPI pair below. - -- Reset from reset_125m only: the engine resynchronises its own response - -- channel by draining it, so nothing else should be clearing these. - signal hash_cmd_fifo_wdata : std_logic_vector(31 downto 0); - signal hash_cmd_fifo_write : std_logic; + -- Hashing engine <-> spi_nor_top's hash client port. The engine owns the + -- FIFOs on this path; these are their spi_nor-side ends, one flash. signal hash_cmd_fifo_rdata : std_logic_vector(31 downto 0); - signal hash_cmd_fifo_rdack : std_logic; - signal hash_cmd_fifo_rempty : std_logic; + signal hash_cmd_fifo_rdack : std_logic_vector(0 downto 0); + signal hash_cmd_fifo_rempty : std_logic_vector(0 downto 0); signal hash_data_fifo_wdata : std_logic_vector(7 downto 0); - signal hash_data_fifo_write : std_logic; - signal hash_rsp_fifo_rdata : std_logic_vector(7 downto 0); - signal hash_rsp_fifo_rdack : std_logic; - signal hash_rsp_fifo_rempty : std_logic; + signal hash_data_fifo_write : std_logic_vector(0 downto 0); signal espi_cmd_fifo_rdata : std_logic_vector(31 downto 0); signal espi_cmd_fifo_rdack : std_logic; signal espi_cmd_fifo_rempty : std_logic; @@ -383,65 +377,24 @@ begin espi_data_fifo_wdata => espi_data_fifo_wdata, espi_data_fifo_write => espi_data_fifo_write, hash_cmd_fifo_rdata => hash_cmd_fifo_rdata, - hash_cmd_fifo_rdack => hash_cmd_fifo_rdack, - hash_cmd_fifo_rempty => hash_cmd_fifo_rempty, + hash_cmd_fifo_rdack => hash_cmd_fifo_rdack(0), + hash_cmd_fifo_rempty => hash_cmd_fifo_rempty(0), hash_data_fifo_wdata => hash_data_fifo_wdata, - hash_data_fifo_write => hash_data_fifo_write + hash_data_fifo_write => hash_data_fifo_write(0) ); - -- Hashing engine -> SPI NOR - hash_spinor_cmd_fifo: entity work.dcfifo_xpm - generic map( - fifo_write_depth => 256, - data_width => 32, - showahead_mode => true - ) - port map( - wclk => clk_125m, - reset => reset_125m, - write_en => hash_cmd_fifo_write, - wdata => hash_cmd_fifo_wdata, - wfull => open, - wusedwds => open, - rclk => clk_125m, - rdata => hash_cmd_fifo_rdata, - rdreq => hash_cmd_fifo_rdack, - rempty => hash_cmd_fifo_rempty, - rusedwds => open - ); - -- SPI NOR -> hashing engine - hash_spinor_data_fifo: entity work.dcfifo_xpm - generic map( - fifo_write_depth => 256, - data_width => 8, - showahead_mode => true - ) - port map( - wclk => clk_125m, - reset => reset_125m, - write_en => hash_data_fifo_write, - wdata => hash_data_fifo_wdata, - wfull => open, - wusedwds => open, - rclk => clk_125m, - rdata => hash_rsp_fifo_rdata, - rdreq => hash_rsp_fifo_rdack, - rempty => hash_rsp_fifo_rempty, - rusedwds => open - ); - resize_axil(fabric_responders(4), responders_8b(4)); hash_engine_inst: entity work.hash_engine_top port map( clk => clk_125m, reset => reset_125m, axi_if => responders_8b(4), - cmd_fifo_wdata => hash_cmd_fifo_wdata, - cmd_fifo_write => hash_cmd_fifo_write, - rsp_fifo_rdata => hash_rsp_fifo_rdata, - rsp_fifo_rdack => hash_rsp_fifo_rdack, - rsp_fifo_rempty => hash_rsp_fifo_rempty + flash_cmd_rdata => hash_cmd_fifo_rdata, + flash_cmd_rdack => hash_cmd_fifo_rdack, + flash_cmd_rempty => hash_cmd_fifo_rempty, + flash_rsp_wdata => hash_data_fifo_wdata, + flash_rsp_write => hash_data_fifo_write ); -- eSPI block -> SPI NOR diff --git a/hdl/projects/grapefruit/sims/grapefruit_top_tb.vhd b/hdl/projects/grapefruit/sims/grapefruit_top_tb.vhd new file mode 100644 index 00000000..78014b9d --- /dev/null +++ b/hdl/projects/grapefruit/sims/grapefruit_top_tb.vhd @@ -0,0 +1,33 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; + +library vunit_lib; + context vunit_lib.vunit_context; + +-- Analysis-only smoke test: it does not drive the top level, its job is to +-- make a simulator compile the whole design tree, which the synthesis-only +-- build rules do not do on their own. +entity grapefruit_top_tb is + generic ( + runner_cfg : string + ); +end entity; + +architecture tb of grapefruit_top_tb is +begin + bench: process + begin + test_runner_setup(runner, runner_cfg); + while test_suite loop + if run("analyses") then + info("grapefruit_top and its dependencies analysed"); + end if; + end loop; + test_runner_cleanup(runner); + wait; + end process; +end tb; From aa192805ae560197e466e135ec8da54b935f4840 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Fri, 11 Sep 2026 15:57:32 -0400 Subject: [PATCH 06/12] espi/spi_nor: SAFS flash write and erase The eSPI flash channel only ever forwarded reads; a PUT_FLASH_NP write was parsed and dropped and an erase was not parsed at all, so a host that tried either waited forever for a completion. Metro's second eSPI target fronts the Versal's boot flash and the SP5 has to be able to program it, so writes and erases now go through, behind a permission that is off by default. Permission is the AND of a FLASH_WRITES_ALLOWED generic on espi_target_top (and the sp5_espi_flash_subsystem wrapper) and a new flash_write_enable bit in the control register, so an instance built without the generic can never be opened up by software, and one built with it still has to be enabled at runtime. A write or erase that arrives while not permitted takes a descriptor marked refused and is answered with an unsuccessful completion rather than silence. In the flash channel the descriptor gains a kind. A write's payload is captured into the descriptor's DPR slot as the command is parsed (ahead of the CRC; a bad CRC just leaves the slot to be overwritten) and streamed out to the flash block ahead of the two command words, whose length word now carries the kind in its top nibble. Writes and erases come back as a single status byte and complete with success_no_data or the unsuccessful cycle type. The completion header is latched when the GET arrives so a zero-length completion can retire its descriptor straight away, and the response processor skips the payload stage for length zero. The link layer sizer learns that a flash write carries its payload in the command. spi_nor_top's eSPI client sequences the flash side: write enable, quad page program chunked at page boundaries (or 4k/64k erase; the 32k code has no 4-byte-address opcode here and is refused), then a status poll until write-in-progress clears, with a bound on polls so a missing part does not hang the channel. The engine's outbound byte stream is muxed between hubris' TX FIFO and the eSPI payload FIFO for the duration of a program. Read chunk issue now holds go until the engine has started, the same way the hash client does; the old exit-on-idle dropped every chunk after the first, which nothing hit only because the channel's maximum payload is smaller than a chunk. The eSPI sim's fake flash is now memory backed, so reads are checked against known contents (closing the old TODO), and writes and erases can be verified by reading back. A new spi_nor harness drives the eSPI client FIFOs directly against the flash VC. --- cos_soak_100_26p25.json | 53 ++++ hdl/ip/vhd/espi/espi_target_top.vhd | 25 +- .../vhd/espi/flash_channel/flash_channel.vhd | 209 +++++++++++++--- .../espi/flash_channel/flash_channel_pkg.vhd | 43 +++- hdl/ip/vhd/espi/link_layer/link_layer_pkg.vhd | 20 +- hdl/ip/vhd/espi/sims/espi_tb.vhd | 122 +++++++++- hdl/ip/vhd/espi/sims/espi_tb_pkg.vhd | 82 +++++++ hdl/ip/vhd/espi/sims/espi_th.vhd | 12 + .../sims/models/espi_controller_vc_pkg.vhd | 107 ++++++++ .../espi/sims/models/fake_flash_txn_mgr.vhd | 131 +++++++--- hdl/ip/vhd/espi/sys_regs/espi_regs.rdl | 6 + hdl/ip/vhd/espi/sys_regs/espi_regs.vhd | 3 + .../vhd/espi/txn_layer/command_processor.vhd | 35 ++- .../vhd/espi/txn_layer/espi_protocol_pkg.vhd | 12 +- .../vhd/espi/txn_layer/response_processor.vhd | 10 +- hdl/ip/vhd/espi/txn_layer/txn_layer_top.vhd | 2 + .../espi_txn/espi_flash_txn_mgr.vhd | 229 +++++++++++++++--- .../sims/spi_nor_espi_tb.vhd | 192 +++++++++++++++ .../sims/spi_nor_espi_th.vhd | 218 +++++++++++++++++ hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd | 23 +- .../sp5_espi_flash_subsystem.vhd | 60 ++++- 21 files changed, 1477 insertions(+), 117 deletions(-) create mode 100644 cos_soak_100_26p25.json create mode 100644 hdl/ip/vhd/spi_nor_controller/sims/spi_nor_espi_tb.vhd create mode 100644 hdl/ip/vhd/spi_nor_controller/sims/spi_nor_espi_th.vhd diff --git a/cos_soak_100_26p25.json b/cos_soak_100_26p25.json new file mode 100644 index 00000000..648d5c11 --- /dev/null +++ b/cos_soak_100_26p25.json @@ -0,0 +1,53 @@ +{ + "seed": 478, + "steps": [ + { + "divisor": 2, + "freq_mhz": 100.0, + "ok": true, + "udp_timeouts_retried": 9, + "phases": { + "A_const_reads": { + "ok": true, + "values": [ + "0x000001de" + ] + }, + "B_scratch": { + "ok": true, + "patterns": 68, + "mismatches": [] + }, + "C_stress": { + "ok": true, + "write_burst": 1024, + "read_words": 1024, + "mismatches": [] + }, + "D_soak": { + "ok": true, + "ops": 14923578, + "mismatches": [] + }, + "E_throughput": { + "ok": true, + "wire_write_MBps": 1.224, + "wire_read_MBps": 0.997, + "single_read_us": 226.8, + "fmc_read_MBps": 11.39, + "fmc_read_ns_per_word": 351, + "fmc_read_fixed_ns_per_word": 309, + "fmc_write_MBps": 49.98, + "fmc_write_ns_per_word": 80, + "note": "fmc_* numbers are bus line rate (delta-timed block ops); wire_* include per-word network cost and are insensitive to the FMC clock" + } + }, + "btr1": "0x0010ffff", + "clkdiv_verified": true, + "sp_ticks_before": 48439, + "sp_ticks_after": 3649492, + "sp_rebooted": false + } + ], + "restored_baseline_ok": true +} \ No newline at end of file diff --git a/hdl/ip/vhd/espi/espi_target_top.vhd b/hdl/ip/vhd/espi/espi_target_top.vhd index e5d47c67..38ffd482 100644 --- a/hdl/ip/vhd/espi/espi_target_top.vhd +++ b/hdl/ip/vhd/espi/espi_target_top.vhd @@ -20,6 +20,14 @@ use work.link_layer_pkg.all; use work.axil15x32_pkg.all; entity espi_target_top is + generic ( + -- Whether this instance can ever forward SAFS flash writes and + -- erases. False strips the ability out of the build; true still + -- leaves it gated by the flash_write_enable control bit, which + -- resets to off. Refused writes and erases get an unsuccessful + -- completion. + FLASH_WRITES_ALLOWED : boolean := false + ); port ( clk : in std_logic; reset : in std_logic; @@ -43,6 +51,10 @@ entity espi_target_top is flash_rfifo_rdack : out std_logic; flash_rfifo_rempty: in std_logic; flash_fifo_clear : out std_logic; + -- Write payload FIFO, host to flash. Only ever written on an + -- instance built with FLASH_WRITES_ALLOWED; leave open otherwise. + flash_wfifo_data : out std_logic_vector(7 downto 0); + flash_wfifo_write : out std_logic; -- Interfaces to the UART block to_sp_uart_data : out std_logic_vector(7 downto 0); to_sp_uart_valid: out std_logic; @@ -66,6 +78,8 @@ architecture rtl of espi_target_top is signal flash_np_free : std_logic; signal flash_c_avail : std_logic; signal flash_channel_enable : boolean; + signal flash_write_enable : std_logic; + signal flash_writes_permitted : std_logic; signal dbg_chan : dbg_chan_t; signal spec_regs : spec_regs_t; signal response_done : boolean; @@ -258,6 +272,7 @@ begin stuff_fifo => stuff_fifo, stuff_wds => stuff_wds, dbg_chan => dbg_chan, + flash_write_enable => flash_write_enable, spec_regs_view => spec_regs, post_code => post_code, post_code_valid => post_code_valid, @@ -286,6 +301,7 @@ begin data_to_host => txn_resp, data_from_host => txn_cmd, alert_needed => alert_needed, + flash_writes_allowed => flash_writes_permitted, flash_req => flash_req, flash_resp => flash_resp, response_done => response_done, @@ -320,6 +336,11 @@ begin oob_enabled => oob_enabled ); + -- Both halves of the write permission have to agree; the generic keeps + -- the register bit from doing anything on an instance that must never + -- let the host write. + flash_writes_permitted <= '1' when FLASH_WRITES_ALLOWED and flash_write_enable = '1' else '0'; + -- flash access channel logic flash_channel_inst: entity work.flash_channel port map( @@ -335,7 +356,9 @@ begin flash_cfifo_write => flash_cfifo_write, flash_rfifo_data => flash_rfifo_data, flash_rfifo_rdack => flash_rfifo_rdack, - flash_rfifo_rempty => flash_rfifo_rempty + flash_rfifo_rempty => flash_rfifo_rempty, + flash_wfifo_data => flash_wfifo_data, + flash_wfifo_write => flash_wfifo_write ); -- uart channel logic diff --git a/hdl/ip/vhd/espi/flash_channel/flash_channel.vhd b/hdl/ip/vhd/espi/flash_channel/flash_channel.vhd index 8ad67bb6..f667a98f 100644 --- a/hdl/ip/vhd/espi/flash_channel/flash_channel.vhd +++ b/hdl/ip/vhd/espi/flash_channel/flash_channel.vhd @@ -6,6 +6,14 @@ -- channel. It is responsible for queueing up transactions, issuing commands to -- to the spi flash block and providing flash response data to the transaction -- layer. +-- +-- Reads, writes and erases all take a descriptor and a 1kB slot of the DPR. +-- For a read the slot holds the flash data on its way back to the host. For a +-- write it holds the host's payload, captured as the command is still being +-- parsed (before its CRC is known good), and streamed out to the flash block +-- only once the descriptor is enqueued. Writes and erases come back from the +-- flash block as a single status byte, which lands at offset 0 of the slot +-- and only decides whether the completion is reported as successful. library ieee; use ieee.std_logic_1164.all; @@ -13,6 +21,7 @@ use ieee.numeric_std.all; use ieee.numeric_std_unsigned.all; use work.espi_base_types_pkg.all; +use work.espi_protocol_pkg.all; use work.flash_channel_pkg.all; entity flash_channel is @@ -40,6 +49,11 @@ entity flash_channel is flash_rfifo_data : in std_logic_vector(7 downto 0); flash_rfifo_rdack : out std_logic; flash_rfifo_rempty: in std_logic; + -- write payload fifo, host to flash. A whole payload is pushed before + -- the command words for it are, so the flash block never sees a + -- write command it does not already have all the bytes for. + flash_wfifo_data : out std_logic_vector(7 downto 0); + flash_wfifo_write : out std_logic ); end; @@ -51,7 +65,8 @@ architecture rtl of flash_channel is constant max_txn_size : integer := 1024; subtype desc_index_t is natural range 0 to num_descriptors - 1; signal dpr_waddr : std_logic_vector(11 downto 0); - signal dpr_raddr : std_logic_vector(11 downto 0); + signal dpr_wdata : std_logic_vector(7 downto 0); + signal dpr_wren : std_logic; signal readdata : std_logic_vector(7 downto 0); function add_wrap(a : natural; max: natural) return natural is @@ -63,8 +78,19 @@ architecture rtl of flash_channel is end if; end function; + -- Bytes the flash block hands back for a descriptor: the data for a + -- read, one status byte for anything else. + function flash_side_bytes(desc : descriptor_t) return std_logic_vector is + begin + if desc.kind = flash_rd then + return desc.xfr_size_bytes; + else + return To_Std_Logic_Vector(1, desc.xfr_size_bytes'length); + end if; + end function; + - type cmd_state_t is (idle, issue_flash_addr, issue_flash_len, wait_for_data); + type cmd_state_t is (idle, stream_payload, issue_flash_addr, issue_flash_len, wait_for_data); type complete_state_t is (idle, read_dpr); type reg_type is record @@ -76,22 +102,63 @@ architecture rtl of flash_channel is cmd_queue: command_queue_t; dpr_write_en: std_logic; dpr_wdata_buf: std_logic_vector(7 downto 0); + -- host payload capture, registered so it shares the DPR write port + -- with the flash-side path above without ever colliding with it + host_wr_en : std_logic; + host_wdata : std_logic_vector(7 downto 0); + host_waddr : std_logic_vector(11 downto 0); + -- payload stream out to the flash block + wfifo_write : std_logic; + wfifo_wdata : std_logic_vector(7 downto 0); + -- DPR read address, registered so the read port sees a flop rather + -- than a counter, an adder and the reader mux: the LUTRAM read plus + -- the response processor's own muxing already fills most of a + -- 125MHz period. + dpr_raddr : std_logic_vector(11 downto 0); + -- completion header, latched when the get arrives so the descriptor + -- can be retired before the header has gone out on the wire + resp_tag : std_logic_vector(3 downto 0); + resp_length : std_logic_vector(11 downto 0); + resp_cycle_type : std_logic_vector(7 downto 0); tail_desc: desc_index_t; issue_desc: desc_index_t; head_desc: desc_index_t; flash_np_free : std_logic; flash_c_avail: std_logic; end record; - constant reg_reset : reg_type := (idle, idle, 0, 0, 0, (others => descriptor_init), '0', (others => '0'), 0, 0, 0, '0', '0'); + constant reg_reset : reg_type := ( + flash_cmd_state => idle, + compl_state => idle, + flash_side_cntr => 0, + flash_write_addr_offset => 0, + compl_side_cntr => 0, + cmd_queue => (others => descriptor_init), + dpr_write_en => '0', + dpr_wdata_buf => (others => '0'), + host_wr_en => '0', + host_wdata => (others => '0'), + host_waddr => (others => '0'), + wfifo_write => '0', + wfifo_wdata => (others => '0'), + dpr_raddr => (others => '0'), + resp_tag => (others => '0'), + resp_length => (others => '0'), + resp_cycle_type => success_with_data_only, + tail_desc => 0, + issue_desc => 0, + head_desc => 0, + flash_np_free => '0', + flash_c_avail => '0' + ); signal r, rin : reg_type; - signal dpr_wdata: std_logic_vector(7 downto 0); + signal dpr_wdata_dbg: std_logic_vector(7 downto 0); signal dpr_rdata: std_logic_vector(7 downto 0); signal dpr_read_ack: std_logic; signal dpr_wr_delay: std_logic; attribute mark_debug of r : signal is "TRUE"; - attribute mark_debug of dpr_wdata : signal is "TRUE"; + attribute mark_debug of dpr_wdata_dbg : signal is "TRUE"; attribute mark_debug of dpr_rdata : signal is "TRUE"; attribute mark_debug of dpr_read_ack : signal is "TRUE"; attribute mark_debug of dpr_wr_delay : signal is "TRUE"; @@ -102,13 +169,13 @@ begin dbg_regs: process (clk, reset) begin if reset then - dpr_wdata <= (others => '0'); + dpr_wdata_dbg <= (others => '0'); dpr_rdata <= (others => '0'); dpr_read_ack <= '0'; dpr_wr_delay <= '0'; elsif rising_edge(clk) then dpr_rdata <= readdata; - dpr_wdata <= flash_rfifo_data; + dpr_wdata_dbg <= flash_rfifo_data; dpr_read_ack <= response.ready; dpr_wr_delay <= r.dpr_write_en; end if; @@ -120,16 +187,22 @@ begin flash_np_free <= r.flash_np_free; -- flash_c_avail is set when we have pending data to be read back out, but critically this status needs to represent - -- the state *after* any current message, so if we're responding now and this response is the only one available, + -- the status *after* any current message, so if we're responding now and this response is the only one available, -- this needs to be set to 0. flash_c_avail <= r.flash_c_avail when enabled else '0'; + -- The length word carries the request kind in its top nibble; reads + -- encode as zero there so the flash block sees the original two-word + -- command for them. flash_cfifo_data <= r.cmd_queue(r.issue_desc).sp5_addr when r.flash_cmd_state = issue_flash_addr else - resize(r.cmd_queue(r.issue_desc).xfr_size_bytes, flash_cfifo_data'length) when r.flash_cmd_state = issue_flash_len else + to_kind_bits(r.cmd_queue(r.issue_desc).kind) & resize(r.cmd_queue(r.issue_desc).xfr_size_bytes, flash_cfifo_data'length - 4) when r.flash_cmd_state = issue_flash_len else (others => '0'); flash_cfifo_write <= '1' when r.flash_cmd_state = issue_flash_addr or r.flash_cmd_state = issue_flash_len else '0'; + flash_wfifo_data <= r.wfifo_wdata; + flash_wfifo_write <= r.wfifo_write; + -- Let's put a 4kB buffer here as a starting point and see how it goes, this would allow 4 1024Byte max size transactions -- or we could shrink and say 2 2kB etc. We know we're only going read on this interface so we don't have to worry so much about -- various concurrency issues here. This should fit in a single 32kb block ram on the FPGA @@ -142,21 +215,26 @@ begin port map( wclk => clk, waddr => dpr_waddr, - wdata => r.dpr_wdata_buf, - wren => r.dpr_write_en, + wdata => dpr_wdata, + wren => dpr_wren, rclk => clk, - raddr => dpr_raddr, + raddr => r.dpr_raddr, rdata => readdata ); response.data <= readdata; response.valid <= '1' when r.compl_state = read_dpr else '0'; - response.tag <= r.cmd_queue(r.tail_desc).tag; - response.length <= r.cmd_queue(r.tail_desc).xfr_size_bytes; - response.cycle_type <= "00001111"; -- successful completion of with data, only completion for a split txn - - - dpr_waddr <= To_Std_Logic_Vector(r.issue_desc * max_txn_size + r.flash_write_addr_offset, 12); - dpr_raddr <= To_Std_Logic_Vector(r.tail_desc * max_txn_size + r.compl_side_cntr, 12); + response.tag <= r.resp_tag; + response.length <= r.resp_length; + response.cycle_type <= r.resp_cycle_type; + + -- One write port, two writers. The host path is registered off the + -- incoming stream (which cannot be stalled) and the flash path only + -- schedules itself on a cycle where the host is not writing, so the two + -- enables are never set together. + dpr_wren <= r.host_wr_en or r.dpr_write_en; + dpr_waddr <= r.host_waddr when r.host_wr_en = '1' else + To_Std_Logic_Vector(r.issue_desc * max_txn_size + r.flash_write_addr_offset, 12); + dpr_wdata <= r.host_wdata when r.host_wr_en = '1' else r.dpr_wdata_buf; -- We have two state machines running here as both need to be able to update -- the descriptor queues. @@ -178,21 +256,37 @@ begin begin v := r; + ------- + -- Capturing a write payload into the head descriptor's slot + ------ + -- This happens while the command is still arriving, so nothing is + -- known about its CRC yet. If the CRC turns out bad the enqueue + -- below never happens and the next request simply overwrites the + -- slot. The offset is clamped to the slot so an oversized length + -- wraps within it rather than trampling a neighbour. + v.host_wr_en := request.wdata_valid; + v.host_wdata := request.wdata; + v.host_waddr := To_Std_Logic_Vector(r.head_desc * max_txn_size + to_integer(request.wdata_idx(9 downto 0)), 12); + ------- -- Adding new requestes to the processing queue ------ - -- Command processing requests flash command queue, having already filtered - -- out any invalid commands such as writes/erases + -- Command processing requests flash command queue. Writes and erases + -- the command processor was not permitted to accept arrive marked + -- refused and still take a descriptor, so that the host gets an + -- unsuccessful completion rather than silence. -- we have 4 queue (txn) slots and can do 1024 byte per transaction max -- We simply carve up the DPR into 4 slots and then use those one for -- each descriptor, and we use the descriptors in order. if request.flash_np_enqueue_req and r.flash_np_free = '1' then + v.cmd_queue(r.head_desc).kind := request.kind; v.cmd_queue(r.head_desc).sp5_addr := request.sp5_flash_address; v.cmd_queue(r.head_desc).xfr_size_bytes := request.espi_hdr.length; v.cmd_queue(r.head_desc).active := true; v.cmd_queue(r.head_desc).tag := request.espi_hdr.tag; v.cmd_queue(r.head_desc).flash_issued := false; v.cmd_queue(r.head_desc).done := false; + v.cmd_queue(r.head_desc).failed := false; v.head_desc := add_wrap(r.head_desc, desc_index_t'high); end if; @@ -227,12 +321,39 @@ begin -- and store the data back in the DPR and then issue a completion request, and hold until -- the master does a get to get the data v.dpr_write_en := '0'; --only single cycle reads, default to 0 + v.wfifo_write := '0'; case r.flash_cmd_state is when idle => -- have active command that hasn't been issued to flash if flash_issue_needed then + v.flash_side_cntr := 0; + case r.cmd_queue(r.issue_desc).kind is + when flash_refused => + -- Nothing goes to the flash; it is complete + -- (unsuccessfully) as soon as it is looked at. + v.cmd_queue(r.issue_desc).flash_issued := true; + v.cmd_queue(r.issue_desc).done := true; + v.cmd_queue(r.issue_desc).failed := true; + v.issue_desc := add_wrap(r.issue_desc, desc_index_t'high); + when flash_wr => + v.flash_cmd_state := stream_payload; + when others => + v.flash_cmd_state := issue_flash_addr; + end case; + end if; + -- Push the captured payload out ahead of the command words. + -- readdata is combinational off dpr_raddr, so the byte for this + -- cycle's counter is registered on the way out. + when stream_payload => + if r.compl_state /= idle then + null; -- completion owns the read port + elsif r.flash_side_cntr = r.cmd_queue(r.issue_desc).xfr_size_bytes then + v.flash_side_cntr := 0; v.flash_cmd_state := issue_flash_addr; - + else + v.wfifo_write := '1'; + v.wfifo_wdata := readdata; + v.flash_side_cntr := r.flash_side_cntr + 1; end if; -- issue to flash, and wait until we get all the data back -- and have stored it into the DPR. We can't issue more than @@ -248,18 +369,24 @@ begin v.cmd_queue(r.issue_desc).flash_issued := true; when wait_for_data => - if r.flash_side_cntr = r.cmd_queue(r.issue_desc).xfr_size_bytes then + if r.flash_side_cntr = flash_side_bytes(r.cmd_queue(r.issue_desc)) then v.cmd_queue(r.issue_desc).done := true; v.flash_cmd_state := idle; v.flash_side_cntr := 0; v.issue_desc := add_wrap(r.issue_desc, desc_index_t'high); -- "empty" isn't strictly valid if we're acking this cycle since this write could - -- empty it. We only check for empty on a cycle where we're not acking - elsif not flash_rfifo_rempty and (not r.dpr_write_en) then + -- empty it. We only check for empty on a cycle where we're not acking. + -- A host payload byte arriving this cycle takes the write port next cycle. + elsif flash_rfifo_rempty = '0' and r.dpr_write_en = '0' and request.wdata_valid = '0' then v.dpr_write_en := '1'; v.dpr_wdata_buf := flash_rfifo_data; v.flash_write_addr_offset := r.flash_side_cntr; v.flash_side_cntr := r.flash_side_cntr + 1; + -- for a write or erase the one byte back is a status, + -- zero meaning the flash block finished it cleanly + if r.cmd_queue(r.issue_desc).kind /= flash_rd then + v.cmd_queue(r.issue_desc).failed := flash_rfifo_data /= x"00"; + end if; end if; end case; @@ -271,12 +398,24 @@ begin if request.flash_get_req and flash_c_avail = '1' then v.compl_state := read_dpr; v.compl_side_cntr := 0; + v.resp_tag := r.cmd_queue(r.tail_desc).tag; + if r.cmd_queue(r.tail_desc).kind = flash_rd then + v.resp_length := r.cmd_queue(r.tail_desc).xfr_size_bytes; + v.resp_cycle_type := success_with_data_only; + elsif r.cmd_queue(r.tail_desc).failed then + v.resp_length := (others => '0'); + v.resp_cycle_type := unsuccessful_no_data_only; + else + v.resp_length := (others => '0'); + v.resp_cycle_type := success_no_data; + end if; end if; when read_dpr => -- We have a done descriptor, we need to read the data back out - -- to the eSPI master - if r.compl_side_cntr = r.cmd_queue(r.tail_desc).xfr_size_bytes then + -- to the eSPI master. A completion without data retires + -- straight away; the header fields were latched above. + if r.compl_side_cntr = r.resp_length then v.cmd_queue(r.tail_desc).active := false; v.cmd_queue(r.tail_desc).done := false; v.cmd_queue(r.tail_desc).flash_issued := false; @@ -287,6 +426,18 @@ begin end if; end case; + ------ + -- One read port, two readers. Completions win; payload streaming + -- pauses for as long as one is in progress. Computed from the next + -- state so that the registered address always matches the counter + -- the reader is on. + ------ + if v.flash_cmd_state = stream_payload and v.compl_state = idle then + v.dpr_raddr := To_Std_Logic_Vector(v.issue_desc * max_txn_size + v.flash_side_cntr, 12); + else + v.dpr_raddr := To_Std_Logic_Vector(v.tail_desc * max_txn_size + v.compl_side_cntr, 12); + end if; + if not enabled then -- If we're not enabled, reset the state machine v := reg_reset; @@ -304,6 +455,4 @@ begin end process; - - -end rtl; \ No newline at end of file +end rtl; diff --git a/hdl/ip/vhd/espi/flash_channel/flash_channel_pkg.vhd b/hdl/ip/vhd/espi/flash_channel/flash_channel_pkg.vhd index 32de120a..c972e681 100644 --- a/hdl/ip/vhd/espi/flash_channel/flash_channel_pkg.vhd +++ b/hdl/ip/vhd/espi/flash_channel/flash_channel_pkg.vhd @@ -12,7 +12,19 @@ use work.espi_base_types_pkg.all; package flash_channel_pkg is constant num_descriptors : integer := 4; + -- What a queued flash request is. "refused" is a write or erase that + -- arrived while writes were not permitted: it still occupies a + -- descriptor so that the host gets a completion, an unsuccessful one, + -- rather than waiting forever. + type flash_kind_t is (flash_rd, flash_wr, flash_er, flash_refused); + + -- Encoding of the kind in the top nibble of the length word handed to + -- the flash controller. Reads encode as zero so a controller that only + -- understands the original two-word read command sees nothing new. + function to_kind_bits(kind : flash_kind_t) return std_logic_vector; + type descriptor_t is record + kind : flash_kind_t; sp5_addr : std_logic_vector(31 downto 0); xfr_size_bytes : std_logic_vector(11 downto 0); ready_bytes: std_logic_vector(11 downto 0); @@ -20,16 +32,20 @@ package flash_channel_pkg is active: boolean; -- valid waiting for processing or being processed flash_issued: boolean; done: boolean; + -- set once the flash controller reported the write or erase failed + failed: boolean; end record; constant descriptor_init : descriptor_t := ( + kind => flash_rd, sp5_addr => (others => '0'), xfr_size_bytes => (others => '0'), ready_bytes => (others => '0'), tag => (others => '0'), active => false, flash_issued => false, - done => false + done => false, + failed => false ); type command_queue_t is array(0 to num_descriptors - 1) of descriptor_t; @@ -38,11 +54,18 @@ package flash_channel_pkg is type flash_channel_req_t is record espi_hdr : espi_cmd_header; sp5_flash_address : std_logic_vector(31 downto 0); + kind : flash_kind_t; flash_np_enqueue_req : boolean; flash_get_req : boolean; + -- Write payload, streamed as the command is parsed, ahead of the + -- enqueue that follows a good CRC. + wdata : std_logic_vector(7 downto 0); + wdata_valid : std_logic; + wdata_idx : std_logic_vector(11 downto 0); end record; view flash_chan_req_sink of flash_channel_req_t is - espi_hdr, sp5_flash_address, flash_np_enqueue_req, flash_get_req : in; + espi_hdr, sp5_flash_address, kind, flash_np_enqueue_req, flash_get_req, + wdata, wdata_valid, wdata_idx : in; end view; alias flash_chan_req_source is flash_chan_req_sink'converse; @@ -63,4 +86,18 @@ package flash_channel_pkg is end view; alias flash_chan_resp_sink is flash_chan_resp_source'converse; -end package; \ No newline at end of file +end package; + +package body flash_channel_pkg is + + function to_kind_bits(kind : flash_kind_t) return std_logic_vector is + begin + case kind is + when flash_rd => return x"0"; + when flash_wr => return x"1"; + when flash_er => return x"2"; + when flash_refused => return x"0"; + end case; + end function; + +end package body; \ No newline at end of file diff --git a/hdl/ip/vhd/espi/link_layer/link_layer_pkg.vhd b/hdl/ip/vhd/espi/link_layer/link_layer_pkg.vhd index 307b0524..83177847 100644 --- a/hdl/ip/vhd/espi/link_layer/link_layer_pkg.vhd +++ b/hdl/ip/vhd/espi/link_layer/link_layer_pkg.vhd @@ -64,7 +64,6 @@ package body link_layer_pkg is opcode_get_configuration | opcode_reset | opcode_get_flash_c | - opcode_put_flash_np | opcode_get_np | opcode_put_vwire | opcode_get_oob | @@ -84,6 +83,10 @@ package body link_layer_pkg is when opcode_put_np | opcode_get_pc => return true; + when opcode_put_flash_np => + -- reads and erases are address only; a write carries its + -- payload so its size is not known until the length is + return h.cycle_type /= flash_write; when others => return false; end case?; @@ -96,7 +99,8 @@ package body link_layer_pkg is -- '-' (don't care) values. case? h.opcode is when opcode_put_pc | - opcode_put_oob => + opcode_put_oob | + opcode_put_flash_np => return true; when others => return false; @@ -149,10 +153,14 @@ package body link_layer_pkg is return To_StdLogicVector(0, 13); end case; when opcode_put_flash_np => - -- opcode, standard header (3), 4 address bytes, length bytes, crc - -- but we only support opcodes of reads, which don't have length data - -- in the cmd payload - return To_StdLogicVector(1 + 3 + 4 + 1, 13); + -- opcode, standard header (3), 4 address bytes, crc, plus the + -- payload for a write. For an erase the length field is a + -- size code and there is no payload. + if h.cycle_type = flash_write then + return To_StdLogicVector(1 + 3 + 4 + to_integer(h.len) + 1, 13); + else + return To_StdLogicVector(1 + 3 + 4 + 1, 13); + end if; when opcode_put_oob => -- opcode, standard header (3), length bytes, crc return To_StdLogicVector(1 + 3 + to_integer(h.len) + 1, 13); diff --git a/hdl/ip/vhd/espi/sims/espi_tb.vhd b/hdl/ip/vhd/espi/sims/espi_tb.vhd index da7671d2..242a3699 100644 --- a/hdl/ip/vhd/espi/sims/espi_tb.vhd +++ b/hdl/ip/vhd/espi/sims/espi_tb.vhd @@ -19,6 +19,7 @@ use work.espi_spec_regs_pkg.all; use work.espi_regs_pkg; use work.espi_dbg_vc_pkg.all; use work.espi_tb_pkg.all; +use work.espi_protocol_pkg.all; use work.sp5_post_code_pkg.all; entity espi_tb is @@ -52,6 +53,8 @@ begin variable payload_size : integer; variable response : resp_t := (queue => new_queue, num_bytes => 0, response_code => (others => '0'), status => (others => '0'), crc_ok => false); variable pcfree_deasserted : boolean; + variable cycle_type : std_logic_vector(7 downto 0); + variable length_field : std_logic_vector(11 downto 0); begin -- Always the first thing in the process, set up things for the VUnit test runner test_runner_setup(runner, runner_cfg); @@ -186,14 +189,129 @@ begin end if; get_flash_c(net, 16, my_queue, response_code, status, crc_ok); check(crc_ok, "CRC Check failed"); - -- TODO: the data's not coming back right. for j in 0 to 15 loop - report "Flash Byte: " & to_hstring(to_unsigned(pop_byte(my_queue), 8)); + check_equal(To_Std_Logic_Vector(pop_byte(my_queue), 8), fake_flash_pattern(16#03020000# + j), + "Flash byte " & integer'image(j)); end loop; end loop; -- would normally wait for the completion alert now wait for 300 us; + elsif run("flash_write") then + flash_cap_reg.flash_channel_enable := '1'; + set_config(net, CH3_CAPABILITIES_OFFSET, pack(flash_cap_reg), response_code, status, crc_ok); + check(crc_ok, "Set Config CRC Check failed"); + write_bus(net, bus_handle, To_StdLogicVector(espi_regs_pkg.CONTROL_OFFSET, bus_handle.p_address_length), + espi_regs_pkg.CONTROL_FLASH_WRITE_ENABLE_MASK); + wait_until_idle(net, bus_handle); + + -- Erase, so the write lands on ones and reads back exactly + put_flash_erase(net, X"00001000", flash_erase_4k, response_code, status, crc_ok); + check(crc_ok, "put_flash_erase CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 0, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "erase completion CRC Check failed"); + check_equal(cycle_type, success_no_data, "erase completion cycle type"); + check_equal(length_field, std_logic_vector'(x"000"), "erase completion length"); + + -- 64 bytes is the channel's max payload, written across a + -- page boundary to make sure nothing wraps on the way through + for i in 0 to 63 loop + push_byte(my_queue, to_integer(write_pattern(16#10F0# + i))); + end loop; + put_flash_write(net, X"000010F0", 64, my_queue, response_code, status, crc_ok); + check(crc_ok, "put_flash_write CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 0, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "write completion CRC Check failed"); + check_equal(cycle_type, success_no_data, "write completion cycle type"); + check_equal(length_field, std_logic_vector'(x"000"), "write completion length"); + + -- Read back the written range plus a byte either side + put_flash_read(net, X"000010EF", 66, response_code, status, crc_ok); + check(crc_ok, "put_flash_read CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 66, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "read completion CRC Check failed"); + check_equal(cycle_type, success_with_data_only, "read completion cycle type"); + check_equal(To_Std_Logic_Vector(pop_byte(my_queue), 8), std_logic_vector'(x"FF"), "byte before write still erased"); + for i in 0 to 63 loop + check_equal(To_Std_Logic_Vector(pop_byte(my_queue), 8), write_pattern(16#10F0# + i), + "written byte " & integer'image(i)); + end loop; + check_equal(To_Std_Logic_Vector(pop_byte(my_queue), 8), std_logic_vector'(x"FF"), "byte after write still erased"); + elsif run("flash_erase") then + flash_cap_reg.flash_channel_enable := '1'; + set_config(net, CH3_CAPABILITIES_OFFSET, pack(flash_cap_reg), response_code, status, crc_ok); + check(crc_ok, "Set Config CRC Check failed"); + write_bus(net, bus_handle, To_StdLogicVector(espi_regs_pkg.CONTROL_OFFSET, bus_handle.p_address_length), + espi_regs_pkg.CONTROL_FLASH_WRITE_ENABLE_MASK); + wait_until_idle(net, bus_handle); + + put_flash_erase(net, X"00002000", flash_erase_4k, response_code, status, crc_ok); + check(crc_ok, "put_flash_erase CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 0, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "erase completion CRC Check failed"); + check_equal(cycle_type, success_no_data, "erase completion cycle type"); + + -- last bytes of the sector are erased, first of the next are not + put_flash_read(net, X"00002FF8", 16, response_code, status, crc_ok); + check(crc_ok, "put_flash_read CRC Check failed"); + wait_for_alert(net); + get_flash_c(net, 16, my_queue, response_code, status, crc_ok); + check(crc_ok, "read completion CRC Check failed"); + for i in 0 to 7 loop + check_equal(To_Std_Logic_Vector(pop_byte(my_queue), 8), std_logic_vector'(x"FF"), "erased byte " & integer'image(i)); + end loop; + for i in 8 to 15 loop + check_equal(To_Std_Logic_Vector(pop_byte(my_queue), 8), fake_flash_pattern(16#2FF8# + i), "neighbour byte " & integer'image(i)); + end loop; + + -- an unsupported size code is answered, unsuccessfully + put_flash_erase(net, X"00002000", flash_erase_32k, response_code, status, crc_ok); + check(crc_ok, "put_flash_erase CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 0, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "erase completion CRC Check failed"); + check_equal(cycle_type, unsuccessful_no_data_only, "unsupported erase completion cycle type"); + check_equal(length_field, std_logic_vector'(x"000"), "unsupported erase completion length"); + elsif run("write_refused_by_register") then + -- Channel up but the write enable bit left at its reset + -- value: writes and erases are answered, unsuccessfully, and + -- the flash is untouched. + flash_cap_reg.flash_channel_enable := '1'; + set_config(net, CH3_CAPABILITIES_OFFSET, pack(flash_cap_reg), response_code, status, crc_ok); + check(crc_ok, "Set Config CRC Check failed"); + + for i in 0 to 15 loop + push_byte(my_queue, 0); + end loop; + put_flash_write(net, X"00004000", 16, my_queue, response_code, status, crc_ok); + check(crc_ok, "put_flash_write CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 0, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "write completion CRC Check failed"); + check_equal(cycle_type, unsuccessful_no_data_only, "refused write completion cycle type"); + check_equal(length_field, std_logic_vector'(x"000"), "refused write completion length"); + + put_flash_erase(net, X"00004000", flash_erase_4k, response_code, status, crc_ok); + check(crc_ok, "put_flash_erase CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 0, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "erase completion CRC Check failed"); + check_equal(cycle_type, unsuccessful_no_data_only, "refused erase completion cycle type"); + + -- and a read still works afterwards, returning the untouched contents + put_flash_read(net, X"00004000", 16, response_code, status, crc_ok); + check(crc_ok, "put_flash_read CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 16, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "read completion CRC Check failed"); + check_equal(cycle_type, success_with_data_only, "read completion cycle type"); + for i in 0 to 15 loop + check_equal(To_Std_Logic_Vector(pop_byte(my_queue), 8), fake_flash_pattern(16#4000# + i), "untouched byte " & integer'image(i)); + end loop; elsif run("oob_no_pec_uart") then enable_debug_mode(net); --Enable OOB Channel diff --git a/hdl/ip/vhd/espi/sims/espi_tb_pkg.vhd b/hdl/ip/vhd/espi/sims/espi_tb_pkg.vhd index cf369dd9..8c6c2773 100644 --- a/hdl/ip/vhd/espi/sims/espi_tb_pkg.vhd +++ b/hdl/ip/vhd/espi/sims/espi_tb_pkg.vhd @@ -71,6 +71,22 @@ package espi_tb_pkg is constant num_bytes: integer; constant bad_crc : boolean := false ) return cmd_t; + -- Payload is consumed from the queue + impure function build_put_flash_write_cmd( + constant address : in std_logic_vector(31 downto 0); + constant num_bytes: integer; + constant payload : queue_t; + constant bad_crc : boolean := false + ) return cmd_t; + impure function build_put_flash_erase_cmd( + constant address : in std_logic_vector(31 downto 0); + constant size_code: std_logic_vector(11 downto 0); + constant bad_crc : boolean := false + ) return cmd_t; + -- Initial contents of the fake flash behind the test harness. Also a + -- convenient source of write payloads that differ from what is there. + function fake_flash_pattern(constant addr : natural) return std_logic_vector; + function write_pattern(constant addr : natural) return std_logic_vector; impure function build_get_flash_c_cmd( constant bad_crc : boolean := false ) return cmd_t; @@ -197,6 +213,72 @@ package body espi_tb_pkg is return cmd; end function; + function fake_flash_pattern(constant addr : natural) return std_logic_vector is + begin + -- odd multiplier so any aligned 256 byte run is a bijection + return To_Std_Logic_Vector((addr * 7 + 3) mod 256, 8); + end function; + + function write_pattern(constant addr : natural) return std_logic_vector is + begin + -- flash programming can only clear bits, so what is written has to + -- be a subset of the erased state (all ones) and not of the initial + -- pattern: tests erase first and then check for exactly this + return To_Std_Logic_Vector((addr * 13 + 5) mod 256, 8); + end function; + + -- Common header for the put_flash_np family: opcode, cycle type, + -- tag/length, length, then the 32 bit address MSB first. + impure function build_put_flash_hdr( + constant cycle_kind : std_logic_vector(7 downto 0); + constant address : in std_logic_vector(31 downto 0); + constant length_field: std_logic_vector(11 downto 0) + ) return cmd_t is + variable cmd : cmd_t := (new_queue, 0); + begin + push_byte(cmd.queue, to_integer(opcode_put_flash_np)); + push_byte(cmd.queue, to_integer(cycle_kind)); + push_byte(cmd.queue, to_integer("0000" & length_field(11 downto 8))); + push_byte(cmd.queue, to_integer(length_field(7 downto 0))); + push_byte(cmd.queue, to_integer(address(31 downto 24))); + push_byte(cmd.queue, to_integer(address(23 downto 16))); + push_byte(cmd.queue, to_integer(address(15 downto 8))); + push_byte(cmd.queue, to_integer(address(7 downto 0))); + cmd.num_bytes := 8; + return cmd; + end function; + + impure function build_put_flash_write_cmd( + constant address : in std_logic_vector(31 downto 0); + constant num_bytes: integer; + constant payload : queue_t; + constant bad_crc : boolean := false + ) return cmd_t is + variable cmd : cmd_t; + begin + cmd := build_put_flash_hdr(flash_write, address, To_Std_Logic_Vector(num_bytes, 12)); + for i in 0 to num_bytes - 1 loop + push_byte(cmd.queue, pop_byte(payload)); + end loop; + cmd.num_bytes := cmd.num_bytes + num_bytes; + push_byte(cmd.queue, to_integer(crc8_atm(cmd.queue, bad_crc))); + cmd.num_bytes := cmd.num_bytes + 1; + return cmd; + end function; + + impure function build_put_flash_erase_cmd( + constant address : in std_logic_vector(31 downto 0); + constant size_code: std_logic_vector(11 downto 0); + constant bad_crc : boolean := false + ) return cmd_t is + variable cmd : cmd_t; + begin + cmd := build_put_flash_hdr(flash_erase, address, size_code); + push_byte(cmd.queue, to_integer(crc8_atm(cmd.queue, bad_crc))); + cmd.num_bytes := cmd.num_bytes + 1; + return cmd; + end function; + impure function build_put_flash_np_cmd( constant address : in std_logic_vector(31 downto 0); constant num_bytes: integer; diff --git a/hdl/ip/vhd/espi/sims/espi_th.vhd b/hdl/ip/vhd/espi/sims/espi_th.vhd index c14ca38f..08b19d56 100644 --- a/hdl/ip/vhd/espi/sims/espi_th.vhd +++ b/hdl/ip/vhd/espi/sims/espi_th.vhd @@ -37,6 +37,8 @@ architecture th of espi_th is signal flash_rfifo_data : std_logic_vector(7 downto 0); signal flash_rfifo_rdack : std_logic; signal flash_rfifo_rempty : std_logic; + signal flash_wfifo_data : std_logic_vector(7 downto 0); + signal flash_wfifo_write : std_logic; signal axi_if : axil15x32_pkg.axil_t; signal uart_data_line : std_logic; signal uart_handshake : std_logic; @@ -92,7 +94,13 @@ begin bresp => axi_if.write_response.resp ); + -- Built with writes allowed so the write path can be exercised; the + -- runtime enable bit still starts off, which is what the refusal tests + -- rely on. dut: entity work.espi_target_top + generic map ( + FLASH_WRITES_ALLOWED => true + ) port map ( clk => clk, reset => reset, @@ -109,6 +117,8 @@ begin flash_rfifo_data => flash_rfifo_data, flash_rfifo_rdack => flash_rfifo_rdack, flash_rfifo_rempty => flash_rfifo_rempty, + flash_wfifo_data => flash_wfifo_data, + flash_wfifo_write => flash_wfifo_write, to_sp_uart_data => to_sp_uart_data, to_sp_uart_valid => to_sp_uart_valid, to_sp_uart_ready => to_sp_uart_ready, @@ -148,6 +158,8 @@ begin reset => reset, espi_cmd_fifo_data => flash_cfifo_data, espi_cmd_fifo_write => flash_cfifo_write, + espi_wfifo_data => flash_wfifo_data, + espi_wfifo_write => flash_wfifo_write, flash_rdata => flash_rfifo_data, flash_rdata_empty => flash_rfifo_rempty, flash_rdata_rdack => flash_rfifo_rdack diff --git a/hdl/ip/vhd/espi/sims/models/espi_controller_vc_pkg.vhd b/hdl/ip/vhd/espi/sims/models/espi_controller_vc_pkg.vhd index d712a69f..3ed7a919 100644 --- a/hdl/ip/vhd/espi/sims/models/espi_controller_vc_pkg.vhd +++ b/hdl/ip/vhd/espi/sims/models/espi_controller_vc_pkg.vhd @@ -71,6 +71,36 @@ package espi_controller_vc_pkg is variable status : inout std_logic_vector(15 downto 0); variable crc_ok : inout boolean ); + -- As get_flash_c but hands back the completion header too, for + -- completions whose cycle type is the interesting part. + procedure get_flash_completion( + signal net : inout network_t; + constant num_bytes : in integer; + variable data_queue: out queue_t; + variable cycle_type: out std_logic_vector(7 downto 0); + variable length: out std_logic_vector(11 downto 0); + variable response_code: inout std_logic_vector(7 downto 0); + variable status : inout std_logic_vector(15 downto 0); + variable crc_ok : inout boolean + ); + -- payload queue is consumed + procedure put_flash_write( + signal net : inout network_t; + constant address : in std_logic_vector(31 downto 0); + constant num_bytes: in integer; + constant payload : in queue_t; + variable response_code: inout std_logic_vector(7 downto 0); + variable status : inout std_logic_vector(15 downto 0); + variable crc_ok : inout boolean + ); + procedure put_flash_erase( + signal net : inout network_t; + constant address : in std_logic_vector(31 downto 0); + constant size_code: in std_logic_vector(11 downto 0); + variable response_code: inout std_logic_vector(7 downto 0); + variable status : inout std_logic_vector(15 downto 0); + variable crc_ok : inout boolean + ); procedure put_iowr_short4( signal net : inout network_t; @@ -272,6 +302,83 @@ package body espi_controller_vc_pkg is end; + procedure get_flash_completion( + signal net : inout network_t; + constant num_bytes : in integer; + variable data_queue: out queue_t; + variable cycle_type: out std_logic_vector(7 downto 0); + variable length: out std_logic_vector(11 downto 0); + variable response_code: inout std_logic_vector(7 downto 0); + variable status : inout std_logic_vector(15 downto 0); + variable crc_ok : inout boolean + ) is + variable cmd : cmd_t := (new_queue, 0); + variable rx_bytes : integer := 3 + num_bytes + 4; -- 3 bytes header, num_bytes, response, 16bit status, 1 crc, + variable msg_target : actor_t := find("espi_vc"); + variable rx_queue : queue_t := new_queue; + variable hdr_byte : std_logic_vector(7 downto 0); + begin + cmd := build_get_flash_c_cmd; + enqueue_tx_data_bytes(net, msg_target, cmd.num_bytes, cmd.queue); + enqueue_transaction(net, msg_target, cmd.num_bytes, rx_bytes); + get_rx_queue(net, msg_target, rx_queue); + crc_ok := check_queue_crc(rx_queue); -- non-destructive to queue + response_code := std_logic_vector(to_unsigned(pop_byte(rx_queue), 8)); + cycle_type := To_Std_Logic_Vector(pop_byte(rx_queue), 8); + hdr_byte := To_Std_Logic_Vector(pop_byte(rx_queue), 8); + length(11 downto 8) := hdr_byte(3 downto 0); + length(7 downto 0) := To_Std_Logic_Vector(pop_byte(rx_queue), 8); + for i in 0 to num_bytes -1 loop + push_byte(data_queue, pop_byte(rx_queue)); + end loop; + status := get_status_from_queue_and_flush(rx_queue); + end; + + procedure put_flash_write( + signal net : inout network_t; + constant address : in std_logic_vector(31 downto 0); + constant num_bytes: in integer; + constant payload : in queue_t; + variable response_code: inout std_logic_vector(7 downto 0); + variable status : inout std_logic_vector(15 downto 0); + variable crc_ok : inout boolean + ) is + variable cmd : cmd_t := (new_queue, 0); + variable rx_bytes : integer := 4; -- response, 16bit status, 1 crc + variable msg_target : actor_t := find("espi_vc"); + variable rx_queue : queue_t := new_queue; + begin + cmd := build_put_flash_write_cmd(address, num_bytes, payload); + enqueue_tx_data_bytes(net, msg_target, cmd.num_bytes, cmd.queue); + enqueue_transaction(net, msg_target, cmd.num_bytes, rx_bytes); + get_rx_queue(net, msg_target, rx_queue); + crc_ok := check_queue_crc(rx_queue); -- non-destructive to queue + response_code := std_logic_vector(to_unsigned(pop_byte(rx_queue), 8)); + status := get_status_from_queue_and_flush(rx_queue); + end; + + procedure put_flash_erase( + signal net : inout network_t; + constant address : in std_logic_vector(31 downto 0); + constant size_code: in std_logic_vector(11 downto 0); + variable response_code: inout std_logic_vector(7 downto 0); + variable status : inout std_logic_vector(15 downto 0); + variable crc_ok : inout boolean + ) is + variable cmd : cmd_t := (new_queue, 0); + variable rx_bytes : integer := 4; -- response, 16bit status, 1 crc + variable msg_target : actor_t := find("espi_vc"); + variable rx_queue : queue_t := new_queue; + begin + cmd := build_put_flash_erase_cmd(address, size_code); + enqueue_tx_data_bytes(net, msg_target, cmd.num_bytes, cmd.queue); + enqueue_transaction(net, msg_target, cmd.num_bytes, rx_bytes); + get_rx_queue(net, msg_target, rx_queue); + crc_ok := check_queue_crc(rx_queue); -- non-destructive to queue + response_code := std_logic_vector(to_unsigned(pop_byte(rx_queue), 8)); + status := get_status_from_queue_and_flush(rx_queue); + end; + procedure put_iowr_short4( signal net : inout network_t; constant address : in std_logic_vector(15 downto 0); diff --git a/hdl/ip/vhd/espi/sims/models/fake_flash_txn_mgr.vhd b/hdl/ip/vhd/espi/sims/models/fake_flash_txn_mgr.vhd index 434a5864..feb5bd78 100644 --- a/hdl/ip/vhd/espi/sims/models/fake_flash_txn_mgr.vhd +++ b/hdl/ip/vhd/espi/sims/models/fake_flash_txn_mgr.vhd @@ -8,13 +8,19 @@ -- The interface is simple: we have a 32bit wide FIFO for commands, and get 2 -- words there: First word is the 32bit flash address from SP5's perspective --- and the second word is the number of bytes to read. --- The read data is pushed back byte-by-byte into an 8bit wide FIFO. Note that +-- and the second word is the byte count, with the request kind in its top +-- nibble (0 read, 1 write, 2 erase) as flash_channel_pkg encodes it. +-- Read data is pushed back byte-by-byte into an 8bit wide FIFO. Note that -- This fifo is not necessarily deep enough to hold a whole transaction read -- (which may be many sets of 256byte blocks), but the espi block should -- generally be able to keep up so we may not have to model that here. --- To simplify the model, we're just going to put a counting pattern back --- into the FIFO as the "read data" which may facilitate debugging. +-- Writes take their payload from the byte stream the flash channel pushes +-- ahead of the command, and writes and erases answer with one status byte, +-- zero for success, the way the real client in spi_nor_top does. +-- +-- Behind it is a 64kB window of NOR-like memory, initialised to +-- fake_flash_pattern so reads can be checked, with clear-only programming +-- and erase-to-ones so writes and erases can be checked back through reads. library ieee; use ieee.std_logic_1164.all; @@ -25,6 +31,8 @@ library vunit_lib; context vunit_lib.vunit_context; context vunit_lib.com_context; +use work.espi_tb_pkg.all; + entity fake_flash_txn_mgr is port ( clk : in std_logic; @@ -32,6 +40,9 @@ entity fake_flash_txn_mgr is -- espi cmd fifo interface espi_cmd_fifo_data : in std_logic_vector(31 downto 0); espi_cmd_fifo_write : in std_logic; + -- write payload, host to flash + espi_wfifo_data : in std_logic_vector(7 downto 0) := (others => '0'); + espi_wfifo_write : in std_logic := '0'; -- Raw flash read_data flash_rdata : out std_logic_vector(7 downto 0); @@ -42,12 +53,18 @@ end entity; architecture model of fake_flash_txn_mgr is + constant window_bytes : natural := 16#10000#; constant cmd_queue : queue_t := new_queue; + constant wqueue : queue_t := new_queue; signal addr : std_logic_vector(31 downto 0); signal cmd_idx : natural range 0 to 1 := 0; signal write_en : std_logic; signal wdata : std_logic_vector(7 downto 0); + -- SAFS erase size codes + constant erase_4k : std_logic_vector(11 downto 0) := x"001"; + constant erase_64k : std_logic_vector(11 downto 0) := x"003"; + begin -- take in two words from the command fifo, (allow queueing more) @@ -67,46 +84,90 @@ begin else -- push both parts of the command into the fifo push(cmd_queue, addr); -- full address - push(cmd_queue, espi_cmd_fifo_data(11 downto 0)); -- txn size + push(cmd_queue, espi_cmd_fifo_data); -- kind and txn size cmd_idx <= 0; end if; end if; end process; - fake_flash_reads: process + capture_payload: process + begin + wait until rising_edge(clk); + if espi_wfifo_write = '1' then + push(wqueue, espi_wfifo_data); + end if; + end process; + + fake_flash: process + type mem_t is array (0 to window_bytes - 1) of std_logic_vector(7 downto 0); + variable mem : mem_t; variable addr : std_logic_vector(31 downto 0); - variable len : std_logic_vector(11 downto 0); - variable top : std_logic_vector(11 downto 0); - variable data : std_logic_vector(11 downto 0); + variable word : std_logic_vector(31 downto 0); + variable len : natural; + variable idx : natural; + variable erase_bytes : natural; + variable status : std_logic_vector(7 downto 0); + + -- Push one byte back with a few cycles of stall after it, so the + -- downstream logic is seen to cope with data that is not + -- clock-over-clock. The delay is somewhat arbitrary but has to be + -- more than the fifo latency for the stall to be visible. + procedure push_byte(constant b : std_logic_vector(7 downto 0)) is + begin + wdata <= b; + write_en <= '1'; + wait until rising_edge(clk); + write_en <= '0'; + for i in 0 to 5 loop + wait until rising_edge(clk); + end loop; + end procedure; begin + for i in mem'range loop + mem(i) := fake_flash_pattern(i); + end loop; write_en <= '0'; loop - exit when not is_empty(cmd_queue); - wait until falling_edge(clk); - end loop; - addr := pop(cmd_queue); - len := pop(cmd_queue); - top := len; - -- push some data into the fifo - while len > 0 loop - data := top - len; - wdata <= resize(data, wdata'length); - write_en <= '1'; - if rising_edge(clk)then - len := len - 1; - wait on clk; - write_en <= '0'; - -- We want this to be "fast" for sim but representative of a real - -- design so we wait a few cycles before pushing the next data into - -- the fifo. The number for delay here is somewhat arbirary, but - -- we want it to be more than 2 or 3 so fifo latency doesn't mask - -- the delay so that we see our downstream logic does the right thing - -- with stalls in the data. - for i in 0 to 5 loop - wait until rising_edge(clk); - end loop; - end if; - wait on clk; + loop + exit when not is_empty(cmd_queue); + wait until falling_edge(clk); + end loop; + addr := pop_std_ulogic_vector(cmd_queue); + word := pop_std_ulogic_vector(cmd_queue); + len := to_integer(word(11 downto 0)); + status := x"00"; + case word(31 downto 28) is + when x"1" => + -- the flash channel pushes the whole payload before the + -- command, so waiting here is only ever a fifo latency + while length(wqueue) < len loop + wait until rising_edge(clk); + end loop; + for i in 0 to len - 1 loop + idx := (to_integer(addr) + i) mod window_bytes; + mem(idx) := mem(idx) and pop_std_ulogic_vector(wqueue); + end loop; + push_byte(status); + when x"2" => + case word(11 downto 0) is + when erase_4k => erase_bytes := 4096; + when erase_64k => erase_bytes := 65536; + when others => erase_bytes := 0; + end case; + if erase_bytes = 0 then + status := x"01"; + else + for i in 0 to erase_bytes - 1 loop + idx := ((to_integer(addr) / erase_bytes) * erase_bytes + i) mod window_bytes; + mem(idx) := x"FF"; + end loop; + end if; + push_byte(status); + when others => + for i in 0 to len - 1 loop + push_byte(mem((to_integer(addr) + i) mod window_bytes)); + end loop; + end case; end loop; end process; diff --git a/hdl/ip/vhd/espi/sys_regs/espi_regs.rdl b/hdl/ip/vhd/espi/sys_regs/espi_regs.rdl index 26fdc291..9eb6f60c 100644 --- a/hdl/ip/vhd/espi/sys_regs/espi_regs.rdl +++ b/hdl/ip/vhd/espi/sys_regs/espi_regs.rdl @@ -32,6 +32,12 @@ addrmap espi_regs { field { desc = "Issue an espi reset via the debug logic. Cleared by hardware after reset."; } espi_reset[5:5] = 0; + field { + desc = "Allow the host to write and erase the flash over the eSPI flash + channel. Only has effect on a build whose FLASH_WRITES_ALLOWED generic is + set; elsewhere writes are refused regardless. A refused write or erase is + answered with an unsuccessful completion."; + } flash_write_enable[4:4] = 0; field { desc = "Set to one to reset the command FIFO. Cleared by hardware after reset."; } cmd_fifo_reset[3:3] = 0; diff --git a/hdl/ip/vhd/espi/sys_regs/espi_regs.vhd b/hdl/ip/vhd/espi/sys_regs/espi_regs.vhd index dfc21a3d..1cc67cb6 100644 --- a/hdl/ip/vhd/espi/sys_regs/espi_regs.vhd +++ b/hdl/ip/vhd/espi/sys_regs/espi_regs.vhd @@ -25,6 +25,8 @@ entity espi_regs is post_code : in std_logic_vector(31 downto 0); post_code_valid : in std_logic; espi_reset : in std_logic; + -- runtime half of the SAFS write permission, see espi_target_top + flash_write_enable : out std_logic; stuff_fifo : out std_logic; stuff_wds : out std_logic_vector(15 downto 0); -- read-only view of eSPI spec registers @@ -161,6 +163,7 @@ begin dbg_chan.rd.rdack <= '1' when axi_if.read_data.ready = '1' and axi_if.read_data.valid = '1' and resp_fifo_ack = '1' else '0'; dbg_chan.espi_reset <= control_reg.espi_reset; + flash_write_enable <= control_reg.flash_write_enable; read_logic: process(clk, reset) begin diff --git a/hdl/ip/vhd/espi/txn_layer/command_processor.vhd b/hdl/ip/vhd/espi/txn_layer/command_processor.vhd index 282e964a..4806c662 100644 --- a/hdl/ip/vhd/espi/txn_layer/command_processor.vhd +++ b/hdl/ip/vhd/espi/txn_layer/command_processor.vhd @@ -33,6 +33,10 @@ entity command_processor is post_code : out std_logic_vector(31 downto 0); post_code_valid : out std_logic; aborted_due_to_bad_crc : out boolean; + -- Whether SAFS writes and erases may be forwarded to the flash. Ones + -- that arrive while this is low are still queued, marked refused, so + -- the host gets an unsuccessful completion for them. + flash_writes_allowed : in std_logic; -- flash channel requests flash_req : view flash_chan_req_source; -- uart channel put interface here @@ -127,14 +131,14 @@ architecture rtl of command_processor is when opcode_put_flash_np => case header.cycle_kind is when flash_write => - -- Note that while we'll rx this payload, we will not - -- act upon it, as we do not allow flash writes over eSPI + -- The payload streams into the flash channel's DPR + -- as it arrives; see flash_req.wdata below. next_state.next_state := parse_addr_header; next_state.cmd_payload_bytes := to_integer(header.length); - when flash_erase => - -- Note that while we'll rx this payload, we will not - -- act upon it, as we do not allow flash writes over eSPI when others => + -- Reads and erases are address only; for an erase + -- the length field is the block size code, not a + -- payload count. null; end case; when opcode_put_pc => @@ -174,8 +178,27 @@ begin -- pass through the flash channel requests here flash_req.espi_hdr <= r.cmd_header; flash_req.sp5_flash_address <= r.ch_addr; - flash_req.flash_np_enqueue_req <= true when r.valid_redge and r.cmd_header.opcode.value = opcode_put_flash_np and r.cmd_header.cycle_kind = flash_read else false; + flash_req.flash_np_enqueue_req <= true when r.valid_redge and r.cmd_header.opcode.value = opcode_put_flash_np and + (r.cmd_header.cycle_kind = flash_read or + r.cmd_header.cycle_kind = flash_write or + r.cmd_header.cycle_kind = flash_erase) else false; flash_req.flash_get_req <= true when r.valid_redge and r.cmd_header.opcode.value = opcode_get_flash_c else false; + -- Permission is sampled at enqueue time only, via kind. Anything that is + -- not a read and is not permitted is queued as refused. + flash_req.kind <= flash_rd when r.cmd_header.cycle_kind = flash_read else + flash_wr when r.cmd_header.cycle_kind = flash_write and flash_writes_allowed = '1' else + flash_er when r.cmd_header.cycle_kind = flash_erase and flash_writes_allowed = '1' else + flash_refused; + -- Write payload bytes go straight through as they are parsed, indexed + -- from the start of the payload so the channel can place them without + -- keeping its own count across a possibly-aborted command. + flash_req.wdata <= data_from_host.data; + flash_req.wdata_valid <= data_from_host.valid when r.cmd_header.opcode.value = opcode_put_flash_np and + r.cmd_header.cycle_kind = flash_write and + r.state = parse_data else '0'; + flash_req.wdata_idx <= To_Std_Logic_Vector(to_integer(r.cmd_header.length) - r.rem_data_bytes, flash_req.wdata_idx'length) + when r.state = parse_data and r.rem_data_bytes <= to_integer(r.cmd_header.length) else + (others => '0'); post_code <= r.io_wr_data; post_code_valid <= '1' when r.cmd_header.opcode.value = opcode_put_iowr_short_4byte and (r.crc_good or (r.crc_bad and (not regs_if.enforce_crcs))) else '0'; diff --git a/hdl/ip/vhd/espi/txn_layer/espi_protocol_pkg.vhd b/hdl/ip/vhd/espi/txn_layer/espi_protocol_pkg.vhd index ad2fa745..44277404 100644 --- a/hdl/ip/vhd/espi/txn_layer/espi_protocol_pkg.vhd +++ b/hdl/ip/vhd/espi/txn_layer/espi_protocol_pkg.vhd @@ -44,9 +44,19 @@ package espi_protocol_pkg is constant flash_read : std_logic_vector(7 downto 0) := "00000000"; constant success_no_data : std_logic_vector(7 downto 0) := "00000110"; - -- We won't actually write/erase, our behavior catching this is TBD constant flash_write : std_logic_vector(7 downto 0) := "00000001"; constant flash_erase : std_logic_vector(7 downto 0) := "00000010"; + -- Completion cycle types. The "with data" family is 0000_1xx1 with the + -- middle bits as first/last flags; "without data" is 0000_0110 for success + -- and 0000_1x00 for failure. A single unsplit response is the "only" form. + -- Values per the eSPI base specification's completion cycle-type table. + constant success_with_data_only : std_logic_vector(7 downto 0) := "00001111"; + constant unsuccessful_no_data_only : std_logic_vector(7 downto 0) := "00001100"; + -- SAFS flash erase: the length field of the request carries the erase + -- block size as a code rather than a byte count. + constant flash_erase_4k : std_logic_vector(11 downto 0) := x"001"; + constant flash_erase_32k : std_logic_vector(11 downto 0) := x"002"; + constant flash_erase_64k : std_logic_vector(11 downto 0) := x"003"; -- Header Indices for general eSPI packages constant cycle_type_idx : integer := 0; diff --git a/hdl/ip/vhd/espi/txn_layer/response_processor.vhd b/hdl/ip/vhd/espi/txn_layer/response_processor.vhd index 8c2ad072..79379a01 100644 --- a/hdl/ip/vhd/espi/txn_layer/response_processor.vhd +++ b/hdl/ip/vhd/espi/txn_layer/response_processor.vhd @@ -222,7 +222,15 @@ begin if data_to_host.ready then v.resp_idx := r.resp_idx + 1; if r.resp_idx = 2 then - v.state := RESPONSE_PAYLOAD; + -- A write or erase completes with no payload at all; + -- counting one down from a zero length would run the + -- payload stage for 4096 bytes. + if r.temp_length = 0 then + v.state := STATUS; + v.status := live_status; + else + v.state := RESPONSE_PAYLOAD; + end if; end if; end if; when RESPONSE_UART_HEADER => diff --git a/hdl/ip/vhd/espi/txn_layer/txn_layer_top.vhd b/hdl/ip/vhd/espi/txn_layer/txn_layer_top.vhd index b7d979dd..91207341 100644 --- a/hdl/ip/vhd/espi/txn_layer/txn_layer_top.vhd +++ b/hdl/ip/vhd/espi/txn_layer/txn_layer_top.vhd @@ -30,6 +30,7 @@ entity txn_layer_top is flash_np_free : in std_logic; flash_c_avail : in std_logic; -- flash channel requests/responses + flash_writes_allowed : in std_logic; flash_req : view flash_chan_req_source; flash_resp : view flash_chan_resp_sink; -- uart channel requests/responses @@ -128,6 +129,7 @@ begin espi_reset => espi_reset, regs_if => regs_if, vwire_if => vwire_if, + flash_writes_allowed => flash_writes_allowed, flash_req => flash_req, host_to_sp_espi => host_to_sp_espi, running_crc => rx_running_crc, diff --git a/hdl/ip/vhd/spi_nor_controller/espi_txn/espi_flash_txn_mgr.vhd b/hdl/ip/vhd/spi_nor_controller/espi_txn/espi_flash_txn_mgr.vhd index 8ecc8105..ea6212f5 100644 --- a/hdl/ip/vhd/spi_nor_controller/espi_txn/espi_flash_txn_mgr.vhd +++ b/hdl/ip/vhd/spi_nor_controller/espi_txn/espi_flash_txn_mgr.vhd @@ -2,6 +2,13 @@ -- License, v. 2.0. If a copy of the MPL was not distributed with this -- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- Flash client for the eSPI flash channel. Commands arrive as two words: the +-- SP5's address, then a length word whose top nibble says what kind of +-- request it is (see flash_channel_pkg in the eSPI IP). Reads come back on +-- the data FIFO as flash bytes. Writes and erases run the whole flash side +-- sequence here (write enable, program or erase, poll until not busy) and +-- come back as a single status byte, zero for success, so the eSPI side can +-- form a completion without knowing anything about the flash part. library ieee; use ieee.std_logic_1164.all; @@ -38,13 +45,15 @@ entity espi_flash_txn_mgr is -- espi command espi_cmd: out spi_nor_cmd_t; spi_hw_busy : in std_logic; + -- High while the bytes the engine shifts out have to come from the + -- eSPI write payload FIFO rather than the hubris TX FIFO. + tx_from_espi : out std_logic; -- espi data fifo interface espi_flash_data_byte : out std_logic_vector(7 downto 0); flash_data_byte_write : out std_logic; -- Raw flash read_data flash_rdata : in std_logic_vector(7 downto 0); - flash_rdata_write : in std_logic; - -- + flash_rdata_write : in std_logic ); end entity; @@ -52,11 +61,33 @@ architecture rtl of espi_flash_txn_mgr is attribute mark_debug : string; constant max_flash_read_size : natural := 255; + constant page_bytes : natural := 256; constant fast_read_dummy_cycles : natural := 8; - type state_t is (idle, read_cmd_addr, read_cmd_len, issue_read, wait_for_data); + -- Request kinds, matching flash_channel_pkg.to_kind_bits + constant kind_read : std_logic_vector(3 downto 0) := x"0"; + constant kind_write : std_logic_vector(3 downto 0) := x"1"; + constant kind_erase : std_logic_vector(3 downto 0) := x"2"; + -- eSPI SAFS erase size codes carried in the length field + constant erase_4k : std_logic_vector(11 downto 0) := x"001"; + constant erase_64k : std_logic_vector(11 downto 0) := x"003"; + -- Status bytes reported back for writes and erases + constant status_ok : std_logic_vector(7 downto 0) := x"00"; + constant status_unsupported : std_logic_vector(7 downto 0) := x"01"; + constant status_timeout : std_logic_vector(7 downto 0) := x"02"; + -- Polls before a program or erase is given up on. Each poll is a short + -- transaction, hundreds of ns, so this is several seconds: comfortably + -- past a worst case 64kB block erase, but not forever if the part is + -- absent. + constant max_polls : natural := 2**24 - 1; + + type state_t is (idle, read_cmd_addr, read_cmd_len, wait_for_data, + wait_idle, issue_cmd, wait_done, report_status); + -- Which flash transaction the current request is up to + type step_t is (step_read, step_wren, step_program, step_erase, step_poll); type reg_t is record state : state_t; + step : step_t; cmd_rdack: std_logic; data_bytes: natural range 0 to 256; dummy_cycles: natural range 0 to 256; @@ -69,9 +100,14 @@ architecture rtl of espi_flash_txn_mgr is apob_end_addr : std_logic_vector(31 downto 0); next_flash_addr: std_logic_vector(31 downto 0); len: std_logic_vector(31 downto 0); + erase_op : std_logic_vector(7 downto 0); + is_erase : boolean; + status : std_logic_vector(7 downto 0); + polls : natural range 0 to max_polls; end record; constant reg_reset : reg_t := ( state => idle, + step => step_read, cmd_rdack => '0', data_bytes => 0, dummy_cycles => 0, @@ -83,40 +119,59 @@ architecture rtl of espi_flash_txn_mgr is cur_flash_addr => (others => '0'), apob_end_addr => (others => '0'), next_flash_addr => (others => '0'), - len => (others => '0') + len => (others => '0'), + erase_op => SECTOR_ERASE_4BYTE_OP, + is_erase => false, + status => status_ok, + polls => 0 ); signal r, rin: reg_t; attribute mark_debug of r : signal is "TRUE"; - + -- Bytes left in the page cur_addr sits in; a page program wraps inside + -- its page on the part, so a chunk never crosses one. + function bytes_to_page_end(addr : std_logic_vector(31 downto 0)) return natural is + begin + return page_bytes - to_integer(addr(7 downto 0)); + end function; begin espi_cmd.addr <=r.cur_flash_addr; espi_cmd.data_bytes <= To_Std_Logic_Vector(r.data_bytes, espi_cmd.data_bytes'length); espi_cmd.dummy_cycles <= To_Std_Logic_Vector(fast_read_dummy_cycles, espi_cmd.dummy_cycles'length); - espi_cmd.instr <= FAST_READ_4BYTE_QUAD_OP; - espi_cmd.go_flag <= '1' when r.state = issue_read and spi_hw_busy = '0' else '0'; + espi_cmd.instr <= FAST_READ_4BYTE_QUAD_OP when r.step = step_read else + WRITE_ENABLE_OP when r.step = step_wren else + QUAD_INPUT_PAGE_PROGRAM_4BYTE_OP when r.step = step_program else + r.erase_op when r.step = step_erase else + READ_STATUS_REG1_OP; + espi_cmd.go_flag <= '1' when r.state = issue_cmd else '0'; + tx_from_espi <= '1' when r.step = step_program and (r.state = issue_cmd or r.state = wait_done) else '0'; -- Turn the flash data we read back around into the data fifo going to the espi, -- but only when we're expecting data going to the espi block and not hubris FIFOs. - espi_flash_data_byte <= flash_rdata; - flash_data_byte_write <= flash_rdata_write when r.state = wait_for_data else '0'; + -- The status byte for a write or erase goes out the same way. + espi_flash_data_byte <= flash_rdata when r.state = wait_for_data else r.status; + flash_data_byte_write <= flash_rdata_write when r.state = wait_for_data else + '1' when r.state = report_status else + '0'; espi_cmd_fifo_rdack <= r.cmd_rdack; -- state machine that will pull 2 words from the command fifo. -- Word1: is the 32bit SP5 address, which we'll adjust to be the flash address when we pop it - -- Word2: is the transaction length in byte-count. + -- Word2: is the transaction length in byte-count, with the request kind in the top nibble. -- We're going to do page reads, so we'll need to do this in 256byte chunks so long as we have room -- in the data fifo. When we get to rem_bytes < 256 we'll do a final read of the remaining bytes. sm: process(all) variable v: reg_t; + variable kind : std_logic_vector(3 downto 0); begin v := r; -- single cycle flag(s) v.cmd_rdack := '0'; + kind := espi_cmd_fifo_rdata(31 downto 28); case r.state is when idle => @@ -160,23 +215,56 @@ begin v.state := read_cmd_len; when read_cmd_len => - -- This comes 1-indexed from the eSPI block, so we need to subtract 1 below - v.rem_bytes := to_integer(espi_cmd_fifo_rdata(11 downto 0)) - 1; - v.state := issue_read; - -- We're either going to issue the max page size, or the 0-indexed remaining bytes - -- which ever is smaller. - v.txn_bytes := minimum(v.rem_bytes, max_flash_read_size); - -- spi is 1-indexed still, so we need to add 1 here - v.data_bytes := v.txn_bytes + 1; - - when issue_read => - if spi_hw_busy = '0' then - v.state := wait_for_data; - -- Adjust info for a potential next read or so we can decide we're done later - v.rem_bytes := r.rem_bytes - r.txn_bytes; - v.next_flash_addr := r.cur_flash_addr + (r.txn_bytes + 1); - end if; - + v.status := status_ok; + v.polls := 0; + v.is_erase := kind = kind_erase; + case kind is + when kind_write => + -- 1-indexed byte count; the payload is already sitting + -- in the write FIFO in full. + v.rem_bytes := to_integer(espi_cmd_fifo_rdata(11 downto 0)); + v.step := step_wren; + v.data_bytes := 0; + if v.rem_bytes = 0 then + v.state := report_status; + else + v.state := wait_idle; + end if; + when kind_erase => + -- Length is the SAFS erase size code. Only the sizes + -- with a 4-byte-address opcode are offered; anything + -- else is reported back as unsupported. + v.step := step_wren; + v.data_bytes := 0; + v.state := wait_idle; + case espi_cmd_fifo_rdata(11 downto 0) is + when erase_4k => + v.erase_op := SECTOR_ERASE_4BYTE_OP; + when erase_64k => + v.erase_op := BLOCK_ERASE_64K_4BYTE_OP; + when others => + v.status := status_unsupported; + v.state := report_status; + end case; + when others => + -- This comes 1-indexed from the eSPI block, so we need to subtract 1 below. + -- The guard is for simulation: the fifo's showahead word is + -- still the address for a delta after the pop, and a zero + -- length would otherwise put -1 in a natural. + if espi_cmd_fifo_rdata(11 downto 0) = 0 then + v.rem_bytes := 0; + else + v.rem_bytes := to_integer(espi_cmd_fifo_rdata(11 downto 0)) - 1; + end if; + v.step := step_read; + v.state := wait_idle; + -- We're either going to issue the max page size, or the 0-indexed remaining bytes + -- which ever is smaller. + v.txn_bytes := minimum(v.rem_bytes, max_flash_read_size); + -- spi is 1-indexed still, so we need to add 1 here + v.data_bytes := v.txn_bytes + 1; + end case; + when wait_for_data => -- count down when we load data into the fifo if flash_rdata_write = '1' and r.txn_bytes > 0 then @@ -188,13 +276,98 @@ begin v.state := idle; -- last data for this part of the transaction else - v.state := issue_read; + v.state := wait_idle; v.cur_flash_addr := r.next_flash_addr; v.txn_bytes := minimum(v.rem_bytes, max_flash_read_size); v.data_bytes := v.txn_bytes + 1; end if; end if; + -- Each flash transaction is one trip around wait_idle -> + -- issue_cmd -> wait_done (or wait_for_data for a read chunk), + -- with r.step saying which one it is. + when wait_idle => + -- The previous transaction has to be completely finished + -- before asking for the next one, otherwise the busy rise + -- waited on below would be its cs_n, not ours. + if spi_hw_busy = '0' then + case r.step is + when step_program => + v.data_bytes := minimum(r.rem_bytes, bytes_to_page_end(r.cur_flash_addr)); + when step_poll => + v.data_bytes := 1; + when step_read => + null; -- already sized for this chunk + when others => + v.data_bytes := 0; + end case; + v.state := issue_cmd; + end if; + when issue_cmd => + -- Hold go until the engine pulls cs_n low. It ignores go during + -- its minimum cs_n high time, and busy is already low then, so + -- leaving on "not busy" would drop the command. + if spi_hw_busy = '1' then + case r.step is + when step_read => + v.state := wait_for_data; + -- Adjust info for a potential next read or so we can decide we're done later + v.rem_bytes := r.rem_bytes - r.txn_bytes; + v.next_flash_addr := r.cur_flash_addr + (r.txn_bytes + 1); + when step_program => + v.state := wait_done; + v.rem_bytes := r.rem_bytes - r.data_bytes; + v.next_flash_addr := r.cur_flash_addr + r.data_bytes; + when others => + v.state := wait_done; + end case; + end if; + when wait_done => + -- The one byte back from a status poll shows up here; the + -- part reports write-in-progress in bit 0. + if flash_rdata_write = '1' then + v.status := flash_rdata; + end if; + if spi_hw_busy = '0' then + case r.step is + when step_read => + null; -- reads finish out of wait_for_data + when step_wren => + if r.is_erase then + v.step := step_erase; + else + v.step := step_program; + end if; + v.state := wait_idle; + when step_program | step_erase => + v.step := step_poll; + v.state := wait_idle; + when step_poll => + if r.status(0) = '1' then + if r.polls = max_polls then + v.status := status_timeout; + v.state := report_status; + else + v.polls := r.polls + 1; + v.state := wait_idle; + end if; + elsif r.rem_bytes > 0 then + -- more pages of this write to go, each one + -- needs its own write enable + v.status := status_ok; + v.cur_flash_addr := r.next_flash_addr; + v.step := step_wren; + v.state := wait_idle; + else + v.status := status_ok; + v.state := report_status; + end if; + end case; + end if; + when report_status => + -- one status byte goes out on the data fifo this cycle + v.state := idle; + end case; -- easier to set up the combo stuff here so that we diff --git a/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_espi_tb.vhd b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_espi_tb.vhd new file mode 100644 index 00000000..0af6a7fb --- /dev/null +++ b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_espi_tb.vhd @@ -0,0 +1,192 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.numeric_std_unsigned.all; + +library vunit_lib; + context vunit_lib.com_context; + context vunit_lib.vunit_context; + context vunit_lib.vc_context; +use work.spi_nor_tb_pkg.all; +use work.spi_nor_pkg.all; +use work.spi_nor_regs_pkg.all; +use work.spi_nor_target_vc_pkg.all; + +entity spi_nor_espi_tb is + generic ( + runner_cfg : string + ); +end entity; + +architecture tb of spi_nor_espi_tb is +begin + + th: entity work.spi_nor_espi_th; + + bench: process + alias reset is <>; + alias clk is <>; + alias cmd_wdata is <>; + alias cmd_write is <>; + alias payload_wdata is <>; + alias payload_write is <>; + alias data_rdata is <>; + alias data_rdack is <>; + alias data_rempty is <>; + constant flash : actor_t := find("spi_nor_target"); + + -- Request kinds, as flash_channel_pkg.to_kind_bits encodes them in + -- the top nibble of the length word + constant kind_read : std_logic_vector(3 downto 0) := x"0"; + constant kind_write : std_logic_vector(3 downto 0) := x"1"; + constant kind_erase : std_logic_vector(3 downto 0) := x"2"; + constant erase_4k : natural := 1; + constant erase_32k : natural := 2; + constant erase_64k : natural := 3; + + variable data : std_logic_vector(7 downto 0); + + -- Two-word command, the way the flash channel issues one + procedure put_cmd(constant addr : natural; constant kind : std_logic_vector(3 downto 0); constant len : natural) is + begin + wait until rising_edge(clk); + cmd_wdata <= std_logic_vector(to_unsigned(addr, 32)); + cmd_write <= '1'; + wait until rising_edge(clk); + cmd_wdata <= kind & std_logic_vector(to_unsigned(len, 28)); + wait until rising_edge(clk); + cmd_write <= '0'; + end procedure; + + procedure put_payload(constant addr : natural; constant len : natural) is + begin + wait until rising_edge(clk); + for i in 0 to len - 1 loop + payload_wdata <= pattern_byte(addr + i); + payload_write <= '1'; + wait until rising_edge(clk); + end loop; + payload_write <= '0'; + end procedure; + + procedure get_byte(variable b : out std_logic_vector(7 downto 0)) is + begin + loop + wait until rising_edge(clk); + exit when data_rempty = '0'; + end loop; + b := data_rdata; + data_rdack <= '1'; + wait until rising_edge(clk); + data_rdack <= '0'; + end procedure; + + -- Writes and erases report one status byte, zero for success + procedure check_status(constant expected : std_logic_vector(7 downto 0); constant msg : string) is + variable b : std_logic_vector(7 downto 0); + begin + get_byte(b); + check_equal(b, expected, msg); + end procedure; + + procedure check_flash_range(constant addr : natural; constant len : natural; constant erased : boolean; constant msg : string) is + variable b : std_logic_vector(7 downto 0); + begin + for i in 0 to len - 1 loop + read_flash_byte(net, flash, addr + i, b); + if erased then + check_equal(b, std_logic_vector'(x"FF"), msg & " @" & to_hstring(to_unsigned(addr + i, 32))); + else + check_equal(b, pattern_byte(addr + i), msg & " @" & to_hstring(to_unsigned(addr + i, 32))); + end if; + end loop; + end procedure; + + begin + test_runner_setup(runner, runner_cfg); + wait until reset = '0'; + wait for 500 ns; + -- The eSPI client only runs while the SP5 owns the flash + write_bus(net, bus_handle, To_StdLogicVector(SPICR_OFFSET + 16#100#, bus_handle.p_address_length), + SPICR_SP5_OWNS_FLASH_MASK); + wait_until_idle(net, bus_handle); + + while test_suite loop + if run("espi_read") then + fill_pattern(net, flash); + put_cmd(16#1000#, kind_read, 300); + for i in 0 to 299 loop + get_byte(data); + check_equal(data, pattern_byte(16#1000# + i), "read byte " & integer'image(i)); + end loop; + elsif run("espi_write_then_read_back") then + -- window starts erased + put_payload(16#1000#, 64); + put_cmd(16#1000#, kind_write, 64); + check_status(x"00", "write status"); + check_flash_range(16#0FF0#, 16, true, "before write"); + check_flash_range(16#1000#, 64, false, "written"); + check_flash_range(16#1040#, 16, true, "after write"); + -- and it comes back through the read path too + put_cmd(16#1000#, kind_read, 64); + for i in 0 to 63 loop + get_byte(data); + check_equal(data, pattern_byte(16#1000# + i), "read back byte " & integer'image(i)); + end loop; + elsif run("espi_write_crosses_page") then + -- A page program wraps inside its page on the part, so the + -- client has to split this into two programs itself + put_payload(16#10F0#, 64); + put_cmd(16#10F0#, kind_write, 64); + check_status(x"00", "write status"); + check_flash_range(16#1000#, 16, true, "start of first page untouched"); + check_flash_range(16#10F0#, 64, false, "written across the boundary"); + check_flash_range(16#1130#, 16, true, "after write"); + elsif run("espi_write_max_payload_run") then + -- Back to back writes of the channel's largest payload, as a + -- host streaming an image would issue them + for n in 0 to 7 loop + put_payload(16#2000# + n * 64, 64); + put_cmd(16#2000# + n * 64, kind_write, 64); + check_status(x"00", "write " & integer'image(n) & " status"); + end loop; + check_flash_range(16#2000#, 512, false, "streamed"); + elsif run("espi_erase_4k") then + fill_pattern(net, flash); + put_cmd(16#2000#, kind_erase, erase_4k); + check_status(x"00", "erase status"); + check_flash_range(16#1FF0#, 16, false, "before sector"); + check_flash_range(16#2000#, 16, true, "start of sector"); + check_flash_range(16#2FF0#, 16, true, "end of sector"); + check_flash_range(16#3000#, 16, false, "after sector"); + elsif run("espi_erase_64k") then + fill_pattern(net, flash); + put_cmd(16#0000#, kind_erase, erase_64k); + check_status(x"00", "erase status"); + check_flash_range(16#0000#, 16, true, "start of block"); + check_flash_range(16#FFF0#, 16, true, "end of block"); + elsif run("espi_erase_unsupported_size") then + fill_pattern(net, flash); + put_cmd(16#2000#, kind_erase, erase_32k); + check_status(x"01", "unsupported erase status"); + check_flash_range(16#2000#, 16, false, "untouched"); + -- the client is still alive afterwards + put_cmd(16#2000#, kind_read, 4); + for i in 0 to 3 loop + get_byte(data); + check_equal(data, pattern_byte(16#2000# + i), "read after refusal " & integer'image(i)); + end loop; + end if; + end loop; + + wait for 2 us; + test_runner_cleanup(runner); + wait; + end process; + + test_runner_watchdog(runner, 20 ms); +end tb; diff --git a/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_espi_th.vhd b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_espi_th.vhd new file mode 100644 index 00000000..6f7a2bb2 --- /dev/null +++ b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_espi_th.vhd @@ -0,0 +1,218 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +-- Harness for the eSPI client side of spi_nor_top: the three FIFOs the eSPI +-- wrapper would put between the flash channel and this block, with their far +-- ends left for the testbench to drive directly, and the real flash VC on +-- the other side. spi_nor_th covers the hubris register path; this is the +-- path the SP5's SAFS reads, writes and erases take. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.com_context; + context vunit_lib.vunit_context; + context vunit_lib.vc_context; +use work.spi_nor_tb_pkg.all; +use work.axil_common_pkg.all; +use work.axil8x32_pkg; +use work.axil32x32_pkg; +use work.axil26x32_pkg; +use work.axilite_if_2k19_helper_pkg.all; + +entity spi_nor_espi_th is +end entity; + +architecture th of spi_nor_espi_th is + + signal clk : std_logic := '0'; + signal reset : std_logic := '1'; + signal axi_bus : axil26x32_pkg.axil_t; + signal cs_n : std_logic; + signal sclk : std_logic; + signal io : std_logic_vector(3 downto 0); + signal io_o : std_logic_vector(3 downto 0); + signal io_oe : std_logic_vector(3 downto 0); + signal flash_o : std_logic_vector(3 downto 0); + signal flash_oe : std_logic_vector(3 downto 0); + constant config_array : axil_responder_cfg_array_t(0 downto 0) := + ( + 0 => resp_cfg(base_addr => x"00000100", addr_span_bits => 8) + ); + signal responders : axil32x32_pkg.axil_array_t(0 downto 0); + signal responders_8b : axil8x32_pkg.axil_array_t(0 downto 0); + + -- Far ends of the FIFOs, driven and read by the testbench + signal cmd_wdata : std_logic_vector(31 downto 0) := (others => '0'); + signal cmd_write : std_logic := '0'; + signal payload_wdata : std_logic_vector(7 downto 0) := (others => '0'); + signal payload_write : std_logic := '0'; + signal data_rdata : std_logic_vector(7 downto 0); + signal data_rdack : std_logic := '0'; + signal data_rempty : std_logic; + + -- Near ends, into the DUT + signal espi_cmd_fifo_rdata : std_logic_vector(31 downto 0); + signal espi_cmd_fifo_rdack : std_logic; + signal espi_cmd_fifo_rempty : std_logic; + signal espi_data_fifo_wdata : std_logic_vector(7 downto 0); + signal espi_data_fifo_write : std_logic; + signal espi_wfifo_rdata : std_logic_vector(7 downto 0); + signal espi_wfifo_rdack : std_logic; + signal espi_wfifo_rempty : std_logic; + +begin + + clk <= not clk after 4 ns; + reset <= '0' after 200 ns; + + axi_lite_master_inst: entity vunit_lib.axi_lite_master + generic map ( + bus_handle => bus_handle + ) + port map ( + aclk => clk, + arready => axi_bus.read_address.ready, + arvalid => axi_bus.read_address.valid, + araddr => axi_bus.read_address.addr, + rready => axi_bus.read_data.ready, + rvalid => axi_bus.read_data.valid, + rdata => axi_bus.read_data.data, + rresp => axi_bus.read_data.resp, + awready => axi_bus.write_address.ready, + awvalid => axi_bus.write_address.valid, + awaddr => axi_bus.write_address.addr, + wready => axi_bus.write_data.ready, + wvalid => axi_bus.write_data.valid, + wdata => axi_bus.write_data.data, + wstrb => axi_bus.write_data.strb, + bvalid => axi_bus.write_response.valid, + bready => axi_bus.write_response.ready, + bresp => axi_bus.write_response.resp + ); + + axil_interconnect_inst: entity work.axil_interconnect + generic map( + config_array => config_array + ) + port map( + clk => clk, + reset => reset, + initiator => axi_bus, + responders => responders + ); + + resiser: entity work.axil8_resizer port map(fabric => responders(0), responder =>responders_8b(0)); + + cmd_fifo: entity work.dcfifo_xpm + generic map( + fifo_write_depth => 256, + data_width => 32, + showahead_mode => true + ) + port map( + wclk => clk, + reset => reset, + write_en => cmd_write, + wdata => cmd_wdata, + wfull => open, + wusedwds => open, + rclk => clk, + rdata => espi_cmd_fifo_rdata, + rdreq => espi_cmd_fifo_rdack, + rempty => espi_cmd_fifo_rempty, + rusedwds => open + ); + + payload_fifo: entity work.dcfifo_xpm + generic map( + fifo_write_depth => 1024, + data_width => 8, + showahead_mode => true + ) + port map( + wclk => clk, + reset => reset, + write_en => payload_write, + wdata => payload_wdata, + wfull => open, + wusedwds => open, + rclk => clk, + rdata => espi_wfifo_rdata, + rdreq => espi_wfifo_rdack, + rempty => espi_wfifo_rempty, + rusedwds => open + ); + + data_fifo: entity work.dcfifo_xpm + generic map( + fifo_write_depth => 4096, + data_width => 8, + showahead_mode => true + ) + port map( + wclk => clk, + reset => reset, + write_en => espi_data_fifo_write, + wdata => espi_data_fifo_wdata, + wfull => open, + wusedwds => open, + rclk => clk, + rdata => data_rdata, + rdreq => data_rdack, + rempty => data_rempty, + rusedwds => open + ); + + spi_nor_top_inst: entity work.spi_nor_top + port map ( + clk => clk, + reset => reset, + axi_if => responders_8b(0), + cs_n => cs_n, + sclk => sclk, + io => io, + io_o => io_o, + io_oe => io_oe, + sp5_owns_flash => open, + espi_cmd_fifo_rdata => espi_cmd_fifo_rdata, + espi_cmd_fifo_rdack => espi_cmd_fifo_rdack, + espi_cmd_fifo_rempty => espi_cmd_fifo_rempty, + espi_data_fifo_wdata => espi_data_fifo_wdata, + espi_data_fifo_write => espi_data_fifo_write, + espi_wfifo_rdata => espi_wfifo_rdata, + espi_wfifo_rdack => espi_wfifo_rdack, + espi_wfifo_rempty => espi_wfifo_rempty + ); + + flash: entity work.spi_nor_target_vc + generic map ( + actor_name => "spi_nor_target" + ) + port map ( + cs_n => cs_n, + sclk => sclk, + io => io, + io_o => flash_o, + io_oe => flash_oe + ); + + bus_gen: for i in io'range generate + io(i) <= io_o(i) when io_oe(i) = '1' else 'Z'; + io(i) <= flash_o(i) when flash_oe(i) = '1' else 'Z'; + io(i) <= 'H'; + end generate; + + contention_check: process(all) + begin + for i in io'range loop + assert not (io_oe(i) = '1' and flash_oe(i) = '1') + report "Bus contention: controller and flash both driving io(" & to_string(i) & ")" + severity error; + end loop; + end process; + +end th; diff --git a/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd b/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd index 01e0f616..593c33a6 100644 --- a/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd +++ b/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd @@ -46,6 +46,13 @@ entity spi_nor_top is -- requested address espi_data_fifo_wdata : out std_logic_vector(7 downto 0); espi_data_fifo_write : out std_logic; + -- Host to flash bytes for an eSPI write command. The eSPI side pushes + -- a whole payload before the command that consumes it, so this is + -- never read while empty. Tie rempty high on a design whose eSPI + -- instance cannot write. + espi_wfifo_rdata : in std_logic_vector(7 downto 0) := (others => '0'); + espi_wfifo_rdack : out std_logic; + espi_wfifo_rempty : in std_logic := '1'; -- Second flash read client, same command/response FIFO shape as the eSPI -- one above. Used by the hashing engine. Addresses here are raw: none of @@ -82,6 +89,10 @@ architecture rtl of spi_nor_top is signal rx_fifo_write8 : std_logic; signal tx_fifo_read8 : std_logic; signal tx_fifo_data8 : std_logic_vector(7 downto 0); + -- The byte stream the engine actually shifts out, and where it comes from + signal tx_byte : std_logic_vector(7 downto 0); + signal tx_byte_ack : std_logic; + signal tx_from_espi : std_logic; signal rx_fifo_wdat8 : std_logic_vector(7 downto 0); signal tx_fifo_data32 : std_logic_vector(31 downto 0); signal read_ack32 : std_logic; @@ -178,8 +189,8 @@ begin sclk_running => sclk_running, release_lanes => release_lanes, cur_io_mode => cur_io_mode, - tx_fifo_ack => tx_fifo_read8, - tx_fifo_data => tx_fifo_data8, + tx_fifo_ack => tx_byte_ack, + tx_fifo_data => tx_byte, rx_fifo_data => rx_fifo_wdat8, rx_fifo_write => rx_fifo_write8 ); @@ -226,6 +237,13 @@ begin espi_cmd; sp5_owns_flash <= spicr_reg.sp5_owns_flash; + + -- Outbound bytes normally come from hubris' TX FIFO; during an eSPI page + -- program they come from the eSPI write payload FIFO instead. The engine + -- acks whichever it is reading. + tx_byte <= espi_wfifo_rdata when tx_from_espi = '1' else tx_fifo_data8; + tx_fifo_read8 <= tx_byte_ack when tx_from_espi = '0' else '0'; + espi_wfifo_rdack <= tx_byte_ack when tx_from_espi = '1' else '0'; -- TODO: this would be more simple with a mixed width fifo -- but this was faster than digging around making a new wrapper -- for now @@ -366,6 +384,7 @@ begin reset => reset, espi_cmd => espi_cmd, spi_hw_busy => spisr_reg.busy, + tx_from_espi => tx_from_espi, espi_reads_allowed => spicr_reg.sp5_owns_flash, sp_host_image_flash_addr_offset => signed(sp5_flash_offset.offset), amd_begin_apob_flash_addr => apob_flash_addr.offset, diff --git a/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd b/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd index 809d017d..4c48eb3b 100644 --- a/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd +++ b/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd @@ -12,6 +12,18 @@ use work.axi_st8_pkg.all; entity sp5_espi_flash_subsystem is + generic ( + -- Passed to espi_target_top. Off for the SP5 boot flash, where the + -- host must never be able to modify what it boots from. + FLASH_WRITES_ALLOWED : boolean := false; + -- Passed to espi_target_top. A flash-only instance has no use for + -- the post code buffer. + POST_CODE_BUFFER_ENABLED : boolean := true; + -- spi_nor_top's rate and sample point. The defaults are the SP5 boot + -- flash's; a flash on a slower bank or a longer path wants its own. + SPI_NOR_SCLK_DIVISOR : natural := 0; + SPI_NOR_RX_SAMPLE_TAPS : natural range 0 to 4 := 2 + ); port( clk_125m : in std_logic; reset_125m : in std_logic; @@ -61,6 +73,11 @@ architecture rtl of sp5_espi_flash_subsystem is signal flash_rfifo_rempty : std_logic; signal flash_fifo_clear : std_logic; signal fifo_reset : std_logic; + signal flash_wfifo_data : std_logic_vector(7 downto 0); + signal flash_wfifo_write : std_logic; + signal espi_wfifo_rdata : std_logic_vector(7 downto 0); + signal espi_wfifo_rdack : std_logic; + signal espi_wfifo_rempty : std_logic; signal rst_cnts : integer range 0 to 5 := 5; @@ -132,7 +149,41 @@ begin -- Only the link layer runs at 200MHz, the remaining -- logic runs at 125MHz so all the interfaces are synchronous -- to 125MHz + -- Host to flash write payloads. One eSPI write's payload at most sits in + -- here at a time: the eSPI side issues one flash command at a time and + -- the payload is consumed before it reports the command done. Sized for + -- the flash channel's 1kB per-descriptor slot. Absent on a read-only + -- instance, where it would only ever be empty. + wfifo: if FLASH_WRITES_ALLOWED generate + espi_spinor_wdata_fifo: entity work.dcfifo_xpm + generic map( + fifo_write_depth => 1024, + data_width => 8, + showahead_mode => true + ) + port map( + wclk => clk_125m, + reset => fifo_reset, + write_en => flash_wfifo_write, + wdata => flash_wfifo_data, + wfull => open, + wusedwds => open, + rclk => clk_125m, + rdata => espi_wfifo_rdata, + rdreq => espi_wfifo_rdack, + rempty => espi_wfifo_rempty, + rusedwds => open + ); + else generate + espi_wfifo_rdata <= (others => '0'); + espi_wfifo_rempty <= '1'; + end generate; + espi_target_top_inst: entity work.espi_target_top + generic map( + FLASH_WRITES_ALLOWED => FLASH_WRITES_ALLOWED, + POST_CODE_BUFFER_ENABLED => POST_CODE_BUFFER_ENABLED + ) port map( clk_200m => clk_200m, reset_200m => reset_200m, @@ -151,6 +202,8 @@ begin flash_rfifo_data => flash_rfifo_data, flash_rfifo_rdack => flash_rfifo_rdack, flash_rfifo_rempty => flash_rfifo_rempty, + flash_wfifo_data => flash_wfifo_data, + flash_wfifo_write => flash_wfifo_write, to_sp_uart_data => ipcc_uart_from_espi.data, to_sp_uart_valid => ipcc_uart_from_espi.valid, to_sp_uart_ready => ipcc_uart_from_espi.ready, @@ -167,14 +220,14 @@ begin -- round trip out to the flash and back has to land within half an -- sclk period of rx_sample_taps, and above this rate that window -- closes. Faster would need per-lane IDELAY read training. - sclk_divisor => 0, + sclk_divisor => SPI_NOR_SCLK_DIVISOR, -- Sample 8ns after the sclk rising edge. Taps are in half-clk (4ns) -- steps. With the flash IO flops packed into the IOBs the round trip -- out and back is bounded to roughly 3.7..11.6ns, which puts the -- usable sample window at 3.6..11.7ns; 8ns sits about 4ns clear of -- either end. cosmo_timing.xdc carries the arithmetic. Sweep this on -- hardware if reads come back corrupted. - rx_sample_taps => 2, + rx_sample_taps => SPI_NOR_RX_SAMPLE_TAPS, cs_setup_cnts => 4, cs_high_cnts => 7 ) @@ -193,6 +246,9 @@ begin espi_cmd_fifo_rempty => espi_cmd_fifo_rempty, espi_data_fifo_wdata => espi_data_fifo_wdata, espi_data_fifo_write => espi_data_fifo_write, + espi_wfifo_rdata => espi_wfifo_rdata, + espi_wfifo_rdack => espi_wfifo_rdack, + espi_wfifo_rempty => espi_wfifo_rempty, hash_cmd_fifo_rdata => hash_cmd_fifo_rdata, hash_cmd_fifo_rdack => hash_cmd_fifo_rdack, hash_cmd_fifo_rempty => hash_cmd_fifo_rempty, From aba6c93830d8692867b65bcef15db409d6f12b7c Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Fri, 11 Sep 2026 16:24:16 -0400 Subject: [PATCH 07/12] espi: post code buffer in block RAM, and optional The 4k entry post code buffer was distributed RAM: 3.6k LUTRAMs per eSPI target, the largest single consumer in the block. On a design with the DIMM SPD caches and two targets that took the XC7S100 to 95% of its LUTRAM and the placer scattered the buffer across the die, at 2.5ns of negative slack. In block RAM it costs four tiles instead. A block RAM read is registered, so the buffer now answers one cycle after the AXI read is accepted, which is when rvalid rises anyway; the read enable is the accept, so the output holds while the master takes its time. Reads of the buffer are muxed from that register rather than the general rdata register. A new POST_CODE_BUFFER_ENABLED generic on espi_target_top leaves the buffer out altogether, for a target that only ever serves the flash channel and never sees a post code. --- hdl/ip/vhd/espi/espi_target_top.vhd | 7 ++- hdl/ip/vhd/espi/sys_regs/espi_regs.vhd | 62 +++++++++++++++++++------- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/hdl/ip/vhd/espi/espi_target_top.vhd b/hdl/ip/vhd/espi/espi_target_top.vhd index 38ffd482..16710cf0 100644 --- a/hdl/ip/vhd/espi/espi_target_top.vhd +++ b/hdl/ip/vhd/espi/espi_target_top.vhd @@ -26,7 +26,9 @@ entity espi_target_top is -- leaves it gated by the flash_write_enable control bit, which -- resets to off. Refused writes and erases get an unsuccessful -- completion. - FLASH_WRITES_ALLOWED : boolean := false + FLASH_WRITES_ALLOWED : boolean := false; + -- Whether to keep the 4k entry post code buffer, see espi_regs. + POST_CODE_BUFFER_ENABLED : boolean := true ); port ( clk : in std_logic; @@ -265,6 +267,9 @@ begin chip_sel_active <= not txn_csn; -- system (axi-lite) register block espi_sys_regs_inst: entity work.espi_regs + generic map( + POST_CODE_BUFFER_ENABLED => POST_CODE_BUFFER_ENABLED + ) port map( clk => clk, reset => reset, diff --git a/hdl/ip/vhd/espi/sys_regs/espi_regs.vhd b/hdl/ip/vhd/espi/sys_regs/espi_regs.vhd index 1cc67cb6..5dfb797a 100644 --- a/hdl/ip/vhd/espi/sys_regs/espi_regs.vhd +++ b/hdl/ip/vhd/espi/sys_regs/espi_regs.vhd @@ -17,6 +17,12 @@ use work.axil15x32_pkg.all; use work.calc_pkg.log2ceil; entity espi_regs is + generic ( + -- An instance that only ever serves the flash channel never sees a + -- post code, so it can leave the 4k entry buffer out; reads of it + -- then return zero. + POST_CODE_BUFFER_ENABLED : boolean := true + ); port ( clk : in std_logic; reset : in std_logic; @@ -65,6 +71,9 @@ architecture rtl of espi_regs is signal pc_buf_waddr : std_logic_vector(BUFFER_ADDR_WIDTH - 1 downto 0); signal pc_buf_raddr : std_logic_vector(BUFFER_ADDR_WIDTH - 1 downto 0); signal post_code_buffer_rdata : std_logic_vector(31 downto 0); + -- The read in flight is of the buffer, so answer from its output + -- register rather than rdata + signal pc_buf_read : std_logic; begin fifo_status_reg.cmd_used_wds <= dbg_chan.wstatus.usedwds; @@ -75,7 +84,7 @@ begin last_resp_status_reg <= unpack(X"0000" & last_resp_status); live_status_reg <= unpack(X"0000" & live_espi_status); - axi_if.read_data.data <= rdata; + axi_if.read_data.data <= post_code_buffer_rdata when pc_buf_read = '1' else rdata; stuff_wds <= stuff_count.count(15 downto 0); stuff_fifo <= stuff_enable.en; @@ -137,21 +146,38 @@ begin end if; end process; - post_code_buffer: entity work.dual_clock_simple_dpr - generic map( - data_width => 32, - num_words => BUFFER_ENTRIES, - reg_output => false - ) - port map( - wclk => clk, - waddr => pc_buf_waddr, - wdata => post_code, - wren => post_code_valid, - rclk => clk, - raddr => pc_buf_raddr, - rdata => post_code_buffer_rdata - ); + -- The buffer is 128kb, which in distributed RAM was 3.6k LUTRAMs per + -- instance, enough to starve the placer on a part that also carries the + -- DIMM caches. In block RAM the read is registered, so the buffer is + -- answered one cycle after the AXI read is accepted, which is exactly + -- when rvalid rises: the read enable is the accept, so the output holds + -- for as long as the master takes to collect it. + pc_buf: if POST_CODE_BUFFER_ENABLED generate + type pc_mem_t is array (0 to BUFFER_ENTRIES - 1) of std_logic_vector(31 downto 0); + signal pc_mem : pc_mem_t; + attribute ram_style : string; + attribute ram_style of pc_mem : signal is "block"; + begin + pc_mem_write: process(clk) + begin + if rising_edge(clk) then + if post_code_valid then + pc_mem(to_integer(pc_buf_waddr)) <= post_code; + end if; + end if; + end process; + + pc_mem_read: process(clk) + begin + if rising_edge(clk) then + if active_read then + post_code_buffer_rdata <= pc_mem(to_integer(pc_buf_raddr)); + end if; + end if; + end process; + else generate + post_code_buffer_rdata <= (others => '0'); + end generate; -- Axi here are byte_addresses and we need to convert to word addresses for the dpr. pc_buf_raddr <= resize(shift_right(axi_if.read_address.addr - POST_CODE_BUFFER_OFFSET, 2), pc_buf_raddr'length); @@ -170,9 +196,11 @@ begin if reset then rdata <= (others => '0'); resp_fifo_ack <= '0'; + pc_buf_read <= '0'; elsif rising_edge(clk) then resp_fifo_ack <= '0'; if active_read then + pc_buf_read <= '0'; case to_integer(axi_if.read_address.addr) is when FLAGS_OFFSET => rdata <= pack(flags_reg); when CONTROL_OFFSET => rdata <= pack(control_reg); @@ -216,7 +244,7 @@ begin when SPEC_REGS_CH3_CAPABILITIES2_OFFSET => rdata <= espi_spec_regs_pkg.pack(spec_regs_view.ch3_capabilities2); when POST_CODE_BUFFER_MEM_RANGE => - rdata <= post_code_buffer_rdata; + pc_buf_read <= '1'; when others => rdata <= (others => '0'); end case; From b5381577f3fb70fdbfb2089c0fb7c5f2288b10c8 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Tue, 15 Sep 2026 12:12:25 -0400 Subject: [PATCH 08/12] hash_engine: hardware measurement request A sequencer can raise hw_req to have a flash range hashed without software in the loop, for measuring a device's boot image before releasing it from reset. The range comes from HW_FLASH_ADDR/HW_LENGTH, the flash from the HW_FLASH_SEL generic, and the result is snapshotted into HW_DIGEST0..7 so a later software run cannot overwrite it; hw_ack and hw_err answer the request, with the detail in HW_STATUS. A request that lands during a software run is refused rather than restarting it, and a software start during a hardware run is dropped. An unprogrammed range is a zero length and comes back as a cfg_err. Also fixes an abort of a software-fed hash run, or of a flash run abandoned during its prepend, which waited in DRAIN for bytes no read had been issued for. --- hdl/ip/vhd/hash_engine/hash_engine_regs.rdl | 66 +++++++++ hdl/ip/vhd/hash_engine/hash_engine_regs.vhd | 21 +++ hdl/ip/vhd/hash_engine/hash_engine_top.vhd | 139 +++++++++++++++++- hdl/ip/vhd/hash_engine/hash_feeder.vhd | 9 +- .../vhd/hash_engine/sims/hash_engine_tb.vhd | 75 ++++++++++ .../vhd/hash_engine/sims/hash_engine_th.vhd | 8 + 6 files changed, 311 insertions(+), 7 deletions(-) diff --git a/hdl/ip/vhd/hash_engine/hash_engine_regs.rdl b/hdl/ip/vhd/hash_engine/hash_engine_regs.rdl index 24c8cf89..def51f74 100644 --- a/hdl/ip/vhd/hash_engine/hash_engine_regs.rdl +++ b/hdl/ip/vhd/hash_engine/hash_engine_regs.rdl @@ -177,4 +177,70 @@ addrmap hash_engine_regs { DIGEST6->desc = "Digest bytes 24 to 27"; digest_word DIGEST7; DIGEST7->desc = "Digest bytes 28 to 31"; + + // Hardware-requested measurement. A sequencer can ask the engine to hash + // a flash range without software in the loop, for instance to measure a + // device's boot image before releasing it from reset. The range comes from + // the two registers below, the source is fixed by the design, and the + // result is kept apart from the software-driven DIGEST so a later run + // cannot overwrite it. + reg { + name = "Hardware Request Flash Address"; + desc = "Raw byte address of the first byte hashed on a hardware request. + Not remapped, as FLASH_ADDR is not."; + field { + desc = "Flash byte address"; + } addr[31:0] = 0; + } HW_FLASH_ADDR; + + reg { + name = "Hardware Request Length"; + desc = "Number of bytes hashed on a hardware request. Zero makes the + request fail with HW_STATUS.cfg_err, which is what an unprogrammed range + does; the requester decides what to make of that."; + field { + desc = "Message length in bytes"; + } count[31:0] = 0; + } HW_LENGTH; + + reg { + name = "Hardware Request Status"; + desc = "Outcome of the most recent hardware request. All but busy hold + until the next request begins."; + default sw = r; + default hw = w; + field { + desc = "The request arrived while a software-started hash was running + and was refused; the software run was left alone."; + } engine_busy[4:4] = 0; + field { + desc = "The run was ended early by CONTROL.abort."; + } aborted[3:3] = 0; + field { + desc = "The run was refused as a configuration error, see HW_LENGTH."; + } cfg_err[2:2] = 0; + field { + desc = "The run completed and HW_DIGEST holds its result."; + } done[1:1] = 0; + field { + desc = "A hardware-requested run is in flight."; + } busy[0:0] = 0; + } HW_STATUS; + + digest_word HW_DIGEST0; + HW_DIGEST0->desc = "Hardware request digest bytes 0 to 3"; + digest_word HW_DIGEST1; + HW_DIGEST1->desc = "Hardware request digest bytes 4 to 7"; + digest_word HW_DIGEST2; + HW_DIGEST2->desc = "Hardware request digest bytes 8 to 11"; + digest_word HW_DIGEST3; + HW_DIGEST3->desc = "Hardware request digest bytes 12 to 15"; + digest_word HW_DIGEST4; + HW_DIGEST4->desc = "Hardware request digest bytes 16 to 19"; + digest_word HW_DIGEST5; + HW_DIGEST5->desc = "Hardware request digest bytes 20 to 23"; + digest_word HW_DIGEST6; + HW_DIGEST6->desc = "Hardware request digest bytes 24 to 27"; + digest_word HW_DIGEST7; + HW_DIGEST7->desc = "Hardware request digest bytes 28 to 31"; }; diff --git a/hdl/ip/vhd/hash_engine/hash_engine_regs.vhd b/hdl/ip/vhd/hash_engine/hash_engine_regs.vhd index 8624222d..cd953761 100644 --- a/hdl/ip/vhd/hash_engine/hash_engine_regs.vhd +++ b/hdl/ip/vhd/hash_engine/hash_engine_regs.vhd @@ -38,6 +38,12 @@ entity hash_engine_regs is -- Bit 7 downto 0 is hash byte 0, so DIGESTn is digest(32n+31 downto 32n) digest : in std_logic_vector(255 downto 0); + -- hardware request path, see hash_engine_top + hw_flash_addr : out hw_flash_addr_type; + hw_length : out hw_length_type; + hw_status : in hw_status_type; + hw_digest : in std_logic_vector(255 downto 0); + -- Software data FIFO push port wdata_fifo_wdata : out std_logic_vector(31 downto 0); wdata_fifo_write : out std_logic @@ -89,6 +95,8 @@ begin prepend <= rec_reset; flash_addr <= rec_reset; msg_length <= rec_reset; + hw_flash_addr <= rec_reset; + hw_length <= rec_reset; start_strobe <= '0'; abort_strobe <= '0'; elsif rising_edge(clk) then @@ -104,6 +112,8 @@ begin when PREPEND_OFFSET => prepend <= unpack(axi_if.write_data.data); when FLASH_ADDR_OFFSET => flash_addr <= unpack(axi_if.write_data.data); when LENGTH_OFFSET => msg_length <= unpack(axi_if.write_data.data); + when HW_FLASH_ADDR_OFFSET => hw_flash_addr <= unpack(axi_if.write_data.data); + when HW_LENGTH_OFFSET => hw_length <= unpack(axi_if.write_data.data); when others => null; end case; end if; @@ -135,6 +145,17 @@ begin when DIGEST5_OFFSET => rdata <= digest(191 downto 160); when DIGEST6_OFFSET => rdata <= digest(223 downto 192); when DIGEST7_OFFSET => rdata <= digest(255 downto 224); + when HW_FLASH_ADDR_OFFSET => rdata <= pack(hw_flash_addr); + when HW_LENGTH_OFFSET => rdata <= pack(hw_length); + when HW_STATUS_OFFSET => rdata <= pack(hw_status); + when HW_DIGEST0_OFFSET => rdata <= hw_digest(31 downto 0); + when HW_DIGEST1_OFFSET => rdata <= hw_digest(63 downto 32); + when HW_DIGEST2_OFFSET => rdata <= hw_digest(95 downto 64); + when HW_DIGEST3_OFFSET => rdata <= hw_digest(127 downto 96); + when HW_DIGEST4_OFFSET => rdata <= hw_digest(159 downto 128); + when HW_DIGEST5_OFFSET => rdata <= hw_digest(191 downto 160); + when HW_DIGEST6_OFFSET => rdata <= hw_digest(223 downto 192); + when HW_DIGEST7_OFFSET => rdata <= hw_digest(255 downto 224); when others => rdata <= (others => '0'); end case; end if; diff --git a/hdl/ip/vhd/hash_engine/hash_engine_top.vhd b/hdl/ip/vhd/hash_engine/hash_engine_top.vhd index ca07c10d..1b80bfde 100644 --- a/hdl/ip/vhd/hash_engine/hash_engine_top.vhd +++ b/hdl/ip/vhd/hash_engine/hash_engine_top.vhd @@ -27,12 +27,25 @@ use work.keccak_pkg.all; -- The integrator should hold the response FIFO in reset only from the global -- reset. This block never asks for it to be flushed: an abandoned read is dealt -- with by consuming the bytes still owed, see hash_feeder. +-- +-- Besides the register interface there is a hardware request: a sequencer can +-- raise hw_req to have a flash range (HW_FLASH_ADDR/HW_LENGTH, on the flash +-- HW_FLASH_SEL names) hashed without software in the loop, and gets hw_ack +-- back once the run is over, with hw_err saying whether it produced a digest. +-- The digest is kept in its own registers so that a later software run does +-- not overwrite it. A request that lands while a software run is in flight is +-- refused rather than restarting the run; software starts that land while a +-- hardware run is in flight are dropped. Four-phase: the requester holds +-- hw_req until it sees hw_ack, and hw_ack drops once hw_req does. entity hash_engine_top is generic ( -- How many spi_nor flash clients hang off this engine. CONFIG.source -- picks between them for a run; AUX_QSPI is a configuration error -- when there is only one. - NUM_FLASHES : natural range 1 to 2 := 1 + NUM_FLASHES : natural range 1 to 2 := 1; + -- Which flash a hardware request reads: 0 the host flash, 1 the aux + -- flash (which needs NUM_FLASHES = 2). + HW_FLASH_SEL : natural range 0 to 1 := 0 ); port ( clk : in std_logic; @@ -41,6 +54,12 @@ entity hash_engine_top is -- Axilite interface axi_if : view axil_target; + -- Hardware request, see above. Leave hw_req unconnected on a design + -- without a requester. + hw_req : in std_logic := '0'; + hw_ack : out std_logic; + hw_err : out std_logic; + -- The spi_nor side of the engine's own command and response FIFOs, one -- pair per flash. These match spi_nor_top's hash client port shape: -- the flash pops commands (word 0 a byte address, word 1 a byte count) @@ -71,6 +90,36 @@ architecture rtl of hash_engine_top is signal flash_addr : flash_addr_type; signal msg_length : length_type; + -- What the feeder actually sees: the software registers, or the hardware + -- request's configuration while one of those is in flight. + signal feeder_start : std_logic; + signal feeder_cfg : config_type; + signal feeder_prepend : prepend_type; + signal feeder_flash_addr : flash_addr_type; + signal feeder_length : length_type; + + signal hw_flash_addr : hw_flash_addr_type; + signal hw_length : hw_length_type; + signal hw_status : hw_status_type; + signal hw_digest : std_logic_vector(255 downto 0); + + type hw_state_t is (idle, starting, running, acked); + type hw_reg_t is record + state : hw_state_t; + start : std_logic; + active : std_logic; + ack : std_logic; + err : std_logic; + settle : natural range 0 to 3; + status : hw_status_type; + digest : std_logic_vector(255 downto 0); + end record; + constant hw_reg_reset : hw_reg_t := ( + state => idle, start => '0', active => '0', ack => '0', err => '0', + settle => 0, status => rec_reset, digest => (others => '0') + ); + signal hw_r : hw_reg_t; + signal status : status_type; signal progress : progress_type; @@ -119,10 +168,88 @@ begin status => status, progress => progress, digest => digest, + hw_flash_addr => hw_flash_addr, + hw_length => hw_length, + hw_status => hw_status, + hw_digest => hw_digest, wdata_fifo_wdata => sw_fifo_wdata, wdata_fifo_write => sw_fifo_write ); + -- Hardware request sequencing. The feeder latches its configuration on the + -- cycle it accepts a start, so the mux below only has to hold for as long as + -- the request is active, which it does. + feeder_start <= hw_r.start when hw_r.active = '1' else start_strobe; + feeder_cfg <= (source => AUX_QSPI) when hw_r.active = '1' and HW_FLASH_SEL = 1 else + (source => HOST_QSPI) when hw_r.active = '1' else + cfg; + feeder_prepend <= (count => (others => '0')) when hw_r.active = '1' else prepend; + feeder_flash_addr <= (addr => hw_flash_addr.addr) when hw_r.active = '1' else flash_addr; + feeder_length <= (count => hw_length.count) when hw_r.active = '1' else msg_length; + + hw_ack <= hw_r.ack; + hw_err <= hw_r.err; + hw_status <= hw_r.status; + hw_digest <= hw_r.digest; + + hw_request: process(clk, reset) + begin + if reset then + hw_r <= hw_reg_reset; + elsif rising_edge(clk) then + hw_r.start <= '0'; + case hw_r.state is + when idle => + if hw_req = '1' then + hw_r.status <= rec_reset; + hw_r.err <= '0'; + if status.busy = '1' then + -- A software run owns the engine; do not restart + -- it out from under whoever started it. + hw_r.status.engine_busy <= '1'; + hw_r.err <= '1'; + hw_r.ack <= '1'; + hw_r.state <= acked; + else + hw_r.active <= '1'; + hw_r.start <= '1'; + hw_r.status.busy <= '1'; + hw_r.settle <= 0; + hw_r.state <= starting; + end if; + end if; + when starting => + -- The feeder answers a start two cycles later, with either + -- busy or cfg_err. Neither is ours to look at before then. + if hw_r.settle = 2 then + hw_r.state <= running; + else + hw_r.settle <= hw_r.settle + 1; + end if; + when running => + if status.cfg_err = '1' or status.aborted = '1' or + (status.busy = '0' and status.done = '1') then + hw_r.status.busy <= '0'; + hw_r.status.cfg_err <= status.cfg_err; + hw_r.status.aborted <= status.aborted; + hw_r.status.done <= status.done and not status.aborted; + hw_r.err <= status.cfg_err or status.aborted; + if status.done = '1' and status.aborted = '0' then + hw_r.digest <= digest; + end if; + hw_r.active <= '0'; + hw_r.ack <= '1'; + hw_r.state <= acked; + end if; + when acked => + if hw_req = '0' then + hw_r.ack <= '0'; + hw_r.state <= idle; + end if; + end case; + end if; + end process; + -- Software data path. Written 32 bits at a time by the processor and read a -- byte at a time by the feeder, least significant byte first. sw_fifo_reset <= reset or sw_clear; @@ -155,12 +282,12 @@ begin port map ( clk => clk, reset => reset, - start_strobe => start_strobe, + start_strobe => feeder_start, abort_strobe => abort_strobe, - cfg => cfg, - prepend => prepend, - flash_addr => flash_addr, - msg_length => msg_length, + cfg => feeder_cfg, + prepend => feeder_prepend, + flash_addr => feeder_flash_addr, + msg_length => feeder_length, busy => status.busy, done => status.done, aborted => status.aborted, diff --git a/hdl/ip/vhd/hash_engine/hash_feeder.vhd b/hdl/ip/vhd/hash_engine/hash_feeder.vhd index 565f98b0..7e789078 100644 --- a/hdl/ip/vhd/hash_engine/hash_feeder.vhd +++ b/hdl/ip/vhd/hash_engine/hash_feeder.vhd @@ -257,7 +257,14 @@ begin if stop_run then -- Work out what the flash still owes us so DRAIN can swallow it. - v.drain_left := r.flash_req - r.flash_rx; + -- Nothing is owed unless the read command actually went out: a + -- software fed run, or a flash run abandoned during its prepend, + -- would otherwise wait in DRAIN for bytes that never come. + if r.src_qspi = '1' and r.cmd_sent = '1' then + v.drain_left := r.flash_req - r.flash_rx; + else + v.drain_left := (others => '0'); + end if; v.finished := '0'; v.state := DRAIN; else diff --git a/hdl/ip/vhd/hash_engine/sims/hash_engine_tb.vhd b/hdl/ip/vhd/hash_engine/sims/hash_engine_tb.vhd index 86ee1df4..ff1b5c32 100644 --- a/hdl/ip/vhd/hash_engine/sims/hash_engine_tb.vhd +++ b/hdl/ip/vhd/hash_engine/sims/hash_engine_tb.vhd @@ -46,10 +46,14 @@ begin bench: process alias reset is << signal th.reset : std_logic >>; + alias hw_req is << signal th.hw_req : std_logic >>; + alias hw_ack is << signal th.hw_ack : std_logic >>; + alias hw_err is << signal th.hw_err : std_logic >>; variable status : std_logic_vector(31 downto 0); variable rdata : std_logic_vector(31 downto 0); variable dig : std_logic_vector(255 downto 0); + variable err : std_logic; variable expected : digest_t; variable msg : queue_t; @@ -175,6 +179,34 @@ begin check_equal(d, std_logic_vector(e), name); end procedure; + -- Raise the hardware request, wait for the acknowledge, drop it and + -- wait for the acknowledge to clear. Returns what the engine said. + procedure hw_request ( + variable err : out std_logic; + variable hw_status : out std_logic_vector(31 downto 0) + ) is + begin + -- register writes are queued; the request must see them landed + wait_until_idle(net, bus_handle); + hw_req <= '1'; + wait until hw_ack = '1'; + err := hw_err; + read_reg(net, HW_STATUS_OFFSET, hw_status); + hw_req <= '0'; + wait until hw_ack = '0'; + end procedure; + + procedure read_hw_digest ( + variable d : out std_logic_vector(255 downto 0) + ) is + variable w : std_logic_vector(31 downto 0); + begin + for i in 0 to 7 loop + read_reg(net, HW_DIGEST0_OFFSET + 4 * i, w); + d(32 * i + 31 downto 32 * i) := w; + end loop; + end procedure; + -- Drive a literal message through the manual path and check it against a -- published digest, rather than against the software sponge. This is the -- one place the engine is measured against the standard instead of @@ -400,6 +432,49 @@ begin run_flash(4, 80, 16#3000#, "second message, flash sourced"); run_local(8, 40, "third message, back to software"); + elsif run("hw_request_hashes_flash_range") then + -- A sequencer's request measures the programmed range and the + -- digest survives a later software run. + expected := sha3_256_digest(expected_msg(0, 700, true, 16#3000#)); + write_reg(net, HW_FLASH_ADDR_OFFSET, To_StdLogicVector(16#3000#, 32)); + write_reg(net, HW_LENGTH_OFFSET, To_StdLogicVector(700, 32)); + hw_request(err, status); + check_equal(err, '0', "hw request reported an error"); + check_equal((status and HW_STATUS_DONE_MASK) /= (status'range => '0'), true, "HW_STATUS.done"); + check_equal((status and HW_STATUS_BUSY_MASK) = (status'range => '0'), true, "HW_STATUS.busy clear"); + read_hw_digest(dig); + check_equal(dig, std_logic_vector(expected), "hw digest of 700 bytes at 0x3000"); + -- software digest is the same run's result too + read_digest(net, dig); + check_equal(dig, std_logic_vector(expected), "sw digest after hw run"); + + run_flash(0, 100, 16#0100#, "software run after the hw run"); + read_hw_digest(dig); + check_equal(dig, std_logic_vector(expected), "hw digest kept after a software run"); + + elsif run("hw_request_zero_length_is_an_error") then + -- An unprogrammed range: refused as a configuration error, + -- reported, and the engine is still usable afterwards. + hw_request(err, status); + check_equal(err, '1', "hw request should report an error"); + check_equal((status and HW_STATUS_CFG_ERR_MASK) /= (status'range => '0'), true, "HW_STATUS.cfg_err"); + check_equal((status and HW_STATUS_DONE_MASK) = (status'range => '0'), true, "HW_STATUS.done clear"); + run_flash(0, 64, 16#0200#, "software run after a refused hw request"); + + elsif run("hw_request_refused_while_software_busy") then + -- A software run that is waiting on data owns the engine; the + -- request is refused and the software run is left alone. + configure(CFG_LOCAL, 0, 8, 0); + write_reg(net, CONTROL_OFFSET, START_CMD); + write_reg(net, HW_LENGTH_OFFSET, To_StdLogicVector(64, 32)); + hw_request(err, status); + check_equal(err, '1', "hw request should be refused"); + check_equal((status and HW_STATUS_ENGINE_BUSY_MASK) /= (status'range => '0'), true, "HW_STATUS.engine_busy"); + read_reg(net, STATUS_OFFSET, status); + check_equal((status and STATUS_BUSY_MASK) /= (status'range => '0'), true, "software run still busy"); + write_reg(net, CONTROL_OFFSET, ABORT_CMD); + wait_not_busy(net, status); + elsif run("wfifo_full_backpressure") then -- Fill the software FIFO with the engine idle, so nothing drains -- it. Starting a hash first would not work: the core consumes a diff --git a/hdl/ip/vhd/hash_engine/sims/hash_engine_th.vhd b/hdl/ip/vhd/hash_engine/sims/hash_engine_th.vhd index 178136f5..a09ca8f7 100644 --- a/hdl/ip/vhd/hash_engine/sims/hash_engine_th.vhd +++ b/hdl/ip/vhd/hash_engine/sims/hash_engine_th.vhd @@ -35,6 +35,11 @@ architecture th of hash_engine_th is signal rsp_fifo_write : std_logic_vector(0 downto 0); signal rsp_fifo_wfull : std_logic_vector(0 downto 0); + -- Hardware request handshake, driven from the testbench + signal hw_req : std_logic := '0'; + signal hw_ack : std_logic; + signal hw_err : std_logic; + begin @@ -71,6 +76,9 @@ begin clk => clk, reset => reset, axi_if => axi_bus, + hw_req => hw_req, + hw_ack => hw_ack, + hw_err => hw_err, flash_cmd_rdata => cmd_fifo_rdata, flash_cmd_rdack => cmd_fifo_rdack, flash_cmd_rempty => cmd_fifo_empty, From 2bc4ceda5945e2a2e106ad44f3756512b075b771 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Wed, 16 Sep 2026 11:37:09 -0400 Subject: [PATCH 09/12] Re-commonize some stuff --- hdl/projects/cosmo_seq/BUCK | 14 +- hdl/projects/cosmo_seq/cosmo_seq_top.vhd | 21 +- .../cosmo_seq/debug_module/debug_header.vhd | 27 +- .../debug_module/debug_module_top.vhd | 12 +- .../cosmo_seq/debug_module/debug_regs.rdl | 4 +- hdl/projects/cosmo_seq/sequencer/nic_seq.vhd | 18 +- hdl/projects/cosmo_seq/sequencer/seq_sync.vhd | 96 +--- .../cosmo_seq/sequencer/sequencer_io_pkg.vhd | 181 ++++++- .../cosmo_seq/sequencer/sequencer_regs.rdl | 193 +++++++- .../cosmo_seq/sequencer/sequencer_regs.vhd | 47 ++ .../sequencer/sims/sp5_seq_sim_pkg.vhd | 77 +++ .../sequencer/sims/sp5_seq_sim_tb.vhd | 5 +- .../sequencer/sims/sp5_seq_sim_th.vhd | 132 +++++- .../cosmo_seq/sequencer/sims/versal_model.vhd | 100 ++++ .../sequencer/sims/versal_model_msg_pkg.vhd | 85 ++++ .../sequencer/sims/versal_seq_sim_tb.vhd | 271 +++++++++++ .../cosmo_seq/sequencer/sp5_sequencer.vhd | 358 +++++++++++--- hdl/projects/cosmo_seq/sequencer/t6_sync.vhd | 117 +++++ .../cosmo_seq/sequencer/versal_seq.vhd | 440 ++++++++++++++++++ .../cosmo_seq/sequencer/versal_sync.vhd | 213 +++++++++ .../cosmo_seq/sims/cosmo_seq_top_tb.vhd | 34 ++ 21 files changed, 2213 insertions(+), 232 deletions(-) create mode 100644 hdl/projects/cosmo_seq/sequencer/sims/versal_model.vhd create mode 100644 hdl/projects/cosmo_seq/sequencer/sims/versal_model_msg_pkg.vhd create mode 100644 hdl/projects/cosmo_seq/sequencer/sims/versal_seq_sim_tb.vhd create mode 100644 hdl/projects/cosmo_seq/sequencer/t6_sync.vhd create mode 100644 hdl/projects/cosmo_seq/sequencer/versal_seq.vhd create mode 100644 hdl/projects/cosmo_seq/sequencer/versal_sync.vhd create mode 100644 hdl/projects/cosmo_seq/sims/cosmo_seq_top_tb.vhd diff --git a/hdl/projects/cosmo_seq/BUCK b/hdl/projects/cosmo_seq/BUCK index b6bd6e05..eed4e65a 100644 --- a/hdl/projects/cosmo_seq/BUCK +++ b/hdl/projects/cosmo_seq/BUCK @@ -1,4 +1,4 @@ -load("//tools:hdl.bzl", "vhdl_unit", "black_box") +load("//tools:hdl.bzl", "vhdl_unit", "black_box", "vunit_sim") load("//tools:rdl.bzl", "rdl_file") load("//tools:vivado.bzl", "vivado_bitstream") @@ -90,4 +90,14 @@ vivado_bitstream( constraints=glob(["*.xdc"]), pre_synth_tcl_files=glob(["xilinx_ip_gen/*.tcl"]), #post_synth_tcl_files=glob(["*ila.tcl"]), -) \ No newline at end of file +) + +# Analysis-only: vhdl_unit collects sources but does not run a compiler, so +# without this nothing type-checks cosmo_seq_top until Vivado synthesis. +vunit_sim( + name = "cosmo_seq_top_tb", + srcs = glob(["sims/*.vhd"]), + deps = [":cosmo_seq_top"], + visibility = ["PUBLIC"], + standard = "2019", +) diff --git a/hdl/projects/cosmo_seq/cosmo_seq_top.vhd b/hdl/projects/cosmo_seq/cosmo_seq_top.vhd index e0225c43..ee2fdb6b 100644 --- a/hdl/projects/cosmo_seq/cosmo_seq_top.vhd +++ b/hdl/projects/cosmo_seq/cosmo_seq_top.vhd @@ -427,7 +427,12 @@ architecture rtl of cosmo_seq_top is alias a0_ok_to_fpga2 : std_logic is fpga1_to_fpga2_io(2); signal uart_dbg_if : uart_dbg_t; signal allow_backplane_pcie_clk : std_logic; - signal nic_dbg_pins : t6_debug_if; + signal nic_dbg_pins : nic_debug_if; + -- No Versal on this board; the shared sequencer's Versal ports are tied + -- off to these and its Versal outputs left open. + signal versal_rails_unused : versal_power_t := versal_power_absent; + signal versal_boot_unused : versal_boot_t := versal_boot_absent; + signal versal_pcie_unused : versal_pcie_t := versal_pcie_absent; signal reg_alert_l_pins : seq_power_alert_pins_t; signal is_rev1 : std_logic; signal dbg_pins_uart_out : std_logic; @@ -767,7 +772,8 @@ begin resize_axil(fabric_responders(SEQ_RESP_IDX), responders_8b(SEQ_RESP_IDX)); seq: entity work.sp5_sequencer generic map( - CNTS_P_MS => calc_ms(desired_ms => 1, clk_period_ns => 8) + CNTS_P_MS => calc_ms(desired_ms => 1, clk_period_ns => 8), + NIC_KIND => NIC_T6 ) port map( clk => clk_125m, @@ -785,9 +791,15 @@ begin sp5_seq_pins => sp5_seq_pins, nic_rails_pins => nic_rails, nic_seq_pins => nic_seq_pins, + versal_rails_pins => versal_rails_unused, + versal_boot_pins => versal_boot_unused, + versal_pcie_pins => versal_pcie_unused, + versal_held_in_reset => open, + flash_owned_by_seq => open, + hash_req => open, nic_dbg_pins => nic_dbg_pins, - sp5_t6_perst_l => sp5_t6_perst_l, - sp5_t6_faulted => sp5_t6_faulted, + sp5_nic_perst_l => sp5_t6_perst_l, + sp5_nic_faulted => sp5_t6_faulted, ignition_mux_sel => fpga1_to_sp_mux_ign_mux_sel, ignition_creset => fpga1_to_ign_trgt_fpga_creset, reg_alert_l_pins => reg_alert_l_pins @@ -901,6 +913,7 @@ begin reg_alert_l_pins.v0p96_nic_to_fpga1_alert_l <= v0p96_nic_to_fpga1_alert_l; reg_alert_l_pins.pwr_cont2_to_fpga1_alert_l <= pwr_cont2_to_fpga1_alert_l; reg_alert_l_pins.pwr_cont3_to_fpga1_alert_l <= pwr_cont3_to_fpga1_alert_l; + reg_alert_l_pins.pwr_cont4_to_fpga1_alert_l <= '1'; -- no fourth controller on this board resize_axil(fabric_responders(SPD_PROXY_RESP_IDX), responders_8b(SPD_PROXY_RESP_IDX)); dimm_spd_proxy_top_inst: entity work.dimms_subsystem_top diff --git a/hdl/projects/cosmo_seq/debug_module/debug_header.vhd b/hdl/projects/cosmo_seq/debug_module/debug_header.vhd index c35647ce..232b21b1 100644 --- a/hdl/projects/cosmo_seq/debug_module/debug_header.vhd +++ b/hdl/projects/cosmo_seq/debug_module/debug_header.vhd @@ -53,7 +53,7 @@ entity debug_header is mux2_sel_pins : in std_logic_vector(1 downto 0); -- mux2 sel pins debug output to pins mux3_sel_pins : in std_logic_vector(1 downto 0); -- mux3 sel pins debug output to pins -- T6 signals - nic_dbg_pins : view t6_debug_dbg; + nic_dbg_pins : view nic_debug_dbg; -- sp5 toggle pins sp5_debug2_pin : in std_logic; @@ -90,7 +90,7 @@ architecture rtl of debug_header is signal espi_resp_csn_int : std_logic; signal dbg_1v8_ctrl_200 : dbg_1v8_ctrl_type; signal fpga1_spare_reg : std_logic_vector(7 downto 0); - signal nic_dbg_pins_int : t6_debug_if; + signal nic_dbg_pins_int : nic_debug_if; signal sp5_debug2_pin_int : std_logic; signal mux1_sel_int : std_logic_vector(1 downto 0); signal mux2_sel_int : std_logic_vector(1 downto 0); @@ -192,8 +192,8 @@ hdr_dbg_reg_1v8: process(clk_200m, reset_200m) when SP_IPCC_BUS => fpga1_spare_reg(7) <= uart1_fpga1_to_sp_dat_int; fpga1_spare_reg(6) <= uart1_sp_to_fpga1_dat_int; - when T6_SEQUENCER => - -- T6 debug pins + when NIC_SEQUENCER => + -- NIC debug pins, see nic_seq / versal_seq for the taps fpga1_spare_reg(7) <= nic_dbg_pins_int.rails_en; fpga1_spare_reg(6) <= nic_dbg_pins_int.rails_pg; when MUX1_SEL => @@ -247,10 +247,9 @@ hdr_dbg_reg_1v8: process(clk_200m, reset_200m) when SP_IPCC_BUS => fpga1_spare_reg(5) <= uart1_fpga1_to_sp_dat_int; fpga1_spare_reg(4) <= uart1_sp_to_fpga1_dat_int; - when T6_SEQUENCER => - -- T6 debug pins - fpga1_spare_reg(5) <= nic_dbg_pins.cld_rst_l; - fpga1_spare_reg(4) <= nic_dbg_pins.perst_l; + when NIC_SEQUENCER => + fpga1_spare_reg(5) <= nic_dbg_pins.taps(5); + fpga1_spare_reg(4) <= nic_dbg_pins.taps(4); when MUX1_SEL => -- Mux1 select pins fpga1_spare_reg(5) <= mux1_sel_int(1); @@ -302,9 +301,9 @@ hdr_dbg_reg_1v8: process(clk_200m, reset_200m) when SP_IPCC_BUS => fpga1_spare_reg(3) <= uart1_fpga1_to_sp_dat_int; fpga1_spare_reg(2) <= uart1_sp_to_fpga1_dat_int; - when T6_SEQUENCER => - fpga1_spare_reg(3) <= nic_dbg_pins.sp5_mfg_mode_l; - fpga1_spare_reg(2) <= nic_dbg_pins.nic_mfg_mode_l; + when NIC_SEQUENCER => + fpga1_spare_reg(3) <= nic_dbg_pins.taps(3); + fpga1_spare_reg(2) <= nic_dbg_pins.taps(2); when MUX1_SEL => fpga1_spare_reg(3) <= mux1_sel_int(1); fpga1_spare_reg(2) <= mux1_sel_int(0); @@ -354,9 +353,9 @@ hdr_dbg_reg_1v8: process(clk_200m, reset_200m) when SP_IPCC_BUS => fpga1_spare_reg(1) <= uart1_fpga1_to_sp_dat_int; fpga1_spare_reg(0) <= uart1_sp_to_fpga1_dat_int; - when T6_SEQUENCER => - fpga1_spare_reg(1) <= nic_dbg_pins.ext_rst_l; - fpga1_spare_reg(0) <= sp5_debug2_pin_int; -- Unused in this case. + when NIC_SEQUENCER => + fpga1_spare_reg(1) <= nic_dbg_pins.taps(1); + fpga1_spare_reg(0) <= nic_dbg_pins.taps(0); when MUX1_SEL => fpga1_spare_reg(1) <= mux1_sel_int(1); fpga1_spare_reg(0) <= mux1_sel_int(0); diff --git a/hdl/projects/cosmo_seq/debug_module/debug_module_top.vhd b/hdl/projects/cosmo_seq/debug_module/debug_module_top.vhd index c44c0533..3eab51b5 100644 --- a/hdl/projects/cosmo_seq/debug_module/debug_module_top.vhd +++ b/hdl/projects/cosmo_seq/debug_module/debug_module_top.vhd @@ -67,7 +67,7 @@ entity debug_module_top is espi0_sp5_to_fpga1_dat: in std_logic_vector(3 downto 0); espi_resp_csn: in std_logic; --T6 signals - nic_dbg_pins : view t6_debug_dbg; + nic_dbg_pins : view nic_debug_dbg; fpga1_spare_v1p8 : out std_logic_vector(7 downto 0); -- 8 spare pins on the debug header @@ -238,11 +238,11 @@ begin dbg_1v8_ctrl.pins5_4 <= ESPI_BUS; dbg_1v8_ctrl.pins3_2 <= ESPI_BUS; dbg_1v8_ctrl.pins1_0 <= ESPI_BUS; - elsif dbg_convenience.t6_seq_en then - dbg_1v8_ctrl.pins7_6 <= T6_SEQUENCER; - dbg_1v8_ctrl.pins5_4 <= T6_SEQUENCER; - dbg_1v8_ctrl.pins3_2 <= T6_SEQUENCER; - dbg_1v8_ctrl.pins1_0 <= T6_SEQUENCER; + elsif dbg_convenience.nic_seq_en then + dbg_1v8_ctrl.pins7_6 <= NIC_SEQUENCER; + dbg_1v8_ctrl.pins5_4 <= NIC_SEQUENCER; + dbg_1v8_ctrl.pins3_2 <= NIC_SEQUENCER; + dbg_1v8_ctrl.pins1_0 <= NIC_SEQUENCER; end if; when others => null; diff --git a/hdl/projects/cosmo_seq/debug_module/debug_regs.rdl b/hdl/projects/cosmo_seq/debug_module/debug_regs.rdl index 0c0893c7..a73ea1d5 100644 --- a/hdl/projects/cosmo_seq/debug_module/debug_regs.rdl +++ b/hdl/projects/cosmo_seq/debug_module/debug_regs.rdl @@ -126,7 +126,7 @@ addrmap debug_regs { sp_console_bus = 8'h08 {desc = "SP <-> FPGA console UART to pins";}; sp5_console_bus = 8'h09 {desc = "SP5 <-> FPGA console UART to pins";}; sp_ipcc_bus = 8'h0a {desc = "SP <-> FPGA IPCC UART to pins";}; - t6_sequencer = 8'h0b {desc = "T6 sequencer debug output to pins";}; + nic_sequencer = 8'h0b {desc = "NIC sequencer debug output to pins (T6 on cosmo, Versal on metro; see nic_seq/versal_seq for the tap assignment)";}; mux1_sel = 8'h0c {desc = "Mux1 sel pins debug output to pins";}; mux2_sel = 8'h0d {desc = "Mux2 sel pins debug output to pins";}; mux3_sel = 8'h0e {desc = "Mux3 sel pins debug output to pins";}; @@ -166,7 +166,7 @@ addrmap debug_regs { name = "Debug Convenience"; field { desc = "convenience bit for setting up T6 sequencing debug out. This uses 1v8 debug header pins 7..0. Hw clears"; - } t6_seq_en[2:2] = 0; + } nic_seq_en[2:2] = 0; field { desc = "convenience bit for setting up x4 espi debug out. This uses 1v8 debug header pins 7..0. Hw clears"; } espi_dbg_x4_en[1:1] = 0; diff --git a/hdl/projects/cosmo_seq/sequencer/nic_seq.vhd b/hdl/projects/cosmo_seq/sequencer/nic_seq.vhd index d05031ec..c3f634dc 100644 --- a/hdl/projects/cosmo_seq/sequencer/nic_seq.vhd +++ b/hdl/projects/cosmo_seq/sequencer/nic_seq.vhd @@ -29,7 +29,7 @@ entity nic_seq is raw_state : out nic_raw_status_type; api_state : out nic_api_status_type; - nic_dbg_pins : view t6_debug_seq_ss; + nic_dbg_pins : view nic_debug_seq_ss; -- From SP5 hotplug sp5_t6_perst_l : in std_logic; -- follows exactly the power_en hotplug signal. perst_l <= power_en; @@ -103,13 +103,15 @@ begin nic_idle <= '1' when nic_r.state = IDLE else '0'; - nic_dbg_pins.cld_rst_l <= final_nic_outs.cld_rst_l; - nic_dbg_pins.ext_rst_l <= nic_seq_pins.ext_rst_l; + -- Debug header taps, on header pins 5..0 in this order nic_dbg_pins.rails_en <= nic_r.nic_power_en; nic_dbg_pins.rails_pg <= '1' when is_power_good(nic_rails) else '0'; - nic_dbg_pins.nic_mfg_mode_l <= final_nic_outs.nic_mfg_mode_l; - nic_dbg_pins.sp5_mfg_mode_l <= nic_seq_pins.sp5_mfg_mode_l; - nic_dbg_pins.perst_l <= final_nic_outs.perst_l; + nic_dbg_pins.taps(5) <= final_nic_outs.cld_rst_l; + nic_dbg_pins.taps(4) <= final_nic_outs.perst_l; + nic_dbg_pins.taps(3) <= nic_seq_pins.sp5_mfg_mode_l; + nic_dbg_pins.taps(2) <= final_nic_outs.nic_mfg_mode_l; + nic_dbg_pins.taps(1) <= nic_seq_pins.ext_rst_l; + nic_dbg_pins.taps(0) <= '0'; -- Gimlet has the following sequence that was empirically determined to work -- We had to double-perst and we know that cld_rst_l needs to be de-asserted 10ms before perst_l @@ -148,6 +150,8 @@ begin api_state.nic_sm <= NIC_RESET; end if; + -- the Versal states; this NIC never has them + when others => null; end case; end if; @@ -250,6 +254,8 @@ begin v.state := IDLE; end if; + -- the Versal states; this NIC never has them + when others => null; end case; -- MAPO fault handling - monitored in all non-IDLE states diff --git a/hdl/projects/cosmo_seq/sequencer/seq_sync.vhd b/hdl/projects/cosmo_seq/sequencer/seq_sync.vhd index 56d9adb3..4d87f4f3 100644 --- a/hdl/projects/cosmo_seq/sequencer/seq_sync.vhd +++ b/hdl/projects/cosmo_seq/sequencer/seq_sync.vhd @@ -10,6 +10,9 @@ use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.sequencer_regs_pkg.all; +-- Synchronises the SP5-side pins, the fans, the DDR hotswaps and the alert +-- pins, which every SP5 board shares. The NIC-side pins are board specific and +-- have their own synchronisers, t6_sync and versal_sync. entity seq_sync is port ( clk : in std_logic; @@ -22,8 +25,6 @@ entity seq_sync is group_b_pins : view group_b_power_at_fpga; group_c_pins : view group_c_power_at_fpga; sp5_seq_pins : view sp5_seq_at_fpga; - nic_rails_pins : view nic_power_at_fpga; - nic_seq_pins: view nic_seq_at_fpga; reg_alert_l_pins : view power_alert_at_fpga; -- internal, synchronized interfaces rail_masks : in rails_type; @@ -34,15 +35,11 @@ entity seq_sync is group_b : view group_b_power_at_reg; group_c : view group_c_power_at_reg; sp5_seq : view sp5_seq_at_sp5; - nic_rails : view nic_power_at_reg; - nic_seq: view nic_seq_at_nic; reg_alert_l : view power_alert_at_reg; ); end entity; architecture rtl of seq_sync is - signal nic_sync_5v_hsc_pg_l : std_logic; - signal nic_sync_12v_hsc_pg_l : std_logic; signal fan_central_pg_raw : std_logic; signal fan_east_pg_raw : std_logic; signal fan_west_pg_raw : std_logic; @@ -232,87 +229,6 @@ begin sycnd_output => sp5_seq.pwrgd_out ); - -- nic rails sync stuff - nic_rails_pins.nic_hsc_12v.enable <= nic_rails.nic_hsc_12v.enable; - v1p5_nic_a0hp: entity work.meta_sync - port map( - async_input => nic_rails_pins.v1p5_nic_a0hp.pg, - clk => clk, - sycnd_output => nic_rails.v1p5_nic_a0hp.pg - ); - v1p2_nic_pcie_a0hp: entity work.meta_sync - port map( - async_input => nic_rails_pins.v1p2_nic_pcie_a0hp.pg, - clk => clk, - sycnd_output => nic_rails.v1p2_nic_pcie_a0hp.pg - ); - v1p2_nic_enet_a0hp: entity work.meta_sync - port map( - async_input => nic_rails_pins.v1p2_nic_enet_a0hp.pg, - clk => clk, - sycnd_output => nic_rails.v1p2_nic_enet_a0hp.pg - ); - v3p3_nic_a0hp: entity work.meta_sync - port map( - async_input => nic_rails_pins.v3p3_nic_a0hp.pg, - clk => clk, - sycnd_output => nic_rails.v3p3_nic_a0hp.pg - ); - v1p1_nic_a0hp: entity work.meta_sync - port map( - async_input => nic_rails_pins.v1p1_nic_a0hp.pg, - clk => clk, - sycnd_output => nic_rails.v1p1_nic_a0hp.pg - ); - v1p4_nic_a0hp: entity work.meta_sync - port map( - async_input => nic_rails_pins.v1p4_nic_a0hp.pg, - clk => clk, - sycnd_output => nic_rails.v1p4_nic_a0hp.pg - ); - v0p96_nic_vdd_a0hp: entity work.meta_sync - port map( - async_input => nic_rails_pins.v0p96_nic_vdd_a0hp.pg, - clk => clk, - sycnd_output => nic_rails.v0p96_nic_vdd_a0hp.pg - ); - nic_hsc_12v: entity work.meta_sync - port map( - async_input => nic_rails_pins.nic_hsc_12v.pg, - clk => clk, - sycnd_output => nic_sync_12v_hsc_pg_l - ); - nic_hsc_5v: entity work.meta_sync - port map( - async_input => nic_rails_pins.nic_hsc_5v.pg, - clk => clk, - sycnd_output => nic_sync_5v_hsc_pg_l - ); - - -- HSC's are actually pg_l signals, so invert them here - nic_rails.nic_hsc_5v.pg <= not nic_sync_5v_hsc_pg_l; - nic_rails.nic_hsc_12v.pg <= not nic_sync_12v_hsc_pg_l; - -- nic sync-related stuff - nic_seq_pins.cld_rst_l <= nic_seq.cld_rst_l; - nic_seq_pins.perst_l <= nic_seq.perst_l; - nic_seq_pins.eeprom_wp_l <= nic_seq.eeprom_wp_l; - nic_seq_pins.eeprom_wp_buffer_oe_l <= nic_seq.eeprom_wp_buffer_oe_l; - nic_seq_pins.flash_wp_l <= nic_seq.flash_wp_l; - nic_seq_pins.nic_mfg_mode_l <= nic_seq.nic_mfg_mode_l; - nic_seq_pins.nic_pcie_clk_buff_oe_l <= nic_seq.nic_pcie_clk_buff_oe_l; - ext_rst_l_sync: entity work.meta_sync - port map( - async_input => nic_seq_pins.ext_rst_l, - clk => clk, - sycnd_output => nic_seq.ext_rst_l - ); - sp5_mfg_mode_l_sync: entity work.meta_sync - port map( - async_input => nic_seq_pins.sp5_mfg_mode_l, - clk => clk, - sycnd_output => nic_seq.sp5_mfg_mode_l - ); - -- Alert sync stuff @@ -420,6 +336,12 @@ begin clk => clk, sycnd_output => reg_alert_l.pwr_cont3_to_fpga1_alert_l ); + pwr_cont4_to_fpga1_alert_l_sync: entity work.meta_sync + port map( + async_input => reg_alert_l_pins.pwr_cont4_to_fpga1_alert_l, + clk => clk, + sycnd_output => reg_alert_l.pwr_cont4_to_fpga1_alert_l + ); end rtl; diff --git a/hdl/projects/cosmo_seq/sequencer/sequencer_io_pkg.vhd b/hdl/projects/cosmo_seq/sequencer/sequencer_io_pkg.vhd index a8e35f63..ea3da65a 100644 --- a/hdl/projects/cosmo_seq/sequencer/sequencer_io_pkg.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sequencer_io_pkg.vhd @@ -8,10 +8,15 @@ use ieee.numeric_std.all; use work.sp5_power_pkg.all; --- Cosmo-specific sequencer I/O types. The generic rail records and the SP5 --- rail groups they build on live in sp5_power_pkg. +-- Board-side sequencer I/O types for the SP5 boards. The generic rail records +-- and the SP5 rail groups they build on live in sp5_power_pkg; this package +-- holds the NIC-side records for both NIC flavours (cosmo's T6, metro's +-- Versal) and the alert-pin record, which is the union of both boards' pins. package sequencer_io_pkg is + -- Which NIC a board carries. Selects the NIC half of sp5_sequencer. + type nic_kind_t is (NIC_T6, NIC_VERSAL); + type seq_power_alert_pins_t is record smbus_fan_central_hsc_to_fpga1_alert_l : std_logic; smbus_fan_east_hsc_to_fpga1_alert_l : std_logic; @@ -30,6 +35,7 @@ package sequencer_io_pkg is v0p96_nic_to_fpga1_alert_l : std_logic; pwr_cont2_to_fpga1_alert_l : std_logic; pwr_cont3_to_fpga1_alert_l : std_logic; + pwr_cont4_to_fpga1_alert_l : std_logic; -- metro only; cosmo ties it high end record; view power_alert_at_fpga of seq_power_alert_pins_t is smbus_fan_central_hsc_to_fpga1_alert_l : in; @@ -49,6 +55,7 @@ package sequencer_io_pkg is v0p96_nic_to_fpga1_alert_l : in; pwr_cont2_to_fpga1_alert_l : in; pwr_cont3_to_fpga1_alert_l : in; + pwr_cont4_to_fpga1_alert_l : in; end view; alias power_alert_at_reg is power_alert_at_fpga'converse; @@ -77,25 +84,21 @@ package sequencer_io_pkg is end view; alias nic_seq_at_nic is nic_seq_at_fpga'converse; - type t6_debug_if is record - cld_rst_l : std_logic; -- T6 cld reset (FPGA output) - ext_rst_l : std_logic; -- T6 external reset (FPGA input) - rails_en : std_logic; -- T6 power rails enable (FPGA output combined) - rails_pg : std_logic; -- T6 power rails power good (FPGA input combined) - nic_mfg_mode_l : std_logic; -- T6 NIC manufacturing mode (FPGA output) - sp5_mfg_mode_l : std_logic; -- T6 SP5 manufacturing mode (FPGA input) - perst_l : std_logic; -- T6 PCIe reset (FPGA output) + -- What the NIC sequencer offers the debug header: the combined rail + -- enable and power good, plus six taps whose meaning is the NIC's to + -- define. They land on header pins 5..0 in order; see nic_seq (T6) and + -- versal_seq for what each board puts there. + type nic_debug_if is record + rails_en : std_logic; -- NIC power rails enable (FPGA output combined) + rails_pg : std_logic; -- NIC power rails power good (FPGA input combined) + taps : std_logic_vector(5 downto 0); end record; - view t6_debug_seq_ss of t6_debug_if is - cld_rst_l : out; - ext_rst_l : out; + view nic_debug_seq_ss of nic_debug_if is rails_en : out; rails_pg : out; - nic_mfg_mode_l : out; - sp5_mfg_mode_l : out; - perst_l : out; + taps : out; end view; - alias t6_debug_dbg is t6_debug_seq_ss'converse; + alias nic_debug_dbg is nic_debug_seq_ss'converse; -- effectively one enable fires all of this due to the -- hardware design. @@ -124,6 +127,132 @@ package sequencer_io_pkg is end view; alias nic_power_at_reg is nic_power_at_fpga'converse; + -- Metro's Versal rail tree. Everything in here is active high; the + -- synchroniser inverts the schematic's active-low 12V/5V power-good pins + -- on the way in so this layer never has to think about pin polarity. + -- + -- V0P92_NIC_AVCC and V1P2_NIC_AVTT have no FPGA enable -- they cascade off + -- rails we do enable -- so they are readback only. + type versal_power_t is record + hsc_12v : power_rail_t; -- FPGA1_TO_NIC_HSC_EN / V12P0_NIC_A0HP_PG_L + hsc_5v : cascade_power_rail_t; -- cascades off the 12V hotswap + v3p3 : power_rail_t; + v1p8 : power_rail_t; + v1p5 : power_rail_t; + v1p5_avccaux : power_rail_t; + v1p4 : power_rail_t; + v1p1 : power_rail_t; + v0p88 : power_rail_t; + v0p8_vccint : power_rail_t; + v0p92_avcc : cascade_power_rail_t; + v1p2_avtt : cascade_power_rail_t; + end record; + function is_power_good(power_group: versal_power_t) return boolean; + view versal_power_at_fpga of versal_power_t is + hsc_12v : view power_rail_at_fpga; + hsc_5v : view cascade_power_rail_at_fpga; + v3p3 : view power_rail_at_fpga; + v1p8 : view power_rail_at_fpga; + v1p5 : view power_rail_at_fpga; + v1p5_avccaux : view power_rail_at_fpga; + v1p4 : view power_rail_at_fpga; + v1p1 : view power_rail_at_fpga; + v0p88 : view power_rail_at_fpga; + v0p8_vccint : view power_rail_at_fpga; + v0p92_avcc : view cascade_power_rail_at_fpga; + v1p2_avtt : view cascade_power_rail_at_fpga; + end view; + alias versal_power_at_reg is versal_power_at_fpga'converse; + -- What a board without a Versal ties its versal_rails_pins to: nothing + -- enabled, nothing good. Only ever looked at by versal_seq, which such a + -- board does not generate. + constant versal_power_absent : versal_power_t := ( + hsc_12v => (enable => '0', pg => '1'), -- hotswap pg pins are active low + hsc_5v => (pg => '1'), + v3p3 => (enable => '0', pg => '0'), + v1p8 => (enable => '0', pg => '0'), + v1p5 => (enable => '0', pg => '0'), + v1p5_avccaux => (enable => '0', pg => '0'), + v1p4 => (enable => '0', pg => '0'), + v1p1 => (enable => '0', pg => '0'), + v0p88 => (enable => '0', pg => '0'), + v0p8_vccint => (enable => '0', pg => '0'), + v0p92_avcc => (pg => '0'), + v1p2_avtt => (pg => '0') + ); + + -- Versal boot straps and status. mode is driven onto the VP1202's + -- MODE[3:0] pins through a buffer we also enable, and por_b is the + -- device's power-on reset. + type versal_boot_t is record + mode : std_logic_vector(3 downto 0); + mode_buffer_en_l : std_logic; + por_b : std_logic; + err_done_buff_en : std_logic; + done : std_logic; + error_out : std_logic; + end record; + view versal_boot_at_fpga of versal_boot_t is + mode : out; + mode_buffer_en_l : out; + por_b : out; + err_done_buff_en : out; + done : in; + error_out : in; + end view; + alias versal_boot_at_versal is versal_boot_at_fpga'converse; + constant versal_boot_absent : versal_boot_t := ( + mode => (others => '0'), mode_buffer_en_l => '1', por_b => '0', + err_done_buff_en => '0', done => '0', error_out => '0' + ); + + -- One of the Versal's two PCIe channels to the host. + type versal_pcie_chan_t is record + perst_l : std_logic; + prsnt_l : std_logic; + pwren_l : std_logic; + clk_buff_oe_l : std_logic; + end record; + view versal_pcie_chan_at_fpga of versal_pcie_chan_t is + perst_l : out; + prsnt_l : in; + pwren_l : in; + clk_buff_oe_l : out; + end view; + alias versal_pcie_chan_at_nic is versal_pcie_chan_at_fpga'converse; + + type versal_pcie_t is record + cha : versal_pcie_chan_t; + chb : versal_pcie_chan_t; + end record; + view versal_pcie_at_fpga of versal_pcie_t is + cha : view versal_pcie_chan_at_fpga; + chb : view versal_pcie_chan_at_fpga; + end view; + alias versal_pcie_at_nic is versal_pcie_at_fpga'converse; + constant versal_pcie_absent : versal_pcie_t := ( + cha => (perst_l => '0', prsnt_l => '1', pwren_l => '1', clk_buff_oe_l => '1'), + chb => (perst_l => '0', prsnt_l => '1', pwren_l => '1', clk_buff_oe_l => '1') + ); + + -- Likewise what a board without a T6 ties its T6 pins to. + constant nic_power_absent : nic_power_t := ( + v1p5_nic_a0hp => (pg => '0'), + v1p2_nic_pcie_a0hp => (pg => '0'), + v1p2_nic_enet_a0hp => (pg => '0'), + v3p3_nic_a0hp => (pg => '0'), + v1p1_nic_a0hp => (pg => '0'), + v1p4_nic_a0hp => (pg => '0'), + v0p96_nic_vdd_a0hp => (pg => '0'), + nic_hsc_12v => (enable => '0', pg => '1'), -- hotswap pg pins are active low + nic_hsc_5v => (pg => '1') + ); + constant nic_seq_pins_absent : nic_seq_pins_t := ( + cld_rst_l => '0', perst_l => '0', eeprom_wp_l => '0', + eeprom_wp_buffer_oe_l => '1', flash_wp_l => '0', nic_mfg_mode_l => '1', + ext_rst_l => '1', nic_pcie_clk_buff_oe_l => '1', sp5_mfg_mode_l => '1' + ); + end package; package body sequencer_io_pkg is @@ -143,4 +272,22 @@ package body sequencer_io_pkg is ) = '1'; end function; + function is_power_good(power_group: versal_power_t) return boolean is + begin + return ( + power_group.hsc_12v.pg and + power_group.hsc_5v.pg and + power_group.v3p3.pg and + power_group.v1p8.pg and + power_group.v1p5.pg and + power_group.v1p5_avccaux.pg and + power_group.v1p4.pg and + power_group.v1p1.pg and + power_group.v0p88.pg and + power_group.v0p8_vccint.pg and + power_group.v0p92_avcc.pg and + power_group.v1p2_avtt.pg + ) = '1'; + end function; + end package body; diff --git a/hdl/projects/cosmo_seq/sequencer/sequencer_regs.rdl b/hdl/projects/cosmo_seq/sequencer/sequencer_regs.rdl index dbd0768f..3e51676b 100644 --- a/hdl/projects/cosmo_seq/sequencer/sequencer_regs.rdl +++ b/hdl/projects/cosmo_seq/sequencer/sequencer_regs.rdl @@ -1,5 +1,14 @@ -// This is SystemRDL description of the sw-accessible registers in the Cosmo -// Sequencer FPGA block. +// SystemRDL description of the sw-accessible registers in the SP5 sequencer +// block, shared by every SP5 board (cosmo, metro). +// +// The map is the union of what the boards need. The SP5 half is common. The +// NIC half differs by board -- cosmo carries a T6 behind a single reset, metro +// an AMD Versal with a rail per enable, boot straps and two PCIe channels -- +// and both sets of NIC registers exist on both boards: the ones a board has no +// hardware for read as zero and their writes do nothing. Software should key +// off the board (the info block) before trusting a NIC register. Cosmo's +// layout is the original; everything metro added is appended or fills spare +// bits, so cosmo's offsets and bit positions have not moved. addrmap sequencer_regs { name = "Power Sequencing Function"; @@ -87,6 +96,15 @@ This regulator controls VDDCR_CPU1_EN and VDDIO_SP5 rails."; desc = "Live. Set '1' when pwr_cont3_to_fpga1_alert_l is active (low on board). This regulator controls V1P1_SP5_A0, V1P8_SP5_A0 and V3P3_SP5_A0 rails."; } pwr_cont3_to_fpga1_alert[1]; + field { + desc = "Live. Set '1' when pwr_cont4_to_fpga1_alert_l is active (low on board). Metro only."; + } pwr_cont4_to_fpga1_alert[1]; + field { + desc = "Live. Set '1' when the Versal is asserting ERROR_OUT. Metro only."; + } versal_error_out[1]; + field { + desc = "The pre-boot measurement of the NIC image failed (sticky since fpga reset or last clear). Metro only."; + } nic_hash_err[1]; }; // Set up interrupt registers using a common irq_type irq IFR; @@ -111,6 +129,19 @@ This regulator controls V1P1_SP5_A0, V1P8_SP5_A0 and V3P3_SP5_A0 rails."; field { desc = "NIC power OK"; } nicpwrok[1] = 0; + field { + desc = "NIC has finished loading its image (Versal DONE). Metro only."; + } nicdone[1] = 0; + field { + desc = "The hash engine measured the NIC image before the last boot; the + digest is in the hash engine's HW_DIGEST registers. Cleared when the NIC + is next sequenced. Metro only."; + } nic_hash_done[1] = 0; + field { + desc = "The pre-boot measurement failed (see the hash engine's HW_STATUS) and + the NIC was booted regardless. Cleared when the NIC is next sequenced. + Metro only."; + } nic_hash_err[1] = 0; field { desc = "Interrupt Pending Status (set if any enabled IRQ has its flag set)"; } int_pend[31:31] = 0; @@ -236,8 +267,10 @@ This regulator controls V1P1_SP5_A0, V1P8_SP5_A0 and V3P3_SP5_A0 rails."; enum nic_state_machine { IDLE = 8'h00 {desc = "";}; ENABLE_POWER = 8'h01 {desc = "";}; - NIC_RESET = 8'h02 {desc = "";}; + NIC_RESET = 8'h02 {desc = "Rails are up, NIC held in reset (on metro: while the boot mode is strapped)";}; DONE = 8'h03 {desc = "";}; + BOOTING = 8'h04 {desc = "Metro only. POR_B released, waiting for DONE";}; + MEASURING = 8'h05 {desc = "Metro only. Rails are up, holding POR_B while the hash engine measures the boot image";}; }; field { desc = "Nic SM Status"; @@ -260,7 +293,17 @@ This regulator controls V1P1_SP5_A0, V1P8_SP5_A0 and V3P3_SP5_A0 rails."; EARLY_CLD_RST = 8'h03 {desc = "";}; EARLY_PERST = 8'h04 {desc = "";}; EARLY_PERST_ASSERT = 8'h05 {desc = "";}; - DONE = 8'h06 {desc = "";}; + DONE = 8'h06 {desc = "Booted. On metro the boot flash mux is then pointed at the FPGA so the SP5 can reach it over eSPI";}; + HSC_EN = 8'h07 {desc = "Metro (Versal). 12V hotswap enabled, waiting for 12V and 5V";}; + CORE_EN = 8'h08 {desc = "Metro (Versal). VCCINT group enabled, waiting for PG";}; + AUX_EN = 8'h09 {desc = "Metro (Versal). VCCAUX group enabled, waiting for PG";}; + IO_EN = 8'h0a {desc = "Metro (Versal). VCCO group enabled, waiting for PG";}; + RAILS_SETTLE = 8'h0b {desc = "Metro (Versal). All rails good, holding POR_B while they settle";}; + HASH_IMAGE = 8'h0c {desc = "Metro (Versal). Rails up, POR_B held, boot flash mux pointed at the FPGA; waiting for the hash engine to measure the image";}; + HASH_RELEASE = 8'h0d {desc = "Metro (Versal). Measurement over, waiting for the hash engine to drop its acknowledge";}; + MODE_STRAP = 8'h0e {desc = "Metro (Versal). Boot mode driven onto MODE[3:0], buffer enabled";}; + POR_RELEASE = 8'h0f {desc = "Metro (Versal). POR_B released";}; + WAIT_DONE = 8'h10 {desc = "Metro (Versal). Waiting for the Versal to assert DONE";}; }; field { @@ -357,6 +400,37 @@ This regulator controls V1P1_SP5_A0, V1P8_SP5_A0 and V3P3_SP5_A0 rails."; field { desc = "NIC v0p96_nic_vdd_a0hp (discrete pg, enable cascade from nic 5V)"; } v0p96_nic_vdd_a0hp[1]; + // Metro's Versal rail tree. Its hotswaps share the nic_hsc_* bits above. + field { + desc = "Metro only. Versal VCCINT 0.8V"; + } versal_v0p8_vccint[1]; + field { + desc = "Metro only. Versal 0.88V"; + } versal_v0p88[1]; + field { + desc = "Metro only. Versal transceiver AVCC 0.92V (discrete pg, cascade enable)"; + } versal_v0p92_avcc[1]; + field { + desc = "Metro only. Versal 1.1V"; + } versal_v1p1[1]; + field { + desc = "Metro only. Versal transceiver AVTT 1.2V (discrete pg, cascade enable)"; + } versal_v1p2_avtt[1]; + field { + desc = "Metro only. Versal 1.4V"; + } versal_v1p4[1]; + field { + desc = "Metro only. Versal VCCAUX 1.5V"; + } versal_v1p5[1]; + field { + desc = "Metro only. Versal transceiver AVCCAUX 1.5V"; + } versal_v1p5_avccaux[1]; + field { + desc = "Metro only. Versal VCCO 1.8V"; + } versal_v1p8[1]; + field { + desc = "Metro only. Versal VCCO 3.3V"; + } versal_v3p3[1]; }; rails #(.sw_kind(rw)) rail_enables ; @@ -540,5 +614,116 @@ This regulator controls V1P1_SP5_A0, V1P8_SP5_A0 and V3P3_SP5_A0 rails."; } clk_en[0:0] = 0; } pcie_clk_ctrl; + // Everything below is metro's Versal NIC. Cosmo reads zeros here. + + reg { + name = "Versal-related signal readbacks"; + desc = "Metro only. Live status and readbacks of our registers and responses back from the Versal. + For tri-state cases, read '1' as 'Z'. No inversions done here."; + default sw = r; + field { + desc = "POR_B live status, (From FPGA to Versal)"; + } por_b[1]; + field { + desc = "MODE[3:0] live status, (From FPGA to Versal)"; + } mode[4]; + field { + desc = "mode_buffer_en_l live status, (From FPGA to the MODE buffer)"; + } mode_buffer_en_l[1]; + field { + desc = "err_done_buff_en live status, (From FPGA to the DONE/ERROR_OUT buffer)"; + } err_done_buff_en[1]; + field { + desc = "DONE live status, note: only valid once err_done_buff_en is set (From Versal)"; + } done[1]; + field { + desc = "ERROR_OUT live status, note: only valid once err_done_buff_en is set (From Versal)"; + } error_out[1]; + field { + desc = "Channel A perst_l live status, (From FPGA to Versal)"; + } cha_perst_l[1]; + field { + desc = "Channel A prsnt_l live status, (From Versal)"; + } cha_prsnt_l[1]; + field { + desc = "Channel A pwren_l live status, (From Versal)"; + } cha_pwren_l[1]; + field { + desc = "Channel A clk_buff_oe_l live status, (From FPGA to buffer)"; + } cha_clk_buff_oe_l[1]; + field { + desc = "Channel B perst_l live status, (From FPGA to Versal)"; + } chb_perst_l[1]; + field { + desc = "Channel B prsnt_l live status, (From Versal)"; + } chb_prsnt_l[1]; + field { + desc = "Channel B pwren_l live status, (From Versal)"; + } chb_pwren_l[1]; + field { + desc = "Channel B clk_buff_oe_l live status, (From FPGA to buffer)"; + } chb_clk_buff_oe_l[1]; + field { + desc = "Channel A slot sp5_perst_l live status, (From SP5 hotplug to FPGA)"; + } sp5_cha_perst_l[1]; + field { + desc = "Channel B slot sp5_perst_l live status, (From SP5 hotplug to FPGA)"; + } sp5_chb_perst_l[1]; + } versal_readbacks; + + reg { + name = "Versal Overrides"; + desc = "Metro only. Control over the Versal signals for debugging purposes outside the + sequencer logic, muxed in by debug_enables.nic_override. A test MAPO is raised + through nic_overrides.nic_test_mapo as on cosmo."; + field { + desc = "por_b drive, (From FPGA to Versal)"; + } por_b[1] = 0; + field { + desc = "mode_buffer_en_l drive, (From FPGA to the MODE buffer)"; + } mode_buffer_en_l[1] = 1; + field { + desc = "err_done_buff_en drive, (From FPGA to the DONE/ERROR_OUT buffer)"; + } err_done_buff_en[1] = 0; + field { + desc = "Channel A perst_l drive, (From FPGA to Versal)"; + } cha_perst_l[1] = 0; + field { + desc = "Channel A clk_buff_oe_l drive, (From FPGA to buffer)"; + } cha_clk_buff_oe_l[1] = 1; + field { + desc = "Channel B perst_l drive, (From FPGA to Versal)"; + } chb_perst_l[1] = 0; + field { + desc = "Channel B clk_buff_oe_l drive, (From FPGA to buffer)"; + } chb_clk_buff_oe_l[1] = 1; + } versal_overrides; + + reg { + name = "Versal Boot Control"; + desc = "Metro only. Boot-mode straps driven onto the Versal's MODE[3:0] pins while POR_B is held. + The sequencer latches these when it leaves the RAILS_SETTLE state, so changing them + only takes effect on the next Versal power-up."; + field { + desc = "Versal boot mode driven onto MODE[3:0]. 0x0 is JTAG, 0x2 is QSPI32, which + is how the board is wired. JTAG is the escape hatch for bring-up."; + } mode[3:0] = 4'h2; + field { + desc = "Measure the boot image with the hash engine before releasing POR_B. + The range comes from the hash engine's HW_FLASH_ADDR/HW_LENGTH. A failed + measurement is recorded in the status register and the boot goes ahead. + Clear to skip the measurement."; + } hash_image[4:4] = 1; + } versal_boot_ctrl; + + reg { + name = "Board Version"; + desc = "Metro only. Board version straps read directly off the FPGA1_VERSION_ID pins."; + default sw = r; + field { + desc = "FPGA1_VERSION_ID[1:0] strap value"; + } version_id[1:0]; + } board_version; + }; \ No newline at end of file diff --git a/hdl/projects/cosmo_seq/sequencer/sequencer_regs.vhd b/hdl/projects/cosmo_seq/sequencer/sequencer_regs.vhd index 5ba1359f..7bf9dc7c 100644 --- a/hdl/projects/cosmo_seq/sequencer/sequencer_regs.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sequencer_regs.vhd @@ -45,6 +45,20 @@ entity sequencer_regs is -- misc readbacks sp5_readbacks : in sp5_readbacks_type; nic_readbacks : in nic_readbacks_type; + -- power-good summaries for the status register + fans_power_ok : in std_logic; + a0_ok : in std_logic; + nic_power_ok : in std_logic; + -- Metro's Versal NIC. A board without one leaves these at their + -- defaults and the registers read as zero. + nic_done : in std_logic := '0'; + versal_error_out : in std_logic := '0'; + nic_hash_done : in std_logic := '0'; + nic_hash_err : in std_logic := '0'; + versal_readbacks : in versal_readbacks_type; + versal_overrides : out versal_overrides_type; + versal_boot_ctrl : out versal_boot_ctrl_type; + board_version : in board_version_type; -- Ignition mux and reconfig control ignition_mux_sel : out std_logic; ignition_creset : out std_logic; @@ -85,6 +99,7 @@ architecture rtl of sequencer_regs is signal a0_en_last : std_logic; signal rails_pg_max : rails_type; + signal irq_pending : std_logic; signal rdata : std_logic_vector(31 downto 0); signal active_read : std_logic; @@ -99,6 +114,9 @@ architecture rtl of sequencer_regs is -- irq block so it will handle things correctly. constant level_edge_n : irq_type := ( + nic_hash_err => EDGE, + versal_error_out => LEVEL, + pwr_cont4_to_fpga1_alert => LEVEL, pwr_cont3_to_fpga1_alert => LEVEL, pwr_cont2_to_fpga1_alert => LEVEL, pwr_cont1_to_fpga1_alert => LEVEL, @@ -127,12 +145,26 @@ architecture rtl of sequencer_regs is begin + status <= ( + fanpwrok => fans_power_ok, + a0pwrok => a0_ok, + nicpwrok => nic_power_ok, + nicdone => nic_done, + nic_hash_done => nic_hash_done, + nic_hash_err => nic_hash_err, + int_pend => irq_pending + ); + irq_pending <= '1' when (compress(ifr) and compress(ier)) /= (compress(ier)'range => '0') else '0'; + ignition_mux_sel <= ignition_control.mux_to_ignition; ignition_creset <= ignition_control.ignition_creset; allow_backplane_pcie_clk <= pcie_clk_ctrl.clk_en; -- Map a bunch of discrete signals into the irq_raw vector. irq_raw <= ( + nic_hash_err => nic_hash_err, + versal_error_out => versal_error_out, + pwr_cont4_to_fpga1_alert => not reg_alert_l.pwr_cont4_to_fpga1_alert_l, pwr_cont3_to_fpga1_alert => not reg_alert_l.pwr_cont3_to_fpga1_alert_l, pwr_cont2_to_fpga1_alert => not reg_alert_l.pwr_cont2_to_fpga1_alert_l, pwr_cont1_to_fpga1_alert => not reg_alert_l.pwr_cont1_to_fpga1_alert_l, @@ -295,6 +327,8 @@ begin rails_pg_max <= reset_0s; debug_enables <= rec_reset; nic_overrides <= rec_reset; + versal_overrides <= rec_reset; + versal_boot_ctrl <= rec_reset; ignition_control <= rec_reset; pcie_clk_ctrl <= rec_reset; rail_masks <= reset_0s; @@ -304,6 +338,13 @@ begin irq_clear <= reset_0s; -- clear single-cycle flags. igr <= reset_0s; nic_overrides.nic_test_mapo <= '0'; -- Clear test MAPO bit every cycle, so it's a single-cycle pulse when set. + -- Max hold of the live power goods, cleared on a fresh sequence up + -- or by writing the register. + if a0_en_redge then + rails_pg_max <= reset_0s; + else + rails_pg_max <= rails_pg_max or rails_pg_rdbk; + end if; if active_write then case to_integer(axi_if.write_address.addr) is @@ -316,6 +357,8 @@ begin when RAIL_PGS_MAX_HOLD_OFFSET => rails_pg_max <= reset_0s; when DEBUG_ENABLES_OFFSET => debug_enables <= unpack(axi_if.write_data.data); when NIC_OVERRIDES_OFFSET => nic_overrides <= unpack(axi_if.write_data.data); + when VERSAL_OVERRIDES_OFFSET => versal_overrides <= unpack(axi_if.write_data.data); + when VERSAL_BOOT_CTRL_OFFSET => versal_boot_ctrl <= unpack(axi_if.write_data.data); when RAIL_MASKS_OFFSET => rail_masks <= unpack(axi_if.write_data.data); when SP5_SEQ_TEST_MASK_OFFSET => sp5_seq_test_mask <= unpack(axi_if.write_data.data); when IGNITION_CONTROL_OFFSET => ignition_control <= unpack(axi_if.write_data.data); @@ -363,6 +406,10 @@ begin when NIC_OVERRIDES_OFFSET => rdata <= pack(nic_overrides); when IGNITION_CONTROL_OFFSET => rdata <= pack(ignition_control); when PCIE_CLK_CTRL_OFFSET => rdata <= pack(pcie_clk_ctrl); + when VERSAL_READBACKS_OFFSET => rdata <= pack(versal_readbacks); + when VERSAL_OVERRIDES_OFFSET => rdata <= pack(versal_overrides); + when VERSAL_BOOT_CTRL_OFFSET => rdata <= pack(versal_boot_ctrl); + when BOARD_VERSION_OFFSET => rdata <= pack(board_version); when others => rdata <= (others => '0'); end case; end if; diff --git a/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_pkg.vhd b/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_pkg.vhd index 9733554b..255b56b6 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_pkg.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_pkg.vhd @@ -53,6 +53,20 @@ package sp5_seq_sim_pkg is constant rail_name : in string ); + -- Bring the board to A0 and the NIC all the way to DONE. + procedure power_up_to_nic_done ( + signal net : inout network_t + ); + + -- Drop one Versal rail (a rail_model of its own, unlike the T6's rails + -- which sit behind nic_model) once the Versal is up and check the NIC + -- MAPO path, including that the flag can be cleared afterwards. + procedure test_versal_rail_mapo_fault_injection ( + signal net : inout network_t; + constant rail_actor : in actor_t; + constant rail_name : in string + ); + end package; package body sp5_seq_sim_pkg is @@ -202,4 +216,67 @@ package body sp5_seq_sim_pkg is info("NIC MAPO fault injection test completed successfully for rail: " & rail_name); end procedure; + procedure power_up_to_nic_done ( + signal net : inout network_t + ) is + variable read_data : std_logic_vector(31 downto 0); + variable seq_state : seq_api_status_a0_sm; + variable nic_state : nic_api_status_nic_sm; + begin + write_bus(net, bus_handle, + To_StdLogicVector(POWER_CTRL_OFFSET, bus_handle.p_address_length), + POWER_CTRL_A0_EN_MASK); + poll_for_seq_state(net, DONE); + read_bus(net, bus_handle, + To_StdLogicVector(SEQ_API_STATUS_OFFSET, bus_handle.p_address_length), + read_data); + seq_state := encode(read_data(7 downto 0)); + check_equal(seq_state = DONE, true, "Expected A0 sequencer to be in DONE state"); + + poll_for_nic_state(net, DONE); + read_bus(net, bus_handle, + To_StdLogicVector(NIC_API_STATUS_OFFSET, bus_handle.p_address_length), + read_data); + nic_state := encode(read_data(7 downto 0)); + check_equal(nic_state = DONE, true, "Expected NIC sequencer to be in DONE state"); + end procedure; + + procedure test_versal_rail_mapo_fault_injection ( + signal net : inout network_t; + constant rail_actor : in actor_t; + constant rail_name : in string + ) is + variable read_data : std_logic_vector(31 downto 0); + variable nic_state : nic_api_status_nic_sm; + begin + power_up_to_nic_done(net); + + info("Injecting Versal fault on rail: " & rail_name); + disable_power_good(net, rail_actor); + wait for 100 us; + + read_bus(net, bus_handle, + To_StdLogicVector(IFR_OFFSET, bus_handle.p_address_length), read_data); + check_equal((read_data and IFR_NICMAPO_MASK) /= x"00000000", true, + "Expected NICMAPO bit to be set in IFR for rail: " & rail_name); + + read_bus(net, bus_handle, + To_StdLogicVector(NIC_API_STATUS_OFFSET, bus_handle.p_address_length), + read_data); + nic_state := encode(read_data(7 downto 0)); + check_equal(nic_state = IDLE, true, + "Expected NIC sequencer to return to IDLE after MAPO on " & rail_name); + + info("Clearing NICMAPO bit in IFR"); + write_bus(net, bus_handle, + To_StdLogicVector(IFR_OFFSET, bus_handle.p_address_length), + IFR_NICMAPO_MASK); + read_bus(net, bus_handle, + To_StdLogicVector(IFR_OFFSET, bus_handle.p_address_length), read_data); + check_equal((read_data and IFR_NICMAPO_MASK) = x"00000000", true, + "Expected NICMAPO bit to clear for rail: " & rail_name); + + enable_power_good(net, rail_actor); + end procedure; + end package body; \ No newline at end of file diff --git a/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_tb.vhd b/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_tb.vhd index a28e2661..72d2e496 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_tb.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_tb.vhd @@ -14,6 +14,7 @@ library vunit_lib; use work.sp5_seq_sim_pkg.all; use work.sequencer_regs_pkg.all; +use work.sequencer_io_pkg.all; use work.rail_model_msg_pkg; use work.nic_model_msg_pkg.all; @@ -29,11 +30,11 @@ architecture tb of sp5_seq_sim_tb is begin - th: entity work.sp5_seq_sim_th; + th: entity work.sp5_seq_sim_th generic map (NIC_KIND => NIC_T6); bench: process alias reset is << signal th.reset : std_logic >>; - alias sp5_t6_perst_l is << signal th.sp5_t6_perst_l : std_logic >>; + alias sp5_t6_perst_l is << signal th.sp5_nic_perst_l : std_logic >>; variable read_data : std_logic_vector(31 downto 0); variable seq_state : seq_api_status_a0_sm; constant grpa_v3p3_actor : actor_t := find("grpa_v3p3_sp5_a1"); diff --git a/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_th.vhd b/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_th.vhd index b576d472..dfbb33da 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_th.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_th.vhd @@ -16,7 +16,14 @@ use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.sp5_seq_sim_pkg.all; +-- One harness for both NIC flavours: NIC_KIND picks which NIC's models are +-- instantiated, and the testbenches (sp5_seq_sim_tb for the T6, versal_seq_sim_tb +-- for the Versal) each set it. Both NICs' pin records exist as signals so the +-- testbenches can reach them; the absent NIC's are left at their tie-off values. entity sp5_seq_sim_th is + generic ( + NIC_KIND : nic_kind_t := NIC_T6 + ); end entity; architecture th of sp5_seq_sim_th is @@ -49,6 +56,7 @@ architecture th of sp5_seq_sim_th is fan_west_hsc_disable => 'Z' ); signal reg_alert_l_pins : seq_power_alert_pins_t := ( + pwr_cont4_to_fpga1_alert_l => '1', smbus_fan_central_hsc_to_fpga1_alert_l => '1', smbus_fan_east_hsc_to_fpga1_alert_l => '1', smbus_fan_west_hsc_to_fpga1_alert_l => '1', @@ -68,12 +76,31 @@ architecture th of sp5_seq_sim_th is pwr_cont3_to_fpga1_alert_l => '1' ); signal ddr_bulk_pins : ddr_bulk_power_t; - signal nic_rails_pins : nic_power_t; + signal nic_rails_pins : nic_power_t := nic_power_absent; signal a0_ok : std_logic; signal a0_idle : std_logic; - signal sp5_t6_perst_l : std_logic := '1'; + signal sp5_nic_perst_l : std_logic := '1'; + signal sp5_nic_chb_perst_l : std_logic := '1'; signal axi_if : axil8x32_pkg.axil_t; - signal nic_dbg_pins : t6_debug_if; + signal nic_dbg_pins : nic_debug_if; + + -- Versal side + signal versal_rails_pins : versal_power_t := versal_power_absent; + signal versal_boot_pins : versal_boot_t := versal_boot_absent; + signal versal_pcie_pins : versal_pcie_t := ( + cha => (perst_l => 'Z', prsnt_l => '0', pwren_l => '0', clk_buff_oe_l => 'Z'), + chb => (perst_l => 'Z', prsnt_l => '0', pwren_l => '0', clk_buff_oe_l => 'Z') + ); + signal versal_held_in_reset : std_logic; + signal flash_owned_by_seq : std_logic; + -- Hash engine stand-in. The testbench sets how long a measurement takes + -- and whether it fails; the handshake itself is modelled here. + signal hash_req : std_logic; + signal hash_ack : std_logic := '0'; + signal hash_err : std_logic := '0'; + signal hash_model_time : time := 20 us; + signal hash_model_fail : boolean := false; + signal hash_requests : natural := 0; begin @@ -84,7 +111,8 @@ begin -- instantiate the sequencer dut: entity work.sp5_sequencer generic map( - CNTS_P_MS => 100 + CNTS_P_MS => 100, + NIC_KIND => NIC_KIND ) port map( clk => clk, @@ -100,10 +128,20 @@ begin sp5_seq_pins => sp5_seq_pins, nic_rails_pins => nic_rails_pins, nic_seq_pins => nic_seq_pins, + versal_rails_pins => versal_rails_pins, + versal_boot_pins => versal_boot_pins, + versal_pcie_pins => versal_pcie_pins, + versal_held_in_reset => versal_held_in_reset, + flash_owned_by_seq => flash_owned_by_seq, + hash_req => hash_req, + hash_ack => hash_ack, + hash_err => hash_err, + version_id => "01", nic_dbg_pins => nic_dbg_pins, - sp5_t6_perst_l => sp5_t6_perst_l, - irq_l_out => open, - reg_alert_l_pins => reg_alert_l_pins + sp5_nic_perst_l => sp5_nic_perst_l, + sp5_nic_chb_perst_l => sp5_nic_chb_perst_l, + irq_l_out => open, + reg_alert_l_pins => reg_alert_l_pins ); axi_lite_master_inst: entity vunit_lib.axi_lite_master @@ -229,14 +267,76 @@ begin reset => reset, sp5_pins => sp5_seq_pins ); - nic_model_inst: entity work.nic_model - generic map( - actor_name => "nic_model" - ) - port map( - clk => clk, - reset => reset, - nic_rails => nic_rails_pins - ); + t6: if NIC_KIND = NIC_T6 generate + nic_model_inst: entity work.nic_model + generic map( + actor_name => "nic_model" + ) + port map( + clk => clk, + reset => reset, + nic_rails => nic_rails_pins + ); + end generate; + + versal: if NIC_KIND = NIC_VERSAL generate + versal_v3p3: entity work.rail_model + generic map(actor_name => "versal_v3p3") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v3p3); + versal_v1p8: entity work.rail_model + generic map(actor_name => "versal_v1p8") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v1p8); + versal_v1p5: entity work.rail_model + generic map(actor_name => "versal_v1p5") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v1p5); + versal_v1p5_avccaux: entity work.rail_model + generic map(actor_name => "versal_v1p5_avccaux") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v1p5_avccaux); + versal_v1p4: entity work.rail_model + generic map(actor_name => "versal_v1p4") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v1p4); + versal_v1p1: entity work.rail_model + generic map(actor_name => "versal_v1p1") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v1p1); + versal_v0p88: entity work.rail_model + generic map(actor_name => "versal_v0p88") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v0p88); + versal_v0p8_vccint: entity work.rail_model + generic map(actor_name => "versal_v0p8_vccint") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v0p8_vccint); + -- The two transceiver rails have no enable of their own; they cascade + -- off the group that brings them up. + versal_v0p92_avcc: entity work.cascade_rail_model + port map(clk => clk, reset => reset, upstream_pg => versal_rails_pins.v0p88.pg, rail => versal_rails_pins.v0p92_avcc); + versal_v1p2_avtt: entity work.cascade_rail_model + port map(clk => clk, reset => reset, upstream_pg => versal_rails_pins.v1p5.pg, rail => versal_rails_pins.v1p2_avtt); + + -- Four-phase handshake as hash_engine_top does it: acknowledge some + -- time after the request, hold it until the request drops. + hash_model: process + begin + wait until hash_req = '1'; + hash_requests <= hash_requests + 1; + wait for hash_model_time; + if hash_req = '1' then + hash_err <= '1' when hash_model_fail else '0'; + hash_ack <= '1'; + wait until hash_req = '0'; + hash_ack <= '0'; + end if; + end process; + + versal_model_inst: entity work.versal_model + generic map( + actor_name => "versal_model" + ) + port map( + clk => clk, + reset => reset, + hsc_12v => versal_rails_pins.hsc_12v, + hsc_5v => versal_rails_pins.hsc_5v, + boot => versal_boot_pins + ); + end generate; end th; \ No newline at end of file diff --git a/hdl/projects/cosmo_seq/sequencer/sims/versal_model.vhd b/hdl/projects/cosmo_seq/sequencer/sims/versal_model.vhd new file mode 100644 index 00000000..38556b80 --- /dev/null +++ b/hdl/projects/cosmo_seq/sequencer/sims/versal_model.vhd @@ -0,0 +1,100 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; + +use work.sp5_power_pkg.all; +use work.sequencer_io_pkg.all; +use work.versal_model_msg_pkg.all; + +-- Boot-side model of the VP1202. The rails are modelled separately by +-- rail_model instances in the harness; this covers the hotswap pair, whose +-- power-good pins are active low at the FPGA, and the POR_B/DONE handshake. +entity versal_model is + generic ( + actor_name : string := "versal_model"; + -- How long after POR_B releases the device takes to assert DONE. + boot_time : time := 50 us + ); + port ( + clk : in std_logic; + reset : in std_logic; + + hsc_12v : view power_rail_at_reg; + hsc_5v : view cascade_power_rail_at_reg; + + boot : view versal_boot_at_versal + ); +end entity; + +architecture model of versal_model is + + signal boot_allowed : boolean := true; + signal force_error_out : boolean := false; + signal done_int : std_logic := '0'; + signal error_out_int : std_logic := '0'; + +begin + + msg_handler : process + variable self : actor_t; + variable msg_type : msg_type_t; + variable request_msg : msg_t; + begin + self := new_actor(actor_name); + loop + receive(net, self, request_msg); + msg_type := message_type(request_msg); + if msg_type = fail_boot_msg then + info("versal_model: boot will not complete"); + boot_allowed <= false; + elsif msg_type = allow_boot_msg then + info("versal_model: boot allowed"); + boot_allowed <= true; + elsif msg_type = assert_error_out_msg then + info("versal_model: asserting ERROR_OUT"); + force_error_out <= true; + elsif msg_type = clear_error_out_msg then + info("versal_model: clearing ERROR_OUT"); + force_error_out <= false; + else + unexpected_msg_type(msg_type); + end if; + end loop; + wait; + end process; + + -- Hotswap power good is active low at the FPGA pins, and the 5V hotswap + -- cascades off the 12V one. + hsc_12v.pg <= not hsc_12v.enable; + hsc_5v.pg <= not hsc_12v.enable; + + -- DONE comes up some time after POR_B releases, and drops again whenever + -- the device is put back into reset. + boot_sm : process + begin + wait until rising_edge(boot.por_b); + if boot_allowed then + wait for boot_time; + if boot.por_b = '1' then + done_int <= '1'; + end if; + end if; + wait until falling_edge(boot.por_b); + done_int <= '0'; + end process; + + -- The DONE and ERROR_OUT buffers are only enabled by the sequencer once it + -- is looking at them; before that the pins read as their idle level. + boot.done <= done_int when boot.err_done_buff_en = '1' else '0'; + error_out_int <= '1' when force_error_out else '0'; + boot.error_out <= error_out_int when boot.err_done_buff_en = '1' else '0'; + +end model; diff --git a/hdl/projects/cosmo_seq/sequencer/sims/versal_model_msg_pkg.vhd b/hdl/projects/cosmo_seq/sequencer/sims/versal_model_msg_pkg.vhd new file mode 100644 index 00000000..ea75d716 --- /dev/null +++ b/hdl/projects/cosmo_seq/sequencer/sims/versal_model_msg_pkg.vhd @@ -0,0 +1,85 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; + +-- Messages for driving the Versal boot model's failure modes. The rails +-- themselves are separate rail_model instances, so this package only covers +-- what the device does after its rails are up. +package versal_model_msg_pkg is + + constant fail_boot_msg : msg_type_t := new_msg_type("fail_boot"); + constant allow_boot_msg : msg_type_t := new_msg_type("allow_boot"); + constant assert_error_out_msg : msg_type_t := new_msg_type("assert_error_out"); + constant clear_error_out_msg : msg_type_t := new_msg_type("clear_error_out"); + + -- Stop the model from ever asserting DONE, so the sequencer's boot + -- timeout is exercised. + procedure fail_boot ( + signal net : inout network_t; + constant actor : actor_t + ); + + procedure allow_boot ( + signal net : inout network_t; + constant actor : actor_t + ); + + procedure assert_error_out ( + signal net : inout network_t; + constant actor : actor_t + ); + + procedure clear_error_out ( + signal net : inout network_t; + constant actor : actor_t + ); + +end package; + +package body versal_model_msg_pkg is + + procedure fail_boot ( + signal net : inout network_t; + constant actor : actor_t + ) is + variable request_msg : msg_t := new_msg(fail_boot_msg); + begin + send(net, actor, request_msg); + end; + + procedure allow_boot ( + signal net : inout network_t; + constant actor : actor_t + ) is + variable request_msg : msg_t := new_msg(allow_boot_msg); + begin + send(net, actor, request_msg); + end; + + procedure assert_error_out ( + signal net : inout network_t; + constant actor : actor_t + ) is + variable request_msg : msg_t := new_msg(assert_error_out_msg); + begin + send(net, actor, request_msg); + end; + + procedure clear_error_out ( + signal net : inout network_t; + constant actor : actor_t + ) is + variable request_msg : msg_t := new_msg(clear_error_out_msg); + begin + send(net, actor, request_msg); + end; + +end package body; diff --git a/hdl/projects/cosmo_seq/sequencer/sims/versal_seq_sim_tb.vhd b/hdl/projects/cosmo_seq/sequencer/sims/versal_seq_sim_tb.vhd new file mode 100644 index 00000000..9cd62be7 --- /dev/null +++ b/hdl/projects/cosmo_seq/sequencer/sims/versal_seq_sim_tb.vhd @@ -0,0 +1,271 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.numeric_std_unsigned.all; + +library vunit_lib; + context vunit_lib.com_context; + context vunit_lib.vunit_context; + context vunit_lib.vc_context; + +use work.sequencer_regs_pkg.all; +use work.sequencer_io_pkg.all; +use work.sp5_seq_sim_pkg.all; +use work.rail_model_msg_pkg.all; +use work.versal_model_msg_pkg; + +entity versal_seq_sim_tb is + generic ( + runner_cfg : string + ); +end entity; + +architecture tb of versal_seq_sim_tb is +begin + + th: entity work.sp5_seq_sim_th generic map (NIC_KIND => NIC_VERSAL); + + bench: process + alias reset is << signal th.reset : std_logic >>; + alias versal_held_in_reset is << signal th.versal_held_in_reset : std_logic >>; + alias versal_pcie_pins is << signal th.versal_pcie_pins : versal_pcie_t >>; + alias sp5_versal_cha_perst_l is << signal th.sp5_nic_perst_l : std_logic >>; + alias sp5_versal_chb_perst_l is << signal th.sp5_nic_chb_perst_l : std_logic >>; + alias flash_owned_by_seq is << signal th.flash_owned_by_seq : std_logic >>; + alias hash_req is << signal th.hash_req : std_logic >>; + alias hash_model_fail is << signal th.hash_model_fail : boolean >>; + alias hash_requests is << signal th.hash_requests : natural >>; + alias versal_boot_pins is << signal th.versal_boot_pins : versal_boot_t >>; + constant versal_actor : actor_t := find("versal_model"); + variable read_data : std_logic_vector(31 downto 0); + variable versal_state : nic_api_status_nic_sm; + variable rails_pg : rails_type; + variable readbacks : versal_readbacks_type; + variable status : status_type; + variable version : board_version_type; + begin + test_runner_setup(runner, runner_cfg); + wait until reset = '0'; + wait for 500 ns; + + while test_suite loop + if run("normal_power_up") then + power_up_to_nic_done(net); + + -- Every rail should read back good once we are up. + read_bus(net, bus_handle, + To_StdLogicVector(RAIL_PGS_OFFSET, bus_handle.p_address_length), + read_data); + rails_pg := unpack(read_data); + check_equal(rails_pg.versal_v0p8_vccint, '1', + "Expected the Versal VCCINT rail to read power good"); + check_equal(rails_pg.versal_v3p3, '1', + "Expected the Versal 3V3 rail to read power good"); + + -- and the status register should agree. + read_bus(net, bus_handle, + To_StdLogicVector(STATUS_OFFSET, bus_handle.p_address_length), + read_data); + status := unpack(read_data); + check_equal(status.nicpwrok, '1', + "Expected versalpwrok in the status register"); + check_equal(status.nicdone, '1', + "Expected versaldone in the status register"); + + elsif run("boot_mode_is_strapped") then + -- The default boot mode is QSPI32; check it reaches the pins. + power_up_to_nic_done(net); + read_bus(net, bus_handle, + To_StdLogicVector(VERSAL_READBACKS_OFFSET, bus_handle.p_address_length), + read_data); + readbacks := unpack(read_data); + check_equal(readbacks.mode, std_logic_vector'(x"2"), + "Expected MODE[3:0] to be strapped to QSPI32"); + check_equal(readbacks.mode_buffer_en_l, '0', + "Expected the MODE buffer to be enabled"); + + elsif run("pcie_resets_follow_their_slots") then + -- Neither channel leaves reset before the Versal is booted, + -- and afterwards each follows only its own slot's power + -- enable from the SP5 hotplug controller. + check_equal(versal_pcie_pins.cha.perst_l, '0', + "Expected channel A PERST asserted before boot"); + check_equal(versal_pcie_pins.chb.perst_l, '0', + "Expected channel B PERST asserted before boot"); + power_up_to_nic_done(net); + wait for 100 ns; + check_equal(versal_pcie_pins.cha.perst_l, '1', + "Expected channel A PERST released once booted"); + check_equal(versal_pcie_pins.chb.perst_l, '1', + "Expected channel B PERST released once booted"); + + sp5_versal_chb_perst_l <= '0'; + wait for 100 ns; + check_equal(versal_pcie_pins.cha.perst_l, '1', + "Expected channel A PERST unaffected by slot B"); + check_equal(versal_pcie_pins.chb.perst_l, '0', + "Expected channel B PERST to follow slot B"); + read_bus(net, bus_handle, + To_StdLogicVector(VERSAL_READBACKS_OFFSET, bus_handle.p_address_length), + read_data); + readbacks := unpack(read_data); + check_equal(readbacks.sp5_cha_perst_l, '1', "Expected slot A readback high"); + check_equal(readbacks.sp5_chb_perst_l, '0', "Expected slot B readback low"); + check_equal(readbacks.chb_perst_l, '0', "Expected channel B PERST readback low"); + + sp5_versal_chb_perst_l <= '1'; + sp5_versal_cha_perst_l <= '0'; + wait for 100 ns; + check_equal(versal_pcie_pins.cha.perst_l, '0', + "Expected channel A PERST to follow slot A"); + check_equal(versal_pcie_pins.chb.perst_l, '1', + "Expected channel B PERST unaffected by slot A"); + sp5_versal_cha_perst_l <= '1'; + + elsif run("flash_mux_interlock") then + -- The SP may only take the boot flash while POR_B is asserted; + -- after boot the sequencer holds it for the SP5 instead. + check_equal(versal_held_in_reset, '1', + "Expected the Versal to be held in reset before power up"); + check_equal(flash_owned_by_seq, '0', + "Expected no sequencer claim on the flash before power up"); + power_up_to_nic_done(net); + check_equal(versal_held_in_reset, '0', + "Expected the SP's request to be denied once the Versal has booted"); + check_equal(flash_owned_by_seq, '1', + "Expected the sequencer to hold the flash for the SP5 after boot"); + + elsif run("image_is_measured_before_boot") then + -- The sequencer asks the hash engine for a measurement with + -- the rails up, POR_B still held and the flash on the FPGA + -- side, and only releases POR_B once it has an answer. + write_bus(net, bus_handle, + To_StdLogicVector(POWER_CTRL_OFFSET, bus_handle.p_address_length), + POWER_CTRL_A0_EN_MASK); + wait until hash_req = '1' for 50 ms; + check_equal(hash_req, '1', "Expected a measurement request"); + check_equal(versal_boot_pins.por_b, '0', "Expected POR_B held during the measurement"); + check_equal(flash_owned_by_seq, '1', "Expected the flash on the FPGA side during the measurement"); + read_bus(net, bus_handle, + To_StdLogicVector(NIC_API_STATUS_OFFSET, bus_handle.p_address_length), + read_data); + versal_state := encode(read_data(7 downto 0)); + check_equal(versal_state = MEASURING, true, "Expected the MEASURING api state"); + wait until hash_req = '0'; + -- POR_B releases only after the flash has gone back to the Versal + wait until versal_boot_pins.por_b = '1' for 50 ms; + check_equal(versal_boot_pins.por_b, '1', "Expected POR_B released after the measurement"); + check_equal(flash_owned_by_seq, '0', "Expected the flash back with the Versal for boot"); + poll_for_nic_state(net, DONE); + check_equal(flash_owned_by_seq, '1', "Expected the flash back on the FPGA side after boot"); + read_bus(net, bus_handle, + To_StdLogicVector(STATUS_OFFSET, bus_handle.p_address_length), + read_data); + status := unpack(read_data); + check_equal(status.nic_hash_done, '1', "Expected versal_hash_done"); + check_equal(status.nic_hash_err, '0', "Expected no versal_hash_err"); + check_equal(hash_requests, 1, "Expected exactly one measurement"); + + elsif run("failed_measurement_is_recorded_and_boot_continues") then + hash_model_fail <= true; + power_up_to_nic_done(net); + read_bus(net, bus_handle, + To_StdLogicVector(STATUS_OFFSET, bus_handle.p_address_length), + read_data); + status := unpack(read_data); + check_equal(status.nic_hash_done, '0', "Expected no versal_hash_done"); + check_equal(status.nic_hash_err, '1', "Expected versal_hash_err"); + read_bus(net, bus_handle, + To_StdLogicVector(IFR_OFFSET, bus_handle.p_address_length), + read_data); + check_equal((read_data and IFR_NIC_HASH_ERR_MASK) /= (read_data'range => '0'), true, + "Expected the versal_hash_err interrupt flag"); + + elsif run("measurement_can_be_skipped") then + write_bus(net, bus_handle, + To_StdLogicVector(VERSAL_BOOT_CTRL_OFFSET, bus_handle.p_address_length), + VERSAL_BOOT_CTRL_MODE_MASK and x"00000002"); + power_up_to_nic_done(net); + check_equal(hash_requests, 0, "Expected no measurement request"); + read_bus(net, bus_handle, + To_StdLogicVector(STATUS_OFFSET, bus_handle.p_address_length), + read_data); + status := unpack(read_data); + check_equal(status.nic_hash_done, '0', "Expected no versal_hash_done"); + check_equal(status.nic_hash_err, '0', "Expected no versal_hash_err"); + + elsif run("boot_timeout_does_not_drop_power") then + versal_model_msg_pkg.fail_boot(net, versal_actor); + write_bus(net, bus_handle, + To_StdLogicVector(POWER_CTRL_OFFSET, bus_handle.p_address_length), + POWER_CTRL_A0_EN_MASK); + poll_for_seq_state(net, DONE); + poll_for_nic_state(net, BOOTING); + wait for 500 us; + + -- A boot that never completes must not look like a power fault. + read_bus(net, bus_handle, + To_StdLogicVector(NIC_API_STATUS_OFFSET, bus_handle.p_address_length), + read_data); + versal_state := encode(read_data(7 downto 0)); + check_equal(versal_state = BOOTING, true, + "Expected the Versal sequencer to stay in BOOTING"); + read_bus(net, bus_handle, + To_StdLogicVector(IFR_OFFSET, bus_handle.p_address_length), read_data); + check_equal((read_data and IFR_NICMAPO_MASK) = x"00000000", true, + "A failed boot must not raise a Versal MAPO"); + versal_model_msg_pkg.allow_boot(net, versal_actor); + + elsif run("error_out_raises_an_irq") then + power_up_to_nic_done(net); + versal_model_msg_pkg.assert_error_out(net, versal_actor); + wait for 100 us; + read_bus(net, bus_handle, + To_StdLogicVector(IFR_OFFSET, bus_handle.p_address_length), read_data); + check_equal((read_data and IFR_VERSAL_ERROR_OUT_MASK) /= x"00000000", true, + "Expected ERROR_OUT to set its interrupt flag"); + versal_model_msg_pkg.clear_error_out(net, versal_actor); + + elsif run("board_version_readback") then + read_bus(net, bus_handle, + To_StdLogicVector(BOARD_VERSION_OFFSET, bus_handle.p_address_length), + read_data); + version := unpack(read_data); + check_equal(version.version_id, std_logic_vector'("01"), + "Expected the board version straps to read back"); + + elsif run("mapo_fault_v1p1_sp5") then + test_mapo_fault_injection(net, find("grpb_v1p1_sp5"), "v1p1_sp5"); + elsif run("mapo_fault_vddcr_soc") then + test_mapo_fault_injection(net, find("grpc_vddcr_soc"), "vddcr_soc"); + + elsif run("versal_mapo_fault_v0p8_vccint") then + test_versal_rail_mapo_fault_injection(net, find("versal_v0p8_vccint"), "v0p8_vccint"); + elsif run("versal_mapo_fault_v0p88") then + test_versal_rail_mapo_fault_injection(net, find("versal_v0p88"), "v0p88"); + elsif run("versal_mapo_fault_v1p1") then + test_versal_rail_mapo_fault_injection(net, find("versal_v1p1"), "v1p1"); + elsif run("versal_mapo_fault_v1p4") then + test_versal_rail_mapo_fault_injection(net, find("versal_v1p4"), "v1p4"); + elsif run("versal_mapo_fault_v1p5") then + test_versal_rail_mapo_fault_injection(net, find("versal_v1p5"), "v1p5"); + elsif run("versal_mapo_fault_v1p5_avccaux") then + test_versal_rail_mapo_fault_injection(net, find("versal_v1p5_avccaux"), "v1p5_avccaux"); + elsif run("versal_mapo_fault_v1p8") then + test_versal_rail_mapo_fault_injection(net, find("versal_v1p8"), "v1p8"); + elsif run("versal_mapo_fault_v3p3") then + test_versal_rail_mapo_fault_injection(net, find("versal_v3p3"), "v3p3"); + end if; + end loop; + + wait for 2 us; + test_runner_cleanup(runner); + wait; + end process; + + test_runner_watchdog(runner, 20 ms); +end tb; diff --git a/hdl/projects/cosmo_seq/sequencer/sp5_sequencer.vhd b/hdl/projects/cosmo_seq/sequencer/sp5_sequencer.vhd index cac7b964..364e0195 100644 --- a/hdl/projects/cosmo_seq/sequencer/sp5_sequencer.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sp5_sequencer.vhd @@ -18,9 +18,17 @@ use work.sequencer_regs_pkg.all; -- and provides registers for out outputs that are destined for off-chip -- devices. It provides no tri-state logic so tri-stating must be done -- at the chip top if needed/desired. +-- +-- The SP5 half is the same on every board. The NIC half is picked by +-- NIC_KIND: cosmo's T6 or metro's Versal. Both NICs' pin records are ports so +-- that the entity is the same on both boards; a board ties the records for +-- the NIC it does not have to the *_absent constants in sequencer_io_pkg +-- and leaves that NIC's outputs open. The register map is the union of both +-- (see sequencer_regs.rdl), so the registers for the absent NIC read zero. entity sp5_sequencer is generic ( - CNTS_P_MS: integer + CNTS_P_MS: integer; + NIC_KIND : nic_kind_t := NIC_T6 ); port ( clk : in std_logic; @@ -44,22 +52,40 @@ entity sp5_sequencer is group_c_pins : view group_c_power_at_fpga; -- SP5 sequencing I/O sp5_seq_pins : view sp5_seq_at_fpga; - -- nic supplies - nic_rails_pins : view nic_power_at_fpga; - -- nic sequencing I/O - nic_seq_pins: view nic_seq_at_fpga; allow_backplane_pcie_clk : out std_logic; - nic_dbg_pins : view t6_debug_seq_ss; + -- What the NIC sequencer offers the debug header + nic_dbg_pins : view nic_debug_seq_ss; -- regulator alerts reg_alert_l_pins : view power_alert_at_fpga; - sp5_t6_perst_l : in std_logic; - sp5_t6_faulted : out std_logic; + -- From SP5 hotplug: the NIC slot's PERST, which follows the slot power + -- enable. A Versal has two slots; the T6 uses only the first. + sp5_nic_perst_l : in std_logic; + sp5_nic_chb_perst_l : in std_logic := '1'; + sp5_nic_faulted : out std_logic; ignition_mux_sel : out std_logic; ignition_creset : out std_logic; + -- T6 NIC (NIC_KIND = NIC_T6) + nic_rails_pins : view nic_power_at_fpga; + nic_seq_pins: view nic_seq_at_fpga; + -- Versal NIC (NIC_KIND = NIC_VERSAL) + versal_rails_pins : view versal_power_at_fpga; + versal_boot_pins : view versal_boot_at_fpga; + versal_pcie_pins : view versal_pcie_at_fpga; + -- True while POR_B is held low and will stay so, gating the SP's + -- request for the Versal boot-flash mux + versal_held_in_reset : out std_logic; + -- True while the sequencer wants the boot flash on the FPGA side + flash_owned_by_seq : out std_logic; + -- Hash engine hardware request for the pre-boot measurement + hash_req : out std_logic; + hash_ack : in std_logic := '0'; + hash_err : in std_logic := '0'; + -- FPGA1_VERSION_ID board straps, reported straight through + version_id : in std_logic_vector(1 downto 0) := "00" ); end entity; @@ -74,6 +100,9 @@ architecture rtl of sp5_sequencer is signal rail_masks : rails_type; signal fans_power_ok : std_logic; + signal nic_power_ok : std_logic; + -- a0_ok is an output port and this architecture needs to read it + signal a0_ok_int : std_logic; -- We have the following states for the sequencing block -- power ok means we're up and happy -- power idle means we're down and idle @@ -86,25 +115,37 @@ architecture rtl of sp5_sequencer is signal group_b : group_b_power_t; signal group_c : group_c_power_t; signal sp5_seq : sp5_seq_pins_t; - signal nic_rails : nic_power_t; - signal nic_seq : nic_seq_pins_t; signal early_power_ctrl : early_power_ctrl_type; signal early_power_rdbks : early_power_rdbks_type; signal sp5_readbacks : sp5_readbacks_type; - signal nic_readbacks : nic_readbacks_type; - signal nic_overrides : nic_overrides_type; signal debug_enables : debug_enables_type; + signal nic_overrides : nic_overrides_type; signal smerr_assert : std_logic; signal a0_faulted : std_logic; signal nic_faulted : std_logic; signal reg_alert_l : seq_power_alert_pins_t; signal sp5_seq_test_mask : sp5_seq_test_mask_type; - + -- T6 side + signal nic_rails : nic_power_t; + signal nic_seq : nic_seq_pins_t; + signal nic_readbacks : nic_readbacks_type; + -- Versal side + signal versal_rails : versal_power_t; + signal versal_boot : versal_boot_t; + signal versal_pcie : versal_pcie_t; + signal versal_readbacks : versal_readbacks_type; + signal versal_overrides : versal_overrides_type; + signal versal_boot_ctrl : versal_boot_ctrl_type; + signal versal_hash_done : std_logic; + signal versal_hash_failed : std_logic; + signal board_version : board_version_type; begin - sp5_t6_faulted <= nic_faulted; + sp5_nic_faulted <= nic_faulted; + a0_ok <= a0_ok_int; + board_version.version_id <= version_id; sync: entity work.seq_sync port map( @@ -116,8 +157,6 @@ begin group_b_pins => group_b_pins, group_c_pins => group_c_pins, sp5_seq_pins => sp5_seq_pins, - nic_rails_pins => nic_rails_pins, - nic_seq_pins => nic_seq_pins, early_power => early_power, ddr_bulk => ddr_bulk, rail_masks => rail_masks, @@ -126,8 +165,6 @@ begin group_b => group_b, group_c => group_c, sp5_seq => sp5_seq, - nic_rails => nic_rails, - nic_seq => nic_seq, reg_alert_l_pins => reg_alert_l_pins, reg_alert_l => reg_alert_l ); @@ -157,6 +194,17 @@ begin rails_pg_rdbk => rails_pg_rdbk, sp5_readbacks => sp5_readbacks, nic_readbacks => nic_readbacks, + fans_power_ok => fans_power_ok, + a0_ok => a0_ok_int, + nic_power_ok => nic_power_ok, + nic_done => versal_boot.done, + versal_error_out => versal_boot.error_out, + nic_hash_done => versal_hash_done, + nic_hash_err => versal_hash_failed, + versal_readbacks => versal_readbacks, + versal_overrides => versal_overrides, + versal_boot_ctrl => versal_boot_ctrl, + board_version => board_version, ignition_mux_sel => ignition_mux_sel, ignition_creset => ignition_creset, irq_l_out => irq_l_out, @@ -177,17 +225,7 @@ begin early_power_rdbks.fan_hsc_central_pg <= early_power.fan_central_hsc_pg; early_power_rdbks.fan_hsc_east_pg <= early_power.fan_east_hsc_pg; early_power_rdbks.fan_fail <= not early_power.fan_fail; - -- rails - -- NIC rails all cascade enabled from 12V rail - rails_en_rdbk.v0p96_nic_vdd_a0hp <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.v1p1_nic_a0hp <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.v1p4_nic_a0hp <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.v3p3_nic_a0hp <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.v1p2_nic_enet_a0hp <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.v1p2_nic_pcie_a0hp <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.v1p5_nic_a0hp <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.nic_hsc_5v <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.nic_hsc_12v <= nic_rails.nic_hsc_12v.enable; + -- SP5 rails rails_en_rdbk.vddcr_soc <= group_c.vddcr_soc.enable; rails_en_rdbk.vddcr_cpu0 <= group_c.vddcr_cpu0.enable; rails_en_rdbk.vddcr_cpu1 <= group_c.vddcr_cpu1.enable; @@ -198,16 +236,6 @@ begin rails_en_rdbk.v1p5_rtc <= group_a.pwr_v1p5_rtc.enable; rails_en_rdbk.ghijkl_hsc <= ddr_bulk.ghijkl_hsc.enable; rails_en_rdbk.abcdef_hsc <= ddr_bulk.abcdef_hsc.enable; - -- PG readbacks - rails_pg_rdbk.v0p96_nic_vdd_a0hp <= nic_rails.v0p96_nic_vdd_a0hp.pg; - rails_pg_rdbk.v1p1_nic_a0hp <= nic_rails.v1p1_nic_a0hp.pg; - rails_pg_rdbk.v1p4_nic_a0hp <= nic_rails.v1p4_nic_a0hp.pg; - rails_pg_rdbk.v3p3_nic_a0hp <= nic_rails.v3p3_nic_a0hp.pg; - rails_pg_rdbk.v1p2_nic_enet_a0hp <= nic_rails.v1p2_nic_enet_a0hp.pg; - rails_pg_rdbk.v1p2_nic_pcie_a0hp <= nic_rails.v1p2_nic_pcie_a0hp.pg; - rails_pg_rdbk.v1p5_nic_a0hp <= nic_rails.v1p5_nic_a0hp.pg; - rails_pg_rdbk.nic_hsc_5v <= nic_rails.nic_hsc_5v.pg; - rails_pg_rdbk.nic_hsc_12v <= nic_rails.nic_hsc_12v.pg; rails_pg_rdbk.vddcr_soc <= group_c.vddcr_soc.pg; rails_pg_rdbk.vddcr_cpu0 <= group_c.vddcr_cpu0.pg; rails_pg_rdbk.vddcr_cpu1 <= group_c.vddcr_cpu1.pg; @@ -230,17 +258,6 @@ begin sp5_readbacks.thermtrip_l <= sp5_seq.thermtrip_l; sp5_readbacks.smerr_l <= sp5_seq.smerr_l; sp5_readbacks.pwrgd_out <= sp5_seq.pwrgd_out; - -- NIC sequencing readbacks - nic_readbacks.nic_pcie_clk_buff_oe_l <= nic_seq.nic_pcie_clk_buff_oe_l; - nic_readbacks.flash_wp_l <= nic_seq.flash_wp_l; - nic_readbacks.eeprom_wp_buffer_oe_l <= nic_seq.eeprom_wp_buffer_oe_l; - nic_readbacks.eeprom_wp_l <= nic_seq.eeprom_wp_l; - nic_readbacks.sp5_mfg_mode_l <= nic_seq.sp5_mfg_mode_l; - nic_readbacks.nic_mfg_mode_l <= nic_seq.nic_mfg_mode_l; - nic_readbacks.ext_rst_l <= nic_seq.ext_rst_l; - nic_readbacks.perst_l <= nic_seq.perst_l; - nic_readbacks.cld_rst_l <= nic_seq.cld_rst_l; - nic_readbacks.sp5_perst_l <= sp5_t6_perst_l; fans_power_ok <= early_power_rdbks.fan_hsc_west_pg and early_power_rdbks.fan_hsc_central_pg and @@ -256,7 +273,7 @@ begin downstream_idle => nic_idle, therm_trip => therm_trip, smerr_assert => smerr_assert, - a0_ok => a0_ok, + a0_ok => a0_ok_int, a0_idle => a0_idle, a0_faulted => a0_faulted, sw_enable => power_ctrl.a0_en, @@ -270,25 +287,222 @@ begin sp5_seq_pins => sp5_seq ); - nic_seq_inst: entity work.nic_seq - generic map( - CNTS_P_MS => CNTS_P_MS - ) - port map( - clk => clk, - reset => reset, - nic_idle => nic_idle, - nic_faulted => nic_faulted, - sw_enable => power_ctrl.a0_en, - raw_state => nic_raw_status, - api_state => nic_api_status, - upstream_ok => a0_ok, - nic_overrides_reg => nic_overrides, - debug_enables => debug_enables, - sp5_t6_perst_l => sp5_t6_perst_l, - nic_dbg_pins => nic_dbg_pins, - nic_rails => nic_rails, - nic_seq_pins => nic_seq - ); + ----------------------------------------------------------------------- + -- T6 NIC + ----------------------------------------------------------------------- + t6: if NIC_KIND = NIC_T6 generate + t6_sync_inst: entity work.t6_sync + port map( + clk => clk, + reset => reset, + nic_rails_pins => nic_rails_pins, + nic_seq_pins => nic_seq_pins, + nic_rails => nic_rails, + nic_seq => nic_seq + ); + + nic_seq_inst: entity work.nic_seq + generic map( + CNTS_P_MS => CNTS_P_MS + ) + port map( + clk => clk, + reset => reset, + nic_idle => nic_idle, + nic_faulted => nic_faulted, + sw_enable => power_ctrl.a0_en, + raw_state => nic_raw_status, + api_state => nic_api_status, + upstream_ok => a0_ok_int, + nic_overrides_reg => nic_overrides, + debug_enables => debug_enables, + sp5_t6_perst_l => sp5_nic_perst_l, + nic_dbg_pins => nic_dbg_pins, + nic_rails => nic_rails, + nic_seq_pins => nic_seq + ); + + -- NIC rails all cascade enabled from 12V rail + rails_en_rdbk.v0p96_nic_vdd_a0hp <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.v1p1_nic_a0hp <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.v1p4_nic_a0hp <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.v3p3_nic_a0hp <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.v1p2_nic_enet_a0hp <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.v1p2_nic_pcie_a0hp <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.v1p5_nic_a0hp <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.nic_hsc_5v <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.nic_hsc_12v <= nic_rails.nic_hsc_12v.enable; + rails_pg_rdbk.v0p96_nic_vdd_a0hp <= nic_rails.v0p96_nic_vdd_a0hp.pg; + rails_pg_rdbk.v1p1_nic_a0hp <= nic_rails.v1p1_nic_a0hp.pg; + rails_pg_rdbk.v1p4_nic_a0hp <= nic_rails.v1p4_nic_a0hp.pg; + rails_pg_rdbk.v3p3_nic_a0hp <= nic_rails.v3p3_nic_a0hp.pg; + rails_pg_rdbk.v1p2_nic_enet_a0hp <= nic_rails.v1p2_nic_enet_a0hp.pg; + rails_pg_rdbk.v1p2_nic_pcie_a0hp <= nic_rails.v1p2_nic_pcie_a0hp.pg; + rails_pg_rdbk.v1p5_nic_a0hp <= nic_rails.v1p5_nic_a0hp.pg; + rails_pg_rdbk.nic_hsc_5v <= nic_rails.nic_hsc_5v.pg; + rails_pg_rdbk.nic_hsc_12v <= nic_rails.nic_hsc_12v.pg; + -- the Versal bits are not this board's + rails_en_rdbk.versal_v0p8_vccint <= '0'; + rails_en_rdbk.versal_v0p88 <= '0'; + rails_en_rdbk.versal_v0p92_avcc <= '0'; + rails_en_rdbk.versal_v1p1 <= '0'; + rails_en_rdbk.versal_v1p2_avtt <= '0'; + rails_en_rdbk.versal_v1p4 <= '0'; + rails_en_rdbk.versal_v1p5 <= '0'; + rails_en_rdbk.versal_v1p5_avccaux <= '0'; + rails_en_rdbk.versal_v1p8 <= '0'; + rails_en_rdbk.versal_v3p3 <= '0'; + rails_pg_rdbk.versal_v0p8_vccint <= '0'; + rails_pg_rdbk.versal_v0p88 <= '0'; + rails_pg_rdbk.versal_v0p92_avcc <= '0'; + rails_pg_rdbk.versal_v1p1 <= '0'; + rails_pg_rdbk.versal_v1p2_avtt <= '0'; + rails_pg_rdbk.versal_v1p4 <= '0'; + rails_pg_rdbk.versal_v1p5 <= '0'; + rails_pg_rdbk.versal_v1p5_avccaux <= '0'; + rails_pg_rdbk.versal_v1p8 <= '0'; + rails_pg_rdbk.versal_v3p3 <= '0'; + + -- NIC sequencing readbacks + nic_readbacks.nic_pcie_clk_buff_oe_l <= nic_seq.nic_pcie_clk_buff_oe_l; + nic_readbacks.flash_wp_l <= nic_seq.flash_wp_l; + nic_readbacks.eeprom_wp_buffer_oe_l <= nic_seq.eeprom_wp_buffer_oe_l; + nic_readbacks.eeprom_wp_l <= nic_seq.eeprom_wp_l; + nic_readbacks.sp5_mfg_mode_l <= nic_seq.sp5_mfg_mode_l; + nic_readbacks.nic_mfg_mode_l <= nic_seq.nic_mfg_mode_l; + nic_readbacks.ext_rst_l <= nic_seq.ext_rst_l; + nic_readbacks.perst_l <= nic_seq.perst_l; + nic_readbacks.cld_rst_l <= nic_seq.cld_rst_l; + nic_readbacks.sp5_perst_l <= sp5_nic_perst_l; + nic_power_ok <= '1' when is_power_good(nic_rails) else '0'; + + -- Nothing Versal on this board + versal_rails <= versal_power_absent; + versal_boot <= versal_boot_absent; + versal_pcie <= versal_pcie_absent; + versal_readbacks <= (mode => (others => '0'), others => '0'); + versal_hash_done <= '0'; + versal_hash_failed <= '0'; + versal_held_in_reset <= '0'; + flash_owned_by_seq <= '0'; + hash_req <= '0'; + end generate; + + ----------------------------------------------------------------------- + -- Versal NIC + ----------------------------------------------------------------------- + versal: if NIC_KIND = NIC_VERSAL generate + versal_sync_inst: entity work.versal_sync + port map( + clk => clk, + reset => reset, + versal_rails_pins => versal_rails_pins, + versal_boot_pins => versal_boot_pins, + versal_pcie_pins => versal_pcie_pins, + rail_masks => rail_masks, + versal_rails => versal_rails, + versal_boot => versal_boot, + versal_pcie => versal_pcie + ); + + versal_seq_inst: entity work.versal_seq + generic map( + CNTS_P_MS => CNTS_P_MS + ) + port map( + clk => clk, + reset => reset, + versal_idle => nic_idle, + versal_faulted => nic_faulted, + sw_enable => power_ctrl.a0_en, + raw_state => nic_raw_status, + api_state => nic_api_status, + upstream_ok => a0_ok_int, + versal_overrides_reg => versal_overrides, + nic_test_mapo => nic_overrides.nic_test_mapo, + boot_ctrl => versal_boot_ctrl, + debug_enables => debug_enables, + sp5_versal_cha_perst_l => sp5_nic_perst_l, + sp5_versal_chb_perst_l => sp5_nic_chb_perst_l, + versal_dbg_pins => nic_dbg_pins, + versal_held_in_reset => versal_held_in_reset, + flash_owned_by_seq => flash_owned_by_seq, + hash_req => hash_req, + hash_ack => hash_ack, + hash_err => hash_err, + hash_done => versal_hash_done, + hash_failed => versal_hash_failed, + versal_rails => versal_rails, + versal_boot => versal_boot, + versal_pcie => versal_pcie + ); + + -- Each Versal rail has its own enable, staged by versal_seq. The two + -- transceiver rails cascade, so they read back their group's enable. + -- The hotswaps share the nic_hsc_* bits with the T6. + rails_en_rdbk.versal_v3p3 <= versal_rails.v3p3.enable; + rails_en_rdbk.versal_v1p8 <= versal_rails.v1p8.enable; + rails_en_rdbk.versal_v1p5_avccaux <= versal_rails.v1p5_avccaux.enable; + rails_en_rdbk.versal_v1p5 <= versal_rails.v1p5.enable; + rails_en_rdbk.versal_v1p4 <= versal_rails.v1p4.enable; + rails_en_rdbk.versal_v1p2_avtt <= versal_rails.v1p5.enable; + rails_en_rdbk.versal_v1p1 <= versal_rails.v1p1.enable; + rails_en_rdbk.versal_v0p92_avcc <= versal_rails.v0p88.enable; + rails_en_rdbk.versal_v0p88 <= versal_rails.v0p88.enable; + rails_en_rdbk.versal_v0p8_vccint <= versal_rails.v0p8_vccint.enable; + rails_en_rdbk.nic_hsc_5v <= versal_rails.hsc_12v.enable; + rails_en_rdbk.nic_hsc_12v <= versal_rails.hsc_12v.enable; + rails_pg_rdbk.versal_v3p3 <= versal_rails.v3p3.pg; + rails_pg_rdbk.versal_v1p8 <= versal_rails.v1p8.pg; + rails_pg_rdbk.versal_v1p5_avccaux <= versal_rails.v1p5_avccaux.pg; + rails_pg_rdbk.versal_v1p5 <= versal_rails.v1p5.pg; + rails_pg_rdbk.versal_v1p4 <= versal_rails.v1p4.pg; + rails_pg_rdbk.versal_v1p2_avtt <= versal_rails.v1p2_avtt.pg; + rails_pg_rdbk.versal_v1p1 <= versal_rails.v1p1.pg; + rails_pg_rdbk.versal_v0p92_avcc <= versal_rails.v0p92_avcc.pg; + rails_pg_rdbk.versal_v0p88 <= versal_rails.v0p88.pg; + rails_pg_rdbk.versal_v0p8_vccint <= versal_rails.v0p8_vccint.pg; + rails_pg_rdbk.nic_hsc_5v <= versal_rails.hsc_5v.pg; + rails_pg_rdbk.nic_hsc_12v <= versal_rails.hsc_12v.pg; + -- the T6 bits are not this board's + rails_en_rdbk.v0p96_nic_vdd_a0hp <= '0'; + rails_en_rdbk.v1p1_nic_a0hp <= '0'; + rails_en_rdbk.v1p4_nic_a0hp <= '0'; + rails_en_rdbk.v3p3_nic_a0hp <= '0'; + rails_en_rdbk.v1p2_nic_enet_a0hp <= '0'; + rails_en_rdbk.v1p2_nic_pcie_a0hp <= '0'; + rails_en_rdbk.v1p5_nic_a0hp <= '0'; + rails_pg_rdbk.v0p96_nic_vdd_a0hp <= '0'; + rails_pg_rdbk.v1p1_nic_a0hp <= '0'; + rails_pg_rdbk.v1p4_nic_a0hp <= '0'; + rails_pg_rdbk.v3p3_nic_a0hp <= '0'; + rails_pg_rdbk.v1p2_nic_enet_a0hp <= '0'; + rails_pg_rdbk.v1p2_nic_pcie_a0hp <= '0'; + rails_pg_rdbk.v1p5_nic_a0hp <= '0'; + + -- Versal sequencing readbacks + versal_readbacks.por_b <= versal_boot.por_b; + versal_readbacks.mode <= versal_boot.mode; + versal_readbacks.mode_buffer_en_l <= versal_boot.mode_buffer_en_l; + versal_readbacks.err_done_buff_en <= versal_boot.err_done_buff_en; + versal_readbacks.done <= versal_boot.done; + versal_readbacks.error_out <= versal_boot.error_out; + versal_readbacks.cha_perst_l <= versal_pcie.cha.perst_l; + versal_readbacks.cha_prsnt_l <= versal_pcie.cha.prsnt_l; + versal_readbacks.cha_pwren_l <= versal_pcie.cha.pwren_l; + versal_readbacks.cha_clk_buff_oe_l <= versal_pcie.cha.clk_buff_oe_l; + versal_readbacks.chb_perst_l <= versal_pcie.chb.perst_l; + versal_readbacks.chb_prsnt_l <= versal_pcie.chb.prsnt_l; + versal_readbacks.chb_pwren_l <= versal_pcie.chb.pwren_l; + versal_readbacks.chb_clk_buff_oe_l <= versal_pcie.chb.clk_buff_oe_l; + versal_readbacks.sp5_cha_perst_l <= sp5_nic_perst_l; + versal_readbacks.sp5_chb_perst_l <= sp5_nic_chb_perst_l; + nic_power_ok <= '1' when is_power_good(versal_rails) else '0'; + + -- Nothing T6 on this board + nic_rails <= nic_power_absent; + nic_seq <= nic_seq_pins_absent; + nic_readbacks <= (others => '0'); + end generate; -end rtl; \ No newline at end of file +end rtl; diff --git a/hdl/projects/cosmo_seq/sequencer/t6_sync.vhd b/hdl/projects/cosmo_seq/sequencer/t6_sync.vhd new file mode 100644 index 00000000..ef7aecfe --- /dev/null +++ b/hdl/projects/cosmo_seq/sequencer/t6_sync.vhd @@ -0,0 +1,117 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +use work.sp5_power_pkg.all; +use work.sequencer_io_pkg.all; +use work.sequencer_regs_pkg.all; + +-- Synchronises cosmo's T6 NIC pins: the rail power goods and the reset, +-- write-protect and manufacturing-mode handshake lines. The SP5-side pins +-- are done in seq_sync. +entity t6_sync is + port ( + clk : in std_logic; + reset : in std_logic; + + -- pins (unsync'd) interface + nic_rails_pins : view nic_power_at_fpga; + nic_seq_pins: view nic_seq_at_fpga; + -- internal, synchronized interfaces + nic_rails : view nic_power_at_reg; + nic_seq: view nic_seq_at_nic + ); +end entity; + +architecture rtl of t6_sync is + signal nic_sync_5v_hsc_pg_l : std_logic; + signal nic_sync_12v_hsc_pg_l : std_logic; +begin + + -- nic rails sync stuff + nic_rails_pins.nic_hsc_12v.enable <= nic_rails.nic_hsc_12v.enable; + v1p5_nic_a0hp: entity work.meta_sync + port map( + async_input => nic_rails_pins.v1p5_nic_a0hp.pg, + clk => clk, + sycnd_output => nic_rails.v1p5_nic_a0hp.pg + ); + v1p2_nic_pcie_a0hp: entity work.meta_sync + port map( + async_input => nic_rails_pins.v1p2_nic_pcie_a0hp.pg, + clk => clk, + sycnd_output => nic_rails.v1p2_nic_pcie_a0hp.pg + ); + v1p2_nic_enet_a0hp: entity work.meta_sync + port map( + async_input => nic_rails_pins.v1p2_nic_enet_a0hp.pg, + clk => clk, + sycnd_output => nic_rails.v1p2_nic_enet_a0hp.pg + ); + v3p3_nic_a0hp: entity work.meta_sync + port map( + async_input => nic_rails_pins.v3p3_nic_a0hp.pg, + clk => clk, + sycnd_output => nic_rails.v3p3_nic_a0hp.pg + ); + v1p1_nic_a0hp: entity work.meta_sync + port map( + async_input => nic_rails_pins.v1p1_nic_a0hp.pg, + clk => clk, + sycnd_output => nic_rails.v1p1_nic_a0hp.pg + ); + v1p4_nic_a0hp: entity work.meta_sync + port map( + async_input => nic_rails_pins.v1p4_nic_a0hp.pg, + clk => clk, + sycnd_output => nic_rails.v1p4_nic_a0hp.pg + ); + v0p96_nic_vdd_a0hp: entity work.meta_sync + port map( + async_input => nic_rails_pins.v0p96_nic_vdd_a0hp.pg, + clk => clk, + sycnd_output => nic_rails.v0p96_nic_vdd_a0hp.pg + ); + nic_hsc_12v: entity work.meta_sync + port map( + async_input => nic_rails_pins.nic_hsc_12v.pg, + clk => clk, + sycnd_output => nic_sync_12v_hsc_pg_l + ); + nic_hsc_5v: entity work.meta_sync + port map( + async_input => nic_rails_pins.nic_hsc_5v.pg, + clk => clk, + sycnd_output => nic_sync_5v_hsc_pg_l + ); + + -- HSC's are actually pg_l signals, so invert them here + nic_rails.nic_hsc_5v.pg <= not nic_sync_5v_hsc_pg_l; + nic_rails.nic_hsc_12v.pg <= not nic_sync_12v_hsc_pg_l; + -- nic sync-related stuff + nic_seq_pins.cld_rst_l <= nic_seq.cld_rst_l; + nic_seq_pins.perst_l <= nic_seq.perst_l; + nic_seq_pins.eeprom_wp_l <= nic_seq.eeprom_wp_l; + nic_seq_pins.eeprom_wp_buffer_oe_l <= nic_seq.eeprom_wp_buffer_oe_l; + nic_seq_pins.flash_wp_l <= nic_seq.flash_wp_l; + nic_seq_pins.nic_mfg_mode_l <= nic_seq.nic_mfg_mode_l; + nic_seq_pins.nic_pcie_clk_buff_oe_l <= nic_seq.nic_pcie_clk_buff_oe_l; + ext_rst_l_sync: entity work.meta_sync + port map( + async_input => nic_seq_pins.ext_rst_l, + clk => clk, + sycnd_output => nic_seq.ext_rst_l + ); + sp5_mfg_mode_l_sync: entity work.meta_sync + port map( + async_input => nic_seq_pins.sp5_mfg_mode_l, + clk => clk, + sycnd_output => nic_seq.sp5_mfg_mode_l + ); + + +end rtl; diff --git a/hdl/projects/cosmo_seq/sequencer/versal_seq.vhd b/hdl/projects/cosmo_seq/sequencer/versal_seq.vhd new file mode 100644 index 00000000..a85f9396 --- /dev/null +++ b/hdl/projects/cosmo_seq/sequencer/versal_seq.vhd @@ -0,0 +1,440 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +use work.sp5_power_pkg.all; +use work.sequencer_io_pkg.all; +use work.sequencer_regs_pkg.all; + +-- A0HP sequencing and boot supervision for the AMD Versal Premium VP1202 that +-- serves as Metro's NIC. This is the metro-specific counterpart to cosmo_seq's +-- nic_seq: same shape and the same register-driven override story, but a +-- Versal wants a staged rail bring-up followed by a strapped boot rather than +-- the T6's cld_rst/perst dance. +-- +-- Rail grouping and timing below follow the Versal power-up requirements in +-- three stages -- core (VCCINT), then auxiliary (VCCAUX), then I/O (VCCO) -- +-- with the transceiver rails riding along in whichever group enables them. +-- The exact inter-group delays are conservative placeholders and MUST be +-- confirmed against the VP1202 datasheet before hardware bring-up; they are +-- gathered into the constants below so that is a one-line change. +entity versal_seq is + generic( + CNTS_P_MS: integer + ); + port( + clk : in std_logic; + reset : in std_logic; + + sw_enable : in std_logic; + upstream_ok : in std_logic; + versal_idle : out std_logic; + versal_faulted : out std_logic; + debug_enables : in debug_enables_type; + versal_overrides_reg : in versal_overrides_type; + -- Self-clearing test MAPO, shared with the T6 flavour's register + nic_test_mapo : in std_logic; + boot_ctrl : in versal_boot_ctrl_type; + + raw_state : out nic_raw_status_type; + api_state : out nic_api_status_type; + + versal_dbg_pins : view nic_debug_seq_ss; + + -- From SP5 hotplug, one per PCIe channel. Each follows its slot's + -- power enable exactly, as the T6's did on cosmo: perst_l <= power_en. + -- The Versal is one device behind two slots, so power itself is not + -- per channel; only the resets are. + sp5_versal_cha_perst_l : in std_logic; + sp5_versal_chb_perst_l : in std_logic; + + -- True while the Versal is held in reset and not about to be let out + -- of it, i.e. while it is safe for the SP to take the Versal's boot + -- flash away from it through the mux. + versal_held_in_reset : out std_logic; + -- True while the sequencer itself wants the boot flash on the FPGA + -- side of the mux: for the pre-boot measurement, and once the Versal + -- has booted so the SP5 can reach the flash over eSPI. + flash_owned_by_seq : out std_logic; + + -- Hash engine hardware request, see hash_engine_top. Held until + -- acknowledged; hash_err is valid with the acknowledge. + hash_req : out std_logic; + hash_ack : in std_logic; + hash_err : in std_logic; + -- Outcome of the measurement for the current or last boot + hash_done : out std_logic; + hash_failed : out std_logic; + + versal_rails : view versal_power_at_fpga; + versal_boot : view versal_boot_at_fpga; + versal_pcie : view versal_pcie_at_fpga + ); +end entity; + +architecture rtl of versal_seq is + constant ONE_MS : integer := 1 * CNTS_P_MS; + constant TEN_MS : integer := 10 * ONE_MS; + constant TWENTY_MS : integer := 20 * ONE_MS; + -- How long the rails must be stable before POR_B is released. + constant RAIL_SETTLE_MS : integer := 20 * ONE_MS; + -- How long MODE[3:0] must be stable before POR_B is released. + constant MODE_SETUP_MS : integer := 1 * ONE_MS; + -- How long to wait for DONE before calling the boot failed. Versal images + -- are large and come off QSPI, so this is generous on purpose. + constant DONE_TIMEOUT_MS : integer := 5000 * ONE_MS; + + -- Rail groups. The Versal wants VCCINT up before VCCAUX before VCCO. + -- v0p92_avcc and v1p2_avtt have no enable of their own; they cascade, so + -- they are checked in the group whose enable brings them up. + function core_group_good(rails : versal_power_t) return boolean is + begin + return (rails.v0p8_vccint.pg and rails.v0p88.pg and + rails.v0p92_avcc.pg) = '1'; + end function; + + function aux_group_good(rails : versal_power_t) return boolean is + begin + return (rails.v1p5.pg and rails.v1p5_avccaux.pg and rails.v1p4.pg and + rails.v1p1.pg and rails.v1p2_avtt.pg) = '1'; + end function; + + function io_group_good(rails : versal_power_t) return boolean is + begin + return (rails.v1p8.pg and rails.v3p3.pg) = '1'; + end function; + + type versal_r_t is record + state : nic_raw_status_hw_sm; + enable_last : std_logic; + enable_pend : std_logic; + cnts : unsigned(31 downto 0); + cha_perst_l_last : std_logic; + chb_perst_l_last : std_logic; + hsc_en : std_logic; + core_en : std_logic; + aux_en : std_logic; + io_en : std_logic; + por_b : std_logic; + mode : std_logic_vector(3 downto 0); + mode_buffer_en_l : std_logic; + err_done_buff_en : std_logic; + clk_buff_oe_l : std_logic; + rails_expected : std_logic; + faulted : std_logic; + boot_failed : std_logic; + hash_req : std_logic; + hash_done : std_logic; + hash_failed : std_logic; + end record; + + constant versal_r_reset : versal_r_t := ( + state => IDLE, + enable_last => '0', + enable_pend => '0', + cnts => (others => '0'), + cha_perst_l_last => '0', + chb_perst_l_last => '0', + hsc_en => '0', + core_en => '0', + aux_en => '0', + io_en => '0', + por_b => '0', + mode => (others => '0'), + mode_buffer_en_l => '1', + err_done_buff_en => '0', + clk_buff_oe_l => '1', + rails_expected => '0', + faulted => '0', + boot_failed => '0', + hash_req => '0', + hash_done => '0', + hash_failed => '0' + ); + signal r, rin : versal_r_t; + + -- PCIe resets follow the SP5's slot power enables once the Versal has + -- booted, exactly as the T6's did on cosmo, one per channel. + signal cha_perst_l : std_logic; + signal chb_perst_l : std_logic; + signal final_outs : versal_overrides_type; + +begin + + raw_state.hw_sm <= r.state; + versal_idle <= '1' when r.state = IDLE else '0'; + versal_faulted <= r.faulted; + -- POR_B low means the Versal is held off its boot flash, so the SP may + -- safely steal the QSPI mux. Not during MODE_STRAP though: that is the + -- last stop before POR_B releases, and a grant given there would still be + -- in force when it does. + versal_held_in_reset <= '1' when r.por_b = '0' and r.state /= MODE_STRAP else '0'; + -- The sequencer's own claims on the flash: measuring it, and after boot, + -- when the Versal has finished with it and the SP5 gets it over eSPI. + flash_owned_by_seq <= '1' when r.state = HASH_IMAGE or r.state = HASH_RELEASE or + r.state = DONE else '0'; + hash_req <= r.hash_req; + hash_done <= r.hash_done; + hash_failed <= r.hash_failed; + + -- Debug header taps, on header pins 5..0 in this order + versal_dbg_pins.rails_en <= r.io_en; + versal_dbg_pins.rails_pg <= '1' when is_power_good(versal_rails) else '0'; + versal_dbg_pins.taps(5) <= final_outs.por_b; + versal_dbg_pins.taps(4) <= final_outs.cha_perst_l; + versal_dbg_pins.taps(3) <= versal_boot.done; + versal_dbg_pins.taps(2) <= versal_boot.error_out; + versal_dbg_pins.taps(1) <= r.mode(0); + versal_dbg_pins.taps(0) <= final_outs.chb_perst_l; + + api_state_proc: process(clk, reset) + begin + if reset then + api_state.nic_sm <= IDLE; + elsif rising_edge(clk) then + case r.state is + when IDLE => + api_state.nic_sm <= IDLE; + when HSC_EN | CORE_EN | AUX_EN | IO_EN => + api_state.nic_sm <= ENABLE_POWER; + when RAILS_SETTLE | MODE_STRAP => + api_state.nic_sm <= NIC_RESET; + when HASH_IMAGE | HASH_RELEASE => + api_state.nic_sm <= MEASURING; + when POR_RELEASE | WAIT_DONE => + api_state.nic_sm <= BOOTING; + when DONE => + api_state.nic_sm <= DONE; + -- the T6 states; this NIC never has them + when others => null; + end case; + end if; + end process; + + versal_sm: process(all) + variable v : versal_r_t; + variable rails_faulted : std_logic; + begin + v := r; + v.cha_perst_l_last := sp5_versal_cha_perst_l; + v.chb_perst_l_last := sp5_versal_chb_perst_l; + + -- Once we expect the rails to be up, any of them dropping is a fault. + rails_faulted := '1' when r.rails_expected = '1' and + (not is_power_good(versal_rails)) else '0'; + + v.enable_last := sw_enable; + if (sw_enable and not r.enable_last) = '1' or + (r.faulted = '1' and r.cha_perst_l_last = '0' and sp5_versal_cha_perst_l = '1') or + (r.faulted = '1' and r.chb_perst_l_last = '0' and sp5_versal_chb_perst_l = '1') then + -- Same two re-enable paths cosmo's nic_seq has: software toggling + -- the enable, or -- after a MAPO, where the SP5 owns slot power -- + -- the SP5 de-asserting PERST for a fresh attempt. Either slot + -- coming back is enough; there is only the one device to bring up. + v.enable_pend := '1'; + v.faulted := '0'; + v.boot_failed := '0'; + end if; + + case r.state is + when IDLE => + v.hsc_en := '0'; + v.core_en := '0'; + v.aux_en := '0'; + v.io_en := '0'; + v.por_b := '0'; + v.mode_buffer_en_l := '1'; + v.err_done_buff_en := '0'; + v.clk_buff_oe_l := '1'; + v.rails_expected := '0'; + v.hash_req := '0'; + v.cnts := (others => '0'); + if r.enable_pend and upstream_ok then + v.state := HSC_EN; + v.enable_pend := '0'; + v.hash_done := '0'; + v.hash_failed := '0'; + end if; + + when HSC_EN => + -- Nothing downstream reports a valid power good until the + -- hotswaps are on, so this stage waits on them alone. + v.hsc_en := '1'; + v.cnts := (others => '0'); + if (versal_rails.hsc_12v.pg and versal_rails.hsc_5v.pg) = '1' then + v.state := CORE_EN; + end if; + + when CORE_EN => + v.core_en := '1'; + v.cnts := (others => '0'); + if core_group_good(versal_rails) then + v.state := AUX_EN; + end if; + + when AUX_EN => + v.aux_en := '1'; + v.cnts := (others => '0'); + if aux_group_good(versal_rails) then + v.state := IO_EN; + end if; + + when IO_EN => + v.io_en := '1'; + v.cnts := (others => '0'); + if io_group_good(versal_rails) then + v.state := RAILS_SETTLE; + -- Every rail is up now, so hold the whole tree to account. + v.rails_expected := '1'; + end if; + + when RAILS_SETTLE => + v.cnts := r.cnts + 1; + if r.cnts = RAIL_SETTLE_MS then + -- Latch the boot mode here so a register write mid-boot + -- cannot move the straps out from under the Versal. + v.mode := boot_ctrl.mode; + v.cnts := (others => '0'); + if boot_ctrl.hash_image = '1' then + v.state := HASH_IMAGE; + else + v.state := MODE_STRAP; + end if; + end if; + + -- Measure the boot image while the Versal is still in POR and the + -- flash is ours. The hash engine owns the timing: a large image + -- takes seconds, and software can abort a run that is going + -- nowhere, which comes back as an error here. Either way the + -- Versal boots; whether a bad measurement matters is for the SP. + when HASH_IMAGE => + v.hash_req := '1'; + if hash_ack = '1' then + v.hash_req := '0'; + v.hash_done := not hash_err; + v.hash_failed := hash_err; + v.state := HASH_RELEASE; + end if; + + when HASH_RELEASE => + -- Let the handshake finish before anything else can start one + if hash_ack = '0' then + v.state := MODE_STRAP; + end if; + + when MODE_STRAP => + v.mode_buffer_en_l := '0'; + v.err_done_buff_en := '1'; + v.cnts := r.cnts + 1; + if r.cnts = MODE_SETUP_MS then + v.state := POR_RELEASE; + v.cnts := (others => '0'); + end if; + + when POR_RELEASE => + v.por_b := '1'; + v.clk_buff_oe_l := '0'; + v.state := WAIT_DONE; + v.cnts := (others => '0'); + + when WAIT_DONE => + v.cnts := r.cnts + 1; + if versal_boot.done = '1' then + v.state := DONE; + v.cnts := (others => '0'); + elsif versal_boot.error_out = '1' or r.cnts = DONE_TIMEOUT_MS then + -- A boot failure is not a power fault: the rails are fine + -- and there is a device to talk to over JTAG, so stay here + -- and let software decide rather than dropping power. + v.boot_failed := '1'; + v.cnts := r.cnts; + end if; + + when DONE => + if sw_enable = '0' then + v.state := IDLE; + end if; + -- the T6 states; this NIC never has them + when others => null; + end case; + + -- MAPO handling, monitored in every non-IDLE state. A measurement + -- in flight is simply abandoned: the request drops in IDLE and the + -- engine's acknowledge, whenever it comes, is ignored there. + if r.state /= IDLE then + if rails_faulted = '1' or upstream_ok = '0' or + nic_test_mapo = '1' then + v.faulted := '1'; + v.state := IDLE; + v.rails_expected := '0'; + end if; + end if; + + rin <= v; + end process; + + -- PCIe resets follow the SP5 hotplug slot power once the Versal is up. + cha_perst_l <= '1' when r.state = DONE and sp5_versal_cha_perst_l = '1' and + debug_enables.force_nic_reset = '0' else '0'; + chb_perst_l <= '1' when r.state = DONE and sp5_versal_chb_perst_l = '1' and + debug_enables.force_nic_reset = '0' else '0'; + + reg_proc: process(clk, reset) + begin + if reset then + r <= versal_r_reset; + elsif rising_edge(clk) then + r <= rin; + end if; + end process; + + -- Register and mux the Versal outputs, letting the debug registers take + -- them over wholesale when asked. + out_reg: process(clk, reset) + begin + if reset then + final_outs <= (others => '0'); + final_outs.mode_buffer_en_l <= '1'; + final_outs.cha_clk_buff_oe_l <= '1'; + final_outs.chb_clk_buff_oe_l <= '1'; + elsif rising_edge(clk) then + if debug_enables.nic_override then + final_outs <= versal_overrides_reg; + else + final_outs.por_b <= r.por_b and not debug_enables.force_nic_reset; + final_outs.mode_buffer_en_l <= r.mode_buffer_en_l; + final_outs.err_done_buff_en <= r.err_done_buff_en; + final_outs.cha_perst_l <= cha_perst_l; + final_outs.chb_perst_l <= chb_perst_l; + final_outs.cha_clk_buff_oe_l <= r.clk_buff_oe_l; + final_outs.chb_clk_buff_oe_l <= r.clk_buff_oe_l; + end if; + end if; + end process; + + versal_boot.por_b <= final_outs.por_b; + versal_boot.mode <= r.mode; + versal_boot.mode_buffer_en_l <= final_outs.mode_buffer_en_l; + versal_boot.err_done_buff_en <= final_outs.err_done_buff_en; + + versal_pcie.cha.perst_l <= final_outs.cha_perst_l; + versal_pcie.cha.clk_buff_oe_l <= final_outs.cha_clk_buff_oe_l; + versal_pcie.chb.perst_l <= final_outs.chb_perst_l; + versal_pcie.chb.clk_buff_oe_l <= final_outs.chb_clk_buff_oe_l; + + -- One enable per rail, staged by the state machine above. + versal_rails.hsc_12v.enable <= r.hsc_en; + versal_rails.v0p8_vccint.enable <= r.core_en; + versal_rails.v0p88.enable <= r.core_en; + versal_rails.v1p5.enable <= r.aux_en; + versal_rails.v1p5_avccaux.enable <= r.aux_en; + versal_rails.v1p4.enable <= r.aux_en; + versal_rails.v1p1.enable <= r.aux_en; + versal_rails.v1p8.enable <= r.io_en; + versal_rails.v3p3.enable <= r.io_en; + +end rtl; diff --git a/hdl/projects/cosmo_seq/sequencer/versal_sync.vhd b/hdl/projects/cosmo_seq/sequencer/versal_sync.vhd new file mode 100644 index 00000000..d935a1d1 --- /dev/null +++ b/hdl/projects/cosmo_seq/sequencer/versal_sync.vhd @@ -0,0 +1,213 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +use work.sp5_power_pkg.all; +use work.sequencer_io_pkg.all; +use work.sequencer_regs_pkg.all; + +-- Synchronises metro's Versal NIC pins: the rail power goods, the boot +-- status pins and the two PCIe channels' slot inputs. The SP5-side pins are +-- done in seq_sync. +entity versal_sync is + port ( + clk : in std_logic; + reset : in std_logic; + + -- pins (unsync'd) interface + versal_rails_pins : view versal_power_at_fpga; + versal_boot_pins : view versal_boot_at_fpga; + versal_pcie_pins : view versal_pcie_at_fpga; + -- internal, synchronized interfaces + rail_masks : in rails_type; + versal_rails : view versal_power_at_reg; + versal_boot : view versal_boot_at_versal; + versal_pcie : view versal_pcie_at_nic + ); +end entity; + +architecture rtl of versal_sync is + signal versal_sync_5v_hsc_pg_l : std_logic; + signal versal_sync_12v_hsc_pg_l : std_logic; + signal versal_v3p3_pg_raw : std_logic; + signal versal_v1p8_pg_raw : std_logic; + signal versal_v1p5_pg_raw : std_logic; + signal versal_v1p5_avccaux_pg_raw : std_logic; + signal versal_v1p4_pg_raw : std_logic; + signal versal_v1p1_pg_raw : std_logic; + signal versal_v0p88_pg_raw : std_logic; + signal versal_v0p8_vccint_pg_raw : std_logic; + signal versal_v0p92_avcc_pg_raw : std_logic; + signal versal_v1p2_avtt_pg_raw : std_logic; +begin + + -- Versal rails sync stuff + versal_rails_pins.v3p3.enable <= versal_rails.v3p3.enable; + versal_rails_pins.v1p8.enable <= versal_rails.v1p8.enable; + versal_rails_pins.v1p5.enable <= versal_rails.v1p5.enable; + versal_rails_pins.v1p5_avccaux.enable <= versal_rails.v1p5_avccaux.enable; + versal_rails_pins.v1p4.enable <= versal_rails.v1p4.enable; + versal_rails_pins.v1p1.enable <= versal_rails.v1p1.enable; + versal_rails_pins.v0p88.enable <= versal_rails.v0p88.enable; + versal_rails_pins.v0p8_vccint.enable <= versal_rails.v0p8_vccint.enable; + versal_rails_pins.hsc_12v.enable <= versal_rails.hsc_12v.enable; + + versal_v3p3_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v3p3.pg, + clk => clk, + sycnd_output => versal_v3p3_pg_raw + ); + versal_rails.v3p3.pg <= versal_v3p3_pg_raw when rail_masks.versal_v3p3 = '0' else '0'; + + versal_v1p8_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v1p8.pg, + clk => clk, + sycnd_output => versal_v1p8_pg_raw + ); + versal_rails.v1p8.pg <= versal_v1p8_pg_raw when rail_masks.versal_v1p8 = '0' else '0'; + + versal_v1p5_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v1p5.pg, + clk => clk, + sycnd_output => versal_v1p5_pg_raw + ); + versal_rails.v1p5.pg <= versal_v1p5_pg_raw when rail_masks.versal_v1p5 = '0' else '0'; + + versal_v1p5_avccaux_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v1p5_avccaux.pg, + clk => clk, + sycnd_output => versal_v1p5_avccaux_pg_raw + ); + versal_rails.v1p5_avccaux.pg <= versal_v1p5_avccaux_pg_raw when rail_masks.versal_v1p5_avccaux = '0' else '0'; + + versal_v1p4_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v1p4.pg, + clk => clk, + sycnd_output => versal_v1p4_pg_raw + ); + versal_rails.v1p4.pg <= versal_v1p4_pg_raw when rail_masks.versal_v1p4 = '0' else '0'; + + versal_v1p1_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v1p1.pg, + clk => clk, + sycnd_output => versal_v1p1_pg_raw + ); + versal_rails.v1p1.pg <= versal_v1p1_pg_raw when rail_masks.versal_v1p1 = '0' else '0'; + + versal_v0p88_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v0p88.pg, + clk => clk, + sycnd_output => versal_v0p88_pg_raw + ); + versal_rails.v0p88.pg <= versal_v0p88_pg_raw when rail_masks.versal_v0p88 = '0' else '0'; + + versal_v0p8_vccint_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v0p8_vccint.pg, + clk => clk, + sycnd_output => versal_v0p8_vccint_pg_raw + ); + versal_rails.v0p8_vccint.pg <= versal_v0p8_vccint_pg_raw when rail_masks.versal_v0p8_vccint = '0' else '0'; + + versal_v0p92_avcc_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v0p92_avcc.pg, + clk => clk, + sycnd_output => versal_v0p92_avcc_pg_raw + ); + versal_rails.v0p92_avcc.pg <= versal_v0p92_avcc_pg_raw when rail_masks.versal_v0p92_avcc = '0' else '0'; + + versal_v1p2_avtt_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v1p2_avtt.pg, + clk => clk, + sycnd_output => versal_v1p2_avtt_pg_raw + ); + versal_rails.v1p2_avtt.pg <= versal_v1p2_avtt_pg_raw when rail_masks.versal_v1p2_avtt = '0' else '0'; + + versal_hsc_12v_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.hsc_12v.pg, + clk => clk, + sycnd_output => versal_sync_12v_hsc_pg_l + ); + + versal_hsc_5v_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.hsc_5v.pg, + clk => clk, + sycnd_output => versal_sync_5v_hsc_pg_l + ); + + -- The hotswap controllers report power good active low, so invert here and + -- let everything above this line treat power good as active high. + versal_rails.hsc_12v.pg <= (not versal_sync_12v_hsc_pg_l) when rail_masks.nic_hsc_12v = '0' else '0'; + versal_rails.hsc_5v.pg <= (not versal_sync_5v_hsc_pg_l) when rail_masks.nic_hsc_5v = '0' else '0'; + + -- Versal boot straps and status + versal_boot_pins.mode <= versal_boot.mode; + versal_boot_pins.mode_buffer_en_l <= versal_boot.mode_buffer_en_l; + versal_boot_pins.por_b <= versal_boot.por_b; + versal_boot_pins.err_done_buff_en <= versal_boot.err_done_buff_en; + + versal_done_sync: entity work.meta_sync + port map( + async_input => versal_boot_pins.done, + clk => clk, + sycnd_output => versal_boot.done + ); + + versal_error_out_sync: entity work.meta_sync + port map( + async_input => versal_boot_pins.error_out, + clk => clk, + sycnd_output => versal_boot.error_out + ); + + -- Versal PCIe channels + versal_pcie_pins.cha.perst_l <= versal_pcie.cha.perst_l; + versal_pcie_pins.cha.clk_buff_oe_l <= versal_pcie.cha.clk_buff_oe_l; + versal_pcie_pins.chb.perst_l <= versal_pcie.chb.perst_l; + versal_pcie_pins.chb.clk_buff_oe_l <= versal_pcie.chb.clk_buff_oe_l; + + versal_cha_prsnt_l_sync: entity work.meta_sync + port map( + async_input => versal_pcie_pins.cha.prsnt_l, + clk => clk, + sycnd_output => versal_pcie.cha.prsnt_l + ); + + versal_cha_pwren_l_sync: entity work.meta_sync + port map( + async_input => versal_pcie_pins.cha.pwren_l, + clk => clk, + sycnd_output => versal_pcie.cha.pwren_l + ); + + versal_chb_prsnt_l_sync: entity work.meta_sync + port map( + async_input => versal_pcie_pins.chb.prsnt_l, + clk => clk, + sycnd_output => versal_pcie.chb.prsnt_l + ); + + versal_chb_pwren_l_sync: entity work.meta_sync + port map( + async_input => versal_pcie_pins.chb.pwren_l, + clk => clk, + sycnd_output => versal_pcie.chb.pwren_l + ); + + +end rtl; diff --git a/hdl/projects/cosmo_seq/sims/cosmo_seq_top_tb.vhd b/hdl/projects/cosmo_seq/sims/cosmo_seq_top_tb.vhd new file mode 100644 index 00000000..b685b112 --- /dev/null +++ b/hdl/projects/cosmo_seq/sims/cosmo_seq_top_tb.vhd @@ -0,0 +1,34 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; + +library vunit_lib; + context vunit_lib.vunit_context; + +-- Analysis-only smoke test, as metro_seq has: it does not drive the top level, +-- its job is to make a simulator compile the whole design tree, which the +-- synthesis-only build rules do not do on their own. Behavioural coverage +-- lives in the per-subsystem testbenches. +entity cosmo_seq_top_tb is + generic ( + runner_cfg : string + ); +end entity; + +architecture tb of cosmo_seq_top_tb is +begin + bench: process + begin + test_runner_setup(runner, runner_cfg); + while test_suite loop + if run("analyses") then + info("cosmo_seq_top and its dependencies analysed"); + end if; + end loop; + test_runner_cleanup(runner); + wait; + end process; +end tb; From 43fd19eff5b5d943e57e0f9ccebdf97af9cacb6e Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Wed, 16 Sep 2026 12:24:13 -0400 Subject: [PATCH 10/12] spi_nor: bus enable at the pin flops; versal flash mux control stops muxing pins versal_flash_subsystem parked the Versal flash bus by muxing cs_n, sclk and the lane enables after spi_nor_top's output flops. Those flops are dedicated IOB copies, and the timing window on the flash pins is derived from them being in the IOBs; a LUT between flop and pin pulls them out, and the shared-sequencer rebuild landed that path 15ps outside its 5ns budget. spi_nor_top gets a bus_enable input instead, applied at the D input of the pin flops in spi_clk_gen, spi_link and spi_txn_mgr: cs_n high, sclk low, lanes released within a clock of it dropping, with the flops still in the IOBs. The eSPI flash wrapper passes it through, and the mux control block now drives only that and the mux pins. --- hdl/ip/vhd/spi_nor_controller/link/spi_clk_gen.vhd | 6 +++++- hdl/ip/vhd/spi_nor_controller/link/spi_link.vhd | 9 ++++++++- hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd | 10 ++++++++++ hdl/ip/vhd/spi_nor_controller/spi_txn/spi_txn_mgr.vhd | 4 +++- .../sp5_espi_flash_subsystem.vhd | 4 ++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/hdl/ip/vhd/spi_nor_controller/link/spi_clk_gen.vhd b/hdl/ip/vhd/spi_nor_controller/link/spi_clk_gen.vhd index e11fa293..e8b3e1a9 100644 --- a/hdl/ip/vhd/spi_nor_controller/link/spi_clk_gen.vhd +++ b/hdl/ip/vhd/spi_nor_controller/link/spi_clk_gen.vhd @@ -13,6 +13,10 @@ entity spi_clk_gen is reset : in std_logic; divisor : in unsigned(15 downto 0); enable : in boolean; + -- Low parks the pin copy at '0' regardless of what the internal clock + -- is doing, so a design that shares the flash with another master can + -- let go of the bus without a mux between the IOB flop and the pin. + bus_enable : in std_logic := '1'; -- For internal consumers: edge detection, phase counting, debug sclk : out std_logic; -- A second copy of the same flop, for the pin and nothing else. Both @@ -101,7 +105,7 @@ begin nxt_sclk := not sclk_int; end if; sclk_int <= nxt_sclk; -- assign value to output - sclk_pin <= nxt_sclk; -- IOB-resident duplicate, same edge + sclk_pin <= nxt_sclk and bus_enable; -- IOB-resident duplicate, same edge else sclk_int <= '0'; sclk_pin <= '0'; diff --git a/hdl/ip/vhd/spi_nor_controller/link/spi_link.vhd b/hdl/ip/vhd/spi_nor_controller/link/spi_link.vhd index c03edd0a..ebda533c 100644 --- a/hdl/ip/vhd/spi_nor_controller/link/spi_link.vhd +++ b/hdl/ip/vhd/spi_nor_controller/link/spi_link.vhd @@ -33,6 +33,8 @@ entity spi_link is -- Lanes to stop driving early, ahead of a controller-to-flash -- turnaround, so the two ends are never enabled at once release_lanes : in std_logic_vector(3 downto 0); + -- Low parks the pins: sclk low, lanes released. See spi_nor_top. + bus_enable : in std_logic := '1'; rx_byte : out std_logic_vector(7 downto 0); rx_byte_done : out boolean; -- The next byte to shift out. Must be held ready ahead of the reload @@ -170,6 +172,7 @@ begin reset => reset, divisor => divisor, enable => sclk_running, + bus_enable => bus_enable, sclk => sclk_int, sclk_pin => sclk_pin, sclk_fall_now => sclk_fall_now @@ -289,7 +292,11 @@ begin tx_reg <= nxt_tx_reg; io_o <= io_out_bits(nxt_tx_reg, nxt_mode); - io_oe <= oe and not release_lanes; + if bus_enable = '1' then + io_oe <= oe and not release_lanes; + else + io_oe <= (others => '0'); + end if; end if; end process; diff --git a/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd b/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd index 593c33a6..bf2aa9a6 100644 --- a/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd +++ b/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd @@ -35,6 +35,14 @@ entity spi_nor_top is io_o : out std_logic_vector(3 downto 0); io_oe : out std_logic_vector(3 downto 0); sp5_owns_flash : out std_logic; + -- Low parks the flash pins (cs_n high, sclk low, lanes released) + -- at the IOB flops themselves, for a design where the flash is + -- reached through a mux shared with another master. The controller + -- keeps running; only the pins are held off, so a grant lost mid + -- transaction stops driving within a clock. Muxing after the flops + -- instead would cost them their IOB placement and the read timing + -- window that depends on it. + bus_enable : in std_logic := '1'; -- eSPI transaction interface. -- FIFO the command, which is simply an 32bit address -- as the first word and the transaction length as the @@ -146,6 +154,7 @@ begin in_rx_phases => in_rx_phases, sclk_running => sclk_running, release_lanes => release_lanes, + bus_enable => bus_enable, rx_byte => link_rx_byte, rx_byte_done => rx_byte_done, tx_byte => link_tx_byte, @@ -178,6 +187,7 @@ begin -- link i/f cs_n => cs_n_internal, cs_n_pin => cs_n, + bus_enable => bus_enable, sclk => sclk_internal, rx_byte_done => rx_byte_done, rx_link_byte => link_rx_byte, diff --git a/hdl/ip/vhd/spi_nor_controller/spi_txn/spi_txn_mgr.vhd b/hdl/ip/vhd/spi_nor_controller/spi_txn/spi_txn_mgr.vhd index aff99f20..62a4671d 100644 --- a/hdl/ip/vhd/spi_nor_controller/spi_txn/spi_txn_mgr.vhd +++ b/hdl/ip/vhd/spi_nor_controller/spi_txn/spi_txn_mgr.vhd @@ -33,6 +33,8 @@ entity spi_txn_mgr is -- Second copy of the cs_n flop, for the pin only, so it can be packed -- into the IOB. Same reasoning as spi_clk_gen's sclk_pin. cs_n_pin : out std_logic; + -- Low parks the pin copy high (deselected). See spi_nor_top. + bus_enable : in std_logic := '1'; sclk : in std_logic; rx_byte_done : in boolean; rx_link_byte : in std_logic_vector(7 downto 0); @@ -445,7 +447,7 @@ begin tx_pre <= next_tx(r, spi_cmd, tx_fifo_data); -- Duplicate of r.csn, driven from the same next-state value so the -- two flops always agree and change on the same edge. - cs_n_pin <= rin.csn; + cs_n_pin <= rin.csn or not bus_enable; end if; end process; diff --git a/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd b/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd index 4c48eb3b..52951b18 100644 --- a/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd +++ b/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd @@ -47,6 +47,9 @@ entity sp5_espi_flash_subsystem is spi_nor_dat : in std_logic_vector(3 downto 0); spi_nor_dat_o : out std_logic_vector(3 downto 0); spi_nor_dat_oe : out std_logic_vector(3 downto 0); + -- Parks the flash pins when low, see spi_nor_top. Only a design that + -- shares the flash through a mux needs to drive it. + spi_nor_bus_enable : in std_logic := '1'; -- spi_nor_top's second flash client port, for the hashing engine. The -- engine sits at the project top rather than in here so that one engine @@ -241,6 +244,7 @@ begin io_o => spi_nor_dat_o, io_oe => spi_nor_dat_oe, sp5_owns_flash => open, + bus_enable => spi_nor_bus_enable, espi_cmd_fifo_rdata => espi_cmd_fifo_rdata, espi_cmd_fifo_rdack => espi_cmd_fifo_rdack, espi_cmd_fifo_rempty => espi_cmd_fifo_rempty, From c83741ab1d6d0266579cbf81a4dd5573e0e2ae90 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Thu, 17 Sep 2026 15:36:41 -0400 Subject: [PATCH 11/12] cosmo_seq: generate the system PLL as sys_pll and export the IP scripts Nothing in board_support is cosmo-specific: the other SP5 boards use the same 50MHz input and the same 125/200MHz tree. The clock wizard bakes its module name into the netlist and into the clock names the timing constraints use, so for another board to share board_support it has to generate the very same IP. The PLL is now sys_pll rather than cosmo_pll, its script and the fmc_pll one are exported so a dependent board's bitstream can source them, and board_support is public. The clock names in cosmo_timing.xdc follow; nothing else referenced them. --- hdl/projects/cosmo_seq/BUCK | 20 +++++++++++++++++++ .../{cosmo_pll.vhd => sys_pll.vhd} | 2 +- .../board_support/board_support_top.vhd | 2 +- hdl/projects/cosmo_seq/cosmo_timing.xdc | 2 +- .../{cosmo_pll_ip.tcl => sys_pll_ip.tcl} | 8 ++++---- 5 files changed, 27 insertions(+), 7 deletions(-) rename hdl/projects/cosmo_seq/black_box_entities/{cosmo_pll.vhd => sys_pll.vhd} (96%) rename hdl/projects/cosmo_seq/xilinx_ip_gen/{cosmo_pll_ip.tcl => sys_pll_ip.tcl} (87%) diff --git a/hdl/projects/cosmo_seq/BUCK b/hdl/projects/cosmo_seq/BUCK index eed4e65a..89c5eed7 100644 --- a/hdl/projects/cosmo_seq/BUCK +++ b/hdl/projects/cosmo_seq/BUCK @@ -44,6 +44,10 @@ vhdl_unit( standard = "2019", ) +# Clocks, resets, the info block and the status LED. Nothing in here is +# cosmo-specific -- the other SP5 boards use the same 50MHz input and the same +# 125/200MHz tree -- so metro depends on this target and on the IP scripts +# exported below rather than carrying copies. vhdl_unit( name = "board_support", srcs = glob(["board_support/*.vhd"], exclude = [ @@ -57,6 +61,22 @@ vhdl_unit( "//hdl/ip/vhd/info:info", ], standard = "2019", + visibility = ["PUBLIC"], +) + +# The clock wizard bakes the module name into the generated netlist and into +# the clock names the timing constraints use, so a board that shares +# board_support has to generate the very same IP: these are the scripts for it. +export_file( + name = "sys_pll_ip_tcl", + src = "xilinx_ip_gen/sys_pll_ip.tcl", + visibility = ["PUBLIC"], +) + +export_file( + name = "fmc_pll_ip_tcl", + src = "xilinx_ip_gen/fmc_pll_ip.tcl", + visibility = ["PUBLIC"], ) diff --git a/hdl/projects/cosmo_seq/black_box_entities/cosmo_pll.vhd b/hdl/projects/cosmo_seq/black_box_entities/sys_pll.vhd similarity index 96% rename from hdl/projects/cosmo_seq/black_box_entities/cosmo_pll.vhd rename to hdl/projects/cosmo_seq/black_box_entities/sys_pll.vhd index 2aad2efc..2bf47bf0 100644 --- a/hdl/projects/cosmo_seq/black_box_entities/cosmo_pll.vhd +++ b/hdl/projects/cosmo_seq/black_box_entities/sys_pll.vhd @@ -9,7 +9,7 @@ use ieee.std_logic_1164.all; use ieee.numeric_std.all; use ieee.numeric_std_unsigned.all; -entity cosmo_pll is +entity sys_pll is port ( clk_50m : in std_logic; clk_125m : out std_logic; diff --git a/hdl/projects/cosmo_seq/board_support/board_support_top.vhd b/hdl/projects/cosmo_seq/board_support/board_support_top.vhd index c865b0fa..de38fc32 100644 --- a/hdl/projects/cosmo_seq/board_support/board_support_top.vhd +++ b/hdl/projects/cosmo_seq/board_support/board_support_top.vhd @@ -75,7 +75,7 @@ begin ); -- Xilinx PLL instantiation - pll: entity work.cosmo_pll + pll: entity work.sys_pll port map ( clk_50m => board_50mhz_clk, clk_125m => clk_125m, diff --git a/hdl/projects/cosmo_seq/cosmo_timing.xdc b/hdl/projects/cosmo_seq/cosmo_timing.xdc index 04dd36d1..d47fc0e6 100644 --- a/hdl/projects/cosmo_seq/cosmo_timing.xdc +++ b/hdl/projects/cosmo_seq/cosmo_timing.xdc @@ -8,7 +8,7 @@ create_clock -name fmc_virt_clk -period 10.000; # The FMC MMCM's output clock is derived from fmc_clk_pin and must stay in # the synchronous group with it and the virtual clock. -set_clock_groups -asynchronous -group [get_clocks -include_generated_clocks {fmc_clk_pin fmc_virt_clk}] -group {clk_125m_cosmo_pll} -group {clk_200m_cosmo_pll} +set_clock_groups -asynchronous -group [get_clocks -include_generated_clocks {fmc_clk_pin fmc_virt_clk}] -group {clk_125m_sys_pll} -group {clk_200m_sys_pll} # ####################### diff --git a/hdl/projects/cosmo_seq/xilinx_ip_gen/cosmo_pll_ip.tcl b/hdl/projects/cosmo_seq/xilinx_ip_gen/sys_pll_ip.tcl similarity index 87% rename from hdl/projects/cosmo_seq/xilinx_ip_gen/cosmo_pll_ip.tcl rename to hdl/projects/cosmo_seq/xilinx_ip_gen/sys_pll_ip.tcl index 900aa60f..a6434825 100644 --- a/hdl/projects/cosmo_seq/xilinx_ip_gen/cosmo_pll_ip.tcl +++ b/hdl/projects/cosmo_seq/xilinx_ip_gen/sys_pll_ip.tcl @@ -1,5 +1,5 @@ # Generate the PLL (copied from tcl console using the IP generator) -create_ip -name clk_wiz -vendor xilinx.com -library ip -version 6.0 -module_name cosmo_pll +create_ip -name clk_wiz -vendor xilinx.com -library ip -version 6.0 -module_name sys_pll set_property -dict [list \ CONFIG.CLKIN1_JITTER_PS {200.0} \ CONFIG.CLKOUT1_JITTER {154.207} \ @@ -11,7 +11,7 @@ set_property -dict [list \ CONFIG.CLKOUT2_USED {true} \ CONFIG.CLK_OUT1_PORT {clk_125m} \ CONFIG.CLK_OUT2_PORT {clk_200m} \ - CONFIG.Component_Name {cosmo_pll} \ + CONFIG.Component_Name {sys_pll} \ CONFIG.MMCM_CLKFBOUT_MULT_F {20.000} \ CONFIG.MMCM_CLKIN1_PERIOD {20.000} \ CONFIG.MMCM_CLKIN2_PERIOD {10.0} \ @@ -20,5 +20,5 @@ set_property -dict [list \ CONFIG.NUM_OUT_CLKS {2} \ CONFIG.PRIMARY_PORT {clk_50m} \ CONFIG.PRIM_IN_FREQ {50} \ -] [get_ips cosmo_pll] -synth_ip [get_ips cosmo_pll] \ No newline at end of file +] [get_ips sys_pll] +synth_ip [get_ips sys_pll] \ No newline at end of file From a212703479ece782c57b83e92e26c88bac8fec33 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Fri, 28 Aug 2026 11:32:57 -0400 Subject: [PATCH 12/12] metro_seq: new sequencer FPGA design for the Metro motherboard Metro's sequencer FPGA (U27) is the same XC7S100-1FGGA484I cosmo uses and plays the same role, so this reuses cosmo_seq's blocks by target dependency rather than forking them: board support and clocking, the SP5 eSPI/SPI-NOR service, the DIMM SPD proxy, the hotplug expanders, the UART routing, the I2C muxes, the debug header, and the whole sequencer including its register map. Metro's own RTL is the top level, its constraints, the register map composition, and the control block for the Versal boot-flash mux. What differs is below the SP5 layer. Metro's NIC is an AMD Versal Premium VP1202, not a Chelsio T6. The shared sp5_sequencer is built with NIC_KIND => NIC_VERSAL, which selects versal_seq in place of nic_seq: - ten rails with an enable each, staged core -> aux -> I/O, instead of one hotswap enable cascading the lot - active-low 12V/5V hotswap power good, inverted at the pin so the records stay active high - MODE[3:0]/POR_B boot straps and DONE/ERROR_OUT monitoring in place of the T6's cld_rst/perst dance - two PCIe channels, each on its own SP5 hotplug slot (channel B on the expander's otherwise unused fifth bank) with its own PERST - the boot image measured by the hash engine before POR_B is released, the digest kept in the engine's HW_DIGEST registers; a failed measurement is recorded and the boot goes ahead - the QSPI boot flash behind a mux: the FPGA holds it while measuring and again once the Versal has booted, so the SP5 can reach it over a second eSPI target on the eSPI1 pins (SAFS reads, writes and erases enabled there) and the SP over the spi_nor registers The two eSPI targets share one hash engine (NUM_FLASHES => 2) and the second is built without the post code buffer, which only eSPI0 sees. The pinout follows cosmo's closely: of the 289 pins used on both boards, 254 carry the same function and no functional group changed bank. The 35 that were repurposed are almost all the Versal landing on pins the T6 held -- its per-rail enables and power goods take cosmo's unnamed nic_pg_* spares and its write-protect pins, and MODE[0], MODE[1] and MODE[3] take cld_rst_l, ext_rst_l and the now-absent MCIO alert. Pin assignments were recovered from schematic 913-0000028 rev 1 sheets 57-59 and reviewed against the rendered drawing; metro_seq_pins.xdc names its source. Verified: versal_seq_sim_tb (in cosmo_seq/sequencer, on the shared harness) passes 20 of 20, covering A1/A0 bring-up, Versal rail sequencing, the POR_B/DONE handshake, the pre-boot measurement and its failure and skip paths, per-channel PERST, per-rail MAPO fault injection, the boot timeout, and the flash-mux interlock; the top-level analysis testbench compiles the whole tree. The design routes with all timing constraints met and produces a bitstream. --- .github/filters.yml | 6 + hdl/projects/metro_seq/BUCK | 74 ++ hdl/projects/metro_seq/metro_seq_pins.xdc | 341 +++++ hdl/projects/metro_seq/metro_seq_top.rdl | 29 + hdl/projects/metro_seq/metro_seq_top.vhd | 1126 +++++++++++++++++ hdl/projects/metro_seq/metro_timing.xdc | 354 ++++++ .../metro_seq/sims/metro_seq_top_tb.vhd | 41 + hdl/projects/metro_seq/versal_subsystem/BUCK | 24 + .../versal_subsystem/versal_flash_regs.rdl | 50 + .../versal_flash_subsystem.vhd | 131 ++ 10 files changed, 2176 insertions(+) create mode 100644 hdl/projects/metro_seq/BUCK create mode 100644 hdl/projects/metro_seq/metro_seq_pins.xdc create mode 100644 hdl/projects/metro_seq/metro_seq_top.rdl create mode 100644 hdl/projects/metro_seq/metro_seq_top.vhd create mode 100644 hdl/projects/metro_seq/metro_timing.xdc create mode 100644 hdl/projects/metro_seq/sims/metro_seq_top_tb.vhd create mode 100644 hdl/projects/metro_seq/versal_subsystem/BUCK create mode 100644 hdl/projects/metro_seq/versal_subsystem/versal_flash_regs.rdl create mode 100644 hdl/projects/metro_seq/versal_subsystem/versal_flash_subsystem.vhd diff --git a/.github/filters.yml b/.github/filters.yml index c3548d3e..36beec85 100644 --- a/.github/filters.yml +++ b/.github/filters.yml @@ -30,5 +30,11 @@ cosmo_hp: cosmo_seq: - 'hdl/projects/cosmo_seq/**/*' +# metro_seq depends on cosmo_seq's shared subsystems, so a change there has to +# rebuild metro too. +metro_seq: + - 'hdl/projects/metro_seq/**/*' + - 'hdl/projects/cosmo_seq/**/*' + cosmo_ignition: - 'hdl/projects/cosmo_ignition/**/*' \ No newline at end of file diff --git a/hdl/projects/metro_seq/BUCK b/hdl/projects/metro_seq/BUCK new file mode 100644 index 00000000..b93e1d2c --- /dev/null +++ b/hdl/projects/metro_seq/BUCK @@ -0,0 +1,74 @@ +load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim") +load("//tools:rdl.bzl", "rdl_file") +load("//tools:vivado.bzl", "vivado_bitstream") + +rdl_file( + name = "metro_seq_top_rdl", + src = "metro_seq_top.rdl", + deps = [ + "//hdl/projects/cosmo_seq/debug_module:debug_regs_rdl", + "//hdl/projects/cosmo_seq/sequencer:sequencer_regs_rdl", + "//hdl/projects/metro_seq/versal_subsystem:versal_flash_regs_rdl", + "//hdl/projects/cosmo_seq/dimms_subsystem:dimm_regs_rdl", + "//hdl/projects/cosmo_seq/sp_i2c_subsystem:sp_i2c_regs_rdl", + "//hdl/ip/vhd/info:info_regs_rdl", + "//hdl/ip/vhd/spi_nor_controller:spi_nor_regs_rdl", + "//hdl/ip/vhd/hash_engine:hash_engine_regs_rdl", + "//hdl/ip/vhd/espi:espi_regs_rdl", + "//hdl/ip/vhd/i2c/io_expanders/PCA9506ish:pca9506_regs_rdl", + ], + outputs = [ + "metro_seq_top.adoc", + "metro_seq_top.html", + "metro_seq_top.json", + ], +) + +vhdl_unit( + name = "metro_seq_top", + srcs = ["metro_seq_top.vhd"], + deps = [ + ":metro_seq_top_rdl", + # Clocks, resets, info block and LED are the same on both boards, PLL + # IP included; see the export_file targets next to it. + "//hdl/projects/cosmo_seq:board_support", + "//hdl/ip/vhd/axi_blocks:axilite_common_pkg", + "//hdl/ip/vhd/axi_blocks:axil_interconnect", + "//hdl/ip/vhd/common:tristate_if_pkg", + "//hdl/ip/vhd/fmc_if:stm32h7_fmc_target", + "//hdl/ip/vhd/hash_engine:hash_engine_top", + # The sequencer is shared: one entity, NIC_KIND => NIC_VERSAL here. + "//hdl/projects/cosmo_seq/sequencer:sequencer", + "//hdl/projects/metro_seq/versal_subsystem:versal_flash_subsystem", + "//hdl/projects/cosmo_seq/debug_module:debug_module_top", + # Shared verbatim with cosmo_seq -- identical hardware on both boards. + "//hdl/projects/cosmo_seq/sp5_espi_flash_subsystem:sp5_espi_flash_subsystem", + "//hdl/projects/cosmo_seq/sp5_uart_subsystem:sp5_uart_subsystem", + "//hdl/projects/cosmo_seq/sp_i2c_subsystem:sp_i2c_subsystem", + "//hdl/projects/cosmo_seq/sp5_hotplug_subsystem:sp5_hotplug_subsystem", + "//hdl/projects/cosmo_seq/dimms_subsystem:dimms_subsystem_top", + ], + standard = "2019", +) + +vivado_bitstream( + name = "metro_seq", + top_entity_name = "metro_seq_top", + top = ":metro_seq_top", + part = "xc7s100fgga484-1", + constraints = glob(["*.xdc"]), + pre_synth_tcl_files = [ + "//hdl/projects/cosmo_seq:sys_pll_ip_tcl", + "//hdl/projects/cosmo_seq:fmc_pll_ip_tcl", + ], +) + +# Analysis-only: vhdl_unit collects sources but does not run a compiler, so +# without this nothing type-checks metro_seq_top until Vivado synthesis. +vunit_sim( + name = "metro_seq_top_tb", + srcs = glob(["sims/*.vhd"]), + deps = [":metro_seq_top"], + visibility = ["PUBLIC"], + standard = "2019", +) diff --git a/hdl/projects/metro_seq/metro_seq_pins.xdc b/hdl/projects/metro_seq/metro_seq_pins.xdc new file mode 100644 index 00000000..3e628929 --- /dev/null +++ b/hdl/projects/metro_seq/metro_seq_pins.xdc @@ -0,0 +1,341 @@ +# Pin assignments for the Metro sequencer FPGA (U27, XC7S100-1FGGA484I). +# +# Derived from schematic 913-0000028 rev 1 sheets 57-59 (dated 6/17/2026) and +# reviewed against the rendered drawing. IOSTANDARD follows the bank supply: +# banks 13/14/15/16/36 are on V3P3_SYS_A2, banks 34/35 on V1P8_SYS_A2. +# +# Dedicated configuration and JTAG pins (CCLK_0, INIT_B_0, PROGRAM_B_0, DONE_0, +# TCK/TDI/TDO/TMS) are wired on the schematic but are not fabric I/O and so have +# no port here. + +set_property -dict { PACKAGE_PIN M8 IOSTANDARD LVCMOS18 } [get_ports { clk_50mhz_fpga1_2 }]; +set_property -dict { PACKAGE_PIN P15 IOSTANDARD LVCMOS33 } [get_ports { clk_50mhz_fpga1_1 }]; +set_property -dict { PACKAGE_PIN T16 IOSTANDARD LVCMOS33 } [get_ports { clk_buff_m2_rsw_to_fpga1_los_l }]; +set_property -dict { PACKAGE_PIN R20 IOSTANDARD LVCMOS33 } [get_ports { clk_buff_nic_to_fpga1_los_l }]; +set_property -dict { PACKAGE_PIN AA13 IOSTANDARD LVCMOS33 } [get_ports { dimm_a_pg }]; +set_property -dict { PACKAGE_PIN AB13 IOSTANDARD LVCMOS33 } [get_ports { dimm_b_pg }]; +set_property -dict { PACKAGE_PIN W11 IOSTANDARD LVCMOS33 } [get_ports { dimm_c_pg }]; +set_property -dict { PACKAGE_PIN Y11 IOSTANDARD LVCMOS33 } [get_ports { dimm_d_pg }]; +set_property -dict { PACKAGE_PIN AB11 IOSTANDARD LVCMOS33 } [get_ports { dimm_e_pg }]; +set_property -dict { PACKAGE_PIN AB12 IOSTANDARD LVCMOS33 } [get_ports { dimm_f_pg }]; +set_property -dict { PACKAGE_PIN AA17 IOSTANDARD LVCMOS33 } [get_ports { dimm_g_pg }]; +set_property -dict { PACKAGE_PIN AB18 IOSTANDARD LVCMOS33 } [get_ports { dimm_h_pg }]; +set_property -dict { PACKAGE_PIN AA18 IOSTANDARD LVCMOS33 } [get_ports { dimm_i_pg }]; +set_property -dict { PACKAGE_PIN Y18 IOSTANDARD LVCMOS33 } [get_ports { dimm_j_pg }]; +set_property -dict { PACKAGE_PIN V16 IOSTANDARD LVCMOS33 } [get_ports { dimm_k_pg }]; +set_property -dict { PACKAGE_PIN U16 IOSTANDARD LVCMOS33 } [get_ports { dimm_l_pg }]; +# eSPI0 DAT1 and ALERT are swapped relative to the schematic names, the same +# way cosmo_seq swaps them. Both boards' schematics name W3 ALERT_L and Y3 +# DAT1, and both carry the same note: the ALERT ball doubles as DAT1 when the +# SP5 boots over SAFS eSPI, which is what this FPGA serves. cosmo_seq_pins.xdc +# resolves it the same way and has been through bring-up; do not "correct" +# these back to the schematic names without re-reading that note. +set_property -dict { PACKAGE_PIN W3 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi0_sp5_to_fpga1_dat[1] }]; +set_property -dict { PACKAGE_PIN U3 IOSTANDARD LVCMOS18 } [get_ports { espi0_sp5_to_fpga1_clk }]; +set_property -dict { PACKAGE_PIN T3 IOSTANDARD LVCMOS18 } [get_ports { espi0_sp5_to_fpga1_cs_l }]; +set_property -dict { PACKAGE_PIN U4 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi0_sp5_to_fpga1_dat[0] }]; +set_property -dict { PACKAGE_PIN Y3 IOSTANDARD LVCMOS18 } [get_ports { espi0_fpga1_to_sp5_alert_l }]; +set_property -dict { PACKAGE_PIN U1 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi0_sp5_to_fpga1_dat[2] }]; +set_property -dict { PACKAGE_PIN U2 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi0_sp5_to_fpga1_dat[3] }]; +set_property -dict { PACKAGE_PIN N3 IOSTANDARD LVCMOS18 } [get_ports { espi1_fpga1_to_sp5_alert_l }]; +set_property -dict { PACKAGE_PIN M1 IOSTANDARD LVCMOS18 } [get_ports { espi1_sp5_to_fpga1_clk }]; +set_property -dict { PACKAGE_PIN N4 IOSTANDARD LVCMOS18 } [get_ports { espi1_sp5_to_fpga1_clk_2 }]; +set_property -dict { PACKAGE_PIN M2 IOSTANDARD LVCMOS18 } [get_ports { espi1_sp5_to_fpga1_cs_l }]; +set_property -dict { PACKAGE_PIN M3 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi1_sp5_to_fpga1_dat[0] }]; +set_property -dict { PACKAGE_PIN M4 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi1_sp5_to_fpga1_dat[1] }]; +set_property -dict { PACKAGE_PIN L1 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi1_sp5_to_fpga1_dat[2] }]; +set_property -dict { PACKAGE_PIN K1 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi1_sp5_to_fpga1_dat[3] }]; +set_property -dict { PACKAGE_PIN Y1 IOSTANDARD LVCMOS18 } [get_ports { espi_sp5_to_fpga1_reset_l }]; +set_property -dict { PACKAGE_PIN D4 IOSTANDARD LVCMOS33 } [get_ports { fan_central_hsc_to_fpga1_pg }]; +set_property -dict { PACKAGE_PIN F1 IOSTANDARD LVCMOS33 } [get_ports { fan_east_hsc_to_fpga1_pg }]; +set_property -dict { PACKAGE_PIN V20 IOSTANDARD LVCMOS33 } [get_ports { fan_to_fpga1_fan_fail }]; +set_property -dict { PACKAGE_PIN E4 IOSTANDARD LVCMOS33 } [get_ports { fan_west_hsc_to_fpga1_pg }]; +set_property -dict { PACKAGE_PIN H20 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[16] }]; +set_property -dict { PACKAGE_PIN H21 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[17] }]; +set_property -dict { PACKAGE_PIN J22 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[18] }]; +set_property -dict { PACKAGE_PIN K22 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[19] }]; +set_property -dict { PACKAGE_PIN M16 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[20] }]; +set_property -dict { PACKAGE_PIN L20 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[21] }]; +set_property -dict { PACKAGE_PIN L21 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[22] }]; +set_property -dict { PACKAGE_PIN L22 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[23] }]; +set_property -dict { PACKAGE_PIN J17 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_adv_l }]; +set_property -dict { PACKAGE_PIN H17 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_bl_l[0] }]; +set_property -dict { PACKAGE_PIN G17 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_bl_l[1] }]; +set_property -dict { PACKAGE_PIN F19 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_clk }]; +set_property -dict { PACKAGE_PIN H16 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_cs_l }]; +set_property -dict { PACKAGE_PIN L18 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[0] }]; +set_property -dict { PACKAGE_PIN L19 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[1] }]; +set_property -dict { PACKAGE_PIN M15 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[2] }]; +set_property -dict { PACKAGE_PIN L15 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[3] }]; +set_property -dict { PACKAGE_PIN G20 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[4] }]; +set_property -dict { PACKAGE_PIN H19 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[5] }]; +set_property -dict { PACKAGE_PIN M17 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[6] }]; +set_property -dict { PACKAGE_PIN M18 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[7] }]; +set_property -dict { PACKAGE_PIN L16 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[8] }]; +set_property -dict { PACKAGE_PIN K17 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[9] }]; +set_property -dict { PACKAGE_PIN G21 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[10] }]; +set_property -dict { PACKAGE_PIN G22 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[11] }]; +set_property -dict { PACKAGE_PIN J19 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[12] }]; +set_property -dict { PACKAGE_PIN J20 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[13] }]; +set_property -dict { PACKAGE_PIN K18 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[14] }]; +set_property -dict { PACKAGE_PIN K19 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[15] }]; +set_property -dict { PACKAGE_PIN H18 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_oe_l }]; +set_property -dict { PACKAGE_PIN K16 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_wait_l }]; +set_property -dict { PACKAGE_PIN G16 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_we_l }]; +set_property -dict { PACKAGE_PIN AA3 IOSTANDARD LVCMOS18 } [get_ports { fpga1_debug_uart_buf_oe_en_l }]; +set_property -dict { PACKAGE_PIN Y8 IOSTANDARD LVCMOS18 } [get_ports { fpga1_espi0_cs_l_buff_oe_en_l }]; +set_property -dict { PACKAGE_PIN W2 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[0] }]; +set_property -dict { PACKAGE_PIN U5 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[1] }]; +set_property -dict { PACKAGE_PIN W1 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[2] }]; +set_property -dict { PACKAGE_PIN V7 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[3] }]; +set_property -dict { PACKAGE_PIN V1 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[4] }]; +set_property -dict { PACKAGE_PIN V6 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[5] }]; +set_property -dict { PACKAGE_PIN U8 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[6] }]; +set_property -dict { PACKAGE_PIN T8 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[7] }]; +set_property -dict { PACKAGE_PIN C22 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_0 }]; +set_property -dict { PACKAGE_PIN K15 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_1 }]; +set_property -dict { PACKAGE_PIN D21 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_2 }]; +set_property -dict { PACKAGE_PIN J15 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_3 }]; +set_property -dict { PACKAGE_PIN C20 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_4 }]; +set_property -dict { PACKAGE_PIN B21 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_5 }]; +set_property -dict { PACKAGE_PIN D20 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_6 }]; +set_property -dict { PACKAGE_PIN B22 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_7 }]; +set_property -dict { PACKAGE_PIN U19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_status_led }]; +set_property -dict { PACKAGE_PIN AA11 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_bp_buff_output_en_l }]; +set_property -dict { PACKAGE_PIN G2 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fan_central_hsc_disable }]; +set_property -dict { PACKAGE_PIN G1 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fan_east_hsc_disable }]; +set_property -dict { PACKAGE_PIN E1 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fan_west_hsc_disable }]; +set_property -dict { PACKAGE_PIN B6 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fpga2_io[0] }]; +set_property -dict { PACKAGE_PIN A3 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fpga2_io[1] }]; +set_property -dict { PACKAGE_PIN A4 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fpga2_io[2] }]; +set_property -dict { PACKAGE_PIN A5 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fpga2_io[3] }]; +set_property -dict { PACKAGE_PIN B3 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fpga2_io[4] }]; +set_property -dict { PACKAGE_PIN A2 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fpga2_io[5] }]; +set_property -dict { PACKAGE_PIN E19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_i2c_mux1_sel[0] }]; +set_property -dict { PACKAGE_PIN A20 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_i2c_mux1_sel[1] }]; +set_property -dict { PACKAGE_PIN G15 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_i2c_mux2_sel[0] }]; +set_property -dict { PACKAGE_PIN D19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_i2c_mux2_sel[1] }]; +set_property -dict { PACKAGE_PIN F4 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_i2c_mux3_sel[0] }]; +set_property -dict { PACKAGE_PIN D1 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_i2c_mux3_sel[1] }]; +set_property -dict { PACKAGE_PIN C3 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_ign_trgt_fpga_creset }]; +set_property -dict { PACKAGE_PIN C4 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_jtag_mux_sel }]; +set_property -dict { PACKAGE_PIN AB21 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_m2_apml_xltr_en }]; +set_property -dict { PACKAGE_PIN W18 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_m2a_hsc_en }]; +set_property -dict { PACKAGE_PIN N19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_m2a_perst_l }]; +set_property -dict { PACKAGE_PIN Y19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_m2b_hsc_en }]; +set_property -dict { PACKAGE_PIN P21 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_m2b_perst_l }]; +set_property -dict { PACKAGE_PIN T22 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_nic_hsc_en }]; +set_property -dict { PACKAGE_PIN AB20 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_pcie_clk_buff_m2a_oe_l }]; +set_property -dict { PACKAGE_PIN AB19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_pcie_clk_buff_m2b_oe_l }]; +set_property -dict { PACKAGE_PIN V18 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_pcie_clk_buff_nic_cha_oe_l }]; +set_property -dict { PACKAGE_PIN Y22 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_pcie_clk_buff_nic_chb_oe_l }]; +set_property -dict { PACKAGE_PIN V19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_pcie_clk_buff_rsw_oe_l }]; +set_property -dict { PACKAGE_PIN Y12 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp5_apml_xltr_en }]; +set_property -dict { PACKAGE_PIN T2 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_sp5_espi_kbrst_l }]; +set_property -dict { PACKAGE_PIN AB10 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_sp5_pwr_btn_l }]; +set_property -dict { PACKAGE_PIN L7 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_sp5_pwrgd }]; +set_property -dict { PACKAGE_PIN AA8 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_sp5_romtype0 }]; +set_property -dict { PACKAGE_PIN AB7 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_sp5_rsmrst_l }]; +set_property -dict { PACKAGE_PIN AA7 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_sp5_sys_reset_l }]; +set_property -dict { PACKAGE_PIN E11 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_int_l }]; +set_property -dict { PACKAGE_PIN D11 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_irq_l[1] }]; +set_property -dict { PACKAGE_PIN F13 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_irq_l[2] }]; +set_property -dict { PACKAGE_PIN F14 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_irq_l[3] }]; +set_property -dict { PACKAGE_PIN F12 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_irq_l[4] }]; +set_property -dict { PACKAGE_PIN A19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_irq_l[5] }]; +set_property -dict { PACKAGE_PIN E17 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_irq_l[6] }]; +set_property -dict { PACKAGE_PIN F10 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_misc_a }]; +set_property -dict { PACKAGE_PIN G11 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_misc_b }]; +set_property -dict { PACKAGE_PIN F11 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_misc_c }]; +set_property -dict { PACKAGE_PIN G10 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_misc_d }]; +set_property -dict { PACKAGE_PIN J21 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_mux_ign_mux_sel }]; +set_property -dict { PACKAGE_PIN C2 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_v12_ddr5_abcdef_hsc_en }]; +set_property -dict { PACKAGE_PIN C1 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_v12_ddr5_ghijkl_hsc_en }]; +set_property -dict { PACKAGE_PIN R5 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_vercel_flash_qspi_mux_en_l }]; +set_property -dict { PACKAGE_PIN R4 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_vercel_flash_qspi_mux_sel }]; +set_property -dict { PACKAGE_PIN F18 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_versal_erro_done_buff_en }]; +set_property -dict { PACKAGE_PIN D2 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_versal_mode[0] }]; +set_property -dict { PACKAGE_PIN E3 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_versal_mode[1] }]; +set_property -dict { PACKAGE_PIN F6 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_versal_mode[2] }]; +set_property -dict { PACKAGE_PIN B7 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_versal_mode[3] }]; +set_property -dict { PACKAGE_PIN J16 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_versal_mode_buffer_en_l }]; +set_property -dict { PACKAGE_PIN E12 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_versal_por_b }]; +set_property -dict { PACKAGE_PIN AB2 IOSTANDARD LVCMOS18 } [get_ports { fpga1_uart0_buff_oe_en_l }]; +set_property -dict { PACKAGE_PIN AB3 IOSTANDARD LVCMOS18 } [get_ports { fpga1_uart1_buff_oe_en_l }]; +set_property -dict { PACKAGE_PIN D5 IOSTANDARD LVCMOS33 } [get_ports { fpga1_version_id[0] }]; +set_property -dict { PACKAGE_PIN G8 IOSTANDARD LVCMOS33 } [get_ports { fpga1_version_id[1] }]; +set_property -dict { PACKAGE_PIN Y4 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_genint_l }]; +set_property -dict { PACKAGE_PIN H2 IOSTANDARD LVCMOS18 } [get_ports { hdt_conn_to_mux_testen }]; +set_property -dict { PACKAGE_PIN J1 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_dat }]; +set_property -dict { PACKAGE_PIN J3 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_dbreq_l }]; +set_property -dict { PACKAGE_PIN K3 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_en_l }]; +set_property -dict { PACKAGE_PIN H4 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_sel }]; +set_property -dict { PACKAGE_PIN J2 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_tck }]; +set_property -dict { PACKAGE_PIN K4 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_tms }]; +set_property -dict { PACKAGE_PIN K2 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_trst_l }]; +set_property -dict { PACKAGE_PIN H5 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_xtrig5_l }]; +set_property -dict { PACKAGE_PIN H7 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_xtrig6_l }]; +set_property -dict { PACKAGE_PIN H6 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_xtrig7_l }]; +set_property -dict { PACKAGE_PIN H3 IOSTANDARD LVCMOS18 } [get_ports { hdt_mux_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN AA15 IOSTANDARD LVCMOS33 } [get_ports { i2c_sp5_sec_v3p3_scl }]; +set_property -dict { PACKAGE_PIN AA16 IOSTANDARD LVCMOS33 } [get_ports { i2c_sp5_sec_v3p3_sda }]; +set_property -dict { PACKAGE_PIN N6 IOSTANDARD LVCMOS18 } [get_ports { i2c_sp5_to_fpgax_hp_scl }]; +set_property -dict { PACKAGE_PIN P7 IOSTANDARD LVCMOS18 } [get_ports { i2c_sp5_to_fpgax_hp_sda }]; +set_property -dict { PACKAGE_PIN M21 IOSTANDARD LVCMOS33 } [get_ports { i2c_sp_to_fpga1_scl }]; +set_property -dict { PACKAGE_PIN N21 IOSTANDARD LVCMOS33 } [get_ports { i2c_sp_to_fpga1_sda }]; +set_property -dict { PACKAGE_PIN P1 IOSTANDARD LVCMOS18 } [get_ports { i2c_sp_to_nic_sysmon_alert_l }]; +set_property -dict { PACKAGE_PIN AB4 IOSTANDARD LVCMOS18 } [get_ports { i3c_fpga1_to_dimm_abcdef_scl }]; +set_property -dict { PACKAGE_PIN AB5 IOSTANDARD LVCMOS18 } [get_ports { i3c_fpga1_to_dimm_abcdef_sda }]; +set_property -dict { PACKAGE_PIN Y5 IOSTANDARD LVCMOS18 } [get_ports { i3c_fpga1_to_dimm_ghijkl_scl }]; +set_property -dict { PACKAGE_PIN Y6 IOSTANDARD LVCMOS18 } [get_ports { i3c_fpga1_to_dimm_ghijkl_sda }]; +set_property -dict { PACKAGE_PIN C19 IOSTANDARD LVCMOS33 } [get_ports { i3c_fpga1_to_dimm_oe_l }]; +set_property -dict { PACKAGE_PIN AA1 IOSTANDARD LVCMOS18 } [get_ports { i3c_sp5_to_fpga1_abcdef_scl }]; +set_property -dict { PACKAGE_PIN AA2 IOSTANDARD LVCMOS18 } [get_ports { i3c_sp5_to_fpga1_abcdef_sda }]; +set_property -dict { PACKAGE_PIN AB6 IOSTANDARD LVCMOS18 } [get_ports { i3c_sp5_to_fpga1_ghijkl_scl }]; +set_property -dict { PACKAGE_PIN AA6 IOSTANDARD LVCMOS18 } [get_ports { i3c_sp5_to_fpga1_ghijkl_sda }]; +set_property -dict { PACKAGE_PIN C18 IOSTANDARD LVCMOS33 } [get_ports { i3c_sp5_to_fpga1_oe_l }]; +set_property -dict { PACKAGE_PIN E5 IOSTANDARD LVCMOS33 } [get_ports { m2a_hsc_to_fpga1_fault_l }]; +set_property -dict { PACKAGE_PIN V22 IOSTANDARD LVCMOS33 } [get_ports { m2a_to_fpga1_pedet }]; +set_property -dict { PACKAGE_PIN U22 IOSTANDARD LVCMOS33 } [get_ports { m2a_to_fpga1_prsnt_l }]; +set_property -dict { PACKAGE_PIN G7 IOSTANDARD LVCMOS33 } [get_ports { m2b_hsc_to_fpga1_fault_l }]; +set_property -dict { PACKAGE_PIN Y21 IOSTANDARD LVCMOS33 } [get_ports { m2b_to_fpga1_pedet }]; +set_property -dict { PACKAGE_PIN W21 IOSTANDARD LVCMOS33 } [get_ports { m2b_to_fpga1_prsnt_l }]; +set_property -dict { PACKAGE_PIN E18 IOSTANDARD LVCMOS33 } [get_ports { main_hsc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN N17 IOSTANDARD LVCMOS33 } [get_ports { pcie_aux_fpga1_to_rsw_perst_l }]; +set_property -dict { PACKAGE_PIN N20 IOSTANDARD LVCMOS33 } [get_ports { pcie_aux_rsw_to_fpga1_prsnt_buff_l }]; +set_property -dict { PACKAGE_PIN P22 IOSTANDARD LVCMOS33 } [get_ports { pcie_aux_rsw_to_fpga1_pwrflt_buff_l }]; +set_property -dict { PACKAGE_PIN P8 IOSTANDARD LVCMOS18 } [get_ports { pcie_fpga1_to_nic_cha_perst_l }]; +set_property -dict { PACKAGE_PIN N7 IOSTANDARD LVCMOS18 } [get_ports { pcie_fpga1_to_nic_chb_perst_l }]; +set_property -dict { PACKAGE_PIN N8 IOSTANDARD LVCMOS18 } [get_ports { pcie_nic_to_fpga1_cha_prsnt_l }]; +set_property -dict { PACKAGE_PIN N5 IOSTANDARD LVCMOS18 } [get_ports { pcie_nic_to_fpga1_cha_pwren_l }]; +set_property -dict { PACKAGE_PIN R7 IOSTANDARD LVCMOS18 } [get_ports { pcie_nic_to_fpga1_chb_prsnt_l }]; +set_property -dict { PACKAGE_PIN R6 IOSTANDARD LVCMOS18 } [get_ports { pcie_nic_to_fpga1_chb_pwren_l }]; +set_property -dict { PACKAGE_PIN V13 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont1_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN T12 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont1_to_fpga1_vddcr_cpu0_pg }]; +set_property -dict { PACKAGE_PIN V14 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont1_to_fpga1_vddcr_cpu1_pg }]; +set_property -dict { PACKAGE_PIN T13 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont1_to_fpga1_vddcr_soc_pg }]; +set_property -dict { PACKAGE_PIN V15 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont1_to_fpga1_vddio_sp5_pg }]; +set_property -dict { PACKAGE_PIN W15 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont2_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN W16 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont3_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN W14 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont3_to_fpga1_cfp }]; +set_property -dict { PACKAGE_PIN Y14 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont3_to_fpga1_vrhot_n }]; +set_property -dict { PACKAGE_PIN U17 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont4_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN P16 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont4_to_fpga1_cfp }]; +set_property -dict { PACKAGE_PIN P17 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont4_to_fpga1_vrhot_n }]; +set_property -dict { PACKAGE_PIN W12 IOSTANDARD LVCMOS33 } [get_ports { pwr_fpga1_to_v1p5_sp5_rtc_a2_en }]; +set_property -dict { PACKAGE_PIN U12 IOSTANDARD LVCMOS33 } [get_ports { pwr_v1p5_sp5_rtc_a2_to_fpga1_pg }]; +set_property -dict { PACKAGE_PIN P6 IOSTANDARD LVCMOS18 } [get_ports { qspi_fpga1_to_vercel_flash_mux_cs_l }]; +set_property -dict { PACKAGE_PIN K6 IOSTANDARD LVCMOS18 } [get_ports { qspi_fpga1_to_vercel_flash_mux_d[0] }]; +set_property -dict { PACKAGE_PIN J6 IOSTANDARD LVCMOS18 } [get_ports { qspi_fpga1_to_vercel_flash_mux_d[1] }]; +set_property -dict { PACKAGE_PIN J8 IOSTANDARD LVCMOS18 } [get_ports { qspi_fpga1_to_vercel_flash_mux_d[2] }]; +set_property -dict { PACKAGE_PIN K8 IOSTANDARD LVCMOS18 } [get_ports { qspi_fpga1_to_vercel_flash_mux_d[3] }]; +set_property -dict { PACKAGE_PIN M5 IOSTANDARD LVCMOS18 } [get_ports { qspi_fpga1_to_vercel_flash_mux_sck }]; +set_property -dict { PACKAGE_PIN P20 IOSTANDARD LVCMOS33 } [get_ports { rsw_to_sp5_pcie_attached_buff_l }]; +set_property -dict { PACKAGE_PIN C16 IOSTANDARD LVCMOS33 } [get_ports { seq_rev_id[0] }]; +set_property -dict { PACKAGE_PIN B15 IOSTANDARD LVCMOS33 } [get_ports { seq_rev_id[1] }]; +set_property -dict { PACKAGE_PIN B16 IOSTANDARD LVCMOS33 } [get_ports { seq_rev_id[2] }]; +set_property -dict { PACKAGE_PIN C5 IOSTANDARD LVCMOS33 } [get_ports { smbus_fan_central_hsc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN D6 IOSTANDARD LVCMOS33 } [get_ports { smbus_fan_east_hsc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN C6 IOSTANDARD LVCMOS33 } [get_ports { smbus_fan_west_hsc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN AA22 IOSTANDARD LVCMOS33 } [get_ports { smbus_ibc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN D7 IOSTANDARD LVCMOS33 } [get_ports { smbus_m2_hsc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN D8 IOSTANDARD LVCMOS33 } [get_ports { smbus_nic_hsc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN A10 IOSTANDARD LVCMOS33 } [get_ports { smbus_therm_ne_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN C9 IOSTANDARD LVCMOS33 } [get_ports { smbus_therm_nw_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN B9 IOSTANDARD LVCMOS33 } [get_ports { smbus_therm_sc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN A9 IOSTANDARD LVCMOS33 } [get_ports { smbus_therm_se_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN E8 IOSTANDARD LVCMOS33 } [get_ports { smbus_therm_sw_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN F5 IOSTANDARD LVCMOS33 } [get_ports { smbus_v12_ddr5_abcdef_hsc_to_fpga1_alert }]; +set_property -dict { PACKAGE_PIN F8 IOSTANDARD LVCMOS33 } [get_ports { smbus_v12_ddr5_ghijkl_hsc_to_fpga1_alert }]; +set_property -dict { PACKAGE_PIN K5 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN AA9 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_debug1 }]; +set_property -dict { PACKAGE_PIN AB9 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_debug2 }]; +set_property -dict { PACKAGE_PIN L6 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_pwrgd_out }]; +set_property -dict { PACKAGE_PIN J7 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_pwrok_unbuf }]; +set_property -dict { PACKAGE_PIN L4 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_sp5_reset_l }]; +set_property -dict { PACKAGE_PIN AA10 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_slp_s3_l }]; +set_property -dict { PACKAGE_PIN Y7 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_slp_s5_l }]; +set_property -dict { PACKAGE_PIN L8 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_smerr_l }]; +set_property -dict { PACKAGE_PIN W7 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_spd_host_ctrl_l }]; +set_property -dict { PACKAGE_PIN L5 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_thermtrip_l }]; +set_property -dict { PACKAGE_PIN T1 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_nic_mfg_mode_l }]; +set_property -dict { PACKAGE_PIN G13 IOSTANDARD LVCMOS33 } [get_ports { sp_to_fpga1_mux_reset_l }]; +set_property -dict { PACKAGE_PIN C15 IOSTANDARD LVCMOS33 } [get_ports { sp_to_fpga1_system_reset_l }]; +set_property -dict { PACKAGE_PIN N1 IOSTANDARD LVCMOS18 } [get_ports { spi0_sp5_to_fpga1_cs_l }]; +set_property -dict { PACKAGE_PIN P3 IOSTANDARD LVCMOS18 } [get_ports { spi1_sp5_to_fpga1_cs_l }]; +set_property -dict { PACKAGE_PIN P2 IOSTANDARD LVCMOS18 } [get_ports { spi2_sp5_to_fpga1_cs_l }]; +set_property -dict { PACKAGE_PIN E15 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga1_to_flash_clk }]; +set_property -dict { PACKAGE_PIN C17 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga1_to_flash_cs_l }]; +set_property -dict { PACKAGE_PIN E16 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga1_to_flash_dat[0] }]; +set_property -dict { PACKAGE_PIN B14 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga1_to_flash_dat[1] }]; +set_property -dict { PACKAGE_PIN A14 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga1_to_flash_dat[2] }]; +set_property -dict { PACKAGE_PIN A16 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga1_to_flash_dat[3] }]; +set_property -dict { PACKAGE_PIN T7 IOSTANDARD LVCMOS18 } [get_ports { uart0_fpga1_to_sp5_dat_buff }]; +set_property -dict { PACKAGE_PIN W5 IOSTANDARD LVCMOS18 } [get_ports { uart0_fpga1_to_sp5_rts_l_buff }]; +set_property -dict { PACKAGE_PIN F15 IOSTANDARD LVCMOS33 } [get_ports { uart0_fpga1_to_sp_dat }]; +set_property -dict { PACKAGE_PIN C13 IOSTANDARD LVCMOS33 } [get_ports { uart0_fpga1_to_sp_rts_l }]; +set_property -dict { PACKAGE_PIN T6 IOSTANDARD LVCMOS18 } [get_ports { uart0_sp5_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN R2 IOSTANDARD LVCMOS18 } [get_ports { uart0_sp5_to_fpga1_int }]; +set_property -dict { PACKAGE_PIN W4 IOSTANDARD LVCMOS18 } [get_ports { uart0_sp5_to_fpga1_rts_l }]; +set_property -dict { PACKAGE_PIN G14 IOSTANDARD LVCMOS33 } [get_ports { uart0_sp_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN D13 IOSTANDARD LVCMOS33 } [get_ports { uart0_sp_to_fpga1_rts_l }]; +set_property -dict { PACKAGE_PIN V5 IOSTANDARD LVCMOS18 } [get_ports { uart1_fpga1_to_sp5_dat_buff }]; +set_property -dict { PACKAGE_PIN A12 IOSTANDARD LVCMOS33 } [get_ports { uart1_fpga1_to_sp_dat }]; +set_property -dict { PACKAGE_PIN C10 IOSTANDARD LVCMOS33 } [get_ports { uart1_fpga1_to_sp_rts_l }]; +set_property -dict { PACKAGE_PIN V4 IOSTANDARD LVCMOS18 } [get_ports { uart1_sp5_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN B13 IOSTANDARD LVCMOS33 } [get_ports { uart1_sp_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN A13 IOSTANDARD LVCMOS33 } [get_ports { uart1_sp_to_fpga1_rts_l }]; +set_property -dict { PACKAGE_PIN F17 IOSTANDARD LVCMOS33 } [get_ports { uart8_fpga1_to_sp_dat }]; +set_property -dict { PACKAGE_PIN A21 IOSTANDARD LVCMOS33 } [get_ports { uart8_sp_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN D22 IOSTANDARD LVCMOS33 } [get_ports { uart_debug_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN F22 IOSTANDARD LVCMOS33 } [get_ports { uart_debug_to_fpga1_rts_l }]; +set_property -dict { PACKAGE_PIN E22 IOSTANDARD LVCMOS33 } [get_ports { uart_fpga1_to_debug_dat }]; +set_property -dict { PACKAGE_PIN F21 IOSTANDARD LVCMOS33 } [get_ports { uart_fpga1_to_debug_rts_l }]; +set_property -dict { PACKAGE_PIN D15 IOSTANDARD LVCMOS33 } [get_ports { uart_local_fpga1_to_sp_dat }]; +set_property -dict { PACKAGE_PIN C12 IOSTANDARD LVCMOS33 } [get_ports { uart_local_fpga1_to_sp_rts_l }]; +set_property -dict { PACKAGE_PIN D14 IOSTANDARD LVCMOS33 } [get_ports { uart_local_sp_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN D12 IOSTANDARD LVCMOS33 } [get_ports { uart_local_sp_to_fpga1_rts_l }]; +set_property -dict { PACKAGE_PIN N18 IOSTANDARD LVCMOS33 } [get_ports { v0p88_nic_a0hp_en }]; +set_property -dict { PACKAGE_PIN T17 IOSTANDARD LVCMOS33 } [get_ports { v0p88_nic_a0hp_pg }]; +set_property -dict { PACKAGE_PIN R19 IOSTANDARD LVCMOS33 } [get_ports { v0p8_nic_vccint_a0hp_en }]; +set_property -dict { PACKAGE_PIN R18 IOSTANDARD LVCMOS33 } [get_ports { v0p8_nic_vccint_a0hp_pg }]; +set_property -dict { PACKAGE_PIN F20 IOSTANDARD LVCMOS33 } [get_ports { v0p92_nic_avcc_a0hp_pg }]; +set_property -dict { PACKAGE_PIN G4 IOSTANDARD LVCMOS33 } [get_ports { v12_ddr5_abcdef_a0_pg }]; +set_property -dict { PACKAGE_PIN G3 IOSTANDARD LVCMOS33 } [get_ports { v12_ddr5_ghijkl_a0_pg }]; +set_property -dict { PACKAGE_PIN T19 IOSTANDARD LVCMOS33 } [get_ports { v12p0_nic_a0hp_pg }]; +set_property -dict { PACKAGE_PIN G6 IOSTANDARD LVCMOS33 } [get_ports { v12p0_nic_a0hp_to_fpga1_fault_l }]; +set_property -dict { PACKAGE_PIN W8 IOSTANDARD LVCMOS18 } [get_ports { v1p1_i3c_a2_pg }]; +set_property -dict { PACKAGE_PIN R16 IOSTANDARD LVCMOS33 } [get_ports { v1p1_nic_a0hp_en }]; +set_property -dict { PACKAGE_PIN AB16 IOSTANDARD LVCMOS33 } [get_ports { v1p1_sp5_en }]; +set_property -dict { PACKAGE_PIN T15 IOSTANDARD LVCMOS33 } [get_ports { v1p1_sp5_pg }]; +set_property -dict { PACKAGE_PIN AA21 IOSTANDARD LVCMOS33 } [get_ports { v1p2_fpga2_a2_pg }]; +set_property -dict { PACKAGE_PIN A18 IOSTANDARD LVCMOS33 } [get_ports { v1p2_nic_avtt_a0hp_pg }]; +set_property -dict { PACKAGE_PIN U20 IOSTANDARD LVCMOS33 } [get_ports { v1p4_nic_a0hp_en }]; +set_property -dict { PACKAGE_PIN N22 IOSTANDARD LVCMOS33 } [get_ports { v1p5_nic_a0hp_en }]; +set_property -dict { PACKAGE_PIN U18 IOSTANDARD LVCMOS33 } [get_ports { v1p5_nic_a0hp_pg }]; +set_property -dict { PACKAGE_PIN B19 IOSTANDARD LVCMOS33 } [get_ports { v1p5_nic_avccaux_a0hp_en }]; +set_property -dict { PACKAGE_PIN N15 IOSTANDARD LVCMOS33 } [get_ports { v1p5_nic_avccaux_a0hp_pg }]; +set_property -dict { PACKAGE_PIN D18 IOSTANDARD LVCMOS33 } [get_ports { v1p8_nic_a0hp_en }]; +set_property -dict { PACKAGE_PIN Y13 IOSTANDARD LVCMOS33 } [get_ports { v1p8_nic_a0hp_pg }]; +set_property -dict { PACKAGE_PIN AB17 IOSTANDARD LVCMOS33 } [get_ports { v1p8_sp5_en }]; +set_property -dict { PACKAGE_PIN T14 IOSTANDARD LVCMOS33 } [get_ports { v1p8_sp5_pg }]; +set_property -dict { PACKAGE_PIN Y20 IOSTANDARD LVCMOS33 } [get_ports { v2p5_fpga2_a2_pg }]; +set_property -dict { PACKAGE_PIN T21 IOSTANDARD LVCMOS33 } [get_ports { v2p5_mgmt_a2_pg }]; +set_property -dict { PACKAGE_PIN AA20 IOSTANDARD LVCMOS33 } [get_ports { v3p3_fpga2_a2_pg }]; +set_property -dict { PACKAGE_PIN V21 IOSTANDARD LVCMOS33 } [get_ports { v3p3_m2a_a0hp_pg_l }]; +set_property -dict { PACKAGE_PIN W22 IOSTANDARD LVCMOS33 } [get_ports { v3p3_m2b_a0hp_pg_l }]; +set_property -dict { PACKAGE_PIN A17 IOSTANDARD LVCMOS33 } [get_ports { v3p3_nic_a0hp_en }]; +set_property -dict { PACKAGE_PIN R17 IOSTANDARD LVCMOS33 } [get_ports { v3p3_nic_a0hp_pg }]; +set_property -dict { PACKAGE_PIN W17 IOSTANDARD LVCMOS33 } [get_ports { v3p3_sp5_en }]; +set_property -dict { PACKAGE_PIN U15 IOSTANDARD LVCMOS33 } [get_ports { v3p3_sp5_pg }]; +set_property -dict { PACKAGE_PIN T20 IOSTANDARD LVCMOS33 } [get_ports { v5p0_nic_a0hp_pg }]; +set_property -dict { PACKAGE_PIN F7 IOSTANDARD LVCMOS33 } [get_ports { v5p0_nic_a0hp_to_fpga1_fault_l }]; +set_property -dict { PACKAGE_PIN Y17 IOSTANDARD LVCMOS33 } [get_ports { vddcr_cpu0_en }]; +set_property -dict { PACKAGE_PIN AB14 IOSTANDARD LVCMOS33 } [get_ports { vddcr_cpu1_en }]; +set_property -dict { PACKAGE_PIN AA14 IOSTANDARD LVCMOS33 } [get_ports { vddcr_soc_en }]; +set_property -dict { PACKAGE_PIN V12 IOSTANDARD LVCMOS33 } [get_ports { vddio_sp5_en }]; +set_property -dict { PACKAGE_PIN A11 IOSTANDARD LVCMOS33 } [get_ports { versal_to_fpga1_done }]; +set_property -dict { PACKAGE_PIN C11 IOSTANDARD LVCMOS33 } [get_ports { versal_to_fpga1_error_out }]; +set_property -dict { PACKAGE_PIN C8 IOSTANDARD LVCMOS33 } [get_ports { vr_v1p8_sys_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN A6 IOSTANDARD LVCMOS33 } [get_ports { vr_v3p3_sys_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN B8 IOSTANDARD LVCMOS33 } [get_ports { vr_v5p0_sys_to_fpga1_alert_l }]; diff --git a/hdl/projects/metro_seq/metro_seq_top.rdl b/hdl/projects/metro_seq/metro_seq_top.rdl new file mode 100644 index 00000000..db085b55 --- /dev/null +++ b/hdl/projects/metro_seq/metro_seq_top.rdl @@ -0,0 +1,29 @@ +addrmap pca9506_axi_regs { + name = "pca9506 registers"; + desc = "Registers accessible on the i2c bus for controlling the emulated PCA9545 i2c mux"; + + default regwidth = 32; + default sw = rw; + default hw = r; + + pca9506_raw_regs #(.SIZE(32)) i2c; + +}; +// Offsets here must match the config_array constant in metro_seq_top.vhd; the +// two are kept in step by hand, as they are in cosmo_seq. +addrmap metro_seq_top { + default regwidth = 32; + + info_regs info @ 0x0; + spi_nor_regs spi_nor @ 0x0100; + sequencer_regs sequencer @ 0x0200; + sp_i2c_regs sp_i2c @ 0x0300; + pca9506_axi_regs fpga1_hotplug @ 0x0400; + dimm_regs dimms @ 0x0500; + debug_regs debug_ctrl @ 0x0600; + hash_engine_regs hash @ 0x0700; + spi_nor_regs versal_flash @ 0x0800; + versal_flash_regs versal_flash_ctrl @ 0x0900; + espi_regs espi @ 0x8000; + espi_regs espi1 @ 0x10000; +}; diff --git a/hdl/projects/metro_seq/metro_seq_top.vhd b/hdl/projects/metro_seq/metro_seq_top.vhd new file mode 100644 index 00000000..308b3a19 --- /dev/null +++ b/hdl/projects/metro_seq/metro_seq_top.vhd @@ -0,0 +1,1126 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +-- Metro Sequencer FPGA targeting the Spartan-7 (U27, XC7S100-1FGGA484I). +-- +-- Structurally this is cosmo_seq's top with the T6 NIC replaced by an AMD +-- Versal Premium VP1202: the SP-facing FMC bus, eSPI/SPI-NOR service, DIMM SPD +-- proxy, hotplug emulation, UART routing and I2C muxing are all the shared +-- blocks, and the Versal sequencing and boot-flash mux are Metro's own. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.numeric_std_unsigned.all; + +use work.axil_common_pkg.all; +use work.axil26x32_pkg; +use work.axil8x32_pkg; +use work.axil32x32_pkg; +use work.axil15x32_pkg; +use work.axi_st8_pkg; +use work.axilite_if_2k19_helper_pkg.all; +use work.i2c_common_pkg.all; +use work.time_pkg.all; +use work.tristate_if_pkg.all; + +use work.sp5_power_pkg.all; +use work.sequencer_io_pkg.all; +use work.sp5_uart_subsystem_pkg.all; + +entity metro_seq_top is + port ( + -- Board clocks and resets + clk_50mhz_fpga1_2 : in std_logic; + clk_50mhz_fpga1_1 : in std_logic; + clk_buff_m2_rsw_to_fpga1_los_l : in std_logic; + clk_buff_nic_to_fpga1_los_l : in std_logic; + sp_to_fpga1_system_reset_l : in std_logic; + -- FMC interface to the SP + fmc_sp_to_fpga1_a : in std_logic_vector(23 downto 16); + fmc_sp_to_fpga1_adv_l : in std_logic; + fmc_sp_to_fpga1_bl_l : in std_logic_vector(1 downto 0); + fmc_sp_to_fpga1_clk : in std_logic; + fmc_sp_to_fpga1_cs_l : in std_logic; + fmc_sp_to_fpga1_da : inout std_logic_vector(15 downto 0); + fmc_sp_to_fpga1_oe_l : in std_logic; + fmc_sp_to_fpga1_wait_l : out std_logic; + fmc_sp_to_fpga1_we_l : in std_logic; + -- eSPI interfaces to the SP5 + espi0_fpga1_to_sp5_alert_l : in std_logic; + espi0_sp5_to_fpga1_clk : in std_logic; + espi0_sp5_to_fpga1_cs_l : in std_logic; + espi0_sp5_to_fpga1_dat : inout std_logic_vector(3 downto 0); + espi1_fpga1_to_sp5_alert_l : in std_logic; + espi1_sp5_to_fpga1_clk : in std_logic; + espi1_sp5_to_fpga1_clk_2 : in std_logic; + espi1_sp5_to_fpga1_cs_l : in std_logic; + espi1_sp5_to_fpga1_dat : inout std_logic_vector(3 downto 0); + espi_sp5_to_fpga1_reset_l : in std_logic; + -- SP5 boot flash (SPI-NOR) + spi0_sp5_to_fpga1_cs_l : in std_logic; + spi1_sp5_to_fpga1_cs_l : in std_logic; + spi2_sp5_to_fpga1_cs_l : in std_logic; + spi_fpga1_to_flash_clk : out std_logic; + spi_fpga1_to_flash_cs_l : out std_logic; + spi_fpga1_to_flash_dat : inout std_logic_vector(3 downto 0); + -- Versal (VP1202) boot straps, status and boot-flash mux + fpga1_to_vercel_flash_qspi_mux_en_l : out std_logic; + fpga1_to_vercel_flash_qspi_mux_sel : out std_logic; + fpga1_to_versal_erro_done_buff_en : out std_logic; + fpga1_to_versal_mode : out std_logic_vector(3 downto 0); + fpga1_to_versal_mode_buffer_en_l : out std_logic; + fpga1_to_versal_por_b : out std_logic; + qspi_fpga1_to_vercel_flash_mux_cs_l : out std_logic; + qspi_fpga1_to_vercel_flash_mux_d : inout std_logic_vector(3 downto 0); + qspi_fpga1_to_vercel_flash_mux_sck : out std_logic; + versal_to_fpga1_done : in std_logic; + versal_to_fpga1_error_out : in std_logic; + -- Versal power rails + fpga1_to_nic_hsc_en : out std_logic; + sp5_to_nic_mfg_mode_l : in std_logic; + v0p88_nic_a0hp_en : out std_logic; + v0p88_nic_a0hp_pg : in std_logic; + v0p8_nic_vccint_a0hp_en : out std_logic; + v0p8_nic_vccint_a0hp_pg : in std_logic; + v0p92_nic_avcc_a0hp_pg : in std_logic; + v12p0_nic_a0hp_pg : in std_logic; + v12p0_nic_a0hp_to_fpga1_fault_l : in std_logic; + v1p1_nic_a0hp_en : out std_logic; + v1p2_nic_avtt_a0hp_pg : in std_logic; + v1p4_nic_a0hp_en : out std_logic; + v1p5_nic_a0hp_en : out std_logic; + v1p5_nic_a0hp_pg : in std_logic; + v1p5_nic_avccaux_a0hp_en : out std_logic; + v1p5_nic_avccaux_a0hp_pg : in std_logic; + v1p8_nic_a0hp_en : out std_logic; + v1p8_nic_a0hp_pg : in std_logic; + v3p3_nic_a0hp_en : out std_logic; + v3p3_nic_a0hp_pg : in std_logic; + v5p0_nic_a0hp_pg : in std_logic; + v5p0_nic_a0hp_to_fpga1_fault_l : in std_logic; + -- Versal PCIe channels + fpga1_to_pcie_clk_buff_nic_cha_oe_l : out std_logic; + fpga1_to_pcie_clk_buff_nic_chb_oe_l : out std_logic; + pcie_fpga1_to_nic_cha_perst_l : out std_logic; + pcie_fpga1_to_nic_chb_perst_l : out std_logic; + pcie_nic_to_fpga1_cha_prsnt_l : in std_logic; + pcie_nic_to_fpga1_cha_pwren_l : in std_logic; + pcie_nic_to_fpga1_chb_prsnt_l : in std_logic; + pcie_nic_to_fpga1_chb_pwren_l : in std_logic; + -- SP5 power rails and sequencing + fpga1_to_sp5_apml_xltr_en : out std_logic; + fpga1_to_sp5_espi_kbrst_l : in std_logic; + fpga1_to_sp5_pwr_btn_l : out std_logic; + fpga1_to_sp5_pwrgd : out std_logic; + fpga1_to_sp5_romtype0 : in std_logic; + fpga1_to_sp5_rsmrst_l : out std_logic; + fpga1_to_sp5_sys_reset_l : out std_logic; + pwr_cont1_to_fpga1_alert_l : in std_logic; + pwr_cont1_to_fpga1_vddcr_cpu0_pg : in std_logic; + pwr_cont1_to_fpga1_vddcr_cpu1_pg : in std_logic; + pwr_cont1_to_fpga1_vddcr_soc_pg : in std_logic; + pwr_cont1_to_fpga1_vddio_sp5_pg : in std_logic; + pwr_cont2_to_fpga1_alert_l : in std_logic; + pwr_cont3_to_fpga1_alert_l : in std_logic; + pwr_cont3_to_fpga1_cfp : in std_logic; + pwr_cont3_to_fpga1_vrhot_n : in std_logic; + pwr_cont4_to_fpga1_alert_l : in std_logic; + pwr_cont4_to_fpga1_cfp : in std_logic; + pwr_cont4_to_fpga1_vrhot_n : in std_logic; + pwr_fpga1_to_v1p5_sp5_rtc_a2_en : out std_logic; + pwr_v1p5_sp5_rtc_a2_to_fpga1_pg : in std_logic; + sp5_to_fpga1_alert_l : in std_logic; + sp5_to_fpga1_debug1 : out std_logic; + sp5_to_fpga1_debug2 : in std_logic; + sp5_to_fpga1_pwrgd_out : in std_logic; + sp5_to_fpga1_pwrok_unbuf : in std_logic; + fpga1_to_sp5_reset_l : in std_logic; + sp5_to_fpga1_slp_s3_l : in std_logic; + sp5_to_fpga1_slp_s5_l : in std_logic; + sp5_to_fpga1_smerr_l : in std_logic; + sp5_to_fpga1_spd_host_ctrl_l : in std_logic; + sp5_to_fpga1_thermtrip_l : in std_logic; + v1p1_i3c_a2_pg : in std_logic; + v1p1_sp5_en : out std_logic; + v1p1_sp5_pg : in std_logic; + v1p2_fpga2_a2_pg : in std_logic; + v1p8_sp5_en : out std_logic; + v1p8_sp5_pg : in std_logic; + v2p5_fpga2_a2_pg : in std_logic; + v2p5_mgmt_a2_pg : in std_logic; + v3p3_fpga2_a2_pg : in std_logic; + v3p3_sp5_en : out std_logic; + v3p3_sp5_pg : in std_logic; + vddcr_cpu0_en : out std_logic; + vddcr_cpu1_en : out std_logic; + vddcr_soc_en : out std_logic; + vddio_sp5_en : out std_logic; + -- DDR bulk power and DIMM power good + dimm_a_pg : in std_logic; + dimm_b_pg : in std_logic; + dimm_c_pg : in std_logic; + dimm_d_pg : in std_logic; + dimm_e_pg : in std_logic; + dimm_f_pg : in std_logic; + dimm_g_pg : in std_logic; + dimm_h_pg : in std_logic; + dimm_i_pg : in std_logic; + dimm_j_pg : in std_logic; + dimm_k_pg : in std_logic; + dimm_l_pg : in std_logic; + fpga1_to_v12_ddr5_abcdef_hsc_en : out std_logic; + fpga1_to_v12_ddr5_ghijkl_hsc_en : out std_logic; + v12_ddr5_abcdef_a0_pg : in std_logic; + v12_ddr5_ghijkl_a0_pg : in std_logic; + -- DIMM SPD I3C + i3c_fpga1_to_dimm_abcdef_scl : inout std_logic; + i3c_fpga1_to_dimm_abcdef_sda : inout std_logic; + i3c_fpga1_to_dimm_ghijkl_scl : inout std_logic; + i3c_fpga1_to_dimm_ghijkl_sda : inout std_logic; + i3c_fpga1_to_dimm_oe_l : out std_logic; + i3c_sp5_to_fpga1_abcdef_scl : inout std_logic; + i3c_sp5_to_fpga1_abcdef_sda : inout std_logic; + i3c_sp5_to_fpga1_ghijkl_scl : inout std_logic; + i3c_sp5_to_fpga1_ghijkl_sda : inout std_logic; + i3c_sp5_to_fpga1_oe_l : out std_logic; + -- M.2 hotplug + fpga1_to_m2_apml_xltr_en : out std_logic; + fpga1_to_m2a_hsc_en : out std_logic; + fpga1_to_m2a_perst_l : out std_logic; + fpga1_to_m2b_hsc_en : out std_logic; + fpga1_to_m2b_perst_l : out std_logic; + fpga1_to_pcie_clk_buff_m2a_oe_l : out std_logic; + fpga1_to_pcie_clk_buff_m2b_oe_l : out std_logic; + m2a_hsc_to_fpga1_fault_l : in std_logic; + m2a_to_fpga1_pedet : in std_logic; + m2a_to_fpga1_prsnt_l : in std_logic; + m2b_hsc_to_fpga1_fault_l : in std_logic; + m2b_to_fpga1_pedet : in std_logic; + m2b_to_fpga1_prsnt_l : in std_logic; + v3p3_m2a_a0hp_pg_l : in std_logic; + v3p3_m2b_a0hp_pg_l : in std_logic; + -- Backplane / rear switch + fpga1_to_bp_buff_output_en_l : out std_logic; + fpga1_to_pcie_clk_buff_rsw_oe_l : out std_logic; + pcie_aux_fpga1_to_rsw_perst_l : out std_logic; + pcie_aux_rsw_to_fpga1_prsnt_buff_l : in std_logic; + pcie_aux_rsw_to_fpga1_pwrflt_buff_l : in std_logic; + rsw_to_sp5_pcie_attached_buff_l : in std_logic; + -- Fans and thermal + fan_central_hsc_to_fpga1_pg : in std_logic; + fan_east_hsc_to_fpga1_pg : in std_logic; + fan_to_fpga1_fan_fail : in std_logic; + fan_west_hsc_to_fpga1_pg : in std_logic; + fpga1_to_fan_central_hsc_disable : out std_logic; + fpga1_to_fan_east_hsc_disable : out std_logic; + fpga1_to_fan_west_hsc_disable : out std_logic; + smbus_fan_central_hsc_to_fpga1_alert_l : in std_logic; + smbus_fan_east_hsc_to_fpga1_alert_l : in std_logic; + smbus_fan_west_hsc_to_fpga1_alert_l : in std_logic; + smbus_therm_ne_to_fpga1_alert_l : in std_logic; + smbus_therm_nw_to_fpga1_alert_l : in std_logic; + smbus_therm_sc_to_fpga1_alert_l : in std_logic; + smbus_therm_se_to_fpga1_alert_l : in std_logic; + smbus_therm_sw_to_fpga1_alert_l : in std_logic; + -- Regulator and hotswap alerts + i2c_sp_to_nic_sysmon_alert_l : in std_logic; + main_hsc_to_fpga1_alert_l : in std_logic; + smbus_ibc_to_fpga1_alert_l : in std_logic; + smbus_m2_hsc_to_fpga1_alert_l : in std_logic; + smbus_nic_hsc_to_fpga1_alert_l : in std_logic; + smbus_v12_ddr5_abcdef_hsc_to_fpga1_alert : in std_logic; + smbus_v12_ddr5_ghijkl_hsc_to_fpga1_alert : in std_logic; + vr_v1p8_sys_to_fpga1_alert_l : in std_logic; + vr_v3p3_sys_to_fpga1_alert_l : in std_logic; + vr_v5p0_sys_to_fpga1_alert_l : in std_logic; + -- I2C to the SP and SP5 + fpga1_to_i2c_mux1_sel : out std_logic_vector(1 downto 0); + fpga1_to_i2c_mux2_sel : out std_logic_vector(1 downto 0); + fpga1_to_i2c_mux3_sel : out std_logic_vector(1 downto 0); + i2c_sp5_sec_v3p3_scl : inout std_logic; + i2c_sp5_sec_v3p3_sda : inout std_logic; + i2c_sp5_to_fpgax_hp_scl : inout std_logic; + i2c_sp5_to_fpgax_hp_sda : inout std_logic; + i2c_sp_to_fpga1_scl : inout std_logic; + i2c_sp_to_fpga1_sda : inout std_logic; + sp_to_fpga1_mux_reset_l : in std_logic; + -- UARTs + uart0_fpga1_to_sp5_dat_buff : out std_logic; + uart0_fpga1_to_sp5_rts_l_buff : out std_logic; + uart0_fpga1_to_sp_dat : out std_logic; + uart0_fpga1_to_sp_rts_l : out std_logic; + uart0_sp5_to_fpga1_dat : in std_logic; + uart0_sp5_to_fpga1_int : in std_logic; + uart0_sp5_to_fpga1_rts_l : in std_logic; + uart0_sp_to_fpga1_dat : in std_logic; + uart0_sp_to_fpga1_rts_l : in std_logic; + uart1_fpga1_to_sp5_dat_buff : out std_logic; + uart1_fpga1_to_sp_dat : out std_logic; + uart1_fpga1_to_sp_rts_l : out std_logic; + uart1_sp5_to_fpga1_dat : in std_logic; + uart1_sp_to_fpga1_dat : in std_logic; + uart1_sp_to_fpga1_rts_l : in std_logic; + uart8_fpga1_to_sp_dat : out std_logic; + uart8_sp_to_fpga1_dat : in std_logic; + uart_debug_to_fpga1_dat : in std_logic; + uart_debug_to_fpga1_rts_l : in std_logic; + uart_fpga1_to_debug_dat : out std_logic; + uart_fpga1_to_debug_rts_l : out std_logic; + uart_local_fpga1_to_sp_dat : in std_logic; + uart_local_fpga1_to_sp_rts_l : in std_logic; + uart_local_sp_to_fpga1_dat : in std_logic; + uart_local_sp_to_fpga1_rts_l : in std_logic; + -- HDT debug mux + hdt_conn_to_mux_testen : in std_logic; + hdt_fpga1_to_mux_dat : in std_logic; + hdt_fpga1_to_mux_dbreq_l : in std_logic; + hdt_fpga1_to_mux_en_l : in std_logic; + hdt_fpga1_to_mux_sel : in std_logic; + hdt_fpga1_to_mux_tck : in std_logic; + hdt_fpga1_to_mux_tms : in std_logic; + hdt_fpga1_to_mux_trst_l : in std_logic; + hdt_fpga1_to_mux_xtrig5_l : in std_logic; + hdt_fpga1_to_mux_xtrig6_l : in std_logic; + hdt_fpga1_to_mux_xtrig7_l : in std_logic; + hdt_mux_to_fpga1_dat : in std_logic; + -- Ignition, straps, spares and misc + fpga1_debug_uart_buf_oe_en_l : out std_logic; + fpga1_espi0_cs_l_buff_oe_en_l : out std_logic; + fpga1_spare_v1p8 : out std_logic_vector(7 downto 0); + fpga1_spare_v3p3_0 : out std_logic; + fpga1_spare_v3p3_1 : out std_logic; + fpga1_spare_v3p3_2 : out std_logic; + fpga1_spare_v3p3_3 : out std_logic; + fpga1_spare_v3p3_4 : in std_logic; + fpga1_spare_v3p3_5 : in std_logic; + fpga1_spare_v3p3_6 : out std_logic; + fpga1_spare_v3p3_7 : out std_logic; + fpga1_status_led : out std_logic; + fpga1_to_fpga2_io : out std_logic_vector(5 downto 0); + fpga1_to_ign_trgt_fpga_creset : out std_logic; + fpga1_to_jtag_mux_sel : out std_logic; + fpga1_to_sp_int_l : in std_logic; + fpga1_to_sp_irq_l : out std_logic_vector(6 downto 1); + fpga1_to_sp_misc_a : in std_logic; + fpga1_to_sp_misc_b : in std_logic; + fpga1_to_sp_misc_c : in std_logic; + fpga1_to_sp_misc_d : in std_logic; + fpga1_to_sp_mux_ign_mux_sel : out std_logic; + fpga1_uart0_buff_oe_en_l : out std_logic; + fpga1_uart1_buff_oe_en_l : out std_logic; + fpga1_version_id : in std_logic_vector(1 downto 0); + sp5_to_fpga1_genint_l : out std_logic; + seq_rev_id : in std_logic_vector(2 downto 0); + ); +end entity; + +architecture rtl of metro_seq_top is + + signal clk_125m : std_logic; + signal reset_125m : std_logic; + signal clk_200m : std_logic; + signal reset_200m : std_logic; + signal reset_fmc : std_logic; + alias fmc_clk : std_logic is fmc_sp_to_fpga1_clk; + -- deskewed/phase-shifted FMC clock from the MMCM in board_support; the + -- FMC domain runs on this, never on the raw pin + signal fmc_clk_buf : std_logic; + signal fmc_capture_clk_buf : std_logic; + constant INFO_RESP_IDX : integer := 0; + constant SPINOR_RESP_IDX: integer := 1; + constant SEQ_RESP_IDX: integer := 2; + constant SP_I2C_RESP_IDX: integer := 3; + constant SP5_HP_RESP_IDX : integer := 4; + constant SPD_PROXY_RESP_IDX : integer := 5; + constant DBG_CTRL_RESP_IDX : integer := 6; + constant ESPI_RESP_IDX: integer := 7; + constant HASH_RESP_IDX : integer := 8; + constant VERSAL_FLASH_RESP_IDX : integer := 9; + constant VERSAL_FLASH_CTRL_RESP_IDX : integer := 10; + constant ESPI1_RESP_IDX : integer := 11; + + constant config_array : axil_responder_cfg_array_t := + (INFO_RESP_IDX => resp_cfg(base_addr => x"00000000", addr_span_bits => 8), + SPINOR_RESP_IDX => resp_cfg(base_addr => x"00000100", addr_span_bits => 8), + SEQ_RESP_IDX => resp_cfg(base_addr => x"00000200", addr_span_bits => 8), + SP_I2C_RESP_IDX => resp_cfg(base_addr => x"00000300", addr_span_bits => 8), + SP5_HP_RESP_IDX => resp_cfg(base_addr => x"00000400", addr_span_bits => 8), + SPD_PROXY_RESP_IDX => resp_cfg(base_addr => x"00000500", addr_span_bits => 8), + DBG_CTRL_RESP_IDX => resp_cfg(base_addr => x"00000600", addr_span_bits => 8), + -- eSPI is the largest register file and the most distant block, and it + -- owns the worst 125MHz path in the design, so give the fabric a cycle + -- in each direction to get there and back. + ESPI_RESP_IDX => resp_cfg(base_addr => x"00008000", addr_span_bits => 15, pipe_stages => 1), + HASH_RESP_IDX => resp_cfg(base_addr => x"00000700", addr_span_bits => 8), + VERSAL_FLASH_RESP_IDX => resp_cfg(base_addr => x"00000800", addr_span_bits => 8), + VERSAL_FLASH_CTRL_RESP_IDX => resp_cfg(base_addr => x"00000900", addr_span_bits => 8), + -- Second eSPI target, same size and same reasoning as the first. + ESPI1_RESP_IDX => resp_cfg(base_addr => x"00010000", addr_span_bits => 15, pipe_stages => 1) + ); + signal fmc_axi_if : axil26x32_pkg.axil_t; + signal fabric_responders : axil32x32_pkg.axil_array_t(config_array'range); + signal responders_8b : axil8x32_pkg.axil_array_t(config_array'range); + signal responders_15b : axil15x32_pkg.axil_array_t(config_array'range); + signal fmc_internal_data_out : std_logic_vector(15 downto 0); + signal fmc_data_out_hiz: std_logic_vector(15 downto 0); + + signal spinor_io_o : std_logic_vector(3 downto 0); + signal spinor_io_oe : std_logic_vector(3 downto 0); + signal espi_io_o : std_logic_vector(3 downto 0); + signal espi_io_oe : std_logic_vector(3 downto 0); + signal espi1_io_o : std_logic_vector(3 downto 0); + signal espi1_io_oe : std_logic_vector(3 downto 0); + -- The eSPI1 wrapper's spi_nor drives the Versal flash pins directly and + -- parks them unless the mux control block has the flash. + signal versal_flash_bus_enable : std_logic; + -- eSPI1 has no IPCC UART behind it; its peripheral channel is tied off. + signal espi1_uart_from_axi_st : axi_st8_pkg.axi_st_t; + signal espi1_uart_to_axi_st : axi_st8_pkg.axi_st_t; + -- hash engine <-> spi_nor flash client port(s) + -- One hash engine serves both flashes: the SP5 boot flash behind eSPI0 + -- and the Versal boot flash behind eSPI1. + constant HASH_NUM_FLASHES : natural := 2; + signal hash_flash_cmd_rdata : std_logic_vector(31 downto 0); + signal hash_flash_cmd_rdack : std_logic_vector(HASH_NUM_FLASHES - 1 downto 0); + signal hash_flash_cmd_rempty : std_logic_vector(HASH_NUM_FLASHES - 1 downto 0); + signal hash_flash_rsp_wdata : std_logic_vector(HASH_NUM_FLASHES * 8 - 1 downto 0); + signal hash_flash_rsp_write : std_logic_vector(HASH_NUM_FLASHES - 1 downto 0); + + signal ipcc_uart_from_espi_axi_st : axi_st8_pkg.axi_st_t; + signal ipcc_uart_to_espi_axi_st : axi_st8_pkg.axi_st_t; + signal a0_ok : std_logic; + signal a0_idle : std_logic; + signal ddr_bulk : ddr_bulk_power_t; + signal sp5_group_a : group_a_power_t; + signal sp5_group_b : group_b_power_t; + signal sp5_group_c : group_c_power_t; + signal versal_rails : versal_power_t; + signal versal_boot : versal_boot_t; + signal versal_pcie : versal_pcie_t; + signal versal_held_in_reset : std_logic; + signal flash_owned_by_seq : std_logic; + signal versal_hash_req : std_logic; + signal versal_hash_ack : std_logic; + signal versal_hash_err : std_logic; + signal sp5_seq_pins : sp5_seq_pins_t; + signal early_power : early_power_t; + signal sp_scl_o : std_logic; + signal sp_scl_oe : std_logic; + signal sp_sda_o : std_logic; + signal sp_sda_oe : std_logic; + signal sp5_scl_o : std_logic; + signal sp5_scl_oe : std_logic; + signal sp5_sda_o : std_logic; + signal sp5_sda_oe : std_logic; + + signal sp5_versal_power_en : std_logic; + signal sp5_versal_cha_perst_l : std_logic; + signal sp5_versal_chb_perst_l : std_logic; + signal sp5_versal_faulted : std_logic; + signal vercel_flash_io_o : std_logic_vector(3 downto 0); + signal vercel_flash_io_oe : std_logic_vector(3 downto 0); + signal espi_resp_csn : std_logic; + signal hp_int_n : std_logic; + + signal fpga1_to_pcie_clk_buff_rsw_oe_l_int : std_logic; + + signal sp5_abcdef_scl_if : tristate; + signal sp5_abcdef_sda_if : tristate; + signal sp5_ghijkl_scl_if : tristate; + signal sp5_ghijkl_sda_if : tristate; + signal dimm_abcdef_scl_if : tristate; + signal dimm_abcdef_sda_if : tristate; + signal dimm_ghijkl_scl_if : tristate; + signal dimm_ghijkl_sda_if : tristate; + + signal amd_hp_irq_n_final : std_logic; + -- Metro's FPGA2 sends its hotplug interrupts straight to the SP + -- (FPGA2_TO_SP_INT[1..3]), so unlike cosmo there is no FPGA2 IRQ arriving + -- here to fold in. + alias a0_ok_to_fpga2 : std_logic is fpga1_to_fpga2_io(2); + signal uart_dbg_if : uart_dbg_t; + signal allow_backplane_pcie_clk : std_logic; + signal versal_dbg_pins : nic_debug_if; + signal reg_alert_l_pins : seq_power_alert_pins_t; + -- No T6 on this board; the shared sequencer's T6 ports are tied off to + -- these and its T6 outputs left open. + signal nic_rails_unused : nic_power_t := nic_power_absent; + signal nic_seq_pins_unused : nic_seq_pins_t := nic_seq_pins_absent; + signal dbg_pins_uart_out : std_logic; + signal dbg_pins_uart_out_rts_l : std_logic; + signal dbg_pins_uart_in : std_logic; + signal dbg_pins_uart_in_rts_l : std_logic; + signal uart_headder_fall_back_to_debug_pins : std_logic; + signal sp_mux_reset_l_syncd : std_logic; + +begin + + meta_sync_inst_mux_reset_l: entity work.meta_sync + port map( + async_input => sp_to_fpga1_mux_reset_l, + clk => clk_125m, + sycnd_output => sp_mux_reset_l_syncd + ); + + -- SP5 SEC I2C: pins are wired but nothing drives this bus yet. + i2c_sp5_sec_v3p3_scl <= 'Z'; + i2c_sp5_sec_v3p3_sda <= 'Z'; + -- misc things tied: + fpga1_to_fpga2_io(5 downto 3) <= (others => 'Z'); + fpga1_to_fpga2_io(1 downto 0) <= (others => 'Z'); + fpga1_to_sp5_sys_reset_l <= 'Z'; -- We don't use this in product, external PU. + fpga1_to_sp_irq_l(6 downto 2) <= (others => '1'); + -- The JTAG mux stays pointed at the external header; the FPGA only takes it + -- when someone deliberately drives this from a debug session. + fpga1_to_jtag_mux_sel <= '0'; + -- Metro adds a buffer enable for the dedicated debug UART header alongside + -- the two SP-facing ones. + fpga1_debug_uart_buf_oe_en_l <= '0'; + -- The low half of the 3V3 spare header. Nothing drives these, and they go + -- to a header someone may well jumper, so hold them off rather than leaving + -- output ports undriven for the tools to resolve however they like. + fpga1_spare_v3p3_0 <= 'Z'; + fpga1_spare_v3p3_1 <= 'Z'; + fpga1_spare_v3p3_2 <= 'Z'; + fpga1_spare_v3p3_3 <= 'Z'; + fpga1_to_bp_buff_output_en_l <= '0'; -- This buffer has to be enabled to see any BP PCIe signals + -- Enable various buffers when we're in A0: + fpga1_espi0_cs_l_buff_oe_en_l <= '0' when sp5_seq_pins.pwr_good else 'Z'; + fpga1_to_sp5_apml_xltr_en <= sp5_seq_pins.pwr_good; + fpga1_to_m2_apml_xltr_en <= sp5_seq_pins.pwr_good; + fpga1_uart0_buff_oe_en_l <= '0' when a0_ok else '1'; + fpga1_uart1_buff_oe_en_l <= '0' when a0_ok else '1'; -- not used but why not enable anyway? + uart1_fpga1_to_sp5_dat_buff <= '1'; -- Make this idle generally, buffer protects from cross-drive + + i3c_sp5_to_fpga1_oe_l <= '0' when sp5_seq_pins.pwr_good else '1'; + -- Metro does not carry the rev1 cosmo buffer bug, so the DIMM side can be + -- enabled regardless of SP5 power state, which is what we actually want. + i3c_fpga1_to_dimm_oe_l <= '0'; + + --------------------------------------------- + -- FMC to AXI Interface from the SP + --------------------------------------------- + stm32h7_fmc_target_inst: entity work.stm32h7_fmc_target + port map( + chip_reset => reset_fmc, + fmc_clk => fmc_clk_buf, + fmc_capture_clk => fmc_capture_clk_buf, + a(24 downto 20) => "00000", + a(19 downto 16) => fmc_sp_to_fpga1_a(19 downto 16), + --a(23 downto 16) => fmc_sp_to_fpga1_a, + addr_data_in => fmc_sp_to_fpga1_da, + data_out => fmc_internal_data_out, + data_out_hiz => fmc_data_out_hiz, + ne(3 downto 1) => "111", + ne(0) => fmc_sp_to_fpga1_cs_l, + noe => fmc_sp_to_fpga1_oe_l, + nwe => fmc_sp_to_fpga1_we_l, + nl => fmc_sp_to_fpga1_adv_l, + nwait => fmc_sp_to_fpga1_wait_l, + timeout_count => open, + contention_count => open, + aclk => clk_125m, + aresetn => not reset_125m, + axi_if => fmc_axi_if + ); + -- tristate control for the FMC data bus + -- per-bit tristate, hiz already in OBUFT T polarity so each pin's T + -- flop packs into its IOB with no inverter in between + fmc_da_tris: for i in fmc_sp_to_fpga1_da'range generate + fmc_sp_to_fpga1_da(i) <= 'Z' when fmc_data_out_hiz(i) = '1' else fmc_internal_data_out(i); + end generate; + + -- Axi decode/interconnect + axil_interconnect_inst: entity work.axil_interconnect + generic map( + config_array => config_array + ) + port map( + clk => clk_125m, + reset => reset_125m, + initiator => fmc_axi_if, + responders => fabric_responders + ); + + -- Block that generates our clocks, resets and + -- deals with core board-level functionality + -- includes the common "info" block on the axi bus + resize_axil(fabric_responders(INFO_RESP_IDX), responders_8b(INFO_RESP_IDX)); + board_support_inst: entity work.board_support + port map( + board_50mhz_clk => clk_50mhz_fpga1_1, + sp_fmc_clk => fmc_clk, + fmc_clk_buf => fmc_clk_buf, + fmc_capture_clk_buf => fmc_capture_clk_buf, + sp_system_reset_l => sp_to_fpga1_system_reset_l, + clk_125m => clk_125m, + reset_125m => reset_125m, + clk_200m => clk_200m, + reset_200m => reset_200m, + reset_fmc => reset_fmc, + fpga1_status_led => fpga1_status_led, + hubris_compat_ver => seq_rev_id, + info_axi_if => responders_8b(INFO_RESP_IDX) + ); + + -- espi and flash interface block + -- espi and spi-nor blocks manage their own synchronization. + -- only a tiny portion of the espi design runs at 200MHz + -- all the system interfaces run at 125MHz for common clocking + resize_axil(fabric_responders(ESPI_RESP_IDX), responders_15b(ESPI_RESP_IDX)); + resize_axil(fabric_responders(SPINOR_RESP_IDX), responders_8b(SPINOR_RESP_IDX)); + resize_axil(fabric_responders(HASH_RESP_IDX), responders_8b(HASH_RESP_IDX)); + espi_spinor_ss: entity work.sp5_espi_flash_subsystem + port map( + clk_125m => clk_125m, + reset_125m => reset_125m, + clk_200m => clk_200m, + reset_200m => reset_200m, + espi_axi_if => responders_15b(ESPI_RESP_IDX), + espi_csn => espi0_sp5_to_fpga1_cs_l, + espi_clk => espi0_sp5_to_fpga1_clk, + espi_dat => espi0_sp5_to_fpga1_dat, + espi_dat_o => espi_io_o, + espi_dat_oe => espi_io_oe, + response_csn => espi_resp_csn, -- debugging with saleae if you have access + ipcc_uart_from_espi => ipcc_uart_from_espi_axi_st, + ipcc_uart_to_espi => ipcc_uart_to_espi_axi_st, + spinor_axi_if => responders_8b(SPINOR_RESP_IDX), + spi_nor_csn => spi_fpga1_to_flash_cs_l, + spi_nor_clk => spi_fpga1_to_flash_clk, + spi_nor_dat => spi_fpga1_to_flash_dat, + spi_nor_dat_o => spinor_io_o, + spi_nor_dat_oe => spinor_io_oe, + hash_cmd_fifo_rdata => hash_flash_cmd_rdata, + hash_cmd_fifo_rdack => hash_flash_cmd_rdack(0), + hash_cmd_fifo_rempty => hash_flash_cmd_rempty(0), + hash_data_fifo_wdata => hash_flash_rsp_wdata(7 downto 0), + hash_data_fifo_write => hash_flash_rsp_write(0) + ); + + -- SHA3 hashing engine. It reads flash through spi_nor_top's second client + -- port and owns the FIFOs on that path; it sits here rather than inside the + -- eSPI wrapper so one engine can serve more than one flash. + hash_engine_inst: entity work.hash_engine_top + generic map( + NUM_FLASHES => HASH_NUM_FLASHES, + -- The sequencer's pre-boot measurement is of the Versal image, on + -- the flash behind eSPI1. + HW_FLASH_SEL => 1 + ) + port map( + clk => clk_125m, + reset => reset_125m, + axi_if => responders_8b(HASH_RESP_IDX), + hw_req => versal_hash_req, + hw_ack => versal_hash_ack, + hw_err => versal_hash_err, + flash_cmd_rdata => hash_flash_cmd_rdata, + flash_cmd_rdack => hash_flash_cmd_rdack, + flash_cmd_rempty => hash_flash_cmd_rempty, + flash_rsp_wdata => hash_flash_rsp_wdata, + flash_rsp_write => hash_flash_rsp_write + ); + -- Second eSPI target on the SP5's other eSPI port, fronting the Versal's + -- boot flash. This is the one place the host may write flash over SAFS, + -- which is how a Versal image gets loaded; the enable bit in its control + -- register still has to be set by the SP. There is no IPCC UART behind + -- this port, and no post codes either: the SP5 only writes those to + -- eSPI0, so the buffer for them is left out. + -- Its spi_nor runs at half the SP5 boot flash's rate, 125MHz + -- / (2 * 2) = 31.25MHz: nothing here is on a boot path, the SP only + -- reaches this flash while the Versal is held in reset, so there is no + -- reason to run at the ceiling. The slower rate widens the read sample + -- window from roughly 3.6..11.7ns to -4.4..19.7ns, which leaves + -- rx_sample_taps = 2 (8ns) far from either edge instead of about 4ns + -- clear of both. metro_timing.xdc carries the arithmetic. + resize_axil(fabric_responders(ESPI1_RESP_IDX), responders_15b(ESPI1_RESP_IDX)); + resize_axil(fabric_responders(VERSAL_FLASH_RESP_IDX), responders_8b(VERSAL_FLASH_RESP_IDX)); + espi1_uart_from_axi_st.ready <= '1'; + espi1_uart_to_axi_st.valid <= '0'; + espi1_uart_to_axi_st.data <= (others => '0'); + espi1_versal_flash_ss: entity work.sp5_espi_flash_subsystem + generic map( + FLASH_WRITES_ALLOWED => true, + POST_CODE_BUFFER_ENABLED => false, + SPI_NOR_SCLK_DIVISOR => 1, + SPI_NOR_RX_SAMPLE_TAPS => 2 + ) + port map( + clk_125m => clk_125m, + reset_125m => reset_125m, + clk_200m => clk_200m, + reset_200m => reset_200m, + espi_axi_if => responders_15b(ESPI1_RESP_IDX), + espi_csn => espi1_sp5_to_fpga1_cs_l, + espi_clk => espi1_sp5_to_fpga1_clk, + espi_dat => espi1_sp5_to_fpga1_dat, + espi_dat_o => espi1_io_o, + espi_dat_oe => espi1_io_oe, + response_csn => open, + ipcc_uart_from_espi => espi1_uart_from_axi_st, + ipcc_uart_to_espi => espi1_uart_to_axi_st, + spinor_axi_if => responders_8b(VERSAL_FLASH_RESP_IDX), + spi_nor_csn => qspi_fpga1_to_vercel_flash_mux_cs_l, + spi_nor_clk => qspi_fpga1_to_vercel_flash_mux_sck, + spi_nor_dat => qspi_fpga1_to_vercel_flash_mux_d, + spi_nor_dat_o => vercel_flash_io_o, + spi_nor_dat_oe => vercel_flash_io_oe, + spi_nor_bus_enable => versal_flash_bus_enable, + hash_cmd_fifo_rdata => hash_flash_cmd_rdata, + hash_cmd_fifo_rdack => hash_flash_cmd_rdack(1), + hash_cmd_fifo_rempty => hash_flash_cmd_rempty(1), + hash_data_fifo_wdata => hash_flash_rsp_wdata(15 downto 8), + hash_data_fifo_write => hash_flash_rsp_write(1) + ); + + --Tristates for spi-nor flash pins and espi + spi_nor_espi_tris:process(all) + begin + for i in spi_fpga1_to_flash_dat'range loop + spi_fpga1_to_flash_dat(i) <= spinor_io_o(i) when spinor_io_oe(i) = '1' else 'Z'; + espi0_sp5_to_fpga1_dat(i) <= espi_io_o(i) when espi_io_oe(i) = '1' else 'Z'; + espi1_sp5_to_fpga1_dat(i) <= espi1_io_o(i) when espi1_io_oe(i) = '1' else 'Z'; + end loop; + end process; + + -- UART subsystem + -- stuff externally synchronized inside the UART block(s) + sp5_uart_ss: entity work.sp5_uart_subsystem + port map( + clk => clk_125m, + reset => reset_125m, + dbg_if => uart_dbg_if, + in_a0 => a0_ok, + -- UART pins + -- IPCC SP side + ipcc_from_sp => uart1_sp_to_fpga1_dat, + ipcc_to_sp => uart1_fpga1_to_sp_dat, + ipcc_from_sp_rts_l => uart1_sp_to_fpga1_rts_l, + ipcc_to_sp_rts_l => uart1_fpga1_to_sp_rts_l, + -- UART0 SP-side + console_from_sp => uart0_sp_to_fpga1_dat, + console_to_sp_dat => uart0_fpga1_to_sp_dat, + console_to_sp_rts_l => uart0_fpga1_to_sp_rts_l, + console_from_sp_rts_l => uart0_sp_to_fpga1_rts_l, + -- UART0 SP5-side + host_from_fpga => uart0_fpga1_to_sp5_dat_buff, + host_to_fpga => uart0_sp5_to_fpga1_dat, + host_from_fpga_rts_l => uart0_fpga1_to_sp5_rts_l_buff, + host_to_fpga_rts_l => uart0_sp5_to_fpga1_rts_l, + uart_from_fpga => open, + uart_to_fpga => '1', + uart_from_fpga_rts_l => open, + uart_to_fpga_rts_l => '0', + -- IPCC "UART" from espi + ipcc_from_espi => ipcc_uart_from_espi_axi_st, + ipcc_to_espi => ipcc_uart_to_espi_axi_st, + -- + dbg_pins_uart_out => dbg_pins_uart_out, + dbg_pins_uart_out_rts_l => dbg_pins_uart_out_rts_l, + dbg_pins_uart_in => dbg_pins_uart_in, + dbg_pins_uart_in_rts_l => dbg_pins_uart_in_rts_l + ); + + -- Metro has a dedicated debug UART header, so there is no fallback onto the + -- spare pins and no rev-conditional NIC power-good remap the way cosmo has. + uart_fpga1_to_debug_dat <= dbg_pins_uart_out; + dbg_pins_uart_in <= uart_debug_to_fpga1_dat; + uart_fpga1_to_debug_rts_l <= dbg_pins_uart_in_rts_l; + dbg_pins_uart_out_rts_l <= uart_debug_to_fpga1_rts_l; + fpga1_spare_v3p3_6 <= 'Z'; + fpga1_spare_v3p3_7 <= 'Z'; + + -- UART8 is a Metro addition: a fifth SP-facing UART, data only, with no + -- flow control and no peer defined on the schematic. Park it until we know + -- what it is meant to carry, rather than guessing at a mapping. + uart8_fpga1_to_sp_dat <= '1'; + + -- SP I2C muxes + -- i2c is the only input, sycn'd inside the mux block(s) + resize_axil(fabric_responders(SP_I2C_RESP_IDX), responders_8b(SP_I2C_RESP_IDX)); + sp_i2c_subsystem_inst: entity work.sp_i2c_subsystem + port map( + clk => clk_125m, + reset => reset_125m, + in_a0 => a0_ok, + sp_mux_reset_l => sp_mux_reset_l_syncd, + axi_if => responders_8b(SP_I2C_RESP_IDX), + sp_scl => i2c_sp_to_fpga1_scl, + sp_scl_o => sp_scl_o, + sp_scl_oe => sp_scl_oe, + sp_sda => i2c_sp_to_fpga1_sda, + sp_sda_o => sp_sda_o, + sp_sda_oe => sp_sda_oe, + i2c_mux1_sel => fpga1_to_i2c_mux1_sel, + i2c_mux2_sel => fpga1_to_i2c_mux2_sel, + i2c_mux3_sel => fpga1_to_i2c_mux3_sel, + -- cosmo drives an M.2 translator enable off mux1; metro's translator + -- enable is tied to SP5 power good above, so leave this open. + i2c_mux1_en => open + ); + --Tristates for spi-nor flash pins and espi + i2c_sp_to_fpga1_scl <= sp_scl_o when sp_scl_oe = '1' else 'Z'; + i2c_sp_to_fpga1_sda <= sp_sda_o when sp_sda_oe = '1' else 'Z'; + + -- SP5 I2c hotplug expanders + -- Inputs synchronized inside the block + resize_axil(fabric_responders(SP5_HP_RESP_IDX), responders_8b(SP5_HP_RESP_IDX)); + sp5_hotplug_subsystem_inst: entity work.sp5_hotplug_subsystem + generic map( + NIC2_SLOT_ENABLED => true + ) + port map( + clk => clk_125m, + reset => reset_125m, + axi_if => responders_8b(SP5_HP_RESP_IDX), + allow_backplane_pcie_clk => allow_backplane_pcie_clk, + sp5_i2c_sda => i2c_sp5_to_fpgax_hp_sda, + sp5_i2c_sda_o => sp5_sda_o, + sp5_i2c_sda_oe => sp5_sda_oe, + sp5_i2c_scl => i2c_sp5_to_fpgax_hp_scl, + sp5_i2c_scl_o => sp5_scl_o, + sp5_i2c_scl_oe => sp5_scl_oe, + int_n => hp_int_n, + a0_ok => a0_ok, + m2a_pedet => m2a_to_fpga1_pedet, + m2a_prsnt_l => m2a_to_fpga1_prsnt_l, + m2a_hsc_en => fpga1_to_m2a_hsc_en, + m2a_perst_l => fpga1_to_m2a_perst_l, + m2a_pwr_fault_l => m2a_hsc_to_fpga1_fault_l, + pcie_clk_buff_m2a_oe_l => fpga1_to_pcie_clk_buff_m2a_oe_l, + m2b_pedet => m2b_to_fpga1_pedet, + m2b_prsnt_l => m2b_to_fpga1_prsnt_l, + m2b_hsc_en => fpga1_to_m2b_hsc_en, + m2b_perst_l => fpga1_to_m2b_perst_l, + m2b_pwr_fault_l => m2b_hsc_to_fpga1_fault_l, + pcie_clk_buff_m2b_oe_l => fpga1_to_pcie_clk_buff_m2b_oe_l, + -- The Versal takes the slot the T6 has on cosmo for its channel A, + -- and the otherwise unused fifth expander bank for channel B. Both + -- are the one device, so both slots report the same fault; each has + -- its own presence and its own PERST. + t6_power_en => sp5_versal_power_en, + t6_perst_l => sp5_versal_cha_perst_l, + t6_faulted => sp5_versal_faulted, + t6_prsnt_l => pcie_nic_to_fpga1_cha_prsnt_l, + nic2_power_en => open, + nic2_perst_l => sp5_versal_chb_perst_l, + nic2_faulted => sp5_versal_faulted, + nic2_prsnt_l => pcie_nic_to_fpga1_chb_prsnt_l, + pcie_aux_rsw_perst_l => pcie_aux_fpga1_to_rsw_perst_l, + pcie_aux_rsw_prsnt_buff_l => pcie_aux_rsw_to_fpga1_prsnt_buff_l, + pcie_aux_rsw_pwrflt_buff_l=> pcie_aux_rsw_to_fpga1_pwrflt_buff_l, + pcie_clk_buff_rsw_oe_l => fpga1_to_pcie_clk_buff_rsw_oe_l_int, + rsw_sp5_pcie_attached_buff_l =>rsw_to_sp5_pcie_attached_buff_l + ); + + a0_ok_to_fpga2 <= a0_ok; -- A0 OK signal to fpga2, used for power sequencing + + amd_hp_irq_n_final <= hp_int_n; + sp5_to_fpga1_genint_l <= '0' when amd_hp_irq_n_final = '0' else 'Z'; + + fpga1_to_pcie_clk_buff_rsw_oe_l <= '0' when fpga1_to_pcie_clk_buff_rsw_oe_l_int = '0' else 'Z'; + + --Tristates for spi-nor flash pins and espi + i2c_sp5_to_fpgax_hp_scl <= sp5_scl_o when sp5_scl_oe = '1' else 'Z'; + i2c_sp5_to_fpgax_hp_sda <= sp5_sda_o when sp5_sda_oe = '1' else 'Z'; + + --Block that deals with sequencing the SP5 and nic etc + -- inputs synchronized inside the block + resize_axil(fabric_responders(SEQ_RESP_IDX), responders_8b(SEQ_RESP_IDX)); + seq: entity work.sp5_sequencer + generic map( + CNTS_P_MS => calc_ms(desired_ms => 1, clk_period_ns => 8), + NIC_KIND => NIC_VERSAL + ) + port map( + clk => clk_125m, + reset => reset_125m, + axi_if => responders_8b(SEQ_RESP_IDX), + a0_ok => a0_ok, + a0_idle => a0_idle, + irq_l_out => fpga1_to_sp_irq_l(1), + allow_backplane_pcie_clk => allow_backplane_pcie_clk, + early_power_pins => early_power, + ddr_bulk_pins => ddr_bulk, + group_a_pins => sp5_group_a, + group_b_pins => sp5_group_b, + group_c_pins => sp5_group_c, + sp5_seq_pins => sp5_seq_pins, + versal_rails_pins => versal_rails, + versal_boot_pins => versal_boot, + versal_pcie_pins => versal_pcie, + nic_dbg_pins => versal_dbg_pins, + nic_rails_pins => nic_rails_unused, + nic_seq_pins => nic_seq_pins_unused, + versal_held_in_reset => versal_held_in_reset, + flash_owned_by_seq => flash_owned_by_seq, + hash_req => versal_hash_req, + hash_ack => versal_hash_ack, + hash_err => versal_hash_err, + version_id => fpga1_version_id, + sp5_nic_perst_l => sp5_versal_cha_perst_l, + sp5_nic_chb_perst_l => sp5_versal_chb_perst_l, + sp5_nic_faulted => sp5_versal_faulted, + ignition_mux_sel => fpga1_to_sp_mux_ign_mux_sel, + ignition_creset => fpga1_to_ign_trgt_fpga_creset, + reg_alert_l_pins => reg_alert_l_pins + ); + + -- early power related pins + early_power.fan_central_hsc_pg <= fan_central_hsc_to_fpga1_pg; + early_power.fan_east_hsc_pg <= fan_east_hsc_to_fpga1_pg; + early_power.fan_west_hsc_pg <= fan_west_hsc_to_fpga1_pg; + early_power.fan_fail <= fan_to_fpga1_fan_fail; + fpga1_to_fan_central_hsc_disable <= early_power.fan_central_hsc_disable; + fpga1_to_fan_east_hsc_disable <= early_power.fan_east_hsc_disable; + fpga1_to_fan_west_hsc_disable <= early_power.fan_west_hsc_disable; + + -- Bulk DDR power control and HSC readback + ddr_bulk.abcdef_hsc.pg <= v12_ddr5_abcdef_a0_pg; + fpga1_to_v12_ddr5_abcdef_hsc_en <= ddr_bulk.abcdef_hsc.enable; + ddr_bulk.ghijkl_hsc.pg <= v12_ddr5_ghijkl_a0_pg; + fpga1_to_v12_ddr5_ghijkl_hsc_en <= ddr_bulk.ghijkl_hsc.enable; + -- SP5 rails + -- group A enables and PGs + pwr_fpga1_to_v1p5_sp5_rtc_a2_en <= sp5_group_a.pwr_v1p5_rtc.enable; + sp5_group_a.pwr_v1p5_rtc.pg <= pwr_v1p5_sp5_rtc_a2_to_fpga1_pg; + v3p3_sp5_en <= sp5_group_a.v3p3_sp5_a1.enable; + sp5_group_a.v3p3_sp5_a1.pg <= v3p3_sp5_pg; + v1p8_sp5_en <= sp5_group_a.v1p8_sp5_a1.enable; + sp5_group_a.v1p8_sp5_a1.pg <= v1p8_sp5_pg; + -- group B enable and pg + v1p1_sp5_en <= sp5_group_b.v1p1_sp5.enable; + sp5_group_b.v1p1_sp5.pg <= v1p1_sp5_pg; + -- group C enables and pgs + vddio_sp5_en <= sp5_group_c.vddio_sp5_a0.enable; + sp5_group_c.vddio_sp5_a0.pg <= pwr_cont1_to_fpga1_vddio_sp5_pg; + vddcr_cpu1_en <= sp5_group_c.vddcr_cpu1.enable; + sp5_group_c.vddcr_cpu1.pg <= pwr_cont1_to_fpga1_vddcr_cpu1_pg; + vddcr_cpu0_en <= sp5_group_c.vddcr_cpu0.enable; + sp5_group_c.vddcr_cpu0.pg <= pwr_cont1_to_fpga1_vddcr_cpu0_pg; + vddcr_soc_en <= sp5_group_c.vddcr_soc.enable; + sp5_group_c.vddcr_soc.pg <= pwr_cont1_to_fpga1_vddcr_soc_pg; + -- Versal rails. Every rail here has its own enable, unlike cosmo's T6 + -- where a single hotswap enable cascaded the lot. + -- + -- The two hotswap power goods are active low at the pin, despite the port + -- names, which follow cosmo_seq's. Pass them through raw: versal_sync is + -- the single place that inverts them, exactly as seq_sync does on cosmo. + -- Inverting here as well would double up and hand the sequencer the + -- opposite of the truth. + fpga1_to_nic_hsc_en <= versal_rails.hsc_12v.enable; + versal_rails.hsc_12v.pg <= v12p0_nic_a0hp_pg; + versal_rails.hsc_5v.pg <= v5p0_nic_a0hp_pg; + v3p3_nic_a0hp_en <= versal_rails.v3p3.enable; + versal_rails.v3p3.pg <= v3p3_nic_a0hp_pg; + v1p8_nic_a0hp_en <= versal_rails.v1p8.enable; + versal_rails.v1p8.pg <= v1p8_nic_a0hp_pg; + v1p5_nic_a0hp_en <= versal_rails.v1p5.enable; + versal_rails.v1p5.pg <= v1p5_nic_a0hp_pg; + v1p5_nic_avccaux_a0hp_en <= versal_rails.v1p5_avccaux.enable; + versal_rails.v1p5_avccaux.pg <= v1p5_nic_avccaux_a0hp_pg; + v1p4_nic_a0hp_en <= versal_rails.v1p4.enable; + -- There is no power-good pin for the 1V4 rail on this board, so mirror its + -- enable the way cosmo does for its rev1 1V4. + versal_rails.v1p4.pg <= versal_rails.v1p4.enable; + v1p1_nic_a0hp_en <= versal_rails.v1p1.enable; + -- Likewise no discrete 1V1 power good; it comes up with the aux group. + versal_rails.v1p1.pg <= versal_rails.v1p1.enable; + v0p88_nic_a0hp_en <= versal_rails.v0p88.enable; + versal_rails.v0p88.pg <= v0p88_nic_a0hp_pg; + v0p8_nic_vccint_a0hp_en <= versal_rails.v0p8_vccint.enable; + versal_rails.v0p8_vccint.pg <= v0p8_nic_vccint_a0hp_pg; + -- Transceiver rails: readback only, they cascade off the groups above. + versal_rails.v0p92_avcc.pg <= v0p92_nic_avcc_a0hp_pg; + versal_rails.v1p2_avtt.pg <= v1p2_nic_avtt_a0hp_pg; + + -- SP5 sequence-related pins + sp5_seq_pins.thermtrip_l <= sp5_to_fpga1_thermtrip_l; + sp5_seq_pins.smerr_l <= sp5_to_fpga1_smerr_l; + sp5_seq_pins.reset_l <= fpga1_to_sp5_reset_l; + sp5_seq_pins.pwr_ok <= sp5_to_fpga1_pwrok_unbuf; + fpga1_to_sp5_pwr_btn_l <= '0' when sp5_seq_pins.pwr_btn_l = '0' else 'Z'; + sp5_seq_pins.slp_s3_l <= sp5_to_fpga1_slp_s3_l; + sp5_seq_pins.slp_s5_l <= sp5_to_fpga1_slp_s5_l; + fpga1_to_sp5_rsmrst_l <= sp5_seq_pins.rsmrst_l; + -- Board-type strap the SP5 samples at power up. a1_a0_seq drives its + -- is_cosmo output high here because it is cosmo's block; Metro is not + -- cosmo, so hold the pin low and leave that output unread until Metro's own + -- board-identity convention is settled with the SP5 firmware. + sp5_to_fpga1_debug1 <= '0'; + fpga1_to_sp5_pwrgd <= sp5_seq_pins.pwr_good; + + -- Versal boot straps and status + fpga1_to_versal_por_b <= versal_boot.por_b; + fpga1_to_versal_mode <= versal_boot.mode; + fpga1_to_versal_mode_buffer_en_l <= versal_boot.mode_buffer_en_l; + fpga1_to_versal_erro_done_buff_en <= versal_boot.err_done_buff_en; + versal_boot.done <= versal_to_fpga1_done; + versal_boot.error_out <= versal_to_fpga1_error_out; + + -- Versal PCIe, two channels. The clock buffer output enables are open + -- drain on this board, same as the M.2 and backplane ones. + pcie_fpga1_to_nic_cha_perst_l <= versal_pcie.cha.perst_l; + versal_pcie.cha.prsnt_l <= pcie_nic_to_fpga1_cha_prsnt_l; + versal_pcie.cha.pwren_l <= pcie_nic_to_fpga1_cha_pwren_l; + fpga1_to_pcie_clk_buff_nic_cha_oe_l <= '0' when versal_pcie.cha.clk_buff_oe_l = '0' else 'Z'; + pcie_fpga1_to_nic_chb_perst_l <= versal_pcie.chb.perst_l; + versal_pcie.chb.prsnt_l <= pcie_nic_to_fpga1_chb_prsnt_l; + versal_pcie.chb.pwren_l <= pcie_nic_to_fpga1_chb_pwren_l; + fpga1_to_pcie_clk_buff_nic_chb_oe_l <= '0' when versal_pcie.chb.clk_buff_oe_l = '0' else 'Z'; + + -- Versal boot flash mux on sheet 137. The controller behind it is the + -- eSPI1 wrapper's spi_nor above; this block only gates its pins on the + -- mux being granted, which needs the Versal held in reset. + resize_axil(fabric_responders(VERSAL_FLASH_CTRL_RESP_IDX), responders_8b(VERSAL_FLASH_CTRL_RESP_IDX)); + versal_flash_ss: entity work.versal_flash_subsystem + port map( + clk => clk_125m, + reset => reset_125m, + ctrl_axi_if => responders_8b(VERSAL_FLASH_CTRL_RESP_IDX), + versal_held_in_reset => versal_held_in_reset, + flash_owned_by_seq => flash_owned_by_seq, + flash_qspi_mux_sel => fpga1_to_vercel_flash_qspi_mux_sel, + flash_qspi_mux_en_l => fpga1_to_vercel_flash_qspi_mux_en_l, + flash_bus_enable => versal_flash_bus_enable + ); + vercel_flash_tris: process(all) + begin + for i in qspi_fpga1_to_vercel_flash_mux_d'range loop + qspi_fpga1_to_vercel_flash_mux_d(i) <= + vercel_flash_io_o(i) when vercel_flash_io_oe(i) = '1' else 'Z'; + end loop; + end process; + + -- SP5 <-> FPGA busses (filtered in proxy block) + i3c_sp5_to_fpga1_abcdef_scl <= sp5_abcdef_scl_if.o when sp5_abcdef_scl_if.oe else 'Z'; + sp5_abcdef_scl_if.i <= i3c_sp5_to_fpga1_abcdef_scl; + i3c_sp5_to_fpga1_abcdef_sda <= sp5_abcdef_sda_if.o when sp5_abcdef_sda_if.oe else 'Z'; + sp5_abcdef_sda_if.i <= i3c_sp5_to_fpga1_abcdef_sda; + i3c_sp5_to_fpga1_ghijkl_scl <= sp5_ghijkl_scl_if.o when sp5_ghijkl_scl_if.oe else 'Z'; + sp5_ghijkl_scl_if.i <= i3c_sp5_to_fpga1_ghijkl_scl; + i3c_sp5_to_fpga1_ghijkl_sda <= sp5_ghijkl_sda_if.o when sp5_ghijkl_sda_if.oe else 'Z'; + sp5_ghijkl_sda_if.i <= i3c_sp5_to_fpga1_ghijkl_sda; + -- FPGA <-> DIMMs busses (filtered in proxy block) + i3c_fpga1_to_dimm_abcdef_scl <= dimm_abcdef_scl_if.o when dimm_abcdef_scl_if.oe else 'Z'; + dimm_abcdef_scl_if.i <= i3c_fpga1_to_dimm_abcdef_scl; + i3c_fpga1_to_dimm_abcdef_sda <= dimm_abcdef_sda_if.o when dimm_abcdef_sda_if.oe else 'Z'; + dimm_abcdef_sda_if.i <= i3c_fpga1_to_dimm_abcdef_sda; + i3c_fpga1_to_dimm_ghijkl_scl <= dimm_ghijkl_scl_if.o when dimm_ghijkl_scl_if.oe else 'Z'; + dimm_ghijkl_scl_if.i <= i3c_fpga1_to_dimm_ghijkl_scl; + i3c_fpga1_to_dimm_ghijkl_sda <= dimm_ghijkl_sda_if.o when dimm_ghijkl_sda_if.oe else 'Z'; + dimm_ghijkl_sda_if.i <= i3c_fpga1_to_dimm_ghijkl_sda; + + + reg_alert_l_pins.smbus_fan_central_hsc_to_fpga1_alert_l <= smbus_fan_central_hsc_to_fpga1_alert_l; + reg_alert_l_pins.smbus_fan_east_hsc_to_fpga1_alert_l <= smbus_fan_east_hsc_to_fpga1_alert_l; + reg_alert_l_pins.smbus_fan_west_hsc_to_fpga1_alert_l <= smbus_fan_west_hsc_to_fpga1_alert_l; + reg_alert_l_pins.smbus_ibc_to_fpga1_alert_l <= smbus_ibc_to_fpga1_alert_l; + reg_alert_l_pins.smbus_m2_hsc_to_fpga1_alert_l <= smbus_m2_hsc_to_fpga1_alert_l; + reg_alert_l_pins.smbus_nic_hsc_to_fpga1_alert_l <= smbus_nic_hsc_to_fpga1_alert_l; + reg_alert_l_pins.smbus_v12_ddr5_abcdef_hsc_to_fpga1_alert <= smbus_v12_ddr5_abcdef_hsc_to_fpga1_alert; + reg_alert_l_pins.smbus_v12_ddr5_ghijkl_hsc_to_fpga1_alert <= smbus_v12_ddr5_ghijkl_hsc_to_fpga1_alert; + -- cosmo-only alert pins, absent here + reg_alert_l_pins.smbus_v12_mcio_a0hp_hsc_to_fpga1_alert_l <= '1'; + reg_alert_l_pins.v0p96_nic_to_fpga1_alert_l <= '1'; + reg_alert_l_pins.main_hsc_to_fpga1_alert_l <= main_hsc_to_fpga1_alert_l; + reg_alert_l_pins.vr_v1p8_sys_to_fpga1_alert_l <= vr_v1p8_sys_to_fpga1_alert_l; + reg_alert_l_pins.vr_v3p3_sys_to_fpga1_alert_l <= vr_v3p3_sys_to_fpga1_alert_l; + reg_alert_l_pins.vr_v5p0_sys_to_fpga1_alert_l <= vr_v5p0_sys_to_fpga1_alert_l; + reg_alert_l_pins.pwr_cont1_to_fpga1_alert_l <= pwr_cont1_to_fpga1_alert_l; + reg_alert_l_pins.pwr_cont2_to_fpga1_alert_l <= pwr_cont2_to_fpga1_alert_l; + reg_alert_l_pins.pwr_cont3_to_fpga1_alert_l <= pwr_cont3_to_fpga1_alert_l; + reg_alert_l_pins.pwr_cont4_to_fpga1_alert_l <= pwr_cont4_to_fpga1_alert_l; + + resize_axil(fabric_responders(SPD_PROXY_RESP_IDX), responders_8b(SPD_PROXY_RESP_IDX)); + dimm_spd_proxy_top_inst: entity work.dimms_subsystem_top + generic map( + CLK_PER_NS => 8, + I2C_MODE => FAST_PLUS + ) + port map( + clk => clk_125m, + reset => reset_125m, + axi_if => responders_8b(SPD_PROXY_RESP_IDX), + in_a0 => a0_ok, + dimm_a_pcamp => dimm_a_pg, + dimm_b_pcamp => dimm_b_pg, + dimm_c_pcamp => dimm_c_pg, + dimm_d_pcamp => dimm_d_pg, + dimm_e_pcamp => dimm_e_pg, + dimm_f_pcamp => dimm_f_pg, + dimm_g_pcamp => dimm_g_pg, + dimm_h_pcamp => dimm_h_pg, + dimm_i_pcamp => dimm_i_pg, + dimm_j_pcamp => dimm_j_pg, + dimm_k_pcamp => dimm_k_pg, + dimm_l_pcamp => dimm_l_pg, + cpu_scl_if0 => sp5_abcdef_scl_if, + cpu_sda_if0 => sp5_abcdef_sda_if, + cpu_scl_if1 => sp5_ghijkl_scl_if, + cpu_sda_if1 => sp5_ghijkl_sda_if, + dimm_scl_if0 => dimm_abcdef_scl_if, + dimm_sda_if0 => dimm_abcdef_sda_if, + dimm_scl_if1 => dimm_ghijkl_scl_if, + dimm_sda_if1 => dimm_ghijkl_sda_if + ); + + resize_axil(fabric_responders(DBG_CTRL_RESP_IDX), responders_8b(DBG_CTRL_RESP_IDX)); + debug_module_top_inst: entity work.debug_module_top + port map( + clk_200m => clk_200m, + reset_200m => reset_200m, + clk => clk_125m, + reset => reset_125m, + axi_if => responders_8b(DBG_CTRL_RESP_IDX), + in_a0 => a0_ok, + -- Metro has no FPGA2 hotplug IRQ arriving here, so tie the tap idle. + fpga2_hp_irq_n => '1', + hp_int_n => hp_int_n, + sp5_debug2_pin => sp5_to_fpga1_debug2, + uart_headder_fall_back_to_debug_pins => uart_headder_fall_back_to_debug_pins, + uart_dbg_if => uart_dbg_if, + -- hotplug + i2c_sp5_to_fpgax_hp_sda => i2c_sp5_to_fpgax_hp_sda, + i2c_sp5_to_fpgax_hp_scl => i2c_sp5_to_fpgax_hp_scl, + -- sp + i2c_sp_to_fpga1_scl => i2c_sp_to_fpga1_scl, + i2c_sp_to_fpga1_sda => i2c_sp_to_fpga1_sda, + -- sp5 i2c + i2c_sp5_sec_to_fpga1_scl => i2c_sp5_sec_v3p3_scl, + i2c_sp5_sec_to_fpga1_sda => i2c_sp5_sec_v3p3_sda, + -- dimms + i3c_sp5_to_fpga1_abcdef_scl => i3c_sp5_to_fpga1_abcdef_scl, + i3c_sp5_to_fpga1_abcdef_sda => i3c_sp5_to_fpga1_abcdef_sda, + i3c_sp5_to_fpga1_ghijkl_scl => i3c_sp5_to_fpga1_ghijkl_scl, + i3c_sp5_to_fpga1_ghijkl_sda => i3c_sp5_to_fpga1_ghijkl_sda, + i3c_fpga1_to_dimm_abcdef_scl => i3c_fpga1_to_dimm_abcdef_scl, + i3c_fpga1_to_dimm_abcdef_sda => i3c_fpga1_to_dimm_abcdef_sda, + i3c_fpga1_to_dimm_ghijkl_scl => i3c_fpga1_to_dimm_ghijkl_scl, + i3c_fpga1_to_dimm_ghijkl_sda => i3c_fpga1_to_dimm_ghijkl_sda, + -- UARTs + uart1_sp_to_fpga1_dat => uart1_sp_to_fpga1_dat, + uart1_fpga1_to_sp_dat => uart1_fpga1_to_sp_dat, + uart0_sp_to_fpga1_dat => uart0_sp_to_fpga1_dat, + uart0_fpga1_to_sp_dat => uart0_fpga1_to_sp_dat, + uart0_fpga1_to_sp5_dat => uart0_fpga1_to_sp5_dat_buff, + uart0_sp5_to_fpga1_dat => uart0_sp5_to_fpga1_dat, + -- ESPI signals + espi0_sp5_to_fpga_clk => espi0_sp5_to_fpga1_clk, + espi0_sp5_to_fpga_cs_l => espi0_sp5_to_fpga1_cs_l, + espi0_sp5_to_fpga1_dat => espi0_sp5_to_fpga1_dat, + espi_resp_csn => espi_resp_csn, + nic_dbg_pins => versal_dbg_pins, + -- MUX + mux1_sel => fpga1_to_i2c_mux1_sel, + mux2_sel => fpga1_to_i2c_mux2_sel, + mux3_sel => fpga1_to_i2c_mux3_sel, + + fpga1_spare_v1p8 => fpga1_spare_v1p8 + ); + + +end rtl; diff --git a/hdl/projects/metro_seq/metro_timing.xdc b/hdl/projects/metro_seq/metro_timing.xdc new file mode 100644 index 00000000..154161e7 --- /dev/null +++ b/hdl/projects/metro_seq/metro_timing.xdc @@ -0,0 +1,354 @@ +# Timing constraints for the Metro sequencer FPGA. +# +# Ported from cosmo_seq's cosmo_timing.xdc. The two boards run the same blocks +# at the same rates, so the structure and the reasoning in the comments carry +# over; what is board specific is the port names, the PLL-derived clock names, +# and the FMC trace-delay numbers flagged below. + +create_clock -add -name sys_clk_pin -period 20.000 -waveform {0 10.000} [get_ports { clk_50mhz_fpga1_1 }]; +create_clock -add -name fmc_clk_pin -period 10.000 -waveform {0 5.000} [get_ports { fmc_sp_to_fpga1_clk }]; + +# +# FMC interface constraints +# Create a virtual clock, to represent the source clock of the FMC interface +create_clock -name fmc_virt_clk -period 10.000; + +# The FMC MMCM's output clock is derived from fmc_clk_pin and must stay in +# the synchronous group with it and the virtual clock. +set_clock_groups -asynchronous -group [get_clocks -include_generated_clocks {fmc_clk_pin fmc_virt_clk}] -group {clk_125m_sys_pll} -group {clk_200m_sys_pll} + + +# ####################### +# FMC Interface +# ####################### + +# SP outputs a continuous clock here. +# The FMC interface is constrained at 100MHz (10ns period), the CLKDIV=1 +# target rate; the same analysis is a strict superset of 50 and 66.67 MHz +# operation. The internal FMC domain clock comes from an MMCM in +# phase-alignment mode (see xilinx_ip_gen/fmc_pll_ip.tcl), so the flops +# see the pin clock plus the deliberate +45deg phase shift and STA +# accounts for it via the generated clock. +# FPGA's input delays have to be low enough that they don't run into the +# uncertainty region due to any possible skew. skew_bre is the shortest trace +# delay vs the clock, and skew_are is the longest trace delay vs the clock. +# +# !!! The six delays below are cosmo's, carried over unchanged. Metro's FMC +# !!! routing has not been measured yet, so these are a placeholder that keeps +# !!! the constraint structure meaningful, not a description of this board. +# !!! Re-derive them from Metro's trace lengths (clock, longest and shortest +# !!! data, and the wait line) before trusting a timing report for hardware. +# !!! The conversion is length_mm * 6.8ns/m. + +set max_clock_delay 0.4134 +set min_clock_delay 0.3602 +set max_data_delay 0.4186 +set min_data_delay 0.3573 +set max_wait_delay 0.4134 +set min_wait_delay 0.3635 + +# ################# +# Input constraints. +# Effectively longest data delay, fastest clock arrival at FPGA. +# input_max = clk_ext_delay_max + extTco_max + board_delay_max - fpga_clk_delay_min +# Effectively shortest data delay, slowest clock arrival at FPGA. +# input_min = clk_ext_delay_min + extTco_min + board_delay_min - fpga_clk_delay_max + +# For the inputs data valid before rising edge can be calculated based on the SP's datasheet timings and trace delays. +# td(CLKL-NExL) clock to out is max 1ns +# td(CLKH_NExH) is min +# td(CLKL-AV) 2.5ns +# td(CLKH-AIV) 8ns? +# td(CLKL-NOEL) 1.5ns +# td(CLKH-NOEH) 7.5ns +# td(CLKL-ADV) 3 ns +# td(CLKL-ADIV) 0 ns +# tsu(ADV-CLKH) 3 ns +# th(CLKH-ADV) 0 +# tsu(NWAIT-CLKH) 3 ns (worst read timing) +# th(CLKH-NWAIT) 2 ns (worst write timing) + +# Source sync so external_clk_delay is 0. +# Setup time is 1ns, and we include the 1/2 period due to SP shifting the data out on the falling edge. +set sp_output_half_period 5.0 +set sp_0_hold 0 +set sp_clk_delay 0 + +# We have our 1/2 period of 5 ns due to SP outputting on falling edges, plus the td in the datasheet +set td_clkl_nehl 1 +set nl_output_delay [expr {$sp_output_half_period + $td_clkl_nehl}] +set max_nl [expr {$sp_clk_delay + $nl_output_delay + $max_data_delay - $min_clock_delay}] +# latest clock, earliest data. We assume a hold time of 0 for the SP, but the SP +# holds its outputs until the *next falling edge*, so the earliest change is a +# half period after the capture edge. Without that term Vivado assumes the data +# can change at the capture edge itself, which manufactures a phantom +# input-hold requirement of nearly a half period. +# min external: fastest data, slowest clock +set min_nl [expr {$sp_clk_delay + $sp_output_half_period + $sp_0_hold + $min_data_delay - $max_clock_delay}] + +# Apply to all of these pins with similar or better timing relationships. +set_input_delay -clock fmc_virt_clk -max $max_nl [get_ports fmc_sp_to_fpga1_cs_l] +set_input_delay -clock fmc_virt_clk -min $min_nl [get_ports fmc_sp_to_fpga1_cs_l] +set_input_delay -clock fmc_virt_clk -max $max_nl [get_ports fmc_sp_to_fpga1_we_l] +set_input_delay -clock fmc_virt_clk -min $min_nl [get_ports fmc_sp_to_fpga1_we_l] +set_input_delay -clock fmc_virt_clk -max $max_nl [get_ports fmc_sp_to_fpga1_oe_l] +set_input_delay -clock fmc_virt_clk -min $min_nl [get_ports fmc_sp_to_fpga1_oe_l] +set_input_delay -clock fmc_virt_clk -max $max_nl [get_ports fmc_sp_to_fpga1_adv_l] +set_input_delay -clock fmc_virt_clk -min $min_nl [get_ports fmc_sp_to_fpga1_adv_l] +set_input_delay -clock fmc_virt_clk -max $max_nl [get_ports fmc_sp_to_fpga1_bl_l] +set_input_delay -clock fmc_virt_clk -min $min_nl [get_ports fmc_sp_to_fpga1_bl_l] + +# Address has diff relationship 2.5ns (max tco) +set td_clkl_av 2.5 +set a_output_delay [expr {$sp_output_half_period + $td_clkl_av}] +set max_a [expr {$sp_clk_delay + $a_output_delay + $max_data_delay - $min_clock_delay}] +# Still 0 hold on these pins, held to the next falling edge as above. +set min_a [expr {$sp_clk_delay + $sp_output_half_period + $sp_0_hold + $min_data_delay - $max_clock_delay}] +set_input_delay -clock fmc_virt_clk -max $max_a [get_ports fmc_sp_to_fpga1_a[*]] +set_input_delay -clock fmc_virt_clk -min $min_a [get_ports fmc_sp_to_fpga1_a[*]] + +# Data in has diff relationship 3ns (max tco) +set td_clkl_adv 3 +set ad_output_delay [expr {$sp_output_half_period + $td_clkl_adv}] +set max_ad [expr {$sp_clk_delay + $ad_output_delay + $max_data_delay - $min_clock_delay}] +# Still 0 hold on these pins, held to the next falling edge as above. +set min_ad [expr {$sp_clk_delay + $sp_output_half_period + $sp_0_hold + $min_data_delay - $max_clock_delay}] +set_input_delay -clock fmc_virt_clk -max $max_ad [get_ports fmc_sp_to_fpga1_da[*]] +set_input_delay -clock fmc_virt_clk -min $min_ad [get_ports fmc_sp_to_fpga1_da[*]] + +# The MMCM's +45deg phase shift puts the internal capture edge at 1.25 ns, +# and STA's default edge relationship then times input paths from the +# virtual-clock launch at 0 to that 1.25 ns edge -- a nonsense 1.25 ns +# requirement. The intended capture edge is the *next* shifted edge at +# 11.25 ns, which is exactly what setup-2 selects. The default hold +# relationship (one cycle before the setup edge, back at 1.25 ns) is the +# right check and passes with the half-period of real SP hold, so no -hold +# adjustment. Output paths need nothing: launch at 1.25 ns against the SP's +# capture at 10 ns is already the correct single-cycle relationship. +set_multicycle_path 2 -setup -from [get_clocks fmc_virt_clk] -to [get_clocks *fmc_pll*] +set_multicycle_path 2 -setup -from [get_clocks fmc_clk_pin] -to [get_clocks *fmc_pll*] + +#### END Of inputs + +# ################# +# Output constraints. +# Effectively need to meet setup time with longest FPGA data delay and fastest clock arrival at other device. +# output_max = fpga_clk_delay_max + board_delay_max + extTsu - ext_clk_delay_min +# Effectively need to meet hold time with shortest FPGA data delay and slowest clock arrival at other device. +# input_min = fpga_clk_delay_min + board_delay_min - extTh - ext_clk_delay_max + +# Ext setup time is 3ns +set tsu_nwait_clkh 3 +# SP rising edge samples so 0 clock delay at external device. +set max_wait [expr {$tsu_nwait_clkh + $max_wait_delay + $max_clock_delay - $sp_clk_delay}] +# Ext hold time is 2ns, still 0 clock delay at external device. +set th_clkh_nwait 2 +set min_wait [expr {$min_clock_delay + $min_wait_delay - $th_clkh_nwait - $sp_clk_delay}] +set_output_delay -clock fmc_virt_clk -max $max_wait [get_ports fmc_sp_to_fpga1_wait_l] +set_output_delay -clock fmc_virt_clk -min $min_wait [get_ports fmc_sp_to_fpga1_wait_l] + +# Ext setup time is 3ns +set tsu_adv_clkh 3 +# Ext hold time is 0ns +set th_clkh_adv 0 +# Still 0 clk delay at external device +set max_da [expr {$tsu_adv_clkh + $max_data_delay + $max_clock_delay - $sp_clk_delay}] +set min_da [expr {$min_data_delay - $th_clkh_adv + $min_clock_delay - $sp_clk_delay}] +set_output_delay -clock fmc_virt_clk -max $max_da [get_ports fmc_sp_to_fpga1_da[*]] +set_output_delay -clock fmc_virt_clk -min $min_da [get_ports fmc_sp_to_fpga1_da[*]] + + +# The streaming FSM presents read beats on consecutive cycles, so the +# word0->word1 transition is a true single-cycle path and no multicycle +# exception applies to the data pins. Single-cycle closes because the output +# and tristate flops pack into the IOBs: data_out_reg and data_out_hiz_int_reg +# are one flop per pin by construction, and nwait, a single-cycle path the SP +# samples every rising edge, gets the same treatment. If a board cannot close +# this way, set the extra_beat_setup generic and add setup-2/hold-1 exceptions +# on the data/tristate paths to match. +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/data_out_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/data_out_hiz_int_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/nwait_reg}] + +# End FMC + +set_false_path -from [get_ports {*}] -to [get_ports {fpga1_spare_v3p3*}] +set_false_path -from [get_ports {*}] -to [get_ports {fpga1_spare_v1p8[*]}] + +# ####################### +# eSPI Interface +# ####################### +# TODO: This is likely not correct but I need to re-write the link-layer logic again +# and then re-constrain +# 20MHz espi constraints, 50ns clock periods. +# ESPI interface has 2.2ns of trace delay +# AMD says 7ns of data setup +# AMD says 0.3ns of data hold +# AMD Data output valid time min 1 max 3 +# in delay max = tco_ext to max delay ext to fpga +# in delay min = minTco_ext to min delay ext to fpga +# out delay max = ext setup + max delay fpga to external +# out delay min = ext hold + min delay fpga to external + +# when sending to the SP5, it's going to take 2.2ns of trace time, and it needs to be there +# Clock took 2.2 ns to get to us, it's going to take 2.2ns of trace time to get back to the SP5 +# and SP5 wants 7 ns of setup time. We also eat ~4ns by syncing the espi clock. + +# outputs +# max = 7ns (SP5's needed setup time) + clock delay to FPGA (2.2ns) + return delay (2.2ns) +# min = .3ns (SP5's needed hold time) + clock delay to FPGA (2.2ns) + return delay (2.2ns) + + +# Data +# max= 7.5ns (1/2 period) + 3ns (maxreal tco) +# min= 7.5ns (1/2 period) + 1ns (min real tco) + +# This is a stop-gap to provide some kind of output timing constraints per the eSPI base spec +set_max_delay -to [get_ports espi0_sp5_to_fpga1_dat[*]] 6 +set_min_delay -to [get_ports espi0_sp5_to_fpga1_dat[*]] 0 +# eSPI1 is a second instance of the same target on the SP5's other eSPI +# port, serving the Versal's boot flash; same link, same budget. +set_max_delay -to [get_ports espi1_sp5_to_fpga1_dat[*]] 6 +set_min_delay -to [get_ports espi1_sp5_to_fpga1_dat[*]] 0 + +# ####################### +# SPI NOR flash interface (Winbond W25Q01JV) +# ####################### +# sclk is toggled by fabric logic off clk_125m at clk/2, so 62.5MHz, a 16ns +# period with an 8ns half period. Nothing inside the FPGA is clocked by it, so +# there is deliberately no create_generated_clock here: what actually has to be +# bounded is the clock-to-data skew leaving the FPGA and the pin-to-flop delay +# coming back, and both are directly constrainable. +# +# Trace delays are short and local; using the same 6.8ns/m as the FMC block +# above. +# On cosmo flash trace min is 31.982mm (Dat0), and max is 33.436 (Dat3) +# so min = 0.031982m * 6.8ns/m = 0.217 +# max = 0.033436 * 6.8ns/m = 0.227 +set flash_trace_max 0.227 +set flash_trace_min 0.217 + +# Pull the launch flops into the IOBs. Every one of these is a dedicated +# duplicate whose only load is its pin (see spi_clk_gen's sclk_pin and +# spi_txn_mgr's cs_n_pin), which is what makes packing legal. It matters a lot: +# left in the fabric the placer put them wherever it liked and measured 12 to 13 +# ns of routing to the pin, which both blew the clock-to-data skew budget and +# pushed the read round trip past every available sample point. In the IOB the +# delay is small, deterministic, and the same for all four. +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/clk_gen/sclk_pin_reg}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/spi_txn_mgr_inst/cs_n_pin_reg}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_o_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_oe_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_cap_*_reg[*]}] + +# ################# +# Outputs: sclk, and dat[] during the instruction, address and write phases. +# cs_n is handled separately below. +# +# The part samples mosi on the sclk rising edge, and the FPGA launches both mosi +# and the sclk falling edge from the same clk edge. So the flash sees a full half +# period of setup, less whatever skew the IOBs and routing add between the clock +# pin and the data pins: +# +# skew_budget = half_period - tDVCH = 8.0 - 2.0 = 6.0 ns +# +# Constraining all of these pins into one delay window makes the worst-case skew +# the difference between the two bounds, which is the quantity that matters here: +# the same clock insertion delay applies to every one of these launch flops, so it +# cancels out of the skew and only the window width has to fit the budget. +# +# Note the window looks wide (4.5ns against a 6ns budget) for four pins that are +# all IOB-packed and launched off the same clk edge. That is because max and min +# delay checks compare the slow corner of one path against the fast corner of +# another, so most of the width is process/voltage/temperature spread rather than +# pin-to-pin skew, which is well under a nanosecond here. The bound is a tripwire +# against a pin losing its IOB or picking up extra logic, not a skew estimate. +# +# Hold is not a concern for the part: mosi is held until the following falling +# edge, 8ns after the sampling edge, against a tCHDX of 3ns. +# +# These numbers assume the IOB packing above. Packed, the flop-to-pin delay is +# about 3.3ns and essentially all of it is logic -- 0.001ns of routing -- so a +# tight window is both meetable and meaningful. Left in the fabric the same paths +# measured 12 to 13ns of routing and varied by several ns between builds. +# Metro has two spi_nor_top instances: the SP5 boot flash, on the same 3V3 pins +# cosmo uses, and the Versal's boot flash behind the mux on sheet 137. The IOB +# properties above cover both already, because their -hier filters match on the +# instance name rather than the path to it. The delay windows do not, because +# the two flashes sit in banks at different voltages. +set_max_delay 5.0 -to [get_ports {spi_fpga1_to_flash_clk \ + spi_fpga1_to_flash_dat[*]}] +set_min_delay 0.5 -to [get_ports {spi_fpga1_to_flash_clk \ + spi_fpga1_to_flash_dat[*]}] + +# The Versal flash pins are in bank 35 at 1.8V, where the OBUFT is both slower +# and wider-spread than the 3V3 one the window above was measured against: the +# same IOB-packed path measures 2.9ns min to 7.0ns max here, against roughly +# 1.5 to 3.3ns on the 3V3 pins. Both ends of the window move with it, so the +# floor of 0.5ns is simply unreachable in this bank and says nothing about +# whether the path is healthy. +# +# What the window is for is unchanged: bounding the spread between sclk and the +# data pins so the flash sees them together. That spread is small for the same +# reason it is on the 3V3 pins -- every one of these is a dedicated IOB flop in +# one bank, launched off the same edge, so almost all of the width below is +# common-mode process/voltage/temperature spread rather than pin-to-pin skew. +# The bound is a tripwire against a pin losing its IOB or picking up fabric +# logic, and the max side plus the IOB properties are what actually catch that. +set_max_delay 5.0 -to [get_ports {qspi_fpga1_to_vercel_flash_mux_sck \ + qspi_fpga1_to_vercel_flash_mux_d[*]}] +set_min_delay 0.0 -to [get_ports {qspi_fpga1_to_vercel_flash_mux_sck \ + qspi_fpga1_to_vercel_flash_mux_d[*]}] + +# Two things are deliberately outside that window, because pulling them into it +# would make the placer work hard on paths that have an order of magnitude more +# real slack than the data pins do: +# +# cs_n only has to be low before the first sclk edge and stay low after +# the last. spi_txn_mgr spends cs_setup_cnts = 4 clk cycles, 32ns, +# on each, against tSLCH/tCHSH of 5ns. +# the tristate carries no data and only has to have settled before the part +# enable starts driving, which release_lanes gives it a full sclk cycle +# to do. +set_max_delay 16.0 -to [get_ports {spi_fpga1_to_flash_cs_l \ + qspi_fpga1_to_vercel_flash_mux_cs_l}] +set_max_delay 16.0 -from [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_oe_reg[*]}] \ + -to [get_ports {spi_fpga1_to_flash_dat[*] \ + qspi_fpga1_to_vercel_flash_mux_d[*]}] + +# ################# +# Inputs: dat[] during read phases. +# +# The controller samples read data at a fixed point S after the sclk rising edge, +# set by the rx_sample_taps generic on spi_nor_top. S has to satisfy +# +# round_trip_valid - half_period <= S <= half_period + round_trip_hold +# +# where round_trip_valid is built from the part's tCLQV and round_trip_hold from +# its tCLQX. Note the upper limit comes from tCLQX, not tCLQV: sampling too late +# catches the next bit rather than the current one. +# +# With the IOB packing and the output bounds above: +# flop to sclk pin 3.30 max 1.50 min +# sclk trace 0.40 0.10 +# flash tCLQV / tCLQX 6.00 1.50 +# data trace back 0.40 0.10 +# pin to capture flop 1.50 0.50 +# ----- ----- +# round_trip_valid max 11.60 round_trip_hold min 3.70 +# +# so at an 8ns half period S has to land in 3.6 .. 11.7ns. rx_sample_taps = 2 puts +# it at 8ns, about 4ns clear of either limit. spi_nor_fast_tb and +# spi_nor_fast_quick_io_tb model these delays and check both corners. +# +# If reads are marginal on hardware, sweep rx_sample_taps before assuming anything +# else is wrong; taps are 4ns apart so 1 and 3 bracket the shipped value. +# +# -datapath_only because this is a pin to flop propagation bound, not a +# synchronous transfer: without it Vivado charges the MMCM's clock insertion +# delay against the budget and the check becomes meaningless. +# +# The dedicated capture flops in spi_link are the only loads on these pins, so +# these paths are exactly the pin-to-flop delay. +set_max_delay 3.0 -datapath_only -from [get_ports {spi_fpga1_to_flash_dat[*] \ + qspi_fpga1_to_vercel_flash_mux_d[*]}] \ No newline at end of file diff --git a/hdl/projects/metro_seq/sims/metro_seq_top_tb.vhd b/hdl/projects/metro_seq/sims/metro_seq_top_tb.vhd new file mode 100644 index 00000000..8d6007fe --- /dev/null +++ b/hdl/projects/metro_seq/sims/metro_seq_top_tb.vhd @@ -0,0 +1,41 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; + +library vunit_lib; + context vunit_lib.vunit_context; + +-- Analysis-only smoke test. It does not drive the top level; its job is to make +-- a simulator compile the whole design tree, which the synthesis-only build +-- rules do not do on their own. Behavioural coverage lives in the per-subsystem +-- testbenches. +-- +-- Known gap: nothing exercises metro_seq_top itself, so the pin-to-record glue +-- in its architecture is unverified. That glue has already been wrong once -- +-- the hotswap power goods were inverted both here and in the sequencer synchroniser, which +-- the sequencer testbench cannot see because it instantiates the sequencer +-- directly. Driving 264 ports from a testbench is the obvious next step if that +-- layer keeps biting. +entity metro_seq_top_tb is + generic ( + runner_cfg : string + ); +end entity; + +architecture tb of metro_seq_top_tb is +begin + bench: process + begin + test_runner_setup(runner, runner_cfg); + while test_suite loop + if run("analyses") then + info("metro_seq_top and its dependencies analysed"); + end if; + end loop; + test_runner_cleanup(runner); + wait; + end process; +end tb; diff --git a/hdl/projects/metro_seq/versal_subsystem/BUCK b/hdl/projects/metro_seq/versal_subsystem/BUCK new file mode 100644 index 00000000..b76d8607 --- /dev/null +++ b/hdl/projects/metro_seq/versal_subsystem/BUCK @@ -0,0 +1,24 @@ +load("//tools:hdl.bzl", "vhdl_unit") +load("//tools:rdl.bzl", "rdl_file") + +rdl_file( + name = "versal_flash_regs_rdl", + src = "versal_flash_regs.rdl", + outputs = [ + "versal_flash_regs_pkg.vhd", + "versal_flash_regs.html", + "versal_flash_regs.json", + ], + visibility = ["PUBLIC"], +) + +vhdl_unit( + name = "versal_flash_subsystem", + srcs = ["versal_flash_subsystem.vhd"], + deps = [ + ":versal_flash_regs_rdl", + "//hdl/ip/vhd/axi_blocks:axilite_if_2k19", + ], + visibility = ["PUBLIC"], + standard = "2019", +) diff --git a/hdl/projects/metro_seq/versal_subsystem/versal_flash_regs.rdl b/hdl/projects/metro_seq/versal_subsystem/versal_flash_regs.rdl new file mode 100644 index 00000000..467efb45 --- /dev/null +++ b/hdl/projects/metro_seq/versal_subsystem/versal_flash_regs.rdl @@ -0,0 +1,50 @@ +// SystemRDL description of the sw-accessible registers owning the Versal's +// boot-flash mux on the Metro sequencer FPGA. + +addrmap versal_flash_regs { + name = "Versal boot flash mux control"; + desc = ""; + + default regwidth = 32; + default sw = rw; + default hw = r; + + reg { + name = "Flash Mux Control"; + desc = "Requests the Versal's QSPI boot flash for the FPGA. The request + is only honoured while the sequencer is holding the Versal in reset, so + that the FPGA and the Versal can never drive the flash at once. The + sequencer also takes the flash on its own, without a request, while it + measures the image before boot and once the Versal has booted; see + mux_status.seq_owned."; + field { + desc = "Set to 1 to ask for the Versal's boot flash."; + } request[1] = 0; + } mux_ctrl; + + reg { + name = "Flash Mux Status"; + default sw = r; + field { + desc = "Set when the FPGA actually owns the flash: the sequencer + claimed it, or the request was made and the sequencer is holding + the Versal in reset."; + } granted[1] = 0; + field { + desc = "Set while the sequencer itself is claiming the flash for + the FPGA, during the pre-boot measurement and after the Versal has + booted."; + } seq_owned[1] = 0; + field { + desc = "Live readback of the mux select pin (From FPGA to the mux)"; + } mux_sel[1] = 0; + field { + desc = "Live readback of the mux enable pin, active low (From FPGA to the mux)"; + } mux_en_l[1] = 1; + field { + desc = "Live readback of whether the sequencer is holding the Versal + in reset, i.e. whether a request would be granted."; + } versal_held_in_reset[1] = 0; + } mux_status; + +}; diff --git a/hdl/projects/metro_seq/versal_subsystem/versal_flash_subsystem.vhd b/hdl/projects/metro_seq/versal_subsystem/versal_flash_subsystem.vhd new file mode 100644 index 00000000..326c57de --- /dev/null +++ b/hdl/projects/metro_seq/versal_subsystem/versal_flash_subsystem.vhd @@ -0,0 +1,131 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.numeric_std_unsigned.all; + +use work.axil8x32_pkg; +use work.versal_flash_regs_pkg.all; + +-- Control of the mux on schematic sheet 137 that hands the Versal's QSPI boot +-- flash to the FPGA, so a Versal image can be read back or written without a +-- separate programmer. The controller that drives the flash is the spi_nor in +-- the eSPI1 wrapper at the top level, which is how the SP5 reaches it over +-- SAFS as well as the SP over registers; this block only decides whether it +-- may drive the part. +-- +-- The mux is switched to the FPGA when the sequencer asks for it, to measure +-- the image before boot and to hand the flash to the SP5 once the Versal has +-- booted, and on the SP's request while the sequencer is holding the Versal in +-- POR. Either way the Versal is never driving the flash at the same time. The +-- SP's request is a level, not a pulse: if the Versal is released while the SP +-- still holds the request, ownership drops on its own. +entity versal_flash_subsystem is + port( + clk : in std_logic; + reset : in std_logic; + + -- Control and status for the mux itself + ctrl_axi_if : view axil8x32_pkg.axil_target; + + -- From the sequencer: high while POR_B is asserted and will stay so + versal_held_in_reset : in std_logic; + -- From the sequencer: it wants the flash on the FPGA side itself + flash_owned_by_seq : in std_logic; + + -- Mux control pins + flash_qspi_mux_sel : out std_logic; + flash_qspi_mux_en_l : out std_logic; + + -- To the flash controller: high while it may drive the flash. Goes + -- to spi_nor_top's bus_enable, which parks the pins at the IOB flops + -- when low; muxing the pins here instead would pull those flops out + -- of the IOBs. + flash_bus_enable : out std_logic + ); +end entity; + +architecture rtl of versal_flash_subsystem is + + signal active_read : std_logic; + signal active_write : std_logic; + signal rdata : std_logic_vector(31 downto 0); + signal mux_ctrl : mux_ctrl_type; + signal mux_status : mux_status_type; + signal granted : std_logic; + +begin + + -- The interlock. The SP requesting is not enough; the sequencer has to be + -- holding the Versal off the flash as well. The sequencer's own claim + -- needs no request. + granted <= flash_owned_by_seq or (mux_ctrl.request and versal_held_in_reset); + flash_qspi_mux_sel <= granted; + flash_qspi_mux_en_l <= not granted; + + -- The controller parks its pins whenever we do not own the flash, so + -- losing the grant mid-transaction stops driving within a clock. + flash_bus_enable <= granted; + + mux_status <= ( + granted => granted, + mux_sel => granted, + mux_en_l => not granted, + versal_held_in_reset => versal_held_in_reset, + seq_owned => flash_owned_by_seq + ); + + axil_target_txn_inst: entity work.axil_target_txn + port map( + clk => clk, + reset => reset, + arvalid => ctrl_axi_if.read_address.valid, + arready => ctrl_axi_if.read_address.ready, + awvalid => ctrl_axi_if.write_address.valid, + awready => ctrl_axi_if.write_address.ready, + wvalid => ctrl_axi_if.write_data.valid, + wready => ctrl_axi_if.write_data.ready, + bvalid => ctrl_axi_if.write_response.valid, + bready => ctrl_axi_if.write_response.ready, + bresp => ctrl_axi_if.write_response.resp, + rvalid => ctrl_axi_if.read_data.valid, + rready => ctrl_axi_if.read_data.ready, + rresp => ctrl_axi_if.read_data.resp, + active_read => active_read, + active_write => active_write + ); + ctrl_axi_if.read_data.data <= rdata; + + write_logic: process(clk, reset) + begin + if reset then + mux_ctrl <= rec_reset; + elsif rising_edge(clk) then + if active_write then + case to_integer(ctrl_axi_if.write_address.addr) is + when MUX_CTRL_OFFSET => mux_ctrl <= unpack(ctrl_axi_if.write_data.data); + when others => null; + end case; + end if; + end if; + end process; + + read_logic: process(clk, reset) + begin + if reset then + rdata <= (others => '0'); + elsif rising_edge(clk) then + if active_read then + case to_integer(ctrl_axi_if.read_address.addr) is + when MUX_CTRL_OFFSET => rdata <= pack(mux_ctrl); + when MUX_STATUS_OFFSET => rdata <= pack(mux_status); + when others => rdata <= (others => '0'); + end case; + end if; + end if; + end process; + +end rtl;