When One Digit Cuts Your SPI Throughput in Half
Some embedded problems end with a board spin or a new algorithm. Others end with a one-line change, after a lot of careful measurement.
A few years back we supported an aerospace team building a sensor system aimed at early fault detection in a constrained, high-reliability environment. The system listened using a distributed microphone array (more than a dozen channels). Machine learning monitored the baseline acoustic signature and flagged abnormalities, including the faint new hum or buzz that can show up before a failure.
The problem was not the machine learning. It was an SPI throughput bottleneck that kept raw microphone data from moving through the pipeline fast enough.
Context and constraints
At a high level, the pipeline looked like this:
Multiple microphones feed an FPGA for parallel capture and preprocessing.
Data is buffered (including external memory).
A Linux-based controller pulls data from the FPGA over SPI, stores or forwards it, and hands it to downstream processing.
A few constraints made this harder than a typical lab demo:
Many channels equal more aggregate data rate, and therefore more FIFO pressure.
Multiple operating modes: higher-rate modes can stress the buffering and transaction overhead.
FPGA plus Linux: great capability, but many more layers (driver, DMA, cache, buffer sizing).
Limited headroom: protocol overhead and gaps can erase your margin.
For teams doing this often, a repeatable process for driver and pipeline work matters. See Embedded Linux development and driver work and performance tuning and profiling.
The SPI throughput bottleneck symptom
SPI was configured for a target clock rate that should have supported the required data volume. But measured throughput was consistently closer to half of what the target implied.
That “stuck at about half” pattern is a useful clue. Clean 2x or 4x errors often mean you landed in a different discrete hardware state (divider choice, fallback mode, missing DMA). They are less likely to be random inefficiency.
Reproducing the issue on hardware
Once we had hardware in hand, we reproduced the same throughput cap.
Our debug flow was straightforward. First, we confirmed the system-level symptom (buffers backing up, missed capture windows). Then we inspected the SPI transaction pattern (transfer sizes, gaps, chip select behavior). Next, we measured the actual SPI clock on the wire, rather than blindly trusting the configured number. Lastly, we reviewed the code path end-to-end and hunted for parameters the stack might quantize or clamp.
The key observation we made was the stability of the system. The system was not occasionally slow; it was reliably slow. That pushed us away from noise and toward configuration and driver behavior.
Reviewing the SPI setup
The system used Linux SPI from user space to talk to the FPGA. The FPGA acted as an SPI slave, streaming data out of a FIFO. Linux was the master.
We reviewed:
Mode (CPOL/CPHA), bits per word
Chip select timing
Transfer sizes and batching
Any delays between transfers
Nothing obvious was wrong. The transfer sizes were reasonable for bulk streaming, and there was no intentional throttling.
At this point, it is tempting to jump straight to a heavier fix (custom kernel driver, DMA rework, bypassing the Linux path). Those can be valid solutions, but they add complexity and schedule risk. Before paying that cost, we wanted to exhaust the boring possibilities.
The odd detail: 3,999,999 instead of 4,000,000
The one thing that stood out to us was the SPI clock target constant.
Instead of requesting 4,000,000, the code requested 3,999,999. That kind of “one less than the round number” value often comes from a defensive habit: someone trying to stay under a limit, avoid a driver threshold, or force a particular divider choice. The catch is that SPI clocks are generated with discrete prescalers, and Linux drivers pick an achievable setting. This means that a 1 Hz nudge can put you on the wrong side of a boundary and drop the actual clock far more than expected.
At first glance, 3,999,999 vs 4,000,000 should be meaningless. But in real hardware and drivers, it is not continuous. The requested value is an input to a selection algorithm, and that led us to a simple hypothesis:
The driver was quantizing the requested speed to a discrete hardware setting.
3,999,999 was falling just below a boundary, picking the next slower divider.
Requesting exactly 4,000,000 would select the faster setting and recover throughput.
The one-line change that fixed the bottleneck
We changed one line:
#define SPI_RATE 4000000After that, the SPI bus ran at the intended clock, and throughput was sufficient to meet the pipeline needs. The main bottleneck was resolved.
We also did a small amount of follow-on work, including SDRAM validation code, to increase confidence in the buffering path under sustained load. But the schedule-threatening issue was addressed with that one configuration change.
Practical takeaways
A few lessons we carry into any high-throughput SPI (or any clocked bus) effort:
Measure the real bus clock. Do not assume the configured number is on the wire.
When you see 2x or 4x errors, suspect dividers and discrete modes first.
Boundary values matter. If the hardware allows 4,000,000, request 4,000,000.
Treat OS APIs as hints, not guarantees. Drivers can round, clamp, or fall back.
Do not jump to complexity too early. Validate simple configuration and timing first.
If you are dealing with a similar “it should work on paper” throughput wall, we typically treat it as a pipeline instrumentation problem: measure each stage, confirm actual clocks, confirm buffer sizes and batching, then consider deeper architectural changes.
Related services:
Want a second set of eyes?
If you have a data path that looks fine on paper but is missing throughput on real hardware, we can usually get to root cause quickly with a short, structured debug pass. Schedule a call.

