按鍵中斷驅動程式

tstars發表於2024-05-27

前言:前面我們完成了led、按鍵的驅動程式開發,但是這兩者本質是一樣的,都是操作GPIO引腳,本次實驗將基於按鍵去學習如何開發中斷驅動程式,按鍵按下時產生一個外部中斷,然後在中斷處理程式中控制led的亮滅。

1、中斷系統介紹

1.1 中斷系統的概念
中斷指的是在程式正常執行過程中發生了某一事件(比如按下某個按鍵、檢測到某個訊號、甚至可以是使用程式軟體觸發的事件等),程式在執行完當前指令後會跳轉到記憶體空間中特定的位置(地址)去執行,這個特定的位置一般是由晶片硬體規定的,部分晶片也提供了暫存器可用於配置這個特定的位置應該在何處。這種能夠打斷程式正常執行的事件通常是很多個,因此對每個事件都應該有一個特定的跳轉位置,這些位置一般是被設定為一片緊挨著連續的空間,這些空間就叫做中斷向量表。不同架構的晶片的中斷系統會存在差異,但是概念是一樣的,下面會分別介紹cortex-M核心和cortex-A核心的中斷系統。

1.2 cortex-M核心的中斷系統
在STM32中,有一個硬體用於管理中斷,這個硬體叫做NVIC(內嵌向量中斷控制器)。STM32屬於cortex-M核心,cortex-M核心支援16個系統中斷和240個外部中斷,具有256級的可程式設計中斷優先順序設定,但是一般在具體的實現中不會全部實現,STM32F407的系統中斷只有10個,外部中斷有82個。對於cortex-M核心來說,每一箇中斷都是獨立的,沒有進行分類/分級的樹狀結構,因此對於每一箇中斷來說都有一箇中斷向量地址,因此它的中斷向量表一共有256箇中斷地址。在實際程式設計中,由於晶片並不是實現了全部的256箇中斷,因此在中斷向量表中就會使用一條無意義的指令作為佔位來跳過這些沒有實現的中斷,以保持序號的不變。

1.3 cortex-A核心的中斷系統

1.3.1 cortex-A核心中斷系統概述

對於cortex-A核心,也有中斷向量表,只不過採用了分級/分類的樹形結構,因此中斷向量表的佔用空間更小更簡潔。cortex-A核心只有8箇中斷(異常),因此中斷向量表只有8項,這8箇中斷其實應該是8類中斷

上表中,IRQ中斷和FIQ中斷實際上不是單指某一箇中斷,而是某一類中斷,這兩類中斷下面還可以細分為多種具體的中斷。
一般我們常用的就是外部中斷(IRQ)。
對於Cortex-A架構的晶片來說,cpu所有的外部中斷都屬於IRQ中斷,也即任意一個外部中斷髮生都會最終表現給cpu核心的都是IRQ中斷,相當於整合了。但是具體是IRQ中斷中得到哪一個,可以在IRQ中斷函式里面讀取指定的暫存器來確定,再跳轉到真正的中斷服務程式去執行。

如上圖所示,可以認為IRQ是一個父節點,其下有多個子節點,也就是具體的中斷。當左側的任意一箇中斷髮生的時候都會觸發IRQ中斷。

在cortex-A架構的晶片中,用於管理中斷的硬體叫GIC控制器。

從上圖可以看出,GIC接收外部中斷,最終只給核心四個訊號,其中FIQ和IRQ就是快速中斷和外部中斷,VFIQ和VIRQ是用於虛擬化的,猜測是提供給作業系統用於中斷號對映的硬體輔助。

下面以GIC的V2版本為例進一步介紹GIC。

