1. Linux驅(qū)動(dòng)實(shí)踐:一起來梳理【中斷】的前世今生(附代碼)

        共 5704字,需瀏覽 12分鐘

         ·

        2021-12-17 20:09

        作 ?者:道哥,10+年嵌入式開發(fā)老兵,專注于:C/C++、嵌入式、Linux。

        關(guān)注下方公眾號(hào),回復(fù)【書籍】,獲取 Linux、嵌入式領(lǐng)域經(jīng)典書籍;回復(fù)【PDF】,獲取所有原創(chuàng)文章( PDF 格式)。

        目錄

        • Linux 中斷的知識(shí)點(diǎn)梳理

          • 中斷的分類

          • 中斷號(hào)和中斷向量

          • 中斷服務(wù)程序ISR

          • 上半部分和下半部分

        • 中斷處理的注冊(cè)和注銷 API

        • 實(shí)操:捕獲鍵盤中斷

          • 示例代碼

          • 驅(qū)動(dòng)程序傳參

          • IO編址:IO端口和IO內(nèi)存

          • 編譯、測(cè)試、驗(yàn)證


        別人的經(jīng)驗(yàn),我們的階梯!

        大家好,我是道哥,今天我為大伙兒解說的技術(shù)知識(shí)點(diǎn)是:【Linux 中斷的注冊(cè)和處理】。

        在前兩篇文章中,描述的是在應(yīng)用層如何調(diào)用驅(qū)動(dòng)函數(shù)來控制GPIO,以及在驅(qū)動(dòng)中如何發(fā)送發(fā)送信號(hào)給應(yīng)用層。

        假如存在這樣一個(gè)需求:應(yīng)用程序需要監(jiān)控某個(gè)硬件GPIO口的電平狀態(tài),當(dāng)發(fā)生變化時(shí),應(yīng)用程序就做出相應(yīng)的動(dòng)作。

        利用之前已經(jīng)介紹的知識(shí),是可以完成這個(gè)需求的。

        比如:在驅(qū)動(dòng)程序中不停的讀取GPIO口的狀態(tài),一旦發(fā)生變化,就把新的電平狀態(tài)通過信號(hào)發(fā)送到應(yīng)用層。

        這樣的方式稱作:輪詢。

        輪詢方式的缺點(diǎn)顯而易見:輪詢的時(shí)間間隔應(yīng)該是多少毫秒(or 微秒),才比較合適呢?

        輪詢太慢:可能會(huì)丟失信號(hào);輪詢太快:消耗 CPU 資源!

        因此,在實(shí)際的產(chǎn)品中,用中斷觸發(fā)的方式才是更切合實(shí)際的選擇!

        本文所有的描述和測(cè)試,都是在 x86 平臺(tái)上完成的;

        Linux 中斷的知識(shí)點(diǎn)梳理

        中斷的分類

        Linux 的版本在持續(xù)更新,對(duì)中斷的處理方式也在不停的發(fā)生變化。

        下面幾張圖,是以前在學(xué)習(xí)時(shí)畫的思維導(dǎo)圖

        這幾張圖比較清晰地描述了在Linux操作系統(tǒng)中,關(guān)于中斷的一些基本概念。

        這張圖的結(jié)構(gòu)還是比較清晰的,基本上概括了Linux系統(tǒng)中的中斷分類

        另外,在很多關(guān)于中斷的書籍中,大部分都是從基礎(chǔ)的 PIC(可編程中斷控制器)開始講解的。

        如果您想非常具體、專業(yè)、深入的了解關(guān)于中斷的相關(guān)內(nèi)容,有一篇文章《Interrupt in Linux.pdf》講得非常好(文章的后面部分我也沒有看懂)。

        在文末有下載鏈接,感興趣的小伙伴可以學(xué)習(xí)一下。

        中斷號(hào)和中斷向量

        這張圖只要記住中斷號(hào)與中斷向量的關(guān)系就可以了:

        1. 中斷號(hào)與中斷控制器(PIC/APIC)相關(guān);

        2. 中斷向量與 CPU 相關(guān),用來查找中斷處理函數(shù)的入口地址;

        中斷服務(wù)例程 ISR

        中斷服務(wù)程序,就是針對(duì)每一個(gè)中斷如何進(jìn)行處理。

        如果您了解Linux中斷的相關(guān)內(nèi)容,一定會(huì)看到這樣的描述:中斷處理分為上半部分和下半部分。

        上半部分不能消耗太多的時(shí)間,主要處理與硬件相關(guān)的重要工作;其他不重要的工作,都放在下半部分去做。

        從上面這張圖中可以看出,用來完成下半部分工作有好幾種機(jī)制可以選擇,每一種方式都是針對(duì)不同的需求場(chǎng)景。

        在每一種下半部分機(jī)制中,Linux都設(shè)計(jì)了非常方便的接口函數(shù)。

        作為開發(fā)者的我們來說,使用這些下半部分的機(jī)制很簡(jiǎn)單,只需要幾個(gè)函數(shù)調(diào)用即可。

        例如:如果使用工作隊(duì)列來實(shí)現(xiàn)下半部分的工作,只需要2步動(dòng)作:

        1. 定義處理函數(shù)

        static struct work_struct mywork;

        static void mywork_handler(struct work_struct *work)
        {
        printk("This is myword_handler...\n");
        }

        2. 在中斷處理函數(shù)中,注冊(cè)注冊(cè)函數(shù)

        INIT_WORK(&mywork, mywork_handler);                                                         
        schedule_work(&mywork);

        下面幾張圖,是針對(duì)每一種“下半部分”處理機(jī)制的一些特點(diǎn),注意:有些機(jī)制在新版本中已經(jīng)廢棄不用了,了解即可。

        中斷處理的注冊(cè)和注銷 API

        所謂的中斷注冊(cè),就是告訴操作系統(tǒng):我對(duì)哪個(gè)中斷感興趣。

        當(dāng)這些中斷發(fā)生的時(shí)候,請(qǐng)通知我。通知的方式就是:調(diào)用一個(gè)預(yù)先注冊(cè)好的回調(diào)函數(shù)。

        驅(qū)動(dòng)程序可以通過函數(shù) request_irq(),向操作系統(tǒng)注冊(cè),并且激活指定的中斷線:

        int request_irq(unsigned int irq, 
        irq_handler_t handler,
        unsigned long flags,
        const char *devname,
        void *dev_id);

        參數(shù)說明:

        irq: 申請(qǐng)的硬件中斷號(hào);

        handler: 中斷處理函數(shù)。一旦中斷發(fā)生,這個(gè)函數(shù)就被調(diào)用;

        flags: 中斷的屬性,例如:IRQF_DISABLED,IRQF_TIMER,IRQF_SHARED;

        devname: 中斷驅(qū)動(dòng)程序的名稱,在 /proc/interrupts 文件中看到對(duì)應(yīng)的內(nèi)容;

        dev_id: 中斷程序的唯一標(biāo)識(shí),比如:在共享中斷中,可以用來區(qū)分不同的中斷處理程序;

        驅(qū)動(dòng)程序通過函數(shù) free_irq(),向操作系統(tǒng)注銷一個(gè)中斷處理函數(shù):

        void free_irq(unsigned int irq, void *dev_id);

        參數(shù)說明:

        irq: 硬件中斷號(hào);

        dev_id: 中斷程序的唯一標(biāo)識(shí);

        實(shí)操:捕獲鍵盤中斷

        示例代碼

        有了上面的知識(shí)鋪墊,下面就來實(shí)操一下,實(shí)現(xiàn)的功能是:

        捕獲鍵盤的中斷,在中斷處理函數(shù)中,打印出按鍵的掃描碼,如果是 ESC 鍵被按下,就打印出指定的信息。

        與往常一樣,操作的目錄位于:tmp/linux-4.15/drivers 目錄下。

        $ mkdir my_driver_interrupt
        $ touch driver_interrupt.c

        文件內(nèi)容:

        #include 
        #include
        #include

        // 中斷號(hào)
        static int irq;

        // 驅(qū)動(dòng)程序名稱
        static char * devname;

        // 用來接收加載驅(qū)動(dòng)模塊時(shí)傳入的參數(shù)
        module_param(irq, int, 0644);
        module_param(devname, charp, 0644);

        // 定義驅(qū)動(dòng)程序的 ID,在中斷處理函數(shù)中用來判斷是否需要處理
        #define MY_DEV_ID 1211

        // 驅(qū)動(dòng)程序數(shù)據(jù)結(jié)構(gòu)
        struct myirq
        {
        int devid;
        };

        // 保存驅(qū)動(dòng)程序的所有信息
        struct myirq mydev ={ MY_DEV_ID };

        // 鍵盤相關(guān)的 IO 端口
        #define KBD_DATA_REG 0x60
        #define KBD_STATUS_REG 0x64
        #define KBD_SCANCODE_MASK 0x7f
        #define KBD_STATUS_MASK 0x80

        // 中斷處理函數(shù)
        static irqreturn_t myirq_handler(int irq, void * dev)
        {
        struct myirq mydev;
        unsigned char key_code;
        mydev = *(struct myirq*)dev;

        // 檢查設(shè)備 id,只有當(dāng)相等的時(shí)候才需要處理
        if (MY_DEV_ID == mydev.devid)
        {
        // 讀取鍵盤掃描碼
        key_code = inb(KBD_DATA_REG);

        /* 這里如果放開,每次按鍵都會(huì)打印出很多信息
        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;
        }

        // 驅(qū)動(dòng)模塊初始化函數(shù)
        static int __init myirq_init(void)
        {
        printk("myirq_init is called. \n");

        // 注冊(cè)中斷處理函數(shù)
        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;
        }

        // 驅(qū)動(dòng)模塊退出函數(shù)
        static void __exit myirq_exit(void)
        {
        printk("myirq_exit is called. \n");

        // 注銷中斷處理函數(shù)
        free_irq(irq, &mydev);
        }

        MODULE_LICENSE("GPL");
        module_init(myirq_init);
        module_exit(myirq_exit);

        上面的代碼,有兩個(gè)小的知識(shí)點(diǎn)。

        向驅(qū)動(dòng)程序傳參

        示例代碼中,在調(diào)用 request_irq 時(shí),需要指定中斷號(hào)驅(qū)動(dòng)程序的名稱。

        這兩個(gè)參數(shù)是在加載驅(qū)動(dòng)模塊的時(shí)候,從命令行傳入的。

        在驅(qū)動(dòng)程序中,通過下面兩行代碼即可實(shí)現(xiàn)參數(shù)的接收:

        module_param(irq, int, 0644);
        module_param(devname, charp, 0644);

        module_param 是一個(gè)宏定義,定義在 include/linux/moduleparam.h 文件中,具體定義如下:

        #define module_param(name, type, perm)                
        module_param_named(name, name, type, perm);

        name: 存儲(chǔ)參數(shù)的變量名;

        type: 變量的類型;

        perm: 訪問參數(shù)的權(quán)限,表示此參數(shù)在sysfs文件系統(tǒng)中所對(duì)應(yīng)的文件節(jié)點(diǎn)的屬性;

        IO地址:IO端口和IO內(nèi)存

        這是讀取 IO 外設(shè)的兩種不同方式

        IO 端口有兩種編址方式:統(tǒng)一編址和獨(dú)立編址。

        統(tǒng)一編制

        主存單元所在的地址空間,劃出一部分出來,專門用來把IO外設(shè)寄存器的地址映射到這部分劃出來的地址空間中。

        統(tǒng)一編址的好處是:讀取IO外設(shè)的時(shí)候,就好像讀取普通的內(nèi)存地址空間中的數(shù)據(jù)一樣。

        獨(dú)立編址

        IO 外設(shè)的地址空間,與主存單元的地址空間是兩個(gè)獨(dú)立的地址空間,此時(shí),IO地址一般稱作: IO端口。

        我們?cè)谧x寫IO外設(shè)的時(shí)候,從這些 “IO端口” 中讀寫就可以了。不同的外設(shè),被分配了不同的 IO 端口號(hào)。

        CPU 提供了一些列函數(shù)來讀寫 IO 端口,例如:

        // 讀寫一個(gè)字節(jié)
        unsigned inb(unsigned port);
        void outb(unsigned char byte, unsigned port);

        // 讀寫一個(gè)字
        unsigned inw(unsigned port);
        void outw(unsigned short word, unsigned port);

        編譯、驗(yàn)證

        編譯驅(qū)動(dòng)模塊:

        $ make
        輸出文件:driver_interrupt.ko

        因?yàn)槲覀儾东@的是鍵盤中斷(中斷號(hào):1),先看一下在加載驅(qū)動(dòng)模塊之前的中斷驅(qū)動(dòng)程序 head /proc/interrupts

        可以把 demsg 的輸出也清理一下:dmesg -c

        執(zhí)行下面指令來加載驅(qū)動(dòng)模塊(傳遞2個(gè)參數(shù)):

        insmod driver_interrupt.ko irq=1 devname=myirq

        再次執(zhí)行一下指令 head /proc/interrupts 查看驅(qū)動(dòng)程序:

        中斷號(hào) 1 的右側(cè),是不是看到了我們的驅(qū)動(dòng)程序:my_irq?

        再來看一下 dmesg 的輸出信息:

        成功注冊(cè)了中斷號(hào)1的處理函數(shù)!

        此時(shí),按幾次鍵盤左上角的 ESC 鍵,然后再查看 dmesg 的輸出信息:

        以上,就是最簡(jiǎn)單的中斷注冊(cè)和相應(yīng)的中斷處理函數(shù)!

        在實(shí)際的項(xiàng)目中,如果要把中斷信息通知到應(yīng)用層,可以通過上一篇文章介紹的發(fā)送信號(hào)來實(shí)現(xiàn),或者通過其他的回調(diào)機(jī)制也可以。

        下一篇文章,我們?cè)谶@個(gè)示例代碼上進(jìn)行擴(kuò)展,看一下:中斷處理中每一個(gè)“下半部分”機(jī)制應(yīng)該如何編程。


        ------ End ------

        文中的測(cè)試代碼和相關(guān)文檔,已經(jīng)放在網(wǎng)盤了。

        在公眾號(hào)【IOT物聯(lián)網(wǎng)小鎮(zhèn)】后臺(tái)回復(fù)關(guān)鍵字:1212,即可獲取下載地址。

        強(qiáng)烈建議您看一下網(wǎng)盤里的這篇文檔:《Interrupt in Linux.pdf》,一定有很大收獲!

        謝謝!


        推薦閱讀

        【1】《Linux 從頭學(xué)》系列文章

        【2】C語言指針-從底層原理到花式技巧,用圖文和代碼幫你講解透徹

        【3】原來gdb的底層調(diào)試原理這么簡(jiǎn)單

        【4】?jī)?nèi)聯(lián)匯編很可怕嗎?看完這篇文章,終結(jié)它!

        其他系列專輯:精選文章、應(yīng)用程序設(shè)計(jì)物聯(lián)網(wǎng)、 C語言。


        星標(biāo)公眾號(hào),第一時(shí)間看文章!


        瀏覽 44
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 黄色高清无码网站 | 狠狠干第一页 | 无码毛片在外观看 | chinese国内自拍露脸videos | 五月香婷婷在线观看视频亚洲香蕉 |