FreeRTOS-07任务切换 对vPortSVCHandler和xPortPendSVHandler的理解

文章详细解析了FreeRTOS中的TCB块概念,以及vPortSVCHandler和xPortPendSVHandler在任务启动和上下文切换中的关键作用,涉及Cortex-M3内核、寄存器操作和中断管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

任务的基本单位是TCB块,相当于任务的身份证,其基本成员包括栈指针,栈所在地址,链表节点地址,如下:

FreeRTOS.h

typedef struct tskTaskControlBlock
{
	volatile StackType_t    *pxTopOfStack;    /* 栈顶 */

	ListItem_t			    xStateListItem;   /* 任务节点 */
    
    StackType_t             *pxStack;         /* 任务栈起始地址 */
	                                          /* 任务名称,字符串形式 */
	char                    pcTaskName[ configMAX_TASK_NAME_LEN ];  
} tskTCB;
typedef tskTCB TCB_t;

vPortSVCHandler和xPortPendSVHandler讲解

FreeRTOS启动调度器时,将调用prvStartFirstTask启动第一个任务:

/*
 * 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.3,百度搜索“PM0056”即可找到这个文档
 * 在Cortex-M中,内核外设SCB的地址范围为:0xE000ED00-0xE000ED3F
 * 0xE000ED008为SCB外设中SCB_VTOR这个寄存器的地址,里面存放的是向量表的起始地址,即MSP的地址
 */
__asm void prvStartFirstTask( void )
{
	PRESERVE8

	/* 在Cortex-M中,0xE000ED08是SCB_VTOR这个寄存器的地址,
       里面存放的是向量表的起始地址,即MSP的地址 */
	ldr r0, =0xE000ED08
	ldr r0, [r0]
	ldr r0, [r0]

	/* 设置主堆栈指针msp的值 */
	msr msp, r0
    
	/* 使能全局中断 */
	cpsie i
	cpsie f
	dsb
	isb
	
    /* 调用SVC去启动第一个任务 */
    /*产生系统调用,服务号 0表示 SVC 中断,接下来将会执行 SVC 中
断服务函数*/
	svc 0  
	nop
	nop
}

vPortSVCHandler

第一个任务的加载通过触发SVC中断实现, SVC 中断要想被成功响应,其函数名必须与向量表注册的名称一致,在启动文件的向量表中,SVC 的中断服务函数注册的名称是 SVC_Handler,但是在 FreeRTOS 中,官方版本写的是 vPortSVCHandler(),为了能够顺利的响应 SVC 中断,FreeRTOS通过在FreeRTOSConfig.h 中添加添加宏定义的方法来修改函数名,对于PendSV和SysTick的处理同理。

FreeRTOSConfig.h

/* Definitions that map the FreeRTOS port interrupt handlers to their CMSIS
   standard names. */
#define vPortSVCHandler    SVC_Handler
#define xPortPendSVHandler PendSV_Handler

/* IMPORTANT: FreeRTOS is using the SysTick as internal time base, thus make sure the system and peripherials are
              using a different time base (TIM based for example).
 */
#define xPortSysTickHandler SysTick_Handler

vPortSVCHandler()函数开始真正启动第一个任务,不再返回,实现如下

port.c

__asm void vPortSVCHandler( void )
{
    extern pxCurrentTCB;
	PRESERVE8

	/* Get the location of the current TCB. */
	ldr	r3, =pxCurrentTCB	//r3=&pxCurrentTCB,即r3指向当前执行任务的TCB指针所在地址
	ldr r1, [r3]			//r1=*r3=pxCurrentTCB,既让r1指向当前任务的TCB
	ldr r0, [r1]			//r0=*r1=pxTopOfStack,即让r0执行当前任务栈顶
	/* Pop the core registers. */
    //将当前任务栈内容pop,保存入cpu寄存器,注意序号小的寄存器会先被pop,所以pop顺序:r4...r10,r11
    //调用最后一行bx r14时,剩下的 xPSR、PC、LR、R12、R3 - R0会自动出栈
	ldmia r0!, {r4-r11} 
	msr psp, r0		// 将当前任务栈顶赋给psp, 即psp = pxTopOfStack,执行后效果如图1所示			
	isb				// 指令同步隔离,确保之前的指令都已执行完毕
        
    // r0清0,用于关中断
    //设置 basepri 寄存器的值为 0,即关闭所有中断。basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽,但如果设置成0,则不关闭任何中断
	mov r0, #0		
	msr	basepri, r0
    
    //任务上下文加载完毕,中断执行结束,返回用户线程
    //在ARM中,使用r14来保存子程序的返回地址(即上一个程序的地址),执行后效果如图2所示
	bx r14
}

这里代码中的pxCurrentTCB是当前执行的任务指针,定义在task.c

/* 当前正在运行的任务的任务控制块指针,默认初始化为NULL */
TCB_t * volatile pxCurrentTCB = NULL;

图1, msr psp, r0执行后效果:

image-20231218231604044