檢視上圖可以看到GIC控制器分為兩部分,第一部分就是分發器(Distributor),第二部分就是CPU介面端,前者用於接收發生的具體中斷,進行處理後傳送給CPU介面端,CPU介面端才會傳送請求給核心,因此CPU介面端是分發器和CPU核心的橋樑。
對於GIC中斷控制器來說,中斷可以分為三類:
(1)SPI共享中斷:所有核共享的中斷,也即所有的核心都可以處理這些中斷,比如按鍵中斷、串列埠中斷;
(2)PPI私有中斷:每個核都會有屬於自己的中斷,這些中斷就是PPI中斷,這些中斷只能由特定得到核心才能夠處理;
(3)SGI軟體生成中斷:透過軟體向特定的暫存器寫入指定的內容才觸發這個中斷,通常作業系統的系統呼叫就是透過這個中斷來陷入核心實現。

對於Cortex-A架構的晶片,每個CPU核都最多支援1020箇中斷,中斷ID範圍是0 ~ 1019,其中前16箇中,ID0 ~ ID15屬於SGI中斷,接下來的16箇中斷,ID16 ~ ID31屬於這個核的PPI中斷,剩下的ID32 ~ ID1019屬於所有核共享的SPI中斷,也可以認為每個CPU擁有一份SPI的備份,但實際上在片上系統中只有1份SPI,是屬於所有核共享的,具體發生時由哪個核處理,由GIC進行控制。

這裡插入一個個人對於片上系統資源和CPU內部資源的理解:在目前的晶片上,除了由CPU核心外,還整合了許多的外設介面,對於這些外設介面,我們使用的那些名為SW_MUX_PAD什麼的暫存器來進行配置和操作,這些暫存器實際上屬於記憶體資源,所有我們看到它們的地址其實是跟真正的記憶體DDR統一編址,訪問它們就和訪問記憶體一樣,這些資源是屬於所有CPU核心的,是屬於片上系統資源。而每個CPU核心有自己的暫存器比如R0,R1,...,Rn這些屬於CPU的通用暫存器,是CPU自帶的,每個CPU核是獨立擁有的,不同於前者的那些介面配置和操作暫存器。

GPIO中斷、串列埠中斷等都屬於SPI中斷,具體哪個中斷ID對應哪個中斷由半導體廠商定義,下面是IMX6ULL晶片的中斷源及其對應的中斷ID。

在IMX6ULL晶片中支援128個SPI中斷,再加上PPI和SGI一共32箇中斷,IMX6ULL支援的中斷源有160個。

1.3.2 GIC中斷控制器
前面已經提到,GIC控制器分為分發器端和CPU介面端,這兩部分要相互配合才能完成GIC控制器的功能。
分發器(Distributor)主要完成如下工作:

CPU介面端(CPU Interface)主要完成以下工作:
(1)使能或者關閉傳送到 CPU Core 的中斷請求訊號;
(2)應答中斷;
(3)通知中斷處理完成;
(4)設定優先順序掩碼,透過掩碼來設定哪些中斷不需要上報給 CPU Core;
(5)定義搶佔策略;
(6)當多箇中斷到來的時候,選擇優先順序最高的中斷通知給 CPU Core。

分發器和CPU介面端是串聯工作的,只有兩部分都完成配置才能讓GIC中斷控制器真正的工作。

下面簡單介紹一下如何配置GIC控制器,在linux驅動開發中這部分一般是由晶片廠家的BSP工程師完成,但是我們也應該要有一定的瞭解,就是我要懂得,但是沒必要做這個苦差事。
對於GIC中斷控制器的暫存器配置,涉及到CP15協處理器,這一部分的內容可以參考正點原子的Linux驅動開發資料,其中有一個我認為比較重要的點就是使用CP15協處理器設定中斷向量表的偏移。

