FreeRTOS实时操作系统:STM32多任务调度与消息队列实战

前言

在嵌入式开发中,当系统需要同时处理多个任务(如按键扫描、LED闪烁、串口通信)时,传统的裸机轮询方式不仅代码耦合度高,实时性也难以保证。FreeRTOS作为一个轻量级开源实时操作系统,专为资源受限的MCU设计,仅需4KB RAM即可运行。本文以STM32F103C8T6为核心平台,手把手带你实现一个基于FreeRTOS的三任务调度系统,包含任务创建、消息队列通信和优先级管理,让你快速上手RTOS开发。

硬件准备

元件 数量 参考价格
STM32F103C8T6最小系统板 1 ¥12
USB转TTL模块(CH340) 1 ¥5
LED(红色/蓝色各一) 2 ¥0.5
220Ω限流电阻 2 ¥0.2
按键开关 1 ¥0.5
杜邦线 若干 ¥1
ST-Link V2下载器 1 ¥8

接线表:

STM32引脚 外设
PA0 LED1(蓝)→ 220Ω → GND
PA1 LED2(红)→ 220Ω → GND
PA2 按键 → 3.3V(外部上拉)
PA9 (USART1_TX) CH340 RX
PA10 (USART1_RX) CH340 TX

核心代码

以下代码在Keil MDK或STM32CubeIDE中均可编译运行,需提前使用CubeMX生成FreeRTOS基础工程(选择CMSIS_V2封装)。

/* FreeRTOS多任务调度——LED控制与串口通信示例 */
/* 开发环境:STM32CubeIDE + FreeRTOS CMSIS_V2 */

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "cmsis_os2.h"

/* 任务句柄 */
TaskHandle_t ledTaskHandle = NULL;
TaskHandle_t uartTaskHandle = NULL;
TaskHandle_t keyTaskHandle = NULL;

/* 消息队列句柄——用于任务间通信 */
QueueHandle_t msgQueue = NULL;

/* LED控制结构体——通过队列传递的消息类型 */
typedef struct {
    uint8_t ledId;      /* 0=LED1, 1=LED2 */
    uint8_t state;      /* 0=灭, 1=亮 */
    uint16_t duration;  /* 保持时间(ms) */
} LedCommand_t;

/* 任务1:LED控制任务——接收队列指令控制LED */
void LedTask(void *argument) {
    LedCommand_t cmd;
    BaseType_t ret;

    /* 初始化GPIO:PA0、PA1推挽输出 */
    GPIO_InitTypeDef gpio = {0};
    __HAL_RCC_GPIOA_CLK_ENABLE();
    gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1;
    gpio.Mode = GPIO_MODE_OUTPUT_PP;
    gpio.Pull = GPIO_NOPULL;
    gpio.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOA, &gpio);

    while (1) {
        /* 从队列接收消息,阻塞等待 */
        ret = xQueueReceive(msgQueue, &cmd, portMAX_DELAY);
        if (ret == pdPASS) {
            /* 根据指令控制对应LED */
            uint16_t pin = (cmd.ledId == 0) ? GPIO_PIN_0 : GPIO_PIN_1;
            HAL_GPIO_WritePin(GPIOA, pin,
                (cmd.state) ? GPIO_PIN_SET : GPIO_PIN_RESET);
            vTaskDelay(pdMS_TO_TICKS(cmd.duration));
        }
    }
}

/* 任务2:按键扫描任务——检测按键并发送队列消息 */
void KeyTask(void *argument) {
    GPIO_InitTypeDef gpio = {0};
    __HAL_RCC_GPIOA_CLK_ENABLE();
    gpio.Pin = GPIO_PIN_2;
    gpio.Mode = GPIO_MODE_INPUT;
    gpio.Pull = GPIO_PULLDOWN;
    HAL_GPIO_Init(GPIOA, &gpio);

    LedCommand_t cmd;
    uint8_t lastState = 0;

    while (1) {
        uint8_t curState = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2);
        /* 检测上升沿(按键按下) */
        if (curState == 1 && lastState == 0) {
            cmd.ledId = 0;       /* 控制蓝色LED */
            cmd.state = 1;        /* 点亮 */
            cmd.duration = 500;   /* 保持500ms */
            xQueueSend(msgQueue, &cmd, 0);
        }
        lastState = curState;
        vTaskDelay(pdMS_TO_TICKS(20)); /* 20ms消抖周期 */
    }
}

