ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32温度电压电流检测报警器仿真设计:从ADC采样到OLED显示全流程

STM32温度电压电流检测报警器仿真设计:从ADC采样到OLED显示全流程

先说明一点:这里的“仿真设计”不是让你只看电路图,而是指先通过电子仿真软件把整条逻辑链路——传感器采样、ADC转换、OLED刷新、阈值报警——完整跑通,再去动实物。这个顺序对STM32这类项目来说,能省掉大量焊板、换件、抓瞎的时间。下面把我的做法和踩过的坑完整写出来。

1. 为什么要把温度、电压、电流做成三位一体的检测报警器

很多刚接触STM32的开发者做过温湿度计,也做过电压表,但把温度、电压、电流三个参数同时采集、实时显示并且带报警功能,才算真正把一个嵌入式小系统闭环了。这个项目的价值在于:它逼着你去处理“多路模拟量采集”“数据换算”“人机交互”“异常处理”四件事,而这四件事正是绝大多数工业监控、设备保护、实验室仪表项目的通用骨架。

1.1 三路测量量的量程、精度与显示优先级

我这次定的指标很贴近常见设备监控场景:温度用NTC热敏电阻或18B20这类数字传感器,量程做到0到100摄氏度左右,精度正负1度;电压针对0到24V直流电源,因为这是实验室和工作台上最常见的供电范围;电流则设计为0到2A,覆盖大多数小功率负载。

量程具体怎么定,直接决定了你的前端电路难度。

电压如果是0到24V,必须分压后才能进STM32的ADC,因为ADC输入引脚最高承受3.3V。常见的做法是把24V按比例缩小到3.0V左右,留一点裕量。电流稍微麻烦一点,0到2A如果直接用采样电阻,电阻上的压降是电流乘以阻值,阻值选大了损耗大、发热明显,选小了信号太小,后级放大又容易引入噪声。

显示优先级也要提前想清楚。OLED屏幕面积有限,我建议把三个数值做成一屏同时显示,报警时再切换状态,而不是做成翻页菜单。理由很简单:检测报警器的使用场景里,操作者需要在一秒内同时掌握三路状态,翻页会漏掉关键信息。

1.2 报警阈值怎么定才既灵敏又不误报

这是整个项目里最容易被低估的部分。

很多人写报警逻辑就是一句“大于某个值就响”,实际用起来会发现两个问题:一是负载正常波动时,电流在阈值附近来回跳,蜂鸣器跟着响一下停一下,非常烦人;二是报警之后即使参数恢复正常,蜂鸣器还不肯停,必须重新上电。

正确的做法是引入迟滞区间。比如温度上限设定为70度,那么触发温度设为70度,恢复解除设为65度,中间这5度是缓冲带。电流上限同理,触发2A、解除1.85A。这样即便负载有小幅波动,系统也不会反复触发。报警标志位还需要带一个锁定机制,比如“连续三次采样都越限才确认报警”,避免单次尖峰误判。这套逻辑写起来不难,但它是整个程序里最值得静下心来设计的状态机。

1.3 这块OLED屏幕在这类小系统里的特殊价值

前一段时间聊到显示方案时,有人问为什么不用LCD1602,便宜又常见。我的答案是:对于三路数值加报警状态的界面,OLED的小尺寸、高对比和自发光特性要好用得多。1602的蓝底白字在强光下可读性差,而且只能显示有限字符;OLED可以自己定义字体、画进度条、放大报警文字,界面灵活很多。

我选的是0.96寸128x64分辨率的I2C接口OLED,主控是SSD1306,接线只需要VCC、GND、SCL、SDA四根线。四线接法非常省IO,而且这类屏幕几乎每家都能直接点亮。唯一要注意的是有些模块需要3.3V供电,如果板子上同时有5V,接线前先确认供电电压,不然很容易烧模块。

2. 硬件方案定夺:温度传感器、分压网络与采样电阻的取舍

