Multichannel ADC using DMA on STM32

Previously we have tried to do a single conversion of one ADC channel. We were waiting for the ADC result in a loop, which isn’t an effective way of using processor resources. It is better to trigger a conversion and wait for the conversion to complete the interrupt. This way, a processor can do other tasks rather than wait for ADC conversion to complete. This time we will go through another example to set up more than one channel and read ADC values using interrupt service routine.

testing_stm32_ADC_DMA

How does multichannel ADC conversion works?

If we need to convert several channels continuously, we need to set up Sequence registers (ADC_SQRx). There are three sequence registers: ADC_SQR1, ADC_SQR2, and ADC_SQR3 where we can set up a maximum of 16 channels in any order. Conversion sequence starts with SQ1[4:0] settings in ADC_SQR3 register. Bits [4:0] hold the number of ADC channels.

ADC_SQR3

All 16 sequence channels can be set up the same way through all SQR registers. Then in the ADC_SQR1 register, there are four bits marked L[3:0] where you can set the number how many times sequence reading will be repeated.

Another thing we will have to take care of is to set up sample time for each channel. As we know, each channel in sequence can be set for different conversion times. The sampling time for each channel can be set up in two registers: ADC_SMPR1 and ADC_AMPR2. There are three bits for each channel in sequence.

ADC_SMPR2

If you use a standard peripheral library setting up multichannel ADC becomes an easy task.

Setting up multichannel ADC conversion with DMA write

Let’s write an example where we will read the first 8 ADC channels four times using scan mode. Then we calculate an average value of each channel and later print results on a terminal screen using UART.

We will write ADC values to memory by using a DMA channel. Once all data is stored in memory, a DMA transfer complete interrupt will be generated to trigger averaging and output. In the STM32F100x datasheet, we find that ADC pins are assigned alternate functions as follows:

  • ADC1_IN0 – PA0
  • ADC1_IN1 – PA1
  • ADC1_IN2 – PA2
  • ADC1_IN3 – PA3
  • ADC1_IN4 – PA4
  • ADC1_IN5 – PA5
  • ADC1_IN6 – PA6
  • ADC1_IN7 – PA7
  • ADC1_IN8 – PB0
  • ADC1_IN9 – PB1
  • ADC1_IN10 – PC0
  • ADC1_IN11 – PC1
  • ADC1_IN12 – PC2
  • ADC1_IN13 – PC3
  • ADC1_IN14 – PC4
  • ADC1_IN15 – PC5

We will need to set up pins A0 to A7 as analog inputs for the first eight channels. Then we can set up an ADC conversion mode. Also, we need to set up Scan Conversion Mode to go through all channels selected in ADC1_SQRx registers. In the peripheral library, this looks like:

ADC_InitStructure.ADC_ScanConvMode = ENABLE;

Then we must enable to enable continuous conversion mode as we want to cycle through channel list several times:

ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;

Then we indicate the number of channels to be converted in scan mode:

ADC_InitStructure.ADC_NbrOfChannel = 8;

The next thing is to indicate which channels and what order we need to convert. For this, we set up each channel individually with commands:

ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_41Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_41Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_41Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_41Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 5, ADC_SampleTime_41Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 6, ADC_SampleTime_41Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 7, ADC_SampleTime_41Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_7, 8, ADC_SampleTime_41Cycles5);

I’ve chosen to go all eight channels in the row from 0 to 7. But you can mess up the numbers as you like. The rest is to set up DMA where it copies ADC values to memory on each EOC event. After DMA copies a predefined number of values, it generates an interrupt. Then we can manipulate data as we like. As in our example, we average multiple instances.

This is a result on the terminal screen.

stm32_ADC_DMA_results

You can hook up a potentiometer or any other analog sensor to each channel to see its ADC value.

Working C code of multichannel ADC

Here is the complete main source code if you would like to analyze or use fragments for your purposes:

