3. ARM架构核心概念
各位同学,咱们今天聊点硬核的——任务管理。说白了,RTOS 的核心就是管好一堆任务,让它们看起来像在“同时运行”。我做了这么多年嵌入式,见过太多系统崩溃,十有八九都是任务没管好。
嗯,咱们从最基础的说起。
3.1 任务创建、删除、挂起与恢复
任务在 RTOS 里就是一个无限循环的函数。你想想看,一个单片机要处理按键、刷屏幕、读传感器,每个功能都得是个独立任务。
3.1.1 任务创建
以 FreeRTOS 为例,创建任务用 xTaskCreate()。原型长这样:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 堆栈深度,单位是字
void *pvParameters, // 传给任务的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
我在项目中遇到过一个问题:新手喜欢把堆栈设得特别大,觉得“反正内存够”。结果呢?一个项目里 20 个任务,每个堆栈 1024 字,光堆栈就吃掉 80KB RAM。对于 STM32F4 这种只有 128KB RAM 的芯片,基本就废了。
uxTaskGetStackHighWaterMark() 在运行时查看实际使用峰值,再调整到 1.5 倍左右。
3.1.2 任务删除
任务自己可以调用 vTaskDelete(NULL) 来“自杀”。也可以让别人删它:
TaskHandle_t xTaskHandle;
// 创建时拿到句柄
xTaskCreate(vTaskFunction, "Task1", 256, NULL, 1, &xTaskHandle);
// 在别处删除
vTaskDelete(xTaskHandle);
3.1.3 挂起与恢复
挂起就是让任务“睡觉”,但不删除它。恢复就是叫醒它。
vTaskSuspend(xTaskHandle); // 挂起
vTaskResume(xTaskHandle); // 恢复
挂起的任务不参与调度,优先级再高也没用。我常用这个做“低功耗模式切换”——系统要休眠时,把所有非关键任务挂起,只留一个唤醒任务。
3.2 任务优先级与堆栈配置
优先级是 RTOS 调度的心脏。FreeRTOS 的优先级数值越小,优先级越低。0 是空闲任务,最高优先级由 configMAX_PRIORITIES 决定。
| 优先级值 | 典型用途 | 说明 |
|---|---|---|
| 0 | 空闲任务 | 系统自动创建,不可删除 |
| 1-3 | 后台任务 | 数据记录、日志输出、状态检测 |
| 4-6 | 普通任务 | 传感器读取、协议处理、UI 刷新 |
| 7-9 | 实时任务 | 控制环路、通信响应、中断下半部 |
堆栈配置这块,我建议遵循“够用就好,留有余量”。
堆栈配置黄金法则:
- 任务函数内部局部变量越多,堆栈需求越大
- 函数调用嵌套深度每增加一层,大约多消耗 20-40 字节
- 中断嵌套会额外消耗当前任务的堆栈
- 浮点运算任务需要额外 100-200 字节保存 FPU 上下文
3.3 调度器启动与钩子函数
任务都创建好了,怎么让它们跑起来?调用 vTaskStartScheduler()。
int main(void) {
// 硬件初始化
HAL_Init();
SystemClock_Config();
// 创建任务
xTaskCreate(vTaskLED, "LED", 128, NULL, 1, NULL);
xTaskCreate(vTaskUART, "UART", 256, NULL, 2, NULL);
// 启动调度器——这行之后,main 函数就不再执行了
vTaskStartScheduler();
// 如果跑到这里,说明内存不够启动调度器
while(1);
}
调度器启动后,系统会先创建空闲任务,然后开始按优先级调度。这里有个细节:vTaskStartScheduler() 会先初始化系统时钟节拍(SysTick),然后切换到第一个最高优先级的就绪任务。
3.3.1 钩子函数
钩子函数是 RTOS 留给用户的“后门”。FreeRTOS 提供了几个:
- 空闲任务钩子:空闲任务每次循环都会调用。适合做低功耗休眠、CPU 利用率统计。
- 栈溢出钩子:检测到堆栈溢出时触发。调试神器。
- 调度器挂起钩子:调度器挂起/恢复时调用。
// 空闲钩子示例:进入休眠
void vApplicationIdleHook(void) {
__WFI(); // 等待中断,进入休眠
}
// 栈溢出钩子示例:打印错误
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
printf("Stack overflow in task: %s\n", pcTaskName);
// 这里可以记录错误,然后复位系统
}
3.4 任务通知与事件组
任务之间怎么通信?除了队列和信号量,FreeRTOS 还提供了两种轻量级机制:任务通知和事件组。
3.4.1 任务通知
任务通知比信号量快 45% 左右,而且不需要额外创建对象。每个任务都有一个 32 位的通知值。
// 发送通知
xTaskNotifyGive(xTaskHandle); // 相当于给信号量
// 等待通知
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 清空通知值,无限等待
任务通知的几种模式:
| 模式 | 函数 | 用途 |
|---|---|---|
| 发送通知(递增) | xTaskNotifyGive() |
类似二值信号量 |
| 设置特定位 | xTaskNotify() |
事件标志 |
| 覆写通知值 | xTaskNotify() |
传递数据 |
| 等待通知 | ulTaskNotifyTake() |
阻塞等待 |
3.4.2 事件组
事件组就是一堆二进制标志位,最多 24 位(FreeRTOS 保留高 8 位)。多个任务可以等待同一个事件组,也可以设置不同的事件位。
EventGroupHandle_t xEventGroup;
// 创建事件组
xEventGroup = xEventGroupCreate();
// 任务 A:等待事件位 0 和 1 同时置位
EventBits_t uxBits = xEventGroupWaitBits(
xEventGroup, // 事件组句柄
(1 << 0) | (1 << 1), // 等待位 0 和 1
pdTRUE, // 退出时清除这些位
pdTRUE, // 等待所有位(pdFALSE 表示任意一个即可)
portMAX_DELAY // 无限等待
);
// 任务 B:设置事件位 0
xEventGroupSetBits(xEventGroup, (1 << 0));
// 任务 C:设置事件位 1
xEventGroupSetBits(xEventGroup, (1 << 1));
事件组的典型应用场景:
- 等待多个外设初始化完成
- 多条件触发任务执行
- 同步多个任务的执行阶段
选型建议:
- 简单的一对一同步 → 任务通知(最快、最省内存)
- 一对多同步或复杂条件组合 → 事件组
- 需要传递数据 → 队列
- 资源互斥访问 → 互斥量
3.5 本章知识体系
下面这张图把任务管理的核心脉络串起来了:
嗯,任务管理这块内容不少,但核心就三件事:创建好任务、配好优先级、选对通信方式。你想想看,只要把这三点拿捏住,大部分 RTOS 项目都不会出大问题。
我个人的经验是:先画任务关系图,再写代码。别急着敲键盘,把每个任务的优先级、堆栈大小、通信方式列个表格,评审一遍再动手。这样能省掉后面至少一半的调试时间。