linux內核有中斷函數。在Linux內核中要想使用某個中斷是需要申請的,而request_irq()函數用于申請中斷,中斷使用完成以后就要通過free_irq()函數釋放掉相應的中斷;還有enable_irq()和disable_irq(),它們用于使能和禁止指定的中斷。
本教程操作環境:linux7.3系統、Dell G3電腦。
1.Linux中斷
1.1 Linux中斷API函數
request_irq函數
在 Linux 內核中要想使用某個中斷是需要申請的,request_irq 函數用于申請中斷,request_irq函數可能會導致睡眠,因此不能在中斷上下文或者其他禁止睡眠的代碼段中使用 request_irq 函數。request_irq 函數會激活(使能)中斷,所以不需要我們手動去使能中斷,request_irq 函數原型如下:
irq:要申請中斷的中斷號。
handler:中斷處理函數,當中斷發生以后就會執行此中斷處理函數。
flags:中斷標志,可以在文件 include/linux/interrupt.h 里面查看所有的中斷標志
name:中斷名字,設置以后可以在/proc/interrupts 文件中看到對應的中斷名字。
dev:如果將 flags 設置為 IRQF_SHARED 的話,dev 用來區分不同的中斷,一般情況下將dev 設置為設備結構體,dev 會傳遞給中斷處理函數 irq_handler_t 的第二個參數。
返回值:0 中斷申請成功,其他負值 中斷申請失敗,如果返回-EBUSY 的話表示中斷已經被申請了。
free_irq
使用中斷的時候需要通過 request_irq 函數申請,使用完成以后就要通過 free_irq 函數釋放掉相應的中斷。如果中斷不是共享的,那么 free_irq 會刪除中斷處理函數并且禁止中斷。free_irq函數原型如下所示:
函數參數和返回值含義如下:
irq:要釋放的中斷。
dev:如果中斷設置為共享(IRQF_SHARED)的話,此參數用來區分具體的中斷。共享中斷只有在釋放最后中斷處理函數的時候才會被禁止掉。
返回值:無。
中斷處理函數
使用 request_irq 函數申請中斷的時候需要設置中斷處理函數,中斷處理函數格式如下所示:
中斷使能和禁止函數
常用的中斷使用和禁止函數如下所示:
enable_irq 和 disable_irq 用于使能和禁止指定的中斷,irq 就是要禁止的中斷號。disable_irq函數要等到當前正在執行的中斷處理函數執行完才返回,因此使用者需要保證不會產生新的中斷,并且確保所有已經開始執行的中斷處理程序已經全部退出。在這種情況下,可以使用另外一個中斷禁止函數:
disable_irq_nosync 函數調用以后立即返回,不會等待當前中斷處理程序執行完畢。上面三個函數都是使能或者禁止某一個中斷,有時候我們需要關閉當前處理器的整個中斷系統,也就是在學習 STM32 的時候常說的關閉全局中斷,這個時候可以使用如下兩個函數:
local_irq_enable 用于使能當前處理器中斷系統,local_irq_disable 用于禁止當前處理器中斷系統。假如 A 任務調用 local_irq_disable 關閉全局中斷 10S,當關閉了 2S 的時候 B 任務開始運行,B 任務也調用 local_irq_disable 關閉全局中斷 3S,3 秒以后 B 任務調用 local_irq_enable 函數將全局中斷打開了。此時才過去 2+3=5 秒的時間,然后全局中斷就被打開了,此時 A 任務要關閉 10S 全局中斷的愿望就破滅了,然后 A 任務就“生氣了”,結果很嚴重,可能系統都要被A 任務整崩潰。為了解決這個問題,B 任務不能直接簡單粗暴的通過 local_irq_enable 函數來打開全局中斷,而是將中斷狀態恢復到以前的狀態,要考慮到別的任務的感受,此時就要用到下面兩個函數:
1.2 上半部和下半部
在有些資料中也將上半部和下半部稱為頂半部和底半部,都是一個意思。我們在使用request_irq 申請中斷的時候注冊的中斷服務函數屬于中斷處理的上半部,只要中斷觸發,那么中斷處理函數就會執行。我們都知道中斷處理函數一定要快點執行完畢,越短越好,但是現實往往是殘酷的,有些中斷處理過程就是比較費時間,我們必須要對其進行處理,縮小中斷處理函數的執行時間。比如電容觸摸屏通過中斷通知 SOC 有觸摸事件發生,SOC 響應中斷,然后通過 IIC 接口讀取觸摸坐標值并將其上報給系統。但是我們都知道 IIC 的速度最高也只有400Kbit/S,所以在中斷中通過 IIC 讀取數據就會浪費時間。我們可以將通過 IIC 讀取觸摸數據的操作暫后執行,中斷處理函數僅僅相應中斷,然后清除中斷標志位即可。這個時候中斷處理過程就分為了兩部分:
上半部:上半部就是中斷處理函數,那些處理過程比較快,不會占用很長時間的處理就可以放在上半部完成。
下半部:如果中斷處理過程比較耗時,那么就將這些比較耗時的代碼提出來,交給下半部去執行,這樣中斷處理函數就會快進快出。
因此,Linux 內核將中斷分為上半部和下半部的主要目的就是實現中斷處理函數的快進快出,那些對時間敏感、執行速度快的操作可以放到中斷處理函數中,也就是上半部。剩下的所有工作都可以放到下半部去執行,比如在上半部將數據拷貝到內存中,關于數據的具體處理就可以放到下半部去執行。至于哪些代碼屬于上半部,哪些代碼屬于下半部并沒有明確的規定,一切根據實際使用情況去判斷,這個就很考驗驅動編寫人員的功底了。這里有一些可以借鑒的參考點:
①、如果要處理的內容不希望被其他中斷打斷,那么可以放到上半部。
②、如果要處理的任務對時間敏感,可以放到上半部。
③、如果要處理的任務與硬件有關,可以放到上半部
④、除了上述三點以外的其他任務,優先考慮放到下半部。
下半部機制:
軟中斷
一開始 Linux 內核提供了“bottom half”機制來實現下半部,簡稱“BH”。后面引入了軟中斷和 tasklet 來替代“BH”機制,完全可以使用軟中斷和 tasklet 來替代 BH,從 2.5 版本的 Linux內核開始 BH 已經被拋棄了。Linux 內核使用結構體 softirq_action 表示軟中斷, softirq_action結構體定義在文件 include/linux/interrupt.h 中,內容如下:
在 kernel/softirq.c 文件中一共定義了 10 個軟中斷,如下所示:
NR_SOFTIRQS 是枚舉類型,定義在文件 include/linux/interrupt.h 中,定義如下:
可以看出,一共有 10 個軟中斷,因此 NR_SOFTIRQS 為 10,因此數組softirq_vec 有 10 個元素。softirq_action 結構體中的 action 成員變量就是軟中斷的服務函數,數組 softirq_vec 是個全局數組,因此所有的 CPU(對于 SMP 系統而言)都可以訪問到,每個 CPU 都有自己的觸發和控制機制,并且只執行自己所觸發的軟中斷。但是各個 CPU 所執行的軟中斷服務函數確是相同的,都是數組 softirq_vec 中定義的 action 函數。要使用軟中斷,必須先使用 open_softirq 函數注冊對應的軟中斷處理函數,open_softirq 函數原型如下:
nr:要開啟的軟中斷,在示例代碼 51.1.2.3 中選擇一個。
action:軟中斷對應的處理函數。
返回值:沒有返回值。
注冊好軟中斷以后需要通過 raise_softirq 函數觸發,raise_softirq 函數原型如下:
軟中斷必須在編譯的時候靜態注冊!Linux 內核使用 softirq_init 函數初始化軟中斷,softirq_init 函數定義在 kernel/softirq.c 文件里面,函數內容如下:
tasklet
tasklet 是利用軟中斷來實現的另外一種下半部機制,在軟中斷和 tasklet 之間,建議大家使用 tasklet。Linux 內核使用結構體
第 489 行的 func 函數就是 tasklet 要執行的處理函數,用戶定義函數內容,相當于中斷處理函數。如果要使用 tasklet,必須先定義一個 tasklet,然后使用 tasklet_init 函數初始化 tasklet,taskled_init 函數原型如下:
函數參數和返回值含義如下:
t:要初始化的 tasklet
func:tasklet 的處理函數。
data:要傳遞給 func 函數的參數
返回值:沒有返回值。
也可以使用宏DECLARE_TASKLET來一次性完成tasklet的定義和初始化DECLARE_TASKLET 定義在 include/linux/interrupt.h 文件中,定義如下:
其中 name 為要定義的 tasklet 名字,這個名字就是一個 tasklet_struct 類型的時候變量,func就是 tasklet 的處理函數,data 是傳遞給 func 函數的參數。
在上半部,也就是中斷處理函數中調用 tasklet_schedule 函數就能使 tasklet 在合適的時間運行,tasklet_schedule 函數原型如下:
關于 tasklet 的參考使用示例如下所示:
工作隊列
工作隊列是另外一種下半部執行方式,工作隊列在進程上下文執行,工作隊列將要推后的工作交給一個內核線程去執行,因為工作隊列工作在進程上下文,因此工作隊列允許睡眠或重新調度。因此如果你要推后的工作可以睡眠那么就可以選擇工作隊列,否則的話就只能選擇軟中斷或 tasklet。
Linux 內核使用 work_struct 結構體表示一個工作,內容如下(省略掉條件編譯):
這些工作組織成工作隊列,工作隊列使用 workqueue_struct 結構體表示,內容如下(省略掉條件編譯):
Linux 內核使用工作者線程(worker thread)來處理工作隊列中的各個工作,Linux 內核使用worker 結構體表示工作者線程,worker 結構體內容如下:
從示例代碼 51.1.2.10 可以看出,每個 worker 都有一個工作隊列,工作者線程處理自己工作隊列中的所有工作。在實際的驅動開發中,我們只需要定義工作(work_struct)即可,關于工作隊列和工作者線程我們基本不用去管。簡單創建工作很簡單,直接定義一個 work_struct 結構體變量即可,然后使用 INIT_WORK 宏來初始化工作,INIT_WORK 宏定義如下:
1.3 設備樹中斷信息節點
如果使用設備樹的話就需要在設備樹中設置好中斷屬性信息,Linux 內核通過讀取設備樹中的中斷屬性信息來配置中斷。對于中斷控制器而言,設備樹綁定信息參考文檔Documentation/devicetree/bindings/arm/gic.txt。打開 imx6ull.dtsi 文件,其中的 intc 節點就是I.MX6ULL 的中斷控制器節點,節點內容如下所示:
第 2 行,compatible 屬性值為“arm,cortex-a7-gic”在 Linux 內核源碼中搜索“arm,cortex-a7-gic”即可找到 GIC 中斷控制器驅動文件。
第 3 行,#interrupt-cells 和#address-cells、#size-cells 一樣。表示此中斷控制器下設備的 cells大小,對于設備而言,會使用 interrupts 屬性描述中斷信息,#interrupt-cells 描述了 interrupts 屬性的 cells 大小,也就是一條信息有幾個 cells。每個 cells 都是 32 位整形值,對于 ARM 處理的GIC 來說,一共有 3 個 cells,這三個 cells 的含義如下:
第一個 cells:中斷類型,0 表示 SPI 中斷,1 表示 PPI 中斷。
第二個 cells:中斷號,對于 SPI 中斷來說中斷號的范圍為 0~987,對于 PPI 中斷來說中斷號的范圍為 0~15。
第三個 cells:標志,bit[3:0]表示中斷觸發類型,為 1 的時候表示上升沿觸發,為 2 的時候表示下降沿觸發,為 4 的時候表示高電平觸發,為 8 的時候表示低電平觸發。bit[15:8]為 PPI 中斷的 CPU 掩碼。
第 4 行,interrupt-controller 節點為空,表示當前節點是中斷控制器。
對于 gpio 來說,gpio 節點也可以作為中斷控制器,比如 imx6ull.dtsi 文件中的 gpio5 節點內容如下所示:
第 4 行,interrupts 描述中斷源信息,對于 gpio5 來說一共有兩條信息,中斷類型都是 SPI,觸發電平都是 IRQ_TYPE_LEVEL_HIGH。不同之處在于中斷源,一個是 74,一個是 75,打開可以打開《IMX6ULL 參考手冊》的“Chapter 3 Interrupts and DMA Events”章節,找到表 3-1,有如圖 50.1.3.1 所示的內容:
從圖 50.1.3.1 可以看出,GPIO5 一共用了 2 個中斷號,一個是 74,一個是75。其中 74 對 應 GPIO5_IO00~GPIO5_IO15 這低 16 個 IO,75 對應GPIO5_IO16~GPIOI5_IO31 這高 16 位 IO。 第 8 行,interrupt-controller 表明了 gpio5 節點也是個中斷控制器,用于控制 gpio5 所有 IO
的中斷。
第 9 行,將#interrupt-cells 修改為 2。
打開 imx6ull-alientek-emmc.dts 文件,找到如下所示內容:
1.4 獲取中斷號
編寫驅動的時候需要用到中斷號,我們用到中斷號,中斷信息已經寫到了設備樹里面,因此可以通過 irq_of_parse_and_map 函數從 interupts 屬性中提取到對應的設備號,函數原型如下:
函數參數和返回值含義如下:
dev:設備節點。
index:索引號,interrupts 屬性可能包含多條中斷信息,通過 index 指定要獲取的信息。
返回值:中斷號。
如果使用 GPIO 的話,可以使用 gpio_to_irq 函數來獲取 gpio 對應的中斷號,函數原型如下:
2.驅動代碼
#include?<linux> #include?<linux> #include?<linux> #include?<linux> #include?<linux> #include?<linux> #include?<linux> #include?<linux> #include?<linux> #include?<linux> #include?<linux>? #include?<linux> #include?<linux> #include?<linux> #include?<linux> #include?<linux> #include?<linux> #include?<linux> #include?<asm> #include?<linux> #include?<linux> #define?IMX6UIRQ_CNT????????????1???????????????/*?設備號個數?*/ #define?IMX6UIRQ_NAME???????????"irqDev"????????/*?名字?*/ #define?KEY0VALUE???????????????0X01????????????/*?KEY0?按鍵值?*/ #define?INVAKEY?????????????????0XFF????????????/*?無效的按鍵值?*/ #define?KEY_NUM?????????????????1???????????????/*?按鍵數量?*/ /*?可能會有好多按鍵,通過結構體數組來描述按鍵?*/ /*?中斷?IO?描述結構體?*/ struct?irq_keydesc?{ ????int?gpio;???????????????????????????????/*?gpio?*/ ????int?irqnum;?????????????????????????????/*?中斷號?*/ ????unsigned?char?value;????????????????????/*?按鍵對應的鍵值?*/ ????char?name[10];??????????????????????????/*?名字?*/ ????irqreturn_t?(*handler)(int,?void?*);????/*?中斷服務函數?*/ }; /*?irq設備結構體?*/ struct?imx6uirq_dev?{ ????dev_t???????????????devid;??????????/*?設備號?*/ ????struct?cdev?????????cdev;???????????/*?字符設備?*/ ????struct?class????????*class;?????????/*?類?*/ ????struct?device???????*device;????????/*?設備?*/ ????int?????????????????major;??????????/*?注設備號?*/ ????int?????????????????minor;??????????/*?次設備號?*/ ????struct?device_node??*nd;????????????/*?設備節點?*/ ????atomic_t????????????keyvalue;???????/*?有效的按鍵鍵值?*/ ????atomic_t????????????releasekey;?????/*?標記是否完成一次完成的按鍵*/ ????struct?timer_list???timer;??????????/*?定義一個定時器*/ ????struct?irq_keydesc??irqkeydesc[KEY_NUM];?/*?按鍵描述數組?*/ ????unsigned?char???????curkeynum;??????/*?當前的按鍵號?*/ }; struct?imx6uirq_dev??irqDev;?/*?定義LED結構體?*/ /*?@description?:?中斷服務函數,開啟定時器,延時?10ms, *?定時器用于按鍵消抖。 *?兩個參數是中斷處理函數的必須寫法 *?@param?-?irq?:?中斷號 *?@param?-?dev_id?:?設備結構。 *?@return?:?中斷執行結果 */ static?irqreturn_t?key0_handler(int?irq,?void?*dev_id) { ????struct?imx6uirq_dev?*dev?=?(struct?imx6uirq_dev?*)dev_id; ????/*?采用定時器削抖,如果再定時器時間內還是這個值,說明是真的按下了,在定時器中斷中處理?*/ ????/*?這里設置為0是簡易處理,因為只有一個按鍵?*/ ????/*?有其他按鍵要再建一個中斷處理函數,并把curkeynum改成相應的按鍵值?*/ ????/*?注意不能所有按鍵用一個中斷函數,第一是一起按的時候會出錯?*/ ????/*?第二,無法用curkeynum判斷使用的是第幾個按鍵?*/ ????dev->curkeynum?=?0; ????/*?傳遞給定時器的參數,注意要強轉,在中斷處理函數里面再轉回來?*/ ????dev->timer.data?=?(volatile?long)dev_id; ????/*?mod_timer會啟動定時器,第二個參數是要修改的超時時間?*/ ????mod_timer(&dev->timer,?jiffies?+?msecs_to_jiffies(10)); ????return?IRQ_RETVAL(IRQ_HANDLED); } ?/*?@description?:?定時器服務函數,用于按鍵消抖,定時器到了以后 *?再次讀取按鍵值,如果按鍵還是處于按下狀態就表示按鍵有效。 *?@param?–?arg?:?設備結構變量 *?@return?:?無 */ void?timer_function(unsigned?long?arg) { ????unsigned?char?value; ????unsigned?char?num; ????struct?irq_keydesc?*keydesc; ????struct?imx6uirq_dev?*dev?=?(struct?imx6uirq_dev?*)arg;? ????/*?因為只有一個按鍵,這里是0?*/ ????num?=?dev->curkeynum; ????keydesc?=?&dev->irqkeydesc[num];? ????value?=?gpio_get_value(keydesc->gpio);?/*?讀取?IO?值?*/ ????if(value?==?0){?/*?按下按鍵?*/ ????????atomic_set(&dev->keyvalue,?keydesc->value); ????} ????else{?/*?按鍵松開?*/ ????????/*?這種情況是按下再松開的松開,使用keyValue加上releaseKey?*/ ????????/*?沒按下的話,?releasekey一直為0*/ ????????atomic_set(&dev->keyvalue,?0x80?|?keydesc->value); ????????atomic_set(&dev->releasekey,?1);?/*?標記松開按鍵?*/? ????}? } /* *?@description?:?按鍵?IO?初始化 *?@param?:?無 *?@return?:?無 */ static?int?keyio_init(void) { ????unsigned?char?i?=?0; ????int?ret?=?0; ????/*?1.獲取key節點?*/ ????irqDev.nd?=?of_find_node_by_path("/key"); ????if?(irqDev.nd==?NULL){ ????????printk("key?node?not?find!rn"); ????????return?-EINVAL; ????} ????/*?對每個按鍵都提取?GPIO?*/ ????for?(i?=?0;?i?private_data?=?&irqDev; ????return?0; } static?int?imx6uirq_release(struct?inode?*inode,?struct?file?*filp) { ????//struct?imx6uirq_dev??*dev?=?(struct?imx6uirq_dev??*)filp->private_data; ????return?0; } ?/* *?@description?:?從設備讀取數據 *?@param?–?filp?:?要打開的設備文件(文件描述符) *?@param?–?buf?:?返回給用戶空間的數據緩沖區 *?@param?-?cnt?:?要讀取的數據長度 *?@param?–?offt?:?相對于文件首地址的偏移 *?@return?:?讀取的字節數,如果為負值,表示讀取失敗 */ static?ssize_t?imx6uirq_read(struct?file?*filp,?char?__user?*buf,?size_t?cnt,?loff_t?*offt) { ????int?ret?=?0; ????unsigned?char?keyvalue?=?0;?????/*?按鍵值?*/ ????unsigned?char?releasekey?=?0;???/*?標記是否一次完成?*/ ????struct?imx6uirq_dev?*dev?=?(struct?imx6uirq_dev?*)filp->private_data; ????keyvalue?=?atomic_read(&dev->keyvalue); ????releasekey?=?atomic_read(&dev->releasekey); ????if?(releasekey)?{?/*?有按鍵按下?*/ ????????if?(keyvalue?&?0x80)?{ ????????????keyvalue?&=?~0x80;?/*?因為中斷中或了一個0x80,這里面去掉0x80?*/ ????????????ret?=?copy_to_user(buf,?&keyvalue,?sizeof(keyvalue)); ????????}?else?{ ????????????goto?data_error; ????????} ????????atomic_set(&dev->releasekey,?0);?/*?按下標志清零?*/ ????}?else?{?/*?沒有按下?*/ ????????goto?data_error; ????} ????return?0; data_error: ????return?-EINVAL; } /*?字符設備操作集?*/ static?const?struct?file_operations?imx6uirq_fops?=?{ ????.owner?=?THIS_MODULE, ????.open?=?imx6uirq_open, ????.release?=?imx6uirq_release, ????.read?=?imx6uirq_read }; /*?模塊入口函數?*/ static?int?__init?imx6uirq_init(void) { ????/*?定義一些所需變量?*/ ????int?ret?=?0; ????/*?1.?注冊字符設備驅動?*/ ????irqDev.major?=?0; ????if(irqDev.major)?{ ????????irqDev.devid?=?MKDEV(irqDev.major,?0); ????????ret?=?register_chrdev_region(irqDev.devid,?IMX6UIRQ_CNT,?IMX6UIRQ_NAME?);?? ????}?else?{ ????????alloc_chrdev_region(&irqDev.devid,?0,?IMX6UIRQ_CNT,?IMX6UIRQ_NAME?); ????????irqDev.major?=?MAJOR(irqDev.devid); ????????irqDev.minor?=?MINOR(irqDev.devid); ????} ????if(ret?<h2 id="3應用代碼"><strong>3.應用代碼</strong></h2> <pre class="brush:js;toolbar:false;">#include?<sys> #include?<sys> #include?<fcntl.h> #include?<stdio.h> #include?<unistd.h> #include?<stdlib.h> #include?<string.h> #include?"linux/ioctl.h" /*? ?*?argc:?應用程序參數個數 ?*?argv[]:?參數是什么,具體的參數,說明參數是字符串的形式 ?*?.chrdevbaseApp?<filename>??0表示關燈,1表示開燈 ?*?.chrdevbaseApp?/dev/led?0?關燈 ?*?.chrdevbaseApp?/dev/led?1?開燈 ?*?*/ int?main(int?argc,?char?*argv[]) { ????if(argc?!=?2) ????{ ????????printf("Error?Usage!rn"); ????????return?-1; ????} ???? ????int?fd,?ret; ????char?*filename; ????unsigned?char?data; ????filename?=?argv[1]; ????fd?=?open(filename,?O_RDWR); ????if(fd?<h2 id="4使用tasklet處理中斷下半部"><strong>4.使用tasklet處理中斷下半部</strong></h2> <pre class="brush:js;toolbar:false;">#include?<sys> #include?<sys> #include?<fcntl.h> #include?<stdio.h> #include?<unistd.h> #include?<stdlib.h> #include?<string.h> #include?"linux/ioctl.h" /*? ?*?argc:?應用程序參數個數 ?*?argv[]:?參數是什么,具體的參數,說明參數是字符串的形式 ?*?.chrdevbaseApp?<filename>??0表示關燈,1表示開燈 ?*?.chrdevbaseApp?/dev/led?0?關燈 ?*?.chrdevbaseApp?/dev/led?1?開燈 ?*?*/ int?main(int?argc,?char?*argv[]) { ????if(argc?!=?2) ????{ ????????printf("Error?Usage!rn"); ????????return?-1; ????} ???? ????int?fd,?ret; ????char?*filename; ????unsigned?char?data; ????filename?=?argv[1]; ????fd?=?open(filename,?O_RDWR); ????if(fd?<h2 id="5-工作隊列處理下半部"><strong>5. 工作隊列處理下半部</strong></h2> <p>開發方式同tasklet<br><strong>注意work是可以推導出設備dev結構體的,所以一般將work放在dev結構體里</strong></p> <p>相關推薦:《<a href="http://www.php.cn/course/list/33.html" target="_blank">Linux視頻教程</a>》</p></filename></string.h></stdlib.h></unistd.h></stdio.h></fcntl.h></sys></sys>