In the previous article we wired a BenchPod to a stepper-motor rig and caught a stall the honest way: a real NEMA motor, a shunt and a current-sense amp, and a correlated capture showing the coil current going flat while the firmware's STEP pulses marched on. That article ended with a promise — that the fault run you provoke once by yanking a wire could be saved to the waveform library and replayed back through the DAC as a deterministic stand-in for the motor.
This article delivers on that promise. The motor is gone from the bench. In its place, the BenchPod's DAC replays the recorded coil-current waveform — power failure included, at the exact same millisecond, every single run — while the logic analyzer watches what the firmware does about it. The question we're answering is no longer "does the firmware detect a stall?" It's "how many milliseconds does it take?" — and that number is now something CI can assert on.
Three instruments, one timeline
The piece that makes this work is new gateware for the BenchPod's iCE40 FPGA: the DAC, ADC, and logic analyzer now run at the same time, on one shared timeline. You create a waveform and watch the target react on the same clock — not three loosely synced captures stitched together afterwards.
The part that matters most is where the trigger lives. The DAC replay is armed in the FPGA fabric and fires on the capture's hardware t0 — the same edge that starts the logic-analyzer sampling. Coordinating that from server software, across a network round-trip and two command queues, would be at the mercy of milliseconds of jitter; doing it in fabric aligns stimulus and capture to within ±20 ns, roughly 1000× tighter. When the capture later shows the firmware reacting 35 ms after the fault, all 35 of those milliseconds belong to the firmware — none of them are test-rig slop.
The plumbing behind it is the BenchPod's 8 MB PSRAM, divided between the instruments. An API call stores the waveform into the PSRAM first; arming the run then fires the DAC/ADC/LA machinery, which streams its capture data back into the remaining space. The web UI does the arithmetic for you before you commit to a run — for the capture below it reports:
A 24,800-sample DAC replay will sit at the top of PSRAM, so this capture can use the space below it: max LA 4,169,504 samples.
Not bad for a $6–8 FPGA (an ICE40UP5K-SG48I) on a roughly $100 bill of materials.
A motor that dies on schedule
Here's the run. The waveform is the coil-current recording from the previous article's rig — a healthy stepping motor whose power is cut about 255 ms in. The device-under-test is the same STM32F446RE running the `scenario-sensors-stm32` example firmware, with its coil-sense input now fed by the BenchPod's DAC instead of a shunt amplifier.
The flow, end to end:
- Over the UART console — bridged into the same page — start the motor with coil monitoring on:
stepmotor 10000 fwd verify. The firmware begins pulsing STEP and watching its coil-sense ADC every 100 ms. - Arm the capture with Start DAC with capture. The DAC loads the "step motor power failure" waveform from the library into PSRAM and waits, armed in fabric.
- The capture's hardware t0 fires the DAC. For the first 255 ms the firmware sees a perfectly healthy coil swing; then the recording goes flat, and the DAC's stop mode holds that dead level.
- The logic analyzer, sampling the STEP line on the same timeline at 80 kS/s, records exactly when the pulses stop.

The firmware side tells the same story in the console, in the same window:
> stepmotor 10000 fwd verify
STEP: 10000 step(s) dir=fwd (PC10=STEP, PC11=DIR=LOW) at ~500 steps/s
STEP verify: watching coil current on PA0 (ADC1_IN0) every 100 ms; failure = swing
drops below 250 mV *after* the motor has been seen running
verify cycle=41 step=2047/10000 failure detected: coil current went flat on PA0,
Vpp=24 mV < 250 mV (motor stalled / open coil)
STEP FAULT: coil current went flat after 2047/10000 step(s) fwd (motor not running)The console proves that the firmware detected the fault. The capture proves when: the coil goes flat at ~255 ms and the last STEP pulse lands at ~290 ms — about 35 ms of detection latency, comfortably inside the firmware's 100 ms polling budget. Without the shared timeline, that 35 ms would be unmeasurable; with it, it's a number you can put a limit on.
Here's the full flow as a video — waveform loaded, motor started over UART, DAC and logic analyzer firing together, and the firmware calling the fault:
Why replay beats the real motor in CI
The real motor was essential exactly once — to record what a genuine power failure looks like at the coil. After that, it's a liability on a CI rig: it wears, it drifts out of alignment, it needs mounting and power, and the one thing it won't do is fail identically twice. The replayed waveform does. The fault lands at the same sample offset every run, which is what turns "the firmware seemed to catch it" into a regression test with a pass/fail line.
And you're not limited to faults you managed to record. The replay pipeline can inject faults into a healthy recording programmatically — drop the swing to zero at any chosen offset, and you have a stall fixture for a scenario you never physically provoked. Recording once and synthesizing failure modes from it scales a lot better than breaking hardware creatively.
The same run, as a pytest
Everything above was done from the web UI, but the point of a deterministic fixture is automation. With the Python package, the co-triggered run is a few lines — on_capture=True arms the DAC in fabric, and the next capture fires it at t0:
FAULT_AT_S = 0.255 # power failure baked into the recording
BUDGET_S = 0.100 # firmware polls the coil every 100 ms
def test_stall_stops_step_pulses_within_budget(bp):
wf = bp.waveforms.find("step motor power failure")
with bp.replay_waveform(wf, on_capture=True): # armed, not yet driving
la = bp.capture_la(40_000, sample_rate_mhz=0.08) # t0 fires the DAC; 500 ms window
step = la.channel(9) # STEP pulses from the DUT
edges = [i for i in range(1, len(step)) if step[i] != step[i - 1]]
stopped_at = edges[-1] / la.sample_rate_hz
assert stopped_at - FAULT_AT_S < BUDGET_SAssert on the console side too if you like — bp.capture_uart(..., until="STEP FAULT") catches the firmware's own report. Either way, the test now fails loudly the day someone slows the coil-monitoring loop or breaks the stall handling, and it runs on every commit from CI with no motor anywhere near the rack.
Wrapping up
The previous article made the case for watching the digital command and the analog reality on one clock. This one closes the loop: the analog reality can now come from the BenchPod too, phase-locked to the capture in the FPGA fabric, so a recorded failure becomes a permanent, deterministic member of your test suite. For the capture-and-replay foundations, see recording and replaying analog signals; for driving the same bench conversationally, see driving the BenchPod with Claude.