4、移植核心步骤(一):中断与异常处理移植——PendSV、SysTick、SVC中断服务函数实现,中断优先级配置。
好,咱们开始动手了。
前面几章我们把理论基础铺了一遍,从ARM Cortex-M的架构到FreeRTOS的任务切换原理。说实话,那些东西看着有点枯燥,但它们是地基。现在,我们要真正开始写代码了。
这一章,我带你搞定三个最关键的中断服务函数:SVC、PendSV和SysTick。再加上中断优先级的配置。嗯,这三兄弟搞定了,你的RTOS就算活了一半。
4.1 为什么是这三个中断?
你想想看,一个RTOS要运行,核心就两件事:启动第一个任务和切换任务。
SVC(系统服务调用)用来启动第一个任务。PendSV(可挂起的系统异常)用来做任务切换。SysTick(系统滴答定时器)用来提供时间片。
说白了,SVC是“点火”,PendSV是“换挡”,SysTick是“节拍器”。
我在项目中遇到过一个问题:有同事把这三个中断的优先级配反了,结果系统一跑就死。嗯,优先级配置这事,咱们后面细说。
4.2 SVC中断服务函数——启动第一个任务
SVC中断,全称是Supervisor Call。它是个软件触发的中断。你执行一条SVC #0指令,CPU就会立刻进入SVC异常处理。
FreeRTOS用它来做什么?启动第一个任务。
系统初始化完成后,CPU还在特权模式下跑初始化代码。这时候,我们调用SVC指令,在SVC中断服务函数里,把第一个任务的寄存器值加载到CPU中,然后跳转到任务去执行。
核心思路:SVC中断里,我们手动模拟一次“任务切换”,把第一个任务的栈内容弹出到CPU寄存器,然后执行BX R14指令返回——注意,这里的返回地址已经被我们篡改成任务的入口地址了。
代码长这样:
__asm void vPortSVCHandler( void )
{
/* 加载任务栈指针到R3 */
ldr r3, =pxCurrentTCB
ldr r1, [r3]
ldr r0, [r1]
/* 从任务栈中恢复寄存器 */
ldmia r0!, {r4-r11}
msr psp, r0
isb
/* 设置返回值为任务模式,使用PSP */
mov r0, #0
msr basepri, r0
/* 返回,触发异常返回 */
orr r14, r14, #0x0D
bx r14
}
这段代码我解释一下:
pxCurrentTCB是当前任务控制块的指针,里面存着任务栈顶地址。- 我们从栈里恢复
r4-r11这些通用寄存器。 - 然后设置
PSP(进程栈指针),告诉CPU以后用进程栈。 - 最后修改
R14(链接寄存器)的位[2:0]为0xD,表示返回后使用PSP、进入线程模式、使用非特权级。
个人经验:我第一次写这个函数时,忘了加isb指令(指令同步屏障)。结果在某些Cortex-M4芯片上,PSP设置后没生效,任务栈指针乱飞。嗯,从那以后我养成了习惯:修改PSP后一定加isb。
4.3 PendSV中断服务函数——任务切换的核心
PendSV是RTOS的心脏。每次任务切换,都是通过触发PendSV异常来完成的。
为什么不用SVC?因为SVC是同步的,你调用它时,CPU必须立即响应。而PendSV是可挂起的,你可以“攒”多个切换请求,等当前中断处理完了再统一处理。这样能避免中断嵌套带来的优先级反转问题。
PendSV的代码:
__asm void xPortPendSVHandler( void )
{
/* 保存当前任务的上下文 */
mrs r0, psp
isb
ldr r3, =pxCurrentTCB
ldr r2, [r3]
stmdb r0!, {r4-r11}
str r0, [r2]
/* 选择下一个要运行的任务 */
stmdb sp!, {r3, r14}
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp!, {r3, r14}
/* 加载新任务的上下文 */
ldr r1, [r3]
ldr r0, [r1]
ldmia r0!, {r4-r11}
msr psp, r0
isb
bx r14
}
这段代码做了三件事:
- 保存现场:把当前任务的
r4-r11压入栈中,更新TCB中的栈指针。 - 选择新任务:调用
vTaskSwitchContext,让调度器决定下一个跑谁。 - 恢复现场:从新任务的栈里弹出
r4-r11,设置PSP,然后返回。
注意:在调用vTaskSwitchContext之前,我设置了basepri寄存器。这是为了屏蔽所有优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断。说白了,就是在切换任务期间,别让其他中断来捣乱。
我曾经见过一个案例:有人没设basepri,结果在任务切换过程中来了个高优先级中断,中断里调用了xQueueSend,导致调度器状态混乱。嗯,血的教训。
4.4 SysTick中断服务函数——时间片的节拍器
SysTick是个简单的定时器。它每隔固定的时间(比如1ms)触发一次中断。在中断里,我们做两件事:
- 调用
xTaskIncrementTick(),更新系统滴答计数。 - 如果当前任务的时间片用完了,就触发PendSV,让调度器切换任务。
代码很简单:
void xPortSysTickHandler( void )
{
/* 设置PendSV的挂起位,但不立即执行 */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
/* 更新系统滴答 */
if( xTaskIncrementTick() != pdFALSE )
{
/* 如果有更高优先级的任务就绪,触发PendSV */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
你可能会问:为什么这里要设置两次PendSV挂起位?
嗯,第一次设置是为了确保在SysTick退出后,PendSV一定会被执行。第二次设置是因为xTaskIncrementTick()可能返回pdTRUE,表示有更高优先级的任务需要切换。两次设置不会造成重复执行,因为PendSV的挂起位是“写1置位,写0无效”的。
我的习惯:我会把SysTick的优先级设成configKERNEL_INTERRUPT_PRIORITY,也就是最低优先级。这样其他中断可以打断SysTick,但SysTick不会打断其他中断。为什么?因为SysTick只是更新计数,不涉及任务切换的原子操作,被打断也没关系。
4.5 中断优先级配置——别让优先级害了你
中断优先级配置,是RTOS移植中最容易出坑的地方。我见过太多人在这里翻车。
ARM Cortex-M的中断优先级寄存器是8位的,但具体用几位,由芯片厂商决定。比如STM32只用了高4位,低4位固定为0。所以你在配置优先级时,要左移4位。
FreeRTOS要求:
- SVC:优先级最高(数值最小),因为它是启动任务的,不能被其他中断打断。
- PendSV:优先级最低(数值最大),因为它要在所有中断处理完后才执行。
- SysTick:优先级可以设成最低,或者比PendSV高一点点。
典型的配置代码:
/* 设置SVC优先级为最高 */
NVIC_SetPriority( SVCall_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY );
/* 设置PendSV优先级为最低 */
NVIC_SetPriority( PendSV_IRQn, configLIBRARY_KERNEL_INTERRUPT_PRIORITY );
/* 设置SysTick优先级为最低 */
NVIC_SetPriority( SysTick_IRQn, configLIBRARY_KERNEL_INTERRUPT_PRIORITY );
其中configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY和configLIBRARY_KERNEL_INTERRUPT_PRIORITY在FreeRTOSConfig.h中定义。
避坑指南:我曾经在一个项目中,把PendSV的优先级设成了0(最高)。结果呢?每次SysTick触发PendSV,PendSV立即执行,把当前中断给抢断了。那个中断是个UART接收中断,数据还没收完就被打断,导致丢数据。嗯,从那以后我记住了:PendSV必须是所有中断里优先级最低的那个。
4.6 中断优先级分组——别忽略这个细节
Cortex-M支持中断优先级分组。你可以把优先级分成“抢占优先级”和“子优先级”。FreeRTOS要求使用4位抢占优先级、0位子优先级的分组方式。
配置代码:
/* 设置优先级分组为4位抢占优先级,0位子优先级 */
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
为什么必须这样?因为FreeRTOS内部用basepri寄存器来屏蔽中断,而basepri只比较抢占优先级,不比较子优先级。如果你用了子优先级,basepri就管不住了。
警告:如果你在项目中同时使用了其他RTOS或者裸机代码,一定要确保所有中断的优先级分组一致。否则,一个中断可能被另一个意想不到的中断打断,导致难以调试的bug。
4.7 总结与检查清单
好,这一章的内容就这些。我们来梳理一下,你移植完成后应该检查什么:
| 检查项 | 要求 | 常见错误 |
|---|---|---|
| SVC中断函数 | 能正确加载第一个任务的上下文 | 忘了设置PSP,或忘了加isb |
| PendSV中断函数 | 能保存/恢复任务上下文,调用vTaskSwitchContext |
没设basepri,导致切换过程中断 |
| SysTick中断函数 | 更新滴答计数,触发PendSV | 忘了设置PendSV挂起位 |
| 中断优先级 | SVC最高,PendSV最低,SysTick最低 | PendSV优先级设得太高 |
| 优先级分组 | 4位抢占优先级,0位子优先级 | 用了子优先级导致basepri失效 |
下一章,我们会继续深入,讲任务栈的初始化、任务控制块的创建,以及如何让第一个任务真正跑起来。嗯,到时候你会看到,前面这些中断函数是怎么被串联起来的。
公众号:蓝海资料掘金营,微信deep3321