[ 附件影像 RECORD ]
FreeRTOS学习笔记 · 第十二讲:4.2Queue消息队列编程示例

FreeRTOS学习笔记 · 第十二讲:4.2Queue消息队列编程示例

[ SCAN_URL ]
[ 归档时间 ]:2026-04-01 19:06 [ 课题责任人 ]:文止墨 [ 档案分类 ]:嵌入式, 文章, 编程

每次创建新项目的时候我都觉得很麻烦,哪怕所有的库都是写好的还是很麻烦。尤其是 EUBF 字库,强绑定一个线程用来执行存储介质状态机。不过不过不使用 EUBF 的话我就得把我需要的汉字手动提取字模了……更麻烦了。

说到这点,我一开始时处理 SD 卡挂载是想写一个阻塞函数在 main 里死等直到超时,但却发现 HAL_Delay 不知为何失效了。

经过艰苦排查最后发现由于我在 FATFS 组件里开启了文件重入(_FS_REENTRANT),导致当我尝试挂载文件系统时,f_montsyscall.c里调用了osMutexNew,这个函数最终会调用xQueueGenericReset,而它会进入临界区。

我们说过进入临界区是有一个计数器来处理嵌套的,即port.c里的uxCriticalNesting,而这个变量初始被赋值为 0xaaaaaaaa。

因此虽然程序正确的调用了退出临界区的函数,但这个计数器没有被归零,因此系统此时依然处于中断屏蔽状态里。

最终,当代码执行到osKernelStart时,系统会调用xPortStartScheduler来执行第一个任务,并在这里将计数器和中断屏蔽寄存器清零。

为什么 FreeRTOS 要这么做?

当程序第一次调用 rtos 相关函数时,系统便认为此时正在搭建系统运行环境。如果此时突如其来一个硬件中断,且该中断里调用了 rtos 的 API 函数的话,由于 rtos 系统仍未初始化完毕,任何 API 都可能指向一个错误的指针或内存空间。为了防止这种情况,freertos 会保持中断屏蔽状态直到开启调度器。

我是如何找到这个问题的原因的?

我发现将 TIM6 定时器中断优先级从 15 调整到 0 时这个问题就会消失,因此我怀疑是 freertos 发力了,因为此时没有别的中断会抢占 TIM6 的中断。经过多次调整优先级后我确定了这个想法,因为只要优先级高于 5 就能正常延时。

之后就是询问 AI freertos 会不会在初始化前就开启中断屏蔽,AI 说不会。我又说我同时在使用 FATFS 和 SDIO_SD,AI 说 FF 的文件重入会根据是否使用 rtos 创建互斥量,我去看了一下还真是如此,随后沿着调用栈不断深入,我发现创建互斥量会进入临界状态。

但这依然没有解决我的疑惑,因为这些代码是 CubeMX 生成的,肯定不会出现忘记退出这种操作。随后我又在临界区相关的两个函数(vPortRaiseBASEPRIvPortSetBASEPRI)里设置断点,发现它们压根就没被调用过。

这时 AI 建议我去查看系统寄存器,尤其是 xPSR 和 BASEPRI ,找到这俩在哪又费了我一番功夫。查看寄存器过后终于能确定系统处于中断屏蔽的状态里。于是我又问为什么正常退出了还是会屏蔽中断,AI 给了我上面的答复。

那为什么我之前设置断点没有触发呢?因为这两个函数被设置为强制内联函数1,也就是说在编译时就把函数调用替换成函数里面的内容以减少调用函数的时间。这样做会让相关的断点失效。当我将断点设置在它们下方时,就能发现问题了:

此时查看调用堆栈可知,挂载SD用的f_mont会调用一系列函数来创建互斥量,最终调用vPortEnterCritical进入临界区。

那我还能说什么呢,这里面随便一个难题就不是我能解决的了。我给 AI 磕一个呗。2

mykey.h
C
#ifndef __MYKEY_H__
#define __MYKEY_H__

#include "main.h"
#include <stdint.h>

#ifndef  KEY0_Pin
    #define KEY0_Pin GPIO_PIN_3
#endif

#ifndef  KEY0_GPIO_Port
    #define KEY0_GPIO_Port GPIOH
#endif

#ifndef  KEY1_Pin
    #define KEY1_Pin GPIO_PIN_2 
#endif

#ifndef  KEY1_GPIO_Port
    #define KEY1_GPIO_Port GPIOH
#endif

#ifndef  KEY2_Pin
    #define KEY2_Pin GPIO_PIN_13
#endif

#ifndef  KEY2_GPIO_Port
    #define KEY2_GPIO_Port GPIOC
#endif


#ifndef  KEY_UP_Pin
    #define KEY_UP_Pin GPIO_PIN_0
#endif

#ifndef  KEY_UP_GPIO_Port
    #define KEY_UP_GPIO_Port GPIOA
#endif