整个硬件链路里,温度通道是相对独立的,电压通道和电流通道则都依赖STM32的ADC。这里有一个容易忽略的事实:ADC是单端输入,所以所有被采样信号最终都要换算成相对于GND的电压,然后由程序反推物理量。我用一个土办法理解这个系统:传感器把物理量变成电压,电路把电压调到ADC能安全接受的区间,ADC把电压变成数字,程序把数字变成人能看懂的数。

2.1 温度采集:数字传感器和NTC两条路线怎么选

温度部分我先说NTC,因为它最贴近模拟信号的采样本质。NTC热敏电阻的阻值随温度非线性变化,通常用100k或10k规格,配合一个固定电阻分压,中间抽头接到ADC。换算温度时,最常用的是查表法:厂商提供一张“温度—阻值”表,程序里建一个数组做线性插值,算出对应温度。还有一种更标准的做法是用Steinhart-Hart方程直接计算,公式长一点,但精度比查表高,而且不需要占太多Flash。

数字传感器则是另一条更省事的路子。18B20这类器件直接把温度转换成数字量,通过单总线协议读出,不需要ADC参与。好处是精度稳定、接线简单,坏处是协议时序要求严格,不能使用标准I2C,而且要小心处理总线延时。

如果让我给第一次做这个项目的人推荐,我会说:先用NTC,因为一路模拟信号才能让你真正理解“分压—ADC—标定”这个过程;等这路彻底跑通了,再换18B20体会数字接口的方便。仿真里NTC和18B20都有现成模型,两条路都可以在电脑上先完整跑一遍。

2.2 电压采集:分压电阻背后的阻抗匹配问题

0到24V电压直接接STM32引脚必烧,分压是必须的。我这里用的是电阻分压网络,R1和R2把输入电压缩小到原值的约1/8。比如R1取100k,R2取12.5k,那24V输入时ADC引脚电压正好是24乘以12.5除以112.5,约等于2.67V,留出安全裕量。

这里有个非常重要的细节:STM32的ADC输入阻抗是有限的,数据手册里通常会给一个建议的最大源阻抗。如果你分压电阻总值太高,比如两个电阻加起来超过几十k,ADC采样时内部的采样电容来不及充满电,读数就会偏小。这是仿真里完全体现不出来的问题,实物调试时却能让你花掉一整天。

我的处理是分压网络后面加一个电压跟随器,用普通的单运放实现。跟随器的作用是“把采样网络和ADC隔离开”:电阻分压得到的源信号稳定在跟随器输入端,跟随器输出端的驱动能力足够给ADC采样电容充电。硬件上也就多一个芯片和几个去耦电容,但读数稳定性会明显改善。如果实在不想加运放,那就把分压电阻总值控制在10k以内,并且用软件多次采样取均值弥补。

2.3 电流采集:低边采样电阻加运放的小信号通路

电流检测有两条路:低边采样和高边采样。低边采样是把采样电阻放在负载和GND之间,通过测量电阻两端电压差算电流。高边采样是把采样电阻放在电源正端和负载之间,好处是不干扰公共地,很多电源系统必须这么干,但共模电压很高,需要专门的电流检测放大器,成本稍高。

我选低边,因为这项目是单电源正压供电,低边采样不会引入大范围共模问题,电路简单。

具体参数是:采样电阻取0.1欧姆,2A电流时两端压降只有0.2V,这么小的信号直接进ADC在12位分辨率下大概能分辨1到2mA,但受噪声影响抖动会比较大。所以一般会在采样电阻后面加一级同相放大电路,比如放大10倍,让满量程信号达到2V,把ADC的量程利用率提上来。

放大之后就要注意运放的供电范围了。常见运放在3.3V单电源下输出很难满摆幅,如果满量程算出来是2V以上,要把放大倍数往低调一点,别让输出顶到电源轨。还要在采样电阻两端和运放输入端做RC滤波,因为这个信号太小,很容易把电源纹波和走线噪声一起放大。

2.4 OLED接线、I2C时序与上拉电阻的细节

OLED这块很简单,但越简单越容易出事。SSD1306的I2C地址通常是0x3C或0x3D,我用的模块默认0x3C。接线时SCL接PB6、SDA接PB7,这是STM32F103系列上I2C1的标准引脚。

