天天看點

基於tiny4412的Linux內核移植 --- 執行個體學習中斷背後的知識(1)

作者:彭東林

郵箱:[email protected]

QQ:405728433

tiny4412 ADK

Linux-4.9

前面幾篇博文列舉了在有裝置樹的時候,gpio中斷的用法示例。下面我們嘗試分析一下Linux核心是如何做到的,如果哪寫的有問題,歡迎大家批評指正,謝謝。

還是以GPIO中斷為例分析,對于tiny4412,gpio中斷可以分為兩種,外部中斷和普通的GPIO中斷

外部可喚醒中斷:exynos4412提供了32個外部可喚醒中斷,分别是GPX0到GPX3。按鍵中斷分别使用了外部可喚醒中斷XEINT26、XEINT27、XEINT28以及XEINT29,對應的GPIO分别是GPIOX3_2、GPIOX3_3、GPIOX3_4和GPIO3_5,當按下鍵的時候,會在對應的GPIO上面産生一個下降沿.

其餘的GPIO也可以産生中斷,但是不具備喚醒功能,從中斷的實體連接配接來看,外部中斷可以直接對應的GIC上面的一個SPI實體中斷号,而普通的GPIO中斷是多個GPIO對應GIC上的同一個SPI中斷。

關于GIC的知識請參考exynos4412的datasheet的"9 Interrupt Controller",這裡簡單說明一下:exynos4412使用的GIC是v2版本,支援16個SGI中斷、16個PPI中斷以及128個SPI中斷。

基於tiny4412的Linux內核移植 --- 執行個體學習中斷背後的知識(1)

結合上面的一張圖說明一下:

對于外部中斷XEINT0-15,每一個都對應的SPI中斷,但是XEINT16-31共享了同一個SPI中斷。這裡引腳上産生中斷後,會直接通知GIC,然後GIC會通過irq或者firq觸發某個CPU中斷。

對于其他的pinctrl@11000000中的其他普通的GPIO來說,它們産生中斷後,并沒有直接通知GIC,而是先通知pinctrl@11000000,然後pinctrl@11000000再通過SPI-46通知GIC,然後GIC會通過irq或者firq觸發某個CPU中斷。

其中涉及到了多個irq domain, GIC子產品的irq domain 1, 三星為每一組GPIO都建立了一個irq domain, 好處是通過gpio引腳的編号就可以知道對應的hwirq是多少(如gpx3_2在gpx3這個bank對應的domain中的hwriq就是2),irq domain存放的的hwirq(來自硬體寄存器)到virq(邏輯中斷号,全局唯一)的映射

上面的每一個irq_domain都對應一個irq_chip,irq_chip是kernel對中斷控制器的軟體抽象

上面SPI中斷括号中的數字表示的發生中斷後,實際從gic的ICCIAR_CPUn寄存器中讀取出來的中斷号,可以參考4412的datasheet的9.2.2 GIC Interrupt Table

基於tiny4412的Linux內核移植 --- 執行個體學習中斷背後的知識(1)
基於tiny4412的Linux內核移植 --- 執行個體學習中斷背後的知識(1)

關于Linux的中斷子系統這部分知識可以參考下面幾篇蝸窩科技的博文,這幾篇講的比較偏理論,結合執行個體的話,會更容易了解。

Linux kernel的中斷子系統之(一):綜述

Linux kernel的中斷子系統之(二):IRQ Domain介紹

linux kernel的中斷子系統之(三):IRQ number和中斷描述符

linux kernel的中斷子系統之(四):High level irq event handler

Linux kernel中斷子系統之(五):驅動申請中斷API

Linux kernel的中斷子系統之(六):ARM中斷處理過程

linux kernel的中斷子系統之(七):GIC代碼分析

首先看一下涉及到的裝置樹中的節點:

說明:

tiny4412上的root gic就是上面的"arm,cortex-a9-gic",它的interrupt cells是3, 表示引用gic上的一個中斷需要三個參數

