FreeRTOS实时操作系统:STM32多任务调度与消息队列实战
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空闲时间。
实验效果
- 下载程序后,通过串口助手(115200-8-N-1)连接CH340,每秒收到一条日志:
[1000] 系统运行中, 栈剩余: 98 words - 按下按键(PA2→3.3V),蓝色LED亮起500ms后自动熄灭
- 串口日志持续打印不受LED闪烁影响,说明两个任务在"同时运行"
- 使用
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覆盖旧数据(仅适用于单元素队列)。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)