上拉电阻是个高频翻车点。很多开发板已经把I2C上拉电阻做在板上了,但OLED模块自己不带或者是断开的,如果你用的是杜邦线连接,线又比较长,那么总线信号上升沿会变缓,表现为时好时坏,偶尔花屏。稳妥做法是外接两个4.7k上拉电阻,SCL和SDA各一个,连接到3.3V。仿真里你基本感受不到这个问题,因为虚拟环境里信号是理想的。

中断式的I2C和模拟I2C的选择,我建议新手直接用软件模拟I2C,也就是用两个普通GPIO引脚手动翻转时钟和数据线。原因很简单:硬件I2C的外设配置多,容易卡在状态标志位上,软件模拟虽然占一点CPU时间,但时序完全可控,排查问题容易得多。一个0.96寸屏幕刷新一次大概几毫秒到十几毫秒,对这类检测报警器完全够用。

3. 程序框架:ADC采样、OLED刷新与报警判定如何不互相拖累

写程序之前先想清楚一个矛盾:ADC需要持续采集,OLED需要刷新显示,报警需要随时响应,如果全部挤在一个主循环里,任何一个环节耗时都会拖累另外两个。我把程序拆成几块来看。

3.1 先搭一个定时器驱动的调度外壳,而不是裸奔轮询

很多入门例程里的裸机写法是这样的:主循环里先读温度,再读电压,再刷新屏幕,然后从头再来。问题在于OLED刷新一屏如果用了十几毫秒,这期间报警判定的实时性就差了一截。当然这个项目对实时性要求没那么苛刻,但更合理的做法是让自己从一开始就养成“分时调度”的习惯。

我用的方法很朴素:一个定时器产生1ms时基,程序里维护两个计数器,每50ms触发一次采集任务,每100ms触发一次OLED刷新任务,每200ms触发一次报警判定任务。主循环只负责查标志位,谁时间到了就执行谁,任务内部尽量不阻塞。这样的好处是每个功能都有独立的执行节奏,某一路采集因为外部干扰慢了半拍,不会拖累屏幕刷新。

3.2 ADC多通道采样:用规则组加序列安排三路模拟量

STM32的ADC有个规则组通道概念,也就是说你可以把多个通道排成一个序列,然后连续转换。我实际用的是单次采样加程序轮询的写法:每次切换一个通道,启动ADC,等转换完成,读结果,再切下一个。

这样做的原因有两个:一是三路数据更新频率不一致,温度变化慢,电压和电流变化快,统一用扫描模式反而让每一路的更新频率被最慢的路拖着;二是扫描模式下触发DMA还得同时管理数据缓冲和通道顺序,调试起来比轮询烦。轮询主要缺点是比较浪费CPU,但在这个量级的采样频率下根本无所谓。

读取ADC结果之后,还有一个单位换算的细节。12位ADC的值范围是0到4095,它对应的输入电压是值乘以3.3除以4095。然后电压通道还要除以分压比还原出真实电压,电流通道要先除以放大倍数再除以采样电阻值还原出真实电流。这些系数建议写成宏定义,不要散落在代码里,否则过两天你自己都忘了哪个数是怎么来的。

// ADC读取单个通道的平均值,减小噪声 uint16_t read_adc_avg(ADC_TypeDef* adc, uint8_t ch, uint8_t times) { uint32_t sum = 0; for (uint8_t i = 0; i < times; i++) { adc->SEQSR &= ~(0x1F << 0); // 清空通道配置 adc->SEQSR |= (ch & 0x1F) << 0; // 选择通道 adc->CR2 |= (1 << 30); // 启动转换 while (!(adc->SR & (1 << 1))); // 等待EOC标志 sum += adc->DR; } return (uint16_t)(sum / times); } float get_voltage(void) { float adc_v = read_adc_avg(ADC1, 0, 8) * 3.3f / 4095.0f; return adc_v * VOLTAGE_DIVIDER_RATIO; // 还原分压前的真实电压 }

3.3 OLED显示驱动的关键:帧缓冲区与动态刷新