图2,bx r14执行后效果, 这个时候出栈使用的是 PSP 指针,自动将栈中的剩下内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)同时 PSP 的值也将更新,即指向任务栈的栈顶

image-20231218232726184

这里要注意一下LDR的使用。LDR有指令和伪指令两种用法,一定要区分开。

区别一
ldr r3, = 变量
ldr r3, = 标号
ldr r3, = 立即数

区别二
ldr r3, = 立即数
ldr r3, 立即数 , 就是把立即数这个地址中的值存放到r0中

步骤:

pxCurrentTCB 是一个指针变量,指向当前运行任务的TCB。
首先将pxCurrentTCB 的 地址赋给r3,即 r3 = & pxCurrentTCB ;
然后把pxCurrentTCB 的值赋值给r1,即r1 = pxCurrentTCB 。
最后pxCurrentTCB所指的TCB的第一个成员变量(任务堆栈地址)赋给r0,即r1 = [r3] = *pxCurrentTCB= pxCurrentTCB->pxTopOfStack
把人为入栈的寄存器r4 - r11手动出栈,剩下的 xPSR、PC、LR、R12、R3 - R0会自动出栈。
把出栈完成之后的栈顶地址赋给psp,供任务使用。
这里要注意的就是第一句,是把pxCurrentTCB 的地址赋值给r3,而不是把他的值赋值r3。

xPortPendSVHandler

__asm void xPortPendSVHandler( void )
{
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;

	PRESERVE8
        
	/*r0=psp, 进入PendSV中断时,上个任务环境即		     
	xPSR,PC,R14,R12,R3,R2,R1,R0这些将自动保存入任务栈,
	剩下R4-R11需要手动保存,同时PSP将自动更新(在更新之前 PSP 指向任务栈的栈顶),
	此时 PSP是"上文"任务的堆栈指针,具体指向见图3*/
	mrs r0, psp 
	isb			//确保之前指令已执行(为什么mrs或者msr执行完之后就要接一个isb或者dsb?不知道)
	/* Get the location of the current TCB. */
	ldr	r3, =pxCurrentTCB	//r3=&pxCurrentTCB
	ldr	r2, [r3]			//r2=*r3=pxCurrentTCB

	/* Save the core registers. */
	stmdb r0!, {r4-r11}	//将cpu寄存器保存入"上文"任务栈,注意push总是先push序号大的,因此push顺序:r11,r10....r4

	/* Save the new top of stack into the first member of the TCB. */
	str r0, [r2]	//*r2=r0 => pxTopOfStack=p0, 更新"上文"任务的栈顶

	stmdb sp!, {r0, r3}	//入栈栈顶指针和pxCurrentTCB,这个栈的指针是MSP,注意顺序:r3,r0
    /* 至此,上下文切换的"上文"环境保存完成 */
    
    //关中断,高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断都将被屏蔽,configMAX_SYSCALL_INTERRUPT_PRIORITY的值在FreeRTOSConfig.h定义
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0
        
	dsb	//数据隔离,同步之前对msr的操作
	isb	//指令隔离,确保之前所有指令已执行完毕,之后的指令使用的是正确的basepri配置
        
	bl vTaskSwitchContext	//跳转到vTaskSwitchContext函数去执行,pxCurrentTCB将被更改指向下一个任务
    //开中断
	mov r0, #0
	msr basepri, r0
   
	ldmia sp!, {r0, r3}	//从MSP栈加载r0和r3,此时r3已经指向新任务pxCurrentTCB的地址值,注意pop顺序:r0,r3
	
    /* 以下为上下文切换的"下文"环境切换 */
	/* The first item in pxCurrentTCB is the task top of stack. */
	ldr r1, [r3]	//r1=*r3=pxCurrentTCB,即新任务的TCB
	ldr r0, [r1]	//r0=*r1=pxTopOfStack,即新任务的栈顶指针

	/* Pop the core registers. */
	ldmia r0!, {r4-r11}	//将新任务的任务栈数据加载入cpu寄存器r4-r11
    
    /* 更新psp的值,等PendSV退出时,会以psp作为基地址,将任务栈中剩下的内容自动加载到CPU寄存器 */
    /* 剩下的内容包括: xPSR、PC、LR、r12、r3、r2、r1、r0 */
	msr psp, r0
	isb
	bx r14	//中断结束返回
}

图3,mrs r0, psp 执行后

image-20231218233650182

总结

其实看懂代码之后就知道上下文切换的实质是将哪个任务栈的数据加载入cpu寄存器中,新任务加载时,旧任务的数据将由cpu寄存器通过汇编代码加载回其任务栈,然后屏蔽中断,调用切换函数修改当前指向的TCB,再恢复中断,通过新任务的TCB将新任务的任务栈数据加载入cpu, 自此切换完成。

这里最难的理解点就是vPortSVCHandler和xPortPendSVHandler的代码了,需要对Cotrex内核和arm汇编有基本的了解,可以看下Cortex-M3权威指南的第三(了解寄存器和栈)和第四章(arm指令集)入门,我这里卡了很久。

