news 2026/10/5 6:11:53

STM32计价电子秤设计:HX711称重与OLED交互全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32计价电子秤设计:HX711称重与OLED交互全解析

1. 从零搭建一台计价电子秤,为什么我选了STM32而不是Arduino

很多人第一次接触电子秤项目,脑子里蹦出来的第一个方案是Arduino加HX711模块,网上教程一抓一大把,接线简单、库也现成。我最早也是这么干的,一块UNO加一个称重传感器,半天就能读出重量。但真要把这东西做成一台能摆在台面上用的计价秤,问题就全冒出来了:单价怎么输入?总价怎么实时刷新?去皮之后怎么保证精度不漂?断电了数据还在不在?Arduino那点RAM和Flash,光是驱动一块OLED再挂个数码管就快满了,更别提还要做按键菜单和掉电存储。

所以这个项目我最终选了STM32F103C8T6作为主控。原因很直接:72MHz的Cortex-M3内核,64KB Flash、20KB SRAM,跑一个带菜单状态机的计价逻辑绰绰有余;片上资源丰富,I2C、SPI、USART、定时器一应俱全,驱动OLED和HX711不需要软件模拟时序;最关键的是,STM32的生态足够成熟,HAL库把底层寄存器操作封装得很干净,我可以把精力放在业务逻辑上,而不是天天对着参考手册数时钟周期。

这篇文章我会把整个计价电子秤的设计思路、选型理由、电路细节、代码框架和调试过程中踩过的坑全部摊开讲。适合正在做嵌入式课程设计的学生、想入门STM32外设驱动的初学者,以及需要快速复现一个完整小项目的工程师。读完你至少能拿到三样东西:一套可用的硬件方案、一份能跑通的软件架构、以及几个只有实际焊过板子调过程序才知道的避坑经验。

2. 计价电子秤的核心需求拆解与方案定型

2.1 一台"计价秤"和一台"称重秤"的本质区别

称重秤只需要回答一个问题:这东西多重。计价秤要回答的是一串问题:多重、单价多少、总价多少、去皮了没有、超载了没有、这次称重记录要不要存。这就决定了计价秤的软件复杂度比纯称重秤高一个量级。

我把需求拆成四层。第一层是称重层,负责把HX711的原始ADC值转换成稳定的重量读数,核心指标是精度和抗抖动。第二层是计价层,根据重量和单价算出总价,涉及浮点运算和金额取整规则。第三层是交互层,包括按键输入单价、OLED显示重量/单价/总价、去皮和清零操作。第四层是数据层,把单价和校准参数存进Flash,断电不丢。

这四层里,称重层是地基。地基不稳,后面算出来的总价全是错的。所以整个项目的技术难点其实集中在HX711的采样处理和滤波算法上,而不是大家以为的显示或者按键。

2.2 主控、传感器、显示器的选型对比

选型这件事我习惯列个表,把候选方案的优缺点摆在一起看,比在脑子里纠结靠谱得多。

方案主控称重ADC显示优点缺点
入门方案Arduino UNOHX711模块1602 LCD资料多、上手快资源紧张、无法做复杂菜单
本项目方案STM32F103C8T6HX711模块0.96寸OLED资源充足、I2C驱动简单、显示效果好需要一定STM32基础
高配方案STM32F407专用24位ADCTFT彩屏精度高、界面炫成本高、开发周期长

最终定下来的是中间这条路线。STM32F103C8T6这块芯片被叫做"最小系统板之王"不是没道理的,价格便宜、资料海量、随便一个开发板都能跑。HX711是专为称重传感器设计的24位ADC,内置增益放大器,直接对接桥式传感器,省掉了自己搭仪表放大器的麻烦。显示用0.96寸OLED,I2C接口只占两个引脚,对比度高,在光线暗的柜台环境下比LCD看得清楚。

2.3 系统框图的逻辑梳理

整个系统的信号流向是这样的:称重传感器(惠斯通电桥)输出差分毫伏信号,HX711对这个微弱信号做24位ADC转换并放大128倍,通过两根线(SCK和DOUT)把数据串行送给STM32。STM32拿到原始值后做滤波和标定换算,得到重量,再结合按键输入的单价算出总价,最后通过I2C把三个数据刷到OLED上。按键负责单价输入、去皮、清零,Flash负责保存单价和校准系数。

这里有个容易被忽略的点:HX711和STM32之间是双向的,STM32要主动给SCK发时钟脉冲,HX711才会在DOUT上吐数据。这跟I2C那种带地址的协议完全不同,本质上是HX711在"被动应答"。理解这一点,后面写驱动的时候就不会被时序搞晕。

3. HX711称重电路:那些 datasheet 不会告诉你的细节

3.1 惠斯通电桥和HX711是怎么配合的

