Project 8: DMA-Based ADC Ring Buffer

A high-throughput ADC streaming pipeline using DMA circular buffers and half-transfer interrupts.

Quick Reference

Attribute Value
Difficulty Level 3: Advanced
Time Estimate 1-2 weeks
Main Programming Language C (Alternatives: C++, Rust, Ada)
Alternative Programming Languages C++, Rust, Ada
Coolness Level Level 4: Hardcore Tech Flex
Business Potential 1. The “Resume Gold”
Prerequisites ADC sampling, DMA basics, interrupts
Key Topics DMA circular mode, ring buffers, throughput

1. Learning Objectives

By completing this project, you will:

  1. Configure DMA in circular mode for ADC streaming.
  2. Process half-buffer chunks without losing samples.
  3. Balance buffer size, latency, and CPU load.
  4. Validate throughput with measured sample counts.

2. All Theory Needed (Per-Concept Breakdown)

DMA and Circular Buffering

Fundamentals DMA moves data between peripherals and memory without CPU intervention. In STM32, DMA channels can be triggered by peripherals like ADCs or SPI. Circular mode allows continuous streaming into a ring buffer, which is essential for high-rate sampling. Understanding DMA is how you build systems that collect data without losing samples.

Deep Dive into the concept A DMA controller is a bus master that can read from a peripheral register and write to memory autonomously. For ADC streaming, the DMA reads the ADC data register on each conversion complete event and writes it into a buffer. In circular mode, the DMA wraps around when it reaches the end of the buffer, which enables continuous acquisition. The DMA can generate half-transfer and transfer-complete interrupts, which is a common pattern for double-buffering: you process half the buffer while the DMA fills the other half. Correct configuration requires setting the peripheral and memory addresses, transfer length, data width, and increment modes. If you misconfigure widths, your data will be scrambled. If you forget to enable memory increment, you will overwrite a single location. DMA also interacts with interrupt latency and buffer sizes. A small buffer reduces latency but increases interrupt frequency; a large buffer reduces interrupts but increases processing delay. This trade-off is an engineering decision: choose buffer size based on sampling rate and processing load. On STM32F3, DMA channels are shared among peripherals, so you must ensure you select a channel that supports your peripheral. Another subtlety is cache coherency on MCUs with caches; the STM32F3 does not have data cache, so you avoid that complexity. DMA is most powerful when combined with timer-triggered ADC sampling, creating a pipeline where the CPU only handles data in chunks. That is the pattern used in audio capture, motor control telemetry, and high-speed sensor acquisition.

How this fit on projects In DMA-Based ADC Ring Buffer, you configure DMA in circular mode to stream ADC samples into a ring buffer and process them in blocks.

Definitions & key terms

  • DMA channel -> A hardware data mover connected to specific peripheral request lines.
  • Circular mode -> DMA mode where transfers wrap around to the start of the buffer.
  • Half-transfer -> Interrupt event when half the buffer is filled.
  • Transfer-complete -> Interrupt event when the buffer is fully filled.
  • Ring buffer -> Circular data structure used for continuous streams.

Mental model diagram (ASCII)

ADC -> DMA -> [Buffer A | Buffer B] -> CPU processing
      ^
 half/full interrupts

How it works (step-by-step, with invariants and failure modes)

  1. Configure ADC to trigger conversions at a fixed rate.
  2. Set DMA source to ADC data register and destination to buffer.
  3. Enable circular mode and half/complete interrupts.
  4. Process data in half-buffer chunks and track overrun errors.
  5. Invariant: DMA keeps up with sampling rate; failure mode: overrun if CPU cannot process fast enough.

Minimal concrete example

// DMA setup (pseudocode)
DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR;
DMA1_Channel1->CMAR = (uint32_t)adc_buffer;
DMA1_Channel1->CNDTR = BUFFER_LEN;
DMA1_Channel1->CCR |= DMA_CCR_CIRC | DMA_CCR_MINC;

