-
[리눅스 커널] Interrupt - IRQ domainLinux/kernel 2023. 11. 7. 00:59
글의 참고
- https://www.kernel.org/doc/Documentation/IRQ-domain.txt
- https://www.kernel.org/doc/html/latest/core-api/irq/irq-domain.html#hierarchy-irq-domain
- http://www.wowotech.net/irq_subsystem/irq-domain.html
- https://www.kernel.org/doc/Documentation/devicetree/bindings/interrupt-controller/interrupts.txt
- https://www.cnblogs.com/tureno/articles/6403946.html
- https://www.cnblogs.com/pengdonglin137/p/7466342.html
글의 전제
- 밑줄로 작성된 글은 강조 표시를 의미한다.
- 그림 출처는 항시 그림 아래에 표시했다.
글의 내용
- Overview
" 리눅스 커널에서 peripheral interrupt 를 구분하기 위해서 2가지 ID`s 를 사용한다.
1. IRQ number : CPU 는 각 peripheral interrupt 를 쉽게 관리하기 위해 숫자에 매핑한다. 이 숫자를 IRQ number 라고 부른다. IRQ number 는 virtual interrupt ID 라고도 불리며, 하드웨어와는 전혀 관련이 없다. IRQ number 는 오직 CPU 가 각 peripheral interrupt 를 쉽게 구분하기 위해서 사용하는 용도일 뿐이다.
2. HW interrupt ID : 인터럽트 컨트롤러에는 시스템에 존재하는 다양한 peripheral 들의 interrupt request lines 이 연결되어 있다. peripheral 은 CPU 와 통신하기 위해 interrupt signal 을 생성해서 interrupt request lines 으로 전송한다. 인터럽트 컨트롤러는 interrupt signal 을 받고, CPU 에게 전달하기 위해서 interrupt signal 에 대응하는 hardware interrupt ID 라는 것을 CPU 에게 전달한다. 즉, 인터럽트 컨트롤러는 peripheral interrupt 는 쉽게 구분하기 위해서 사용된다. 만약, cascading 을 지원하는 인터럽트 컨트롤러 같은 경우는 hardware interrupt ID 하나가 특정 peripheral interrupt 를 나타내지 않는다(예를 들어, x86 의 PIC)." 위에 내용을 통해 알 수 있는 사실은 CPU 와 interrupt controller 가 인터럽트를 구분하는 방식이 서로 다르다는 것을 알 수 있다. 드라이버 개발자라면 어떤 입장을 받아들여야 할까? CPU 가 이해하고 있는 `IRQ number` 를 통해 인터럽트를 구분하는 것이 좋다. 왜냐면, IRQ number 는 소프트웨어적인 virtual interrupt ID 이기 때문에, 나중에 인터럽트 컨트롤러가 바뀌더라도, 즉, 하드웨어가 바뀌더라도 코드에는 영향을 주지 않기 때문이다. 그렇다면, 여기서 이 글의 주제가 나온다.
리눅스 커널의 인터럽트 서브-시스템은 어떻게 hardware interrupt ID 를 IRQ number 로 매핑할까?
- History
" 옛날에 옛적에는 시스템에 인터럽트 컨트롤러가 하나여서 hardware interrupt ID 를 IRQ number 로 매핑하는 방식이 굉장히 심플했다. 실제 인터럽트 컨트롤러의 hardware interrupt number 가 IRQ number 와 1:1 대응됬다고 보면 된다. 예를 들어, 대부분의 SoC 에서 인터럽트 컨트롤러는 `interrupt status register` 를 가지고 있다. 이 레지스터는 아마 64 비트 혹은 그 이상일 것이며, 각 비트는 리눅스가 사용하는 IRQ number 와 직접적으로 매핑될 수 있었다. 왜냐면, 이 시절에는 시스템에 interrupt controller 가 딱 한개만 존재하거나, interrupt controllers 가 여러 개여도 hardware interrupts 자체가 많이 없었기 때문이다. 그리고, 또 이 시절에는 인터럽트 컨트롤러의 interrupt status register 에 GPIO interrupt 가 딱 한 비트만 할당됬었다. 그래서, 모든 GPIO interrupts 들은 딱 하나의 hardware interrupt number 만 사용해야 했었다. 예를 들어, 3 개의 GPIO[0], GPIP[1], GPIO[2] 에서 interrupt 가 발생할 경우, 전부 hardware interrupt number 127 에 대응된다는 것이다. 이러한 방식을 `interrupt cascading` 이라고 한다.
" 초기에는 인터럽트 컨트롤러만 irqchip 으로 추상화를 시켰다. 그러나, 기술이 발전하면서 대부분의 인터럽트가 GPIO 를 통해서 발생하는 케이스가 많아지면서, GPIO controller 또한 interrupt controller 로 인식하게 됬다. 결국, 시스템에서 최소 2개의 인터럽트 컨트롤러가 나오게 되었다.
1. 인터럽트만 관장하는 인터럽트 컨트롤러
2. GPIO controller 타입의 인터럽트 컨트롤러" 시스템의 복잡성이 더욱 증가하고 그 만큼 peripehral interrupt 의 수도 증가하면서, 시스템에는 더 많은 interrupt controllers 들이 필요해졌다. 하드웨어 복잡해지는 만큼 리눅스 커널도 이에 맞게 새로운 interrupt management 메커니즘이 필요해졌다. 고게 바로 `irq domain` 이다.
" domain 이란 말은 range 와 유사하다. 즉, 범위가 있다는 것이다. 그렇면서, 동시에 범위밖으로 초과하면 안된다. 만약, 범위 밖으로 나가게 되면 그것들은 더 이상 의미가 없어진다. 시스템에 존재하는 모든 interrupt controllers 들은 자기만의 interrupt domain 을 갖는다. 예를 들어, interrupt controller A 는 device 1, device 2, device 3 의 interrupt 를 책임지고, interrupt controller B 는 device 4, device 5, interrupt controller A 의 interrupt 를 책임질 수 있다. 뭔가 이상하다. interrupt controller B 가 interrupt controller A 의 interrupts 를 책임진다? 이게 무슨뜻일까?
- Use case : Exynos4412 interrupt controller structure
" 위에서 언급한 interrupt controller A 와 interrupt controller B 의 관계를 어떻게 설명할 수 있을까? Exynos4412 의 hardware interrupt block 구조를 살펴보면, 2개의 interrupt controller 가 사용되는 것을 알 수 있다.
1. GIC(PL390) - interrupt controller B
2. INT_COMBINER - interrupt controller A
Exynos 4412 Interrupt controller" 위 그림에서 GIC 와 INT_COMBINER 이 형성하는 구조를 `Cascade` 라고 한다. interrupt cascade 는 쉽게, 다수의 interrupt source inputs 를 하나의 interrupt group 으로 묶어서 GIC 에게 전달한다는 것이다. 이 때, 하나의 interrupt group 이 GIC 입장에서는 하나의 interrupt source 로 대응한다.
" interrupt combiner 는 총 116 개의 interrupt source inputs 을 가질 수 있고, 116 개의 interrupt source inputs 들을 18 개의 interrupt group outputs 으로 묶어서 관리할 수 있다.
Exynos 4412 Interrupt combiner" cascade 는 좀 더 자세히 보기 위해 Exynos4412 datasheet 를 가져왔다. GIC 의 `SPI Port No 0` 을 보면, INT_COMBINER 의 IntG0_0, IntG0_1, IntG0_2, IntG0_3 에 mapping 되는 것을 볼 수 있다. 이 말은 INT_COMBINER 에서 `MDNIE_LCD0[0], MDNIE_LCD0[1], MDNIE_LCD0[2], MDNIE_LCD0[3]` interrupt 가 발생하면, GIC 의 SPI Port No 0 으로 forwarding 된다는 소리다.
- device tree [참고1]
" device tree 를 보면 peripheral interrupts 와 interrupt controllers 가 이루는 tree structure 를 확인할 수 있다. 그리고, interrupt controllers 들간에 cascading 관계까지도 확인할 수 있다. interrupt controller 는 자신만의 irq domain 를 하나 가질 수 있으며, 해당 irq domain 에 속하는 peripherals 들이 interrupt 를 사용하기 위해서는 반드시 자신이 속한 interrupt controller 의 rule 을 따라야 한다. `History` 섹션에서 설명했던 예시를 다시 가져오자. interrupt controller A 는 device 1, device 2, device 3 의 interrupt 를 책임지고, interrupt controller B 는 device 4, device 5, interrupt controller A 의 interrupt 를 책임진다고 한다. 이 때, interrupt controller A 만에 irq domain A 가 만들어지고, interrupt controller B 만에 irq doamin B 가 만들어진다. irq domain 은 interrupt controller 만 형성할 수 있으며, interrupt controller 마다 irq domain 내에 규칙이 서로 다르다. 예를 들어, irq domain A 내에서는 interrupt trigger mode 가 반드시 level-sensitive 만 사용할 수 있고, irq domain B 에서는 level-sensitive or edge-trigger 모두 가능할 수 도 있다. 이렇게 각 interrupt controller 가 형성하는 irq domain 마다 그 성질이 다르다는 것을 미리 알고 있어야한다.
" 이 섹션에서는 2 가지 내용을 알아본다.
1. interrupt controller node - interrupt client node 에서 interrupt 를 사용하기 위해서 지켜야 하는 가이드를 제공해야 한다.
2. interrupt client node - interrupt 를 사용하기 위해 interrupt controller 가 제공하는 가이드를 지켜야 한다.1. interrupt client nodes
" Required properties
1. interrupts : interrupt 를 발생시키는 device 는 반드시 `interrups` 프로퍼티를 명시해야 한다(`Nodes that describe devices which generate interrupts must contain an "interrupts" property`). 여기서 주의할 점은 interrupts 프로퍼티에 명시할 내용은 `device -> interrupt controller` 로 발생하는 interrupt 를 명시해야 한다. 즉, device 입장에서 interrupt output 이여야 한다. 이 말은 interrupts 프로퍼티에 작성되는 IRQ number 를 device 의 pin number 가 아닌, mapping 된 interrupt controller 의 SPI 번호를 작성해야 한다. interrupt specifier 에 명시할 format 은 interrupt controller 의 `#interrupt-cells` 값에 따라 달라진다. 그리고, interrupts 프로퍼티에 interupt number 를 명시할 때, IRQ number 가 아닌, hwirq 를 명시해야 한다.
// Documentation/devicetree/bindings/interrupt-controller/interrupts.txt - v6.5 Example: interrupt-parent = <&intc1>; interrupts = <5 0>, <6 0>;
위에 예시에서는 5번, 6번 interrupt 를 `&intc1` 로 routing 하는 것을 알 수 있다.
" Optional properties
1. interrupt-parent : device 의 interrupt 가 어떤 interrupt controller 에게 routing 되는지를 명시한다. 이 때, interrupt controller 의 phandle 을 명시한다. `interrups` 프로퍼티에 명시된 interrupt specifiers 들은 모두 `interrupt-parent` 에 명시된 phandle 로 routing 된다. 그렇다면, interrupt 발생시키는 모든 디바이스는 `interrupt-parent` 속성을 명시해야 하는 걸까? 그렇지 않다. exynos4412 에서 interrupt combiner 는 원래라면 interrupt-parent 로 `&gic` 를 지정해야 한다. 그러나, interrupt combiner 는 `interrupt-parent` 속성을 명시하고 있지 않다. 그 이유는 root node 에서 interrupt-parent 속성을 명시해놓고 있기 때문이다. 즉, `interrupts` 속성을 명시했는데, `interrupt-parent` 속성을 명시하지 않았다면, 자신의 부모 노드의 `interrupt-parent` 속성을 물려받게 된다. 결과적으로, exynos4412 SoC 에서는 default interrupt controller 로 `&gic` 를 사용한다고 보면 된다.
2. interrupts-extended : device 가 여러 개의 interrupts 를 가지고 있는데, 이 때, 인터럽트들이 하나의 interrupt controller 에게 routing 되는 것이 아니라, 여러 interrupt controllers 들에게 routing 되야 할 때, 사용한다. 예시 코드에서는 5번 interrupt 를 `&intc2` 로 routing 하고, 1번 interrupt 를 `&intc2` 로 routing 한다. 참고로, 이 프로퍼티는 `interrupts` 프로퍼티와 기능적으로 겹치게 된다. 그래서, `interrupts` 와 `interrupts-extended` 프로퍼티가 모두 명시되어 있을 경우, `interrupts-extended` 속성을 우선시 한다.
// Documentation/devicetree/bindings/interrupt-controller/interrupts.txt - v6.5 Example: interrupts-extended = <&intc1 5 1>, <&intc2 1 0>;
2. interrupt controller nodes
" Required properties
1. interrupt-controller : device 가 interrupt controller 라면, `interrupt-controller` 프로퍼티를 명시한다. boolean 타입이기 때문에 그냥 명시만 하면된다.
2. interrupt-cells : 하나의 interrupt 를 설정하는데, 몇 개의 cells 을 명시해야 하는지를 나타낸다. 예시를 통해 알아보자.
2.1 먼저, cell 이 `1` 인 경우를 보자. 아래의 예시를 보면, `sic` 가 `vic` 를 interrupt-parent 로 지정하고, `interrupts = <31>` 을 명시하고 있다. 즉, interrupts 프로퍼티를 명시할 때, cell(interrupt number)을 한 개만 명시하고 있다. 그러나, 일반적으로는 아래와 같이 명시하지 않는다. specifier 를 명시할 때는, phandle 을 같이 명시하는 것이 기본이지만, interrupt 같은 경우는 interrupt-parent 프로퍼티에 interrupt controller 에 대한 phandle 을 명시하기 때문에, interrupt specifier 에서 굳이 phandle 을 명시하지 않는다.// Documentation/devicetree/bindings/interrupt-controller/interrupts.txt - v6.5 Example: vic: intc@10140000 { compatible = "arm,versatile-vic"; interrupt-controller; #interrupt-cells = <1>; reg = <0x10140000 0x1000>; }; sic: intc@10003000 { compatible = "arm,versatile-sic"; interrupt-controller; #interrupt-cells = <1>; reg = <0x10003000 0x1000>; interrupt-parent = <&vic>; interrupts = <31>; /* Cascaded to vic */ };
2.2 cell 이 `2` 인 경우를 보자. 이제부터는 interrupt client nodes 들은 interrupt specifier 의 첫 번째 cell 에는 interrupt number, 두 번째 cell 에는 interrupt trigger mode 를 입력해야 한다. 크게 edge-trigger 와 level-trigger 로 나뉘는 것을 알 수 있다.// Documentation/devicetree/bindings/interrupt-controller/interrupts.txt - v6.5 - bits[3:0] trigger type and level flags 1 = low-to-high edge triggered 2 = high-to-low edge triggered 4 = active high level-sensitive 8 = active low level-sensitive
// Documentation/devicetree/bindings/interrupt-controller/interrupts.txt - v6.5 Example: i2c@7000c000 { gpioext: gpio-adnp@41 { compatible = "ad,gpio-adnp"; reg = <0x41>; interrupt-parent = <&gpio>; interrupts = <160 1>; gpio-controller; #gpio-cells = <1>; interrupt-controller; #interrupt-cells = <2>; nr-gpios = <64>; }; sx8634@2b { compatible = "smtc,sx8634"; reg = <0x2b>; interrupt-parent = <&gpioext>; interrupts = <3 0x8>; #address-cells = <1>; #size-cells = <0>; threshold = <0x40>; sensitivity = <7>; }; };
`gpioext` 는 interrupt controller 이고, `sx8634` 는 3번 interrupt 를 low-level trigger 에 active 시킨다. 또 눈여겨 볼만한 점은 gpio controller 가 interrupt controller 로도 사용되는 것을 알 수 있다.3. interrupt wakeup parent
" Optional properties
1. wakeup-parent : 몇몇 SoC 에서는 interrupt controller 를 always powered on 으로 설계했다. 그래서, system suspended 상태에서 wakeup interrupt 로 설정된 인터럽트가 interrupt controller 에게 routed 되면, SoC 를 wakeup 시킨다. 이러한 interrupt controller 는 `interrupt-parent` 속성에 명시되면 안되고, 대신에 `wakeup-parent` 속성에 명시해야 한다.
// Documentation/devicetree/bindings/interrupt-controller/interrupts.txt - v6.5 Example: wakeup-parent = <&pdc_intc>;
- Interface
1. register irq domain with the system
" hwirq(HW interrupt ID) 를 통해서 IRQ number(Linux irq) 를 lookup 하는 `reverse mapping` 방법은 총 4 가지가 존재한다.
1. linear mapping
2. radix tree mapping
3. no mapping
4. legacy mapping(1) Linear mapping
" 제일 먼저 볼 맵핑은 `linear mapping` 이다(대다수의 drivers 들은 이 방식을 사용한다). 사실, 이 방식은 hardware interrupt ID 를 index 로 하는 `lookup table` 이라고 봐도 무방하다. 즉, hardware interrupt ID 를 전달하면, 이에 대응하는 IRQ number 가 나오게 된다. linear mapping 은 fixed size table 을 사용한다.
" linear mapping 은 hwirqs 개수가 고정이면서, 256 개 보다 적을 때, 좋은 선택이 될 수 있다. 이 방식의 이점은 역시나 lookup time 이 고정이라는 점이다(배열을 사용하다 보니 상당히 빠르다). 그리고, 사용중 인 IRQs 들에 대해서만 irq_desc 를 할당하기 때문에 메모리 측면에서도 좋다. linear irq domain mapping 을 형성하기 위해서는 아래의 API 를 사용하면 된다.
// include/linux/irqdomain.h - v3.17-rc7 /** * irq_domain_add_linear() - Allocate and register a linear revmap irq_domain. * @of_node: pointer to interrupt controller's device tree node. * @size: Number of interrupts in the domain. * @ops: map/unmap domain callbacks * @host_data: Controller private data pointer */ static inline struct irq_domain *irq_domain_add_linear(struct device_node *of_node, unsigned int size, const struct irq_domain_ops *ops, void *host_data) { return __irq_domain_add(of_node, size, size, 0, ops, host_data); }
(2) Radix Tree map
" HW interrupt ID 와 IRQ number 의 mapping 관계를 형성하기 위해 Radix tree 를 사용한다. Radix tree 에 저장된 IRQ number 를 조회하기 위해서는 HW interrupt ID 가 lookup key 로 사용된다. 만약, linear mapping 관계를 형성하기 어려울 경우, Radix tree map 을 고려해 볼 수 있다(사실, 리눅스 커널에서 Radix tree 를 사용하는데 hardware platforms 은 powerPC 와 MIPS 뿐이다). radix tree irq domain mapping 관계를 형성 할 것이라면, 아래의 API 를 사용하면 된다.
// include/linux/irqdomain.h - v3.17-rc7 static inline struct irq_domain *irq_domain_add_tree(struct device_node *of_node, const struct irq_domain_ops *ops, void *host_data) { return __irq_domain_add(of_node, 0, ~0, 0, ops, host_data); }
(3) no map
" 일부 interrupt controllers 들은 상당히 막강한 기능을가지고 있다. 예를 들어, PowerPC 의 MPIC(Multi-Processor Interrupt Contoller) 같은 경우, HW interrupt ID 와 IRQ number 를 mapping 시킬 필요가 없다. 왜냐면, HW interrupt ID configuration register 에 직접 IRQ number 를 write 할 수 있기 때문이다. writing 이후, peripheral interrupts 가 발생하면, 이에 대응하는 IRQ number 가 CPU 로 forwarding 된다고 볼 수 있다. nomap irq domain mapping 을 형성하기 위해서는 아래의 API 를 사용하면 된다(대다수의 drivers 들은 이 방식을 사용하지 않는다).
// include/linux/irqdomain.h - v3.17-rc7 static inline struct irq_domain *irq_domain_add_nomap(struct device_node *of_node, unsigned int max_irq, const struct irq_domain_ops *ops, void *host_data) { return __irq_domain_add(of_node, 0, max_irq, max_irq, ops, host_data); }
(4) Legacy
" radix tree, nomap 과 함께 굉장히 특수한 케이스에서 사용되는 mapping 방식이다. 이 mapping 방식을 사용하는 경우는 `hwirqs 를 pre-defined and fixed IRQ numbers 와 mapping 해야 할 때` 이다. 예전 리눅스 커널(device tree 가 도입 되기전) 에서는 board file 이라고 해서 hardware 에 대한 정보를 직접 소스 코드에 명시적으로 작성했다. 그래서, `arch/arm` 폴더에는 굉장히 많은 board specific codes 들이 넘쳐놨고, 이 파일들에는 device 와 관련된 많은 정보들이 정적으로 선언되었다(여기서 `정적` 은 static 키워드를 의미하는게 아니다. 동적 할당의 반대인 정적을 의미한다). 예를 들어, 각 device 의 IRQ resources 정보들도 board file 에 명시적으로 선언되었다.
" 그런데, 이렇게 board file 을 사용하면, hwirq 와 IRQ number 를 직접 코드에 명시적으로 작성할 수 밖에 없었다. 이것 때문에 각 peripheral interupt 들이 특정 IRQ number 에 fixed 되어버렸다. 이렇게 fixed mapping 관계를 실제 kernel 에 추가하기 위해서는 `irq_domain_add_legacy()` or `irq_domain_add_simple()` 함수를 사용해야 한다.
/ /kernel/irq/irqdomain.c - v3.17-rc7 /** * irq_domain_add_simple() - Register an irq_domain and optionally map a range of irqs * .... * * Allocates an irq_domain, and optionally if first_irq is positive then also * allocate irq_descs and map all of the hwirqs to virqs starting at first_irq. * * This is intended to implement the expected behaviour for most * interrupt controllers. If device tree is used, then first_irq will be 0 and * irqs get mapped dynamically on the fly. However, if the controller requires * static virq assignments (non-DT boot) then it will set that up correctly. */ struct irq_domain *irq_domain_add_simple(struct device_node *of_node, unsigned int size, unsigned int first_irq, const struct irq_domain_ops *ops, void *host_data)
/** * irq_domain_add_legacy() - Allocate and register a legacy revmap irq_domain. * .... * * 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, unsigned int first_irq, irq_hw_number_t first_hwirq, const struct irq_domain_ops *ops, void *host_data)
2. create mapping for irq domain
" 이전 섹션에서는 irq domain mapping 종류와 system 에 등록하는 API 에 대해 알아봤다. 아직 구체적으로 hwirq 와 IRQ number 가 어떻게 매핑되는지는 설명하지 않았다.
(1) irq_create_mapping
" `irq_create_mapping()` 함수는 hwirq 와 IRQ number 의 mapping 관계를 형성한다. 그런데, 왜 hwirq 와 irq domain 을 전달하고, IRQ number 는 전달하지 않을까? IRQ number 는 리눅스에서 생성하고 리눅스가 관리하는 interrupt number 다. 그러므로, 리눅스(irq_create_mapping) 는 자신만의 interrupt number mapping 방식을 통해 인자로 전달된 hwirq 를 IRQ number 로 mapping 하고, 이 mapping 관계를 인자로 전달된 irq domain 에 저장한다. 그래서, irq_create_mapping() 함수에서는 IRQ number 를 인자로 전달하지 않아도 되는 것이다. mapping 이 성공하고, irq domain 에 추가까지 완료되면, irq_create_mapping() 함수는 IRQ number 를 리턴한다(IRQ number 를 동적 할당된다). prototype 은 다음과 같다.
// kernel/irq/irqdomain.c - v3.17-rc7 /** * irq_create_mapping() - Map a hardware interrupt into linux irq space * @domain: domain owning this hardware interrupt or NULL for default domain * @hwirq: hardware irq number in that domain space * * Only one mapping per hardware interrupt is permitted. Returns a linux * irq number. * If the sense/trigger is to be specified, set_irq_type() should be called * on the number returned from that call. */ unsigned int irq_create_mapping(struct irq_domain *domain, irq_hw_number_t hwirq) { unsigned int hint; int virq; .... /* Look for default domain if nececssary */ if (domain == NULL) domain = irq_default_domain; .... /* Check if mapping already exists */ virq = irq_find_mapping(domain, hwirq); .... /* Allocate a virtual interrupt number */ hint = hwirq % nr_irqs; if (hint == 0) hint++; virq = irq_alloc_desc_from(hint, of_node_to_nid(domain->of_node)); if (virq <= 0) virq = irq_alloc_desc_from(1, of_node_to_nid(domain->of_node)); .... if (irq_domain_associate(domain, virq, hwirq)) { irq_free_desc(virq); return 0; } .... return virq; }
" 이 함수를 호출할 때, 주의할 점이 있다. 바로, driver 는 자신이 전달하는 hwirq 를 다른 pin number 들과 헷갈려서는 안된다는 것이다. 예를 들어, GPIO type interrupt 같은 경우, irq_create_mapping() 함수에 GPIO 번호를 던지면 큰일난다. 왜냐면, GPIO 번호가 hwirq 가 아니기 때문이다. GPIO 를 interrupt 로 사용하는 경우는 반드시 GPIO 와 hwirq 과 어떻게 mapping 되는지를 알고 있어야 한다. 아래 그림은 GPIO05 가 hwirq03 에 mapping 되는 것을 보여준다. 여기서는 block diagram 으로 보여줬지만, 실무에서는 반드시 datasheet 를 살펴봐야 한다. schematic 을 본다고 GPIO 와 hwirq 의 관계를 알 수 는 없다. 그리고, GPIO 를 interrupt 로 사용하는 경우가 아니라면, 대부분의 경우는 hwirq 는 device driver 레벨에서 in-visible 하다. 즉, 신경쓰지 않아도 되는 경우가 많다.
(2) irq_create_strict_mappings
" 이 함수는 이름에서도 알 수 있다시피(of - open firmware), device tree 에서 irq domain 와 hwirq 를 가져와 irq_create_mapping() 함수에 전달하여 mapping 관계를 생성한다. 참고로, 이 함수도 irq_create_mapping() 함수와 같이 하나의 mapping 관계만 생성한다. mapping 에 성공하면, IRQ number(virq) 를 반환한다.
// kernel/irq/irqdomain.c - v3.17-rc7 unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)
" 일반적으로, interrupt 를 사용하는 peripherals 들은 device tree 에 node 를 작성할 때, 반드시 interrupt 관련 정보를 작성하기 마련이다. 그리고, device driver 는 probe 시점에 device tree 에 작성해놓은 자신이 driving 할 peripehral interrupt 를 알고 싶기 마련이다. 이 때, `of_parse_and_map()` 함수를 사용할 수 있다. 첫 번째 인자 `dev` 는 device node 와 mapping 되는 구조체여야 하며, 두 번째 인자 `index` 는 device tree 의 `interrupts` 프로퍼티에서 몇 번째를 가져올지를 명시한다.
" `of_irq_parse_one()` 함수를 통해 `of_phandle_args(oirq)` 에 특정 interrupt 정보가 저장되면, irq_create_of_mapping() 함수를 호출해서 of_phandle_args 에 저장되어 있는 hwirq 를 IRQ number 와 mapping 시킨 뒤, device driver 에게 IRQ number(virq) 를 반환한다. 그렇면, device driver 를 irq_of_parse_and_map() 함수를 통해 반환받은 IRQ number 를 사용해서 request_irq() 함수 등을 호출할 수 있다.
//drivers/of/irq.c - v3.17-rc7 /** * irq_of_parse_and_map - Parse and map an interrupt into linux virq space * @dev: Device node of the device whose interrupt is to be mapped * @index: Index of the interrupt to map * * This function is a wrapper that chains of_irq_parse_one() and * irq_create_of_mapping() to make things easier to callers */ unsigned int irq_of_parse_and_map(struct device_node *dev, int index) { struct of_phandle_args oirq; if (of_irq_parse_one(dev, index, &oirq)) // dev 와 연관된 interrupts 속성을 파싱해서 oriq 에 dev 의 index 번째 interrupt 정보를 저장. return 0; return irq_create_of_mapping(&oirq); // of_irq_parse_one() 함수에서 파싱한 interrupt 정보를 IRQ number 로 mapping. }
(3) irq_create_strict_mappings
" 단일이 아닌, 다수의 mappings 이 필요할 때, 사용된다. 그리고, 이 함수는 위에 함수들과 다르게 커널 내부에서 자동으로 IRQ number 를 할당하는 것이 아닌, irq_create_strict_mappings() 함수를 호출하는 driver engineers 에 의해서 특정 IRQ numbers 들로 mapping 된다. 즉, 주석에서 볼 수 있다시피, linux irq numbers(IRQ numbers) 가 pre-defined 되어 있는 상황에서 사용하는 함수다.
// kernel/irq/irqdomain.c - v3.17-rc7 /** * irq_create_strict_mappings() - Map a range of hw irqs to fixed linux irqs * @domain: domain owning the interrupt range * @irq_base: beginning of linux IRQ range * @hwirq_base: beginning of hardware IRQ range * @count: Number of interrupts to map * * This routine is used for allocating and mapping a range of hardware * irqs to linux irqs where the linux irq numbers are at pre-defined * locations. For use by controllers that already have static mappings * to insert in to the domain. * * Non-linear users can use irq_create_identity_mapping() for IRQ-at-a-time * domain insertion. * * 0 is returned upon success, while any failure to establish a static * mapping is treated as an error. */ int irq_create_strict_mappings(struct irq_domain *domain, unsigned int irq_base, irq_hw_number_t hwirq_base, int count) { int ret; ret = irq_alloc_descs(irq_base, irq_base, count, of_node_to_nid(domain->of_node)); .... irq_domain_associate_many(domain, irq_base, hwirq_base, count); return 0; }
(4) irq_create_direct_mappings
" nomap mapping 시에 사용되는 방식이다. 즉, interrupt controller 가 hardware level 에서 hardware interrupt number 를 IRQ number 로 mapping 시키는 방식이다. 이 방식에서는 IRQ number 를 hardware interrupt number 로 생각하면 된다. 그래서, 아래 mapping 코드를 보면, irq_domain_associate() 함수에 두 번째 인자는 hwirq 고, 세 번째 인자는 IRQ number(virq) 다. 그런데, nomap 에서는 2 개 모두 virq 를 전달하고 있다.
/** * irq_create_direct_mapping() - Allocate an irq for direct mapping * @domain: domain to allocate the irq for or NULL for default domain * * This routine is used for irq controllers which can choose the hardware * interrupt numbers they generate. In such a case it's simplest to use * the linux irq as the hardware interrupt number. It still uses the linear * or radix tree to store the mapping, but the irq controller can optimize * the revmap path by using the hwirq directly. */ unsigned int irq_create_direct_mapping(struct irq_domain *domain) { unsigned int virq; .... virq = irq_alloc_desc_from(1, of_node_to_nid(domain->of_node)); .... if (irq_domain_associate(domain, virq, virq)) { .... } return virq; }
- Mapping Database
1. Overview
" hwirq 와 IRQ number 의 mapping database 는 커널 초기화 시점에 생성된다. 구체적인 process 는 다음과 같다.
1. dts file 은 실제 하드웨어 레벨에서 interrupt controllers 와 peripherals interrupt 간에 topology 를 명시하고 있다. dts file 은 부트 로더가 리눅스 커널을 메모리에 로딩해서 entry point 로 jump 를 할 때, 함께 전달한다.
2. device tree 가 초기화되면, 시스템에 존재하는 모든 device nodes 들이 tree structure 를 형성하게 된다. 이 때, 모든 interrupt controllers 와 peripheral interrupts 들의 device nodes 들도 당연히 생성되며, 마찬가지로 tree structure 를 형성한다.
3. irq chip driver 가 초기화 되는 시점에, `of_irq_init()` 함수가 호출된다. of_irq_init() 함수에서 모든 device nodes 를 탐색해서 matching 되는 모든 interrupt controllers 들을 초기화한다(이 때, matching 이란 `struct of_device_id` 구조체에 name, type, compatible 를 비교해서 점수를 매긴다. 디테일한 내용은 이 글을 참고하자). matching 된 interrupt controllers 의 초기화 함수를 호출하는 순서는 당연히 root, first level, second level 으로 top-down 방식으로 진행된다.
" irq chip driver 초기화 과정에서 irq domain 을 kernel 에 등록하는 process 가 진행된다. 일부 interrupt controllers 들은 이 시점에 mappings(hwirq and IRQ number) 를 형성하기도 한다.
4. interrupt 를 사용하는 devie driver 들이 초기화되는 과정에서 자신의 hwirqs 를 IRQ number 로 mapping 한다.2. during initialization of irq chip driver, register the irq domain
" GICv2 의 초기화 과정중에 irq domain 을 kernel 에 등록하는 코드가 있다.
// 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) { irq_hw_number_t hwirq_base; struct gic_chip_data *gic; int gic_irqs, irq_base, i; .... if (gic_nr == 0 && (irq_start & 31) > 0) { // --- 1 hwirq_base = 16; .... } else { .... } .... gic_irqs -= hwirq_base; // if (of_property_read_u32(node, "arm,routable-irqs", &nr_routable_irqs)) { irq_base = irq_alloc_descs(irq_start, 16, gic_irqs, // `gic_irqs` 개의 irq_descs 할당을 요청하고, root GIC 이기 때문에 IRQ number 의 scan 을 16 부터 시작한다. numa_node_id()); .... gic->domain = irq_domain_add_legacy(node, gic_irqs, irq_base, hwirq_base, &gic_irq_domain_ops, gic); // irq domain 을 kernel 에 등록하고, mapping 관계를 생성한다. } .... }
" GICv2 에서는 irq domain 을 등록할 때, 왜 irq_domain_add_legacy() 사용할까? v3.17-rc7 의 GICv2 코드에서 아직 최신 irq domain mapping 방식을 지원하지 않기 때문이다. 사실 이부분은 굉장히 안타까운 부분이다. 내가 실무에서 리눅스를 처음 만질때도 이미 v4.14 였다. 그런데, v3.17 때면 아마 board specific file 을 통해서 hardware resources 들을 제공하고 있었을 것이다. 그렇기 때문에, v3.17-rc7 의 GICv2 코드에서는 default 로 irq_domain_add_legacy() 함수를 사용해서 irq domain mapping 을 형성한다. 그렇다면, 이후에 버전에서는 어떻게 되었을까? v5.14 를 확인해보면, DT or ACPI 를 지원하면, `irq_domain_create_linear()` 함수를 호출하도록 변경되었다.
// drivers/irqchip/irq-gic.c - v5.14 static int gic_init_bases(struct gic_chip_data *gic, struct fwnode_handle *handle) { .... if (handle) { /* DT/ACPI */ gic->domain = irq_domain_create_linear(handle, gic_irqs, &gic_irq_domain_hierarchy_ops, gic); } else { /* Legacy support */ .... irq_base = irq_alloc_descs(16, 16, gic_irqs, numa_node_id()); .... gic->domain = irq_domain_add_legacy(NULL, gic_irqs, irq_base, 16, &gic_irq_domain_ops, gic); } .... }
" irq_domain_add_legacy() 함수는 irq domain 관련 2 가지 작업을 수행한다.
1. __irq_domain_add() 함수를 통해서 irq domain 을 생성과 동시에 kernel 에 등록한다. 이 때, __irq_domain_add() 함수에 size 가 0 으로 전달되면, irq domain mapping 방식을 radix tree 를 사용하며, 0 이 아니면, linear mapping 을 사용한다.
2. __irq_domain_add() 함수를 통해서 생성된 irq domain 에는 어떠한 mappings 관계도 존재하지 않는다. 실제 mapping 관계를 형성하는 함수는 irq_domain_associate_many() 함수다. 이 함수에 mapping 할 hwirq base number, IRQ base number, mapping 할 개수(size), mapping 을 저장할 irq domain 을 전달한다.// drivers/irqchip/irq-gic.c - v3.17-rc7 /** * 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, unsigned int first_irq, irq_hw_number_t first_hwirq, const struct irq_domain_ops *ops, void *host_data) { struct irq_domain *domain; domain = __irq_domain_add(of_node, first_hwirq + size, // register the irq domain with kernel first_hwirq + size, 0, ops, host_data); if (!domain) return NULL; irq_domain_associate_many(domain, first_irq, first_hwirq, size); // create mapping return domain; }
" irq_domain_add_legacy() 함수는 irq domain 관련 2 가지 작업을 수행한다.
1. __irq_domain_add() 함수를 통해서 irq domain 을 생성과 동시에 kernel 에 등록한다. 이 때, __irq_domain_add() 함수에 size 가 0 으로 전달되면, irq domain mapping 방식을 radix tree 를 사용하며, 0 이 아니면, linear mapping 을 사용한다.
2. __irq_domain_add() 함수를 통해서 생성된 irq domain 에는 어떠한 mappings 관계도 존재하지 않는다. 실제 mapping 관계를 형성하는 함수는 irq_domain_associate_many() 함수다. 이 함수에 mapping 할 hwirq base number, IRQ base number, mapping 할 개수(size), mapping 을 저장할 irq domain 을 전달한다." v3.17-rc7 에서 `__irq_domain_add()` 함수는 irq domain 생성, 초기화, 등록 이라는 3 작업을 수행한다. 눈 여겨볼 점은 `size` 파라미터다. 주석을 보면, size 가 0 이면, radix mapping 을 사용하고, 0 이 아니면 linear mapping 을 사용한다고 한다. 이 내용은 `__irq_domain_add()` 함수에서는 확인할 수 없다. 실제 어떤 mapping 을 확인하려면, irq_domain_associae() 함수를 살펴봐야 한다. 그리고, irq domain 에 커널에 등록된다는 것이 거창한 작업이 아니다. 단지, 커널이 관리하는 global irq domain list 인 `irq_domain_list` 에 새로 생성한 irq domain 을 연결하는 것 뿐이다.
// kernel/irq/irqdomain.c - v3.17-rc7 static LIST_HEAD(irq_domain_list); .... /** * __irq_domain_add() - Allocate a new irq_domain data structure * @of_node: optional device-tree node of 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: map/unmap 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 device_node *of_node, int size, irq_hw_number_t hwirq_max, int direct_max, const struct irq_domain_ops *ops, void *host_data) { struct irq_domain *domain; domain = kzalloc_node(sizeof(*domain) + (sizeof(unsigned int) * size), GFP_KERNEL, of_node_to_nid(of_node)); .... /* Fill structure */ INIT_RADIX_TREE(&domain->revmap_tree, GFP_KERNEL); domain->ops = ops; domain->host_data = host_data; domain->of_node = of_node_get(of_node); domain->hwirq_max = hwirq_max; domain->revmap_size = size; domain->revmap_direct_max_irq = direct_max; .... list_add(&domain->link, &irq_domain_list); .... .... return domain; }
" `irq_domain_associate_many()` 함수는 IRQ base number(irq_base) 와 hwiq base number(hwirq_base) 를 mapping 시키기 위해 실제 hwirq 와 IRQ number 를 mappping 하는 `irq_domain_associae()` 함수를 count(gic_irqs) 만큼 반복한다.
// kernel/irq/irqdomain.c - v3.17-rc7 void irq_domain_associate_many(struct irq_domain *domain, unsigned int irq_base, irq_hw_number_t hwirq_base, int count) { int i; .... for (i = 0; i < count; i++) { irq_domain_associate(domain, irq_base + i, hwirq_base + i); } }
// kernel/irq/irqdomain.c - v3.17-rc7 int irq_domain_associate(struct irq_domain *domain, unsigned int virq, irq_hw_number_t hwirq) { struct irq_data *irq_data = irq_get_irq_data(virq); // --- 1 int ret; .... if (domain->ops->map) { // --- 2 ret = domain->ops->map(domain, virq, hwirq); .... } if (hwirq < domain->revmap_size) { // --- 3 domain->linear_revmap[hwirq] = virq; // for linear mapping } else { .... radix_tree_insert(&domain->revmap_tree, hwirq, irq_data); // for radix tree mapping .... } .... irq_clear_status_flags(virq, IRQ_NOREQUEST); // --- 4 return 0; }
(1) irq_domain_associate() 함수의 virq 는 IRQ number, hwirq 는 HW interrupt ID 를 의미한다. 이 2 개의 번호는 첫 번째 인자로 전달된 irq domain 에서 mapping 관계를 형성하게 된다. `irq_get_irq_data()` 함수는 인자로 IRQ number 를 받아서 이에 대응하는 struct irq_desc 를 찾아내고, struct irq_desc->irq_data 를 반환한다(gic_init_base() -> irq_alloc_descs() 함수에서 이미 IRQ number 16 을 시작으로 gic_irqs 개수만큼 struct irq_desc 를 생성했다).
(2) 여기서 중요한 내용이 나온다. `domain(struct irq_domain)->ops->map` 이 등록이 되어야 있어야 mapping 을 진행할 수 있다.(irq_domain 에 irq chip driver 에서 구현한 struct irq_domain_ops 를 연결하는 과정은 __irq_domain_add() 함수에서 이루어진다). 즉, hwirq 와 IRQ number 를 구체적으로 어떻게 mapping 시킬지는 driver-specific 하다는 뜻이다.
(3) driver-specific mapping 이 성공적으로 마무리되면, 이제 리눅스 커널에서 어떻게 hwirq 와 IRQ number 를 mapping 할지가 남았다. __irq_domain_add() 함수에서 size 가 0 이면 radix tree, 아니면 linear mapping 을 사용한다고 했다. 여기서 그 내용을 확인할 수 있다. `hwirq < domain->revmap_size` 에서 revmap_size 는 GICv2 같은 경우, hwirq_base(hwirq) + gic_irqs 를 저장한다. 즉, `hwirq - (hwirq_base) < revmap_size - (hwirq_base)` 를 하면, `0 < size(gic_irqs)` 가 된다. 결론적으로, GICv2 같은 경우는 `domain->linear_revmap[hwirq] = virq;` 를 실행한다고 볼 수 있다.
(4) 만약, 특정 IRQ number 에 IRQ_NOREQUEST 플래그가 설정되면, `request_irq()` 함수에서 사용할 수 없다. irq_clear_status_flags() 함수를 인자로 전달된 IRQ number 에 IRQ_NOREQUEST 를 해제하는 함수다. 즉, 이 시점에는 IRQ number 가 hwirq 와 mapping 되었으므로, high-level irq handler 에서 사용해도 된다는 것을 의미한다.
'Linux > kernel' 카테고리의 다른 글
[리눅스 커널] Interrupt - ARMv7 interrupt handling process (0) 2023.11.14 [리눅스 커널] Scheduler - introduction (0) 2023.11.10 [리눅스 커널] Kernel command-line parameters (0) 2023.11.06 [리눅스 커널] Performance - CPU optimization basic (0) 2023.11.06 [리눅스 커널] cache - basic (1) 2023.11.04