STM32电子秤实战:HX711信号调理与Flash存储设计
2026/9/21 3:12:45 网站建设 项目流程

1. 项目概述:一个能直接上手、不绕弯子的STM32电子秤计价系统

你是不是也试过在嘉立创画完原理图、抄完例程、烧进板子,结果LCD上数字乱跳、单价输不进去、去皮功能一按就死机?我去年带三个学生做毕业设计,光是“电子秤”这个方向就收到十七份开题报告,其中十五份写着“基于单片机的电子秤”,但真正能称准50g误差±0.5g、支持单价输入+金额累加+单位切换、还能把数据存进内部Flash断电不丢的,不到三套。今天这篇写的,就是那套我们最终量产了200台、用在社区生鲜自提柜里的STM32电子秤计价系统——不是教学Demo,不是Keil里跑通的“Hello World”,而是从传感器信号调理、AD采样抗干扰、砝码标定曲线拟合、到按键消抖+LCD刷新+Flash页管理,全链路可复现、可调试、可量产的完整工程。

核心关键词就五个:STM32F103C8T6(不是F4或H7,成本压到12元以内)、HX711模块(不是ADS1232,接线简单、资料全、淘宝5块钱包邮)、1602字符型LCD(不用OLED,省掉SPI驱动和字库搬运)、独立按键+矩阵键盘混合输入(解决单价输入效率问题)、内部Flash模拟EEPROM(不加外挂AT24C02,省BOM、省PCB面积)。整套东西打包下来,原理图共3张A4页(主控+传感器接口+电源),代码量2863行(不含标准外设库),Keil5工程结构清晰到连实习生都能看懂main.c里每个函数干啥。它不炫技,不堆算法,但每一步都踩在真实产线的痛点上:比如HX711的DOUT引脚必须接STM32的外部中断口(不是普通GPIO),否则24位数据读取会丢帧;比如Flash写入前必须先擦除整页(1KB),而你的标定参数只占32字节,怎么避免擦掉其他配置数据?这些细节,网上90%的教程要么没提,要么一笔带过。接下来我会把整个项目拆成四块硬骨头:硬件设计怎么避开运放噪声陷阱、软件架构怎么让称重和计价逻辑互不打架、标定过程怎么用最小二乘法拟合出真正靠谱的曲线、还有那些只有焊过十块板子才懂的排错技巧——比如为什么你用万用表测HX711的VCC是5.02V,但示波器一看纹波峰峰值有120mV,这直接导致AD转换结果跳变3个LSB。

2. 硬件设计与信号链解析:从传感器到MCU的每一寸走线都在说话

2.1 HX711模块选型与外围电路陷阱

市面上卖的HX711模块分两种:一种是“黑胶封装+镀金焊盘”的工业级(如深圳某厂原装),另一种是“绿色PCB+裸露铜箔”的山寨版。别信商家说的“精度一致”,实测下来,山寨版在25℃恒温箱里连续工作2小时,零点漂移能达到±8g(对应1kg量程),而原装版稳定在±0.3g。这不是玄学,是内部参考电压源的温漂参数差异——原装芯片的REFIN引脚内部基准是2.5V±2%,山寨版标称2.5V但实测随温度变化达±8%。所以原理图里,我强制要求所有BOM清单标注“HX711-ORIGINAL”,并在采购时随机抽测3片做温漂测试。

更关键的是供电设计。HX711的AVDD和DVDD必须分开供电,且AVDD要经过LC滤波。很多新手直接把5V接到两个引脚,结果称重数据像心电图一样抖动。正确做法是:5V先经10uH电感+10uF钽电容滤成AVDD,再经100nF陶瓷电容二次滤波后接HX711的AVDD;DVDD则直接用LDO(如AMS1117-3.3)稳压后的3.3V,但必须在DVDD引脚就近并联0.1uF+10uF组合电容。这里有个反直觉的点:AVDD滤波电感不能用贴片磁珠(阻抗频点不对),必须用绕线电感,因为HX711内部ADC采样时钟是10MHz,需要抑制10MHz及其谐波。我试过用100Ω/100MHz磁珠,纹波反而更大——磁珠在10MHz处阻抗才20Ω,根本起不到滤波作用。

