STM32 ADC tutorial using DMA with HAL Code Example

In this tutorial, we will explain the basic principles of Analog to Digital Converter (ADC) and Direct Memory Access (DMA) of the STM32 microcontroller. To get you started, we will show you how to interface multiple channels ADC using DMA in STM32 Nucleo development Board and STM32Cube IDE.

You may also like reading:

Our other STM32-related tutorials are:

STM32 ADC peripheral overview

The STM32 microcontrollers provide an Analog-to-Digital Converter (ADC) peripheral that allows you to convert analog signals into digital values. The ADC peripheral is a crucial feature for many embedded applications that involve reading analog sensors or acquiring analog signals for processing.

Here is an overview of the STM32 ADC peripheral:

Resolution

The ADC resolution determines the number of digital bits in the converted result. STM32 microcontrollers offer various ADC resolutions, such as 12-bit, 16-bit, or even higher. Like, STM32F446RE MCU supports 12-bit ADC.

Conversion Modes

The ADC can operate in different conversion modes:

  • Single Conversion Mode: In this mode, a single conversion is triggered, and the ADC converts one analog input channel at a time.
  • Continuous Conversion Mode: In continuous mode, the ADC continuously converts multiple channels without the need for retriggering.
  • Scan Mode: The scan mode allows you to convert multiple channels sequentially with a single trigger.

Sampling Time

The sampling time refers to the duration during which the ADC samples the analog input voltage. It is essential to set an appropriate sampling time to allow the ADC to capture the analog signal accurately.

For example, if the ADC clock is 14 MHz and has a sampling time of 1.5 cycles then the ADC conversion time is 1us. Check your STM32 Microcontroller datasheet to read more about ADC Sampling and Covertion time.

Channels

The STM32 ADC peripheral supports multiple channels for converting multiple analog inputs. Each channel can be individually configured with specific settings such as sampling time, resolution, and input voltage range.

Conversion Trigger

The ADC conversion can be triggered in various ways:

  • Software Trigger: Conversion is initiated by software control.
  • External Trigger: An external event or signal triggers the conversion. This can be a timer, an external interrupt, or a specific signal level change.

Interrupts and DMA

The ADC peripheral can generate interrupts upon the completion of conversion or when specific conditions are met. Additionally, you can use DMA to transfer the converted data to memory without CPU intervention.

Analog Watchdog

The ADC peripheral often includes an analog watchdog feature. This allows you to set thresholds to monitor the converted values and trigger an interrupt or take other actions if the converted value falls outside the specified range.

Calibration

STM32 ADCs typically provide calibration mechanisms to improve accuracy. These calibration techniques compensate for factors like offset and gain errors.

It’s important to consult the specific microcontroller’s reference manual and datasheet for detailed information on the ADC peripheral’s capabilities, registers, and configuration options. The STM32Cube software development platform from STMicroelectronics also offers libraries, examples, and configuration tools to simplify the ADC peripheral’s usage.

DMA in STM32 Microcontroller

DMA stands for Direct Memory Access. DMA is a feature available in many microcontrollers, including those in the STM32 series, that allows for efficient data transfer between peripherals and memory without involving the CPU.

The DMA controller acts as an intermediary between the peripherals and the memory, enabling direct data transfers. Instead of the CPU being responsible for moving data between peripherals and memory, the DMA controller takes over this task, freeing up the CPU to perform other tasks.

The DMA controller operates independently and can transfer data in various modes, such as single, circular, or burst mode. It can transfer data to or from peripherals, memory, or even between different memory locations.

Using DMA can greatly enhance the performance of a microcontroller, especially in scenarios where frequent and high-speed data transfers are required, such as audio processing from ADC, data logging, or communication protocols like UART, SPI, or I2C.

STM32 DMA overview

STM32 Multiple Channels ADC interfacing using DMA: The Project

In this section of the tutorial, we will explore the STM32 ADC peripheral in DMA mode by interfacing with three potentiometers. We will connect the three potentiometers to three of the ADC channels (ADC 1) and collect the data from the three channels at the same time by using the DMA controller without interrupting the processor. We will show the value of the ADC Channels in a serial terminal software (like Tera Term) of the computer by using UART.

STM32 ADC DMA Turorial

Component List

