Skip to content

01 · Task模型与执行上下文

FreeRTOS学习的第一步,不是记住 xTaskCreate() 如何调用,而是理解:为什么一个嵌入式系统需要 Task。


1. 从裸机程序到RTOS系统

在传统裸机开发中,程序通常采用前后台结构:

c
int main(void)
{
    System_Init();

    while(1)
    {
        Sensor_Task();

        Motor_Task();

        Communication_Task();
    }
}

这种方式的特点是:

  • 程序只有一个执行流;
  • CPU按照代码顺序执行;
  • 所有功能共享当前运行环境。

对于简单系统:

读取按键
刷新LED
采集ADC
发送串口

完全可以满足需求。

但是随着系统复杂度增加,问题逐渐出现。

例如机器人系统中:

模块要求
电机控制固定周期、高实时性
IMU采集固定频率
通信协议快速响应
显示界面低实时性

这些任务:

  • 执行周期不同;
  • 优先级不同;
  • 响应要求不同。

如果继续使用裸机轮询:

text
while(1)



任务1



任务2



任务3

那么:

一个耗时较长的任务可能影响整个系统响应。

例如:

c
while(1)
{
    Motor_Control();

    Delay_ms(100);

    UART_Process();
}

如果Motor_Control()占用时间过长:

UART可能无法及时处理。

因此RTOS出现的目的:

将一个复杂系统拆分成多个独立执行单元,由调度器根据规则分配CPU时间。

这个独立执行单元,就是Task。


2. Task到底是什么?

初学FreeRTOS时容易产生一个误区:

认为:

c
void Motor_Task(void)
{

}

这个函数就是一个Task。

实际上:

这个函数只是Task的入口。

一个完整Task包含:

Task

├── Task Function

├── Task Stack

└── CPU Context

也就是说:

FreeRTOS创建Task时,并不是简单保存一个函数地址。

它需要额外保存:

  • 任务运行需要的栈空间;
  • 任务当前执行位置;
  • CPU寄存器状态;
  • 优先级;
  • 状态信息。

因此:

Task本质上是一个拥有独立执行上下文(Context)的运行实体。


3. 什么是执行上下文(Context)

所谓执行上下文,可以理解为:

CPU暂停当前程序后,未来能够准确恢复运行所需要保存的全部信息。

例如:

CPU正在执行:

c
Motor_Control();

突然需要切换到:

c
Communication_Task();

CPU必须保存Motor_Task当前状态:

包括:

  • 执行到哪条指令;
  • 使用了哪些寄存器;
  • 当前栈在哪里。

否则恢复Motor_Task时:

CPU不知道:

“我刚才执行到哪里?”

这就是Context存在的意义。


4. 从普通函数调用理解CPU执行流程

为了理解Task切换,需要先理解普通函数。

例如:

c
int main(void)
{
    A();
}


void A(void)
{
    foo();
}

CPU执行过程:

main()



A()



foo()



A()



main()

为什么foo执行完成后知道返回A?

因为函数调用过程中保存了执行信息。


5. PC、LR、SP分别负责什么

5.1 PC(Program Counter)

PC:

程序计数器。

作用:

保存CPU下一条准备执行的指令地址。

CPU运行:

本质就是不断:

读取PC地址



执行指令



PC更新

所以PC决定:

CPU现在运行到哪里。


LR:

Link Register。

用于保存函数返回地址。

例如:

c
main()
{
    A();
}

调用A时:

CPU需要记住:

A执行完成后

返回main中的哪个位置

这个地址保存在LR。


5.3 SP(Stack Pointer)

SP:

Stack Pointer,栈指针。

很多初学者容易误解:

认为SP指向整个Stack空间顶部。

实际上:

SP表示:

当前CPU正在使用的栈位置。

它随着函数调用不断变化。

例如:

进入函数:

SP下降



分配新的栈空间

函数返回:

SP恢复



释放当前函数栈帧

6. Stack与函数栈帧

函数运行过程中,需要保存:

  • 局部变量;
  • 返回地址;
  • 参数;
  • 临时数据;
  • 保存寄存器。

这些数据存放在Stack中。

每进入一个函数,就可能形成一个:

Stack Frame(函数栈帧)

例如:

高地址


main Stack Frame


A Stack Frame

局部变量
返回信息
保存寄存器


foo Stack Frame

局部变量
返回信息
保存寄存器


低地址

Cortex-M架构:

Stack通常向低地址增长。

也就是说:

高地址

0x20001000



Stack增长方向


0x20000000

低地址

7. 为什么函数调用可以自动返回?

因为普通函数存在明确的调用关系:

main



A



foo

foo结束:

CPU根据保存的返回信息:

foo



A



main

继续执行。

但是Task不同。


8. 为什么RTOS Task不能依靠return恢复?

假设:

c
void TaskA(void)
{
    while(1)
    {

    }
}

TaskA通常不会退出。

运行过程中:

TaskA运行



暂停



TaskB运行



恢复TaskA

这里不存在:

return

也不存在:

调用者

所以CPU不能依靠传统函数返回机制恢复。

必须保存:

TaskA暂停时的完整现场

这就是RTOS需要:

  • Task Stack
  • TCB
  • Context Switch

的原因。


9. 为什么每个Task必须拥有独立Stack

如果多个Task共享一个Stack:

Shared Stack

TaskA数据

TaskB数据

当:

TaskA暂停



TaskB运行

TaskB可能覆盖TaskA原来的数据。

恢复TaskA:

可能出现:

  • 返回地址错误;
  • 局部变量错误;
  • CPU状态错误。

因此:

每个Task必须拥有自己的Stack。

RAM


TaskA Stack


TaskB Stack


TaskC Stack

这也是RTOS能够同时管理多个任务的基础。


10. 本章总结

通过本章,需要建立以下核心认识:

1. Task不是函数

函数只是代码入口。

Task包含:

任务代码

+

独立Stack

+

执行Context

2. 裸机和RTOS最大的区别

裸机:

一个执行流



顺序运行

RTOS:

多个Task



调度器管理CPU

3. RTOS实现多任务的核心

不是同时运行多个CPU。

而是:

保存当前Task现场



恢复另一个Task现场



让CPU像继续运行一样

FreeRTOS 内核与工程实践