How to Configure STM32 ADC for Periodic Sampling using DMA and Timers

A detailed guide to setting up the Analog-to-Digital Converter (ADC) on the STM32H7 series for autonomous, periodic data acquisition with minimal CPU overhead.

As embedded systems engineers, one of our core challenges is efficiently bridging the analog and digital worlds. This tutorial provides the professional, low-overhead method for continuous Analog-to-Digital Conversion (ADC) on high-performance STM32 microcontrollers, specifically using the H7 series. We will utilize a combination of the ADC, a Hardware Timer, and DMA (Direct Memory Access) to create a set-and-forget acquisition system.

1. The Pro Approach: Why Use ADC + Timer + DMA?

In high-performance embedded systems, minimizing CPU burden is paramount. When reading an ADC, the traditional methods are inefficient:

MethodCPU InteractionCPU LoadUse Case
PollingCPU constantly checks status flags.HighOne-shot, non-time-critical reads.
InterruptCPU jumps to a handler for every sample.ModerateLow-frequency periodic sampling.
DMADedicated hardware moves data to RAM.MinimalHigh-speed, continuous, and periodic acquisition. (Our Goal)

By using a Hardware Timer to trigger the ADC, we ensure perfectly periodic sampling. DMA then handles the conversion and storage, offloading the CPU entirely.

2. The Analog Front-End: Preparing Your Signal

Before writing any line of code, the analog signal must be conditioned. In our example, we are reading potentiometers that vary a voltage between 0V and 3.3V.

Crucial Step: Signal Filtering

Image of RC Low Pass Filter Schematic

Raw analog signals are always noisy. To prevent high-frequency interference (like digital switching noise) from corrupting your ADC readings, you must implement an RC Low-Pass Filter (Resistor-Capacitor) immediately before the ADC pin.

  • Note: The professional design in the reference video uses an RC filter with a cutoff frequency around 310 Hz.

3. CubeIDE Configuration: The Three-Step Hardware Setup

We use STM32CubeIDE to establish the core configuration for our peripherals.

3.1. System and Clock Configuration

  1. Debugging: Set System Core -> SYS -> Debug to Serial Wire.
  2. Clock Speed: In the Clock Configuration tab, set the MCU core frequency to a high value, such as 280 MHz, letting the tool resolve the PLL settings.

3.2. ADC Setup and Ranking

The goal is a sequence conversion for three inputs (e.g., IN16, IN17, and IN14).

  1. ADC Parameters:
    • Resolution: Set to 16-bit.
    • Number of Conversions: Set to 3.
    • Conversion Selection: Set to End of Sequence of Conversions.
    • Sampling Time: Use a long time, e.g., 400 cycles, for increased accuracy for slow signals.
  2. Trigger Source: Set the External Trigger Selection to your chosen timer output, for example, Timer 8 Trigger Out Event (TIM8_TRGO).
  3. Ranks: Define the exact order of sampling (e.g., Rank 1: Ch 16, Rank 2: Ch 17, Rank 3: Ch 14).

3.3. DMA and Timer Setup (The Sampling Loop)

The DMA handles data movement, and the Timer controls the rate.

  1. DMA Configuration:
    • Add a DMA Request for ADC1.
    • Direction: Peripheral to Memory.
    • Data Width: Half Word (16-bit).
    • Mode: Set to Circular. This is critical for continuous, autonomous operation.
    • Linkage: Ensure ADC Parameter Settings is set to DMA Circular Mode.
  2. Timer Configuration (10 Hz Example):
    • Select Timers -> TIM8 and set the Clock Source to Internal Clock.
    • We aim for fupdate​=10 Hz from an fcore​=280 MHz clock.
    • The formula is:f_update=(Prescaler+1)×(Period+1)f_core​
    • Settings: Prescaler to 27999 and Counter Period to 999 to achieve exactly 10 Hz.
    • Trigger Output (TRGO): Set Trigger Event Selection to Update Event.

4. The Firmware: Writing the C Code (main.c)

After code generation, implement the control and data processing logic.

4.1. Define Global Data Buffers

The DMA will write directly to these buffers. Always use the volatile keyword to prevent the compiler from optimizing access to these externally modified variables.

#define ADC_NUM_CONVERSIONS 3
volatile uint16_t ADC_Data[ADC_NUM_CONVERSIONS];
volatile float Pot_Voltage[ADC_NUM_CONVERSIONS];

4.2. The Startup Sequence (main())

Initialize and activate the entire acquisition pipeline in sequence.

/* 1. Calibrate the ADC for better accuracy */
HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);

/* 2. Start ADC with DMA transfer */
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)ADC_Data, ADC_NUM_CONVERSIONS);

/* 3. Start the Timer, which triggers the ADC via TRGO */
HAL_TIM_Base_Start(&htim8);

4.3. Data Conversion Callback

This callback executes every time the DMA successfully transfers the entire sequence of samples (i.e., every 1/10 of a second).

// Conversion factor: V_ref / (2^Resolution - 1)
#define ADC_RAW_TO_VOLTAGE (3.3f / 65535.0f) // For 3.3V VREF and 16-bit resolution

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    if (hadc->Instance == ADC1) {
        // Convert raw ADC data to actual voltage values
        for (int i = 0; i < ADC_NUM_CONVERSIONS; i++) {
            Pot_Voltage[i] = (float)ADC_Data[i] * ADC_RAW_TO_VOLTAGE;
        }
        // The Pot_Voltage[] array is now ready to be consumed by the application logic
    }
}

5. Advanced H7 Quirk: Conquering Cache Coherency

This section is mandatory for reliable operation on cached architectures like the STM32H7.

The Problem: The H7’s high-speed Data Cache (D-Cache) stores copies of RAM data for faster CPU access. When DMA writes new ADC data directly to the RAM buffer, the CPU might continue reading stale data from its D-Cache, ignoring the updates made by the DMA. This makes your system appear broken!

The Solution: Non-Cacheable DMA Buffer Memory

For non-high-speed ADC applications (like reading potentiometers), the simplest workaround is to place the DMA buffer in a dedicated RAM region that the CPU is instructed never to cache.

  1. Enable MPU/Caches: In CubeIDE, enable Instruction Cache, Data Cache, and the Memory Protection Unit (MPU).
  2. MPU Configuration: Configure an MPU region (e.g., Region 0) to cover a low-priority RAM section like RAM_SRD. Crucially, disable the Cachable permission.
  3. Linker Script Update: Modify the .ld file (e.g., STM32H7B0VBTX_FLASH.ld) to define a new section (.no_cache_dma) that targets the non-cacheable RAM_SRD region.
  4. Buffer Placement: Apply a special compiler attribute to your DMA buffer to force it into this non-cacheable region:
// Add this attribute to place the variable in the non-cacheable RAM_SRD section
__attribute__((section(".no_cache_dma"), aligned (32)))
volatile uint16_t ADC_Data[ADC_NUM_CONVERSIONS];

By placing the buffer outside the D-Cache domain, you ensure the CPU always reads the most current data written by the DMA engine.

6. Conclusion: A High-Performance Loop

You have now successfully engineered an STM32 ADC acquisition system that operates autonomously. The Hardware Timer acts as a precise clock source, triggering the ADC. The DMA handles the data pipeline, and the CPU is only awakened via the HAL_ADC_ConvCpltCallback to perform the final conversion from raw data to a usable voltage value. This professional setup ensures accurate, periodic sampling with minimal resource consumption.

For a full visual guide and a deep dive into the practical steps, make sure to check out the original video: http://www.youtube.com/watch?v=_K3GvQkyarg

Leave a Reply

Your email address will not be published. Required fields are marked *