Linux 中断实验
目錄
- Linux 中斷簡介
- Linux 中斷API 函數
- 上半部與下半部
- 設備樹中斷信息節點
- 獲取中斷號
- 硬件原理圖分析
- 實驗程序編寫
- 修改設備樹文件
- 按鍵中斷驅動程序編寫
- 編寫測試APP
- 運行測試
- 編譯驅動程序和測試APP
- 運行測試
不管是裸機實驗還是Linux 下的驅動實驗,中斷都是頻繁使用的功能,關于I.MX6U 的中斷原理已經在第十七章做了詳細的講解,在裸機中使用中斷我們需要做一大堆的工作,比如配置寄存器,使能IRQ 等等。Linux 內核提供了完善的中斷框架,我們只需要申請中斷,然后注冊中斷處理函數即可,使用非常方便,不需要一系列復雜的寄存器配置。本章我們就來學習一下如何在Linux 下使用中斷。
Linux 中斷簡介
Linux 中斷API 函數
先來回顧一下裸機實驗里面中斷的處理方法:
①、使能中斷,初始化相應的寄存器。
②、注冊中斷服務函數,也就是向irqTable 數組的指定標號處寫入中斷服務函數
②、中斷發生以后進入IRQ 中斷服務函數,在IRQ 中斷服務函數在數組irqTable 里面查找具體的中斷處理函數,找到以后執行相應的中斷處理函數。
在Linux 內核中也提供了大量的中斷相關的API 函數,我們來看一下這些跟中斷有關的API 函數:
1、中斷號
每個中斷都有一個中斷號,通過中斷號即可區分不同的中斷,有的資料也把中斷號叫做中斷線。在Linux 內核中使用一個int 變量表示中斷號,關于中斷號我們已經在第十七章講解過了。
2、request_irq 函數
在Linux 內核中要想使用某個中斷是需要申請的,request_irq 函數用于申請中斷,request_irq函數可能會導致睡眠,因此不能在中斷上下文或者其他禁止睡眠的代碼段中使用request_irq 函數。request_irq 函數會激活(使能)中斷,所以不需要我們手動去使能中斷,request_irq 函數原型如下:
函數參數和返回值含義如下:
irq:要申請中斷的中斷號。
handler:中斷處理函數,當中斷發生以后就會執行此中斷處理函數。
flags:中斷標志,可以在文件include/linux/interrupt.h 里面查看所有的中斷標志,這里我們介紹幾個常用的中斷標志,如表51.1.1.1 所示:
| IRQF_SHARED | 多個設備共享一個中斷線,共享的所有中斷都必須指定此標志。如果使用共享中斷的話,request_irq 函數的dev 參數就是唯一區分他們的標志。 |
| IRQF_ONESHOT | 單次中斷,中斷執行一次就結束。 |
| IRQF_TRIGGER_NONE | 無觸發。 |
| IRQF_TRIGGER_RISING | 上升沿觸發。 |
| IRQF_TRIGGER_FALLING | 下降沿觸發。 |
| IRQF_TRIGGER_HIGH | 高電平觸發。 |
| IRQF_TRIGGER_LOW | 低電平觸發。 |
比如I.MX6U-ALPHA 開發板上的KEY0 使用GPIO1_IO18,按下KEY0 以后為低電平,因此可以設置為下降沿觸發,也就是將flags 設置為IRQF_TRIGGER_FALLING。表51.1.1.1 中的這些標志可以通過“|”來實現多種組合。
name:中斷名字,設置以后可以在/proc/interrupts 文件中看到對應的中斷名字。
dev:如果將flags 設置為IRQF_SHARED 的話,dev 用來區分不同的中斷,一般情況下將dev 設置為設備結構體,dev 會傳遞給中斷處理函數irq_handler_t 的第二個參數。
返回值:0 中斷申請成功,其他負值中斷申請失敗,如果返回-EBUSY 的話表示中斷已經被申請了。
3、free_irq 函數
使用中斷的時候需要通過request_irq 函數申請,使用完成以后就要通過free_irq 函數釋放掉相應的中斷。如果中斷不是共享的,那么free_irq 會刪除中斷處理函數并且禁止中斷。free_irq函數原型如下所示:
函數參數和返回值含義如下:
irq:要釋放的中斷。
dev:如果中斷設置為共享(IRQF_SHARED)的話,此參數用來區分具體的中斷。共享中斷只有在釋放最后中斷處理函數的時候才會被禁止掉。
返回值:無。
4、中斷處理函數
使用request_irq 函數申請中斷的時候需要設置中斷處理函數,中斷處理函數格式如下所示:
第一個參數是要中斷處理函數要相應的中斷號。第二個參數是一個指向void 的指針,也就是個通用指針,需要與request_irq 函數的dev 參數保持一致。用于區分共享中斷的不同設備,dev 也可以指向設備數據結構。中斷處理函數的返回值為irqreturn_t 類型,irqreturn_t 類型定義如下所示:
10 enum irqreturn { 11 IRQ_NONE = (0 << 0), 12 IRQ_HANDLED = (1 << 0), 13 IRQ_WAKE_THREAD = (1 << 1), 14 }; 15 16 typedef enum irqreturn irqreturn_t;可以看出irqreturn_t 是個枚舉類型,一共有三種返回值。一般中斷服務函數返回值使用如下形式:
return IRQ_RETVAL(IRQ_HANDLED)5、中斷使能與禁止函數
常用的中斷使用和禁止函數如下所示:
void enable_irq(unsigned int irq) void disable_irq(unsigned int irq)enable_irq 和disable_irq 用于使能和禁止指定的中斷,irq 就是要禁止的中斷號。disable_irq函數要等到當前正在執行的中斷處理函數執行完才返回,因此使用者需要保證不會產生新的中斷,并且確保所有已經開始執行的中斷處理程序已經全部退出。在這種情況下,可以使用另外一個中斷禁止函數:
void disable_irq_nosync(unsigned int irq)disable_irq_nosync 函數調用以后立即返回,不會等待當前中斷處理程序執行完畢。上面三個函數都是使能或者禁止某一個中斷,有時候我們需要關閉當前處理器的整個中斷系統,也就是在學習STM32 的時候常說的關閉全局中斷,這個時候可以使用如下兩個函數:
local_irq_enable() local_irq_disable()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 函數來打開全局中斷,而是將中斷狀態恢復到以前的狀態,要考慮到別的任務的感受,此時就要用到下面兩個函數:
local_irq_save(flags) local_irq_restore(flags)這兩個函數是一對,local_irq_save 函數用于禁止中斷,并且將中斷狀態保存在flags 中。local_irq_restore 用于恢復中斷,將中斷到flags 狀態。
上半部與下半部
在有些資料中也將上半部和下半部稱為頂半部和底半部,都是一個意思。我們在使用request_irq 申請中斷的時候注冊的中斷服務函數屬于中斷處理的上半部,只要中斷觸發,那么中斷處理函數就會執行。我們都知道中斷處理函數一定要快點執行完畢,越短越好,但是現實往往是殘酷的,有些中斷處理過程就是比較費時間,我們必須要對其進行處理,縮小中斷處理函數的執行時間。比如電容觸摸屏通過中斷通知SOC 有觸摸事件發生,SOC 響應中斷,然后通過IIC 接口讀取觸摸坐標值并將其上報給系統。但是我們都知道IIC 的速度最高也只有400Kbit/S,所以在中斷中通過IIC 讀取數據就會浪費時間。我們可以將通過IIC 讀取觸摸數據的操作暫后執行,中斷處理函數僅僅相應中斷,然后清除中斷標志位即可。這個時候中斷處理過程就分為了兩部分:
上半部:上半部就是中斷處理函數,那些處理過程比較快,不會占用很長時間的處理就可以放在上半部完成。
下半部:如果中斷處理過程比較耗時,那么就將這些比較耗時的代碼提出來,交給下半部去執行,這樣中斷處理函數就會快進快出。
因此,Linux 內核將中斷分為上半部和下半部的主要目的就是實現中斷處理函數的快進快出,那些對時間敏感、執行速度快的操作可以放到中斷處理函數中,也就是上半部。剩下的所有工作都可以放到下半部去執行,比如在上半部將數據拷貝到內存中,關于數據的具體處理就可以放到下半部去執行。至于哪些代碼屬于上半部,哪些代碼屬于下半部并沒有明確的規定,一切根據實際使用情況去判斷,這個就很考驗驅動編寫人員的功底了。這里有一些可以借鑒的參考點:
①、如果要處理的內容不希望被其他中斷打斷,那么可以放到上半部。
②、如果要處理的任務對時間敏感,可以放到上半部。
③、如果要處理的任務與硬件有關,可以放到上半部
④、除了上述三點以外的其他任務,優先考慮放到下半部。
上半部處理很簡單,直接編寫中斷處理函數就行了,關鍵是下半部該怎么做呢?Linux 內核提供了多種下半部機制,接下來我們來學習一下這些下半部機制。
1、軟中斷
一開始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 個軟中斷,如下所示:
static struct softirq_action softirq_vec[NR_SOFTIRQS];NR_SOFTIRQS 是枚舉類型,定義在文件include/linux/interrupt.h 中,定義如下:
enum { HI_SOFTIRQ=0, /* 高優先級軟中斷*/ TIMER_SOFTIRQ, /* 定時器軟中斷*/ NET_TX_SOFTIRQ, /* 網絡數據發送軟中斷*/ NET_RX_SOFTIRQ, /* 網絡數據接收軟中斷*/ BLOCK_SOFTIRQ, BLOCK_IOPOLL_SOFTIRQ, TASKLET_SOFTIRQ, /* tasklet軟中斷*/ SCHED_SOFTIRQ, /* 調度軟中斷*/ HRTIMER_SOFTIRQ, /* 高精度定時器軟中斷*/ RCU_SOFTIRQ, /* RCU軟中斷*/ NR_SOFTIRQS };可以看出,一共有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 函數原型如下:
函數參數和返回值含義如下:
nr:要觸發的軟中斷,在示例代碼51.1.2.3 中選擇一個。
返回值:沒有返回值。
軟中斷必須在編譯的時候靜態注冊!Linux 內核使用softirq_init 函數初始化軟中斷,softirq_init 函數定義在kernel/softirq.c 文件里面,函數內容如下:
從示例代碼51.1.2.4 可以看出,softirq_init 函數默認會打開TASKLET_SOFTIRQ 和HI_SOFTIRQ。
2、tasklet
tasklet 是利用軟中斷來實現的另外一種下半部機制,在軟中斷和tasklet 之間,建議大家使用tasklet。Linux 內核使用tasklet_struct 結構體來表示tasklet:
第489 行的func 函數就是tasklet 要執行的處理函數,用戶定義函數內容,相當于中斷處理函數。如果要使用tasklet,必須先定義一個tasklet,然后使用tasklet_init 函數初始化tasklet,taskled_init 函數原型如下:
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);函數參數和返回值含義如下:
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 函數原型如下:
函數參數和返回值含義如下:
t:要調度的tasklet,也就是DECLARE_TASKLET 宏里面的name。
返回值:沒有返回值。
關于tasklet 的參考使用示例如下所示:
2、工作隊列
工作隊列是另外一種下半部執行方式,工作隊列在進程上下文執行,工作隊列將要推后的工作交給一個內核線程去執行,因為工作隊列工作在進程上下文,因此工作隊列允許睡眠或重新調度。因此如果你要推后的工作可以睡眠那么就可以選擇工作隊列,否則的話就只能選擇軟中斷或tasklet。
Linux 內核使用work_struct 結構體表示一個工作,內容如下(省略掉條件編譯):
struct work_struct { atomic_long_t data; struct list_head entry; work_func_t func; /* 工作隊列處理函數*/ };這些工作組織成工作隊列,工作隊列使用workqueue_struct 結構體表示,內容如下(省略掉條件編譯):
struct workqueue_struct { struct list_head pwqs; struct list_head list; struct mutex mutex; int work_color; int flush_color; atomic_t nr_pwqs_to_flush; struct wq_flusher *first_flusher; struct list_head flusher_queue; struct list_head flusher_overflow; struct list_head maydays; struct worker *rescuer; int nr_drainers; int saved_max_active; struct workqueue_attrs *unbound_attrs; struct pool_workqueue *dfl_pwq; char name[WQ_NAME_LEN]; struct rcu_head rcu; unsigned int flags ____cacheline_aligned; struct pool_workqueue __percpu *cpu_pwqs; struct pool_workqueue __rcu *numa_pwq_tbl[]; };Linux 內核使用工作者線程(worker thread)來處理工作隊列中的各個工作,Linux 內核使用worker 結構體表示工作者線程,worker 結構體內容如下:
struct worker { union { struct list_head entry; struct hlist_node hentry; }; struct work_struct *current_work; work_func_t current_func; struct pool_workqueue *current_pwq; bool desc_valid; struct list_head scheduled; struct task_struct *task; struct worker_pool *pool; struct list_head node; unsigned long last_active; unsigned int flags; int id; char desc[WORKER_DESC_LEN]; struct workqueue_struct *rescue_wq; };從示例代碼51.1.2.10 可以看出,每個worker 都有一個工作隊列,工作者線程處理自己工作隊列中的所有工作。在實際的驅動開發中,我們只需要定義工作(work_struct)即可,關于工作隊列和工作者線程我們基本不用去管。簡單創建工作很簡單,直接定義一個work_struct 結構體變量即可,然后使用INIT_WORK 宏來初始化工作,INIT_WORK 宏定義如下:
#define INIT_WORK(_work, _func)_work 表示要初始化的工作,_func 是工作對應的處理函數。
也可以使用DECLARE_WORK 宏一次性完成工作的創建和初始化,宏定義如下:
n 表示定義的工作(work_struct),f 表示工作對應的處理函數。
和tasklet 一樣,工作也是需要調度才能運行的,工作的調度函數為schedule_work,函數原型如下所示:
函數參數和返回值含義如下:
work:要調度的工作。
返回值:0 成功,其他值失敗。
關于工作隊列的參考使用示例如下所示:
設備樹中斷信息節點
如果使用設備樹的話就需要在設備樹中設置好中斷屬性信息,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 文件,找到如下所示內容:
fxls8471 是NXP 官方的6ULL 開發板上的一個磁力計芯片,fxls8471 有一個中斷引腳鏈接到了I.MX6ULL 的SNVS_TAMPER0 因腳上,這個引腳可以復用為GPIO5_IO00。
第5 行,interrupt-parent 屬性設置中斷控制器,這里使用gpio5 作為中斷控制器。
第6 行,interrupts 設置中斷信息,0 表示GPIO5_IO00,8 表示低電平觸發。
簡單總結一下與中斷有關的設備樹屬性信息:
①、#interrupt-cells,指定中斷源的信息cells 個數。
②、interrupt-controller,表示當前節點為中斷控制器。
③、interrupts,指定中斷號,觸發方式等。
④、interrupt-parent,指定父中斷,也就是中斷控制器。
獲取中斷號
編寫驅動的時候需要用到中斷號,我們用到中斷號,中斷信息已經寫到了設備樹里面,因此可以通過irq_of_parse_and_map 函數從interupts 屬性中提取到對應的設備號,函數原型如下:
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)函數參數和返回值含義如下:
dev:設備節點。
index:索引號,interrupts 屬性可能包含多條中斷信息,通過index 指定要獲取的信息。
返回值:中斷號。
如果使用GPIO 的話,可以使用gpio_to_irq 函數來獲取gpio 對應的中斷號,函數原型如下:
函數參數和返回值含義如下:
gpio:要獲取的GPIO 編號。
返回值:GPIO 對應的中斷號。
硬件原理圖分析
本章實驗硬件原理圖參考15.2 小節即可。
實驗程序編寫
本實驗對應的例程路徑為:開發板光盤-> 2、Linux 驅動例程-> 13_irq。
本章實驗我們驅動I.MX6U-ALPHA 開發板上的KEY0 按鍵,不過我們采用中斷的方式,并且采用定時器來實現按鍵消抖,應用程序讀取按鍵值并且通過終端打印出來。通過本章我們可以學習到Linux 內核中斷的使用方法,以及對Linux 內核定時器的回顧。
修改設備樹文件
本章實驗使用到了按鍵KEY0,按鍵KEY0 使用中斷模式,因此需要在“key”節點下添加中斷相關屬性,添加完成以后的“key”節點內容如下所示:
1 key { 2 #address-cells = <1>; 3 #size-cells = <1>; 4 compatible = "atkalpha-key"; 5 pinctrl-names = "default"; 6 pinctrl-0 = <&pinctrl_key>; 7 key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */ 8 interrupt-parent = <&gpio1>; 9 interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */ 10 status = "okay"; 11 };第8 行,設置interrupt-parent 屬性值為“gpio1”,因為KEY0 所使用的GPIO 為
GPIO1_IO18,也就是設置KEY0 的GPIO 中斷控制器為gpio1。
第9 行,設置interrupts 屬性,也就是設置中斷源,第一個cells 的18 表示GPIO1 組的18號IO。IRQ_TYPE_EDGE_BOTH 定義在文件include/linux/irq.h 中,定義如下:
從示例代碼51.3.1.2 中可以看出,IRQ_TYPE_EDGE_BOTH 表示上升沿和下降沿同時有效,相當于KEY0 按下和釋放都會觸發中斷。
設備樹編寫完成以后使用“make dtbs”命令重新編譯設備樹,然后使用新編譯出來的imx6ull-alientek-emmc.dtb 文件啟動Linux 系統。
按鍵中斷驅動程序編寫
新建名為“13_irq”的文件夾,然后在13_irq 文件夾里面創建vscode 工程,工作區命名為“imx6uirq”。工程創建好以后新建imx6uirq.c 文件,在imx6uirq.c 里面輸入如下內容:
#include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/init.h> #include <linux/module.h> #include <linux/errno.h> #include <linux/gpio.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/of.h> #include <linux/of_address.h> #include <linux/of_gpio.h> #include <linux/semaphore.h> #include <linux/timer.h> #include <linux/of_irq.h> #include <linux/irq.h> #include <asm/mach/map.h> #include <asm/uaccess.h> #include <asm/io.h> /*************************************************************** Copyright ? ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 文件名 : imx6uirq.c 作者 : 左忠凱 版本 : V1.0 描述 : Linux中斷驅動實驗 其他 : 無 論壇 : www.openedv.com 日志 : 初版V1.0 2019/7/26 左忠凱創建 ***************************************************************/ #define IMX6UIRQ_CNT 1 /* 設備號個數 */ #define IMX6UIRQ_NAME "imx6uirq" /* 名字 */ #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 *); /* 中斷服務函數 */ };/* imx6uirq設備結構體 */ struct imx6uirq_dev{dev_t devid; /* 設備號 */struct cdev 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 imx6uirq; /* irq設備 *//* @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;dev->curkeynum = 0;dev->timer.data = (volatile long)dev_id;mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); /* 10ms定時 */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;num = dev->curkeynum;keydesc = &dev->irqkeydesc[num];value = gpio_get_value(keydesc->gpio); /* 讀取IO值 */if(value == 0){ /* 按下按鍵 */atomic_set(&dev->keyvalue, keydesc->value);}else{ /* 按鍵松開 */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;imx6uirq.nd = of_find_node_by_path("/key");if (imx6uirq.nd== NULL){printk("key node not find!\r\n");return -EINVAL;} /* 提取GPIO */for (i = 0; i < KEY_NUM; i++) {imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd ,"key-gpio", i);if (imx6uirq.irqkeydesc[i].gpio < 0) {printk("can't get key%d\r\n", i);}}/* 初始化key所使用的IO,并且設置成中斷模式 */for (i = 0; i < KEY_NUM; i++) {memset(imx6uirq.irqkeydesc[i].name, 0, sizeof(name)); /* 緩沖區清零 */sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i); /* 組合名字 */gpio_request(imx6uirq.irqkeydesc[i].gpio, imx6uirq.irqkeydesc[i].name);gpio_direction_input(imx6uirq.irqkeydesc[i].gpio); imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i); #if 0imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio); #endifprintk("key%d:gpio=%d, irqnum=%d\r\n",i, imx6uirq.irqkeydesc[i].gpio, imx6uirq.irqkeydesc[i].irqnum);}/* 申請中斷 */imx6uirq.irqkeydesc[0].handler = key0_handler;imx6uirq.irqkeydesc[0].value = KEY0VALUE;for (i = 0; i < KEY_NUM; i++) {ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, imx6uirq.irqkeydesc[i].handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc[i].name, &imx6uirq);if(ret < 0){printk("irq %d request failed!\r\n", imx6uirq.irqkeydesc[i].irqnum);return -EFAULT;}}/* 創建定時器 */init_timer(&imx6uirq.timer);imx6uirq.timer.function = timer_function;return 0; }/** @description : 打開設備* @param - inode : 傳遞給驅動的inode* @param - filp : 設備文件,file結構體有個叫做private_data的成員變量* 一般在open的時候將private_data指向設備結構體。* @return : 0 成功;其他 失敗*/ static int imx6uirq_open(struct inode *inode, struct file *filp) {filp->private_data = &imx6uirq; /* 設置私有數據 */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;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 struct file_operations imx6uirq_fops = {.owner = THIS_MODULE,.open = imx6uirq_open,.read = imx6uirq_read, };/** @description : 驅動入口函數* @param : 無* @return : 無*/ static int __init imx6uirq_init(void) {/* 1、構建設備號 */if (imx6uirq.major) {imx6uirq.devid = MKDEV(imx6uirq.major, 0);register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME);} else {alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);imx6uirq.major = MAJOR(imx6uirq.devid);imx6uirq.minor = MINOR(imx6uirq.devid);}/* 2、注冊字符設備 */cdev_init(&imx6uirq.cdev, &imx6uirq_fops);cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);/* 3、創建類 */imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);if (IS_ERR(imx6uirq.class)) {return PTR_ERR(imx6uirq.class);}/* 4、創建設備 */imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);if (IS_ERR(imx6uirq.device)) {return PTR_ERR(imx6uirq.device);}/* 5、初始化按鍵 */atomic_set(&imx6uirq.keyvalue, INVAKEY);atomic_set(&imx6uirq.releasekey, 0);keyio_init();return 0; }/** @description : 驅動出口函數* @param : 無* @return : 無*/ static void __exit imx6uirq_exit(void) {unsigned int i = 0;/* 刪除定時器 */del_timer_sync(&imx6uirq.timer); /* 刪除定時器 *//* 釋放中斷 */for (i = 0; i < KEY_NUM; i++) {free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq);}cdev_del(&imx6uirq.cdev);unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);device_destroy(imx6uirq.class, imx6uirq.devid);class_destroy(imx6uirq.class); }module_init(imx6uirq_init); module_exit(imx6uirq_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("zuozhongkai");第38~43 行,結構體irq_keydesc 為按鍵的中斷描述結構體,gpio 為按鍵GPIO 編號,irqnum為按鍵IO 對應的中斷號,value 為按鍵對應的鍵值,name 為按鍵名字,handler 為按鍵中斷服務函數。使用irq_keydesc 結構體即可描述一個按鍵中斷。
第47~60 行,結構體imx6uirq_dev 為本例程設備結構體,第55 行的keyvalue 保存按鍵值,第56 行的releasekey 表示按鍵是否被釋放,如果按鍵被釋放表示發生了一次完整的按鍵過程。
第57 行的timer 為按鍵消抖定時器,使用定時器進行按鍵消抖的原理已經在19.1 小節講解過了。第58 行的數組irqkeydesc 為按鍵信息數組,數組元素個數就是開發板上的按鍵個數,I.MX6U-ALIPHA 開發板上只有一個按鍵,因此irqkeydesc 數組只有一個元素。第59 行的curkeynum 表示當前按鍵。
第62 行,定義設備結構體變量imx6uirq。
第70~78 行,key0_handler 函數,按鍵KEY0 中斷處理函數,參數dev_id 為設備結構體,也就是imx6uirq。第74 行設置curkeynum=0,表示當前按鍵為KEY0,第76 行使用mod_timer函數啟動定時器,定時器周期為10ms。
第85~103,timer_function 函數,定時器定時處理函數,參數arg 是設備結構體,也就是imx6uirq,在此函數中讀取按鍵值。第95 行通過gpio_get_value 函數讀取按鍵值。如果為0 的話就表示按鍵被按下去了,按下去的話就設置imx6uirq 結構體的keyvalue 成員變量為按鍵的鍵值,比如KEY0 按鍵的話按鍵值就是KEY0VALUE=0。如果按鍵值為1 的話表示按鍵被釋放了,按鍵釋放了的話就將imx6uirq 結構體的keyvalue 成員變量的最高位置1,表示按鍵值有效,也就是將keyvalue 與0x80 進行或運算,表示按鍵松開了,并且設置imx6uirq 結構體的releasekey成員變量為1,表示按鍵釋放,一次有效的按鍵過程發生。
第110~159 行,keyio_init 函數,按鍵IO 初始化函數,在驅動入口函數里面會調用keyio_init來初始化按鍵IO。第131~142 行輪流初始化所有的按鍵,包括申請IO、設置IO 為輸入模式、從設備樹中獲取IO 的中斷號等等。第136 行通過irq_of_parse_and_map 函數從設備樹中獲取按鍵IO 對應的中斷號。也可以使用gpio_to_irq 函數將某個IO 設置為中斷狀態,并且返回其中斷
號。第144 和145 行設置KEY0 按鍵對應的按鍵中斷處理函數為key0_handler、KEY0 的按鍵值為KEY0VALUE。第147~153 行輪流調用request_irq 函數申請中斷號,設置中斷觸發模式為IRQF_TRIGGER_FALLING 和IRQF_TRIGGER_RISING,也就是上升沿和下降沿都可以觸發中斷。最后,第156 行初始化定時器,并且設置定時器的定時處理函數。
第168~172 行,imx6uirq_open 函數,對應應用程序的open 函數。
第182~207 行,imx6uirq_read 函數,對應應用程序的read 函數。此函數向應用程序返回按鍵值。首先判斷imx6uirq 結構體的releasekey 成員變量值是否為1,如果為1 的話表示有一次有效按鍵發生,否則的話就直接返回-EINVAL。當有按鍵事件發生的話就要向應用程序發送按鍵值,首先判斷按鍵值的最高位是否為1,如果為1 的話就表示按鍵值有效。如果按鍵值有效
的話就將最高位清除,得到真實的按鍵值,然后通過copy_to_user 函數返回給應用程序。向應用程序發送按鍵值完成以后就將imx6uirq 結構體的releasekey 成員變量清零,準備下一次按鍵操作。
第210~214 行,按鍵中斷驅動操作函數集imx6uirq_fops。
第221~253 行,驅動入口函數,第250 和251 行分別初始化imx6uirq 結構體中的原子變量keyvalue 和releasekey,第252 行調用keyio_init 函數初始化按鍵所使用的IO。
第261~ 275 行,驅動出口函數,第265 行調用del_timer_sync 函數刪除定時器,第268~270行輪流釋放申請的所有按鍵中斷。
編寫測試APP
測試APP 要實現的內容很簡單,通過不斷的讀取/dev/imx6uirq 文件來獲取按鍵值,當按鍵按下以后就會將獲取到的按鍵值輸出在終端上,新建名為imx6uirqApp.c 的文件,然后輸入如下所示內容:
#include "stdio.h" #include "unistd.h" #include "sys/types.h" #include "sys/stat.h" #include "fcntl.h" #include "stdlib.h" #include "string.h" #include "linux/ioctl.h" /*************************************************************** Copyright ? ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 文件名 : imx6uirqApp.c 作者 : 左忠凱 版本 : V1.0 描述 : 定時器測試應用程序 其他 : 無 使用方法 :./imx6uirqApp /dev/imx6uirq 打開測試App 論壇 : www.openedv.com 日志 : 初版V1.0 2019/7/26 左忠凱創建 ***************************************************************//** @description : main主程序* @param - argc : argv數組元素個數* @param - argv : 具體參數* @return : 0 成功;其他 失敗*/ int main(int argc, char *argv[]) {int fd;int ret = 0;int data = 0;char *filename;unsigned char data;if (argc != 2) {printf("Error Usage!\r\n");return -1;}filename = argv[1];fd = open(filename, O_RDWR);if (fd < 0) {printf("Can't open file %s\r\n", filename);return -1;}while (1) {ret = read(fd, &data, sizeof(data));if (ret < 0) { /* 數據讀取錯誤或者無效 */} else { /* 數據讀取正確 */if (data) /* 讀取到數據 */printf("key value = %#X\r\n", data);}}close(fd);return ret; }第45~53 行的while 循環用于不斷的讀取按鍵值,如果讀取到有效的按鍵值就將其輸出到終端上。
運行測試
編譯驅動程序和測試APP
1、編譯驅動程序
編寫Makefile 文件,本章實驗的Makefile 文件和第四十章實驗基本一樣,只是將obj-m 變量的值改為imx6uirq.o,Makefile 內容如下所示:
第4 行,設置obj-m 變量的值為imx6uirq.o。
輸入如下命令編譯出驅動模塊文件:
編譯成功以后就會生成一個名為“imx6uirq.ko”的驅動模塊文件。
2、編譯測試APP
輸入如下命令編譯測試imx6uirqApp.c 這個測試程序:
編譯成功以后就會生成imx6uirqApp 這個應用程序。
運行測試
將上一小節編譯出來imx6uirq.ko 和imx6uirqApp 這兩個文件拷貝到
rootfs/lib/modules/4.1.15 目錄中,重啟開發板,進入到目錄lib/modules/4.1.15 中,輸入如下命令加載imx6uirq.ko 驅動模塊:
驅動加載成功以后可以通過查看/proc/interrupts 文件來檢查一下對應的中斷有沒有被注冊上,輸入如下命令:
cat /proc/interrupts結果如圖51.4.2.1 所示:
從圖51.4.2.1 可以看出imx6uirq.c 驅動文件里面的KEY0 中斷已經存在了,觸發方式為跳邊沿(Edge),中斷號為49。
接下來使用如下命令來測試中斷:
按下開發板上的KEY0 鍵,終端就會輸出按鍵值,如圖51.4.2.2 所示:
從圖51.4.2.2 可以看出,按鍵值獲取成功,并且不會有按鍵抖動導致的誤判發生,說明按鍵消抖工作正常。如果要卸載驅動的話輸入如下命令即可:
總結
以上是生活随笔為你收集整理的Linux 中断实验的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java第一个helloworld_Ja
- 下一篇: Linux 阻塞和非阻塞IO 实验