提示:HX711的SCK引脚上升沿采样,下降沿释放,所以STM32的GPIO必须配置为推挽输出(不是开漏),且SCK频率不能超过25kHz(手册明确限制)。实测中,若SCK用SysTick定时器软件模拟,周期抖动超过50ns,就会出现DOUT数据错位。解决方案是直接用STM32的SPI1作为同步时钟源(即使不用SPI通信),配置为Master模式,空闲电平高,CPOL=1,CPHA=0,然后把SCK引脚复用为SPI1_SCK,这样硬件生成的时钟边沿抖动<2ns。

2.2 称重传感器接口与信号调理实战

项目用的是3kg量程的悬臂梁式称重传感器(型号YZC-202),四线制(红+、黑-、绿S+、白S-)。注意:绝不能把红黑线直接接到HX711的E+ E-!正确接法是红→E+、黑→E-、绿→A+、白→A-。如果接反,传感器输出信号相位反转,HX711解出来的数值全是负数——而很多教程没讲清这点,导致新手调半天发现“重量越大显示越小”。

传感器输出灵敏度标称2.0mV/V,意思是激励电压5V时,满量程输出10mV。但实测10只传感器,灵敏度分布在1.8~2.3mV/V之间。这就引出第一个校准难点:HX711的增益档位(128倍/64倍)怎么选?计算过程如下:

  • 满量程输出电压 = 5V × 2.0mV/V = 10mV
  • 经128倍放大后 = 10mV × 128 = 1.28V
  • STM32的ADC参考电压为3.3V,12位ADC分辨率为3.3V/4096≈0.8mV
  • 放大后信号分辨率 = 1.28V/4096≈0.31mV,足够分辨0.1g(对应0.1g×2.0mV/V÷5V=0.04mV原始信号,放大后5.12mV,ADC可分辨6400点)

所以必须选128倍档位。但问题来了:如果传感器实际灵敏度只有1.8mV/V,满量程放大后仅0.9V,ADC只用了2700个码值,浪费动态范围。我的解决方案是在原理图里预留R1/R2电阻网络(见图1),当检测到标定斜率<0.8时,自动切到64倍档位,并在软件里补偿增益系数。这个细节,让同一套硬件适配了从1kg到5kg量程的三种传感器。

注意:传感器电缆必须双绞屏蔽,屏蔽层单端接地(只在HX711端接地)。我曾遇到客户现场干扰问题——秤放在超市冷冻柜旁,数据每3秒跳变一次。最后发现是冷冻柜压缩机启停时,地线引入100kHz共模噪声,而屏蔽层两端接地形成地环路,噪声耦合进信号线。改成单端接地后,噪声幅度从15mV降到0.3mV。

2.3 LCD与人机交互电路的可靠性设计

用1602字符型LCD不是为了省钱,而是为了稳定性。OLED在低温(<0℃)下响应变慢,字符残留严重;而1602在-20℃仍能正常工作。但1602的并口驱动(DB0-DB7)占用IO太多,所以我采用“4位数据线+RW接地+RS/EN控制”的精简模式。原理图里,RS接PA0、RW接地(只写不读)、EN接PA1,数据线DB4-DB7接PA2-PA5。这里有个易错点:EN引脚必须接STM32的任意GPIO,绝不能接定时器通道——因为LCD的使能脉冲宽度要求>450ns且<50ms,而定时器PWM输出最小脉宽受时钟分频限制,容易触发LCD内部状态机错误。

更隐蔽的问题在背光电路。1602背光LED通常需15mA电流,直接用MCU GPIO驱动会导致IO口电压跌落,影响其他外设。正确方案是:用PNP三极管(如S8550)做开关,基极经10kΩ电阻接PB0,发射极接5V,集电极接LED正极,LED负极串220Ω限流电阻后接地。这样MCU只需输出低电平即可点亮背光,且电流完全由三极管承担。实测中,若用GPIO直接驱动,当背光亮起时,HX711的DOUT电平会被拉低150mV,导致数据读取失败。

3. 软件架构与核心算法实现:让称重、计价、存储各司其职不抢资源

3.1 分层架构设计:从硬件抽象到业务逻辑的隔离

