对于xtensa
,中断/异常向量表借助链接脚本来实现。具体的说,中断/异常的入口(含最初的处理程序,不仅仅是一个地址)被等间隔的链接,以s3的链接脚本为例components/esp_system/ld/esp32s3/sections.ld.in
:
.iram0.vectors :
{
_iram_start = ABSOLUTE(.);
/* Vectors go to IRAM */
_vector_table = ABSOLUTE(.);
. = 0x0;
KEEP(*(.WindowVectors.text));
. = 0x180;
KEEP(*(.Level2InterruptVector.text));
. = 0x1c0;
KEEP(*(.Level3InterruptVector.text));
. = 0x200;
KEEP(*(.Level4InterruptVector.text));
. = 0x240;
KEEP(*(.Level5InterruptVector.text));
. = 0x280;
KEEP(*(.DebugExceptionVector.text));
. = 0x2c0;
KEEP(*(.NMIExceptionVector.text));
. = 0x300;
KEEP(*(.KernelExceptionVector.text));
. = 0x340;
KEEP(*(.UserExceptionVector.text));
. = 0x3C0;
KEEP(*(.DoubleExceptionVector.text));
. = 0x400;
......
} > iram0_0_seg
至于间隔的大小,则应该是体系结构决定的。从入口开始的一段代码大小有限,只有0x40
字节,这段代码还会跳转执行其它代码,进而完成中断处理。
riscv的异常和中断涉及到以下csr
:
这些在riscv的spec里面都描述的很详细了。和riscv标准实现稍有不同的是,esp32c3的mip的bit[31:1]标记31个中断。产生中断时,采用向量模式跳转至相应中断入口。中断向量表的第0项则留给异常使用。
既然mtvec
存储中断/异常向量表的基址,那么入口就很明确了。一般来说会在启动的起始阶段设置这个基址,IDF当然也不例外:
void IRAM_ATTR call_start_cpu0(void)
{
......
// Move exception vectors to IRAM
cpu_hal_set_vecbase(&_vector_table);
......
}
当有多核存在时,其它核心当然也要设置自己的中断/异常基址:
void IRAM_ATTR call_start_cpu1(void)
{
......
cpu_hal_set_vecbase(&_vector_table);
......
}
不难看出,由于调用的是hal
层的函数,上述代码在xtensa
架构中也是存在的。1.1
节节选的代码中也有_vector_table
。riscv
的_vector_table
则位于components/riscv/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
.option pop
.size _vector_table, .-_vector_table
这段代码已经写的很清楚了,可以看出,当前,异常和部分中断会进panic
,其余中断则是共用入口_interrupt_handler
。
对于xtensa,不同级别的中断有着不同的入口,以Level2
中断入口为例,在freertos/port/xtensa
目录下的xtensa_vector.S
:
.begin literal_prefix .Level2InterruptVector
.section .Level2InterruptVector.text, "ax"
.global _Level2Vector
.type _Level2Vector,@function
.align 4
_Level2Vector:
wsr a0, EXCSAVE_2 /* preserve a0 */
call0 _xt_medint2 /* load interrupt handler */
/* never returns here - call0 is used as a jump (see note at top) */
.end literal_prefix
对于中断来说,可见,入口处的代码仅有有限的几条指令,主要是跳转执行_xt_medint2
,该函数的定义紧跟着上面的入口代码:
.section .iram1,"ax"
.type _xt_medint2,@function
.align 4
_xt_medint2:
mov a0, sp /* sp == a1 */
addi sp, sp, -XT_STK_FRMSZ /* allocate interrupt stack frame */
s32i a0, sp, XT_STK_A1 /* save pre-interrupt SP */
rsr a0, EPS_2 /* save interruptee's PS */
s32i a0, sp, XT_STK_PS
rsr a0, EPC_2 /* save interruptee's PC */
s32i a0, sp, XT_STK_PC
rsr a0, EXCSAVE_2 /* save interruptee's a0 */
s32i a0, sp, XT_STK_A0
movi a0, _xt_medint2_exit /* save exit point for dispatch */
s32i a0, sp, XT_STK_EXIT
/* EXCSAVE_2 should now be free to use. Use it to keep a copy of the
current stack pointer that points to the exception frame (XT_STK_FRAME).*/
#ifdef XT_DEBUG_BACKTRACE
#ifndef __XTENSA_CALL0_ABI__
mov a0, sp
wsr a0, EXCSAVE_2
#endif
#endif
/* Save rest of interrupt context and enter RTOS. */
call0 XT_RTOS_INT_ENTER /* common RTOS interrupt entry */
/* !! We are now on the RTOS system stack !! */
/* Set up PS for C, enable interrupts above this level and clear EXCM. */
#ifdef __XTENSA_CALL0_ABI__
movi a0, PS_INTLEVEL(2) | PS_UM
#else
movi a0, PS_INTLEVEL(2) | PS_UM | PS_WOE
#endif
wsr a0, PS
rsync
/* OK to call C code at this point, dispatch user ISRs */
dispatch_c_isr 2 XCHAL_INTLEVEL2_MASK
/* Done handling interrupts, transfer control to OS */
call0 XT_RTOS_INT_EXIT /* does not return directly here */
不难看出中断处理的流程大体是:
xtensa
使用了dispatch_c_isr汇编宏来完成中断的分发,这个汇编宏会从_xt_interrupt_table
取出用户注册的handler并跳转执行。具体的说,根据coreid
以及int num
计算出相应handler在_xt_interrupt_table
中的偏移,并从该内存地址加载handler的入口地址,进而跳转执行,代码如下:
......
get_percpu_entry_for a3, a12 /* a3 = int num; a12 will be set to coreid */
movi a4, _xt_interrupt_table
addx8 a3, a3, a4 /* a3 = address of interrupt table entry */
l32i a4, a3, XIE_HANDLER /* a4 = handler address */
#ifdef __XTENSA_CALL0_ABI__
mov a12, a6 /* save in callee-saved reg */
l32i a2, a3, XIE_ARG /* a2 = handler arg */
callx0 a4 /* call handler */
mov a2, a12
#else
mov a2, a6 /* save in windowed reg */
l32i a6, a3, XIE_ARG /* a6 = handler arg */
callx4 a4 /* call handler */
#endif
......
当然,dispatch_c_isr也做了一些其它的事情,没时间去细究…
值得一提的是,在链接脚本中的中断/异常向量表里,我们没有看到Level1
中断入口,但Level1
中断入口确实是存在的,存在于_xt_lowint1
:
.section .iram1,"ax"
.type _xt_lowint1,@function
.align 4
_xt_lowint1:
mov a0, sp /* sp == a1 */
addi sp, sp, -XT_STK_FRMSZ /* allocate interrupt stack frame */
s32i a0, sp, XT_STK_A1 /* save pre-interrupt SP */
rsr a0, PS /* save interruptee's PS */
s32i a0, sp, XT_STK_PS
rsr a0, EPC_1 /* save interruptee's PC */
s32i a0, sp, XT_STK_PC
rsr a0, EXCSAVE_1 /* save interruptee's a0 */
s32i a0, sp, XT_STK_A0
movi a0, _xt_user_exit /* save exit point for dispatch */
s32i a0, sp, XT_STK_EXIT
......
/* Save rest of interrupt context and enter RTOS. */
call0 XT_RTOS_INT_ENTER /* common RTOS interrupt entry */
......
/* OK to call C code at this point, dispatch user ISRs */
dispatch_c_isr 1 XCHAL_INTLEVEL1_MASK
/* Done handling interrupts, transfer control to OS */
call0 XT_RTOS_INT_EXIT /* does not return directly here */
谁会跳转_xt_lowint1
呢?不难找到:
.type _xt_user_exc,@function
.align 4
_xt_user_exc:
/* If level 1 interrupt then jump to the dispatcher */
rsr a0, EXCCAUSE
beqi a0, EXCCAUSE_LEVEL1INTERRUPT, _xt_lowint1
......
继续往上追:
.begin literal_prefix .UserExceptionVector
.section .UserExceptionVector.text, "ax"
.global _UserExceptionVector
.type _UserExceptionVector,@function
.align 4
_UserExceptionVector:
wsr a0, EXCSAVE_1 /* preserve a0 */
call0 _xt_user_exc /* user exception handler */
/* never returns here - call0 is used as a jump (see note at top) */
.end literal_prefix
至于为什么Level1
中断要和_UserExceptionVector
共用入口,这可能是体系结构决定的。
对于xtensa,不同的异常也有着不同的入口。异常通常由系统处理,但也有例外_UserExceptionVector
,至少从名字上看,这是用户异常。因此对于异常,分为用户异常和系统异常分别讨论。
对于用户异常,用户可以注册自己的handler。通过调用函数xt_set_exception_handler
可以把handler注册到_xt_exception_table
,往简单了说就是向一个函数指针表填一个地址。既然可以注册,那当用户异常发生时就需要分发,事实确实如此。从_UserExceptionVector
跳转执行_xt_user_exc
,该函数会判断是否是Level1
中断,如果是则跳转执行_xt_lowint1
,否则会在后续的处理中分发用户异常:
.type _xt_user_exc,@function
.align 4
_xt_user_exc:
......
rsr a2, EXCCAUSE /* recover exc cause */
movi a3, _xt_exception_table
get_percpu_entry_for a2, a4
addx4 a4, a2, a3 /* a4 = address of exception table entry */
l32i a4, a4, 0 /* a4 = handler address */
#ifdef __XTENSA_CALL0_ABI__
mov a2, sp /* a2 = pointer to exc frame */
callx0 a4 /* call handler */
#else
mov a6, sp /* a6 = pointer to exc frame */
callx4 a4 /* call handler */
#endif
......
对于系统异常,以_KernelExceptionVector
为例:
.begin literal_prefix .KernelExceptionVector
.section .KernelExceptionVector.text, "ax"
.global _KernelExceptionVector
.align 4
_KernelExceptionVector:
wsr a0, EXCSAVE_1 /* preserve a0 */
call0 _xt_kernel_exc /* kernel exception handler */
/* never returns here - call0 is used as a jump (see note at top) */
.end literal_prefix
不难看出,由于异常入口的代码段大小有限,真正处理异常的是_xt_kernel_exc
:
_xt_kernel_exc:
#if XCHAL_HAVE_DEBUG
break 1, 0 /* unhandled kernel exception */
#endif
movi a0,PANIC_RSN_KERNELEXCEPTION
wsr a0,EXCCAUSE
call0 _xt_panic /* does not return */
rfe /* make a0 point here not there */
可见,该异常会导致系统panic,在_xt_panic
中:
.section .iram1,"ax"
.global panicHandler
.global _xt_panic
.type _xt_panic,@function
.align 4
.literal_position
.align 4
_xt_panic:
......
//Call panic handler
mov a6,sp
call4 panicHandler
ret
panicHandler
就是IDF的panic处理函数了。既然说到了panic,不妨统计一下都有哪些异常会导致panic:
.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
......
/* call the C dispatcher */
mv a0, sp /* argument 1, stack pointer */
csrr a1, mcause /* argument 2, interrupt number */
/* mask off the interrupt flag of mcause */
li t0, 0x7fffffff
and a1, a1, t0
jal _global_interrupt_handler
......
/* Yield to the next task is needed: */
mv a0, sp
call rtos_int_exit
......
restore_mepc
restore_regs
/* exit, this will also re-enable the interrupts */
mret
.size _interrupt_handler, .-_interrupt_handler
实在没什么好说的。保存现场,调用_global_interrupt_handler
,恢复现场。不难猜到,_global_interrupt_handler
肯定是去调用用户注册的handler:
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
和rtos_int_exit
,有兴趣的可以看看,IDF中FreeRTOS的上下文切换。
进_panic_handler
,没什么好说的。
异常有自己的入口,且一般是由系统处理的。但xtensa的用户异常比较特殊,支持用户注册自己的handler,异常注册的逻辑如下:
xt_exc_handler xt_set_exception_handler(int n, xt_exc_handler f)
{
xt_exc_handler old;
if( n < 0 || n >= XCHAL_EXCCAUSE_NUM )
return 0; /* invalid exception number */
/* Convert exception number to _xt_exception_table name */
n = n * portNUM_PROCESSORS + xPortGetCoreID();
old = _xt_exception_table[n];
if (f) {
_xt_exception_table[n] = f;
}
else {
_xt_exception_table[n] = &xt_unhandled_exception;
}
return ((old == &xt_unhandled_exception) ? 0 : old);
}
以双核为例,异常的handler table
中这样排布着用户注册的handler:
| core0 exc0 handler | core1 exc0 handler | core0 exc1 handler | core1 exc1 handler | …
倘若注册时传入空指针,则使用默认的handler,也即xt_unhandled_exception
。
除了少数系统使用的中断,比如提供systick的定时器中断,大部分中断都由用户使用,相应的handler也是用户来注册。与异常注册的逻辑稍有不同的是,中断的handler是有参数的,中断注册的逻辑如下:
xt_handler xt_set_interrupt_handler(int n, xt_handler f, void * arg)
{
xt_handler_table_entry * entry;
xt_handler old;
if( n < 0 || n >= XCHAL_NUM_INTERRUPTS )
return 0; /* invalid interrupt number */
if( Xthal_intlevel[n] > XCHAL_EXCM_LEVEL )
return 0; /* priority level too high to safely handle in C */
/* Convert exception number to _xt_exception_table name */
n = n * portNUM_PROCESSORS + xPortGetCoreID();
entry = _xt_interrupt_table + n;
old = entry->handler;
if (f) {
entry->handler = f;
entry->arg = arg;
}
else {
entry->handler = &xt_unhandled_interrupt;
entry->arg = (void*)n;
}
return ((old == &xt_unhandled_interrupt) ? 0 : old);
}
riscv的异常均由系统(也即IDF自身)处理,因此不支持异常的注册。中断注册的逻辑非常简单:
static intr_handler_item_t s_intr_handlers[32];
void intr_handler_set(int int_no, intr_handler_t fn, void *arg)
{
assert_valid_rv_int_num(int_no);
s_intr_handlers[int_no] = (intr_handler_item_t) {
.handler = fn,
.arg = arg
};
}
中断的分发是由_global_interrupt_handler
来完成的:
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);
}
}
相比xtensa
,riscv
真是一股清流,至少对于软件来说是这样(-_-!!)
先写到这里,因为对xtensa架构没什么了解,还有很多细节是没有分析的,有时间有需要再说吧。