/* 任务3:UART日志任务——每秒上报系统状态 */
void UartTask(void *argument) {
    /* 初始化串口USART1:115200-8-N-1 */
    /* 此处省略HAL_UART_Init配置代码 */

    char buf[64];
    UBaseType_t stackHighWater;
    uint32_t tickCount = 0;

    while (1) {
        tickCount = xTaskGetTickCount();
        stackHighWater = uxTaskGetStackHighWaterMark(NULL);
        sprintf(buf, "[%lu] 系统运行中, 栈剩余: %u words\r\n",
                tickCount, stackHighWater);
        /* HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 100); */
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

/* 主函数:创建任务和队列,启动调度器 */
int main(void) {
    HAL_Init();
    SystemClock_Config(); /* 配置系统时钟72MHz */

    /* 创建消息队列:最多存放5个LedCommand */
    msgQueue = xQueueCreate(5, sizeof(LedCommand_t));
    configASSERT(msgQueue != NULL);

    /* 创建三个任务 */
    xTaskCreate(LedTask,  "LED控制",  128, NULL, 2, &ledTaskHandle);
    xTaskCreate(KeyTask,  "按键扫描", 128, NULL, 3, &keyTaskHandle);
    xTaskCreate(UartTask, "UART日志", 256, NULL, 1, &uartTaskHandle);

    /* 启动调度器 */
    vTaskStartScheduler();

    /* 正常情况下不会执行到这里 */
    while (1);
}

代码解读

1. 消息队列(xQueueCreate / xQueueSend / xQueueReceive)

队列是FreeRTOS中最核心的任务间通信机制。本例中LedCommand_t结构体通过队列从KeyTask传递到LedTask,实现了生产者-消费者模型。xQueueReceive第二个参数设为portMAX_DELAY,LedTask在没有消息时会阻塞挂起,不消耗CPU周期——这正是RTOS相比裸机轮询的核心优势。

2. 任务优先级策略

KeyTask优先级最高(3),保证按键响应实时性;LedTask居中(2);UartTask最低(1),因为日志上报可以容忍延迟。当三个任务同时就绪时,FreeRTOS总是执行最高优先级的就绪任务。

3. 任务栈大小估算

  • LedTask(128 words):约512字节,控制逻辑简单
  • KeyTask(128 words):约512字节,含GPIO操作
  • UartTask(256 words):约1KB含sprintf缓冲区

实际开发中建议使用uxTaskGetStackHighWaterMark()监测栈使用峰值,再精细化调整。栈过大会浪费RAM,栈过小会触发栈溢出(FreeRTOS默认提供configCHECK_FOR_STACK_OVERFLOW检测选项)。

4. vTaskDelay vs 裸机HAL_Delay

vTaskDelay(pdMS_TO_TICKS(20))让出CPU给其他任务执行,而裸机中的HAL_Delay()是阻塞等待——这就是RTOS实现"伪并行"的关键:利用任务切换充分利用MCU空闲时间。

实验效果

  1. 下载程序后,通过串口助手(115200-8-N-1)连接CH340,每秒收到一条日志:[1000] 系统运行中, 栈剩余: 98 words
  2. 按下按键(PA2→3.3V),蓝色LED亮起500ms后自动熄灭
  3. 串口日志持续打印不受LED闪烁影响,说明两个任务在"同时运行"
  4. 使用uxTaskGetSystemState()可以获取所有任务状态:运行、就绪、阻塞、挂起

验证方法: 将UartTask优先级改为3(与KeyTask相同),观察按键响应是否因优先级反转变得卡顿——这是理解RTOS优先级管理的最佳动手实验。

常见问题

Q:程序编译后提示undefined reference to vTaskStartScheduler
A:检查CubeMX中FreeRTOS是否正确配置。需要在Project Manager → Project → FreeRTOS选型中选择"CMSIS_V2"封装,并确保勾选了"Use FreeRTOS"。如果手动移植,还需添加FreeRTOS_Source文件夹到工程并包含所有.c文件。

Q:任务不切换,LED一直亮着不灭?
A:确认每个任务内部都有vTaskDelay()或等待队列等阻塞调用。如果一个任务内没有阻塞点(全部是纯计算),会独占CPU,三个任务中至少要有一个主动让出。另外检查configTICK_RATE_HZ是否设为1000(对应pdMS_TO_TICKS的正确换算)。

Q:按键响应有延迟,有时按了没反应?
A:原因可能是KeyTask优先级不够高,或被高优先级任务长期抢占。将KeyTask优先级提到最高(configMAX_PRIORITIES-1)。如果使用抢占式调度器(configUSE_PREEMPTION=1),每tick中断都会检查优先级变化,响应延迟理论上不超过一个tick周期(通常1ms)。

Q:消息队列中的数据会丢失吗?
A:如果KeyTask连续快速发送(比如按住按键不放),队列满时xQueueSend返回errQUEUE_FULL。解决方法:①增大队列深度(xQueueCreate第一个参数);②使用xQueueSendToFront做紧急插入;③改用xQueueOverwrite覆盖旧数据(仅适用于单元素队列)。

 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