新手玩STM32,做到GPIO点灯、串口打印这些基础操作之后,下一个绕不开的坎就是ADC。这个实验的经典程度不亚于点灯,但它比点灯有意思得多——因为ADC是把物理世界和数字世界打通的第一座桥。你写一万行代码算Fibonacci,单片机也只能在数字世界里自嗨,但一旦接上ADC,它就能“看见”光的强弱、电池的电量、旋钮的角度、电流的大小,这些全是真实物理量。
这篇实战笔记拿光敏传感器开刀,完整走一遍“模拟电压采集 → ADC量化 → 软件滤波 → I2C OLED实时显示”的链路,用的是STM32F103C8T6(蓝丸板)加HAL库。普通人玩这种实验,往往是代码跑通了、屏上有数字了就算完事,但真正有价值的细节在于:ADC的采样时间对结果的影响、轮询和DMA在实时显示场景下该怎么选、传感器输出和实际光照强度的换算关系,以及I2C刷屏时怎么避免主循环卡顿。这篇不光是给你贴一段能跑通的代码,而是把每个关键选择背后的道理讲透,照着做一遍,等于把模拟电路、ADC外设、总线通信这三块知识点串起来了。
1. 为什么这个实验是STM32入门的“及格线”
1.1 一个传感器应用背后藏着的三条知识线
很多教程把ADC采集实验简化成“配置一个外设,读一个寄存器,打印一个数字”,这是远远不够的。光敏传感器这个实验牵出了三条必须打通的知识线:
第一条是硬件链路。光敏电阻本身是一个随光照强度改变阻值的元件,它必须和一个固定电阻组成分压电路,才能把“电阻变化”转化成“电压变化”。这条链路里藏着欧姆定律、分压原理、阻抗匹配这些模拟电路的基本功。很多新手在这一步就开始踩坑——传感器模块买回来后不知道AO和DO怎么区别,或者直接把AO引脚接到单片机上就完事,完全没想过输出电压的范围是否匹配ADC的输入范围。
第二条是ADC外设本身。STM32的ADC内部是一个逐次逼近型转换器,它靠“二分法比较”的方式把连续电压离散化成12位的数字量。分辨率、参考电压、采样时间、转换模式这些参数,每一个都对结果有实质影响。
第三条是数据传输与呈现。采集到的数字量要变成人眼能看懂的信息,途径很多:串口打印、OLED显示、蓝牙上传手机App。这篇用的是I2C接口的OLED屏幕,所以又牵出一条I2C时序、地址匹配、显存刷新机制的知识线。
这三条线,其实就是一个真实产品的基本骨架:感知层、处理层、呈现层。把这三层都打通,哪怕功能很简单,你的工程能力和只会在开发板上点灯的同学就已经彻底拉开差距了。
1.2 我为什么推荐光敏传感器作为ADC入门器件
有人会用电位器来做ADC实验,也有人用声音模块、火焰传感器、MQ系列气体传感器。相比之下,光敏传感器有两个无可替代的优势。
第一是物理量变化直观、响应快。手指在传感器上一档,或者拿手机闪光灯照一下,输出立刻就有明显变化,调试的时候不需要什么特殊仪器,肉眼就能判断程序有没有跑通。而MQ气体传感器要预热很久,火焰传感器对调试环境有要求,电位器又太“人造”了——它绕过了真实的物理量采集过程。
第二是它兼顾了“模拟量”和“数字量”两种输出。市面上常见的基于LM393比较器的光敏传感器模块,会同时引出AO(模拟输出)和DO(数字输出)两个引脚。DO通过板上的电位器调节阈值,输出0或1;AO则是原始的分压电压。这样同一个模块既能用来体验ADC采集模拟量,也能用来做阈值触发的数字开关,一个器件玩出两种实验,很划算。
另外,光敏传感器模块的价格极低,几块钱就能买到,把AO引脚的电压值测准了,对今后搞电池电压监测、电流采样这类工业项目也有直接的迁移价值。
2. 硬件准备与接线避坑指南
2.1 器件清单与选购注意事项
我做这个实验用到的器件如下:
| 器件 | 型号/规格 | 数量 | 备注 |
|---|---|---|---|
| 主控板 | STM32F103C8T6(蓝丸板) | 1 | 其他F1系列也可,方法相同 |
| 光敏传感器模块 | LM393(AO+DO输出) | 1 | 几块钱那种即可 |
| OLED屏 | 0.96寸 SSD1306 I2C 128x64 | 1 | 选4脚版本,VCC+SCL+SDA+GND |
| 面包板 | 830孔 | 1 | 方便改线 |
| 杜邦线 | 母对母 | 若干 | 蓝丸板引脚是排针,需要母头线 |
选购时有三点容易翻车,我挨个说明。
第一,OLED屏有I2C和SPI两种接口版本,外观长得很像,都是4脚或7脚。买的时候要看清楚标题或描述里是否标注了“I2C版本”。如果是7个引脚,那是SPI/并口版本,接线和控制方式完全不同。如果只有4脚(VCC/GND/SCL/SDA),才是I2C。
第二,部分光敏传感器模块上有电位器(蓝色小方块,用螺丝刀可以拧),用于调节DO的翻转阈值。拧的时候要用无感螺丝刀,或者干脆用绝缘胶带包住金属螺丝刀,避免干扰电路。我第一次调DO阈值时吃了这个亏——螺丝刀金属杆碰到电位器调节端,人体感应电压直接干扰了比较器输出。
第三,蓝丸板C8T6的ADC输入引脚不要接超过3.3V的电压。板上虽然有3.3V稳压芯片,但很多模块如果你直接给它供5V,AO输出的高电平会被上拉到接近5V,直接灌进ADC引脚,轻则读数全是4095(超出范围),重则烧掉PA口。稳妥做法是:光敏模块的VCC接3.3V,保证AO输出最高不超过3.3V,ADC就安全了。这个经验适用于绝大多数量程在3.3V以内的模块。
2.2 接线表与供电细节
完整接线关系如下:
| 外设 | 引脚 | 连接目标 | 说明 |
|---|---|---|---|
| 光敏模块 | VCC | 3V3 | 不要接5V,理由见上文 |
| 光敏模块 | GND | GND | 共地,必须和单片机共地 |
| 光敏模块 | AO | PA1 | ADC1的通道1(或其他可用通道) |
| 光敏模块 | DO | 不用 | 本次实验只用AO |
| OLED模块 | VCC | 3V3 | OLED内部稳压后可运行在3.3V |
| OLED模块 | GND | GND | 共地 |
| OLED模块 | SCL | PB6 | STM32F103的I2C1_SCL默认引脚 |
| OLED模块 | SDA | PB7 | STM32F103的I2C1_SDA默认引脚 |
有一个细节值得啰嗦:共地的问题。ADC测量的是一个电压差值,单片机读的是引脚相对于GND的电压,如果传感器模块的GND和单片机的GND不是一个电位,读出来的值会莫名其妙地偏大或偏小,而且带有随机性。所以无论接什么模块,先确认地线已接好,这是排查一切“读数不稳定”问题的第一顺位检查项。
另外,很多光敏模块AO引脚的默认输出电压在正常光照下是1V左右,手指盖住后可能升到2.5V以上,不同模块差异不小,原因在于板上固定电阻的阻值不同。所以不要照搬别人的“电压-光照换算公式”,必须以你自己模块的实测数据为准。
2.3 光敏模块的两个输出口分别怎么用
这里多说一句模块内部的电路结构,对理解后面的标定很有帮助。
这种LM393光敏模块本质上做了一件事:把光敏电阻和一个固定电阻分压,得到随光照变化的电压V_ao,然后把这个V_ao送进LM393比较器的一个输入端,另一个输入端接电位器设定的参考电压V_ref。比较器输出就是DO:当V_ao大于V_ref,DO输出低电平(约0V);小于V_ref,DO输出高电平(约3.3V)。所以DO信号是一个反相的数字量。
理解了这个结构,你就知道两件事:
- DO可以作为“光线足够暗”或“光线足够亮”的开关信号,直接接GPIO检测,甚至接中断触发,适合做光控灯、光控报警这类应用;
- AO是连续的模拟电压,能反映光照的细微变化,适合做光线强度测量、环境光自适应、屏幕亮度调节这类应用。
本次实验走AO路线。如果你想把两种方式都体验,可以把DO也接一个GPIO,比如PA0,在程序里用HAL_GPIO_ReadPin同时读它,打印到OLED上。
3. ADC采集的核心原理与参数选择
3.1 从“分压”到“量化”:ADC是怎么工作的
先把电压产生的环节搞清楚。光敏电阻的阻值随光照增加而下降(这是硫化镉光敏电阻的特性,光线越强,阻值越低)。模块上,它和一定值电阻R串联,接在VCC和GND之间,AO节点取中间的电压:
V_ao = VCC × R / (R + R_light)
光照强 → R_light下降 → V_ao下降;光照弱 → R_light上升 → V_ao上升。所以AO的电压和光照强度是反比关系,这在后面标定时要用到。
接下来是ADC的量化过程。STM32F103内部集成了一个12位逐次逼近型ADC,它把0~Vref(通常Vref就是VCC,即3.3V)这个连续范围离散成4096个台阶。输入的模拟电压Vin对应输出数字量D:
D = Vin / Vref × 4096
反过来,已知数字量D还原电压:
Vin = D / 4096 × 3.3V
这里的“逐次逼近”四个字,理解起来不复杂,打个比方:猜一个0到4095之间的数,每次猜中间值,比较器告诉你猜测偏大还是偏小,逐步缩小区间,12次比较之后就锁定最终数字量。这就是SAR ADC内部的基本逻辑。
STM32的ADC转换时间由采样阶段和转换阶段组成:
T_conv = (采样时间 + 12.5个ADC时钟周期) × T_ADCCLK
采样时间的作用是让ADC内部的采样电容充分充到和输入电压一致。如果你选的采样时间太短,电容还没充满就进入比较阶段,读出来的值就会系统性地偏小——这是新手最容易忽略的一个坑。
3.2 分辨率、参考电压、采样时间的取舍
STM32F103的ADC是12位分辨率,但允许配置为12位、10位、8位或6位。这个参数该怎么选?
分辨率每降低一个档次,转换速度略快,但精度损失明显。比如把分辨率降到8位,原本能区分4096个等级,现在只能区分256个等级,光敏传感器两毫米的距离变化可能都反映不出来。实际项目中,如果没有特殊的速度需求,直接保持12位默认值,不要乱动。
参考电压这个参数更重要。F103的参考电压引脚VREF+通常和VDD绑在一起,也就是3.3V。如果一个电压源不稳,会直接影响所有ADC读数。有些高端板子会用独立的基准电压芯片(比如2.5V或4.096V)来供电,测量精度更高。蓝丸板没有独立基准,就用3.3V当参考,前提是板上的LDO稳压输出比较稳定,对光敏实验来说精度足够。
如果你的应用需要更高精度,进阶做法是用内部参考电压通道(STM32F1系列有内部参考电压接口),在运行时实时校准参考电压的漂移,这是另一个话题了。
采样时间的设置,我在工程里直接拉满到239.5周期,ADC预分频设为6分频(ADCCLK = 72MHz / 6 = 12MHz),这样单次转换时间为:
T_conv = (239.5 + 12.5) / 12MHz ≈ 21µs
这个速度对光敏传感器来说绰绰有余。光线的变化是慢变量,一秒钟采样一百次都很冗余。如果你后续做的项目是采样音频或者电机相电流,需要几十万次每秒的采样率,那么采样时间就要适当缩短,换取速度。
3.3 读取模式选择:轮询、中断还是DMA
HAL库给ADC提供了三种读取模式,这个选择非常影响工程结构。
- 轮询模式:程序调用
HAL_ADC_Start后,循环等待转换完成,HAL_ADC_PollForConversion直到标志位置位,再读数据寄存器。简单直接,占CPU,转换期间只能干等。 - 中断模式:转换完成后触发中断,在中断回调里取数据。适合低延迟响应场景,但频繁中断会打断主循环。
- DMA模式:ADC转换完数据自动搬运到内存,CPU不参与,适合连续采样大批量数据的场景。
对于光敏传感器这个实验,我建议直接用轮询模式。理由是:单次转换只要约20µs,这段时间CPU就算被占用也几乎无感知;程序逻辑清晰,适合新手理解。不要一上来就追求高端的DMA,把简单的东西搞复杂。
但是有一个例外:如果你后续要接多个模拟量通道,希望ADC自动轮流扫描所有通道,并且数据要同时保存到数组里,再叠加滤波算法,那用DMA是更合理的选择。DMA配合多通道扫描,可以做到“转换完所有通道之前CPU完全不管”,数据一致性非常好。这个实验先把轮询跑熟,下一步再升级到DMA不迟。
4. 工程搭建与代码实现:从CubeMX到业务逻辑
4.1 CubeMX配置要点逐项说明
用STM32CubeMX生成工程,我习惯先把时钟树配好,再配外设,顺序能减少很多返工。这里直接给出关键配置项和设置值。
时钟树(最高72MHz):
- HSE:外部晶振8MHz(蓝丸板上的晶振)
- PLL源:HSE
- PLL倍频:x9
- SYSCLK:72MHz
- APB1分频:2 → 36MHz
- APB2分频:1 → 72MHz
注意,STM32F103的ADC挂在APB2上,最高输入时钟不能超过14MHz,所以必须设置ADC预分频器为6分频,得到12MHz的ADCCLK。
ADC1配置:
- Channel 1(对应PA1):Enabled
- 扫描模式:Disabled(只采一个通道)
- 连续转换模式:Disabled(每次手动触发一次转换)
- 采样时间:239.5 cycles(拉满)
- 转换模式:Single conversion
- 12位分辨率:保持默认
这样配置出来,每次用HAL_ADC_Start启动一次单通道转换,然后等待完成,读值,符合轮询逻辑。
I2C1配置:
- I2C1:Enabled
- 模式:I2C
- 速率:400KHz(Fast Mode)
- 地址大小:7位
这里注意:SSD1306支持的速率范围很宽,从100KHz到1MHz都可以,但STM32F1的I2C硬件模块在400KHz下需要合理配置时序参数,CubeMX一般能自动计算好。如果后续你发现OLED屏幕显示花屏或偶发无响应,把速率降到100KHz,大部分问题会消失。原因是OLED模组引线过长、面包板接触不良,或者模块上拉电阻值不匹配,高速模式下容性负载过大会导致信号变形。
生成工程时,选择HAL库,Toolchain为MDK-ARM。生成后,在main.c里替换用户代码段。
4.2 主循环与ADC轮询读取代码
核心读取函数写成这样:
#include "adc.h" #include "oled.h" #include <stdio.h> #include <string.h> #define ADC_REF_VOLTAGE 3.3f #define ADC_FULL_SCALE 4096.0f uint32_t adc_read_raw(void) { HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK) { return HAL_ADC_GetValue(&hadc1); } return 0; } float adc_read_voltage(void) { uint32_t raw = adc_read_raw(); return (float)raw * ADC_REF_VOLTAGE / ADC_FULL_SCALE; }这段代码本身不复杂,但有一个关键点:HAL_ADC_PollForConversion的超时时间参数。我设置了100毫秒,正常情况下单次转换20µs就完成,不可能超时。这个100ms的兜底是为了防止某种外设异常导致标志位一直不置位,程序永远卡死在那里。养成给所有阻塞操作加超时这个习惯很重要,尤其在未来调试复杂外设的时候,能帮你快速定位是哪一环卡住了。
接下来是滤波处理。ADC单次采样的数据一定是有噪声的,尤其是光敏传感器这种模拟输出,受环境光抖动、供电纹波、面包板接触电阻的影响,原始值通常是波动的,比如在600和620之间乱跳。如果把原始值直接送到OLED刷新,屏幕上数字肉眼可见地跳变,观感很差,而且没有意义。
我用一个简单的滑动平均滤波:
#define FILTER_SIZE 16 uint32_t adc_filtered_value(void) { uint32_t sum = 0; for (int i = 0; i < FILTER_SIZE; i++) { sum += adc_read_raw(); } return sum / FILTER_SIZE; }每次读取连续16次ADC值取平均。光敏传感器是慢变量,16次采样大约耗时320µs,也就是大约0.3毫秒,这个开销完全可以接受,换来的是显示数据的稳定。
如果想更平滑,可以用一阶低通滤波:
float lowpass_filter(float new_sample, float old_value, float alpha) { return alpha * new_sample + (1.0f - alpha) * old_value; }alpha取0.2~0.3,响应速度快,平滑效果也够。这两种滤波方式都建议亲手实现一遍,因为ADC滤波是所有传感器项目的地基,后面做温湿度、气压、电流采样全都要用。
4.3 OLED驱动与I2C显示实现的取舍
SSD1306 OLED的驱动网上代码一抓一大把,我直接讲关键逻辑。它本质是一个带I2C接口的显存设备:128x64分辨率对应显存大小是128×64/8 = 1024字节,1代表屏幕上的白点,0代表黑点。你需要把要显示的像素数据准备好,通过I2C写入SSD1306的图形显存(GDDRAM),屏幕硬件负责把显存内容渲染出来。
这里有个重要原理:SSD1306有两个I2C地址,0x3C和0x3D,取决于模块背面一个电阻的焊接位置。绝大多数模块默认是0x3C。如果你的OLED一直没有响应,先查地址,不要只顾着检查接线。
我的OLED驱动核心函数就两个:
void OLED_Clear(void); void OLED_ShowString(uint8_t line, uint8_t col, const char *str);显示字符串内部会把ASCII字符的点阵数据写入对应的显存位置。写显存时需要先通过I2C发送控制字节(0x00表示接下来是命令,0x40表示接下来是数据),然后连续发送数据。
I2C发送函数用HAL库的标准接口:
void OLED_WriteCommand(uint8_t cmd) { uint8_t buffer[2] = {0x00, cmd}; HAL_I2C_Master_Transmit(&hi2c1, 0x3C << 1, buffer, 2, 100); } void OLED_WriteData(uint8_t data) { uint8_t buffer[2] = {0x40, data}; HAL_I2C_Master_Transmit(&hi2c1, 0x3C << 1, buffer, 2, 100); }注意HAL库的HAL_I2C_Master_Transmit接口中,从机地址需要左移一位(7位地址变8位),因为它的内部处理了读写位。如果你直接传0x3C,等于把两个地址搞混了,发送的地址错位,屏幕肯定不亮。这是我见过新手翻车最多的一个点,没有之一。
主循环的调度逻辑:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC1_Init(); MX_I2C1_Init(); OLED_Init(); OLED_Clear(); OLED_ShowString(0, 0, "ADC LIGHT SENSOR"); float voltage = 0.0f; float old_volt = 0.0f; char line_buffer[20]; while (1) { uint32_t raw = adc_filtered_value(); voltage = (float)raw * ADC_REF_VOLTAGE / ADC_FULL_SCALE; voltage = lowpass_filter(voltage, old_volt, 0.3f); old_volt = voltage; sprintf(line_buffer, "Raw: %4d", raw); OLED_ShowString(2, 0, line_buffer); sprintf(line_buffer, "Vol: %.2f V", voltage); OLED_ShowString(3, 0, line_buffer); /* 简易光照强度条 */ uint8_t bar_len = (uint8_t)((raw * 16) / 4096 + 1); OLED_ShowBar(5, 0, bar_len); HAL_Delay(50); } }显示部分我加了一个“光强条”,用条形长度直观展示当前光照强弱。这个看起来很炫的功能,实现上其实就是把指定行的一段像素置白,十几行代码而已,但用户体验完全不一样——纯数字的读数和可视化的条形图,观感差距是很明显的。
为了显示这个条,需要额外实现一个写显存区域的方法:
void OLED_ShowBar(uint8_t row, uint8_t col, uint8_t length) { for (uint8_t i = 0; i < length; i++) { OLED_DrawPixel(row, col + i, 1); } for (uint8_t i = length; i < 16; i++) { OLED_DrawPixel(row, col + i, 0); } }OLED的刷新节奏用50ms一次,对光敏传感器足够。如果你在屏幕上额外显示帧率、日期、时间等变量,注意同一个I2C总线上的总带宽限制:400KHz的I2C最高每秒约50KB数据,一次全屏刷新(1024字节)需要约20ms,所以两个模块共享一个I2C时,刷新间隔不要低于50ms,否则总线会堵,优先级低的任务会被饿死。
5. 调试过程与实测数据分析
5.1 把ADC读数换算成“光照强度”的正确做法
很多教程到电压显示就停住了,实际上电压并不直观。你去跟别人说“当前光照电压是1.2V”,听的人没有概念;你说“现在约300勒克斯”,专业感立刻上来了。但勒克斯怎么来?需要标定。
简单标定方法如下:用手机的照度计App(随便一个就行)作为参考标准,在固定光源条件下,同时记录ADC读数和App显示的勒克斯值,记录几个不同光照条件的数据点,然后用模拟或者查表方式映射。
我测到的一组典型数据如下:
| 光照场景 | 手机照度计读数 (lux) | ADC原始值 | AO电压 (V) |
|---|---|---|---|
| 手指完全盖住 | 约5 | 3520 | 2.84 |
| 室内普通光照 | 约150 | 1800 | 1.45 |
| 手机闪光灯直照 | 约3000 | 200 | 0.16 |
可以看到AO电压和勒克斯值成反比关系,但反比不是线性的,因为光敏电阻的阻值-光照曲线本身是指数关系。严谨做法是通过光度计标定多点数据,拟合曲线;工程上偷懒一点,可以直接在代码里建一个查询表,用线性插值。
但大多数人做实验不会真的去校准勒克斯,我个人的建议是:你的程序输出电压值和原始值即可,光照强度换算留给有标定条件的场景。原因很简单,不同光敏模块的曲线千差万别,出厂没有保证,强行标定的数据只对你手上的模块有效,不具备普适性,反而会让读者误以为有一套通用公式。把原始值、电压值做实,已经是入门ADC实验的正确姿势。
5.2 首次上电后黑屏或读数异常的排查链路
这个实验看起来简单,但实际调试中,一次就跑通的人很少。我把最常见的故障现象、原因和排查方法整理成一个表格,建议你把它当作排查手册来用:
| 故障现象 | 可能原因 | 排查与解决 |
|---|---|---|
| OLED完全黑屏,无任何显示 | I2C地址错误 | 确认模块地址是0x3C还是0x3D;确认传参时已左移一位 |
| OLED黑屏,但串口打印正常 | OLED供电不足 | 部分OLED模块需要较高电流,试试独立供电,或换更短杜邦线 |
| OLED显示残影、花屏 | I2C速率过高或引线过长 | 将I2C时钟降至100KHz,缩短导线长度 |
| ADC读数恒定4095 | 输入电压超过3.3V,或引脚没接到AO | 万用表测AO引脚电压,确认接线正确 |
| ADC读数恒定0 | 引脚没使能或接错通道 | 检查PA1在CubeMX里是否配置为ADC1通道1 |
| 读数波动过大 | 电源纹波或面包板接触不良 | 换用杜邦线直连,光敏模块VCC接3.3V,加滤波电容 |
有一个排查经历值得分享。我曾经在一个项目里把光敏模块的AO接到PA7,主板供电来自USB转串口模块的3.3V,结果读数像心电图一样乱跳,怎么都稳定不下来。后来用示波器一量,发现USB转串口模块的3.3V本身就有大约200mV的纹波,ADC的参考电压就是VDD,纹波直接灌进基准里了。把主控改由电池供电后,纹波降到30mV以内,读数立刻稳定了。所以做ADC实验,保证干净的电源是第一位的,滤波算法救不了脏电源。
5.3 实测数据与波形:滤波前后的差异怎么看
滤波前后差异有多大?我用串口把原始值和滤波值同时打出来,每隔50ms采一个点,连续采样100个点,手动记录数据。
原始值的波动范围大约是50个LSB左右(12位精度下,50/4096 ≈ 1.2%),肉眼可见地在OLED上乱跳;经过16次滑动平均加一阶低通滤波后,波动范围压缩到3个LSB以内,显示非常稳定。代价是响应延迟:从光照突变到OLED数值稳定到新值,大约需要300ms,这个延迟对光敏传感器完全不影响使用。
滤波算法的选择建议:
- 慢变量(光、温度、湿度、气压):滑动平均 + 一阶低通,稳定第一。
- 快变量(音频、振动、电机相电流):中位值滤波或卡尔曼滤波,兼顾实时性。
- 突发脉冲干扰(静电放电、电机启停干扰):先取中位值,再用滑动平均,抗脉冲能力强。
在这个实验里,我顺手把中位值滤波也做了一版,取5次采样的中间值,效果和滑动平均接近,但代码量略大。最终选择滑动平均,原因很简单:它不消耗额外内存排序,实测够用。
6. 经验总结:ADC应用场景的三个实战纪律
这个实验做完后,回头分析一下,我提炼出了三条对后续各种传感器项目都适用的纪律,记录如下。
第一条:先量电压,再谈代码。ADC程序跑不通,先用万用表量模块AO和GND之间的电压,确认这是一个正常范围内的模拟电压。如果你连输入电压是多少都不知道,就去查ADC配置,那是浪费时间。硬件信号正确性是软件一切调试的前提,这条规律在工业现场尤其重要。
第二条:参考电压决定天花板。ADC的满量程精度上限由参考电压的稳定度决定,不是由ADC位数决定。24位ADC配一个纹波巨大的参考源,照样读不准。所以做精密测量时,优先考虑参考源的方案,其次才是选更高分辨率的ADC芯片。
第三条:明确一个值就够了。光敏传感器这类慢变量,采集频率不用太高,滤波拉满,显示刷新频率降到20Hz以内,系统稳定性和功耗都能得到优化。不要为了“帧率”去买单,除了自我感动没有任何工程价值。相反,如果未来做高频采集,速度、精度、稳定性三者的平衡才是核心,每个参数都要自己权衡,不能照抄别人的配置。
这套ADC + 光敏 + OLED的组合,代码量不大,但每一步都藏着一个值得研究的细节。做完之后,建议你自己跑一遍扩展任务:把光敏传感器换成电位器,或者把OLED改成串口绘图上位机,甚至把滤波算法换成卡尔曼滤波对比效果,收获会更大。至少,下次你再看到“STM32 ADC采集”这几个字,心里已经自发地冒出一个清单:电压范围、参考源、采样时间、滤波稳不稳、显示怎么刷新、CPU占用多少。这些清单式的工程直觉,才是做嵌入式最有价值的东西。