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

FreeRTOS消息队列实验中的按键“失灵”谜案:一次调用引发的后果

写在前面

最近在学习FreeRTOS的消息队列,自己写了一个小实验:创建两个任务,Send_Task负责检测按键(KEY1和KEY2),一旦有按键按下,就通过消息队列发送对应的数值(1或2);Receive_Task则阻塞等待队列消息,收到后通过串口打印出来。

实验运行起来,却发现一个诡异的现象:当我把Send_Task中两个按键检测之间的vTaskDelay(50);注释掉后,KEY2就彻底“罢工”了,无论怎么按都不响应;而一旦加上这50ms的延时,一切又恢复正常。

起初我怀疑是消息队列长度不够导致发送失败,但检查代码后发现队列长度已经设为4,完全够用。经过一番排查,最终把目光锁定在了按键扫描函数Key_GetNum()的实现上。下面我详细还原这个问题从出现到解决的全过程,希望能给遇到类似问题的朋友一些启发。

一、实验环境与代码框架

  • 硬件:STM32F103 开发板
  • RTOS:FreeRTOS V10.4.1
  • 外设:两个独立按键(KEY1 – PB1,KEY2 – PB11),串口1用于打印
  • 源代码 https://github.com/wzdffffff/Freerots/tree/queue

1.1 消息队列的创建

#define QUEUE_LEN 4 #define QUEUE_SIZE 4 QueueHandle_t Test_Queue; // 在 AppTaskCreate 中创建 Test_Queue = xQueueCreate((UBaseType_t)QUEUE_LEN, (UBaseType_t)QUEUE_SIZE);

1.2 接收任务

static void Receive_Task(void* parameter) { BaseType_t xReturn; uint32_t r_queue; while (1) { xReturn = xQueueReceive(Test_Queue, &r_queue, portMAX_DELAY); if(pdTRUE == xReturn) printf("本次接收到的数据是%d\r\n", r_queue); else printf("数据接收出错,错误代码0x%lx\r\n", xReturn); } }

1.3 发送任务(问题版本!!!)

