STM32 ADC Multiple Channels Using LL Drivers (DMA Mode)
Analog to Digital Converter, or ADC, is one of the most commonly used peripherals in STM32. In Part 1 of this ADC series, we learned how to use ADC in blocking mode and interrupt mode using LL drivers.
Both methods work well, but they are not efficient when we read multiple ADC channels continuously. This is where DMA comes into the picture.
In this tutorial, we will learn how to use STM32 ADC with DMA for multiple channels using Low Layer (LL) drivers. We will configure the ADC in scan mode, enable DMA, and store all ADC values directly into a memory buffer. Then convert the ADC values to voltage range and print the output on the serial console.
The CPU will not wait for conversion. It will not handle interrupts for every channel. DMA will do all the heavy lifting for us.
Prerequisites:
- Since this is Part 2 in the ADC series with LL drivers, I would advise you to go through the PART 1 too.
- I am going to use UART to redirect the printf output to serial console. Please go through the UART tutorial also.

Why We Should Use DMA with STM32 ADC
Before we move forward, we should answer one very important question.
Can we read multiple ADC channels without DMA?
Yes, we can. STM32 allows reading multiple channels even in blocking and interrupt modes. But the way it works is not ideal.
Technically, STM32 allows configuring more than one ADC channel even in blocking or interrupt mode. But the ADC hardware still produces one conversion result at a time as there is only one ADC data register.
So without DMA:
- Each channel conversion must be handled separately
- The CPU must read the ADC data register after every conversion
- Channel sequencing must be managed in software
In blocking mode, the CPU waits for each channel conversion to finish.
In interrupt mode, the CPU handles an interrupt for every single conversion.
This quickly becomes complex and inefficient when more channels are added. Because of this, blocking and interrupt modes are not practical solutions for multi-channel ADC.
DMA solves this exact problem by automatically storing each channel result into memory in the correct order.
How DMA Solves These Problems
DMA completely changes how multiple ADC channels are handled.
With DMA:
- ADC runs in scan mode
- It converts all configured channels one by one
- DMA automatically stores each result into a buffer
The CPU does not:
- Wait for conversions
- Handle per-channel interrupts
- Copy data manually
All channel values land in memory in the correct order. This makes multiple ADC channels practical and efficient.
The image below shows how ADC multi-channel conversion works without DMA and how DMA simplifies the entire process by directly storing ADC values into memory.
STM32 ADC DMA Working Concept
Before writing any code, we should clearly understand how ADC and DMA work together in STM32. Once the concept is clear, the code will make much more sense.
ADC Scan Mode for Multiple Channels
To read multiple ADC channels, STM32 uses scan mode. In scan mode:
- We configure more than one ADC channel
- Each channel is given a rank
- ADC converts channels one after another
For example:
- Rank 1 → Channel 0
- Rank 2 → Channel 1
- Rank 3 → Channel 4
- Rank 4 → Channel 8
The ADC follows this order every time it runs.
Without DMA, the CPU must read the result after every conversion.
With DMA, the ADC simply keeps converting and hands the data over automatically.
DMA Circular Buffer Concept
DMA needs a place to store ADC results. That place is a memory buffer. We define an array in RAM, for example: uint16_t buffer[4];
buffer[0]for Channel 0buffer[1]for Channel 1buffer[2]for Channel 4buffer[3]for Channel 8
When DMA is enabled:
- Each ADC conversion result is copied into the buffer
- The order matches the ADC scan sequence
If circular mode is used, DMA does not stop after one transfer. It goes back to the first buffer location and starts again. This means the buffer always contains latest ADC values for all channels.
Data Flow from ADC to Memory
The complete data flow looks like this:
ADC converts a channel → Result goes to ADC data register → DMA reads the data register → DMA writes the value into memory buffer.
The image below shows how STM32 ADC works in scan mode with DMA and how the conversion results are automatically stored into a circular memory buffer without CPU involvement.
Later, the CPU simply reads the buffer whenever it needs the ADC values. This is why ADC with DMA is fast, clean, and reliable, especially for multiple channels.
STM32CubeMX Configuration for ADC Multiple Channels
In this section, we will configure the ADC using STM32CubeMX. We will only do the basic configuration here. The idea is to let CubeMX handle the peripheral setup, while we write the ADC logic ourselves using LL drivers in STM32CubeIDE.
STM32 Clock Configuration
Before generating the project, we must configure the system clock. Open the Clock Configuration tab in CubeMX.
For the STM32F446RE, we will use the 8 MHz external crystal in bypass mode. This is the default clock source on the Nucleo board. The bypass mode uses the clock from the on-board ST-Link MCU.
We will feed this into the PLL and run the system at 180 MHz, which is the maximum frequency for this MCU.
The image below shows the complete PLL and clock configuration used to achieve 180 MHz.
This fast system clock ensures accurate delays and smooth blinking using LL drivers. The SysTick timer will also run from this 180 MHz system clock, which becomes important in the delay functions we will use later.
ADC Configuration
For this tutorial, we will use ADC1 in scan mode with DMA. The image below shows the ADC configuration for STM32F446RE in CubeMX.
We will cover the theory of ADC in more detail as we move ahead in this ADC series. For now, we will focus only on the settings that are important for multi-channel ADC with DMA.
Resolution
This option selects the ADC resolution.
A higher resolution gives more accurate readings, but it also slightly increases the conversion time. In most applications, 12-bit resolution is the best choice and is commonly used.
STM32F446 supports:
- 12-bit
- 10-bit
- 8-bit
- 6-bit
For this tutorial, we use 12-bit resolution.
With 12-bit resolution, the ADC output range is:
- 0 to 4095 (2¹² − 1)
End of Conversion Selection (EOC Selection)
This setting decides when the End Of Conversion flag is set.
There are two options:
- End of each conversion (EOC)
- End of entire sequence (EOS)
For multi-channel ADC with DMA, this setting is very important. Since we are converting multiple channels in scan mode, we want the ADC to treat the whole sequence as one operation.
Therefore, the End of Sequence (EOS) option is used.
This ensures that the ADC completes all channel conversions before signaling completion.
STM32F103C8 does not let us configure the ADC Resolution, it uses 12-bit by default.
It also does not let us choose the End Of Conversion Flag. By default the End Of Conversion is set to End Of Sequence (EOS).
Sampling Configuration
We are using multiple ADC channels, so each channel is assigned a rank. The ADC converts channels in the order of their ranks.
For example:
- Rank 1 → Channel 0
- Rank 2 → Channel 1
- Rank 3 → Channel 4
- Rank 4 → Channel 8
Each channel can have its own sampling time.
In this tutorial, we use a higher sampling time for better stability. Analog signals like potentiometers and sensors are often noisy. A longer sampling time allows the ADC to read the voltage more reliably.
This slightly increases conversion time, but it gives more stable results.
Data Alignment
This option decides how ADC data is stored in the data register. We use right alignment.
Right alignment is preferred because:
- The ADC value appears directly as a normal number
- No extra shifting is required in software
Left alignment is mainly useful when working with lower resolutions.
Scan Conversion Mode
Scan mode allows the ADC to convert multiple channels automatically.
For this tutorial:
- Scan mode is enabled
- Multiple channels are converted in sequence
Scan mode is mandatory when working with ADC + DMA and more than one channel.
Continuous Conversion Mode
When continuous conversion mode is enabled, the ADC keeps converting automatically after the first start command.
In this mode:
- The ADC runs continuously
- There is no need to trigger each conversion manually
- The conversion sequence repeats again and again
Since we are using ADC with DMA, continuous mode makes perfect sense here. The ADC continuously converts all configured channels in scan mode. DMA continuously transfers the conversion results into the memory buffer.
This combination allows:
- Continuous sampling
- Minimum CPU involvement
- Always-updated ADC values in memory
For this tutorial, continuous conversion mode is enabled because it works naturally with DMA and multi-channel ADC.
Discontinuous Conversion Mode
Discontinuous mode works only with scan mode. It allows the ADC to convert a limited number of channels per trigger instead of the full sequence.
This mode is useful in advanced applications, but it is not required for this tutorial. So we keep discontinuous mode disabled.
DMA Continuous Requests
This option allows the ADC to generate DMA requests continuously. Since we are using ADC with DMA, this option must be enabled. It allows DMA to keep transferring ADC data into memory without stopping after one sequence.
Enable ADC DMA
To use ADC DMA mode, we must enable it first. Go to the DMA Settings section inside the ADC configuration and add the DMA request.
Make sure the DMA mode is set to circular. This allows the DMA to start another conversion as soon as the previous one finishes. The ADC is configured in 12bit Resolution, therefore we must at least use the Data Width of Half Word (16 bits).
UART Configuration
I am going to use UART to print the ADC result on the serial console. I have already covered a tutorial explaining how to configure and use UART peripheral of the STM32 with LL drivers. The image below shows the UART configuration.
To enable USART2 in CubeMX:
- Set the Mode to Asynchronous.
- CubeMX automatically assigns the correct pins:
- PA2 -> TX
- PA3 -> RX
- Keep the Baud Rate to a common value like 115200.
Enabling the LL Drivers
Now open Project Manager → Advanced Settings. Enable LL Drivers for all peripherals. This ensures CubeMX generates clean, lightweight LL code.
The image below shows the LL driver selection in CubeMX.

