基于STM32与HX711的智能体重秤设计:从传感器标定到滤波算法
1. 项目概览与硬件出发点智能体重秤这个项目我在不同阶段接触过好几个版本从最初的纯硬件方案到后面带蓝牙、带APP的完整系统都试过。这次分享的是一个比较经典的组合STM32F103C8T6 HX711称重模块 电阻应变式压力传感器 OLED显示屏。代码、原理图、Proteus仿真三件套齐全非常适合做课程设计、毕业设计或者刚入门嵌入式想找个完整项目练手的朋友。先说这个方案的选型逻辑。称重行业里最常见的传感器是电阻应变式压力传感器内部是四臂惠斯通电桥结构输出的是毫伏级别的差分电压信号。STM32自带的ADC虽然是12位但直接用单片机ADC去采这种微弱信号精度和稳定性都撑不住。所以中间必须加一级高精度ADC前端HX711就是干这个活的——它集成了24位Sigma-Delta ADC、可编程增益放大器PGA和内参基准源配合专用的串行通信协议只需要两根IO口就能完成数据读取这在整个方案里是最省心的一环。MCU选STM32F103C8T6不是因为它的ADC有多强而是因为外设丰富、例程多、价格便宜稳定跑HX711驱动和OLED显示绰绰有余学习成本也低。这套方案能解决的问题很明确一是用专业称重芯片替代裸ADC采集保证精度和长期稳定性二是代码架构拆得比较清楚HX711驱动、滤波算法、显示逻辑各自独立方便移植和扩展三是提供了Proteus仿真文件没有实物也能先把整个逻辑跑通。如果你手里暂时没有传感器和开发板完全可以先在仿真环境里把数据通路捋顺再回头做实物效率会高很多。2. 硬件电路设计的几个关键点2.1 传感器与HX711的信号链路传感器部分推荐选量程5kg到10kg的单点式平行梁传感器四根线分别是红E、黑E-、绿S、白S-。接线时务必让E/S接到HX711模块的对应端子上E-/S-同理接反了数据全反读出来的重量是负的排查起来很容易懵。HX711模块一般有两种供电模式一种是从MCU的3.3V取电另一种是独立AVCC供电。这里我建议用独立供电或者至少单独走线不要和数字电路混在一起。称重传感器的信号变化非常微小满量程输出通常只有10mV左右如果电源纹波大这些噪声会直接耦合进测量结果。实际做下来电源是否干净对读数稳定性的影响甚至比滤波算法还大。HX711增益方面模块上的通道A默认增益128x配合5kg传感器完全够用。芯片内部还有个通道B增益固定32x一般接第二路传感器时才用这个项目用不上可以不管。数据输出速率默认10SPS也就是每秒10次转换HX711_Rate引脚上拉到高电平可以切到80SPS。我建议保持默认10SPS采样率低一点换来的是更稳定的读数对体重秤这种场景响应速度不是瓶颈。注意HX711模块上的供电电压和逻辑电平要匹配。如果你用5V单片机模块可以接5V供电如果用3.3V的STM32模块最好也从3.3V取电避免电平不匹配导致通信错误。2.2 OLED显示与按键电路显示部分我用的是0.96寸I2C接口OLEDSSD1306驱动芯片分辨率128x64。I2C方式只占用SCL和SDA两根线连线简洁适合新手焊接和排查。SCL接PB6SDA接PB7这是STM32F103的I2C1默认引脚固件里直接使用硬件I2C代码量会少很多。不过硬件I2C在STM32F103上偶尔会碰上总线锁死问题尤其是和某些外设握手时。如果你是用标准库开发建议在初始化前做一次软件复位GPIO的保险动作或者干脆升级到CubeMXHAL库HAL库对I2C的时序处理更完善锁死概率低很多。这里没有对错只有熟练度的区别顺手哪个用哪个。按键就一颗接PA0配置成下拉输入用于触发去皮功能和进入校准模式。按键电路加个10k下拉电阻不要依赖STM32内部下拉内部下拉的阻值偏大在潮湿环境或者线缆较长时容易误触发。2.3 电源方案电池供电的话最常见的是两节18650串联供电经过AMS1117-3.3稳压给MCU和OLEDHX711直接从3.3V取电。如果用的是3.7V单节锂电池那稳压到3.3V之后压差只有0.4VAMS1117这种LDO压差不够输出会不稳这时候要换用XC6206或RT9193这类低压差LDO。电源设计上有个容易被忽略的点HX711模块的参考电压和激励电压共用。如果LDO输出纹波大传感器的激励电压本身就是波动的直接导致测量结果跟着波动。所以电源滤波电容要舍得加LDO前后各放一个100uF电解电容加104瓷片电容HX711的AVCC引脚附近再单独放一个10uF钽电容实测下来读数稳定性明显改善。3. 软件架构与模块实现3.1 HX711驱动如何编写HX711的通信协议相当简洁本质就是SPI的一个变体DOUT引脚输出数据SCK引脚由MCU控制产生时钟脉冲。读数据时先把SCK拉低并等待DOUT变为低电平表示转换完成然后连续产生24个时钟脉冲在脉冲的上升沿依次读取DOUT电平得到24位二进制数据。下面这段是我在项目中实测稳定运行的驱动代码// HX711 引脚定义 #define HX711_SCK_PIN GPIO_PIN_12 #define HX711_SCK_PORT GPIOB #define HX711_DOUT_PIN GPIO_PIN_13 #define HX711_DOUT_PORT GPIOB #define HX711_SCK_HIGH() HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_SET) #define HX711_SCK_LOW() HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_RESET) #define HX711_DOUT_READ() HAL_GPIO_ReadPin(HX711_DOUT_PORT, HX711_DOUT_PIN) uint32_t HX711_ReadRaw(void) { uint32_t data 0; uint8_t i; // 等待DOUT变低表示ADC转换完成 while (HX711_DOUT_READ() GPIO_PIN_SET); // 读取24位数据MSB在前 for (i 0; i 24; i) { HX711_SCK_HIGH(); delay_us(5); HX711_SCK_LOW(); delay_us(5); data (data 1) | HX711_DOUT_READ(); } // 第25个脉冲设置增益为128并让芯片进入下一次转换 HX711_SCK_HIGH(); delay_us(5); HX711_SCK_LOW(); return data; }这段代码里有两个细节值得说。第一个是while (HX711_DOUT_READ() GPIO_PIN_SET);这个等待循环如果DOUT一直没有变低程序会卡死在这里。正常运行时不会出问题但一旦传感器断线、模块损坏程序就会死在等待中。所以更稳妥的做法是加一个超时保护比如等待100ms后直接返回上一次的读数这样系统即使在外设异常时也能继续运行不至于整个流程卡死。第二个是第25个脉冲它决定了下次转换的增益这里保持128x不变如果误写了脉冲数会导致后续读数的换算比例全部错乱。3.2 从ADC原始值到真实重量HX711输出的原始值是带符号的24位数据范围大约是-8388608到8388607。但这个值和重量之间没有一个统一的绝对换算关系因为每个传感器的灵敏度、电桥的不平衡程度都不一样。所以必须要做一件事标定。标定公式其实就一行weight (raw_value - offset_value) / scale_factor其中offset_value是空载时的ADC原始值也叫零点值scale_factor是单位重量对应的ADC变化量。具体标定步骤空载状态下连续读取50次原始值取平均记为offset_value。放上一个已知重量的砝码比如1kg标准砝码读取稳定后的原始值记为load_value。计算scale_factor (load_value - offset_value) / 1000单位是“ADC原始值/克”。完成之后存到Flash里掉电不丢失。我一般把校准参数的存储地址固定下来比如放在Flash最后一个扇区每次上电先读出来如果校验失败就强制进入校准模式。下面是在OLED上显示重量的核心代码int32_t raw HX711_ReadRaw(); int32_t filtered MovingAverage(raw); float weight_g (float)(filtered - calib_offset) / (float)calib_scale; // 去皮功能记录当前重量作为新的零点 if (Button_Pressed) { calib_offset filtered; } // 动态显示保留一位小数 char buf[20]; sprintf(buf, %.1f g, weight_g); OLED_ShowString(2, 3, buf);这里有个稳定性问题必须处理体重秤上的人站着不会完全静止呼吸、重心微调都会让读数波动。所以滤波必须做不能直接把原始值拿来显示否则屏幕上的数字会一直跳。滤波策略我后面单独讲。3.3 叠加还是平均滤波算法取舍体重秤的数据噪声主要来自两个地方一是电源纹波和电磁干扰引入的高频噪声二是人体自身的微动产生的低频波动。针对不同的噪声源滤波策略完全不同。高频噪声用滑动平均滤波基本思路是维护一个固定长度的环形缓冲区每次新数据进来把最旧的数据挤出去然后对缓冲区内的所有数据求平均。这样既能平滑毛刺又不会让响应变得太迟钝。#define FILTER_N 20 int32_t filter_buf[FILTER_N]; uint8_t filter_idx 0; int32_t filter_sum 0; uint8_t filter_count 0; int32_t MovingAverage(int32_t new_sample) { // 先减去最旧的数据 if (filter_count FILTER_N) filter_sum - filter_buf[filter_idx]; else filter_count; // 写入新数据并累加 filter_buf[filter_idx] new_sample; filter_sum new_sample; filter_idx (filter_idx 1) % FILTER_N; return filter_sum / filter_count; }这个写法比每次全量数组求和快得多时间复杂度从O(N)降到了O(1)嵌入式场景下更合适。窗口长度N的选择需要在平稳性和响应速度之间做权衡。N太小滤波效果差数字还在跳N太大换人站上去半天才能稳定下来体验很糟糕。我用下来20左右比较合适既能压住高频毛刺又能在2到3秒内稳定显示。但滑动平均只能解决随机噪声对低频的人体晃动没什么效果。实际测试中你会发现即使滤波了显示数字还是会缓慢波动几十克。这时候加一个判断逻辑连续几次读数之间的差值小于某个阈值比如50克才刷新显示否则保持上次显示值。从使用者的角度来看就是屏幕上的数字快速稳定下来不再频繁跳变。这个机制的学名叫死区判断在仪表类产品里很常见实现起来却非常简单几行代码的事。3.4 显示与人机交互逻辑OLED显示我分了三块区域顶部显示当前重量的大字最大字号中间一行显示去皮状态提示底部显示电量或者校准状态。SSD1306的驱动用现成的库就行自己从寄存器层面写驱动库性价比太低除非你的目标是学习OLED底层协议。UI逻辑上状态机是必需品。我定义了几个状态STATE_STANDBY待机、STATE_MEASURE测量中、STATE_CALIBRATE校准中。按键短按进入去皮长按3秒进入校准模式。用状态机管理之后代码结构清晰了很多后续想加蓝牙、加历史记录功能只需要在对应状态里做状态迁移即可不会牵扯到其他地方。4. 仿真搭建与实测数据4.1 Proteus仿真环境的建立Proteus仿真这个环节很多人容易卡在“传感器怎么模拟”这个问题上。Proteus元件库里并没有直接提供HX711的模型一般用两种方式解决一种是用虚拟电阻网络模拟惠斯通电桥配合一个ADC芯片做信号采集另一种是直接用Proteus里的虚拟仪器把HX711的DOUT引脚接到一个信号发生器上模拟脉冲输出。我实际用的是第二种方案的变体用一个虚拟的单片机板子输出模拟的HX711时序连接到目标STM32的PB12和PB13引脚。原理很简单——仿真里没有真实传感器那就人为构造一组符合HX711时序的数字波形让STM32认为它读到了一个真实的ADC值。调试完整个软件流程后再把这部分替换成真实硬件驱动即可。仿真最大的价值在于验证两个东西一是GPIO引脚的初始化是否配置正确二是逻辑层的状态迁移是否按预期工作。很多初学者在实物上遇到“读数全是乱码”的问题其实根因不是传感器坏了而是GPIO初始化配错引脚或者时钟没开启。在Proteus里通过虚拟示波器观察SCK和DOUT的时序关系可以很直观地发现这类问题。4.2 实测数据与精度分析实物搭建完成后我用标准砝码做了精度测试。测试结果整理成下表砝码重量(g)实测显示(g)绝对误差(g)相对误差(%)0空载0.00.00.010099.8-0.20.2500499.6-0.40.0810001000.30.30.0320002001.11.10.05550004998.7-1.30.026从数据可以看出小重量时误差百分比偏高主要因为零点漂移和机械回程差的影响占比较大。到了大重量段系统的线性度表现良好。这个精度已经超过市面上很多定位普通消费级的电子秤了。细究误差来源最大的贡献者其实是传感器本身的非线性。电阻应变式传感器在全量程范围内并不是完美的线性响应特别是加载和卸载过程中存在滞后。解决方法是软件补偿可以做分段线性插值——把全量程分成5段每段单独标定一个scale_factor使用时分段选不同的系数。这种方法能显著压低非线性误差代价是标定工作量增加。如果只是做课程设计整体单段标定已经够用了。5. 常见问题与排查实录5.1 读数一直漂移停不下来现象空载时显示值从0慢慢涨到3g、5g或者随机跳变。排查顺序先看电源纹波。用示波器量LDO输出端的波形如果纹波峰峰值超过50mV先解决电源问题。最简单的是加大滤波电容或者换一个质量更好的LDO。检查传感器固定结构。传感器螺栓有没有拧紧有没有和秤体外壳接触产生应力传感器底座安装不平会导致传感器自身承受额外应力直接表现就是零点漂移。检查HX711模块的AVCC和地线走线。模块的模拟地和数字地应该在电源入口处单点汇接不要形成地环路。程序层面还有一个隐蔽因素每次读取HX711之间如果没有足够的间隔芯片可能还没有完成下一次转换就又被读取数据会周期性重复看起来就像漂移。确保读数据之前DOUT已经变低这其实就是驱动里那个等待循环存在的原因。5.2 放上重物读数不变化或者变化极小这个问题我排查过一次最后发现是HX711增益设置错误。前面提到的第25个脉冲如果没发出去芯片还停留在上一次的增益配置状态但这不会导致完全无响应。真正导致“读数不变化”的常见原因是传感器的两根信号线S/S-接反了或者有一只断了。另外有个非硬件原因代码里把原始值强制类型转换成了uint32_t如果传感器输出负值比如传感器受压为负向形变整个数据就变成了一个很大的正数后续计算全乱。HX711读回来的数据本身就是带符号的要用有符号类型存储和运算我见过不少新手在这里翻车。5.3 校准后换一个传感器读数全乱原因很简单不同的传感器灵敏度和零点偏移都不一样校准参数不能通用。我之前遇到过朋友拿着我的校准参数去跑自己的秤结果显示值漂到天边去了。所以每一台秤都必须单独做校准而且校准之后如果更换了传感器必须重新做。一个实用建议是把校准参数按“传感器编号量程”分组存到Flash里程序上电时可以通过按键选择不同的校准参数组。这样手头即使有多个传感器也能随时切换对应的标定数据生产或者测试时非常方便。这里提醒一句搬运模块的时候如果力度过大或者跌落传感器可能产生不可逆的塑性形变校准参数会失准这时候别指望软件能救回来直接换传感器更划算。6. 项目可扩展的方向这套硬件和软件架构本身留了很大的扩展空间稍微改改就能演进成其他产品形态。最容易加的是蓝牙功能。STM32F103的串口资源充足接一个HC-06或者JDY-31蓝牙模块把当前重量字符串通过串口发送到手机APP即可。配上定时上传的逻辑就能做体重趋势曲线这正是市面上智能体脂秤的基础功能。如果不想用外挂蓝牙模块可以换用带蓝牙的MCU型号比如STM32WB系列代码逻辑几乎不用动只是把串口发送改成BLE的notify而已。数据存储方面可以外接一个小型EEPROM比如AT24C02或者直接用STM32内部的Flash模拟EEPROM记录每天早上的体重数据。配合OLED端的简单菜单逻辑在秤上直接查看一周体重变化曲线完全不依赖手机体验也不错。如果想把精度再往上推一个档次可以考虑更换传感器为更高精度的悬臂梁式传感器或者改用HX720这种更新一代的ADC芯片它支持更高的分辨率和更低的噪声电路兼容性也更好。但核心的标定流程、滤波策略、显示逻辑基本不用变这也说明好的软件架构本身是有迁移价值的。最后再多说一句实操心得做这类带传感器的小项目优先级永远是先让数据“读得稳”再谈“读得准”。“稳”靠的是电源、布线和滤波这是硬件设计决定的“准”靠的是标定和补偿这是算法层面决定的。顺序反了只会越调越痛苦。我这次分享的所有代码和设计思路都是围绕这一条主线展开的看懂这条主线剩下的事情就都是水到渠成了。