Common misconceptions

  • DMA is always faster ignores bus contention and setup overhead.
  • Circular buffers never overflow ignores CPU processing limits.
  • DMA removes the need for interrupts ignores half/full transfer events.

Check-your-understanding questions

  1. Why use half-transfer interrupts in DMA streaming?
  2. What happens if the CPU cannot process a buffer before it is overwritten?
  3. How do you choose buffer length for a given sampling rate?

Check-your-understanding answers

  1. They enable double-buffering so you can process one half while the other fills.
  2. Data is overwritten, causing lost samples or corrupted streams.
  3. Choose a length that balances processing latency and interrupt overhead; compute based on sample rate and processing time.

Real-world applications

  • Audio capture and streaming.
  • High-speed ADC logging in instrumentation.
  • Continuous sensor telemetry for control systems.

Where you’ll apply it

  • In this project: see Section 4.2 Key Components and Section 6.2 Critical Test Cases.
  • Also used in: P06 DAC Waveform Generator (for triggered updates).

References

  • STM32F3 Reference Manual (DMA chapter).
  • ST application notes on ADC + DMA pipelines.
  • Elecia White, ‘Making Embedded Systems’ (data streaming).

Key insights

  • DMA turns high-rate data capture into a buffer-management problem rather than an ISR problem.

Summary DMA is the enabler of high-throughput embedded pipelines. With circular buffers and half-transfer interrupts, you can stream data continuously without losing samples.

Homework/Exercises to practice the concept

  1. Compute buffer length needed for 5 kHz sampling with 100 ms latency.
  2. Simulate a DMA ring buffer and practice indexing wraparound.
  3. Measure CPU load with and without DMA for the same sampling rate.

Solutions to the homework/exercises

  1. Buffer length = 5,000 samples/s * 0.1 s = 500 samples.
  2. Use modulo indexing or a head/tail pointer; wrap at buffer length.
  3. DMA reduces CPU load by avoiding per-sample ISR overhead.

ADC Sampling, Quantization, and Signal Conditioning

Fundamentals An ADC converts an analog voltage into a digital number. The conversion depends on a reference voltage (Vref), a sampling capacitor, and the selected resolution. Sampling rate determines how often you capture the signal, while resolution determines the size of each voltage step. If you sample too slowly, you miss signal changes; if you sample too fast, you may overwhelm your firmware or capture noise. Understanding sampling time, reference voltage, and input impedance is essential for trustworthy measurements.

Deep Dive into the concept The STM32F3 ADCs use a sample-and-hold circuit that captures the input voltage onto a capacitor, then converts it to a digital code. The sample time must be long enough for the input to charge the capacitor through the source impedance; if it is too short, readings are low or noisy. Resolution (e.g., 12-bit) defines 2^N discrete levels, so the least significant bit represents Vref / 4096 volts. This means that noise or quantization error of even one LSB can be significant for low-level signals. Sampling rate is constrained by conversion time and the ADC clock. If you increase sampling frequency, you may need to reduce sample time or use multiple ADCs in interleaved mode. Aliasing is a key risk: if your signal contains frequency components above half the sampling rate, those components will appear as false lower-frequency signals. In practice, you mitigate aliasing with analog low-pass filters or by choosing a sufficiently high sampling rate. Calibration matters: STM32 ADCs often support calibration routines to reduce offset and gain error. Performing calibration at startup improves accuracy. For reliable measurements, you must also consider Vref stability. If Vref fluctuates, the ADC readings fluctuate even if the input is constant. Some designs use the internal reference voltage or measure Vrefint to compensate. Finally, logging ADC results can introduce jitter if done inside an interrupt. A better approach is to buffer results (possibly with DMA) and log them in a lower-priority task. This separation ensures that conversion timing remains accurate while still providing readable logs. In an ADC logging project, you will configure a timer trigger, set up the ADC sampling parameters, and validate that the measured voltages match a known reference. This approach builds the habit of treating ADC data as engineering data, not just numbers on a screen.