称重传感器内部是一个惠斯通电桥,四个应变片贴在金属弹性体上。受力变形时,应变片阻值变化,电桥失去平衡,在输出端产生一个差压信号。这个信号非常微弱,满量程通常只有1mV到2mV,直接给STM32的ADC根本分辨不出来。

HX711干的事就是把这个差压信号放大128倍,再做24位ADC转换。24位是什么概念?2的24次方约等于1677万,理论上能把满量程信号分成1600多万份。当然实际有效位数达不到24位,但即便只有18位有效,对于5kg量程来说,分辨率也能到0.02g级别,做计价秤完全够用。

接线的时候,传感器的四根线要严格对应:E+和E-是激励电压,A+和A-是差分信号输出。红黑绿白四色线的定义各家传感器可能不一样,千万不要凭颜色猜,一定要看传感器附带的说明书或者用万用表量一下。我见过有人把激励线和信号线接反,结果HX711读出来全是噪声,查了一下午才发现是线序问题。

3.2 HX711的采样率到底选10Hz还是80Hz

这是热词里出现频率很高的问题。HX711的RATE引脚决定输出速率:拉低是10Hz,拉高是80Hz。很多人想当然地选80Hz,觉得采样越快越好。但实际用下来,10Hz才是计价秤的正确选择。

原因在于噪声。HX711在80Hz模式下的噪声明显大于10Hz模式,读数跳动更厉害。计价秤对实时性要求不高,人放上去等个0.1秒完全能接受,但读数如果一直跳,用户会觉得这秤"不准"。10Hz模式下配合软件滤波,读数能稳到只跳最后一个字,体验好得多。

我的做法是RATE引脚直接接地,固定10Hz。然后在软件里做滑动平均滤波,每采集10个点取一次平均,这样既保证了稳定性,又不会让响应变得太迟钝。

3.3 电源和接地的处理经验

HX711对电源噪声很敏感。如果直接用STM32板子上的3.3V给HX711供电,而板子上还有OLED、按键这些负载,电源纹波会耦合进ADC,导致读数漂移。

我的处理方式是:HX711的VCC和AVDD单独走一条线到电源入口,中间加一个100Ω电阻和10uF电容做RC滤波。模拟地和数字地在HX711下方单点汇合,不要大面积铺地连在一起。传感器的那根屏蔽线,屏蔽层只在一端接地,另一端悬空,避免形成地环路。

这些细节在datasheet里不会写,但实际调试时,做了和没做,读数稳定性差一个数量级。我第一版没做电源滤波,空载读数在±5g之间跳,加了RC滤波之后降到±0.5g以内。

4. STM32端的软件架构:从HAL库初始化到计价状态机

4.1 用CubeMX配置外设的正确姿势

我习惯用STM32CubeMX做初始化配置,省去手写时钟树和引脚配置的麻烦。这个项目需要配置的外设不多,但有几个地方容易配错。

时钟树方面,F103C8T6外部晶振是8MHz,经过PLL倍频到72MHz作为系统时钟。APB1总线最高36MHz,APB2最高72MHz。I2C挂在APB1上,所以I2C时钟频率不能超过36MHz,实际配置成100kHz或400kHz都可以,OLED用400kHz刷屏更快。

I2C引脚我选的是PB6(SCL)和PB7(SDA),这是F103的I2C1默认复用引脚。配置成开漏输出、上拉使能。这里有个坑:STM32内部上拉电阻大约40kΩ,对于400kHz的I2C来说偏弱,波形上升沿会变缓。稳妥的做法是在PCB上外接两个4.7kΩ上拉电阻到3.3V。如果只是飞线测试,内部上拉也能凑合用,但刷屏速度会受影响。

HX711的两个引脚SCK和DOUT我配成普通GPIO。SCK是输出,DOUT是输入。注意DOUT要配置成上拉输入或者外部加上拉,因为HX711的DOUT是开漏输出,没有上拉的话读不到高电平。

4.2 HX711驱动代码的时序要点

HX711的通信时序说简单也简单,说讲究也讲究。核心规则是:DOUT变低表示数据准备好,此时给SCK发25到27个脉冲,前24个脉冲读出24位数据(补码形式),第25个脉冲选择下一次的通道和增益。

long HX711_Read(void) { long value = 0; uint8_t i; while(HAL_GPIO_ReadPin(HX711_DOUT_GPIO_Port, HX711_DOUT_Pin) == GPIO_PIN_SET); for(i = 0; i < 24; i++) { HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_SET); value = value << 1; HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET); if(HAL_GPIO_ReadPin(HX711_DOUT_GPIO_Port, HX711_DOUT_Pin) == GPIO_PIN_SET) value++; } 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); value ^= 0x800000; return value; }