回憶在編寫裸機例程的時候,我們設定程式的連結地址為0x87800000,因此當我們編譯完成程式後,使用imxdownload軟體生成.imx檔案燒入sd卡,其實是編譯生成的.bin檔案前面加上了一系列的頭部,這些頭部裡面就包含了0x87800000這個資訊,上電前設定啟動方式為sd卡,上電後晶片的內部boot rom程式碼會執行,然後讀取sd卡中的程式碼並將其中的.bin讀取到記憶體的0x87800000處,晶片的內部boot rom程式碼執行完一些初始化操作後會跳轉到連結地址0x87800000處執行。但是,剛上電時並沒有設定中斷向量表得到偏移,因此中斷向量表的起始地址是0,因此復位中斷的中斷向量是0,但是最終程式會跳到0x87800000處執行,並且我們自己設定的中斷向量表地址也從那裡開始,因此跳到0x87800000那裡之後,那裡是我們寫的一條復位中斷的跳轉指令,跳轉到我們自己寫的復位函式里執行,在那裡我們就需要設定中斷向量表的偏移為0x87800000,這樣當發生中斷時,程式碼才會跳到我們設定好的中斷向量處執行而不是跳到boot rom的中斷向量表執行。

接下來介紹中斷的使能
中斷的使能包括兩部分,一部分是總的中斷使能,一部分是具體的中斷使能。思考一下,所有的外部中斷經過中斷控制器後都表現為IRQ中斷,因此相當於有一個彙總,所以應該有一個總的中斷使能/禁止,然後對於某一個具體的中斷,可以使用也可以不使用,因此對於每一個具體的中斷應該還有中斷使能/禁止。通常,總的中斷使能應該有核心決定,因此是在通用暫存器CPSR中的I位決定中斷的總使能/總禁止。對於具體的某一箇中斷,由中斷控制器來確定使能/禁止,在IMX6ULL中就是由GIC的暫存器GICD_ISENABLERn來確定。

接下來介紹中斷的優先順序
GIC控制器最多支援256個優先順序(這裡的優先順序包括搶佔優先順序和次優先順序,也即256 = 搶佔優先順序個數 * 次優先順序個數),但是具體實現多少個優先順序由晶片廠家決定,數字越低優先順序越高。對於cortex-A核心,使用32個優先順序,也即用5位來表示優先順序,因此imx6ull的GICC_PMR暫存器需要配置為0b11111000

接下來要確定5位(級)優先順序中多少位確定搶佔優先順序,多少位確定子優先順序

如果5個優先順序位全部設定為搶佔優先順序,那麼就沒有子優先順序。

2、imx6ull的中斷硬體系統

3、裸機如何編寫中斷程式
在裸機中編寫中斷程式時顯然我們需要配置許多暫存器,包括GIC控制器的暫存器和CP15協處理器的暫存器,總的流程如下:
(1)確定使用哪個中斷;
(2)配置GIC中斷控制器,包括設定GIC的優先順序位數,搶佔優先順序和子優先順序位數,設定中斷的觸發模式,設定中斷的優先順序,使能具體的中斷,使能總中斷等;
(3)編寫中斷處理函式;
(4)配置CP15協處理器的額暫存器;
(5)設定中斷向量表;

上述只是列出了一個大概的程式關鍵點,可以看出配置中斷系統的暫存器還是比較麻煩的,所以在嵌入式Linux驅動開發中,這部分工作一般是晶片廠家的BSP工程師完成,我們只需要按照它們得到說明書來在他們的基礎上開發就行,術業有專攻,我們可以不幹這些麻煩的事情,但是我們也得懂得原理,畢竟有時候可能還得親自上陣。

4、使用Linux框架編寫中斷驅動程式

在linux驅動開發中,關於GIC中斷控制器,晶片廠家的BSP工程師通常已經寫好了相應的驅動和框架,我們要做的就是基於這個框架實現我們想要的外設的中斷驅動。

4.1 Linux系統中如何處理中斷
在Linux系統中,中斷分為硬體中斷和軟體中斷,對於硬體中斷要求:不能巢狀,越快越好。軟體中斷通常是用於實現系統呼叫時陷入核心執行核心中對應的函式。

