问题现象与背景

在STM32F4系列MCU中,ADC支持规则通道组和注入通道组。规则组用于常规连续或扫描转换,注入组用于高优先级插队转换。许多开发者习惯用HAL_ADC_PollForConversion()等待规则转换完成,或直接查询ADC_FLAG_EOC标志。但当注入通道与规则通道混用时,常出现以下异常:

  • 规则通道数据读取到旧值或错位值
  • 注入转换完成后,规则通道的EOC标志被意外清除或误置
  • 使用中断方式时,规则组和注入组中断服务函数互相干扰

根本原因在于:EOC标志仅表示规则组转换结束,JEOC表示注入组转换结束,但两者在状态寄存器中的清除逻辑和转换序列的相互影响容易被忽视。

ADC状态标志机制解析

STM32F4的ADC状态寄存器(ADC_SR)中关键标志位:

  • EOC:规则通道转换结束。当规则组所有通道转换完成时置1。
  • JEOC:注入通道转换结束。当注入组所有通道转换完成时置1。
  • STRT:规则转换启动标志。
  • JSTRT:注入转换启动标志。

关键陷阱:当注入组被触发时,会打断正在进行的规则组转换序列。此时规则组转换被暂停,注入组完成后恢复。但若在注入转换期间查询EOC,可能因规则组尚未完成而读到0;更常见的是,注入转换完成后,若未正确清除JEOC,后续规则转换的EOC置位可能被误判为注入完成。

此外,使用HAL_ADC_GetValue()读取规则数据时,若未确认EOC已置位,可能读到上一次的转换结果。而注入通道数据必须通过HAL_ADCEx_InjectedGetValue()读取,两者数据寄存器独立(ADC_DR vs ADC_JDRx),但标志位管理不当会导致时序错乱。

配置修正步骤

1. 明确转换模式与触发源

  • 规则组:使用软件触发或定时器触发,配置为单次或连续扫描模式。
  • 注入组:使用外部触发(如定时器TRGO)或软件触发,配置为单次转换。
  • 禁止在规则组连续转换模式下频繁触发注入组而不处理JEOC,否则EOC可能被淹没。

2. 正确使能中断与标志清除

  • 规则组转换完成中断:使能EOC中断,在中断中读取ADC_DR并清除EOC。
  • 注入组转换完成中断:使能JEOC中断,在中断中读取ADC_JDRx并清除JEOC。
  • 注意:清除EOC需读ADC_DR,清除JEOC需读ADC_JDRx,不可用__HAL_ADC_CLEAR_FLAG直接写0清除(部分系列支持,但F4推荐读寄存器清除)。

3. 注入转换期间保护规则数据

  • 若规则组正在转换时注入组插入,规则组转换结果可能被覆盖。建议在注入触发前,先读取并保存规则组最新数据,或使用DMA搬运规则数据。
  • 注入组转换完成后,等待规则组恢复并再次产生EOC后再读取规则数据。

4. 使用HAL库的推荐流程

// 规则组轮询读取(非中断方式)
if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) {
    uint32_t rule_val = HAL_ADC_GetValue(&hadc1);
    // 处理规则数据
}

// 注入组读取(需先启动注入转换)
HAL_ADCEx_InjectedStart(&hadc1);
if (HAL_ADCEx_InjectedPollForConversion(&hadc1, 10) == HAL_OK) {
    uint32_t inj_val = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);
    // 处理注入数据
}

关键修正:在混用场景下,不要用HAL_ADC_PollForConversion等待注入转换,也不要用HAL_ADC_GetValue读取注入数据。两者必须严格区分API。

完整代码示例

以下代码展示规则通道(PA0,ADC_CHANNEL_0)与注入通道(PA1,ADC_CHANNEL_1)混用,使用中断方式分别处理EOC和JEOC。

ADC_HandleTypeDef hadc1;
volatile uint32_t rule_data = 0;
volatile uint32_t inj_data = 0;

void ADC_Init(void) {
    __HAL_RCC_ADC1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1;
    gpio.Mode = GPIO_MODE_ANALOG;
    gpio.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &gpio);

    hadc1.Instance = ADC1;
    hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
    hadc1.Init.Resolution = ADC_RESOLUTION_12B;
    hadc1.Init.ScanConvMode = ENABLE;          // 规则组扫描
    hadc1.Init.ContinuousConvMode = DISABLE;   // 单次转换
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    hadc1.Init.NbrOfConversion = 1;
    hadc1.Init.DMAContinuousRequests = DISABLE;
    hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; // 单通道转换后置EOC
    HAL_ADC_Init(&hadc1);

    // 规则通道配置
    ADC_ChannelConfTypeDef sConfig = {0};
    sConfig.Channel = ADC_CHANNEL_0;
    sConfig.Rank = 1;
    sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    // 注入通道配置
    ADC_InjectionConfTypeDef sInjConfig = {0};
    sInjConfig.InjectedChannel = ADC_CHANNEL_1;
    sInjConfig.InjectedRank = ADC_INJECTED_RANK_1;
    sInjConfig.InjectedSamplingTime = ADC_SAMPLETIME_84CYCLES;
    sInjConfig.InjectedOffset = 0;
    sInjConfig.InjectedNbrOfConversion = 1;
    sInjConfig.InjectedDiscontinuousConvMode = DISABLE;
    sInjConfig.AutoInjectedConv = DISABLE;      // 禁止自动注入,手动触发
    sInjConfig.ExternalTrigInjecConvEdge = ADC_EXTERNALTRIGINJECCONVEDGE_NONE;
    HAL_ADCEx_InjectedConfigChannel(&hadc1, &sInjConfig);

    // 使能中断
    HAL_NVIC_SetPriority(ADC_IRQn, 1, 0);
    HAL_NVIC_EnableIRQ(ADC_IRQn);
    __HAL_ADC_ENABLE_IT(&hadc1, ADC_IT_EOC | ADC_IT_JEOC);
}

// 规则转换完成中断回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
    if (hadc->Instance == ADC1) {
        rule_data = HAL_ADC_GetValue(hadc); // 读DR自动清EOC
    }
}

// 注入转换完成中断回调
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc) {
    if (hadc->Instance == ADC1) {
        inj_data = HAL_ADCEx_InjectedGetValue(hadc, ADC_INJECTED_RANK_1); // 读JDR清JEOC
    }
}

// 主循环中触发转换
void ADC_Trigger(void) {
    HAL_ADC_Start_IT(&hadc1);          // 启动规则组,等待EOC中断
    HAL_ADCEx_InjectedStart_IT(&hadc1); // 启动注入组,等待JEOC中断
}

注意事项

  • 标志清除顺序:先读数据寄存器再清标志,HAL库函数已封装此逻辑,不要手动操作ADC_SR。
  • 注入触发时机:若规则组正在转换,注入触发会暂停规则序列。建议在规则组转换间隙触发注入,或接受规则数据延迟。
  • EOCSelection配置:ADC_EOC_SINGLE_CONV表示每个通道转换完就置EOC,ADC_EOC_SEQ_CONV表示整个序列完成才置EOC。混用时建议用单通道模式,避免序列未完成时误判。
  • DMA与注入混用:若规则组使用DMA,注入组不可使用DMA,且注入转换期间DMA传输可能暂停,需确保缓冲区足够。
  • 调试建议:在中断入口翻转GPIO,用示波器观察EOC与JEOC的时序关系,确认无标志竞争。

通过以上配置修正,可彻底解决STM32F4 ADC注入与规则通道混用时的EOC标志误判问题,确保两组数据独立、可靠地采集。