• linux驱动移植中断子系统执行流程


    上一篇博客在最后,我们大致介绍了一下中断子系统的执行流程,这一节我们将从Linux源码层面去中断是如何原型。

    一、裸机中断

    我们首先回忆一下裸机程序中的中断流程是怎样的,以Mini2440按键K1外部中断为例

    1、使能外部,开启外部中断EINTMASK 、中断源INTMSK 、开启IRQ总中断;

    2、按键按下,触发中断源EINT8_23、CPU接收到中断信号;

    3、CPU跳转到中断向量入口执行(0x18中断地址处);

    • 使用stmdb将寄存器值保存在栈顶(保护现场)
    • 执行中断处理函数
    • 使用ldmia将栈顶处数据读出到寄存器中,并使pc=lr(恢复现场)

    二、Linux中的中断处理

    这一节我们将分析linux中CPU跳转到中断向量入口执行之后的流程:

    • 异常向量表0xFFFF0018中断地址处定义的vector_irq;
    • irq_handler中断处理函数的执行,在该中断处理函数中会根据IRQ编号找到绑定的我们自己的中断处理函数;

    关于中断的注册(使能)、卸载(关闭)、以及每一个中断和对应中断处理函数的绑定会在后面小节一一介绍。

    2.1 高端异常向量模式

    实际上,ARM的异常向量表有两种选择,一种是低端向量,向量地址位于0x00000000,另一种是高端地址,向量地址0xffff0000,linux中选择使用高端向量模式,也就是说,当异常发生时,CPU会把PC指针自动跳转到始于0xffff0000开始的某一个地址上:

    地址异常模式
    FFFF0000 复位 管理模式(linux内核运行模式)
    FFFF0004 未定义指令 未定义模式
    FFFF0008 软中断(swi) 管理模式
    FFFF000C Prefetch abort 终止模式
    FFFF0010 Data abort 终止模式
    FFFF0014 保留 保留
    FFFF0018 IRQ 中断模式
    FFFF001C FIQ 快中断模式

    2.2 异常向量表

    异常向量表在arch/arm/kernel/entry-armv.S中定义,为了方便讨论,下面只列出部分关键的代码:

    .L__vectors_start:
            W(b)    vector_rst
            W(b)    vector_und
            W(ldr)  pc, .L__vectors_start + 0x1000
            W(b)    vector_pabt
            W(b)    vector_dabt
            W(b)    vector_addrexcptn
            W(b)    vector_irq
            W(b)    vector_fiq

    __vectors_start是向量表基地址,在arch/arm/kernel/vmlinux.lds中定义了__vectors_start的链接地址为0xffff0000:

     __vectors_start = .; 
    .vectors 0xffff0000 : AT(__vectors_start) { 
        *(.vectors) 
    } 
    . = __vectors_start + SIZEOF(.vectors);
     __vectors_end = .;
    __stubs_start = .; 
    .stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) { 
        *(.stubs) 
    } 
    . = __stubs_start + SIZEOF(.stubs); 
    __stubs_end = .; 

    这里定义两个段:

    .vectors段:起始地址为__vectors_start(0xffff0000),结束地址为__vectors_end;

    .stubs段:起始地址为__stubs_start(0xffff0000+0x1000) ,结束地址为__stubs_end ;

    在链接脚本中定义的变量是可以直接在汇编代码中使用的,比如这里的__vectors_start、__vectors_end 等。

    2.3 vector_sub宏

    异常向量表中的vector_rst、vector_und等是通过vector_stub宏定义的:

    /*
     * Vector stubs.
     *
     * This code is copied to 0xffff1000 so we can use branches in the
     * vectors, rather than ldr's.  Note that this code must not exceed
     * a page size.
     *
     * Common stub entry macro:
     *   Enter in IRQ mode, spsr = SVC/USR CPSR, lr = SVC/USR PC
     *
     * SP points to a minimal amount of processor-private memory, the address
     * of which is copied into r0 for the mode specific abort handler.
     */
            .macro  vector_stub, name, mode, correction=0
            .align  5
    
    vector_\name:                                                   @定义不同的宏 比如vector_irq
            .if \correction                                         @判断correction是否为0      
            sub     lr, lr, #\correction                            @计算返回地址
            .endif
    
            @
            @ Save r0, lr_<exception> (parent PC) and spsr_<exception>
            @ (parent CPSR)
            @
            stmia   sp, {r0, lr}            @ save r0, lr
            mrs     lr, spsr
            str     lr, [sp, #8]            @ save spsr
    
            @
            @ Prepare for SVC32 mode.  IRQs remain disabled.
            @
            mrs     r0, cpsr
            eor     r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
            msr     spsr_cxsf, r0
    
            @
            @ the branch table must immediately follow this code
            @
            and     lr, lr, #0x0f
     THUMB( adr     r0, 1f                  )
     THUMB( ldr     lr, [r0, lr, lsl #2]    )
            mov     r0, sp
     ARM(   ldr     lr, [pc, lr, lsl #2]    )
            movs    pc, lr                  @ branch to handler in SVC mode
    ENDPROC(vector_\name)
    .align 2
    @ handler address follow this label
    1:
    .endm

    定义带有三个参数的宏vector_stub。比如下面语句:

     vector_stub     irq, IRQ_MODE, 4

    宏展开后就变成了:

    vector_irq:
            sub     lr, lr, 4                     @计算返回地址  lr=宝成的是进入中断前PC的值,就是发生中断所在的指令地址+8  这里减44,也就是正常执行的下一条指令地址
    
            @
            @ Save r0, lr_<exception> (parent PC) and spsr_<exception>
            @ (parent CPSR)
            @
            stmia   sp, {r0, lr}            @ save r0, lr   @保存r0、lr
            mrs     lr, spsr                @ lr=spsr 这里保存的是上一个模式下的cpsr的值
            @在保存现场时,如果处于svc模式下时,cpsr寄存器是写入irq模式下的spsr_irq寄存器,而不是svc模式下的spsr_svc,这样,在中断模式下恢复的话,
            @将spsr_irq寄存器里的内容写入cpsr,就能恢复到svc模式了,因为,spsr_irq寄存器里的内容就是svc模式下的状态
            str     lr, [sp, #8]            @ save spsr     @保存spsr
    
            @
            @ Prepare for SVC32 mode.  IRQs remain disabled.
            @
            mrs     r0, cpsr                                 @r0=cpsr
            eor     r0, r0, #(IRQ_MODE ^ SVC_MODE | PSR_ISETSTATE)  
            msr     spsr_cxsf, r0                            @修改当前模式下的spsr 写回
    
             @
             @ the branch table must immediately follow this code
             @
             and    lr, lr, #0x0f               @lr=lr&0x0f 获取进入irq模式前的cpsr的模式位
             mov  r0, sp
             ldr     lr, [pc, lr, lsl #2]       @如果进入irq前是usr,则lr=*(PC+0<<2),即__irq_usr, 如果进入irq前是svc,lr=*(PC+3<<2),即__irq_svc
    
             movs pc, lr                        @跳转到下面某处,且目标寄存器是pc,指令S结尾,最后会恢复cpsr.
         
             .long __irq_usr                              @  0  (USR_26 / USR_32)
             .long __irq_invalid                          @  1  (FIQ_26 / FIQ_32)
    
             .long __irq_invalid                          @  2  (IRQ_26 / IRQ_32)
             .long __irq_svc                              @  3  (SVC_26 / SVC_32)
             .long __irq_invalid                          @  4
             .long __irq_invalid                          @  5
             .long __irq_invalid                          @  6
             .long __irq_invalid                          @  7
             .long __irq_invalid                          @  8
             .long __irq_invalid                          @  9
             .long __irq_invalid                          @  a
             .long __irq_invalid                          @  b
             .long __irq_invalid                          @  c
             .long __irq_invalid                          @  d
             .long __irq_invalid                          @  e
             .long __irq_invalid                          @  f

    从上面代码中的注释可以看出:

    • 将发生异常前的各个寄存器值保存在SP栈里,若是中断异常,则PC=PC-4,也就是CPU下个要运行的位置处;
    • 然后根据进入中断前的工作模式不同,程序下一步将跳转到_irq_usr 、或__irq_svc等位置;

    执行完后,栈空间信息如下:

    2.4 irq_handler

    对于系统的外部设备来说,通常都是使用IRQ中断,所以我们只关注__irq_usr和__irq_svc,两者的区别是进入和退出中断时是否进行用户栈和内核栈之间的切换,还有进程调度和抢占的处理等。

    __irq_usr、__irq_svc实现均在arch/arm/kernel/entry-armv.S:

    __irq_usr:
            usr_entry        @ 栈中保护中断上下文 r0~r12、sp、lr、pc、cpsr
            kuser_cmpxchg_check
            irq_handler
            get_thread_info tsk
            mov     why, #0
            b       ret_to_user_from_irq
    __irq_svc:
            svc_entry
            irq_handler
    
    #ifdef CONFIG_PREEMPT                           @ 配置了抢占 
            ldr     r8, [tsk, #TI_PREEMPT]          @ get preempt count
            ldr     r0, [tsk, #TI_FLAGS]            @ get flags
            teq     r8, #0                          @ if preempt count != 0
            movne   r0, #0                          @ force flags to 0
            tst     r0, #_TIF_NEED_RESCHED
            blne    svc_preempt
    #endif
    
            svc_exit r5, irq = 1                    @ return from exception

    两个函数最终都会进入irq_handler这个宏:

            .macro  irq_handler
    #ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
            ldr     r1, =handle_arch_irq
            mov     r0, sp
            badr    lr, 9997f
            ldr     pc, [r1]
    #else
            arch_irq_handler_default
    #endif
    9997:
            .endm

    如果选择了CONFIG_GENERIC_MULTI_IRQ_HANDLER配置项,则意味着允许平台的代码可以动态设置irq处理程序,平台代码可以修改全局变量:handle_arch_irq,从而可以修改irq的处理程序。

    这里我们先讨论默认的实现:arch_irq_handler_default。

    2.5 arch_irq_handler_default

    arch_irq_handler_default它位于arch/arm/include/asm/entry-macro-multi.S中:

    /*
     * Interrupt handling.  Preserves r7, r8, r9
     */
            .macro  arch_irq_handler_default
            get_irqnr_preamble r6, lr
    1:      get_irqnr_and_base r0, r2, r6, lr            @获取硬件中断号,r0=硬件中断号
            movne   r1, sp                               #r1=sp (栈中存放的寄存器上下文信息,对应结构体struct pt_regs) 
            @
            @ routine called with r0 = irq number, r1 = struct pt_regs *
            @
            badrne  lr, 1b
            bne     asm_do_IRQ                       
    
    #ifdef CONFIG_SMP                                 @配置了多核
            /*
             * XXX
             *
             * this macro assumes that irqstat (r2) and base (r6) are
             * preserved from get_irqnr_and_base above
             */
            ALT_SMP(test_for_ipi r0, r2, r6, lr)
            ALT_UP_B(9997f)
            movne   r1, sp
            badrne  lr, 1b
            bne     do_IPI
    #endif
    9997:
            .endm

    get_irqnr_preamble和get_irqnr_and_base两个宏由machine级的代码定义,目的就是从中断控制器中获得硬件中断号,紧接着就调用asm_do_IRQ,从这个函数开始,中断程序进入C代码中,传入的参数是硬件中断号和寄存器结构指针,这个函数在arch/arm/kernel/irq.c中实现:

    /*
     * handle_IRQ handles all hardware IRQ's.  Decoded IRQs should
     * not come via this function.  Instead, they should provide their
     * own 'handler'.  Used by platform code implementing C-based 1st
     * level decoding.
     */
    void handle_IRQ(unsigned int irq, struct pt_regs *regs)    // 参数1为中断编号,参数2寄存器结构指针,保存现场时的寄存器信息
    {
            __handle_domain_irq(NULL, irq, false, regs);
    }
    
    /*
     * asm_do_IRQ is the interface to be used from assembly code.
     */
    asmlinkage void __exception_irq_entry
    asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
    {
            handle_IRQ(irq, regs);
    }

    到这里,中断程序完成了从asm代码到C代码的传递,并且获得了引起中断的硬件中断号。

    2.6 __handle_domain_irq

    __handle_domain_irq定义在文件kernel/irq/irqdesc.c中:

    /**
     * __handle_domain_irq - Invoke the handler for a HW irq belonging to a domain
     * @domain:     The domain where to perform the lookup
     * @hwirq:      The HW irq number to convert to a logical one
     * @lookup:     Whether to perform the domain lookup or not
     * @regs:       Register file coming from the low-level handling code
     *
     * Returns:     0 on success, or -EINVAL if conversion has failed
     */
    int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
                            bool lookup, struct pt_regs *regs)
    {
            struct pt_regs *old_regs = set_irq_regs(regs);
            unsigned int irq = hwirq;
            int ret = 0;
    
            irq_enter();                  //  更新一些系统统计信息,禁止内核抢占
    
    #ifdef CONFIG_IRQ_DOMAIN
            if (lookup)
                    irq = irq_find_mapping(domain, hwirq);  // 根据中断域和硬件中断号获取IRQ编号
    #endif
    
            /*
             * Some hardware gives randomly wrong interrupts.  Rather
             * than crashing, do something sensible.
             */
            if (unlikely(!irq || irq >= nr_irqs)) {    // 中断号超出范围
                    ack_bad_irq(irq);
                    ret = -EINVAL;
            } else {
                    generic_handle_irq(irq);       // 重点时这里
            }
    
            irq_exit();               // 退出的时候会检查是否由软中断,如果有。则调用软中断
            set_irq_regs(old_regs);
            return ret;
    }

    在函数内部首先调用set_irq_regs保存被中断的线程的上下文,线程的上下文被保存在old_regs中,以便中断退出之后可以恢复。

    irq_enter()和irq_exit()函数分别用于标记generic handler的进入和退出。

    generic_handle_irq是通用逻辑层提供的API,通过该API,中断的控制被传递到了与体系结构无关的中断流控层:

    int generic_handle_irq(unsigned int irq)
    {
        struct irq_desc *desc = irq_to_desc(irq);   // 通过IRQ编号获取中断描述符
     
        if (!desc)
            return -EINVAL;
        generic_handle_irq_desc(irq, desc);
        return 0;
    }

    最终会进入该irq中断描述符的流控处理回调中:

    static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
    {
        desc->handle_irq(irq, desc);
    }

    handle_irq使用chip结构中的函数清除、屏蔽或者重新使能中断,还要调用用户在action链表中注册的中断处理函数。

    三、内核启动中断初始化

    3.1 start_kernel

    我们之前介绍到u-boot将linux内核加载到内存空间后,会跳转到内核执行,内核执行的起始代码实际上是start_kernel函数。

    • 首先,在setup_arch函数中,early_trap_init被调用,主要进行异常向量表和异常处理函数的的代码重定位工作;
    • 然后,start_kernel发出early_irq_init调用,early_irq_init属于与硬件和平台无关的通用逻辑层,它完成irq_desc结构的内存申请,为它们其中某些字段填充默认值,完成后调用体系相关的arch_early_irq_init函数完成进一步的初始化工作,不过ARM体系没有实现arch_early_irq_init;
    • 接着,start_kernel发出init_IRQ调用,它会直接调用所属板子machine_desc结构体中的init_irq回调。machine_desc通常在板子的特定代码中,使用MACHINE_START和MACHINE_END宏进行定义;
    • machine_desc->init_irq完成对中断控制器的初始化,为每个irq_desc结构安装合适的流控handler,为每个irq_desc结构安装irq_chip指针,使他指向正确的中断控制器所对应的irq_chip结构的实例,同时,如果该平台中的中断线有多路复用(多个中断公用一个irq中断线)的情况,还应该初始化irq_desc中相应的字段和标志,以便实现中断控制器的级联;

    3.1 初始化异常向量表

    early_trap_init位于arch/arm/kernel/traps.c:

    void __init early_trap_init(void *vectors_base)
    {
    #ifndef CONFIG_CPU_V7M
            unsigned long vectors = (unsigned long)vectors_base;
            extern char __stubs_start[], __stubs_end[];
            extern char __vectors_start[], __vectors_end[];
            unsigned i;
    
            vectors_page = vectors_base;
    
            /*
             * Poison the vectors page with an undefined instruction.  This
             * instruction is chosen to be undefined for both ARM and Thumb
             * ISAs.  The Thumb version is an undefined instruction with a
             * branch back to the undefined instruction.
             */
            for (i = 0; i < PAGE_SIZE / sizeof(u32); i++)
                    ((u32 *)vectors_base)[i] = 0xe7fddef1;
    
            /*
             * Copy the vectors, stubs and kuser helpers (in entry-armv.S)
             * into the vector page, mapped at 0xffff0000, and ensure these
             * are visible to the instruction stream.
             */
            memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
            memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start);
    
            kuser_init(vectors_base);
    
            flush_icache_range(vectors, vectors + PAGE_SIZE * 2);
    #else /* ifndef CONFIG_CPU_V7M */
            /*
             * on V7-M there is no need to copy the vector table to a dedicated
             * memory area. The address is configurable and so a table in the kernel
             * image can be used.
             */
    #endif
    }

    以上两个memcpy会把__vectors_start开始的代码拷贝到0xffff0000处,也就是.vectors段的内容,即异常向量表的内容;

    把__stubs_start开始的代码拷贝到0xffff0000+0x1000处,也就是.stubs段的内容,也就是处理跳转的部分;

    这样,异常到来时,CPU就可以正确地跳转到相应异常向量入口并执行他们。

    3.2  基于数组方式分配中断描述符

    early_irq_init位于kernel/irq/irqdesc.c文件:

    int __init early_irq_init(void)
    {
            int count, i, node = first_online_node;
            struct irq_desc *desc;
    
            init_irq_default_affinity();
    
            printk(KERN_INFO "NR_IRQS: %d\n", NR_IRQS);   // 输出IRQ数量
    
            desc = irq_desc;
            count = ARRAY_SIZE(irq_desc);                 // 中断描述符数组大小 
    
            for (i = 0; i < count; i++) {                 // 遍历每一个中断描述符、初始化一些成员
                    desc[i].kstat_irqs = alloc_percpu(unsigned int);  // 分配per-CPU变量
                    alloc_masks(&desc[i], node);
                    raw_spin_lock_init(&desc[i].lock);                   // 自旋锁初始化
                    lockdep_set_class(&desc[i].lock, &irq_desc_lock_class);
                    mutex_init(&desc[i].request_mutex);                  // 互斥锁初始化
                    desc_set_defaults(i, &desc[i], node, NULL, NULL);
            }
            return arch_early_irq_init();   // 抵用arch相关的初始化函数,这里为空
    }

    内核默认采用基于数组的方式来创建irq_desc数组(kernel/irq/irqdesc.c):

    struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {  // 分配irq_desc
        [0 ... NR_IRQS-1] = {
            .handle_irq    = handle_bad_irq,
            .depth        = 1,
            .lock        = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
        }
    };

    还记的之前将linux内核移植到Mini2440开发板启动时的输出信息么:

    NR_IRQS: 111
    S3C2440: IRQ Support
    irq: clearing pending status 00000002

    可以发现NR_IRQS数量为111,定义在arch/arm/mach-s3c24xx/include/mach/irqs.h:

    #define IRQ_S3C2416_I2S1        S3C2416_IRQ(7)        // 7 + 58 + 29 + 16 = 110
    
    #if defined(CONFIG_CPU_S3C2416)     // 24xx系列 定义了这个
    #define NR_IRQS (IRQ_S3C2416_I2S1 + 1)     // 111
    #else
    #define NR_IRQS (IRQ_S3C2443_AC97 + 1)
    #endif

    注意:

    如果配置了CONFIG_SPARSE_IRQ,内核使用基数树(radix tree)来动态分配irq_desc结构:

    int __init early_irq_init(void)
    {
            int i, initcnt, node = first_online_node;
            struct irq_desc *desc;
    
            init_irq_default_affinity();
    
            /* Let arch update nr_irqs and return the nr of preallocated irqs */
            initcnt = arch_probe_nr_irqs();                     // 体系结构相关的代码来决定预先分配的中断描述符的个数
            printk(KERN_INFO "NR_IRQS: %d, nr_irqs: %d, preallocated irqs: %d\n",
                   NR_IRQS, nr_irqs, initcnt);
    
            if (WARN_ON(nr_irqs > IRQ_BITMAP_BITS))
                    nr_irqs = IRQ_BITMAP_BITS;
    
            if (WARN_ON(initcnt > IRQ_BITMAP_BITS))
                    initcnt = IRQ_BITMAP_BITS;
    
            if (initcnt > nr_irqs)       // initctn是需要在初始化的时候预分配的IRQ的个数,nr_irqs是当前系统中IR编号的最大值
                    nr_irqs = initcnt;
    
            for (i = 0; i < initcnt; i++) {
                    desc = alloc_desc(i, node, 0, NULL, NULL);   // 动态分配irq_desc
                    set_bit(i, allocated_irqs);
                    irq_insert_desc(i, desc);
            }
            return arch_early_irq_init();
    }
    View Code

    early_irq_init用于动态申请irq_desc结构的内存空间,具体分配多少个中断描述符通过arch_probe_nr_irqs得到的:

    int __init arch_probe_nr_irqs(void)
    {
        nr_irqs = machine_desc->nr_irqs ? machine_desc->nr_irqs : NR_IRQS;
        return nr_irqs;
    }

    3.3 初始化中断描述符

    init_IRQ在文件arch/arm/kernel/irq.c定义:

    void __init init_IRQ(void)
    {
            int ret;
    
            if (IS_ENABLED(CONFIG_OF) && !machine_desc->init_irq)
                    irqchip_init();
            else
                    machine_desc->init_irq();   // 执行
    
            if (IS_ENABLED(CONFIG_OF) && IS_ENABLED(CONFIG_CACHE_L2X0) &&
                (machine_desc->l2c_aux_mask || machine_desc->l2c_aux_val)) {
                    if (!outer_cache.write_sec)
                            outer_cache.write_sec = machine_desc->l2c_write_sec;
                    ret = l2x0_of_init(machine_desc->l2c_aux_val,
                                       machine_desc->l2c_aux_mask);
                    if (ret && ret != -ENODEV)
                            pr_err("L2C: failed to init: %d\n", ret);
            }
    
            uniphier_cache_init();
    }

    可以看到如果定义了device tree,但是没有定义machine_desc->init_irq,我们这边定义了machine_desc->init_irq。

    因此这里直接调用所属板子machine_desc结构体中的init_irq回调。machine_desc通常在板子的特定代码中,使用MACHINE_START和MACHINE_END宏进行定义。

    比如在arch/arm/mach-s3c24xx/mach-smdk2440.c文件中定义:

    MACHINE_START(S3C2440, "SMDK2440")
            /* Maintainer: Ben Dooks <ben-linux@fluff.org> */
            .atag_offset    = 0x100,
    
            .init_irq       = s3c2440_init_irq,
            .map_io         = smdk2440_map_io,
            .init_machine   = smdk2440_machine_init,
            .init_time      = smdk2440_init_time,
    MACHINE_END

    struct machine_desc ,这个结构体在内核移植中起到相当重要的作用,内核通过machine_desc结构体来控制系统体系架构相关部分的初始化。具体可以参考博客MACHINE_START与MACHINE_END

    由于machine_desc->init_irq初始化为s3c2440_init_irq,定位到文件drivers/irqchip/irq-s3c24xx.c:

    void __init s3c2440_init_irq(void)
    {
            pr_info("S3C2440: IRQ Support\n");
    
    #ifdef CONFIG_FIQ
            init_FIQ(FIQ_START);
    #endif
    
            s3c_intc[0] = s3c24xx_init_intc(NULL, &init_s3c2440base[0], NULL,    // 初始化32个主中断源相关的中断
                                            0x4a000000);
            if (IS_ERR(s3c_intc[0])) {
                    pr_err("irq: could not create main interrupt controller\n");
                    return;
            }
    
            s3c24xx_init_intc(NULL, &init_eint[0], s3c_intc[0], 0x560000a4);      // 初始化外部中断相关的中断、外部中断4~7、8~23分别对应主中断源中的的EINT4~7、EINT8~23
            s3c_intc[1] = s3c24xx_init_intc(NULL, &init_s3c2440subint[0],         // 初始化带有子中断的内部中断相关的中断
                                            s3c_intc[0], 0x4a000018);
    }

    在前已经动态分配了中断描述符,这里将会设置各种中断的触发模式以及中断处理函数等,设置这些内容就是将irq_desc结构数组中的成员初始化。

    我们下面以这行代码为例讲解:

         s3c_intc[0] = s3c24xx_init_intc(NULL, &init_s3c2440base[0], NULL,
                                            0x4a000000);                // 源挂起寄存器  SRCPND

    其中init_s3c2440base、init_s3c2440subint、init_eint这些数组是用来描述S3C2440芯片的60个中断源的,这些中断又分为三类:

    • 主中断32个;
    • 带子中断的内部中断15个;
    • 外部中断24个;
    static struct s3c_irq_data init_s3c2440base[32] = {   // 对应S3C2440的32个主中断源
            { .type = S3C_IRQTYPE_EINT, }, /* EINT0 */
            { .type = S3C_IRQTYPE_EINT, }, /* EINT1 */
            { .type = S3C_IRQTYPE_EINT, }, /* EINT2 */
            { .type = S3C_IRQTYPE_EINT, }, /* EINT3 */
            { .type = S3C_IRQTYPE_LEVEL, }, /* EINT4to7 */
            { .type = S3C_IRQTYPE_LEVEL, }, /* EINT8to23 */
            { .type = S3C_IRQTYPE_LEVEL, }, /* CAM */
            { .type = S3C_IRQTYPE_EDGE, }, /* nBATT_FLT */
            { .type = S3C_IRQTYPE_EDGE, }, /* TICK */
            { .type = S3C_IRQTYPE_LEVEL, }, /* WDT/AC97 */
            { .type = S3C_IRQTYPE_EDGE, }, /* TIMER0 */
            { .type = S3C_IRQTYPE_EDGE, }, /* TIMER1 */
            { .type = S3C_IRQTYPE_EDGE, }, /* TIMER2 */
            { .type = S3C_IRQTYPE_EDGE, }, /* TIMER3 */
            { .type = S3C_IRQTYPE_EDGE, }, /* TIMER4 */
            { .type = S3C_IRQTYPE_LEVEL, }, /* UART2 */
            { .type = S3C_IRQTYPE_EDGE, }, /* LCD */
            { .type = S3C_IRQTYPE_EDGE, }, /* DMA0 */
            { .type = S3C_IRQTYPE_EDGE, }, /* DMA1 */
            { .type = S3C_IRQTYPE_EDGE, }, /* DMA2 */
            { .type = S3C_IRQTYPE_EDGE, }, /* DMA3 */
            { .type = S3C_IRQTYPE_EDGE, }, /* SDI */
            { .type = S3C_IRQTYPE_EDGE, }, /* SPI0 */
            { .type = S3C_IRQTYPE_LEVEL, }, /* UART1 */
            { .type = S3C_IRQTYPE_LEVEL, }, /* NFCON */
            { .type = S3C_IRQTYPE_EDGE, }, /* USBD */
            { .type = S3C_IRQTYPE_EDGE, }, /* USBH */
            { .type = S3C_IRQTYPE_EDGE, }, /* IIC */
            { .type = S3C_IRQTYPE_LEVEL, }, /* UART0 */
            { .type = S3C_IRQTYPE_EDGE, }, /* SPI1 */
            { .type = S3C_IRQTYPE_EDGE, }, /* RTC */
            { .type = S3C_IRQTYPE_LEVEL, }, /* ADCPARENT */
    };
    
    static struct s3c_irq_data init_s3c2440subint[32] = {    // S3C2440带有子中断的内部中断 一共15个中断源   对应6个主中断源
            { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 28 }, /* UART0-RX */
            { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 28 }, /* UART0-TX */
            { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 28 }, /* UART0-ERR */
            { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 23 }, /* UART1-RX */
            { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 23 }, /* UART1-TX */
            { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 23 }, /* UART1-ERR */
            { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 15 }, /* UART2-RX */
            { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 15 }, /* UART2-TX */
            { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 15 }, /* UART2-ERR */
            { .type = S3C_IRQTYPE_EDGE, .parent_irq = 31 }, /* TC */
            { .type = S3C_IRQTYPE_EDGE, .parent_irq = 31 }, /* ADC */
            { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 6 }, /* CAM_C */
            { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 6 }, /* CAM_P */
            { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 9 }, /* WDT */
            { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 9 }, /* AC97 */
    };
    static struct s3c_irq_data __maybe_unused init_eint[32] = {     // 24个外部中断源 对应5个主中断源
            { .type = S3C_IRQTYPE_NONE, }, /* reserved */
            { .type = S3C_IRQTYPE_NONE, }, /* reserved */
            { .type = S3C_IRQTYPE_NONE, }, /* reserved */
            { .type = S3C_IRQTYPE_NONE, }, /* reserved */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 4 }, /* EINT4 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 4 }, /* EINT5 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 4 }, /* EINT6 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 4 }, /* EINT7 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT8 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT9 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT10 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT11 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT12 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT13 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT14 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT15 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT16 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT17 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT18 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT19 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT20 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT21 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT22 */
            { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT23 */
    };
    View Code

    我们顺便看一下sec_irq_data、s3c_irq_init的结构:

    struct s3c_irq_data {        // 用于保存中断源信息
            unsigned int type;
            unsigned long offset;
            unsigned long parent_irq;           // 如果是子中断,这里保存子中断对应的主中断硬件编号
    
            /* data gets filled during init */
            struct s3c_irq_intc *intc;           // 存放中断控制器信息
            unsigned long sub_bits;              // 如果当前是主中断源,并且有多个子中断,那么子中断对应的硬件编号为置 1
            struct s3c_irq_intc *sub_intc;       // 子中断中断控制器信息
    };
    
    /*
     * Structure holding the controller data
     * @reg_pending         register holding pending irqs
     * @reg_intpnd          special register intpnd in main intc
     * @reg_mask            mask register
     * @domain              irq_domain of the controller
     * @parent              parent controller for ext and sub irqs
     * @irqs                irq-data, always s3c_irq_data[32]
     */
    struct s3c_irq_intc {                                   // 保存中断控制器数据
            void __iomem            *reg_pending;           // 指定源挂起寄存器
            void __iomem            *reg_intpnd;            // 指定中断挂起寄存器
            void __iomem            *reg_mask;              // 指定中断屏蔽寄存器
            struct irq_domain       *domain;
            struct s3c_irq_intc     *parent;
            struct s3c_irq_data     *irqs;
    };
    
    /*
     * Array holding pointers to the global controller structs
     * [0] ... main_intc
     * [1] ... sub_intc
     * [2] ... main_intc2 on s3c2416
     */
    static struct s3c_irq_intc *s3c_intc[3];

    这里使用s3c_irq_init保存中断控制器的信息,比如寄存器信息、中断域、以及当前中断控制器所控制的中断源。我们后面也将这个结构称为中断控制器,这个结构不同于irq_chip(irq_chip保存的主要是关中断、开中断、屏蔽中断等回调函数)。

    实际上S3C2440物理只有一个中断控制器,这里软件抽象出来3个中断控制器:

    • 一个根中断控制器管理主中断源;
    • 两个子中断控制器,一个用于管理外部中断源、另一个管理带有子中断的内部中断源;

    为了方便理解,我们将这三个中断控制器以级联的方式展示:

    3.4 machine_desc->init_irq

    machine_desc->init_irq被赋值为s3c24xx_init_intc我们继续看一下s3c24xx_init_intc函数,该函数位于drivers/irqchip/irq-s3c24xx.c:

    static struct s3c_irq_intc * __init s3c24xx_init_intc(struct device_node *np,
                                           struct s3c_irq_data *irq_data, // 中断源信息数组 
                                           struct s3c_irq_intc *parent,   // 指定父中断控制器
                                           unsigned long address)         // 指定中断控制寄存器基地址
    {
            struct s3c_irq_intc *intc;
            void __iomem *base = (void *)0xf6000000; /* static mapping */
            int irq_num;
            int irq_start;
            int ret;
    
            intc = kzalloc(sizeof(struct s3c_irq_intc), GFP_KERNEL);   // 动态分配src_irq_init结构
            if (!intc)
                    return ERR_PTR(-ENOMEM);
    
            intc->irqs = irq_data;
    
            if (parent)
                    intc->parent = parent;                                 // 设置父中断控制器  
    
            /* select the correct data for the controller.
             * Need to hard code the irq num start and offset
             * to preserve the static mapping for now    
    * 根据实际物理地址,配置MMU映射后的中断相关寄存器地址
    */ switch (address) { case 0x4a000000: // SRCPND 源挂起寄存器 pr_debug("irq: found main intc\n"); intc->reg_pending = base; // 指定SRCPND intc->reg_mask = base + 0x08; // 指定INTMASK intc->reg_intpnd = base + 0x10; // 指定INTPND irq_num = 32; // 中断源数量 irq_start = S3C2410_IRQ(0); // S3C2410_IRQ定义为((X) + S3C2410_CPUIRQ_OFFSET) 最终等于0+16=16 break; case 0x4a000018: // SUBSRCPND 次级源挂起寄存器 pr_debug("irq: found subintc\n"); intc->reg_pending = base + 0x18; // 指定SUBSRCPND intc->reg_mask = base + 0x1c; // 指定INTSUBMSK irq_num = 29; // 中断源数量 为啥是29? irq_start = S3C2410_IRQSUB(0); // 0+ 16 + 58 = 74 break; case 0x4a000040: pr_debug("irq: found intc2\n"); intc->reg_pending = base + 0x40; intc->reg_mask = base + 0x48; intc->reg_intpnd = base + 0x50; irq_num = 8; irq_start = S3C2416_IRQ(0); break; case 0x560000a4: // EINTMASK 外部中断屏蔽寄存器 pr_debug("irq: found eintc\n"); base = (void *)0xfd000000; intc->reg_mask = base + 0xa4; // EINTMASK intc->reg_pending = base + 0xa8; // EINTPND irq_num = 24; // 中断源数量 irq_start = S3C2410_IRQ(32); // 32+16=48 break; default: pr_err("irq: unsupported controller address\n"); ret = -EINVAL; goto err; } /* now that all the data is complete, init the irq-domain */ s3c24xx_clear_intc(intc); // 清除中断挂起 intc->domain = irq_domain_add_legacy(np, irq_num, irq_start, // 初始化中断域 0, &s3c24xx_irq_ops, intc); if (!intc->domain) { pr_err("irq: could not create irq-domain\n"); ret = -EINVAL; goto err; } set_handle_irq(s3c24xx_handle_irq); // 设置irq总中断入口程序为s3c24xx_handle_irq return intc; err: kfree(intc); return ERR_PTR(ret); }

    这个函数主要做了以下事情:

    • 为每一个中断控制器。动态分配s3c_irq_intc;
    • 初始化s3c_irq_intc中断相关寄存器:源挂起寄存器、中断屏蔽寄存器、中断挂起寄存器;
    • 清除中断挂起状态;
    • 为每一个中断控制器分配irq_domain,并进行初始化,最后追加到全局链表irq_domain_list;
    • 设置中断统一入口程序为s3c24xx_handle_irq;

    代码执行完之后s3c_intc结构大致如下:

    四、s3c24xx_init_intc代码分析

    由于s3c24xx_init_intc里面主要是s3c2440处理器中断相关的初始化代码,并且内容比较多,且比较重要,因此我们单独进行分析每一块内容。

    4.1 s3c24xx_clear_intc

     其中s3c24xx_clear_intc通过向INTPND或者EINTPND相应位写1来清除中断的挂起状态:

    static void s3c24xx_clear_intc(struct s3c_irq_intc *intc)
    {
            void __iomem *reg_source;
            unsigned long pend;
            unsigned long last;
            int i;
    
            /* if intpnd is set, read the next pending irq from there */
            reg_source = intc->reg_intpnd ? intc->reg_intpnd : intc->reg_pending;  // 中断挂起期存器?中断挂起期存器:源挂起寄存器
    
            last = 0;
            for (i = 0; i < 4; i++) {
                    pend = readl_relaxed(reg_source);    // 读取寄存器的值
    
                    if (pend == 0 || pend == last)
                            break;
    
                    writel_relaxed(pend, intc->reg_pending);   // 写1、清除挂起状态
                    if (intc->reg_intpnd)
                            writel_relaxed(pend, intc->reg_intpnd);
    
                    pr_info("irq: clearing pending status %08x\n", (int)pend);
                    last = pend;
            }
    }

    4.2 创建并初始化irq_domain

    intc->domain = irq_domain_add_legacy(np, irq_num, irq_start,
                                                 0, &s3c24xx_irq_ops,
                                                 intc);

    其中参数s3c24xx_irq_ops类型为irq_domain_ops,这个我们在上一篇博客中介绍过:

    static const struct irq_domain_ops s3c24xx_irq_ops = {
            .map = s3c24xx_irq_map,          // 比较重要,单独介绍
            .xlate = irq_domain_xlate_twocell,
    };

    irq_domain_add_legacy函数在kernel/irq/irqdomain.c中定义,还函数用于分配struct irq_domain,这里通过host_data字段绑定了中断控制器和中断域的关系,最后将创建好的struct irq_domain添加到全局链表irq_domain_list:

    /**
     * irq_domain_add_legacy() - Allocate and register a legacy revmap irq_domain.
     * @of_node: pointer to interrupt controller's device tree node.
     * @size: total number of irqs in legacy mapping
     * @first_irq: first number of irq block assigned to the domain
     * @first_hwirq: first hwirq number to use for the translation. Should normally
     *               be '0', but a positive integer can be used if the effective
     *               hwirqs numbering does not begin at zero.
     * @ops: map/unmap domain callbacks
     * @host_data: Controller private data pointer
     *
     * Note: the map() callback will be called before this function returns
     * for all legacy interrupts except 0 (which is always the invalid irq for
     * a legacy controller).
     */
    struct irq_domain *irq_domain_add_legacy(struct device_node *of_node,
                                             unsigned int size,            //中断数量 以主中断为例 32
                                             unsigned int first_irq,       //起始IRQ编号  16
                                             irq_hw_number_t first_hwirq,  //起始硬件中断号   0
                                             const struct irq_domain_ops *ops,
                                             void *host_data)              // 中断控制器 
    {
            struct irq_domain *domain;
    
            domain = __irq_domain_add(of_node_to_fwnode(of_node), first_hwirq + size,
                                      first_hwirq + size, 0, ops, host_data);
            if (domain)
                    irq_domain_associate_many(domain, first_irq, first_hwirq, size);
    
            return domain;
    }

    其中irq_domain_add定义如下:

    /**
     * __irq_domain_add() - Allocate a new irq_domain data structure
     * @fwnode: firmware node for the interrupt controller
     * @size: Size of linear map; 0 for radix mapping only
     * @hwirq_max: Maximum number of interrupts supported by controller
     * @direct_max: Maximum value of direct maps; Use ~0 for no limit; 0 for no
     *              direct mapping
     * @ops: domain callbacks
     * @host_data: Controller private data pointer
     *
     * Allocates and initialize and irq_domain structure.
     * Returns pointer to IRQ domain, or NULL on failure.
     */
    struct irq_domain *__irq_domain_add(struct fwnode_handle *fwnode, int size,     // size为线性映射表大小 0+32=32
                                        irq_hw_number_t hwirq_max, int direct_max,  // hwiq_max硬件中断号最大值 0+32=32
                                        const struct irq_domain_ops *ops,           // 该结构存放大量函数指针
                                        void *host_data)                             //中断控制器    
    {
            struct device_node *of_node = to_of_node(fwnode);
            struct irqchip_fwid *fwid;
            struct irq_domain *domain;
    
            static atomic_t unknown_domains;
    
            domain = kzalloc_node(sizeof(*domain) + (sizeof(unsigned int) * size), // 动态分配struct irq_domain、最后4*size是线性lookup table表大小
                                  GFP_KERNEL, of_node_to_nid(of_node));
            if (WARN_ON(!domain))
                    return NULL;
    
            if (fwnode && is_fwnode_irqchip(fwnode)) {
                    fwid = container_of(fwnode, struct irqchip_fwid, fwnode);
    
                    switch (fwid->type) {
                    case IRQCHIP_FWNODE_NAMED:
                    case IRQCHIP_FWNODE_NAMED_ID:
                            domain->name = kstrdup(fwid->name, GFP_KERNEL);
                            if (!domain->name) {
                                    kfree(domain);
                                    return NULL;
                            }
                            domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
                            break;
                    default:
                            domain->fwnode = fwnode;
                            domain->name = fwid->name;
                            break;
                    }
            } else if (of_node) {
                    char *name;
    
                    /*
                     * DT paths contain '/', which debugfs is legitimately
                     * unhappy about. Replace them with ':', which does
                     * the trick and is not as offensive as '\'...
                     */
                    name = kasprintf(GFP_KERNEL, "%pOF", of_node);
                    if (!name) {
                            kfree(domain);
                            return NULL;
                    }
    
                    strreplace(name, '/', ':');
    
                    domain->name = name;
                    domain->fwnode = fwnode;
                    domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
            }
    
            if (!domain->name) {
                    if (fwnode)
                            pr_err("Invalid fwnode type for irqdomain\n");
                    domain->name = kasprintf(GFP_KERNEL, "unknown-%d",
                                             atomic_inc_return(&unknown_domains));
                    if (!domain->name) {
                            kfree(domain);
                            return NULL;
                    }
                    domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
            }
    
            of_node_get(of_node);
     /* Fill structure */
            INIT_RADIX_TREE(&domain->revmap_tree, GFP_KERNEL);
            mutex_init(&domain->revmap_tree_mutex);
            domain->ops = ops;                     // 设置操作机,用于创建映射、翻译等 
            domain->host_data = host_data;         // 绑定中断控制器
            domain->hwirq_max = hwirq_max;        // 设置硬件中断号最大值
            domain->revmap_size = size;           // 设置查找表lookup table大小 
            domain->revmap_direct_max_irq = direct_max;
            irq_domain_check_hierarchy(domain);   // 检查中断域是否级联
    
            mutex_lock(&irq_domain_mutex);
            debugfs_add_domain_dir(domain);
            list_add(&domain->link, &irq_domain_list);   // 添加到列表,以便发生中断时能能够找到对应的中断域
            mutex_unlock(&irq_domain_mutex);
    
            pr_debug("Added domain %s\n", domain->name);
            return domain;
    }

    4.3 设置中断描述符成员handle_irq

    domain->ops指向了s3c24xx_irq_ops,其成员map指向了s3c24xx_irq_map,也是定义在drivers/irqchip/irq-s3c24xx.c,

    static int s3c24xx_irq_map(struct irq_domain *h, unsigned int virq,    
                                                            irq_hw_number_t hw)  
    {
            struct s3c_irq_intc *intc = h->host_data;         // 获取中断域绑定的中断控制器
            struct s3c_irq_data *irq_data = &intc->irqs[hw];  // 根据硬件中断号获取中断源信息
            struct s3c_irq_intc *parent_intc;
            struct s3c_irq_data *parent_irq_data;
            unsigned int irqno;
    
            /* attach controller pointer to irq_data */
            irq_data->intc = intc;
            irq_data->offset = hw;
    
            parent_intc = intc->parent;              // 如果有父中断控制器
    
            /* set handler and flags */
            switch (irq_data->type) {    // 根据中断源类型设置中断处理函数
            case S3C_IRQTYPE_NONE:
                    return 0;
            case S3C_IRQTYPE_EINT:                // 外部中断
                    /* On the S3C2412, the EINT0to3 have a parent irq
                     * but need the s3c_irq_eint0t4 chip
                     */
                    if (parent_intc && (!soc_is_s3c2412() || hw >= 4))
                            irq_set_chip_and_handler(virq, &s3c_irqext_chip,
                                                     handle_edge_irq);
                    else
                            irq_set_chip_and_handler(virq, &s3c_irq_eint0t4,
                                                     handle_edge_irq);
                    break;
            case S3C_IRQTYPE_EDGE:            // 双边沿触发中断 
                    if (parent_intc || intc->reg_pending == S3C2416_SRCPND2)
                            irq_set_chip_and_handler(virq, &s3c_irq_level_chip,
                                                     handle_edge_irq);
                    else
                            irq_set_chip_and_handler(virq, &s3c_irq_chip,
                                                     handle_edge_irq);
                    break;
            case S3C_IRQTYPE_LEVEL:            // 电平触发  
                    if (parent_intc)
                            irq_set_chip_and_handler(virq, &s3c_irq_level_chip,
                                                     handle_level_irq);
                    else
                            irq_set_chip_and_handler(virq, &s3c_irq_chip,
                                                     handle_level_irq);
                    break;
            default:
                    pr_err("irq-s3c24xx: unsupported irqtype %d\n", irq_data->type);
                    return -EINVAL;
            }
     irq_set_chip_data(virq, irq_data);
    
            if (parent_intc && irq_data->type != S3C_IRQTYPE_NONE) {  // 有父级中断控制器,即子中断,需要修改主中断源的中断处理函数为s3c_irq_demux
                    if (irq_data->parent_irq > 31) {
                            pr_err("irq-s3c24xx: parent irq %lu is out of range\n",
                                   irq_data->parent_irq);
                            return -EINVAL;
                    }
    
                    parent_irq_data = &parent_intc->irqs[irq_data->parent_irq];  // 获取子中断对应的主中断源信息
                    parent_irq_data->sub_intc = intc;
                    parent_irq_data->sub_bits |= (1UL << hw);
    
                    /* attach the demuxer to the parent irq */
                    irqno = irq_find_mapping(parent_intc->domain,   // 根据主中断硬件中断号和中断域获取主中断编号
                                             irq_data->parent_irq);
                    if (!irqno) {
                            pr_err("irq-s3c24xx: could not find mapping for parent irq %lu\n",
                                   irq_data->parent_irq);
                            return -EINVAL;
                    }
                    irq_set_chained_handler(irqno, s3c_irq_demux);   // 设置
            }
    
            return 0;
    }

    第一个参数为中断域、第二个参数为IRQ编号,第三个为硬件中断号。

    这里做了以下事情:

    • 通过中断域irq_domain获取到其关联的中断控制器s3c_irq_intc;
    • 根据硬件中断号hw从中断控制器的irqs获取中断源信息;
    • 然后调用irq_set_chip_and_handler,根据IRQ编号(虚拟中断号)找到irq_desc,然后根据不同的中断触发类型,设置其成员handle_irq回调和irq_chip指针;
    • 如果是子中断,还需要调用irq_set_chained_handler设置主中断所对应的irq_desc.handle_irq回调函数;之所以调用irq_set_chained_handler,主要是因为子中断控制器在把多个irq汇集起来后,输出端连接到根中断控制器的其中一个irq中断线输入脚,这意味着,每个子中断控制器的中断发生时,CPU一开始只会得到根中断控制器的irq编号,然后进入该irq编号对应的irq_desc.handle_irq回调,该回调我们不能使用流控层定义好的几个流控函数,而是要自己实现一个函数,该函数负责从子中断控制器中获得irq的中断源,并计算出对应新的irq编号,然后调用新irq所对应的irq_desc.handle_irq回调,这个回调使用流控层的标准实现;

    irq_set_chip_and_handler定义在include/linux/irq.h文件中:

    static inline void irq_set_chip_and_handler(unsigned int irq, struct irq_chip *chip,
                                                irq_flow_handler_t handle)
    {
            irq_set_chip_and_handler_name(irq, chip, handle, NULL);
    }

    irq_set_chip_and_handler_name定义在 kernel/irq/chip.c:

    void
    irq_set_chip_and_handler_name(unsigned int irq, struct irq_chip *chip,
                                  irq_flow_handler_t handle, const char *name)
    {
            irq_set_chip(irq, chip);
            __irq_set_handler(irq, handle, 0, name);
    }

    首先调用irq_set_chip绑定irq到chip;然后调用__irq_set_handler初始化中断描述的handle_irq回调函数:

    void
    __irq_set_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained,
                      const char *name)
    {
            unsigned long flags;
            struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, 0);
    
            if (!desc)
                    return;
    
            __irq_do_set_handler(desc, handle, is_chained, name);
            irq_put_desc_busunlock(desc, flags);
    }

    4.4  设置中断统一入口函数handle_arch_irq

    s3c24xx_clear_intc函数最后调用set_handle_irq(s3c24xx_handle_irq)设置中断统一入口函数为s3c24xx_handle_irq,位于kernel/irq/handle.c文件

    #ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
    int __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
    {
            if (handle_arch_irq)
                    return -EBUSY;
    
            handle_arch_irq = handle_irq;
            return 0;
    }

    可以看到如果我们定义了CONFIG_GENERIC_IRQ_MULTI_HANDLER,将会设置handle_arch_irq为s3c24xx_handle_irq,有没有发现这里很熟悉,这是因为在之前我们介绍irq_hanlder中就是调用的handle_arch_irq函数进行中断处理的。

    .macro  irq_handler
    #ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
            ldr     r1, =handle_arch_irq
            mov     r0, sp
            badr    lr, 9997f
            ldr     pc, [r1]
    #else
            arch_irq_handler_default
    #endif
    9997:
            .endm

    之前我们分析的是中断处理的默认实现:arch_irq_handler_default。

    既然看到这里了,那我顺便也看一下s3c24xx_handle_irq的实现,位于drivers/irqchip/irq-s3c24xx.c:

    asmlinkage void __exception_irq_entry s3c24xx_handle_irq(struct pt_regs *regs)
    {
            do {
                    if (likely(s3c_intc[0]))
                            if (s3c24xx_handle_intc(s3c_intc[0], regs, 0))
                                    continue;
    
                    if (s3c_intc[2])
                            if (s3c24xx_handle_intc(s3c_intc[2], regs, 64))
                                    continue;
    
                    break;
            } while (1);
    }

    s3c_intc[2] 这边没有使用,传入的struct pt_regs *regs参数其实就是之前保存现场的那些寄存器:

    static inline int s3c24xx_handle_intc(struct s3c_irq_intc *intc,              
                                          struct pt_regs *regs, int intc_offset)   // 硬件中断号起始偏移
    {
            int pnd;
            int offset;
    
            pnd = readl_relaxed(intc->reg_intpnd);  // 读取中断挂机寄存器,从而知道触发了哪些中断源
            if (!pnd)
                    return false;
    
            /* non-dt machines use individual domains */
            if (!irq_domain_get_of_node(intc->domain))
                    intc_offset = 0;
    
            /* We have a problem that the INTOFFSET register does not always
             * show one interrupt. Occasionally we get two interrupts through
             * the prioritiser, and this causes the INTOFFSET register to show
             * what looks like the logical-or of the two interrupt numbers.
             *
             * Thanks to Klaus, Shannon, et al for helping to debug this problem
             */
            offset = readl_relaxed(intc->reg_intpnd + 4);    // 读取中断偏移寄存器 获取硬件中断号
    
            /* Find the bit manually, when the offset is wrong.
             * The pending register only ever contains the one bit of the next
             * interrupt to handle.
             */
            if (!(pnd & (1 << offset)))                    // 再次校验发生了中断
                    offset =  __ffs(pnd);
    
            handle_domain_irq(intc->domain, intc_offset + offset, regs);
            return true;
    }

     s3c24xx_handle_intc先读取中断挂起寄存器,判断有没有中断发生,然后读取中断偏移寄存器,根据寄存器请求位,找到的中断号,然后调用handle_domain_irq。

    handle_domain_irq函数定义在include/linux/irqdesc.h文件:

    static inline int handle_domain_irq(struct irq_domain *domain,
                                        unsigned int hwirq, struct pt_regs *regs)
    {
            return __handle_domain_irq(domain, hwirq, true, regs);
    }

    参数如下:

    • 第一个参数传入了中断域;
    • 第二个参数传入了硬件中断号;
    • 第三个参数传入了寄存器结构指针,即中断发生时的寄存器信息。

    这里调用了__handle_domain_irq,前面内容已经分析过了,这里就不再重复分析了。

    五、中断流控处理回调handle_irq

    linux中断子系统对几种常见的流控类型进行了抽象,并为它们实现了相应的标准函数,我们只要选择相应的函数,赋值给irq所对应的irq_desc结构的handle_irq字段中即可。这些标准的回调函数都是irq_flow_handler_t类型,定义在include/linux/irqhandler.h:

    typedef void (*irq_flow_handler_t)(struct irq_desc *desc);

    5.1 标准流控回调函数

    目前在linux内核实现了以下这些标准流控回调函数,定义在kernel/irq/chip.c中:

    • handle_nested_irq:用于处理使用线程的嵌套中断;
    • handle_simple_irq: Simple and software-decoded IRQs.
    • handle_untracked_irq:Simple and software-decoded IRQs.
    • handle_level_irq:电平触发类型的中断处理函数;
    • handle_fasteoi_irq:用于需要响应eoi的中断控制器;
    • handle_fasteoi_nmi:irq handler for NMI interrupt lines;
    • handle_edge_irq:边沿触发类型的中断处理函数;
    • handle_percpu_irq:Per CPU local irq handler;
    • ...

    驱动程序和板级代码可以通过以下几个API设置中断的流控回调函数:

    • irq_set_handler;
    • irq_set_chip_and_handler;
    • irq_set_chip_and_handler_name;

    5.2 handle_simple_irq

    /**
     *      handle_simple_irq - Simple and software-decoded IRQs.
     *      @desc:  the interrupt description structure for this irq
     *
     *      Simple interrupts are either sent from a demultiplexing interrupt
     *      handler or come from hardware, where no interrupt hardware control
     *      is necessary.
     *
     *      Note: The caller is expected to handle the ack, clear, mask and
     *      unmask issues if necessary.
     */
    void handle_simple_irq(struct irq_desc *desc)
    {
            raw_spin_lock(&desc->lock);  // 首先获取自旋锁
    
            if (!irq_may_run(desc))
                    goto out_unlock;
    
            desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
    
            if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {  // 没有指定中断处理函数、或者中断禁止
                    desc->istate |= IRQS_PENDING;   // 挂起标志位置1
                    goto out_unlock;
            }
    
            kstat_incr_irqs_this_cpu(desc);
            handle_irq_event(desc);
    
    out_unlock:
            raw_spin_unlock(&desc->lock);  // 释放锁
    }

    该函数主要进行了以下操作:

    • 首先获取中断描述符的自旋锁;
    • 判断当前中断处理程序是已经执行,如果是退出;
    • 清除状态位IRQS_REPLAY 、IRQS_WAITING;
    • 调用handle_irq_event处理irq_desc中的action链表;
    • 释放自旋锁;

    在irq_may_run中检测IRQD_IRQ_INPROGRESS标志位,在SMP系统中,同一个中断信号有可能发往多个CPU,但是中断处理只应该处理一次,所以设置状态为IRQD_IRQ_INPROGRESS,其他CPU执行此中断时都会先检查此状态。

    static bool irq_may_run(struct irq_desc *desc)
    {
            unsigned int mask = IRQD_IRQ_INPROGRESS | IRQD_WAKEUP_ARMED;
    
            /*
             * If the interrupt is not in progress and is not an armed
             * wakeup interrupt, proceed.
             */
            if (!irqd_has_set(&desc->irq_data, mask))
                    return true;
    
            /*
             * If the interrupt is an armed wakeup source, mark it pending
             * and suspended, disable it and notify the pm core about the
             * event.
             */
            if (irq_pm_check_wakeup(desc))
                    return false;
    
            /*
             * Handle a potential concurrent poll on a different core.
             */
            return irq_check_poll(desc);
    }

    5.3 handle_level_irq

    /**
     *      handle_level_irq - Level type irq handler
     *      @desc:  the interrupt description structure for this irq
     *
     *      Level type interrupts are active as long as the hardware line has
     *      the active level. This may require to mask the interrupt and unmask
     *      it after the associated handler has acknowledged the device, so the
     *      interrupt line is back to inactive.
     */
    void handle_level_irq(struct irq_desc *desc)
    {
            raw_spin_lock(&desc->lock);
            mask_ack_irq(desc);
    
            if (!irq_may_run(desc))  
                    goto out_unlock;
    
            desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
    
            /*
             * If its disabled or no action available
             * keep it masked and get out of here
             */
            if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {   // 没有设置处理程序,或者中断禁止
                    desc->istate |= IRQS_PENDING;         
                    goto out_unlock;
            }
    
            kstat_incr_irqs_this_cpu(desc);
            handle_irq_event(desc);
    
            cond_unmask_irq(desc);
    
    out_unlock:
            raw_spin_unlock(&desc->lock);
    }

    该函数用于处理电平中断,电平型触发中断在有效电平下会保持其中断请求状态。那么这需要在中断处理函数首先将该中断关闭(reg_mask寄存器),同时清除中断挂起(reg_pending寄存器),最后再开启该中断(reg_mask寄存器)。由于在开始将该中断关闭,在最后将该中断打开,在这期间,是不会有来自同一中断源的中断来干扰这段程序的执行的,也就该中断源不会产生新的中断,中断不会嵌套。

    该函数主要进行了以下操作:

    • 首先获取中断描述符的自旋锁;
    • mask_ack_irq屏蔽中断,实际调用的是irq_chip 的irq_mask_ack(&desc->irq_data),通过写中断屏蔽寄存器reg_mask、reg_pending实现
    • 判断当前中断处理程序是否在执行,如果是退出;
    • 清除状态位IRQS_REPLAY 、IRQS_WAITING;
    • 调用handle_irq_event处理irq_desc中的action链表;
    • cond_unmask_irq取消中断屏蔽,实际调用的是irq_chip 的irq_unmask(&desc->irq_data),通过写中断屏蔽寄存器reg_mask实现;;
    • 释放自旋锁;

    六、handle_irq_event

    在linux的流控处理函数中均会执行handle_irq_event函数,该函数定义在kernel/irq/handle.c文件中,在该函数内部会执行irq_desc中的action链表。

    6.1 handle_irq_event

    irqreturn_t handle_irq_event(struct irq_desc *desc)
    {
            irqreturn_t ret;
    
            desc->istate &= ~IRQS_PENDING;                      // 清除挂起标志位
            irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);     // 设置中断处理标志位
            raw_spin_unlock(&desc->lock);                       // 释放锁
    
            ret = handle_irq_event_percpu(desc);                // 中断处理
    
            raw_spin_lock(&desc->lock);                         // 获取锁
            irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);   // 清除中断处理标志位
            return ret;
    }

    在SMP系统中,同一个中断信号有可能发往多个CPU,但是中断处理只应该处理一次,所以设置状态为IRQD_IRQ_INPROGRESS,其他CPU执行此中断时都会先检查此状态。在函数执行完毕会将标志位清除。

    6.2 handle_irq_event_percpu

    irqreturn_t handle_irq_event_percpu(struct irq_desc *desc)
    {
            irqreturn_t retval;
            unsigned int flags = 0;
    
            retval = __handle_irq_event_percpu(desc, &flags);
    
            add_interrupt_randomness(desc->irq_data.irq, flags);
    
            if (!noirqdebug)
                    note_interrupt(desc, retval);
            return retval;
    }

    6.3 __handle_irq_event_percpu

    irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
    {
            irqreturn_t retval = IRQ_NONE;
            unsigned int irq = desc->irq_data.irq;
            struct irqaction *action;
    
            record_irq_time(desc);
    
            for_each_action_of_desc(desc, action) {
                    irqreturn_t res;
    
                    trace_irq_handler_entry(irq, action);
                    res = action->handler(irq, action->dev_id);  // 调用action->handler、即request_threaded_irq时注册的primary handler
                    trace_irq_handler_exit(irq, action, res);
    
                    if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pS enabled interrupts\n",  // 如果当前CPU总中断被禁止,返回非0,否则返回0
                                  irq, action->handler))
                            local_irq_disable();       // 关闭当前CPU所有中断
    
                    switch (res) {
                    case IRQ_WAKE_THREAD:      // 唤醒中断线程
                            /*
                             * Catch drivers which return WAKE_THREAD but
                             * did not set up a thread function
                             */
                            if (unlikely(!action->thread_fn)) {
                                    warn_no_thread(irq, action);
                                    break;
                            }
    
                            __irq_wake_thread(desc, action);   // 唤醒中断线程
    
                            /* Fall through - to add to randomness */
                    case IRQ_HANDLED:
                            *flags |= action->flags;
                            break;
    
                    default:
                            break;
                    }
    
                    retval |= res;
            }
    
            return retval;
    }

    6.4 __irq_wake_thread

    void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action)
    {
            /*
             * In case the thread crashed and was killed we just pretend that
             * we handled the interrupt. The hardirq handler has disabled the
             * device interrupt, so no irq storm is lurking.
             */
            if (action->thread->flags & PF_EXITING)  // 设置了PF_EXITING标志 退出
                    return;
    
            /*
             * Wake up the handler thread for this action. If the
             * RUNTHREAD bit is already set, nothing to do.
             */
            if (test_and_set_bit(IRQTF_RUNTHREAD, &action->thread_flags)) // 已经在执行
                    return;
    
            /*
             * It's safe to OR the mask lockless here. We have only two
             * places which write to threads_oneshot: This code and the
             * irq thread.
             *
             * This code is the hard irq context and can never run on two
             * cpus in parallel. If it ever does we have more serious
             * problems than this bitmask.
             *
             * The irq threads of this irq which clear their "running" bit
             * in threads_oneshot are serialized via desc->lock against
             * each other and they are serialized against this code by
             * IRQS_INPROGRESS.
             *
             * Hard irq handler:
             *
             *      spin_lock(desc->lock);
             *      desc->state |= IRQS_INPROGRESS;
             *      spin_unlock(desc->lock);
             *      set_bit(IRQTF_RUNTHREAD, &action->thread_flags);
             *      desc->threads_oneshot |= mask;
             *      spin_lock(desc->lock);
             *      desc->state &= ~IRQS_INPROGRESS;
             *      spin_unlock(desc->lock);
             *
             * irq thread:
             *
             * again:
             *      spin_lock(desc->lock);
             *      if (desc->state & IRQS_INPROGRESS) {
             *              spin_unlock(desc->lock);
             *              while(desc->state & IRQS_INPROGRESS)
             *                      cpu_relax();
             *              goto again;
             *      }
             *      if (!test_bit(IRQTF_RUNTHREAD, &action->thread_flags))
             *              desc->threads_oneshot &= ~mask;
             *      spin_unlock(desc->lock);
             *
             * So either the thread waits for us to clear IRQS_INPROGRESS
             * or we are waiting in the flow handler for desc->lock to be
             * released before we reach this point. The thread also checks
             * IRQTF_RUNTHREAD under desc->lock. If set it leaves
             * threads_oneshot untouched and runs the thread another time.
             */
            desc->threads_oneshot |= action->thread_mask;
    
            /*
             * We increment the threads_active counter in case we wake up
             * the irq thread. The irq thread decrements the counter when
             * it returns from the handler or in the exit path and wakes
             * up waiters which are stuck in synchronize_irq() when the
             * active count becomes zero. synchronize_irq() is serialized
             * against this code (hard irq handler) via IRQS_INPROGRESS
             * like the finalize_oneshot() code. See comment above.
             */
            atomic_inc(&desc->threads_active);    // 线程唤醒次数
    
            wake_up_process(action->thread);   // 唤醒线程
    }

    最后调用wake_up_process唤醒中断线程。

    七、总结

    7.1 中断处理流程总结

    通过上面的分析我们大概了解到内核启动的时候,进行了如下操作:

    • 为每个中断源分配struct irq_desc,以S3C2440为例,这里一共分配至111个中断,其中包含15个软件中断、32个主中断、15个内部子中断、24个外部中断等;
    • 由于主中断和内部子中断、外部中断存在级联的关系,初始化三个struct s3c_irq_intc结构,保存这三类中断的信息,并通过s3c24xx_init_intc函数进行中断初始化;以32个主中断为例:
      • irqs字段保存所有的主中断,指针类型,每一个成员都是struct s3c_irq_data类型;
      • 动态分配中断域istruct rq_domain,并进行初始化,实现硬件中断号到虚拟中断号的映射,其中domain->ops->map函数被用来初始化irq_desc的中断流控回调handle_irq以及irq_chip指针;

    当中断发生时,将会执行中断处理程序irq_handler:

    • s3c24xx_handle_int函数首先获取硬件中断号,这里指的是主中断号;
    • 然后执行__handle_domain_irq:
      • 调用irq_find_mapping,根据硬件中断号以及根中断控制器对应的中断域irq_domain获取IRQ编号;
      • 调用generic_handle_irq:
        • 调用irq_to_desc根据IRQ编号获取中断描述符;
        • 调用中断描述符中的流控处理回调handle_irq;
        • handle_irq使用chip结构中的函数清除、屏蔽或者重新使能该中断,还要调用用户在action链表中注册的primary handler,同时如果注册了中断线程化处理函数,还会唤醒中断线程,执行threaded interrupt handler。

    在SMP系统下,对于handle_level_irq而言,一次典型的情况是:

    • 中断控制器接收到中断信号,发送给一个或多个CPU,并执行中断处理函数;
    • 在中断处理函数中CPU会通知中断控制器屏蔽该中断(mask_ack_irq);
    • 之后当执行中断服务例程(handle_irq_event)时会设置该中断描述符的状态为IRQD_IRQ_INPROGRESS,表明其他CPU如果执行该中断就直接退出,因为本CPU已经在处理了。

    因此可以推断出:

    • Linux中中断处理程序无需重入。当一个给定的中断处理程序正在执行时,相应的中断在所有处理器上都会被屏蔽,以防在同一个中断上接收另一个新的中断。也就是说,同一个中断,同一个中断处理程序,在执行完毕之前不可能同时在2个处理器上执行,加上中断处理程序不能休眠,因此无需考虑可重入性。
    • 中断处理程序可以被中断(不是同一个中断)也被称作嵌套中断(实际上在较新的linux版本中,在中断处理程序执行过程中硬件总中断是被屏蔽的,这样就不会在嵌套)。

    7.2 具体中断的数据结构框图

    这里以EINT0中断为例,当内核启动完后,我们绘制其中断描述符以及相关结构信息大致如下:

    7.3 中断相关的目录结构

    include/linux下目录,存放linux内核中断相关的头文件:

    • irq.h:放irq_chip、irq_data结构相关定义;
    • irqdesc.h:存放irq_desc 结构相关定义;
    • irqdomain.h:存放irq_domain结构相关定义;

    kernel/irq下目录,存放linue内核中断相关的实现:

    • irqdesc.c:中断描述符分配等相关代码;
    • irqdomain.c:中断域初始化等相关代码;
    • chip.c:标准标准流控回调函数、中断使能、屏蔽等实现;
    • handle.c;

    drivers/irqchip目录下:

    • irq-s3c24xx.c:SOC相关的中断初始化;

    arch/arm目录下:

    • kernel/entry-armv.S:异常向量表相关代码;
    • kernel/irq.c:中断处理相关代码;
    • kernel/traps.c;
    • mach-s3c24xx/include/mach/irqs.h:SOC相关的IRQ编号定义;
    • mach-s3c24xx/mach-smdk2440.c:存放有SOC中断初始化的代码;

    以上各个目录只是列出部分文件。

    参考文章

    [1]Linux中断(interrupt)子系统之二:arch相关的硬件封装层

    [2]5.分析内核中断运行过程,以及中断3大结构体:irq_desc、irq_chip、irqaction(详解)

    [3]linux3.10 中断处理过程(二)s3c2440中断控制器及处理函数的初始化

    [4]linux驱动之中断

    [5]【原创】Linux中断子系统(一)-中断控制器及驱动分析

    [6]linux kernel的中断子系统之(三):IRQ number和中断描述符

    [7]Linux中断(interrupt)子系统之三:中断流控处理层

    [8]关于handle_level_irq、handle_edge_irq和中断嵌套问题

    [9]linux中断源码分析 - 中断发生(三)

  • 相关阅读:
    Silverlight4 打印 生成文件过大解决
    清理 Visual Studio 工具箱 的冗杂控件(第三方控件卸载不完全)
    SQL2005SP4生成数据库脚本视图依赖顺序错误问题
    SQL Server 2005 企业版没有 Management Studio管理工具
    Excel Reader 轻量级
    开发托管ActiveX或第三方程序托管插件时调试问题解决方法
    Entity Framework 从数据库生成模型丢失数据库文档不完美解决方案
    今天写了个很蛋疼的sql语句
    WinForm 内嵌 Office 文档 解决方案测试(非DSOFRAME 纯C#代码,网上独一份)
    生产者、消费者问题之闹钟
  • 原文地址:https://www.cnblogs.com/zyly/p/15952962.html
Copyright © 2020-2023  润新知