行业资讯
📅 2026/8/29 19:00:43
基于STM32与HX711的电子秤项目实战:从传感器驱动到系统设计
1. 项目背景与核心挑战最近在整理过往的竞赛项目资料翻到了当年参加第九届蓝桥杯嵌入式国赛时做的“电子秤”程序。这个项目可以说是嵌入式竞赛中的一个经典考题它完美地将单片机外设应用、传感器数据处理、人机交互和实时控制等多个核心知识点融合在了一起。对于很多刚接触嵌入式特别是STM32平台的同学来说这个项目就像一道“综合大餐”能让你把之前零散学到的GPIO、ADC、定时器、LCD显示、按键扫描等知识真正串联起来解决一个实际问题。这个电子秤项目的核心就是利用压力传感器通常是应变片式或HX711模块采集微弱的模拟电压信号经过放大和AD转换后得到代表重量的数字量再通过标定算法将其转换为实际的重量值如克或千克并实时显示在LCD屏幕上。听起来流程很清晰对吧但真正做起来从硬件连接到软件滤波从标定算法到抗干扰处理每一步都可能藏着“坑”。我当时也是调试了整整一天才让秤的读数稳定下来。今天我就把这个项目的完整实现思路、关键代码解析以及那些容易踩坑的细节系统地梳理一遍希望能给正在备赛蓝桥杯或者想通过一个完整项目来巩固STM32技能的朋友们一些实实在在的参考。2. 硬件系统架构与核心器件选型要完成一个电子秤首先得搞清楚我们需要哪些硬件以及它们之间是如何协同工作的。蓝桥杯嵌入式竞赛通常使用指定的CT117E开发板基于STM32F103系列但核心思路是通用的。2.1 核心控制器与开发环境主控芯片是STM32F103RBT6这是一款基于ARM Cortex-M3内核的微控制器拥有128KB Flash和20KB RAM性能对于电子秤应用绰绰有余。开发环境我使用的是Keil MDK-ARM这也是竞赛推荐的环境。这里有个小经验务必在项目开始时就根据官方提供的固件库或HAL库模板建立工程而不是从头新建。因为模板里已经配置好了系统时钟、调试接口等基础设置能避免很多底层驱动的麻烦。我当时为了“练手”自己从头配时钟树结果在ADC采样时序上卡了半天得不偿失。2.2 称重传感器与信号调理电路这是电子秤的“心脏”。竞赛中常用的方案有两种模拟传感器仪表放大器使用电阻应变片式传感器其输出是毫伏级的差分信号。需要外接一个仪表放大器如INA125进行放大再将放大后的单端信号送入MCU的ADC引脚。集成模块方案如HX711这是一个专为电子秤设计的24位A/D转换器芯片。它内部集成了可编程增益放大器PGA和稳压电路直接与传感器桥式电路连接通过简单的串行接口如GPIO模拟与MCU通信。在竞赛的有限时间和资源下我强烈推荐使用HX711模块。理由很简单它省去了复杂的外围放大电路设计抗干扰能力强24位的高分辨率也能轻松满足竞赛精度要求让你能把精力集中在软件逻辑上。我当时的方案就是HX711。它与STM32的连接仅需4根线VCC、GND、DT数据线、SCK时钟线。DT和SCK连接到STM32的任意两个GPIO口即可用软件模拟时序进行通信。2.3 人机交互界面LCD显示开发板板载LCD是必须使用的。我们需要显示当前重量、单价、总价等信息。关键在于设计一个清晰、稳定的显示界面。切忌在中断服务函数或高频循环中直接刷新整个屏幕这会导致屏幕闪烁甚至死机。正确的做法是只在重量值发生“有效变化”比如变化超过一个阈值时去更新显示区域。矩阵键盘用于输入单价、切换模式如去皮、校准、清零等。这里的关键是可靠的按键扫描与消抖程序。我采用定时器中断每10ms扫描一次键盘配合状态机算法可以非常稳定地识别短按、长按等事件避免误触发。2.4 系统供电与基准电压这是一个容易被忽略但至关重要的点。电子秤的精度极度依赖ADC的参考电压的稳定性。STM32F103的ADC可以使用VDDA通常与VCC相连作为参考电压。如果开发板是USB供电电源纹波可能较大会直接影响ADC读数跳动。如果条件允许最好使用稳定的线性稳压电源供电。在软件上我们可以通过软件滤波和算法来一定程度上抑制电源噪声但硬件的稳定是基础。3. HX711模块驱动与重量数据采集选定了HX711第一步就是写出它的驱动程序。HX711的通信协议很简单但时序要求严格。3.1 通信时序模拟HX711采用类似SPI的协议但需要MCU主动产生时钟。基本读取流程如下当DT线为高电平时表示HX711未准备好数据。MCU将SCK置低并持续检测DT线。一旦DT线变低表示数据已准备好。MCU随后产生25个脉冲时钟SCK从低到高再到低为一个脉冲。前24个脉冲时钟HX711会在每个SCK上升沿从高位到低位依次输出24位数据。第25个脉冲用于选择下一次转换的通道和增益。我用GPIO模拟的代码核心逻辑如下以标准库为例// 假设 HX711_DT_Pin 连接在 GPIOA Pin0, HX711_SCK_Pin 连接在 GPIOA Pin1 #define HX711_DT_READ() GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) #define HX711_SCK_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_1) #define HX711_SCK_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_1) // 读取一次HX711的原始AD值 int32_t HX711_Read(void) { uint32_t count 0; uint8_t i; // 等待HX711准备好 (DT变为低电平) while (HX711_DT_READ() 1); // 读取24位数据 for (i 0; i 24; i) { HX711_SCK_HIGH(); // 产生上升沿数据位输出 delay_us(1); // 短暂延时确保稳定 count 1; // 左移为下一位腾出空间 HX711_SCK_LOW(); delay_us(1); if (HX711_DT_READ() 1) { count; } } // 第25个脉冲设置通道和增益为128通常用于通道A HX711_SCK_HIGH(); delay_us(1); HX711_SCK_LOW(); // 处理符号位24位数据为补码形式 if (count 0x800000) { // 检查最高位第24位是否为1 count | 0xFF000000; // 如果是则将其扩展为32位有符号数的负数形式 } return (int32_t)count; }注意这里的delay_us(1)非常关键。HX711对SCK高电平脉冲的宽度有最小要求典型值0.1us以上虽然STM32速度很快但为了兼容性加上一个微秒级的延时是稳妥的做法。可以用SysTick定时器实现一个精准的微秒延时函数。3.2 数据滤波处理直接读取的HX711原始值噪声很大会不停跳动。必须进行软件滤波。我尝试了多种方法算术平均滤波连续采样N次取平均值。简单但实时性差且对于偶然的脉冲干扰抑制能力弱。中值滤波采样N次排序后取中间值。对脉冲干扰效果好但计算量稍大。一阶滞后滤波低通滤波filtered_val α * raw_val (1-α) * last_filtered_val。这种方法非常适用于电子秤这种变化相对缓慢的信号能很好地平滑数据且计算量小。我最终采用的是一种“复合滤波”策略效果很好硬件就绪后延时在HX711上电或复位后等待至少200ms再开始读数让传感器和芯片内部稳定。连续采样与剔除快速连续采样10次去掉一个最大值和一个最小值消除偶然的毛刺。均值滤波对剩余的8个值求平均作为本次的“原始采样值”。一阶滞后滤波将上述平均值再经过一阶滞后滤波处理得到最终用于计算的稳定值。α系数我设置为0.2在响应速度和稳定性之间取得了不错的平衡。// 一阶滞后滤波函数 float FirstOrderFilter(float new_value, float old_value, float alpha) { return alpha * new_value (1 - alpha) * old_value; } // 在任务循环中 raw_avg GetFilteredRawValue(); // 经过前述步骤1-3得到的值 stable_ad_value FirstOrderFilter(raw_avg, stable_ad_value, 0.2);4. 重量标定算法与校准流程得到稳定的AD值后如何把它变成以“克”为单位的重量呢这就需要标定。这是电子秤程序中最核心的算法部分。4.1 标定原理与公式我们假设传感器在理想情况下是线性的即 AD值AD与 重量Weight满足一次函数关系Weight K * AD B其中K是比例系数斜率B是零点偏移截距。标定的目的就是求出K和B这两个参数。我们需要两个已知点零点秤盘上无任何物品时的AD值记为AD0对应的重量W0 0g。砝码点在秤盘上放置一个已知准确重量的砝码如500g标准砝码记录此时的AD值记为AD1对应的重量W1 500g。将这两组数据代入公式0 K * AD0 B B -K * AD0 500 K * AD1 B联立方程即可解出K 500 / (AD1 - AD0) B -K * AD0在实际程序中我们通常先计算K再计算B。4.2 软件标定功能实现我们需要在程序中设计一个“校准模式”。通常通过按键组合如长按某个键进入。进入零点校准LCD提示“请清空秤盘按OK键”。用户移走所有物品后按键程序连续采样一段时间计算平均值得出AD0并存入Flash如STM32内部的EEPROM模拟区域。进入砝码校准LCD提示“请放500g砝码按OK键”。用户放置标准砝码后按键程序同样采样计算得出AD1并存入Flash。计算并保存参数根据AD0和AD1实时计算出K和B也存入Flash。以后每次上电都从Flash中读取这些参数使用。这里有一个巨大的坑Flash的写入寿命和写入时机。STM32的Flash扇区擦写次数典型值是1万次。如果你每次校准都直接写很快芯片就会报废。正确的做法是在Flash中开辟一个专门的结构体存储区。只有校准数据确实发生变化时才执行擦写操作。可以先在RAM中修改数据用户确认校准完成后再一次性写入Flash。更进阶的做法是使用磨损均衡算法但竞赛项目中只要注意不要在校准循环里疯狂写Flash就行。4.3 去皮与清零功能清零Zero就是重新获取当前的AD值作为新的零点AD0并立即更新重量计算。用于消除秤盘本身的轻微重量或环境漂移。实现简单直接采样赋值即可。去皮Tare这是商业电子秤的必备功能。当放上容器时按“去皮”键程序会记录当前重量作为“皮重”TareWeight。之后显示和计算的重量都是净重 当前计算总重 - 皮重。容器拿走皮重依然有效再次按“去皮”键则清除皮重。实现时需要在重量计算流程的最后一步减去皮重并确保去皮状态在LCD上有清晰图标提示。5. 系统任务设计与软件架构一个稳定的电子秤程序不能把所有代码都堆在main函数的while(1)里。合理的任务划分能让程序逻辑清晰且易于维护和调试。5.1 基于时间片轮询的简单调度对于蓝桥杯这种资源有限、功能确定的项目采用一个简单的“时间片轮询”架构就足够了。我们利用一个1ms的定时器中断作为系统心跳。// 在1ms定时器中断中 void TIMx_IRQHandler(void) { if (TIM_GetITStatus(TIMx, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIMx, TIM_IT_Update); // 定义任务计数器 static uint16_t TASK1_CNT 0, TASK2_CNT 0, TASK3_CNT 0; // 任务1按键扫描与处理每10ms执行一次 if (TASK1_CNT 10) { TASK1_CNT 0; Key_Scan_Task_Flag 1; // 设置标志位 } // 任务2HX711数据采集与滤波每20ms执行一次 if (TASK2_CNT 20) { TASK2_CNT 0; HX711_Sample_Task_Flag 1; } // 任务3重量计算与显示更新每100ms执行一次 if (TASK3_CNT 100) { TASK3_CNT 0; Weight_Update_Task_Flag 1; } } }然后在主循环中轮询检查这些标志位并执行相应的任务函数。int main(void) { // 硬件初始化时钟、GPIO、定时器、LCD、Flash... System_Init(); // 从Flash加载标定参数 K, B Load_Calibration_Params(); while (1) { if (Key_Scan_Task_Flag) { Key_Scan_Task_Flag 0; Key_Scan_Task(); // 扫描键盘更新键值状态机 } if (HX711_Sample_Task_Flag) { HX711_Sample_Task_Flag 0; // 采集、滤波得到稳定的 raw_ad_value HX711_Sample_Task(); } if (Weight_Update_Task_Flag) { Weight_Update_Task_Flag 0; // 使用 K, B 计算重量处理去皮更新显示 Weight_Update_Task(); } // 其他低优先级任务如LED指示等 // ... } }这种架构保证了关键任务如按键响应、数据采集的定时执行避免了因为某个函数执行过久而阻塞其他功能。5.2 重量计算与显示任务详解在Weight_Update_Task()函数中包含了核心的业务逻辑void Weight_Update_Task(void) { float raw_weight; float display_weight; // 1. 计算原始重量 raw_weight g_calib_K * g_stable_ad_value g_calib_B; // 2. 处理去皮 if (g_tare_enabled) { display_weight raw_weight - g_tare_weight; if (display_weight 0) display_weight 0; // 去皮后重量不为负 } else { display_weight raw_weight; } // 3. 单位转换与格式化 (例如计算单位为克显示保留1位小数) // 4. 判断重量是否稳定例如连续几次变化小于某个阈值 if (IsWeightStable(display_weight)) { g_stable_weight display_weight; // 5. 只有重量稳定或发生变化时才更新LCD对应区域避免闪烁 if (fabs(g_stable_weight - g_last_display_weight) DISPLAY_THRESHOLD) { Update_Weight_On_LCD(g_stable_weight); g_last_display_weight g_stable_weight; } } // 6. 更新单价、总价等其他信息显示 // ... }5.3 按键处理与状态机电子秤需要响应多种按键操作数字键输入单价、去皮、清零、校准等。我使用了一个状态机来管理typedef enum { MODE_NORMAL, // 正常称重模式 MODE_PRICE_INPUT, // 单价输入模式 MODE_CALIB_ZERO, // 零点校准模式 MODE_CALIB_WEIGHT // 砝码校准模式 } System_Mode_t; System_Mode_t g_sys_mode MODE_NORMAL; void Key_Process(uint8_t key_value) { switch (g_sys_mode) { case MODE_NORMAL: if (key_value KEY_TARE) { // 执行去皮逻辑 Do_Tare(); } else if (key_value KEY_ZERO) { // 执行清零逻辑 Do_Zero(); } else if (key_value KEY_CALIB) { // 长按进入校准模式 g_sys_mode MODE_CALIB_ZERO; LCD_Show_Calib_Prompt(); } else if (key_value KEY_0 key_value KEY_9) { // 切换到单价输入模式并记录第一个数字 g_sys_mode MODE_PRICE_INPUT; Start_Price_Input(key_value); } break; case MODE_PRICE_INPUT: // 处理数字输入、退格、确认、取消等 Process_Price_Input(key_value); if (输入确认或取消) { g_sys_mode MODE_NORMAL; } break; case MODE_CALIB_ZERO: if (key_value KEY_OK) { // 保存零点AD值 Save_Zero_Point(); g_sys_mode MODE_CALIB_WEIGHT; LCD_Show_Calib_Weight_Prompt(); } else if (key_value KEY_CANCEL) { g_sys_mode MODE_NORMAL; } break; case MODE_CALIB_WEIGHT: if (key_value KEY_OK) { // 保存砝码点AD值计算并保存K,B Save_Weight_Point(); Calculate_Params(); g_sys_mode MODE_NORMAL; LCD_Show_Normal(); } else if (key_value KEY_CANCEL) { g_sys_mode MODE_NORMAL; } break; } }状态机让复杂的交互逻辑变得条理清晰每个状态下只处理有限的按键事件代码可读性和可维护性大大增强。6. 精度优化与抗干扰实践做到基本功能后就要追求稳定性和精度了。以下是几个实战中总结的要点6.1 降低ADC噪声与软件滤波进阶除了前面提到的一阶滤波还可以电源去耦在HX711和STM32的电源引脚附近紧挨着芯片放置一个0.1uF和一个10uF的电容这对滤除高频噪声至关重要。数字隔离如果条件允许在HX711的DT和SCK线与STM32之间串联一个100欧姆左右的电阻可以削弱信号线上的振铃和过冲。滑动平均滤波维护一个固定长度的队列存储最近N次滤波后的值求平均作为输出。这对缓慢漂移的抑制效果更好但会引入一定的延迟。6.2 温度漂移补偿应变片传感器对温度比较敏感。如果竞赛环境温度变化大可能会发现零点或斜率随时间缓慢变化。一个简单的补偿思路是在MCU上找一个ADC通道连接一个热敏电阻测量环境温度。在不同温度下如10°C, 25°C, 40°C进行标定得到多组K(T), B(T)。在实际使用时根据当前温度T_now通过插值法如线性插值计算出当前的K和B用于计算。这能显著提升电子秤在全温度范围内的精度。当然这对竞赛来说属于“加分项”基础功能稳定是首要目标。6.3 显示刷新优化与用户体验局部刷新LCD驱动函数通常提供画点、画线、显示字符的函数。更新重量时只重写数字所在的区域而不是清屏重绘整个界面可以极大减少闪烁。稳定指示当重量值在短时间内变化小于某个阈值时可以在LCD上显示一个“稳定”图标如一个小锁给用户明确的反馈。负重量处理在软件中要限制最终显示重量不小于零。如果因为噪声或去皮操作导致计算值为负应强制显示为0.0。7. 调试技巧与常见问题排查开发过程中肯定会遇到各种问题。分享几个我当时的调试方法问题读数跳动非常大完全无法稳定。排查首先用万用表测量HX711的VCC和GND之间电压是否稳定。然后将DT和SCK线断开在代码里将AD值固定为一个常数观察显示是否还跳动。如果不跳问题在HX711或传感器之前如果还跳问题在MCU或显示部分。可能原因与解决电源噪声更换为电池或线性电源供电。传感器未固定好确保传感器受力均匀且牢固安装。时序问题检查SCK脉冲的延时delay_us(1)是否足够。可以用逻辑分析仪抓取DT和SCK的波形看是否符合HX711数据手册的时序图。滤波参数不当调整一阶滤波的α系数或增加采样平均的次数。问题校准后放上砝码显示重量不准且非线性。排查进行多点校准。记录空载、100g、200g、500g等多个点的AD值在坐标纸上画出来看是否成一条直线。可能原因与解决传感器量程过小或过载确保测试重量在传感器额定量程内。机械结构问题秤盘是否与传感器接触点摩擦结构是否对称这些机械因素会导致严重的非线性。标定过程操作不当校准时秤盘和砝码必须静止等待读数稳定后再按键确认。问题按键反应迟钝或连击。排查在按键扫描函数中通过串口打印出每次检测到的键值观察其变化规律。解决确保使用了状态机消抖。我最常用的是“四状态法”IDLE-PRESS_DEBOUNCE-PRESSED-RELEASE_DEBOUNCE配合10ms的定时扫描可以完美识别短按、长按、连按且绝无抖动。问题进入校准模式后参数无法保存。排查单步调试检查写Flash前擦除操作是否成功检查Flash控制状态寄存器以及写入的数据地址是否在正确的扇区内。解决仔细阅读STM32参考手册中关于Flash编程的章节。特别注意写Flash前必须先解锁写操作必须按半字16位或字32位进行并且地址必须对齐。写入后最好立刻读回验证。回过头看这个电子秤项目虽然不大但“麻雀虽小五脏俱全”。它强迫你去思考一个完整嵌入式产品的方方面面从传感器信号拾取、数据处理算法、人机交互逻辑到系统任务调度、非易失存储甚至机械结构的影响。把这样一个项目从头到尾扎扎实实地做一遍、调一遍远比孤立地学习十个外设实验收获要大得多。在调试过程中那种从读数乱跳到最终稳定显示的成就感是理论学习无法替代的。如果你正在准备类似的竞赛或项目我的建议是不要只满足于“跑通”多问几个“为什么”为什么用这种滤波这个参数怎么来的还有没有更好的方法这种深度的思考和实践才是嵌入式开发能力提升的关键。