How this fit on projects In DMA-Based ADC Ring Buffer, you build a reliable sampling pipeline that converts raw codes into voltages and logs them with timestamps.

Definitions & key terms

  • Vref -> Reference voltage that defines the ADC’s full-scale range.
  • LSB -> Least significant bit; the smallest measurable voltage step.
  • Sample time -> Duration the ADC input capacitor is connected to the signal.
  • Aliasing -> False low-frequency signals caused by undersampling.
  • Quantization -> Mapping a continuous voltage to discrete digital levels.

Mental model diagram (ASCII)

Analog Signal -> Sample/Hold -> Quantize -> Digital Code -> Scale to Volts

How it works (step-by-step, with invariants and failure modes)

  1. Select ADC resolution and sampling time based on signal bandwidth and source impedance.
  2. Configure a timer trigger to start conversions at a fixed rate.
  3. Read raw ADC codes and convert to voltages using Vref and resolution.
  4. Log or buffer results without disturbing conversion timing.
  5. Invariant: sampling interval remains constant; failure mode: jitter or aliasing from poor trigger design.

Minimal concrete example

// Convert raw 12-bit ADC to volts
float volts = (raw * 3.3f) / 4095.0f;
printf("raw=%u volts=%.3f\n", raw, volts);

Common misconceptions

  • Higher resolution always means better accuracy ignores Vref noise and calibration.
  • Sampling faster always improves data ignores aliasing and firmware bandwidth.
  • ADC codes are already in volts ignores scaling and reference voltage.

Check-your-understanding questions

  1. How does source impedance affect ADC sample time?
  2. What happens if you sample a 1 kHz signal at 1 kHz?
  3. Why might two boards report different ADC codes for the same input?

Check-your-understanding answers

  1. High source impedance charges the sample capacitor slowly, requiring longer sample time.
  2. You risk aliasing and will not capture the waveform shape; you need >2 kHz sampling.
  3. Differences in Vref, calibration, and analog noise cause variation.

Real-world applications

  • Sensor acquisition in industrial monitoring.
  • Audio sampling and signal analysis.
  • Battery voltage measurement and power management.

Where you’ll apply it

References

  • STM32F3 Reference Manual (ADC chapter).
  • ‘The Art of Electronics’ (sampling fundamentals).
  • Elecia White, ‘Making Embedded Systems’ (signal acquisition).

Key insights

  • ADC data is only trustworthy when sampling time, reference voltage, and timing are engineered, not guessed.

Summary ADC sampling is a pipeline from physics to numbers. Proper sampling time, calibration, and timing validation turn raw codes into reliable measurements.

Homework/Exercises to practice the concept

  1. Compute LSB size for 12-bit ADC with 3.3 V reference.
  2. Measure a known voltage divider and compare raw code to expected value.
  3. Design a sampling plan for a sensor that changes at 5 Hz.

Solutions to the homework/exercises

  1. LSB = 3.3 / 4096 ~ 0.000805 V.
  2. Expected code is (Vout / 3.3) * 4095; small error due to tolerance is normal.
  3. Sampling at 50-100 Hz provides margin to capture changes without aliasing.

Timers, Prescalers, and Interrupt Scheduling

Fundamentals Hardware timers are the MCU’s timekeeping engines. They count ticks derived from a clock source and can generate periodic events without CPU busy-waiting. By configuring a prescaler and auto-reload value, you set a precise interval. Timers can trigger interrupts, toggle pins, or drive DMA. Interrupts allow your firmware to react to timer events with deterministic latency, which is the basis of real-time scheduling. Using timers and interrupts instead of delay loops is the core technique for reliable embedded timing.

