1. 程式人生 > >ARM GIC 中斷架構

ARM GIC 中斷架構

一、前言

GIC(Generic Interrupt Controller)是ARM公司提供的一個通用的中斷控制器,其architecture specification目前有四個版本,V1~V4(V2最多支援8個ARM core,V3/V4支援更多的ARM core,主要用於ARM64伺服器系統結構)。目前在ARM官方網站只能下載到Version 2的GIC architecture specification,因此,本文主要描述符合V2規範的GIC硬體及其驅動。

具體GIC硬體的實現形態有兩種,一種是在ARM vensor研發自己的SOC的時候,會向ARM公司購買GIC的IP,這些IP包括的型號有:PL390,GIC-400,GIC-500。其中GIC-500最多支援128個 cpu core,它要求ARM core必須是ARMV8指令集的(例如Cortex-A57),符合GIC architecture specification version 3。另外一種形態是ARM vensor直接購買ARM公司的Cortex A9或者A15的IP,Cortex A9或者A15中會包括了GIC的實現,當然,這些實現也是符合GIC V2的規格。

本文在進行硬體描述的時候主要是以GIC-400為目標,當然,也會順便提及一些Cortex A9或者A15上的GIC實現。

本文主要分析了linux kernel中GIC中斷控制器的驅動程式碼(位於drivers/irqchip/irq-gic.c和irq-gic-common.c)。 irq-gic-common.c中是GIC V2和V3的通用程式碼,而irq-gic.c是V2 specific的程式碼,irq-gic-v3.c是V3 specific的程式碼,不在本文的描述範圍。本文主要分成三個部分:第二章描述了GIC V2的硬體;第三章描述了GIC V2的初始化過程;第四章描述了底層的硬體call back函式。

注:具體的linux kernel的版本是linux-3.17-rc3。

二、GIC-V2的硬體描述

1、GIC-V2的輸入和輸出訊號

(1)GIC-V2的輸入和輸出訊號示意圖

要想理解一個building block(無論軟體還是硬體),我們都可以先把它當成黑盒子,只是研究其input,output。GIC-V2的輸入和輸出訊號的示意圖如下(注:我們以GIC-400為例,同時省略了clock,config等訊號):

gic-400 

(2)輸入訊號

上圖中左邊就是來自外設的interrupt source輸入訊號。分成兩種型別,分別是PPI(Private Peripheral Interrupt)和SPI(Shared Peripheral Interrupt)。其實從名字就可以看出來兩種型別中斷訊號的特點,PPI中斷訊號是CPU私有的,每個CPU都有其特定的PPI訊號線。而SPI是所有CPU之間共享的。通過暫存器GICD_TYPER可以配置SPI的個數(最多480個)。GIC-400支援多少個SPI中斷,其輸入訊號線就有多少個SPI interrupt request signal。同樣的,通過暫存器GICD_TYPER也可以配置CPU interface的個數(最多8個),GIC-400支援多少個CPU interface,其輸入訊號線就提供多少組PPI中斷訊號線。一組PPI中斷訊號線包括6個實際的signal:

(a)nLEGACYIRQ訊號線。對應interrupt ID 31,在bypass mode下(這裡的bypass是指bypass GIC functionality,直接連線到某個processor上),nLEGACYIRQ可以直接連到對應CPU的nIRQCPU訊號線上。在這樣的設定下,該CPU不參與其他屬於該CPU的PPI以及SPI中斷的響應,而是特別為這一根中斷線服務。

(b)nCNTPNSIRQ訊號線。來自Non-secure physical timer的中斷事件,對應interrupt ID 30。

(c)nCNTPSIRQ訊號線。來自secure physical timer的中斷事件,對應interrupt ID 29。

(d)nLEGACYFIQ訊號線。對應interrupt ID 28。概念同nLEGACYIRQ訊號線,不再描述。

(e)nCNTVIRQ訊號線。對應interrupt ID 27。Virtual Timer Event,和虛擬化相關,這裡不與描述。

(f)nCNTHPIRQ訊號線。對應interrupt ID 26。Hypervisor Timer Event,和虛擬化相關,這裡不與描述。

對於Cortex A15的GIC實現,其PPI中斷訊號線除了上面的6個,還有一個叫做Virtual Maintenance Interrupt,對應interrupt ID 25。