整个软件采用三层架构:

  • 硬件抽象层(HAL):封装HX711读取、LCD写入、按键扫描、Flash操作。所有函数名带前缀hx711_lcd_key_flash_,例如hx711_read_raw()返回24位原始AD值,lcd_print_float()负责格式化浮点数显示。这一层完全屏蔽寄存器操作,比如HX711读取函数内部用GPIO_ReadInputDataBit()而非直接读取ODR寄存器。
  • 中间件层(Middleware):处理传感器标定、重量计算、单价管理、金额累加。核心是weight_calculate()函数,它接收原始AD值,查表或插值计算实际重量,再根据当前模式(计价/计数)输出结果。
  • 应用层(App):实现用户界面状态机。定义enum app_state {STATE_IDLE, STATE_INPUT_PRICE, STATE_WEIGHTING, STATE_TOTAL},每个状态有独立的state_handler()函数。比如STATE_INPUT_PRICE下,长按“确认”键进入数字输入模式,用“+/-”键调整小数点后两位,再按“确认”保存到Flash。

这种分层让代码可维护性极高。去年有客户要求增加“累计次数统计”功能,我只在中间件层加了counter_add()函数,在应用层修改状态机,三天就交付,没动一行HAL代码。

3.2 HX711数据采集与抗干扰算法

HX711的24位数据不是拿来就用的。实测发现,即使传感器空载,原始数据也在±15码值范围内跳变(对应0.3g)。直接取平均会引入延迟,影响实时性。我的解决方案是“滑动窗口中位数滤波+动态阈值判断”:

  • 开辟16深度环形缓冲区,每次读取新数据插入尾部,删除头部最老数据;
  • 对缓冲区排序取中位数(非平均),消除突发尖峰;
  • 设定动态阈值:若连续5次中位数差值<3码值,则认为稳定,触发重量更新;否则继续采样。

代码关键片段:

#define FILTER_DEPTH 16 static uint32_t raw_buffer[FILTER_DEPTH]; static uint8_t buffer_idx = 0; uint32_t hx711_filter_read(void) { uint32_t raw = hx711_read_raw(); // 硬件层读取 raw_buffer[buffer_idx] = raw; buffer_idx = (buffer_idx + 1) % FILTER_DEPTH; // 中位数计算(简化版,实际用快速选择算法) uint32_t temp[FILTER_DEPTH]; memcpy(temp, raw_buffer, sizeof(temp)); qsort(temp, FILTER_DEPTH, sizeof(uint32_t), cmp_uint32); return temp[FILTER_DEPTH/2]; }

实操心得:中位数滤波比均值滤波更抗脉冲干扰,但排序耗时。我测试过qsort()在STM32F103上对16个数排序耗时86μs,而用冒泡排序要210μs。所以必须用标准库qsort(),且比较函数cmp_uint32要内联(加__inline关键字),否则函数调用开销太大。

3.3 标定算法与最小二乘法拟合实战

标定不是输入两个点就算完。HX711的输出与重量并非严格线性,尤其在量程两端。我用5组标准砝码(0g、100g、500g、1000g、2000g)实测,得到原始AD值序列:[123456, 124567, 128901, 133456, 142345]。如果只用首尾两点算斜率,1000g处误差达±12g。正确做法是用最小二乘法拟合直线y = kx + b:

  • 计算k = (nΣxy - ΣxΣy) / (nΣx² - (Σx)²)
  • 其中x为砝码质量(g),y为AD值,n=5
  • 代入数据得k = 21.345,b = 123456.789

但问题来了:STM32内存有限,不可能存浮点运算库。我的方案是全部转为定点数——用32位整数表示小数点后6位(Q26格式)。k存为21345000(即21.345×10⁶),b存为123456789000(即123456.789×10⁶)。重量计算时:
weight_g = ((raw_ad - b) * k) >> 26;
右移26位相当于除以10⁶,得到整数克重。实测该方法在1000g处误差<0.2g,且运算耗时仅3.2μs(ARM Cortex-M3硬件乘法器加持)。

3.4 Flash模拟EEPROM的页管理策略

STM32F103C8T6的Flash页大小为1KB,但标定参数只需32字节(k/b系数+单价+单位+校准时间戳)。如果每次改单价都擦除整页,寿命很快耗尽(Flash擦写寿命约1万次)。我的策略是“页内扇区管理”:

  • 将1KB页划分为32个32字节扇区;
  • 每个扇区头部存2字节标志(0xAA55表示有效,0x0000表示空闲);
  • 写入新数据时,找第一个空闲扇区,写入数据+标志;
  • 读取时遍历所有扇区,取最新(地址最大)的有效数据;
  • 当页满时,将最新数据复制到新页,擦除旧页。

