1
\$\begingroup\$

I'm using an STM32F103C8 to read 3 ADC channels, and have used CubeMX + HAL to configure the ADC to shuttle the ADC values into a buffer.

I've been able to accomplish this with DMA and polling: I've seen both as generally acceptable ways to do this online. However, I haven't been able to find a configuration that will let me scan through the channels by manually starting conversions in the EOC interrupt. In all my attempts, either it doesn't advance channels, or the interrupt isn't fired at all.

I'd prefer to use interrupts because my one DMA channel on the device is used to buffer some fairly high-frequency audio data, but I'm also just disturbed that I can't understand the way the ADC interrupts work in conjunction with scan mode. I've used the following approaches:

  • DMA: This seems to be the authoritative way to scan multiple channels and store their respective results. Notably, the user manual in §11.3.8 ¶3 says:

    When using scan mode, DMA bit must be set and the direct memory access controller is used to transfer the converted data of regular group channels to SRAM after each update of the ADC_DR register.

    I was able to get it to work with the intuitive settings in CubeMX:

    • ADC_Settings:
      • Scan conversion mode: Enabled
      • Continuous mode: Enabled
      • Discontinuous mode: Disabled
    • ADC_Regular_Conversion_Mode
      • Enable Regular Conversions: Enable
      • Number of Conversion: 3
      • External Trigger Conversion Source: Regular Conversion launched by Software
      • <channel configurations and ranks...>

    plus a circular halfword-aligned DMA, and a straightforward call to HAL_ADC_Start_DMA() in the source.

  • Polling: I tried following this answer which disables both continuous and discontinuous modes, and is able to step through the channels with successive calls to HAL_ADC_PollForConversion alone. I found that I needed to enable discontinuous mode with group sizes of 1, i.e.:

    hadc1.Init.DiscontinuousConvMode = ENABLE;
    hadc1.Init.NbrOfDiscConversion = 1;
    

    Then stepping through the channels with HAL_ADC_PollForConversion worked without a hitch.

  • Interrupts: I've tried every permutation of scan mode, discontinuous mode and number of discontinuous conversions, and none of them will let me step through the channels in the HAL_ADC_ConvCpltCallback interrupt routine. Here is the routine I'm using:

    #define NUM_ADC_BUF 8
    #define NUM_ADC_CH 3
    volatile uint16_t adc_buf[NUM_ADC_BUF][NUM_ADC_CH];
    void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
      if(hadc->Instance == ADC1) {
        adc_buf[adc_buf_idx & (NUM_ADC_BUF - 1)][adc_ch++] = HAL_ADC_GetValue(hadc);
        if(adc_ch == NUM_ADC_CH) {
          adc_ch = 0;
          adc_buf_idx++;
        }
    
        HAL_ADC_Start_IT(hadc);
      }
    }
    

    Within the ADC_Settings in CubeMX, here are my attempts and their outcomes:

    +-------------------+-----------+--------------------+---------------------+------------------------------------------------------+
    | (Continuous mode) | Scan mode | Discontinuous mode | Number of           |                      Outcome                         |
    |                   |           |                    | discontinuous conv. |                                                      |
    +-------------------+-----------+--------------------+---------------------+------------------------------------------------------+
    | DISABLED          | ENABLED   | ENABLED            | 3                   | Only highest-rank-# (rank 3) channel result is given |
    | DISABLED          | ENABLED   | ENABLED            | 1                   | Interrupt never fires: EOC never set                 |
    | DISABLED          | ENABLED   | DISABLED           | N/A                 | Only highest-rank-# (rank 3) channel result is given |
    | DISABLED          | DISABLED  | ENABLED            | 3                   | Only lowest-rank-# (rank 1) channel result is given  |
    | DISABLED          | DISABLED  | ENABLED            | 1                   | Only lowest-rank-# (rank 1) channel result is given  |
    | DISABLED          | DISABLED  | DISABLED           | N/A                 | Only lowest-rank-# (rank 1) channel result is given  |
    +-------------------+-----------+--------------------+---------------------+------------------------------------------------------+
    

    As you can see, no combinations work properly. Is this just impossible? I suppose I can take the excerpt I quoted from the user manual to mean that the only way to use scan is to DMA, and that the polling works as a feature not formally supported. Is this true?

    For reference, here is my untouched auto-generated adc.c from CubeMX:

    /* Includes ------------------------------------------------------------------*/
    #include "adc.h"
    
    /* USER CODE BEGIN 0 */
    
    /* USER CODE END 0 */
    
    ADC_HandleTypeDef hadc1;
    
    /* ADC1 init function */
    void MX_ADC1_Init(void)
    {
      ADC_ChannelConfTypeDef sConfig = {0};
    
      /** Common config 
      */
      hadc1.Instance = ADC1;
      hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
      hadc1.Init.ContinuousConvMode = DISABLE;
      hadc1.Init.DiscontinuousConvMode = ENABLE;
      hadc1.Init.NbrOfDiscConversion = 3;
      hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
      hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
      hadc1.Init.NbrOfConversion = 3;
      if (HAL_ADC_Init(&hadc1) != HAL_OK)
      {
        Error_Handler();
      }
      /** Configure Regular Channel 
      */
      sConfig.Channel = ADC_CHANNEL_4;
      sConfig.Rank = ADC_REGULAR_RANK_1;
      sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
      if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
      {
        Error_Handler();
      }
      /** Configure Regular Channel 
      */
      sConfig.Channel = ADC_CHANNEL_8;
      sConfig.Rank = ADC_REGULAR_RANK_2;
      if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
      {
        Error_Handler();
      }
      /** Configure Regular Channel 
      */
      sConfig.Channel = ADC_CHANNEL_9;
      sConfig.Rank = ADC_REGULAR_RANK_3;
      if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
      {
        Error_Handler();
      }
    
    }
    
    void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle)
    {
    
      GPIO_InitTypeDef GPIO_InitStruct = {0};
      if(adcHandle->Instance==ADC1)
      {
      /* USER CODE BEGIN ADC1_MspInit 0 */
    
      /* USER CODE END ADC1_MspInit 0 */
     /* ADC1 clock enable */
     __HAL_RCC_ADC1_CLK_ENABLE();
    
     __HAL_RCC_GPIOA_CLK_ENABLE();
     __HAL_RCC_GPIOB_CLK_ENABLE();
     /**ADC1 GPIO Configuration    
     PA4     ------> ADC1_IN4
     PB0     ------> ADC1_IN8
     PB1     ------> ADC1_IN9 
     */
     GPIO_InitStruct.Pin = Xin_Pin;
     GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
     HAL_GPIO_Init(Xin_GPIO_Port, &GPIO_InitStruct);
    
     GPIO_InitStruct.Pin = Zin_Pin|Yin_Pin;
     GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
     HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    
     /* ADC1 interrupt Init */
     HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0);
     HAL_NVIC_EnableIRQ(ADC1_2_IRQn);
      /* USER CODE BEGIN ADC1_MspInit 1 */
    
      /* USER CODE END ADC1_MspInit 1 */
      }
    }
    
    void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
    {
    
      if(adcHandle->Instance==ADC1)
      {
      /* USER CODE BEGIN ADC1_MspDeInit 0 */
    
      /* USER CODE END ADC1_MspDeInit 0 */
     /* Peripheral clock disable */
     __HAL_RCC_ADC1_CLK_DISABLE();
    
     /**ADC1 GPIO Configuration    
     PA4     ------> ADC1_IN4
     PB0     ------> ADC1_IN8
     PB1     ------> ADC1_IN9 
     */
     HAL_GPIO_DeInit(Xin_GPIO_Port, Xin_Pin);
    
     HAL_GPIO_DeInit(GPIOB, Zin_Pin|Yin_Pin);
    
     /* ADC1 interrupt Deinit */
     HAL_NVIC_DisableIRQ(ADC1_2_IRQn);
      /* USER CODE BEGIN ADC1_MspDeInit 1 */
    
      /* USER CODE END ADC1_MspDeInit 1 */
      }
    } 
    
    /* USER CODE BEGIN 1 */
    
    /* USER CODE END 1 */
    
