Motor control is one of those places where the digital story and the analog story only make sense together. The firmware toggles a STEP line and flips a DIR line — pure digital. Whether the shaft actually moved shows up somewhere else entirely: in the coil current, an analog waveform. A test that only watches the GPIO sees a motor that is stepping perfectly right up until the moment it isn't.
So the interesting question isn't "did the firmware emit step pulses?" It's "did the firmware notice when the pulses stopped meaning anything?" — and, crucially, how fast. Answering that on every commit is a mixed-signal, hardware-in-the-loop (HIL) problem: you need one instrument that samples the digital and analog domains together, wired to the real device, reachable from CI.

Mixed-signal capture: both domains, one clock
The BenchPod samples its logic-analyzer channels and its analog front-end off the same iCE40 FPGA clock. That shared timebase is the whole point: when you see the STEP pulses on the logic lanes and the coil current on the ADC lane in the same capture, the alignment between them is real, not stitched together after the fact from two instruments with two clocks.
In this setup the device-under-test is an STM32F446RE. The wiring is deliberately modest:
- STEP / DIR from the DUT go to two logic-analyzer channels, so the pod records exactly when the firmware commands each step and which direction it asked for.
- Coil current is sensed across a shunt resistor, amplified by an INA282 current-sense amp, and fed into the BenchPod's ADC. A running motor shows up as a clean current swing on every step; a stalled or unpowered motor goes flat.
That's it — a shunt, one amplifier, and a few jumper wires. Nothing about it is precious, which matters: this is hardware you can leave permanently wired to your target and let CI reach over the network whenever it wants.
Drive the motor from the same place you watch it
The DUT exposes a UART console, and the BenchPod bridges that console into the same web interface as the capture. The rotors command here isn't a BenchPod built-in — it's part of a specific example firmware, the `scenario-sensors-stm32` project in our examples repo, which runs on the STM32F446RE DUT and includes the coil-monitoring verify logic used below. So the whole test is driven from one place — type a command, watch both domains respond:
> rotors step 1000
STEP: 1000 step(s) dir=fwd (PC10=STEP, PC11=DIR=LOW) at ~500 steps/s
STEP done: 1000 step(s) fwdCapture that run and you get the picture you actually want: the ADC lane shows the coil current stepping as a steady square wave, and the LA10 lane shows the STEP pulses lined up underneath it, sample-for-sample.

Testing stepper-motor failure modes
A healthy run is the easy half. The reason to instrument both domains is to test what happens when the motor fails — and a stepper can fail in several distinct ways, each of which reads differently across the two lanes.
1. Loss of power, detection off. Cut power to the motor while the firmware isn't watching the coil, and the digital side keeps going as if nothing happened — the STEP pulses march on in the logic-analyzer output while the coil current has already gone flat. This is the silent-failure baseline: the firmware is "stepping" a motor that isn't turning.
2. Loss of power, firmware watching. Now turn on coil monitoring and repeat. The firmware samples the coil current and, when the swing collapses, aborts and reports a fault:
> rotors step 3000 fwd verify 100
STEP: 3000 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 = peak-to-peak swing < 250 mV (motor flat; any key aborts)
verify cycle=10 step=250/3000 motor ok: coil Vpp=914 mV (min=1128 max=2042)
verify cycle=20 step=498/3000 motor ok: coil Vpp=719 mV (min=1313 max=2032)
verify cycle=25 step=621/3000 failure detected: coil current flat on PA0,
Vpp=12 mV < 250 mV (motor stalled / open coil)
STEP FAULT: coil current went flat after 621/3000 step(s) fwd (motor not running)In the capture, the tell is unmistakable: the coil current steps cleanly for the first stretch, then the swing collapses and the ADC lane clamps to a flat voltage — the current-sense amp's zero-current level — while the STEP pulses were still being commanded right up to the fault.

Because both the pulses and the current live in one correlated capture, you can measure the exact gap between "current went flat" and "firmware called STEP FAULT" — the firmware's real detection latency. That number is a test you can assert on: not just that it detects a stall, but that it does so within your timing budget, and that the budget holds commit after commit.
3. The subtle one. The nastiest failure isn't a clean power loss at all — it's a partial stall that shows up only as tiny overshoots on the current waveform, well inside the normal swing. The firmware's threshold never trips. Seeing it at all requires the analog resolution to catch a small delta against a moving baseline, and it's exactly the kind of case a GPIO-only view is blind to. Capturing it is the first step to teaching the firmware to catch it.

From a bench capture to a CI test
The move that makes all of this repeatable is that these captures don't have to stay on the bench. Save a run — including a fault run — to the waveform library, and you can replay it back through the DAC as a deterministic stand-in for the motor. The stall you managed to provoke once by yanking a wire becomes a fixture that every CI run can feed to the DUT on demand, with no motor to wear out or get out of alignment.
And the DAC works the other direction too: instead of only replaying a fault, the BenchPod can inject one — drive the coil-sense line low mid-run and measure whether the DUT flags the fault inside its response window. That turns "we're pretty sure the firmware handles a stall" into a test that fails loudly the day someone breaks the handling.
The ingredients are cheap and the wiring is trivial, but the capability isn't: one instrument that watches the digital command and the analog reality on a shared clock, driven from the same console that runs your CI. For the digital-plus-scripting side of the same bench, see driving the BenchPod with Claude and the pytest framework.