【FreeRTOS】中断机制

【FreeRTOS】之中断机制

在FreeRTOS中,中断是实现实时性必要的操作。一款芯片的中断涉及到硬件触发,软件触发,软件中断处理。所以FreeRTOS的中断机制其实不好单独拿出来看。FreeRTOS关于中断能做到的是提供一套专门在中断服务函数中使用的API,比如:xQueueSendToBack()对应xQueueSendToBackFromISR()
注意:下文有对于指令集的区分,主要以ESP-IDF(RISC-V为例)

中断处理

中断处理主要包括硬件处理部分和软件处理部分(不同的指令集架构有不同)

  • 硬件处理:响应中断请求,关闭中断,保存断点(也就是将程序计数器PC的值保存起来,以保证执行完中断之后可以正确的返回到原来的程序),并且引出中断服务程序的入口地址(由中断隐指令完成)
  • 软件处理:保护现场(主要是保存某些通用寄存器的内容),执行中断服务程序,恢复现场,返回断点

中断处理流程

情景假设:用户在系统正在运行Task1时按下按键,此时中断的处理流程如下。

  • CPU跳到中断向量指向的固定地址执行代码,此跳转是硬件实现的
    • 关中断,防止在保存现场时被打断,导致不能在处理完新中断后正确的返回之前的地址
    • 保存断点
    • 保存现场:Task1被打断时,需要先保存Task1的运行环境,比如各类寄存器的值,保存现场参考系统时钟中断章节
    • 开中断,让更高优先级的中断可以打断此中断
    • 分辨中断类型,调用与此中断相关的中断处理服务函数(ISR,interrupt service routine),如果同时有多个中断,挑选最高优先级的中断执行。
    • 关中断,保护恢复现场过程
    • 恢复现场:继续运行Task1,
    • 开中断
    • 返回断点

中断处理过程

中断处理的规则(注意事项)

  • 中断服务程序执行时间要尽可能的短,否则:
    • 执行时间过长导致其他低中断优先级无法被处理:实时性无法保证
    • ISR在内核中被调用,此时用户任务无法被执行:系统显得很卡顿
  • 非常耗时的中断如何处理?
    • ISR:尽快做清理、记录工作,然后通过事件或者队列等方式触发某个任务,这就需要ISR与任务之间进行通信
  • FreeRTOS把任务认为是硬件无关的,由程序员决定任务的优先级,由调度器决定任务何时运行;而ISR虽然也是软件实现,但它因为与硬件密切相关而被认为是硬件的一部分,它的执行由硬件触发决定
  • 最低优先级的中断可以打断高优先级的任务

内核为中断提供两套API

以写队列为例。

xQueueSendToBack():

  • 可阻塞
  • 可唤醒等待接收的tasK:
    • DelayList[] ==> ReadyList[]
    • 挑出最高优先级的任务运行

xQueueSendToBackFromISR():

  • 不可阻塞
  • 可以唤醒对方:
    • DelayList[] ==> ReadyList[]
    • 不挑出最高优先级的任务,只是记录是否有更高优先级的任务被唤醒

为何要为ISR专门设计一套API?

  • 普通的API,比如QueueSend在写满队列,可以进入阻塞状态,但ISR调用API时,因为ISR不是任务,所以不能进入阻塞状态
使用两套API的好处
  • 使用同一套函数的话,需要增加额外的判断代码、增加额外的分支,使得函数更长、更复杂、难以测试在任务、ISR中调用时,需要的参数不一样,比如:
    • 在任务中调用:需要指定超时时间,表示如果不成功就阻塞一会
    • 在ISR中调用:不需要指定超时时间,无论是否成功都要即刻返回
    • 如果强行把两套函数揉在一起,会导致参数臃肿、无效
  • 移植FreeRTOS时,还需要提供监测上下文的函数,比如is_in_isr()。有些处理器架构没有办法轻易分辨当前是处于任务中,还是处于ISR中,就需要额外添加更多、更复杂的代码

From_ISR API核心(pxHigherPriorityTaskWoken)

用pxHigherPriorityTaskWoken参数的返回值判断有无更高优先级的任务被唤醒。若为true则代表有高优先级任务被唤醒。可以在适当的位置进行任务切换。

为何xQueueSendToBackFromISR()中不切换最高优先级的任务?

原因:中断服务函数执行时间要尽可能的短

