RTOS 与裸机开发
从裸机轮询与前后台系统的局限出发,理解 RTOS 与实时性的本质,认识 FreeRTOS 的移植结构与任务概念,写出第一个双任务程序。
从裸机轮询与前后台系统的局限出发,理解 RTOS 与实时性的本质,认识 FreeRTOS 的移植结构与任务概念,写出第一个双任务程序。
while(1) 大循环轮询各任务,加上中断服务程序处理紧急事件。逻辑简单、资源开销最小,但任务耦合紧、实时性差、随功能增加会迅速变成难以维护的"意大利面代码"。for(;;) 循环)。每个任务拥有独立的栈空间、优先级和任务控制块 TCB。多个任务"看起来"在并发运行,实际是内核在单核 CPU 上快速切换它们的执行上下文。configUSE_PREEMPTION 等配置项决定。portable/ 目录,例如 port.c、portmacro.h。它实现上下文切换、临界区进入/退出、SysTick 初始化等。用户只需选对内核架构(如 GCC/ARM_CM4F),无需改动通用内核 tasks.c、queue.c。config* 宏决定是否启用抢占、Tick 频率、最大优先级数、堆大小、是否包含互斥量/软件定时器等功能,从而在几 KB 的 MCU 上按需裁剪内核体积。假设一个设备需要:每 10ms 采集一次 ADC、每 100ms 刷新 OLED、随时响应按键、每秒通过串口上报数据。用裸机的前后台方式写,通常长这样:
/* 裸机前后台系统:一个大循环轮询所有任务 */ int main(void) { HAL_Init(); SystemClock_Config(); BSP_Init(); /* 初始化 ADC/OLED/UART/按键 */ while (1) { task_adc_sample(); /* 可能耗时 2ms */ task_oled_refresh(); /* 可能耗时 30ms —— 阻塞了整圈! */ task_key_scan(); /* 按键要等 OLED 刷完才被扫到 */ task_uart_report(); /* 串口发送也要排队等待 */ } }
① 响应延迟不可控:OLED 刷新耗时 30ms 时,按键要等 30ms 后才被扫描到,无法保证响应时间上限。② 任务耦合:每个函数都要写成"非阻塞"的状态机形式,不能用简单的 HAL_Delay(),否则整圈卡死。③ 难以扩展:新增功能要小心插入循环,一处耗时增加就影响全局,维护成本随功能数量非线性上升。
用 FreeRTOS,上面四个功能变成四个独立任务,各自可以放心地"阻塞式"延时。内核保证高优先级任务能及时抢占低优先级任务:
FreeRTOS 内核非常精简,核心只有几个 C 文件。理解目录结构,才能正确地把它加入你的工程:
用 STM32CubeMX 可在 Middleware → FREERTOS 里勾选 CMSIS_V2 接口,自动把上述文件加入工程并生成 FreeRTOSConfig.h 与默认任务。ESP-IDF 则天生内置 FreeRTOS,直接 #include "freertos/FreeRTOS.h" 即可使用(注意 ESP32 是双核 SMP 版本)。本教程以标准 Cortex-M 单核 FreeRTOS 为主讲解,代码在 ESP32 上同样适用。
下面是一份最小可用的配置片段,每一项都直接影响内核行为:
/* FreeRTOSConfig.h — 核心配置项 */ #define configUSE_PREEMPTION 1 /* 1=抢占式 0=协作式 */ #define configCPU_CLOCK_HZ 168000000 /* CPU 主频 */ #define configTICK_RATE_HZ 1000 /* 系统节拍 1000Hz → 1ms 一个 tick */ #define configMAX_PRIORITIES 8 /* 优先级数:0(最低)~7(最高) */ #define configMINIMAL_STACK_SIZE 128 /* 空闲任务栈:单位是字(word)=4字节 */ #define configTOTAL_HEAP_SIZE (15*1024) /* 内核堆总大小 15KB */ #define configUSE_16_BIT_TICKS 0 /* 32 位 tick 计数(Cortex-M 用 32 位) */ #define configUSE_MUTEXES 1 /* 启用互斥量 */ #define configUSE_TIMERS 1 /* 启用软件定时器 */ #define configUSE_IDLE_HOOK 0 /* 是否启用空闲钩子 */ #define configCHECK_FOR_STACK_OVERFLOW 2 /* 栈溢出检测方法 2 */ /* Cortex-M 中断优先级配置(第 9 章详解) */ #define configPRIO_BITS 4 #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
下面创建两个任务:LedTask 每 500ms 翻转一次 LED,PrintTask 每 1s 打印一次心跳。两者独立并发,各自用 vTaskDelay 放心阻塞——这正是 RTOS 相较裸机最直观的好处。
#include "FreeRTOS.h" #include "task.h" #include "main.h" #include <stdio.h> /* 任务句柄,用于后续管理任务 */ TaskHandle_t g_ledHandle = NULL; TaskHandle_t g_printHandle = NULL; /* 任务函数原型:参数固定为 void*,函数永不返回 */ void LedTask(void *pvParameters) { (void) pvParameters; /* 未用到参数,避免告警 */ for (;;) { /* 任务主体是无限循环 */ HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12); vTaskDelay(pdMS_TO_TICKS(500)); /* 阻塞 500ms,让出 CPU */ } } void PrintTask(void *pvParameters) { (void) pvParameters; uint32_t beat = 0; for (;;) { printf("heartbeat #%lu, free heap = %u\r\n", beat++, (unsigned)xPortGetFreeHeapSize()); vTaskDelay(pdMS_TO_TICKS(1000)); } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART2_UART_Init(); /* 创建任务:函数、名字、栈深(word)、参数、优先级、句柄 */ xTaskCreate(LedTask, "LED", 128, NULL, 1, &g_ledHandle); xTaskCreate(PrintTask, "PRINT", 256, NULL, 2, &g_printHandle); /* 启动调度器:此函数正常情况下永不返回 */ vTaskStartScheduler(); /* 只有堆不足以创建空闲任务时才会执行到这里 */ while (1) { } }
任务函数必须是无限循环 for(;;)。如果某个任务的函数体执行完毕并 return,会导致栈失衡、程序跑飞或触发 configASSERT。若确实需要"用完即弃"的任务,应在函数末尾调用 vTaskDelete(NULL) 让内核回收自己,而不是直接返回。
| 参数 | 含义 | 常见误区 |
|---|---|---|
| pxTaskCode | 任务函数指针 | — |
| pcName | 任务名(调试用字符串) | 长度受 configMAX_TASK_NAME_LEN 限制 |
| usStackDepth | 栈深度,单位是字(word) | Cortex-M 上 1 word=4 字节,填 128 实际是 512 字节,别当成字节 |
| pvParameters | 传给任务的参数 | 不要传栈上局部变量地址 |
| uxPriority | 优先级,数值越大越高 | 不得 ≥ configMAX_PRIORITIES |
| pxCreatedTask | 返回任务句柄 | 不需要可传 NULL |
裸机开发用 while(1) 大循环轮询任务,构成前后台系统:主循环是后台、中断是前台。其致命缺陷是响应延迟不可控、任务强耦合、难以扩展。RTOS 把大循环拆成多个独立任务,由调度器按优先级分配 CPU,让高优先级事件在确定时间内被响应——这就是实时性(硬实时=错过截止即失败,软实时=偶尔超时可接受)。FreeRTOS 是免费开源、应用最广的确定性内核,源码核心是 tasks.c/queue.c/list.c,CPU 相关代码在 portable/ 移植层,用户只需维护 FreeRTOSConfig.h 裁剪配置。任务是最小调度单位,本质是永不返回的 for(;;) 函数,拥有独立栈与优先级;切换任务时通过 PendSV 完成上下文切换。用 xTaskCreate() 创建任务(注意栈深单位是字,非字节),最后 vTaskStartScheduler() 启动调度器且永不返回。任务函数绝不能 return,用完应 vTaskDelete(NULL)。