作業系統執行硬體中斷時要求不能巢狀,越快越好,但是對於確實需要較長時間的中斷,該如何處理?
採用將中斷分為上下部的方法來處理,其中上半部在關中斷的狀態下執行,下半部在開中斷的狀態下執行,這樣在中斷下半部是執行被其它中斷打斷的。
對於中斷下半部,也有兩種方法,分別是使用tasklet或者執行緒化。
(1)使用tasklet處理中斷下半部
這種方法時候中斷下半部執行時間不太長的任務,此時允許其它中斷髮生,但是簡單執行完上半部後又繼續回到原來的下半部執行,因此此時cpu並不能進行執行緒的排程。處理流程如下圖所示。


在tasklet中,多箇中斷的下半部是匯聚在一起處理的,具體分析可以看韋東山的驅動開發文件。

(2)使用工作佇列-中斷下半部執行緒化
對於耗時比較長的中斷比如一分鐘,兩分鐘,通常是將中斷下半部作為一個核心執行緒來執行,在中斷上半部喚醒核心執行緒,這樣其它執行緒也有機會執行而不會卡頓。這個核心執行緒一般是由系統建立,叫做kworker執行緒,kworker執行緒會去工作佇列(worker queue)取出一個work來執行裡面的函式。

為了實現對中斷的軟體支援,Linux系統提供了一系列的資料結構,下面介紹Linux 中斷系統中的重要資料結構

上圖列出了Linux中斷系統的重要資料結構及其連線方式
最核心的資料結構體是struct irq_desc結構體。對於每一個硬體中斷,都用一個struct irq_desc結構體來描述,對於所有使用的中斷,會透過一個struct irq_desc結構體陣列來同一管理。struct irq_desc結構體裡面有很多內容,核心的內容如下:

在struct irq_desc結構體中,handle_irq就是發生中斷時要去執行的上半部函式,action是中斷下半部,它是一個函式連結串列,可以是空,1個,也可以是多個。執行完handle_irq後會依次執行action連結串列裡的所有函式。

繼續看上面linux系統的中斷軟體處理流程,多個外部裝置共享一個引腳的GPIO中斷,所有的GPIO中斷最終傳送給GIC控制器一類中斷,比如IRQ中斷,然後才去中斷CPU的執行,那麼中斷處理的時候就是反過來的,首先會在中斷髮生後跳轉到對應的中斷向量地址後跳轉到這一類中斷的處理函式中讀取GIC控制器確定發生了哪一類中斷,也即圖中的A中斷,比如IRQ中斷,然後再讀取GPIO相關暫存器確定是發生了哪一個GPIO中斷,比如GPIO1_IO03引腳中斷,也即B中斷,然後然後執行B對應的irq_desc結構體的handle_irq函式,在裡面再呼叫結構體對應的action連結串列裡的函式。
根據上面的分析,我們需要做的工作如下:
(1)確定需要使用的中斷的GPIO
(2)為這個中斷編寫action中的函式,可以只有上半部或下半部,也可以兩個都有;
(4)向核心註冊編寫的中斷處理函式,實際上是建立action結構體
需要注意的是,在註冊中斷函式時,irq_desc結構體是在核心初始化時就已經建立好了,向核心註冊中斷函式時只是將某一個沒有被使用的irq_desc結構體和我們所使用的物理中斷關聯起來,並建立action結構體

而GIC部分的結構體及函式,一般廠家的BSP工程師已經幫我們做好,基於GPIO子系統我們也不需要直接讀取暫存器來獲取具體發生的GPIO中斷號,只需要在裝置樹裡指定好要使用的GPIO引腳並且在程式碼裡使用提供的API獲取中斷即可。

下面再看iqr_desc結構體中一個重要的結構體irqaction

在呼叫函式request_irq或request_threaded_irq時,實際上是建立一個irqaction結構體,然後放進irq_desc結構體的action連結串列中(如果還沒有關聯一個irq_desc結構體的話會先關聯一個irq_desc結構體)。

接下來是其中的irq_data結構體,關鍵是irq_data中的irq_chip結構體和irq_domain結構體,前者定義了一系列和晶片的中斷相關的函式,包括中斷使能,清中斷等等,後者在結構體中定義了將硬體中斷號轉換為軟中斷號的函式,以及解析裝置樹的中斷屬性的函式。

