编辑
2025-04-30
记录知识
0
请注意,本文编写于 58 天前,最后修改于 39 天前,其中某些信息可能已经过时。

目录

什么是timer
注册中断
中断发生
保存中断上下文
切换到中断栈
进入ISR程序
中断返回
返回中断栈
恢复中断上下文
主动调用线程调度
总结

之前讲清楚了中断向量表,rtems中只实现了sp0和spx的irq中断。那么本文基于timer中断的入口handle来讲解中断是如何触发的

什么是timer

操作系统的ticker是基于硬件timer实现的,为了了解中断触发,timer是非常合适的一个例子,系统启动后,记录的每一个tick值都是一次timer的定时到期。
在arm64中,timer默认是读取cntfrq_el0寄存器,如下

image.png

其代码实现如下

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的使用,主要如下几个步骤

  1. 设置cntp_ctl 的enable 为1
  2. 设置cntp_cval 一个初值

至此timer会倒计时发生中断,然后就是中断触发了,对于中断触发,其流程如下

  1. 中断发生,进入irq
  2. 保存中断上下文
  3. 读取GIC控制器,确定ISR程序
  4. 进入中断ISR程序
  5. 中断返回
  6. 恢复中断上下文
  7. 返回中断现场

关于中断等下会讲,现在先简单把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; }

可以发现其做了如下几个事情

  1. 原子的保持中断入口
  2. 用位图标记是否中断共享
  3. 在gic-v3中设置redistributor使能

至此,通过上述操作,中断会注册到向量表上,如下

&_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

可以看的,其操作如下

  • 将x1-x21保存在sp位置往前推的位置 (前变基)
  • 保存q0-q31
  • 保存ELR和SPSR
  • 保存FPSR和FPCR

切换到中断栈

将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的结构体
  • 对中断嵌套计数器(PER_CPU_ISR_NEST_LEVEL)和线程调度等级(PER_CPU_THREAD_DISPATCH_DISABLE_LEVEL)自加
  • 保存LR寄存器到x21
  • 进入中断分发函数bsp_interrupt_dispatch
  • 从x21中恢复LR寄存器
  • 从percpu结构体中加载thread_dispatch_disable_level/dispatch_necessary/isr_dispatch_disable/isr_nest_level
  • 自减1操作后存储到percpu变量
  • 将dispatch_necessary赋值给x0,作为函数的返回值

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;

进入ISR程序

根据上面的解析,此时中断会调整到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); } }

可以看的上述代码,它步骤如下

  1. 读取了ICC_IAR1获得了中断号
  2. 然后打开全局中断
  3. 从中断号和bsp_interrupt_dispatch_table计算出ISR函数地址
  4. 执行ISR

对于执行函数,如下

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中,就介绍了中断返回的部分代码,这里简单复述一下

  1. 恢复LR寄存器
  2. 设置percpu结构体

返回中断栈

AArch64_Interrupt_Handler返回之后,还是回到中断向量表指定函数_AArch64_Exception_interrupt_no_nest中了,这里继续做剩下的操作,如下

  1. 将spsel设置为1,切回sp_el1栈
  2. 在AArch64_Interrupt_Handler会返回dispatch_necessary,这里判断x0的值是否为0
  3. 如果为0,则恢复中断上下文
  4. 如果为1,则进入主动调用线程调度,这里最后讲
  5. 最后执行恢复中断上下文

恢复中断上下文

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

保存和恢复的步骤是差不多的,其步骤简单介绍如下

  • 恢复浮点相关寄存器
  • 恢复spsr和elr
  • 恢复x1-x21寄存器
  • 清空独占监视器

等到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的中断管理流程