之前讲清楚了中断向量表,rtems中只实现了sp0和spx的irq中断。那么本文基于timer中断的入口handle来讲解中断是如何触发的
操作系统的ticker是基于硬件timer实现的,为了了解中断触发,timer是非常合适的一个例子,系统启动后,记录的每一个tick值都是一次timer的定时到期。
在arm64中,timer默认是读取cntfrq_el0寄存器,如下
其代码实现如下
void arm_generic_timer_get_config( uint32_t *frequency, uint32_t *irq ) { uint64_t val; __asm__ volatile ( "mrs %[val], cntfrq_el0" : [val] "=&r" (val) ); *frequency = val; #ifdef ARM_GENERIC_TIMER_USE_VIRTUAL *irq = BSP_TIMER_VIRT_PPI; #elif defined(AARCH64_GENERIC_TIMER_USE_PHYSICAL_SECURE) *irq = BSP_TIMER_PHYS_S_PPI; #else *irq = BSP_TIMER_PHYS_NS_PPI; #endif }
我们知道默认情况下使用的是no-secure el0,所以中断号设置是30,如下
#define BSP_TIMER_PHYS_NS_PPI 30
对于timer的使用,主要如下几个步骤
至此timer会倒计时发生中断,然后就是中断触发了,对于中断触发,其流程如下
关于中断等下会讲,现在先简单把timer说清楚
关于设置cntp_ctl,代码如下
void arm_gt_clock_set_control(uint32_t ctl) { __asm__ volatile ( #ifdef AARCH64_GENERIC_TIMER_USE_VIRTUAL "msr cntv_ctl_el0, %[ctl]" #elif defined(AARCH64_GENERIC_TIMER_USE_PHYSICAL_SECURE) "msr cntps_ctl_el1, %[ctl]" #else "msr cntp_ctl_el0, %[ctl]" #endif : : [ctl] "r" (ctl) ); }
关于设置timer的计数值,代码如下
void arm_gt_clock_set_compare_value(uint64_t cval) { __asm__ volatile ( #ifdef AARCH64_GENERIC_TIMER_USE_VIRTUAL "msr cntv_cval_el0, %[cval]" #elif defined(AARCH64_GENERIC_TIMER_USE_PHYSICAL_SECURE) "msr cntps_cval_el1, %[cval]" #else "msr cntp_cval_el0, %[cval]" #endif : : [cval] "r" (cval) ); }
至此,我们先简单把timer说清楚了。
对于一个中断,我们需要填充其ISR才能正常工作,所以为了让中断触发能够跳到自己设置的ISR,我们需要注册中断,RTEMS中注册中断的方式如下
static void arm_gt_clock_handler_install(rtems_interrupt_handler handler) { rtems_status_code sc; rtems_interrupt_entry_initialize( &arm_gt_interrupt_entry, handler, &arm_gt_clock_instance, "Clock" ); sc = rtems_interrupt_entry_install( arm_gt_clock_instance.irq, RTEMS_INTERRUPT_UNIQUE, &arm_gt_interrupt_entry ); if (sc != RTEMS_SUCCESSFUL) { bsp_fatal(BSP_ARM_FATAL_GENERIC_TIMER_CLOCK_IRQ_INSTALL); } }
这里的install动作我们关注如下调用
rtems_interrupt_entry_install bsp_interrupt_entry_install bsp_interrupt_entry_install_first
此函数的代码如下
static rtems_status_code bsp_interrupt_entry_install_first( rtems_vector_number vector, rtems_option options, rtems_interrupt_entry *entry ) { rtems_vector_number index; index = vector; bsp_interrupt_entry_store_release( bsp_interrupt_get_dispatch_table_slot( index ), entry ); bsp_interrupt_set_handler_unique( index, RTEMS_INTERRUPT_IS_UNIQUE( options ) ); bsp_interrupt_vector_enable( vector ); return RTEMS_SUCCESSFUL; }
可以发现其做了如下几个事情
至此,通过上述操作,中断会注册到向量表上,如下
&_Record_Interrupt_dispatch_table[ 30 ]; bsp_interrupt_dispatch_table[ 30 ]
上面两个地址是相等的
bsp_interrupt_dispatch_table[ i ] = &_Record_Interrupt_entry_table[ i ];
此时当中断发生时,可以通过此表找到对应的ISR。
对于timer中断,其通过curr_el_spx_irq触发,之前提过中断向量表了,这里我们只关心行为,那么首先要做的是JUMP_HANDLER
.macro JUMP_HANDLER /* Mask to use in BIC, lower 7 bits */ mov x0, #0x7f /* LR contains PC, mask off to the base of the current vector */ bic x0, lr, x0 /* Load address from the last word in the vector */ ldr x0, [x0, #0x78] /* * Branch and link to the address in x0. There is no reason to save the current * LR since it has already been saved and the current contents are junk. */ blr x0 /* Pop x0,lr from stack */ ldp x0, lr, [sp], #0x10 /* Return from exception */ eret nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop .endm
进入中断处理程序的代码是
ldr x0, [x0, #0x78]
根据RTEMS的中断管理之中断向量表
的分析,其入口函数是_AArch64_Exception_interrupt_no_nest
。
对于_AArch64_Exception_interrupt_no_nest
的代码,其实现如下
_AArch64_Exception_interrupt_no_nest: /* Execution template: Save volatile registers on thread stack(some x, all q, ELR, etc.) Switch to interrupt stack Execute interrupt handler Switch to thread stack Call thread dispatch Restore volatile registers from thread stack Return to embedded exception vector code */ /* Push interrupt context */ push_interrupt_context /* * Switch to interrupt stack, interrupt dispatch may enable interrupts causing * nesting */ msr spsel, #0 /* Jump into the handler */ bl .AArch64_Interrupt_Handler /* * Switch back to thread stack, interrupt dispatch should disable interrupts * before returning */ msr spsel, #1 /* * Check thread dispatch necessary, ISR dispatch disable and thread dispatch * disable level. */ cmp x0, #0 bne .Lno_need_thread_dispatch bl _AArch64_Exception_thread_dispatch .Lno_need_thread_dispatch: /* * SP should be where it was pre-handler (pointing at the exception frame) * or something has leaked stack space */ /* Pop interrupt context */ pop_interrupt_context /* Return to vector for final cleanup */ ret
可以看的保存中断上下文代码是push_interrupt_context
,那么其实现如下
.macro push_interrupt_context /* * Push x1-x21 on to the stack, need 19-21 because they're modified without * obeying PCS */ stp lr, x1, [sp, #-0x10]! stp x2, x3, [sp, #-0x10]! stp x4, x5, [sp, #-0x10]! stp x6, x7, [sp, #-0x10]! stp x8, x9, [sp, #-0x10]! stp x10, x11, [sp, #-0x10]! stp x12, x13, [sp, #-0x10]! stp x14, x15, [sp, #-0x10]! stp x16, x17, [sp, #-0x10]! stp x18, x19, [sp, #-0x10]! stp x20, x21, [sp, #-0x10]! /* * Push q0-q31 on to the stack, need everything because parts of every register * are volatile/corruptible */ stp q0, q1, [sp, #-0x20]! stp q2, q3, [sp, #-0x20]! stp q4, q5, [sp, #-0x20]! stp q6, q7, [sp, #-0x20]! stp q8, q9, [sp, #-0x20]! stp q10, q11, [sp, #-0x20]! stp q12, q13, [sp, #-0x20]! stp q14, q15, [sp, #-0x20]! stp q16, q17, [sp, #-0x20]! stp q18, q19, [sp, #-0x20]! stp q20, q21, [sp, #-0x20]! stp q22, q23, [sp, #-0x20]! stp q24, q25, [sp, #-0x20]! stp q26, q27, [sp, #-0x20]! stp q28, q29, [sp, #-0x20]! stp q30, q31, [sp, #-0x20]! /* Get exception LR for PC and spsr */ mrs x0, ELR_EL1 mrs x1, SPSR_EL1 /* Push pc and spsr */ stp x0, x1, [sp, #-0x10]! /* Get fpcr and fpsr */ mrs x0, FPSR mrs x1, FPCR /* Push fpcr and fpsr */ stp x0, x1, [sp, #-0x10]! .endm
可以看的,其操作如下
将spsel设置为0,这样栈跳到sp_el0上,然后执行AArch64_Interrupt_Handler
msr spsel, #0 bl .AArch64_Interrupt_Handler
我们先看AArch64_Interrupt_Handler做了哪些事情
.AArch64_Interrupt_Handler: /* Get per-CPU control of current processor */ GET_SELF_CPU_CONTROL SELF_CPU_CONTROL_GET_REG /* Increment interrupt nest and thread dispatch disable level */ ldr w2, [SELF_CPU_CONTROL, #PER_CPU_ISR_NEST_LEVEL] ldr w3, [SELF_CPU_CONTROL, #PER_CPU_THREAD_DISPATCH_DISABLE_LEVEL] add w2, w2, #1 add w3, w3, #1 str w2, [SELF_CPU_CONTROL, #PER_CPU_ISR_NEST_LEVEL] str w3, [SELF_CPU_CONTROL, #PER_CPU_THREAD_DISPATCH_DISABLE_LEVEL] /* Save LR */ mov x21, LR /* Call BSP dependent interrupt dispatcher */ bl bsp_interrupt_dispatch /* Restore LR */ mov LR, x21 /* Load some per-CPU variables */ ldr w0, [SELF_CPU_CONTROL, #PER_CPU_THREAD_DISPATCH_DISABLE_LEVEL] ldrb w1, [SELF_CPU_CONTROL, #PER_CPU_DISPATCH_NEEDED] ldr w2, [SELF_CPU_CONTROL, #PER_CPU_ISR_DISPATCH_DISABLE] ldr w3, [SELF_CPU_CONTROL, #PER_CPU_ISR_NEST_LEVEL] /* Decrement levels and determine thread dispatch state */ eor w1, w1, w0 sub w0, w0, #1 orr w1, w1, w0 orr w1, w1, w2 sub w3, w3, #1 /* Store thread dispatch disable and ISR nest levels */ str w0, [SELF_CPU_CONTROL, #PER_CPU_THREAD_DISPATCH_DISABLE_LEVEL] str w3, [SELF_CPU_CONTROL, #PER_CPU_ISR_NEST_LEVEL] /* Return should_skip_thread_dispatch in x0 */ mov x0, x1 /* Return from handler */ ret
主要做了如下事情,代码有注释,也很好理解
percpu结构体的四个变量如下
/** * This contains the current interrupt nesting level on this * CPU. */ uint32_t isr_nest_level; /** * @brief Indicates if an ISR thread dispatch is disabled. * * This flag is context switched with each thread. It indicates that this * thread has an interrupt stack frame on its stack. By using this flag, we * can avoid nesting more interrupt dispatching attempts on a previously * interrupted thread's stack. */ uint32_t isr_dispatch_disable; /** * @brief The thread dispatch critical section nesting counter which is used * to prevent context switches at inopportune moments. */ volatile uint32_t thread_dispatch_disable_level; /** * @brief This is set to true when this processor needs to run the thread * dispatcher. * * It is volatile since interrupts may alter this flag. * * This member is not protected by a lock and must be accessed only by this * processor. Code (e.g. scheduler and post-switch action requests) running * on another processors must use an inter-processor interrupt to set the * thread dispatch necessary indicator to true. * * @see _Thread_Get_heir_and_make_it_executing(). */ volatile bool dispatch_necessary;
根据上面的解析,此时中断会调整到bsp_interrupt_dispatch
中,然后寻找ISR程序运行,bsp_interrupt_dispatch
代码如下
void bsp_interrupt_dispatch(void) { while (true) { uint32_t icciar = READ_SR(ICC_IAR1); rtems_vector_number vector = GIC_CPUIF_ICCIAR_ACKINTID_GET(icciar); uint32_t status; if (!bsp_interrupt_is_valid_vector(vector)) { break; } status = arm_interrupt_enable_interrupts(); bsp_interrupt_handler_dispatch_unchecked(vector); arm_interrupt_restore_interrupts(status); WRITE_SR(ICC_EOIR1, icciar); } }
可以看的上述代码,它步骤如下
对于执行函数,如下
static inline void bsp_interrupt_dispatch_entries( const rtems_interrupt_entry *entry ) { do { ( *entry->handler )( entry->arg ); entry = bsp_interrupt_entry_load_acquire( &entry->next ); } while ( RTEMS_PREDICT_FALSE( entry != NULL ) ); }
值得注意的是,我们这里的handler就是之前注册的timer中断
Clock_driver_support_install_isr( Clock_isr );
也就是Clock_isr函数,这个函数也就是计算tick后重新激活timer。这里主要关心中断触发,接下来看中断返回
在AArch64_Interrupt_Handler
中,就介绍了中断返回的部分代码,这里简单复述一下
在AArch64_Interrupt_Handler
返回之后,还是回到中断向量表指定函数_AArch64_Exception_interrupt_no_nest
中了,这里继续做剩下的操作,如下
AArch64_Interrupt_Handler
返回之后,就直接恢复中断上下文,恢复的步骤就是将之前保存的寄存器恢复,函数是pop_interrupt_context
其实现如下
.macro pop_interrupt_context /* Pop fpcr and fpsr */ ldp x0, x1, [sp], #0x10 /* Restore fpcr and fpsr */ msr FPCR, x1 msr FPSR, x0 /* Pop pc and spsr */ ldp x0, x1, [sp], #0x10 /* Restore exception LR for PC and spsr */ msr SPSR_EL1, x1 msr ELR_EL1, x0 /* Pop q0-q31 */ ldp q30, q31, [sp], #0x20 ldp q28, q29, [sp], #0x20 ldp q26, q27, [sp], #0x20 ldp q24, q25, [sp], #0x20 ldp q22, q23, [sp], #0x20 ldp q20, q21, [sp], #0x20 ldp q18, q19, [sp], #0x20 ldp q16, q17, [sp], #0x20 ldp q14, q15, [sp], #0x20 ldp q12, q13, [sp], #0x20 ldp q10, q11, [sp], #0x20 ldp q8, q9, [sp], #0x20 ldp q6, q7, [sp], #0x20 ldp q4, q5, [sp], #0x20 ldp q2, q3, [sp], #0x20 ldp q0, q1, [sp], #0x20 /* Pop x1-x21 */ ldp x20, x21, [sp], #0x10 ldp x18, x19, [sp], #0x10 ldp x16, x17, [sp], #0x10 ldp x14, x15, [sp], #0x10 ldp x12, x13, [sp], #0x10 ldp x10, x11, [sp], #0x10 ldp x8, x9, [sp], #0x10 ldp x6, x7, [sp], #0x10 ldp x4, x5, [sp], #0x10 ldp x2, x3, [sp], #0x10 ldp lr, x1, [sp], #0x10 /* Must clear reservations here to ensure consistency with atomic operations */ clrex .endm
保存和恢复的步骤是差不多的,其步骤简单介绍如下
等到AArch64_Interrupt_Handler
返回之后,代码回到宏JUMP_HANDLER上,我们之前代码执行的blr,那么接下来做如下动作
blr x0 /* Pop x0,lr from stack */ ldp x0, lr, [sp], #0x10 /* Return from exception */ eret
这里从sp开始,去16个字节保存到x0和lr寄存器上,然后调用eret返回到中断前的状态,恢复ELR_EL1和SPSR_EL1寄存器。这里因为JUMP_HANDLER是宏,所以继续回溯到curr_el_spx_irq函数中来看ldp这条指令,如下
curr_el_spx_irq: stp x0, lr, [sp, #-0x10]! /* Push x0,lr on to the stack */ bl curr_el_spx_irq_get_pc /* Get current execution address */ curr_el_spx_irq_get_pc: /* The current PC is now in LR */ JUMP_HANDLER JUMP_TARGET_SPx .balign 0x80
可以看的,之前stp其实讲x0和lr的值保存在sp-0x10的位置,并修改了sp的值,所以eret之前做的就是恢复进入中断前的x0和lr寄存器。
至此,一次timer中断就完成返回到了中断开始之前的状态。
主动调用线程调度的函数是_AArch64_Exception_thread_dispatch
代码如下
_AArch64_Exception_thread_dispatch: /* Get per-CPU control of current processor */ GET_SELF_CPU_CONTROL SELF_CPU_CONTROL_GET_REG /* Thread dispatch */ mrs NON_VOLATILE_SCRATCH, DAIF .Ldo_thread_dispatch: /* Set ISR dispatch disable and thread dispatch disable level to one */ mov w0, #1 str w0, [SELF_CPU_CONTROL, #PER_CPU_ISR_DISPATCH_DISABLE] str w0, [SELF_CPU_CONTROL, #PER_CPU_THREAD_DISPATCH_DISABLE_LEVEL] /* Save LR */ mov x21, LR /* Call _Thread_Do_dispatch(), this function will enable interrupts */ mov x0, SELF_CPU_CONTROL mov x1, NON_VOLATILE_SCRATCH mov x2, #0x80 bic x1, x1, x2 bl _Thread_Do_dispatch /* Restore LR */ mov LR, x21 /* Disable interrupts */ msr DAIF, NON_VOLATILE_SCRATCH #ifdef RTEMS_SMP GET_SELF_CPU_CONTROL SELF_CPU_CONTROL_GET_REG #endif /* Check if we have to do the thread dispatch again */ ldrb w0, [SELF_CPU_CONTROL, #PER_CPU_DISPATCH_NEEDED] cmp w0, #0 bne .Ldo_thread_dispatch /* We are done with thread dispatching */ mov w0, #0 str w0, [SELF_CPU_CONTROL, #PER_CPU_ISR_DISPATCH_DISABLE] /* Return from thread dispatch */ ret
这里好像没什么好详细讲的了,其实就是主动调用_Thread_Do_dispatch
,主动让调度器开始下一个高优先级任务。
至此,本文详细的通过timer中断介绍了中断触发的详细过程。有助于了解RTEMS在aarch64上,以及gic-v3的中断管理流程