简介:面向嵌入式入门及电子秤方案开发者,这份基于STM32与HX711的电子秤工程提供了从传感器信号采集到重量显示的一整套代码实现,可直接作为小量程称重产品的原型参考。资源共98个文件,其中包含44个C语言源文件和44个头文件,另有Keil工程配置(uvprojx/uvoptx)、启动文件、清理脚本及说明文档,压缩包仅498KB,目录划分清晰,便于按模块阅读和移植。
程序基于STM32F4系列,通过GPIO模拟HX711的SCK与DOUT时序,实现高精度A/D数据读取;代码中加入限幅平均滤波算法,有效抑制称重噪声,同时提供去皮和比例调整功能,方便用户校准零点与量程。工程内还包含系统时钟、串口、延时等基础驱动模块,适合希望在真实代码中理解HX711通信协议、GPIO操作及数字滤波处理的开发者学习使用。目前该资源已有2980人学习,是一份高性价比的STM32称重应用示例。
1. 先把HX711和电子秤的原理搞明白
1.1 HX711到底是什么,为什么要选它
STM32做电子秤,最绕不开的一个芯片就是HX711。很多刚接触称重项目的朋友第一反应是“直接用STM32的ADC不就行了吗”,真这么做的人基本都栽了跟头。原因很简单,称重传感器输出的差分信号只有毫伏级别,而STM32内置ADC的分辨率是12位,参考电压通常是3.3V,算下来一个LSB对应大约0.8mV,连传感器满量程输出的十分之一都分辨不出来,更别提做个精度还过得去的电子秤了。
HX711是专门为桥式传感器设计的24位高精度ADC芯片,内部集成了低噪声可编程增益放大器(PGA)、稳压电源和一个串行通信接口。它可以直接给传感器的电桥供电,同时把微弱的差分信号放大到可以被24位ADC采样的范围,再将结果通过一个简单的两线接口输出给单片机。说白了,HX711就是专门干“把微小重量变化变成数字”这件事的,省掉了外面一堆运放和基准源,成本几块钱,稳定性却比用分立元件搭的方案好得多。
这也是为什么你在淘宝上搜称重模块,十个里有八个都是HX711。它和STM32的搭配几乎是教科书级的组合:HX711负责模拟前端的数据采集,STM32负责数据处理、显示、按键、通信这些所有“不擅长模拟电路”的活儿。
1.2 称重传感器(应变片桥)的基本原理
理解了HX711,还得知道它前面接的传感器是个什么东西。绝大多数电子秤用的是电阻应变式称重传感器,内部核心是一个惠斯通电桥,由四个应变电阻组成。当传感器受到压力发生微小形变时,电桥两臂的电阻值发生变化,导致差分输出端的电压不再平衡,这个电压差就和施加的压力成正比。
这个电压差有多小呢?以常见的5kg量程传感器为例,满量程输出灵敏度通常在1mV/V左右。如果激励电压是5V,满量程差分输出也只有大约5mV。你没看错,5kg的力压上去,输出才5毫伏。所以必须经过HX711内部那级增益可选的放大器(常见配置是128倍增益)把信号放大到ADC可采样的范围。
搞清楚这一层关系,后面对代码里的“原始值”心里就有底了。你在程序里读到的那个整数,并不是重量,而是经过放大、采样、量化后的原始AD值。把它变成克数,需要经过标定,这部分后面专门讲。
2. 硬件连接与模块选型
2.1 引脚定义与接线(以STM32F103C8T6为例)
HX711模块的引脚非常少,一共就两排,传感器接口四个脚(E+、E-、A+、A-),单片机接口四个脚(VCC、GND、SCK、DOUT),外加一个数字电源VCC。其中E+和E-是给传感器电桥供电的,A+和A-是接收差分信号的输入。
以最常见的STM32F103C8T6蓝色Pill板为例,推荐接线如下:
| HX711引脚 | STM32引脚 | 说明 |
|---|---|---|
| VCC | 5V或3.3V | 建议5V,传感器激励电压高,信噪比更好 |
| GND | GND | 必须共地 |
| SCK | PA0(任意GPIO) | 时钟输入,由STM32产生脉冲 |
| DOUT | PA1(任意GPIO) | 数据输出,HX711转换为电平信号给STM32 |
| E+ | 传感器红线 | 电桥正激励 |
| E- | 传感器黑线 | 电桥负激励 |
| A+ | 传感器绿线 | 差分信号正 |
| A- | 传感器白线 | 差分信号负 |
这里有个容易搞错的细节:很多HX711模块上面已经自带了传感器接口的丝印,你只需要把传感器的四根线按颜色插上去就行。但不同厂家的传感器线色定义不完全统一,最可靠的办法是用万用表测一下:红黑两线之间的电阻最大(通常是激励端),绿白两线之间电阻其次(信号端)。别光看颜色就往上怼,我已经见过好几次因为线序接反导致读数一直是负数的案例了。
2.2 供电与布线的几个坑
硬件上最容易出问题的不是接线对不对,而是电源和噪声。HX711虽然内部有稳压,但对外部电源的纹波还是比较敏感的。如果你用开发板的3.3V LDO供电,板载稳压芯片本身就带一定纹波,读数会出现周期性的缓慢漂移。实测下来,直接用USB 5V给HX711的VCC供电,比用3.3V供电的稳定性要好一截,因为传感器激励电压高了,同样的应变对应的差分信号幅度更大,相当于变相提高了信噪比。
另外一个容易踩的坑是SCK和DOUT的线长和走线。这两根线如果太长,或者和电机、继电器这类大电流线路靠得太近,容易引入共模干扰,导致数据偶发跳变。解决办法有两个:一是尽量缩短HX711到传感器之间的线缆,二是SCK和DOUT上各加一个10kΩ上拉电阻到3.3V,用更强的驱动能力抵抗干扰。
注意:HX711的DOUT输出的是开漏信号,虽然很多模块上已经自带上拉,但如果你用的是那种裸芯片方案,上拉电阻必须自己加,否则STM32读到的一直是低电平。
3. 软件编写:从零写HX711驱动
3.1 驱动代码:时序理解是关键
HX711的通信协议是两线制的,一个时钟线SCK,一个数据线DOUT。工作流程是这样的:平时DOUT保持高电平,当HX711内部完成一次AD转换后,DOUT会拉低,告诉单片机“数据准备好了”。单片机检测到DOUT变低后,开始向SCK发送25个脉冲,前24个脉冲对应读出24位数据(高位在前),第25个脉冲决定下一次转换的通道和增益。
这里有个很关键的细节:脉冲的宽度不能太短。HX711数据手册上给出SCK高电平和低电平的最小时间是0.2微秒,你用STM32的GPIO翻转来产生脉冲,完全能满足时序要求,不需要用到什么高级外设。但如果你的系统主频跑得很高,比如168MHz的F4系列,建议在每次翻转之间加几个空指令或者使用短延时,避免脉冲太窄导致误码。
驱动代码的写法有很多种,标准库和HAL库都有,本质就是对两路GPIO的读写。下面给出一个精简但完整可用的驱动,以标准库为例:
#include "hx711.h" // 初始化GPIO:SCK为推挽输出,DOUT为浮空输入或上拉输入 void HX711_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // SCK - PA0,推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // DOUT - PA1,上拉输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_0); // SCK默认高电平 } // 读取一次AD值,返回24位有符号数 int32_t HX711_Read(void) { uint32_t data = 0; uint8_t i; // 等待DOUT变低,表示数据准备好 while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1)); for (i = 0; i < 24; i++) { GPIO_SetBits(GPIOA, GPIO_Pin_0); // SCK拉高 data = data << 1; GPIO_ResetBits(GPIOA, GPIO_Pin_0); // SCK拉低 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1)) { data++; } } // 第25个脉冲:设置下次转换通道和增益(此处选择A通道,128倍增益) GPIO_SetBits(GPIOA, GPIO_Pin_0); GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 24位数据以补码形式存储,转换为有符号数 return (int32_t)(data ^ 0x800000) - 0x800000; }这段代码有个小细节值得说一下:数据读取是在SCK拉低之后去读DOUT的电平,而不是拉高的时候读。因为HX711是在SCK的下降沿把数据移出到DOUT上的,你在SCK低电平期间读DOUT才能保证读到的是稳定的当前位。很多初学者喜欢在拉高后马上读,也能工作,但时序上不够稳健,在干扰大的环境里更容易出错。
如果你用的是HAL库,逻辑完全一样,把GPIO的读写替换成HAL_GPIO_ReadPin和HAL_GPIO_WritePin就行。另外,网上有的驱动加了一个超时检测,防止DOUT一直不拉低导致程序卡死在while循环里,这个建议加上,特别是量产项目,一颗传感器虚焊就能让整个系统死等。
3.2 24位有符号数的处理
HX711输出的24位数据是二进制补码形式,最高位是符号位。在存储进uint32_t变量时,如果不做处理,直接当无符号数用,会得到0xFFFFFF(正数)和0x800000(负数)这种值,无法区分正负。所以上面代码里用了(data ^ 0x800000) - 0x800000这个技巧:先翻转符号位,再减去0x800000,实际上就是把24位补码转换成了标准的32位有符号整数。
实际测试中,空载时HX711读数一般在几十万到几百万之间,具体数值取决于传感器量程和增益。读数可能为正也可能为负,因为电桥输出不一定是绝对零点,上一节提到的线序接反也会导致符号翻转。千万不要在程序里假设读数永远是正数,否则去皮功能做出来会非常别扭。
4. 标定与滤波:让数据真正稳定
4.1 两点标定法:把AD值变成克数
拿到AD原始值之后,接下来要做的事情就一个:找到AD值和真实重量之间的线性关系。HX711配合应变式传感器,在量程范围内线性度非常好,所以用两个点的标定就足够了,这就是所谓的“两点标定法”。
步骤很简单:
- 在空秤状态下读取一个稳定的AD值,记为zero_value。
- 放上一个已知重量的砝码(比如500g),读取AD值,记为weight_value。
- 计算比例系数:
scale = (weight_value - zero_value) / 500.0,单位是“每克对应的AD值”。 - 之后任意时刻的重量为:
weight = (current_value - zero_value) / scale。
这里有几个容易出问题的地方。首先,砝码的重量越接近满量程越好,比如5kg的传感器,最好用2kg或3kg的砝码标定,这样计算出的scale更准确。如果只用50g的小砝码标定,传感器本身的非线性误差会被放大到整个量程。其次,标定砝码要放在传感器受力正中心的位置,偏心加载会引入额外的弯矩,导致标定结果偏大或偏小。
我个人习惯把zero_value和scale两个参数保存在STM32内部Flash的最后几个扇区,这样每次上电不需要重新标定。注意写入Flash之前要确认扇区没有被程序代码占用,F103C8T6的Flash一共64KB,把自己写的参数放在最后一个页(0x0800FC00附近)是比较安全的做法。
4.2 数据滤波方案:别让读数像心电图
电子秤是一个对稳定性要求很高的系统,而HX711的裸数据在无载荷状态下也会有正负几个码的跳动。如果是几百克的微小重量,几个码的跳动可能对应一两克,肉眼可见地跳数字,非常影响体验。所以滤波是必不可少的环节。
最常见的做法是滑动平均滤波,维护一个长度为N的环形缓冲区,每次采样后计算平均值。N取8到16之间比较合适,太小了滤波效果不明显,太大了反应迟钝。注意电子秤的这个“迟钝”是需要刻意保留的:称重场景下,我们希望显示值平滑过渡,而不是像体温计一样跳来跳去,所以稍微牺牲一点响应速度是可以接受的。
下面给一个简单的滑动平均滤波实现:
#define FILTER_LEN 10 int32_t filter_buffer[FILTER_LEN]; uint8_t filter_index = 0; int32_t filter_sum = 0; int32_t HX711_Filter(int32_t new_value) { filter_sum -= filter_buffer[filter_index]; // 去掉最旧的数据 filter_buffer[filter_index] = new_value; // 写入新数据 filter_sum += new_value; filter_index = (filter_index + 1) % FILTER_LEN; return filter_sum / FILTER_LEN; }如果你发现滑动平均之后读数仍然不够稳,可以考虑配合中值滤波使用——先取5个原始值排序取中间值,再对这个中间值做滑动平均。这种组合滤波在称重场景下效果很好,但代价是单次转换时间变长(HX711的转换速率默认10Hz,每次读取需要100ms),如果整个系统只需要每秒更新几次显示,完全够用。
注意:特别追求速度的场景,比如动态称重,可以调节HX711的速率选择引脚(RATE引脚拉高可到80Hz),但代价是噪声明显变大,滤波效果也会打折扣。静态称重基本都用10Hz就够了。
5. 常见问题与排查实录
5.1 读数为0、数据乱跳、数值为负——一张表说清楚
做一个电子秤项目,十个有九个的问题都出在“数据看起来不对”上面。我把这几年被问得最多的问题整理成了一个速查表,基本覆盖了HX711项目里80%的坑:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读数一直为0 | DOUT始终为高,GPIO配置错误或芯片未供电 | 用万用表量HX711的VCC和GND;检查DOUT引脚配置是否为输入模式 |
| 读数一直满量程/0x7FFFFF | SCK和DOUT接反了 | 对照原理图重新检查接线 |
| 读数在正负之间乱跳 | 供电不稳、传感器线缆过长、接触不良 | 改用5V供电,缩短传感器线缆,检查焊接点 |
| 读数为负 | 传感器信号线A+/A-接反了 | 交换绿白两根线 |
| 数据稳定但重量不对 | 标定系数计算错误 | 重新用标准砝码标定,并确认标定时读数是否已稳定 |
| DOUT一直为低,程序卡死在等待循环 | HX711损坏、SCK没有正常拉高拉低 | 用示波器查看SCK是否有脉冲,若无则检查GPIO是否被复用 |
其中“DOUT一直为低”这个问题最隐蔽,因为HX711转换完成后DOUT会被拉低,如果程序在读完一次后没有正确发送第25个脉冲,HX711会一直认为数据还没被读走,DOUT就一直保持低电平,下一次读取就会卡死。所以如果你改过驱动代码,一定要检查第25个脉冲是否完整执行。
5.2 一个真实的排查案例
我自己做过一个项目,客户反馈称重数据每隔几秒就跳变一次,幅度有十几克。一开始怀疑是传感器质量问题,换了好几个都没解决。后来用示波器挂在HX711的电源引脚上才发现,每当板上的继电器吸合时,电源就有一个明显的跌落尖峰。原因是继电器和HX711共用了同一路5V电源,继电器线圈启动瞬间电流高达上百毫安,直接把供电电压拉下去了。
后来在HX711的VCC和GND之间加了一个100μF电解电容和一个0.1μF陶瓷电容,并把继电器的供电单独走线,问题就消失了。这个案例说明,HX711这类模拟前端芯片,对电源纯净度的敏感程度远超你的预期。PCB布板时,给HX711的电源引脚加一个RC滤波,甚至用一颗单独的LDO供电,都是值得的投入。
另外一个常见问题是开发板上电时STM32的GPIO默认状态。很多STM32的引脚在上电瞬间是浮空状态,如果它恰好和HX711的SCK接在一起,可能在初始化之前产生乱脉冲,导致HX711进入错误状态。解决方法是上电后在初始化代码里先把SCK引脚拉低并配置为推挽输出,再去初始化DOUT为输入。这样能保证HX711在复位后处于一个已知的静止状态。
6. 从电子秤到更多场景的一些经验
6.1 去皮功能的实现细节
电子秤必备的功能之一就是去皮。硬件上,去皮就是一个按键,按下后把当前读数作为新的零点。但软件实现有个坑:如果去皮后直接修改zero_value,会导致重量显示出现跳变。正确做法是维护一个tare_offset变量,清零时把当前读到的AD值累加到tare_offset上,实际重量用(current_value - tare_offset - zero_value) / scale计算。这样去皮前后的显示值连续,不会被“重置到零”的过程打断。
去皮功能还有个边界情况要考虑:如果秤台上放了超过满量程的物体,去皮的零点本身就超载了,显示出来的重量就会失真。所以量产产品里一般会在去皮前判断当前读数是否在正常范围内,超出量程则拒绝去皮并给出提示。
6.2 给自己的项目留一点扩展空间
做完一个HX711电子秤,你会发现自己已经掌握了一整套“弱信号采集”的套路:传感器选型、模拟前端设计、ADC时序驱动、数据处理与标定、系统抗干扰。这套东西换个传感器就能用在很多其他项目里,比如拉力计、压力计、液位检测,甚至简易的电子天平。
从代码架构的角度,建议把HX711的驱动、滤波、标定分成独立模块,用接口和上层业务解耦。这样后面如果想换更高精度的ADC芯片,比如ADS1232或者MCP3561,只需要替换最底层的驱动文件,上层显示、按键、通信完全不用动。我自己的项目就是先基于HX711跑通了整个系统,后来客户要求更高精度,换硬件后只改了驱动文件里读数的部分,基本算是无缝切换。
做嵌入式的过程中我最大的体会是,像电子秤这种看似简单的项目,真正动手做一遍,能在模拟前端、时序、软件架构、抗干扰设计这些方向上积累很多课本上学不到的经验。你踩过的每一个坑,最后都会变成下一块板子上的设计准则。
本文还有配套的精品资源,点击获取