對於Cortex A9的GIC實現,其PPI中斷訊號線包括5根:

(a)nLEGACYIRQ訊號線和nLEGACYFIQ訊號線。對應interrupt ID 31和interrupt ID 28。這部分和上面一致。

(b)由於Cortext A9的每個處理器都有自己的Private timer和watch dog timer,這兩個HW block分別使用了ID 29和ID 30

(c)Cortext A9內嵌一個global timer為系統內的所有processor共享,對應interrupt ID 27

關於private timer和global timer的描述,請參考時間子系統的相關文件。

關於一系列和虛擬化相關的中斷,請參考虛擬化的系列文件。

(3)輸出訊號

所謂輸出訊號,其實就是GIC和各個CPU直接的介面,這些介面包括:

(a)觸發CPU中斷的訊號。nIRQCPU和nFIQCPU訊號線,熟悉ARM CPU的工程師對這兩個訊號線應該不陌生,主要用來觸發ARM cpu進入IRQ mode和FIQ mode。

(b)Wake up訊號。nFIQOUT和nIRQOUT訊號線,去ARM CPU的電源管理模組,用來喚醒CPU的

(c)AXI slave interface signals。AXI(Advanced eXtensible Interface)是一種匯流排協議,屬於AMBA規範的一部分。通過這些訊號線,ARM CPU可以和GIC硬體block進行通訊(例如暫存器訪問)。

(4)中斷號的分配

GIC-V2支援的中斷型別有下面幾種:

(a)外設中斷(Peripheral interrupt)。有實際物理interrupt request signal的那些中斷,上面已經介紹過了。

(b)軟體觸發的中斷(SGI,Software-generated interrupt)。軟體可以通過寫GICD_SGIR暫存器來觸發一箇中斷事件,這樣的中斷,可以用於processor之間的通訊。

(c)虛擬中斷(Virtual interrupt)和Maintenance interrupt。這兩種中斷和本文無關,不再贅述。

為了標識這些interrupt source,我們必須要對它們進行編碼,具體的ID分配情況如下:

(a)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。

(b)ID32~ID1019用於SPI。 這是GIC規範的最大size,實際上GIC-400最大支援480個SPI,Cortex-A15和A9上的GIC最多支援224個SPI。

2、GIC-V2的內部邏輯

(1)GIC的block diagram

GIC的block diagram如下圖所示:

 gic

GIC可以清晰的劃分成兩個block,一個block是Distributor(上圖的左邊的block),一個是CPU interface。CPU interface有兩種,一種就是和普通processor介面,另外一種是和虛擬機器介面的。Virtual CPU interface在本文中不會詳細描述。

(2)Distributor 概述

Distributor的主要的作用是檢測各個interrupt source的狀態,控制各個interrupt source的行為,分發各個interrupt source產生的中斷事件分發到指定的一個或者多個CPU interface上。雖然Distributor可以管理多個interrupt source,但是它總是把優先順序最高的那個interrupt請求送往CPU interface。Distributor對中斷的控制包括:

(1)中斷enable或者disable的控制。Distributor對中斷的控制分成兩個級別。一個是全域性中斷的控制(GIC_DIST_CTRL)。一旦disable了全域性的中斷,那麼任何的interrupt source產生的interrupt event都不會被傳遞到CPU interface。另外一個級別是對針對各個interrupt source進行控制(GIC_DIST_ENABLE_CLEAR),disable某一個interrupt source會導致該interrupt event不會分發到CPU interface,但不影響其他interrupt source產生interrupt event的分發。

(2)控制將當前優先順序最高的中斷事件分發到一個或者一組CPU interface。當一箇中斷事件分發到多個CPU interface的時候,GIC的內部邏輯應該保證只assert 一個CPU。

(3)優先順序控制。

(4)interrupt屬性設定。例如是level-sensitive還是edge-triggered

(5)interrupt group的設定

Distributor可以管理若干個interrupt source,這些interrupt source用ID來標識,我們稱之interrupt ID。

(3)CPU interface

CPU interface這個block主要用於和process進行介面。該block的主要功能包括:

(a)enable或者disable CPU interface向連線的CPU assert中斷事件。對於ARM,CPU interface block和CPU之間的中斷訊號線是nIRQCPU和nFIQCPU。如果disable了中斷,那麼即便是Distributor分發了一箇中斷事件到CPU interface,但是也不會assert指定的nIRQ或者nFIQ通知processor。

