借助Linux中斷機制的按鍵開關功能實現(xiàn)

????????AlienTek的IMX6ULL開發(fā)板自帶了一個按鍵和一個LED燈,這兩個外設分別接在兩個不同的GPIO端口,各自獨立。我們想把按鍵作為燈的開關,通過按壓按鍵來控制燈的亮滅,即燈亮時按一下則燈滅,燈滅時按一下則燈亮,這里的“按一下”是指按鍵從被按下到抬起后的整個過程。對開發(fā)板SOC而言,按鍵的物理狀態(tài)可通過與其相連的GPIO端口的電平值得到反映,GPIO本身可作為中斷控制器,GPIO輸入電平的值或者電平值的變化都可以作為中斷觸發(fā)信號,中斷的處理過程由Linux中斷子系統(tǒng)作統(tǒng)一管理。我們可以利用Linux框架下的中斷來實現(xiàn)前面所說的按鍵開關功能。
[1] Hardware層
① 電路原理
????????首先要明確按鍵和燈的硬件信息。LED燈和按鍵的硬件原理圖主要如下。據(jù)圖我們可得知LED0的陰極和GPIO1_03連接,陽極電平為3.3V,所以GPIO1_03輸出低電平時燈亮,否則燈滅。按鍵與引腳UART1_CTS連接,這是哪個GPIO端口?查閱IMX6ULL的參考手冊如下圖,可以得知UART1_CTS復用GPIO1_18的功能,我們可以通過設置復用寄存器,把該引腳的功能配成GPIO,那么根據(jù)原理圖可知,當按鍵按下時GPIO1_18輸入低電平,平時輸入高電平。





② 硬件的設備樹表示
????????當前的Linux內(nèi)核框架下,開發(fā)板的硬件信息都是基于設備樹呈現(xiàn)的,所以我們要把以上的硬件外設寫成設備樹的形式以供內(nèi)核讀取。我們直接在設備樹SOC的根目錄下創(chuàng)建2個獨立節(jié)點,分別表示LED和按鍵,如下。LED的節(jié)點名稱為gpioled,其節(jié)點屬性pinctrl-0指向pinctrl_led節(jié)點,它描述了GPIO1_03的復用、電氣屬性等信息,字段MX6UL_PAD_GPIO1_IO03__GPIO1_IO03可在imx6ul-pinfunc.h中查到,它是由5個十六進制數(shù)組成的宏定義,表示該端口配置為GPIO功能,即GPIO1_IO03。后面跟的數(shù)0x10B0表示了GPIO1_IO03的電氣配置參數(shù),它的各位含義可參照參考手冊中的寄存器IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03。屬性字段led-gpio描述了gpioled節(jié)點使用到的GPIO信息,“&gpio1 3”就是指GPIO1_03,GPIO_ACTIVE_LOW表示“低電平有效”,“有效”在這里是指LED燈亮,由于GPIO輸出低電平時燈亮,所以我們說低有效。這樣LED的設備樹節(jié)點用到的信息就完成了。按鍵的節(jié)點名稱為key,它用到的引腳GPIO信息和LED的引腳信息寫法類似,無須贅述。需要關注的是,字段interrupt-parent意思是中斷繼承,因為我們要利用按鍵GPIO的中斷功能,所以需要在這里添加中斷相關的信息,中斷繼承的內(nèi)容是&gpio1,GPIO1本身是中斷控制器,這里的意思是按鍵連接的GPIO的中斷觸發(fā)由GPIO1中斷控制器處理。后面的interrupts字段,18表示GPIO1_18在GPIO1中斷控制器的中斷索引,IRQ_TYPE_EDGE_RISING表示GPIO輸入電平上升沿觸發(fā)中斷,也就是按鍵彈起時中斷應得到響應。
[2] Driver層
????????我們在driver層定義一個字符設備,整個驅動框架按Linux字符設備框架搭建,主要的幾個任務有從設備樹獲取定義好的硬件信息、按鍵GPIO申請中斷、定義中斷服務函數(shù)、按鍵消抖、LED亮滅控制等。我們設想的開關控制燈亮滅的邏輯是:按鍵被按壓完成后觸發(fā)中斷,在中斷服務函數(shù)中更新燈當前的亮滅狀態(tài),driver把該狀態(tài)上報到app層,app層讀取LED狀態(tài)并據(jù)此發(fā)送相應的控制命令到driver,driver根據(jù)命令控制LED燈GPIO的輸出電平從而控制亮滅。在文件操作結構體file_operations中,我們可以實現(xiàn)read和ioctl這兩個接口,前者用來向應用層報告“按鍵被按”這個消息,后者用來實現(xiàn)LED燈的控制方法。
????????對于機械按鍵而言,往往需要考慮按鍵抖動問題,在按鍵剛按完后立即讀對應的GPIO電平,由于抖動存在常會出現(xiàn)隨機的電平值,時高時低,難以說明按鍵的真正狀態(tài)。一般需要在按鍵動作結束后間隔幾毫秒至幾十毫秒,再去獲取GPIO電平,此時基本上能得到真實的按鍵狀態(tài),即消抖。但是在中斷處理函數(shù)中最好不要做任何延時操作,我們可以利用內(nèi)核定時器的特性,當按鍵動作后中斷被觸發(fā),在中斷處理函數(shù)中激活定時器,這樣定時器將在給定的時間后調用定時器處理函數(shù),我們在定時器處理函數(shù)中再去讀取按鍵GPIO電平。
① 設備結構
????????我們首先定義設備結構體,如下,把能用到的幾乎所有信息都寫進去了。成員ledIndex和keyIndex表示LED和按鍵對應的GPIO編號,這是通過系統(tǒng)提供的of函數(shù)從設備樹外設節(jié)點獲取得到的,后面要用于燈的控制及中斷申請等操作。成員keyRelease標志按鍵的動作是否完成,完成為true,否則為false。成員irqHandler將來要指向我們自定義的中斷處理函數(shù)。
② 驅動模塊初始化
????????驅動初始化時主要處理3件事情,即注冊字符設備(注冊cdev、創(chuàng)建類和設備等)、從設備樹獲取外設節(jié)點(申請GPIO使用權、申請中斷等)和初始化定時器(指定定時器觸發(fā)函數(shù))。獲取外設節(jié)點的GPIO信息后,首先要調用函數(shù)gpio_request獲得GPIO的使用權限。對于按鍵GPIO,先調用gpio_direction_input將GPIO1_18配置成輸入模式,然后調用gpio_to_irq根據(jù)GPIO編號得到中斷號,并指定該中斷號關聯(lián)的中斷服務函數(shù),再調用request_irq向系統(tǒng)申請按鍵的GPIO中斷,中斷觸發(fā)方式要按上升沿配置,并且要給這個接口傳入設備實例指針,這樣在中斷服務函數(shù)中就可以獲取到設備信息。在中斷處理中我們激活定時器,讓它在一定時間后執(zhí)行定時器觸發(fā)函數(shù)。在定時器觸發(fā)函數(shù)中,我們應該讀取GPIO1_18的電平值,如果為高電平則表示按鍵已經(jīng)彈起,按鍵動作完成了,否則表示按鍵還處于被按壓的狀態(tài),按鍵動作未完成。
③ 系統(tǒng)調用接口實現(xiàn)
????????我們主要實現(xiàn)read和ioctl。在read中,我們先判斷設備實例中的keyRelease標志是否為true,是則設置燈控制標志,如果當前燈亮則標志為關燈,否則標志為開燈,然后將keyRelease和燈控制標志一起通過copy_to_user報告給上層。在ioctl中,根據(jù)上層發(fā)送來的燈控制命令調用gpio_set_value設置LED GPIO的輸出電平,實現(xiàn)開燈或關燈,這里的燈控制命令就是上面設備結構體中定義的SETLEDON和SETLEDOFF。
④ driver完整代碼
[3] App層
????????我們在應用層使用read輪詢是否有內(nèi)核上報按鍵完成標志和燈控制標志,一旦有則根據(jù)燈控制標志來調用ioctl的具體命令,向內(nèi)核發(fā)送LED控制信號,主要的實現(xiàn)如下:
[4] 實踐結果
????????編寫驅動模塊的Makefile,編譯更新后的設備樹、內(nèi)核驅動和上層應用,得到新的內(nèi)核模塊、dtb及可執(zhí)行文件,把它們導入到IMX6ULL開發(fā)板系統(tǒng)中的相應目錄,重啟開發(fā)板。首先加載驅動模塊,可看到內(nèi)核打印信息:

