前阵子帮开小卖部的亲戚做了一个基于STM32的智能计价电子秤。起初我心想,称重乘单价而已,能有多复杂?等真正把方案落地,才发现从传感器信号链、24位ADC、标定算法到按键状态机,每一环都有坑。下面把这套项目的完整方案整理出来:硬件怎么选、电路怎么搭、固件怎么写、标定怎么做、实测精度如何,以及我踩过的几个典型坑,一次性讲透。不论你是做嵌入式课程设计、毕业设计,还是打算入门STM32的信号采集与处理,这套方案都是一个可以完整复现的练手项目。
1. 从"称重"到"计价":需求拆解与整体方案选型
1.1 为什么传统电子秤满足不了这个场景
传统电子秤只能显示重量。市场摊位要算钱,要么卖家用笔算,要么按计算器,速度慢还容易出错。智能计价电子秤要解决的问题其实就三个:准确称出质量、让卖家输入单价、自动算出金额并同时显示。看似简单,但把这三个需求落到嵌入式方案上,每条都有几个难点。
称重要准,意味着模拟前端要有足够的分辨率和抗干扰能力;单价输入要顺手,意味着按键和显示交互要合理;金额计算看着简单,但涉及小数和重量实时刷新,处理不好会出现跳字、抖字乃至计算错误。所以这个项目虽然看起来"不就是个秤",实际横跨了传感器信号调理、ADC采集、滤波算法、人机交互、定点数运算和低功耗设计等多个嵌入式基本功,非常适合当作一个完整的练手项目来做。
1.2 方案选型:为什么是STM32F103C8T6加HX711
我这次选的组合很常见:STM32F103C8T6做主控,HX711做称重ADC。先说主控。F103C8T6是Cortex-M3内核,主频72MHz,Flash 64KB,RAM 20KB,价格便宜,生态成熟,标准库和HAL库的例子一搜一大把,对课程设计和毕业设计来说资料足够,出问题也好排查。更重要的是它的外设接口足够灵活:I2C、SPI、UART、GPIO都是现成的,后续想加蓝牙模块、WiFi模块、微型打印机都不会被接口卡住。
再来说HX711。它是专为电子秤设计的24位Sigma-Delta ADC,内部集成了可编程增益放大器,增益可选32、64、128,带两个差分输入通道,数据输出速率10Hz或80Hz可调。通俗点说,它就是把称重传感器输出的微小电压信号直接放大并数字化,输出24位数字结果,然后通过两线串行接口送给单片机。两线接口意味着单片机端只占用两个GPIO,无论用标准库还是HAL库都很容易驱动。
为什么不直接用STM32内置ADC?这个问题后面专门讲,先放一个简单结论:STM32F103的12位ADC直接采样称重传感器信号,分辨率差了太多,达不到计价秤需要的精度。
1.3 技术指标与功能清单
动手之前先明确指标,不然后面验证无从下手。这套方案我定的设计指标是:
- 最大称量:5kg
- 显示分辨率:1g
- 综合误差:±2g以内(标定后、常温条件下)
- 采样刷新率:大约10次/秒
- 单价输入范围:0.00元到99.99元/500g
- 金额显示范围:0.00元到999.99元
- 附加功能:去皮、清零、超量程报警、开机自动回零
功能清单对应下来就是:一个5kg量程的电阻应变式称重传感器、一块HX711模块、STM32F103C8T6最小系统板、一块0.96寸OLED显示屏、一个4x4矩阵键盘、一个蜂鸣器,外加电源和结构件。整机成本大约在五六十元以内,作为课程设计或者毕业设计完全够用。
这里补充一个会影响后面的设计决策:单价按每500g输入,还是按kg输入,直接决定计价计算逻辑。国内菜市场习惯按斤标价,电子秤行业标准里单价和金额也按每500g填报,我按每500g处理,代码里统一用"分"做整数运算,避免浮点数误差。这一点在计价模块会展开讲。
2. 称重传感器与HX711:信号链上每一环都不能省
2.1 电阻应变式传感器是怎么把重量变成电信号的
称重传感器内部是电阻应变片,贴在弹性体上。当被测物体压到弹性体时,弹性体发生微小形变,应变片电阻值跟着变化,四片应变片搭成惠斯通电桥,把电阻变化转换成差分电压输出。
用大白话说:你在秤盘上放东西,传感器会吐出一个很小的电压差,这个电压差正比于重量。但问题在于,这个电压实在太小了。以我用的5kg量程传感器为例,激励电压5V时,满量程输出约5mV,也就是说每克重量大约对应1微伏的电压变化。1微伏什么概念?普通数字万用表的毫伏档都很难稳定分辨。所以这个信号必须经过高精度差分放大和24位ADC数字化,才能被单片机使用。这就是HX711存在的意义。
2.2 HX711的关键参数解读
HX711是在称重领域里非常经典的一颗ADC芯片,几个关键参数我列出来:
- 分辨率:24位,无失码
- 内置PGA:增益32/64/128可选
- 差分输入范围:±20mV(增益128时)
- 数据速率:10SPS/80SPS可选
- 工作电压:2.6V到5.5V
- 通信接口:2线数字接口
重点解释两个容易被忽略的点。第一是输入范围。增益128时,差分输入范围是±20mV,也就是说传感器满量程5mV只占了ADC量程的四分之一,对应数字码值大约在200万左右。按5kg满量程分摊,每克重对应约400个码值,做1g显示分辨率绰绰有余,甚至显示0.1g也有余量。
第二是采样率。HX711默认的10SPS模式噪声最小,适合静态称重;80SPS模式下速度变快,但噪声会明显变大。电子秤本质是静态称重场景,我建议固定用10SPS,不要为了"显示刷新快"去切80SPS,那是饮鸩止渴。
2.3 传感器接线颜色和HX711引脚说明
市面上的称重传感器线色基本统一,但也见过乱标的,接线前最好用万用表确认一下:
- 红:E+,电桥正激励
- 黑:E-,电桥负激励
- 白:A+(或S+),信号正
- 绿:A-(或S-),信号负
HX711模块上常见的标注是E+、E-、A+、A-、B+、B-。A通道接称重传感器,B通道空着,这个方案用不到。HX711与STM32的接线只需要两根线,我分配如下:
| HX711引脚 | STM32引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 模块供电,和MCU同电源域 |
| GND | GND | 共地 |
| DOUT | PB0 | 数据输出,空闲时为高 |
| PD_SCK | PB1 | 时钟输入,由主机驱动 |
3. 电源、显示与按键:硬件设计里的细节决定成败
3.1 电源方案:LDO为什么比DC-DC更适合这套电路
智能计价秤对电源噪声很敏感。HX711内部的24位ADC精度很高,对供电纹波特别敏感,如果把5V直接通过DC-DC降压给HX711供电,开关电源的纹波会直接耦合进称重结果,表现为读数跳变。我的做法是整体用USB 5V输入,再通过AMS1117-3.3转成3.3V,HX711的VCC和AVDD都从3.3V取,模拟电源路径上再加0.1uF和10uF电容组合。
另一个细节是传感器激励。很多现成的HX711模块在板子上已经做了传感器激励引脚的引出,并放了去耦电容,所以只要VCC本身干净,激励就不会太差;如果自己画板,建议让E+从经过LC滤波的模拟电源路径取,并且靠近HX711摆放去耦电容。传感器激励电压波动会直接影响输出信号,用LDO稳住激励是提升稳定性的关键一步。
3.2 显示器件选型:OLED还是LCD1602
显示我最后选了0.96寸I2C接口OLED,SSD1306驱动。原因有三:第一个是显示内容多,OLED可以自定义显示三行以上信息,把重量、单价、金额和状态提示放在同一屏;LCD1602只有两行,信息排版很憋屈。第二个是I2C接线省引脚,SDA和SCK两根线就能搞定,留给按键和HX711的IO很充裕。第三个是带字库的LCD模块在5V和3.3V电平转换上容易出问题,OLED在3.3V系统里是天然配套。
OLED驱动代码网上很多,这里不展开,但要注意SSD1306是屏幕刷新率不高的器件,不要在每次重量变化时都全屏清空重绘,应该只在数值变化时更新对应区域,否则会出现明显闪烁,还会拖慢主循环,连累HX711的读取时序。这个问题我在后面的踩坑记录里还会具体说。
3.3 矩阵键盘和状态提示
按键我用了4x4矩阵键盘,占用8个GPIO。为什么不用独立的键盘芯片?因为矩阵键盘的扫描逻辑本身就是一个很好的嵌入式基本功,状态机、消抖、组合键都能练到,而且成本最低。键位布局我定义为:
| 1 | 2 | 3 | 清零 |
|---|---|---|---|
| 4 | 5 | 6 | 去皮 |
| 7 | 8 | 9 | 单价 |
| 0 | . | 确认 | 退格 |
"单价"键进入单价输入模式,"确认"键结束输入,回到称重模式。这个交互逻辑不是随便定的,它模拟了市面上商用收款秤的通用操作方法,用户不需要看说明书就能上手。
蜂鸣器接在PB5,用于按键反馈和超载报警。超载判定条件很简单:滤波后的重量超过最大量程的105%,蜂鸣器连续鸣叫,显示屏显示"OVER"。
4. 固件实现:从HX711驱动到计价状态机的完整逻辑
4.1 HX711数据读取:别小看两线时序
HX711的读取时序是:单片机不断读取DOUT引脚,当DOUT从高电平变为低电平时,表示一次24位转换数据已就绪。此时单片机在PD_SCK上产生24个上升沿,每个上升沿从DOUT移出一位数据,MSB先出,数据是24位二进制补码。读完24位后,再产生第25个上升沿,这个脉冲用来设置下一轮转换的增益和通道——发一个脉冲表示下次按A通道128倍增益工作;如果发两个脉冲是按B通道32倍;发三个脉冲是A通道64倍。
核心代码大概是这样的:
int32_t HX711_Read(void) { int32_t value = 0; uint8_t i; // 等待数据就绪:DOUT由高变低 while (HAL_GPIO_ReadPin(HX711_DOUT_GPIO_Port, HX711_DOUT_Pin) != GPIO_PIN_RESET); // 读取24位数据 for (i = 0; i < 24; i++) { HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_SET); value = (value << 1) | HAL_GPIO_ReadPin(HX711_DOUT_GPIO_Port, HX711_DOUT_Pin); HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET); } // 第25个脉冲,设置下一次为A通道、增益128 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); // 24位补码扩展为int32 if (value & 0x800000) { value |= 0xFF000000; } return value; }这里面有三个坑。第一个是等待DOUT变低前,如果主循环里还干别的事(比如刷OLED),很可能错过DOUT下降沿,所以要么在定时中断里读,要么保证主循环调用这个函数的周期小于HX711的100ms转换周期。第二个是除了第25个脉冲之外,不要在24个数据位之间额外增加延时,但每位移完之后PD_SCK拉低的时间要大于1微秒,才能满足HX711时序要求。第三个是补码扩展必须做,否则读出来的负值会被当成正数,去皮和回零全都会乱套。
4.2 滤波算法:称重读数为什么不能直接展示
即使HX711质量不错,原始读数也避免不了轻微的跳动。原因包括传感器机械振动、电源纹波、环境电磁干扰。如果直接把原始值换算成克数显示,屏幕末位会一直跳,顾客看着就不专业。
我采用的组合滤波策略:先用一个5点中值滤波剔除明显干扰,再对中值滤波后的序列做10次滑动平均。中值滤波的作用是去掉脉冲型干扰,比如不小心碰到秤台产生的冲击;滑动平均的作用是平滑随机噪声。这个组合在静态称重场景下效果很好,代价是响应速度慢了一点,大约0.5秒后读数稳定,对计价秤来说这个延迟用户基本无感,换来的是干净的显示,划算。
4.3 标定算法:把ADC码值翻译成克数
HX711输出的是数字码值,跟克数之间没有必然换算关系。每只传感器的灵敏度、安装偏差、增益误差都不一样,所以必须标定。标定其实就两步:测零点和测已知重量。
第一步,空秤状态下读滤波后的码值,记为zero_raw。第二步,放一个已知重量的砝码,比如1000g标准砝码,读滤波后的码值,记为span_raw。那么每克对应的码值就是:
scale_per_gram = (float)(span_raw - zero_raw) / 1000.0f;之后任意时刻的重量计算公式:
weight_g = (float)(raw - zero_raw - tare_raw) / scale_per_gram; if (weight_g < 0.5f) weight_g = 0.0f;tare_raw是去皮时记录的空容器读数。标定参数需要保存在掉电不丢失的地方,我用的是STM32内部Flash的最后一页,F103C8T6有64KB Flash,最后1KB专门留出来,通过HAL_FLASH_Program写入,上电时再读出来。
标定流程在固件里做成一个隐藏菜单:长按"单价"键5秒进入标定模式,第一次提示放空秤按确认,第二次提示放1000g砝码再按确认,标定完成后自动退出,参数写Flash。这个交互不要学那些把标定代码写死、只能改源码重烧的做法,因为传感器用久了会疲劳,秤体搬运或更换传感器后都需要重新标定,留一个操作入口非常实用。
4.4 计价计算:用整数避免浮点误差
再次强调,金额计算全部用整数。单价的单位是"分",重量单位是"克",如果约定单价是每500g的价格,那么金额(分)的计算公式是:
uint32_t amount_cents = (uint32_t)weight_g * price_cents / 500;举例:重量350g,单价8.00元/500g,单价换算成分是800。350乘以800除以500等于560,也就是5.60元。全部是32位整数运算,没有浮点精度问题。为什么不用float?单次浮点看不出问题,但如果连续多次计算再累加,单精度只有约7位有效数字,商用计价场景不允许这种隐性问题。这个思路在嵌入式里就叫"定点化"——能用整数就不用浮点。
重量显示也做了处理:显示值保留一位小数,但计价用的是未舍入的内部值,避免出现"显示8.0克但金额按8.9克算"的乌龙。显示和计算分离,这也是智能秤和普通"读个数自己算"秤的重要区别。
4.5 按键状态机:从输入到称重的切换逻辑
固件整体用一个简单状态机组织,主要状态有三个:
- 称重状态(默认):实时显示重量、单价、金额
- 单价输入状态:数字键输入单价,显示区显示当前输入
- 标定状态:按标定流程引导用户操作
按键消抖用20ms延时加电平确认。单价输入用字符串拼接再转整数的方式实现,限制最多5位(对应99.99元),输入过程中"."键作为小数分隔控制。确认键结束输入并保存单价,同时在非易失存储里也存一份,下次开机还会记住上次的单价。
这里有一个容易忽略的交互细节:去皮后重量显示为0,但内部tare_raw有了偏移量,此时如果放上商品,计算逻辑要同时考虑zero_raw和tare_raw,千万别把去皮偏移覆盖掉零点偏移,否则重新开机后秤会"飘"。
5. 实测数据、误差归因和几个典型踩坑记录
5.1 实测精度数据
标定完成后我连续测了几组数据。用一套实验室电子天平(分辨率0.01g)做参照,结果如下:
| 参照重量(g) | 本机显示(g) | 误差(g) |
|---|---|---|
| 0.00 | 0.0 | 0.0 |
| 10.00 | 10.1 | +0.1 |
| 100.00 | 99.8 | -0.2 |
| 500.00 | 500.2 | +0.2 |
| 1000.00 | 1000.4 | +0.4 |
| 2000.00 | 1999.6 | -0.4 |
| 5000.00 | 5003.0 | +3.0 |
前段线性很好,接近满量程时误差明显增大。这个趋势是正常的,传感器本身有非线性,满量程附近灵敏度会略有变化。如果要求全量程都做到±2g,就需要做多点分段标定,比如每500g取一个标定点做线性插值,而不是只用一点斜率。对菜市场计价场景来说,常见称重区间在100g到2kg,这个精度已经足够。
5.2 踩坑一:HX711供电纹波导致读数乱跳
第一版我用了一个3.3V的DC-DC模块直接给整板供电,结果空秤读数波动超过±30个码值,换算成重量就是乱跳。排查过程是先怀疑传感器,换了一只新传感器,问题依旧;再怀疑滤波算法,去掉滤波直接读原始值,跳动依旧;最后用示波器看供电,发现DC-DC的开关纹波大概有50mV左右,而HX711的模拟电源对噪声非常敏感。换成AMS1117线性稳压后,空秤波动降到±3个码值以内,问题解决。
这个坑说明一个原则:模拟前端和数字电路的电源必须分开处理。至少做到HX711的VCC前面加磁珠或者LC滤波,PCB布局上模拟地和数字地单点汇接。
5.3 踩坑二:标定时砝码放偏导致标定失真
标定时我把1000g砝码随手放在秤盘边上,标定完成后发现小重量段全部偏小。原因是传感器对偏载很敏感,同一重量放在秤盘中心和边缘,输出不一样。这个现象叫偏载误差,正规电子秤会有四角调整,但我们的简易结构做不到,所以标定时机必须保证砝码放在秤盘中心,并且用同一个参考位置来做。实测同样1000g砝码,中心位置和边缘位置的码值差大约0.3%,反映到重量上就是3g。
5.4 踩坑三:机械结构引起的回零慢
秤盘靠四颗螺丝固定在传感器上,最初螺丝拧紧程度不统一,导致每次放上重物再拿走后,回零要等好几秒,甚至回不到原来位置。原因有两个:一是传感器弹性体受力不均,产生了应力残留;二是螺丝孔处的结构变形导致迟滞。处理办法是四颗螺丝按对角线顺序、用相同力矩拧紧,同时给秤盘和传感器支架之间加了一个薄的硅胶垫,吸收装配应力。改完之后回零恢复时间缩短到2秒以内。
5.5 踩坑四:OLED刷新闪烁和局部更新
一开始我在主循环里每次读到新重量就全屏清一次再重绘,结果OLED刷新频率跟不上,屏幕一直在闪。SSD1306的I2C速率有限,全屏64x32的缓冲区刷新一次要几十毫秒,10Hz的显示更新叠加这个耗时,主循环被卡死,HX711的读数据时序都受影响。后来改成只在数值变化时更新对应文字区域,刷新时间大幅缩短,HX711读取也能稳定在10Hz周期内完成。这也是一个提醒:显示刷新和传感器采集在裸机方案里是互相争抢CPU的两个任务,一定要估算好各自的耗时,别让它们互相拖累。
6. 这套方案还能怎么扩展,以及我个人的一些建议
6.1 扩展方向:通信、存储与操作系统
如果做毕设想出彩,这套方案可以往几个方向延伸。第一个是加通信模块,比如HC-05蓝牙或者ESP8266 WiFi,把每次称重的重量、单价、金额、时间打包上传到手机App或者小程序,实现店铺销售数据统计。第二个是加微型热敏打印机,打印含二维码的购物小票,这样整套系统就非常接近商用的POS秤。第三个是引出串口和计算机做上位机联调,展示重量波形和标定曲线,这个方向适合往"嵌入式+上位机"的方向延展。
软件层面的扩展首推FreeRTOS。当前裸机状态机已经够用,但一旦加了蓝牙、打印、存储、屏幕多个任务,裸机循环就会变得难以维护。用FreeRTOS之后,HX711采集、显示刷新、按键扫描、通信协议各自独立任务,用队列传递数据,代码结构会清晰很多。我在这个项目验证完裸机版本之后,也顺手把FreeRTOS版本跑通了,整体线程模型就是四个任务加两个消息队列,开发效率提升明显。
6.2 模块化设计:把称重模块独立出来
我的最终版本把电路分成了两块板:一块是主控板,包含STM32、OLED、键盘、蜂鸣器;另一块是称重板,包含HX711和传感器接口。两块板之间用排针排母连接。这个设计一开始看起来多此一举,但后来发现好处很多:一是调试时可以单独用USB转TTL工具直接测HX711原始码值,不用带键盘屏幕;二是称重板可以复用到其他项目,比如改造成智能配料秤、液体计量器;三是主控板升级换代不需要动模拟部分,降低了重新验证的成本。
6.3 个人心得:做这类项目,时间和精力怎么分配
最后说点个人的体会。做完这个项目最大的感受是:整个方案的难点根本不在STM32本身,而在于模拟信号处理和机械结构。打个比方,STM32就像一个人,学习能力强,什么活都能干;但HX711和传感器才是真正出力气的地方,它们的电源、布线、安装方式决定了最终称重准不准。很多人做电子秤项目把精力全放在代码上,结果程序写得很花哨,秤却连基本精度都达不到,这就是本末倒置。
如果你也想复现这个项目,我的建议是:先把HX711原始码值通过串口打印出来,用肉眼观察空秤和加载时的码值变化,把信号链路调稳了,再开始写显示、计价、状态机这些上层逻辑。硬件不稳定,软件写再多也是空中楼阁。反过来,只要传感器信号稳定,后面的逻辑都是水到渠成的事。