Until this week, the BenchPod's logic analyzer topped out at 12 MS/s. That's plenty for I2C, a 115200 baud console or a PWM output. It's not enough for a 10 MHz SPI bus, a 3 Mbaud UART or a pulse you want to time to the nanosecond: at 12 MS/s one sample is 83 ns, and a 6 MHz SPI clock gets two samples per bit.
Firmware 3.9.6 raises that to 48 MS/s on all 14 channels, on every pod already in the field. No new hardware and no new API: you ask for a faster rate and you get it.
Why 12 MS/s was the limit
The pod's FPGA samples all 14 channels at once and writes every sample to memory. A sample takes two bytes, and the path into memory moves one byte per clock. That fixes the rate at 12 million samples a second, whether anything happened on the wires or not.
And mostly nothing did. A UART at idle, an SPI bus between transfers, a PWM output between edges: almost every sample is a copy of the one before it. Storing all of them spends the memory bandwidth on nothing.
Record the changes, not the samples
Above 12 MS/s the pod records differently. The FPGA still looks at every channel at 48 MHz, but it only writes something when a channel changes: the new state of all 14 channels and the number of ticks since the last change.
That moves the limit from how long you capture to how busy the signals are:
- Resolution is one tick, 20.8 ns, on every channel.
- Sustained, the pod keeps up with about 6 million changes per second, all channels together.
- Bursts faster than that go into an on-chip buffer of about 32,000 changes first.
- Idle lines cost almost nothing.
Here's what that means for common buses:
| Signal | Changes per second | At 48 MS/s |
|---|---|---|
| UART at 115200 baud | about 0.1 million | indefinitely |
| I2C at 400 kHz | about 1.2 million | indefinitely |
| CAN FD, 2-5 Mbit/s data phase | 1-3 million | indefinitely |
| SPI at 10 MHz, clocking nonstop | 20+ million | about 1.5 ms, or forever with gaps between transfers |
Real SPI traffic has gaps between transfers, and that's where the buffer drains. On our bench, 24 back-to-back 768-byte transfers at 3 and 6 MHz produced 338,000 changes, ten times the buffer, without losing one.
Nothing new to learn
We could have added a mode="changes" option. We didn't. The pod picks the method from the rate you ask for, runs at 48, 24 or 16 MS/s, and turns the changes back into ordinary samples before it replies. So capture_la() returns the same LaCapture as always, and the decoders work on it unchanged:
from embeddedci.benchpod import BenchPod, Trigger
with BenchPod("192.168.1.213", la_voltage=3.3) as bp:
# 10 ms of SPI at 48 MS/s, starting when chip select drops.
la = bp.capture_la(480_000, sample_rate_hz=48_000_000,
trigger=Trigger(4, "falling", pre_samples=4_800))
assert not la.truncated, la.truncated
frames = la.decode("spi", sclk=1, mosi=2, miso=3, cs=4)
assert frames[0].mosi == 0x9F # the driver asked for the flash's JEDEC IDla.sample_rate_hz is always the rate the pod actually ran. Ask for 20 MS/s and you get 24.
Pre-trigger: what happened just before
The same release adds pre-trigger. With Trigger(..., pre_samples=N) the pod keeps a rolling window of history while it waits, and the capture starts up to N samples before the trigger. la.pre_samples tells you how many it holds, and the trigger sits at that index.
That's what you want when the trigger is the symptom: an alarm pin going high, a reset line dropping, a chip select that shouldn't have fired. The question is what the other lines were doing just before. The pod never holds the trigger back to fill the window, so if the event comes early you get less history, but you never miss the event. Pre-trigger works at every rate, not only above 12 MS/s.
When it's too busy
If the signals change faster than the pod can store for longer than the buffer lasts, the capture stops early. It doesn't fail or return garbage: you get every sample up to that point, exact, and la.truncated says why ("busy" or "memory"):
la = bp.capture_la(2_000_000, sample_rate_hz=48_000_000)
if la.truncated:
print(f"stopped after {la.duration * 1e3:.2f} ms: {la.truncated}")For a dense signal that never pauses, 12 MS/s still gives you the longer window, since its cost doesn't depend on activity.
Measured on the bench
Before the release we ran it against our process-node DUT and the pod's own outputs:
- Pulse timing. 4 µs pulses from the pod's FPGA: all 4,999 measured exactly 192 ticks at 48 MS/s, and exactly 96 and 64 at 24 and 16 MS/s.
- SPI. The pod's SPI master at 6 MHz, 512 bytes: all 4,096 clock edges there, every byte decoded.
- UART. The DUT's console at 115200 baud, triggered on a falling edge, decoded to the text it printed.
- Regression. The full hardware test suite over the LAN and the cloud, with nothing broken.
The timing check is the kind of assertion 48 MS/s makes practical in CI:
widths = la.pulse_widths(9)
assert all(abs(w - 4e-6) <= 21e-9 for w in widths) # within one tickOne honest limit: a glitch shorter than one tick (20.8 ns) can fall between two samples. 48 MS/s narrows that window, but it doesn't close it.
Getting it
Update the pod to firmware 3.9.6 from its Settings tab or with benchpod flash-self; the new FPGA images come with it. Then update the SDK and MCP server (embeddedci 2.13.7, embeddedci-mcp 2.13.8). The web app's logic analyzer offers 16, 24 and 48 MS/s as soon as the pod reports it can, and the MCP capture_la tool takes the faster rates and a trigger_pre_samples argument, so agents get it too.
The details are in the pytest framework docs and the MCP docs. Got a fast bus you've been waiting to test? Tell us about it.