This step is important because CubeMX uses HAL by default. Switching to LL gives you direct control over the GPIO registers.
Next, go to Code Generator and select “Generate Peripheral Initialization as a pair of .c/.h files per peripheral”. This will generate a separate source files for all the peripherals we will use in a project.
Wiring Diagram for Multiple ADC Channels
I am going to use an Analog output device, which will generate the analog voltages on 4 different channels. These outputs will be connected to the STM32 ADC1 channels. The image below shows the wiring between the Analog device and the STM32.
I am using ADC1 Channel 0, channel 1, channel 4 and channel 8. These channels are chosen because their pins are easier to connect on the nucleo-F446 dev board. Each output from the Analog device is connected to a STM32 ADC channel.
I have also connected the grounds together. This is to make sure there is a common reference point for both the devices.
STM32 ADC DMA Code Walkthrough Using LL Drivers
In this section, we will walk through the code step by step and understand how ADC with DMA works using LL drivers.
ADC Buffer and Global Variables
First, we need a buffer where DMA will store ADC values. Since we are converting four ADC channels, we define an array of size four.
uint16_t ADC_Buffer[4];Each index of this buffer maps directly to an ADC channel based on the scan order. We also define variables to convert raw ADC values into voltages.
float V0, V1, V2, V3;DMA keeps updating ADC_Buffer continuously. We simply read from this buffer whenever we need the latest ADC values.
Retargeting printf Using USART
To display ADC values, we use printf. For this to work, we must retarget printf to USART.
Here is the required function:
int __io_putchar (int ch)
{
while (!LL_USART_IsActiveFlag_TXE(USART2));
LL_USART_TransmitData8(USART2, (char)ch);
return ch;
}This sends every character printed by printf through USART2. Once this is done, we can directly use printf inside the main loop to view ADC values on the serial terminal.
Starting ADC Conversion with DMA
Now we configure and start ADC with DMA.
We first disable the DMA stream before configuring it. This is mandatory to avoid undefined behavior.
LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_0);
while (LL_DMA_IsEnabledStream(DMA2, LL_DMA_STREAM_0));Next, we configure the peripheral and memory addresses.
LL_DMA_SetPeriphAddress(DMA2, LL_DMA_STREAM_0, LL_ADC_DMA_GetRegAddr(ADC1, LL_ADC_DMA_REG_REGULAR_DATA));
LL_DMA_SetMemoryAddress(DMA2, LL_DMA_STREAM_0, (uint32_t)ADC_Buffer);
LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_0, 4);Then we enable the DMA stream and start ADC conversion.
LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_0);
LL_ADC_Enable(ADC1);
LL_ADC_REG_StartConversionSWStart(ADC1);At this point:
- ADC starts converting channels in scan mode
- DMA transfers results into
ADC_Buffer - The process runs continuously
DMA Interrupt Handler
DMA can generate an interrupt when the transfer is complete. Here is the DMA interrupt handler used in this tutorial:
void ADC_DMA_IRQHandler(void)
{
if (LL_DMA_IsActiveFlag_TC0(DMA2))
{
LL_DMA_ClearFlag_TC0(DMA2);
}
}In this example, we only clear the interrupt flag. Later, this interrupt can be used to:
- Process ADC data
- Trigger calculations
- Synchronize tasks
For now, clearing the flag is enough.
Reading ADC Values in main Loop
DMA continuously updates the ADC buffer. So inside the main loop, we only read the buffer and process the data.
V0 = (float)(ADC_Buffer[0] * 3.3 / 4095);
V1 = (float)(ADC_Buffer[1] * 3.3 / 4095);
V2 = (float)(ADC_Buffer[2] * 3.3 / 4095);
V3 = (float)(ADC_Buffer[3] * 3.3 / 4095);Each value is converted into voltage using:
- Reference voltage = 3.3V
- ADC resolution = 12-bit
Finally, we print the values.
printf("CH0: %.2f, CH1: %.2f, CH4: %.2f, CH8: %.2f\n\n", V0, V1, V2, V3);DMA keeps running in the background. The buffer always contains the latest ADC values.
Complete STM32 ADC DMA Multiple Channel Code Example
Below is the complete code for reading 4 ADC1 channels using the DMA circular mode.
int __io_putchar (int ch)
{
while (!LL_USART_IsActiveFlag_TXE(USART2));
LL_USART_TransmitData8(USART2, (char)ch);
return ch;
}
uint16_t ADC_Buffer[4];
float V0, V1, V2, V3;
void ADC_DMA_Start(uint16_t *buffer, size_t len)
{
LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_0);
while (LL_DMA_IsEnabledStream(DMA2, LL_DMA_STREAM_0));
LL_DMA_SetPeriphAddress(DMA2, LL_DMA_STREAM_0, LL_ADC_DMA_GetRegAddr(ADC1, LL_ADC_DMA_REG_REGULAR_DATA));
LL_DMA_SetMemoryAddress(DMA2, LL_DMA_STREAM_0, (uint32_t)buffer);
LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_0, len);
LL_DMA_EnableIT_TC(DMA2, LL_DMA_STREAM_0);
LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_0);
LL_ADC_Enable(ADC1);
LL_ADC_REG_StartConversionSWStart(ADC1);
}
void ADC_DMA_IRQHandler(void)
{
if (LL_DMA_IsActiveFlag_TC0(DMA2))
{
LL_DMA_ClearFlag_TC0(DMA2);
}
}
int main(void)
{
LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_SYSCFG);
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_PWR);
NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
NVIC_SetPriority(SysTick_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),15, 0));
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
MX_ADC1_Init();
ADC_DMA_Start(ADC_Buffer, 4);
while (1)
{
V0 = (float)(ADC_Buffer[0] * 3.3 / 4095);
V1 = (float)(ADC_Buffer[1] * 3.3 / 4095);
V2 = (float)(ADC_Buffer[2] * 3.3 / 4095);
V3 = (float)(ADC_Buffer[3] * 3.3 / 4095);
printf("CH0: %.2f, CH1: %.2f, CH4: %.2f, CH8: %.2f\n\n",
V0, V1, V2, V3);
LL_mDelay(200);
}
}Output of STM32 ADC DMA Multiple Channels
The image below shows the serial monitor output printed using printf, and the voltage generated by the analog device as well.
Each line displays:
- ADC Channel 0 voltage is 0V, same as the CH0 voltage of the Analog device.
- ADC Channel 1 voltage is 3.3V, same as the CH1 voltage generated by the device.
- ADC Channel 4 voltage is 0.77V, almost same as the CH4 voltage of the device.
- ADC Channel 8 voltage is 2.83V, again almost equal to the CH8 voltage of the device.
The values update continuously because:
- ADC runs in scan mode
- DMA keeps updating the buffer
- The CPU only reads the latest values
This confirms that STM32 ADC with DMA for multiple channels is working correctly.
Conclusion
In this tutorial, we covered how to use STM32 ADC with DMA for multiple channels using LL drivers. We started by understanding why blocking and interrupt modes are not practical for multi-channel ADC, then explored the working concept of ADC scan mode and DMA circular buffering. Finally, we walked through the complete code, including buffer setup, DMA configuration, interrupt handling, and reading ADC values using printf.
Using ADC with DMA makes real-world STM32 projects far more efficient and scalable. It allows continuous, multi-channel sampling with minimal CPU involvement, stable timing, and clean code. This approach is especially useful in applications like sensor data acquisition, motor control, data logging, and monitoring systems where multiple analog signals must be sampled reliably and continuously.
Browse More STM32 LL Tutorials
STM32 LL GPIO Input and EXTI Interrupt Tutorial: Read Buttons With Low-Level Drivers
STM32 UART using LL Drivers (Part 1): Transmit using Polling Mode
STM32 UART using LL Drivers (Part 2): Transmit using Interrupt & DMA
STM32 UART using LL Drivers (Part 3): Receive Data in Blocking Mode
STM32 UART using LL Drivers (Part 4): Receive Data in Interrupt Mode
STM32 UART using LL Drivers (Part 5): Receive Using DMA (Normal and Circular Mode)
STM32 ADC Using LL Drivers (Part 1): Single Channel Blocking and Interrupt Mode
STM32 LL ADC Project Download
Info
You can help with the development by DONATING Below.
To download the project, click the DOWNLOAD button.
STM32 LL ADC DMA FAQs
No. A single DMA stream is enough. DMA transfers the ADC results sequentially into a buffer based on the scan order.
Yes. You only need to update the ADC channel ranks and the buffer length. The DMA logic remains the same.
Yes. Even at low speeds, DMA keeps the code clean and avoids unnecessary CPU load, especially with multiple channels.
Data will be overwritten incorrectly. The buffer size must always match the total number of ADC channels in the scan sequence.
Yes. ADC with DMA works well with FreeRTOS. The DMA interrupt can be used to notify a task when new data is ready.










