
这次想跟各位聊聊我手头刚落地的一个嵌入式项目基于STM32的智能计价电子秤。从电阻应变式称重传感器、HX711采样、OLED显示、矩阵键盘到计价逻辑、EEPROM校准参数保存整条链路全部由一颗STM32F103C8T6完成。整机成本控制在百元以内实现的功能却覆盖了传统商用电子秤九成以上。文章里我会把硬件接线表、HX711驱动代码、标定公式和调试踩坑记录全部放出来。不管你是做嵌入式毕业设计、想搞一台智能仪表原型机还是单纯想拿STM32练手这篇内容都能帮你省下大量查资料和试错的时间。1. 项目概述这是一台“信号采集人机交互数据管理”的微型设备1.1 先拆需求电子秤的核心功能到底有哪些接到这个需求时我的第一反应不是直接画电路板而是先把手头的要求拆成一条条能落地的功能清单。对于一台智能计价电子秤用户眼中的核心功能至少有六项第一称重准确显示单位实时可见第二支持去皮和清零第三可以输入单价由系统自动计算总价第四可以累计多笔交易金额第五掉电之后还能保留标定参数和常用单价第六最好留有通信口方便接蓝牙模块或者把数据上传到外部平台。除了这六项我还额外加入了几个“智能”特性自动零点跟踪也就是一段时间内检测到秤上没有重物时自动微调零点消除温漂带来的误差超载报警重量超过量程后蜂鸣器鸣叫并禁止计价手动标定模式不需要外接电脑在按键上操作就能完成两点标定掉电保护把参数和当前交易状态实时存进EEPROM。这些功能看着不多但每一项背后都对应着驱动、逻辑、存储三层代码的配合做起来并不轻松。1.2 芯片选型逻辑为什么必须用STM32芯片选择上我直接否掉了51和Arduino。51虽然便宜但外设资源太少要接24位ADC、I2C EEPROM、OLED再加USB通信总线和中断资源根本不够用。Arduino Uno上手快但它的AVR芯片在运行多任务状态机时RAM和Flash都比较紧张而且后续要做成产品时Arduino的硬件生态和引脚排布也不够工整。最终选了STM32F103C8T672MHz主频、64KB Flash、20KB SRAM片上USB、ADC、I2C、USART全都有一颗不到十块钱性价比非常高。选择STM32还有一个更实际的原因资料实在太多了。不管是HX711的驱动还是OLED的显示网上都能找到一大把参考代码。当你遇到问题时可以快速搜索到别人踩坑的帖子这会大大缩短开发周期。对于学生做毕业设计、工程师做产品原型来说生态就是生产力。如果你后续想把项目升级成物联网网关STM32加FreeRTOS的组合也是成熟方案不会被现有代码锁死。1.3 系统分层从硬件到业务逻辑的完整链路整个系统可以分成五层。最底层是电源和接口层包括5V适配器输入、LDO降压到3.3V以及传感器激励电源。往上是指标层由称重传感器和HX711组成。再往上是主控层STM32负责采集数据、滤波、标定并运行整机的功能状态机。再往上是交互层包括OLED显示、LED指示灯和矩阵键盘。最高层是数据层包含EEPROM固化的标定参数、单价表以及UART/TTL通信接口。这种分层不是随便分的。它意味着如果你想把OLED换成LCD12864只需要改driver层不用动业务逻辑如果你想换一颗MCU只要把驱动层重新适配计价状态机和标定算法可以原封不动地搬过去。我在后面写代码时也严格按照这个分层来做事实证明后期加蓝牙通信、加USB虚拟串口时几乎没动过核心逻辑的代码。2. 硬件设计小成本大摊位的电气基础2.1 核心模块选型传感器、ADC芯片、显示与输入称重传感器我选的是常见的电阻应变式铝制传感器量程5kg精度可以达到0.01kg。这种传感器的本质是一个全桥电路受力后电阻值会发生变化差分输出电压非常微弱通常只有几毫伏不能直接进STM32的ADC。因此中间必须加一颗专业的前端芯片也就是HX711。这颗24位Delta-Sigma ADC内置了可编程增益放大器支持增益64和128同时供电电压范围宽接口只有两线非常符合这种低成本仪表的场景。显示器件我一开始纠结过OLED和LCD1602最终选了0.96寸OLED常用驱动是SSD1306通过I2C接口连接只需要接两根线。相比LCD1602需要8根数据线或者4线模式一堆引脚OLED的接线和驱动简单得多而且可视角度和对比度都更好。如果你做的是工业场景也可以换成LCD12864或者断码段码屏代码逻辑基本一致。按键部分我使用的是4x4矩阵键盘16个按键覆盖了从0到9的数字输入、小数点、去皮、累计、清除、确认等全部操作。矩阵键盘比独立按键省IO用8个引脚驱动16个按键对IO资源有限的MCU来说非常友好。主控选择STM32F103C8T6之后我还加了一颗AT24C02 EEPROM用于数据保存、一个蜂鸣器用于按键反馈和报警。整个BOM清单算下来核心元器件成本在20元以内加上传感器和外壳整机成本不到100元。这个成本控制对于想做成产品原型或者参加比赛的人来说相当有吸引力。2.2 IO资源规划与电路连接要点规划好外设之后我来列出STM32的IO分配表这是硬件设计的第一份文档画板子、写代码都要以它为基准。功能芯片引脚说明HX711 SCKPA0时钟线25个脉冲完成一次读取HX711 DOUTPA1数据输出DOUT拉低表示数据就绪OLED SCLPB6I2C时钟与EEPROM共享OLED SDAPB7I2C数据与EEPROM共享AT24C02 A0/A1/A2接地从机地址设为0xA0UART1 TX/RXPA9/PA10调试串口UART2 TX/RXPA2/PA3预留蓝牙模块矩阵键盘行线PC0-PC34行扫描输出矩阵键盘列线PC4-PC74列输入带上拉蜂鸣器PC5按键反馈与超载报警电源电路是最容易翻车的地方。称重传感器和HX711的模拟电源与STM32的数字电源必须做好隔离至少要用0欧电阻或者磁珠将AGND和DGND分开否则数字噪声会直接串进模拟量里导致称重数据频繁跳字。我给HX711的AVDD接了一个RC滤波器并在传感器激励端并联了104和10uF的两级去耦电容实测读数稳定了很多。另外要注意HX711的模拟电源AVDD与数字电源VCC虽然都由同一路3.3V或5V供电但供给传感器的激励电压建议用独立的线性稳压器或者至少用电感隔离。如果HX711和电机、继电器等感性负载共用电源那些瞬间浪涌会让ADC数据变得完全没法用。3. 软件开发环境与整体架构3.1 工程搭建CubeMX配置与目录规划我使用STM32CubeMX配合Keil MDK 5来搭建工程。先用CubeMX创建基于STM32F103C8T6的工程配置72MHz主频时钟使能GPIO、I2C、USART、TIM等外设然后生成初始化代码。有人喜欢用标准库或者寄存器操作但对于这种外设多的项目HAL库加上CubeMX可以减少非常多的底层初始化工作。不过HAL库的抽象层可能会让某些对时序要求高的外设驱动变慢比如HX711这种IO翻转很快的接口我建议直接用寄存器位操作或者LL库来操作引脚而不是调用GPIO_SetPinLevel函数。实测下来直接操作BSRR/BRR寄存器比调用HAL库函数快好几倍而且代码量也没增加多少。工程目录我按driver、app、service、bsp四个层次划分bsp板级初始化代码包括时钟、GPIO、I2C、TIM等driverHX711、OLED、EEPROM、矩阵键盘等外设驱动service滤波算法、标定算法、计价状态机、菜单逻辑app主函数和任务调度。这样的分层好处很明显以后换屏、换按键方案、换MCU时只需要改driver层业务逻辑和算法代码不用动。我用这个工程做过二次修改从OLED换成LCD1602只花了半小时。3.2 裸机超级循环还是FreeRTOS有朋友一上来就问要不要上FreeRTOS。我的建议是如果你的项目功能真的需要并发任务比如一边测温度一边处理UI一边网口通信那么上RTOS没有问题但如果只是为了显得高级那就没必要。这台电子秤的主循环大约只需要处理三件事HX711采样、OLED刷新、按键检测完全可以用一个5ms周期的超级循环完成。RTOS会增加上下文切换开销和调试复杂度对这颗MCU来说属于过度设计。不过为了后续拓展我采用了基于周期调度的超级循环框架。用一个1ms定时器中断作为时间基准维护一个任务表每个任务有独立的周期HX711采样周期为10ms按键扫描为10msOLED刷新为100ms计价显示刷新为500ms。主循环永远只做三件事查当前时间、查任务表、执行到期任务。这个结构既不引入RTOS复杂度又能保证各个任务的实时性后续加一个超时报警任务只需要往任务表里加一行。3.3 计价状态机别用一堆if-else硬扛计价逻辑我不建议用一堆if-else去写而是用状态机来做。整个系统可以划分成4个状态空闲、称重、单价输入、结算。空闲状态下按键可以被唤醒称重状态下实时显示重量和总价单价输入状态下数字键和小数点可修改单价结算状态下按确认完成一次交易把本次金额加入累计值。状态之间的切换由按键事件触发处理上你只需要一个state变量然后在主循环里不断查询当前状态并调用对应的处理函数。比如按键事件“去皮”不管在哪个状态都会先做一个按键合法性判断再调用去皮函数。这种写法让界面逻辑和业务逻辑分离代码读起来一目了然。维护起来也比一长串if-else舒服得多因为你能一眼看出某个状态会进入哪些下一个状态。4. 核心模块实现从读传感器到显示、计价4.1 HX711驱动时序、读取、滤波与标定4.1.1 读懂HX711的通信时序HX711的时序逻辑很简单上电后DOUT为高电平表示数据还没就绪当DOUT被拉低时说明转换完成准备好输出数据。此时MCU需要在SCK引脚一共输出25个脉冲前24个脉冲用来读出24位二进制数据第25个脉冲用来设置下一次转换的增益。每个SCK高/低电平的持续时间不少于0.2微秒。值得注意的是如果你在第25个脉冲之后继续保持SCK为高电平超过60微秒HX711会进入掉电模式这是很多人不小心触发的坑。读取数据时要注意24位数据是以二进制补码形式输出的在增益为128时可读输出的有效范围大约为0x800000到0x7FFFFF。我写了一个简单的读取函数用上升沿逐个脉冲读取DOUT状态拼成一个int32_t值。如果DOUT一直为高超过500毫秒还没拉低就返回超时错误避免在主循环里死等。uint8_t HX711_Read(int32_t *value) { uint8_t i; uint32_t raw 0; // DOUT高电平表示数据未就绪等待拉低 if (HX711_DOUT_READ()) return 0; // 前24个脉冲读取24位数据 for (i 0; i 24; i) { HX711_SCK_HIGH(); delay_us(1); HX711_SCK_LOW(); delay_us(1); raw (raw 1) | HX711_DOUT_READ(); } // 第25个脉冲设置增益为128 HX711_SCK_HIGH(); delay_us(1); HX711_SCK_LOW(); // 24位补码转int32_t if (raw 0x800000) raw | 0xFF000000; *value (int32_t)raw; return 1; }4.1.2 数据滤波单纯平均是不够的HX711虽然精度高但传感器输出电压极其微弱哪怕一两毫伏的干扰都会被放大成很大的数字跳变。我采用了滑动平均滤波也叫递推平均滤波保留最近N次采样值每次新采样进来去掉最老的值求平均值。N我取10对于5kg量程的秤来说1秒10次采样滤波拖尾不会太明显。如果你想做得更细可以在采样值入库前再用一个中值滤波去除极端毛刺。有一个细节很多人会忽略滤波不能只在采样层做在标定和计价时也要统一使用同一个滤波结果否则你会遇到一个奇怪的现象——零点漂移已经补了但放上重物后跳变仍然很严重。原因是传感器本身的机械滞后和温漂不是单纯软件滤波能解决的。最有效的硬件法是加屏蔽线、让传感器引线尽量短并把HX711周围的地平面做大软件滤波只能缓解不能根治。4.1.3 标定公式与流程重量到底怎么算出来的电子秤的标定是整个项目最重要的环节没有之一。传感器的输出是毫伏级差分电压经过HX711放大后得到的是一个与质量线性相关的原始整数但这个比例系数每个传感器都不一样。我的做法是两点标定空秤状态下连续读取50次滤波后的HX711原始读数取平均值记为zero放上一个已知重量的标准砝码比如2kg读取稳定后的平均值记为value计算比例系数 scale (value - zero) / 2000单位是每克对应的ADC读数实际称重时重量gram (current - zero) / scale。把zero和scale存进EEPROM下次开机直接装载不需要每次上电都重新标定。实际标定时有几个坑第一称重平台必须先水平放置四角都要受力均匀否则你会在不同的角得到不同读数这就是四角误差需要在机械结构上做调平第二标定用的标准砝码一定要足够准有条件用校准砝码没有的话拿几个矿泉水瓶配重也可以但只能当参考第三标定过程要在传感器预热3到5分钟后进行因为第一次上电时传感器内部温度还没稳定零漂会很明显。4.2 非阻塞按键扫描矩阵键盘的正确姿态矩阵键盘的扫描如果用阻塞延迟整个主循环就会被卡住。我采用非阻塞扫描用一个10ms的定时中断或者主循环中判断时间戳达到10ms就调用一次KeyScan函数。函数内部先把所有行线置为低电平读取列线的状态如果检测到低电平再逐行拉低并读取对应的列线识别按键编号然后通过消抖逻辑连续确认10ms到20ms后再返回按键事件。消抖不仅仅是为了防抖动还可以顺便实现长按和连击逻辑。比如在单价输入模式下按住数字键1超过500ms可以快速连续输入多个1。如果没有这种需求也可以只识别短按。整个扫描函数里禁止使用延时只用时间戳判断这样才能保证主循环不会被阻塞。4.3 OLED显示信息分层与刷新策略OLED屏的驱动本身不复杂SSD1306支持I2C和SPI两种接口我使用的是I2C接口只需要按照命令字节和数据的顺序操作即可。显示界面我分成重量区、单价区和总价区三个区域。重量区实时显示当前重量保留两位小数单位显示kg单价区显示用户输入的单价保留两位小数总价区用大字体显示金额。如果用了大字体点阵需要自己做取模或者用开源工具生成字库。注意OLED刷新不要过于频繁100ms刷新一次足够否则会白屏闪烁。另外一个经验是OLED驱动时最好把显示缓冲区开在MCU的RAM里所有绘图操作先画到buffer再一次性刷新到屏幕。这样避免了一边改内容一边刷屏导致的闪烁而且代码结构更干净。SSD1306的显存是128x64位用缓存数组是1KB在F103上完全放得下。4.4 计价逻辑整数运算代替浮点精度才有保障计价其实是纯逻辑计算但要防止数据类型溢出。我用重量以克为单位整数存储单价以“分/kg”为单位整数存储总价 重量 * 单价 / 1000再除以100得到元。这样全程使用整数运算避免浮点导致的不确定性。最后显示时再补小数位。这种做法的好处是可以精确到分不会出现浮点误差。如果你一定要用浮点务必用float并保留两位但在嵌入式中能避免浮点就避免。另外价格输入功能我设计的是支持最多8个数字键输入前两位为整数部分后面四到五位小数采用加权方式写入小数点通过确认键切换。这个交互逻辑虽然细节多但做出来后用户使用起来非常直观。4.5 去皮、清零与超载保护去皮和清零功能很容易混淆。清零是把当前重量作为新的零点保存到RAM中主要用于消除秤盘和轻微残留物去皮是把当前重量作为皮重保存称重显示毛重减去皮重的净重并且去皮之后还可以继续累计。状态机上我把去皮定义为一个单独事件调用去皮函数后当前HX711读数被记录为tareOffset之后的重量计算改为 rawValue - zero - tareOffset。清零则直接修改zero值并需要保存到EEPROM。超载保护也不能省。当滤波后的重量超过量程上限比如超过5.1kg时蜂鸣器长鸣计价显示区显示超量程提示并禁止继续计算金额。电子秤在商用场景里最怕的就是用户不看量程硬往上放重物如果没有超载保护传感器非常容易永久损坏。5. 数据存储、通信扩展与产品化细节5.1 AT24C02掉电存储与参数恢复AT24C02是一片256字节的I2C EEPROM在3.3V供电下最高支持400kHz时钟。我在开头规划4个字节存标定零点4个字节存比例系数剩下几十个字节存最近几次交易的单价、总价和时间戳。写EEPROM时要注意AT24C02每个page是8字节写操作不能跨页必须按页拆分否则数据会乱掉。另外EEPROM写入次数有限不能频繁写所以不要在每次称重结束后立刻写入而是加一个掉电自动保存逻辑或者隔一段时间保存一次。掉电检测具体可以这样做用一个电阻分压网络检测电源电压当主电源降到3.5V以下时触发一个外部中断。STM32利用电源掉电到完全关机前的几十毫秒时间把当前标定参数和交易数据写入EEPROM。需要注意的是HX711和传感器先断电所以读取这些外设不可靠只能把已保存的RAM数据写进EEPROM。我实际测试过从检测到掉电到电压跌破工作范围大约有20到50毫秒的窗口写入128字节数据完全来得及。5.2 UART、蓝牙与USB扩展为什么要在电子秤上留通信口因为很多商用场景需要把重量数据同步到外部系统比如后台软件、手机APP甚至云平台。我预留UART2接口默认波特率9600输出轻量级JSON帧。接一个常见的HC-05蓝牙模块后手机端就能通过串口调试APP看到数据。如果要做更远距离的通信可以换成NRF24L01或者LoRa模块但要注意这些模块需要额外的IO口和中断最好在规划引脚时先留出位置。STM32F103C8T6本身带USB外设可以做成USB HID设备也可以做成USB CDC虚拟串口。CDC模式调试很方便和UART类似但要处理好USB时钟源和上拉电阻。我第一次移植的时候因为没有在CubeMX中选择正确的时钟源电脑一直提示无法识别设备后来把USB引脚配置好、时钟源切换到PLL产生的48MHz频率并把晶振准确率稳定在0.25%以内问题就解决了。USB功能在开发阶段相当实用因为不需要额外接一个USB转串口板子直接通过TYPE-C线就能在电脑上打印调试日志。5.3 从原型到产品外壳、绝缘与工艺电路板设计好之后外壳对传感器精度的影响超乎想象。如果你只是随便把传感器螺丝拧在一块木板上受力不均匀会直接导致四角误差和重复性变差。我的做法是传感器一端固定在底座金属板上另一端通过双头螺柱连接秤盘确保受力方向始终垂直于应变梁。安装时必须保证四个固定螺丝的扭矩一致否则会让应变梁产生初始应力导致零点漂移严重。还有一个小细节是称重传感器支架不能有毛刺也不能有明显的磕碰变形。我打磨过几次之后发现机械结构的平整度比电路精度更容易影响称重结果。你花大把时间优化软件滤波可能还没有把底座调平来得管用。6. 调试实录与常见问题排查表6.1 HX711读数跳动不已的排查路径这个现象占了整个项目调试时间的80%。我的排查步骤是先用示波器看DOUT波形排除引脚虚焊和信号没接好的问题再用万用表检查HX711的AVDD电压是否稳定然后在传感器和HX711之间用屏蔽线替换普通杜邦线。如果这些都排除了大概率是零点未校正或滤波参数不合适。我的项目里最后发现是传感器金属外壳没接地直接导致数据小幅跳动接好地之后稳定很多。针对电源噪声我额外加了LC滤波即一个电感和一个电容组成的低通滤波器给HX711供电。效果很明显原来10mV级别的纹波降到了1mV以内。需要注意的是HX711电源端的去耦电容一定要尽量靠近芯片的AVDD引脚否则滤波等于白做。6.2 OLED白屏、花屏的处理OLED白屏最常见的两个原因是I2C地址不对和复位脚悬空。SSD1306器件地址默认是0x787位地址0x3C但有的屏模块是0x7A你需要看背面的电阻配置。另一个原因是OLED的复位脚RES如果悬空上电后主控发初始化指令它可能不响应必须要有一个明确的高电平复位过程即使硬件上没有复位引脚也要在代码里先拉高RES或直接简单延时后重新初始化。花屏的原因通常是I2C速率过高和供电不足。SSD1306的I2C最高支持到400kHz但如果你把速率调到1MHz很多屏就会乱码。解决办法是降速到100kHz或200kHz同时加大OLED供电的电容。晶体振荡器频率不准也会影响I2C时序但F103内部RC振荡器在常温下勉强可用你要不放心可以改成外部晶振。6.3 STM32芯片被锁死无法烧录的应急处理很多人在调试SWD下载时遇到过这个问题代码里把调试接口引脚当成普通IO用结果程序跑一次之后调试器再也连不上了。处理方法是按住板上复位键不放在Keil下载时先点击下载按钮再松开复位键利用芯片复位瞬间的操作擦除Flash更简单的方法是直接用串口ISP把Flash全部擦除。日常开发时我会在代码初始化最后加一个延时判断比如3秒内没收到任何命令就恢复调试引脚功能留一个后门避免自己把自己锁死。6.4 称重偏大偏小与四角误差的校正思路重量偏大偏小优先怀疑标定系数重新做一次标准砝码标定。如果发现四个角落读数偏差超过5g那就是机械结构问题需要用垫片把秤盘四个支撑点调平。有些高精度电子秤还会做多点分段标定比如5kg量程分成3段每段用不同的比例系数但这需要更大的Flash存储和更复杂的算法对入门项目来说两点标定足够。6.5 常见问题速查表故障现象可能原因解决思路读数跳动不停止电源纹波大、屏蔽不良、零点漂移加强去耦、接地屏蔽、重新校准零点读数一直为0传感器接线错误、增益配置不对用万用表测差分电压、检查第25脉冲重量偏大偏小标定系数不准用标准砝码重新标定OLED花屏I2C速率过高、供电不足降速、加大电源电容烧录失败调试引脚被复用、电源不稳定复位时序擦除检查BOOT0按键误触发上拉配置不对、消抖时间过短配置上拉、增加消抖时间数据保存丢失写EEPROM跨页按页拆分或调整存储布局USB无法识别USB时钟源/晶振不准配置USB时钟源、使用准确晶振整套项目做到最后个人最大的体会不是代码写得有多漂亮而是“稳定”两个字有多难。电子秤这种产品看起来功能简单但一旦放到真实使用环境中温度变化、电磁干扰、机械抖动都会让数据肉眼可见地漂移。真正能扛住商用的方案往往会在硬件隔离、标定流程和滤波策略上做足功夫而不是一味堆功能。如果你也想自己动手做一个我强烈建议先按标准方案跑通基础功能再逐步升级不要把第一步的目标定成“什么都加进去”那往往是后期BUG不断的根源。这套基于STM32的智能计价电子秤完整方案从硬件选型到软件状态机我都有详细记录希望它能帮你在嵌入式项目上少走一些弯路。