\$\endgroup\$
8
  • \$\begingroup\$ What does the Reference Manual say? \$\endgroup\$ Jun 7, 2020 at 0:17
  • \$\begingroup\$ @P__J__ Besides the quote I've mentioned, I couldn't find any detail on how successive conversions relate to the groups of channels. I would've accepted the fact that scan and discontinuous modes only make sense with a DMA if there wasn't a way to step through channels in polling mode as well, which is not documented in the referece manual. \$\endgroup\$
    – concat
    Jun 7, 2020 at 5:09
  • \$\begingroup\$ Everything is documented there. But for sure not in the HAL documentation or infile help (you took the quote from there). Use registers and everything "magically" will start to work as intended. Abandon HAL for such a simple peripherals!! \$\endgroup\$ Jun 7, 2020 at 8:33
  • \$\begingroup\$ @P__J__ Are you saying that interrupt-based scanning should be possible? I'd love to be proven wrong about what's in the reference manual about this, but from multiple reads over, the manual really doesn't say anything aside from "set scan mode = 1" to get channels to increment. \$\endgroup\$
    – concat
    Jun 7, 2020 at 18:03
  • \$\begingroup\$ What is the value of ADC->CR2 : EOCS bit? \$\endgroup\$
    – Tagli
    Apr 2, 2021 at 17:20

1 Answer 1

2
\$\begingroup\$

Unlike F4 series, ADC in F1 series can generate only one (single) interrupt at the end of the whole scanning sequence. That's why DMA is a must when using scanning mode.

However, I think it's still possible to configure ADC for interrupt based scanning, but you have to do this manually:

  1. Disable scanning and continuous mode. Configure ADC for single conversion.
  2. Select the ADC channel using ADC->SQR3 : SQ1 bits (1st conversion in regular sequence)
  3. Enable EOC interrupt using ADC->CR1 : EOCIE bit
  4. In interrupt service routine (ISR), collect the result from ADC->DR.
  5. In ISR, Repeat step 2 to configure ADC for the next channel.
  6. ADC needs to be triggered manually or by using an external event.

For periodic sampling of multiple channels, you will probably need 2 TIM modules and some TIM interrupts. One timer (Slave) controls the timing between each channel of a scanning group and triggers the ADC module. This timer needs to be aware of its number of overflows (using an interrupt) so it can disable itself after all the channels in the sequence are sampled and converted. Another timer (Master) controls the timing between the scanning sequences, and it activates the slave timer (and probably the first scan in the sequence).

I don't know how busy your DMA is, but I would definitely try using DMA before I try the method I described above.

\$\endgroup\$
1
  • \$\begingroup\$ Figures, that would explain why I can only ever get the last channel in the sequence using scan mode. In my case I convert the channels continuously (i.e. no delay between scanning sequences) so I think I should only ever need the "slave" timer, but I agree this is probably the best it gets on F1. Good to know this exists in F4! \$\endgroup\$
    – concat
    Apr 11, 2021 at 6:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.