ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32与HX711称重传感器电子秤设计:从硬件接线的校准滤波实战

STM32与HX711称重传感器电子秤设计:从硬件接线的校准滤波实战 在嵌入式小项目里用STM32做称重系统是我觉得性价比最高、最适合练手的组合之一。一块STM32F103C8T6最小系统板、一个HX711称重模块、一只5kg量程的桥式称重传感器、再加一块0.96寸OLED屏几十块钱就能搭出一台完整的电子秤称得准、显示清楚、还能随时扩展通信和存储功能。很多朋友看到HX711就以为它跟普通ADC一样直接用结果读出来的数据要么稳定不下来要么标定完还是飘最后只能放弃。这个项目不挑单片机基础GPIO会了就能起步真正难的是把HX711的数据读稳、校准靠谱、OLED界面不花屏而这几点恰好是很多教程一笔带过的地方。我打算从硬件接线讲起把HX711的单线数据时序、SSD1306驱动和最后的核心校准逻辑完整走一遍适合正在做单片机课设、毕业设计或者想入门传感器信号采集的读者参考。1. 项目整体设计与硬件选型思路1.1 为什么是HX711而不是单片机的内置ADC先明确一个核心问题STM32自带的ADC已经是12位如果只是做一个粗略的“有没有重量”检测确实够用。但要称出“多少克”12位的分辨率远远不够。市面常见的桥式称重传感器比如5kg量程、灵敏度2mV/V在5V激励电压下满量程输出大约只有10mV。STM32的12位ADC参考电压3.3V时一个LSB对应约0.8mV10mV信号换算下来只有12个LSB左右相当于5kg范围只能分约400g一档这种精度连“称着玩”都不够看。HX711方案就完全是另一个思路它内部是一个24位差分ADC配合可编程增益放大器PGA增益最高可设128倍。同样是5kg传感器10mV信号经过128倍放大后变成1.28V再交给24位ADC量化分辨率能轻松做到零点几克。再加上HX711的接口只需要DOUT和SCK两个引脚复用GPIO模拟时序就能通信价格也就两三块钱没有理由不用它。顺带提一句这个项目里STM32本身不是高精度称重的瓶颈它的角色更多是“读数据、做运算、管显示”。所以哪怕你用STM32F103的入门型号72MHz主频也绰绰有余真正的难点在通信时序和校准逻辑上。1.2 系统架构与器件清单整个系统的数据流是这样的称重传感器把重力变化转成微弱的差分电压HX711把这个电压放大并数字化STM32通过GPIO模拟时序把数据读出来经过滤波和校准换算成克最后在OLED上显示同时通过串口输出到上位机方便调试。我这次用的器件清单如下器件型号/规格说明主控STM32F103C8T6最小系统板蓝板/黑板均可64KB Flash称重传感器5kg桥式传感器半桥/全桥带应变片的铝制传感器ADC模块HX711模块淘宝常见红绿板或蓝板带稳压显示屏0.96寸OLEDSSD1306驱动I2C四针版128x64分辨率按键轻触按键x2用于零点校准和系数校准电源USB转TTL或电池建议5V供电OLED用3.3V其他杜邦线若干、砝码500g/1kg砝码用于标定系数没有的话用已知重量的水瓶替代接线方面我用的引脚分配是PC13和PC14接HX711的DT和SCK这里特意避开调试下载器占用的SWD引脚比如PA13/PA14免得下载程序时互相干扰。OLED我直接用软件I2CSCL接PB6、SDA接PB7因为软件方式不依赖硬件I2C外设不容易出现I2C卡死的问题。两个按键接PA0和PA1按下为低电平内部上拉即可。1.3 硬件接线的几个关键细节HX711模块的引脚从输出侧开始分别是VCC、GND、DT也叫DOUT、SCK传感器侧有E、E-、A、A-、B、B-。我把E接传感器红线和黑线E-接白线A接绿线A-接屏蔽线没有就不接。不同厂家的传感器线色可能不一样买回来第一件事就是用万用表测电阻判断哪对是激励端、哪对是信号端千万不要完全照抄网上某个教程的线色。电源处理要特别注意HX711模块上有稳压芯片一般支持3.3V到5V供电但传感器桥路的激励电压通常直接用模块的VCC。我实际测试时给HX711供5V比供3.3V的信号幅度更大稳定性也更好所以建议系统用5V给HX711供电OLED则接3.3V。OLED的VCC不能接5V时间太长虽然很多模块带了稳压但长期用还是按3.3V来。OLED的I2C地址默认是0x3C如果模块背面的SA0电阻焊了或没焊地址可能变成0x3D。程序里最好加一个“扫描地址”的调试步骤确认当前屏的地址再往下写不然会白白折腾半天。2. HX711通信协议与驱动实现2.1 HX711的时序原理看不懂协议就没法调稳HX711实际上就是个24位ADC外加串行接口。它和单片机之间没有地址、没有片选只有两根线DT和SCK。当HX711完成一次转换后会把DT拉低相当于告诉单片机“数据准备好了你赶紧来读”。单片机看到DT为低就开始在SCK引脚上产生时钟脉冲每产生一个下降沿HX711就把24位转换结果从高位到低位依次送到DT上。读满24个脉冲之后数据就全部取完了。但这里还有一个关键点HX711把第25个脉冲作为“通道和增益选择”的信号。第25个脉冲高电平持续期间HX711会根据脉冲总数决定下一次转换的通道和增益具体对应关系是25个脉冲选通道A增益12826个脉冲选通道B增益3227个脉冲选通道A增益64。代码里最简单的方式就是每次都发25个脉冲固定工作在通道A、增益128这也是称重传感器的标准用法。用生活化类比说HX711和单片机的通信节奏类似于门铃对讲DT拉低就是“按门铃”叫你去取数据SCK的24个脉冲就像24张被顺次递出来的卡片第25个脉冲是在为下一次“叫门”选择音量挡位。理解了这套“叫门—递卡—转档”的流程HX711就读通了。2.2 STM32驱动代码实现代码我基于标准外设库来写因为GPIO模拟的方式天然对HAL库也通用你只需要改一下引脚初始化宏即可。先定义引脚#define HX711_DT_IN() GPIO_ReadInputDataBit(HX711_PORT, HX711_DT_PIN) #define HX711_SCK_HIGH() GPIO_SetBits(HX711_PORT, HX711_SCK_PIN) #define HX711_SCK_LOW() GPIO_ResetBits(HX711_PORT, HX711_SCK_PIN) #define HX711_PORT GPIOC #define HX711_DT_PIN GPIO_Pin_13 #define HX711_SCK_PIN GPIO_Pin_14 #define HX711_IO_OUTPUT() /* SCK为输出 */初始化函数里把DT设为输入、SCK设为输出初始电平保持低void HX711_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Pin HX711_DT_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(HX711_PORT, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin HX711_SCK_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(HX711_PORT, GPIO_InitStructure); HX711_SCK_LOW(); }读取一次原始数据的核心函数如下uint32_t HX711_Read(void) { uint32_t data 0; uint8_t i; // 等待DOUT拉低表示转换完成、数据就绪 // 正常情况下最快几毫秒最慢约100ms while (HX711_DT_IN() SET); for (i 0; i 24; i) { HX711_SCK_HIGH(); delay_us(1); HX711_SCK_LOW(); delay_us(1); data 1; if (HX711_DT_IN() SET) { data; } } // 第25个脉冲切换回通道A、增益128 HX711_SCK_HIGH(); delay_us(1); HX711_SCK_LOW(); delay_us(1); // 数据是24位补码转成无符号便于计算 return data ^ 0x800000; }这里有几个经验点。第一read函数开头的while等待不能加超时保护我在调试时见过有人加了超时返回0然后重量数据偶发跳变找半天才发现是超时逻辑偷跑了。真要加超时建议用标志位记录返回0的重试次数而不是直接返回0了事。第二SCK高电平和低电平之间的延时至少1微秒HX711的数据手册要求SCK频率最高不能太高1us的延时在72MHz下用几个NOP循环就能实现。第三读回的数据是24位补码最高位是符号位转换成无符号数之后再减零点算出来的差值才是真正的重量增量。2.3 读数的坐标系和符号问题24位补码的理论范围是-8388608到8388607对应增益128下的差分输入范围大约-20mV到20mV。空载时读数并不固定会在某个中心点附近波动这个值就是零点偏移。放上重物后读数朝一个方向增加如果传感器线接反或者受力方向变读数可能朝反方向走。实际项目里不用关心补码的符号细节只要在初始化阶段把空载的零点值记录下来后续所有重量计算都用“当前读数减零点”的差值。这里有个容易踩的坑如果你的传感器接反了放重物时读数反而变小那零点减当前值算出来的重量会成为负数。遇到这种情况先把传感器A和A-两根线互换让放重物后读数增加再继续校准。3. OLED显示与重量界面设计3.1 SSD1306驱动要点和花屏原因排查OLED屏这部分0.96寸的SSD1306几乎成了标配I2C接口只有四根线焊接和接线都非常简单。SSD1306内部有128x64的显存也就是128列、64行每一列由8个像素组成一个字节总共1024字节。这块显存是掉电就丢的所以每次上电都要执行一整套显示初始化命令比如关闭显示、设置时钟分频、开启电荷泵、设置列地址扫描方向等。初始化命令序列必须完整漏掉任何一条都可能出现完全不亮、偏暗、花屏或显示错位的问题。我常用的初始化代码如下void OLED_Init(void) { delay_ms(100); OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 时钟分频 OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); // 复用率1/64 OLED_WriteCmd(0xD3); OLED_WriteCmd(0x00); // 显示偏移 OLED_WriteCmd(0x40); // 起始行0 OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 开启电荷泵 OLED_WriteCmd(0x20); OLED_WriteCmd(0x00); // 水平寻址模式 OLED_WriteCmd(0xA1); // 段重映射 OLED_WriteCmd(0xC8); // COM扫描方向 OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); // COM引脚配置 OLED_WriteCmd(0x81); OLED_WriteCmd(0xCF); // 对比度 OLED_WriteCmd(0xD9); OLED_WriteCmd(0xF1); // 预充电周期 OLED_WriteCmd(0xDB); OLED_WriteCmd(0x40); // VCOMH OLED_WriteCmd(0xA4); // 恢复显示 OLED_WriteCmd(0xA6); // 正常显示 OLED_WriteCmd(0xAF); // 开启显示 }如果OLED不亮优先检查I2C地址和电源。地址不对时初始化命令发出去但屏没有任何反应很多“OLED不亮”其实是地址0x3C和0x3D没区分。花屏则多半是初始化命令缩短了或者MCU复位后OLED没有复位建议上电延时至少100ms再发命令。还有一类问题是用硬件I2C时总线被卡死SCL和SDA一直为低这属于典型的I2C总线错误软件I2C基本不会碰到。3.2 字符与中文显示的实现方式SSD1306没有内置字库所以要显示数字和汉字就得自己准备字模。对于称重界面我的做法是准备两套字模一套8x16的ASCII字模用于显示数字和单位一套16x16的汉字字模用于显示“重量”、“校准”这类中文提示。字模可以用PCtoLCD2002或类似取模软件生成一般来说选“纵向取模、字节正序”的方式这样和OLED的显存布局是匹配的。每个8x16字符占16个字节每个16x16汉字占32个字节。显示时先设置页地址和列地址然后连续写对应字节到显存即可。核心的写点函数和字符显示函数大概是这样的结构void OLED_SetPos(unsigned char x, unsigned char y) { // y为页地址范围0~7 OLED_WriteCmd(0xB0 y); OLED_WriteCmd(((x 0xF0) 4) | 0x10); OLED_WriteCmd((x 0x0F) | 0x01); } void OLED_ShowChar(unsigned char x, unsigned char y, unsigned char ch) { unsigned char i; OLED_SetPos(x, y); for (i 0; i 8; i) { OLED_WriteData(F8X16[(ch - ) * 16 i]); } OLED_SetPos(x, y 1); for (i 0; i 8; i) { OLED_WriteData(F8X16[(ch - ) * 16 i 8]); } }需要说明的是设置列地址时我是按128列、8页的方式也就是水平寻址模式。写数据时OLED内部列地址会自动加1页地址在写完一整行后并不会自动跳到下一页所以跨页显示字符时需要像我上面那样分别设置两次页地址。3.3 称重界面布局与主程序整合称重界面我建议做成“大数字、只留一行辅助信息”的风格。OLED本身分辨率不高如果塞太多内容反而看不清。我的布局是第一行显示“Weight:”第二行用8x16字符放大显示5位数字第三行显示单位“g”底部偶尔显示校准状态。主循环的逻辑并不复杂伪代码是while (1) { if (按键1按下) { zero_offset HX711_Read(); // 记录当前空载读数 OLED_ShowString(0, 6, Zero OK); } if (按键2按下) { // 在传感器上放标准砝码比如500g standard_raw HX711_Read(); // scale (standard_raw - zero_offset) / 砝码重量 } raw 滤波后的读数; weight (float)(raw - zero_offset) / scale; OLED_ShowFloat(weight); printf(weight %.1f g\r\n, weight); delay_ms(100); }这里有个非常实用的细节称重循环里的延时不要超过200ms。HX711默认输出速率是10Hz也就是每100ms可以拿到一次新的转换结果。如果你延时太久读出来的永远是旧数据用户体验会非常差。用100ms到150ms的刷新间隔既能让滤波有足够的数据输入OLED也不会感觉闪烁。4. 称重校准与滤波算法实战4.1 校准原理两步搞定零点和系数HX711读回来的值是一个无符号整数这个值本身和“克”没有任何关系必须通过校准把ADC读数映射到真实重量。校准的核心公式只有两个变量weight (raw - zero_offset) / scale其中zero_offset是空载时的读数scale是每克重量对应的读数增量。校准的第一阶段是零点校准。放上空托盘等读数稳定后连续读十次取平均把这个平均值存成zero_offset。第二阶段是系数校准。在托盘上放一个已知重量的砝码比如500g同样等读数稳定后读十次取平均得到standard_raw然后算出scale (standard_raw - zero_offset) / 500单位是“每克对应的ADC计数”。举个例子我实测这块模块时空载零点大约是8387600左右放上500g砝码后读数变成8389210那么scale (8389210 - 8387600) / 500 3.22也就是每克约3.22个计数。再把读到的800元砝码或水瓶换算基本能精确到正负2克以内。如果你的传感器量程更大比如20kg灵敏度不变时scale会小很多所以传感器选型要匹配目标精度不能拿20kg称5g的东西。校准数据必须持久化保存。最简单的办法是存到STM32内部Flash的最后一个扇区上电时先读取Flash里的zero_offset和scale没有数据时再用默认值。不要每次上电都重新校准也别指望用户会用按键来校。这个环节在实际产品中是体验差距最大的地方。4.2 滤波算法选择中值加均值最稳HX711读数的波动来源有两个一个是传感器本身的应力和电磁干扰另一个是HX711转换结果本身的量化误差和抖动。直接把这个原始值换算成重量显示你会发现末位数字疯狂跳看着非常不专业。滤波我的首选不是简单平均值而是“中值滤波加滑动平均”的组合。先读5次原始值把这5个数排序后取中间那个这个中值能干掉突发的毛刺脉冲然后把最近10次中值结果再做一次滑动平均用来平滑传感器自重和机械振动的低频波动。这个组合在称重这种慢变信号上非常有效而且计算量极低。滤波代码实现如下static uint32_t filter_buf[10]; static uint8_t filter_index 0; uint32_t weight_filter(void) { uint32_t temp[5], mid; uint8_t i, j; for (i 0; i 5; i) { temp[i] HX711_Read(); } // 冒泡排序取中值 for (i 0; i 4; i) { for (j i 1; j 5; j) { if (temp[i] temp[j]) { mid temp[i]; temp[i] temp[j]; temp[j] mid; } } } // 中值进入滑动队列 filter_buf[filter_index] temp[2]; filter_index (filter_index 1) % 10; // 计算队列平均值 uint32_t sum 0; for (i 0; i 10; i) { sum filter_buf[i]; } return sum / 10; }判断滤波参数是否合适的方法很直观把滤波前后的原始值同时通过串口打印出来观察空载时的最大波动范围。如果空载时滤波前波动在正负5个计数滤波后能压到正负1个计数那就说明参数合理。如果波动还是大先把机械结构固定好不要用杜邦线悬空挂着传感器那不是算法能解决的问题。4.3 校准实操记录和避坑经验我实际调这块板子时记录过一组数据拿来给大家做个参照。空载时滤波后读数是8387621放上一个500g砝码后滤波后读数是8389235差值为1614这样scale3.228计数/克。随后我放上一瓶标称550ml的矿泉水读数差值为1776换算出来重量是550.2g偏差不到1g。这个数据说明只要传感器机械结构固定好HX711在5kg量程下做到±1g是完全可以达到的。但我也遇到过完全相反的体验传感器直接放在泡沫盒上箱子还在晃HG这个重量的读数就在20g范围内跳。后来把传感器底座换成铝型材托盘用硬纸板叠层固定读数立刻稳定下来。机械结构对高精度称重的影响远大于算法这是我反复栽坑后的最大体会。校准操作里还有两个细节。第一校准用的砝码最好压在传感器受力区域的中心位置跟实际使用习惯一致。如果日常是把物体放在托盘左侧那校准时也应该放在左侧否则会产生偏载误差。第二百分比误差公式记住一个大概概念500g砝码校准时如果读数差只有1600左右那scale取整时尽量保留三位小数否则几克以内的物体误差会被放大。5. 常见问题排查与避坑经验实录5.1 常见问题速查表我把做这个项目时见过的高频问题整理成了一张表很多问题一看现象就能猜到根因现象可能原因排查方法HX711读数始终为0DOUT接错引脚、传感器激励线接反用万用表测DT电平检查E/E-极性读数始终0xFFFFFF传感器信号线虚接、增益设置不对检查A/A-接线重新上电重量值乱跳机械结构松动、电源不稳固定传感器检查供电纹波OLED完全不亮I2C地址不对、电荷泵未开启扫描0x3C/0x3D地址确认初始化含0x8D/0x14OLED花屏错位初始化命令不完整、供电不足重烧完整初始化序列加100ms延时重量有规律漂移温度影响、传感器受热远离发热元件做零点追踪校准后小物体不准scale舍入误差大、非线性用小砝码复校增加多次采样上电重量不是0零点未保存或托盘压着传感器空载状态下重新校准零点其中“读数始终为0”和“读数始终0xFFFFFF”新手最容易混淆。前者通常意味着DOUT端根本没产生低电平信号通信压根没开始后者反而说明通信成功了但读到的24位数据符号位一直是1多半是传感器信号极性反了或者是传感器根本没受力时零点落在了负端区域。两者排查方向完全不同不要一上来就怀疑芯片坏了。5.2 实战避坑心得关于传感器线色我再说一次不同厂家的4线制传感器线色并不统一有的红色是E有的红色是A淘宝上买的模块配送的传感器尤其混乱。正确做法是拿到传感器先用万用表测四条线之间的电阻电阻值较大且成对出现的是激励端电阻值较小的一对是信号端。电源问题也必须花心思。HX711对电源纹波比较敏感如果系统里还有电机、继电器这种大电流器件最好给HX711单独加一颗10uF电解电容和0.1uF瓷片电容并联在电源引脚附近。称重传感器和主控板尽量共地不要用某些USB转串口供给的脏电去供电。还有一个容易忽略的点STM32程序下载后第一次运行HX711的while等待循环如果卡住往往是传感器还没供电。HX711上电后需要一小段时间稳定一般建议在初始化函数里先延时200ms再去读。另外如果你用在线调试器单步执行HX711容易因为超时卡住最好在串口打印模式下观察输出。最后是关于Flash保存参数。如果每次上电都能自动读取上次校准的结果项目的完整度会提升一大截。保存时建议在参数结构体末尾放一个校验值比如前面所有字节的异或结果读取时先检验校验值再决定用不用存储的校准参数这样能避免Flash里存了半截数据导致误标定。我个人在实际操作中最大的体会是硬件固定方式和电源质量决定了这个项目90%的稳定性算法和代码只占很小比例。传感器不要用长杜邦线悬空悬挂电源不要跟开发板的USB口共用一个容易波动的5V。把这两点做好你手里的STM32HX711OLED组合不仅是一个能用的电子秤还可以轻松扩展成厨房称重、配料称重、快递计重甚至带蓝牙上报的小应用。网上有人把同样的方案做到±0.2g精度靠的不是换了多贵的芯片而是把结构和滤波参数一点点磨到极致。这套组合后续能玩的方向还很多比如双量程切换、触摸式零点校准、上位机实时曲线每一项都能让这个基础项目脱胎换骨。
返回列表