esp-idf的中断和异常管理——中断和异常的处理流程

目录

  • 1 中断/异常向量表
    • 1.1 xtensa
    • 1.2 riscv
  • 2 中断/异常的处理流程
    • 2.1 xtensa
      • 2.1.1 中断
      • 2.1.2 异常
    • 2.2 riscv
      • 2.2.1 中断
      • 2.2.2 异常
  • 3 注册中断/异常的handler
    • 3.1 xtensa
      • 3.1.1 异常
      • 3.1.2 中断
    • 3.2 riscv
  • 后记

1 中断/异常向量表

1.1 xtensa

对于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字节,这段代码还会跳转执行其它代码,进而完成中断处理。

1.2 riscv

riscv的异常和中断涉及到以下csr

  • mcause,存储中断/异常原因,MSB记录是中断(1)还是异常(0)
  • mepc,存储中断/异常返回地址
  • mtvec,存储中断/异常向量表的基址
  • mtval,存储异常值
  • mip,中断pending
  • mie,中断使能

这些在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_tableriscv_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

2 中断/异常的处理流程

2.1 xtensa

2.1.1 中断

对于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 */

不难看出中断处理的流程大体是:

  1. 保存sp、ps、pc、a0(返回地址)
  2. 为dispatch设置退出地址_xt_medint2_exit
  3. 调用RTOS的int_enter函数
  4. 分发中断,也即调用用户注册的handler

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共用入口,这可能是体系结构决定的。

2.1.2 异常

对于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:

  • _DoubleExceptionVector
  • _KernelExceptionVector
  • _UserExceptionVector==>_xt_user_exc==>_xt_to_coproc_exc==>_xt_coproc_exc==>.L_goto_invalid==>L_xt_coproc_invalid

2.2 riscv

2.2.1 中断

	.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_enterrtos_int_exit,有兴趣的可以看看,IDF中FreeRTOS的上下文切换。

2.2.2 异常

_panic_handler,没什么好说的。

3 注册中断/异常的handler

3.1 xtensa

3.1.1 异常

异常有自己的入口,且一般是由系统处理的。但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

3.1.2 中断

除了少数系统使用的中断,比如提供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);
}

3.2 riscv

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);
    }
}

相比xtensariscv真是一股清流,至少对于软件来说是这样(-_-!!)

后记

先写到这里,因为对xtensa架构没什么了解,还有很多细节是没有分析的,有时间有需要再说吧。

你可能感兴趣的:(#,esp-idf的中断和异常管理,esp-idf)