天天看点

eCos中断响应详解,基于Cortex-M架构

本文阐述eCos在Cortex-M架构中的中断响应过程。eCos是开源免版税的抢占式实时操作系统。其最大亮点是可配置,与其配套的图形化配置工具提供组件管理、选项配置、自动化单元测试等。

使能和屏蔽中断

Cortex-M有三个特殊功能寄存器与中断使能和屏蔽有关,分别是primask、faultmask、basepri,设置primask可以屏蔽除了硬ault异常和NMI异常之外的所有异常和中断,设置faultmask可以屏蔽除了NMI异常之外的所有异常和中断,设置basepri可以屏蔽优先级等于或低于当前basepri值的所有异常和中断,但basepri为0时表示使能所有异常和中断。

eCos使用basepri控制中断使能和屏蔽。在整个eCos运行期间,包括初始化、中断处理过程,primask和faultmask寄存器都处于默认状态,即清除状态,在整个运行期间硬fault异常总是使能的。进入初始化例程后,用法fault、总线fault、存储器管理fault被使能且优先级设置为0。eCos在初始化早期将basepri设置成优先级低于0(优先级数值大于0)的最高有效优先级,最高有效优先级定义为宏CYGNUM_HAL_CORTEXM_PRIORITY_MAX,

CYGNUM_HAL_CORTEXM_PRIORITY_MAX = 1<<(8-CYGNUM_HAL_CORTEXM_PRIORITY_LEVEL_BITS)      

CYGNUM_HAL_CORTEXM_PRIORITY_LEVEL_BITS与Cortex-M核心的配置有关,不同的Cortex-M变种使用不同的数值,例如Kinetis和STM32为4,LPC17xx为5。

屏蔽中断

eCos将basepri设置为CYGNUM_HAL_CORTEXM_PRIORITY_MAX来屏蔽中断。

// hal/cortexm/arch/<version>/include/hal_intr.h:239
# define HAL_DISABLE_INTERRUPTS(__old)          \
    __asm__ volatile (                          \
        "mrs    %0, basepri             \n"     \
        "mov    r1,%1                   \n"     \
        "msr    basepri,r1              \n"     \
        : "=&r" (__old)                         \
        :  "r" (CYGNUM_HAL_CORTEXM_PRIORITY_MAX)\
        : "r1"                                  \
        );
#endif      

(4)将原来的basepri复制到__old,供下次恢复basepri值。

(6)将CYGNUM_HAL_CORTEXM_PRIORITY_MAX复制给basepri,屏蔽所有优先级等于或低于CYGNUM_HAL_CORTEXM_PRIORITY_MAX的中断。

使能中断

eCos将basepri设置为0来使能中断。

// hal/cortexm/arch/<version>/include/hal_intr.h:260
# define HAL_ENABLE_INTERRUPTS()                \
    __asm__ volatile (                          \
        "mov    r1,#0                   \n"     \
        "msr    basepri,r1              \n"     \
        :                                       \
        :                                       \
        : "r1"                                  \
        );
#endif      

(5)将pasepri设置为0,使能所有中断。

中断优先级

eCos将basepri设置为CYGNUM_HAL_CORTEXM_PRIORITY_MAX来屏蔽中断,这意味着所有中断优先级都不能高于CYGNUM_HAL_CORTEXM_PRIORITY_MAX,否则HAL_DISABLE_INTERRUPTS宏将不能正常工作,为了将中断优先级等于低于CYGNUM_HAL_CORTEXM_PRIORITY_MAX,在设置中断优先级时,eCos对优先级数值做了修改。

// hal/cortexm/arch/<version>/src/hal_misc.c:502
__externC void hal_interrupt_set_level( cyg_uint32 vector, cyg_uint32 level )
{
    cyg_uint32 l = (level)+CYGNUM_HAL_CORTEXM_PRIORITY_MAX;
    if( l > 0xFF ) l = 0xFF; /* clamp to 0xFF */
    ......
}      

(3)将优先级数值加上CYGNUM_HAL_CORTEXM_PRIORITY_MAX,这将保证中断优先级等于或低于CYGNUM_HAL_CORTEXM_PRIORITY_MAX。

(4)如果超过0xFF,那么设置为0xFF,避免高字节截断引起优先级紊乱,0xFF是Cortex-M最低优先级。

中断响应

eCos将中断处理分成两部分:ISR和DSR。中断响应的过程也就分成了两种,一种是ISR没有要求执行DSR或者调度器被加锁的情况,这种情况下执行ISR后立即返回被中断线程;另一种是ISR要求执行DSR并且调度器未加锁的情况,这种情况下执行ISR后接着执行DSR,在DSR中可能将某些线程从等待状态切换到就绪状态并执行线程调度,DSR执行完成后可能返回当前线程,也可能返回刚刚被列入就绪队列的更高优先级的线程。

eCos在Cortex-M架构下的中断响应过程如下图所示,下图的响应过程是要求执行DSR的过程。

​​

eCos中断响应详解,基于Cortex-M架构

​​

图中S为Cyg_Scheduler的缩写,I为Cyg_Interrupt的缩写,S::unlock()意为Cyg_Scheduler::unlock()。

中断响应过程涉及4个运行状态Thread、ISR、PendSV、SVC,Thread使用PSP堆栈,ISR、PendSV、SVC均使用MSP堆栈,包括7个步骤,1次中断向量响应,1次PendSV响应,2次SVC响应。

{1}中断产生,当前线程执行过程被打断,CPU进入中断异常向量开始中断响应,eCos将所有中断包括SysTick的中断向量设置为hal_default_interrupt_vsr,因此首先进入hal_default_interrupt_vsr函数,该函数读取中断号后调用hal_deliver_interrupt对中断进行分发,hal_deliver_interrupt函数调用对应的ISR,并根据需要挂起一个PendSV请求。

{2}中断向量返回后通过Cortex-M的咬尾中断机制直接进入PendSV向量,PendSV构造一个假的异常堆栈,通过这个假的异常堆栈在线程环境下调用hal_interrupt_end函数,从PendSV返回后,因为假的异常堆栈的存在并不返回当前线程被中断的代码处继续执行,而是返回到hal_interrupt_end函数,此外假的异常堆栈将hal_interrupt_end函数的返回点设置为hal_interrupt_end_done。看起来就像是在当前线程被中断处调用了hal_interrupt_end_done,然后hal_interrupt_end_done调用了hal_interrupt_end函数一样。

{3}返回到线程环境后开始执行hal_interrupt_end,依次调用interrupt_end、Cyg_Scheduler::unlock、Cyg_Scheduler::unlock_inner、Cyg_Scheduler::call_pending_DSRs、Cyg_Interrupt::call_pending_DSRs,eCos希望在专用的中断堆栈环境下执行DSR,因此通过swi指令进入SVC系统调用向量,在SVC环境下调用DSR,SVC使用的MSP堆栈,eCos将MSP用作专用的中断和异常堆栈。

{4}CPU执行到swi指令后进入SVC系统调用向量,这次系统调用将调用hal_call_dsrs_vsr函数,hal_call_dsrs_vsr直接跳转到cyg_interrupt_call_pending_DSRs,cyg_interrupt_call_pending_DSRs调用Cyg_Interrupt::call_pending_DSRs_inner来调用DSR。

{5}SVC系统调用向量返回当前线程后,最终返回到hal_interrupt_end_done。前面是通过假的异常堆栈帧在当前线程中断点插入对hal_interrupt_end的调用,hal_interrupt_end_done的作用是撤销假堆栈帧恢复当前线程的被中断点运行环境,通过swi指令进入SVC系统调用来恢复线程堆栈。

{6}CPU执行到swi指令后进入sVC系统调用向量,这次系统调用将调用hal_interrupt_end_vsr,hal_interrupt_end_vsr的作用是撤销假堆栈恢复当前线程堆栈指针,然后返回。

{7}终于,中断响应完成,返回到线程被中断点继续执行,需要注意的是{5}和{6}之间会进行调度,如果调度了更高优先级的线程,当返回到这里的时候已经过去比较长的时间了。

接下来依据中断响应过程的调用次序,详细讲述各函数。

hal_default_interrupt_vsr

在HAL的初始化过程,所有中断向量服务例程包括SysTick的向量服务例程均被设置成hal_default_interrupt_vsr,因此产生中断后首先进入的就是向量服务例程hal_default_interrupt_vsr。

hal/cortexm/arch/<version>/src/vectors.S:212
        .type   hal_default_interrupt_vsr, %function
hal_default_interrupt_vsr:

        push    {lr}                    // Save return link
        sub     sp,#4                   // Realign SP to 8 bytes

        mrs     r0,ipsr                 // R0 = arg0 = vector number
        sub     r0,#15                  // Adjust to interrupt range

        bl      hal_deliver_interrupt

        add     sp,#4                   // pop alignment padding
        pop     {pc}                    // Pop LR and return      

(5)向量服务例程首先保存当前的lr寄存器值到堆栈,因为后面马上要使用bl指令调用函数,bl指令将会修改lr寄存器。

(6)堆栈按8字节对齐。

(8)读取当前中断号。

(9)对中断号进行修正,ipsr中存储的实际上是向量编号,这里实际需要的是中断编号,减掉15而不是16的原因是eCos将SysTick也作为中断来处理,SysTick的异常编号为15,对应中断编号0。

(11)调用hal_deliver_interrupt函数分发中断服务例程。

(13)重新修正堆栈指针。

(14)将lr寄存器弹出到pc,等价于mov pc, lr,从中断返回,如果没有DSR需要执行,立即返回被中断线程,如果需要执行DSR,那么执行咬尾中断,进入PendSVC异常服务例程。

hal_deliver_interrupt

VSR读取当前中断号后调用中断分发函数,中断分发函数根据中断号调用对应的ISR。

// hal/cortexm/arch/<version>/src/hal_misc.c:371
void hal_deliver_interrupt( cyg_uint32 vector )
{
    register cyg_uint32 isr_result;
    register cyg_isr *isr;
    cyg_bool pendsvc = false;

    isr = (cyg_isr *)hal_interrupt_handlers[vector];

    // Call the ISR
    isr_result = isr( vector, hal_interrupt_data[vector] );

    // If the ISR has returned the CALL_DSR bit, post the DSR and set
    // the pendable SVC exception pending.
    if( isr_result & CYG_ISR_CALL_DSR )
    {
        cyg_interrupt_post_dsr( hal_interrupt_objects[vector] );

        // Post the pendable SVC to call interrupt_end(). But only if
        // the scheduler lock is currently zero. If it is non zero
        // then interrupt_end will do nothing useful, so avoid calling
        // it.
        if( cyg_scheduler_sched_lock == 0 )
            pendsvc = true;
    }

    // Post the pendable SVC if required.
    if( pendsvc )
    {
        cyg_uint32 icsr;
        HAL_READ_UINT32( CYGARC_REG_NVIC_BASE+CYGARC_REG_NVIC_ICSR, icsr );
        icsr |= CYGARC_REG_NVIC_ICSR_PENDSVSET;
        HAL_WRITE_UINT32( CYGARC_REG_NVIC_BASE+CYGARC_REG_NVIC_ICSR, icsr );
    }
}      

(2)中断号作为中断分发函数的入参传入。

(8)以中断号作为索引读取ISR函数指针,ISR函数指针是在驱动程序通过调用cyg_drv_interrupt_attach来设置的。

(11)调用ISR,ISR入参为中断向量号以及用户数据,多个中断源可能会使用相同的ISR,根据中断向量号或用户数据可以判断到底是哪一个中断。

(15)判断ISR返回值,看是否需要调用DSR。

(17)如果需要调用DSR,那么调用cyg_interrupt_post_dsr将当前中断对应的DSR追加到DSR列表。

(23)判断当前线程是否对调度器进行加锁,如果未加锁,那么允许执行DSR。

(28-34)如果需要执行DSR,那么将ICSR寄存器PENDSVSET置位,这将挂起一个PendSV请求,因为PendSV的优先级被初始化为最低优先级,因此不会对当前中断处理进行抢占,只有中断向量服务函数返回后才会执行PendSV。

疑问:(31-33)读取ICSR寄存器修改后写回,这个过程没有进行关中断保护,如果在(31)读取ICSR后产生了更高优先级的中断,那么将会抢占当前中断处理进入嵌套中断处理流程执行更高优先级的中断服务例程,当高优先级中断服务例程返回后,ICSR寄存器的值与icsr临时变量存储的值还保持一致吗?

hal_pendable_svc_vsr

eCos中ISR的优先级要绝对高于DSR且不可配置,DSR仅在ISR执行完成后执行,如果产生了嵌套中断,那么要等所有ISR全部执行完成后再执行DSR。Cortex-M的中断控制器与核心紧密结合,只有执行了hal_default_interrupt_vsr最后一条指令(pop {pc})后,中断处理才算完成。当前中断处理完成后,要么返回被中断线程,要么开始进入低优先级的中断服务例程。eCos为了保证ISR的优先级绝对高于DSR,且在ISR完成后有机会调用DSR,使用了Cortex-M的PendSV机制,在PendSV处理DSR相关请求。PendSV优先级被设置成最低优先级,如果当前有ISR正在执行,由于PendSV的优先级最低,不可能抢占ISR,因此可以保证ISR优先级绝对高于DSR,即使当前中断同样是最低优先级,由于Cortex-M不支持同优先级的抢占,因此也会保证在ISR执行完后再处理DSR。

中断向量服务例程完成后,通过Cortex-M的咬尾中断机制进入PendSV向量服务例程。

// hal/cortexm/arch/<v>/src/vectors.S:257
        .type   hal_pendable_svc_vsr, %function
hal_pendable_svc_vsr:

        mrs     r12,psp                 // R12 = thread's PSP
        sub     r0,r12,#HAL_SAVEDREG_AUTO_FRAME_SIZE            // Make space for frame
        msr     psp,r0                  // Put it back

        ldr     r3,=0x01000000          // R3 = PSR = thumb bit set
        ldr     r2,=hal_interrupt_end   // R2 = PC = interrupt end entry point
        ldr     r1,=hal_interrupt_end_done // R1 = LR = restore code
        stmfd   r12!,{r0-r3}            // Save fake R12, LR, PC, PSR
        stmfd   r12!,{r0-r3}            // Save fake R0-R3

        bx      lr                      // Return to hal_interrupt_end      

(5)读取线程堆栈寄存器,eCos中线程使用PSP为堆栈,PendSV使用MSP最为堆栈,因此通过MRS指令读取PSP寄存器。

(6)为后面的假异常堆栈帧预留空间。

(7)将修改后的堆栈写回PSP寄存器。

(9-13)构造假异常堆栈帧,假异常堆栈帧的PSR=0x01000000,PC=hal_interrupt_end,LR=hal_interrupt_end_done,其他寄存器值的内容无关紧要,但是必须按照Cortex-M异常响应时自动压栈的次序压入。

(15)从PendSV返回,Cortex-M执行到这条语句时,将自动从PSP堆栈中恢复R0-R3,R12,LR,PC,PSR,因此当返回后,PC=hal_interrupt_end,LR=hal_interrupt_end_done,也就是说并不是返回当前线程的被中断点,而是返回到了hal_interrupt_end函数,而且从hal_interrupt_end返回后还不是线程被中断点,而是返回hal_interrupt_end_done。eCos通过构造假异常堆栈帧的手段在线程环境环境下调用部分中断处理相关代码。

hal_interrupt_end

hal_interrupt_end的主要作用是调用interrupt_end函数,interrupt_end属于eCos内核的中断处理内容。

// hal/cortexm/arch/<v>/src/hal_misc.c:430
__externC void hal_interrupt_end( void )
{
#ifdef CYGFUN_HAL_COMMON_KERNEL_SUPPORT
    cyg_scheduler_sched_lock++;
#endif

   interrupt_end(0,0,0);
}      

(5)调度器锁进行加锁,在interrupt_end将对调度器解锁,这里的加锁是为了与interrupt_end中的解锁配套。cyg_scheduler_sched_lock是Cyg_Scheduler_SchedLock::sched_lock的符号别名,在kernel/<version>/include/smp.hxx:414定义二者的别名关系。

(8)调用interrupt_end,interrupt_end是属于内核中断处理的内容,interrupt_end函数的关键是调用Cyg_Scheduler::unlock函数,Cyg_Scheduler::unlock调用Cyg_Scheduler::unlock_inner,Cyg_Scheduler::unlock_inner调用Cyg_Interrupt::call_pending_DSRs,Cyg_Interrupt::call_pending_DSRs最终调用DSR。

Cyg_Interrupt::call_pending_DSRs是通过HAL宏来调用DSR的,因为eCos需要在中断堆栈中调用DSR,而堆栈切换是与硬件息息相关的,因此必须通过HAL宏来实现,该宏为HAL_INTERRUPT_STACK_CALL_PENDING_DSRS,在Cortex-M架构中,该宏定义如下

// hal/cortexm/arch/<v>/include/hal_intr.h:303
#define HAL_INTERRUPT_STACK_CALL_PENDING_DSRS()         \
{                                                       \
    __asm__ volatile (                                  \
        "ldr     r3,=hal_call_dsrs_vsr          \n"     \
        "swi 0                                  \n"     \
        :                                               \
        :                                               \
        : "r3"                                          \
        );                                              \
}      

(5)将SVC系统调用服务需要调用的函数指针通过R3传入,R0-R2参数不使用。

(6)使用swi指令触发系统调用服务。

hal_default_svc_vsr

eCos中断响应过程包括了2次SVC系统调用服务,因此eCos定义了一个通用的SVC向量服务例程来根据传入的参数调用响应的函数,当执行到swi指令时将进入SVC向量服务例程响应SVC系统调用服务。

// hal/cortexm/arch/<v>/src/vectors.S:348
        .type   hal_default_svc_vsr, %function
hal_default_svc_vsr:

        mrs     r12,psp
        ldmfd   r12,{r0-r3}
        bx      r3                      // Jump to routine in R3      

(5-6)从PSP堆栈取参数。R0-R3是在进入SVC向量服务例程时由Cortex-M自动压入PSP堆栈的。

(7)R3存储的是被调用函数指针,跳转到该函数。

hal_call_dsrs_vsr

// hal/cortexm/arch/<v>/src/vectors.S:326
        .type   hal_call_dsrs_vsr, %function
hal_call_dsrs_vsr:
        .extern cyg_interrupt_call_pending_DSRs
        b       cyg_interrupt_call_pending_DSRs      

(5)直接跳转到cyg_interrupt_call_pending_DSRs,cyg_interrupt_call_pending_DSRs在kernel/<version>/src/intr/intr.cxx:233中定义,cyg_interrupt_call_pending_DSRs的作用是调用DSR。

hal_interrupt_end_done

DSR调用完成后,从SVC向量服务例程返回到线程环境,然后沿着函数调用路径退回到hal_interrupt_end,从hal_interrupt_end返回时返回到hal_interrupt_end_done,这个返回过程是在构造假异常堆栈帧时设定的。

// hal/cortexm/arch/<v>/src/vectors.S:289
        .type   hal_interrupt_end_done, %function
hal_interrupt_end_done:
        ldr  r3,=hal_interrupt_end_vsr
        swi  0      

(4)将hal_interrupt_end_vsr函数地址保存到R3,SVC系统调用将通过R3调用hal_interrupt_end_vsr。

(5)触发SVC系统调用。

hal_interrupt_end_vsr

hal_interrupt_end_vsr的作用是撤销假异常堆栈帧,恢复线程在中断自动压栈后的状态。

// hal/cortexm/arch/<v>/src/vectors.S:306
        .type   hal_interrupt_end_vsr, %function
hal_interrupt_end_vsr:
        mrs     r12,psp                 // R12 = thread's PSP
        add     r12,#HAL_SAVEDREG_AUTO_FRAME_SIZE                 // Skip our saved state
        msr     psp,r12                 // Restore thread's PSP

        bx      lr                      // And return      

(4)读取线程堆栈PSP。

继续阅读