Skip to main content
Visitor II
August 2, 2024
Question

Correct ADC settings for DMA

  • August 2, 2024
  • 7 replies
  • 3415 views

Hello, I need help with ADC and DMA settings. My application is VERY EASY but I do not understand the setup parameters. The application is that I just start CPU and the ADC raw value should independently transfer via DMA to variable. When I need the value (randomly) I just read the internal variable. I saw several youtube manuals but I still have 0 on output even when there is 1V. I use PA0 pin (ADC1, chan 0) on very easy CPU STM32G031. I need to use DMA Channel 4 (Channel 1 and 2 are used for USART_RX and TX)

My setup:

StastnyPetr_0-1722611177464.png

 

StastnyPetr_1-1722611224695.png

 

The generated code is like this (I use LL):

 

static void MX_ADC1_Init(void)
{

 /* USER CODE BEGIN ADC1_Init 0 */

 /* USER CODE END ADC1_Init 0 */

 LL_ADC_InitTypeDef ADC_InitStruct = {0};
 LL_ADC_REG_InitTypeDef ADC_REG_InitStruct = {0};

 LL_GPIO_InitTypeDef GPIO_InitStruct = {0};

 /* Peripheral clock enable */
 LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_ADC);

 LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
 /**ADC1 GPIO Configuration
 PA0 ------> ADC1_IN0
 */
 GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
 GPIO_InitStruct.Mode = LL_GPIO_MODE_ANALOG;
 GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
 LL_GPIO_Init(GPIOA, &GPIO_InitStruct);

 /* ADC1 DMA Init */

 /* ADC1 Init */
 LL_DMA_SetPeriphRequest(DMA1, LL_DMA_CHANNEL_4, LL_DMAMUX_REQ_ADC1);

 LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_4, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);

 LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_4, LL_DMA_PRIORITY_LOW);

 LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_4, LL_DMA_MODE_CIRCULAR);

 LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_4, LL_DMA_PERIPH_NOINCREMENT);

 LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_4, LL_DMA_MEMORY_INCREMENT);

 LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_4, LL_DMA_PDATAALIGN_WORD);

 LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_4, LL_DMA_MDATAALIGN_WORD);

 /* USER CODE BEGIN ADC1_Init 1 */

 /* USER CODE END ADC1_Init 1 */
 /** Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion)
 */

 #define ADC_CHANNEL_CONF_RDY_TIMEOUT_MS ( 1U)
 #if (USE_TIMEOUT == 1)
 uint32_t Timeout ; /* Variable used for Timeout management */
 #endif /* USE_TIMEOUT */

 ADC_InitStruct.Clock = LL_ADC_CLOCK_SYNC_PCLK_DIV2;
 ADC_InitStruct.Resolution = LL_ADC_RESOLUTION_12B;
 ADC_InitStruct.DataAlignment = LL_ADC_DATA_ALIGN_RIGHT;
 ADC_InitStruct.LowPowerMode = LL_ADC_LP_MODE_NONE;
 LL_ADC_Init(ADC1, &ADC_InitStruct);
 LL_ADC_REG_SetSequencerConfigurable(ADC1, LL_ADC_REG_SEQ_CONFIGURABLE);

 /* Poll for ADC channel configuration ready */
 #if (USE_TIMEOUT == 1)
 Timeout = ADC_CHANNEL_CONF_RDY_TIMEOUT_MS;
 #endif /* USE_TIMEOUT */
 while (LL_ADC_IsActiveFlag_CCRDY(ADC1) == 0)
 {
 #if (USE_TIMEOUT == 1)
 /* Check Systick counter flag to decrement the time-out value */
 if (LL_SYSTICK_IsActiveCounterFlag())
 {
 if(Timeout-- == 0)
 {
 Error_Handler();
 }
 }
 #endif /* USE_TIMEOUT */
 }
 /* Clear flag ADC channel configuration ready */
 LL_ADC_ClearFlag_CCRDY(ADC1);
 ADC_REG_InitStruct.TriggerSource = LL_ADC_REG_TRIG_SOFTWARE;
 ADC_REG_InitStruct.SequencerLength = LL_ADC_REG_SEQ_SCAN_DISABLE;
 ADC_REG_InitStruct.SequencerDiscont = LL_ADC_REG_SEQ_DISCONT_DISABLE;
 ADC_REG_InitStruct.ContinuousMode = LL_ADC_REG_CONV_CONTINUOUS;
 ADC_REG_InitStruct.DMATransfer = LL_ADC_REG_DMA_TRANSFER_UNLIMITED;
 ADC_REG_InitStruct.Overrun = LL_ADC_REG_OVR_DATA_PRESERVED;
 LL_ADC_REG_Init(ADC1, &ADC_REG_InitStruct);
 LL_ADC_SetOverSamplingScope(ADC1, LL_ADC_OVS_DISABLE);
 LL_ADC_SetTriggerFrequencyMode(ADC1, LL_ADC_CLOCK_FREQ_MODE_HIGH);
 LL_ADC_SetSamplingTimeCommonChannels(ADC1, LL_ADC_SAMPLINGTIME_COMMON_1, LL_ADC_SAMPLINGTIME_1CYCLE_5);
 LL_ADC_SetSamplingTimeCommonChannels(ADC1, LL_ADC_SAMPLINGTIME_COMMON_2, LL_ADC_SAMPLINGTIME_1CYCLE_5);
 LL_ADC_DisableIT_EOC(ADC1);
 LL_ADC_DisableIT_EOS(ADC1);

 /* Enable ADC internal voltage regulator */
 LL_ADC_EnableInternalRegulator(ADC1);
 /* Delay for ADC internal voltage regulator stabilization. */
 /* Compute number of CPU cycles to wait for, from delay in us. */
 /* Note: Variable divided by 2 to compensate partially */
 /* CPU processing cycles (depends on compilation optimization). */
 /* Note: If system core clock frequency is below 200kHz, wait time */
 /* is only a few CPU processing cycles. */
 uint32_t wait_loop_index;
 wait_loop_index = ((LL_ADC_DELAY_INTERNAL_REGUL_STAB_US * (SystemCoreClock / (100000 * 2))) / 10);
 while(wait_loop_index != 0)
 {
 wait_loop_index--;
 }
 /** Configure Regular Channel
 */
 LL_ADC_REG_SetSequencerRanks(ADC1, LL_ADC_REG_RANK_1, LL_ADC_CHANNEL_0);
 LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_0, LL_ADC_SAMPLINGTIME_COMMON_1);
 /* USER CODE BEGIN ADC1_Init 2 */

 /* USER CODE END ADC1_Init 2 */

}