軟中斷號概念:在中斷系統,以irq中斷為例,有一組struct irq_desc結構體陣列,使用到的每一個硬體中斷都會和一個struct irq_desc結構體關聯起來,這個結構體在陣列中的序號就是軟中斷號。在向核心request_irq或者request_threaded_irq註冊中斷時,會將這個中斷硬體中斷號和一個軟體中斷號也即irq_desc陣列中的一個序號關聯起來作為一個數字對存放在記憶體特定區域,在發生中斷時就可以讀取暫存器獲取硬體中斷號後直接查表得到軟體中斷號然後執行所關聯的irq_desc結構體中的中斷處理函式。

關於irq_data結構體的詳細介紹參考韋東山的驅動開發入門手冊

以上關於Linux系統的中斷處理就介紹到這裡。

4.2 裝置樹
首先明確一點,中斷是屬於外設的功能,與引腳用於什麼模式無關,也即比如某一個引腳可以配置為uart的tx功能,也可以配置為GPIO的輸入功能,那麼我們就可以在它配置為GPIO輸入功能的情況下設定低電平觸發中斷(或者高電平,上升沿,下降沿),又或者某兩個引腳被配置為uart功能時,當接收緩衝區有資料時觸發中斷。
因此,按鍵中斷驅動程式中引腳的裝置樹節點和沒有使用中斷的是一樣的。

4.3 Linux中斷驅動程式碼編寫

點選檢視程式碼
/*
按鍵中斷驅動程式
在裝置樹中已經描述了所用的引腳,在裝置樹中利用gpio子系統配置了資訊,核心初始化時會根據配置資訊將引腳配置為GPIO輸入模式
在程式碼檔案中需要:
(1)指定引腳使用中斷功能,獲取引腳中斷號,編寫中斷處理函式,向核心註冊中斷
(2)填充file_operation的內容,並向核心註冊
(3)填充platform_driver結構體
(4)編寫驅動入口和出口函式

程式碼執行邏輯:註冊.ko驅動檔案時,執行init函式,在init函式中註冊platform_driver,註冊後會檢查是否有匹配的platform_device,如果有,執行platform_driver裡的
probe函式,在probe函式里需要註冊file_operation結構體以註冊驅動,建立class,建立裝置節點檔案,獲取裝置樹引腳資訊,當應用程式使用open開啟節點時會執行
file_operation中的open函式,在open函式里執行配置GPIO輸入,註冊中斷等操作。
*/

#include "asm/uaccess.h"
#include "linux/export.h"
#include "linux/irqreturn.h"
#include "linux/kdev_t.h"
#include "linux/wait.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/ktime.h>
#include <linux/delay.h>

static int major;
struct class *key_class;
static int key_val = 1;
static int condition = 0;
static DECLARE_WAIT_QUEUE_HEAD(key_wait);

struct key_gpio{
    int gpio;   //gpio引腳整數值
    struct gpio_desc *key_desc;  //gpio引進描述結構體
    int irq;    //引腳中斷號
    int flag;   //引腳標誌
};

static struct key_gpio *gpio_keys;  //定義一個結構體指標,用於動態記憶體分配,可以如果是多個引腳按鍵就是對應結構體陣列首地址

int key_open(struct inode *node, struct file *file)
{
    /*應用程式呼叫open開啟節點時會呼叫
    在這裡,設定引腳為輸出
    */

    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    gpiod_direction_input(gpio_keys->key_desc);
    return 0;
}

ssize_t key_read(struct file *file, char __user *user, size_t count, loff_t *offset)
{
    /*
    在read函式中,將當前執行緒休眠,等待按鍵中斷喚醒
    */
    int err;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    wait_event_interruptible(key_wait, condition);

    printk("key %d condition %d key_val %d\n", gpio_keys->gpio, condition, key_val); //當condition為0時程序睡眠,因此condition初始化為0,在中斷中將condition置1喚醒程序

    err = copy_to_user(user, &key_val, 4);
    

    key_val = 1;
    condition = 0;    //重置為0

    return count;
}

