4. 单次转换模式:轮询方式读取与阻塞式API实现

单次转换模式,说白了就是让ADC只干一次活——启动一次,转换一次,然后停下来等你来取结果。这是最基础、最容易理解的ADC工作方式,也是我当年入门STM32时第一个上手的模式。

你想想看,如果连一次转换都搞不定,那连续转换、注入组、DMA这些高级玩法就更别想了。所以这一章,咱们把单次转换模式吃透。

4.1 单次转换模式的核心流程

单次转换模式的执行路径其实很简单,就三步:

  1. 启动转换:软件触发或外部触发,让ADC开始干活
  2. 等待完成:CPU轮询状态寄存器,直到转换结束
  3. 读取结果:从数据寄存器把转换值取走

嗯,这里要注意——单次转换模式每次只能转换一个通道。如果你想扫多个通道,得手动切换通道再重新启动。我在一个温湿度采集项目里就踩过这个坑,当时以为配置了多通道就能自动扫完,结果读出来的全是同一个通道的值。

关键点:单次转换模式下,每次转换完成后ADC会自动停止。你必须再次触发才能开始下一次转换。

4.2 轮询方式读取的实现

轮询,就是CPU死盯着ADC的状态寄存器不放。虽然听起来有点傻,但在转换时间很短(微秒级)的场景下,这反而是最高效的方式。

我个人习惯用HAL_ADC_PollForConversion()这个函数来做轮询。它的内部实现其实就是不断检查ADC状态寄存器的EOC(End of Conversion)标志位。

// 阻塞式单次转换示例
uint32_t ADC_Read_SingleChannel(ADC_HandleTypeDef* hadc)
{
    uint32_t adc_value = 0;
    
    // 启动ADC转换
    if (HAL_ADC_Start(hadc) != HAL_OK)
    {
        Error_Handler();
    }
    
    // 轮询等待转换完成,超时时间设为10ms
    if (HAL_ADC_PollForConversion(hadc, 10) == HAL_OK)
    {
        // 读取转换结果
        adc_value = HAL_ADC_GetValue(hadc);
    }
    else
    {
        // 超时处理
        adc_value = 0xFFFFFFFF;
    }
    
    // 停止ADC(单次模式其实可以省略,但养成习惯)
    HAL_ADC_Stop(hadc);
    
    return adc_value;
}

这段代码看起来简单,但有几个细节值得注意:

  • 超时时间:10ms是我根据ADC时钟和采样周期算出来的,后面会讲怎么算
  • 错误处理:超时返回0xFFFFFFFF,方便上层判断
  • 停止ADC:单次模式转换完会自动停,但显式调用HAL_ADC_Stop能让代码逻辑更清晰

我的习惯:每次读取完都调用HAL_ADC_Stop,哪怕文档说可以省略。因为我在一个项目中遇到过连续读取时ADC状态机卡死的问题,加上这行代码后问题就消失了。

4.3 阻塞式API的设计思路

阻塞式API,就是调用者必须等结果回来才能继续往下走。这种API设计简单,适合在任务调度不复杂的场景下使用。

我建议把阻塞式API封装成下面这种形式:

/**
 * @brief  阻塞式单通道ADC读取
 * @param  channel: ADC通道号 (0-15)
 * @param  timeout_ms: 超时时间,单位毫秒
 * @retval 转换结果,超时返回0xFFFFFFFF
 */
uint32_t ADC_ReadBlocking(uint32_t channel, uint32_t timeout_ms)
{
    ADC_ChannelConfTypeDef sConfig = {0};
    uint32_t result = 0;
    
    // 配置通道
    sConfig.Channel = channel;
    sConfig.Rank = ADC_REGULAR_RANK_1;
    sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
    
    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
    {
        return 0xFFFFFFFF;
    }
    
    // 启动+轮询+读取
    if (HAL_ADC_Start(&hadc1) == HAL_OK)
    {
        if (HAL_ADC_PollForConversion(&hadc1, timeout_ms) == HAL_OK)
        {
            result = HAL_ADC_GetValue(&hadc1);
        }
        HAL_ADC_Stop(&hadc1);
    }
    
    return result;
}

