1. 称重项目为什么值得用STM32加HX711这套组合来做
称重这个需求看起来简单,但真正落到嵌入式项目里,坑比想象中多得多。我最早接触电子秤类项目时,以为无非就是传感器读个数、屏幕显示一下就完事,结果第一版做出来,空载读数在±30克之间乱跳,放上500克砝码显示487克,换了三块HX711模块才找到问题所在。后来陆续做了配料秤、快递称重台、厨房电子秤几个变种,才慢慢把STM32加HX711加OLED这套组合吃透。
这套方案的核心价值在于:HX711把称重传感器微弱的毫伏级差分信号放大并转换成24位数字量,STM32负责采样时序、滤波算法、标定换算和人机交互,OLED则把重量、单位、状态实时呈现出来。三者分工明确,成本极低,一套下来物料成本通常不超过三十块,却能做出精度到1克、稳定性不错的成品。适合谁参考?正在做毕业设计的学生、想入门嵌入式传感器采集的开发者、需要给设备加称重功能的工程师,以及单纯想搞个实用小工具的手工爱好者。
关键词里提到的STM32、HX711、称重模块、OLED,正好覆盖了这个项目的四个核心环节。我下面会从硬件连接、HX711的采样时序与速率选择、STM32端的驱动与滤波、OLED显示与标定流程、以及实际调试中那些文档里不会写的坑,一层层拆开讲。你不需要有很深的嵌入式基础,只要会点C语言、能看懂电路图,跟着走就能复现。
先说清楚一个前提:称重项目的精度,七分靠机械结构和传感器本身,三分才靠电路和代码。如果你的传感器量程选得离谱、安装方式有应力干扰,再好的代码也救不回来。所以我会在讲代码之前,先把硬件选型和安装的底层逻辑讲透,这部分才是决定成败的地方。
2. 硬件选型与接线:从传感器到HX711再到STM32
2.1 称重传感器怎么选,量程和灵敏度决定了什么
称重传感器本质是个惠斯通电桥,受力后桥臂电阻变化,输出一个和受力成正比的差分电压。常见的悬臂梁式传感器,灵敏度标称一般是1mV/V,意思是供5V激励电压时,满量程输出5mV。这个信号非常微弱,STM32自带的12位ADC根本分辨不出来,所以才需要HX711这种专用24位ADC来做前端。
选传感器时,量程要留出余量。你要称的最大重量如果是5公斤,别选5公斤量程的,选10公斤的。原因有两个:一是满量程附近线性度会变差,二是留余量能避免偶尔超载损坏传感器。我见过有人用1公斤量程的传感器去称1.2公斤的东西,当场就把应变片压变形了,读数再也回不到零点。
灵敏度方面,市面上常见的有1mV/V和2mV/V两种。2mV/V的信号更强,对HX711来说更容易分辨,但价格稍贵。如果你做的是小量程高精度秤,比如0到500克,优先选2mV/V的。另外要注意传感器的四线还是五线接法,五线多一根反馈线,能补偿导线电阻带来的激励电压衰减,长距离传输时更准。
2.2 HX711模块的引脚定义和与STM32的接线逻辑
HX711模块通常有十个引脚,分两组。一组是传感器侧:E+、E-、A+、A-、B+、B-,其中A通道增益128,B通道增益32。另一组是MCU侧:VCC、GND、DT(数据)、SCK(时钟)。我们称重一般用A通道,因为增益高、分辨率好。
接线时最容易出错的是传感器和HX711之间的四根线。传感器上一般标着E+、E-、S+、S-,对应接到HX711的E+、E-、A+、A-。这里有个坑:不同厂家的传感器线色定义不一样,红黑绿白分别代表什么,一定要看传感器说明书,不能凭经验。我就因为想当然把激励线和信号线接反过,结果HX711读数一直是满量程饱和值,查了半天才发现。
HX711和STM32的连接就简单了,DT接任意GPIO输入,SCK接任意GPIO输出,VCC接3.3V或5V都行,模块自带稳压。但要注意,如果STM32是3.3V供电,HX711的VCC也建议接3.3V,这样DT和SCK的电平匹配,不会出现3.3V MCU读5V模块时的电平兼容问题。虽然HX711的DT是开漏输出,理论上能容忍5V,但稳妥起见还是统一电压。
| 传感器引脚 | HX711引脚 | 说明 |
|---|---|---|
| E+ | E+ | 激励正,通常红或黑 |
| E- | E- | 激励负 |
| S+ | A+ | 信号正,差分输入 |
| S- | A- | 信号负,差分输入 |
2.3 供电与去耦:为什么你的读数总在跳
很多人接完线发现读数不稳,第一反应是代码滤波没做好,其实八成是供电问题。HX711内部有稳压和振荡电路,对电源纹波比较敏感。如果你用USB供电,电脑USB口的纹波可能就有几十毫伏,直接耦合到模拟前端上。
我的做法是:在HX711的VCC和GND之间并一个10微法和一个0.1微法的电容,0.1微法尽量靠近模块引脚。如果条件允许,给HX711单独用一个低压差线性稳压器供电,和STM32的数字电源分开。另外,传感器的屏蔽线要单端接地,别两端都接,否则形成地环路反而引入干扰。
还有一点,HX711模块上那个跳线,默认是10Hz采样率,短接后变80Hz。这个选择后面会详细讲,但硬件上先知道有这么个东西。如果你做的是静态称重,10Hz足够;如果是动态称重比如流水线上快速过秤,才需要80Hz。
3. HX711的采样时序与速率选择:10Hz还是80Hz
3.1 读懂HX711的串行通信协议
HX711不是标准的I2C或SPI,它自己定义了一套简单的两线时序。DT是数据线,SCK是时钟线。当DT为高电平时,表示转换未完成,MCU要等;当DT变低,表示数据准备好了,MCU开始发时钟脉冲读数据。
读一次数据的流程是:先等DT变低,然后发25到27个SCK脉冲。前24个脉冲读出24位数据,第25个脉冲用来选择下一次转换的通道和增益。具体来说,第25个脉冲的个数决定下一轮用哪个通道:25个脉冲选A通道128增益,26个选B通道32增益,27个选A通道64增益。我们称重固定用A通道128增益,所以每次发25个脉冲就行。
这里有个细节:24位数据是二进制补码格式,最高位是符号位。读出来后要先做符号扩展,再转成有符号的32位整数。很多人直接当无符号数处理,结果负重量显示成一个巨大的正数,就是因为没处理补码。
// HX711读一次原始数据的典型实现 int32_t HX711_Read(void) { int32_t value = 0; uint8_t i; while (HX711_DT_READ() == 1); // 等待DT变低 for (i = 0; i < 24; i++) { HX711_SCK_HIGH(); delay_us(1); value = value << 1; HX711_SCK_LOW(); delay_us(1); if (HX711_DT_READ()) { value++; } } // 第25个脉冲,选择A通道128增益 HX711_SCK_HIGH(); delay_us(1); HX711_SCK_LOW(); delay_us(1); // 补码转有符号数 if (value & 0x800000) { value |= 0xFF000000; } return value; }3.2 10Hz和80Hz的取舍,不只是快慢问题
HX711的采样率由内部振荡器决定,默认10Hz,短接模块上的RATE引脚后变80Hz。表面看80Hz更快,但代价是有效位数下降。10Hz模式下,HX711的噪声等效输入大约50nV,有效位数能到21位左右;80Hz模式下噪声增大,有效位数大概降到19位。
这意味着什么?假设你用5公斤量程、2mV/V灵敏度的传感器,5V激励,满量程输出10mV。21位有效分辨率下,最小可分辨约10mV除以2的21次方,大约是4.8纳伏,换算成重量约2.4微克,当然实际受机械噪声限制远达不到。但19位时,分辨率就差了4倍。所以静态称重坚决用10Hz,动态快速称重才考虑80Hz。
我做过一个快递称重台,要求放上去1秒内出稳定读数。用10Hz的话,1秒才10个样本,滤波后响应偏慢;换80Hz后1秒有80个样本,做滑动平均后响应快很多,精度也够用。所以速率选择要看你的应用场景,没有绝对的好坏。
3.3 采样时序中的延时与稳定性
HX711的SCK高电平持续时间要求最小0.2微秒,低电平也是0.2微秒。STM32跑72MHz时,一个指令周期约14纳秒,所以直接翻转GPIO就够快了,但为了保险,我还是加了1微秒的延时。这个延时不能省,尤其是在长导线连接时,信号边沿变缓,太快的时钟可能导致数据错位。
另外,读数据前等待DT变低要有超时机制。正常情况下HX711每100毫秒(10Hz)或12.5毫秒(80Hz)拉低一次DT。如果程序卡在while等待里超过200毫秒,说明硬件出问题了,应该跳出并报错,而不是死等。我在实际项目里就遇到过传感器线断了一根,DT一直不拉低,程序卡死导致看门狗复位,加了超时后才定位到是接线问题。
4. STM32端的驱动、滤波与标定换算
4.1 GPIO模拟时序还是硬件SPI,为什么我选模拟
HX711的时序虽然像SPI,但不是标准SPI,因为它的数据线方向是单向的但时钟极性特殊,用硬件SPI反而不好控制第25个脉冲的通道选择。所以绝大多数方案都用GPIO模拟时序,灵活且可靠。STM32的HAL库提供HAL_GPIO_WritePin和HAL_GPIO_ReadPin,直接调用就行,性能足够。
如果你追求极致速度,可以用寄存器操作BSRR和IDR,把读一位的时间压到几百纳秒。但对10Hz采样来说完全没必要,HAL库的可读性更好,维护起来方便。我用STM32F103C8T6实测,HAL库模拟时序读一次数据大约几十微秒,相对于100毫秒的采样周期可以忽略。
4.2 滤波算法:滑动平均、中值滤波还是卡尔曼
原始数据读出来后不能直接用,必须滤波。我试过几种方案,各有适用场景。
滑动平均最简单,维护一个长度为N的数组,每次新数据进来替换最旧的,输出平均值。N取10到20比较合适,太大响应慢,太小滤波效果差。缺点是遇到突发干扰(比如有人碰了一下秤台)会把干扰平均进去,恢复慢。
中值滤波是取最近N个数据的中位数,能有效剔除脉冲干扰。N一般取5或7。缺点是计算量稍大,需要排序,但对STM32来说小菜一碟。我通常把中值滤波和滑动平均结合:先中值滤波去掉突发尖峰,再滑动平均平滑随机噪声。
卡尔曼滤波理论上最优,但对称重这种一维静态信号有点杀鸡用牛刀,而且参数调起来麻烦。除非你的应用对动态响应要求极高,否则前两种组合就够了。
#define FILTER_N 10 int32_t filter_buf[FILTER_N]; uint8_t filter_idx = 0; int32_t HX711_GetFiltered(void) { int32_t raw = HX711_Read(); filter_buf[filter_idx] = raw; filter_idx = (filter_idx + 1) % FILTER_N; int32_t sum = 0; for (uint8_t i = 0; i < FILTER_N; i++) { sum += filter_buf[i]; } return sum / FILTER_N; }4.3 标定:从原始值到克数的两步法
滤波后的原始值还是个没有物理意义的整数,要换算成重量,需要标定。标准做法是两点标定:空载时读一个值叫零点值,放上已知重量的砝码读一个值叫满度值,然后线性换算。
公式是:重量 = (当前值 - 零点值) / (满度值 - 零点值) * 砝码重量。
这里的关键是砝码要选你常用量程附近的值。如果你平时称几十克的东西,却用5公斤砝码标定,小量程段的误差会被放大。我一般建议用满量程的50%到80%作为标定砝码。比如10公斤量程,用5公斤或8公斤砝码标。
标定值要存在Flash里,掉电不丢失。STM32F103的Flash操作不难,HAL库有现成的HAL_FLASH_Program。注意写之前要先擦除整个页,STM32F103C8T6的页大小是1KB。我通常把零点值和满度值存在一个专门的页里,开机时读出来。
typedef struct { int32_t zero; int32_t full; float weight; uint32_t magic; } CalibData; CalibData calib; void LoadCalib(void) { memcpy(&calib, (void*)CALIB_ADDR, sizeof(CalibData)); if (calib.magic != 0x5A5A1234) { calib.zero = 0; calib.full = 100000; calib.weight = 1000.0f; calib.magic = 0x5A5A1234; } } float GetWeight(void) { int32_t raw = HX711_GetFiltered(); return (float)(raw - calib.zero) / (float)(calib.full - calib.zero) * calib.weight; }4.4 温漂与零点跟踪:长时间使用的必修课
称重传感器对温度敏感,温度变化几度,零点可能漂移几十克。如果你的秤要长时间通电,必须做零点跟踪。原理是:当读数在零点附近小幅波动且持续一段时间(比如3秒),就认为当前是空载,慢慢把零点值往当前值靠。
实现上,维护一个零点偏移量,每次检测到稳定且接近零时,让零点值以很小的步长逼近当前读数。步长要小,比如每次调整0.1%,避免把真实的小重量误判为零点。这个功能在电子秤芯片里是标配,自己写也不复杂,但能大幅提升长期稳定性。
5. OLED显示:从驱动到界面的完整实现
5.1 0.96寸OLED的I2C驱动要点
0.96寸OLED基本都用SSD1306驱动芯片,四针I2C接口,VCC、GND、SCL、SDA。STM32用硬件I2C或软件模拟都行。硬件I2C速度快,但STM32F103的硬件I2C有历史遗留的卡死问题,很多人宁愿用软件模拟。我用软件模拟I2C,GPIO翻转,稳定可靠,速率400kHz下刷一屏128x64也就十几毫秒。
SSD1306的初始化序列比较长,网上有现成的,但要注意几个关键命令:设置对比度、设置显示模式、设置多路复用率、设置显示偏移、设置时钟分频、设置预充电周期、设置COM引脚配置、设置VCOMH电压。这些参数影响显示效果和寿命,不能随便改。特别是对比度,设太高OLED老化快,设太低看不清,一般0xCF左右比较合适。
5.2 显示汉字和自定义图形的方法
SSD1306本身不带字库,显示英文和数字可以用内置的6x8或8x16点阵,但汉字必须自己取模。取模软件用PCtoLCD2002,设置成阴码、逐列式、顺向,生成十六进制数组。一个16x16的汉字占32字节,显示时按页写入。
显示重量时,我一般用大号数字字体,比如16x32的数字,这样远处也能看清。数字字体可以自己取模,也可以用现成的。单位“g”或“kg”用小字体显示在旁边。界面上再留一行显示状态,比如“稳定”“标定中”“超载”。
// 显示一个16x16汉字 void OLED_ShowCN(uint8_t x, uint8_t page, const uint8_t *cn) { for (uint8_t i = 0; i < 16; i++) { OLED_WriteData(cn[i]); } OLED_SetPos(x, page + 1); for (uint8_t i = 0; i < 16; i++) { OLED_WriteData(cn[i + 16]); } }5.3 界面刷新策略:别让刷屏拖慢采样
OLED刷新是I2C通信,一屏全刷要传1024字节,400kHz下大约20毫秒。如果你每次采样都全屏刷新,10Hz采样下CPU有20%时间花在刷屏上,而且屏幕会闪。正确做法是局部刷新:只更新重量数字所在的区域,其他不变。
更进一步,重量数字变化时才刷新,没变化就不刷。我通常把重量值转成字符串,和上一次的字符串比较,不同才刷。这样静态称重时屏幕几乎不占用CPU时间。另外,I2C通信期间如果HX711的DT变低,可能错过采样窗口,所以刷屏最好放在两次采样之间,或者用中断方式读HX711。
5.4 常见显示问题:花屏、不亮、残影
OLED不亮,先查供电和I2C地址。SSD1306的地址通常是0x78或0x7A,取决于SA0引脚。如果地址不对,初始化命令发不进去,屏幕自然不亮。用逻辑分析仪抓一下I2C波形,看有没有应答,最快定位。
花屏一般是初始化序列不对或复位不完整。SSD1306上电后需要一段复位时间, datasheet要求至少100毫秒。有些模块没有复位引脚,靠上电复位,如果STM32启动太快,可能在OLED还没准备好时就发命令,导致花屏。解决办法是初始化前加200毫秒延时。
残影是OLED的固有问题,长时间显示固定内容会烧屏。缓解办法是降低对比度、定期移动显示位置、或者设置自动熄屏。如果做产品,这些都要考虑。
6. 调试中那些文档不会告诉你的坑
6.1 读数跳动的排查链路
读数跳动是最常见的问题,排查要按顺序来。第一步,看跳动幅度。如果跳动几十克,先查供电纹波,用示波器看HX711的VCC。第二步,看跳动频率。如果和50Hz工频同步,说明有工频干扰,检查传感器屏蔽和接地。第三步,看是否和触摸有关。手碰秤台读数就变,说明机械结构有应力,传感器安装螺丝拧太紧或底板不平。
我遇到过一次,读数每隔几秒跳一次,幅度不大但很规律。查了半天发现是旁边有个继电器在周期性吸合,电磁干扰耦合进来了。把HX711远离继电器、加屏蔽罩后解决。所以干扰源排查要跳出电路本身,看整个系统的电磁环境。
6.2 标定后小量程不准的原因
用大砝码标定后,称小东西不准,这是线性度问题。传感器在大量程和小量程的线性度不一样,用单点标定无法覆盖。解决办法是做多点标定,比如0、25%、50%、75%、100%五个点,用分段线性插值。虽然代码复杂点,但精度提升明显。
另一个原因是零点漂移。标定时的零点和实际使用时的零点可能不同,尤其是传感器安装后有个蠕变过程。我的经验是,传感器装好后先通电老化24小时,再标定,这样零点稳定得多。
6.3 HX711模块的质量差异
市面上HX711模块质量参差不齐,便宜的几块钱,贵的十几块。差异主要在稳压芯片和PCB布局。便宜模块用的稳压芯片噪声大,PCB走线也不讲究,模拟地和数字地混在一起。我买过一批便宜模块,十个里有三个读数明显偏大。后来换了一家贵点的,一致性就好很多。
如果你批量做产品,建议自己画HX711电路,用REF3030之类的基准源,模拟部分单独铺地,成本增加不多但稳定性提升明显。如果只是自己做着玩,买模块时看评价,选销量高、有原理图的。
6.4 程序卡死与看门狗
HX711等待DT变低的while循环是卡死高发区。如果传感器没接好或模块坏了,DT永远不拉低,程序就死在那里。必须加超时,超时后返回错误码,让上层处理。同时开独立看门狗,万一真卡死了能自动复位。
STM32的独立看门狗用LSI时钟,配置简单。HAL库的HAL_IWDG_Init和HAL_IWDG_Refresh,在主循环里定期喂狗。注意喂狗间隔要小于看门狗溢出时间,我一般设1秒溢出,主循环200毫秒喂一次。
7. 从能用到好用:几个提升体验的细节
7.1 按键交互与标定流程设计
一个完整的称重项目,按键是必须的。至少三个键:去皮、标定、单位切换。去皮就是把当前重量设为零点,标定就是进入标定模式后放砝码、输入砝码重量。按键处理要消抖,我用的是定时器20毫秒扫描一次,连续两次相同才确认。
标定流程我设计成:长按标定键3秒进入,屏幕提示“空载”,稳定后按确认记录零点;然后提示“放砝码”,输入砝码重量,稳定后按确认记录满度值;最后保存到Flash,重启生效。这个流程用户不容易误操作。
7.2 超载与欠载保护
传感器有量程限制,超过120%可能永久损坏。程序里要判断,如果重量超过满量程的110%,显示“超载”并报警,同时停止更新零点。欠载就是负得太多,一般是传感器接反或零点漂移,显示“错误”提示检查。
7.3 低功耗设计思路
如果做电池供电的秤,低功耗很重要。STM32可以进Stop模式,HX711也有掉电模式。但HX711从掉电唤醒后需要稳定时间,不能立即采样。我的做法是:无操作30秒后进Stop,按键中断唤醒,唤醒后延时100毫秒再读HX711。这样一节18650电池能用几个月。
7.4 数据记录与串口输出
很多场景需要记录重量数据,比如配料过程。加一个串口输出,把每次稳定的重量和时间戳发到上位机,用Excel或Python分析。STM32的UART配置简单,printf重定向到UART就行。注意串口输出不要放在中断里,会阻塞,放在主循环里判断标志位发送。
8. 关于这套方案的扩展与个人体会
这套STM32加HX711加OLED的框架,稍加改动就能扩展出很多变种。比如加个DS3231时钟芯片,做成带时间记录的称重终端;加个SD卡模块,离线存储称重数据;加个继电器,做成定量控制,到重量自动停。我甚至见过用它做鱼缸自动喂食器的,称重控制饲料量,思路是一样的。
我个人在实际操作中的体会是,称重项目最花时间的从来不是写代码,而是硬件调试和标定。代码部分一天就能写完,但让读数稳定、精度达标,可能要调一个星期。所以如果你刚开始做,别急着写代码,先把传感器装好、供电弄干净、用示波器看波形,把硬件底子打牢,后面会省很多事。
最后分享一个小技巧:调试阶段在OLED上同时显示原始值、滤波值和换算后的重量,这样一眼就能看出问题出在哪一环。原始值跳,是硬件问题;原始值稳但滤波值跳,是滤波参数问题;滤波值稳但重量不对,是标定问题。这个三行显示法帮我省了无数排查时间。