Deep Dive into the concept STM32 timers are flexible peripherals that can operate as basic timebases, output compare generators, PWM engines, or input capture units. At their core is a counter clocked by a timer clock derived from the APB bus. The prescaler divides the timer clock, and the auto-reload register sets the period at which the counter resets. Each update event can trigger an interrupt, set a status flag, or generate a DMA request. The advantage over software delays is determinism: the hardware counts regardless of CPU load. However, interrupts introduce latency. The NVIC prioritizes interrupts, and higher-priority ISRs can delay timer handlers. If you rely on timer interrupts for scheduling, you must measure latency and account for jitter. A timer-driven LED sequencer is an ideal training ground: you build a state machine that advances on each timer tick, ensuring that your pattern timing is stable even if the CPU is busy. Advanced timers also support one-pulse mode, dead-time insertion, and complementary outputs, which are critical for motor control but beyond the basics here. For a correct timer configuration, you compute the prescaler as (timer_clock / desired_tick) - 1, and the auto-reload as (tick_rate / desired_interval) - 1. You then verify by toggling a GPIO on each interrupt and measuring the period. Misconfigurations are easy to spot: a factor-of-two error usually means you forgot the APB timer clock doubling rule. Another common failure is forgetting to clear interrupt flags, which results in repeated or missed interrupts. In short, timers are your deterministic clockwork. Mastering them means you can build schedules, measure drift, and guarantee timing, which is the heart of embedded systems.

How this fit on projects In DMA-Based ADC Ring Buffer, you design a timer-driven schedule that advances a state machine without delay loops, proving deterministic timing.

Definitions & key terms

  • Prescaler -> Divider that slows the timer clock.
  • Auto-reload -> Value that sets the timer period.
  • Update event -> Timer overflow event that can trigger interrupt or DMA.
  • Interrupt latency -> Time between event and ISR execution.
  • Jitter -> Variation in interrupt timing due to system load.

Mental model diagram (ASCII)

Timer clock -> prescaler -> counter -> overflow -> ISR -> state machine step

How it works (step-by-step, with invariants and failure modes)

  1. Select a timer and enable its clock.
  2. Compute prescaler and auto-reload for desired tick rate.
  3. Enable update interrupt and configure NVIC priority.
  4. In ISR, update the state machine and clear interrupt flags.
  5. Invariant: timer tick period remains stable; failure mode: drift or missed interrupts if flags are not cleared.

Minimal concrete example

// Timer ISR toggles LED state
void TIM2_IRQHandler(void) {
if (TIM2->SR & TIM_SR_UIF) {
    TIM2->SR &= ~TIM_SR_UIF;
    step_led_pattern();
}
}

Common misconceptions

  • Delays are fine for scheduling ignores CPU load and jitter.
  • Timer interrupts are always on time ignores NVIC priority and blocking ISRs.
  • Auto-reload equals milliseconds ignores prescaler and timer clock source.

Check-your-understanding questions

  1. What causes a factor-of-two error in timer frequency?
  2. How do you measure jitter in a timer-driven system?
  3. Why must you clear the update flag in the ISR?

Check-your-understanding answers

  1. Forgetting that timers may run at twice the APB clock when the prescaler is not 1.
  2. Toggle a GPIO in the ISR and measure period variation with a logic analyzer.
  3. If not cleared, the ISR may immediately retrigger or mask new interrupts.

Real-world applications

  • LED sequencing and deterministic UI timing.
  • Sensor sampling schedules with fixed intervals.
  • Control loops in motors or power electronics.

Where you’ll apply it

References

  • STM32F3 Reference Manual (timer chapter).
  • Joseph Yiu, ‘The Definitive Guide to ARM Cortex-M3/M4’ (interrupts).
  • Elecia White, ‘Making Embedded Systems’ (timing and scheduling).

Key insights

  • Timers create deterministic time; interrupts make that time actionable without blocking the CPU.

Summary Timers and interrupts are the foundation of real-time embedded behavior. They replace blocking delays with precise schedules and make timing measurable.

Homework/Exercises to practice the concept

  1. Calculate prescaler and auto-reload for a 250 ms timer tick at 72 MHz.
  2. Toggle a GPIO on each timer interrupt and measure drift over 5 minutes.
  3. Experiment with different NVIC priorities and observe latency.

