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.
- 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.
- 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 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).
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.
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
- Next, we enable DMA in DMA settings: press 'Add' and choose ADC1. Then, choose Circular mode to have continuous conversion
- Finally, we can enable the DMA interrupts within NVIC Settings. After that, we can generate the code.
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 Programming Resources
You can also find the video tutorial of STM32 ADC