这阵子帮人做了个基于STM32的智能计价电子秤方案,从硬件选型、代码驱动到最终标定全程走了一遍。正好不少朋友在问这个题目能不能做成毕设、能不能真正商用,我干脆把这个项目的完整实现思路和我实际踩过的坑一起整理出来。做这个东西的初衷很简单:市面上几十块钱的厨房秤只能看重量,遇到按单价算钱的场景(水果摊、快递计费、小作坊配料)根本不够用。而STM32方案成本低、外设全、资料多,做一个能称重、能去皮、能通过按键设置单价并自动计算金额的智能电子秤,是性价比极高的嵌入式练手项目。这套方案既可以作为毕业设计交差,也能扩展成真正可用的产品原型,无论你是刚学完51单片机想往上走,还是准备找嵌入式相关工作想攒项目经验,都值得完整走一遍。
我在实际做的时候,最初版本大概花了三天搭完硬件和基础驱动,后面一个月都在调精度、处理各种边界情况。这篇文章会把整个项目的需求拆解、硬件接线、关键代码、定标校准、实测数据、踩坑记录全部讲清楚,涉及到的每一处"为什么这样做"我都会说明白,尽量让零基础的读者也能照着做出来。
1. 项目需求拆解与系统方案:为什么选STM32方案
1.1 功能需求从使用场景反推
智能计价电子秤的"智能"并不神秘,本质就是把三个动作合并成一个:感知重量、读取单价、计算总价。我先把需求理成一张表,后续所有设计都围绕它展开。
| 功能项 | 需求描述 | 实现难度 |
|---|---|---|
| 称重 | 最大量程5kg,显示分辨率1g | 中 |
| 去皮 | 放容器后一键清零 | 低 |
| 单价输入 | 支持0.1元步进调整 | 中 |
| 金额计算 | 总价=重量×单价,实时更新 | 低 |
| 超量程报警 | 超过5kg时蜂鸣/闪烁提示 | 低 |
| 显示 | 重量、单价、金额三行同屏 | 中 |
围绕这个需求表,系统天然分成四个模块:传感器采集模块(称重传感器+HX711)、主控模块(STM32)、人机交互模块(OLED+按键)、电源模块。每个模块之间的接口非常清晰,这也是这个项目适合做毕设的原因——它完整覆盖了嵌入式系统"采集-处理-输出"的闭环,但又不需要你啃下射频、网口这些偏门内容。
1.2 主控选型:STM32F103C8T6是当前最优解
很多人纠结要不要上F4、H7,或者干脆用ESP32。我的观点是:这个项目用STM32F103C8T6是性能和成本的最优平衡点。
先说为什么不用51单片机。51的GPIO操作虽然简单,但处理定时器、I2C、外部中断这些外设时要各种软件模拟,而且Flash只有8KB级别,一套OLED显示驱动加上HX711时序代码就已经快塞满了,更别提后续还要加菜单逻辑、浮点运算。STM32F103C8T6虽然价格也就十几块钱,但主频72MHz、64KB Flash、20KB RAM,跑这些任务绰绰有余,外设的资源也丰富得多。
再解释一下为什么不用ESP32。ESP32的Wi-Fi/蓝牙功能在这个项目里属于冗余,而且它的ADC精度并不好,做称重这类高精度模拟量采集并不合适。STM32的库函数生态非常成熟:标准外设库、HAL库、LL库资料铺天盖地,出了问题几乎都能搜到答案。对于毕设或者项目练手,资料多本身就是最大的开发效率保障。
这个项目的核心链路是"模拟小信号采集",所以重点应该在传感器和ADC前端,而不是主控本身。STM32在这里更多承担"调度者"角色:通过GPIO模拟时序从HX711读数据,做滤波处理,换算重量,驱动OLED显示,扫描按键状态。这块板子完全撑得住。
1.3 称重方案定型:电阻应变式传感器与HX711的经典组合
称重传感器我最开始试过直接用STM32内置ADC读电桥输出,结果精度完全不可用。原因很简单:5kg量程的电阻应变式传感器满量程输出只有约10mV(激励电压5V、灵敏度2mV/V时),你要在10mV范围里分辨1g对应的0.002mV,这已经超出了STM32内置12位ADC的实用精度范围(3.3V参考电压下1个LSB约0.8mV)。
HX711就是为解决这个问题而生的:它是一颗24位高精度ADC芯片,内置一个低噪声可编程增益放大器(PGA),增益可选32/64/128,专为桥式传感器设计。用128倍增益时,满量程5kg对应的ADC输出范围很宽,1g的重量变化换算到ADC码值上清晰可见。它的通信接口只有两根线(DOUT和PD_SCK),跟SPI很像但更简单,非常适合单片机用IO模拟时序去驱动。
传感器选型上,我建议直接买铝制单点式称重传感器,量程选5kg或10kg。这类传感器本身就是为电子秤设计的,四个角有固定孔,中间的受力柱是称重台面支撑点,安装起来比悬臂梁式省心得多。要注意的是传感器额定输出常见有1mV/V和2mV/V之分,购传感器时留意一下规格,量程留出20%以上余量(比如你想称5kg就选6kg或10kg传感器),这样线性度和重复性会好很多。
2. 硬件接线与电路设计的关键细节
2.1 最小系统板选型与常用外设接线总览
开发方式我推荐直接买STM32F103C8T6核心板,不要自己画最小系统。因为等你真正要量产或者交设计文档的时候再画PCB不迟,前期快速跑通功能最重要。核心板自带了8MHz晶振、复位电路、LDO稳压、USB转串口,插上USB线就能烧录调试。
接线方案如下表,这里有个容易忽视的地方:HX711的传感器激励电压(E+和E-)接的是外部5V,而逻辑供电(VCC)建议接3.3V。原因是HX711的内部参考电压经稳压后,数字接口电平与VCC相关,接3.3V可以保证和STM32的GPIO电平直接匹配,不需要电平转换。
| HX711引脚 | 接STM32/电源 | 说明 |
|---|---|---|
| VCC | 3.3V | 数字逻辑供电 |
| DVDD | 3.3V | 数字电源引脚,可接VCC |
| DOUT | PB0 | 数据输出 |
| PD_SCK | PB1 | 时钟输入 |
| E+ | 5V | 传感器激励正 |
| E- | GND | 传感器激励负 |
| S+ | A+ | 传感器信号正(接HX711 A+) |
| S- | A- | 传感器信号负(接HX711 A-) |
OLED我用的0.96寸SSD1306接口是I2C,把SDA接PB7、SCL接PB6,用的是STM32的硬件I2C(就是I2C1的两个引脚)。按键做三个:一个去皮键、一个加价键、一个减价键,分别接PA0、PA1、PA2,配上10kΩ下拉电阻到地,按下时读到高电平。
2.2 电源系统的常见坑:模拟电路对纹波极其敏感
这个项目最容易被忽视的就是电源。HX711的模拟输入要分辨微伏级别的差分信号,如果电源纹波大,读数就会上下乱跳。我第一次调试时直接用USB口的5V给传感器激励供电,结果空秤读数稳定在±20g摆动,完全没法用。
后面改成这样解决的:USB的5V先经过一个AMS1117-3.3给STM32供电,传感器激励用一个单独的5V稳压源(或者直接从USB的5V经过LC滤波再给传感器),同时HX711的电源引脚并联一个100uF电解电容和一个0.1uF瓷片电容去耦。这样一来,读数马上就稳了许多。
如果你想让模拟部分更干净,可以加一颗隔离型的DCDC或者低噪声LDO给传感器独立供电,但做项目的话LC滤波+大电容基本够用了。还有一个细节:传感器信号线和激励线最好双绞,减少空间电磁干扰,尤其是旁边有继电器或者电机这种感性负载的时候。
2.3 安装结构对精度的影响比电路更大
这事我在教训里才深刻体会到。电子秤的传感器不是随便固定在桌面就行的:
- 传感器必须水平安装,四条固定螺丝要均匀拧紧,不能有某个角受力偏大。
- 称重台面(托板)要放置在传感器受力柱的正上方,重心尽量居中。
- 如果台面四角受力不均,同一个东西放在秤盘的左边和右边,读数可能差别几十克。
我实际做的时候用了块3mm厚的亚克力板当秤盘,四角垫了四个橡胶脚垫,然后中心位置有一个凸台压在传感器受力柱上,这样大部分重力能垂直传递到传感器中心。虽然比不上商用秤的精密结构,但调试下来四角差异控制在±3g以内,作为教学或样品已经合格。
3. 核心代码实现:从HX711读取到金额计算全流程
3.1 HX711时序驱动:读懂24位补码是关键
HX711的数据读取协议很简单,但第一次接触的人往往会栽在数据格式上。官方时序是这样的:当DOUT引脚变为低电平时表示转换完成、数据可读,此时单片机给PD_SCK发送25个脉冲。前24个脉冲把24位数据逐位移出,第25个脉冲用于设置下一轮转换的增益和通道(默认128倍增益对应A通道,这正是称重传感器常用的配置)。
我贴出成熟的读取代码框架,配合注释说明:
uint32_t HX711_Read(void) { uint32_t buf = 0; while (HAL_GPIO_ReadPin(DOUT_GPIO_Port, DOUT_Pin)); // 等待DOUT拉低,数据就绪 for (uint8_t i = 0; i < 24; i++) { HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_SET); buf = buf << 1; if (HAL_GPIO_ReadPin(DOUT_GPIO_Port, DOUT_Pin)) { buf++; } HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_RESET); } // 第25个脉冲:设置下一轮为A通道,128倍增益 HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_RESET); // 数据是二进制补码,需要转换为有符号数 return buf ^ 0x800000; }这里有一个非常容易被忽略的点:24位移出来的原始数据是补码格式,最高位是符号位。如果直接把它当无符号数计算,会出现"空秤读数很大、放重物读数变小"这种反直觉的现象。处理方式就是上面代码里的buf ^ 0x800000——把最高位翻转,相当于把补码转成有符号数,这样空秤约等于零点附近的小值,加压时ID增大,数值一正一负才符合直觉。
3.2 读数滤波:用"去极值滑动平均"兼顾响应速度和稳定性
直接从HX711读到的单次数据即使在静态下也会有波动,尤其是环境振动或者电源纹波大时。滤波策略我实测下来最有效的是"去极值滑动平均":
每次连续采样N个原始值(我取21个),去掉其中的最大值和最小值,剩下19个求平均。这样做能滤掉偶发的尖峰干扰,而且比单纯的多重平均抗脉冲干扰能力更强。采样频率可以通过HX711的速率模式选择引脚(RATE)来设置:接GND是10Hz,接VCC是80Hz。我用的是10Hz,因为80Hz虽然响应快,但量化噪声明显增大,静态读数会跳。
采样循环和滤波逻辑如下:
#define SAMPLE_NUM 21 uint32_t raw_values[SAMPLE_NUM]; uint32_t HX711_Read_Filtered(void) { for (uint8_t i = 0; i < SAMPLE_NUM; i++) { raw_values[i] = HX711_Read(); delay_ms(5); } // 找出最大值、最小值 uint32_t max_val = raw_values[0], min_val = raw_values[0]; uint32_t sum = 0; for (uint8_t i = 0; i < SAMPLE_NUM; i++) { if (raw_values[i] > max_val) max_val = raw_values[i]; if (raw_values[i] < min_val) min_val = raw_values[i]; sum += raw_values[i]; } return (sum - max_val - min_val) / (SAMPLE_NUM - 2); }3.3 重量与金额换算:零点校准和标定系数的数学基础
拿到稳定滤波的ADC值之后,把它换算成重量,本质上是一元线性回归:
重量(克) = (当前ADC值 - 零点ADC值) / 每克ADC值
两个参数需要标定:零点ADC值(空秤读数)和每克ADC值(标定系数)。前者在开机初始化时采样一次就行,后者需要一个标准砝码。我这里的做法是开机后空秤状态下取20次滤波读数求平均作为zero_offset,然后放一个500g标准砝码,测出adc_500g,于是:
float scale = (float)(adc_500g - zero_offset) / 500.0f; // 每克对应的ADC码值 float weight = (float)(filtered_value - zero_offset) / scale; // 当前重量,单位g金额计算就很简单了,但有一个单位陷阱。市面上的电子秤通常以"元/斤"或"元/千克"为单位,我建议代码里统一用"元/500g"(即每斤的价格)作为单价的用户输入单位,然后显示的时候把克转换成斤来相乘:
float price_per_jin = 5.60f; // 用户输入:5.6元/斤 float weight_jin = weight / 500.0f; float total_money = weight_jin * price_per_jin;注意浮点数在单片机上做乘除没问题,但显示到OLED上时最好把金额化为分(整数),比如int money_cents = (int)(total_money * 100);,然后拆出元和分来显示,避免浮点转字符串时出现5.599999这种毛病。
3.4 按键扫描与去皮逻辑:状态消抖和长按连续调节
按键处理看起来简单,但设计不好会出现"按一下数字跳好几下""去皮键连击"的情况。我用的方案是:
定时器每10ms扫一次按键,连续读到3次相同电平才视为有效状态变化(20ms以上消抖)。加减价键做成短按每按一次单价±0.1元,长按(超过500ms)则每100ms自动±0.5元,方便从十几块快速调到几块。
去皮逻辑有个细节要注意:不能在每次读取循环里直接清零,否则在读数的中间态会被反复清零。正确做法是检测到去皮按键按下后,立即把当前滤波后的重量当作新零点,也就是更新zero_offset:
if (button_tare.pressed) { zero_offset = HX711_Read_Filtered(); weight = 0.0f; }这实际上把"去皮"和"重新标零"统一了,容器放上去之后按一下,系统就把当前重力视为0,后面再放东西就只显示净重,非常实用。
4. 标定、精度控制与误差处理
4.1 为什么必须做两点标定而不是直接读取说明书参数
不少第一次做称重项目的人会有个疑问:传感器和HX711都是现成的,为什么不直接按官方例程里的固定系数转换?原因在于:传感器灵敏度有离散性(同一批次都有±10%的偏差),HX711内部放大器增益也有微小差异,机械安装的应力分布会改变输出特性,所以每个装配好的设备都必须单独做标定,商用秤出厂前也都要逐台标定。
我建议至少做两点标定,如果追求更准可以做三点或四点。两点标定就是上面写到的:一个零点和任意一个标准重量点,算出线性系数。三点标定则是增加一个中间点,计算两段不同斜率,能修正传感器在小重量段的非线性,但代码复杂度和标定工时要高不少。对5kg量程来说,两点标定已经能把误差控制在±1%以内,这在大多数应用场景是够用的。
4.2 标定实操流程与数据记录
标定一定要在机械结构完全固定、秤盘放置好之后进行,别拆了外壳再标定,否则装机之后系数全变了。我的流程是:
- 上电预热5分钟,让传感器和HX711温度稳定。
- 确保秤盘上没有任何东西,按去皮键,读取零点,代码里记录
zero_offset。 - 放置500g标准砝码(我用的是电子秤校验砝码,没有的话用一瓶500ml矿泉水误差其实也能接受,大概±2g左右)。
- 等待读数稳定,记录ADC值,算出
scale。 - 移除砝码,再放上去,重复3次,确认重复性。
- 如果重复性不好(超过±3g),先检查脚垫是否平整、螺丝是否松动,再重新标定。
实测一组数据给大家参考(用的5kg传感器,增益128,10Hz速率,空秤零点约820000):
| 标准重量(g) | 首次读数(g) | 二次读数(g) | 三次读数(g) | 最大误差(g) |
|---|---|---|---|---|
| 0 | 0 | 1 | 0 | 1 |
| 200 | 201 | 200 | 200 | 1 |
| 500 | 500 | 501 | 499 | 1 |
| 1000 | 1000 | 1001 | 1000 | 1 |
| 2000 | 2001 | 2000 | 2002 | 2 |
| 5000 | 5002 | 5001 | 5000 | 2 |
4.3 温漂、蠕变和自动零点追踪
这里有个坑是长期运行才会暴露出来的:传感器和应变片的静态特性会随温度变化,尤其是刚从冷环境搬到暖环境,开机半小时内零点会缓慢漂移几十克。如果只是毕设演示,这个问题不致命,但要做成产品就必须处理。
我采用的方案是"自动零点追踪":当系统检测到当前重量在±5g以内持续超过10秒时,自动把当前值归零。这样即使传感器因温度漂移缓慢变化,只要秤盘上没有物体,系统就会慢慢回零,显示不会一直停留在"3g"这种状态。注意去皮状态下不能启用自动零点追踪,否则用户称重途中轻微晃动设备就可能被误认为是空秤而自动清零。
具体代码类似这样,放在主循环的最后:
int16_t display_weight = (int16_t)(weight * 10); // 内部按0.1g分辨率判断 if (tare_enabled == 0 && display_weight >= -50 && display_weight <= 50) { idle_timer++; if (idle_timer > 100) // 持续约1秒 { zero_offset = HX711_Read_Filtered(); idle_timer = 0; } } else { idle_timer = 0; }5. 实测过程与踩坑记录
5.1 显示协议验证:OLED的I2C地址和显存刷新问题
SSD1306这颗屏网上驱动代码很多,容易踩的坑却有两个:一是I2C地址是0x78还是0x7A。0.96寸OLED模块通常硬件将SA0引脚拉低,地址是0x78(即7位地址0x3C左移一位)。如果读不到设备,先量一下模块背面SA0和SA1的焊盘有没有被改动。
二是OLED显存是1024字节(128×64/8),很多精简驱动为了省MCU内存选择页地址模式,分页刷新。我建议用显存数组全量刷新,虽然每帧要传1024字节,I2C在400kHz速率下也就20多毫秒,完全不影响体验,但代码逻辑简单很多,显示也不会出现刷新撕裂。
STM32的硬件I2C有个经典毛病:在F1系列上如果不加超时处理容易卡死在忙碌状态。实在不想折腾就直接用软件模拟I2C,OLED和HX711都靠GPIO翻电平,性能在这个场景下没有区别。我最后是硬件I2C加250ms超时,跑了两周没出问题,供参考。
5.2 读数漂移的排除过程:从电源到线材再到机械应力
有一次进行连续读数测试,发现空秤读数每隔几十秒就会跳一下,幅度十几克。排查链路是这样的:
- 先看是不是HX711的问题:换了一块HX711模块,故障依旧。
- 再看是不是电源问题:用电池单独给传感器供电,居然还是跳。
- 最后检查接线:发现传感器信号线的一段靠近了STM32核心板的USB线上方,USB线充电时产生了交变干扰。把信号线和电源线分开、重新整理线束之后,问题消失。
这个经历说明,在模拟小信号系统里,布线就是电气的机械结构,整理走线和固定线束不是可有可无的事。给称重传感器信号线套铜箔屏蔽并单点接地,是低成本高收益的做法。
另外还有一次是传感器螺丝没上紧,造成读数越来越慢地漂移,那其实是机械蠕变,不是电路问题。这种"软故障"最容易让人在代码里找原因找半天,却没想到是结构件松动。标定前务必把所有机械连接处拧紧到扭力一致。
5.3 超量程与异常输入处理
代码层面要处理的边界条件也不少。超量程时我让蜂鸣器报警并在OLED上显示"超出量程",防止用户把5kg的东西硬压上去损坏传感器。单价输入限制在0.1到999.9元之间,防止误触造成金额荒谬。去皮键要在重量稳定时才能生效,避免抖动瞬间把波动值当新零点。
金额显示我固定保留两位小数,重量显示保留一位小数(克),这样在OLED有限的宽度上排版比较整齐。实测稳定从按下到显示更新的总延迟大约0.5秒,主要受10Hz转换速率和滤波采样时间限制,用户主观感受是"即时响应"的,完全够用。
5.4 烧录调试的体验优化:串口打印和断点调试不能少
开发过程中强烈建议在代码里保留一个串口调试输出开关,通过宏定义控制。串口打印HX711原始ADC值、滤波后的值、标定系数、计算重量。在C8T6上没有多余UART的场景很罕见,直接复用板载USB转串口芯片对应的PA9/PA10即可。串口打印给了问题定位极大的方便,比如可以只打印原始ADC值来判断是传感器问题还是计费逻辑问题。
如果你手上有ST-Link或者J-Link,直接把SWD调试口接上,在Keil里设断点、在线看变量,比串口打印更好用。我实测下来这个项目用SWD调试的效率提升是肉眼可见的,尤其是排查滤波数组越界、浮点溢出这类问题,能直接定位到崩溃现场。
6. 功能延伸与下一步扩展方向
这套硬件平台做完电子秤之后,还能顺理成章扩展出很多功能,扩展空间也是这个项目作为毕设或产品的价值体现。
第一,加EEPROM存储标定参数。现在标定参数是存在代码里的,每次重新烧录都要重新标定。加一颗AT24C02或者在STM32内部Flash里开一个扇区专门存zero_offset和scale,开机自动加载,这样换电池、断电重启后不用重新标定,非常实用。标定时校准一下,写完EEPROM再做校验读取,确认无误后设备才算真正"出厂"。
第二,加蓝牙/Wi-Fi模块做数据上报。通过串口连接一个HC-06蓝牙模块或者ESP8266,把称重数据传到手机App或小程序,这在水果店结算、快递驿站场景里很有竞争力。技术上和主功能完全解耦,相当于在现有串口打印的代码基础上换一个输出目标,难度不高但给人的项目完整度印象完全不同。
第三,加打印功能。热敏打印机模块在电商上几十块钱就能买到,串口指令基本兼容,打一张带重量、单价、金额的小票,整台设备就接近商用的电子秤了。注意打印时要记得把金额转换成字符数组,并校验打印缓冲区的长度是否超出,别让浮点转字符串这一步毁掉整单数据。
我自己做完这版之后的最大感触是:这个项目的代码量不算大,核心驱动就三块——HX711、OLED、按键扫描,但把所有边界情况都处理好、让设备真的稳定跑一周不带重启,花费的精力和时间远超过写出第一版代码。做嵌入式项目,往往是"功能实现"只是开始,"稳定可靠"才是真正的分水岭。
如果你照着这个方案做下来,卡在某个环节的时候,优先检查硬件连接和供电,再去看代码逻辑,不要一开始就怀疑是HX711芯片坏了。我做这个项目的分享就到这里,希望这份完整的方案能帮你顺利跑通自己的智能电子秤。