这段代码有几个关键点。第一,等待DOUT变低的循环不能加超时,否则第一次上电时HX711还没准备好,会直接跳过。第二,第25个脉冲不能省,它决定了下次转换的通道A增益128,这是称重最常用的配置。第三,读出来的24位是补码,最高位是符号位,异或0x800000是把它转成偏移二进制,方便后续处理。

SCK的高电平持续时间不能太短,HX711要求至少0.2us。HAL库的GPIO翻转函数有函数调用开销,在72MHz下大概几百纳秒,刚好够用。如果发现读数不稳定,可以在SCK翻转之间加几个NOP延时。

4.3 从原始ADC值到克数的标定换算

HX711读出来的是一个24位整数,跟实际重量是线性关系。标定就是找这个线性关系的两个参数:零点偏移和比例系数。

零点偏移的获取很简单:空载时读若干次取平均,记为offset。比例系数需要放一个已知重量的砝码,读出此时的值,减去offset,再除以砝码重量,得到每克对应的ADC值。

float Get_Weight(void) { long raw = HX711_Read(); float weight = (float)(raw - offset) / scale; if(weight < 0.5f) weight = 0.0f; return weight; }

这里的scale就是比例系数。我一般用500g砝码标定,因为500g在5kg量程的中间位置,线性度最好。标定完之后,把offset和scale存进Flash,下次上电直接读出来用,不用重新标定。

有个细节要注意:HX711的读数会随温度漂移,尤其是刚上电的几分钟内。所以标定最好在通电预热5分钟之后做,否则冷机标定的参数热机后会偏。如果对精度要求高,可以在软件里加一个自动零点跟踪:当读数连续一段时间都在零点附近小幅波动时,缓慢更新offset。

5. OLED显示与按键交互:让秤"好用"的关键

5.1 0.96寸OLED的I2C驱动和汉字显示

0.96寸OLED用的是SSD1306驱动芯片,分辨率128x64,I2C地址通常是0x78(8位地址)或0x3C(7位地址)。STM32的HAL库I2C函数用的是7位地址,所以填0x3C。

驱动OLED的核心是三个函数:初始化、写命令、写数据。初始化就是往SSD1306的一堆寄存器里写配置值,网上有现成的初始化序列,直接抄就行。写命令和写数据的区别在于控制字节:0x00表示后面跟的是命令,0x40表示后面跟的是数据。

void OLED_WriteCmd(uint8_t cmd) { HAL_I2C_Mem_Write(&hi2c1, OLED_ADDR, 0x00, I2C_MEMADD_SIZE_8BIT, &cmd, 1, 100); } void OLED_WriteData(uint8_t data) { HAL_I2C_Mem_Write(&hi2c1, OLED_ADDR, 0x40, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); }

显示汉字需要取模。我一般用PCtoLCD2002这个软件,设置成阴码、逐列式、顺向,生成的字模数组直接放到代码里。一个16x16的汉字占32个字节,显示的时候分两页写进去。

计价秤的界面我设计成三行:第一行显示重量,第二行显示单价,第三行显示总价。重量用大号字体,单价和总价用小号。这样用户在柜台上一眼就能看到最重要的重量信息。

5.2 按键消抖和状态机设计

计价秤需要至少四个按键:数字输入、小数点、去皮、清零。如果按键多,可以用矩阵键盘,但这个项目四个键就够了,直接接GPIO,配置成上拉输入,按键按下时读低电平。

按键消抖我试过两种方案。一种是延时消抖,检测到按下后延时20ms再读一次,确认按下才响应。这种方法简单,但会阻塞主循环。另一种是定时器消抖,用SysTick每10ms扫描一次按键状态,连续三次读到按下才确认。我最终用的是定时器方案,因为主循环里还要刷OLED和读HX711,不能被按键延时卡住。

状态机方面,我把系统分成几个状态:称重状态、单价输入状态、去皮状态。称重状态下,OLED实时刷新重量和总价;按下单价输入键后进入输入状态,数字键用来输入单价,确认后回到称重状态。去皮键在称重状态下按下,把当前重量记为皮重,之后显示的重量都减去皮重。

5.3 总价计算的取整规则

总价等于重量乘以单价。重量单位是克,单价单位是元/千克,所以总价等于重量除以1000再乘以单价。这里涉及浮点运算,STM32F103没有硬件浮点单元,浮点运算是软件模拟的,速度慢但够用。

金额显示一般保留两位小数。四舍五入的处理要注意,C语言的float精度有限,直接printf("%.2f")有时候会出现0.1+0.2=0.30000001这种问题。我的做法是先把结果乘以100,加上0.5后取整,再除以100,这样能保证显示正确。

float total = weight / 1000.0f * price; int total_cents = (int)(total * 100 + 0.5f); float total_display = total_cents / 100.0f;

6. 调试过程中踩过的坑和排查思路

6.1 读数一直跳:从电源查到滤波