Component NameQuantityPurchase Link
STM32 Nucleo Development Board1Amazon
10K Potentiometer3Amazon
Breadboard1Amazon
Jumper Wire Pack1Amazon

For troubleshooting, some extremely useful test equipment

Equipment NamePurchase Link
Best Oscilloscope for ProfessionalsAmazon
Best Oscilloscope for Beginners and StudentsAmazon
Logic AnalyzerAmazon
Best Budget MultimeterAmazon
Adjustable Bench Power SupplyAmazon

Affiliate Disclosure: When you click on links to make a purchase, this can result in this website earning a commission. Affiliate programs and affiliations include, but are not limited to Amazon.com

Circuit Diagram

STM32 ADC DMA Circuit Diagram

Pin connection

STM32 NUCLEO-F446RE PinPotentiometer 1Potentiometer 2Potentiometer 3
3.3 VoltVccVccVcc
GroundGroundGroundGround
PA0 / A0Output Pin
PA1 / A1Output Pin
PA3 / A2Output Pin

Preparing STM32Cube IDE for the project

For project creation in Stm32CubeIDE, please visit your previous tutorial. Link is given below:

After creating the project in Stm32CubeIDE, Enable the ADC1 peripheral from the device configuration tools of Stm32CubeIDE. For, ADC1 we configure GPIO A pin 0 (PA0) as ADC Channel 1, GPIO A pin 1 (PA1) as ADC Channel 2, and GPIO A pin 4 (PA4) as ADC Channel 3 respectively.

STM32 ADC Configuration in STM32CUBE IDE

After enabling ADC channels, click on ADC1 Parameters Settings and configure the following parameters:

  • Select the ADC Resulation as a 12-bits
  • Enable Scan Conversion Mode
  • Enable Continuous Conversion Mode
  • Enable DMA Continous Request
  • Select the Number of conversions. In our case, we enable 3 channels for conversion
STM32 ADC Configuration in STM32CUBE IDE

Next, click on DMA Settings and select DMA Request as ADC1. Go to DMA Request Settings and select the Circular Mode for continuous DMA Request. Also, the data direction should be marked as Peripheral to Memory.

STM32  ADC DMA Configuration in STM32CUBE IDE

If the ADC Configuration is finished, then click on USART2 and Configure the peripheral. As we are using Nucleo-F446RE development board, STM32Cube IDE configure the USART2 peripheral by default for programming and debugging purpose.

STM32 USART configuration in stm32cube ide

Project Code and Description

main.c file of STM32CubeIDE

// Includes
#include "main.h"
#include "stdio.h"
#include "stdint.h"

// Private variables
 ADC_HandleTypeDef hadc1;
 DMA_HandleTypeDef hdma_adc1;
 UART_HandleTypeDef huart2;

// Private function prototypes
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_USART2_UART_Init(void);
static void MX_ADC1_Init(void);

// This array hold the the channels ADC value
volatile uint16_t adc_dma_result[3];
// This variable calculate the array length.
// In our case, array size in 3
int adc_channel_count = sizeof(adc_dma_result)/sizeof(adc_dma_result[0]);
// This flag will help to detect
// the DMA conversion completed or not
uint8_t adc_conv_complete_flag = 0;

// This character buffer array will
// store the result after conversion complete
char dma_result_buffer[100];

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  // Reset of all peripherals, Initializes the Flash interface and the Systick.
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART2_UART_Init();
  MX_ADC1_Init();

  // Initialize the DMA conversion
  HAL_ADC_Start_DMA(&hadc1, (uint32_t *) adc_dma_result , adc_channel_count);

  /* Infinite loop */
  while (1)
  {
       // when adc_conv_complete_flag is set to 1,
       // that means DMA conversion is completed
       if(adc_conv_complete_flag == 1){
	  // this snprintf function helps to convert the adc_dma_result array
	  // into string and store in dma_result_buffer character array
	  snprintf(dma_result_buffer, 100, "CH_1: %d, CH_2: %d, CH_3: %d\r\n", adc_dma_result[0], adc_dma_result[1], adc_dma_result[2]);
	  // we just send the dma_result_buffer character array with ADC values
	  // to our computer serial terminal software (Tera Term) using UART peripheral of STM32
	  HAL_UART_Transmit(&huart2, (uint8_t *) dma_result_buffer, sizeof(dma_result_buffer), HAL_MAX_DELAY);
	  // adc_conv_complete_flag variable is set to 0, because,
         // we alert this flag variable for new DMA conversion completion
	 adc_conv_complete_flag = 0;
        // delay for 500 Milliseconds
        HAL_Delay(500);
     }
  }
}