int key_close(struct inode *node, struct file *file)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    gpiod_direction_output(gpio_keys->key_desc, 0);    //設定gpio為輸出,並預設輸出高電平
    return 0;
}

struct file_operations key_opr = {
    .owner = THIS_MODULE,
    .open = key_open,
    .read = key_read,
    .release = key_close
};


static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    /*按鍵中斷處理函式,在這裡喚醒等待按鍵的休眠佇列*/
    struct key_gpio *key = dev_id;  //這裡體現了結構體的好處,可以一次性傳入許多資料
    // int val;
    
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    key_val = gpiod_get_value(key->key_desc);

    key_val ^= (1);   //為什麼要這麼操作?為什麼是引腳號左移8位?左移8位相當於乘上256

    condition = 1;  //置1,用於下面的喚醒程序
    wake_up_interruptible(&key_wait);

    return IRQ_HANDLED;
}

int key_probe(struct platform_device *pdev)
{
    /*
    驅動和裝置匹配時執行,在這裡需要的工作如下:
    (1)讀取GPIO資訊,配置為輸入
    (2)獲取引腳中斷號,註冊中斷
    (3)註冊file_operations結構體,註冊驅動
    (4)建立裝置
    */
    int err;
	struct device_node *node = pdev->dev.of_node;
    enum of_gpio_flags flag;    //用於獲取引腳的active狀態標誌位
    
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    gpio_keys = kzalloc(sizeof(struct key_gpio), GFP_KERNEL);

    gpio_keys->key_desc = gpiod_get(&pdev->dev, "key", 0); //獲取描述引腳的結構體
    gpio_keys->gpio = of_get_gpio_flags(node, 0, &flag);   //獲取描述引腳的整數值和標誌
    // if(gpio_keys->gpio < 0)
	// {
	// 	printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
	// 	return -1;
	// }

    gpio_keys->flag = flag & OF_GPIO_ACTIVE_LOW;
    gpio_keys->irq  = gpiod_to_irq(gpio_keys->key_desc);    //獲取引腳對應的中斷號,也可以用gpio_to_irq(gpio_keys->gpio)

    /*註冊中斷*/
    err = request_irq(gpio_keys->irq, gpio_key_isr, IRQF_TRIGGER_FALLING, "gpio_key", gpio_keys);

    /*註冊驅動,file_operations結構體*/
    major = register_chrdev(0, "key_interrupt", &key_opr);
    key_class = class_create(THIS_MODULE, "key_class");
    if(IS_ERR(key_class))
    {
        printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
        gpiod_put(gpio_keys->key_desc);
        unregister_chrdev(major, "key_interrupt");
        return PTR_ERR(key_class);
    }
    device_create(key_class, NULL, MKDEV(major, 0), NULL, "key_0");

    return 0;
}
int key_remove(struct platform_device *pdev)
{
    /*解除安裝裝置或驅動時執行
    銷燬裝置節點,銷燬類,撤銷驅動,撤銷中斷,撤銷引腳引用
    */
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    device_destroy(key_class, MKDEV(major, 0));
    class_destroy(key_class);
    unregister_chrdev(MKDEV(major, 0), "key_interrupt");

    free_irq(gpio_keys->irq, gpio_keys);
    gpiod_put(gpio_keys->key_desc);
    return 0;
}

static const struct of_device_id keys_match_table[] = {
    {
        .compatible = "mykey_driver_interrupt"
    }
};

struct platform_driver key_drv= {
    .probe = key_probe,
    .remove = key_remove,
    .driver = {
        .name = "mykey_driver_interrupt",        //driver中的.name欄位必須加上,不然執行時報錯,暫時不懂得為什麼
        .of_match_table = keys_match_table,
    }
};

static int __init key_drv_init(void)
{

    int err;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    err = platform_driver_register(&key_drv);

    return err;
}

static void __exit key_drv_exit(void)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    platform_driver_unregister(&key_drv);
}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");


相關文章