news 2026/5/4 18:34:27

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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,欢迎在评论区分享你的经历!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/4 18:32:27

借助Taotoken多模型聚合能力为智能客服系统提供降级容灾方案

借助Taotoken多模型聚合能力为智能客服系统提供降级容灾方案 1. 智能客服系统的稳定性挑战 在构建智能客服系统时,服务稳定性直接影响终端用户体验。传统单一模型接入方式存在明显局限性:当主模型服务出现响应延迟或突发故障时,客服对话可能…

作者头像 李华
网站建设 2026/5/4 18:31:40

解锁百度网盘隐藏潜能:macOS平台逆向工程实践与速度优化探索

解锁百度网盘隐藏潜能:macOS平台逆向工程实践与速度优化探索 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 你是否曾经面对百度网盘那令人…

作者头像 李华
网站建设 2026/5/4 18:30:45

云天励飞冲刺港股:年营收13亿亏4亿 东海云天系减持 套现超7亿

雷递网 雷建平 5月3日深圳云天励飞技术股份有限公司(证券代码:688343 证券简称:云天励飞)日前递交招股书,准备在港交所上市。云天励飞已在科创板上市,截至最后一个交易日,公司股价为88元&#x…

作者头像 李华
网站建设 2026/5/4 18:30:43

开源AI代理行为管控工具ZLAR-Gate:从钩子策略到生产部署全解析

1. 项目概述:从“黑盒”到“白盒”,AI治理的平民化工具 如果你最近在玩Claude Code、Cursor或者Windsurf这类AI编程助手,或者正在用Telegram Bot处理一些自动化任务,可能会有一个隐隐的担忧:这家伙到底背着我执行了哪…

作者头像 李华
网站建设 2026/5/4 18:26:26

使用Taotoken CLI工具一键生成多开发环境配置统一团队接入

使用Taotoken CLI工具一键生成多开发环境配置统一团队接入 1. 安装Taotoken CLI工具 Taotoken CLI工具提供两种安装方式,适合不同使用场景。对于需要频繁使用CLI的团队技术负责人或DevOps工程师,推荐全局安装: npm install -g taotoken/ta…

作者头像 李华