显示部分我建议屏幕驱动总是维护一块128x8字节的帧缓冲。SSD1306是一页一页发送数据的,一页八行像素,总共八页。每次更新时,先把要显示的内容画进帧缓冲,然后一次性把整块缓冲或者变化的部分发给屏幕。这样比逐字发送灵活得多,因为你可以在任何位置上画图、写字、组合数字。

字库方面,常用的中文字库需要自己取模,我会准备一套12x12或16x16的ASCII和少量汉字点阵。如果项目里要显示“温度”“电压”“电流”这几个汉字,程序一开始就把这些字符对应的点阵烧进去。报警状态时,可以在屏幕右上角画一个大大的实心圆点,或者把报警项的数字反白显示,比单纯靠文字更醒目。

刷屏策略上也有一个细节:不要每次刷新都清屏再重画,否则会闪。正确做法是只更新数值变化的部分,比如只把数字区域擦掉重写。看着好像没什么技术含量,实际体验差别很大。仿真里感觉不到闪烁,实物上频率一高就原形毕露。

3.4 报警状态机:迟滞、消抖与蜂鸣器驱动

报警逻辑我写成一个小状态机,每个监测项都有自己的状态:正常、预报警、报警。进入报警的条件是连续N次采样都越限,解除报警的条件是连续M次采样都回到迟滞区间以内。这里N和M的作用就是软件消抖。

蜂鸣器驱动一定要提醒一句:不要直接把蜂鸣器接在GPIO引脚上。普通的无源蜂鸣器工作电流动辄几十毫安,STM32引脚直接驱动会发热甚至烧引脚。正确接法是通过一个NPN三极管或MOS管去驱动,GPIO接基极,基极串联一个1k限流电阻,蜂鸣器接在电源和集电极之间,发射极直接到地。这个电路很简单,但很多人觉得“不就是一个蜂鸣器嘛”而省略,然后翻车。

报警响铃方式也值得设计一下:连续长鸣和间歇短鸣有完全不同的心理效果。我会让报警时以1Hz的频率响0.5秒停0.5秒,三个通道同时报警时用不同的节奏区分,这个体验做得好的话,整个产品感会提升一大截。

4. 仿真设计的实操细节:先把逻辑跑通,再动手焊板

这类型项目用仿真软件先行验证,是性价比最高的一条路线。板子还没焊,元器件还没买,我就能先把ADC采样的时序、OLED驱动代码、报警状态机的转移逻辑全部在虚拟环境里跑起来,确认C语言逻辑没有大问题后,再把同样的工程烧到实物上。这么做的最大好处是:硬件问题和软件问题分开排查。仿真跑不通,问题多半出在代码逻辑;仿真能跑但实物不行,再聚焦到硬件细节上。

4.1 仿真环境搭建与固件配置的常见门槛

仿真环境的搭建有几个门槛。第一,要正确选择单片机型号,不能选错封装或Flash大小;第二,要设置好晶振频率,因为这个值直接影响定时器延时、串口波特率、I2C时序,在仿真里如果频率设错,整个系统就变成“慢动作”。第三,程序烧录方式要选对,通常用HEX文件或直接加载编译好的可执行文件,我用的是把编译生成的HEX文件加载进MCU的方式。

把这些配置做完,程序能跑起来是最让人踏实的时刻。我第一次跑通时,OLED上温度显示稳定,电压随电位器旋动摇摆,电流数值跟随负载变化,那种整条链路全通的感觉比焊好板子还爽快。

4.2 元件模型差异:仿真能跑和实物能用的分界线

仿真里有个很大的陷阱,就是元件模型都是理想化的。举个例子,仿真里的NTC模型阻值和温度的关系非常标准,但实物的NTC每批次都有偏差,标称100k的可能实际只有97k,这会让显示温度整体偏移好几度。解决办法是实物上电后拿冰水和沸水做两点校准,回头修正程序里的分压比和偏移量。

蜂鸣器在仿真里通常就是一个小喇叭模型,点击它甚至能听到声音,但它的驱动电流特性完全模拟不出来,所以你在仿真里直接把蜂鸣器接在引脚上没事,实物上却会烧引脚。我在仿真里还是会按实物接法画三极管驱动电路,保持两边一致,避免“仿真能跑,实物烧引脚”的尴尬。

