In this tutorial, we will explore how to configure and use the ADC (Analog-to-Digital Converter) in the STM32F407 microcontroller in scan mode with DMA (Direct Memory Access). This setup is particularly useful when working with multiple analog inputs that need to be converted into digital values efficiently. We will also discuss the concept of ADC channel ranking, different operational modes such as continuous and non-continuous conversion, and the differences between normal and circular DMA modes.
Overview of ADC in STM32F407
The STM32F407 microcontroller comes with a 12-bit resolution ADC that can handle up to 16 multiplexed input channels. This makes it ideal for applications requiring the processing of multiple analog signals, such as reading from sensor arrays. The ADC supports single-shot and continuous conversion modes, with the ability to automatically scan through multiple channels in sequence. Conversion can be triggered either through software commands or external hardware signals, providing flexibility in its integration into various systems. The DMA module can be used in conjunction with the ADC to transfer conversion data directly to memory, which minimizes CPU load and allows real-time data collection for high-performance applications.
ADC Scan Mode with DMA
In scan mode, the ADC sequentially converts multiple channels, each identified by a specific rank. This ensures that channels are processed in the required order, which is especially important when dealing with structured or time-sensitive data. When used with DMA, the ADC can transfer the results of these conversions directly into a memory buffer without involving the CPU. This combination of ADC and DMA significantly enhances efficiency and reduces processing overhead, as the CPU is free to handle other tasks during data transfer.
Configuring ADC with DMA
To set up ADC in scan mode with DMA, you need to configure various peripherals and settings. First, enable the necessary clocks for the ADC and GPIO peripherals. For instance, enabling the clock for ADC1 and GPIOA can be achieved using the appropriate RCC commands. Next, configure the GPIO pins that will serve as analog inputs. For example, setting PA0 as an analog input involves configuring its mode to GPIO_Mode_AN and disabling any pull-up or pull-down resistors.
The ADC itself must be initialized with the desired resolution, scan mode, and conversion mode settings. You can enable scan mode by setting ADC_ScanConvMode to ENABLE, which allows the ADC to automatically move through the sequence of channels. The number of conversions in the sequence is set using ADC_NbrOfConversion, while each channel’s rank in the sequence is configured using the ADC_RegularChannelConfig function. This ensures that the channels are converted in the required order, with each channel assigned a unique rank.
Of course, all of these can be easily done using STM32CubeMX. An example configuration is shown in the following section.
DMA configuration involves defining the memory buffer where the ADC data will be stored, specifying the size of the buffer, and setting the transfer mode. The base address of the ADC data register and the memory buffer address is linked in the DMA configuration, and the DMA mode is set to either normal or circular, depending on the application’s needs. Finally, both the ADC and DMA modules must be enabled, and the ADC conversion process must be started using a software command or an external trigger.
Operational Modes of the ADC
The STM32F407 ADC can operate in continuous or non-continuous conversion modes. In continuous conversion mode, the ADC repeatedly converts data from the selected channels without any additional input or trigger, making it suitable for real-time data acquisition tasks. This mode is enabled by setting the ADC_ContinuousConvMode flag to ENABLE. On the other hand, the non-continuous conversion mode completes a single cycle of conversions and then stops, requiring a new trigger to restart. This mode is useful for applications that need periodic sampling or where power efficiency is critical.
DMA Modes
The DMA module operates in either normal or circular mode. In normal mode, data transfer stops once the specified number of conversions has been completed. This mode is best suited for applications where data is required only once or at periodic intervals. In contrast, circular mode continuously transfers data, looping back to the start of the memory buffer once it reaches the end. This mode is ideal for applications that require real-time monitoring or continuous data streams, as it eliminates the need for manual intervention to restart the transfer.
ADC Channel Ranking
Channel ranking is a crucial aspect of ADC configuration in scan mode. Each channel is assigned a rank that determines its position in the conversion sequence. For example, a channel with rank 1 will be converted first, followed by channels with higher ranks. This ensures that data is processed in a predictable order, which is important in multi-sensor systems where timing or channel order matters. The ranking is configured using the ADC_RegularChannelConfig function, which specifies the channel number, rank, and sample time for each channel.
STM32CubeMX Configuration
Here I am using ADC1 of the STM32F407 and its inputs Channel 0 to Channel 3. The ADC resolution is set to 12 bits, scan conversion mode enabled (to use multiple channels) and the End of Conversion Flag will be set at the end of a single channel conversion. For the multiscan to work, the number of conversions must be set to the number of channels used, in my case, 4 channels. Then I set the ranking of each channel according to the order I want the data to be stored. This means the first data will be coming from channel 0, the second data from channel 1, and so on.
Next, I add DMA to ADC1 where the mode is Normal and the increment address is set to memory. The data width is set to half-word only since our ADC resolution is just 12 bits. Remember that a whole word in STM32 is 32 bits.
My STM32F407 development board only has an 8 MHz external crystal but I want to maximize its performance. Here is my clock configuration:
Full Example Code
The following code is generated once you click “GENERATE CODE” in STM32CubeMX. I have removed the comments for brevity.
#include "main.h"
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_ADC1_Init(void);
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
while (1)
{
}
}
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
RCC_OscInitStruct.PLL.PLLM = 8;
RCC_OscInitStruct.PLL.PLLN = 160;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
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();
}
}
static void MX_ADC1_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = ENABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
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 = 4;
hadc1.Init.DMAContinuousRequests = DISABLE;
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
if (HAL_ADC_Init(&hadc1) != HAL_OK)
{
Error_Handler();
}
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = 2;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
sConfig.Channel = ADC_CHANNEL_2;
sConfig.Rank = 3;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
sConfig.Channel = ADC_CHANNEL_3;
sConfig.Rank = 4;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
}
static void MX_DMA_Init(void)
{
__HAL_RCC_DMA2_CLK_ENABLE();
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}
static void MX_GPIO_Init(void)
{
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOH_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
}
void Error_Handler(void)
{
__disable_irq();
while (1)
{
}
}
void assert_failed(uint8_t *file, uint32_t line)
{
}
We will need to add to this! First, we initiate ADC reading with DMA support using:
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcBuffer, BUFFER_SIZE)
Where adcBuffer is where the ADC results are stored and the BUFFER_SIZE is the number of ADC channels used.
#define BUFFER_SIZE 4
uint16_t adcBuffer[BUFFER_SIZE];
In our example, we disabled continuous mode so the conversion stops when HAL_ADC_Start_DMA is called and done. If you need always to do the conversion, there are two options: to put HAL_ADC_Start_DMA inside the loop or to enable continuous conversion mode.
hadc1.Init.ContinuousConvMode = ENABLE;
Next, we add a callback function that triggers every time the conversion is done.
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc1)
{
// This function is automatically called when DMA completes a transfer
}
You can add to this function whatever you want done when the conversion completes.
So our full example code is now:
#include "main.h"
#define BUFFER_SIZE 4
uint16_t adcBuffer[BUFFER_SIZE];
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_ADC1_Init(void);
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
while (1)
{
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcBuffer, BUFFER_SIZE)
}
}
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
RCC_OscInitStruct.PLL.PLLM = 8;
RCC_OscInitStruct.PLL.PLLN = 160;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
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();
}
}
static void MX_ADC1_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = ENABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
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 = 4;
hadc1.Init.DMAContinuousRequests = DISABLE;
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
if (HAL_ADC_Init(&hadc1) != HAL_OK)
{
Error_Handler();
}
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = 2;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
sConfig.Channel = ADC_CHANNEL_2;
sConfig.Rank = 3;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
sConfig.Channel = ADC_CHANNEL_3;
sConfig.Rank = 4;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
}
static void MX_DMA_Init(void)
{
__HAL_RCC_DMA2_CLK_ENABLE();
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}
static void MX_GPIO_Init(void)
{
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOH_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc1)
{
// This function is automatically called when DMA completes a transfer
}
void Error_Handler(void)
{
__disable_irq();
while (1)
{
}
}
void assert_failed(uint8_t *file, uint32_t line)
{
}
Summary
This tutorial outlined how to configure the STM32F407 ADC in scan mode with DMA for efficient data acquisition from multiple channels. We explored the roles of continuous and non-continuous conversion modes, discussed the benefits of normal versus circular DMA, and demonstrated how to rank ADC channels in the conversion sequence. This setup is highly effective for applications requiring real-time data processing and minimizes CPU overhead, making it ideal for embedded systems.