Running the testbench¶
Behavioural simulation of the full path — decoder, FIFO chain, clock-domain
crossing, and the real ReadFromBlockPipeOut call — against the Opal Kelly and
DDR3 simulation models. No hardware required.
It is how the reset sequencer and the DDR3 path are verified.
Setup¶
cd hdl/source/sim
conda env create -f tb_environment.yml
conda activate wireless_fpga_tb
Vivado must be installed; tb_generator.py finds it at
C:/Xilinx/Vivado/2023.2/bin/vivado.bat on Windows or
/opt/Xilinx/Vivado/2023.1/bin/vivado otherwise. Override with:
export VIVADO_PATH=/your/path/to/vivado
Run it¶
python tb_generator.py
Output:
Cannot find preamble
channel 1 (pipe 0xa0): error rate 0.0, data length 400
Cannot find preamble
channel 2 (pipe 0xa1): error rate 0.0, data length 400
run folder: .../runs/test-1790...
tb_generator.py renders the testbench from tb_USBInterface.v.j2, generates
the Manchester stimulus, runs xsim, then decodes what came back out of both
pipes and scores each.
Cannot find preamble there is correct output
It comes from the inverted copy of the stream, which check_channel
scores as well to catch a Manchester convention mismatch. When the stream
decodes correctly, the inverted one contains no preamble — so this line
appearing exactly once per channel, next to an error rate of 0.0, is the
healthy case. Its absence would be the thing to look at.
The memory controller is a model¶
Both channels cache into DDR3, and MIG calibration does not converge in this
simulation — the Micron model reports tWLS violations on DQS and
init_calib_complete never asserts within any simulated time anyone has been
willing to wait. Left alone, that would hide both channels.
So the default run swaps the controller, not the design:
hdl/source/sim/ddr3/mig_ui_model.sv replaces xem7310_a75_mig behind a
SIM_MIG_MODEL define that is set on the simulation fileset only. The arbiter,
all four DDR3 FIFOs, the reset sequencer and both Opal Kelly pipes are the ones
that go into the bitfile.
| variable | effect |
|---|---|
| (default) | both channels, MIG replaced by the behavioural model. Channel 2 is pulsed during fifo_reset, so its recovered clock runs through the reset |
CH2_RESET_PULSES=0 |
channel 2 stays idle across the fifo_reset window, as on hardware — the case the reset sequencer exists for |
CH2_ONLY=1 |
skips the calibration wait and the pipe 0xA0 read — roughly halves the runtime when only channel 2 matters |
REAL_MIG=1 |
the actual MIG and the Micron DDR3 model. Does not work; kept so the calibration problem can be reproduced |
What the model does not prove
It is a model of the MIG's user interface, not of DDR3. No refresh, no
bank or row timing, no bandwidth limit, no read/write reordering. It says
the logic driving app_* is correct. It says nothing about whether the
controller is configured correctly or meets DDR3 timing — those stay
hardware questions.
What it is deliberately awkward about: app_rdy and app_wdf_rdy are
withheld pseudo-randomly, so a design that only works against an
always-ready controller fails here.
How the channels are distinguished¶
Channel 2 gets a different payload from channel 1 on purpose:
seq = Bits(hex="0x" + "123456789a" * 10) # channel 1
seq2 = Bits(hex="0x" + "a987654321" * 10) # channel 2
With identical stimulus, cross-wired channels — or a pipe 0xA1 that returned
channel 1's buffer — would pass silently. Both pipes are read from one block in
the testbench, because ReadFromBlockPipeOut writes into a shared pipeOut
array and two concurrent readers would corrupt each other.
Watchdog¶
ep_ready comes from prog_empty, so a pipe that never reaches its threshold
blocks the read forever. The testbench fails loudly after TIMEOUT_NS instead
of hanging with no output:
TIMEOUT: no $finish after 8000000 ns.
A pipe read is most likely still blocked waiting on ep_ready.
That message is the signature of a FIFO reset that never completed — the failure the reset sequencer prevents.
Waveforms¶
vivado -mode batch -source runs/test-<timestamp>/show_waveform.tcl
When chasing a channel-2 problem, look at dec2_clk, dec2_fifo_reset and
fifo2_ddr3_out's prog_empty.
The arbiter testbench¶
hdl/source/sim/tb_ddr3_ui.v is a separate, much smaller testbench for
ddr3_ui, the block that shares the one MIG between both channels.
The two are complementary, not redundant. The full testbench proves the whole path works; this one reaches the states the full testbench cannot. Its stimulus is 400 bits per channel — nowhere near enough to wrap a ring, fill one, or hold both channels backlogged long enough for starvation to show. This one shrinks the rings to 4 burst slots so all of that happens in microseconds, and it runs in seconds rather than minutes.
cd hdl/source/sim
xvlog ../design/ddr3/ddr3_ui.v tb_ddr3_ui.v
xelab work.tb_ddr3_ui -s tb_ddr3_ui_sim
xsim tb_ddr3_ui_sim -runall
ch1 delivered 200/200 words
ch2 delivered 200/200 words
at first completion: ch1 200, ch2 199
ring slots per channel: 4
PASS
What it checks:
| Round trip | every word each channel writes comes back, in order |
| Isolation | each word carries a channel tag; arriving on the wrong sink fails |
| Addressing | every burst must address its own channel's ring, asserted per command |
| Ring wrap | RING_ADDR_BITS is overridden to 5 — 4 burst slots per ring — so both rings wrap tens of times and sit at full for most of the run |
| Fairness | neither channel may take more than twice the other's bursts |
Fairness has to be sampled mid-run
The obvious check — compare the final delivered counts — is worthless here, and passed a deliberately broken fixed-priority arbiter. Both channels always finish: once the greedy one's source drains, the starved one gets the whole controller and catches up. The testbench snapshots both counts at the moment the first channel completes instead. Under fixed priority that reads 200 to 0.
Both failure modes were confirmed to fail it
Before trusting the pass, the arbiter was broken two ways on purpose — collapsing both rings onto one address range, and replacing round-robin with fixed priority — and the testbench caught each. A test that has never been seen to fail is not evidence.
Traps¶
A convention mismatch scores ~30%, not 0%
If the stimulus and MANCHESTER_IEEE disagree, every bit comes back
complemented, but the score is a plausible ~30% rather than an obvious
100%: the preamble 0x0f0f0f0f inverts to 0xf0f0f0f0, which contains the
same 16-bit search pattern shifted by four bits. check_channel scores the
inverted stream too and names a convention mismatch explicitly.
A passing simulation is not a passing bitfile
A behavioural model has ideal clocks. Every rate up to 50 MHz decodes perfectly here, and that says nothing about jitter, analog edges or Y6/AA6 crosstalk on real hardware.