4. 单次转换模式:轮询方式读取与阻塞式API实现
单次转换模式,说白了就是让ADC只干一次活——启动一次,转换一次,然后停下来等你来取结果。这是最基础、最容易理解的ADC工作方式,也是我当年入门STM32时第一个上手的模式。
你想想看,如果连一次转换都搞不定,那连续转换、注入组、DMA这些高级玩法就更别想了。所以这一章,咱们把单次转换模式吃透。
4.1 单次转换模式的核心流程
单次转换模式的执行路径其实很简单,就三步:
- 启动转换:软件触发或外部触发,让ADC开始干活
- 等待完成:CPU轮询状态寄存器,直到转换结束
- 读取结果:从数据寄存器把转换值取走
嗯,这里要注意——单次转换模式每次只能转换一个通道。如果你想扫多个通道,得手动切换通道再重新启动。我在一个温湿度采集项目里就踩过这个坑,当时以为配置了多通道就能自动扫完,结果读出来的全是同一个通道的值。
关键点:单次转换模式下,每次转换完成后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 单次转换模式的知识结构
下面这张图把单次转换模式的核心知识点串起来了:
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,方便上层调用