作 者:道哥,10+年嵌入式開發老兵,專注於:C/C++、嵌入式、Linux。
關注下方公眾號,回覆【書籍】,獲取 Linux、嵌入式領域經典書籍;回覆【PDF】,獲取所有原創文章( PDF 格式)。
目錄
別人的經驗,我們的階梯!
大家好,我是道哥,今天我為大夥兒解說的技術知識點是:【Linux 中斷的註冊和處理】。
在前兩篇文章中,描述的是在應用層如何呼叫驅動函式來控制GPIO
,以及在驅動中如何傳送傳送訊號給應用層。
假如存在這樣一個需求:應用程式需要監控某個硬體GPIO
口的電平狀態,當發生變化時,應用程式就做出相應的動作。
利用之前已經介紹的知識,是可以完成這個需求的。
比如:在驅動程式中不停的讀取GPIO
口的狀態,一旦發生變化,就把新的電平狀態通過訊號傳送到應用層。
這樣的方式稱作:輪詢。
輪詢方式的缺點顯而易見:輪詢的時間間隔應該是多少毫秒(or 微秒),才比較合適呢?
輪詢太慢:可能會丟失訊號;輪詢太快:消耗 CPU 資源!
因此,在實際的產品中,用中斷觸發的方式才是更切合實際的選擇!
本文所有的描述和測試,都是在 x86 平臺上完成的;
Linux 中斷的知識點梳理
中斷的分類
Linux
的版本在持續更新,對中斷的處理方式也在不停的發生變化。
下面幾張圖,是以前在學習時畫的思維導圖。
這幾張圖比較清晰地描述了在Linux
作業系統中,關於中斷的一些基本概念。
這張圖的結構還是比較清晰的,基本上概括了Linux
系統中的中斷分類。
另外,在很多關於中斷的書籍中,大部分都是從基礎的 PIC
(可程式設計中斷控制器)開始講解的。
如果您想非常具體、專業、深入的瞭解關於中斷的相關內容,有一篇文章《Interrupt in Linux.pdf》講得非常好(文章的後面部分我也沒有看懂)。
在文末有下載連結,感興趣的小夥伴可以學習一下。
中斷號和中斷向量
這張圖只要記住中斷號與中斷向量的關係就可以了:
中斷號與中斷控制器(PIC/APIC)相關;
中斷向量與 CPU 相關,用來查詢中斷處理函式的入口地址;
中斷服務例程 ISR
中斷服務程式,就是針對每一箇中斷如何進行處理。
如果您瞭解Linux
中斷的相關內容,一定會看到這樣的描述:中斷處理分為上半部分和下半部分。
上半部分不能消耗太多的時間,主要處理與硬體相關的重要工作;其他不重要的工作,都放在下半部分去做。
從上面這張圖中可以看出,用來完成下半部分工作有好幾種機制可以選擇,每一種方式都是針對不同的需求場景。
在每一種下半部分機制中,Linux
都設計了非常方便的介面函式。
作為開發者的我們來說,使用這些下半部分的機制很簡單,只需要幾個函式呼叫即可。
例如:如果使用工作佇列來實現下半部分的工作,只需要2
步動作:
1. 定義處理函式
static struct work_struct mywork;
static void mywork_handler(struct work_struct *work)
{
printk("This is myword_handler...\n");
}
2. 在中斷處理函式中,註冊註冊函式
INIT_WORK(&mywork, mywork_handler);
schedule_work(&mywork);
下面幾張圖,是針對每一種“下半部分”處理機制的一些特點,注意:有些機制在新版本中已經廢棄不用了,瞭解即可。
中斷處理的註冊和登出 API
所謂的中斷註冊,就是告訴作業系統:我對哪個中斷感興趣。
當這些中斷髮生的時候,請通知我。通知的方式就是:呼叫一個預先註冊好的回撥函式。
驅動程式可以通過函式 request_irq(),向作業系統註冊,並且啟用指定的中斷線:
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *devname,
void *dev_id);
引數說明:
irq: 申請的硬體中斷號;
handler: 中斷處理函式。一旦中斷髮生,這個函式就被呼叫;
flags: 中斷的屬性,例如:IRQF_DISABLED,IRQF_TIMER,IRQF_SHARED;
devname: 中斷驅動程式的名稱,在 /proc/interrupts 檔案中看到對應的內容;
dev_id: 中斷程式的唯一標識,比如:在共享中斷中,可以用來區分不同的中斷處理程式;
驅動程式通過函式 free_irq(),向作業系統登出一箇中斷處理函式:
void free_irq(unsigned int irq, void *dev_id);
引數說明:
irq: 硬體中斷號;
dev_id: 中斷程式的唯一標識;
實操:捕獲鍵盤中斷
示例程式碼
有了上面的知識鋪墊,下面就來實操一下,實現的功能是:
捕獲鍵盤的中斷,在中斷處理函式中,列印出按鍵的掃描碼,如果是 ESC 鍵被按下,就列印出指定的資訊。
與往常一樣,操作的目錄位於: tmp/linux-4.15/drivers 目錄下。
$ mkdir my_driver_interrupt
$ touch driver_interrupt.c
檔案內容:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>
// 中斷號
static int irq;
// 驅動程式名稱
static char * devname;
// 用來接收載入驅動模組時傳入的引數
module_param(irq, int, 0644);
module_param(devname, charp, 0644);
// 定義驅動程式的 ID,在中斷處理函式中用來判斷是否需要處理
#define MY_DEV_ID 1211
// 驅動程式資料結構
struct myirq
{
int devid;
};
// 儲存驅動程式的所有資訊
struct myirq mydev ={ MY_DEV_ID };
// 鍵盤相關的 IO 埠
#define KBD_DATA_REG 0x60
#define KBD_STATUS_REG 0x64
#define KBD_SCANCODE_MASK 0x7f
#define KBD_STATUS_MASK 0x80
// 中斷處理函式
static irqreturn_t myirq_handler(int irq, void * dev)
{
struct myirq mydev;
unsigned char key_code;
mydev = *(struct myirq*)dev;
// 檢查裝置 id,只有當相等的時候才需要處理
if (MY_DEV_ID == mydev.devid)
{
// 讀取鍵盤掃描碼
key_code = inb(KBD_DATA_REG);
/* 這裡如果放開,每次按鍵都會列印出很多資訊
printk("key_code: %x %s\n",
key_code & KBD_SCANCODE_MASK,
key_code & KBD_STATUS_MASK ? "released" : "pressed");
*/
// 判斷:是否為 ESC 鍵
if (key_code == 0x01)
{
printk("EXC key is pressed! \n");
}
}
return IRQ_HANDLED;
}
// 驅動模組初始化函式
static int __init myirq_init(void)
{
printk("myirq_init is called. \n");
// 註冊中斷處理函式
if(request_irq(irq, myirq_handler, IRQF_SHARED, devname, &mydev)!=0)
{
printk("register irq[%d] handler failed. \n", irq);
return -1;
}
printk("register irq[%d] handler success. \n", irq);
return 0;
}
// 驅動模組退出函式
static void __exit myirq_exit(void)
{
printk("myirq_exit is called. \n");
// 登出中斷處理函式
free_irq(irq, &mydev);
}
MODULE_LICENSE("GPL");
module_init(myirq_init);
module_exit(myirq_exit);
上面的程式碼,有兩個小的知識點。
向驅動程式傳參
示例程式碼中,在呼叫 request_irq 時,需要指定中斷號和驅動程式的名稱。
這兩個引數是在載入驅動模組的時候,從命令列傳入的。
在驅動程式中,通過下面兩行程式碼即可實現引數的接收:
module_param(irq, int, 0644);
module_param(devname, charp, 0644);
module_param 是一個巨集定義,定義在 include/linux/moduleparam.h 檔案中,具體定義如下:
#define module_param(name, type, perm)
module_param_named(name, name, type, perm);
name: 儲存引數的變數名;
type: 變數的型別;
perm: 訪問引數的許可權,表示此引數在sysfs檔案系統中所對應的檔案節點的屬性;
IO地址:IO埠和IO記憶體
這是讀取 IO 外設的兩種不同方式。
IO 埠有兩種編址方式:統一編址和獨立編址。
統一編制
把主存單元所在的地址空間,劃出一部分出來,專門用來把IO
外設暫存器的地址對映到這部分劃出來的地址空間中。
統一編址的好處是:讀取IO
外設的時候,就好像讀取普通的記憶體地址空間中的資料一樣。
獨立編址
IO
外設的地址空間,與主存單元的地址空間是兩個獨立的地址空間,此時,IO地址一般稱作: IO埠。
我們在讀寫IO
外設的時候,從這些 “IO埠” 中讀寫就可以了。不同的外設,被分配了不同的 IO 埠號。
CPU
提供了一些列函式來讀寫 IO
埠,例如:
// 讀寫一個位元組
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
// 讀寫一個字
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
編譯、驗證
編譯驅動模組:
$ make
輸出檔案:driver_interrupt.ko
因為我們捕獲的是鍵盤中斷(中斷號:1),先看一下在載入驅動模組之前的中斷驅動程式 head /proc/interrupts
:
可以把 demsg 的輸出也清理一下:dmesg -c
執行下面指令來載入驅動模組(傳遞2
個引數):
insmod driver_interrupt.ko irq=1 devname=myirq
再次執行一下指令 head /proc/interrupts
檢視驅動程式:
在中斷號 1 的右側,是不是看到了我們的驅動程式:my_irq?
再來看一下 dmesg 的輸出資訊:
成功註冊了中斷號1的處理函式!
此時,按幾次鍵盤左上角的 ESC 鍵,然後再檢視 dmesg 的輸出資訊:
以上,就是最簡單的中斷註冊和相應的中斷處理函式!
在實際的專案中,如果要把中斷資訊通知到應用層,可以通過上一篇文章介紹的傳送訊號來實現,或者通過其他的回撥機制也可以。
下一篇文章,我們在這個示例程式碼上進行擴充套件,看一下:中斷處理中每一個“下半部分”機制應該如何程式設計。
文中的測試程式碼和相關文件,已經放在網盤了。
在公眾號【IOT物聯網小鎮】後臺回覆關鍵字:1212,即可獲取下載地址。
強烈建議您看一下網盤裡的這篇文件:《Interrupt in Linux.pdf》,一定有很大收穫!
謝謝!
推薦閱讀
【2】C語言指標-從底層原理到花式技巧,用圖文和程式碼幫你講解透徹