OLED在仿真里也经常有简化的显示模型,但I2C的时序检查是实打实的。用虚拟示波器挂到SCL和SDA上看波形,能发现很多代码层面的问题,比如时钟频率太高、数据建立时间不够、地址写错。这些发现拿到实物上同样有效。

4.3 用虚拟仪器验证三路采集的标定方法

仿真里最好用的调试手段是那些虚拟仪器。调节电位器相当于给电压通道一个可变的输入电压,我可以一边旋转电位器,一边观察OLED上的读数是否随电压值成线性变化。如果读到某个位置突然跳变到4095,说明输入超出ADC量程了,要回头检查分压比和放大倍数。

电流通道的标定相对麻烦一些,因为仿真里要有一个可调负载。可以用一个可变电阻负载配合电压源来充当,改变阻值就等于改变电流。这时OLED上的电流读数应该能跟随变化,同时计算出的功率值也应该符合欧姆定律,如果偏差太大,往往是换算系数写错了。

虚拟示波器也是标定路上必备的工具。我会同时观察NTC分压点的电压波形和ADC采样时刻的数据,确认采样没有发生在信号跳变的瞬间。这里有个小技巧:把采样放到定时器中断里去触发,避开主循环中OLED刷新等耗时操作,采样毛刺会明显减少。

5. 调试阶段的典型翻车现场与完整排查链路

这部分我拿遇到过的三个真实问题来复盘。每一个都能单独写一篇排错教程,这里串成一条线,讲清楚“症状—猜测—验证—修复”的完整流程。

5.1 OLED花屏、白屏与乱码的常规检查路径

OLED不亮或者花屏,头号嫌疑永远是I2C地址和接线,而不是代码。有人会反复翻驱动库,改初始化序列,折腾一晚上,最后发现只是模块地址是0x3D而程序里写的是0x3C。我的检查顺序很固定:先拿万用表量屏的VCC和GND是否有电,再查SCL和SDA是否确实接到了目标的两个引脚,然后用一个最简单的扫描程序全地址扫描一遍,看总线在哪个地址上有应答。这套流程五分钟就能定位九成问题。

花屏和乱码的另一个来源是字库取模方式不对。市面上取模软件多得很,有的支持横向取模有的支持纵向取模,如果取模方式和驱动里用的显示函数不匹配,就会看到字符颠倒、错位、残缺。我的建议是初始化阶段先显示一个全屏的测试图形,比如画满斜线或者矩形,验证底层画点函数正确,再往上叠加字库和数字显示。底层对了,上层才谈得上排查。

还有一类特殊问题只出现在实物上:杜邦线太长或者接触不良,导致I2C应答不稳定,屏幕偶尔亮偶尔灭。这让人抓狂的地方在于,你换一个变量可能就好了,过一会儿又复现。遇到这种情况别急着改代码,先把线重新插一遍,最好换成短线或者直接焊接。

5.2 ADC读数漂移、跳变与换算系数的误差来源

实测中发现电压显示比万用表测出来偏大0.3V,这是非常典型的问题,不是ADC坏了,而是参考电压和换算系数不对。

STM32的内部参考电压通常在2.9到3.3V之间,不是一个精确的固定值,所以直接用3.3V参与换算本身就有误差。精度要求高的话,需要先通过内部参考电压通道来标定VREFINT,反推实际工作电压,再用这个电压值做换算系数。实作上可以先用万用表测一下板子的3.3V实际值,把这个值写进代码里做宏定义,能立刻减少一部分误差。

接着看驱动能力问题。分压电阻阻值如果太大,采样电容充电不足,读数会系统性偏低;如果分压比算错,读数会整体偏大。要把误差拆开,方法很简单:给一个已知电压,比如从电源引一个5V过来,看程序显示多少,反推误差比例。如果比例恰好接近分压比的倒数,那基本就是换算系数问题;如果随输入电压不同而变化,那就是采样电容或阻抗问题。

