尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于FreeRTOS的STM32电子秤系统设计:从传感器采集到数据存储的实战解析

基于FreeRTOS的STM32电子秤系统设计:从传感器采集到数据存储的实战解析 1. 项目概述与核心需求拆解“蓝桥杯嵌入式第九届国赛程序——电子秤”这个标题一出来相信很多参加过蓝桥杯或者正在备赛的嵌入式开发者都会会心一笑。这不仅仅是一个简单的课程设计它浓缩了当时国赛对选手在实时操作系统应用、高精度数据采集、复杂人机交互以及稳定系统架构设计上的综合考察。我当年作为参赛者和后来的指导者反复研究过这个赛题它就像一块试金石能清晰检验出一个嵌入式开发者是否具备了从“会写代码”到“能做好项目”的跨越能力。简单来说这个项目要求我们基于指定的竞赛平台通常是STM32G431或类似系列微控制器设计并实现一个具备称重、计价、存储、查询等功能的电子秤系统。它远不止是接个压力传感器读个数那么简单。核心需求隐藏在几个关键词背后“国赛级别”意味着对功能的完整性、运行的稳定性以及代码的优雅性有极高要求“电子秤”则点明了其工业控制类应用的属性对测量的准确性、实时性和人机交互的友好性提出了挑战。这个项目非常适合有一定STM32和RTOS基础想挑战综合项目实战、深入理解嵌入式系统设计思想的开发者。通过复现它你不仅能巩固传感器、显示屏、按键、EEPROM等外设的驱动能力更能学会如何在一个多任务环境中让它们协同、稳定、高效地工作。2. 系统整体设计与RTOS任务规划面对这样一个功能复杂的系统采用裸机前后台编程模式会很快陷入状态机混乱、响应迟缓的泥潭。因此使用实时操作系统进行任务划分是必然选择。回顾第九届国赛的典型要求系统通常需要实现称重测量、价格计算、商品信息管理、交易记录存储与查询、用户界面交互等功能。我的设计思路是根据功能的内聚性和实时性要求将系统分解为以下几个独立的任务并为它们分配合理的优先级。2.1 核心任务分解与优先级设定一个稳健的任务结构是系统流畅运行的基石。以下是经过实践验证的任务划分方案传感器数据采集任务这是系统的“感官”优先级最高。它需要以固定的频率例如100Hz读取压力传感器如HX711的数据并进行初步的滤波如滑动平均或卡尔曼滤波。其输出是经过处理的、稳定的重量原始值。高优先级确保了数据采集的及时性为后续计算提供可靠输入。业务逻辑处理任务这是系统的“大脑”优先级次高。它接收采集任务送来的重量数据结合当前选中的商品单价进行实时计价计算。同时它管理着商品列表商品编号、名称、单价和交易流水时间、商品、重量、金额的数据结构。当用户确认交易时它负责生成一条完整的交易记录。这个任务处理了核心的业务状态转移。人机交互任务这是系统的“脸面”优先级中等。它负责管理LCD显示屏的刷新和矩阵键盘的扫描。显示内容需要根据系统状态如待机、称重、设置、查询动态更新。键盘扫描需要及时响应用户的按键输入并将其转化为系统事件如“按键1按下”、“确认键按下”发送给业务逻辑任务。为了不阻塞其他任务显示刷新可以采用条件刷新的策略只有数据变化时才更新特定区域。数据存储任务这是系统的“记忆”优先级较低。它负责将业务逻辑任务生成的交易记录异步地写入到非易失存储器中。国赛平台通常提供EEPROM模拟区域。由于EEPROM写入速度慢、寿命有限此任务必须谨慎设计。我通常采用一个队列Queue来接收待存储的记录任务平时处于阻塞状态。当队列中有数据时被唤醒执行写入操作。写入时需要处理好磨损均衡和掉电保护的问题。系统监控与看门狗任务这是系统的“保镖”优先级可配置为最低但需定期执行。它负责监控其他任务的生命信号通过任务通知或信号量并定时“喂”独立看门狗。一旦发现某个任务长时间无响应可以执行系统复位或错误处理流程确保系统能从异常中恢复。注意优先级设置需要避免“优先级反转”。例如数据存储任务在写EEPROM时如果关闭了中断或占用了过长时间可能会阻塞更高优先级的任务。解决方案是使用RTOS提供的信号量或事件组进行同步并将耗时的操作分段执行。2.2 任务间通信机制选型任务划分好了如何让它们高效、安全地对话FreeRTOS提供了多种通信机制我的选型原则是简单场景用通知单向数据用队列同步互斥用信号量。重量数据传递从采集任务到业务逻辑任务是单向、高频、连续的数据流。这里使用队列Queue是最合适的。采集任务将滤波后的重量值放入队列业务逻辑任务从队列中取出。队列的缓冲特性可以平滑两个任务执行速度的微小差异。用户事件传递从人机交互任务到业务逻辑任务是单向、低频、事件型的消息。例如“按键按下”、“确认交易”。使用任务通知Task Notification或队列均可。任务通知更轻量适合简单事件如果需要传递更复杂的事件结构体则使用小队列。交易记录传递从业务逻辑任务到数据存储任务是单向、低频、数据块的传递。由于一条交易记录包含多个字段使用队列传递结构体指针是最佳实践。业务逻辑任务在堆上分配一个记录结构体内存填充数据后将指针发送给存储任务的队列。存储任务在处理完毕后必须负责释放这块内存防止内存泄漏。资源保护商品列表、当前交易状态等全局数据结构会被多个任务访问如业务逻辑任务修改显示任务读取。必须使用互斥信号量Mutex进行保护确保数据的一致性。3. 核心模块实现与关键技术细节有了顶层设计我们深入各个模块看看具体实现中的“魔鬼细节”。3.1 高精度称重传感器数据采集与处理称重的核心是HX711模块它是一个24位A/D转换器专门用于称重传感器。驱动它不难但要做好却需要功夫。驱动与读取标准的GPIO模拟时序读取数据即可。关键在于HX711有通道A增益128用于称重传感器和通道B增益32。我们必须使用通道A。每次读取后数据是24位有符号补码需要转换为一个int32_t类型的原始值。数字滤波算法传感器的原始数据噪声很大直接显示会跳动不止。必须滤波。滑动平均滤波最简单有效。维护一个固定长度的数组每次新数据覆盖最旧的数据然后计算平均值。长度选择10-20点在平滑度和响应速度间折衷。代码实现简单但会引入一定的滞后。#define FILTER_LEN 10 static int32_t weight_buffer[FILTER_LEN] {0}; static uint8_t buffer_index 0; int32_t HX711_Get_Filtered_Value(void) { int32_t raw_val HX711_Read(); // 假设的读取函数 weight_buffer[buffer_index] raw_val; buffer_index (buffer_index 1) % FILTER_LEN; int64_t sum 0; for(int i0; iFILTER_LEN; i) { sum weight_buffer[i]; } return (int32_t)(sum / FILTER_LEN); }一阶滞后滤波低通滤波Y(n) α * X(n) (1-α) * Y(n-1)。其中α是滤波系数0α1X是新值Y是滤波输出。α越小越平滑滞后越大。这种方法计算量小适合在资源受限的MCU上运行。static int32_t filtered_weight 0; #define ALPHA 0.2f // 滤波系数可调整 int32_t HX711_Get_LPF_Value(void) { int32_t raw_val HX711_Read(); filtered_weight (int32_t)(ALPHA * raw_val (1-ALPHA) * filtered_weight); return filtered_weight; }标定与换算这是将原始AD值转换为实际重量克的关键步骤。零点标定秤盘空载时读取一个稳定的滤波后原始值记为AD_ZERO。砝码标定放上一个已知重量的标准砝码如500g读取稳定的原始值记为AD_WEIGHT。计算系数系数K (AD_WEIGHT - AD_ZERO) / 砝码实际重量(500.0)。这个K值就是每克对应的AD值变化量。实时换算实际重量(克) (当前AD值 - AD_ZERO) / K。实操心得标定过程最好做一个专门的“标定模式”在系统里将计算出的AD_ZERO和K值保存到EEPROM。上电时读取。标定时务必确保秤台稳定、无风、传感器预热几分钟这样得到的参数才准确。另外K值建议用float类型存储和计算以保证精度。3.2 基于FreeRTOS的稳定业务逻辑实现业务逻辑任务是一个状态机它根据当前重量、用户输入和内部状态来决定系统的行为。状态定义典型的系统状态包括SYS_IDLE待机显示零点、SYS_WEIGHING称重中动态显示重量和金额、SYS_SETTING设置商品单价、SYS_QUERY查询历史记录。事件处理任务主体在一个无限循环中通常使用xQueueReceive或ulTaskNotifyTake等待事件到来。事件可能来自按键切换商品、确认、查询、设置或来自重量数据的显著变化例如重量从0变为大于阈值。关键代码片段示意void BusinessLogic_Task(void *pvParameters) { SystemState_t currState SYS_IDLE; Product_t currProduct {0}; Transaction_t pendingTrans {0}; Event_t receivedEvent; // 从EEPROM加载商品列表和标定参数 Load_System_Data(); while(1) { // 等待事件超时时间可以设为一个节拍用于处理无事件时的自动逻辑如超时返回待机 if(xQueueReceive(eventQueue, receivedEvent, pdMS_TO_TICKS(100)) pdPASS) { switch(currState) { case SYS_IDLE: if(receivedEvent.type EVT_KEY receivedEvent.key KEY_SET) { Enter_Setting_Mode(); currState SYS_SETTING; } else if(receivedEvent.type EVT_WEIGHT_CHANGE receivedEvent.weight WEIGHT_THRESHOLD) { // 检测到放上物品进入称重状态 currState SYS_WEIGHING; pendingTrans.weight receivedEvent.weight; pendingTrans.product Get_Default_Product(); // 获取默认或上次商品 Update_Display_Weighing(pendingTrans); // 刷新显示 } break; case SYS_WEIGHING: if(receivedEvent.type EVT_KEY) { // 处理切换商品、确认交易等 Handle_Weighing_Key(receivedEvent.key, pendingTrans, currState); } else if(receivedEvent.type EVT_WEIGHT_CHANGE) { // 重量更新重新计算金额并刷新显示 pendingTrans.weight receivedEvent.weight; pendingTrans.total pendingTrans.weight * pendingTrans.product.price / 1000.0; // 假设单价是分/克 Update_Display_Weighing(pendingTrans); } break; // ... 处理其他状态 } } else { // 超时处理例如称重状态下长时间无操作判断为交易完成或取消 if(currState SYS_WEIGHING) { // 可以添加超时自动保存交易或返回待机的逻辑 } } // 其他周期性处理例如检查是否需要自动保存 } }3.3 掉电保护与EEPROM数据存储策略国赛平台Flash模拟的EEPROM写入次数有限通常10万次且写入速度慢。粗暴地频繁写入会很快损坏存储单元。交易记录存储设计结构体定义一条记录包含时间戳、商品ID、重量、总价等。存储区规划在EEPROM中划出一块连续区域作为“循环队列”存储区。定义一个写指针指向下一个可写的位置。再定义一个记录总数。写入流程业务逻辑任务生成一条完整的交易记录。将该记录结构体的指针发送到存储任务的队列。存储任务被唤醒将记录按写指针位置写入EEPROM。写入成功后写指针和记录总数更新并将这两个关键索引值本身也写入EEPROM中一个固定的头部区域。关键技巧头部索引的写入应该放在最后并且使用一个“双备份”或“标志位”机制。例如先写索引A区再写索引B区。上电初始化时读取A区和B区通过校验和或序列号判断哪个是最后成功写入的。这可以防止在写入索引过程中掉电导致索引损坏、全部记录丢失的灾难性后果。读取流程上电时从EEPROM头部读取有效的写指针和记录总数。查询功能时根据指针和总数逆序或顺序读取记录并显示。磨损均衡的简单实现由于是循环队列写指针会在存储区内循环移动自然实现了对整个存储区域的均匀磨损避免了固定位置反复擦写。注意事项EEPROM单次写入的数据量是有限的如1字节、2字节、4字节。写入一个结构体时需要将其拆分为多个符合对齐要求的写入操作。务必查阅你所用MCU的Flash模拟EEPROM的编程手册遵守其写入粒度和等待时间的要求。在写入期间根据RTOS和驱动设计可能需要挂起调度或关闭中断但时间必须极短。4. 人机交互界面与用户体验优化界面的友好程度直接决定了产品的“质感”。在资源受限的MCU上我们需要用代码精心雕琢。4.1 LCD显示界面分层与动态刷新国赛用的LCD通常是128x64或类似的点阵屏驱动库已提供。我们的工作是组织内容。界面分层对应系统的每个状态设计一个独立的显示函数。如Draw_Idle_Screen(),Draw_Weighing_Screen(Transaction_t* trans),Draw_Setting_Screen(Product_t* prod)等。动态刷新策略避免全屏刷新导致的闪烁。只刷新变化的部分。称重界面重量和总价是频繁变化的数字。可以单独记录它们上一次显示的值只有在本次计算出的值与上次不同时才调用局部清空和重绘函数。静态的标题、单位等元素只在进入该界面时绘制一次。菜单界面在商品列表或记录列表中滚动时采用“整页刷新”或“光标移动”策略。如果一屏显示5条翻页时全刷如果是光标在固定位置列表内移动可以只反显光标所在行。抗闪烁技巧在修改屏幕某一区域前如果该区域原有内容复杂可以先画一个背景色的矩形块覆盖它再绘制新内容这比直接绘制新内容覆盖旧内容有时更可靠能避免残影。4.2 矩阵键盘扫描与软件去抖矩阵键盘的扫描通常放在一个定时器中断或一个高优先级的任务中。这里讲更稳健的软件去抖。经典状态机去抖为每个按键定义一个状态机通常4个状态IDLE-PRESS_DETECT-PRESS_CONFIRMED-RELEASE_DETECT。IDLE默认状态。扫描到按键按下进入PRESS_DETECT记录时间戳。PRESS_DETECT等待约10-20ms消抖时间。再次扫描如果按键仍按下则确认为有效按下进入PRESS_CONFIRMED状态并产生一个“按键按下”事件发送给业务逻辑任务。如果发现按键已释放则回到IDLE认为是抖动。PRESS_CONFIRMED等待按键释放。RELEASE_DETECT检测到按键释放等待10-20ms消抖。确认释放后回到IDLE状态并可选择是否产生一个“按键释放”事件。这种方法彻底消除了抖动带来的多次触发问题并且可以轻松支持长按检测在PRESS_CONFIRMED状态计时超过某个阈值则产生长按事件。5. 系统调试、测试与性能优化实录程序写完了怎么确保它稳定可靠以下是我踩过坑后总结的流程。5.1 分模块集成测试与系统联调不要一次性写完所有代码再调试。应遵循“驱动层-任务层-业务层”自底向上的集成顺序。传感器驱动测试先写一个简单的裸机程序确保能正确读取HX711数据并验证滤波和标定算法在PC端模拟或简单环境下是否工作。单个任务测试创建人机交互任务测试LCD显示和键盘扫描是否正常事件能否正确产生。创建数据采集任务测试能否以固定频率发布重量数据到队列。双任务联调让业务逻辑任务运行订阅重量队列和按键事件。测试放上物品能否显示重量按键能否切换状态。加入存储任务测试交易确认后记录能否被正确加入队列并最终写入EEPROM。这里要重点测试边界情况队列满时怎么办EEPROM写入失败怎么办我建议在存储任务中加入重试机制和错误日志可以输出到串口。压力与稳定性测试快速连续按键测试事件队列是否溢出系统是否死锁。频繁放取重物测试重量更新是否流畅业务逻辑能否跟上。长时间运行让系统连续运行数小时观察是否有内存泄漏通过FreeRTOS的内存统计功能查看任务栈是否溢出利用FreeRTOS的栈溢出检测钩子函数。模拟异常在调试时手动“拔出”传感器模拟断线看系统是否会进入一个合理的错误状态如显示“传感器错误”而不是死机。5.2 常见问题排查速查表下表列出了开发过程中最可能遇到的典型问题及排查思路问题现象可能原因排查思路与解决方案重量显示数值乱跳或为01. HX711时序错误2. 电源或传感器连接不稳3. 滤波算法参数不当或未生效1. 用逻辑分析仪或示波器检查SCK和DOUT引脚时序对照HX711手册。2. 检查VCC、GND确保传感器供电稳定。测量传感器桥压是否正常。3. 输出原始AD值观察调整滤波算法参数。确认滤波函数被正确调用。按键反应迟钝或失灵1. 键盘扫描任务优先级过低或阻塞2. 去抖算法有bug3. GPIO配置错误如上拉未开启1. 提高键盘扫描任务的优先级确保其执行周期稳定如10ms一次。2. 简化程序去掉去抖逻辑测试原始扫描是否灵敏逐步定位问题。3. 检查硬件原理图和代码初始化确认按键引脚配置为输入上拉模式。系统运行一段时间后死机1. 任务栈溢出2. 堆内存耗尽3. 中断服务程序(ISR)处理不当4. 看门狗未及时喂食1. 启用FreeRTOS的栈溢出检测configCHECK_FOR_STACK_OVERFLOW。2. 使用xPortGetFreeHeapSize()监控堆空间检查是否有内存泄漏。3. 检查ISR中是否调用了不可重入函数或进行了过长时间操作。4. 确认看门狗任务在运行且喂狗间隔小于看门狗超时时间。EEPROM存储的数据丢失或错乱1. 写入过程中系统复位或掉电2. 磨损均衡或索引管理逻辑错误3. 写入地址越界1. 强化掉电保护机制采用“双备份索引”法。2. 仔细调试存储逻辑添加串口打印跟踪每一次写入和指针更新。3. 在写入前增加地址范围校验断言。LCD显示局部花屏或残影1. 局部刷新逻辑错误未清除旧内容2. 显存GRAM更新与屏刷新不同步3. SPI/I2C通信受干扰1. 确保在更新变数字区域前先用背景色填充该区域矩形。2. 查阅LCD驱动芯片手册确认是否需要发送“更新显示”命令。3. 检查通信线路在SCL/SDA线上增加上拉电阻缩短走线。5.3 资源优化与性能提升技巧在资源紧张的MCU上每一字节内存和每一微秒CPU时间都值得珍惜。栈空间分配不要盲目给任务分配大栈。通过测试观察每个任务运行时的最大栈使用量FreeRTOS的uxTaskGetStackHighWaterMark函数在此基础上增加20%-30%的安全余量即可。人机交互和业务逻辑任务通常需要较多栈空间。队列深度设置队列不是越大越好。重量数据队列深度设为5-10足矣事件队列深度设为3-5存储队列深度设为2-3。过深的队列会浪费内存并增加延迟。使用Tickless Idle模式如果系统在无操作时大部分时间处于空闲可以启用FreeRTOS的Tickless Idle模式。这会在空闲时停止SysTick定时器让MCU进入低功耗睡眠模式显著降低功耗。这对于电池供电的电子秤原型很有意义。避免在任务中忙等待任何需要等待的地方都应使用RTOS的阻塞机制vTaskDelay,xQueueReceive,ulTaskNotifyTake等让出CPU给其他任务提高系统整体效率。浮点数运算STM32G4有硬件FPU使用float类型进行单价、总价计算没有问题。如果是在没有FPU的芯片上应考虑使用定点数运算库来提升速度。
返回列表