// when DMA conversion is completed, HAL_ADC_ConvCpltCallback function
// will interrupt the processor. You can find this function in
// Drivers>STM32F4xx_HAL_Drivers>stm32f4xx_hal_adc.c file as __weak attribute
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc){
	// I set adc_conv_complete_flag variable to 1 when,
	// HAL_ADC_ConvCpltCallback function is call.
	adc_conv_complete_flag = 1;
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Configure the main internal regulator output voltage
  */
  __HAL_RCC_PWR_CLK_ENABLE();
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 4;
  RCC_OscInitStruct.PLL.PLLN = 180;
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
  RCC_OscInitStruct.PLL.PLLQ = 2;
  RCC_OscInitStruct.PLL.PLLR = 2;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Activate the Over-Drive mode
  */
  if (HAL_PWREx_EnableOverDrive() != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief ADC1 Initialization Function
  * @param None
  * @retval None
  */
static void MX_ADC1_Init(void)
{
  ADC_ChannelConfTypeDef sConfig = {0};

  /** Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion)
  */
  hadc1.Instance = ADC1;
  hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
  hadc1.Init.Resolution = ADC_RESOLUTION_12B;
  hadc1.Init.ScanConvMode = ENABLE;
  hadc1.Init.ContinuousConvMode = ENABLE;
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc1.Init.NbrOfConversion = 3;
  hadc1.Init.DMAContinuousRequests = ENABLE;
  hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
  if (HAL_ADC_Init(&hadc1) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure for the selected ADC regular channel its corresponding rank in the sequencer and its sample time.
  */
  sConfig.Channel = ADC_CHANNEL_0;
  sConfig.Rank = 1;
  sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure for the selected ADC regular channel its corresponding rank in the sequencer and its sample time.
  */
  sConfig.Channel = ADC_CHANNEL_1;
  sConfig.Rank = 2;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure for the selected ADC regular channel its corresponding rank in the sequencer and its sample time.
  */
  sConfig.Channel = ADC_CHANNEL_4;
  sConfig.Rank = 3;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief USART2 Initialization Function
  * @param None
  * @retval None
  */
static void MX_USART2_UART_Init(void)
{
  /* USER CODE END USART2_Init 1 */
  huart2.Instance = USART2;
  huart2.Init.BaudRate = 115200;
  huart2.Init.WordLength = UART_WORDLENGTH_8B;
  huart2.Init.StopBits = UART_STOPBITS_1;
  huart2.Init.Parity = UART_PARITY_NONE;
  huart2.Init.Mode = UART_MODE_TX_RX;
  huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart2.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart2) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * Enable DMA controller clock
  */
static void MX_DMA_Init(void)
{

  /* DMA controller clock enable */
  __HAL_RCC_DMA2_CLK_ENABLE();

  /* DMA interrupt init */
  /* DMA2_Stream0_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);

}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOH_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);

  /*Configure GPIO pin : B1_Pin */
  GPIO_InitStruct.Pin = B1_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(B1_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pin : LD2_Pin */
  GPIO_InitStruct.Pin = LD2_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(LD2_GPIO_Port, &GPIO_InitStruct);

}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

Output of the Code

STM32 ADC DMA Output

As we mentioned earlier, we are using Tera Term Serial Terminal Software to communicate with STM32 Microcontroller using USART. It is an open source and free software. You can download the software from here. After downloading, go to Setup > Serial Port and select the PORT where the Microcontroller is connected. Also, select  Boud rate (Speed), in our case we select the Speed of 115200 for our Nucleo development board. After configuring all the parameters, click on New Setting and open the Serial Monitor window where you can see the output.

7 min

Video Tutorial

Mahamudul Hasan

I hold a B.Sc degree in Electrical & Electronic Engineering from Daffodil International University, Bangladesh. I am a Printed Circuit Board (PCB) designer and Microcontroller programmer with an avid interest in Embedded System Design and IoT. As an Engineer, I love taking challenges and love being part of the solution. My Linkedin Profile Link: https://linkedin.com/in/mheee

Recent Posts