最后不要忽略“接地”这个隐形因素。STM32的ADC以GND为参考,如果板上数字地和模拟地靠得太近,开OLED、驱动蜂鸣器时地电位会被扰动,ADC读数就会出现和这些动作同步的跳变。仿真里GND永远干净,实物上却要用星型接地把模拟部分的地单独汇集。

5.3 小信号放大通道的噪声与偏置排查

电流通道的采样电阻压降只有零点几伏,放大10倍后又叠加了运放自身的偏置电压失调。普通运放的输入失调电压可能有几毫伏,反算到电流上就是几十毫安的零点漂移。表现就是电流显示不为零,即使负载断开了屏幕上仍显示几十毫安。

排查这一路的思路和电压通道不一样,因为它不是线性缩放误差,而是叠加了固定偏置。我会先断开采样电阻交流信号,只放大电阻本身的直流压降,看输出有多少固定的偏移量,然后在程序里减掉这个偏移。更彻底的办法是改用专用电流检测运放,这类芯片内部做了零点校准,漂移可以做到非常小,缺点是成本上去了。

另一个常见问题是运放输出端没有加滤波电容,小信号放大后带着高频毛刺进ADC,读数跳得厉害。我在运放输出和ADC引脚之间加一个100到470纳法的电容配合一个几百欧的限流电阻,组成RC低通滤波,把带宽压到采样频率以下,效果立竿见影。

5.4 一个实测案例的完整排查链条:从电压偏大0.3V开始

有一次给A同学调试一套类似的检测报警器,现象是电压显示24.3V,万用表量实际输入24.0V,偏大0.3V。这个误差看起来不大,但对于监测设备来说,用户会非常在意。

我先让它测量一个5V标准源,显示5.08V,误差比例约1.6%,这说明不是固定偏移而是比例误差。比例误差的常规嫌疑就是参考电压和分压比。我们先量了板子上的3.3V,实际是3.36V,而代码里用的是3.30V,这一步就产生了约1.8%的正偏差,和现象吻合。

把换算系数改成实际的3.36V之后,5V输入显示4.97V,误差又变成了负的。这说明分压比也需要修正。我们用万用表量出总电阻和采样电阻的实际阻值,发现标称值偏差约1%,把宏定义里的比例系数改成实测值后,24V输入显示24.02V,误差控制在正负0.1V以内。整个过程没有改过一行逻辑代码,全是系数校准。这就是为什么我坚持把所有换算系数放在宏定义或配置表里,校准的时候只需要改一个数,不需要翻代码。

6. 这轮做完之后的扩展方向和我的一点实话

如果你顺着上面这套流程把项目跑通了,那你手里已经有了一套相当完整的小型检测系统基础。剩下的扩展方向我挑两个说,都是我实际做过或者见过别人做得好的。

第一是本地存储和远程报警。OLED显示毕竟是本地交互,如果设备装在不方便靠近的地方,就需要把三路数据写入EEPROM或Flash,留一份历史记录;同时可以加一个无线模块,把数据推送到手机或上位机,阈值越限时发一条远程报警消息。这个扩展的价值在于:检测报警器真正重要的不是“当时显示出来”,而是“事后能查”和“异地能知”。

第二是低功耗化。如果这套设备将来要用电池供电,OLED不能一直亮着,可以按需点亮十几秒后自动熄屏;ADC采样频率可以降到一分钟一次,其他时间让MCU进入休眠;报警检测保持一个极低频率的外部唤醒。这个方向做完,整个项目的实用场景就从实验室工作台延伸到了便携巡检设备。

最后说点实在话。温度电压电流检测报警器本身不算什么前沿项目,但它把“模拟前端设计——数字采集转换——人机交互——异常处理”这条嵌入式最核心的链路完整串了一遍,而且出错点集中在几个非常典型的环节:分压阻抗匹配、参考电压校准、小信号噪声、报警消抖、OLED接线。如果你自己从零做一遍,并且不跳过任何一个排查过程,那你会比单纯看十篇教程都更清楚自己的STM32到底是怎么把一块电压变成一串你能看懂的字符的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进