4、移植核心步骤(一):中断与异常处理移植——PendSV、SysTick、SVC中断服务函数实现,中断优先级配置。

好,咱们开始动手了。

前面几章我们把理论基础铺了一遍,从ARM Cortex-M的架构到FreeRTOS的任务切换原理。说实话,那些东西看着有点枯燥,但它们是地基。现在,我们要真正开始写代码了。

这一章,我带你搞定三个最关键的中断服务函数:SVCPendSVSysTick。再加上中断优先级的配置。嗯,这三兄弟搞定了,你的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
}

这段代码做了三件事:

  1. 保存现场:把当前任务的r4-r11压入栈中,更新TCB中的栈指针。
  2. 选择新任务:调用vTaskSwitchContext,让调度器决定下一个跑谁。
  3. 恢复现场:从新任务的栈里弹出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_PRIORITYconfigLIBRARY_KERNEL_INTERRUPT_PRIORITYFreeRTOSConfig.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