(b)ackowledging中斷。processor會向CPU interface block應答中斷(應答當前優先順序最高的那個中斷),中斷一旦被應答,Distributor就會把該中斷的狀態從pending狀態修改成active或者pending and active(這是和該interrupt source的訊號有關,例如如果是電平中斷並且保持了該asserted電平,那麼就是pending and active)。processor ack了中斷之後,CPU interface就會deassert nIRQCPU和nFIQCPU訊號線。

(c)中斷處理完畢的通知。當interrupt handler處理完了一箇中斷的時候,會向寫CPU interface的暫存器從而通知GIC CPU已經處理完該中斷。做這個動作一方面是通知Distributor將中斷狀態修改為deactive,另外一方面,CPU interface會priority drop,從而允許其他的pending的interrupt向CPU提交。

(d)設定priority mask。通過priority mask,可以mask掉一些優先順序比較低的中斷,這些中斷不會通知到CPU。

(e)設定preemption的策略

(f)在多箇中斷事件同時到來的時候,選擇一個優先順序最高的通知processor

(4)例項

我們用一個實際的例子來描述GIC和CPU介面上的互動過程,具體過程如下:

xxx

(注:圖片太長,因此豎著放,看的時候有點費勁,就當活動一下脖子吧)

首先給出前提條件:

(a)N和M用來標識兩個外設中斷,N的優先順序大於M

(b)兩個中斷都是SPI型別,level trigger,active-high

(c)兩個中斷被配置為去同一個CPU

(d)都被配置成group 0,通過FIQ觸發中斷

下面的表格按照時間軸來描述互動過程:

時間 互動動作的描述
T0時刻 Distributor檢測到M這個interrupt source的有效觸發電平
T2時刻 Distributor將M這個interrupt source的狀態設定為pending
T17時刻 大約15個clock之後,CPU interface拉低nFIQCPU訊號線,向CPU報告M外設的中斷請求。這時候,CPU interface的ack暫存器(GICC_IAR)的內容會修改成M interrupt source對應的ID
T42時刻 Distributor檢測到N這個優先順序更高的interrupt source的觸發事件
T43時刻 Distributor將N這個interrupt source的狀態設定為pending。同時,由於N的優先順序更高,因此Distributor會標記當前優先順序最高的中斷
T58時刻 大約15個clock之後,CPU interface拉低nFIQCPU訊號線,向CPU報告N外設的中斷請求。當然,由於T17時刻已經assert CPU了,因此實際的電平訊號仍然保持asserted。這時候,CPU interface的ack暫存器(GICC_IAR)的內容會被更新成N interrupt source的ID
T61時刻 軟體通過讀取ack暫存器的內容,獲取了當前優先順序最高的,並且狀態是pending的interrupt ID(也就是N interrupt source對應的ID),通過讀該暫存器,CPU也就ack了該interrupt source N。這時候,Distributor將N這個interrupt source的狀態設定為pending and active(因為是電平觸發,只要外部仍然有asserted的電平訊號,那麼一定就是pending的,而該中斷是正在被CPU處理的中斷,因此狀態是pending and active) 
注意:T61標識CPU開始服務該中斷
T64時刻 3個clock之後,由於CPU已經ack了中斷,因此GIC中CPU interface模組 deassert nFIQCPU訊號線,解除發向該CPU的中斷請求
T126時刻 由於中斷服務程式操作了N外設的控制暫存器(ack外設的中斷),因此N外設deassert了其interrupt request signal
T128時刻 Distributor解除N外設的pending狀態,因此N這個interrupt source的狀態設定為active
T131時刻 軟體操作End of Interrupt暫存器(向GICC_EOIR暫存器寫入N對應的interrupt ID),標識中斷處理結束。Distributor將N這個interrupt source的狀態修改為idle 
注意:T61~T131是CPU服務N外設中斷的的時間區域,這個期間,如果有高優先順序的中斷pending,會發生中斷的搶佔(硬體意義的),這時候CPU interface會向CPU assert 新的中斷。
T146時刻 大約15個clock之後,Distributor向CPU interface報告當前pending且優先順序最高的interrupt source,也就是M了。漫長的pending之後,M終於迎來了春天。CPU interface拉低nFIQCPU訊號線,向CPU報告M外設的中斷請求。這時候,CPU interface的ack暫存器(GICC_IAR)的內容會修改成M interrupt source對應的ID
T211時刻 CPU ack M中斷(通過讀GICC_IAR暫存器),開始處理低優先順序的中斷。