pinctrl@11000000的interrupt parent是interrupt-controller@10490000,可以看到,它的interrupts屬性含有三個參數,含義是引用GIC的SPI-46

gpx3本身也充當一個中斷控制器,它的interrupt parent也是interrupt-controller@10490000,gpx3的interrupt cell是2, 表示引用gpx3的一個中斷需要2個參數

interrupt_xeint26_29的interrupt parent是gpx3,它的interrupts含有四組參數,分别對應gpiox3_2、gpiox3_3、gpiox3_4和gpiox3_5,每組的第二個參數表示的是中斷類型,IRQ_TYPE_EDGE_FALLING表示下降沿觸發,可以參考arch/arm/boot/dts/include/dt-bindings/interrupt-controller/irq.h

wakeup-interrupt-controller我覺得隻是一個軟體上面的抽象,對應的是XEINT16-31,其interrupts對應的就是SPI-32,從datasheet上也可以看到,EINT16-31對應的都是SPI-32.

下面分幾個部分來說明一下,這裡不适合把大段的核心代碼貼過來,隻把一些關鍵的部分列出來,對于自己詳細分析核心代碼有幫助。

第一部分: GIC中斷控制器的注冊

第二部分:裝置樹的device node在向platfomr_device轉化的過程中節點的interrupts屬性的處理

第三部分:GPIO控制器驅動的注冊,大部分GPIO控制器同時具備interrupt controller的功能,就像上面的GPIOX3和GPIOM4等等

第四部分:引用GPIO中斷的節點的解析

相關代碼:

drivers/irqchip/irq-gic.c

arch/arm/mach-exynos/exynos.c

arch/arm/kernel/entry-armv.S

gic中斷控制器的初始化和注冊是在函數gic_of_init中做的,這個函數是怎麼被執行到的呢?這個檔案中定義了下面的結構:

分析發現,IRQCHIP_DECLARE宏會定義出一個__of_table_cortex_a9_gic的變量,gic_of_init被指派給其data成員,這個變量被存放到了核心鏡像的__irqchip_of_table段,在kernel啟動時平台代碼exynos.c中的函數exynos_init_irq會被調用,這個函數會調用irqchip_init --> of_irq_init,of_irq_init就會周遊__irqchip_of_table,按照interrupt controller的連接配接關系從root開始,依次初始化每一個interrupt controller,此時gic_of_init會被調用,比如以下面這張圖為例:

基於tiny4412的Linux內核移植 --- 執行個體學習中斷背後的知識(1)

上圖中每一個圓圈都代表一個interrupt-controller,以此都成了系統的中斷樹,其中的數字表示的是of_irq_init函數初始化中斷控制器的順序。

gic_of_init主要做如下幾件事:

設定__smp_cross_call為gic_raise_softirq, 它的作用是觸發SGI中斷,用于CPU之間通信

設定handle_arch_irq為gic_handle_irq。在kernel發生中斷後,會跳轉到彙編代碼entry-armv.S中__irq_svc處,進而調用handle_arch_irq,進而進入GIC驅動,進行後續的中斷處理

計算這個GIC子產品所支援的中斷個數gic_irqs,然後建立一個linear irq domain。此時尚未配置設定virq,也沒有建立hwirq跟virq的映射

在初始化的時候既沒有給hwirq配置設定對應的virq,也沒有建立二者之間的映射,這部分工作會到後面有人引用GIC上的某個中斷時再配置設定和建立。

drivers/of/platform.c

這個轉化過程是調用of_platform_populate開始的,以pinctrl@11000000為例,暫時隻關心interrupts屬性的處理,函數調用關系:

of_platform_populate

  ---> of_platform_bus_create

    ---> of_platform_device_create_pdata

      ---> of_device_alloc:

這裡主要涉及到兩個函數of_irq_count和of_irq_to_resource_table,傳入的np就是pinctrl@11000000節點。

of_irq_count

這個函數會解析interrupts屬性,并統計其中描述了幾個中斷。

