做这个智能体重秤,起因特别朴素:某天整理工作台,翻出两片落了灰的STM32F103C8T6核心板,旁边正好有一组称重传感器和HX711模块。网上讲HX711读数的教程一抓一大把,但大多只写到"能读出数字",很少有人把原理图、PCB和仿真一起讲透。我决定自己把整个链路从头到尾走一遍:传感器原理、硬件连接、原理图设计、驱动代码、标定算法、仿真调试、实物复现,最后整理成一个可以直接照抄作业的开源包。这篇博文就是这次实践的完整记录。如果你正在找第一个真正"有闭环"的STM32项目——有模拟信号采集、有算法处理、有显示界面、还有实物交互——智能体重秤是非常合适的切入点。下文会把每个环节的"为什么"也讲清楚,而不只是堆代码。
1. 为什么选体重秤这个项目:定位与选型逻辑
1.1 项目闭环完整,难度梯度合理
做小项目练手最怕什么?怕做到一半发现坑太深,做完又发现只是点了个灯。体重秤这个题目好就好在麻雀虽小、五脏俱全:称重传感器输出的是毫伏级模拟信号,要经过放大和24位ADC才能被单片机识别,这涉及信号调理;拿到原始数据之后还要做标定、滤波、去皮,这涉及算法;最后还要通过OLED显示、按键交互把结果呈现出来,这涉及用户界面。从模拟前端到数字后端,再到人机交互,是一条非常完整的链路。
对比纯点灯、纯串口打印这类练习,智能体重秤的工程量适中,一个人利用周末完全能做出来。而且它天然具备"做完真的有用"的属性,放在家里每天称体重,每称一次都是对系统稳定性的验证。这种持续的反馈对学习动力的帮助,是任何实验板练习都给不了的。
1.2 为什么是STM32,而不是51或Arduino
很多人会问:做个体重秤,用STC89C52不也行吗?用Arduino岂不是更快?确实都能做,但选型要看学习目的。
用51的话,8位MCU没有硬件I2C外设,接OLED得用GPIO模拟I2C;没有足够强的中断管理,HX711的时序配合和低功耗设计都别扭。Arduino则把底层封装得太死,大部分时间在调库,写完可能还是说不清ADC内部采样的过程。STM32F103C8T6价格已经压到几块钱,Cortex-M3内核、72MHz主频、丰富的外设、成熟的标准外设库和HAL库生态,既能让你直接操作寄存器理解原理,又不会难到劝退。论性价比和学习效率,这个芯片是当前最平衡的选择。
还有一个务实原因:这个项目我要做仿真。Proteus对STM32F103系列的支持已经非常成熟,可以把代码在电脑上先跑通,再动手焊板子。这一点对于"零基础抄作业"的读者尤其友好。
1.3 开源包到底开了什么
这次开源没有只丢一份源码了事,而是包含四部分:Keil MDK工程源码、Altium Designer格式的原理图、Proteus仿真工程、以及一份详细的项目文档。源码可以直接编译烧录;原理图可以对照着看硬件接线;仿真工程可以在没有实物的情况下先体验完整流程;文档里写了传感器选型、校准步骤和常见问题排查。后面第7节我会逐一说明这几个文件的用法。
2. 硬件架构拆解:从重量到数字的完整链路
2.1 称重传感器是怎么工作的
项目用的是最常见的悬臂梁式称重传感器,额定载荷选50kg。它内部的核心元件是电阻应变片,粘贴在弹性体的受力部位。当外力使弹性体发生微小形变时,应变片也跟着拉伸或压缩,导致电阻值变化。但单个应变片的阻值变化率只有千分之几,直接测电阻几乎测不出有效信号,所以传感器内部把四片应变片接成了惠斯通电桥。
电桥的四个桥臂中,对角的两片随拉伸而阻值变大,另外两片随压缩而阻值变小。这样在电桥的两个输出端之间,就会产生一个与受力大小成正比的差分电压。这个电压非常小,典型灵敏度是2mV/V。意思是:每1V激励电压、满载时输出约2mV。假设传感器供电5V,50kg满载时输出大约10mV。要分辨出0.1kg的变化,系统至少要能稳定分辨0.02mV左右的信号——这已经超出普通单片机内置ADC的能力范围了。
2.2 为什么一定要HX711
STM32F103C8T6内置12位ADC,满量程3.3V时,最低分辨位约0.8mV。用它直接测量毫伏级的电桥输出,相当于拿米尺去量头发丝直径,误差会大到完全不可用。所以必须在前级加一个仪表放大器加高精度ADC的芯片,HX711就是为称重场景定制的方案。
HX711内部集成了低噪声可编程增益放大器(PGA)和一个24位Σ-Δ型ADC。增益可选64、128,常用通道A增益128。信号经过128倍放大后,满量程约1.28V,落在ADC的合理输入范围内。虽然24位的理论分辨率在实际中会因为噪声只能兑现到17~19位左右,但用来分辨0.02mV级别的变化已经绰绰有余。
2.3 系统连接关系
主机和模块之间的连接非常少,这也是这个项目适合入门的重要原因。
| HX711模块引脚 | 接STM32F103C8T6 | 说明 |
|---|---|---|
| VCC | 5V | 模块自带稳压,可为传感器提供稳定激励 |
| GND | GND | 共地 |
| SCK | PB1 | 时钟输入,推挽输出 |
| DOUT | PB0 | 数据输出,浮空输入 |
OLED屏采用0.96寸I2C接口,SCL接PB6,SDA接PB7,正好是STM32的I2C1引脚。另外留两个按键:一个用于去皮/清零,一个用于切换kg/lb单位并进入标定模式。整个系统的硬件关系就是把"重量→差分电压→HX711数字量→STM32处理→OLED显示"这条链路串起来。
3. 原理图设计要点与电源方案
3.1 最小系统:晶振和复位别省事
很多人用现成核心板习惯了,直接拿板子上的晶振和复位电路用,等到自己画原理图时容易漏东西。STM32F103C8T6的最小系统必须包含:8MHz晶振加两个20pF负载电容、BOOT0下拉到地的10k电阻(确保从Flash启动)、NRST复位脚接100nF电容到地、以及每个VDD引脚旁放100nF去耦电容,电源入口再放一个10uF钽电容或电解电容。
这里有个小细节:虽然有内部RC振荡器能跑起来,但USB通信和高精度定时都依赖外部晶振。体重秤虽不用USB,但HX711的时序依赖稳定的时钟,建议老老实实画上晶振。PCB布局时晶振尽量靠近MCU,底下铺地,走线不要平行长距离。
3.2 电源:模拟部分和数字部分要分清
电源是这个项目最容易出问题的环节。HX711是混合信号芯片,它对电源纹波很敏感。实测中,如果直接把HX711的VCC和单片机共用一个未经处理的5V,读数跳动会明显增大。我的方案是:USB 5V进来之后先经AMS1117-3.3给MCU供电,HX711模块单独从5V取电。注意模块上的AVDD和数字电源之间已经有去耦设计,但如果自己画板用裸芯片,一定要在AVDD引脚附近放10uF+100nF两级去耦电容。
另一个容易忽略的点是传感器激励电压的稳定性。电桥输出与激励电压成正比,激励抖动会直接折算成重量读数误差。HX711模块内部通常用稳压源给传感器供电,这是选模块而不是裸芯片的最大好处——省去了自己搭基准源的工作。
3.3 显示与按键接口
OLED的I2C接口需要接上拉电阻,标准I2C一般用4.7k欧上拉到3.3V。有些现成OLED模块上已经有了上拉电阻,画原理图时先确认,避免重复上拉导致信号驱动能力不足。按键处理最简单的方式是接GPIO输入加上拉,按下接地;如果担心按键抖动,在软件里做20ms延时消抖即可,不必硬件加RC滤波。
蜂鸣器我加了一个,但用三极管驱动,而不是让GPIO直接推蜂鸣器。STM32的GPIO灌电流能力有限,直接驱动蜂鸣器可能造成MCU复位或蜂鸣器音量不足。用8050三极管做开关,GPIO通过1k电阻接基极,效果非常稳定。测量稳定之后响一声,交互体验提升一个档次。
4. HX711驱动与称重算法:代码核心拆解
4.1 通信时序:只有两根线,但时序有讲究
HX711和MCU之间是纯GPIO控制的串行协议,接口只有DOUT和SCK两根线。DOUT是数据输出,SCK是时钟输入。当HX711完成一次模数转换后,DOUT会从高电平拉低,表示数据就绪。此时MCU开始从SCK输入脉冲,每来一个上升沿,就把当前数据位移出一位,MSB先行,一共移出24位。第25个脉冲用来设置下一次转换的通道和增益——发送25个脉冲表示继续使用通道A增益128。
这里有个新手常犯的错误:读完24位数据后忘记给第25个脉冲。如果少了这个脉冲,HX711不会切换增益配置,下次读出数据时状态不确定,看起来就是"读数偶尔对、偶尔乱"。我建议把25个脉冲的发送逻辑固定在读取函数里,不管是第一次读还是后续循环读,每次都发满25个。
4.2 驱动代码实现
下面是核心读取函数,我做了完整的注释。
// hx711.c #include "hx711.h" // PB1 -> SCK, PB0 -> DOUT void HX711_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // DOUT 为输入,浮空输入即可 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStructure); // SCK 为推挽输出,初始为低 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_WriteBit(GPIOB, GPIO_Pin_1, Bit_RESET); } int32_t HX711_Read(void) { int32_t val = 0; uint8_t i; // 等待DOUT拉低,表示数据就绪 while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == Bit_SET); // 读24位数据,MSB先行 for (i = 0; i < 24; i++) { GPIO_WriteBit(GPIOB, GPIO_Pin_1, Bit_SET); delay_us(1); val = val << 1; GPIO_WriteBit(GPIOB, GPIO_Pin_1, Bit_RESET); delay_us(1); if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == Bit_SET) val++; } // 第25个脉冲,设置通道A增益128 GPIO_WriteBit(GPIOB, GPIO_Pin_1, Bit_SET); delay_us(1); GPIO_WriteBit(GPIOB, GPIO_Pin_1, Bit_RESET); delay_us(1); // HX711输出24位有符号补码,扩展到32位有符号数 if (val & 0x800000) val |= 0xFF000000; return val; }代码本身不复杂,但有两个细节值得展开。第一,SCK高低电平之间至少要保证1us的延时,HX711的数据手册要求SCK频率不能太高,实际使用1us足够稳定;第二,数据是24位有符号补码,正数高位为0,负数高位为1,所以判断最高位后要进行符号扩展,否则负数会变成一个很大的正数。
4.3 标定算法与数据存储
裸读到的HX711数值和真实重量之间是线性关系,可以写成:重量 =(当前读数 - 空载偏移量)/ 标定系数。所以标定只需要两个关键参数:空载偏移量和每单位重量对应的读数增量。
标定流程很简单。上电稳定后,在秤台无负载时连续读10次取平均,得到offset;然后放上一个已知重量的标准砝码(我用的是5kg配重块),再次读10次取平均,得到loaded。标定系数就是:
scale_coeff = (loaded - offset) / 5.0 // 单位:读数/kg之后任意时刻的重量计算就是:
weight = (HX711_Read() - offset) / scale_coeff这两个参数必须保存到非易失存储里,否则每次断电都要重新标定。STM32F103C8T6没有内置EEPROM,但有1KB的备用寄存器区,也可以直接操作Flash。最简单可靠的做法是把参数写入Flash末尾的某个扇区,写入前先加一个约定好的magic字段(比如0xA5A5),读取时校验失败就提示用户重新标定。
typedef struct { uint16_t magic; // 0xA5A5 表示已标定 int32_t offset; // 空载读数 float scale_coeff; // 每kg对应读数 } CalibData;标定模式通过按键进入:长按单位键3秒进入标定状态,OLED提示放上空载并记录偏移,再提示放上5kg砝码并记录斜率,整个过程一目了然。
5. 仿真与调试:实物动手前先跑通逻辑
5.1 仿真工程怎么搭
我在Proteus里新建工程,放置STM32F103C8T6、HX711、OLED、一个电位器、一个虚拟串口终端。如果没有现成的HX711模型,就用电位器模拟传感器输出:电位器的两端接HX711的A+和A-输入,旋动电位器就相当于改变电桥输出电压。这样虽然不能完全模拟真实传感器的微弱信号,但足以验证整条逻辑链路——HX711读数的变化、OLED的刷新、按键状态机切换、标定流程是否正确。
Proteus里的STM32需要导入hex文件。在MDK的Options for Target -> Output选项卡里勾选Create HEX File,编译后把生成的hex文件填到单片机属性里。晶振频率设为8MHz,和原理图一致。仿真运行后,OLED会显示当前"重量",旋动电位器数值随之变化,虚拟串口终端同时打印原始读数,方便观察。
5.2 仿真和实物的差别
必须说清楚:仿真验证的是程序逻辑,不是模拟性能。Proteus里的电位器输出信号是干净的、无噪声的,而真实传感器输出带着工频干扰、机械振动、温漂和电源噪声。所以仿真跑通了,不等于实物就能稳定工作。我在实际项目中遇到的情况是:仿真一次通过,实物上读数每分钟漂移几十个字。这很正常,不必焦虑,后面第6节的滤波和校准方案就是用来处理这些问题的。
5.3 调试技巧:串口比OLED好用得多
在仿真和实物调试阶段,强烈建议优先用串口打印,而不是盯着OLED。OLED刷新有延迟,串口则可以把每次原始读数一帧一帧打出来,方便观察波形和趋势。MDK里勾选MicroLIB后,直接重写fputc函数就能用printf打印到串口。
int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }调试阶段我把滤波前的原始值和滤波后的计算值同时打出来,对比效果立竿见影。等滤波参数调好了,再切换到OLED进入正常显示模式。
6. 实测校准与常见坑:把误差按在地上摩擦
6.1 回零与去皮
实物做出来之后,第一个要处理的是零点的漂移问题。传感器在空载时,由于粘贴应力、温度变化和机械结构变形,输出并不严格为零。我写了一个开机回零逻辑:上电后延时1秒,等电源和HX711稳定,连续采样10次取平均作为初始offset。但机械结构在长期使用后可能发生形变,零点会缓慢变化,所以保留了一个去皮按键:按键后把当前读数作为新的offset,相当于随时手动回零。
这里有个实用经验:称重平台必须放在坚硬、平整的地面上。地毯、软垫会导致传感器受力不均匀,读数偏大且重复性差。我调试时发现同一物体在瓷砖和木地板上读数差了将近0.3kg,排查了半天才发现是地面软硬导致的机械问题,不是电路问题。
6.2 漂移来源与对策
实物调试最大的坑是读数漂移,主要来源有三个。
第一是电源温漂。HX711和传感器上电后,芯片内部温度缓慢上升,导致基准电压变化。对策是先让系统预热几分钟,或者用软件在稳定后自动记录零点。第二是传感器本身的蠕变,即长时间加载后输出会缓慢变化。这属于机械特性,只能在结构上改善,对家用精度要求来说不用过于纠结。第三是电磁干扰,尤其是传感器线长且没有屏蔽时,读数会周期性跳动。对策是尽量让传感器线短一些,如果无法缩短,就采用双绞线走线,PCB上传感器接口下方铺地。
我测试时发现,HX711的SCK信号线如果和供电线平行走太长距离,数字串扰会耦合进模拟前端,直接表现为读数跳变。解决方法是SCK走线尽量短,且在SCK引脚附近加一个22欧串联电阻抑制振铃。
6.3 软件滤波:中值加平均的双保险
真实世界中,称重信号的噪声主要分两类:随机白噪声和冲击性脉冲噪声。白噪声用平均滤波压制,冲击噪声用中值滤波消除。我的方案是:连续读5次,去掉最大值和最小值,对中间3次取平均。这样既滤掉了踩秤瞬间的冲击干扰,又平滑了随机噪声。
int32_t HX711_GetFilteredValue(void) { int32_t buf[5]; int32_t tmp; uint8_t i, j; for (i = 0; i < 5; i++) { buf[i] = HX711_Read(); delay_ms(20); } // 简单冒泡排序,找出中间值 for (i = 0; i < 4; i++) for (j = 0; j < 4 - i; j++) if (buf[j] > buf[j + 1]) { tmp = buf[j]; buf[j] = buf[j + 1]; buf[j + 1] = tmp; } // 去掉最大值和最小值后取平均 return (buf[1] + buf[2] + buf[3]) / 3; }20ms的采样间隔对应约50Hz采样率,对体重这种缓慢变化的物理量完全够用。如果单次测量反应偏慢,可以把间隔缩到10ms。实际体验下来,这个滤波方案能让读数稳定在±0.05kg以内,体重秤完全够格。
7. 开源包内容与复现指南
7.1 目录结构说明
开源包按功能分成四个目录,每个目录都有对应的README说明。
SmartBodyScale/ ├── Doc/ # 项目文档、数据手册、标定说明 ├── Firmware/ # MDK工程源码 │ ├── Core/ # 启动文件、系统时钟配置 │ ├── Drivers/ # HX711、OLED、按键、蜂鸣器驱动 │ └── User/ # main.c、标定逻辑、界面逻辑 ├── Hardware/ # 原理图工程和PDF └── Simulation/ # Proteus仿真工程Firmware里我按外设拆分了驱动文件,每个文件对应一个硬件模块。新手抄作业时可以只改main.c里的逻辑,不用动底层驱动;想深入学习时可以逐个阅读驱动文件,理解每个寄存器配置的目的。
7.2 从零复现的五个步骤
第一步,安装MDK5和对应芯片支持包,编译Firmware工程,确认能通过并生成hex文件。第二步,打开Simulation里的Proteus工程,加载hex文件跑仿真,旋动模拟传感器输出的电位器,看OLED和串口输出是否正常。第三步,按照Hardware里的原理图准备元器件,注意传感器的四根线颜色和定义要对照模块丝印确认。第四步,按照原理图焊接,焊接顺序建议从电源部分开始,先量电压再上芯片。第五步,烧录程序后进行标定:开机空载记录零点,放上标准砝码记录斜率,然后反复验证几个不同重量的读数。
7.3 复现中的常见问题
我把调试过程中遇到的高频问题整理成了表格。
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 读数一直是0x800000附近 | DOUT未正确连接或HX711未供电 | 检查接线和电源电压 |
| 放上重物读数无变化 | 传感器激励线或信号线接反 | 核对E+/E-、A+/A-定义 |
| 读数跳动剧烈 | 电源纹波大或滤波器效果不足 | 增加去耦电容,检查滤波参数 |
| 标定后误差大 | 秤面受力不均匀或地面不平 | 调整机械结构,放坚硬地面 |
| 偶尔读数为负 | 传感器线缆接触不良 | 重新插拔并固定线缆 |
传感器线缆的颜色定义是个经典陷阱。市面上常见的四线制传感器,红色E+、黑色E-、白色A+、绿色A-,但我手上有过不同品牌颜色定义完全相反的情况。接线前务必用万用表量一下,确认信号线之间的阻值关系:E+和E-之间的阻抗一般比A+和A-之间大,且A+到E+和A-到E-的阻值应该接近相等。这个检查能避免调试时浪费大量时间。
最后说点个人体会。这个项目做完后,我最深的感受是:真正花时间的不是HX711那几行读数据的代码,而是标定、滤波和机械结构。代码层面的问题,仿真阶段基本都能暴露;但实物上的温漂、噪声、受力不均,只能靠实际测试一点点磨。有一个小技巧分享给大家:拿到传感器后,先用手分别按压秤面的四个角,观察四个角读数是否一致。如果偏差很大,说明秤台结构没有调平或者传感器受力点不对,这时候再怎么调代码都白搭。把机械层面的问题解决掉,再回来调软件,效率会高很多。这个顺序,是我踩了无数次坑之后总结出来的。