当前位置: 首页 > news >正文

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

1. 项目概述与核心需求拆解

“蓝桥杯嵌入式第九届国赛程序——电子秤”,这个标题一出来,相信很多参加过蓝桥杯或者正在备赛的嵌入式开发者都会会心一笑。这不仅仅是一个简单的课程设计,它浓缩了当时国赛对选手在实时操作系统应用、高精度数据采集、复杂人机交互以及稳定系统架构设计上的综合考察。我当年作为参赛者和后来的指导者,反复研究过这个赛题,它就像一块试金石,能清晰检验出一个嵌入式开发者是否具备了从“会写代码”到“能做好项目”的跨越能力。

简单来说,这个项目要求我们基于指定的竞赛平台(通常是STM32G431或类似系列微控制器),设计并实现一个具备称重、计价、存储、查询等功能的电子秤系统。它远不止是接个压力传感器读个数那么简单。核心需求隐藏在几个关键词背后:“国赛级别”意味着对功能的完整性、运行的稳定性以及代码的优雅性有极高要求;“电子秤”则点明了其工业控制类应用的属性,对测量的准确性、实时性和人机交互的友好性提出了挑战。这个项目非常适合有一定STM32和RTOS基础,想挑战综合项目实战、深入理解嵌入式系统设计思想的开发者。通过复现它,你不仅能巩固传感器、显示屏、按键、EEPROM等外设的驱动能力,更能学会如何在一个多任务环境中,让它们协同、稳定、高效地工作。

2. 系统整体设计与RTOS任务规划

面对这样一个功能复杂的系统,采用裸机前后台编程模式会很快陷入状态机混乱、响应迟缓的泥潭。因此,使用实时操作系统进行任务划分是必然选择。回顾第九届国赛的典型要求,系统通常需要实现称重测量、价格计算、商品信息管理、交易记录存储与查询、用户界面交互等功能。我的设计思路是,根据功能的内聚性和实时性要求,将系统分解为以下几个独立的任务,并为它们分配合理的优先级。

2.1 核心任务分解与优先级设定

一个稳健的任务结构是系统流畅运行的基石。以下是经过实践验证的任务划分方案:

  1. 传感器数据采集任务:这是系统的“感官”,优先级最高。它需要以固定的频率(例如100Hz)读取压力传感器(如HX711)的数据,并进行初步的滤波(如滑动平均或卡尔曼滤波)。其输出是经过处理的、稳定的重量原始值。高优先级确保了数据采集的及时性,为后续计算提供可靠输入。

  2. 业务逻辑处理任务:这是系统的“大脑”,优先级次高。它接收采集任务送来的重量数据,结合当前选中的商品单价,进行实时计价计算。同时,它管理着商品列表(商品编号、名称、单价)和交易流水(时间、商品、重量、金额)的数据结构。当用户确认交易时,它负责生成一条完整的交易记录。这个任务处理了核心的业务状态转移。

  3. 人机交互任务:这是系统的“脸面”,优先级中等。它负责管理LCD显示屏的刷新和矩阵键盘的扫描。显示内容需要根据系统状态(如待机、称重、设置、查询)动态更新。键盘扫描需要及时响应用户的按键输入,并将其转化为系统事件(如“按键1按下”、“确认键按下”)发送给业务逻辑任务。为了不阻塞其他任务,显示刷新可以采用条件刷新的策略,只有数据变化时才更新特定区域。

  4. 数据存储任务:这是系统的“记忆”,优先级较低。它负责将业务逻辑任务生成的交易记录,异步地写入到非易失存储器中。国赛平台通常提供EEPROM模拟区域。由于EEPROM写入速度慢、寿命有限,此任务必须谨慎设计。我通常采用一个队列(Queue)来接收待存储的记录,任务平时处于阻塞状态。当队列中有数据时,被唤醒,执行写入操作。写入时,需要处理好磨损均衡和掉电保护的问题。

  5. 系统监控与看门狗任务:这是系统的“保镖”,优先级可配置为最低但需定期执行。它负责监控其他任务的生命信号(通过任务通知或信号量),并定时“喂”独立看门狗。一旦发现某个任务长时间无响应,可以执行系统复位或错误处理流程,确保系统能从异常中恢复。