簡化如下:找到pinctrl@11000000節點的所隸屬的interrupt-controller,即interrupt-controller@10490000節點,然後獲得其#interrupt-cells屬性的值,因為隻要知道了這個值,也就知道了在interrupts屬性中描述一個中斷需要幾個參數,也就很容易知道interrupts所描述的中斷個數。這裡關鍵的函數是of_irq_parse_one:

nr表示的是index,of_irq_parse_one每次成功傳回,都表示成功從interrupts屬性中解析到了第nr個中斷,同時将關于這個中斷的資訊存放到irq中,struct of_phandle_args的含義如下:

最後将解析到的中斷個數傳回。

of_irq_to_resource_table

知道interrupts中描述了幾個中斷後,這個函數開始将這些中斷轉換為resource,這個是由of_irq_to_resource函數完成。

第二個參數i表示的是index,即interrupts屬性中的第i個中斷。

是以,分析重點是irq_of_parse_and_map,這個函數會獲得pinctrl@11000000節點的interrupts屬性的第index個中斷的參數,這是通過of_irq_parse_one完成的,然後獲得該中斷所隸屬的interrupt-controller的irq domain,也就是前面GIC注冊的那個irq domain,利用該domain的of_xlate函數從前面表示第index個中斷的參數中解析出hwirq和中斷類型,最後從系統中為該hwriq配置設定一個全局唯一的virq,并将映射關系存放到中斷控制器的irq domain中,也就是gic的irq domain。

下面結合kernel代碼分析一下:

    ---> irq_create_of_mapping

        ---> irq_create_fwspec_mapping

看一下gic irq domain的translate的過程:

            --->gic_irq_domain_translate

通過這個函數,我們就獲得了fwspec所表示的hwirq和type

接着看一下irq_find_mapping,如果hwirq之前跟virq之間發生過映射,會存放到irq domain中,這個函數就是查詢irq domain,以hwirq為索引,尋找virq

            ---> irq_find_mapping

 下面分析virq的配置設定以及映射,對于GIC irq domain,由于其ops定義了alloc,在注冊irq domain的時候會執行domain->flags |= IRQ_DOMAIN_FLAG_HIERARCHY

            ---> irq_domain_alloc_irqs

                ----> irq_domain_alloc_irq_data 會根據virq獲得對應的irq_desc,然後将domain指派給irq_desc->irq_data->domain

                ----> irq_domain_alloc_irqs_recursive 這個函數會調用gic irq domain的domain->ops->alloc,即gic_irq_domain_alloc

下面分析irq_create_mapping,對于irq domain的ops中沒有定義alloc的domain,會執行這個函數

            ---> irq_create_mapping 為hwirq配置設定virq,并存放映射到irq domain中

至此,device node在轉化為platform_device過程中的interrupts屬性的處理就暫時分析完畢,後面會注冊該platform_device,然後比對到的platform_driver的probe就會被調用。

drivers/pinctrl/samsung/pinctrl-samsung.c

drivers/pinctrl/samsung/pinctrl-exynos.c

在pinctrl@11000000節點轉化成的platform_device被注冊的時候,samsung_pinctrl_probe會被調用。這個函數目前我們先隻分析跟中斷相關的。

首先分析一個samsung_pinctrl_get_soc_data

    ----> samsung_pinctrl_get_soc_data

接着分析samsung_gpiolib_register

    ----> samsung_gpiolib_register

對于普通的可以産生中斷的gpio,會由exynos_eint_gpio_init處理

    ----> exynos_eint_gpio_init

上面也隻是建立了irq domain,還沒有存放任何中斷映射關系,在需要的時候才會映射。

對于具備喚醒功能的外部中斷功能的gpio,由exynos_eint_wkup_init處理

    ----> exynos_eint_wkup_init