Solutions to the homework/exercises

  1. A 1 kHz tick requires prescaler 71999; a 250 ms interval uses auto-reload 249.
  2. Drift near zero indicates correct timer configuration; large drift indicates clock misconfig.
  3. Raising priority reduces latency but can starve lower-priority handlers.

3. Project Specification

3.1 What You Will Build

A streaming ADC pipeline that continuously fills a ring buffer with DMA and processes samples in blocks.

3.2 Functional Requirements

  1. DMA Circular Mode: Configure DMA to stream ADC samples into a ring buffer.
  2. Half/Full Interrupts: Handle half and full transfer interrupts for processing.
  3. Throughput Validation: Measure effective sample rate and detect overruns.
  4. Statistics: Compute min/mean/max per buffer chunk.

3.3 Non-Functional Requirements

  • Performance: Handle at least 5 kHz sampling without overruns.
  • Reliability: Run for 10 minutes without missing samples.
  • Usability: Clear log showing buffer health and throughput.

3.4 Example Usage / Output

DMA stream: 5000 Hz
Half buffer processed: 250 samples
Overruns: 0
Status: PASS

3.5 Data Formats / Schemas / Protocols

Stream log: BUF=<half|full> COUNT=<int> OVERRUN=<int>

3.6 Edge Cases

  • Buffer too small causing frequent interrupts.
  • CPU cannot process half-buffer before next interrupt.
  • DMA channel misconfigured leading to data corruption.
  • ADC sampling rate higher than DMA throughput.

3.7 Real World Outcome

You will observe continuous sampling without data loss, with periodic buffer processing logs.

3.7.1 How to Run (Copy/Paste)

$ make flash
$ screen /dev/tty.usbmodem* 115200

3.7.2 Golden Path Demo (Deterministic)

  • Sample at 5 kHz with 500-sample buffer; confirm zero overruns.

3.7.3 CLI Transcript (Success)

DMA stream: 5000 Hz
BUF=half COUNT=250 OVERRUN=0
BUF=full COUNT=250 OVERRUN=0
RESULT=PASS
# Exit code: 0

3.7.4 Failure Demo (Too Small Buffer)

BUF=half COUNT=50 OVERRUN=3
RESULT=FAIL
# Exit code: 2

4. Solution Architecture

DMA streams ADC samples into a ring buffer; half/full interrupts trigger processing.

4.1 High-Level Design

ADC -> DMA (circular) -> [Half Buffer | Half Buffer] -> Processing

4.2 Key Components

Component Responsibility Key Decisions
ADC Trigger Fixed-rate sampling Timer trigger for stability
DMA Engine Streams samples into buffer Circular mode with half/full interrupts
Processor Consumes buffer halves and computes stats Process in main loop

4.3 Data Structures (No Full Code)

typedef struct {
uint16_t samples[500];
volatile uint8_t half_ready;
volatile uint8_t full_ready;
} adc_dma_buffer_t;

4.4 Algorithm Overview

DMA Processing

  1. On half-transfer, mark half_ready.
  2. Process half buffer in main loop.
  3. On full-transfer, mark full_ready.

Complexity: O(N) per half-buffer.


5. Implementation Guide

5.1 Development Environment Setup

make init
make flash
screen /dev/tty.usbmodem* 115200

5.2 Project Structure

project-root/
|-- src/
|   |-- main.c
|   |-- drivers/
|   `-- app/
|-- include/
|-- Makefile
`-- README.md

5.3 The Core Question You’re Answering

“How do I stream high-rate sensor data without losing samples?”

5.4 Concepts You Must Understand First

  1. DMA circular mode and half-transfer interrupts.
  2. ADC sampling timing and rate selection.
  3. Buffer sizing trade-offs.

5.5 Questions to Guide Your Design

  1. What buffer length gives you enough processing time?
  2. How will you detect overruns?
  3. What is the maximum sustainable sample rate?

5.6 Thinking Exercise

Buffer Sizing