static void Send_Task(void* parameter) { BaseType_t xReturn; uint32_t send_data1 = 1; uint32_t send_data2 = 2; while (1) { if(Key_GetNum() == 1) { printf("发送消息send_data1!\r\n"); xReturn = xQueueSend(Test_Queue, &send_data1, 0); if(pdPASS == xReturn) printf("消息send_data1发送成功!\r\n"); } // vTaskDelay(50); // 注释掉这一行后 KEY2 失效 if(Key_GetNum() == 2) { printf("发送消息send_data2!\r\n"); xReturn = xQueueSend(Test_Queue, &send_data2, 0); if(pdPASS == xReturn) printf("消息send_data2发送成功!\r\n"); } vTaskDelay(20); } }

1.4 按键扫描函数(经典阻塞式消抖)

uint8_t Key_GetNum(void) { uint8_t KeyNum = 0; if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) { vTaskDelay(20); // 延时消抖 while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0); // 等待松手 vTaskDelay(20); KeyNum = 1; } if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0) { vTaskDelay(20); while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0); vTaskDelay(20); KeyNum = 2; } return KeyNum; }

二、现象复现与分析

2.1 诡异的现象

vTaskDelay(50):先按KEY1,串口打印“发送消息send_data1!成功”;再按KEY2,同样正常打印“发送消息send_data2!成功”。一切完美。

注释掉vTaskDelay(50):KEY1依然工作,但KEY2无论怎么按都毫无反应,串口没有任何关于KEY2的打印。仿佛KEY2被系统遗忘了。

2.2 第一反应:队列满了?

按照常规思维,发送失败最常见的原因就是队列已满。于是我检查了队列长度(4个元素),两个消息才占2个位置,不可能满。而且代码里也没有其他任务往队列里写数据。退一步说,即便队列真的满了,xQueueSend会返回errQUEUE_FULL,但我的代码里只判断了pdPASS并打印成功,没有处理失败情况。不过关键现象是:printf("发送消息send_data2!\r\n");都没有执行,说明程序根本没有进入if(Key_GetNum()==2)分支。因此问题不在队列,而在按键检测本身。

2.3 深入分析:Key_GetNum()的阻塞特性

观察Key_GetNum()的实现,发现它是一个“阻塞式消抖”函数:

  • 当检测到引脚为低电平时,立即调用vTaskDelay(20)等待20个tick(约200ms,假设tick=10ms)。

  • 然后死循环while等待按键松开,在此期间任务一直占用CPU(虽然会因vTaskDelay而被挂起,但逻辑上是阻塞的)。

  • 最后再次延时20tick后返回键值。

也就是说,一次Key_GetNum()调用从检测到按键按下到函数返回,至少需要经过消抖延时 + 等待松手 + 再次消抖的时间,实际耗时通常大于200ms。在这段漫长的时间内,当前任务(Send_Task)是处于阻塞或运行状态的(等待松手时是运行状态不断读GPIO,浪费CPU)。

2.4 连续调用两次的后果

现在回到Send_Task

if(Key_GetNum()==1) { ... } // 第一次调用 // 没有延时 if(Key_GetNum()==2) { ... } // 第二次调用

假设你按下了KEY1:

  • 第一次调用Key_GetNum()被触发,由于KEY1引脚为低,进入内部逻辑:延时20tick → 等待KEY1松开 → 再延时20tick → 返回1。此时的你,手指还按在KEY1上吗?大概率是的,因为函数会一直等到你松开才返回。所以在你松开KEY1之前,这个函数不会返回。

  • 等你终于松开KEY1,Key_GetNum()返回1,于是第一个if成立,发送消息1。

  • 紧接着,第二个Key_GetNum()被调用。但此刻KEY1已经松开,KEY2也没有被按下(你还没来得及按),那么Key_GetNum()会从上到下扫描GPIO:PB1为高、PB11为高,两个if都不满足,直接返回0。因此第二个if永远不成立。

如果你试图直接按KEY2(不按KEY1):

  • 第一次Key_GetNum()首先检查PB1,是高的,不进入第一个if;接着检查PB11,此时你按下了KEY2,PB11为低,于是进入第二个if内部,延时、等待松手、延时后返回2。

  • 返回2后,if(Key_GetNum()==1)比较的是2 == 1吗?显然不成立,所以第一个if不会执行。但奇怪的是,第二个if(Key_GetNum()==2)会被再次调用!因为这是两个独立的调用,第二次调用Key_GetNum()时,KEY2已经被你松开(经过第一次调用的等待松手),所以返回0,依然进不了第二个if。

结论:无论你按哪个键,第二个if(Key_GetNum()==2)永远拿不到有效的键值2。这就是KEY2“失效”的根本原因。

2.5 为什么加上vTaskDelay(50)就正常了?

当我们在两个if之间插入vTaskDelay(50)

if(Key_GetNum()==1) { ... } vTaskDelay(50); if(Key_GetNum()==2) { ... }

此时,如果你先按KEY1然后迅速松开,第一次调用返回1并发送消息。然后任务延时50tick。在这50tick内,你可以从容地按下KEY2。等延时结束,第二次调用Key_GetNum()时,KEY2正处于按下状态(或者刚被松开但仍在消抖窗口内),函数会检测到PB11低电平,进入内部逻辑,最终返回2,从而进入第二个if。

但是这里有一个隐含条件:你必须在50tick内完成按键2的按下并松开(实际上只需要按下一瞬间,因为函数会等待松手)。如果按得太慢,延时结束后你还没按,第二次调用依然返回0。所以这个方案并不稳定,并不能真正解决问题。


三、正确的解决方案

明白了问题根源在于连续调用了两次会阻塞的按键检测函数,解决方案自然就清晰了:只调用一次Key_GetNum(),将返回值保存下来,然后分别判断

3.1 修改后的Send_Task代码

static void Send_Task(void* parameter) { BaseType_t xReturn; uint32_t send_data1 = 1; uint32_t send_data2 = 2; uint8_t key; // 保存按键值的变量 while (1) { key = Key_GetNum(); // 只调用一次,获取当前按键值 if(key == 1) { printf("发送消息send_data1!\r\n"); xReturn = xQueueSend(Test_Queue, &send_data1, 0); if(pdPASS == xReturn) printf("消息send_data1发送成功!\r\n"); } else if(key == 2) { printf("发送消息send_data2!\r\n"); xReturn = xQueueSend(Test_Queue, &send_data2, 0); if(pdPASS == xReturn) printf("消息send_data2发送成功!\r\n"); } vTaskDelay(20); // 周期性延时,避免任务空跑 } }

3.2 为什么这样就能解决问题?

  • 现在整个循环只调用一次Key_GetNum(),该调用会完整地处理一次按键:等待按键按下(如果有)、消抖、等待松手、返回键值。

  • 返回的键值被保存在局部变量key中,后续所有的判断都基于这个值。

  • 无论按键是1还是2,都能被正确识别并进入对应的分支。

  • 即使你不加中间的vTaskDelay(50),KEY2也能正常工作。因为当你按下KEY2时,key得到的就是2,第二个else if会匹配成功。


四、总结与反思

问题现象错误原因解决方案
注释掉vTaskDelay(50)后 KEY2 无响应连续两次调用阻塞式按键检测函数,第二次调用永远拿不到有效键值只调用一次Key_GetNum(),将返回值保存后判断
加上vTaskDelay(50)后看似正常,实际不稳定延时给了用户切换按键的时间,但依赖运气同上,从根本上消除问题

几点重要启示:

  1. 不要在短时间内多次调用带有阻塞或清除状态的函数。尤其是按键读取、传感器数据读取等函数,它们往往只提供“一次性”的结果。

  2. 理解Key_GetNum()的内部实现至关重要。如果它是非破坏性读取(如仅返回当前电平,不等待松手),连续调用不一定出问题;但阻塞式消抖函数天然不适合被连续调用。

  3. 调试时不要急于归咎于队列、信号量等高级组件。先确认程序是否进入了你想让它进入的分支。最简单的方法就是加打印语句,观察逻辑流程。

  4. 在RTOS中,尽量避免长时间阻塞或死循环等待外部事件。使用状态机或事件标志组,让任务能够及时让出CPU。

希望这篇博客能帮助你理解Key_GetNum()调用次数带来的微妙差异,以及如何设计更健壮的按键处理逻辑。如果你也遇到过类似的“幽灵”bug,欢迎在评论区分享你的经历!

http://www.jsqmd.com/news/752370/

相关文章:

  • 从驱动安装到流控配置:一份给单片机新手的CH9101与FT232R实战避坑指南
  • 自动驾驶视频生成的3D高斯泼溅优化实践
  • 飞腾ARM服务器离线部署指南:手把手教你为银河麒麟V10 SP2搭建私有yum仓库
  • 5分钟极速搞定!Axure RP免费中文语言包完整安装指南 [特殊字符]
  • 2026年腾讯云零基础教程:OpenClaw如何搭建?Coding Plan配置与大模型接入流程
  • 2026年初中英语考纲词汇表(1600词)PDF电子版
  • 5步掌握semi-utils:从批量水印到专业摄影作品展示的完整实践
  • 3步快速修复Notepad--文件关联:告别双击无法打开的烦恼
  • 观察Taotoken在多模型聚合调用下的延迟表现与路由稳定性
  • AI智能体安全连接外部应用:Council Composio技能模块详解
  • STM32H7开发笔记(七):MPU引入与讲解 - EM
  • 使用Taotoken CLI工具一键生成多开发环境配置统一团队接入
  • Harepacker-resurrected:MapleStory游戏资源定制与地图编辑技术指南
  • 如何用嘎嘎降AI处理公务员申论:政府文件行政公文降AI免费操作完整教程
  • 光电编码器怕灰,磁性编码器怕干扰?深入拆解电容式编码器,看它如何用‘数字游标卡尺’原理搞定恶劣环境
  • 通过Taotoken平台统一管理分散的API Key并设置访问权限
  • ArcGIS Pro死活读不了Excel?别急着重装Office,试试这个静默安装命令
  • 保姆级教程:用CloudCompare的PCA功能一键搞定点云最小包围盒(附避坑指南)
  • AI 率 75% 起步怎么不打散学术腔?极高档位降 AI 攻略 4 步。
  • SAP ABAP实战:手把手教你搞定MARC表字段增强与自定义屏幕(附完整配置截图)
  • S32K146的CAN FD配置避坑指南:从EB Tresos Studio配置到波特率计算的完整流程
  • 深入UVM宏的‘终点站’:手把手带你调试`uvm_do_on_pri_with`的源码与执行流
  • OpenWrt安装Alpine包管理器后,如何安全卸载Java?保姆级防崩指南来了
  • VideoDownloadHelper终极指南:5分钟掌握浏览器视频下载神器
  • 新手友好:在快马平台用AI生成你的第一个凯撒密码程序
  • AI自动生成代码PR:基于AutoPR的GitHub Issue自动化处理实践
  • 微信聊天记录备份“笨”办法实测:不Root、不越狱、零风险,三小时搞定几万条记录
  • 体验 Taotoken 多模型路由在高峰时段的请求稳定性与低延迟
  • 面试官总问Redis分布式锁?从Redisson源码角度,聊聊可重入锁和看门狗机制怎么实现的
  • STM32H7开发笔记(三):GPIO-libopencm3库实现 - EM