1. 从零认识你的电子秤“大脑”:HX711 AD芯片
大家好,我是老张,一个在嵌入式圈子里摸爬滚打了十多年的老工程师。今天想和大家聊聊一个特别有意思的小东西——HX711。别看它只是个小小的24位AD芯片,价格也便宜,但在做高精度电子秤、厨房秤这些小玩意儿时,它可是绝对的核心“大脑”。很多新手朋友一听到“24位AD”、“差分放大”这些词就头大,觉得门槛太高。别怕,今天我就用最“人话”的方式,带你从零开始,把它玩转。
简单来说,HX711就是一块专门为称重传感器这类“小信号”设计的芯片。什么叫小信号?我们的称重传感器,在你放上一杯水或者一个苹果时,它输出的电压变化可能只有几十个微伏(uV),比蚊子哼哼的声音还微弱。普通的单片机,比如STM32自带的12位ADC,能分辨的最小电压变化大概在0.8毫伏(mV)左右,对于微伏级的信号,它根本“听不见”。这时候,HX711的作用就来了:它内部自带了一个可编程的放大器,能把传感器那微弱的“哼哼”信号,放大32倍、64倍甚至128倍,变成一个强壮清晰的“喊话”。然后,再用它高精度的24位ADC,把这个放大后的信号转换成数字量,送给我们的主控芯片(比如STM32)去处理。
我刚开始接触它的时候,也觉得数据手册上那些时序图、增益选择有点复杂。但实际用起来你会发现,它的设计非常贴心,把很多复杂的模拟电路都集成到了芯片内部。你只需要接很少的外围元件,甚至只用两颗电阻,就能给它和传感器提供一个稳定的工作电压。这大大降低了我们硬件设计的难度和出错的概率。对于想入门嵌入式、做个实物项目练手的朋友来说,用HX711搭配STM32做一个电子秤,绝对是性价比超高、成就感满满的选择。你不仅能学到模拟信号采集、数字滤波、传感器标定这些硬核知识,还能得到一个实实在在、能显示读数的小工具,放在家里称个咖啡豆、小零件,别提多实用了。
2. 硬件连接:手把手搭建你的电子秤“骨架”
理论说再多,不如动手接上线。这一部分,咱们就来实实在在的把硬件搭起来。你需要准备的东西不多:一块STM32最小系统板(我用的是常见的STM32F103C8T6,也就是“蓝板”)、一个HX711模块(某宝上几块钱一个,自带稳压和滤波,强烈建议新手直接买模块,省心)、一个称重传感器(常见的是5kg量程的铝合金传感器,带HX711接口)、一个0.96寸的OLED显示屏(I2C接口),再加上几根杜邦线就够了。
首先是电源,这是保证精度的关键。很多朋友在这里踩过坑。HX711模块和称重传感器最好用5V供电。为什么?因为传感器的输出信号幅度和供电电压是成正比的。5V供电能让传感器输出更大的信号范围,被HX711放大后,信噪比更好,最终读数更稳定。而我们的STM32和OLED屏,通常用3.3V。所以,如果你的开发板有独立的5V输出引脚(比如USB输入经过稳压后的5V),就用它给HX711模块供电。如果没有,用一个外部的5V电源适配器也行,但要确保和STM32共地。STM32和OLED屏则用3.3V。切记不要用STM32的3.3V去给HX711和传感器供电,否则灵敏度会大打折扣,而且可能驱动能力不足。
接着是信号线连接,非常简单:
- HX711模块 → STM32:模块上通常有四个引脚:VCC、GND、DT(DOUT)、SCK。VCC接5V,GND接STM32的GND。DT(数据线)接STM32的任何一个GPIO口,我习惯接
PB15。SCK(时钟线)接另一个GPIO口,我接PB14。这两个口在软件里配置成推挽输出和上拉输入就行。 - 称重传感器 → HX711模块:传感器一般有4根线:红(电源+)、黑(电源-)、白(信号+)、绿(信号-)。对应接到HX711模块的E+、E-、A+、A-上。颜色可能因厂家而异,但接线逻辑不变。
- OLED屏 → STM32:I2C接口的OLED就四根线:VCC(3.3V)、GND、SCL、SDA。SCL和SDA我分别接到STM32的
PB11和PB10上,这两个引脚需要配置为开漏输出模式并启用内部上拉。
硬件连好后,先别急着写代码。用万用表量一下各电源引脚电压是否正常,尤其是HX711模块的输出给传感器的电压(通常是4.3V左右)。确保没有虚焊、短路。硬件是软件的基石,这一步稳了,后面调试会顺利很多。
3. 深入核心:搞懂HX711的数据通信协议
线接好了,下一步就是让STM32和HX711“对话”。这个过程其实就是读写一串串的脉冲信号,理解了它的时序,你就掌握了HX711的命门。我当初就是在这里卡了好久,翻来覆去看数据手册,最后才摸清门道。
HX711采用了一种叫做“同步半双工”的通信方式。听起来高级,其实很简单。它有两根线和单片机交互:DOUT(数据输出)和PD_SCK(时钟输入)。DOUT是HX711发给单片机的,PD_SCK是单片机发给HX711的。通信的主动权完全在单片机手里:单片机通过控制PD_SCK引脚产生时钟脉冲,同时在每个脉冲的上升沿去读取DOUT引脚的状态,拼凑出24位的数据。
这里有几个必须注意的细节,也是新手最容易出错的地方:
- 等待就绪:在开始发时钟脉冲之前,必须先检查
DOUT引脚是否为低电平。只有当DOUT为低时,才表示HX711已经准备好了一次新的转换数据。如果DOUT是高,你就必须等待,并且在等待期间,PD_SCK必须保持为低电平。我见过有朋友的程序一上来就狂发时钟,结果读回来的全是乱码,问题就出在这里。 - 脉冲个数:读取一次数据,需要发送25到27个时钟脉冲。具体发多少个,取决于你选择的通道和增益:
- 通道A,增益128:发25个脉冲。前24个脉冲读取24位数据,第25个脉冲用于设置下次转换的参数。
- 通道A,增益64:发27个脉冲。
- 通道B,增益32:发26个脉冲。 脉冲数不能多也不能少,否则通信会错乱。我们做电子秤一般用通道A增益128,所以发25个脉冲就行。
- 数据格式:读回来的24位数据是二进制补码形式。这意味着数据的最高位是符号位。对于称重传感器,我们通常只关心正向压力,所以实际处理时,我会用一个
count = count ^ 0x800000的操作,将这个有符号的补码转换成一个0~16777215之间的无符号正数,这样后续计算重量就方便多了。
为了让你更直观地理解,我写了一个最基础的读取函数(基于HAL库),你可以看看里面的每一步:
uint32_t HX711_Read(void) { uint32_t count = 0; uint8_t i; HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_RESET); // SCK拉低 delay_us(1); // 短暂延时 // 1. 等待DOUT变低(就绪) while (HAL_GPIO_ReadPin(DOUT_GPIO_Port, DOUT_Pin) == GPIO_PIN_SET); // 2. 发出24个时钟脉冲,并读取数据 for (i = 0; i < 24; i++) { HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_SET); // SCK拉高(上升沿) delay_us(1); count = count << 1; // 数据左移,为新位腾出空间 HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_RESET); // SCK拉低 delay_us(1); if (HAL_GPIO_ReadPin(DOUT_GPIO_Port, DOUT_Pin) == GPIO_PIN_SET) { count++; // 如果DOUT为高,则该位为1 } } // 3. 发出第25个脉冲,选择下次为通道A增益128模式 HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_SET); delay_us(1); count = count ^ 0x800000; // 补码转无符号正数(关键步骤!) HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_RESET); delay_us(1); return count; }这个函数里的delay_us可以用STM32的定时器精确实现,也可以简单用空循环替代(精度要求不高时)。把这段逻辑吃透,你的HX711驱动就成功了一大半。
4. 从原始数据到精准重量:标定与滤波算法实战
当你成功从HX711读出一串数字,比如856723时,你可能会懵:这离“克”这个单位还差得远呢。没错,这一步就是要把这个原始的AD值,转换成我们看得懂的重量。这个过程叫做标定,它是整个电子秤项目成败的关键,也是最能体现工程师经验的地方。
首先,我们需要理解一个概念:皮重(Tare)。即使秤盘上什么都没放,由于传感器本身的制造误差、安装应力、电路偏移等原因,HX711也会读出一个非零的AD值。这个值就是皮重。所以,在称重前,我们必须先“去皮”。方法很简单:在空秤状态下,连续读取多次HX711的值(比如100次),然后取平均值,把这个值存起来,记为Weight_Maopi。以后每次称重时,读到的原始值减去这个皮重,得到的就是物体产生的净AD值。
接下来是标定。你需要一个已知重量的标准砝码,比如100克。把砝码稳稳地放在秤盘上,读取此时的净AD值(减去皮重后的值)。假设你读到的值是41200。那么,一个简单的线性关系就建立了:41200个AD值 对应 100克。由此可以算出比例系数(我常叫它“GapValue”):GapValue = 41200 / 100 = 412。这意味着,在这个系统里,每412个AD值的变化,大约代表1克重量的变化。
于是,你的重量计算函数就可以这样写:
s32 Get_Weight(void) { uint32_t current_ad = HX711_Read(); if(current_ad > Weight_Maopi) { int32_t net_ad = current_ad - Weight_Maopi; // 净AD值 int32_t weight = (int32_t)((float)net_ad / GapValue); // 计算重量 return weight; } return 0; }但事情还没完。你会发现,直接这样算出来的重量,数值会在真实值附近跳来跳去,很不稳定。这是因为存在各种干扰:电源纹波、空气流动、微小的震动等等。这就需要引入数字滤波。对于电子秤这种慢变信号,一个简单又有效的办法是滑动平均滤波。我常用的方法是:开辟一个数组,缓存最近10次的重量计算结果,每次显示时,都取这10个值的平均值。代码大概长这样:
#define FILTER_LEN 10 int32_t weight_buffer[FILTER_LEN] = {0}; uint8_t buffer_index = 0; int32_t Get_Filtered_Weight(void) { int32_t raw_weight = Get_Weight(); // 获取原始计算重量 weight_buffer[buffer_index] = raw_weight; // 存入缓冲区 buffer_index = (buffer_index + 1) % FILTER_LEN; // 索引循环 int64_t sum = 0; for(int i=0; i<FILTER_LEN; i++) { sum += weight_buffer[i]; } return (int32_t)(sum / FILTER_LEN); // 返回平均值 }实测下来,经过这样的滤波处理,重量读数会变得非常稳定,几乎看不到跳动。你还可以尝试更高级的滤波算法,比如一阶低通滤波,但滑动平均对于入门项目来说,已经完全够用且效果显著。
5. 让结果一目了然:OLED屏驱动与界面设计
辛苦了半天,数据总算算准了,总不能一直靠串口打印看重量吧?是时候请出我们的“脸面”——OLED显示屏了。0.96寸的OLED屏,分辨率128x64,功耗低、显示清晰,通过I2C总线与STM32连接,只需要两根数据线,非常节省IO口。
驱动OLED屏,你不需要从零开始写底层驱动。网上有大量开源的OLED驱动库,比如SSD1306的驱动。你只需要把对应的.c和.h文件添加到你的工程里,根据你的硬件连接,修改一下I2C的读写函数或者引脚定义,就能轻松调用OLED_ShowString、OLED_ShowNum这样的函数来显示内容了。
对于电子秤的界面,我的设计原则是:简洁、清晰、信息明确。通常我会把屏幕分为三个区域:
- 标题区:屏幕顶部显示“电子秤”或“Weight Scale”这样的固定标题。
- 主显示区:屏幕中央用最大的字体显示当前重量,单位是“g”。这是视觉焦点。
- 状态区:屏幕底部可以显示一些状态信息,比如电池电量图标、单位切换提示(g/kg),或者一个稳定的指示标志。
这里有个实用的小技巧:为了提升用户体验,我通常会在重量显示上做一点动画效果。比如,当重量发生变化时,新数值不是“唰”一下直接跳变,而是有一个快速的数字递增或递减的滚动效果。虽然只是视觉上的小把戏,但能让你的电子秤看起来更“高级”、更“流畅”。实现起来也不难,就是在更新显示前,判断一下新旧数值的差值,然后以一定的步进,分几次显示中间值,最后到达目标值。
另外,记得在程序里合理控制屏幕刷新频率。重量数据我们可能每100ms更新一次,但屏幕不需要刷新得这么快。可以设置一个500ms的定时刷新,或者只在重量值确实发生变化时才刷新屏幕,这样可以降低系统功耗,尤其是在电池供电的场景下。
6. 工程整合与进阶优化:打造一个稳健的系统
前面我们把各个模块都逐个击破了,现在要把它们整合到一个完整的、健壮的系统里。这不仅仅是把代码堆在一起,而是要思考如何让它们协调工作,并且处理各种异常情况。
首先,我建议你使用状态机的思想来组织主程序。电子秤的工作流程可以抽象成几个状态:上电初始化->等待稳定(去皮)->稳定称重->超载报警。用一个状态变量来记录当前状态,在while(1)主循环里根据状态执行不同的操作。这样写出来的代码结构清晰,易于维护和扩展。比如,在“等待稳定”状态,你可以连续采样一段时间,判断数据波动是否小于某个阈值,从而确认空秤状态是否稳定,然后再进行去皮操作。
其次,增加按键功能。除了显示重量,一个实用的电子秤至少需要两个按键:一个“去皮/归零”键,一个“单位切换”键(克/盎司)。按键处理要加入消抖,并且最好用中断或定时扫描的方式,避免阻塞主循环。当按下“去皮”键时,就调用之前写的Get_Maopi函数,更新皮重值。
异常处理是区分业余作品和专业作品的重要一环。你的代码里应该考虑到:
- 传感器断线检测:如果HX711的
DOUT线一直为高电平(等待超时),或者读回来的AD值长时间为0或满量程,可能是传感器或HX711损坏、断线了。此时应该在OLED屏上显示“Err Sensor”之类的提示。 - 超量程保护:当计算出的重量超过你传感器量程的110%时(比如5kg的传感器,超过5.5kg),应该停止增加显示,并闪烁显示“OVER”警告,防止传感器永久损坏。
- 电源管理:如果你用的是电池,可以增加一个简单的电量检测电路(电阻分压接到ADC),在屏幕上显示电量图标。当电压过低时,提示“Low Bat”并进入休眠模式。
最后,是关于精度再提升的进阶思考。如果你发现线性度还不够好(比如100g很准,但500g偏差较大),可以考虑进行两点标定。即用两个不同重量的砝码(如100g和500g),建立一次线性方程(y = kx + b),而不是简单的单点比例。这能更好地补偿传感器的非线性误差。代码上就是解一个二元一次方程组,求出k和b。虽然稍微复杂点,但对于追求精度的你来说,绝对是值得的。
把这些都做好,你的电子秤就不再是一个简单的课堂实验,而是一个接近产品级的、可靠实用的小工具了。我把自己在项目中反复调试、踩坑后总结出的核心源码框架都分享了出来,你可以基于它去搭建、去修改、去创造。嵌入式开发的乐趣,就在于这种从无到有、把想法变成实物的过程。希望这篇指南能帮你少走些弯路,顺利点亮你的第一个高精度电子秤。如果在实际操作中遇到什么问题,不妨多看看数据手册,多调试一下时序和参数,那种解决问题的成就感,才是最棒的。