Sample rate: 5 kHz
Half buffer: 250 samples -> 50 ms of data
You must process each half in <50 ms

5.7 The Interview Questions They’ll Ask

  1. Why use DMA for ADC streaming?
  2. What is the trade-off between buffer size and latency?
  3. How do half-transfer interrupts help?

5.8 Hints in Layers

Hint 1: Start with a low sample rate to validate DMA. Hint 2: Use half-transfer interrupts for processing. Hint 3: Track overruns with a counter.

5.9 Books That Will Help

Topic Book Chapter
DMA basics Making Embedded Systems Ch. 9
ADC pipelines STM32 Reference Manual ADC/DMA chapters

5.10 Implementation Phases

Phase 1: DMA Setup (3 days)

Configure ADC + DMA circular mode.

Phase 2: Buffer Processing (4 days)

Handle half/full interrupts and compute stats.

Phase 3: Stress Test (3 days)

Increase sampling rate and detect overruns.

5.11 Key Implementation Decisions

Decision Options Recommendation Rationale
Buffer size 100 vs 500 vs 1000 500 Balanced latency and overhead
Processing ISR vs main loop Main loop Keep ISR short

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Unit Tests Buffer indexing Half/full flag logic
Integration Tests DMA streaming Stable sample rate logging
Edge Case Tests Overruns Artificial processing delays

6.2 Critical Test Cases

  1. DMA Stream: Continuous stream with zero overruns.
  2. Half-buffer Timing: Processing completes before next half-fill.
  3. Overrun Detection: Overrun counter increments when processing delayed.

6.3 Test Data

Half=250 samples, Full=250 samples, Overruns=0

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

Pitfall Symptom Solution
Wrong DMA channel No data or corrupted data Verify DMA request mapping
Buffer too small Frequent overruns Increase buffer size
Processing in ISR Missed interrupts Move processing to main loop

7.2 Debugging Strategies

  • Toggle a GPIO on half/full interrupts to measure frequency.
  • Log buffer indices and detect gaps.
  • Reduce sample rate to isolate issues.

7.3 Performance Traps

Processing too much per half-buffer can starve DMA and cause overruns.


8. Extensions & Challenges

8.1 Beginner Extensions

  • Log RMS value per buffer.

8.2 Intermediate Extensions

  • Add a simple digital filter per buffer.

8.3 Advanced Extensions

  • Stream buffer to PC over UART/USB for plotting.

9. Real-World Connections

9.1 Industry Applications

  • Audio capture: Continuous DMA streaming for audio.
  • Instrumentation: High-rate sensor acquisition.
  • CMSIS-DSP examples: Buffer processing patterns.
  • STM32CubeF3: ADC + DMA reference projects.

9.3 Interview Relevance

  • DMA and ring buffer design questions.
  • Throughput vs latency trade-offs.

10. Resources

10.1 Essential Reading

  • Making Embedded Systems by Elecia White - DMA and streaming concepts.
  • STM32F3 Reference Manual by ST - ADC and DMA configuration.

10.2 Video Resources

  • STM32 DMA circular mode tutorial.
  • ADC + DMA streaming demo.

10.3 Tools & Documentation

  • Logic analyzer: Validate sampling timing.
  • STM32CubeIDE: Build and debug.

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain DMA circular mode.
  • I understand half-transfer interrupts.
  • I can size buffers based on rate and latency.

11.2 Implementation

  • DMA stream runs without overruns.
  • Processing handles half-buffers correctly.
  • Throughput report matches sample rate.

11.3 Growth

  • I can scale to higher sample rates.
  • I can identify bottlenecks in streaming pipelines.
  • I can explain DMA trade-offs in interviews.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • DMA circular mode configured.
  • Half/full interrupts processed.
  • Overrun tracking implemented.

Full Completion:

  • Stable streaming at 5 kHz.
  • Stats per buffer logged.

Excellence (Going Above & Beyond):

  • Higher-rate streaming with filtering or host visualization.