STM32 ADC: Polling, Interrupt, DMA

STM32 ADC: Polling, Interrupt, DMA


7 minute read

Reading analog signals is essential in many embedded programming applications. Therefore, modern microcontrollers always contain analog-to-digital converters(ADC) and STM32 MCUS is not an exception. STM32 ADC peripheral provides a wide range of functionalities and it allows us to efficiently read not only a single channel but multiple analog signals at the same time.  

This article aims to deliver comprehensive guidance on STM32 ADC peripheral configuration. In addition, I will show how to sample two analog channels in Polling, Interrupt, and DMA modes. We will use STM32 CubeMx to configure the peripherals, and HAL API to develop our code. Nucleo-L476RG is the board I used in this article, but the tutorial applies to other STM32 MCU boards. 

STM32 ADC Configuration using CubeMx

The Images below show the steps to configure the ADC peripheral using STM32CubeMx:

  • Within ADC1, I use IN1 and IN2 to read analog signals. I choose a single-ended option to enable these channels.

STM32 ADC Configuration

  • I keep the rest of the parameters in default values except the clock prescaler. I divide the clock by 64. 
  • To sample two analog signals consecutively, I define the Number of Conversions as 2 
  • The next step is to define the order of channels to be sampled and the sampling time. As shown in the picture below, I sample Channel 1, then Channel 2. The sampling time is 12.5 clock cycles. 

STM32 ADC Configuration

  • Once we defined all these steps, we can generate the code. Also, do not forget to check the pins of the microcontroller where analog channels are connected. In my case, I have PC0 and PC1. STM32 ADC Pins

STM32 ADC Polling Mode

There are three ways of working with the ADC peripheral: Polling, Interrupts, and DMA. First, we will consider the Polling Mode which is the simplest among those three. It requires writing a few lines of code to start the ADC, poll the channels, obtain the data, and stop the ADC. We can do it periodically within the while loop. In addition, we can call a function for the ADC calibration for better accuracy.

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

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

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_ADC1_Init();
  // STM32 ADC POLLING MODE EXAMPLE
  HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);
  HAL_Delay(500);
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  // start ADC, poll for conversion and get the sampled data
	  HAL_Delay(1000);
	  HAL_ADC_Start(&hadc1);
	  HAL_ADC_PollForConversion(&hadc1, 1000);
	  channel1 = HAL_ADC_GetValue(&hadc1);
	  HAL_ADC_PollForConversion(&hadc1, 1000);
	  channel2 = HAL_ADC_GetValue(&hadc1);
	  HAL_ADC_Stop(&hadc1);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

To test the code, we can debug it and monitor channel1 and channel2 variables using live expressions. In the screenshot below, I have connected channel 1 to GND and channel 2 to the Power supply which leads to having almost maximum value (2^12 = 4096).

STM32 CubeMx Live Expressions

The simplicity of the Polling mode comes with a price: it is not efficient and difficult to control the sampling time. To make the sampling process smarter, in the next section we will use the sampling in the Interrupt Mode

STM32 ADC Interrupt Mode

To implement the Interrupts, we need to enable the Interrupts in the CubeMx File and the continuous conversion mode. Then, generate the code again to have these changes reflected on your code.

STM32 ADC Enable continuous conversion

STM32 ADC Enable Interrupts

To start the ADC in the interrupt mode, we need to call the HAL_ADC_Start_IT function. Since we enabled the continuous conversion mode, the STM32 MCU will periodically sample the analog channels and invoke the HAL_ADC_ConvCpltCallback function every time the sampling happens. Our only task will be reading analog data within this callback function. In addition, as I mentioned before, we are sampling two channels consecutively. To differentiate these two cases, I created a counter inside of the callback function. If you are sampling just one signal, you can remove this counter and just read the analog data. The complete implementation is shown in the code snippet below:

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    // Read & Update The ADC Result
	static uint8_t counter;
	if(counter == 0)
	{
		// channel 1 sampling
		channel1 = HAL_ADC_GetValue(&hadc1);
		counter = 1;
	}
	else
	{
		// channel 2 sampling
		channel2 = HAL_ADC_GetValue(&hadc1);
		counter = 0;
	}

}
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

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

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_ADC1_Init();
  /* USER CODE BEGIN 2 */
  HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);
  HAL_ADC_Start_IT(&hadc1);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

STM32 ADC DMA Mode

Direct Memory Access (DMA) is a powerful tool to preserve the computational power of the microcontrollers. In our specific case, we can use DMA to do automatic transactions of analog data to a buffer. So, unlike in the previous case, we do not need to read the ADC register (no need to call HAL_ADC_GetValue()) inside the callback function. Instead, DMA will handle all these steps. 

First, let's start by enabling DMA using the CubeMx tool. You can refer to the following steps to configure the DMA:

  • First, we enable DMA Continuous Requests on Parameter settings

STM32 ADC DMA

  • Next, we enable DMA in DMA settings: press 'Add' and choose ADC1. Then, choose Circular mode to have continuous conversion

STM32 ADC DMA 2

  • Finally, we can enable the DMA interrupts within NVIC Settings. After that, we can generate the code. 

STM32 ADC DMA Interrupt enable

Finally, we can call HAL_ADC_START_DMA to start the DMA for ADC. There, we need to define the array address to store data (adc_data in my code) and the number of samples (2 in my project). Also, as you may notice, our callback function is empty: data transaction to the array happens automatically in the background. 

/* USER CODE BEGIN 0 */
uint16_t adc_data[2];
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
}
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

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

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_ADC1_Init();
  /* USER CODE BEGIN 2 */
  HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);
  HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_data, 2);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Again, we can test this code in debugging mode using the Live expressions:

STM32 ADC DMA test

STM32 Programming Resources

You can also find the video tutorial of STM32 ADC

 

« Back to Blog