FreeRtos——1、多任务与“上下文切换”的代价

前言

很多工程师习惯了裸机开发,觉得裸机逻辑好理解:代码是结构化、序列化 的,每一行代码什么时候跑、谁先谁后,在 while(1) 里一目了然。

但启动 RTOS后,这个世界就变了。

RTOS 最大的特点是"不确定性"。你的一行指令跑了一半,可能随时被硬件掐断;你的全局变量刚改完,可能就被另一个任务改了。这种从"顺序执行"到"任务抢占"的转变,就是很多 Bug 之源。RTOS的这种不确定性,源于 CPU 的"分身之术"------上下文切换(Context Switch)

1. CPU 的分身之术

在 STM32(Cortex-M)内核中,CPU 其实是极其单纯的,它只认两个关键指针:

  • PC (Program Counter): 决定下一行代码跑哪。

  • SP (Stack Pointer): 决定当前的局部变量和函数调用栈存在哪。

实操观察:寄存器的"瞬间变化"

我们在 STM32CubeIDE 中开启 CMSIS-V2,创建两个任务:Task_HighTask_Low

cpp 复制代码
/* 任务 A:优先级高 (osPriorityAboveNormal) */
void StartHighTask(void *argument) {
    uint32_t count_a = 0;
    for(;;) {
        count_a++;  // 在此行打断点 (Breakpoint 1)
        osDelay(10); // 关键:调用此函数会触发调度
    }
}

/* 任务 B:优先级低 (osPriorityNormal) */
void StartLowTask(void *argument) {
    uint32_t count_b = 0;
    for(;;) {
        count_b++;  // 在此行打断点 (Breakpoint 2)
    }
}

实验步骤:

  1. 进入 Debug 模式,打开 Registers 窗口。

  2. 当停在 count_b++ 时,记录下 R13 (SP) 的值(假设是 0x20000A00)。

  3. 点击 Resume,当停在 count_a++ 时,再次观察 SP

  4. 你会发现 SP 瞬间变成了类似 0x20000C00 的位置。

底层逻辑: RTOS 为每个任务分配了独立的堆栈空间。切换任务时,内核做了三件事:

  1. 把当前 CPU 所有的寄存器(R0-R15, xPSR)像压压缩包一样,全部压入 Task_Low 的堆栈。

  2. 把 CPU 的 SP 指针强行指向 Task_High 的堆栈顶部。

  3. Task_High 的堆栈里把上次保存的寄存器数值"弹"回 CPU 硬件。

这就是上下文(Context)。对于 CPU 来说,它只是在不同的内存块(堆栈)之间跳来跳去,而对你来说,任务就像是在同时运行。

2. 调度的本质:抢占式 vs 协作式

FreeRTOSConfig.h 中,有一个决定生死定义的宏:

复制代码
#define configUSE_PREEMPTION 1  // 1 为抢占式

为什么说高优先级任务是很霸道的?

在抢占式调度下,只要 Task_HighosDelay 中醒来,内核会立即 暴力切断 Task_Low

  • 代价: 这种切换不是发生在代码的行与行之间,而是可能发生在汇编指令级之间。

  • 风险: 如果你的 Task_Low 正在写一个 64 位的数据(需要两步汇编操作),写了一半被抢占,Task_High 读到的就是一个"脏数据"。

3. 可重入性(Reentrancy)

这是裸机开发者最容易掉进去的深渊。为什么在两个任务里同时用 printfstrtok,系统会偶尔 HardFault 或者打印乱码?

案例分析

cpp 复制代码
// 这是一个不可重入函数(Non-reentrant)
char* GlobalBuffer; 

void MyDataProcess(char* input) {
    GlobalBuffer = input;      // 第一步:修改全局指针
    osDelay(1);               // 假设这里发生抢占!
    printf("%s", GlobalBuffer); // 第二步:打印内容
}

当两个任务同时调用 MyDataProcess 时,GlobalBuffer 这个全局变量就成了冲突点。进阶思想:

  1. 能用局部变量就不用全局变量。 局部变量在各自任务的 Stack 里,是天然隔离的。

  2. 必须共享时,请加锁(下面某个章节我们会讲到互斥锁)。

4. 本章总结

RTOS 的便利不是免费的午餐,他是有代价的。

  • 上下文切换耗时: 在 STM32G0 (M0+) 上,一次切换大约消耗几百个时钟周期。

  • 内存开销: 每个任务都要独立分配堆栈,RAM 消耗远超裸机。

老鸟建议: 如果你的逻辑极其简单,裸机 while(1) 就能搞定,不要为了显摆而上 RTOS。RTOS 是为了解决复杂逻辑耦合实时响应需求而生的。

相关推荐
螺丝钉的扭矩一瞬间产生高能蛋白5 小时前
深入剖析FreeRTOS优先级继承机制:vTaskPriorityInherit与xTaskPriorityDisinherit源码解析
stm32·freertos·嵌入式软件·优先级反转
济6177 小时前
FreeRTOS基础知识---为什么使用FreeRTOS以及其核心功能
嵌入式·freertos
炸膛坦客1 天前
FreeRTOS 学习:(二十八)任务调度器 + 启动第一个任务(了解)
stm32·单片机·操作系统·freertos
炸膛坦客1 天前
FreeRTOS 学习:(二十七)死等延时函数会对任务调度产生什么影响
stm32·操作系统·freertos
Zeku3 天前
TCP交错传输多通道实现原理
stm32·freertos·linux应用开发
MR_Promethus5 天前
FreeRTOS 学习笔记
freertos
Hello_Embed6 天前
Modbus 传感器开发:STM32F030 libmodbus 移植
笔记·stm32·学习·freertos·modbus
叫我韬韬6 天前
硬核调试:在 Keil 中通过全手动栈回溯定位 FreeRTOS 死机任务
stm32·单片机·freertos
风痕天际8 天前
ESP32-S3开发教程6:硬件定时器
单片机·嵌入式硬件·嵌入式·esp32·freertos·esp32s3