ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] Interrupt - GICv2 part 2
    Linux/kernel 2023. 12. 24. 14:47

    글의 참고

    - DEN0013D_cortex_a_series_PG.pdf - (Interrupt Hanling - 12.2 The Generic Interrupt Controller p12-7)

    CoreLink GIC-400 Generic Interrupt Controller Technical Reference Manual

    ARM Generic Interrupt Controller Architecture Specification Version 2.0

    - https://zhuanlan.zhihu.com/p/586238925

    - http://www.wowotech.net/irq_subsystem/gic_driver.html

    - https://www.elecfans.com/d/2260663.html

    - https://www.cnblogs.com/LoyenWang/p/12996812.html

    - https://zhuanlan.zhihu.com/p/520171265?utm_id=0

    - https://zhuanlan.zhihu.com/p/641041561


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.

     


    글의 내용

    - Initialization of each GIC CPU Interface on secondary CPUs

    " multi-processors 시스템에서 커널 초기화 코드를 여러 CPUs 에서 실행하는 것은 불가능하다. 즉, 하나의 CPU 만이 커널 초기화 코드를 실행할 수 있다. intel 에서는 이러한 CPU 를 Boot Processor(BSP) 라고 불렀으며, 시스템에 전원이 인가되는 시점에 하드웨어 레벨에서 여러 CPUs 중에서 하나를 골라 BSP 로 명명한다. BSP 가 아닌 CPUs 들은 Secondary CPUs 혹은 Non-Boot CPUs 라고 부른다. 그렇다면,  Non-boot CPUs 들은 어떻게 될까? reset state 에서 BSP 의 IPI(Inter-Processor Interrupt) 를 기다리게 된다. BSP 가 어느 정도 시스템 초기화를 마무리하면, reset state 에 있는 Non-boot CPUs 들에게 이제 boot 를 해도 된다는 IPI 를 전송한다(이 IPI 에는 Non-boot CPUs 들이 code jump 할 entry point 도 명시되어 있음). 그런데, 이 시점에는 이미 대부분의 hardware & software 초기화가 완료된 상태이기 때문에, Non-boot CPUs 들의 kernel entry point(secondary_start_kernel()) 는 BSP 와는 달라야 한다(start_kernel()). ARMv7 architecture 에서 BSP 는 GICv2 를 초기화하기 위해 git_init_bases() 함수를 호출한다.

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    void __init gic_init_bases(unsigned int gic_nr, int irq_start,
    			   void __iomem *dist_base, void __iomem *cpu_base,
    			   u32 percpu_offset, struct device_node *node)
    {
    	....
    	gic_dist_init(gic); // GIC Distributor 초기화
    	gic_cpu_init(gic); // BSP GIC CPU Interface 만 초기화
    	gic_pm_init(gic); // GIC Power Management 초기화
    }

     

     

    " 그런데, 뭔가 코드가 이상한 것 같다. GIC Distributor 와 GIC Power Management 는 global 하게 사용되는 module 이다. 모든 CPUs 가 git_init_bases() 함수를 호출하면, 매번 GIC Distributor 와 GIC Power Management 가 초기화되는 꼴이다. 그러나, 다행이도 git_init_bases() 함수는 BSP 에 의해서만 호출된다. 왜냐면, 이건 BSP 는 start_kernel() 함수를 호출하고, Secondary CPUs 들은 secondary_start_kernel() 함수를 호출하기 때문이다.

     

     

     

    " 위 hardware block diagram 은 GICv2 내부 구조를 보여준다. GIC Distributor 는 딱 하나만 존재하고, GIC CPU Interface 들은 각 CPU 마다 존재하는 것을 알 수 있다. 그렇다면, secondary CPUs 들도 자신의 GIC CPU Interface 를 초기호해야 하는 것 아닐까? 즉, 시스템에 존재하는 모든 CPUs 가 각자 gic_cpu_init() 함수를 호출해야 하는 것 아닐까? BSP 는 확인했으니, 이번에는 secondary CPUs 들만 확인해보자.

     

     

    " BSP 가 GICv2 를 초기화할 때, secondary CPUs 들이 각자 자신의 GIC CPU Interface 를 초기화할 수 있도록, `gic_cpu_notifier` callback 함수를 등록해준다.

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    void __init gic_init_bases(unsigned int gic_nr, int irq_start,
    			   void __iomem *dist_base, void __iomem *cpu_base,
    			   u32 percpu_offset, struct device_node *node)
    {
    	....
    
    	if (gic_nr == 0) {
    #ifdef CONFIG_SMP
    		....
    		register_cpu_notifier(&gic_cpu_notifier);
    #endif
    	}
    
    	....
    }

     

     

    " gic_cpu_nottifier->notifier_call` 에 gic_secondary_init() 함수가 등록되어 있다. 직관적으로 이름만 봐도 secondary CPUs 들을 위한 것임을 알 수 있다. 이 함수 내용을 보면, CPU_STARTING 혹은 CPU_STARTING_FROZEN 메시지를 받으면, gic_cpu_init() 함수를 호출해서 각자 자신의 GIC CPU Interface 를 초기화한다. 그렇다면, secondary CPUs 는 언제 gic_secondary_init() 함수를 호출할까? 

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    #ifdef CONFIG_SMP
    static int gic_secondary_init(struct notifier_block *nfb, unsigned long action,
    			      void *hcpu)
    {
    	if (action == CPU_STARTING || action == CPU_STARTING_FROZEN)
    		gic_cpu_init(&gic_data[0]);
    	return NOTIFY_OK;
    }
    
    /*
     * Notifier for enabling the GIC CPU interface. Set an arbitrarily high
     * priority because the GIC needs to be up before the ARM generic timers.
     */
    static struct notifier_block gic_cpu_notifier = {
    	.notifier_call = gic_secondary_init,
    	.priority = 100,
    };
    #endif

     

     

    " 리눅스 커널의 secondary CPUs 들의 entry point 는 secondary_start_kernel() 함수라고 했다. 이 함수에서 notify_cpu_starting() 함수를 호출하는데, notify_cpu_starting() 함수안에서 cpu_nofity(`gic_secondary_init`) 함수를 호출한다. 이 때, CPU_STARTING 메시지를 함께 전달한다(notify_cpu_starting() 함수를 보면, default 로 CPU_STARTING 메시지를 전달한다).

    #ifdef CONFIG_SMP
    ....
    
    /**
     * notify_cpu_starting(cpu) - call the CPU_STARTING notifiers
     * @cpu: cpu that just started
     *
     * This function calls the cpu_chain notifiers with CPU_STARTING.
     * It must be called by the arch code on the new cpu, before the new cpu
     * enables interrupts and before the "boot" cpu returns from __cpu_up().
     */
    void notify_cpu_starting(unsigned int cpu)
    {
    	unsigned long val = CPU_STARTING;
    
    #ifdef CONFIG_PM_SLEEP_SMP
    	if (frozen_cpus != NULL && cpumask_test_cpu(cpu, frozen_cpus))
    		val = CPU_STARTING_FROZEN;
    #endif /* CONFIG_PM_SLEEP_SMP */
    	cpu_notify(val, (void *)(long)cpu);
    }
    
    #endif /* CONFIG_SMP */

     

     

     

     

    - GIC callbacks

    - irq domain callbacks

    " interrupt controller 들은 자신만의 irq domain 을 갖는다. GIC 또한 자신만의 irq domain 을 가지며, irq domain 은 IRQ number 와 GIC hwirq 의 mappping 및 unmapping 에 대한 메커니즘을 가지고 있다. `.map` 은 어떻게 IRQ number 와 GIC hwirq 가 mapping 될지를 결정하며, `.unmap` 은 IRQ number 와 GIC hwirq 가 unmapping 될지를 결정한다. 그리고, `irq_domain_ops` 는 커널에서 관리되기 때문에, `.map` 및 `.unmap` callback 함수가 호출되는 시점 또한 커널에 의해서 결정된다.

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    static const struct irq_domain_ops gic_irq_domain_ops = {
    	.map = gic_irq_domain_map,
    	.unmap = gic_irq_domain_unmap,
    	.xlate = gic_irq_domain_xlate,
    };

     

     

    " 그런데, gic_irq_domain_ops 은 언제 커널에 등록될까? irq_domain_add_*() 함수가 호출되면, GIC 의 irq domain 이 새로 생성된다. 그 생성된 struct irq_domain->ops 에 gic_irq_domain_ops 가 연결된다. irq domain 의 생성 및 초기화 과정이 궁금하다면 이 글을 참고하자.

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    void __init gic_init_bases(unsigned int gic_nr, int irq_start,
    			   void __iomem *dist_base, void __iomem *cpu_base,
    			   u32 percpu_offset, struct device_node *node)
    {
    	....
    	if (of_property_read_u32(node, "arm,routable-irqs",
    				 &nr_routable_irqs)) {
    		....
    		gic->domain = irq_domain_add_legacy(node, gic_irqs, irq_base,
    					hwirq_base, &gic_irq_domain_ops, gic);
    	} else {
    		gic->domain = irq_domain_add_linear(node, nr_routable_irqs,
    						    &gic_irq_domain_ops,
    						    gic);
    	}
    	....
    }

     

     

     

    1. gic_irq_domain_map 

    " IRQ number 와 GIC hardware interrupt ID 를 매핑 관계를 만들 때, `gic_irq_domain_map()` callback 함수가 호출된다. 인자를 보면, irq 는 software IRQ number 를 의미하며, hw 는 hardware interrupt number 를 의미한다.

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
    				irq_hw_number_t hw)
    {
    	if (hw < 32) { // for SGIs & PPIs
    		irq_set_percpu_devid(irq); // --- 1
    		irq_set_chip_and_handler(irq, &gic_chip, // --- 2
    					 handle_percpu_devid_irq);
    		set_irq_flags(irq, IRQF_VALID | IRQF_NOAUTOEN); // --- 3
    	} else { // for SPIs
    		irq_set_chip_and_handler(irq, &gic_chip, // --- 4
    					 handle_fasteoi_irq);
    		set_irq_flags(irq, IRQF_VALID | IRQF_PROBE);
    
    		gic_routable_irq_domain_ops->map(d, irq, hw); // --- 5
    	}
    	irq_set_chip_data(irq, d->host_data); // --- 6
    	return 0;
    }

     

     

     

    (1) `SGI & PPI` 와 `SPI` 의 가장 큰 차이점은 `Per-CPU` 다. 즉, SPI 는 모든 CPUs 에서 공유되는 interrupts 지만, SGI 와 PPI 는 per-CPU memory region 에 할당되고, IRQ_PER_CPU & IRQ_PER_CPU_DEVID 플래그가 설정된다.

    // kernel/irq/irqdesc.c - v3.17-rc7
    int irq_set_percpu_devid(unsigned int irq)
    {
    	struct irq_desc *desc = irq_to_desc(irq);
    	....
    
    	desc->percpu_enabled = kzalloc(sizeof(*desc->percpu_enabled), GFP_KERNEL);
    	....
        
    	irq_set_percpu_devid_flags(irq);
    	return 0;
    }
    // include/linux/irq.h - v3.17-rc7
    static inline void irq_set_percpu_devid_flags(unsigned int irq)
    {
    	irq_set_status_flags(irq,
    			     IRQ_NOAUTOEN | IRQ_PER_CPU | IRQ_NOTHREAD |
    			     IRQ_NOPROBE | IRQ_PER_CPU_DEVID);
    }

     

     

     

    (2) `irq_set_chip_and_handler()` 함수는 IRQ number 에 대응하는 interrupt descriptor 에 irq chip 을 설정하고(irq_set_chip()), high-level irq flow handler 로 `handle_percpu_devid_irq` 를 설정한다(`__irq_set_handler()`). 

    // include/linux/irq.h - v3.17-rc7
    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);
    }
    // kernel/irq/chip.c - v3.17-rc7
    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 - set the irq chip for an irq
     *	@irq:	irq number
     *	@chip:	pointer to irq chip description structure
     */
    int irq_set_chip(unsigned int irq, struct irq_chip *chip)
    {
    	unsigned long flags;
    	struct irq_desc *desc = irq_get_desc_lock(irq, &flags, 0);
    
    	....
    	desc->irq_data.chip = chip;
    	....
        
    	return 0;
    }
    // kernel/irq/chip.c - v3.17-rc7
    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);
    
    	....
    	desc->handle_irq = handle;
    	desc->name = name;
    
    	....
    }

     

     

     

    (3) 앞에서 irq chip 과 high-level irq flow handler 가 설정됬으니, irq_desc 의 irq flag 에 `IRQF_VALID` 를 설정한다. 그리고, IRQF_NOAUTOEN 도 함께 설정해서 user 에서 `reuqeust_irq()` 함수를 호출했을 때, 자동으로 해당 irq 및 nmi 가 enable 되지 않도록 막는다. 즉, user 가 명시적으로 enable_irq() 혹은 enabie_nmi() 함수를 호출해서 해당 irq 및 nmi 를 enable 해야 한다. 그리고, IRQF_* 플래그는 irq_desc 쪽에 설정되고, IRQD_* 플래그는 irq_data 에 설정된다.

    / /arch/arm/kernel/irq.c - v3.17-rc7
    void set_irq_flags(unsigned int irq, unsigned int iflags)
    {
    	unsigned long clr = 0, set = IRQ_NOREQUEST | IRQ_NOPROBE | IRQ_NOAUTOEN;
    	....
    
    	if (iflags & IRQF_VALID)
    		clr |= IRQ_NOREQUEST;
    	if (iflags & IRQF_PROBE)
    		clr |= IRQ_NOPROBE;
    	if (!(iflags & IRQF_NOAUTOEN))
    		clr |= IRQ_NOAUTOEN;
    	/* Order is clear bits in "clr" then set bits in "set" */
    	irq_modify_status(irq, clr, set & ~clr);
    }
    // kernel/irq/chip.c - v3.17-rc7
    void irq_modify_status(unsigned int irq, unsigned long clr, unsigned long set)
    {
    	unsigned long flags;
    	struct irq_desc *desc = irq_get_desc_lock(irq, &flags, 0);
    
    	....
            irq_settings_clr_and_set(desc, clr, set);
    
    	irqd_clear(&desc->irq_data, IRQD_NO_BALANCING | IRQD_PER_CPU |
    		   IRQD_TRIGGER_MASK | IRQD_LEVEL | IRQD_MOVE_PCNTXT);
    	if (irq_settings_has_no_balance_set(desc))
    		irqd_set(&desc->irq_data, IRQD_NO_BALANCING);
    	if (irq_settings_is_per_cpu(desc))
    		irqd_set(&desc->irq_data, IRQD_PER_CPU);
    	if (irq_settings_can_move_pcntxt(desc))
    		irqd_set(&desc->irq_data, IRQD_MOVE_PCNTXT);
    	if (irq_settings_is_level(desc))
    		irqd_set(&desc->irq_data, IRQD_LEVEL);
    
    	irqd_set(&desc->irq_data, irq_settings_get_trigger_mask(desc));
    
    	irq_put_desc_unlock(desc, flags);
    }

     

     

     

    (4) SPI 같은 경우는 high-level irq flow handler 로 `handle_fasteoi_irq` 를 설정한다. handle_fasteoi_irq() 핸들러는 크게 2 가지 작업을 수행한다.

    1. handle_irq_event() -> handle_irq_event_percpu() -> struct irqaction->handler() 를 호출한다. 즉, driver irq handler 를 호출한다.

    2. driver irq handler 가 마무리 되면(handler_irq_event() 함수가 호출된 후), cond_unmask_eoi_irq() 함수를 호출해서 EOI 를 write 한다.
    // kernel/irq/chip.c - v3.17-rc7
    /**
     *	handle_fasteoi_irq - irq handler for transparent controllers
     *	@irq:	the interrupt number
     *	@desc:	the interrupt description structure for this irq
     *
     *	Only a single callback will be issued to the chip: an ->eoi()
     *	call when the interrupt has been serviced. This enables support
     *	for modern forms of interrupt handlers, which handle the flow
     *	details in hardware, transparently.
     */
    void handle_fasteoi_irq(unsigned int irq, struct irq_desc *desc)
    {
    	struct irq_chip *chip = desc->irq_data.chip;
    	....
    	handle_irq_event(desc);
    
    	cond_unmask_eoi_irq(desc, chip);
    	....
    }

     

     

     

    (5) `routable` 이라는 용어는 `arm,routable-irqs` 속성과 관련있다. 즉, 동적으로 peripheral interrutp request lines 을 GICs 에 mapping 하기 위한 메커니즘을 의미한다(이 때, 반드시 hardware 적으로 지원이 필요하다. 즉, 동적으로 mapping 할 수 있는 hardware 가 존재해야 한다(crossbar / multiplexer)). 그러나, 이 기능은 현재 사용되지 않는다[참고1 참고2].

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    /* Default functions for routable irq domain */
    static int gic_routable_irq_domain_map(struct irq_domain *d, unsigned int irq,
    			      irq_hw_number_t hw)
    {
    	return 0;
    }
    
    static void gic_routable_irq_domain_unmap(struct irq_domain *d,
    					  unsigned int irq)
    {
    }
    
    static int gic_routable_irq_domain_xlate(struct irq_domain *d,
    				struct device_node *controller,
    				const u32 *intspec, unsigned int intsize,
    				unsigned long *out_hwirq,
    				unsigned int *out_type)
    {
    	*out_hwirq += 16;
    	return 0;
    }
    
    static const struct irq_domain_ops gic_default_routable_irq_domain_ops = {
    	.map = gic_routable_irq_domain_map,
    	.unmap = gic_routable_irq_domain_unmap,
    	.xlate = gic_routable_irq_domain_xlate,
    };
    
    const struct irq_domain_ops *gic_routable_irq_domain_ops =
    					&gic_default_routable_irq_domain_ops;

     

     

     

    2. gic_irq_domain_unmap 

    " gic_irq_domain_unmap() 함수는 gic_irq_doman_map() 함수의 반대 역할은 한다고 보면 된다. 즉, IRQ number 와 GIC 의 hwirq 가 unmap 될 때, 이 함수가 호출된다. 그런데, gic_routable_irq_domain_ops->unmap() callback 함수가 사실 비어있다. 즉, v3.17-rc7 에서 irq domain unmap 프로세스에서 GIC 는 특별히 하는 일이없다.

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    static void gic_irq_domain_unmap(struct irq_domain *d, unsigned int irq)
    {
    	gic_routable_irq_domain_ops->unmap(d, irq);
    }

     

     

     

    3. gic_irq_domain_xlate

    " interrupt controller 는 device tree 에서 공식적으로 정의된 standard properties 말고도, 자신만의 device binding 전략을 만들 수 있다. 예를 들어, GICv2 같은 경우는 `#interrupt-cells = <3>` 로 해서 GICv2 를 사용하는 peripherals 들로 부터 3 개의 interrupt cells 을 받는다. device driver 는 probe 시점에 device tree 에 작성해놓은 peripehral interrupt 를 알고 싶기 마련이다. 이 때, irq_domain_ops->xlate(gic_irq_domain_xlate) 를 통해서 device node 의 interrupt-parent 와 interrupts 속성을 파싱해서 hwirq 와 IRQ number 를 mapping 한다.

    // include/linux/irq.h - v3.17-rc7
    enum {
    	....
    	IRQ_TYPE_SENSE_MASK	= 0x0000000f
    	....
    };
    // drivers/irqchip/irq-gic.c - v3.17-rc7
    static int gic_irq_domain_xlate(struct irq_domain *d,
    				struct device_node *controller,
    				const u32 *intspec, unsigned int intsize,
    				unsigned long *out_hwirq, unsigned int *out_type)
    {
    	unsigned long ret = 0;
    
    	if (d->of_node != controller)
    		return -EINVAL;
    	if (intsize < 3)
    		return -EINVAL;
    
    	/* Get the interrupt number and add 16 to skip over SGIs */
    	*out_hwirq = intspec[1] + 16; // --- 1
    
    	/* For SPIs, we need to add 16 more to get the GIC irq ID number */
    	if (!intspec[0]) {
    		ret = gic_routable_irq_domain_ops->xlate(d, controller, // --- 1
    							 intspec,
    							 intsize,
    							 out_hwirq,
    							 out_type);
    
    		if (IS_ERR_VALUE(ret))
    			return ret;
    	}
    
    	*out_type = intspec[2] & IRQ_TYPE_SENSE_MASK; // --- 2
    
    	return ret;
    }

     

     

    (1) `/Documentation/devicetree/bindings/interrupt-controller/arm,gic.txt` 문서를 보면, `interrupts` specifier 에 3 개의 cells 가 있다.

    1. 첫 번째 cell - 0 이면 SPI interrupts, 1 이면 PPI interrupts.
    2. 두 번째 cell - hardware interrupt number 를 명시한다. SPI 라면 0 ~ 987 을 명시하고, PPI 라면 0 ~ 15 를 명시한다.
    3. 세 번째 cell - interrupt trigger mode 를 명시한다.

     

    " intspec[1] 은 hwirq 를 의미한다. device tree 에 PPI 및 SPI 를 명시할 때는, 2 개 모두 base number 를 0 으로 잡기 때문에, 실제 GICv2 에 설정할 때는 SGI 에 할당된 번호를 skip 해야 한다. 즉, 16 을 더해야 한다. 그런데, SPI 는 32 를 더해야 하는거 아닌가? 그래서, `if(!intspec[0])` 조건문이 참이라는 것은 현재 설정중 인 interrupt 의 type 이 SPI 임을 의미한다. 그리고, gic_routable_irq_domain_ops->xlate() 함수 내부를 살펴보면, out_hwirq 에 16 을 또 더하는 것을 확인할 수 있다.

     

     

    (2) GICv2 에서 intspec[2] 는 `interrupt trigger mode` 를 의미한다. SPI 같은 경우, intspec[2] 에서 0 ~ 3 번째 비트까지만 값이 유효하다(PPI 가 8 ~ 15 번째 비트를 사용한다). 그리고, GICv2 에서 high edge trigger 와 high level-sensitive 설정만 유효하다.

     

     

    " irq_domain_ops-xlate() callback 함수는 `irq_create_of_mapping()` 함수에서 사용된다. irq_create_of_mapping() 함수는 interrupt-parent 와 interrupts 프로퍼티를 파싱한 데이터를 저장한 `of_phandle_args` 구조체를 인자로 받는다. 이 정보는 다시 irq_domain_ops->xlate() 함수에 전달되고, xlate 에서 interrupts 속성을 파싱해서 hwirq 에 hardware interrupt number, type 에 interrupt trigger mode 를 저장한다. 그리고, 실제 hwirq 와 IRQ number 의 mapping 은 irq_create_mapping() 함수를 통해서 진행된다.

    // kernel/irq/irqdomain.c - v3.17-rc7
    unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)
    {
    	struct irq_domain *domain;
    	....
    
    	domain = irq_data->np ? irq_find_host(irq_data->np) : irq_default_domain;
    	....
    
    	/* If domain has no translation, then we assume interrupt line */
    	if (domain->ops->xlate == NULL)
    		hwirq = irq_data->args[0];
    	else {
    		if (domain->ops->xlate(domain, irq_data->np, irq_data->args,
    					irq_data->args_count, &hwirq, &type))
    			return 0;
    	}
    
    	/* Create mapping */
    	virq = irq_create_mapping(domain, hwirq);
    	....
    }

     

     

     

     

    - irq chip callbacks

    " struct irq_chip 은 물리적인 interrupt controller 를 추상화한 구조체다. v3.17-rc7 을 기준으로 이 구조체의 독특한 특징은 데이터 필드는 name, flags 만 있고, 나머지 필드는 전부 callbacks 함수들로 구성되어 있다. 이 callbacks 함수들은 실제 대부분의 interrupt controller 에서 제공하는 기능들을 추상화 시켰다. 리눅스 커널에서 CPU 및 SoC 제조사들은 자신들의 interrupt controller 가 지원하는 기능들을 소프트웨어적으로 제공하기 위해서 이 구조체를 구현한다. 커널은 구현된 irq_chip 을 사용해서 interrupt controller level 에서 수행해야 할 작업들은 수행한다.

    static struct irq_chip gic_chip = {
    	.name			= "GIC",
    	.irq_mask		= gic_mask_irq,
    	.irq_unmask		= gic_unmask_irq,
    	.irq_eoi		= gic_eoi_irq,
    	.irq_set_type		= gic_set_type,
    	.irq_retrigger		= gic_retrigger,
    #ifdef CONFIG_SMP
    	.irq_set_affinity	= gic_set_affinity,
    #endif
    	.irq_set_wake		= gic_set_wake,
    };

     

     

    " 그렇다면, irq_chip 은 커널에 언제 등록될까? irq_chip 은 커널에 등록된다기 보다는 각 interrupt descritor 에 등록된다. IRQ number 와 GIC hwirq 가 mapping 되야 할 때, 커널은 `gic_irq_domain_map` callback 함수를 호출한다. 이 때, 내부적으로 `irq_set_chip_and_handler() -> irq_set_chip_and_handler_name() -> irq_set_irq()` 함수가 호출되고, IRQ number 에 대응하는 irq_desc->irq_data.chip 에 gic_chip(irq_chip) 을 저장한다.

    /**
     *	irq_set_chip - set the irq chip for an irq
     *	@irq:	irq number
     *	@chip:	pointer to irq chip description structure
     */
    int irq_set_chip(unsigned int irq, struct irq_chip *chip)
    {
    	unsigned long flags;
    	struct irq_desc *desc = irq_get_desc_lock(irq, &flags, 0);
    
    	....
    	desc->irq_data.chip = chip;
    	....
        
    	return 0;
    }

     

     

     

     

    0. gic_arch_extn

    " SoC 제조사는 arm 라이센스를 구매할 때, GIC IP 는 구매하지 않고 arm SoC 만 구매할 경우, arm 에서 제공하는 GIC driver 를 사용할 확률이 높다. 왜냐면, GIC IP 를 구매하지 않았기 때문에, 자체 GIC 를 설계할 수 도 없거니와, 자체 GIC 에 최적화된 device driver 도 만들 수 가 없다. 이 때, arm 에서 SoC 제조사에게 integrated GIC 의 동작을 `fine-tune` 할 수 있게 `gic_arch_extn` 전역 변수를 제공한다. 이 전역 변수는 GICv2 에서 구현한 `gic_chip` 의 모든 callbacks 함수 내부에서 호출되도록 구현되어 있다. 결국, SoC 제조사에서 integrated GIC 를 자신들에 사용 시나리오에 맞게 fine-tune 하고 싶다면, gic_arch_extn 변수의 callbacks 함수들을 구현하면 된다.

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    /*
     * Supported arch specific GIC irq extension.
     * Default make them NULL.
     */
    struct irq_chip gic_arch_extn = {
    	.irq_eoi	= NULL,
    	.irq_mask	= NULL,
    	.irq_unmask	= NULL,
    	.irq_retrigger	= NULL,
    	.irq_set_type	= NULL,
    	.irq_set_wake	= NULL,
    };

     

     

     

    1. gic_mask_irq

    " interrupt enable / disable 은 CPU 레벨에서도 가능하지만(우리가 흔히 아는 local_irq_*() 함수는 CPU 레벨에서 동작한다), interrupt controller 에서도 가능하다. `gic_mask_irq()` callback 함수는 GIC Distributor 에서 GIC CPU Interface 로 interrupt 가 forwarding 되는 disable 한다. 즉, mask 한다.

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    /*
     * Routines to acknowledge, disable and enable interrupts
     */
    static void gic_mask_irq(struct irq_data *d)
    {
    	u32 mask = 1 << (gic_irq(d) % 32);
    
    	raw_spin_lock(&irq_controller_lock);
    	writel_relaxed(mask, gic_dist_base(d) + GIC_DIST_ENABLE_CLEAR + (gic_irq(d) / 32) * 4);
    	if (gic_arch_extn.irq_mask)
    		gic_arch_extn.irq_mask(d);
    	raw_spin_unlock(&irq_controller_lock);
    }

     

     

    " GIC 는 `Interrupt Clear-Enable(GICD_ICENABLERn)` 레지스터들을 여러 개 가지고 있다(Interrupt Clear-Enable Registers 의 구체적인 개수는 GIC 에서 지원되는 hardware interrupts 개수에 따라 달라진다). `Interrupt Clear-Enable` 레지스터의 각 비트는 대응되는 interrupt 가 GIC Distributor 에서 GIC CPU interface 로 forwarding 될 수 있는지를 결정한다. 만약, 특정 bit 에 1 을 writing 할 경우, forwarding 이 disabled 된다. 즉, 해당 interrupt 는 masked 된다. 그런데, 주의할 점이 있다. 0 을 writing 하는 것은 아무런 효과도 없다는 것이다. 그러나, 지금까지 언급된 내용이 적용되는 interrupt type 은 SPI 와 PPI 다. SGI 는 GICD_ISENABLER0 과 GICD_ICENABLER0 을 통해서 enabled or disabled 된다.

     


    ARM Generic Interrupt Controller Architecture Specification Version 2.0

     

     

    " 그런데, 위에 문서를 보면 GICD_ICENABLERn 레지스터는 interrupt 를 disable 하는 용도로만 사용하는 것 같다. 그렇다면, interrupt 를 다시 enable(unmaks()) 해주는 레지스터 또한 별도로 존재하는 것 일까? 다음 callback 함수를 분석해보자.

     

     

     

    2. gic_unmask_irq

    " `gic_unmask_irq()` callback 함수는 gic_mask_irq() 함수의 완전 반대 역할을 한다고 보면 된다. 즉, GIC Distributor 에서 GIC CPU Interface 로 interrupt 가 forwarding 되는 enable 한다. 즉, unmask 한다.

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    static void gic_unmask_irq(struct irq_data *d)
    {
    	u32 mask = 1 << (gic_irq(d) % 32);
    
    	raw_spin_lock(&irq_controller_lock);
    	if (gic_arch_extn.irq_unmask)
    		gic_arch_extn.irq_unmask(d);
    	writel_relaxed(mask, gic_dist_base(d) + GIC_DIST_ENABLE_SET + (gic_irq(d) / 32) * 4);
    	raw_spin_unlock(&irq_controller_lock);
    }

     

     

    " GIC 는 `Interrupt Set-Enable` 레지스터들을 여러 개 가지고 있다. Interrupt Set-Enable 레지스터의 각 비트는 대응되는 interrupt 가 GIC Distributor 에서 GIC CPU interface 로 forwarding 될 수 있는지를 결정한다. 만약, 특정 bit 에 1 을 writing 할 경우, forwarding 이 enabled 된다. 즉, 해당 interrupt 는 unmasked 된다. 그런데, 주의할 점이 있다. 0 을 writing 하는 것은 아무런 효과도 없다는 것이다. 즉, Interrupt Clear-Enable Registers 와 동일하다.

     


    ARM Generic Interrupt Controller Architecture Specification Version 2.0

     

     

     

    3. gic_eoi_irq

    " EOI 는 `End Of Interrupt` 의 약자다. CPU 가 interrupt handling 을 마무리하면, GIC 에게 EOI 를 전달할 수 있다. processor 는 GIC CPU Interface 

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    static void gic_eoi_irq(struct irq_data *d)
    {
    	if (gic_arch_extn.irq_eoi) {
    		raw_spin_lock(&irq_controller_lock);
    		gic_arch_extn.irq_eoi(d);
    		raw_spin_unlock(&irq_controller_lock);
    	}
    
    	writel_relaxed(gic_irq(d), gic_cpu_base(d) + GIC_CPU_EOI);
    }

     

     

    " ARM Generic Interrupt Controller Architecture Specification Version 2.0 문서에서 processor 가 interrupt handling 을 완료했을 때, 과정을 다음과 같이 명시하고 있다. 먼저, 앞쪽 문단의 내용은 interrupt completion 과정이 2 단계로 이루어져 있다는 것을 설명한다. 먼저, 다른 interrupts 들을 처리할 수 있도록 interrupt priority 를 drop 한다. 그리고, GIC Distributor 가 interrupt 의 active state 를 in-active 로 만듬으로써 CPU 가 처리중 인 interrupt 없음으로 만든다.

    When the interrupt handler on the processor has completed the processing of an interrupt, it writes to the CPU interface to indicate interrupt completion. There are two stages to interrupt completion:


     1. priority drop, meaning the priority of the processed interrupt can no longer prevent the signaling of another interrupt to the processor
    2. interrupt deactivation, meaning the Distributor removes the active state of the interrupt.


    In a GICv2 implementation, the GICC_CTLR.EOImode bit determines whether: 


    1. the two stages happen together, when the processor writes to the CPU interface End of Interrupt register
    2. the two stages are separated, so that:

      - priority drop happens when the processor writes to the CPU interface End of Interrupt register
      - interrupt deactivation happens later, when the processor writes to the CPU interface Deactivate Interrupt register.

    - 참고 : ARM Generic Interrupt Controller Architecture Specification Version 2.0

     

     

    " GICv2 같은 경우는 위에서 언급된 2 가지 과정이 한 번에 발생할 수 도 있고, 각각 별개로 수행되도록 할 수 도 있다. 이건, GICC_CTRL.EOImode 비트를 통해서 결정된다. GICv2 에서 GICC_CTRL.EOImode 비트가 0 이면, EOIR 에 처리가 끝난 interrupt ID 를 writing 하면, priority drop 과 interrupt deactivation 이 함께 수행된다. GICC_CTRL.EOImode 비트가 1 이면, priority drop 만 수행된다(`ARM Generic Interrupt Controller Architecture Specification Version 2.0 - 3.2.1 Priority drop and interrupt deactivation` 참고)

     

     

     

    4. gic_set_type

    " `gic_set_type()` 함수는 interrupt trigger mode(level-sensitive or edge-trigger) 를 설정하는데, 사용된다. 주의할 점이 있다. SGIs 같은 경우는, 하드웨어 레벨에서 기본적으로 edge-trigger mode 로만 동작하고, edge-trigger mode 만 지원한다. 그래서, get_set_type 에 hwirq 가 16 미만으로 전달되면 에러를 유발한다. 왜냐면, SGI 는 설정할 수 없기 때문이다. 그리고, GIC 같은 경우는 2 가지 interrupt mode 만 지원한다.

    1. level-sensitive 로 설정될 경우, high level trigger 만 지원한다.
    2. edge-trigger 로 설정될 경우, rising edge trigger 만 지원한다.
    // drivers/irqchip/irq-gic.c - v3.17-rc7
    static int gic_set_type(struct irq_data *d, unsigned int type)
    {
    	void __iomem *base = gic_dist_base(d);
    	unsigned int gicirq = gic_irq(d);
    
    	/* Interrupt configuration for SGIs can't be changed */
    	if (gicirq < 16)
    		return -EINVAL;
    
    	if (type != IRQ_TYPE_LEVEL_HIGH && type != IRQ_TYPE_EDGE_RISING)
    		return -EINVAL;
    
    	raw_spin_lock(&irq_controller_lock);
    
    	if (gic_arch_extn.irq_set_type)
    		gic_arch_extn.irq_set_type(d, type);
    
    	gic_configure_irq(gicirq, type, base, NULL);
    
    	raw_spin_unlock(&irq_controller_lock);
    
    	return 0;
    }

     

     

    " 그리고, 주의할 점이 있다. interrupt mode 를 변경할 때는 반드시 아래의 순서로 진행해야 한다.

    1. 설정할 interrupt 를 먼저 disabled
    2. interrupt mode 변경
    3. re-enable disabled interrupt 

     

     

     

    5. gic_retrigger

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    static int gic_retrigger(struct irq_data *d)
    {
    	if (gic_arch_extn.irq_retrigger)
    		return gic_arch_extn.irq_retrigger(d);
    
    	/* the genirq layer expects 0 if we can't retrigger in hardware */
    	return 0;
    }

     

     

    " irq retrigger 의 내용을 알려면 system suspend 및 resume 에 대한 flow 를 알고 있어야 한다. 디테일한 내용은 이 글을 참고하자. 쉽게 요약하면, system 이 suspended 에서 resume 될 때, resume process 의 초기 시점에 local_irq_enable() 함수가 호출된다. 그런데, 이 시점에는 아직 peripherals 및 system(CPU 포함) 이 interrupt 를 처리할 수 있을 정도의 상태가 아니기 때문에, interrupt 를 처리할 수 있는 시점이 될 때 까지 interrupt 에 대한 처리를 pending 한다. 그리고, interrupt handling 이 가능한 시점 check_irq_resend() 함수를 호출해서 interrupt controller 가, 즉, 하드웨어 레벨에서 irq re-trigger 기능을 지원하는지를 판단한다. 만약, 하드웨어적으로 irq trigger 기능을 지원하면(interrupt controller 가 irq_chip->irq_retrigger() callback 함수를 구현했으면), CONFIG_HARDIRQS_SW_RESEND 컨피그 내부 코드는 실행되지 않는다. 그러나, 하드웨어적으로 irq trigger 기능을 지원하지 않으면, 소프트웨어적으로 irq re-trigger 기능을 emulate 한다.

    // kernel/irq/resend.c - v3.17-rc7
    /*
     * IRQ resend
     *
     * Is called with interrupts disabled and desc->lock held.
     */
    void check_irq_resend(struct irq_desc *desc, unsigned int irq)
    {
    	....
    	if (desc->istate & IRQS_PENDING) {
    		....
    
    		if (!desc->irq_data.chip->irq_retrigger ||
    		    !desc->irq_data.chip->irq_retrigger(&desc->irq_data)) {
    #ifdef CONFIG_HARDIRQS_SW_RESEND
    			....
                
    			set_bit(irq, irqs_resend);
    			tasklet_schedule(&resend_tasklet);
    #endif
    		}
    	}
    }

     

     

    "  CONFIG_HARDIRQS_SW_RESEND 컨피그는 하드웨어적으로 irq re-trigger 기능을 지원하지 않을 경우, 소프트웨적으로 irq re-trigger 기능을 emulate 하기 위해 사용되는 컨피그다.

    It's necessary to enable CONFIG_HARDIRQS_SW_RESEND when you want to use the delayed interrupt disable feature and your hardware is not capable of retriggering an interrupt.

    - 참고 : https://docs.kernel.org/core-api/genericirq.html

     

     

     

    6. gic_set_affinity

    " multi-processor 환경에서 peripherals 들이 발생시킨 interrupts 들은 여러 processors 들에게 distribute 될 수 있다. 그런데, 특정 interrupt 를 특정 CPUs 들에게만 혹은 하나의 CPU 에게만 forwarding 시킬 수 있다. 예를 들어, hwirq 3 이 CPU[1], CPU[3] 에게로만 forwarding 되면, hwirq 3 이 CPU[1] 과 CPU[3] 에 affine 하다고 한다.

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    #ifdef CONFIG_SMP
    static int gic_set_affinity(struct irq_data *d, const struct cpumask *mask_val,
    			    bool force)
    {
    	void __iomem *reg = gic_dist_base(d) + GIC_DIST_TARGET + (gic_irq(d) & ~3);
    	unsigned int cpu, shift = (gic_irq(d) % 4) * 8;
    	u32 val, mask, bit;
    
    	if (!force)
    		cpu = cpumask_any_and(mask_val, cpu_online_mask); // online CPUs 들만 추출한다.
    	else
    		cpu = cpumask_first(mask_val); // CPU 상태(online, offline 등)에 관계없이 cpumask 에서 제일 첫 번째 CPU 를 얻어온다.
    
    	if (cpu >= NR_GIC_CPU_IF || cpu >= nr_cpu_ids)
    		return -EINVAL;
    
    	raw_spin_lock(&irq_controller_lock);
    	mask = 0xff << shift;
    	bit = gic_cpu_map[cpu] << shift;
    	val = readl_relaxed(reg) & ~mask;
    	writel_relaxed(val | bit, reg);
    	raw_spin_unlock(&irq_controller_lock);
    
    	return IRQ_SET_MASK_OK;
    }
    #endif

     

     

    " GICv2 part 1 에서 `Interrupt Processor Targets Registers(GIC_ITARGETSRn)` 에 대해 알아봤다. 이 레지스터는 특정 interrupt 가 어떤 processor 로 forwarding 될지를 명시하는 레지스터다. 즉, affinity 를 설정하는 레지스터다. GICv2 는 8 개의 processors 를 지원하기 때문에, 각 interrupt 가 affine 될 수 있는 CPU 의 개수는 8 개이다. GIC_ITARGETSRn 는 32 비트로 구성되어 있으며, 8 비트당 하나의 interrupt affinity 를 나타낼 수 있다. shift 값은 GIC_ITARGETSRn 에서 특정 hwirq 의 bit-offset 을 나타낸다. 예를 들어, hwirq 3 은 24 번째 비트가 start bit 가 된다. `shift = (3 % 4) * 8 = 24` 가 된다. `mask = 0xff << 24` 를 통해 mask = 0xFF00_0000 이 된다. hwirq 5 같은 경우는 8 번째 비트가 start bit 가 되어야 한다. `shift = (5 % 4) * 8 = 8` 이 된다. `mask = 0xff << 8` 를 통해 mask = 0x0000_00FF 이 된다. gic_cpu_map 배열은 interrupt 가 발생했을 때, GIC Distributor 가 어떤 GIC CPU Interface 로 forwarding 해야 하는지를 나타낸다. 예를 들어, gic_cpu_map[3] 에서 0x20 이 나오면, CPU 3 번은 GIC CPU Interface 5 번과 연결되어 있고, GIC Distributor 는 CPU 3 으로 interrupt 를 forwarding 해야 할 경우, GIC CPU Interface 5 으로 forwarding 한다.

     

     

     

    7. gic_set_wake

    " 실제 업무를 진행할 때, 특정 인터럽트 핀을 systme wake-up pin 으로 설정해야 하는 경우가 있다. 예를 들어, 전장 프로젝트 같은 경우, 차량에 시동이 걸리면, MCU 에 IGN_ON_OFF 시그널이 전달되고, MCU 는 해당 시그널을 AP 쪽으로 forwarding 하게 된다. AP 는 GPIO 를 통해서 이 시그널을 받게 되는데, 이 때, AP 는 시동이 OFF 되면 SLEEP, 시동이 ON 이면 WAKE 라고 설정하는 경우가 많다. 이 때, 리눅스에서는 enable_irq_wake() 함수를 통해서 SLEEP 과 연관있는 GPIO 를 system wake-up pin 으로 설정할 수 있다.

    // drivers/irqchip/irq-gic.c - v3.17-rc7
    #ifdef CONFIG_PM
    static int gic_set_wake(struct irq_data *d, unsigned int on)
    {
    	int ret = -ENXIO;
    
    	if (gic_arch_extn.irq_set_wake)
    		ret = gic_arch_extn.irq_set_wake(d, on);
    
    	return ret;
    }
    
    #else
    #define gic_set_wake	NULL
    #endif

     

     

    " 그렇다면, `irq_chip->irq_set_wake()` callback 함수는 언제 사용될까? 아래 block diagram 에서 볼 수 있다시피, enable_irq_wake() / disable_irq_wake() 함수를 호출하면, 최종적으로 `strcut irq_chip->irq_set_wake()` 함수가 호출되서 인자로 전달된 interrupt 를 system wake-up pin 으로 enable 및 disable 한다.

     

     

Designed by Tistory.