简介:一套基于 STM32CubeMX 与 HAL 库的 HX711 压力传感器开发资源,面向 5kg 量程称重模块,帮助嵌入式开发者解决高精度称重数据采集、串口实时输出与 OLED 屏幕显示问题。HX711 为 24 位 A/D 转换芯片,资源中完整展示了其与 STM32 的配合方式。资源包为 zip 压缩格式,大小 24.05MB,共 1278 个文件,以 C 源码、H 头文件、启动汇编文件、链接脚本和 .ioc 工程配置为主,并附带编译生成的 hex/axf 文件,工程可在 Keil 中直接打开编译。已有 5531 人学习下载,适合需要快速上手 HX711 与 STM32 开发的学生和工程师。内容涵盖传感器引脚连接说明、皮重扣除与 GapValue 系数校准思路,只需修改宏定义即可调整重量换算系数;同时提供串口助手查看数据及带字库 OLED 实时显示的实现代码,整体目录结构清晰,可作为称重类项目的参考模板。 把HX711接上STM32,通电之后打开串口助手,看到的不是稳定数字,而是在一组值里来回跳——这个场景我猜很多人经历过。HX711是电子秤项目里最常见的一款24位ADC压力传感器模块,配合STM32F103C8T6这类入门级MCU,再加一块0.96寸OLED,就能做成一个完整的称重显示设备。这篇文章把整套流程拆开:HX711的硬件机制、STM32CubeMX图形化配置、驱动代码编写,再到标定公式推导,全都过一遍,而且用的是HAL库。适合手里有板子、想快速跑通“压力采集→串口输出→屏幕显示”这条链路的开发者参考。
1. HX711的工作机制:为什么这个模块只要两根线就能读数
1.1 24位ADC的价值
HX711是一片专为电子秤设计的24位高精度ADC,内部集成了稳压电源、差分输入、可编程增益放大器和串行通信接口。之所以要24位,是因为称重传感器的输出信号非常微弱——常见的悬臂梁式压力传感器,满量程输出通常只有1mV/V到2mV/V,激励电压为5V时,满量程差分电压也就5mV到10mV。如果拿普通10位或12位ADC去采,10mV信号被分到4096份里,每份只有几微伏,再加上噪声,基本上什么都测不准。
24位ADC把同样的信号分成16777216份,即使放大倍数不高也能保持足够的分辨率。HX711内部还带一个低噪声稳压器,可以给传感器直接供电,简化了外围电路。这也是为什么几乎所有市面上的称重模块都拿它当核心芯片。
1.2 通道、增益与数据格式
HX711有A、B两个输入通道,A通道增益可选128倍或64倍,B通道固定32倍。A通道通常接称重传感器,因为称重传感器是差分输出,需要高增益来放大微弱信号。芯片通过PD_SCK引脚发送脉冲个数来切换通道和增益:
- 25个脉冲:A通道,128倍增益
- 26个脉冲:B通道,32倍增益
- 27个脉冲:A通道,64倍增益
每次转换完成后,DOUT引脚会从高电平拉低,表示“数据准备好了”。这时主控单片机发送25个SCK脉冲,就能在脉冲的下降沿之前依次读出24位数据。数据以二进制补码形式存放,最高位是符号位,所以读出来的24位数据要手动扩展到32位的有符号整数,否则满量程会变成正的16777215,而不是-8388608到8388607。
1.3 接线与模块版本注意
模块接线很简单,核心就是四根线:
| 模块引脚 | 接STM32 | 说明 |
|---|---|---|
| VCC | 3.3V或5V | 很多模块板载稳压,5V供电也行 |
| GND | GND | 共地必须接好 |
| SCK/PD_SCK | PA0 | 时钟输入,由MCU控制 |
| DOUT | PA1 | 数据输出,推挽输出 |
有个容易踩的坑是丝印不统一。有的模块把SCK标成PD_SCK,还有的版本叫做SPI模式——不过HX711本质上不是SPI协议,它只是两根线的高电平脉冲时序,不需要片选信号,也不需要时钟极性和相位配置。实际操作时只要确认DOUT接MCU输入、SCK接MCU输出,一般就不会接反。
2. CubeMX图形化配置:从时钟到外设的完整清单
2.1 新建工程与时钟树设置
打开STM32CubeMX,选择STM32F103C8T6这个型号。第一个要处理的是RCC时钟。HSE选择Crystal/Ceramic Resonator,也就是外部晶振,然后在Clock Configuration页面里把系统时钟配置到72MHz。配置方法是:HCLK输入72,总线分频器会自动调整,PLL Source选HSE,倍频系数取9。如果不配外部晶振,也可以直接用HSI内部8MHz时钟,但串口波特率精度会差一些,115200这种高速率下偶尔会出现误码。
我习惯先把USART1和调试串口搞好,因为后面所有调试信息都要靠它输出。如果这一步忘了开UART,代码写完才发现,就得回CubeMX重新勾选再生成,工程还能保留,但比较麻烦。
2.2 Pinout配置清单
在Pinout & Configuration页面里依次配置:
- PA0设置为GPIO_Output,也就是接HX711 SCK
- PA1设置为GPIO_Input,接HX711 DOUT
- USART1设置为Asynchronous异步模式,波特率115200,8位数据,无校验,1位停止位
- I2C1设置为I2C标准模式或快速模式,如果用的是0.96寸OLED且支持硬件I2C,可以直接用I2C1的PB6和PB7
很多教程喜欢用软件模拟I2C驱动OLED,因为SSD1306对时序要求不算苛刻,软件模拟也稳定。不过我建议能用硬件I2C就用硬件I2C,HAL库对硬件I2C的支持很成熟,代码量更少,OLED刷新也不会被其他中断频繁打断。软件I2C只是多两个GPIO口和高低电平翻转的问题,如果板子引脚紧张,再考虑软件方案。
2.3 中断与NVIC配置
串口部分建议打开USART1全局中断,方便后面做中断接收或空闲中断。如果只是调试用的串口输出,不配置中断也能工作,因为发送走的是阻塞轮询方式;但接收数据时会遇到“收一次就再也不进中断”的情况——多半是中断标志没清除,或者没有配置NVIC优先级。最稳妥的做法是在CubeMX里把USART1中断优先级设置成1到3之间的一个值,别用0,给系统滴答中断留点余地。
HX711本身不需要外部中断,读取完全靠等待DOUT拉低,所以不用在EXTI上花时间。生成代码前在Project Manager里选好Toolchain为MDK-ARM,Code Generator勾选“Generate peripheral initialization as a pair of .c/.h files per peripheral”,这样每个外设单独一个文件,代码结构清楚,改起来方便。
3. 驱动代码与数据稳定化:让原始值从跳变成稳
3.1 读取一个完整24位整数
CubeMX生成代码之后,在main.c或者单独建一个hx711.c,先实现最底层的读取函数。核心逻辑是:等待DOUT从高变低,然后连续产生24个SCK脉冲,沿路把DOUT电平读进来,拼成一个32位的整数,再做符号扩展。下面是经过超时保护的基础读取函数:
uint8_t HX711_Read(int32_t *value) { uint32_t data = 0; uint16_t timeout = 10000; while (HAL_GPIO_ReadPin(HX711_DOUT_GPIO_Port, HX711_DOUT_Pin) == GPIO_PIN_SET) { if (--timeout == 0) return 0; } for (uint8_t i = 0; i < 24; i++) { HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_SET); data <<= 1; if (HAL_GPIO_ReadPin(HX711_DOUT_GPIO_Port, HX711_DOUT_Pin) == GPIO_PIN_SET) data |= 1; HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET); } /* 第25个脉冲,回到A通道128倍增益 */ HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET); /* 24位补码转32位有符号数 */ if (data & 0x800000) data |= 0xFF000000; *value = (int32_t)data; return 1; }注意最后那个第25个脉冲极其关键。HX711的协议要求读数据时至少要发出25个SCK脉冲,前24个脉冲移出数据,第25个脉冲告诉芯片继续按当前通道和增益工作。如果漏了这第25个脉冲,下次读取时芯片可能切到别的通道,数据乱掉。
3.2 为什么读出来的值会跳
很多人第一次读HX711,发现零点状态下数值不停变化,就以为是传感器坏了。实际上HX711本身分辨率极高,一颗5kg的传感器,满量程输出10mV,增益128倍后也就是1.28V左右,分配到24位ADC上,每个LSB对应的重量可能只有零点几克。此时传感器自身的噪声、电源纹波、甚至导线间的电磁干扰都会被放大到几克乃至十几克的波动。
应对办法是软件滤波。我自己测试下来,最实用的组合是“多次采样取平均值”或者“去掉最大最小值再取平均”。称重场景不像高速采集,100ms读一次完全够用,10次采集中值平均之后,静态读数能稳定到±1克以内。下面这个函数就是连续读20次取平均,每次间隔5ms:
int32_t HX711_GetAverage(uint8_t times) // times通常取10或20 { int32_t sum = 0; int32_t raw = 0; for (uint8_t i = 0; i < times; i++) { if (HX711_Read(&raw)) sum += raw; HAL_Delay(5); } return sum / times; }3.3 初始化阶段的保护
还有两个细节值得写进代码里。第一个是上电后HX711需要几十毫秒才会进入稳定工作状态,CubeMX生成的main函数里加个HAL_Delay(200)再循环读取,避免开头几个异常值进滤波。第二个是超时保护不能省。如果传感器信号线松了、模块没供电,DOUT就一直保持高电平,while循环会卡死整个程序。所以我在读取函数里加了timeout,返回0表示读取失败,主程序可以用这个标志位做错误提示。
4. 标定的本质:原始值如何换算成重量
4.1 一点法还是两点法
HX711输出的是ADC原始值,不是重量。重量和原始值呈线性关系,所以标定就是要找出这条直线的斜率。一个常见的误区是查芯片手册去算一个固定的换算系数,比如“多少原始值等于1克”。这行不通,因为传感器量程、灵敏度、供电电压、增益设置都会影响斜率,每个传感器个体也有差异,必须实际标定。
最简单的做法是零点加砝码的一点标定:
float scale = 0.0f; int32_t zero = 0; // 空载时读取的平均值 // 放一个已知重量的砝码,比如1000g,读取稳定值 int32_t raw_1000 = HX711_GetAverage(20); scale = (float)(raw_1000 - zero) / 1000.0f; // 后续换算重量 float weight = (float)(current_raw - zero) / scale;这是利用“直线过原点”的假设,把零点当作(0,0),另一个点确定斜率。实际应用中由于传感器安装应力、ADC偏移等因素,零点不一定完全落在原点上,所以更准确的是两点标定:记录空载时的原始值raw0和已知重量weight0,再记录放砝码时的raw1和weight1,然后解出斜率gain和偏移offset:
float gain = (weight1 - weight0) / (float)(raw1 - raw0); float offset = weight0 - gain * (float)raw0; // 任一时刻的重量 float weight = gain * (float)current_raw + offset;两点法能同时消除零点漂移和满量程误差,是电子秤项目里最实用的标定方式。
4.2 标定步骤与数据记录
实际操作时我建议这样标定:
- 空载状态下,让设备稳定运行至少10秒,取20次平均得到raw0
- 放一个接近量程60%到80%的标准砝码,同样稳定后取20次平均得到raw1,记录砝码实际重量weight1
- 把gain和offset通过串口打印出来,存到本地,方便以后微调
砝码选择很有讲究。太轻的砝码,比如10克,信号幅值小,噪声占比大,标定出来的斜率误差会被放大;太重,超出传感器线性区,标定结果也不准。尽量选传感器量程的1/2左右,效果最好。没有标准砝码时,可以用已知重量的纯净水,但要注意容器自重也要算进去,而且水会蒸发,标定过程要快。
4.3 标定参数保存与恢复
标定参数如果每次开机都要重新标定,那项目就太费劲了。STM32F103C8T6内部有64KB Flash,留出最后一页存标定数据是常见做法。使用HAL库的Flash接口时要注意先擦除再写入,而且擦除以页为单位。写入时可以把gain和offset打包成一个结构体,写进固定地址;开机时读取并做一次合理性校验,比如gain必须在某两个阈值之间,否则就认为没有标定过,提示用户重新标定。
这件事最好在设计初期就考虑进去,别等代码写完再改。我就是一开始没做Flash存储,每次断电重新标定,后来加了存储功能,整个设备才真正能用。
5. 串口与OLED双通道输出:工程的最后一步
5.1 printf重定向到串口
CubeMX生成的HAL库默认没有把printf映射到串口,需要自己重写fputc函数。在usart.c文件末尾加上:
#include <stdio.h> int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }然后勾选Keil MDK里Target选项卡的“Use MicroLIB”选项,就能直接使用printf格式化输出了。不加MicroLIB的话,标准库的printf会占用大量Flash空间,而且重定向可能不生效。
调试输出建议设计成一个方便看的数据帧格式。比如每200ms输出一次,格式为:
raw: 123456, weight: 125.3g, status: OK如果后续要接上位机做数据记录,也可以用CSV格式,比如raw,weight。这样在串口调试助手里就能直接看到传感器原始值和换算后的重量,排查问题时特别有用。HX711模块的数据手册说24位有效位数,实际用下来真正稳定的位数大概在20位左右,算法做好之后串口看到的重量能稳定到0.1g级别。
5.2 OLED显示内容与刷新节流
屏幕显示部分,我用的是0.96寸SSD1306 OLED,I2C接口。CubeMX里配好硬件I2C1之后,把SSD1306的驱动库拷到工程里,初始化函数里传入&hi2c1,调用OLED_Init()就能点亮屏幕。显示内容除了实时重量,还可以加上状态信息,比如是否标定、当前滤波模式、供电电压等。
OLED刷新有个禁忌:不要在while循环里每一轮都全屏刷新,会明显看到闪烁。称重显示场景下每秒刷新1到2次就足够肉眼观察了,也就是每500ms到1000ms刷新一次。我这里用一个简单的毫秒时间戳控制:
if (HAL_GetTick() - last_display_time >= 500) { OLED_Clear(); OLED_ShowString(0, 0, "Weight:"); OLED_ShowNum(0, 16, (int)(weight * 10), 5, 12); // 放大10倍显示一位小数 OLED_ShowString(70, 16, "g"); OLED_Refresh(); last_display_time = HAL_GetTick(); }关于显示内容,不要在OLED上频繁更新小数点的位置,否则数字跳变时会出现残影。最好固定位数,比如统一显示5位整数加一位小数,不足位的地方补零。
5.3 串口接收“只收一次”的排查经验
标题里还提到了“串口中断接收只收一次”这个热搜问题,跟串口相关,我额外多说两句。如果开了USART1中断接收,却发现只能进一次回调,大概率是中断标志没有处理干净。HAL库的接收中断回调里一般会重新调用HAL_UART_Receive_IT()开启下一次接收,这句一定不能遗漏,否则中断标志被HAL库清除后就没人再开启接收了。另外检查一下NVIC里USART1中断使能是否勾选,以及CubeMX生成的DMA配置是否和普通中断冲突。
如果项目里串口数据量比较大,用空闲中断加DMA是更稳定的方案。空闲中断可以在接收完一帧数据后立刻触发,配合DMA半满中断和全满中断,能有效避免“高速数据进来时CPU来不及处理”导致丢字节的问题。这个方案稍微复杂一点,但一旦调好,后续不管接什么上位机都省心。
6. 实机调试中的坑与应对
6.1 电源纹波是最大的天敌
HX711对电源噪声极其敏感。我接过一个临时用面包板搭的电路,STM32和HX711共用一个USB转串口模块的5V供电,结果数值在几百的范围内乱跳。后来把HX711的VCC改从STM32板上的3.3V引脚单独取电,又在模块电源引脚旁边加了一颗100uF电解电容和一颗0.1uF陶瓷电容,读数立刻稳了不少。如果系统里有电机、继电器这类大电流负载,一定要做电源隔离或者至少物理上把模拟电源和数字电源分开布局。
传感器信号线也要注意。尽量用双绞线或者屏蔽线连接,长度不要超过30厘米。导线太长会有天线效应,把你不需要的工频干扰全部收进来,滤波算法再强也没用。
6.2 传感器量程与ADC量程匹配
很多刚入门的朋友会买一个5kg的传感器,却用10kg的满量程来标定,甚至把传感器超量程使用。压力传感器有一个线性工作区,超出量程后输出电压不再随重量线性变化,就算软件标定得再准,重量大了照样差。更严重的是超量程会让弹性体产生永久变形,传感器最终报废。
选传感器时还要算一遍,确保满量程输出经过增益放大后不会超出ADC参考电压。5V激励电压、2mV/V灵敏度的传感器,满量程输出10mV,经过128倍放大是1.28V,在HX711内部参考电压范围内,没问题。但如果是那种满量程输出比较高的特种传感器,128倍增益之后可能削波,这时要切成64倍增益。
6.3 热漂移与零点漂移
设备连续运行半小时以上,很多传感器会随着温度升高出现零点飘移,这是传感器弹性体和应变片的物理特性决定的,不是电路问题。解决思路有两种:一是尽量让设备工作在稳定的环境温度里,不要在窗户边或者发热源旁边;二是在软件里增加“去皮”功能,或者定期自动采集空载数据刷新零点。
我在实际项目中做过一个简单开机自动去皮:上电后假设秤盘上没东西,等系统稳定后自动记录零点。但这个逻辑有个前提,必须确保上电时秤盘是空的,否则程序会把东西的重量当成零点。更稳妥的做法是加一个独立按键,用户按一下执行去皮,这样才能应对所有场景。
6.4 模块版本与供电电压差异
市面上HX711模块版本很多,有些用5V供电,有些做了电平转换支持3.3V。STM32F103C8T6的引脚耐压是5V兼容的,但如果用的是STM32G0系列或者GD32的某些型号,IO不一定耐压5V,那就需要确认模块电平是否兼容。另外,HX711模块板载稳压芯片不同,最低工作电压也不同,有些模块3.3V供电时传感器激励电压会低于标称值,导致满量程输出偏小,影响标定精度。
如果发现标定完的scale在不同供电电压下偏差很大,检查一下模块上有没有跳线或焊盘可以切换参考电压。没有的话,就固定用一个电压供电,不要今天3.3V明天5V。
6.5 给新手的最后建议
这套方案跑通之后,建议再做三件事:把读取时间统计一下,看看一次完整读取加滤波周期是多少,方便做采样频率规划;把异常数据识别逻辑加进去,比如重量突变超过阈值就标记为无效;最后就是多做几组不同重量的标定测试,验证线性度。HX711这个模块虽然便宜,但用好了可以承担很精确的称重任务。我自己测下来,做好滤波和标定之后,1kg量程的模块静态精度做到±0.5g以内是没问题的。
当然,调试过程中还会遇到一些看似莫名其妙的问题,比如读出来永远是0、或者永远是满量程。遇到这种先别急着改代码,用万用表量一下传感器E+和E-之间的电压有没有正常建立,再量A+和A-之间的差分电压在不同负载下有没有变化。传感器没输出,软件写得再好也白搭。
本文还有配套的精品资源,点击获取