
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寸OLEDI2C接口只占两个引脚对比度高在光线暗的柜台环境下比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总线最高36MHzAPB2最高72MHz。I2C挂在APB1上所以I2C时钟频率不能超过36MHz实际配置成100kHz或400kHz都可以OLED用400kHz刷屏更快。I2C引脚我选的是PB6SCL和PB7SDA这是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驱动芯片分辨率128x64I2C地址通常是0x788位地址或0x3C7位地址。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.10.20.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。标定的时候不用连电脑看串口直接在屏幕上就能看到数据变化效率高很多。这个页面在正式使用时隐藏通过长按某个组合键进入。