GIC , SPI , PPI (窩窩科技的文章題目改了下)
- 2019 年 10 月 11 日
- 筆記
轉載於: http://www.wowotech.net/irq_subsystem/gic-irq-chip-driver.html
GIC驅動程式碼分析(廢棄)
這份文檔狀態是:廢棄,新的文檔請訪問http://www.wowotech.net/linux_kenrel/gic_driver.html
一、前言
GIC(Generic Interrupt Controller)是ARM公司提供的一個通用的中斷控制器。GIC通過AMBA(Advanced Microcontroller Bus Architecture)這樣的片上匯流排連接到一個或者多個ARM processor上。本文主要分析了linux kernel中GIC中斷控制器的驅動程式碼。
具體的分析方法是按照source code為索引,逐段分析。對於每一段分析的程式碼,力求做到每個細節都清清楚楚。這不可避免要引入很多對GIC的硬體描述,此外,具體GIC中斷控制器的驅動程式碼和linux kernel中斷子系統的交互也會描述,但本文不會描述linux kernel的generic interrupt subsystem。
本文以OMAP4460這款SOC為例,OMAP4460內部集成了GIC的功能。具體的linux kernel的版本是linux3.14.。
二、GIC DTS描述
1、中斷系統概述
對於中斷系統,主要有三個角色:
(1)processor。主要用於處理中斷
(2)Interrupt Generating Device。通過硬體的interrupt line表明自身需要處理器的進一步處理(例如有數據到來、異常狀態等)
(3)interrupt controller。負責收集各個外設的非同步事件,用有序、可控的方式通知一個或者多個processor。
2、DTS如何描述Interrupt Generating Device
對於Interrupt Generating Device,我們需要定義下面兩個屬性:
(1)Interrupt屬性。該屬性主要描述了中斷的HW interrupt ID以及類型。
(2)interrupt-parent 屬性。該屬性主要描述了該設備的interrupt request line連接到哪一個interrupt controller。
在OMAP4460系統中,我們以一個簡單的串口為例子,具體的描述在linux-3.14archarmbootdtsomap4.dtsi文件中:
uart3: serial@48020000 { compatible = "ti,omap4-uart"; reg = <0x48020000 0x100="">; interrupts = <gic_spi 74="" irq_type_level_high=""><GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>; ti,hwmods = "uart3"; clock-frequency = <48000000>; };
對於uart3,interrupts屬性用3個cell(對於device tree,cell是指由32bit組成的一個資訊單位)表示。GIC_SPI 描述了interrupt type。對於GIC,它可以管理4種類型的中斷:
(1)外設中斷(Peripheral interrupt)。根據目標CPU的不同,外設的中斷可以分成PPI(Private Peripheral Interrupt)和SPI(Shared Peripheral Interrupt)。PPI只能分配給一個確定的processor,而SPI可以由Distributor將中斷分配給一組Processor中的一個進行處理。外設類型的中斷一般通過一個interrupt request line的硬體訊號線連接到中斷控制器,可能是電平觸發的(Level-sensitive),也可能是邊緣觸發的(Edge-triggered)。
(2)軟體觸發的中斷(SGI,Software-generated interrupt)。軟體可以通過寫GICD_SGIR暫存器來觸發一個中斷事件,這樣的中斷,可以用於processor之間的通訊。
(3)虛擬中斷(Virtual interrupt)和Maintenance interrupt。這兩種中斷和本文無關,不再贅述。
在DTS中,外設的interrupt type有兩種,一種是SPI,另外一種是PPI。SGI用於processor之間的通訊,和外設無關。
uart3的interrupt屬性中的74表示該外設使用的GIC interrupt ID號。GIC最大支援1020個HW interrupt ID,具體的ID分配情況如下:
(1)ID0~ID31是用於分發到一個特定的process的interrupt。標識這些interrupt不能僅僅依靠ID,因為各個interrupt source都用同樣的ID0~ID31來標識,因此識別這些interrupt需要interrupt ID + CPU interface number。ID0~ID15用於SGI,ID16~ID31用於PPI。PPI類型的中斷會送到指定的process上,和其他的process無關。SGI是通過寫GICD_SGIR暫存器而觸發的中斷。Distributor通過processor source ID、中斷ID和target processor ID來唯一識別一個SGI。
(2)ID32~ID1019用於SPI。
uart3的interrupt屬性中的IRQ_TYPE_LEVEL_HIGH用來描述觸發類型。
很奇怪,uart3並沒有定義interrupt-parent屬性,這裡定義interrupt-parent屬性的是root node,具體的描述在linux-3.14archarmbootdtsomap4.dtsi文件中:
/ { compatible = "ti,omap4430", "ti,omap4"; interrupt-parent = <&gic>; 略去無關內容 }
難道root node會產生中斷到interrupt controller嗎?當然不會,只不過如果一個能夠產生中斷的device node沒有定義interrupt-parent的話,其interrupt-parent屬性就是跟隨parent node。因此,與其在所有的下游設備中定義interrupt-parent,不如統一在root node中定義了。
3、DTS如何描述GIC
linux-3.14archarmbootdtsomap4.dtsi文件中,
gic: interrupt-controller@48241000 { compatible = "arm,cortex-a9-gic"; interrupt-controller; #interrupt-cells = <3>; reg = <0x48241000 0x1000="">, <0x48240100 0x0100="">; };
compatible屬性用來描述GIC的programming model。該屬性的值是string list,定義了一系列的modle(每個string是一個model)。這些字元串列表被作業系統用來選擇用哪一個driver來驅動該設備。假設定義該屬性:compatible = 「a廠商,p產品」, 「標準bbb類型設備」。那麼linux kernel可能首先使用「a廠商,p產品」來匹配適合的driver,如果沒有匹配到,那麼使用字元串「標準bbb類型設備」來繼續尋找適合的driver。compatible屬性有兩個應用場景:
(1)對於root node,compatible屬性是用來匹配machine type的(參考Device Tree相關文檔)
(2)對於普通的HW block的節點,例如interrupt-controller,compatible屬性是用來匹配適合的driver的。
interrupt-controller這個沒有定義value的屬性用來表明本設備節點就是一個interrupt controller。理解#interrupt-cells這個屬性需要理解interrupt specifier和interrupt domain這兩個概念。interrupt specifier其實就是外設interrupt的屬性值,對於uart3而言,其interrupt specifier就是<gic_spi 74="" irq_type_level_high=""><GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,也就是說,interrupt specifier定義了一個外設產生中斷的規格(HW interrupt ID + interrupt type)。具體如何解析interrupt specifier?這個需要限定在一定的上下文中,不同的interrupt controller會有不同的解釋。因此,對於一個包含多個interrupt controller的系統,每個interrupt controller及其相連的外設組成一個interrupt domain,各個外設的interrupt specifier只能在屬於它的那個interrupt domain中得到解析。#interrupt-cells定義了在該interrupt domain中,用多少個cell來描述一個外設的interrupt specifier。
reg屬性定義了GIC的memory map的地址,有兩段,分別描述了CPU interface和Distributor的地址段。理解CPU interface和Distributor這兩個術語需要GIC的一些基本的硬體知識,參考下節描述。
三、GIC的HW block diagram描述
1、GIC HW概述
GIC的block diagram如下圖所示:

GIC可以清晰的劃分成兩個block,一個block是Distributor(上圖的左邊的block),一個是CPU interface。CPU interface有兩種,一種就是和普通processor介面,另外一種是和虛擬機介面的。Virtual CPU interface在本文中不會詳細描述。
2、Distributor
Distributor的主要的作用是檢測各個interrupt source的狀態,控制各個interrupt source的行為,分發各個interrupt source產生的中斷事件到各個processor。Distributor對中斷的控制包括:
(1)中斷enable或者disable的控制。Distributor對中斷的控制分成兩個級別。一個是全局中斷的控制。一旦disable了全局的中斷,那麼任何的interrupt source產生的interrupt event都不會被傳遞到CPU interface。另外一個級別是對針對各個interrupt source進行控制,disable某一個interrupt source會導致該interrupt event不會分發到CPU interface,但不影響其他interrupt source產生interrupt event的分發。
(2)控制中斷事件分發到process。一個interrupt事件可以分發給一個process,也可以分發給若干個process。
(3)優先順序控制。
(3)interrupt屬性設定。例如是level-sensitive還是edge-triggered,是屬於group 0還是group 1。
Distributor可以管理若干個interrupt source,這些interrupt source用ID來標識,我們稱之interrupt ID。
2、CPU interface
CPU interface這個block主要用於和process進行介面。該block的主要功能包括:
(1)enable或者disable。對於ARM,CPU interface block和process之間的中斷訊號線是nIRQ和nFIQ這兩個signal。如果disable了中斷,那麼即便是Distributor分發了一個中斷事件到CPU interface,但是也不會assert指定的nIRQ或者nFIQ通知processor。
(2)ackowledging中斷。processor會向CPU interface block應答中斷,中斷一旦被應答,Distributor就會把該中斷的狀態從pending狀態修改成active。如果沒有後續pending的中斷,那麼CPU interface就會deassert nIRQ或者nFIQ的signal。如果在這個過程中又產生了新的中斷,那麼Distributor就會把該中斷的狀態從pending狀態修改成pending and active。這時候,CPU interface仍然會保持nIRQ或者nFIQ訊號的asserted狀態,也就是向processor signal下一個中斷。
(3)中斷處理完畢的通知。當interrupt handler處理完了一個中斷的時候,會向寫CPU interface的暫存器從而通知GIC CPU已經處理完該中斷。做這個動作一方面是通知Distributor將中斷狀態修改為deactive,另外一方面,如果一個中斷沒有完成處理,那麼後續比該中斷優先順序低的中斷不會assert到processor。一旦標記中斷處理完成,被block掉的那些比當前優先順序低的中斷就會遞交給processor。
(4)設定priority mask。通過priority mask,可以mask掉一些優先順序比較低的中斷,這些中斷不會通知到CPU。
(5)設定preemption的策略
(6)在多個中斷事件同時到來的時候,選擇一個優先順序最高的通知processor
四、系統啟動過程中,如何調用GIC driver的初始化函數
在linux-3.14driversirqchip目錄下保存在各種不同的中斷控制器的驅動程式碼,irq-gic.c就是GIC的驅動程式碼。
1、IRQCHIP_DECLARE宏定義
在linux-3.14driversirqchip目錄下的irqchip.h文件中定義了IRQCHIP_DECLARE宏如下:
#define IRQCHIP_DECLARE(name,compstr,fn) static const struct of_device_id irqchip_of_match_##name __used __section(__irqchip_of_table) = { .compatible = compstr, .data = fn }
這個宏就是被各個irq chip driver用來聲明其DT compatible string和初始化函數的對應關係的。IRQCHIP_DECLARE中的compstr就是DT compatible string,fun就是初始化函數。irq-gic.c文件中聲明的對應關係包括:
IRQCHIP_DECLARE(cortex_a15_gic, "arm,cortex-a15-gic", gic_of_init); IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init); IRQCHIP_DECLARE(msm_8660_qgic, "qcom,msm-8660-qgic", gic_of_init); IRQCHIP_DECLARE(msm_qgic2, "qcom,msm-qgic2", gic_of_init);
從上面的定義可以看出來,A9和A15的gic初始化函數都是gic_of_init。另外兩個定義是和高通的CPU相關,我猜測是高通使用了GIC,但是自己又做了一些簡單的修改,但無論如何,初始化函數都是一個,那就是gic_of_init。
在linux kernel編譯的時候,你可以配置多個irq chip進入內核,編譯系統會把所有的IRQCHIP_DECLARE宏定義的數據放入到一個特殊的section中(section name是__irqchip_of_table),我們稱這個特殊的section叫做irq chip table。這個table也就保存了kernel支援的所有的中斷控制器的驅動程式碼初始化函數和DT compatible string的對應關係。具體執行哪一個初始化函數是由bootloader傳遞給kernel的DTB決定的。
2、OMAP4460的machine定義
在linux-3.14/arch/arm/mach-omap2目錄下的board-generic.c的文件中定義了OMAP4460的machine如下:
DT_MACHINE_START(OMAP4_DT, "Generic OMAP4 (Flattened Device Tree)") 。。。。。。刪除無關程式碼 .init_irq = omap_gic_of_init, 。。。。。。刪除無關程式碼 MACHINE_END
在系統初始化的時候,會調用start_kernel->init_IRQ->machine_desc->init_irq()函數。
3、omap_gic_of_init過程分析
omap_gic_of_init的程式碼如下(刪除了無關程式碼):
void __init omap_gic_of_init(void) { irqchip_init(); }
在driver/irqchip/irqchip.c文件中定義了irqchip_init函數,如下:
void __init irqchip_init(void) { of_irq_init(__irqchip_begin); }
__irqchip_begin就是內核irq chip table的首地址,這個table也就保存了kernel支援的所有的中斷控制器的驅動程式碼初始化函數和DT compatible string的對應關係。of_irq_init函數scan bootloader傳遞來的DTB,找到所有的中斷控制器節點,並形成樹狀結構(系統可以有多個interrupt controller)。之後,從root interrupt controller開始,對於每一個interrupt controller,scan irq chip table,匹配DT compatible string,一旦匹配到,就調用該interrupt controller的初始化函數。具體的程式碼分析可以參考Device Tree程式碼分析文檔。
五、GIC driver初始化程式碼分析
gic_of_init的程式碼如下:
int __init gic_of_init(struct device_node *node, struct device_node *parent) { void __iomem *cpu_base; void __iomem *dist_base; u32 percpu_offset; int irq; if (WARN_ON(!node)) return -ENODEV; dist_base = of_iomap(node, 0);—————-映射GIC Distributor的暫存器地址空間 WARN(!dist_base, "unable to map gic dist registersn"); cpu_base = of_iomap(node, 1);—————-映射GIC CPU interface的暫存器地址空間 WARN(!cpu_base, "unable to map gic cpu registersn"); if (of_property_read_u32(node, "cpu-offset", &percpu_offset))——–處理cpu-offset binding。 percpu_offset = 0; gic_init_bases(gic_cnt, -1, dist_base, cpu_base, percpu_offset, node);))—–主處理過程,後面詳述 if (!gic_cnt) gic_init_physaddr(node); —–對於不支援big.LITTLE switcher(CONFIG_BL_SWITCHER)的系統,該函數為空。 if (parent) {——–處理interrupt級聯 irq = irq_of_parse_and_map(node, 0); gic_cascade_irq(gic_cnt, irq); } gic_cnt++; return 0; }
我們首先看看這個函數的參數,node參數代表需要初始化的那個interrupt controller的device node,parent參數指向其parent。對於cpu-offset binding,可以參考linux-3.14/Documentation/devicetree/bindings/arm/gic.txt文件中關於cpu-offset 的描述。對於OMAP4460,其GIC支援banked register,因此percpu_offset等於0。
gic_init_bases的程式碼如下:
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; BUG_ON(gic_nr >= MAX_GIC_NR); gic = &gic_data[gic_nr]; WARN(percpu_offset, "GIC_NON_BANKED not enabled, ignoring %08x offset!", percpu_offset); gic->dist_base.common_base = dist_base; gic->cpu_base.common_base = cpu_base; gic_set_base_accessor(gic, gic_get_common_base); /* * Initialize the CPU interface map to all CPUs. * It will be refined as each CPU probes its ID. */ for (i = 0; i < NR_GIC_CPU_IF; i++) gic_cpu_map[i] = 0xff; /* * For primary GICs, skip over SGIs. * For secondary GICs, skip over PPIs, too. */ if (gic_nr == 0 && (irq_start & 31) > 0) { hwirq_base = 16; if (irq_start != -1) irq_start = (irq_start & ~31) + 16; } else { hwirq_base = 32; } /* * Find out how many interrupts are supported. * The GIC only supports up to 1020 interrupt sources. */ gic_irqs = readl_relaxed(gic_data_dist_base(gic) + GIC_DIST_CTR) & 0x1f; gic_irqs = (gic_irqs + 1) * 32; if (gic_irqs > 1020) gic_irqs = 1020; gic->gic_irqs = gic_irqs; gic_irqs -= hwirq_base; /* calculate # of irqs to allocate */ irq_base = irq_alloc_descs(irq_start, 16, gic_irqs, numa_node_id()); if (IS_ERR_VALUE(irq_base)) { WARN(1, "Cannot allocate irq_descs @ IRQ%d, assuming pre-allocatedn", irq_start); irq_base = irq_start; } gic->domain = irq_domain_add_legacy(node, gic_irqs, irq_base, hwirq_base, &gic_irq_domain_ops, gic); if (WARN_ON(!gic->domain)) return; if (gic_nr == 0) { #ifdef CONFIG_SMP set_smp_cross_call(gic_raise_softirq);———設定raise SGI的方法 register_cpu_notifier(&gic_cpu_notifier);———在multi processor環境下,當其他processor online的時候,需要調用回調函數來初始化GIC的cpu interface。 #endif set_handle_irq(gic_handle_irq); } gic_chip.flags |= gic_arch_extn.flags; gic_dist_init(gic);———具體的硬體初始程式碼,不再贅述,可以參考GIC reference manual gic_cpu_init(gic); gic_pm_init(gic); }
這個函數的前半部分都是為了向系統中註冊一個irq domain的數據結構。為何需要struct irq_domain這樣一個數據結構呢?從linux kernel的角度來看,任何外部的設備的中斷都是一個非同步事件,kernel都需要識別這個事件。在內核中,用IRQ number來標識某一個設備的某個interrupt request。有了IRQ number就可以定位到該中斷的描述符(struct irq_desc)。但是,對於中斷控制器而言,它不並知道IRQ number,它只是知道HW interrupt number(中斷控制器會為其支援的interrupt source進行編碼,這個編碼被稱為Hardware interrupt number )。不同的軟體模組用不同的ID來識別interrupt source,這樣就需要映射了。如何將Hardware interrupt number 映射到IRQ number呢?這需要一個translation object,內核定義為struct irq_domain。
每個interrupt controller都會形成一個irq domain,負責解析其下游的interrut source。如果interrupt controller有級聯的情況,那麼一個非root interrupt controller的中斷控制器也是其parent irq domain的一個普通的interrupt source。struct irq_domain定義如下:
struct irq_domain { …… const struct irq_domain_ops *ops; void *host_data; …… };
這個數據結構是屬於linux kernel通用中斷子系統的一部分,我們這裡只是描述相關的數據成員。host_data成員是底層interrupt controller的私有數據,linux kernel通用中斷子系統不應該修改它。對於GIC而言,host_data成員指向一個struct gic_chip_data的數據結構,定義如下:
struct gic_chip_data { union gic_base dist_base;———GIC Distributor的地址空間 union gic_base cpu_base;———GIC CPU interface的地址空間 #ifdef CONFIG_CPU_PM———GIC 電源管理相關的成員 u32 saved_spi_enable[DIV_ROUND_UP(1020, 32)]; u32 saved_spi_conf[DIV_ROUND_UP(1020, 16)]; u32 saved_spi_target[DIV_ROUND_UP(1020, 4)]; u32 __percpu *saved_ppi_enable; u32 __percpu *saved_ppi_conf; #endif struct irq_domain *domain;———該GIC對應的irq domain數據結構 unsigned int gic_irqs;———GIC支援的IRQ的數目 #ifdef CONFIG_GIC_NON_BANKED void __iomem *(*get_base)(union gic_base *); #endif };
對於GIC支援的IRQ的數目,這裡還要贅述幾句。實際上並非GIC支援多少個HW interrupt ID,其就支援多少個IRQ。對於SGI,其處理比較特別,並不歸入IRQ number中。因此,對於GIC而言,其SGI(從0到15的那些HW interrupt ID)不需要irq domain進行映射處理,也就是說SGI沒有對應的IRQ number。如果系統越來越複雜,一個GIC不能支援所有的interrupt source(目前GIC支援1020個中斷源,這個數目已經非常的大了),那麼系統還需要引入secondary GIC,這個GIC主要負責擴展外設相關的interrupt source,也就是說,secondary GIC的SGI和PPI都變得冗餘了(這些功能,primary GIC已經提供了)。這些資訊可以協助理解程式碼中的hwirq_base的設定。
一個GIC支援的IRQ的數目可以通過其支援的HW interrupt的數目減去hwirq_base得到,那麼如何獲取GIC支援的HW interrupt的數目呢?GIC有一個暫存器叫做Interrupt Controller Type Register,通過這個暫存器可以獲得下列資訊:
(1)whether the GIC implements the Security Extensions (2)the maximum number of interrupt IDs that the GIC supports (3)the number of CPU interfaces implemented (4)if the GIC implements the Security Extensions, the maximum number of implemented Lockable Shared Peripheral Interrupts (LSPIs).
獲得了GIC支援的IRQ數目後,就可以調用irq_alloc_descs函數分配IRQ描述符資源了。之後,通過調用irq_domain_add_legacy函數註冊GIC的irq domain數據結構。這裡有一個重要的數據結構gic_irq_domain_ops,其類型是struct irq_domain_ops ,定義如下:
struct irq_domain_ops { int (*match)(struct irq_domain *d, struct device_node *node); int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw); void (*unmap)(struct irq_domain *d, unsigned int virq); int (*xlate)(struct irq_domain *d, struct device_node *node, const u32 *intspec, unsigned int intsize, unsigned long *out_hwirq, unsigned int *out_type); };
對於GIC,其irq domain的操作函數是gic_irq_domain_ops,定義如下:
const struct irq_domain_ops gic_irq_domain_ops = { .map = gic_irq_domain_map, .xlate = gic_irq_domain_xlate, };
irq domain的概念是一個通用中斷子系統的概念,在irq chip這個層次,我們需要和通用中斷子系統中的irq domain core code進行介面,gic_irq_domain_ops就是這個介面的定義。
(1)map成員函數:用來建立irq number和hw interrupt ID之間的聯繫 (2)Xlate成員函數:給定某個外設的device tree node 和interrupt specifier,該函數可以解碼出該設備使用的hw interrupt ID和linux irq type value
具體向系統註冊irq domain是通過irq_domain_add_legacy函數進行的:
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, first_hwirq + size, 0, ops, host_data); if (!domain) return NULL; irq_domain_associate_many(domain, first_irq, first_hwirq, size); return domain; }
這個函數主要進行兩個步驟,一個是通過__irq_domain_add將GIC對應的irq domain加入到系統,另外一個就是調用irq_domain_associate_many來建立IRQ number和hw interrupt ID之間的關係。而這個關係的建立實際上是通過irq domain的操作函數中的map回調函數進行的,對於GIC而言就是gic_irq_domain_map,程式碼如下:
static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq, irq_hw_number_t hw) { if (hw < 32) { irq_set_percpu_devid(irq); irq_set_chip_and_handler(irq, &gic_chip, handle_percpu_devid_irq); set_irq_flags(irq, IRQF_VALID | IRQF_NOAUTOEN); } else { irq_set_chip_and_handler(irq, &gic_chip, handle_fasteoi_irq); set_irq_flags(irq, IRQF_VALID | IRQF_PROBE); } irq_set_chip_data(irq, d->host_data); return 0; }
這個函數的主要功能就是把一個IRQ號(參數中的unsigned int irq)和一個hw interrupt ID(參數中的irq_hw_number_t hw)聯繫起來。每個IRQ都有其對應的描述符,用struct irq_desc表示:
struct irq_desc { struct irq_data irq_data; irq_flow_handler_t handle_irq; …… } __
函數irq_set_chip_and_handler其實主要就是設定struct irq_desc中的irq_data和handle_irq成員。handle_irq就是發生了一個IRQ後需要調用的中斷處理函數。irq_data數據結構中的chip成員就是指向具體的硬體資訊,也就是GIC的描述符。我們定義如下:
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, };
struct irq_chip數據結構中有各種各樣的操作函數,例如enable或者disable一個中斷,ack一個中斷等。
綜上所述,經過了初始化過程之後,對於linux的記憶體管理系統而言,增加了GIC的地址空間映射。對於linux kernel的generic interrupt subsystem,增加了若干個IRQ的描述符,並且設定了這些IRQ描述符的處理函數以及相關的irq_data數據結構(和具體HW相關,該數據結構的成員包括抽象具體硬體的interrupt controller的描述符以及irq domain數據結構)。
六、GIC硬體操作
1、gic_mask_irq函數
這個函數用來mask一個interrupt source。程式碼如下:
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 Registers(具體數目是和GIC支援的hw interrupt數目相關,我們前面說過的,GIC是一個高度可配置的interrupt controller)。這些Interrupt Clear-Enable Registers暫存器的每個bit可以控制一個interrupt source是否forward到CPU interface,寫入1表示Distributor不再forward該interrupt,因此CPU也就感知不到該中斷,也就是mask了該中斷。特別需要注意的是:寫入0無效,而不是unmask的操作。
由於不同的SOC廠商在集成GIC的時候可能會修改,也就是說,也有可能mask的程式碼要微調,這是通過gic_arch_extn這個全局變數實現的。在gic-irq.c中這個變數的全部成員都設定為NULL,各個廠商在初始中斷控制器的時候可以設定其特定的操作函數。
2、gic_unmask_irq函數
這個函數用來unmask一個interrupt source。程式碼如下:
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 Registers的暫存器。這些暫存器的每個bit可以控制一個interrupt source。當寫入1的時候,表示Distributor會forward該interrupt到CPU interface,也就是意味這unmask了該中斷。特別需要注意的是:寫入0無效,而不是mask的操作。
3、gic_eoi_irq函數
當processor處理中斷的時候就會調用這個函數用來結束中斷處理。程式碼如下:
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); }
對於GIC而言,其中斷狀態有四種:
中斷狀態 |
描述 |
---|---|
Inactive |
中斷未觸髮狀態,該中斷即沒有Pending也沒有Active |
Pending |
由於外設硬體產生了中斷事件(或者軟體觸發)該中斷事件已經通過硬體訊號通知到GIC,等待GIC分配的那個CPU進行處理 |
Active |
CPU已經應答(acknowledge)了該interrupt請求,並且正在處理中 |
Active and Pending |
當一個中斷源處於Active狀態的時候,同一中斷源又觸發了中斷,進入pending狀態 |
processor ack了一個中斷後,該中斷會被設定為active。當處理完成後,仍然要通知GIC,中斷已經處理完畢了。這時候,如果沒有pending的中斷,GIC就會將該interrupt設定為inactive狀態。操作GIC中的End of Interrupt Register可以完成end of interrupt事件通知。
4、gic_set_type函數
這個函數用來設定一個interrupt source的type,例如是level sensitive還是edge triggered。程式碼如下:
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); u32 enablemask = 1 << (gicirq % 32); u32 enableoff = (gicirq / 32) * 4; u32 confmask = 0x2 << ((gicirq % 16) * 2); u32 confoff = (gicirq / 16) * 4; bool enabled = false; u32 val; /* 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); val = readl_relaxed(base + GIC_DIST_CONFIG + confoff); if (type == IRQ_TYPE_LEVEL_HIGH) val &= ~confmask; else if (type == IRQ_TYPE_EDGE_RISING) val |= confmask; /* * As recommended by the spec, disable the interrupt before changing * the configuration */ if (readl_relaxed(base + GIC_DIST_ENABLE_SET + enableoff) & enablemask) { writel_relaxed(enablemask, base + GIC_DIST_ENABLE_CLEAR + enableoff); enabled = true; } writel_relaxed(val, base + GIC_DIST_CONFIG + confoff); if (enabled) writel_relaxed(enablemask, base + GIC_DIST_ENABLE_SET + enableoff); raw_spin_unlock(&irq_controller_lock); return 0; }
對於SGI類型的interrupt,是不能修改其type的,因為GIC中SGI固定就是edge-triggered。對於GIC,其type只支援高電平觸發(IRQ_TYPE_LEVEL_HIGH)和上升沿觸發(IRQ_TYPE_EDGE_RISING)的中斷。另外需要注意的是,在更改其type的時候,先disable,然後修改type,然後再enable。
5、gic_retrigger
這個介面用來resend一個IRQ到CPU。
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; }
看起來這是功能不是通用GIC擁有的功能,各個廠家在集成GIC的時候,有可能進行功能擴展。
6、gic_set_affinity
在多處理器的環境下,外部設備產生了一個中斷就需要送到一個或者多個處理器去,這個設定是通過設定處理器的affinity進行的。具體程式碼如下:
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 shift = (gic_irq(d) % 4) * 8; unsigned int cpu = cpumask_any_and(mask_val, cpu_online_mask); u32 val, mask, bit; 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; }
GIC Distributor中有一個暫存器叫做Interrupt Processor Targets Registers,這個暫存器用來設訂製定的中斷送到哪個process去。由於GIC最大支援8個process,因此每個hw interrupt ID需要8個bit來表示送達的process。每一個Interrupt Processor Targets Registers由32個bit組成,因此每個Interrupt Processor Targets Registers可以表示4個HW interrupt ID的affinity。
7、gic_set_wake
這個介面用來設定喚醒CPU的interrupt source。對於GIC,程式碼如下:
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; }
設定喚醒的interrupt和具體的廠商相關,這裡不再贅述。
原創文章,轉發請註明出處。蝸窩科技,www.wowotech.net。
GIC驅動程式碼分析(廢棄)
作者:linuxer 發佈於:2014-7-4 14:34 分類:中斷子系統
這份文檔狀態是:廢棄,新的文檔請訪問http://www.wowotech.net/linux_kenrel/gic_driver.html
一、前言
GIC(Generic Interrupt Controller)是ARM公司提供的一個通用的中斷控制器。GIC通過AMBA(Advanced Microcontroller Bus Architecture)這樣的片上匯流排連接到一個或者多個ARM processor上。本文主要分析了linux kernel中GIC中斷控制器的驅動程式碼。
具體的分析方法是按照source code為索引,逐段分析。對於每一段分析的程式碼,力求做到每個細節都清清楚楚。這不可避免要引入很多對GIC的硬體描述,此外,具體GIC中斷控制器的驅動程式碼和linux kernel中斷子系統的交互也會描述,但本文不會描述linux kernel的generic interrupt subsystem。
本文以OMAP4460這款SOC為例,OMAP4460內部集成了GIC的功能。具體的linux kernel的版本是linux3.14.。
二、GIC DTS描述
1、中斷系統概述
對於中斷系統,主要有三個角色:
(1)processor。主要用於處理中斷
(2)Interrupt Generating Device。通過硬體的interrupt line表明自身需要處理器的進一步處理(例如有數據到來、異常狀態等)
(3)interrupt controller。負責收集各個外設的非同步事件,用有序、可控的方式通知一個或者多個processor。
2、DTS如何描述Interrupt Generating Device
對於Interrupt Generating Device,我們需要定義下面兩個屬性:
(1) Interrupt屬性。該屬性主要描述了中斷的HW interrupt ID以及類型。
(2)interrupt-parent 屬性。該屬性主要描述了該設備的interrupt request line連接到哪一個interrupt controller。
在OMAP4460系統中,我們以一個簡單的串口為例子,具體的描述在linux-3.14archarmbootdtsomap4.dtsi文件中:
uart3: serial@48020000 { compatible = "ti,omap4-uart"; reg = <0x48020000 0x100="">; interrupts = <gic_spi 74="" irq_type_level_high=""><GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>; ti,hwmods = "uart3"; clock-frequency = <48000000>; };
對於uart3,interrupts屬性用3個cell(對於device tree,cell是指由32bit組成的一個資訊單位)表示。GIC_SPI 描述了interrupt type。對於GIC,它可以管理4種類型的中斷:
(1)外設中斷(Peripheral interrupt)。根據目標CPU的不同,外設的中斷可以分成PPI(Private Peripheral Interrupt)和SPI(Shared Peripheral Interrupt)。PPI只能分配給一個確定的processor,而SPI可以由Distributor將中斷分配給一組Processor中的一個進行處理。外設類型的中斷一般通過一個interrupt request line的硬體訊號線連接到中斷控制器,可能是電平觸發的(Level-sensitive),也可能是邊緣觸發的(Edge-triggered)。
(2)軟體觸發的中斷(SGI,Software-generated interrupt)。軟體可以通過寫GICD_SGIR暫存器來觸發一個中斷事件,這樣的中斷,可以用於processor之間的通訊。
(3)虛擬中斷(Virtual interrupt)和Maintenance interrupt。這兩種中斷和本文無關,不再贅述。
在DTS中,外設的interrupt type有兩種,一種是SPI,另外一種是PPI。SGI用於processor之間的通訊,和外設無關。
uart3的interrupt屬性中的74表示該外設使用的GIC interrupt ID號。GIC最大支援1020個HW interrupt ID,具體的ID分配情況如下:
(1)ID0~ID31是用於分發到一個特定的process的interrupt。標識這些interrupt不能僅僅依靠ID,因為各個interrupt source都用同樣的ID0~ID31來標識,因此識別這些interrupt需要interrupt ID + CPU interface number。ID0~ID15用於SGI,ID16~ID31用於PPI。PPI類型的中斷會送到指定的process上,和其他的process無關。SGI是通過寫GICD_SGIR暫存器而觸發的中斷。Distributor通過processor source ID、中斷ID和target processor ID來唯一識別一個SGI。
(2)ID32~ID1019用於SPI。
uart3的interrupt屬性中的IRQ_TYPE_LEVEL_HIGH用來描述觸發類型。
很奇怪,uart3並沒有定義interrupt-parent屬性,這裡定義interrupt-parent屬性的是root node,具體的描述在linux-3.14archarmbootdtsomap4.dtsi文件中:
/ { compatible = "ti,omap4430", "ti,omap4"; interrupt-parent = <&gic>; 略去無關內容 }
難道root node會產生中斷到interrupt controller嗎?當然不會,只不過如果一個能夠產生中斷的device node沒有定義interrupt-parent的話,其interrupt-parent屬性就是跟隨parent node。因此,與其在所有的下游設備中定義interrupt-parent,不如統一在root node中定義了。
3、DTS如何描述GIC
linux-3.14archarmbootdtsomap4.dtsi文件中,
gic: interrupt-controller@48241000 { compatible = "arm,cortex-a9-gic"; interrupt-controller; #interrupt-cells = <3>; reg = <0x48241000 0x1000="">, <0x48240100 0x0100="">; };
compatible屬性用來描述GIC的programming model。該屬性的值是string list,定義了一系列的modle(每個string是一個model)。這些字元串列表被作業系統用來選擇用哪一個driver來驅動該設備。假設定義該屬性:compatible = 「a廠商,p產品」, 「標準bbb類型設備」。那麼linux kernel可能首先使用「a廠商,p產品」來匹配適合的driver,如果沒有匹配到,那麼使用字元串「標準bbb類型設備」來繼續尋找適合的driver。compatible屬性有兩個應用場景:
(1)對於root node,compatible屬性是用來匹配machine type的(參考Device Tree相關文檔)
(2)對於普通的HW block的節點,例如interrupt-controller,compatible屬性是用來匹配適合的driver的。
interrupt-controller這個沒有定義value的屬性用來表明本設備節點就是一個interrupt controller。理解#interrupt-cells這個屬性需要理解interrupt specifier和interrupt domain這兩個概念。interrupt specifier其實就是外設interrupt的屬性值,對於uart3而言,其interrupt specifier就是<gic_spi 74="" irq_type_level_high=""><GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,也就是說,interrupt specifier定義了一個外設產生中斷的規格(HW interrupt ID + interrupt type)。具體如何解析interrupt specifier?這個需要限定在一定的上下文中,不同的interrupt controller會有不同的解釋。因此,對於一個包含多個interrupt controller的系統,每個interrupt controller及其相連的外設組成一個interrupt domain,各個外設的interrupt specifier只能在屬於它的那個interrupt domain中得到解析。#interrupt-cells定義了在該interrupt domain中,用多少個cell來描述一個外設的interrupt specifier。
reg屬性定義了GIC的memory map的地址,有兩段,分別描述了CPU interface和Distributor的地址段。理解CPU interface和Distributor這兩個術語需要GIC的一些基本的硬體知識,參考下節描述。
三、GIC的HW block diagram描述
1、GIC HW概述
GIC的block diagram如下圖所示:

GIC可以清晰的劃分成兩個block,一個block是Distributor(上圖的左邊的block),一個是CPU interface。CPU interface有兩種,一種就是和普通processor介面,另外一種是和虛擬機介面的。Virtual CPU interface在本文中不會詳細描述。
2、Distributor
Distributor的主要的作用是檢測各個interrupt source的狀態,控制各個interrupt source的行為,分發各個interrupt source產生的中斷事件到各個processor。Distributor對中斷的控制包括:
(1)中斷enable或者disable的控制。Distributor對中斷的控制分成兩個級別。一個是全局中斷的控制。一旦disable了全局的中斷,那麼任何的interrupt source產生的interrupt event都不會被傳遞到CPU interface。另外一個級別是對針對各個interrupt source進行控制,disable某一個interrupt source會導致該interrupt event不會分發到CPU interface,但不影響其他interrupt source產生interrupt event的分發。
(2)控制中斷事件分發到process。一個interrupt事件可以分發給一個process,也可以分發給若干個process。
(3)優先順序控制。
(3)interrupt屬性設定。例如是level-sensitive還是edge-triggered,是屬於group 0還是group 1。
Distributor可以管理若干個interrupt source,這些interrupt source用ID來標識,我們稱之interrupt ID。
2、CPU interface
CPU interface這個block主要用於和process進行介面。該block的主要功能包括:
(1)enable或者disable。對於ARM,CPU interface block和process之間的中斷訊號線是nIRQ和nFIQ這兩個signal。如果disable了中斷,那麼即便是Distributor分發了一個中斷事件到CPU interface,但是也不會assert指定的nIRQ或者nFIQ通知processor。
(2)ackowledging中斷。processor會向CPU interface block應答中斷,中斷一旦被應答,Distributor就會把該中斷的狀態從pending狀態修改成active。如果沒有後續pending的中斷,那麼CPU interface就會deassert nIRQ或者nFIQ的signal。如果在這個過程中又產生了新的中斷,那麼Distributor就會把該中斷的狀態從pending狀態修改成pending and active。這時候,CPU interface仍然會保持nIRQ或者nFIQ訊號的asserted狀態,也就是向processor signal下一個中斷。
(3)中斷處理完畢的通知。當interrupt handler處理完了一個中斷的時候,會向寫CPU interface的暫存器從而通知GIC CPU已經處理完該中斷。做這個動作一方面是通知Distributor將中斷狀態修改為deactive,另外一方面,如果一個中斷沒有完成處理,那麼後續比該中斷優先順序低的中斷不會assert到processor。一旦標記中斷處理完成,被block掉的那些比當前優先順序低的中斷就會遞交給processor。
(4)設定priority mask。通過priority mask,可以mask掉一些優先順序比較低的中斷,這些中斷不會通知到CPU。
(5)設定preemption的策略
(6)在多個中斷事件同時到來的時候,選擇一個優先順序最高的通知processor
四、系統啟動過程中,如何調用GIC driver的初始化函數
在linux-3.14driversirqchip目錄下保存在各種不同的中斷控制器的驅動程式碼,irq-gic.c就是GIC的驅動程式碼。
1、IRQCHIP_DECLARE宏定義
在linux-3.14driversirqchip目錄下的irqchip.h文件中定義了IRQCHIP_DECLARE宏如下:
#define IRQCHIP_DECLARE(name,compstr,fn) static const struct of_device_id irqchip_of_match_##name __used __section(__irqchip_of_table) = { .compatible = compstr, .data = fn }
這個宏就是被各個irq chip driver用來聲明其DT compatible string和初始化函數的對應關係的。IRQCHIP_DECLARE中的compstr就是DT compatible string,fun就是初始化函數。irq-gic.c文件中聲明的對應關係包括:
IRQCHIP_DECLARE(cortex_a15_gic, "arm,cortex-a15-gic", gic_of_init); IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init); IRQCHIP_DECLARE(msm_8660_qgic, "qcom,msm-8660-qgic", gic_of_init); IRQCHIP_DECLARE(msm_qgic2, "qcom,msm-qgic2", gic_of_init);
從上面的定義可以看出來,A9和A15的gic初始化函數都是gic_of_init。另外兩個定義是和高通的CPU相關,我猜測是高通使用了GIC,但是自己又做了一些簡單的修改,但無論如何,初始化函數都是一個,那就是gic_of_init。
在linux kernel編譯的時候,你可以配置多個irq chip進入內核,編譯系統會把所有的IRQCHIP_DECLARE宏定義的數據放入到一個特殊的section中(section name是__irqchip_of_table),我們稱這個特殊的section叫做irq chip table。這個table也就保存了kernel支援的所有的中斷控制器的驅動程式碼初始化函數和DT compatible string的對應關係。具體執行哪一個初始化函數是由bootloader傳遞給kernel的DTB決定的。
2、OMAP4460的machine定義
在linux-3.14/arch/arm/mach-omap2目錄下的board-generic.c的文件中定義了OMAP4460的machine如下:
DT_MACHINE_START(OMAP4_DT, "Generic OMAP4 (Flattened Device Tree)") 。。。。。。刪除無關程式碼 .init_irq = omap_gic_of_init, 。。。。。。刪除無關程式碼 MACHINE_END
在系統初始化的時候,會調用start_kernel->init_IRQ->machine_desc->init_irq()函數。
3、omap_gic_of_init過程分析
omap_gic_of_init的程式碼如下(刪除了無關程式碼):
void __init omap_gic_of_init(void) { irqchip_init(); }
在driver/irqchip/irqchip.c文件中定義了irqchip_init函數,如下:
void __init irqchip_init(void) { of_irq_init(__irqchip_begin); }
__irqchip_begin就是內核irq chip table的首地址,這個table也就保存了kernel支援的所有的中斷控制器的驅動程式碼初始化函數和DT compatible string的對應關係。of_irq_init函數scan bootloader傳遞來的DTB,找到所有的中斷控制器節點,並形成樹狀結構(系統可以有多個interrupt controller)。之後,從root interrupt controller開始,對於每一個interrupt controller,scan irq chip table,匹配DT compatible string,一旦匹配到,就調用該interrupt controller的初始化函數。具體的程式碼分析可以參考Device Tree程式碼分析文檔。
五、GIC driver初始化程式碼分析
gic_of_init的程式碼如下:
int __init gic_of_init(struct device_node *node, struct device_node *parent) { void __iomem *cpu_base; void __iomem *dist_base; u32 percpu_offset; int irq; if (WARN_ON(!node)) return -ENODEV; dist_base = of_iomap(node, 0);—————-映射GIC Distributor的暫存器地址空間 WARN(!dist_base, "unable to map gic dist registersn"); cpu_base = of_iomap(node, 1);—————-映射GIC CPU interface的暫存器地址空間 WARN(!cpu_base, "unable to map gic cpu registersn"); if (of_property_read_u32(node, "cpu-offset", &percpu_offset))——–處理cpu-offset binding。 percpu_offset = 0; gic_init_bases(gic_cnt, -1, dist_base, cpu_base, percpu_offset, node);))—–主處理過程,後面詳述 if (!gic_cnt) gic_init_physaddr(node); —–對於不支援big.LITTLE switcher(CONFIG_BL_SWITCHER)的系統,該函數為空。 if (parent) {——–處理interrupt級聯 irq = irq_of_parse_and_map(node, 0); gic_cascade_irq(gic_cnt, irq); } gic_cnt++; return 0; }
我們首先看看這個函數的參數,node參數代表需要初始化的那個interrupt controller的device node,parent參數指向其parent。對於cpu-offset binding,可以參考linux-3.14/Documentation/devicetree/bindings/arm/gic.txt文件中關於cpu-offset 的描述。對於OMAP4460,其GIC支援banked register,因此percpu_offset等於0。
gic_init_bases的程式碼如下:
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; BUG_ON(gic_nr >= MAX_GIC_NR); gic = &gic_data[gic_nr]; WARN(percpu_offset, "GIC_NON_BANKED not enabled, ignoring %08x offset!", percpu_offset); gic->dist_base.common_base = dist_base; gic->cpu_base.common_base = cpu_base; gic_set_base_accessor(gic, gic_get_common_base); /* * Initialize the CPU interface map to all CPUs. * It will be refined as each CPU probes its ID. */ for (i = 0; i < NR_GIC_CPU_IF; i++) gic_cpu_map[i] = 0xff; /* * For primary GICs, skip over SGIs. * For secondary GICs, skip over PPIs, too. */ if (gic_nr == 0 && (irq_start & 31) > 0) { hwirq_base = 16; if (irq_start != -1) irq_start = (irq_start & ~31) + 16; } else { hwirq_base = 32; } /* * Find out how many interrupts are supported. * The GIC only supports up to 1020 interrupt sources. */ gic_irqs = readl_relaxed(gic_data_dist_base(gic) + GIC_DIST_CTR) & 0x1f; gic_irqs = (gic_irqs + 1) * 32; if (gic_irqs > 1020) gic_irqs = 1020; gic->gic_irqs = gic_irqs; gic_irqs -= hwirq_base; /* calculate # of irqs to allocate */ irq_base = irq_alloc_descs(irq_start, 16, gic_irqs, numa_node_id()); if (IS_ERR_VALUE(irq_base)) { WARN(1, "Cannot allocate irq_descs @ IRQ%d, assuming pre-allocatedn", irq_start); irq_base = irq_start; } gic->domain = irq_domain_add_legacy(node, gic_irqs, irq_base, hwirq_base, &gic_irq_domain_ops, gic); if (WARN_ON(!gic->domain)) return; if (gic_nr == 0) { #ifdef CONFIG_SMP set_smp_cross_call(gic_raise_softirq);———設定raise SGI的方法 register_cpu_notifier(&gic_cpu_notifier);———在multi processor環境下,當其他processor online的時候,需要調用回調函數來初始化GIC的cpu interface。 #endif set_handle_irq(gic_handle_irq); } gic_chip.flags |= gic_arch_extn.flags; gic_dist_init(gic);———具體的硬體初始程式碼,不再贅述,可以參考GIC reference manual gic_cpu_init(gic); gic_pm_init(gic); }
這個函數的前半部分都是為了向系統中註冊一個irq domain的數據結構。為何需要struct irq_domain這樣一個數據結構呢?從linux kernel的角度來看,任何外部的設備的中斷都是一個非同步事件,kernel都需要識別這個事件。在內核中,用IRQ number來標識某一個設備的某個interrupt request。有了IRQ number就可以定位到該中斷的描述符(struct irq_desc)。但是,對於中斷控制器而言,它不並知道IRQ number,它只是知道HW interrupt number(中斷控制器會為其支援的interrupt source進行編碼,這個編碼被稱為Hardware interrupt number )。不同的軟體模組用不同的ID來識別interrupt source,這樣就需要映射了。如何將Hardware interrupt number 映射到IRQ number呢?這需要一個translation object,內核定義為struct irq_domain。
每個interrupt controller都會形成一個irq domain,負責解析其下游的interrut source。如果interrupt controller有級聯的情況,那麼一個非root interrupt controller的中斷控制器也是其parent irq domain的一個普通的interrupt source。struct irq_domain定義如下:
struct irq_domain { …… const struct irq_domain_ops *ops; void *host_data; …… };
這個數據結構是屬於linux kernel通用中斷子系統的一部分,我們這裡只是描述相關的數據成員。host_data成員是底層interrupt controller的私有數據,linux kernel通用中斷子系統不應該修改它。對於GIC而言,host_data成員指向一個struct gic_chip_data的數據結構,定義如下:
struct gic_chip_data { union gic_base dist_base;———GIC Distributor的地址空間 union gic_base cpu_base;———GIC CPU interface的地址空間 #ifdef CONFIG_CPU_PM———GIC 電源管理相關的成員 u32 saved_spi_enable[DIV_ROUND_UP(1020, 32)]; u32 saved_spi_conf[DIV_ROUND_UP(1020, 16)]; u32 saved_spi_target[DIV_ROUND_UP(1020, 4)]; u32 __percpu *saved_ppi_enable; u32 __percpu *saved_ppi_conf; #endif struct irq_domain *domain;———該GIC對應的irq domain數據結構 unsigned int gic_irqs;———GIC支援的IRQ的數目 #ifdef CONFIG_GIC_NON_BANKED void __iomem *(*get_base)(union gic_base *); #endif };
對於GIC支援的IRQ的數目,這裡還要贅述幾句。實際上並非GIC支援多少個HW interrupt ID,其就支援多少個IRQ。對於SGI,其處理比較特別,並不歸入IRQ number中。因此,對於GIC而言,其SGI(從0到15的那些HW interrupt ID)不需要irq domain進行映射處理,也就是說SGI沒有對應的IRQ number。如果系統越來越複雜,一個GIC不能支援所有的interrupt source(目前GIC支援1020個中斷源,這個數目已經非常的大了),那麼系統還需要引入secondary GIC,這個GIC主要負責擴展外設相關的interrupt source,也就是說,secondary GIC的SGI和PPI都變得冗餘了(這些功能,primary GIC已經提供了)。這些資訊可以協助理解程式碼中的hwirq_base的設定。
一個GIC支援的IRQ的數目可以通過其支援的HW interrupt的數目減去hwirq_base得到,那麼如何獲取GIC支援的HW interrupt的數目呢?GIC有一個暫存器叫做Interrupt Controller Type Register,通過這個暫存器可以獲得下列資訊:
(1)whether the GIC implements the Security Extensions (2)the maximum number of interrupt IDs that the GIC supports (3)the number of CPU interfaces implemented (4)if the GIC implements the Security Extensions, the maximum number of implemented Lockable Shared Peripheral Interrupts (LSPIs).
獲得了GIC支援的IRQ數目後,就可以調用irq_alloc_descs函數分配IRQ描述符資源了。之後,通過調用irq_domain_add_legacy函數註冊GIC的irq domain數據結構。這裡有一個重要的數據結構gic_irq_domain_ops,其類型是struct irq_domain_ops ,定義如下:
struct irq_domain_ops { int (*match)(struct irq_domain *d, struct device_node *node); int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw); void (*unmap)(struct irq_domain *d, unsigned int virq); int (*xlate)(struct irq_domain *d, struct device_node *node, const u32 *intspec, unsigned int intsize, unsigned long *out_hwirq, unsigned int *out_type); };
對於GIC,其irq domain的操作函數是gic_irq_domain_ops,定義如下:
const struct irq_domain_ops gic_irq_domain_ops = { .map = gic_irq_domain_map, .xlate = gic_irq_domain_xlate, };
irq domain的概念是一個通用中斷子系統的概念,在irq chip這個層次,我們需要和通用中斷子系統中的irq domain core code進行介面,gic_irq_domain_ops就是這個介面的定義。
(1)map成員函數:用來建立irq number和hw interrupt ID之間的聯繫 (2)Xlate成員函數:給定某個外設的device tree node 和interrupt specifier,該函數可以解碼出該設備使用的hw interrupt ID和linux irq type value
具體向系統註冊irq domain是通過irq_domain_add_legacy函數進行的:
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, first_hwirq + size, 0, ops, host_data); if (!domain) return NULL; irq_domain_associate_many(domain, first_irq, first_hwirq, size); return domain; }
這個函數主要進行兩個步驟,一個是通過__irq_domain_add將GIC對應的irq domain加入到系統,另外一個就是調用irq_domain_associate_many來建立IRQ number和hw interrupt ID之間的關係。而這個關係的建立實際上是通過irq domain的操作函數中的map回調函數進行的,對於GIC而言就是gic_irq_domain_map,程式碼如下:
static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq, irq_hw_number_t hw) { if (hw < 32) { irq_set_percpu_devid(irq); irq_set_chip_and_handler(irq, &gic_chip, handle_percpu_devid_irq); set_irq_flags(irq, IRQF_VALID | IRQF_NOAUTOEN); } else { irq_set_chip_and_handler(irq, &gic_chip, handle_fasteoi_irq); set_irq_flags(irq, IRQF_VALID | IRQF_PROBE); } irq_set_chip_data(irq, d->host_data); return 0; }
這個函數的主要功能就是把一個IRQ號(參數中的unsigned int irq)和一個hw interrupt ID(參數中的irq_hw_number_t hw)聯繫起來。每個IRQ都有其對應的描述符,用struct irq_desc表示:
struct irq_desc { struct irq_data irq_data; irq_flow_handler_t handle_irq; …… } __
函數irq_set_chip_and_handler其實主要就是設定struct irq_desc中的irq_data和handle_irq成員。handle_irq就是發生了一個IRQ後需要調用的中斷處理函數。irq_data數據結構中的chip成員就是指向具體的硬體資訊,也就是GIC的描述符。我們定義如下:
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, };
struct irq_chip數據結構中有各種各樣的操作函數,例如enable或者disable一個中斷,ack一個中斷等。
綜上所述,經過了初始化過程之後,對於linux的記憶體管理系統而言,增加了GIC的地址空間映射。對於linux kernel的generic interrupt subsystem,增加了若干個IRQ的描述符,並且設定了這些IRQ描述符的處理函數以及相關的irq_data數據結構(和具體HW相關,該數據結構的成員包括抽象具體硬體的interrupt controller的描述符以及irq domain數據結構)。
六、GIC硬體操作
1、gic_mask_irq函數
這個函數用來mask一個interrupt source。程式碼如下:
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 Registers(具體數目是和GIC支援的hw interrupt數目相關,我們前面說過的,GIC是一個高度可配置的interrupt controller)。這些Interrupt Clear-Enable Registers暫存器的每個bit可以控制一個interrupt source是否forward到CPU interface,寫入1表示Distributor不再forward該interrupt,因此CPU也就感知不到該中斷,也就是mask了該中斷。特別需要注意的是:寫入0無效,而不是unmask的操作。
由於不同的SOC廠商在集成GIC的時候可能會修改,也就是說,也有可能mask的程式碼要微調,這是通過gic_arch_extn這個全局變數實現的。在gic-irq.c中這個變數的全部成員都設定為NULL,各個廠商在初始中斷控制器的時候可以設定其特定的操作函數。
2、gic_unmask_irq函數
這個函數用來unmask一個interrupt source。程式碼如下:
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 Registers的暫存器。這些暫存器的每個bit可以控制一個interrupt source。當寫入1的時候,表示Distributor會forward該interrupt到CPU interface,也就是意味這unmask了該中斷。特別需要注意的是:寫入0無效,而不是mask的操作。
3、gic_eoi_irq函數
當processor處理中斷的時候就會調用這個函數用來結束中斷處理。程式碼如下:
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); }
對於GIC而言,其中斷狀態有四種:
中斷狀態 |
描述 |
---|---|
Inactive |
中斷未觸髮狀態,該中斷即沒有Pending也沒有Active |
Pending |
由於外設硬體產生了中斷事件(或者軟體觸發)該中斷事件已經通過硬體訊號通知到GIC,等待GIC分配的那個CPU進行處理 |
Active |
CPU已經應答(acknowledge)了該interrupt請求,並且正在處理中 |
Active and Pending |
當一個中斷源處於Active狀態的時候,同一中斷源又觸發了中斷,進入pending狀態 |
processor ack了一個中斷後,該中斷會被設定為active。當處理完成後,仍然要通知GIC,中斷已經處理完畢了。這時候,如果沒有pending的中斷,GIC就會將該interrupt設定為inactive狀態。操作GIC中的End of Interrupt Register可以完成end of interrupt事件通知。
4、gic_set_type函數
這個函數用來設定一個interrupt source的type,例如是level sensitive還是edge triggered。程式碼如下:
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); u32 enablemask = 1 << (gicirq % 32); u32 enableoff = (gicirq / 32) * 4; u32 confmask = 0x2 << ((gicirq % 16) * 2); u32 confoff = (gicirq / 16) * 4; bool enabled = false; u32 val; /* 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); val = readl_relaxed(base + GIC_DIST_CONFIG + confoff); if (type == IRQ_TYPE_LEVEL_HIGH) val &= ~confmask; else if (type == IRQ_TYPE_EDGE_RISING) val |= confmask; /* * As recommended by the spec, disable the interrupt before changing * the configuration */ if (readl_relaxed(base + GIC_DIST_ENABLE_SET + enableoff) & enablemask) { writel_relaxed(enablemask, base + GIC_DIST_ENABLE_CLEAR + enableoff); enabled = true; } writel_relaxed(val, base + GIC_DIST_CONFIG + confoff); if (enabled) writel_relaxed(enablemask, base + GIC_DIST_ENABLE_SET + enableoff); raw_spin_unlock(&irq_controller_lock); return 0; }
對於SGI類型的interrupt,是不能修改其type的,因為GIC中SGI固定就是edge-triggered。對於GIC,其type只支援高電平觸發(IRQ_TYPE_LEVEL_HIGH)和上升沿觸發(IRQ_TYPE_EDGE_RISING)的中斷。另外需要注意的是,在更改其type的時候,先disable,然後修改type,然後再enable。
5、gic_retrigger
這個介面用來resend一個IRQ到CPU。
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; }
看起來這是功能不是通用GIC擁有的功能,各個廠家在集成GIC的時候,有可能進行功能擴展。
6、gic_set_affinity
在多處理器的環境下,外部設備產生了一個中斷就需要送到一個或者多個處理器去,這個設定是通過設定處理器的affinity進行的。具體程式碼如下:
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 shift = (gic_irq(d) % 4) * 8; unsigned int cpu = cpumask_any_and(mask_val, cpu_online_mask); u32 val, mask, bit; 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; }
GIC Distributor中有一個暫存器叫做Interrupt Processor Targets Registers,這個暫存器用來設訂製定的中斷送到哪個process去。由於GIC最大支援8個process,因此每個hw interrupt ID需要8個bit來表示送達的process。每一個Interrupt Processor Targets Registers由32個bit組成,因此每個Interrupt Processor Targets Registers可以表示4個HW interrupt ID的affinity。
7、gic_set_wake
這個介面用來設定喚醒CPU的interrupt source。對於GIC,程式碼如下:
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; }
設定喚醒的interrupt和具體的廠商相關,這裡不再贅述。
原創文章,轉發請註明出處。蝸窩科技,www.wowotech.net。