在FreeRTOS中,中断是实现实时性必要的操作。一款芯片的中断涉及到硬件触发,软件触发,软件中断处理。所以FreeRTOS的中断机制其实不好单独拿出来看。FreeRTOS关于中断能做到的是提供一套专门在中断服务函数中使用的API,比如:xQueueSendToBack()
对应xQueueSendToBackFromISR()
注意:下文有对于指令集的区分,主要以ESP-IDF(RISC-V为例)
中断处理主要包括硬件处理部分和软件处理部分(不同的指令集架构有不同)
情景假设:用户在系统正在运行Task1时按下按键,此时中断的处理流程如下。
以写队列为例。
is_in_isr()
。有些处理器架构没有办法轻易分辨当前是处于任务中,还是处于ISR中,就需要额外添加更多、更复杂的代码用pxHigherPriorityTaskWoken参数的返回值判断有无更高优先级的任务被唤醒。若为true则代表有高优先级任务被唤醒。可以在适当的位置进行任务切换。
xQueueSendToBackFromISR()
中不切换最高优先级的任务?原因:中断服务函数执行时间要尽可能的短
假设:GPIO中断被触发
中断服务函数GPIO_KEY_ISR{
// 执行两次队列发送
xQueueSendToBackFromISR(); // 假设B被唤醒,B任务的优先级大于当前任务,切换B
xQueueSendToBackFromISR(); // 假设C又被唤醒,C任务的优先级又大于B任务,切换C
// 这时就会发现,发生这种情况其实是不需要切换B任务的,应该直接切换最高优先级的C任务就好了。所以不如先不切换,当函数结束时判断`pxHigherPriorityTaskWoken == TRUE`的时候再切换
}
在用户自己定义的中断服务函数中,在即将退出的函数的时候判断
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);
}
}
如果此中断的处理确实非常耗时,对于这类中断处理流程如下:
先相应最高优先级的中断,待最高优先级中断处理完成恢复现场之后,再处理低优先级中断。
这种情况不会导致低优先级中断丢失,因为有相关的中断源寄存器记录并挂起
注意: 不同指令集架构在系统调度上也有区别,区别是有些架构的CPU支持硬件压栈出栈(比如Cortex-M3),有些架构(RISC-V)只能软件实现。在RISC-V架构中没有PendSV,硬件没有自动压栈功能,切换上下文是由软件实现的。这里主要分析RISC-V。
任务调度就是通过系统中断(系统节拍)中实现的,在systick中断中触发PendSV中断,实际任务切换就是在PendSV中断中完成的,外部的硬件定时器周期性地向CPU发送一个中断(记为TickCount),在中断的ISR中尝试TASK切换并统计系统的状态信息。具体有关系统调度,可看任务调度机制章节。
以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进行分析
这里主要对uxSchedulerRunning与xPortSwitchFlag进行设置,等到硬件定时器(systick)中断产生之后在_interrupt_handler
调用的rtos_int_exit
中根据uxSchedulerRunning
和xPortSwitchFlag
进行处理。
void vPortYieldFromISR( void )
{
traceISR_EXIT_TO_SCHEDULER();
uxSchedulerRunning = 1;
xPortSwitchFlag = 1;
}
https://blog.csdn.net/gzxb1995/article/details/120423869
目录位置:esp-idf/components/freertos/port/riscv
cpu_hal_set_vecbase(&_vector_table);
|-- _vector_table
|-- _interrupt_handler
|-- _global_interrupt_handler
|-- 调用真正有用户注册的中断服务函数
这是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.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
// ······ 此处省略亿点点代码
这是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中
这是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);
}
}
在进行上下文切换时需要保存现场与恢复现场
保存现场是将当前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)
这是在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
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()
// 参考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函数。
// 参考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. */