HomeSTM32 TutorialsSTM32 ADC Using LL Drivers (Part 2): Multiple Channels using DMA Mode

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.
STM32 ADC Multiple Channels Using LL Drivers (DMA Mode)

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.

Image 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 0
  • buffer[1] for Channel 1
  • buffer[2] for Channel 4
  • buffer[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.

Image 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.

Select the HSE Bypass Clock source for Nucleo F446RE in the STM32CubeMX.

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.

Use the PLL to run the STM32 MCU at 180 MHz Clock.

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.

Note for STM32F103C8 This MCU uses an 8 MHz external crystal as well, but its maximum frequency is 72 MHz. If you are using the Blue Pill, configure the PLL so that the final system clock is 72 MHz.

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.

Image shows the STM32 ADC configuration for multiple channels.

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.

Note for STM32F103C8:
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.

Enable the ADC DMA in the STM32CubeMX for multi channel ADC.

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.

STM32 UART configuration for nucleo F446

To enable USART2 in CubeMX:

  1. Set the Mode to Asynchronous.
  2. CubeMX automatically assigns the correct pins:
    • PA2 -> TX
    • PA3 -> RX
  3. 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.

Enable the STM32 LL drivers in STM32CubeMX.

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.

Generate separate file for each peripheral in STM32CubeMX.

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.

Image showing the 4 channels of STM32 ADC1 are connected to the output of the analog device.

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.

image shows the serial monitor output printed using STM32 ADC 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

1 2

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

Subscribe
Notify of

0 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments