4、单通道ADC采集实战:用STM32CubeMX生成代码,读取氧气浓度传感器的模拟电压

好,咱们终于到了动手环节。

前面讲了那么多理论,什么采样定理、量化误差、滤波算法……说实话,光听不练容易犯困。我自己带新人时有个习惯:先让他们把ADC调通,看到电压值在屏幕上跳,兴趣就来了。

这一章,我们就用STM32CubeMX生成一个完整的单通道ADC工程。目标很明确——读取氧气浓度传感器的模拟电压,把原始值换算成实际电压,再映射成氧浓度百分比。

4.1 硬件准备:你需要什么?

先列个清单,免得你焊了半天发现少个电阻。

  • 主控板:STM32F103C8T6(蓝色小板子,我项目里用得最多)
  • 氧气浓度传感器:比如ME2-O2,输出0~2V对应0~25%氧浓度
  • 分压电阻:如果传感器输出超过3.3V,需要分压。ME2-O2最高2V,可以直接接
  • 100nF电容:在ADC引脚对地并联,滤高频噪声。我习惯在传感器输出端也加一个
  • 杜邦线、面包板:调试用
注意:STM32的ADC输入电压绝对不能超过VDDA(通常是3.3V)。我曾经有个同事直接把5V传感器输出怼进去,芯片当场冒烟。嗯,那哥们后来被罚请全组喝奶茶。

4.2 CubeMX配置步骤

打开STM32CubeMX,新建工程,选好你的芯片型号。我以F103C8为例。

4.2.1 配置时钟

先把时钟树搞定。我个人习惯用外部8MHz晶振,通过PLL倍频到72MHz。

  • RCC → HSE → Crystal/Ceramic Resonator
  • Clock Configuration → HCLK设为72MHz

ADC的时钟频率不能太高。F103的ADC最大时钟是14MHz,我一般分频到12MHz,采样时间设长一点,精度更好。

4.2.2 配置ADC

在Pinout & Configuration里找到ADC1,开始配置:

  • Mode:IN0(对应PA0引脚)
  • ADC_Settings
    • Data Alignment:Right alignment(右对齐,默认)
    • Scan Conversion Mode:Disabled(单通道不需要扫描)
    • Continuous Conversion Mode:Enabled(连续转换,省得每次手动触发)
    • Number of Conversion:1
  • ADC_Regular_Conversion
    • Sampling Time:55.5 Cycles(我习惯选长一点,信号更稳)
小技巧:如果你发现采集的电压跳得厉害,试试把采样时间加到239.5 Cycles。代价是转换速度变慢,但精度提升明显。我在做医疗级制氧机时,采样时间都设得很长,毕竟氧浓度数据不能抖。

4.2.3 配置DMA(可选但推荐)

如果你不想让CPU一直轮询ADC,可以用DMA自动搬运数据。

  • 添加DMA:ADC1 → DMA1 Channel1
  • Mode:Circular(循环模式,一直转)
  • Data Width:Half Word(ADC是12位,半字刚好)

用DMA的好处是:CPU可以干别的事,ADC数据自动存到数组里。我早期做项目时傻傻地用轮询,结果CPU占用率飙到30%,后来改用DMA,降到2%不到。

4.3 生成代码并编写逻辑

点击GENERATE CODE,Keil或STM32CubeIDE工程就出来了。打开main.c,找到main()函数。

4.3.1 添加全局变量

/* 在main.c顶部添加 */
uint16_t adc_raw_value = 0;      // ADC原始值(0~4095)
float voltage = 0.0f;            // 实际电压值(单位:V)
float oxygen_concentration = 0.0f; // 氧浓度百分比(%)

4.3.2 启动ADC转换

main()while(1)之前,加上启动代码:

/* 启动ADC校准(F103需要这一步) */
HAL_ADCEx_Calibration_Start(&hadc1);

/* 启动连续转换 */
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&adc_raw_value, 1);

如果你没用DMA,就用:

HAL_ADC_Start(&hadc1);

4.3.3 主循环里做计算

while (1)
{
    if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)
    {
        adc_raw_value = HAL_ADC_GetValue(&hadc1);
        
        /* 换算成电压:3.3V / 4096 * 原始值 */
        voltage = (3.3f / 4096.0f) * (float)adc_raw_value;
        
        /* 映射成氧浓度:传感器0~2V对应0~25% */
        oxygen_concentration = (voltage / 2.0f) * 25.0f;
        
        /* 限幅保护 */
        if (oxygen_concentration < 0.0f) oxygen_concentration = 0.0f;
        if (oxygen_concentration > 25.0f) oxygen_concentration = 25.0f;
    }
    
    HAL_Delay(100);  // 100ms采集一次,够用了
}

关键点:

  • ADC是12位,所以最大值是4095(2^12 - 1),不是4096。但用4096算电压更准,因为0V对应0,3.3V对应4095,中间是线性关系。
  • 传感器映射公式一定要看数据手册。ME2-O2是0~2V对应0~25%,但不同型号可能不一样。

4.4 避坑指南:我踩过的三个坑

做ADC采集,看着简单,实际坑不少。我一个个说:

坑一:参考电压不准

STM32的内部参考电压(VREFINT)其实挺稳的,但如果你用VDDA作为参考,而VDDA有纹波,那采集值就会跳。我曾经在一个项目里,电源纹波200mV,ADC值跳了30多个LSB。

解决办法:在VDDA引脚对地加一个10μF+100nF的滤波电容,或者用外部基准电压芯片(比如TL431)。

坑二:引脚悬空

ADC引脚如果不接传感器,悬空状态下会采集到随机值。我见过有人调试时没接传感器,看到电压在1.2V~2.8V之间乱跳,以为是代码写错了,查了两天。

解决办法:不用的ADC引脚接地,或者配置成GPIO输出低电平。

坑三:采样时间太短

传感器输出有内阻,如果采样时间太短,采样电容充不满电,结果偏小。F103的ADC采样电容约4pF,如果传感器内阻10kΩ,采样时间至少需要1.5μs。

经验值:我一般选55.5 Cycles(约4.6μs@12MHz),对于大多数传感器都够用。

4.5 验证结果

程序烧进去后,用串口打印出来看看:

printf("ADC Raw: %d, Voltage: %.3f V, O2: %.1f%%\r\n", 
       adc_raw_value, voltage, oxygen_concentration);

正常情况,在空气中(21%氧浓度),你应该读到:

  • ADC原始值:约2600~2700(取决于参考电压精度)
  • 电压值:约1.68V(21% / 25% * 2V)
  • 氧浓度:约21%

如果数值偏差大,先检查分压电阻和参考电压。我习惯用万用表量一下传感器输出脚,确认实际电压对不对。

我的习惯:每次调完ADC,我都会在代码里加一个自检逻辑——上电时读一次内部温度传感器通道(比如F103的ADC1_IN16),如果温度值在合理范围(25~45°C),说明ADC硬件工作正常。这个小技巧帮我省了不少排查时间。

4.6 小结

这一章我们走通了完整的ADC采集流程:从CubeMX配置,到代码生成,再到实际换算。说白了,ADC就是个电压-数字转换器,你给它电压,它给你数字,剩下的就是小学数学。

下一章我们会讲怎么对采集到的原始数据进行滤波处理。你想想看,如果ADC值直接拿去用,稍微有点噪声,氧浓度就上下乱跳,病人看着都慌。所以,滤波是必须的。

嗯,今天就到这里。去把你的传感器接上,看看串口打印出来的数据稳不稳。