上面第44和第54行調用irq_of_parse_and_map是必須的,此時相當于兩級中斷控制器的級聯(第一級是gic,第二級是gpx0、gpx1、gpx2和gpx3),它們雖然在實體上面确實連接配接上了,但是還是必須調用該函數将這兩級中斷控制器在軟體上面連接配接配置起來,這樣在第二級中斷發生以後,才能順利地從第一級中斷進入第二級中斷,要完成這一功能,還有一個很重要的函數是48和58行的irq_set_chained_handler_and_data,這個函數會修改virq對應irq_desc的handle_irq,如exynos_irq_demux_eint16_31,這個函數是從第一級中斷gic進入第二級中斷gpx3的入口函數。

下面開始第四部分。

這裡以上面裝置樹中的interrupt_xeint26_29為例,這個節點的interrupt-parent就是gpx3,其interrupts屬性中一共描述了四個中斷,分别是gpx3_2、gpx3_3、gpx3_4和gpx3_5, 分别對應XEINT26到XEINT29.

有個前面第二部分和第三部分的基礎,在将interrupt_xeint26_29轉換成為platform_device的時候,會解析其interrupts屬性,這部分請參考第二部分的分析,不同之處是此時的irq domain是gpx3對應的irq domain,期間會調用該domain的ops->xlate函數,即從第三部分的分析知道,domain的ops就是exynos_eint_irqd_ops,檢視定義可以知道xlate是irq_domain_xlate_twocell,這是kernel提供的對#interrupt-cells為2的中斷控制器的通用處理:

在從interrupts獲得第index個中斷的hwirq和irq type後,就會為這個hwirq從kernel中配置設定一個全局為一個virq,以及對應的irq_desc,然後将它們的映射關系存放到對應gpx3的irq domain中。

從這裡知道了,在Samsung平台上面,每一個gpio bank都對應自己的irq_chip和irq_domain的好處,以gpx3_2為例,它的hwirq就是2,但是要注意,這裡的hwirq僅僅在所處的irq_domain或者說irq_chip内才有意義,不同的irq_domain可能會有相同的hwirq,比如gpx2_2的hwirq也是2,但是每一個hwirq對應的virq是系統唯一的,virq其實就是全局變量allocated_irqs的一個位号,hwirq和virq的映射關系存放在hwirq所處的irq domain中,通過hwirq在所屬的irq domain内可以迅速索引到virq,然後用virq可以索引到對應的唯一的irq_desc,在irq_desc中也有專門的變量用于存放virq、hwirq以及irq_domain,我們在驅動中申請中斷時看到的都是virq,沒有必要關心hwirq或者irq_desc。

下面我們結合開機log,看一下上面框圖中的中斷映射:

要看到這些log,需要打開部分代碼的log或者自己添加一些log語句,下面是patch:

結合開機log,

可以得到下面的中斷映射圖:

基於tiny4412的Linux內核移植 --- 執行個體學習中斷背後的知識(1)

可以看到,每一個hwirq在kernel中都會對應一個唯一的virq,它們的映射關系存放在所屬的irq domain中,每一個virq又可以找到唯一的irq_desc.

arch/arm/kernel/traps.c 

arch/arm/mm/mmu.c

我們以上面框圖中三個比較典型的中斷為例分析: 

1. XEINT15: 因為這個中斷直接對應到了GIC子產品上面的SPI-31

2. XEINT26:因為XEINT24-XEINT31共用了GIC子產品上面的SPI-32,在處理過程中會涉及到demux

3. GPM4-0:: 這是一個普通的可以産生中斷的gpio,在上圖中的pinctrl中具備這個功能的gpio共享的是pinctrl在GIC上面的中斷SPI-46

關于ARM的中斷知識可以參考下面的一篇部落格: Exynos4412裸機開發——中斷處理

XEINT15

彙編部分不打算過多分析,這部分在網上有大量的文章(如Exynos4412 中斷處理流程詳解)。這裡隻需要知道,在irq中斷發生後,PC指針會跳轉到中斷向量表(起始位址0xffff0000)中負責處理irq中斷的位置:

在vector_irq中會跳轉到__irq_svc執行, 緊接着從__irq_svc又跳到irq_handler,irq_handler其實是個宏,它完成的操作是将PC指派為handle_arch_irq的位址。