#define KEY_NUM 4
#define KEY_DEBOUNCE_TICK    1   // 20ms * 1 = 20ms (消抖阈值)
#define KEY_LONG_PRESS_TICK  50  // 20ms * 50 = 1000ms (长按1秒触发)

typedef enum {
  KEY0 = 0,
  KEY1,
  KEY2,
  KEY_UP,
  KEY_NONE
}KeyID_t;


// 按键激活状态类型
typedef enum {
    KEY_ACTIVE_LOW = 0, // 按下时为0
    KEY_ACTIVE_HIGH = 1 // 按下时为1
}KeyActiveLevel_t;

// 按键事件类型
typedef enum{
    KEY_EVENT_NONE = 0,
    KEY_EVENT_PRESS,
    KEY_EVENT_RELEASE,
    KEY_EVENT_LONG_PRESS,
    KEY_EVENT_DOUBLE_PRESS
} KeyEvent_t;


// 按键对象结构体
typedef struct {
    KeyID_t  id;              // 按键编号 (原 KeyName)
    uint8_t  current_state;   // 当前物理状态
    uint8_t  last_state;      // 上次物理状态
    uint16_t press_tick;      // 持续按下时间计数器
    uint8_t  is_long_pressed; // 标记标志:是否已经触发过长按
} KeyDevice_t;

uint8_t myGetKeyPressStateByID(KeyID_t keyid);





#endif
mykey.c
C
#include "mykey.h"

#ifdef USE_RTOS
#include "cmsis_os.h"
#endif

// 按键对应的 GPIO 端口
static GPIO_TypeDef* keyGPIOPorts[] = 
{
    KEY0_GPIO_Port, 
    KEY1_GPIO_Port, 
    KEY2_GPIO_Port, 
    KEY_UP_GPIO_Port
};

// 按键对应的 GPIO 引脚
static uint16_t keyGPIOPins[] = 
{
    KEY0_Pin, 
    KEY1_Pin, 
    KEY2_Pin, 
    KEY_UP_Pin
};

// 按键激活电平
static KeyActiveLevel_t keyActiveLevels[] = 
{
    KEY_ACTIVE_LOW,
    KEY_ACTIVE_LOW,
    KEY_ACTIVE_LOW,
    KEY_ACTIVE_HIGH
};




uint8_t myGetKeyPressStateByID(KeyID_t keyid)
{
    if (keyid >= KEY_NUM) return 0; // 无效的按键 ID,返回未按下状态
    //软件消抖逻辑
    uint8_t key_state = 0;
    
    key_state = HAL_GPIO_ReadPin(keyGPIOPorts[keyid], keyGPIOPins[keyid]) == keyActiveLevels[keyid];
    #ifdef USE_RTOS
    osDelay(20); // 简单的软件消抖,延时20ms
    #else
    HAL_Delay(20); // 简单的软件消抖,延时20ms
    #endif

    if(key_state == (HAL_GPIO_ReadPin(keyGPIOPorts[keyid], keyGPIOPins[keyid]) == keyActiveLevels[keyid])){
        return key_state;
    } else {
        return 0; // 状态不稳定,认为没有按下
    }
}


App_Task_ScanKeys
C
void App_Task_ScanKeys(void *argument)
{
  /* USER CODE BEGIN App_Task_ScanKeys */
  
  // 1. 实例化并初始化所有按键对象
  KeyDevice_t keys[KEY_NUM] = {
      {.id = KEY0,   .last_state = 0, .press_tick = 0, .is_long_pressed = 0},
      {.id = KEY1,   .last_state = 0, .press_tick = 0, .is_long_pressed = 0},
      {.id = KEY2,   .last_state = 0, .press_tick = 0, .is_long_pressed = 0},
      {.id = KEY_UP, .last_state = 0, .press_tick = 0, .is_long_pressed = 0}
  };

  KeyMessage_t msg; // 用于发送队列的局部变量

  /* Infinite loop */
  for(;;)
  {
    for(int i = 0; i < KEY_NUM; i++)
    {
      // 读取当前按键状态
      keys[i].current_state = myGetKeyPressStateByID(keys[i].id);

      // --- 状态机逻辑开始 ---
      if (keys[i].current_state == 1) 
      {
         // 状态:正在被按下
         if (keys[i].last_state == 0) 
         {
             // 动作:刚刚按下 (上升沿)
             keys[i].press_tick = 0;
             keys[i].is_long_pressed = 0; // 清除长按标志
         }
         else 
         {
             // 动作:一直按住不松 (持续高电平)
             keys[i].press_tick++;
             
             // 判断:达到长按时间阈值 且 尚未触发过长按
             if (keys[i].press_tick >= KEY_LONG_PRESS_TICK && !keys[i].is_long_pressed)
             {
                 keys[i].is_long_pressed = 1; // 锁定标志位,防止疯狂触发
                 
                 msg.key_id = keys[i].id;
                 msg.event  = KEY_EVENT_LONG_PRESS; // 产生长按事件

                 // 发送消息入队 (KEY_UP 享受 VIP 插队待遇)
                 if (msg.key_id == KEY_UP) {
                     xQueueSendToFront(Queue_KeysHandle, &msg, 0);
                 } else {
                     xQueueSend(Queue_KeysHandle, &msg, 0);
                 }
             }
         }
      }
      else 
      {
         // 状态:处于松开状态
         if (keys[i].last_state == 1) 
         {
             // 动作:刚刚松开手 (下降沿)
             
             // 如果松手前【没有】触发过长按,并且按下的时间大于消抖时间,则判定为“短按”
             if (!keys[i].is_long_pressed && keys[i].press_tick >= KEY_DEBOUNCE_TICK)
             {
                 msg.key_id = keys[i].id;
                 msg.event  = KEY_EVENT_PRESS; // 产生短按事件

                 if (msg.key_id == KEY_UP) {
                     xQueueSendToFront(Queue_KeysHandle, &msg, 0);
                 } else {
                     xQueueSend(Queue_KeysHandle, &msg, 0);
                 }
             }
         }
         // 动作:松开后清空计时器,为下一次按下做准备
         keys[i].press_tick = 0; 
      }

      // 更新历史状态
      keys[i].last_state = keys[i].current_state; 
    }
    LEDG_Toggle();
    
    osDelay(20); 
  }
  /* USER CODE END App_Task_ScanKeys */
}