三、GIC-V2 irq chip driver的初始化過程

在linux-3.17-rc3\drivers\irqchip目錄下儲存在各種不同的中斷控制器的驅動程式碼,這個版本的核心支援了GICV3。irq-gic-common.c是通用的GIC的驅動程式碼,可以被各個版本的GIC使用。irq-gic.c是用於V2版本的GIC controller,而irq-gic-v3.c是用於V3版本的GIC controller。

1、GIC的device node和GIC irq chip driver的匹配過程

(1)irq chip driver中的宣告

在linux-3.17-rc3\drivers\irqchip目錄下的irqchip.h檔案中定義了IRQCHIP_DECLARE巨集如下:

#define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)

#define OF_DECLARE_2(table, name, compat, fn) \ 
        _OF_DECLARE(table, name, compat, fn, of_init_fn_2)

#define _OF_DECLARE(table, name, compat, fn, fn_type)            \ 
    static const struct of_device_id __of_table_##name        \ 
        __used __section(__##table##_of_table)            \ 
         = { .compatible = compat,                \ 
             .data = (fn == (fn_type)NULL) ? fn : fn  }

這個巨集其實就是初始化了一個struct of_device_id的靜態常量,並放置在__irqchip_of_table section中。irq-gic.c檔案中使用IRQCHIP_DECLARE來定義了若干個靜態的struct of_device_id常量,如下:

IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init); 
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(cortex_a7_gic, "arm,cortex-a7-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);

相容GIC-V2的GIC實現有很多,不過其初始化函式都是一個。在linux kernel編譯的時候,你可以配置多個irq chip進入核心,編譯系統會把所有的IRQCHIP_DECLARE巨集定義的資料放入到一個特殊的section中(section name是__irqchip_of_table),我們稱這個特殊的section叫做irq chip table。這個table也就儲存了kernel支援的所有的中斷控制器的ID資訊(最重要的是驅動程式碼初始化函式和DT compatible string)。我們來看看struct of_device_id的定義:

struct of_device_id 

    char    name[32];------要匹配的device node的名字 
    char    type[32];-------要匹配的device node的型別 
    char    compatible[128];---匹配字串(DT compatible string),用來匹配適合的device node 
    const void *data;--------對於GIC,這裡是初始化函式指標 
};

這個資料結構主要被用來進行Device node和driver模組進行匹配用的。從該資料結構的定義可以看出,在匹配過程中,device name、device type和DT compatible string都是考慮的因素。更細節的內容請參考__of_device_is_compatible函式。

(2)device node

不同的GIC-V2的實現總會有一些不同,這些資訊可以通過Device tree的機制來傳遞。Device node中定義了各種屬性,其中就包括了memory資源,IRQ描述等資訊,這些資訊需要在初始化的時候傳遞給具體的驅動,因此需要一個Device node和driver模組的匹配過程。在Device Tree模組中會包括系統中所有的device node,如果我們的系統使用了GIC-400,那麼系統的device node資料庫中會有一個node是GIC-400的,一個示例性的GIC-400的device node(我們以瑞芯微的RK3288處理器為例)定義如下:

gic: [email protected]
    compatible = "arm,gic-400"; 
    interrupt-controller; 
    #interrupt-cells = <3>; 
    #address-cells = <0>;

    reg = <0xffc01000 0x1000="">,----Distributor address range 
          <0xffc02000 0x1000="">,-----CPU interface address range 
          <0xffc04000 0x2000="">,-----Virtual interface control block 
          <0xffc06000 0x2000="">;-----Virtual CPU interfaces 
    interrupts = ; 
};

(3)device node和irq chip driver的匹配

在machine driver初始化的時候會呼叫irqchip_init函式進行irq chip driver的初始化。在driver/irqchip/irqchip.c檔案中定義了irqchip_init函式,如下:

void __init irqchip_init(void) 

    of_irq_init(__irqchip_begin); 
}