static void MX_DMA_Init(void)
{

 /* Init with LL driver */
 /* DMA controller clock enable */
 LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);

 /* DMA interrupt init */
 /* DMA1_Channel1_IRQn interrupt configuration */
 NVIC_SetPriority(DMA1_Channel1_IRQn, 0);
 NVIC_EnableIRQ(DMA1_Channel1_IRQn);
 /* DMA1_Channel2_3_IRQn interrupt configuration */
 NVIC_SetPriority(DMA1_Channel2_3_IRQn, 0);
 NVIC_EnableIRQ(DMA1_Channel2_3_IRQn);
 /* DMA1_Ch4_5_DMAMUX1_OVR_IRQn interrupt configuration */
 NVIC_SetPriority(DMA1_Ch4_5_DMAMUX1_OVR_IRQn, 0);
 NVIC_EnableIRQ(DMA1_Ch4_5_DMAMUX1_OVR_IRQn);

}

 

 

But how can I setup the variable where is the ADC result saved?

How can I start the ADC?

I tried to use ADC1->CR |= ADC_CR_ADSTART; and  reading ADC1->DR; but only zeroes...

Thank you

 

 

    This topic has been closed for replies.

    7 replies

    Visitor II
    August 4, 2024

    Hello,

    I tried to use HAL library. I exported this settings:

    StastnyPetr_0-1722802952957.png

    StastnyPetr_1-1722802984701.png

     

    And added 

    uint16_t ADCACTValue;
     HAL_ADC_Start_DMA (&hadc1, (uint32_t *)ADCACTValue, 1);

    but the result is still 0.

     

    If I use for testing this in main while cycle, result is correct:

     HAL_ADC_Start(&hadc1);
     HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
     ADCACTValue = (uint16_t)HAL_ADC_GetValue(&hadc1);

     

    What is wrong in my settings? Or do I need to add more code tu run this?

    Thank you

     

     

    Graduate II
    August 4, 2024

    This line caught my eye.

     

     

    uint16_t ADCACTValue;
    HAL_ADC_Start_DMA (&hadc1, (uint32_t *)ADCACTValue, 1);

     

     

    HAL_ADC_Start_DMA(...) takes a pointer as the 2nd argument. ADCACTValue is not an array.  The 2nd argument should be (uint32_t*)&ADCACTValue  .

    Visitor II
    August 5, 2024

    Thank you, you are right, but I tested all. If I use (uint32_t*)&ADCACTValue then program code stuck.

    I exported the code from CubeMX and when I use HAL_ADC_Start_DMA, nothing happens.

    I tested again on NUCLEO kit with HAL and same problem. I am not able to use STM32G031K8 with ADC on DMA. I must say that I spent many hours on this and no result. I really do not know where is the problem - on STM32F103 it worked, but not on STM32G031.

    Super User
    August 5, 2024

    Hi,

    1. set pointer :  (uint32_t*)&ADCACTValue

    2. DMA with transfer 1 sample is totally nonsense, as "circular" = in two blocks, how should this work with 1 sample ???  -> so make an array and transfer 100 samples or whatever, maybe circular with callbacks:

     

    uint16_t sampels[120];
    
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *) &sampels, 100);

     

    - Dont set circular - for only 1 transfer.

    -- or dont use DMA , if you really want only one sample.

    3. set half-word access:

    AScha3_0-1722841475597.png

    4. Use HAL , not LL . (Anyway same speed : if DMA used, this is the speed of the DMA then. HAL or LL or bare metal will change just the setup/call time , maybe 1 x 1 us more, when using HAL. This is nothing to think about..)

     

    Visitor II
    August 5, 2024

    I do understand, but what is the best solution when I only need to permanently send the actual data to one variable and when I need the data (on EXTI from external signal) I just read this variable in EXTI interrupt?

    In interrupt should be only short executions so it does not make a sense to read ADC value and wait for conversion.

    Maybe you can tell me the better solution?

    Super User
    August 5, 2024

    Ok, so you want 1 sample after EXTI event ; and your right > it does not make a sense to read ADC value and wait for conversion <  , so we can use DMA or just INT .

    i would go the simple way : in exti INT , start ADC with INT :  HAL_ADC_Start_IT(..)

     * @brief Enables ADC, starts conversion of regular group with interruption.
     * Interruptions enabled in this function:
     * - EOC (end of conversion of regular group) or EOS (end of 
     * sequence of regular group) depending on ADC initialization 
     * parameter "EOCSelection" (if available)
     * - overrun (if available)
     * Each of these interruptions has its dedicated callback function.
     * @note: Case of multimode enabled (for devices with several ADCs): This 
     * function must be called for ADC slave first, then ADC master. 
     * For ADC slave, ADC is enabled only (conversion is not started). 
     * For ADC master, ADC is enabled and multimode conversion is started.
     * @PAram hadc ADC handle
     * @retval HAL status
     */

    Then get an INT from ADC, when your value is ready. Thats it.

    Super User
    August 5, 2024

    btw

    About timing, INT etc. : it depends on your ADC setting, which way is better/faster:

    - if your ADC is set to average hi speed, lets say 1us conversion time, calling the conversion with INT or DMA return,

    might need the same/or more time, than just blocking/wait for conversion ready .

    Just think: calling an INT needs saving and restoring all cpu registers (or the 12 , as standard call doing),

    so if the cpu running at 50MHz, it might need about (30..40 cycles) 1 us to execute the INT -

    but this is almost same time, or more, the ADC doing its conversion.

    So in this case it might be same speed or faster, to just convert 1 sample and "wait" for result, than using an INT or DMA + INT . Depending, on : how fast you can set the ADC (and dont forget ADC input impedance - need low source  impedance for fast and precise result.)

    Visitor II
    August 5, 2024

    This is very good point, I did not realize it! Thank you!

    Output impedance of my source is around 1 kiloohm, so not very small. Am I correct that in this case I need to setup as sampling time as possible?

     

    StastnyPetr_0-1722853218926.png

    Is necessary to have 160 Cycles?

    Sampling time about 160 Cycles means on 64 MHz conversion time around (1/64meg * 160) = 2,5 us?

    Super User
    August 5, 2024

    The cycles here are on adc-clock , not cpu-clock. So if adc at 32M -> 5us .

    +

    from ds:

    AScha3_0-1722856082596.png

    need about 200ns for < 2kohm input , so at 32M adc clock 7.5 sam.cyc. setting should be ok; 

    Tconv then 20 cyc /32M -> 0.6 us .

    +

    How fast is your input signal ? (Because if not fast, like a pot , adding a 100nF cap at the input gives low impedance, to use short sampling time.)

    Visitor II
    August 5, 2024

    My signal is current feedback from the triac controlled motor. When I switch on the triac, I start to find maximum/minimum (it is offseted to 3.3V/2) current peak to check overtorque.

    So it is 100Hz half-period and ever half period I need to find maximum/minimum value.

    1kOhm output impedance with + 100nF on the input is maybe acceptable (?)

    StastnyPetr_0-1722856113351.png

     

    Super User
    August 5, 2024

    No, 100n too much here. But some cap you should have here, because you might get spikes from switching.

    Maybe 2...10nF could be fine. And some protection of the adc input ... in case a big pulse coming... . 

    Just check with scope, adc input not yet connected, that signal is as expected and in range for adc.

    Visitor II
    August 5, 2024

    It is OK, it is output from hall-probe, galvanicaly isolated. TMCS1101A2BQDRQ1 is the part, it works fine by hardware side. The problem what i have is mostly firmware side :(

    Super User
    August 5, 2024

    Ok. :)

    So need no cap here. (Or not more than 470pF , because 1nF max. load for the sensor.)

    AScha3_0-1722861832749.png

     

    Visitor II
    August 6, 2024

    Thank you for all. Finally I found a way:

    static void ADC_Init(void){
     // Enable the ADC clock
     RCC->APBENR2 |= RCC_APBENR2_ADCEN;
    
     // Enable the ADC voltage regulator
     ADC1->CR |= ADC_CR_ADVREGEN;
     // Wait for the voltage regulator to stabilize
     for (volatile uint32_t i = 0; i < 100000; i++);
    
     // Calibrate the ADC
     ADC1->CR |= ADC_CR_ADCAL;
     while (ADC1->CR & ADC_CR_ADCAL); // Wait for calibration to complete
    
     // Configure ADC
     ADC1->CFGR1 &= ~ADC_CFGR1_RES; // 12-bit resolution
     ADC1->CFGR1 |= ADC_CFGR1_CONT; // Continuous conversion mode
     ADC1->CHSELR = ADC_CHSELR_CHSEL0; // Select channel 0 (PA0)
    
     // Set sampling time to the longest possible (160.5 ADC clock cycles)
     ADC1->SMPR |= ADC_SMPR_SMP2_2 | ADC_SMPR_SMP2_1 | ADC_SMPR_SMP2_0;
    
     // Enable ADC
     ADC1->CR |= ADC_CR_ADEN;
     while (!(ADC1->ISR & ADC_ISR_ADRDY)); // Wait until ADC is ready
    }
    
    int main(void){
    ...
     ADC_Init();
     ADC1->CR |= ADC_CR_ADSTART;
    ...
    }

    and when I need I use: 

    value = ADC1->DR;