handle_arch_irq這個之前在第一部分 GIC控制器中說過,在GIC驅動中會将handle_arch_irq設定為gic_handle_irq,這樣GIC就接管了剩下的工作。

gic_handle_irq

    ---> handle_domain_irq

        ---> __handle_domain_irq(domain, hwirq, true, regs)

            ----> generic_handle_irq

以XEINT15為例,在第三部分 GPIO控制器驅動注冊中exynos_eint_wkup_init-->irq_set_chained_handler_and_data(irq, exynos_irq_eint0_15, &weint_data[idx])

函數irq_set_chained_handler_and_data完成的一個作用就是将virq對應的irq_desc的handle_irq設定為exynos_irq_eint0_15

                ---> exynos_irq_eint0_15

 到這裡似乎分析不下去了,怎麼又到generic_handle_irq?eint_irq對應的irq_desc的handle_irq是什麼東東?

不要忘了,要想使用XEINT15,一般的做法是,先在裝置樹中配置,如:

可以看到,實際在裝置樹中配置的是gpx1_7,其中gpx1的定義如下:

這裡可以算是兩級中斷控制器的級聯了,第一級是GIC中斷控制器,第二級是gpx1這個中斷控制器,隻不過這gpx1上面引用了gic的8個中斷,從spi-24一直到spi-31。這些中斷跟gpx1的8個gpio引腳是一一對應的。而且,gpx1定義裡的interrupts屬性并不是多餘,在第三部分gpio控制器驅動中,函數exynos_eint_wkup_init會處理interrupts屬性,映射關系存放在gic irq domain中,可以想象一下,如果這裡沒有interrupts屬性,gpx1跟gic之間雖然有實體上的連接配接,但是軟體上沒有配置,根本無法完成級聯工作。還有,我們能否直接越過gpx1_7,直接去申請XEINT15?答案是不能?盡管軟體上面在映射時将SPI-31對應的virq的irq_desc的handle_irq初始化為了handle_fasteoi_irq,但是如果不經過gpx1_7,怎麼觸發這個中斷呢?

回到正題,裝置樹裡配置好之後,接下來kernel會将interrupt_xeint15節點的interrupts屬性轉為resource,期間會進行中斷映射。在對應的驅動程式中,我們要做的就剩下獲得這個irq resource,然後調用request_irq,簡單看一下。

requset_irq 

    ---> request_threaded_irq 

        ---> __setup_irq 

            ---> __irq_set_trigger(desc, new->flags & IRQF_TRIGGER_MASK) 

                ---> chip->irq_set_type(&desc->irq_data, flags)

這裡會調用virq所屬的irq_chip的irq_set_type函數,對于gpx1_7就是exynos4210_wkup_irq_chip,它的irq_set_type是exynos_irq_set_type

好了,知道這個後,前面的分析就有眉目了,剛才分析到gpx1_7對應的eint_irq的irq_desc的handle_irq, 其實就是handle_edge_irq

handle_edge_irq

    ---> handle_irq_event

        ---> handle_irq_event_percpu(desc)

            ---> __handle_irq_event_percpu(desc, &flags)

到這裡XEINT15的處理就分析完了。

XEINT26

有了分析XEINT15的基礎,我們隻需要注意不同點。

前面我們知道,XEINT16-31共享了GIC上面的SPI-32,按照分析XEINT15的邏輯:

vector_irq

    ---> __irq_svc

        ---> irq_handler

            ---> gic_handle_irq

                ---> handle_domain_irq

                    ---> __handle_domain_irq

                        ---> exynos_irq_demux_eint16_31

從名字上面都可以看出,這裡要做demux處理:

    ---> exynos_irq_demux_eint

 根據分析XEINT15的邏輯,如果在申請gpx3_2對應的中斷時是選的是邊沿觸發,就是handle_edge_irq,如果是電平觸發,就是handle_level_irq。剩下的分析跟XEINT15一樣了。

GPM4-0