對照driver代碼可知這些日志位置是在驅動模塊加載函數(shù)中,顯示該字符設備的主設備號是248,LED對應GPIO的編號是3,按鍵GPIO編號是18,這些都和硬件信息相符,獲得的中斷號是46。查看系統(tǒng)中的中斷列表:

可見最后一列中斷名稱為key0的那行,正是我們申請的GPIO中斷,最左側是中斷號46,目前被觸發(fā)次數(shù)為0,因為我們還沒按下按鍵。運行上層應用對應的可執(zhí)行文件,然后按下開發(fā)板上的按鍵KEY0數(shù)次,可以看到每完成一次按鍵動作,應用層收到一次標志上報,同時現(xiàn)象上LED亮滅逐次翻轉:

與此對應的,內(nèi)核打印了相同次數(shù)的中斷觸發(fā)、按鍵完成及燈控制命令等信息:

根據(jù)日志信息也可驗證驅動調用順序,每次都是先進入中斷服務key0_irqHandler,再進入定時器觸發(fā)函數(shù)打印按鍵釋放信息,然后打印來自上層的燈控制命令信息。這時再次查看中斷信息:

發(fā)現(xiàn)46號中斷的觸發(fā)次數(shù)為13,對應上面key0_irqHandler triggered日志次數(shù)也是13,但實際上剛才按下按鍵的次數(shù)為10,對應應用層日志也是打印10次,這里就體現(xiàn)了前面所說的按鍵抖動問題,由于抖動一次按鍵動作可能引起了不止一次的GPIO電平上升過程,而定時器延時消抖的作用使得按鍵次數(shù)和燈狀態(tài)翻轉次數(shù)保持了一致。