任务切换
任务切换
异常和中断
异常分为同步异常和异步异常,异步异常就是通常所说的中断。
同步异常:异常与当前正在执行的指令直接相关。
异步异常:异常的发生时间与当前正在执行的指令没有直接关系,可以在任意时刻发生。
异常(Exception)
├── 同步异常(Synchronous)
│ ├── 软件触发:SVC
│ ├── 指令执行错误:除零、非法访问等
│ └── 调试异常
│
└── 异步异常(Asynchronous)
└── 中断(Interrupt)
├── 外部中断
└── 系统定时器中断| 特性 | 同步异常 | 异步异常 / 中断 |
|---|---|---|
| 触发源 | CPU内部或软件指令 | 硬件外设等 |
| 发生时机 | 与当前指令相关 | 与当前指令无关 |
| 典型例子 | SVC、UsageFault、HardFault | GPIO、UART、DMA、SysTick |
| 典型用途 | 系统调用、错误处理 | 响应外部事件、系统节拍 |
Cortex-M 的异常向量表中,系统异常位于前面,外部中断紧随其后。
__Vectors:
DCD __initial_sp
DCD Reset_Handler
DCD NMI_Handler
DCD HardFault_Handler
DCD MemManage_Handler
DCD BusFault_Handler
DCD UsageFault_Handler
DCD 0
DCD 0
DCD 0
DCD 0
DCD SVC_Handler
DCD DebugMon_Handler
DCD 0
DCD PendSV_Handler
DCD SysTick_Handler
; 外部中断
DCD WWDG_IRQHandler
DCD PVD_IRQHandler
; ...在 FreeRTOS 中,与任务切换密切相关的三个异常是:
SVC
│
└── 启动第一个任务
SysTick
│
└── 系统节拍 → 更新时间 → 判断是否需要切换
PendSV
│
└── 真正执行任务上下文切换PendSV异常
PendSV(Pendable Service Call)是 Cortex-M 提供的一种可挂起系统异常。
FreeRTOS 利用 PendSV 来完成任务上下文切换。
任务切换可能由多个场景引起,例如:
任务主动让出CPU
│
▼
taskYIELD()
│
▼
设置PendSV挂起位
│
▼
PendSV异常
│
▼
执行任务切换或者:
SysTick中断
│
▼
xTaskIncrementTick()
│
├── 不需要切换 → 返回
│
└── 需要切换
│
▼
设置PendSV挂起位
│
▼
PendSV异常
│
▼
任务切换因此可以把 PendSV 理解成:
FreeRTOS 在 Cortex-M 上专门用来执行任务上下文切换的异常入口。
任务切换的核心过程可以概括成:
当前任务
│
▼
保存当前任务上下文
│
▼
保存当前任务的栈顶指针到TCB
│
▼
选择下一个任务
│
▼
从新任务TCB获取栈顶指针
│
▼
恢复新任务上下文
│
▼
新任务继续运行获取下一个要运行的任务
PendSV 保存当前任务的上下文之后,需要调用:
vTaskSwitchContext();这个函数的作用并不是直接保存、恢复寄存器,而是:
从就绪任务中选择下一个应该运行的任务,并更新
pxCurrentTCB。
核心代码:
void vTaskSwitchContext( void )
{
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
{
xYieldPending = pdTRUE;
}
else
{
xYieldPending = pdFALSE;
traceTASK_SWITCHED_OUT();
taskCHECK_FOR_STACK_OVERFLOW();
taskSELECT_HIGHEST_PRIORITY_TASK();
traceTASK_SWITCHED_IN();
}
}整个过程:
vTaskSwitchContext()
│
▼
判断调度器是否被挂起
│
├── 是
│ └── 设置 xYieldPending
│
└── 否
│
▼
选择最高优先级任务
│
▼
更新 pxCurrentTCB需要注意:
vTaskSwitchContext() 负责“选谁运行”,PendSV 汇编负责“怎么切过去”。
也就是说:
PendSV
├── 保存旧任务寄存器
│
├── vTaskSwitchContext()
│ └── 决定下一个任务是谁
│
└── 恢复新任务寄存器最高优先级选择
FreeRTOS 会为不同优先级维护对应的就绪任务链表。
可以简单理解为:
pxReadyTasksLists[]
优先级
5 ──► [Task_A] ─► [Task_B]
4 ──► [Task_C]
3 ──► [Task_D] ─► [Task_E]
2 ──► 空
1 ──► [Task_F]
0 ──► [Idle]调度器首先找到:
当前存在就绪任务的最高优先级。
然后从这个优先级对应的就绪链表中选择任务。
核心宏:
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
\
portGET_HIGHEST_PRIORITY( uxTopPriority, \
uxTopReadyPriority ); \
\
configASSERT( \
listCURRENT_LIST_LENGTH( \
&( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
\
listGET_OWNER_OF_NEXT_ENTRY( \
pxCurrentTCB, \
&( pxReadyTasksLists[ uxTopPriority ] ) ); \
}逻辑可以理解成:
找到最高优先级
│
▼
检查该优先级的就绪链表
│
▼
取出下一个任务
│
▼
pxCurrentTCB = 新任务TCB因此:
调度器负责决定“下一个是谁”,而 PendSV 负责完成“从当前任务切换到这个任务”。
FreeRTOS 提供了软件查找和硬件优化两种最高优先级查找方式。
本篇暂时只关注软件方式,硬件优化方式不展开。
系统调用
FreeRTOS 中,任务可以主动请求调度。
例如:
taskYIELD();其最终会通过:
portYIELD();设置 PendSV 挂起位:
#define portYIELD()
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
}Cortex-M 的 ICSR(Interrupt Control and State Register)中:
ICSR
│
└── PENDSVSET
│
└── 置1 → PendSV被挂起因此:
taskYIELD()
│
▼
portYIELD()
│
▼
设置 ICSR.PENDSVSET
│
▼
PendSV异常
│
▼
保存当前任务
│
▼
选择新任务
│
▼
恢复新任务这里需要注意:
不是“所有 FreeRTOS API 都叫系统调用”。
更准确地说,FreeRTOS API 中有些 API 会改变任务状态、触发调度或者主动请求任务切换。
例如:
taskYIELD();
vTaskDelay();
xQueueSend();
xSemaphoreGive();
xTaskNotify();这些 API 是否立即发生上下文切换,要看具体 API、当前任务状态以及调度器状态。
SysTick
SysTick 是 Cortex-M 内核自带的系统定时器。
FreeRTOS 通常利用 SysTick 产生周期性的系统节拍。
例如:
void SysTick_Handler(void)
{
if(xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
xPortSysTickHandler();
}
HAL_IncTick();
}FreeRTOS 的核心处理:
void xPortSysTickHandler(void)
{
uint32_t ulPreviousMask;
ulPreviousMask = portSET_INTERRUPT_MASK_FROM_ISR();
{
if(xTaskIncrementTick() != pdFALSE)
{
*(portNVIC_INT_CTRL) = portNVIC_PENDSVSET;
}
}
portCLEAR_INTERRUPT_MASK_FROM_ISR(ulPreviousMask);
}其中:
xTaskIncrementTick()负责推进 FreeRTOS 的系统 Tick,并检查是否产生了调度需求。
例如:
SysTick到来
│
▼
xTaskIncrementTick()
│
├── 延时任务到期?
│
├── 是否有更高优先级任务进入Ready?
│
├── 时间片是否到期?
│
└── 是否需要任务切换?
│
├── 否 → 返回
│
└── 是
│
▼
设置PendSV挂起位
│
▼
PendSV执行切换所以 SysTick 和 PendSV 的职责不要混淆:
SysTick
│
├── 更新时间
├── 处理延时到期
├── 处理时间片
└── 判断是否需要切换
│
▼
设置PendSV
│
▼
PendSV
│
├── 保存当前任务上下文
├── 选择下一个任务
└── 恢复新任务上下文也就是说:
SysTick 负责“发现需要切换”,PendSV 负责“真正完成切换”。
时间片轮转
当多个任务具有相同的优先级,并且开启时间片调度时,FreeRTOS 可以让这些任务轮流获得 CPU。
例如:
Task A:优先级 2
Task B:优先级 2
Task C:优先级 2它们都处于 Ready 状态时,可以进行时间片轮转:
时间
────────────────────────────────────►
Task A │ Task B │ Task C │ Task A │ Task B │ ...
↑ ↑ ↑
时间片 时间片 时间片每经过一个 Tick,调度器可能让当前任务让出 CPU,从同一优先级的就绪链表中选择下一个任务。
相关配置:
#define configUSE_TIME_SLICING 1
#define configUSE_PREEMPTION 1其中:
configUSE_TIME_SLICING用于控制同优先级任务之间的时间片轮转。
configUSE_PREEMPTION用于控制是否使用抢占式调度。
因此:
configUSE_PREEMPTION = 1
│
▼
允许高优先级任务抢占低优先级任务
configUSE_TIME_SLICING = 1
│
▼
同优先级Ready任务之间可以进行时间片轮转最终整个 FreeRTOS 任务切换过程可以串起来:
任务切换
│
┌────────────┴────────────┐
│ │
主动让出 SysTick
│ │
taskYIELD() xTaskIncrementTick()
│ │
└────────────┬────────────┘
│
▼
设置 PendSV 挂起位
│
▼
PendSV Handler
│
┌────────────┴────────────┐
│ │
保存当前任务现场 vTaskSwitchContext()
│ │
│ 选择下一个任务
│ │
└────────────┬────────────┘
│
▼
恢复新任务现场
│
▼
新任务继续运行
