切换主题
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现在运行到哪里。
5.2 LR(Link Register)
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
↓
foofoo结束:
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
+
执行Context2. 裸机和RTOS最大的区别
裸机:
一个执行流
↓
顺序运行RTOS:
多个Task
↓
调度器管理CPU3. RTOS实现多任务的核心
不是同时运行多个CPU。
而是:
保存当前Task现场
↓
恢复另一个Task现场
↓
让CPU像继续运行一样