__irqchip_begin就是核心irq chip table的首地址,這個table也就儲存了kernel支援的所有的中斷控制器的ID資訊(用於和device node的匹配)。of_irq_init函式執行之前,系統已經完成了device tree的初始化,因此係統中的所有的裝置節點都已經形成了一個樹狀結構,每個節點代表一個裝置的device node。of_irq_init是在所有的device node中尋找中斷控制器節點,形成樹狀結構(系統可以有多個interrupt controller,之所以形成中斷控制器的樹狀結構,是為了讓系統中所有的中斷控制器驅動按照一定的順序進行初始化)。之後,從root interrupt controller節點開始,對於每一個interrupt controller的device node,掃描irq chip table,進行匹配,一旦匹配到,就呼叫該interrupt controller的初始化函式,並把該中斷控制器的device node以及parent中斷控制器的device node作為引數傳遞給irq chip driver。。具體的匹配過程的程式碼屬於Device Tree模組的內容,更詳細的資訊可以參考Device Tree程式碼分析文件

2、GIC driver初始化程式碼分析

(1)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;

    dist_base = of_iomap(node, 0);----------------對映GIC Distributor的暫存器地址空間

    cpu_base = of_iomap(node, 1);----------------對映GIC CPU interface的暫存器地址空間

    if (of_property_read_u32(node, "cpu-offset", &percpu_offset))--------處理cpu-offset屬性。 
        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); ---解析second GIC的interrupts屬性,並進行mapping,返回IRQ number 
        gic_cascade_irq(gic_cnt, irq); 
    } 
    gic_cnt++; 
    return 0; 
}

我們首先看看這個函式的引數,node引數代表需要初始化的那個interrupt controller的device node,parent引數指向其parent。在對映GIC-400的memory map I/O space的時候,我們只是映射了Distributor和CPU interface的暫存器地址空間,和虛擬化處理相關的暫存器沒有對映,因此這個版本的GIC driver應該是不支援虛擬化的(不知道後續版本是否支援,在一個嵌入式平臺上支援虛擬化有實際意義嗎?最先支援虛擬化的應該是ARM64+GICV3/4這樣的平臺)。

要了解cpu-offset屬性,首先要了解什麼是banked register。所謂banked register就是在一個地址上提供多個暫存器副本。比如說系統中有四個CPU,這些CPU訪問某個暫存器的時候地址是一樣的,但是對於banked register,實際上,不同的CPU訪問的是不同的暫存器,雖然它們的地址是一樣的。如果GIC沒有banked register,那麼需要提供根據CPU index給出一系列地址偏移,而地址偏移=cpu-offset * cpu-nr。

interrupt controller可以級聯。對於root GIC,其傳入的parent是NULL,因此不會執行級聯部分的程式碼。對於second GIC,它是作為其parent(root GIC)的一個普通的irq source,因此,也需要註冊該IRQ的handler。由此可見,非root的GIC的初始化分成了兩個部分:一部分是作為一個interrupt controller,執行和root GIC一樣的初始化程式碼。另外一方面,GIC又作為一個普通的interrupt generating device,需要象一個普通的裝置驅動一樣,註冊其中斷handler。理解irq_of_parse_and_map需要irq domain的知識,請參考linux kernel的中斷子系統之(二):irq domain介紹