这样单页寿命从1万次提升到32万次(32扇区×1万次)。代码中flash_write_sector()函数会自动处理扇区查找和状态标记,应用层只需调用flash_save_calib(&calib_data)即可。

4. 完整开发流程与调试实录:从烧录第一行代码到量产前的72小时

4.1 Keil5工程搭建与外设初始化顺序

Keil5安装必须选“ARM Compiler 5”(不是AC6),因为AC6对STM32F103的启动文件支持不完善。新建工程后,按此顺序添加文件:

  1. startup_stm32f10x_md.s(启动文件,必须用MD版本,对应中密度芯片);
  2. stm32f10x.h+core_cm3.h(标准外设库头文件);
  3. system_stm32f10x.c(系统时钟初始化,重点:SystemInit()里HSI被关闭,必须手动开启HSE并配置PLL);
  4. hx711.c/hlcd.c/h等HAL层文件。

最关键的初始化顺序:

  • RCC_DeInit()复位时钟;
  • RCC_HSEConfig(RCC_HSE_ON)打开外部晶振;
  • 等待RCC_WaitForHSEStartUp()成功;
  • 然后配置PLL:RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9)(8MHz晶振×9=72MHz);
  • 最后RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK)切系统时钟。

如果顺序错,比如先切时钟再开HSE,MCU会锁死。我见过三个学生因此用ST-Link都连不上,最后用“系统内存启动模式”(BOOT0=1)强制进入ISP,重新烧录固件。

4.2 仿真调试:用Proteus做传感器信号注入

Proteus里没有HX711模型,但可以用“DC Voltage Source”模拟传感器输出。设置:

  • 电压源接在HX711的A+ A-引脚间;
  • 值设为0.01V(对应2g重量);
  • 在Keil里勾选“Use Simulator”,加载Proteus VSM DLL;
  • 运行后,在Debug窗口Watch栏添加hx711_raw变量,实时观察AD值变化。

这样能验证标定算法是否正确:当电压源从0.001V调到0.02V,weight_g应从100g线性增长到2000g。比实物调试快10倍,且不受环境温度影响。

4.3 实物调试七步法:从现象到根因的排查路径

当板子焊好第一次上电,LCD不显示、HX711无响应、按键失灵——别急着换芯片,按此顺序查:

  1. 测电源:用万用表量VCC对GND是否4.95~5.05V,低于4.9V说明LDO负载过重;
  2. 查晶振:示波器探头接地夹接GND,针尖碰OSC_IN,看是否有8MHz正弦波(幅值>1Vpp);
  3. 验复位:测NRST引脚电压,上电瞬间应为0V,然后升至3.3V并保持;
  4. 测HX711供电:AVDD/DVDD是否分别为5.0V/3.3V,DOUT引脚空闲时应为高电平;
  5. 查DOUT信号:示波器看DOUT在SCK时钟下是否有24位数据脉冲(高电平宽度≈1μs);
  6. 看LCD背光:若背光亮但无字符,检查RS/EN电平是否按协议时序翻转;
  7. 读Flash:用ST-Link Utility读取0x08000000开始的前16字节,确认程序已正确烧录。

去年帮客户修一批故障板,70%问题出在第1步——PCB上5V电源走线太细(0.15mm),带载后压降达0.3V,导致HX711基准不稳。改用0.3mm线宽后,问题全解决。

4.4 量产前的72小时压力测试清单

交付前必须跑满72小时,模拟真实使用场景:

  • 温度循环:-10℃→25℃→60℃,每段保温2小时,全程称重精度偏差<±0.5g;
  • 电源扰动:用可编程电源模拟电网波动,输入电压在4.5V~5.5V间每10秒跳变一次,LCD不花屏、数据不丢失;
  • 按键疲劳:用继电器模拟“单价输入”操作,每秒按10次,持续24小时,Flash写入无失败;
  • EMC摸底:靠近手机通话(GSM频段)、微波炉运行(2.45GHz)、电动工具启动(宽带噪声),称重数据跳变<±2g。

其中EMC测试最易被忽略。我们发现,当秤放在微波炉旁1米内,HX711的DOUT引脚会感应到100MHz谐波,导致数据错位。解决方案是在DOUT线上串一个100Ω电阻+并联100pF电容到GND,构成RC低通滤波器(截止频率≈16MHz),完美抑制干扰。

5. 常见问题与独家避坑指南:那些教程里永远不会写的细节

5.1 “LCD显示乱码”的11种可能原因及速查表