// Includes ------------------------------------------------------------------*/
#include "stm32f10x.h"
#include "usart.h"
#include <stdio.h>
#define ARRAYSIZE 8*4
#define ADC1_DR    ((uint32_t)0x4001244C)
volatile uint16_t ADC_values[ARRAYSIZE];
volatile uint32_t status = 0;

void ADCInit(void);
void DMAInit(void);
int main(void)
{
uint8_t index;
//initialize USART1
Usart1Init();
ADCInit();
DMAInit();

//Enable DMA1 Channel transfer
DMA_Cmd(DMA1_Channel1, ENABLE);
//Start ADC1 Software Conversion
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
//wait for DMA complete
while (!status){};
ADC_SoftwareStartConvCmd(ADC1, DISABLE);
//print averages
/*for(index = 0; index<8; index++)
    {
    printf("ch%d = %d ",index, ADC_values[index]);
    }*/
for(index = 0; index<8; index++){
    printf("\r\n ADC value on ch%d = %d\r\n",
            index, (uint16_t)((ADC_values[index]+ADC_values[index+8]
                    +ADC_values[index+16]+ADC_values[index+24])/4));
}

while (1)
  {
    //interrupts does the job
  }
}

void ADCInit(void){
    //--Enable ADC1 and GPIOA--
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
    GPIO_InitTypeDef GPIO_InitStructure; //Variable used to setup the GPIO pins
    //==Configure ADC pins (PA0 -> Channel 0 to PA7 -> Channel 7) as analog inputs==
    GPIO_StructInit(&GPIO_InitStructure); // Reset init structure, if not it can cause issues...
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1| GPIO_Pin_2| GPIO_Pin_3| GPIO_Pin_4| GPIO_Pin_5| GPIO_Pin_6| GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    ADC_InitTypeDef ADC_InitStructure;
    //ADC1 configuration

    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    //We will convert multiple channels
    ADC_InitStructure.ADC_ScanConvMode = ENABLE;
    //select continuous conversion mode
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;//!
    //select no external triggering
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    //right 12-bit data alignment in ADC data register
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    //8 channels conversion
    ADC_InitStructure.ADC_NbrOfChannel = 8;
    //load structure values to control and status registers
    ADC_Init(ADC1, &ADC_InitStructure);
    //wake up temperature sensor
    //ADC_TempSensorVrefintCmd(ENABLE);
    //configure each channel
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_41Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_41Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_41Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_41Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 5, ADC_SampleTime_41Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 6, ADC_SampleTime_41Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 7, ADC_SampleTime_41Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_7, 8, ADC_SampleTime_41Cycles5);
    //Enable ADC1
    ADC_Cmd(ADC1, ENABLE);
    //enable DMA for ADC
    ADC_DMACmd(ADC1, ENABLE);
    //Enable ADC1 reset calibration register
    ADC_ResetCalibration(ADC1);
    //Check the end of ADC1 reset calibration register
    while(ADC_GetResetCalibrationStatus(ADC1));
    //Start ADC1 calibration
    ADC_StartCalibration(ADC1);
    //Check the end of ADC1 calibration
    while(ADC_GetCalibrationStatus(ADC1));
}
void DMAInit(void){
    //enable DMA1 clock
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    //create DMA structure
    DMA_InitTypeDef  DMA_InitStructure;
    //reset DMA1 channe1 to default values;
    DMA_DeInit(DMA1_Channel1);
    //channel will be used for memory to memory transfer
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    //setting normal mode (non circular)
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    //medium priority
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    //source and destination data size word=32bit
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    //automatic memory destination increment enable.
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    //source address increment disable
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    //Location assigned to peripheral register will be source
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    //chunk of data to be transfered
    DMA_InitStructure.DMA_BufferSize = ARRAYSIZE;
    //source and destination start addresses
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)ADC1_DR;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADC_values;
    //send values to DMA registers
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);
    // Enable DMA1 Channel Transfer Complete interrupt
    DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
    DMA_Cmd(DMA1_Channel1, ENABLE); //Enable the DMA1 - Channel1
    NVIC_InitTypeDef NVIC_InitStructure;
    //Enable DMA1 channel IRQ Channel */
    NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);


}