假设:GPIO中断被触发
中断服务函数GPIO_KEY_ISR{
    // 执行两次队列发送
    xQueueSendToBackFromISR();   // 假设B被唤醒,B任务的优先级大于当前任务,切换B
    xQueueSendToBackFromISR();   // 假设C又被唤醒,C任务的优先级又大于B任务,切换C
    // 这时就会发现,发生这种情况其实是不需要切换B任务的,应该直接切换最高优先级的C任务就好了。所以不如先不切换,当函数结束时判断`pxHigherPriorityTaskWoken == TRUE`的时候再切换
}

使用From_ISR API的好处

  • 效率高,避免不必要的任务切换
  • 简洁的ISR可避免问题更复杂
  • 可移植性高

用法示例

在用户自己定义的中断服务函数中,在即将退出的函数的时候判断

if (pxHigherPriorityTaskWoken == TRUE) {
    // 切换任务
    portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);    // 汇编实现
    // 或者调用下面用C实现的方法
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);     // C实现
}
中断服务函数写法(如何切换任务)

这样的处理方式很常见,比如UART中断:在UART的ISR中读取多个字符,发现收到回车符才进行任务切换。

void XXX_ISR() {
    int i;
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    for (i = 0; i < N; i++) {
        xQueueSendToBackFromISR(..., &xHigherPriorityTaskWoken); /* 被多次调用 */
    }
    /* 最后再决定是否进行任务切换 */
    if (xHigherPriorityTaskWoken == pdTRUE) {
        /* 任务切换 */
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}

中断的延迟处理(Deferring interrupt processing)

如果此中断的处理确实非常耗时,对于这类中断处理流程如下:

  • t1: 任务1运行,任务2阻塞
  • t2: 发生中断,打断任务1,执行此中断的ISR函数
  • t3: 在创建任务的时候任务2的优先级要比任务1高(这是有软件设计者决定的),所以ISR返回后,运行的是任务2,在这里要完成中断的处理。任务2就被称为“deferred processing task”,中断的延迟处理任务
  • t4: 任务2处理完中断后,进入阻塞态以等待下一个中断,任务1重新运行

中断延迟处理方法

如果同时发生多个中断如何处理?

先相应最高优先级的中断,待最高优先级中断处理完成恢复现场之后,再处理低优先级中断。

正在执行高优先级中断时触发一个低优先级中断

这种情况不会导致低优先级中断丢失,因为有相关的中断源寄存器记录并挂起

中断嵌套

  • 指中断处理函数可以被更高优先级的中断打断。
  • 有些架构可以,但RISC-V不存在高优先级中断打断低优先级的硬件支持
    • 因为对于RISC-V架构来说,进入中断之后mstatus寄存器中的MIE域会被硬件自动设为0,这意味着中断被关闭,从而无法响应新的中断。
    • 但理论上使用软件在进入中断之后强制将mstatus中的MIE打开可以实现中断嵌套。

基于中断的时间片机制–系统调度

注意: 不同指令集架构在系统调度上也有区别,区别是有些架构的CPU支持硬件压栈出栈(比如Cortex-M3),有些架构(RISC-V)只能软件实现。在RISC-V架构中没有PendSV,硬件没有自动压栈功能,切换上下文是由软件实现的。这里主要分析RISC-V。

Cortex-M3

任务调度就是通过系统中断(系统节拍)中实现的,在systick中断中触发PendSV中断,实际任务切换就是在PendSV中断中完成的,外部的硬件定时器周期性地向CPU发送一个中断(记为TickCount),在中断的ISR中尝试TASK切换并统计系统的状态信息。具体有关系统调度,可看任务调度机制章节。

RISC-V (ESP-IDF)

以ESP-IDF为例。不管是Xtensa还是RISC-V,都是在启动调度器xPortStartScheduler()的时候启动系统时钟vPortSetupTimer(),在此函数中对硬件定时器进行配置,包括将SysTickIsrHandler()注册为中断处理程序(注册的方式根据指令集架构的不同也会有略微的区别,但目的都是在发生中断之后能够返回中断处理程序的地址)。
SysTickIsrHandler()中的xTaskIncrementTick()会对xTickCount+1,然后判断是否有任务超时,若有则将延时任务列表的第一个任务添加到就绪链表中。如果FreeRTOS配置为可抢占的,且刚拿出来的任务优先级大于当前任务的优先级,则可进行任务切换,返回xSwitchRequired = pdTRUE。然后调用portYIELD_FROM_ISR()设置xPortSwitchFlag = 1(此变量在中断处理程序恢复现场汇编rtos_int_exit中有使用到)。
最后在rtos_int_exit中,lw a0, pxCurrentTCB恢复任务。
有关SysTickIsr中切换任务的API调用关系

xPortStartScheduler()
|-- vPortSetupTimer()
    |-- SysTickIsrHandler()
        |-- xTaskIncrementTick()
        |-- portYIELD_FROM_ISR()  // 这是宏定义这里在RISC-V是vPortYieldFromISR(),而在Xtensa架构中使用了void vPortEvaluateYieldFromISR(int argc, ...);暂不对Xtensa进行分析
vPortYieldFromISR()

这里主要对uxSchedulerRunning与xPortSwitchFlag进行设置,等到硬件定时器(systick)中断产生之后在_interrupt_handler调用的rtos_int_exit中根据uxSchedulerRunningxPortSwitchFlag进行处理。

void vPortYieldFromISR( void )
{
    traceISR_EXIT_TO_SCHEDULER();
    uxSchedulerRunning = 1;
    xPortSwitchFlag = 1;
}

中断代码参考

中断(不同架构对中断的处理有区别,以ESP32C3 RISC-V为例)

https://blog.csdn.net/gzxb1995/article/details/120423869
目录位置:esp-idf/components/freertos/port/riscv

有关中断向量表的主要API调用关系
cpu_hal_set_vecbase(&_vector_table);
|-- _vector_table
    |-- _interrupt_handler
        |-- _global_interrupt_handler
            |-- 调用真正有用户注册的中断服务函数
在启动CPU的时候设置了中断向量表
这是cpu_start.c
void IRAM_ATTR call_start_cpu0(void)
{
    cpu_hal_set_vecbase(&_vector_table);  // 设置中断向量表,_vector_table的实现在vector.S

    ets_set_appcpu_boot_addr(0);

    bootloader_init_mem();
    // ······ 此处省略亿点点代码
_vector_table汇编
这是vector.S
	.balign 0x100
	.global _vector_table
	.type _vector_table, @function
_vector_table:
	.option push
	.option norvc
	j _panic_handler			/* exception handler, entry 0 */
	.rept (ETS_T1_WDT_INUM - 1)
	j _interrupt_handler		/* 24 identical entries, all pointing to the interrupt handler */
	.endr
	j _panic_handler			/* Call panic handler for ETS_T1_WDT_INUM interrupt (soc-level panic)*/
    j _panic_handler			/* Call panic handler for ETS_CACHEERR_INUM interrupt (soc-level panic)*/
    #ifdef CONFIG_ESP_SYSTEM_MEMPROT_FEATURE
    j _panic_handler			/* Call panic handler for ETS_MEMPROT_ERR_INUM interrupt (soc-level panic)*/
	.rept (ETS_MAX_INUM - ETS_MEMPROT_ERR_INUM)
	#else
	.rept (ETS_MAX_INUM - ETS_CACHEERR_INUM)
	#endif
	j _interrupt_handler		/* 6 identical entries, all pointing to the interrupt handler */
	.endr
	// ······ 此处省略亿点点代码
_interrupt_handler汇编
这是vector.S
	中断处理程序
	/* This is the interrupt handler.
	 * It saves the registers on the stack,
	 * prepares for interrupt nesting,
	 * re-enables the interrupts,
	 * then jumps to the C dispatcher in interrupt.c.
	 */
	.global _interrupt_handler
	.type _interrupt_handler, @function
_interrupt_handler:
	/* entry */
	save_regs
	save_mepc

	/* Before doing anythig preserve the stack pointer */
	/* It will be saved in current TCB, if needed */
	mv a0, sp
	call rtos_int_enter

	/* Before dispatch c handler, restore interrupt to enable nested intr */
	csrr s1, mcause
	csrr s2, mstatus

	/* Save the interrupt threshold level */
	la t0, INTERRUPT_CORE0_CPU_INT_THRESH_REG
	lw s3, 0(t0)

	/* Increase interrupt threshold level */
	li t2, 0x7fffffff
	and t1, s1, t2		/* t1 = mcause & mask */
	slli t1, t1, 2 		/* t1 = mcause * 4 */
	la t2, INTC_INT_PRIO_REG(0)
	add t1, t2, t1		/* t1 = INTC_INT_PRIO_REG + 4 * mcause */
	lw t2, 0(t1)		/* t2 = INTC_INT_PRIO_REG[mcause] */
	addi t2, t2, 1		/* t2 = t2 +1 */
	sw t2, 0(t0)		/* INTERRUPT_CORE0_CPU_INT_THRESH_REG = t2 */
	fence

    // ······ 此处省略亿点点代码

	/* call the C dispatcher */
	mv      a0, sp      /* argument 1, stack pointer */
	mv      a1, s1      /* argument 2, interrupt number (mcause) */
	/* mask off the interrupt flag of mcause */
	li	    t0, 0x7fffffff
	and     a1, a1, t0
	jal     _global_interrupt_handler     // 实现在interrupt.c中
_global_interrupt_handler实现
这是interrupt.c
/* called from vectors.S */
// 此函数调用用户注册的handle
void _global_interrupt_handler(intptr_t sp, int mcause)
{
    intr_handler_item_t it = s_intr_handlers[mcause];
    if (it.handler) {
        (*it.handler)(it.arg);
    }
}

保存现场与恢复现场

在进行上下文切换时需要保存现场与恢复现场

rtos_int_enter保护现场

保存现场是将当前CPU中寄存器保存到对应的TCB栈中。

这是在portasm.S
进入中断时,保存现场

/**
 * This function makes the RTOS aware about a ISR entering, it takes the
 * current task stack saved, places into the TCB, loads the ISR stack
 * the interrupted stack must be passed in a0. It needs to receive the
 * ISR nesting code improvements
 */

    .global rtos_int_enter
    .type rtos_int_enter, @function
rtos_int_enter:
    /* preserve the return address */
    mv t1, ra
    mv t2, a0

    /* scheduler not enabled, jump directly to ISR handler */
    lw t0, uxSchedulerRunning
    beq t0,zero, rtos_enter_end

    /* increments the ISR nesting count */
	la t3, uxInterruptNesting
	lw t4, 0x0(t3)
	addi t5,t4,1
	sw  t5, 0x0(t3)

    /* If reached here from another low-prio ISR, skip stack pushing to TCB */
	bne t4,zero, rtos_enter_end

    /* Save current TCB and load the ISR stack */
    lw  t0, pxCurrentTCB
    sw 	t2, 0x0(t0)
    lw  sp, xIsrStackTop

rtos_enter_end:
    mv  ra, t1
    ret

/**
 * Recovers the next task to run stack pointer and place it into
 * a0, then the interrupt handler can restore the context of
 * the next task
 */
    .global rtos_int_exit
    .type rtos_int_exit, @function
rtos_int_exit:
    /* may skip RTOS aware interrupt since scheduler was not started */
    lw t0, uxSchedulerRunning
    beq t0,zero, rtos_exit_end

    /* update nesting interrupts counter */
    la t2, uxInterruptNesting
    lw t3, 0x0(t2)

    /* Already zero, protect against underflow */
    beq t3, zero, isr_skip_decrement
    addi t3,t3, -1
    sw  t3, 0x0(t2)
rtos_int_enter恢复现场
这是在portasm.S
退出中断时,恢复现场
/**
 * Recovers the next task to run stack pointer and place it into
 * a0, then the interrupt handler can restore the context of
 * the next task
 */
    .global rtos_int_exit
    .type rtos_int_exit, @function
rtos_int_exit:
    /* may skip RTOS aware interrupt since scheduler was not started */
    lw t0, uxSchedulerRunning
    beq t0,zero, rtos_exit_end

    /* update nesting interrupts counter */
    la t2, uxInterruptNesting
    lw t3, 0x0(t2)

    /* Already zero, protect against underflow */
    beq t3, zero, isr_skip_decrement
    addi t3,t3, -1
    sw  t3, 0x0(t2)

isr_skip_decrement:

    /* may still have interrupts pending, skip section below and exit */
    bne t3,zero,rtos_exit_end

    /* Schedule the next task if a yield is pending */
    la t0, xPortSwitchFlag
    lw t2, 0x0(t0)
    beq t2, zero, no_switch

    /* preserve return address and schedule next task
       stack pointer for riscv should always be 16 byte aligned */
    addi sp,sp,-16
    sw  ra, 0(sp)
    call vTaskSwitchContext
    lw  ra, 0(sp)
    addi sp, sp, 16

    /* Clears the switch pending flag */
    la t0, xPortSwitchFlag
    mv t2, zero
    sw  t2, 0x0(t0)

no_switch:
    /* Recover the stack of next task and prepare to exit : */
    lw a0, pxCurrentTCB
    lw a0, 0x0(a0)

rtos_exit_end:
    ret
主动任务切换函数vPortYield

void vPortYield(void)这个函数的实现根据MCU的架构会有不同,比如xtensa架构的FreeRTOS已经支持了汇编,而riscv架构中此函数需要自己实现

// ---------------------- Yielding -------------------------

#define portYIELD() vPortYield()
#define portYIELD_FROM_ISR() vPortYieldFromISR()
#define portEND_SWITCHING_ISR(xSwitchRequired) if(xSwitchRequired) vPortYield()
/* Yielding within an API call (when interrupts are off), means the yield should be delayed
   until interrupts are re-enabled.
   To do this, we use the "cross-core" interrupt as a trigger to yield on this core when interrupts are re-enabled.This
   is the same interrupt & code path which is used to trigger a yield between CPUs, although in this case the yield is
   happening on the same CPU.
*/
#define portYIELD_WITHIN_API() portYIELD()
RISCV vPortYield的实现
// 参考ESP-IDF中riscv vPortYield的实现
void vPortYield(void)
{
    if (uxInterruptNesting) {
        vPortYieldFromISR();
    } else {

        esp_crosscore_int_send_yield(0);
        /* There are 3-4 instructions of latency between triggering the software
           interrupt and the CPU interrupt happening. Make sure it happened before
           we return, otherwise vTaskDelay() may return and execute 1-2
           instructions before the delay actually happens.

           (We could use the WFI instruction here, but there is a chance that
           the interrupt will happen while evaluating the other two conditions
           for an instant yield, and if that happens then the WFI would be
           waiting for the next interrupt to occur...)
        */
        while (uxSchedulerRunning && uxCriticalNesting == 0 && REG_READ(SYSTEM_CPU_INTR_FROM_CPU_0_REG) != 0) {}
    }
}
Xtensa vPortYield的实现

在Xtensa中直接用汇编实现了vPortYield函数。

// 参考FreeRTOS-Kernel/portable/ThirdParty/GCC/Xtensa_ESP32/portasm.S
    .globl  vPortYield
    .type   vPortYield,@function
    .align  4
vPortYield:

    #ifdef __XTENSA_CALL0_ABI__
    addi    sp,  sp, -XT_SOL_FRMSZ
    #else
    entry   sp,  XT_SOL_FRMSZ
    #endif

    rsr     a2,  PS
    s32i    a0,  sp, XT_SOL_PC
    s32i    a2,  sp, XT_SOL_PS
    #ifdef __XTENSA_CALL0_ABI__
    s32i    a12, sp, XT_SOL_A12         /* save callee-saved registers      */
    s32i    a13, sp, XT_SOL_A13
    s32i    a14, sp, XT_SOL_A14
    s32i    a15, sp, XT_SOL_A15
    #else
    /* Spill register windows. Calling xthal_window_spill() causes extra    */
    /* spills and reloads, so we will set things up to call the _nw version */
    /* instead to save cycles.                                              */
    movi    a6,  ~(PS_WOE_MASK|PS_INTLEVEL_MASK)  /* spills a4-a7 if needed */
    and     a2,  a2, a6                           /* clear WOE, INTLEVEL    */
    addi    a2,  a2, XCHAL_EXCM_LEVEL             /* set INTLEVEL           */
    wsr     a2,  PS
    rsync
    call0   xthal_window_spill_nw
    l32i    a2,  sp, XT_SOL_PS                    /* restore PS             */
    wsr     a2,  PS
    #endif

    rsil    a2,  XCHAL_EXCM_LEVEL       /* disable low/med interrupts       */

    #if XCHAL_CP_NUM > 0
    /* Save coprocessor callee-saved state (if any). At this point CPENABLE */
    /* should still reflect which CPs were in use (enabled).                */
    call0   _xt_coproc_savecs
    #endif

    movi    a2,  pxCurrentTCB
	getcoreid a3
	addx4	a2,  a3, a2
    l32i    a2,  a2, 0                  /* a2 = pxCurrentTCB                */
    movi    a3,  0
    s32i    a3,  sp, XT_SOL_EXIT        /* 0 to flag as solicited frame     */
    s32i    sp,  a2, TOPOFSTACK_OFFS    /* pxCurrentTCB->pxTopOfStack = SP  */

    #if XCHAL_CP_NUM > 0
    /* Clear CPENABLE, also in task's co-processor state save area. */
    l32i    a2,  a2, CP_TOPOFSTACK_OFFS /* a2 = pxCurrentTCB->cp_state      */
    movi    a3,  0
    wsr     a3,  CPENABLE
    beqz    a2,  1f
    s16i    a3,  a2, XT_CPENABLE        /* clear saved cpenable             */
1:
    #endif

    /* Tail-call dispatcher. */
    call0   _frxt_dispatch
    /* Never reaches here. */

  • 5
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值