(2)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;

    gic = &gic_data[gic_nr];  
    gic->dist_base.common_base = dist_base; ----省略了non banked的情況 
    gic->cpu_base.common_base = cpu_base;  
    gic_set_base_accessor(gic, gic_get_common_base);


    for (i = 0; i < NR_GIC_CPU_IF; i++) ---後面會具體描述gic_cpu_map的含義 
        gic_cpu_map[i] = 0xff;


    if (gic_nr == 0 && (irq_start & 31) > 0) { --------------------(a) 
        hwirq_base = 16; 
        if (irq_start != -1) 
            irq_start = (irq_start & ~31) + 16; 
    } else { 
        hwirq_base = 32; 
    }


    gic_irqs = readl_relaxed(gic_data_dist_base(gic) + GIC_DIST_CTR) & 0x1f; ----(b) 
    gic_irqs = (gic_irqs + 1) * 32; 
    if (gic_irqs > 1020) 
        gic_irqs = 1020; 
    gic->gic_irqs = gic_irqs;

    gic_irqs -= hwirq_base;----------------------------(c) 
   

    if (of_property_read_u32(node, "arm,routable-irqs",----------------(d) 
                 &nr_routable_irqs)) { 
        irq_base = irq_alloc_descs(irq_start, 16, gic_irqs,  numa_node_id()); -------(e) 
        if (IS_ERR_VALUE(irq_base)) { 
            WARN(1, "Cannot allocate irq_descs @ IRQ%d, assuming pre-allocated\n", 
                 irq_start); 
            irq_base = irq_start; 
        }

        gic->domain = irq_domain_add_legacy(node, gic_irqs, irq_base, -------(f) 
                    hwirq_base, &gic_irq_domain_ops, gic); 
    } else { 
        gic->domain = irq_domain_add_linear(node, nr_routable_irqs, --------(f) 
                            &gic_irq_domain_ops, 
                            gic); 
    }

    if (gic_nr == 0) { ---只對root GIC操作,因為設定callback、註冊Notifier只需要一次就OK了 
#ifdef CONFIG_SMP 
        set_smp_cross_call(gic_raise_softirq);------------------(g) 
        register_cpu_notifier(&gic_cpu_notifier);------------------(h) 
#endif 
        set_handle_irq(gic_handle_irq); ---這個函式名字也不好,實際上是設定arch相關的irq handler
    }

    gic_chip.flags |= gic_arch_extn.flags; 
    gic_dist_init(gic);---------具體的硬體初始程式碼,參考下節的描述 
    gic_cpu_init(gic); 
    gic_pm_init(gic); 
}

(a)gic_nr標識GIC number,等於0就是root GIC。hwirq的意思就是GIC上的HW interrupt ID,並不是GIC上的每個interrupt ID都有map到linux IRQ framework中的一個IRQ number,對於SGI,是屬於軟體中斷,用於CPU之間通訊,沒有必要進行HW interrupt ID到IRQ number的mapping。變數hwirq_base表示該GIC上要進行map的base ID,hwirq_base = 16也就意味著忽略掉16個SGI。對於系統中其他的GIC,其PPI也沒有必要mapping,因此hwirq_base = 32。

在本場景中,irq_start = -1,表示不指定IRQ number。有些場景會指定IRQ number,這時候,需要對IRQ number進行一個對齊的操作。

(b)變數gic_irqs儲存了該GIC支援的最大的中斷數目。該資訊是從GIC_DIST_CTR暫存器(這是V1版本的暫存器名字,V2中是GICD_TYPER,Interrupt Controller Type Register,)的低五位ITLinesNumber獲取的。如果ITLinesNumber等於N,那麼最大支援的中斷數目是32(N+1)。此外,GIC規範規定最大的中斷數目不能超過1020,1020-1023是有特別使用者的interrupt ID。

(c)減去不需要map(不需要分配IRQ)的那些interrupt ID,OK,這時候gic_irqs的數值終於和它的名字一致了。gic_irqs從字面上看不就是該GIC需要分配的IRQ number的數目嗎?

(d)of_property_read_u32函式把arm,routable-irqs的屬性值讀出到nr_routable_irqs變數中,如果正確返回0。在有些SOC的設計中,外設的中斷請求訊號線不是直接接到GIC,而是通過crossbar/multiplexer這個的HW block連線到GIC上。arm,routable-irqs這個屬性用來定義那些不直接連線到GIC的中斷請求數目。

(e)對於那些直接連線到GIC的情況,我們需要通過呼叫irq_alloc_descs分配中斷描述符。如果irq_start大於0,那麼說明是指定IRQ number的分配,對於我們這個場景,irq_start等於-1,因此不指定IRQ 號。如果不指定IRQ number的,就需要搜尋,第二個引數16就是起始搜尋的IRQ number。gic_irqs指明要分配的irq number的數目。如果沒有正確的分配到中斷描述符,程式會認為可能是之前已經準備好了。

(f)這段程式碼主要是向系統中註冊一個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 domain的時候還有一個重要的資料結構gic_irq_domain_ops,其型別是struct irq_domain_ops ,對於GIC,其irq domain的操作函式是gic_irq_domain_ops,定義如下:

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, 
};

irq domain的概念是一個通用中斷子系統的概念,在具體的irq chip driver這個層次,我們需要一些解析GIC binding,建立IRQ number和HW interrupt ID的mapping的callback函式,更具體的解析參考後文的描述。

漫長的準備過程結束後,具體的註冊比較簡單,呼叫irq_domain_add_legacy或者irq_domain_add_linear進行註冊就OK了。關於這兩個介面請參考linux kernel的中斷子系統之(二):irq domain介紹