Also, you can download project files [STM32DiscoveryADC_DMAmultiple.zip] that compile with Codebench GCC and Eclipse.

15 Comments:

  1. Hi,
    could you check comments in your source code? It would be confusing sometimes. For example “//setting normal mode (non circular)
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;”
    Regards
    Negio

  2. Fixed code. Now
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    as there is no circular buffer used.
    Please confirm if code works with this change.
    Thanks

  3. How about:
    //wait for DMA complete
    while (!status){};
    The variable ststus is never used?
    Thanks for a good site!

  4. #define ARRAYSIZE 8*4
    should be
    #define ARRAYSIZE 8

    For 8 channels you need only array size to be 8 because you are declaring the array of uint16_t.

  5. But I take 4 readings for each channel and then average them to be printed. 8 channels 4 readings each = 8*4. Of course you could go different way without storing all values then array of 4 would be enough.

  6. please tell me how to restart the convert sequence after the DMA fired an interrupt? because when I use DMA in normal mode, it make 1 convertion and then stop.

  7. Can code work without NVIC and DMA part?

  8. Hello, good tutorial. It is very complicated moment how to send data to uart. I see printf(“ch%d = %d “,index, ADC_values[index]); but there is no function to uart, can you explain this please. Actually, i need to send variables to lcd 1602, but i don’t know how. Thanks
    p.s i guess here is mistake
    ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 6, ADC_SampleTime_41Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 7, ADC_SampleTime_41Cycles5) two times “ADC_Channel_5”

  9. To Galamat:
    It could work without DMA, but not without NVIC. If you set the ADC in Scan-mode, you can basically let the controller run a channel-sequence, just like in this example. The difference is that you would need to manually copy the value for each channel in the Interrupt of the ADC (EOC-Interrupt) into the right place in the array. At the end of each sequence the EOS-Flag would be set and generate another ADC interrupt, so you could use that to reset your array-pointer (index) and start the conversion again manually (or let it scan in continuous mode).
    Using the DMA just makes it a lot better (and faster), since you don’t have to jump into the ADC interrupt and move data around manually after each individual channel conversion.

    AN3116 from ST has a great overview of all the ADC modes and what they are intended to achieve (see CD00258017.pdf)

  10. sudhir acharya

    hi please provide link,and full details of kit

  11. How do I get the assignement PA0 -> Channel0 … PA7 -> Channel 7? Do I have to write a register?

  12. Helpful hints
    1) If you want the ADC/DMA combination to produce repeating values via interrupts then use:
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
    2) The ISR for the DMA is named DMA1_Channel1_IRQHandler. You will find it in the downloadable project but not on this page. The function looks like this:

    void DMA1_Channel1_IRQHandler(void)
    {
    //Test on DMA1 Channel1 Transfer Complete interrupt
    if(DMA_GetITStatus(DMA1_IT_TC1)){
    //Clear DMA1 interrupt pending bits
    DMA_ClearITPendingBit(DMA1_IT_GL1);
    ADC_status=1;
    }
    }

    3) If you use DMA circular and repeating IRQ’s then you don’t need the status variable at all.
    4) Depending on how fast you need the ADC values, you can try using much slower clock rates. That will save CPU cycles wasted on handling the DMA ISR.

  13. DIJITH PADINJARAYIL

    dear admin
    i want to take 8192 half word data from adc dma 16 channels,my purpose is to do fft on each channel data and send using wifi tcp ip, but my doubt is how can switch between each channel using dma,and how can i handle this much data as my memory is on 198k or less.do u have any suggestion.

  14. Hi,

    Can you tell me why do we typecast here:

    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)ADC1_DR;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADC_values;

    when we declare our variable ADC_values as uint16_t?

    Thanks.

Comments are closed