Build a Jitter-Free STM32 ADC Sampler With Timer and DMA
by LokiZhan in Circuits > Microcontrollers
41 Views, 0 Favorites, 0 Comments
Build a Jitter-Free STM32 ADC Sampler With Timer and DMA
Reading an ADC inside a while() loop works fine when all you need is the occasional potentiometer or temperature reading.
It becomes a problem once timing actually matters.
For waveform capture, vibration measurement, digital filtering, or control loops, I want every ADC conversion to begin at a predictable instant. I also do not want the CPU spending time moving every sample into memory.
The setup in this project is:
TIM2 → ADC1 → DMA → RAM
TIM2 defines exactly when a sample starts. ADC1 performs the conversion. DMA moves the result into a circular buffer. The CPU only deals with completed blocks of samples.
For this example, I am using an STM32F446 and sampling a potentiometer on PA0 at 10,000 samples per second.
Supplies
You will need:
- An STM32F446-based development board
- STM32CubeIDE with CubeMX / Device Configuration Tool
- ST-Link or the programming interface used by your board
- 10 kΩ potentiometer
- Breadboard and jumper wires
- USB cable
- Oscilloscope or logic analyzer, optional but useful for verification
Potentiometer Connection
Connect the potentiometer as follows:
- One outer pin → 3.3 V
- Other outer pin → GND
- Centre/wiper pin → PA0 / ADC1_IN0
Do not apply a voltage outside the ADC input range.
Why Not Sample the ADC in Software?
A basic STM32 ADC loop often looks something like this:
There is nothing inherently wrong with this approach for slow measurements.
The problem is timing.
The next conversion depends on whatever else the processor is doing. Interrupts, logging, RTOS scheduling, flash access, communication tasks, or simply additional code inside the loop can shift the actual sampling instant.
The average sample rate may still look correct, but individual samples are no longer guaranteed to be evenly spaced.
For a waveform or FFT input, that matters.
Instead of asking the CPU to decide when each conversion happens, we can give that job to a hardware timer.
Downloads
Set the Sampling Rate
I want a sample rate of:
10 ksample/s
In this example, TIM2 receives a 90 MHz timer clock.
The timer update frequency is:
Choose:
The prescaler reduces the 90 MHz timer clock to:
The auto-reload register then gives:
So TIM2 generates one update event every:
That update event will become the ADC trigger.
Do not simply copy these values to another STM32 project. Check the actual timer input clock in CubeMX first and recalculate PSC and ARR if necessary.
Check That the ADC Is Fast Enough
The timer determines when a conversion starts, but the ADC still needs enough time to finish before the next trigger arrives.
For this STM32F4 example:
- ADC clock = 22.5 MHz
- Sampling time = 15 ADC cycles
- 12-bit conversion processing = 12 cycles
Total conversion time is approximately:
which gives:
Our timer triggers every:
So there is plenty of margin.
This becomes more important when using multiple ADC channels. If one timer event starts a sequence of four channels, the complete sequence must finish before the next trigger.
Configure ADC1 in CubeMX
Open the Device Configuration Tool in STM32CubeIDE.
Enable:
ADC1 → IN0 / PA0
Use these settings:
The important setting here is:
Continuous Conversion Mode = Disabled
The ADC should perform one conversion only when TIM2 sends a trigger.
If continuous mode is enabled, the ADC can start free-running after the first trigger, which defeats the purpose of hardware-controlled sample timing.
Your generated ADC initialization should contain settings similar to:
Configure DMA
Add DMA for ADC1.
Configure it as:
The ADC result is 12 bits, while the ADC data register can conveniently be transferred as a 16-bit value.
That means a uint16_t buffer works well:
Circular mode is important because DMA will continuously reuse the buffer without requiring the CPU to restart each transfer.
Configure TIM2 TRGO
Configure TIM2 as a basic timer.
Use:
Now open:
TIM2 → Master Configuration
Set:
This causes each TIM2 update event to generate the internal TRGO signal.
ADC1 listens for that signal and starts one conversion.
The relevant initialization looks like this:
Start DMA Before Starting the Timer
Once CubeMX has generated the initialization code, start the acquisition system.
The order matters.
First arm ADC + DMA:
Then start TIM2:
So the complete startup sequence is:
DMA is now ready before the first timer trigger arrives.
Because ADC1 uses an external trigger, starting DMA does not immediately begin continuous sampling. Nothing happens until TIM2 starts generating TRGO events.
This avoids losing the first samples while DMA is still being configured.
Use the Circular Buffer in Two Halves
A 512-sample buffer works well because we can divide it into two blocks:
While DMA writes into one half, the application can process the other.
At 10 ksample/s:
So the software gets roughly 25.6 ms to process each half-buffer before DMA returns to it.
I use counters rather than simple boolean flags:
Then use the HAL callbacks:
Keep these callbacks short.
Do not put:
inside the DMA interrupt.
The callback should only indicate that data is ready.
Process the Samples Outside the Interrupt
The main loop can process whichever half DMA has finished with:
Using counters has one useful advantage.
If adc_half_count advances more than once before the foreground code catches up, you know the processing code missed its deadline.
That is much easier to diagnose than silently overwriting a boolean flag.
Convert the ADC Samples to Voltage
For a quick test, calculate the average of each block:
A 12-bit ADC has values from:
If the ADC reference is exactly 3.3 V:
But for a real measurement system, do not assume that your 3.3 V rail is an exact reference.
If VDDA is 3.26 V instead of 3.30 V, that difference becomes a gain error in every calculated voltage.
For accurate measurements, measure or otherwise characterize the ADC reference.
Check Source Impedance
Hardware triggering fixes sampling timing.
It does not guarantee ADC accuracy.
The STM32 ADC contains an internal sampling capacitor that must charge to the input voltage during the selected sampling period.
A low-impedance source such as a potentiometer or op-amp can usually charge it quickly.
A high-resistance sensor divider may not.
If the ADC input has not settled before conversion begins, the timing can be perfect while the ADC value is still wrong.
Possible fixes include:
- Increase ADC sampling time
- Reduce source impedance
- Add an op-amp buffer
- Redesign the analog input network
Do not choose the shortest ADC sampling time just because it gives the highest theoretical sample rate.
Verify the Actual Sample Rate
Do not stop once the code compiles.
Measure it.
One simple method is to toggle a spare GPIO from a timer or processing event and observe it with an oscilloscope or logic analyzer.
At 10 ksample/s, the expected sample interval is:
If your measured rate does not match the calculation, check the STM32 clock tree first.
STM32 timers can receive a multiplied timer clock depending on the APB prescaler configuration, so assuming the timer runs at the APB peripheral clock is a common source of mistakes.
What Happens at Higher Sample Rates?
At 10 ksample/s, the data bandwidth is tiny.
One 16-bit ADC channel produces:
That is easy for DMA.
At 1 MSPS:
Now memory bandwidth, DMA priorities, additional peripherals, and processing deadlines matter more.
The same architecture still works, but you need to consider the whole system.
If block processing takes too long, you can:
- Increase the DMA buffer size
- Reduce the sampling rate
- Move processing into a dedicated RTOS task
- Copy finished blocks into another queue
- Optimize DSP processing
For an RTOS project, the DMA callback can notify a processing task rather than using the counters shown here.
A Note for STM32F7 and H7 Users
The basic timer → ADC → DMA architecture also works on newer STM32 families, but Cortex-M7 devices introduce another issue:
data cache coherency.
DMA writes directly to RAM.
The CPU may still have an older copy of that memory stored in its D-cache.
That means the buffer can contain new samples in physical SRAM while the application still reads stale cached data.
On STM32F7/H7 designs, follow the cache-maintenance and memory-region rules for your specific MCU, or place DMA buffers in an appropriate non-cacheable region.
This issue does not apply in exactly the same way to the STM32F446 example used here.
Where to Go Next
Once this basic system is working, the same pattern can be extended to:
- Multi-channel sensor acquisition
- Current and voltage waveform capture
- Motor-control measurements
- Digital filters
- FFT input buffers
- Audio-frequency acquisition
- Data logging
- Triggered measurement systems
The useful part of the architecture is that each peripheral has one clear job:
Timer → timing
ADC → conversion
DMA → data movement
CPU → block processing
The CPU no longer needs to babysit every sample.
For a deeper discussion of ADC conversion timing, multi-channel throughput, circular DMA deadlines, STM32F7/H7 cache behavior, and production pitfalls, I have a more detailed STM32 timer-triggered ADC sampling guide covering the same architecture in more depth.
That is the main reason I prefer timer-triggered ADC with circular DMA whenever sampling timing matters: once the hardware owns the acquisition timing, the rest of the firmware becomes much easier to reason about.