注意:优先级设置需要避免“优先级反转”。例如,数据存储任务在写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 i=0; i<FILTER_LEN; i++) { sum += weight_buffer[i]; } return (int32_t)(sum / FILTER_LEN); }
  • 一阶滞后滤波(低通滤波)Y(n) = α * X(n) + (1-α) * Y(n-1)。其中α是滤波系数(0<α<1),X是新值,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值转换为实际重量(克)的关键步骤。

  1. 零点标定:秤盘空载时,读取一个稳定的滤波后原始值,记为AD_ZERO
  2. 砝码标定:放上一个已知重量的标准砝码(如500g),读取稳定的原始值,记为AD_WEIGHT
  3. 计算系数:系数K = (AD_WEIGHT - AD_ZERO) / 砝码实际重量(500.0)。这个K值就是每克对应的AD值变化量。
  4. 实时换算实际重量(克) = (当前AD值 - AD_ZERO) / K

实操心得:标定过程最好做一个专门的“标定模式”在系统里,将计算出的AD_ZEROK值保存到EEPROM。上电时读取。标定时,务必确保秤台稳定、无风、传感器预热几分钟,这样得到的参数才准确。另外,K值建议用float类型存储和计算,以保证精度。

3.2 基于FreeRTOS的稳定业务逻辑实现

业务逻辑任务是一个状态机,它根据当前重量、用户输入和内部状态来决定系统的行为。

状态定义:典型的系统状态包括:SYS_IDLE(待机,显示零点)、SYS_WEIGHING(称重中,动态显示重量和金额)、SYS_SETTING(设置商品单价)、SYS_QUERY(查询历史记录)。

事件处理:任务主体在一个无限循环中,通常使用xQueueReceiveulTaskNotifyTake等待事件到来。事件可能来自按键(切换商品、确认、查询、设置)或来自重量数据的显著变化(例如重量从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万次),且写入速度慢。粗暴地频繁写入会很快损坏存储单元。

交易记录存储设计

  1. 结构体定义:一条记录包含时间戳、商品ID、重量、总价等。
  2. 存储区规划:在EEPROM中划出一块连续区域作为“循环队列”存储区。定义一个写指针,指向下一个可写的位置。再定义一个记录总数
  3. 写入流程
    • 业务逻辑任务生成一条完整的交易记录。
    • 将该记录结构体的指针发送到存储任务的队列。
    • 存储任务被唤醒,将记录按写指针位置写入EEPROM。
    • 写入成功后,写指针记录总数更新,并将这两个关键索引值本身也写入EEPROM中一个固定的头部区域。
    • 关键技巧:头部索引的写入应该放在最后,并且使用一个“双备份”或“标志位”机制。例如,先写索引A区,再写索引B区。上电初始化时,读取A区和B区,通过校验和或序列号判断哪个是最后成功写入的。这可以防止在写入索引过程中掉电导致索引损坏、全部记录丢失的灾难性后果。
  4. 读取流程:上电时,从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)。

  1. IDLE:默认状态。扫描到按键按下,进入PRESS_DETECT,记录时间戳。
  2. PRESS_DETECT:等待约10-20ms(消抖时间)。再次扫描,如果按键仍按下,则确认为有效按下,进入PRESS_CONFIRMED状态,并产生一个“按键按下”事件发送给业务逻辑任务。如果发现按键已释放,则回到IDLE(认为是抖动)。
  3. PRESS_CONFIRMED:等待按键释放。
  4. RELEASE_DETECT:检测到按键释放,等待10-20ms消抖。确认释放后,回到IDLE状态,并可选择是否产生一个“按键释放”事件。

这种方法彻底消除了抖动带来的多次触发问题,并且可以轻松支持长按检测(在PRESS_CONFIRMED状态计时,超过某个阈值则产生长按事件)。

5. 系统调试、测试与性能优化实录

程序写完了,怎么确保它稳定可靠?以下是我踩过坑后总结的流程。

5.1 分模块集成测试与系统联调

