
新手玩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或1AO则是原始的分压电压。这样同一个模块既能用来体验ADC采集模拟量也能用来做阈值触发的数字开关一个器件玩出两种实验很划算。另外光敏传感器模块的价格极低几块钱就能买到把AO引脚的电压值测准了对今后搞电池电压监测、电流采样这类工业项目也有直接的迁移价值。2. 硬件准备与接线避坑指南2.1 器件清单与选购注意事项我做这个实验用到的器件如下器件型号/规格数量备注主控板STM32F103C8T6蓝丸板1其他F1系列也可方法相同光敏传感器模块LM393AODO输出1几块钱那种即可OLED屏0.96寸 SSD1306 I2C 128x641选4脚版本VCCSCLSDAGND面包板830孔1方便改线杜邦线母对母若干蓝丸板引脚是排针需要母头线选购时有三点容易翻车我挨个说明。第一OLED屏有I2C和SPI两种接口版本外观长得很像都是4脚或7脚。买的时候要看清楚标题或描述里是否标注了“I2C版本”。如果是7个引脚那是SPI/并口版本接线和控制方式完全不同。如果只有4脚VCC/GND/SCL/SDA才是I2C。第二部分光敏传感器模块上有电位器蓝色小方块用螺丝刀可以拧用于调节DO的翻转阈值。拧的时候要用无感螺丝刀或者干脆用绝缘胶带包住金属螺丝刀避免干扰电路。我第一次调DO阈值时吃了这个亏——螺丝刀金属杆碰到电位器调节端人体感应电压直接干扰了比较器输出。第三蓝丸板C8T6的ADC输入引脚不要接超过3.3V的电压。板上虽然有3.3V稳压芯片但很多模块如果你直接给它供5VAO输出的高电平会被上拉到接近5V直接灌进ADC引脚轻则读数全是4095超出范围重则烧掉PA口。稳妥做法是光敏模块的VCC接3.3V保证AO输出最高不超过3.3VADC就安全了。这个经验适用于绝大多数量程在3.3V以内的模块。2.2 接线表与供电细节完整接线关系如下外设引脚连接目标说明光敏模块VCC3V3不要接5V理由见上文光敏模块GNDGND共地必须和单片机共地光敏模块AOPA1ADC1的通道1或其他可用通道光敏模块DO不用本次实验只用AOOLED模块VCC3V3OLED内部稳压后可运行在3.3VOLED模块GNDGND共地OLED模块SCLPB6STM32F103的I2C1_SCL默认引脚OLED模块SDAPB7STM32F103的I2C1_SDA默认引脚有一个细节值得啰嗦共地的问题。ADC测量的是一个电压差值单片机读的是引脚相对于GND的电压如果传感器模块的GND和单片机的GND不是一个电位读出来的值会莫名其妙地偏大或偏小而且带有随机性。所以无论接什么模块先确认地线已接好这是排查一切“读数不稳定”问题的第一顺位检查项。另外很多光敏模块AO引脚的默认输出电压在正常光照下是1V左右手指盖住后可能升到2.5V以上不同模块差异不小原因在于板上固定电阻的阻值不同。所以不要照搬别人的“电压-光照换算公式”必须以你自己模块的实测数据为准。2.3 光敏模块的两个输出口分别怎么用这里多说一句模块内部的电路结构对理解后面的标定很有帮助。这种LM393光敏模块本质上做了一件事把光敏电阻和一个固定电阻分压得到随光照变化的电压V_ao然后把这个V_ao送进LM393比较器的一个输入端另一个输入端接电位器设定的参考电压V_ref。比较器输出就是DO当V_ao大于V_refDO输出低电平约0V小于V_refDO输出高电平约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它把0Vref通常Vref就是VCC即3.3V这个连续范围离散成4096个台阶。输入的模拟电压Vin对应输出数字量DD 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 读取模式选择轮询、中断还是DMAHAL库给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生成工程我习惯先把时钟树配好再配外设顺序能减少很多返工。这里直接给出关键配置项和设置值。时钟树最高72MHzHSE外部晶振8MHz蓝丸板上的晶振PLL源HSEPLL倍频x9SYSCLK72MHzAPB1分频2 → 36MHzAPB2分频1 → 72MHz注意STM32F103的ADC挂在APB2上最高输入时钟不能超过14MHz所以必须设置ADC预分频器为6分频得到12MHz的ADCCLK。ADC1配置Channel 1对应PA1Enabled扫描模式Disabled只采一个通道连续转换模式Disabled每次手动触发一次转换采样时间239.5 cycles拉满转换模式Single conversion12位分辨率保持默认这样配置出来每次用HAL_ADC_Start启动一次单通道转换然后等待完成读值符合轮询逻辑。I2C1配置I2C1Enabled模式I2C速率400KHzFast 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.20.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)手指完全盖住约535202.84室内普通光照约15018001.45手机闪光灯直照约30002000.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占用多少。这些清单式的工程直觉才是做嵌入式最有价值的东西。