这个API的好处是:

  • 参数明确:通道号和超时时间一目了然
  • 可复用:切换通道时只需改channel参数
  • 错误传递:超时或配置失败都返回同一个错误值

注意:阻塞式API在超时时间内会一直占用CPU。如果你的系统有实时性要求(比如电机控制),建议把超时时间设短一些,或者改用中断方式。

4.4 转换时间的精确计算

转换时间怎么算?这是面试常考的问题,也是实际项目中必须掌握的技能。

ADC转换时间的计算公式如下:

T_conv = (采样周期 + 转换周期) × T_ADCCLK

其中:
- 采样周期:由SMP[2:0]位配置,可选3/15/28/56/84/112/144/480个周期
- 转换周期:固定为12.5个周期(12位分辨率时)
- T_ADCCLK:ADC时钟周期

举个例子,假设ADC时钟为12MHz,采样周期设为3个周期:

T_ADCCLK = 1 / 12MHz ≈ 83.3ns
T_conv = (3 + 12.5) × 83.3ns ≈ 1.29μs

也就是说,一次转换大约1.3微秒。这个时间对于轮询来说完全没问题,CPU等得起。

采样周期配置 总转换周期 12MHz时钟下转换时间 适用场景
3周期 15.5周期 1.29μs 低阻抗信号源
15周期 27.5周期 2.29μs 一般传感器
56周期 68.5周期 5.71μs 中等阻抗信号源
480周期 492.5周期 41.04μs 高阻抗信号源

我曾经在一个电池电压采集项目中,用了480周期的采样时间,结果转换一次要40多微秒。当时觉得太慢了,但后来发现电池电压本身变化就很慢,这个速度完全够用。所以采样周期的选择,关键看信号源的阻抗和你的精度要求。

经验之谈:采样周期越长,采样电容充电越充分,精度越高。但别盲目选最长的,够用就行。我一般先看数据手册里ADC输入阻抗的推荐值,再反推采样周期。

4.5 单次转换模式的知识结构

下面这张图把单次转换模式的核心知识点串起来了:

单次转换模式知识结构图 单次转换模式知识结构 单次转换模式 ① 启动转换 ② 等待完成 ③ 读取结果 软件触发 / 外部触发 HAL_ADC_Start() 轮询EOC标志位 HAL_ADC_PollForConversion() 读取ADC_DR寄存器 HAL_ADC_GetValue() 转换时间 = (采样周期 + 12.5) × T_ADCCLK 阻塞式API:启动 → 轮询 → 读取 → 停止

4.6 实际项目中的避坑指南

单次转换模式虽然简单,但坑也不少。我把自己踩过的坑列出来,你遇到了可以直接对照排查:

  • 坑1:转换结果始终为0 —— 检查ADC是否真的启动了。我遇到过GPIO配置错误导致ADC无法使能的情况
  • 坑2:轮询一直超时 —— 检查ADC时钟是否使能。有一次我忘了在RCC里开启ADC时钟,结果轮询永远等不到EOC
  • 坑3:读取的值跳变很大 —— 采样周期太短了。特别是接长导线或高阻抗信号源时,采样周期至少设到56周期以上
  • 坑4:多通道切换后读到的值不对 —— 切换通道后要等一小段时间再启动转换,给采样电容充电留出时间

我曾经犯过的错:在一个量产项目中,我把ADC采样周期设成了3周期,结果有5%的产品采集的电压值偏差超过2%。排查了两天才发现是PCB走线过长导致信号源阻抗变大。后来把采样周期改成56周期,问题解决。

4.7 小结

单次转换模式+轮询读取,是ADC驱动开发的基础功。你只要记住三点:

  • 流程:启动→轮询→读取→停止,一步都不能少
  • 时间:转换时间 = (采样周期 + 12.5) × ADC时钟周期,采样周期别选太短
  • 封装:写一个带超时参数的阻塞式API,方便上层调用