1. 为什么选择STM32做一台智能计价秤
1.1 从菜市场到实验室:一个被低估的嵌入式练手项目
电子秤这东西,几乎每天都在用,菜市场、快递站、药店、厨房,到处都是它的身影。但大多数人只把它当成一个工具,从来没想过它内部是怎么跑的。我当初选这个题目,说白了就是想找一个"麻雀虽小五脏俱全"的嵌入式项目——它得同时涉及模拟信号采集、数字通信、人机交互、数据存储,最好还能联网。翻来覆去对比了一圈,智能计价电子秤几乎是完美命中所有需求点。
一台计价秤的核心逻辑其实很朴素:称重传感器把重量转成微弱电压信号,ADC把它变成数字量,MCU做滤波和标定,再乘以单价算出总价,最后显示出来。听起来简单,但真正动手做的时候你会发现,光是称重这一环就够折腾好几天。HX711这个24位ADC芯片,采样率到底选10Hz还是80Hz,滤波窗口开多大,零点漂移怎么处理,每一个细节都直接影响最终精度。
选STM32而不是51或者Arduino,理由很直接。第一,STM32的定时器资源丰富,做按键扫描、数码管动态刷新、串口通信互不干扰;第二,内置Flash可以存单价和校准参数,掉电不丢;第三,生态成熟,HAL库、标准库、LL库随你挑,网上资料多到看不过来。至于ESP8266,那是给这台秤加"联网"属性的关键——称重数据上传到云平台,远程就能看库存和销售记录,这才配得上"智能"两个字。
这个项目适合谁?我觉得有三类人值得动手做一遍:一是刚学完STM32基础外设、想找个综合项目练手的学生;二是准备嵌入式岗位面试、需要一个能讲清楚的项目经历的求职者;三是想给自己小店做一台定制计价秤的动手派。不管你是哪一类,只要跟着把硬件搭起来、代码跑通,你对嵌入式系统的理解会上一个台阶。
1.2 整机方案怎么定:模块化拆解思路
做任何嵌入式项目,我习惯先把系统拆成几个独立的功能块,每块单独验证,最后再拼起来。这台秤我拆成了五个模块:称重采集模块、主控与计算模块、人机交互模块、数据存储模块、联网通信模块。
称重采集用HX711配电阻应变式传感器,这是目前低成本高精度称重的标配组合。HX711内部集成了稳压电源、时钟振荡器和24位ADC,外围电路极其简单,两个普通IO口就能读数据。主控选STM32F103C8T6,也就是大家常说的"最小系统板",72MHz主频、64KB Flash、20KB RAM,跑这个项目绰绰有余,关键是便宜好买。
人机交互我用了0.96寸OLED加四个机械按键。有人喜欢用数码管,亮度高、看着像正经电子秤,但数码管需要动态扫描,占用定时器资源,而且显示内容有限。OLED用I2C接口,两根线搞定,能显示中文、能画图标,调试的时候还能当printf用,性价比更高。按键方面,去皮、单价设置、确认、清零,四个键覆盖所有操作。
数据存储直接用STM32内部Flash模拟EEPROM,存单价、校准系数、累计销售额。这里有个坑要注意:STM32的Flash擦写寿命大约1万次,如果每次称重都写,很快就废了。我的做法是只在单价修改和校准完成时写入,日常称重数据放RAM里,定时或关机前再存。
联网模块用ESP8266-01S,通过AT指令和STM32串口通信。为什么不用ESP32直接做主控?因为ESP32的ADC精度和稳定性在称重场景下不如STM32配外置ADC,而且分开设计的好处是联网挂了不影响称重,系统更稳。ESP8266负责把重量、单价、总价、时间戳打包上传到云平台,手机端就能远程查看。
提示:模块化设计最大的好处是调试时能快速定位问题。称重不准就只看HX711那一路,联网不上就只看串口AT指令,不会眉毛胡子一把抓。
2. 硬件电路设计与关键器件选型
2.1 称重传感器与HX711的搭配门道
电阻应变式称重传感器的原理不复杂:金属弹性体受力变形,贴在表面的应变片阻值跟着变,四个应变片组成惠斯通电桥,输出一个和重量成正比的差分电压。常见的有5kg、10kg、20kg量程,我建议做计价秤选5kg或10kg,量程太大精度就稀释了。
传感器输出灵敏度一般是1mV/V,激励电压5V的话,满量程输出才5mV。5kg量程对应5mV,那1g重量只对应1微伏。这个信号有多微弱?你用手靠近导线产生的工频干扰都比它大。所以HX711的24位ADC和内置低噪声放大器就派上用场了,它能把信号放大128倍再采样,等效分辨率轻松做到1g以下。
接线的时候有个细节很多人忽略:传感器四根线——激励正、激励负、信号正、信号负,颜色各厂家不统一,红黑绿白是最常见的,但千万别凭颜色硬接。拿万用表量一下,激励端电阻一般在1kΩ左右,信号端在1kΩ左右,两组之间应该不通。接反了不会烧,但读数会反向或者乱跳。
HX711的电路设计有几个关键点。第一,模拟电源和数字电源要分开,AVDD走独立的LC滤波,否则数字电路的开关噪声会串进ADC。第二,基准电压要稳,HX711内部有稳压管,但最好在VSUP和AVDD之间加个10μF钽电容。第三,通道选择,HX711有A、B两个通道,A通道增益128,B通道增益32,称重必须用A通道。第四,采样率选择,芯片引脚RATE接低电平是10Hz,接高电平是80Hz。10Hz抗干扰好但响应慢,80Hz响应快但噪声大。计价秤我建议用10Hz,因为人对一两秒的稳定时间完全能接受,但读数跳动会让人怀疑秤不准。
2.2 STM32最小系统与外围接口分配
STM32F103C8T6的引脚分配得提前规划好,不然画PCB或者搭洞洞板的时候会打架。我的分配方案是这样的:
| 功能模块 | 引脚 | 说明 |
|---|---|---|
| HX711数据 | PA0 | 普通输入,配置上拉 |
| HX711时钟 | PA1 | 普通输出 |
| OLED SCL | PB6 | I2C1时钟 |
| OLED SDA | PB7 | I2C1数据 |
| 按键去皮 | PA2 | 输入上拉 |
| 按键单价 | PA3 | 输入上拉 |
| 按键确认 | PA4 | 输入上拉 |
| 按键清零 | PA5 | 输入上拉 |
| ESP8266 TX | PA9 | USART1 TX |
| ESP8266 RX | PA10 | USART1 RX |
| 调试串口 | PA13/PA14 | SWD下载调试 |
这里有个经验:HX711的时钟线不要和I2C或者串口共用同一组GPIO,因为HX711读数据时时钟是软件模拟的,时序要求严格,如果被中断打断容易读错。我一开始把HX711的SCK放在PB6,结果和OLED的I2C时钟冲突,调试了半天才发现。
电源部分,整机用5V适配器供电,AMS1117-3.3给STM32和OLED供电,HX711直接用5V。ESP8266峰值电流能到300mA,所以3.3V这一路要留足余量,AMS1117至少选800mA的,旁边并个220μF电解电容。
2.3 ESP8266联网模块的接入方式
ESP8266-01S只有8个引脚,VCC、GND、TX、RX、CH_PD、RST、GPIO0、GPIO2。和STM32连接只需要四根线:VCC、GND、TX、RX。注意ESP8266的TX接STM32的RX,RX接STM32的TX,交叉连接。
CH_PD要拉高才能工作,我直接接3.3V。GPIO0和GPIO2悬空就行,正常运行时不需要管。供电是最大的坑:ESP8266瞬时电流大,如果和STM32共用一路3.3V且走线细,联网瞬间电压跌落会导致STM32复位。我的做法是ESP8266单独用一个AMS1117供电,或者至少在它的VCC脚旁边放一个100μF以上的电容。
AT指令通信的波特率默认是115200,但有些模块出厂是9600,第一次用最好先发AT测试,不回就换波特率再试。STM32这边用USART1,开接收中断,把ESP8266返回的数据存到环形缓冲区里,主循环里解析。
注意:ESP8266和STM32的串口通信一定要加电平匹配。虽然ESP8266是3.3V,STM32也是3.3V,理论上直连没问题,但如果你的STM32板子是5V供电的,TX输出5V电平会打坏ESP8266的RX。保险起见串个1kΩ电阻。
3. 软件架构与核心算法实现
3.1 主程序框架:前后台还是RTOS
这个项目要不要上RTOS?我的答案是:不用。任务数量不多,实时性要求也不苛刻,前后台架构完全够用,而且代码更简单、更好调试。主程序就是一个大循环,配合SysTick定时器做时间片调度。
我的主循环结构是这样的:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); MX_USART1_UART_Init(); MX_TIM2_Init(); HX711_Init(); OLED_Init(); Flash_LoadParams(); ESP8266_Init(); while (1) { Key_Scan(); // 10ms调用一次 Weight_Process(); // 100ms调用一次 OLED_Refresh(); // 200ms调用一次 ESP8266_Process(); // 50ms调用一次 Price_Calculate(); // 重量更新后调用 } }每个任务的调用周期用SysTick的毫秒计数来控制,比如:
if (tick - last_tick >= 10) { last_tick = tick; Key_Scan(); }这种写法比RTOS简单得多,而且不会出现任务优先级反转、栈溢出这些糟心事。对于计价秤这种单线程逻辑清晰的项目,前后台架构是最优解。
3.2 HX711驱动与称重滤波算法
HX711的驱动核心是软件模拟时序。它没有标准SPI接口,数据在时钟上升沿输出,24个时钟脉冲读完24位数据,第25个脉冲选择下一次的通道和增益。
long HX711_Read(void) { long value = 0; uint8_t i; while (HAL_GPIO_ReadPin(HX711_DOUT_PORT, HX711_DOUT_PIN) == GPIO_PIN_SET); for (i = 0; i < 24; i++) { HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_SET); value = value << 1; HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_RESET); if (HAL_GPIO_ReadPin(HX711_DOUT_PORT, HX711_DOUT_PIN) == GPIO_PIN_SET) value++; } HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_RESET); value ^= 0x800000; return value; }读出来的原始值不能直接用,必须滤波。我试过三种方案:均值滤波、中值滤波、滑动窗口加权滤波。均值滤波简单但响应慢,中值滤波抗脉冲干扰好但计算量大,最后我选了去极值滑动平均——连续采10个值,去掉最大最小各两个,剩下6个求平均。这样既抑制了突发干扰,又保证了响应速度。
滤波之后是标定。标定分两步:先空载读零点值,再放标准砝码读满量程值,算出斜率。
weight = (raw_value - zero_offset) * full_scale_weight / (full_scale_raw - zero_offset);这里有个细节:零点会随温度漂移。早上校准好的秤,中午可能就偏了几克。我的做法是每次称重前先判断重量是否小于某个阈值(比如5g),如果是就自动更新零点。这叫"自动跟踪零点",商用秤都有这个功能。
3.3 计价逻辑与按键交互设计
计价逻辑本身不复杂:总价 = 重量 × 单价。但实际使用中有几个场景要处理好。
去皮功能:放上容器后按去皮键,当前重量记为零点,之后显示的重量是净重。这个功能在菜市场特别常用,塑料袋、托盘都要去皮。
单价设置:按单价键进入设置模式,用去皮和清零键加减数值,确认键保存。单价支持到小数点后两位,单位是元/千克。
累计功能:每笔交易完成后按确认键,总价累加到累计销售额里,存Flash。这个功能对小商户很实用,一天下来不用自己加。
按键处理我用状态机+消抖。机械按键按下时会有10-20ms的抖动,直接读会误触发。我的做法是每10ms扫描一次,连续3次读到相同状态才确认。
typedef struct { uint8_t current; uint8_t last; uint8_t count; uint8_t event; } Key_t; void Key_Scan(void) { for (int i = 0; i < KEY_NUM; i++) { uint8_t read = HAL_GPIO_ReadPin(keys[i].port, keys[i].pin); if (read != keys[i].last) { keys[i].count = 0; keys[i].last = read; } else { if (keys[i].count < 3) keys[i].count++; if (keys[i].count == 3 && read == 0) { keys[i].event = 1; // 触发按键事件 } } } }3.4 OLED显示界面布局
0.96寸OLED分辨率128×64,能显示4行16×16的中文字符,或者8行8×16的ASCII。我的界面布局是这样的:
第一行显示"智能计价秤"标题,第二行显示当前重量,第三行显示单价,第四行显示总价。称重时重量数值用大字体,设置模式下单价闪烁提示。
OLED驱动用I2C,STM32的硬件I2C有时候会卡死,我建议用软件模拟I2C,虽然速度慢一点,但稳定可靠。刷新率不用太高,200ms刷一次肉眼完全看不出闪烁。
显示内容要格式化,比如重量显示"1.234 kg",单价显示"12.00 元/kg",总价显示"14.81 元"。浮点数转字符串用sprintf,但要注意STM32的sprintf默认不支持浮点,需要在工程设置里勾选"Use MicroLIB"或者自己写定点数转字符串函数。
4. ESP8266联网与数据上云
4.1 AT指令配置流程与避坑
ESP8266出厂固件默认支持AT指令,但不同版本指令集有差异。我用的AT固件版本是1.7.4,配置流程如下:
AT # 测试模块是否响应 AT+CWMODE=1 # 设置为Station模式 AT+CWJAP="SSID","PWD" # 连接WiFi AT+CIPMUX=0 # 单连接模式 AT+CIPSTART="TCP","api.example.com",80 # 建立TCP连接 AT+CIPSEND=100 # 发送100字节数据每一步都要等模块返回"OK"才能进行下一步。这里最大的坑是超时处理。如果WiFi密码错了,AT+CWJAP会一直重试,STM32如果死等就卡死了。我的做法是每个AT指令发出去后启动一个5秒定时器,超时就报错重来。
还有一个坑是AT+CIPSEND之后要等">"提示符才能发数据,很多人直接发数据导致失败。正确流程是:
ESP8266_SendAT("AT+CIPSEND=50\r\n"); ESP8266_WaitFor(">", 2000); ESP8266_SendData(payload, 50); ESP8266_WaitFor("SEND OK", 5000);4.2 数据上传协议设计
上传的数据我用JSON格式,简单通用:
{ "device_id": "SCALE_001", "weight": 1.234, "price": 12.00, "total": 14.81, "timestamp": 1234567890 }HTTP POST请求手动拼装:
void ESP8266_UploadData(float weight, float price, float total) { char json[128]; char request[256]; sprintf(json, "{\"weight\":%.3f,\"price\":%.2f,\"total\":%.2f}", weight, price, total); sprintf(request, "POST /api/scale/data HTTP/1.1\r\n" "Host: api.example.com\r\n" "Content-Type: application/json\r\n" "Content-Length: %d\r\n" "\r\n" "%s", strlen(json), json); ESP8266_SendAT("AT+CIPSTART=\"TCP\",\"api.example.com\",80\r\n"); ESP8266_WaitFor("OK", 5000); char cmd[32]; sprintf(cmd, "AT+CIPSEND=%d\r\n", strlen(request)); ESP8266_SendAT(cmd); ESP8266_WaitFor(">", 2000); ESP8266_SendData(request, strlen(request)); ESP8266_WaitFor("SEND OK", 5000); ESP8266_SendAT("AT+CIPCLOSE\r\n"); }提示:HTTP请求的Content-Length必须和实际数据长度完全一致,多一个字节少一个字节服务器都会拒绝。我一开始用strlen算长度,结果JSON里有中文就出错了,后来改成纯ASCII才稳定。
4.3 断网重连与数据缓存策略
联网最怕的就是网络不稳定。我的策略是:称重数据先存本地,联网成功后再批量上传。STM32的Flash里开一块区域做环形缓冲区,每条记录16字节,能存几百条。ESP8266每30秒尝试联网一次,连上就把缓冲区里的数据全部发出去,发完清空。
断网检测靠AT指令的返回值。如果AT+CIPSTART返回"ERROR"或者超时,就标记网络断开,等下一个周期再试。这里要注意不要频繁重连,ESP8266重连太频繁会发热甚至死机,我设的最小重连间隔是10秒。
5. 调试过程中踩过的坑与排查实录
5.1 称重读数跳动大的排查思路
这是最常见的问题,读数在±5g范围内乱跳。排查顺序如下:
第一步,检查硬件。传感器接线是否牢固,HX711的AVDD和DVDD是否干净,模拟地和数字地是否单点连接。我遇到过因为杜邦线接触不良导致读数跳变的情况,换了焊接线就好了。
第二步,检查电源。用示波器看HX711的VSUP引脚,如果有明显的纹波,加LC滤波。ESP8266联网瞬间的电流冲击也会影响HX711,所以ESP8266的供电要和模拟部分隔离。
第三步,检查采样率。10Hz模式下读数稳定但慢,80Hz模式下快但噪声大。如果用的是80Hz,试试降到10Hz。
第四步,检查滤波算法。如果原始数据本身跳动就大,滤波只能缓解不能根治。先打印原始值看波动范围,如果原始值波动超过1000个LSB,那就是硬件问题。
第五步,检查机械结构。传感器安装是否平整,受力是否均匀,有没有侧向力。传感器用螺丝固定时不要拧太紧,否则会产生预紧力影响零点。
5.2 ESP8266连不上WiFi的常见原因
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 发AT无响应 | 波特率不对 | 依次试9600、115200 |
| 发AT无响应 | CH_PD没拉高 | CH_PD接3.3V |
| 发AT无响应 | 供电不足 | 单独供电,加100μF电容 |
| CWJAP返回ERROR | 密码错误 | 检查大小写和特殊字符 |
| CWJAP返回ERROR | 路由器是5G频段 | ESP8266只支持2.4G |
| CIPSTART超时 | 服务器地址错误 | 用IP地址代替域名试试 |
| 频繁掉线 | 电源纹波大 | 加LC滤波,换LDO |
我印象最深的一次是ESP8266死活连不上,换了三个模块都不行,最后发现是路由器开了"AP隔离",设备之间不能通信。这种问题查手册根本查不到,只能靠经验。
5.3 Flash参数丢失与写入异常处理
STM32的Flash写入有几个硬性要求:写入前必须先擦除,擦除的最小单位是页(1KB或2KB),写入的数据必须是半字(16位)对齐。我一开始没注意对齐,写进去的数据全是乱的。
正确的Flash写入流程:
void Flash_WriteParams(Params_t *params) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase; erase.TypeErase = FLASH_TYPEERASE_PAGES; erase.PageAddress = PARAMS_ADDR; erase.NbPages = 1; uint32_t page_error; HAL_FLASHEx_Erase(&erase, &page_error); uint16_t *p = (uint16_t *)params; for (int i = 0; i < sizeof(Params_t) / 2; i++) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, PARAMS_ADDR + i * 2, p[i]); } HAL_FLASH_Lock(); }还有一个坑:Flash擦写期间CPU会暂停,如果这时候有中断进来,中断响应会延迟。对于计价秤影响不大,但如果做实时控制就要注意了。
6. 项目扩展方向与个人经验总结
6.1 从计价秤到智能称重终端
这台秤做完之后,我发现可扩展的方向很多。加个热敏打印机就能打小票,加个扫码枪就能扫商品条码自动调单价,加个SD卡就能存几个月的销售记录。如果换成4G模块,还能在没有WiFi的地方用。甚至可以把HX711换成更高精度的ADS1232,做实验室级别的分析天平。
软件层面,可以把ESP8266换成ESP32,直接跑MQTT协议,数据实时推送到手机App。或者加个语音模块,称重结果语音播报,对老年人特别友好。
6.2 给后来者的几点实在建议
第一,先跑通最小系统再扩展。别一上来就把所有模块都焊上去,先让HX711能读出数,再让OLED能显示,再加按键,最后加联网。每加一个模块就测试一次,出问题好定位。
第二,善用串口打印。STM32的USART1接ESP8266了,那就用USART2接USB转串口模块,把调试信息打出来。没有串口打印的嵌入式调试就是盲人摸象。
第三,参数要能在线修改。校准系数、零点偏移、WiFi密码这些别写死在代码里,做成可以通过按键或者串口命令修改的,不然每次改都要重新烧录。
第四,注意电磁兼容。HX711是模拟电路,STM32和ESP8266是数字电路,两者要物理隔离。PCB布局时模拟部分和数字部分分开,地平面单点连接。洞洞板搭的话,HX711的线尽量短,远离晶振和电源。
第五,多备几个传感器。应变式传感器很脆弱,过载、摔打、受潮都可能损坏。我做项目期间坏了两个,都是因为砝码放上去的时候没轻放。
这个项目我从画图到调通大概花了三周,其中一半时间在调称重精度,四分之一在调ESP8266联网,剩下四分之一在写界面和逻辑。如果你时间紧,可以先不做联网,把称重和计价做扎实,那已经是一个完整的嵌入式项目了。联网部分可以后面慢慢加,反正接口都留好了。