第一版焊出来之后,空载读数在±5g之间跳,放上砝码后跳动更大。我按以下顺序排查:

先查电源。用示波器看HX711的VCC引脚,发现上面有大约50mV的纹波,频率跟OLED刷新频率一致。这说明OLED刷屏时的电流波动通过电源耦合到了HX711。解决办法是在HX711的VCC引脚旁边加一个10uF钽电容和一个0.1uF陶瓷电容,就近去耦。

再查接线。传感器的屏蔽线我一开始两端都接地了,形成了地环路,引入了工频干扰。改成单端接地后,跳动幅度减小。

最后查软件。原始读数没有做任何滤波,每个采样点都直接参与计算。加了10点滑动平均后,读数稳定在±0.5g以内。

这个排查过程告诉我一个道理:读数不稳,先查硬件,再查软件。硬件问题软件补不了,软件滤波只能锦上添花,不能雪中送炭。

6.2 OLED不亮或者花屏的几种可能

OLED不亮,最常见的原因是I2C地址不对。SSD1306的地址由DC引脚决定,有的模块是0x78,有的是0x7A。用I2C扫描程序扫一下总线,看看能扫到哪个地址。

花屏通常是初始化序列不对,或者I2C速率太高导致数据丢失。把I2C速率从400kHz降到100kHz试试,如果花屏消失,说明是速率问题,需要检查上拉电阻是否合适。

还有一种情况是OLED供电不足。0.96寸OLED的工作电流大约20mA,如果从STM32的3.3V稳压器取电,而稳压器本身负载已经很重,电压会被拉低。用万用表量一下OLED的VCC引脚,低于3.0V就可能工作不正常。

6.3 标定参数丢失:Flash读写的注意事项

STM32F103的Flash读写以页为单位,一页1KB。写之前必须先擦除,擦除后所有位变成1,写入就是把对应的位清零。如果直接往没擦除的地址写,结果会是原值和写入值的按位与,数据就乱了。

我把标定参数存在Flash的最后一页,地址0x0801FC00。上电时先读出来,如果是0xFFFFFFFF(擦除后的状态),说明还没标定过,用默认值。标定完成后,先擦除这一页,再写入新参数。

#define FLASH_SAVE_ADDR 0x0801FC00 void Save_Calibration(float offset, float scale) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase; erase.TypeErase = FLASH_TYPEERASE_PAGES; erase.PageAddress = FLASH_SAVE_ADDR; erase.NbPages = 1; uint32_t page_error; HAL_FLASHEx_Erase(&erase, &page_error); uint32_t *p = (uint32_t*)&offset; HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_SAVE_ADDR, *p); p = (uint32_t*)&scale; HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_SAVE_ADDR + 4, *p); HAL_FLASH_Lock(); }

注意float在内存里是4字节,用uint32_t指针强转后按字写入。读的时候反过来操作就行。

7. 这个项目还能怎么扩展

基础版本跑通之后,我陆续加了一些扩展功能,这里挑几个实用的说说。

超载报警:当重量超过量程的120%时,OLED闪烁显示"OVERLOAD",同时蜂鸣器响。实现很简单,在称重任务里加一个判断就行。

多单价存储:把常用的几个单价存在Flash里,按键切换。适合水果店这种不同商品不同价格的场景。

串口上传:通过USART把称重数据发给上位机,配合简单的Python脚本就能做数据记录。如果要联网,可以加一个ESP8266模块,把数据传到云平台。不过这就涉及网络通信了,代码量会大不少。

锂电池供电:加一个TP4056充电模块和升压电路,做成便携式。注意HX711对电源噪声敏感,升压电路要选低纹波的,否则称重精度会下降。

我个人觉得最值得加的是自动关机功能。计价秤经常忘了关,电池很快就耗光。用STM32的RTC定时器,检测到一段时间没有按键操作也没有重量变化,就进入待机模式,按任意键唤醒。这个功能代码不多,但实用性很强。

最后分享一个调试小技巧:在OLED上留一个调试页面,显示HX711的原始ADC值、当前offset和scale。标定的时候不用连电脑看串口,直接在屏幕上就能看到数据变化,效率高很多。这个页面在正式使用时隐藏,通过长按某个组合键进入。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 6:11:50

STM32F030RC 驱动 MR25H40CDF SPI MRAM 实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:11:33

从复位向量到RTOS第一个任务:STM32上电启动全流程拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:11:04

基于PIC24与MRAM的工业数据存储方案:SPI驱动与掉电保护设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:10:05

GEOPHYSICS投稿避坑指南:双盲评审与格式规范全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:10:05

数字水蛭危机:AI依赖如何悄悄掏空你的独立思考能力

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:10:04

CH341 StreamI2C字节流命令详解:从EEPROM读写到自定义I2C设备调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华