STM32: ¿Por qué no puedo usar el modo de escaneo en ADC controlado por interrupciones?

Estoy usando un STM32F103C8 para leer 3 canales ADC y he usado CubeMX + HAL para configurar el ADC para transportar los valores ADC a un búfer.

He podido lograr esto con DMA y encuestas: he visto que ambas son formas generalmente aceptables de hacer esto en línea. Sin embargo, no he podido encontrar una configuración que me permita explorar los canales iniciando manualmente las conversiones en la interrupción EOC. En todos mis intentos, o no avanza los canales o la interrupción no se activa en absoluto.

Preferiría usar interrupciones porque mi único canal DMA en el dispositivo se usa para almacenar en búfer algunos datos de audio de bastante alta frecuencia, pero también me preocupa no poder entender la forma en que las interrupciones ADC funcionan junto con el escaneo. modo. He usado los siguientes enfoques:

  • DMA : esta parece ser la forma autorizada de escanear múltiples canales y almacenar sus respectivos resultados. En particular, el manual del usuario en §11.3.8 ¶3 dice:

    Cuando se usa el modo de exploración, se debe configurar el bit DMA y se usa el controlador de acceso directo a la memoria para transferir los datos convertidos de los canales de grupo regulares a SRAM después de cada actualización del registro ADC_DR.

    Pude hacerlo funcionar con la configuración intuitiva en CubeMX:

    • ADC_Configuraciones:
      • Modo de conversión de escaneo: Habilitado
      • Modo continuo: Habilitado
      • Modo discontinuo: Deshabilitado
    • ADC_Regular_Conversion_Mode
      • Habilitar conversiones regulares: Habilitar
      • Número de conversión: 3
      • Fuente de conversión de disparador externo: Conversión regular lanzada por software
      • <configuraciones de canal y rangos...>

    más un DMA alineado con media palabra circular y una llamada directa a HAL_ADC_Start_DMA()la fuente.

  • Sondeo : intenté seguir esta respuesta que deshabilita los modos continuo y discontinuo, y puede recorrer los canales con llamadas sucesivas a HAL_ADC_PollForConversionsolo. Descubrí que necesitaba habilitar el modo discontinuo con tamaños de grupo de 1, es decir:

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

    Luego paso a través de los canales con HAL_ADC_PollForConversiontrabajado sin problemas.

  • Interrupciones: probé todas las permutaciones del modo de exploración, el modo discontinuo y el número de conversiones discontinuas, y ninguna de ellas me permite recorrer los canales en la HAL_ADC_ConvCpltCallbackrutina de interrupción. Aquí está la rutina que estoy usando:

    #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);
      }
    }
    

    Dentro de ADC_Settings en CubeMX, aquí están mis intentos y sus resultados:

    +-------------------+-----------+--------------------+---------------------+------------------------------------------------------+
    | (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  |
    +-------------------+-----------+--------------------+---------------------+------------------------------------------------------+
    

    Como puede ver, ninguna combinación funciona correctamente. ¿Es esto simplemente imposible? Supongo que puedo tomar el extracto que cité del manual del usuario en el sentido de que la única forma de usar el escaneo es DMA, y que el sondeo funciona como una función que no se admite formalmente. ¿Es esto cierto?

    Como referencia, aquí está mi generación automática intacta adc.cde 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 */
    
¿Qué dice el Manual de referencia?
@P__J__ Además de la cita que mencioné, no pude encontrar ningún detalle sobre cómo las conversiones sucesivas se relacionan con los grupos de canales. Habría aceptado el hecho de que los modos de escaneo y discontinuo solo tienen sentido con un DMA si no hubiera una forma de pasar por los canales en el modo de sondeo también, lo cual no está documentado en el manual de referencia.
Todo está documentado allí. Pero seguro que no en la documentación de HAL o en la ayuda del archivo (tomó la cita de allí). Use registros y todo "mágicamente" comenzará a funcionar según lo previsto. ¡Abandona HAL por un periférico tan simple!
@P__J__ ¿Está diciendo que el escaneo basado en interrupciones debería ser posible? Me encantaría estar equivocado sobre lo que hay en el manual de referencia sobre esto, pero de varias lecturas, el manual realmente no dice nada aparte de "establecer el modo de escaneo = 1" para que los canales aumenten.
¿ Cuál es el valor de ADC->CR2: bit EOCS?
@Tagli No veo un poco de EOCS, ¿te refieres a EOCIE?
Mis disculpas, estaba mirando a RM equivocado. EOCS parece estar presente en F4 pero no en F1.
@Tagli ah, lo veo en el F4xx RM, veo por qué preguntarías.

Respuestas (1)

A diferencia de la serie F4, el ADC de la serie F1 puede generar solo una (única) interrupción al final de toda la secuencia de exploración. Es por eso que DMA es imprescindible cuando se usa el modo de escaneo.

Sin embargo, creo que todavía es posible configurar ADC para el análisis basado en interrupciones, pero debe hacerlo manualmente:

  1. Deshabilite el escaneo y el modo continuo. Configure ADC para conversión única.
  2. Seleccione el canal ADC usando ADC->SQR3: bits SQ1 (primera conversión en secuencia regular)
  3. Habilite la interrupción EOC usando ADC->CR1: bit EOCIE
  4. En la rutina de servicio de interrupción (ISR), recopile el resultado de ADC->DR .
  5. En ISR, repita el paso 2 para configurar ADC para el siguiente canal.
  6. ADC debe activarse manualmente o mediante un evento externo.

Para el muestreo periódico de múltiples canales, probablemente necesitará 2 módulos TIM y algunas interrupciones TIM. Un temporizador (Esclavo) controla el tiempo entre cada canal de un grupo de exploración y activa el módulo ADC. Este temporizador debe conocer su número de desbordamientos (usando una interrupción) para que pueda desactivarse después de que todos los canales de la secuencia se muestreen y conviertan. Otro temporizador (maestro) controla el tiempo entre las secuencias de escaneo y activa el temporizador esclavo (y probablemente el primer escaneo de la secuencia).

No sé qué tan ocupado está su DMA, pero definitivamente intentaría usar DMA antes de probar el método que describí anteriormente.

Cifras, eso explicaría por qué solo puedo obtener el último canal en la secuencia usando el modo de escaneo. En mi caso, convierto los canales de forma continua (es decir, sin demora entre las secuencias de escaneo), por lo que creo que solo debería necesitar el temporizador "esclavo", pero estoy de acuerdo en que esto es probablemente lo mejor que obtiene en F1. ¡Es bueno saber que esto existe en F4!