不要一次性写完所有代码再调试。应遵循“驱动层->任务层->业务层”自底向上的集成顺序。

  1. 传感器驱动测试:先写一个简单的裸机程序,确保能正确读取HX711数据,并验证滤波和标定算法在PC端模拟或简单环境下是否工作。
  2. 单个任务测试:创建人机交互任务,测试LCD显示和键盘扫描是否正常,事件能否正确产生。创建数据采集任务,测试能否以固定频率发布重量数据到队列。
  3. 双任务联调:让业务逻辑任务运行,订阅重量队列和按键事件。测试放上物品能否显示重量,按键能否切换状态。
  4. 加入存储任务:测试交易确认后,记录能否被正确加入队列,并最终写入EEPROM。这里要重点测试边界情况:队列满时怎么办?EEPROM写入失败怎么办?我建议在存储任务中加入重试机制和错误日志(可以输出到串口)。
  5. 压力与稳定性测试
    • 快速连续按键:测试事件队列是否溢出,系统是否死锁。
    • 频繁放取重物:测试重量更新是否流畅,业务逻辑能否跟上。
    • 长时间运行:让系统连续运行数小时,观察是否有内存泄漏(通过FreeRTOS的内存统计功能查看),任务栈是否溢出(利用FreeRTOS的栈溢出检测钩子函数)。
    • 模拟异常:在调试时,手动“拔出”传感器(模拟断线),看系统是否会进入一个合理的错误状态(如显示“传感器错误”),而不是死机。

5.2 常见问题排查速查表

下表列出了开发过程中最可能遇到的典型问题及排查思路:

问题现象可能原因排查思路与解决方案
重量显示数值乱跳或为01. HX711时序错误
2. 电源或传感器连接不稳
3. 滤波算法参数不当或未生效
1. 用逻辑分析仪或示波器检查SCK和DOUT引脚时序,对照HX711手册。
2. 检查VCC、GND,确保传感器供电稳定。测量传感器桥压是否正常。
3. 输出原始AD值观察,调整滤波算法参数。确认滤波函数被正确调用。
按键反应迟钝或失灵1. 键盘扫描任务优先级过低或阻塞
2. 去抖算法有bug
3. 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的芯片上,应考虑使用定点数运算库来提升速度。
http://www.cnnetsun.cn/news/4284899.html

相关文章:

  • Deep-Live-Cam 实时换脸工具:3 次点击换掉摄像头里的脸,免费开源完整教程
  • 猿辅导2023校招技术岗笔试(二)全解析:题型、算法与备考策略
  • 专利撰写Skill:用AI Agent将论文idea自动转化为专利交底书
  • DeepSeek 7B 微调把 RTX 4060 撑爆,我在深度学习入门里翻出这 4 个显存优化才跑通
  • DriveTeach-VLA:图像轨迹如何破解自动驾驶预训练难题
  • 设计模式实战:用观察者、策略、命令模式构建可扩展JavaScript计数器
  • 免费aigc检测查重能用于学校提交吗?AI降重结果不能代替正式报告
  • 实时视频问诊中的医疗AI:多模态引擎与工程落地
  • Netdata Windows 监控指南:三步把 Windows 服务器接入实时监控
  • Netdata Windows监控怎么装?5分钟跑通第一张监控图
  • AI写论文哪个软件最好?毕夏AI用“全链路思维”给了一个不一样的答案
  • MinerU 版本升级指南:从 1.x 到 2.7 的完整迁移路径
  • 垂直AI落地陷阱:为什么说大模型在“掷骰子”,以及如何工程化应对
  • C#文件操作全解析:从基础API到高级性能优化实战
  • 瑞萨RZ/G3E 64位MPU:高性能HMI与边缘AI加速的设计解析
  • 层次分析法实战:从原理到Excel/Python实现,解决复杂决策难题
  • 嵌入式多点触控实战:从硬件选型到UI手势系统落地
  • 粒子群算法原理与实战:从优化概念到数学建模应用
  • MATLAB三维绘图从入门到精通:mesh、surf、plot3核心函数详解
  • 网易人机交互算法实习生笔试复盘与备考指南
  • 时间序列分析:AR、MA与ARMA模型原理与实战建模指南
  • RTK卸载指南:3步彻底移除Hook、RTK.md和二进制,不留后患
  • 电竞数据分析实战指南:用公开数据集搭出完整分析链路
  • 触宝科技校招研发笔试题全解析:算法、数据结构与系统设计实战
  • LocalSend 完整使用指南:无网络环境下跨设备传文件的简单教程
  • MySQL核心机制深度解析:B+树索引、事务隔离与SQL优化实战
  • 滴滴算法岗笔试全解析:考点拆解、实战复盘与避坑指南
  • Fira Code 连字编程字体完全指南:从安装、配置到自定义的完整流程
  • Netdata Windows监控实战指南:从单机部署到跨平台统一监控的全解析
  • C++模板编程:从泛型基础到现代概念与工程实践