App_Task_Draw
C
void App_Task_Draw(void *argument)
{
  /* USER CODE BEGIN App_Task_Draw */
  KeyMessage_t msg;
  
  uint8_t  line_count = 0;          // 记录当前屏幕上已经打印了多少行
  uint16_t current_y = 0;           // 记录当前准备打印的 Y 坐标
  
  const uint16_t START_Y = 24;      // 第一行文字的起始 Y 坐标
  const uint16_t LINE_SPACING = 26; // 行距(字体是24,行距设为26能留出2像素的清爽空白)
  const uint16_t FONT_SIZE = 24;    // 字体大小
  uint8_t is_first_message = 1; // 标志位,记录是否是第一条消息
  /* Infinite loop */
  for(;;)
  {
    // 阻塞等待队列消息
    if (xQueueReceive(Queue_KeysHandle, &msg, portMAX_DELAY) == pdTRUE)
    {
        if(is_first_message) {
            // 收到第一条消息时,清屏并重置行计数器
            lcd_dma2d_clear(BLACK);
            line_count = 0;
            is_first_message = 0; // 设置标志位,后续消息不再清屏
        }
        // 判断是否需要清屏(满 9 条则清空)
        if (line_count >= 9) 
        {
            lcd_dma2d_clear(BLACK);
            line_count = 0; // 行计数器归零
        }

        // 计算本次打印的 Y 坐标
        current_y = START_Y + (line_count * LINE_SPACING);

        // 根据按键消息打印对应内容(注意 Y 坐标换成了 current_y)
        switch (msg.key_id)
        {
            case KEY0:
                if (msg.event == KEY_EVENT_PRESS) {
                    lcd_dma2d_show_eubf_str(0, current_y, (char*)"KEY0: 按顺序", "字酷堂板桥体", FONT_SIZE, WHITE);
                } 
                else if (msg.event == KEY_EVENT_LONG_PRESS) {
                    lcd_dma2d_show_eubf_str(0, current_y, (char*)"KEY0: 长按!", "字酷堂板桥体", FONT_SIZE, YELLOW);
                }
                break;

            case KEY1:
                if (msg.event == KEY_EVENT_PRESS) {
                    lcd_dma2d_show_eubf_str(0, current_y, (char*)"KEY1: 按顺序", "字酷堂板桥体", FONT_SIZE, WHITE);
                }
                break;

            case KEY2:
                if (msg.event == KEY_EVENT_PRESS) {
                    lcd_dma2d_show_eubf_str(0, current_y, (char*)"KEY2: 按顺序", "字酷堂板桥体", FONT_SIZE, WHITE);
                }
                break;

            case KEY_UP:
                if (msg.event == KEY_EVENT_PRESS) {
                    // KEY_UP 是高优先级的插队消息,换红色
                    lcd_dma2d_show_eubf_str(0, current_y, (char*)"KEY_UP: 插队啦", "字酷堂板桥体", FONT_SIZE, RED); 
                }
                break;

            default:
                break;
        }

        // 将绘制内容推送到物理屏幕上
        lcd_dma2d_update_screen();
        
        // 行数加 1
        line_count++;

        
        osDelay(1000); 
    }
  }
  /* USER CODE END App_Task_Draw */
}

  1. 说是强制,实际上只是比普通的 inline 关键字“更加”建议编译器内联,实际决定权还是在编译器手里。具体查看这篇帖子 ↩︎
  2. 笑点解析:这张图也是哈基米生成的 ↩︎

[ 发起通讯连接 / INITIATE COMM-LINK ]

[SYS]: 您的回传节点(邮箱)将被严格保密。带有 * 的字段为必填项。


> 终止读取并返回主控制台 <