(g) 一個函式名字是否起的好足可以看出工程師的功力。set_smp_cross_call這個函式看名字也知道它的含義,就是設定一個多個CPU直接通訊的callback函式。當一個CPU core上的軟體控制行為需要傳遞到其他的CPU上的時候(例如在某一個CPU上執行的程序呼叫了系統呼叫進行reboot),就會呼叫這個callback函式。對於GIC,這個callback定義為gic_raise_softirq。這個函式名字起的不好,直觀上以為是和softirq相關,實際上其實是觸發了IPI中斷。

(h)在multi processor環境下,當processor狀態傳送變化的時候(例如online,offline),需要把這些事件通知到GIC。而GIC driver在收到來自CPU的事件後會對cpu interface進行相應的設定。

3、GIC硬體初始化

(1)Distributor初始化,程式碼如下:

static void __init gic_dist_init(struct gic_chip_data *gic) 

    unsigned int i; 
    u32 cpumask; 
    unsigned int gic_irqs = gic->gic_irqs;---------獲取該GIC支援的IRQ的數目 
    void __iomem *base = gic_data_dist_base(gic); ----獲取該GIC對應的Distributor基地址

    writel_relaxed(0, base + GIC_DIST_CTRL); -----------(a)


    cpumask = gic_get_cpumask(gic);---------------(b) 
    cpumask |= cpumask << 8; 
    cpumask |= cpumask << 16;------------------(c) 
    for (i = 32; i < gic_irqs; i += 4) 
        writel_relaxed(cpumask, base + GIC_DIST_TARGET + i * 4 / 4); --(d)

    gic_dist_config(base, gic_irqs, NULL); ---------------(e)

    writel_relaxed(1, base + GIC_DIST_CTRL);-------------(f) 
}

(a)Distributor Control Register用來控制全域性的中斷forward情況。寫入0表示Distributor不向CPU interface傳送中斷請求訊號,也就disable了全部的中斷請求(group 0和group 1),CPU interace再也收不到中斷請求訊號了。在初始化的最後,step(f)那裡會進行enable的動作(這裡只是enable了group 0的中斷)。在初始化程式碼中,並沒有設定interrupt source的group(暫存器是GIC_DIST_IGROUP),我相信預設值就是設定為group 0的。

(b)我們先看看gic_get_cpumask的程式碼:

static u8 gic_get_cpumask(struct gic_chip_data *gic) 

    void __iomem *base = gic_data_dist_base(gic); 
    u32 mask, i;

    for (i = mask = 0; i < 32; i += 4) { 
        mask = readl_relaxed(base + GIC_DIST_TARGET + i); 
        mask |= mask >> 16; 
        mask |= mask >> 8; 
        if (mask) 
            break; 
    }

    return mask; 
}

這裡操作的暫存器是Interrupt Processor Targets Registers,該暫存器組中,每個GIC上的interrupt ID都有8個bit來控制送達的target CPU。我們來看看下面的圖片:

cpu mask

GIC_DIST_TARGETn(Interrupt Processor Targets Registers)位於Distributor HW block中,能控制送達的CPU interface,並不是具體的CPU,如果具體的實現中CPU interface和CPU是嚴格按照上圖中那樣一一對應,那麼GIC_DIST_TARGET送達了CPU Interface n,也就是送達了CPU n。當然現實未必如你所願,那麼怎樣來獲取這個CPU的mask呢?我們知道SGI和PPI不需要使用GIC_DIST_TARGET控制target CPU。SGI送達目標CPU有自己特有的暫存器來控制(Software Generated Interrupt Register),對於PPI,其是CPU私有的,因此不需要控制target CPU。GIC_DIST_TARGET0~GIC_DIST_TARGET7是控制0~31這32個interrupt ID(SGI和PPI)的target CPU的,但是實際上SGI和PPI是不需要控制target CPU的,因此,這些暫存器是read only的,讀取這些暫存器返回的就是cpu mask值。假設CPU0接在CPU interface 4上,那麼執行在CPU 0上的程式在讀GIC_DIST_TARGET0~GIC_DIST_TARGET7的時候,返回的就是0b00010000。

