做嵌入式这几年,体脂秤算是我觉得最适合练手又最容易出成果的项目之一。它麻雀虽小五脏俱全:模拟信号采集、高精度ADC、滤波算法、低功耗管理、人机交互,甚至连物联网扩展的坑都能提前踩一遍。最近我把自己的STM32智能体重秤项目完整开源了,代码、原理图、Proteus仿真文件一套齐全,从零开始讲清楚每一个环节怎么设计、为什么这么设计。这篇文章就是整个项目的复盘总结,适合正在学STM32的同学、准备做称重类产品的开发者,以及想了解传感器信号链怎么搭的硬件爱好者——跟着走一遍,你也能复现一台能测体重、算BMI、带低功耗的智能体脂秤。
1. 项目整体设计与方案选型
1.1 智能体脂秤的核心需求拆解
很多新手拿到体脂秤项目,第一反应是“先找个秤拆了抄电路”。我建议反过来,先静下来列需求清单。我自己做项目习惯把所有功能分成“必须项”和“加分项”,这个习惯帮我避免了很多无效返工。
必须项包括:称重精度至少0.1kg,重复性要好(同一重量多次测量偏差不能太大)、支持去皮功能、能锁定稳定读数、具备自动关机能力。加分项包括:BMI自动计算、历史数据记录、蓝牙上报手机App、多用户识别。我最后做主控方案时,把蓝牙和App全部砍掉了,先把本地功能做到位。原因很简单——蓝牙牵扯到协议栈、App联调、功耗优化,这些任何一个环节都能拖垮整个项目进度。先把称重这件事做扎实,扩展功能都是后面水到渠成的事。
做完需求拆解后,我顺便算了一笔账:如果精度要求0.1kg,人体按最大150kg算,那么满量程需要1500个有效分辨率点。表面上任何12位ADC都能达到4096个码值,但这只是理论分辨率,实际还要考虑噪声、温漂、机械形变,有效精度通常要打个对折再对折。这个认知直接影响了我后续的芯片选型,也让我明白为什么廉价电子秤里清一色都是专用ADC芯片。
1.2 主控选型与系统架构
主控我用的是STM32F103C8T6,这颗芯片在圈内俗称“C8T6”,算得上开源项目的万金油型号。选它没有太多花哨理由:价格便宜、资料铺天盖地、库函数和寄存器开发都有现成参考,Cortex-M3内核跑在72MHz,对付体脂秤这种低频采集场景绰绰有余。即使换成国产GD32或者其它兼容芯片,代码基本不用大改。
系统架构我划分成四个模块:传感器模块负责把重量转化为电信号,核心是电阻应变片式称重传感器配合HX711高精度ADC;主控模块负责数据读取、滤波处理、逻辑控制;显示模块用0.96寸I2C接口的OLED屏,体积小、功耗低,而且驱动代码非常成熟;电源模块用3.7V锂电池供电,配合低功耗控制实现待机自动休眠。
模块划分有个好处:调试时可以单独验证每个部分。比如我先用开发板单独跑通HX711读取,确认传感器输出正常,再接入OLED显示,最后才整合到一起。如果一上来就把所有东西焊在一起,出问题你根本不知道是传感器坏了、线接错了还是代码写错了。
1.3 为什么用HX711而不是STM32内部ADC
这个问题几乎每个看过项目的人都会问。STM32F103自带12位ADC,看起来好像也能用,但实际做出来你会发现精度惨不忍睹。原因要从传感器信号说起:称重传感器的满量程输出通常只有几毫伏到十几毫伏,而12位ADC在3.3V参考电压下一个LSB对应大约0.8mV,根本分辨不了这么微弱的信号变化。即使把STM32的内部放大器打开,噪声和失调电压也会让数据乱跳,更别说做到0.1kg的稳定分辨率。
HX711是专门为衡器设计的24位Σ-Δ型ADC,内部集成了可编程增益放大器,增益支持32、64、128三档,能把传感器发出的微弱差分信号放大到适合采样的范围。我用的是128倍增益档位,配合24位量化精度,理论分辨率能到微伏级别。做个生活化类比:STM32内部ADC相当于用家用体重秤去称一颗芝麻,HX711相当于实验室分析天平,精度完全不在一个维度。
选择专用芯片的代价是通信接口需要额外驱动。HX711只有两根信号线(DOUT数据输出、SCLK时钟输入),时序是典型的SPI变种,用GPIO模拟即可实现,完全不占用硬件外设。这个“用GPIO模拟低速协议”的思路在嵌入式开发里非常实用,我后面写驱动代码时就是基于这个逻辑实现的。
2. 硬件电路设计:称重传感器信号链拆解
2.1 电阻应变片式称重传感器工作原理
要理解整个硬件设计,必须先弄懂传感器内部发生了什么。市面上一两百公斤量程的单点式称重传感器,内部结构是四片电阻应变片贴在一个铝合金弹性体上,接成惠斯通全桥电路。当传感器受压时,弹性体发生微小形变,其中两个相对桥臂的应变片被拉伸、电阻增加,另外两个被压缩、电阻减小,电桥失去平衡,在输出端产生一个与受力成正比的差分电压。
这就是经典的“应变效应”。关键点在于:传感器不是直接输出电压值,而是输出一个与激励电压成正比的差分信号。激励电压越高,输出幅度越大,但功耗和发热也越大。HX711内部产生的稳压电源正好可以作为传感器的激励源,AVDD输出经过RC滤波后接到传感器的E+和E-,传感器的S+和S-再返回HX711的差分输入通道。这样整个信号链在同一个芯片内部闭环,避免了外部电源纹波干扰。
实际操作中我发现,传感器的机械安装比电路更影响精度。传感器必须固定在平整的底板上,受力点要落在传感器的承力柱中心,四角垫脚要对称。曾经有块板子称重时数据忽大忽小,排查半天发现是传感器固定螺丝松动,重新紧固后数据立刻稳定。这个经验让我记住了:高精度测量系统的稳定性,机械结构往往比电路更关键。
2.2 HX711外围电路与PCB布局要点
HX711的外围电路其实非常简单,但有几个电容的位置非常讲究。芯片手册里推荐的电路是这样的:AVDD经过一个10Ω电阻后接100uF电解电容和0.1uF瓷片电容到GND,这两个电容给传感器激励提供稳定的电源;基准参考电容VFB和VBG之间接一个0.1uF电容,它决定了ADC参考电压的稳定度;电源引脚VSUP上的去耦电容尽量靠近芯片放置。
我在第一版PCB上踩过一个坑:为了布线方便把去耦电容放得离芯片很远,结果读出来的数据带有明显的周期性波动。后来查资料才明白,Σ-Δ型ADC内部有开关电容网络,会在电源线上产生瞬态电流尖峰,去耦电容必须紧贴电源引脚才能有效滤除。改板时把电容挪到芯片引脚旁边,数据一下就干净了。这个教训让我明白,对于高精度ADC来说,布局不是“差不多就行”,而是“差一点都不行”。
PCB布局还需要注意模拟地和数字地的处理。HX711芯片本身区分了AGND和DGND,我采用的是单点接地策略:底层整片覆铜为数字地,AGND通过一个0Ω电阻在芯片附近单点连接到数字地,避免数字开关噪声通过地回路串入模拟信号通道。时钟线和数据线尽量远离模拟输入走线,必要的时候加地线屏蔽。虽然体脂秤不是高频设备,但这些细节决定了最终测量的稳定性和一致性。
2.3 电源方案与低功耗设计思路
体脂秤是典型的小型便携设备,电源设计直接决定用户体验。我的方案是3.7V锂电池供电,经过一颗低功耗LDO稳压到3.3V给STM32和OLED供电,HX711的模拟电源由芯片内部稳压产生。整个系统的功耗大头有三个:OLED屏、MCU运行电流、传感器激励电流。
低功耗的关键在于让设备“没事就睡觉”。我设计的策略是:正常称重时全速运行,显示屏亮起;称重结束并锁定读数后,延时10秒自动熄屏;再过5秒,STM32进入STOP模式,同时控制MOS管切断OLED和HX711的电源,整个待机电流能压到微安级别。唤醒方式用按键外部中断,按键按下瞬间同时打开电源通路,MCU从STOP模式唤醒后重新初始化外设。
这个设计里有个容易忽略的点:HX711上电后需要一段时间稳定,所以唤醒后不能立即读数据。我在代码里做了上电延时200ms,等ADC稳定后再开始采样。刚开始做低功耗时没注意这个时序,按键唤醒后第一帧重量数据总是偏大,后来加上延时才解决。便携设备的电源管理,本质上就是在“省电”和“响应速度”之间找平衡,这个平衡点的选择需要实测调整,不能只看芯片手册的理论值。
3. 软件实现:从原始数值到人体体征数据
3.1 HX711驱动代码解析
HX711的时序协议非常简洁,核心就是24个SCLK脉冲把转换结果从DOUT引脚一位一位移出来。我直接基于标准库写了一个轻量驱动,核心逻辑只有几十行。
// HX711_Read: 读取一次24位转换结果 // 返回带符号的原始AD值,范围约 -8388608 ~ 8388607 int32_t HX711_Read(void) { int32_t value = 0; uint8_t i; HX711_DOUT_HIGH(); // 等待DOUT从高电平变为低电平,表示数据就绪 while (HX711_DOUT_READ()); // 超时保护在完整代码中实现 for (i = 0; i < 24; i++) { HX711_SCLK_HIGH(); // 时钟上升沿移出1位 delay_us(1); HX711_SCLK_LOW(); delay_us(1); value = (value << 1) | HX711_DOUT_READ(); } // 第25个脉冲用于设置增益,这里选择下一次转换使用128增益 HX711_SCLK_HIGH(); delay_us(1); HX711_SCLK_LOW(); delay_us(1); // 输入范围为差分输入,最高位为符号位需要做符号扩展 if (value & 0x800000) { value |= 0xFF000000; } return value; }这里重点解释几个细节。DOUT从高电平跳变到低电平表示一次转换完成,读取数据前必须等待这个下降沿,否则读到的可能是上一帧的旧数据。24位数据以二进制补码形式输出,最高位是符号位,所以要做符号扩展才能得到正确的负数。第25个脉冲的作用是设置下一轮转换的增益,默认拉高再拉低就对应128倍增益,千万不能省。
为了确保数据稳定,我在读取前增加了一个“数据就绪超时”判断。正常工作时HX711的转换周期约100ms左右,如果DOUT超过一定时间没有拉低,说明芯片可能没有上电或者通信异常,这时候应该主动报错而不是死循环等待。这个超时保护让我在调试中省了大量时间,尤其是传感器线缆接触不良时,程序不会卡死,而是会打印错误信息方便定位。
3.2 滤波算法:连续采样与滑动均值
HX711的原始数据虽然精度高,但直接拿来显示会发现最后一位数字一直在跳。原因很复杂:有电源噪声、有传感器机械振动、有ADC本身的量化噪声。这时候就需要滤波。我采用的策略是“连续采样+去极值滑动平均”,一共分三步。
第一步,连续采样N次,我取N=20。每次采样间隔约100ms,对应HX711的转换节奏。第二步,对这20个数据排序,去掉最大和最小的各2个值,把剩余16个数取平均。这一步能有效剔除偶然的尖峰脉冲,比如不小心碰了一下秤体产生的冲击信号。第三步,对平均值做一次一阶低通滤波:filtered = (filtered * 7 + new_average * 3) / 10,让显示数值平滑过渡,避免数字突然跳变。
// 滑动均值滤波:输入新采样值,输出滤波后的重量 // 内部维护一个环形缓冲区 int32_t Weight_Filter(int32_t new_sample) { static int32_t buffer[FILTER_NUM]; static uint8_t index = 0; static uint8_t count = 0; int32_t sum = 0, min = 0, max = 0; uint8_t i; buffer[index] = new_sample; index = (index + 1) % FILTER_NUM; if (count < FILTER_NUM) count++; // 先排序取中间值(这里用最简选择法,工程上可换成快速排序) // 实际代码中对 buffer 拷贝后排序,剔掉头尾各2个极值后求平均 sum = 0; for (i = 2; i < FILTER_NUM - 2; i++) sum += sorted_buffer[i]; return sum / (FILTER_NUM - 4); }这里需要说明的是:滤波不是越强越好。滤波太狠,称重反应会变得迟钝,人站上去要等好几秒才能稳定;滤波太弱,数字跳得厉害。我实测下来20个点去掉4个极值再平均,配合一阶低通的参数,能在“响应速度”和“稳定显示”之间取得不错的平衡。具体参数要根据传感器的实际噪声水平调整,不能照抄别人的值。
3.3 体重标定流程与参数存储
传感器输出的AD值和实际重量之间是线性关系,但比例系数(斜率)和零偏(截距)每台设备都不同,必须逐台标定。我的标定方案是经典的两点法:先在空载状态下读取零偏值,然后放一个标准砝码(我用的是10kg砝码)读取满载值,两点确定一条直线,后续任意AD值都能反算成重量。
标定公式非常简单:weight = (ad_value - zero_offset) / scale_factor,其中scale_factor = (ad_full - zero_offset) / 10,表示每公斤重量对应的AD值。这里有个精度技巧:为了保留小数运算,我把重量单位定义为克(g),使用浮点数计算,显示时再除以10转换成公斤。浮点运算在STM32上虽然比整数慢一点,但对体脂秤这种低频设备毫无影响,换来的是代码清晰度高。
标定参数必须掉电保存。我存储在STM32内部Flash的最后几页,利用标准库的Flash读写接口实现。这里有一个经验:写Flash前必须先擦除整个扇区,而且Flash有擦写寿命限制,所以不能频繁写入。我的策略是只有在完成标定时才写一次,平时运行过程中完全不写Flash。体脂秤如果每次关机都存参数,Flash寿命很容易被耗尽。
// 标定参数存储结构体,保证字节对齐 typedef struct { uint32_t magic; // 校验魔数,判断存储是否有效 int32_t zero_offset; // 空载AD值 float scale_factor; // 单位重量对应的AD值 } CalibParam_t; // 保存前先判断数据是否变化,避免无谓的擦写 void Calib_Save(CalibParam_t *param) { param->magic = CALIB_MAGIC; FLASH_Unlock(); FLASH_ErasePage(CALIB_FLASH_ADDR); // 按32位字写入,注意缓存一致性 FLASH_ProgramWord(CALIB_FLASH_ADDR, *(uint32_t*)param); // ... 依次写入全部参数 FLASH_Lock(); }3.4 OLED显示与交互状态机
软件层面的最后一个核心模块是界面逻辑。我没有用简单的顺序执行,而是用一个状态机管理整个称重流程,状态包括:待机态、称重态、稳定态、锁存态、关机态。状态机的好处是逻辑清晰,不会出现“称到一半突然显示上一轮结果”这种混乱情况。
称重态下的判断逻辑是这样的:人站上秤后,系统连续采集重量值,如果连续1秒内重量变化小于0.1kg,就认为数值稳定,进入锁存态。锁存状态下把当前重量定格显示,同时计算出BMI指数。BMI的计算需要身高参数,我在首次开机时让用户通过按键输入身高,存到Flash里,之后每次称重自动计算并显示。这个交互虽然简单,但完整覆盖了“输入-处理-显示-存储”的嵌入式开发闭环。
OLED驱动我用的是标准的SSD1306方案,底层移植开源驱动,上层自己封装了几个界面:称重界面显示实时重量、稳定界面显示最终体重和BMI、设置界面显示身高输入。整个显示刷新放在主循环里,HX711数据读取放在定时器中断外,采用轮询方式,避免中断嵌套带来的数据竞争。这里我的体会是:嵌入式裸机编程中,能用轮询解决的就别用中断,中断用多了调试噩梦就开始了。
4. 原理图阅读与Proteus仿真验证
4.1 原理图关键节点解析
开源包里的原理图是完整的,这里挑几个关键节点帮大家看图。电源入口:电池正极经过一个PMOS管作为电源总开关,控制信号来自MCU的GPIO和按键唤醒电路。这个PMOS用法很典型,导通时源极和漏极电压差接近0,压降小,待机时彻底切断后端负载。
HX711部分的连接需要注意:传感器E+和E-接AVDD经过RC滤波后的节点,S+和S-接芯片的差分输入。很多新手把S+和S-接反了,结果是读数为负。电路上即使接反也不会烧东西,只需要在软件里把符号取反即可,但最好还是按照丝印接正确。模拟输入通道A通道(通道A正负)用于体重秤,通道B是备用通道,接一个参考电阻可以做校准,不过本项目用不到。
还有一个容易被忽略的节点是OLED的I2C地址引脚。SSD1306屏的地址由SA0引脚电平决定,默认是0x78(写地址)和0x7A(读地址)。如果发现屏幕不亮,第一排查项就是I2C地址是否匹配。原理图里我把SA0接地固定为0x78,软件里就直接写死,省去地址扫描。
4.2 Proteus仿真环境搭建
仿真文件我用的Proteus 8版本。搭建仿真环境时,有几个关键点要注意。第一,STM32F103C8T6的仿真模型需要把固件编译成HEX文件加载到芯片里。我用的是Keil MDK配合ARM Compiler,在工程配置里把编译输出格式设为HEX,然后到Proteus里双击MCU元件,加载对应的HEX文件。
第二,Proteus里的HX711元件需要调出对应的模型,连接到虚拟传感器上。Proteus提供了一个叫“应变片传感器”的模拟元件,可以用电位器模拟传感器输出变化,从而验证ADC采集和显示的联动效果。具体操作是:在元件库里搜“HX711”和“LOAD CELL”,放置后连线,运行仿真,然后调整模拟传感器的阻值参数,观察OLED虚拟屏上的重量数字是否按比例变化。
第三,虚拟OLED在Proteus中需要加载SSD1306的液晶模板。如果仿真时显示花屏,大概率是I2C时序太快导致虚拟设备跟不上,这时候可以把I2C通信加上延时,或者在仿真时降低主频。这一点和实际硬件刚好相反,实物上I2C跑400kHz没问题,仿真里反而要降速,属于Proteus仿真特有的坑。
4.3 仿真与实物的差异处理
仿真最大的价值在于验证“逻辑”而不是“物理”。比如你可以用仿真确认滤波算法的流程是否正确、状态机的状态跳转是否符合预期、按键中断能否正确唤醒系统,这些纯逻辑问题在仿真里调试成本远低于实物。
但仿真不能验证的东西更多:模拟噪声、电源纹波、PCB布局的影响、传感器的机械公差,这些在Proteus里统统不存在。所以我的习惯是:先用仿真把功能逻辑跑通,画好原理图和PCB,打样回来后把实物调通,再回到仿真里补充注释说明哪些模块在实物上需要特别关注。两个环境交叉验证,才能确保开源出来的代码和原理图经得起别人复现。
还有一个很实际的建议:仿真文件里的晶振配置和代码里的时钟配置必须一致。我在仿真时发现一个奇怪现象,程序运行一会儿就复位,排查后发现工程里默认选择了8MHz外部晶振,但仿真模型里的晶振设置成了内部RC振荡器,时钟不匹配导致HX711时序错乱。这类问题在实物上也会出现,所以无论仿真还是实物,第一步永远是确认系统时钟正确。
5. 常见问题与调试实录
5.1 重量数据跳变与漂移
这是体脂秤项目里出现频率最高的问题。数据跳变通常是几个原因叠加造成的:供电不稳定、传感器激励噪声大、滤波参数不合适、机械安装松动。我的排查顺序是:先用万用表量传感器的激励电压是否稳定,再看HX711的电源脚纹波,如果都正常,就用串口把原始AD值打出来,人为施加固定压力,观察数据是否依然跳动。
如果原始AD值稳定但滤波后依然跳,那就是滤波参数的问题;如果原始AD值本身就在跳,优先怀疑硬件。我遇到过一次非常隐蔽的干扰:手机放在秤旁边时数据明显波动,检查发现是传感器输入线太长并且没有屏蔽,成了天线耦合了射频干扰。解决办法是把传感器线改成屏蔽线,屏蔽层单端接地,问题立刻消失。这个案例再次说明:信号链上的任何一段都是噪声入口,线缆长度、屏蔽、接地每一步都要认真对待。
5.2 HX711读数一直为0或满量程
HX711读数为0,常见原因是DOUT引脚一直为高电平,说明芯片没有进入数据就绪状态。先检查芯片供电是否正常,再用示波器或逻辑分析仪看SCLK是否有正常脉冲。还有一个经典错误:DOUT和SCLK两根线接反了,导致时序完全错乱。我调试时习惯把这两根线定义为宏,万一接反,只需修改宏定义而不需要重新改线。
读数为满量程(正值或负值溢出)则通常意味着差分输入信号超出范围,常见于传感器E+和S+之间接短路、传感器信号线接触不良、或者增益设置错误。我遇到过把通道选择搞错的案例:代码里发送了通道B的脉冲数(第25个脉冲的时序不同),结果采样到的信号通道不对,读数完全无法解释。排查方法很简单,重新阅读HX711手册关于第25到27个脉冲的设置说明,确认增益和通道匹配即可。
5.3 标定数据掉电丢失
标定数据存储失败是很多复现者会遇到的坑。常见原因包括:Flash扇区地址选错,写到了代码区域导致程序跑飞;擦写之前没有调用解锁函数;写完之后没有及时锁定;以及结构体含有字节对齐填充,按字节写入和按字读取不一致导致校验失败。我在开源代码里已经规避了这些问题,但如果你自己修改存储结构,务必要注意对齐和缓冲策略。
另一个容易忽略的点是系统复位后Flash里的数据是上电默认值,如果魔数校验失败,程序应该进入“重新标定模式”而不是用默认参数继续工作。我默认在出厂固件里设置了一个无效魔数,用户第一次上电会看到“CAL”提示,需要先完成标定才能正常使用。这个设计虽然多了一步操作,但避免了用错误参数称重的严重后果。
5.4 仿真卡死的几个原因
仿真环境下的问题主要集中在HEX文件没有正确加载、晶振配置不匹配、以及虚拟设备时序跟不上。Proteus中点击MCU元件的“Program File”属性加载HEX后,记得确认路径没有中文名和空格,否则加载会失败但不会给出明确报错。
还有一个非常容易犯的错误:仿真里用了多个虚拟终端(Virtual Terminal)同时打印调试信息,导致仿真速度变得极慢,看起来像“卡死”。我后来只在需要调试的模块上挂终端,并且减少打印频率,仿真速度就恢复正常了。仿真毕竟是软件模拟,运行速度受模型复杂度影响很大,遇到明显变慢先排查调试输出,再排查时钟配置,基本都能解决。
6. 项目扩展方向与二次开发建议
这个项目开源后,不少朋友问我下一步该加什么功能。如果硬件不变,纯软件层面可以做:历史数据记录到Flash(需要做磨损均衡,比如按页循环写入)、多人识别(根据体重区间和称重时间推断使用者)、通过按键输入身体活动水平估算体脂率(虽然精度有限,但能演示公式标定的完整流程)。
如果愿意动硬件,推荐两个方向:一是加蓝牙模块,把重量和BMI数据上传到手机App,这需要引入串口通信协议设计和低功耗蓝牙的配对逻辑,工作量直接翻倍但收获也翻倍;二是换用带RTC的芯片方案,实现每日定时提醒测量,这就进入了物联网健康管理设备的范畴。无论走哪个方向,现有代码里的滤波算法、标定流程、状态机框架都能直接复用,这也是我当初把软件分层写的原因——底层驱动和业务逻辑解耦,扩展功能不会牵一发动全身。
我个人在实际操作中的体会是:做这类开源项目,最大的收获不是“作品完成了”,而是“过程可复现”。当我重新整理代码、补全注释、绘制原理图的时候,才发现当初很多“差不多能用”的代码其实经不起推敲。比如HX711驱动的超时保护,一开始根本没有,是后来调试中踩了坑才补上的;滤波参数也是实测了好几组数据才确定下来。如果让我给后来者一个建议:拿到这套代码后,不要急着改功能,先原样搭建起来跑一遍,用串口把所有中间变量打出来看,理解每个参数背后的物理含义,然后再动手修改。这个“理解之后再创造”的过程,比任何代码本身都更有价值。