1. 项目整体方案与技术选型解析1.1 先想清楚智能计价电子秤到底要做什么做嵌入式这几年我见过太多人拿到“基于STM32的智能计价电子秤”这类题目上来就翻芯片手册、找传感器画板子结果做了三个月才发现功能定义都是糊涂的。智能计价电子秤说到底不是一个硬件题目是一个带严格功能约束的小型电子系统。你把它拆开看核心需求无非这么几条能测重量、能显示重量、能输入单价、能算金额顺便要个去皮和超量程报警。如果连需求都没定清楚后面所有选型都是瞎撞。比如很多人问我“要不要用高精度ADC专用芯片”“要不要上RTOS”我反问他你的量程是5kg还是20kg精度要1g还是5g是给生鲜摊贩用的还是给实验室用的这两问一出来一般人就沉默了。量程5kg、精度1g和量程50kg、精度10g传感器方案、前端放大方案完全不一样成本差好几倍。我给的参考需求是这样的称重范围0到5kg分辨率1g支持单价录入和金额自动计算单位为元/kg支持去皮归零超量程时蜂鸣器报警。显示部分用一块小型LCD或OLED即可按键做4x4矩阵或者独立按键都行。这样一个定位刚好覆盖毕设和入门项目的典型场景既不会简单到没东西可写也不会复杂到半年都啃不下来。1.2 系统架构从传感器到显示的完整数据通路这个项目的数据通路特别清晰非常适合用来理解嵌入式系统是怎么一层一层把物理量变成数字再变成人可读信息的。整个链路是称重传感器感受压力输出微弱的差分电压HX711芯片把差分电压放大并转成24位数字量STM32通过模拟时序读取这个数字量经过滤波和标定换算成克数再结合单价计算出金额最后把重量、单价、金额刷到屏幕上。我画一下脑中的结构图底层是电源模块5V USB供电进来一路直接给传感器激励和HX711模拟电源另一路经过稳压降到3.3V给STM32、OLED、逻辑部分。中间层是STM32主控它管三件事读HX711、扫按键、刷新显示。上层是逻辑功能包括去皮、标定、计价、报警。这种分层的好处是每一层都能独立调试传感器硬件没接好可以先不焊用按键把显示和状态机调试完再回头调HX711问题定位快得多。我见过一些同学喜欢一上来就裸机全部读写一气呵成主循环里又把传感器读出来又算金额又扫键盘又刷屏结果一个逻辑Bug查半天。我的建议是哪怕不引入RTOS也要把代码按模块拆开hx711.c管采集、filter.c管数据处理、key.c管按键、oled.c管显示、main.c只管状态流转。这不是为了炫技是为了你自己调试的时候能少掉头发。1.3 关键器件选型与替代方案主控我选STM32F103C8T6这是嵌入式入门的绝对主流。Cortex-M3内核72MHz主频64KB Flash20KB RAM48脚LQFP封装带3个USART、2个I2C、2个SPI12位ADC。这个配置做电子秤绰绰有余价格还便宜生态资料多到你想找不到参考都难。更重要的是它GPIO复用配置非常经典换到后续F4、G0系也一样是那套思路。称重传感器选电阻应变式的悬臂梁传感器量程5kg。这种传感器结构简单四根线红黑是激励绿白是信号配合固定孔可以直接锁在铝型材底座上。内部核心是4个应变片组成的惠斯通电桥受力后电桥失衡输出一个与载荷成比例的差分电压。输出灵敏度一般是2.0mV/V也就是说5V激励下满量程也就10mV。这个量级的信号直接进STM32内置ADC是浪费——内置12位ADC满量程是3.3V采一个10mV的小信号有效分辨率惨不忍睹。这就需要HX711上场。HX711是专为电子秤设计的24位高精度ADC内部自带可编程增益放大器常见的增益有32、64、128支持两路差分输入。选它的核心理由有三个便宜、封装好焊接SOP-16、驱动代码简单只需要两个GPIO口模拟时序就能读数据完全不用碰SPI或I2C协议。同类产品里有ADS1232、CS1232性能相当但资料少一些新手没必要折腾。显示我用的是0.96寸I2C接口OLED128x64分辨率。这里很多新手纠结选LCD1602还是OLEDLCD1602成本略低、中文显示麻烦OLED显示信息密度高接两根线就能用而且网上现成驱动一堆。按键部分配一个4x4矩阵键盘预留单价输入和小键盘功能比单纯几个独立按键灵活得多。矩阵键盘虽然扫描代码稍微多一点但也就十来行的事后面在状态机里可以扩展成完整数字输入。2. 硬件电路设计称重前端与人机交互2.1 电阻应变式传感器与HX711的配合原理先给小白补一点基础惠斯通电桥可以理解成一个特别灵敏的“跷跷板”。四个电阻臂在不受力时完全平衡两个输出端的差分电压为零受力后拉力侧的应变片电阻增大、压力侧的应变片电阻减小电桥失衡输出一个微小的差分电压。这个电压与施加的力基本成线性关系这就是称重传感器能测重力的基本原理。接线的时候一定注意传感器激励方向红黑两根是电源线白绿是信号线。给传感器通电后先测一下白绿两端的差分电压空载时应该是0mV左右会有微弱失调用手压一下应该有明显变化这个验证必须在焊接前做避免焊完才发现传感器是坏的或者焊反了。HX711与STM32的连接我用的两个普通GPIOPD_SCK接PB13DOUT接PB12。芯片上电后DOUT一直保持高电平只有一次ADC转换完成后才拉低。读取时序是这样的主控检测到DOUT为低就开始给PD_SCK发脉冲每个脉冲的高电平期间DOUT输出一位数据从最高位开始总共读24位第25个脉冲用来设定下一轮转换的增益和通道。老规矩通道A、增益128是电子秤最常用的配置。这里有个容易踩的坑DOUT脚不能悬空也不能在没有共地的情况下接ST-Link调试器。很多新手用USB供电但调试器单独供电两边没共地HX711读数要么恒为0xFFFFFF要么乱跳排查半天才发现是地的问题。我习惯在HX711的VCC和GND之间就近并一个100nF高频去耦电容再加一个100uF电解电容电源更干净采样稳定性明显好。2.2 显示、按键与主控引脚分配OLED接I2C1也就是PB6SCL和PB7SDA地址一般是0x3C7位地址。I2C总线是漏极开路的需要上拉电阻STM32内部虽然可以开启上拉但实际用下来还是建议在PCB上直接加两个4.7kΩ上拉到3.3V信号沿速度和稳定性都更好。矩阵键盘我用了4根行线加4根列线接到STM32的PB0到PB7。扫描思路非常笨但有效行线设为推挽输出轮流拉低其中一行列线设为输入上拉检测哪个列被拉低。行列交叉点就是按键位置。比如第一行拉低时读列发现第3列为低那就是(1,3)这个键被按下。引脚分配的时候我特意留了个心眼PB3和PB4这两个脚默认是JTDO和NJTRST不是普通IO。如果哪天真需要它们必须先在代码里禁用JTAG重新映射我一直把它俩留作备用避免上来就给自己挖坑。另外PA13和PA14是SWDIO和SWCLK烧录要用绝对不要分配到外设上去。蜂鸣器接了PB8配一个NPN三极管驱动别用PA12这种特殊引脚万一以后要USB枚举还得重新布线。这个项目本质上不需要USB功能但谁说得准后面想不想加个串口上位机留一点裕量没那么难PCB上多放几个过孔又不花时间。2.3 电源设计与PCB布局里最容易忽视的点电源是这个项目里最容易被低估的一环。很多初学者认为USB供电随便拉个稳压芯片就完事但HX711是24位ADC对模拟电源噪声特别敏感。我建议的供电拓扑是USB的5V进来后直接给传感器激励和HX711的AVDD同时5V经过AMS1117-3.3稳压到3.3V给STM32、OLED、按键和逻辑电路。HX711的VCC模拟电源部分用RC滤波比如串一个10Ω电阻再并一个100uF电容把数字开关噪声隔离在模拟电路之外。PCB布局顺序我走了两版才稳定第一版把HX711放得离STM32太近数字IO翻转的噪声耦合进模拟前端空载时读数跳变0.5g左右。第二版把HX711和传感器接线端子放在板边STM32和电源放另一侧并且模拟地AGND和数字地GND单点连接跳变降到0.1g以内。模拟地单点连接的意思是所有模拟器件的地先汇到一起再通过一个0欧电阻或焊桥连到数字地避免数字电流穿过模拟地平面。新手最容易搞反的还有芯片封装的引脚方向。STM32F103C8T6是LQFP48芯片上有个小圆点小圆点所在的左下角就是第1脚然后逆时针数。焊接之前先用万用表二极管档测一下VDD和VSS有没有短路再上电测量各个电源引脚的电压是否正常。我见过有人把芯片焊反了依然能下载程序——因为SWD只需要两个引脚刚好没受影响但程序一跑全乱这是最简单的排查手段永远不要跳过。3. 软件实现从裸数据到稳定显示的完整链路3.1 HX711驱动与24位数据读取HX711的驱动不需要任何外设就是两个GPIO口配合循环。核心函数就这么一个先等DOUT拉低再发24个时钟脉冲把数据移出来。我贴一下我项目里实际用的代码#define HX711_SCK_PIN GPIO_PIN_13 #define HX711_SCK_PORT GPIOB #define HX711_DOUT_PIN GPIO_PIN_12 #define HX711_DOUT_PORT GPIOB #define HX711_DOUT_READ() HAL_GPIO_ReadPin(HX711_DOUT_PORT, HX711_DOUT_PIN) #define HX711_SCK_HIGH() HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_SET) #define HX711_SCK_LOW() HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_RESET) int32_t HX711_Read(void) { uint32_t data 0; uint8_t i; // DOUT为高表示正在转换为低表示转换完成 while (HX711_DOUT_READ()); for (i 0; i 24; i) { HX711_SCK_HIGH(); data data 1; if (HX711_DOUT_READ()) { data; } HX711_SCK_LOW(); } // 第25个脉冲下一次转换设置为通道A增益128 HX711_SCK_HIGH(); HX711_SCK_LOW(); // 24位数据是二进制补码格式第24位是符号位 if (data 0x800000) { return (int32_t)(data - 0x1000000); } return (int32_t)data; }这段代码已经在我手头好几个项目里跑过了逻辑很清楚。唯一要注意的是那个while循环等待DOUT拉低的逻辑如果HX711没焊好、供电不对、或者DOUT悬空程序会卡死在这个while里。所以调试初期最好在等待之前加个超时计数比如循环10万次还没等到就返回上一次读数至少不会让整机表现成“死机”。还有一个细节第25个脉冲结束之后HX711会自动开始下一轮转换转换时间大概几百微秒。如果主循环里不停调用读取函数采样速度会非常快但数据里夹杂的噪声也多所以后面必须加滤波。3.2 数据滤波为什么裸读数会跳以及怎么压下去HX711是很灵敏的灵敏的代价是噪声也进来了。机械振动、人手抖动、电源纹波、甚至桌面细微的形变都会反映在24位数据上。裸数据直接换算成重量显示你会发现最后一位甚至三位一直跳这在电子秤上是不可接受的。我常用的策略是“排序均值”组合滤波连续读7次原始值从小到大排序弃掉最大值和最小值剩下5个取平均。这个思路结合了中值滤波抗脉冲噪声和均值滤波降随机噪声的优点对电子秤这种场景特别合适。#define FILTER_COUNT 7 int32_t Weight_GetFiltered(void) { int32_t buf[FILTER_COUNT]; int32_t sum 0; uint8_t i, j; int32_t temp; for (i 0; i FILTER_COUNT; i) { buf[i] HX711_Read(); } // 简单冒泡排序7个数据规模很小效率无所谓 for (i 0; i FILTER_COUNT - 1; i) { for (j 0; j FILTER_COUNT - 1 - i; j) { if (buf[j] buf[j 1]) { temp buf[j]; buf[j] buf[j 1]; buf[j 1] temp; } } } for (i 1; i FILTER_COUNT - 1; i) { sum buf[i]; } return sum / (FILTER_COUNT - 2); }实际测下来7次采集加排序平均单次处理时间大约几毫秒对称重场景完全够用。如果你想要更顺滑的显示效果还可以在此基础上做一阶低通滤波当前值等于上次值乘以系数0.7加上新采样值乘以0.3。这样数字跳动肉眼基本看不出来。我还加了一个“回零判定”当计算出的重量小于0.5g并且持续500ms都是这个状态就强制显示为0.0g。这个细节能解决一个很实际的问题——传感器本身有零点漂移空秤经常显示0.2g看着很难受强制回零后观感好很多。3.3 两点标定法与重量换算公式标定是所有电子秤项目的灵魂。很多人把HX711读到的数直接当重量显示当然会乱套。ADC读数和实际重量之间存在一个线性关系实际重量等于当前读数减去零点偏移乘以比例系数。我们要做的就是把这两个参数标定出来。我的标定流程特别简单两步走第一步空秤状态多读几次取平均值作为zero_offset第二步放上一个已知重量的标准砝码我用500g的标准砝码没有就用一袋密封的500g盐讲究点用电子天平复核一下记录带载读数load_offset。比例系数就是砝码重量除以带载与零点的差值int32_t zero_offset 0; // 空秤零点 float adc_per_gram 0.0f; // 每个克对应的ADC读数增量 void Weight_Calibrate(void) { zero_offset Weight_GetFiltered(); // 放上标准砝码例如500g int32_t load_read Weight_GetFiltered(); adc_per_gram (float)(load_read - zero_offset) / 500.0f; } float Weight_GetGram(void) { int32_t raw Weight_GetFiltered(); return ((float)(raw - zero_offset)) / adc_per_gram; }这个两点标定模型对菜市场级别和毕设级别完全够用了。但我必须提醒你一个细节标定必须在系统充分预热后进行。传感器和HX711都有温漂刚上电10分钟和上电1小时的空秤读数能差不少。我踩过这个坑第一天标定测试时精度很好第二天一早开机测试发现偏了3g差点以为传感器坏了其实只是没预热。如果你追求更好一点的线性度可以用三段式分段标定在1g、500g、5kg三个点分别标定区间内用线性插值。这样对非线性误差有明显改善但代码复杂度高一些毕设做两点标定已经能写明白原理了。3.4 计价逻辑、去皮操作与主循环状态机计价逻辑听起来高大上其实就是一行乘法金额等于重量乘单价注意单位换算。如果重量算出来单位是克单价单位是元每千克那金额就是重量除以1000再乘单价。我建议内部全部用float算显示的时候再四舍五入到分。去皮功能实现也很直接按下“去皮”键时把当前滤波后的ADC值记录下来作为tare_offset未来计算重量时先用当前读数减去tare_offset再减去zero_offset。注意去皮之后托盘或容器放上去了如果再把容器拿走计算出来的重量会变成负数这时候要强制显示为0否则会出现“负重量”这种诡异界面。主循环我用一个switch状态机来组织这个结构清晰也好扩展typedef enum { STATE_WEIGHT, // 正常称重 STATE_PRICE_SET, // 设置单价 STATE_SUM, // 累计金额 STATE_OVERLOAD // 超量程报警 } SystemState; void Main_Loop(void) { switch (current_state) { case STATE_WEIGHT: Weight_Update(); if (key_price_pressed) current_state STATE_PRICE_SET; if (weight_gram 5000) current_state STATE_OVERLOAD; break; case STATE_PRICE_SET: Price_InputHandler(); if (key_confirm_long_pressed) current_state STATE_WEIGHT; break; case STATE_SUM: Sum_Product(); current_state STATE_WEIGHT; break; case STATE_OVERLOAD: Buzzer_On(); if (weight_gram 4900) { Buzzer_Off(); current_state STATE_WEIGHT; } break; } }按键扫描用10ms定时器中断打标志主循环检测到标志后做消抖和状态判断。这里强调一点中断里只做置标志和计数千万不要在中断里调用HX711读取因为读取逻辑里有一个等待循环会把中断流程拉得很长轻则影响屏幕刷新重则直接拖垮系统调度。4. 工程搭建与调试实录4.1 Keil MDK工程模板创建与常见报错工程模板这一步真的能劝退很多新手。我见过有人卡在“创建STM32工程”一个礼拜还没跑起来流水灯。几个最常见的问题先说在前面。第一是芯片支持包没装。Keil MDK装好以后默认不带STM32F1的器件支持需要去Pack Installer里安装Keil.STM32F1xx_DFP这个pack。没有它新建工程时Device列表里根本找不到STM32F103C8T6。网上有些人说Keil破坏了版权不能装pack那是老黄历了新版Keil用Community版加DFP包完全可以正常开发学习。第二是C51和ARM共存的问题。很多人电脑上又装了Keil C51又装了MDK装的时候如果你闲得没事把安装目录改到同一个文件夹会出现两个产品互相覆盖的问题打开工程后找不到编译器。解决办法只有一个C51和MDK分开目录安装比如C:\Keil_v5和D:\KeilC51各自独立。第三是工程路径带中文或空格。热词里那个报错信息我印象太深了load d:\stm32 prohect\2-1 stm32工程模板\objects\project.axf error这种就是路径里既有空格又有中文编译器偶尔能编译过但调试器要找axf文件时各种奇怪问题。我的习惯是工程全部放在纯英文路径下比如D:\embed\scale_project文件夹也用小写字母少给自己添堵。4.2 下载调试axf加载失败与连接不上的排查顺序“Load axf error”这句话吓退过无数新手。这个错误的本质是调试器没能把编译好的程序烧录进芯片但它背后可能的原因有好几个不是单靠百度一个报错就能解决的。我总结一套排查顺序排查完基本能解决90%的问题。第一确认ST-Link被电脑识别。打开设备管理器如果插上ST-Link连未知设备都没有第一件事检查USB线是不是只能充电不能传数据。这种线我家里一堆但千万别用在调试器上。第二确认SWD的四根线没接错SWDIO、SWCLK、GND一定有SWDIO和SWCLK接反了是下载失败的高发原因。第三确认目标板供电正常。有个经典场景ST-Link给板子供电时板子上的AMS1117发热严重导致电压跌落芯片根本起不来自然烧不进程序。第四在Keil的Options里检查Debug选项卡是否选了ST-Link DebuggerSettings里能不能识别到目标芯片IDCODE如果没有八成是接线或供电问题如果IDCODE能读到但还是下载失败点开Flash Download勾上Reset and Run然后尝试把下载速度降到1MHz。我也遇到过芯片完全锁死的情况频繁下载把芯片的读保护触发了连接时会提示“Cannot access target”。这种用ST-Link Utility做一次Full Chip Erase就能解锁但要注意整片擦除后程序也没了重新烧一次就好。4.3 几个经典翻车现场我把自己做过电子秤后踩过的坑整理一下这几个问题你早晚会遇到。第一个是延时函数卡死。现象是代码跑到某个Delay函数后整机不动了。最常见原因是你在中断服务函数里调用了HAL_Delay或自写的基于SysTick的延时。SysTick中断优先级默认最低你在一个中断里调用延时SysTick中断根本抢不进来于是死锁。解决手法是中断里只置标志位延时逻辑放到主循环做或者干脆用DWT计数器做延时不进中断就不会有优先级问题。第二个是JTAG引脚复用。如果你的按键正好占用了PB3、PB4、PA15这三个脚你会发现按键死活不工作。这是因为默认情况下它们是JTAG调试口。解决办法是在GPIO初始化之前加这两行__HAL_RCC_AFIO_CLK_ENABLE(); __HAL_AFIO_REMAP_SWJ_DISABLE_JTAG();注意必须先开AFIO时钟再执行重映射顺序反了会看到函数执行成功但引脚状态不变。禁用了JTAG之后还能不能下载程序只要SWDPA13、PA14还保留着就可以正常用ST-Link下载调试完全不受影响。第三个是OLED显示乱码或白屏。九成原因是I2C设备地址不对。0.96寸OLED常见7位地址是0x3C但驱动库里有些写的是8位地址0x78差一个bit就整屏无响应。先写一个I2C扫描程序把所有地址枚举一遍确认实际地址之后再动驱动。另外I2C两根线上一定要有上拉电阻如果板上没焊程序里开启内部上拉能勉强跑但信号质量差长线稍微长一点就乱码。第四个是HX711数据恒为0xFFFFFF或恒为0。先检查DOUT和PD_SCK有没有接反再检查传感器信号线焊盘有没有虚焊然后用万用表量一下传感器红黑之间有没有5V电压。别问为什么问就是十次里有八次是这两件事。5. 常见问题速查与避坑清单5.1 一张表说清楚现象、成因、解法我把这个项目从硬件到软件前后出现的典型问题做成了一个速查表后面做别的嵌入式项目也能用上。很多问题不是技术含量多高而是排查路径不对先把现象定位准确再按表里给的思路查会省非常多时间。现象可能的成因排查与解决下载报axf errorSWD接线错误、目标板未供电、Flash Download未配好按第4.2节顺序排查识别设备、查接线、查供电、勾选Reset and Run、降速到1MHzHX711读数为0xFFFFFFDOUT/SCK接反、传感器未共地、HX711供电异常确认引脚、补共地、量VCC和传感器激励电压重量数值跳动超过1g电源纹波大、滤波深度不够、机械振动加RC滤波、增加采样次数、做排序均值滤波、紧固传感器螺丝空秤不回零零点漂移、传感器预紧力不一致重新标定零点、软件强制回零阈值按键偶尔失灵或乱触发没有消抖、PB3/PB4被JTAG占用加10ms消抖、禁用JTAG并重映射OLED白屏或乱码I2C地址错、SDA/SCL上拉缺失写I2C扫描程序确认地址、补4.7k上拉延时函数卡死中断里调延时、SysTick被更高优先级阻塞中断只置标志位、延时放主循环或改用DWT蜂鸣器一直响不停超量程状态未退出、阈值回差太小增加回差逻辑比如低于4900g才退出报警程序退出低功耗模式后乱跑唤醒源配置不对、时钟切换有问题检查RTC或外部中断配置唤醒后重配时钟焊接完上电短路电源和地焊连、芯片引脚方向反目测万用表二极管档测VDD/GNDLQFP48小圆点左下角是第1脚5.2 做这个项目我个人最有价值的几条经验整个项目做完我最想分享的不是某一颗芯片的用法而是几条做嵌入式项目的通用心法。第一条经验是“先跑最小系统再画功能板”。第一版我直接画了完整的电子秤PCB焊完后一上电找不到问题点到底是传感器电路问题还是主控没跑起来全板都有嫌疑。后来改成先用现成的STM32最小系统板加杜邦线把HX711、OLED、按键都连起来确认整个数据链路逻辑没问题之后再去画整合PCB。这条路走下来第二版PCB一次点亮焊接完基本没调硬件就出数了。第二条经验是“串口打印永远是最好的调试工具”。LCD或OLED显示正常不代表内部数据正常滤波前后的原始值、标定系数、环境温度换算结果我都用串口往电脑上打印。UART不需要太多引脚一根TX加一个CH340转USB就能看所有内部变量。很多时候你看一眼打印数据就知道是零点漂了还是系数算错了完全不用瞎猜。第三条经验是“焊接质量直接决定采样质量”。HX711是24位精度如果你引脚焊接温度太高时间太长或者用了劣质助焊剂没洗干净模拟输入端的漏电流会导致读数缓慢漂移。我调第一版板子时发现重量会慢慢往上“爬”排查了传感器和程序都没问题最后用洗板水把板子彻底洗干净再试数据就稳了。这件事给我印象太深了——嵌入式不光是软件和电路工艺本身也算关键变量。最后建议所有做这个项目的人把代码用Git管起来。很多毕设党改了一堆代码之后发现新方案不行想回退手边没有版本管理就只能重写。用GitHub或Gitee建一个私有仓库每次调通一个功能提交一次哪怕只有你一个人用这个习惯都能让你少走很多弯路。我个人在整个项目里还有一个坚持了很久的习惯每一版硬件打样回来第一件事不是上电而是对照原理图逐个引脚用万用表过一遍电源网络。这个习惯帮我避免过至少三次上电冒烟的惨剧。做电子秤也好做任何STM32项目也好先保命再谈功能这是所有嵌入式从业者用学费换来的共识。