當然,由於GIC-400只支援8個CPU,因此CPU mask值只需要8bit,但是暫存器GIC_DIST_TARGETn返回32個bit的值,怎麼對應?很簡單,cpu mask重複四次就OK了。瞭解了這些知識,回頭看程式碼就很簡單了。

(c)step (b)中獲取了8個bit的cpu mask值,通過簡單的copy,擴充為32個bit,每8個bit都是cpu mask的值,這麼做是為了下一步設定所有IRQ(對於GIC而言就是SPI型別的中斷)的CPU mask。

(d)設定每個SPI型別的中斷都是隻送達該CPU。

(e)配置GIC distributor的其他暫存器,程式碼如下:

void __init gic_dist_config(void __iomem *base, int gic_irqs,  void (*sync_access)(void)) 

    unsigned int i;

    /* Set all global interrupts to be level triggered, active low.    */ 
    for (i = 32; i < gic_irqs; i += 16) 
        writel_relaxed(0, base + GIC_DIST_CONFIG + i / 4);

    /* Set priority on all global interrupts.   */ 
    for (i = 32; i < gic_irqs; i += 4) 
        writel_relaxed(0xa0a0a0a0, base + GIC_DIST_PRI + i);

    /* Disable all interrupts.  Leave the PPI and SGIs alone as they are enabled by redistributor registers.    */ 
    for (i = 32; i < gic_irqs; i += 32) 
        writel_relaxed(0xffffffff, base + GIC_DIST_ENABLE_CLEAR + i / 8);

    if (sync_access) 
        sync_access(); 
}

程式的註釋已經非常清楚了,這裡就不細述了。需要注意的是:這裡設定的都是預設值,實際上,在各種driver的初始化過程中,還是有可能改動這些設定的(例如觸發方式)。

(2)CPU interface初始化,程式碼如下:

static void gic_cpu_init(struct gic_chip_data *gic) 

    void __iomem *dist_base = gic_data_dist_base(gic);-------Distributor的基地址空間 
    void __iomem *base = gic_data_cpu_base(gic);-------CPU interface的基地址空間 
    unsigned int cpu_mask, cpu = smp_processor_id();------獲取CPU的邏輯ID 
    int i;


    cpu_mask = gic_get_cpumask(gic);-------------(a) 
    gic_cpu_map[cpu] = cpu_mask;


    for (i = 0; i < NR_GIC_CPU_IF; i++) 
        if (i != cpu) 
            gic_cpu_map[i] &= ~cpu_mask; ------------(b)

    gic_cpu_config(dist_base, NULL); --------------(c)

    writel_relaxed(0xf0, base + GIC_CPU_PRIMASK);-------(d) 
    writel_relaxed(1, base + GIC_CPU_CTRL);-----------(e) 
}

(a)系統軟體實際上是使用CPU 邏輯ID這個概念的,通過smp_processor_id可以獲得本CPU的邏輯ID。gic_cpu_map這個全部lookup table就是用CPU 邏輯ID作為所以,去尋找其cpu mask,後續通過cpu mask值來控制中斷是否送達該CPU。在gic_init_bases函式中,我們將該lookup table中的值都初始化為0xff,也就是說不進行mask,送達所有的CPU。這裡,我們會進行重新修正。

(b)清除lookup table中其他entry中本cpu mask的那個bit。

(c)設定SGI和PPI的初始值。具體程式碼如下:

void gic_cpu_config(void __iomem *base, void (*sync_access)(void)) 

    int i;

    /* Deal with the banked PPI and SGI interrupts - disable all 
     * PPI interrupts, ensure all SGI interrupts are enabled.     */ 
    writel_relaxed(0xffff0000, base + GIC_DIST_ENABLE_CLEAR); 
    writel_relaxed(0x0000ffff, base + GIC_DIST_ENABLE_SET);

    /* Set priority on PPI and SGI interrupts    */ 
    for (i = 0; i < 32; i += 4) 
        writel_relaxed(0xa0a0a0a0, base + GIC_DIST_PRI + i * 4 / 4);

    if (sync_access) 
        sync_access(); 
}

程式的註釋已經非常清楚了,這裡就不細述了。

(d)通過Distributor中的暫存器可以控制送達CPU interface,中斷來到了GIC的CPU interface是否可以真正送達CPU呢?也不一定,還有一道關卡,也就是CPU interface中的Interrupt Priority Mask Register。這個暫存器設定了一箇中