GIC-400 是 GIC v2 架構,它是設計來配合 ARM CPU 的中斷控制器,能相容 TrustZone 和虛擬化架構、並最多支持到 8 個 ARM cores,這篇會介紹相關的一些重要觀念。
Fig 1. GIC architecture (Image source: p.23 of GIC v2.0 spec)
概觀
GIC-400 可以分成四大模塊,分別是 Distributor (GICD_*)、CPU interface (GICC_*)、virtual control interface (GICH_*) 和 virtual CPU interface (GICV_*),後面兩個模塊是為了虛擬化。請參考 Fig 1,中斷進入 GIC-400 之後,首先由 Distributor 模塊處理,中斷的類型可以分成三種,說明如下:
- SGI (Software Generated Interrupt): 它主要在做 CPU 和 CPU 之間的溝通,觸發方式是對 GICD_SGIR 寫入需求的中斷,這樣就能對特定一個或多個 CPU 發出 SGI,interrupt ID 範圍為 0 ~ 15,Linux v4.14 預先定義了 7 個 SGI ID (IPI_*,在 smp.c 內),而 TrustZone software 一般也會占用一些 SGI ID。
- PPI (Private Peripheral Interrupt): GIC-400 支持 6 個外部 PPI 和 1 個內部 PPI,這 6 個 外部 PPI 包含 Non-secure EL1 physical timer (ID 30)、Secure EL1 physical timer (ID 29)、virtual timer (ID 27) 和 Non-secure EL2 timer (hypervisor timer, ID 26);這 4 個 PPI 一般會和 ARM CPU 的 4 個相對應 timers 接在一起;再加上 2 個電源管理相關的信號中斷。而內部 PPI 是虛擬化環境專用的中斷 (ID 25),一般是在 Guest OS 處理好 hypervisor 發給它的 virtual interrupt 之後,會發這個 PPI 給 hypervisor,請 hypervisor 做後續處理,之後的章節在說明虛擬化時 (),會再詳細地介紹。
- SPI (Shared Peripheral Interrupt): GIC-400 最多可以支持 480 個 SPI,要通知 CPU 中斷的設備,會有實體連線連接到這個 GIC,這些中斷會按順序安排,最多安排到 Interrupt ID 511 (480 SPI + 16 SGI + 16 PPI),一個 SPI 發到 Distributor 後,可以由 distributor 來決定要通知那幾個 CPU。
中斷經過 Distributor 處理之後,優先權高的中斷會先由 CPU interface 發中斷到對應的 CPU core (Linux 把所有中斷的優先權設成一樣,這樣就不會有巢狀中斷的狀況),CPU 再經由讀取、寫入特定 CPU interface 告知 GIC 這個中斷的處理狀況。ARM 官方有兩份關於 GIC-400 的文件,一份是 GIC v2 架構規格書,一份是 GIC-400 技術參考手冊,前者主要說明 GIC v2 的架構、設計、如何控制它和這個 GIC 和其它軟體的互動建議,後者是 GIC-400 對 GIC v2 的實現細節,比如 GIC v2 規定 SPI 最多支持到 1020 個 interrupt IDs,而 GIC-400 最多只能支持到 interrupt ID 511。另外,底下的觀念,建議在讀這兩份規格書前要先理解:
- Banked register: 每個 CPU 都有自己的暫存器,比如某個 PPI 是否要暫時 disable,GIC-400 給每個 CPU 在這個功能上都有自己的設定暫存器,只會 disable 這個 CPU 的中斷,但如果是 SPI 的同樣功能,一但把某個 ID 的 SPI 給 disable 了,就所有 CPU 都收不到了,除了 PPI,SGI 和 SPI 也有某些暫存器有這樣的設計。假如要去設定 CPU 0 的 GICD_IGROUPR0,由於 GICD_IGROUPR0 是 banked register,一般的做法是發軟體中斷 (SGI) 給 CPU ID 0,CPU ID 0 再去寫入 GICD_IGROUPR0 的實體位址,如果是給 CPU ID 1 呢?那就請 CPU ID 1 去寫入 GICD_ISPENDR0 的實體位址,這 2 個 CPU 寫入的實體位址是一樣的嗎?是的!這方面底層的原理請參考 Fig 2,GIC 和 ARM CPU cores 是通過 AXI bus protocol 溝通的,為了 GIC-400,有 6 根特別的信號線 AWUSER[2:0] 和 ARUSER[2:0],把寫入或讀取的 CPU ID 傳給了 GIC-400,這樣 GIC-400 就知道是那個 CPU ID 對它控制了,但這 6 根專門為了 GIC 設計的線,在 GIC v3 之後的架構就丟掉不用了,因為之後的 GIC 版本可以支持到上百個 CPU cores 了。
- Security banked register: 類似前面 Banked register,在 Secure mode (Secure EL1) 底下,某些 GIC 的 registers 會有另一組和 Non-secure mode 不同的 registers,比如 GICD_CTLR 在 Secure mode 底下有另一組 register 可以決定 Secure interrupt 是否要 enable。AXI 的 AWPROT[0:2] 和 ARPROT[0:2] 可以讓 GIC 知道目前存取的 CPU 是在 secure 、privileged 或 normal protection mode。
- Security Extensions: 為了支持 ARM TrustZone 的安全環境而增加的模塊,GIC-400 加上了 Security Extensions 之後, 可以設置一些中斷為 Secure interrupt, 而這些中斷一般會設定成發到 Secure world,並且在安全環境才能操作這些中斷相關的 registers,非安全環境下,這些 registers 的存取為 RAZ/WI (Read-as-zero/Write-ignore),後面章節有詳細的介紹 ()。
- 一個 SOC 的 CPU num、SPI num 、PPI 支持狀況、有多少 Secure interrupt 可以被 lockdown 、是否有支持 Security Extensions 和是否有支持 Hypervisor Extensions 等,這些都是無法被動態設置的,SOC 出廠時都已固定了,在 GICD_TYPER 內查得到這些配置,而規格內提到 unimplemented interrupt,就代表超過 SPI num 的 interrupt ID。
Fig 2. GIC-400 bus overview (Image source: p12 of GIC-400 spec)
Distributor
Distributor 的目的在接收外面進來的中斷信號 (SPI 和 PPI),並把這些中斷轉發到一個或多個 CPU cores,CPU 上的中斷函式會使用 CPU interface (GICC_*)來更新這些中斷的處理狀態,最後由 Distributor 來解除對 CPU 發出的中斷信號,Distributor 的狀態機 (State machine) 和 CPU interface 的控制順序會於下一章節說明 ()。另外值得一提的是,每個中斷 ID 都會有自己的狀態,這些狀態是由 Distributor 來控制並保存的,它們並不是保存在每個 CPU interface 內,一個 CPU 收到中斷,並控制 GIC 的 CPU interface (控制 GICC_* 暫存器),其實並不是更新 CPU interface 的狀態,而是透過 CPU interface 來更新這個中斷於 Distributor 內的狀態。除了從 GIC 外面發進來的中斷,Distributor 也有介面可以更改每一個中斷的狀態來觸發中斷,一個設置成 Edge trigger 的中斷,只要把它的 Pending 設置起來 (GICD_ISPENDRn),GIC 就會對這個中斷預設的一個或多個 CPU 發出中斷,有些 SOC 會預留一些 interrupt ID,它們並沒有接到真正的設備,但 Distributor 內有留下這些中斷的配置,這些 interrupt ID 就可以拿來做 CPU 和 CPU 或軟件內特別的溝通 (SOC 的技術手冊內有些不連續的 interrupt ID,它們很可能就可以拿來做這些個用途),當然,SGI 是一個更正式的方式,不過 Linux、TrustZone 和 ATF 使用的 SGI ID 會越來越多,因此,就算現在還有空的 SGI ID,未來還可能要跟這些軟件搶。
Distributor 的控制介面 (GICD_*) 除了可以更改每一個中斷的狀態之外 (Active 和 Pending),還有設定每個中斷的開關、優先權、Level/Edge trigger、Secure/Non-Secure 和要發到那些 CPU,另外,也提供了查詢中斷支持的狀況。
CPU interface
CPU interface 的目的是做為 Distributor 和 CPU 的介面,如 Fig 1,每個 CPU 都有一套 CPU interface,Distributor 通過它來通知 CPU 中斷,而 CPU 通過它來告知 Distributor 中斷的處理狀況。底下 Fig 3 是 Distributor 內中斷的狀態機,每一個中斷都有自己的狀態,狀態之間的轉換是由外部中斷通知 Distributor 和 CPU 讀寫 GICC_* 暫存器相互作用而發生的。
Fig 3. Interrupt handling state machine (Image from: p42 of GIC v2.0 spec)
外部中斷分成 Edge trigger 和 Level trigger,底下 Fig 4 和 Fig 5 的 "Interrupt signal from peripheral to GIC" 分別代表 Level 和 Edge trigger interrupt,一般來說,使用 Edge trigger 而 CPU 處理不夠及時的狀況下,GIC 的 Distributor 可以幫忙暫存多個中斷,GIC v2 的狀態機可以暫存兩個中斷,而使用 Level trigger,則適合把多個設備合成一個中斷,在軟體收到中斷之後,再去確認那些硬體需要處理,並在處理好這些硬體的中斷需求之後,把這些硬體的中斷需求清除,在所有中斷需求都清除後,這些硬體聯合一起拉到 GIC 的這一根中斷信號就會取消。
Fig 4 是一般狀況下 Level trigger interrupt 於 Linux GIC v2 driver,配合 GIC 狀態機 (Fig 3) 的時序圖,各信號說明如下:
Fig 4 是一般狀況下 Level trigger interrupt 於 Linux GIC v2 driver,配合 GIC 狀態機 (Fig 3) 的時序圖,各信號說明如下:
- Interrupt signal from peripheral to GIC: 外部設備通知 GIC-400 的信號。
- Interrupt signal form GIC to core: GIC-400 通知目標 CPU core(s) 的信號,一般來說 Linux 會把所有 SPI 類型的中斷發到 CPU 0。
- GIC state machine for the interrupt: Distributor 在配合輸入中斷和 Linux 驅動的狀態。
- Linux GIC driver: 請參考 Linux irq-gic.c 的 gic_handle_irq(),配合底下的說明:
- Interrupt vector: CPU 一收到 GIC 發出的中斷,CPU 會馬上跳到中斷向量,並執行 Linux 預先註冊好的中斷函式,再跳到 gic_handle_irq()。
- GIC_CPU_INTACK: 讀取 GIC 的 GICC_IAR,這會得到中斷的 ID,如果中斷已經被讀取,再次讀取會得到無效中斷 ID (spurious ID: 1023,圖中的紅點代表讀到 1023)。
- GIC_CPU_EOI: 把中斷 ID 寫到 GIC 的 GICC_EOIR,Distributor 會清除中斷 ID 的 Pending 狀況。
- Clear interrupt bits: handle_domain_irq() 會叫用先前對這個中斷 ID 註冊好的函式,一般來說,這個函式會確認那些設備發出需求,在做完最初步的處理後,會清除它們的中斷需求狀態,全部清除之後,發到 GIC 的中斷就會解除 (deasserted)。
gic_handle_irq() 會讀取中斷 ID,EOI,再叫用使用者的中斷函式,如果中斷函式清除設備需求之後,馬上又有設備發出了中斷需求,讀取 GIC_CPU_INTACK 時會得到有效的中斷 ID,這時會再處理這個中斷,直到讀取 GIC_CPU_INTACK 時得到無效中斷 (1023) 為止。
一般的中斷只需要注意 Distributor 和 CPU interface 的控制,後面兩章的內容,是在有 TrustZone 或 hypervisor 軟體下才需要了解。
Secure interrupt
ARM 的 TrustZone 是為了保護重要軟硬體和資料在 Secure 環境內,任何 Non-secure 的程式都無法存取它們,Secure interrupts 的相關設定和中斷的通知當然也不能被 Non-secure 環境下的程式存取或修改。在支持 Security Extensions 的 GIC-400,可以把中斷分成兩類,Group 0 為 Secure 環境使用,而 Group 1 為 Non-secure 環境使用,Group 0 的中斷的優先權會比任何 Group 1 的中斷高 (Group 0 會使用低的一半優先值,Group 1 會使用高的一半優先值,越低的值優先權越高),GIC v2.0 的 p80 列出了只能在 Secure 環境下存取的暫存器和 Security banked registers,另外,GICC_A* 是原本 GICC_* 的 Group 0 別名暫存器。如 Fig 6 所示, GICD_IGROUPRn 可以設置每一個中斷 ID 是 Group 0 或是 group 1,GICD_IGROUPRn 只能在 Secure 環境下存取,另外,Group 0 的中斷可以在 Secure 環境下設置 GICC_CTLR 來決定 Secure interrupts 是發出 FIQ 或是 IRQ,一般來說 Secure interrupts 會使用 FIQ,再由 CPU 的 SCR_EL3.FIQ 來決定 FIQ 發到 EL3, 然後由 EL3 進到 Secure world。
另外,GIC-400 可以把 interrupt ID 32 到 63 中的一部分或全部設置成 lockable SPIs (Lockable SPIs 的範圍在 SOC 出廠時就固定,不能動態變更),在 CFGSDISABLE 給高電位時,Lockable SPIs 中斷相關的設定都會被固定,在 Secure 環境也無法更改這些設置。一般在開機時,會在 Secure 環境,由開機相關程式設定好 lockable SPIs 後,就會拉高 CFGSDISABLE,之後的 TrustZone 軟體和整個系統,都無法再更動這些設置,Lockable SPIs 比 Group 0 的保護更嚴謹。
Virtual Interrupt
虛擬中斷 (Virtual interrupt) 的功能,需要 GIC-400 有 Hypervisor Extensions,而 Hypervisor Extensions 必須依靠 Security Extensions,這個功能是為了支持虛擬化,它可以實現在一套系統上運行多套操作系統的虛擬化環境,如 Fig 7 所示,ARM v8 架構的 EL2 是專門為了虛擬化的運行環境,所有跟需擬化相關的硬體和暫存器,都只能在 EL2 才能存取,並且通過設置 HCR_EL2,可以把原本 ARM 指令集內,無法滿足虛擬化需求的指令 trap 到 EL2 運行。
Fig 7. ARM v8 Execution Levels (Image source: here)
一般的 Hypervisor 環境會把 ARM 的 HCR_EL2.IMO 會設置成 1,此時,所有 IRQ 中斷都會發到 EL2,由 Hypervisor 來處理,Hypervisor 處理過後,再轉發 Virtual IRQ 到 Guest OS 的 kernel (EL1)。更詳細的說,GICC_CTLR.EOImodeNS 會被 hypervisor 預設成 1,中斷會按底下的方式處理,
- Hypervisor 一但收到中斷 (使用 GICC_IAR 讀出中斷 ID),就會把中斷 ID 寫入 GICC_EOIR,但此時的 EOI 跟之前不同,它並不會改變中斷的狀態機,只會把這個中斷的優先權降低 (由於 GICC_CTLR.EOImodeNS = 1)。有些中斷如果是 Hypervisor 自己要處理的,會使用 GICC_DIR 來達成原本 EOI 的功能。
- 由於這個中斷的優先權被降低了,因此原本相同或較低優先權的中斷就會再被 hypervisor 收下。
- Hypervisor 收下所有的中斷後,會判斷這些中斷要發給那個或那些 Guest OS(s),請參考 Fig 6,選定要執行的 Guest OS 後,接下來 hypervisor 會把要給這個 Guest OS 的中斷都寫入 List register (GICH_LRn,GIC-400 共有 4 個,也就是 GICH_LR0 ~ GICH_LR3),並把 GICH_LRn.HW 設成 1。
- Hypervisor 會準備好 Guest OS 的環境,然後切換到 Guest OS,Guest OS 一開始執行,會馬上收到中斷 (由於 List register 有 pending interrupts,VIRQ 信號會被提高),GIC 會依 List register 的內容,依次發虛擬中斷 (VIRQ) 給 Guest OS。
- Guest OS 使用虛擬的中斷介面 GICV_* 來處理這些中斷 (Guest OS 的程式是寫到 GICC_* 的,但這個地址被 hypervisor 轉到 GICV_*,這樣維持了原本 Guest OS 的程式,Guest OS 不需要知道是否有 hypervisor 的存在),由於 GICH_LRn.HW = 1,在 Guest OS 寫入 GICV_EOIR 時 (同上,Guest OS 的程式是寫到 GICC_EOIR,被 hypervisor 轉址了),CPU interface 會去通知 Distributor 改變這個中斷的狀態機。
- 如果 Guest OS 尚未把所有 List register 內的中斷都處理完之前,hypervisor 就要轉換到另一個 Guest OS,hypervisor 會把 List register 的狀態存下來,等下次再讓原本 Guest OS 處理。更詳細的來說,Hypervisor 有自己的 timer,會從 ARM CPU 內定時發出中斷,這個中斷是拉到 GIC 的 PPI ID 26,然後由 GIC 再發出 IRQ 到 ARM CPU 的 EL2,EL2 收到任何中斷,都會中止原本 Guest OS 的執行,然後跳回 EL2 的 hypervisor 執行。
為了讓 Guest OS 感覺不到自己是在 hypervisor 的環境下運行,所有 Guest OS 的 GIC 驅動程式會使用的介面,都需要 hypervisor 做特殊處理:
- Distributor: 跟中斷狀態機相關的,如前面所述,是由 List register 和 Distributor 共同配合完成的,而原本 GICD_* 的功能,hypervisor 會給 Guest OS 操作一套虛擬的暫存器,當 Guest OS 對它們讀寫時,會觸發 Exception (data abort exception),這時會 trap 到 hypervisor 環境,hypervisor 會用軟體模擬對 Distributor 的操作,並把結果返回到 Guest OS。Hypervisor 會使用第二層的 MMU table (2-stage MMU),而 Guest OS 則控制自己第一層的 MMU table,Guest OS 第一層 table 映射到的 GICD_* 的實體位址,被 hypervisor 的第二層 MMU table 再次映射到虛擬地址,因此當 Guest OS 存取時,會發 trap 到 hypervisor。
- CPU Interface: GICC_* 和 GICV_* 的每個暫存器的相對位址都是一樣的,這樣確保 Guest OS 操作 GICV_* 時,像是在操作 GICC_*,程式不需要修改,Hypervisor 會把 Guest OS 的 CPU interface 重新映射到 GICV_*,而這些暫存器都是 banked registers,也就是不同 CPU 操作相同的 GICV_* 實體地址,會使用到這個 CPU 自己的那份暫存器,而 hypervisor 則可以透過不同的地址去操作不同 CPU 的那份 GICV_* (Alias virtual CPU interface address)。另外,當 Guest OS 去修改 GICV_CTLR.EnableGrp0 和 GICV_CTLR.EnableGrp1 時,hypervisor 可以透過控制 GICH_HCR,來讓這個行為發出 PPI ID 25,通知 hypervisor 做相對應的處理。