這個跟XEINT不同之處是這是一個普通的可以産生中斷的gpio,這些gpio将來都會pinctrl在GIC上面的SPI-46觸發GIC中斷。在第三部分GPIO控制器驅動中會調用exynos_eint_gpio_init,這個函數首先調用devm_request_irq對SPI-46對應的virq進行了申請,中斷處理函數是exynos_eint_gpio_irq,在這個函數中會查詢到底是那個中斷被觸發了,然後進行demux處理。之後,同樣也建立了irq_domain和irq_chip,這裡的irq_chip是exynos_gpio_irq_chip,它的irq_set_type也是exynos_irq_set_type。

跟XEINT還有一個不同的是,并沒有對SPI-46對應的virq的irq_desc->handle_irq進行修改,保持的還是映射時的初始化值handle_fasteoi_irq。

                        ---> handle_fasteoi_irq

                            ---> handle_irq_event

                                ---> handle_irq_event_percpu

                                    ---> __handle_irq_event_percpu

前面說過,在__handle_irq_event_percpu中會周遊irq_desc的act連結清單,此時就會調用到剛才注冊的中斷處理函數exynos_eint_gpio_irq

                                        ---> exynos_eint_gpio_irq

如果是按照邊沿方式申請的,後面會調用handle_edge_irq,否則是handle_level_irq。

可以将上面三個裝置樹節點的驅動都放到一個驅動裡,也可以分開。為了簡單起見,這裡分開。

這裡僅以interrupt_xeint26_29.c為例,這個是interrupt_xeint26_29對應的驅動程式,其他兩個基本類似,下載下傳位址: https://files.cnblogs.com/files/pengdonglin137/interrupts_demo_drivers.tar.gz

上面的驅動非常簡單,沒什麼好說的,在中斷處理函數中可以将調用棧列印出來,驗證一下我們上面的分析是否正确。

對于interrupt_xeint26_29分别對應的是tiny4412開發闆底闆上面的四個按鍵,對于interrupt_xeint14_15,當點選tiny4412的觸摸屏的時候,XEINT14會被觸發,對于interrupt_gpm4_0,在加載驅動時會被觸發(因為這個gpio接到了led上面,這裡隻是示例)

下面是這三個驅動申請的中斷被觸發時的調用棧:

interrupt_xeint26_29:

interrupt_xeint14_15:

interrupt_gpm4_0:

可以對照一下,跟上面的分析是否一緻。

加載并測試完成上面的三個驅動後,我們可以看一下此時系統的interrupt觸發情況

圖中加紅的部分就是我們在驅動中申請到的中斷在kernel裡的記錄資訊,一般隻有被request的中斷才會出現在上面的記錄之中。

這裡第14行的11000000.pinctrl的中斷觸發計數需要注意一下,可以看到它在CPU0上觸發了1次,其實也就是23行的gpm4_0在CPU0上面的觸發次數,即11000000.pinctrl其實是其下的像gpm4_0這樣的普通gpio中斷總和,也容易了解。

上面顯示的資訊太多,每個字段有時啥意思?我們結合代碼看看。

生成interrupts這個檔案的代碼是 fs/proc/interrupts.c

在 fs/proc/interrupts.c中會調用proc_create("interrupts", 0, NULL, &proc_interrupts_operations)建立,代碼如下:

結合上面/proc/interrupts的輸出分析一下show_interrupts

我們以tiny4412,xint26_29-0為例解釋一下每個字段的含義:

基於tiny4412的Linux內核移植 --- 執行個體學習中斷背後的知識(1)

virq 是全局唯一的,映射關系存放在所屬的中斷控制的irq_domain内

hwirq 隻在所屬的中斷控制器的domain内才有意義

列出的中斷控制器是該virq中斷的直屬上級

接下來說一下arch_show_interrupts(p, prec),對應的函數實作在arch/arm/kernel/irq.c中,是:

可以看到,對于SMP,才輸出IPI,這個好了解,IPIs存在的目的是CPU之間通信用的,如果隻有一個CPU,當然就不需要了。

函數show_ipi_list定義在arch/arm/kernel/smp.c中:

完。

繼續閱讀