Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 42

DMA for ADC Result Transfer

Recommended resources

Hello! Welcome to the next lesson in our module on Data Acquisition and DMA.

In our last session, we mastered the fundamental mechanics of the DMA controller by implementing a memory-to-memory transfer. You learned how to configure a DMA channel's source, destination, data width, and increment modes, and how to use an interrupt to signal the completion of the transfer. This was the "Hello, World!" of DMA, proving that we can move data without CPU intervention.

Today, we apply that knowledge to a real-world scenario. Your learning outcome is to configure DMA for a peripheral-to-memory transfer to read ADC results without CPU intervention. This is one of the most common and powerful use cases for DMA in embedded systems. We will move from simply copying memory to offloading a critical data acquisition task from the CPU, which is a vital skill for any firmware engineer.

1. The 'Why': ADC, CPU, and the DMA Solution

Imagine needing to measure a sensor's voltage 1000 times per second.

  • Polling Method: Your CPU would be stuck in a tight loop, constantly asking the ADC, "Are you done yet?" This wastes millions of CPU cycles that could be used for other tasks, like processing user input or managing a communication protocol.
  • Interrupt Method: This is better. The CPU starts the ADC and goes off to do other work. When the ADC is done, it triggers an interrupt, forcing the CPU to stop, save its context, read the ADC value, store it, restore context, and resume. At 1000 Hz, this context switching adds significant overhead and can still bog down the system.
  • DMA Method: The CPU tells the DMA controller: "When the ADC has a new value, please copy it from the ADC's data register directly to this variable in memory." The CPU then starts the ADC and is completely free. The entire transfer happens in the background, handled by dedicated hardware. This is the most efficient method, especially for high-frequency or multi-channel data acquisition.
Direct Memory Access (DMA) Concept Diagram
This diagram illustrates the DMA method. The data flows directly from the peripheral (our ADC) to memory, bypassing the Processor (CPU) entirely. The CPU only needs to set up the transfer initially.

Let's watch a short video to solidify the concept of peripheral-to-memory transfers.

STM32 DMA PT 1

The video 'STM32 DMA PT 1' by Eddie Amaya provides a clear, high-level explanation of the different DMA transfer types. We'll focus on the part that explains peripheral-to-memory transfers.

Watch the following two segments: 00:00 - 04:11: This covers the fundamental purpose of DMA and the three transfer types. Pay close attention to the 'peripheral to memory' explanation. 06:33 - 08:08: This segment conceptually walks through a peripheral-to-memory transfer using a UART example. The logic is identical for an ADC: the peripheral (ADC) signals it has data, which triggers a DMA request to move that data to a memory buffer.


2. Hands-On Lab: Reading an ADC Channel with DMA

It's time to build this system. We will configure the ADC to perform a single conversion and use the DMA to automatically capture the result into a variable.

Step 1: Project Configuration in STM32CubeIDE

  1. Create a new STM32 project for your STM32F103C8T6 board, or open the one from the previous lesson.
  2. In Pinout & Configuration, go to Analog > ADC1.
  3. Check the box for one of the input channels, for example, IN0. This will assign PA0 as an analog input. You can connect the wiper of a potentiometer to this pin to have a variable voltage source.
  4. In the ADC1 configuration pane, under Parameter Settings, you can leave most settings at their default for now.
  5. Now for the key part. Go to the DMA Settings tab within the ADC1 configuration.
  6. Click Add and select ADC1. A DMA channel will be configured for you.
  7. Verify the following settings:
    • Direction: Peripheral To Memory. This is the core of today's lesson.
    • Mode: Normal. This tells the DMA to stop after the specified number of transfers is complete. We'll explore Circular mode in the next lesson.
    • Increment Address: Check the box for Memory, but leave the box for Peripheral unchecked. This is critical. We want the DMA to write to sequential locations in our memory buffer (if it were larger than 1), but the source address—the ADC's data register (ADC1->DR)—is fixed and does not change.
    • Data Width: Set both Peripheral and Memory to Half Word. The STM32F1's ADC has a 12-bit resolution, but the result is stored in a 16-bit (Half Word) data register.
  8. Go to the NVIC Settings tab and ensure the ADC1 and ADC2 global interrupt is enabled. The HAL uses this interrupt to fire the completion callback after the DMA transfer is done.
STM32CubeMX ADC DMA Peripheral-to-Memory Configuration
This image shows a similar configuration in CubeMX. Note the key settings: Direction is 'Peripheral To Memory', Mode is 'Normal', and Memory Increment is enabled while Peripheral Increment is disabled (not shown, but implied by the use case).
  1. Save the .ioc file to generate the code.

Step 2: Writing the Application Code