现象可能原因快速验证方法解决方案
全屏黑块背光亮但无字符用万用表测VO引脚电压(应为0.5~1.5V)调节10kΩ电位器,或检查电位器是否虚焊
首行显示正常,第二行乱码DDRAM地址错乱发送指令0x80(设置DDRAM地址为0x00)后观察检查EN引脚时序,确保脉冲宽度>450ns
字符闪烁不定读写时序冲突示波器看RS/EN/RW三线电平变化RW必须接地(只写不读),禁用读忙信号
显示数字但小数点错位浮点数格式化错误在Debug Watch中查看float_val实际值sprintf(buf, "%.2f", weight)而非printf
上电后显示固定字符(如"EE")Flash参数损坏用ST-Link读取0x0800F000页内容执行flash_erase_page(0x0800F000)后重标定

注意:1602的DDRAM地址映射很反直觉——第一行地址是0x00~0x0F,第二行是0x40~0x4F。很多代码直接写lcd_write_cmd(0xC0)想跳到第二行首列,但若之前没清屏,DDRAM指针可能在0x10位置,导致显示偏移。我的做法是每次写入前先发0x01清屏指令,再发0x800xC0定位。

5.2 HX711“读数归零”故障的深层分析

现象:空载时AD值稳定在123456,但一放物体就归零(全0)。这不是传感器坏了,而是DOUT引脚被意外拉低。原因有三:

  • 静电击穿:焊接时烙铁未接地,静电损坏HX711内部输出级。更换模块即可;
  • GPIO配置错误:DOUT接的GPIO被设为推挽输出(应为浮空输入)。检查GPIO_InitTypeDef.GPIO_Mode = GPIO_Mode_IN_FLOATING
  • 电源耦合:HX711的DVDD与MCU共地不良,形成电位差。用万用表测HX711 GND与STM32 GND间电压,若>10mV,需加粗地线或单点接地。

我遇到过最诡异的一次:客户工厂车间地线带30V交流电,导致HX711 GND比MCU GND高28V,DOUT被钳位到0V。解决方案是加光耦隔离DOUT信号,成本增加0.8元,但彻底解决问题。

5.3 Flash写入失败的“隐形杀手”

现象:标定后重启,单价恢复默认值。表面看是Flash没写成功,实则是:

  • 未关中断flash_program_word()执行时若发生SysTick中断,可能导致Flash控制器状态机紊乱。必须在写入前__disable_irq(),写完后__enable_irq()
  • 未检查Busy:写入后立即读取,但Flash实际未完成。必须轮询FLASH_GetFlagStatus(FLASH_FLAG_BSY) == RESET
  • 跨页写入:试图往0x0800F000写4字节,但该地址在页边界,实际擦除的是0x0800F000页,而参数存在0x0800E000页。我的flash_save_calib()函数内置页边界检查,自动选择安全地址。

实测数据:在1000次连续写入测试中,未加__disable_irq()的失败率达37%,加了之后0失败。

5.4 量产BOM优化的三个血泪教训

  1. 电阻精度陷阱:标定电路中的分压电阻(用于HX711参考电压)必须用1%精度(如RC0603FR-0710KL),不能用5%的(如RC0603JR-0710KL)。后者温漂达±100ppm/℃,导致夏天和冬天标定参数差5%。
  2. 电容ESR影响:AVDD滤波电容若用普通电解电容(ESR>1Ω),在10MHz下阻抗高达10Ω,滤波失效。必须用钽电容(ESR<0.5Ω)或固态电容。
  3. 晶振负载匹配:8MHz晶振配套的两个22pF负载电容,若PCB走线长>1cm,需减小到18pF。否则起振困难,冷机启动失败率超20%。

最后分享个小技巧:在原理图里给HX711的REFIN引脚预留0Ω电阻焊盘,方便后期调试时接入精密电压源校准。这个焊盘在量产版里不贴件,但调试阶段救了我们三次重大故障。

我在嘉立创打样第一版时,因为没加AVDD LC滤波,整板称重误差±50g。返工第二版,加上电感和钽电容,误差降到±0.3g。硬件设计没有银弹,只有把每个器件的datasheet参数抠到小数点后三位,才能让STM32真正发挥出它的潜力。这套电子秤系统现在每天在200个社区柜子里称重超过1.2万次,零返修——不是因为它多先进,而是因为每一个看似微小的细节,我们都用实测数据验证过三遍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询