参考:

【FreeRTOS】xPortPendSVHandler任务切换代码分析

https://blog.youkuaiyun.com/tao475824827/article/details/105622087

<think>好的,我需要帮助用户查找FreeRTOS任务切换的实现代码或示例。首先,我需要回忆一下FreeRTOS任务切换机制。FreeRTOS任务调度器负责管理任务切换,通常有两种触发方式:一种是系统节拍中断(SysTick),另一种是任务主动让出CPU。 根据用户提供的引用,特别是引用[1]提到的xPortSysTickHandler()函数,这应该是在系统节拍中断中触发任务切换的关键函数。另外,引用[4]中提到了portGET_HIGHEST_PRIORITY宏,这可能与优先级调度有关,用于查找最高优先级的就绪任务。 接下来,我需要确定任务切换的具体代码位置。通常,任务切换的底层代码在port.c或portmacro.h文件中,这些是移植层相关的代码,不同处理器架构会有不同的实现。例如,对于ARM Cortex-M系列,可能会使用PendSV中断来处理任务切换。 用户可能需要查看上下文切换的汇编代码,比如vPortSVCHandler()xPortPendSVHandler(),这些函数负责保存恢复任务上下文。此外,任务切换的触发还可能通过taskYIELD()宏,该宏会触发PendSV中断,进而执行上下文切换。 引用[3]提到任务切换任务管理的核心,因此代码示例应该包括调度器的启动、任务切换的触发点以及上下文切换的具体实现。例如,在任务创建时,使用xTaskCreateStatic(引用[2])来创建静态任务,然后调度器通过portYIELD()或系统节拍中断来切换任务。 需要确保提供的代码示例正确,并指出关键函数宏的位置,同时结合用户提供的引用,特别是关于优先级计算的部分(引用[4]),解释任务切换时如何选择下一个要运行的任务。 最后,生成相关问题时要围绕任务切换的实现细节、调度策略、移植层代码结构等方面提问,帮助用户深入理解。</think>### FreeRTOS任务切换实现代码解析 在FreeRTOS中,任务切换的核心逻辑主要涉及 **调度器** **硬件上下文保存/恢复**,以下是关键代码模块示例说明: --- #### 1. **任务切换触发入口** 任务切换可通过两种方式触发: - **系统节拍中断(SysTick)**:通过`xPortSysTickHandler()`触发调度,检查时间片是否耗尽[^1]。 - **主动让出CPU**:例如调用`taskYIELD()`或阻塞API。 ```c // 系统节拍中断处理函数(以Cortex-M为例) void xPortSysTickHandler(void) { if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) { xTaskIncrementTick(); // 更新系统节拍 if (xTaskIncrementTick() != pdFALSE) { portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; // 触发PendSV中断 } } } ``` --- #### 2. **上下文切换核心代码(汇编层)** FreeRTOS使用 **PendSV中断** 实现上下文切换,具体代码位于移植层文件(如`port.c`): ```assembly ; Cortex-M的PendSV中断处理函数 xPortPendSVHandler: mrs r0, psp ; 获取当前任务栈指针 ldr r3, =pxCurrentTCB ; 加载当前任务控制块地址 ldr r2, [r3] stmdb r0!, {r4-r11} ; 保存寄存器r4-r11到任务栈 str r0, [r2] ; 更新任务栈指针到TCB ; 查找下一个最高优先级任务 ldr r0, =uxTopReadyPriority ldr r0, [r0] ldr r1, =pxReadyTasksLists ldr r2, [r1, r0, lsl #2] ; 获取就绪队列头部任务 str r2, [r3] ; 更新pxCurrentTCB为新任务 ldr r0, [r2] ; 加载新任务的栈指针 ldmia r0!, {r4-r11} ; 恢复寄存器r4-r11 msr psp, r0 ; 更新PSP为新任务栈 bx lr ; 返回后自动恢复剩余寄存器 ``` --- #### 3. **优先级调度实现** 引用[4]中的宏`portGET_HIGHEST_PRIORITY`用于快速找到最高优先级任务: ```c #define portGET_HIGHEST_PRIORITY(uxTopPriority, uxReadyPriorities) \ uxTopPriority = (31UL - (uint32_t) __clz(uxReadyPriorities)) ``` 该宏通过 **前导零指令(__clz)** 计算就绪任务中最高优先级[^4]。 --- #### 4. **任务切换示例(创建任务)** 使用`xTaskCreateStatic`创建静态任务(引用[2]): ```c TaskHandle_t xTask1Handle; StackType_t xTask1Stack[configMINIMAL_STACK_SIZE]; StaticTask_t xTask1TCB; void vTask1(void *pvParameters) { while(1) { // 任务逻辑 vTaskDelay(pdMS_TO_TICKS(100)); // 主动让出CPU } } xTask1Handle = xTaskCreateStatic( vTask1, "Task1", configMINIMAL_STACK_SIZE, NULL, 1, xTask1Stack, &xTask1TCB ); ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值