Navigate to Core/Src/main.c. The code is even simpler than our previous memory-to-memory example.

  1. In the /* USER CODE BEGIN PV */ section, define a variable to hold the ADC result and a flag to signal completion.

    /* USER CODE BEGIN PV */
    uint16_t adc_value;
    volatile uint8_t conversion_complete_flag = 0;
    /* USER CODE END PV */
    

    Remember, the volatile keyword is essential for variables modified within an interrupt and accessed outside of it.

  2. In the /* USER CODE BEGIN 4 */ section, implement the HAL_ADC_ConvCpltCallback. This weak function is automatically called by the HAL driver when an ADC conversion (including its associated DMA transfer) is complete.

    /* USER CODE BEGIN 4 */
    void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
    {
      // This function is called when a conversion is complete and DMA has transferred the result.
      conversion_complete_flag = 1;
    }
    /* USER CODE END 4 */
    
  3. Finally, in main(), inside the while(1) loop, we'll start the conversion and wait for it to complete.

    /* USER CODE BEGIN WHILE */
    while (1)
    {
      // Reset the flag
      conversion_complete_flag = 0;
      
      // Start the ADC in DMA mode
      HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&adc_value, 1);
      
      // Wait for the conversion to complete
      while (conversion_complete_flag == 0)
      {
        // CPU can do other tasks here...
      }
      
      // For this demo, let's add a delay before the next conversion
      HAL_Delay(500);
      
      /* USER CODE END WHILE */
      /* USER CODE BEGIN 3 */
    }
    /* USER CODE END 3 */
    

    Let's dissect HAL_ADC_Start_DMA():

    • &hadc1: A pointer to the ADC handle structure.
    • (uint32_t*)&adc_value: The memory address where the DMA should store the result.
    • 1: The number of data items to transfer. Since we are in Normal mode, the DMA will perform one transfer and then stop.

Step 3: Verification with the Debugger

  1. Place a breakpoint on the HAL_Delay(500); line.
  2. Build the project and start a Debug Session.
  3. Add the adc_value variable to the Expressions view.
  4. Resume (F8) execution.
  5. The code will execute HAL_ADC_Start_DMA(), which starts the ADC. The CPU will then enter the while loop. The ADC will perform its conversion, and upon completion, it will trigger the DMA. The DMA controller will copy the value from ADC1->DR to your adc_value variable. Finally, the HAL_ADC_ConvCpltCallback will be called, setting your flag and allowing the CPU to break out of the while loop.
  6. When execution pauses at your breakpoint, inspect adc_value in the Expressions view. You will see a value between 0 and 4095 (since it's a 12-bit ADC). If you have a potentiometer connected, turn it and resume execution to see the value change on the next loop iteration.

You have successfully used the DMA to acquire data from a peripheral with minimal CPU overhead!


3. Under the Hood: From HAL to Registers

Your goal is to become a professional firmware engineer, so it's valuable to understand what the HAL functions are doing at the register level. This knowledge is indispensable for debugging complex issues.

The article below provides an excellent register-level explanation. You don't need to implement it now, but reading through the configuration steps will connect the HAL abstractions to the hardware reality.

STM32 ADC DMA via Registers Tutorial

The article 'STM32 ADC DMA via Registers Tutorial' from ControllersTech walks through a bare-metal implementation. We'll use it to understand the key registers involved.

Read through the sections on ADC and DMA configuration. Focus on understanding the purpose of these registers, which the HAL configures for you: ADC Control Register 2 (ADC1->CR2): Note the DMA bit (Bit 8) that enables DMA requests from the ADC. DMA Configuration Register (DMAx_StreamY->CR): See how direction, circular mode, increment mode, and data size are set. DMA Address Registers (->PAR, ->M0AR): The PAR (Peripheral Address Register) is set to &ADC1->DR, and the M0AR (Memory 0 Address Register) is set to the address of your buffer (&adc_value). DMA Number of Data Register (->NDTR): This is where the transfer count (in our case, 1) is loaded.

By understanding these registers, you can appreciate that HAL_ADC_Start_DMA() is a convenient wrapper that orchestrates the configuration of both the ADC and DMA peripherals to work together seamlessly.


Conclusion

In this lesson, you took a significant step forward by applying your DMA knowledge to a practical data acquisition problem. You have learned how to offload the repetitive task of reading an ADC to the DMA controller, freeing the CPU for more complex work.

Key Takeaways:

  • Peripheral-to-memory is the most common and powerful DMA transfer mode.
  • The peripheral (ADC) finishing its operation acts as the trigger for the DMA transfer.
  • Key configuration details include setting the direction to Peripheral To Memory, enabling memory address increment, and disabling peripheral address increment.
  • The HAL_ADC_Start_DMA() function is the standard HAL call to initiate an ADC conversion sequence that will be handled by the DMA.
  • The HAL_ADC_ConvCpltCallback() is the appropriate callback to use for signaling the end of a DMA-managed ADC conversion.

Preview of the Next Lesson:
We've successfully read one channel, one time. But what if you need to continuously monitor multiple sensors? In the next lesson, we will set up the ADC in scan mode with DMA to continuously acquire data from multiple channels. We will switch the DMA to Circular mode, a powerful technique that allows for creating a seamless, endless data acquisition pipeline.