視頻:第13.1講 Linux中斷實驗-Linux內核中斷框架簡介_嗶哩嗶哩_bilibili
文檔:《【正點原子】I.MX6U嵌入式Linux驅動開發指南V1.81.pdf》五十一章
1. 中斷API函數
????????每個中斷都有一個中斷號,通過中斷號即可區分不同的中斷。在Linux 內核中使用一個 int 變量表示中斷號。
1.1 申請 request_irq
? ? ? ? 申請一個中斷。
request_irq(unsigned int irq, // 要申請中斷的中斷號irq_handler_t handler, // 中斷處理函數unsigned long flags, // 中斷標志,/linux/interrupt.h里面查看所有的中斷標志const char *name, // 中斷名字。設置以后可以在/proc/interrupts文件中看到對應的中斷名字void *dev) // 如果將flags設置為IRQF_SHARED的話,dev用來區分不同的中斷,// 一般情況下將dev設置為設備結構體,dev會傳遞給中斷處理函數irq_handler_t的第二個參數。 // return : 0中斷申請成功,其他負值中斷申請失敗,返回-EBUSY表示中斷已經被申請
1.2 釋放 free_irq
? ? ? ? 使用完后釋放中斷。
void free_irq(unsigned int irq, // 要釋放的中斷號void *dev) // 如果中斷設置為共享(IRQF_SHARED)的話,此參數用來區分具體的中斷// 共享中斷只有在釋放最后中斷處理函數的時候才會被禁止掉
1.3 中斷處理函數
? ? ? ? 中斷處理函數格式:
irqreturn_t (*irq_handler_t) (int, void *)//第一個int參數是要中斷處理函數要相應的中斷號
//第二個void*參數是一個指向void的指針,也就是個通用指針,需要與request_irq函數的dev參數保持一致。用于區分共享中斷的不同設備,
// dev也可以指向設備數據結構。中斷處理函數的返回值為irqreturn_t類型
1.4 中斷使能 / 禁止
? ? ? ? 使能一個中斷:
void enable_irq(unsigned int irq)
// irq : 要使能的中斷號
? ? ? ? 禁止一個中斷:
void disable_irq(unsigned int irq)
// irq : 要禁止的中斷號
? ? ? ?該函數要等到當前正在執行的中斷處理函數執行完才返回,因此使用者需要保證不會產生新的中斷,并且確保所有已經開始執行的中斷處理程序已經全部退出。
void disable_irq_nosync(unsigned int irq)
// irq : 要禁止的中斷號
????????函數調用以后立即返回,不會等待當前中斷處理程序執行完畢。
1.5 全局使能 / 禁止
? ? ? ? 開啟或關閉當前處理器的整個中斷系統/全局中斷:
local_irq_enable()
local_irq_disable()
?????? local_irq_enable用于使能當前處理器中斷系統,local_irq_disable用于禁止當前處理器中斷 系統。
?(來自《指南》的例子)A任務要關閉全局中斷10s,但關了2s后B任務要關閉全局中斷3s,3s后B又打開了全局中斷。此時就破壞了A任務關閉全局中斷10s的要求,可能會導致一些意外。因此B不能簡單地直接打開全局中斷,而是將中斷恢復到關閉時的狀態。就可以用這兩個函數:
local_irq_save(flags) // 禁止中斷,并將中斷狀態保存在flags中
local_irq_restore(flags) // 恢復中斷,并將中斷狀態恢復到flags狀態
1.6??獲取中斷號
(這部分應該放到下面的設備樹里面,為了整齊我扔到這里來了)
從設備樹中的interupts屬性中獲取中斷號:
// 定義在include/linux/of_irq.h
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
// dev : 設備節點
// index : 索引號
// return: 中斷號
如果使用GPIO,可以直接獲取gpio對應的中斷號:
int gpio_to_irq(unsigned int gpio)
// gpio : 要獲取的GPIO編號
// return: GPIO對應的中斷號
2. 上半部和下半部 / 頂半部和底半部
(直接把《指南》里的內容精簡一下貼過來了:)
????????中斷處理函數一定要快點執行完畢,越短越好,但是有些中斷處理過程就是比較費時間,必須要對其進行處理,縮短中斷處理函數的執行時間。
????????比如電容觸摸屏通過中斷通知SOC有觸摸事件發生,SOC響應中斷,然后通過IIC讀取觸摸坐標值并將其上報給系統。但是IIC的速度最高也只有400Kbit/s,中斷通過IIC讀取數據就會浪費時間。我們可以將IIC讀取觸摸數據的操作暫后執行,中斷處理函數僅僅響應中斷,然后清除中斷標志位即可。
這個時候中斷處理過程就分為了兩部分:
????????上半部:中斷處理函數,那些處理過程比較快、不會占用很長時間的處理就可以放在上半部完成。
????????下半部:如果中斷處理過程比較耗時,那么就將這些比較耗時的代碼交給下半部去執行。
????????因此,Linux內核將中斷分為上半部和下半部的主要目的就是實現中斷處理函數的快進快 出,至于哪些代碼屬于上半部,哪些代碼屬于下半部并沒有明確的規定, 一切根據實際使用情況去判斷,一些參考點:
????????①、如果要處理的內容不希望被其他中斷打斷,可以放到上半部。
????????②、如果要處理的任務對時間敏感,可以放到上半部。
????????③、如果要處理的任務與硬件有關,可以放到上半部?
????????④、除了上述三點以外的其他任務,優先考慮放到下半部。
上半部處理直接編寫中斷處理函數就可以。對于下半部,Linux內核提供了多種下半部機制。
2.1 軟中斷
? ? ? ? (基本不用,隨便看看就星)
? ? ? ? 結構體softirq_action表示軟中斷:
// 定義在include/linux/interrupt.h
struct softirq_action{void (*action)(struct softirq_action *); // action成員變量就是軟中斷的服務函數
};
? ? ? ? 共有10種軟中斷:
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
// NR_SOFTIRQS為10,數組softirq_vec有10個元素
????????數組softirq_vec是全局數組,所有的CPU都可以訪問,每個CPU都有自己的觸發和控制機制,并且只執行自己所觸發的軟中斷。但是各個CPU所執行的軟中斷服務函數是相同的,都是數組softirq_vec中定義的action函數。
// 定義在include/linux/interrupt.h
enum{HI_SOFTIRQ=0, // 高優先級軟中斷TIMER_SOFTIRQ, // 定時器軟中斷NET_TX_SOFTIRQ, // 網絡數據發送軟中斷NET_RX_SOFTIRQ, // 網絡數據接收軟中斷BLOCK_SOFTIRQ, // 塊設備軟中斷BLOCK_IOPOLL_SOFTIRQ, // 塊設備IO輪詢軟中斷TASKLET_SOFTIRQ, // Tasklet軟中斷SCHED_SOFTIRQ, // 調度器軟中斷HRTIMER_SOFTIRQ, // 高精度定時器軟中斷RCU_SOFTIRQ, // RCU 回調軟中斷/* Preferable RCU should always be the last softirq */NR_SOFTIRQS // softirq的總數
};
2.2?tasklet
? ? ? ? 也需要用到上半部,只是上半部的中斷處理函數重點在調用tasklet_schedule。
// 定義在include/linux/interrupt.h
struct tasklet_struct { struct tasklet_struct *next; /* 下一個tasklet */ unsigned long state; /* tasklet狀態 */ atomic_t count; /* 計數器,記錄對tasklet的引用數 */ void (*func)(unsigned long); /* tasklet執行的函數,相當于中斷處理函數 */ unsigned long data; /* 函數func的參數 */
};
2.2.1 初始化
// 定義在kernel/softirq.c
void tasklet_init(struct tasklet_struct *t, // 要初始化的taskletvoid (*func)(unsigned long), // tasklet的處理函數unsigned long data // 要傳遞給func函數的參數)
也可以使用宏DECLARE_TASKLET來一次性完成tasklet的定義和初始化, DECLARE_TASKLET定義在include/linux/interrupt.h文件中,定義如下:
// 定義在include/linux/interrupt.h
#define DECLARE_TASKLET(name, func, data) \ // name要定義的tasklet,func要處理的函數,data要傳給處理函數的參數
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
2.2.2 示例代碼
《指南》中給出了示例代碼:
/* 定義taselet */
struct tasklet_struct testtasklet; /* tasklet處理函數 */
void testtasklet_func(unsigned long data) { /* tasklet具體處理內容 */
}
/* 中斷處理函數 */
irqreturn_t test_handler(int irq, void *dev_id) { ...... /* 調度tasklet */ tasklet_schedule(&testtasklet); ......
}
/* 驅動入口函數 */
static int __init xxxx_init(void) { ...... /* 初始化tasklet */ tasklet_init(&testtasklet, testtasklet_func, data); /* 注冊中斷處理函數 */ request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev); ......
}
2.3?工作隊列
????????工作隊列將要推后的工作交給一個內核線程去執行。因為工作隊列工作在進程上下文,因此工作隊列允許睡眠或重新調度。
????????因此如果你要推后的工作可以睡眠,那么就可以選擇工作隊列,否則的話就只能選擇軟中斷或tasklet。
? ? ? ? 工作結構體定義如下:
// 定義在include/linux/workqueue.h
struct work_struct {atomic_long_t data;struct list_head entry;work_func_t func;
#ifdef CONFIG_LOCKDEPstruct lockdep_map lockdep_map;
#endif
};
? ? ? ? 許多個工作(work_struct)組成工作隊列(workqueue_struct),內核使用工作者線程(worker)來處理工作隊列中的工作。在實際驅動開發中,只需要工作work_struct即可,工作隊列和工作者線程由系統完成,我們不用去管。
? ? ? ? 初始化:
#define INIT_WORK(_work, _func)
// _work: 要初始化的工作
// _func: 工作對應的處理函數#define DECLARE_WORK(n, f)
// n: 定義的工作。這個宏會聲明并定義一個struct work_struct類型的變量n并初始化它。
// f: 工作對應的處理函數示例:DECLARE_WORK(mywork, myfunc);等價于struct work_struct mywork;INIT_WORK(&mywork, myfunc);
????????調度:
bool schedule_work(struct work_struct *work)
// work : 要調度的工作
// return: 0成功,else失敗
????????《指南》給出了工作隊列使用示例代碼51.1.2.11:
/* 定義工作(work) */
struct work_struct testwork; /* work處理函數 */
void testwork_func_t(struct work_struct *work); { /* work具體處理內容 */
}
/* 中斷處理函數 */
irqreturn_t test_handler(int irq, void *dev_id) { ...... /* 調度work */ schedule_work(&testwork); ......
}
/* 驅動入口函數 */
static int __init xxxx_init(void) { ...... /* 初始化work */ INIT_WORK(&testwork, testwork_func_t); /* 注冊中斷處理函數 */ request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev); ......
}
3. 設備樹中的中斷信息節點
????????中斷控制器的設備樹綁定信息參考文檔:linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek\ Documentation\devicetree\bindings\arm\gic.txt
- compatible : should be one of:
?? ?"arm,gic-400"
?? ?"arm,cortex-a15-gic"
?? ?"arm,cortex-a9-gic"
?? ?"arm,cortex-a7-gic"
?? ?"arm,arm11mp-gic"
?? ?"brcm,brahma-b15-gic"
?? ?"arm,arm1176jzf-devchip-gic"
?? ?"qcom,msm-8660-qgic"
?? ?"qcom,msm-qgic2"
- interrupt-controller : Identifies the node as an interrupt controller
- #interrupt-cells : Specifies the number of cells needed to encode an interrupt source. ?The type shall be a <u32> and the value shall be 3.? The 1st cell is the interrupt type; 0 for SPI interrupts, 1 for PPI?interrupts.
? The 2nd cell contains the interrupt number for the interrupt type.
? SPI interrupts are in the range [0-987]. ?PPI interrupts are in the range [0-15].? The 3rd cell is the flags, encoded as follows:
?? ?bits[3:0] trigger type and level flags.
?? ??? ?1 = low-to-high edge triggered?
?? ??? ?2 = high-to-low edge triggered (invalid for SPIs)
?? ??? ?4 = active high level-sensitive
?? ??? ?8 = active low level-sensitive (invalid for SPIs).
?? ?bits[15:8] PPI interrupt cpu mask. ?Each bit corresponds to each of
?? ?the 8 possible cpus attached to the GIC. ?A bit set to '1' indicated
?? ?the interrupt is wired to that CPU. ?Only valid for PPI interrupts.
?? ?Also note that the configurability of PPI interrupts is IMPLEMENTATION
?? ?DEFINED and as such not guaranteed to be present (most SoC available
?? ?in 2014 seem to ignore the setting of this flag and use the hardware
?? ?default value).
? ? ? ? 舉個例子,imx6ull.dtsi中的intc節點就是 I.MX6ULL的中斷控制器節點:
intc: interrupt-controller@00a01000 {compatible = "arm,cortex-a7-gic";#interrupt-cells = <3>; // 此中斷控制器下設備的cells大小為3interrupt-controller; // 表示當前節點是中斷控制器reg = <0x00a01000 0x1000>,<0x00a02000 0x100>;};
????????gpio節點也可以作為中斷控制器。
????????如imx6ull.dtsi文件中的gpio5節點:
gpio5: gpio@020ac000 {compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";reg = <0x020ac000 0x4000>;interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,<GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;gpio-controller; // 中斷控制器,用于控制gpio5所有IO的中斷#gpio-cells = <2>;interrupt-controller;#interrupt-cells = <2>;
};
? ? ? ? 該節點的interrupts這個屬性得細說啊。
? ? ? ? 其中GIC_SPI==0? (定義在include/dt-bindings/interrupt-controller/arm-gic.h),IRQ_TYPE_LEVEL_HIGH==4? (定義在include/dt-bindings/interrupt-controller/irq.h)
? ? ? ? 再參考上方引用塊綁定信息加粗的#interrupt-cells部分,這兩行表示SPI中斷、中斷號為74 75、高電平觸發。
? ? ? ? 至于這個74和75怎么來的,查?《IMX6ULL參考手冊》185頁可以找到下表,即GPIO5有兩個中斷號——74和75。74對應GPIO5_IO0015低16位IO,75對應GPIO5_IO1631高16位IO。
以下內容來自linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek\Documentation\devicetree \bindings\gpio\gpio-mxs.txt:
The Freescale MXS GPIO controller is part of MXS PIN controller. ?The
GPIOs are organized in port/bank. ?Each port consists of 32 GPIOs.As the GPIO controller is embedded in the PIN controller and all the
GPIO ports share the same IO space with PIN controller, the GPIO node
will be represented as sub-nodes of MXS pinctrl node.Required properties for GPIO node:
- compatible : Should be "fsl,<soc>-gpio". ?The supported SoCs include?imx23 and imx28.
- interrupts : Should be the port interrupt shared by all 32 pins.
- gpio-controller : Marks the device node as a gpio controller.
- #gpio-cells : Should be two. ?The first cell is the pin number and?the second cell is used to specify the gpio polarity:
? ? ? 0 = active high
? ? ? 1 = active low
- interrupt-controller: Marks the device node as an interrupt controller.
- #interrupt-cells : Should be 2. ?The first cell is the GPIO number.
? The second cell bits[3:0] is used to specify trigger type and level flags:
? ? ? 1 = low-to-high edge triggered.
? ? ? 2 = high-to-low edge triggered.
? ? ? 4 = active high level-sensitive.
? ? ? 8 = active low level-sensitive.
// imx6ull-alientek-emmc.dtsfxls8471@1e {compatible = "fsl,fxls8471";reg = <0x1e>;position = <0>;interrupt-parent = <&gpio5>; // 父中斷interrupts = <0 8>; // 參照上面引用塊里的interrupts部分。表示GPIO5_IO00,低電平觸發};
4. 實驗:按鍵中斷+定時器消抖+tasklet下半部
視頻:第13.4講 Linux中斷實驗-按鍵中斷實驗驅動編寫(上)_嗶哩嗶哩_bilibili
手冊:《【正點原子】I.MX6U嵌入式Linux驅動開發指南V1.81.pdf》51.3節
4.1 設備樹
? ? ? ? 直接在上次按鍵實驗設備樹節點基礎上增加兩行中斷描述即可。將如下代碼添加到imx6ull-alientek-emmc.dts根節點最后:
key{compatible = "alientek,key";pinctrl-names = "default";pinctrl-0 = <&pinctrl_key>;key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>; status = "okay";interrupt-parent = <&gpio1>;interrupts = <18 IRQ_TYPE_EDGE_BOTH>;};
如果需要配置多個中斷,則寫法為:
編譯為dtb并復制到tftproot,重啟開發板:
make dtbs
sudo cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /.../tftpboot/ -f
可以去開發板檢查一下,應當能看到key節點:
4.2 文件結構
? ? ? ? 和其他實驗一樣,直接貼過來改個文件名:
14_IRQ (工作區)
├── .vscode
│ ├── c_cpp_properties.json
│ └── settings.json
├── 14_irq.code-workspace
├── Makefile
├── irq.c
└── irqAPP.c
4.3 Makefile
CFLAGS_MODULE += -wKERNELDIR := /home/for/linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek # 內核路徑
# KERNELDIR改成自己的?linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek文件路徑(這個文件從正點原子“01、例程源碼”中直接搜,cp到虛擬機里面)CURRENT_PATH := $(shell pwd) # 當前路徑obj-m := irq.o # 編譯文件build: kernel_modules # 編譯模塊kernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
4.4 irq.c
按鍵→中斷處理函數→調用tasklet處理函數→tasklet啟動定時器→定時器處理函數
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/timer.h>
#include <linux/interrupt.h>
#include <linux/atomic.h>#define DEV_CNT 1 /* 設備號數量 */
#define DEV_NAME "timer" /* 設備名 */
#define KEY_NUM 1 /* 按鍵數量 */
#define KEY0_VALUE 0x01 /* 按鍵值(假定釋放為1,按下為0)*/
#define INVAKEY 0xFF /* 按鍵無效值 *//* 按鍵結構體 */
struct irq_keydesc{int gpio; /* io號 */int irqnum; /* 中斷號 */unsigned char value; /* 鍵值 (默認/空閑電平,比如 1) */char name[10]; /* 名字 */irqreturn_t (*handler) (int, void *); /* 中斷處理函數*/struct tasklet_struct tasklet;
};/* 中斷設備結構體 */
struct irq{dev_t devid; // 設備號int major; // 主設備號int minor; // 次struct cdev cdev; // cdevstruct device *device; // 設備struct class *class; // 類struct device_node *nd; // 節點struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 個按鍵struct timer_list timer;// 定時器atomic_t releasekey; // 0表示按鍵釋放,1表示按鍵按下atomic_t keyvalue; // 第八位表示數據是否被讀取:每完成一次按鍵釋放,第八位就會置1,用戶態調用.read時會將其置0,表示數據被讀取// 其余七位表示按鍵的值
};
static struct irq key_irq;/* 打開/讀操作 */
static int irq_open(struct inode *inode, struct file *filp){filp->private_data = &key_irq;return 0;
}
static ssize_t irq_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 irq* dev = filp->private_data;keyvalue = (unsigned char)atomic_read(&dev->keyvalue);releasekey = (unsigned char)atomic_read(&dev->releasekey);if(releasekey){ // 如果按鍵釋放,此時可以讀取數據if(keyvalue & 0x80){ // 如果最高位為1,表示此時數據還未被讀取unsigned char out = keyvalue & 0x7F; // 將最高位置0(最高位為有效位,低7位為實際value)if(copy_to_user(buf, &out, sizeof(out))){ // 返回按鍵值給用戶態return -EFAULT;}ret = sizeof(out);} else {goto data_error;}atomic_set(&dev->releasekey, 0); // 釋放標志清零return ret;} else {goto data_error;}data_error:return -EINVAL;
}/* 操作集 */
static const struct file_operations key_fops = {.owner = THIS_MODULE,.open = irq_open,.read = irq_read,
};/* 中斷處理函數 */
static irqreturn_t key0_handler(int irq, void *dev_id){struct irq *dev = (struct irq*)dev_id;int current_level; // 保存中斷時的電平current_level = gpio_get_value(dev->irqkey[0].gpio);atomic_set(&dev->keyvalue, current_level); // 保存基準電平tasklet_schedule(&dev->irqkey[0].tasklet); // 調度tasklet處理后續邏輯,避免中斷耗時return IRQ_HANDLED;
}/* 定時器處理函數 */
static void timer_func(unsigned long arg){struct irq* dev = (struct irq*)arg;int current_value; // 保存定時器延時后的電平/* 讀取當前按鍵電平 */current_value = gpio_get_value(dev->irqkey[0].gpio);/* 若當前電平與中斷發生時讀取到的電平一致,則有效 */if (current_value == atomic_read(&dev->keyvalue)) {if (current_value == 1) { // 釋放printk("KEY0 release!\r\n");atomic_set(&dev->keyvalue, 0x80 | dev->irqkey[0].value); // 最高位置1,表示有效atomic_set(&dev->releasekey, 1); // 表示已釋放,此時用戶態可讀} else { // 按下printk("KEY0 Push!\r\n");atomic_set(&dev->keyvalue, current_value); // 這里0 表示按下// atomic_set(&dev->releasekey, 0); // 表示未釋放}}
}/* tasklet處理函數 */
static void key_tasklet(unsigned long data){struct irq* dev = (struct irq*)data;printk("key_tasklet\r\n");dev->timer.data = data;mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20)); /* 20ms定時 */}/* 初始化按鍵 */
static int keyio_init(struct irq *dev){int ret = 0;int i = 0;/* 1. 按鍵初始化 */dev->nd = of_find_node_by_path("/key"); // 獲取設備節點if(dev->nd == NULL){ret = -EINVAL;goto fail_nd;}for(i=0; i<KEY_NUM; i++){dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i); // 獲取設備樹中 gpioif (dev->irqkey[i].gpio < 0) {pr_err("get gpio %d failed\n", i);ret = -EINVAL;goto fail_nd;}}for(i=0; i<KEY_NUM; i++){memset(dev->irqkey[i].name, 0, sizeof(dev->irqkey[i].name));sprintf(dev->irqkey[i].name, "KEY%d", i); // 命名ret = gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name); // 申請gpioif (ret) {pr_err("gpio_request %d failed\n", dev->irqkey[i].gpio);goto fail_gpio_req;}gpio_direction_input(dev->irqkey[i].gpio); // 設置io方向dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio);// 獲取中斷號if (dev->irqkey[i].irqnum < 0) {pr_err("gpio_to_irq failed for gpio %d\n", dev->irqkey[i].gpio);ret = dev->irqkey[i].irqnum;goto fail_gpio_req;}}dev->irqkey[0].handler = key0_handler; // 中斷處理函數dev->irqkey[0].value = KEY0_VALUE; // 例如:1 表示 release(高電平),0 表示按下(低電平)/* 2. 中斷初始化 */for(i=0; i<KEY_NUM; i++){ret = request_irq(dev->irqkey[i].irqnum,dev->irqkey[i].handler,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,dev->irqkey[i].name,&key_irq); /* dev_id 傳入結構體指針 */if(ret < 0){printk("irq %d request failed! ret=%d\n", dev->irqkey[i].irqnum, ret);while (--i >= 0) {free_irq(dev->irqkey[i].irqnum, &key_irq);}goto fail_irq;}tasklet_init(&dev->irqkey[0].tasklet, key_tasklet, (unsigned long)dev);}return 0;fail_irq:
fail_gpio_req:for (i = 0; i < KEY_NUM; i++) {if (gpio_is_valid(dev->irqkey[i].gpio))gpio_free(dev->irqkey[i].gpio);}
fail_nd:return ret;
}/* 驅動入口 */
static int __init key_init(void){int ret = 0;/* 1. 注冊字符設備驅動 */key_irq.devid = 0;alloc_chrdev_region(&key_irq.devid, 0, DEV_CNT, DEV_NAME);key_irq.major = MAJOR(key_irq.devid);key_irq.minor = MINOR(key_irq.devid);/* 2. 初始化 cdev */key_irq.cdev.owner = THIS_MODULE;cdev_init(&key_irq.cdev, &key_fops);/* 3. 添加 cdev */ret = cdev_add(&key_irq.cdev, key_irq.devid, DEV_CNT);if (ret) {pr_err("cdev_add failed\n");goto fail_cdev;}/* 4. 創建類 */key_irq.class = class_create(THIS_MODULE, DEV_NAME);if(IS_ERR(key_irq.class)){ret = PTR_ERR(key_irq.class);goto fail_class;}/* 5. 創建設備 */key_irq.device = device_create(key_irq.class, NULL, key_irq.devid, NULL, DEV_NAME);if(IS_ERR(key_irq.device)){ret = PTR_ERR(key_irq.device);goto fail_device;}/* 6. 初始化IO*/ret = keyio_init(&key_irq);if(ret < 0){goto fail_keyinit;}/* 7. 初始化定時器 */init_timer(&key_irq.timer);key_irq.timer.function = timer_func;key_irq.timer.data = (unsigned long)&key_irq; /* 初始化 timer.data 指向設備結構體 *//* 8. 初始化原子變量 */atomic_set(&key_irq.keyvalue, INVAKEY);atomic_set(&key_irq.releasekey, 0);return 0;fail_keyinit:device_destroy(key_irq.class, key_irq.devid);
fail_device:class_destroy(key_irq.class);
fail_class:cdev_del(&key_irq.cdev);
fail_cdev:unregister_chrdev_region(key_irq.devid, DEV_CNT);return ret;
}/* 驅動出口 */
static void __exit key_exit(void){int i=0;/* 釋放中斷 */for(i=0; i<KEY_NUM; i++){free_irq(key_irq.irqkey[i].irqnum, &key_irq);}/* 釋放io */for(i=0; i<KEY_NUM; i++){if (gpio_is_valid(key_irq.irqkey[i].gpio))gpio_free(key_irq.irqkey[i].gpio);}/* 刪除定時器 */del_timer_sync(&key_irq.timer);/* 注銷字符設備驅動 */cdev_del(&key_irq.cdev);unregister_chrdev_region(key_irq.devid, DEV_CNT);device_destroy(key_irq.class, key_irq.devid);class_destroy(key_irq.class);
}module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
4.5 irqAPP.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/ioctl.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>/* * @description : main主程序 * @param - argc : argv數組元素個數 * @param - argv : 具體參數 * @return : 0 成功; else失敗* 調用 ./irqAPP /dev/timer*/ #define LEDOFF 0
#define LEDON 1int main(int argc, char *argv[]){if(argc != 2){ // 判斷用法是否錯誤printf("Error Usage!\r\n");return -1;}char *filename;int fd = 0, ret = 0;unsigned char data;filename = argv[1];fd = open(filename, O_RDWR); // 讀寫模式打開驅動文件filenameif(fd <0){printf("file %s open failed!\r\n");return -1;}while(1) {ret = read(fd, &data, sizeof(data));if (ret > 0) {printf("key value = %#x\r\n", data);} else {// usleep(10000); // 10ms 防止CPU占用過高}}printf("App finished!\r\n");close(fd);return 0;
}
4.6 測試
# VSCODE終端
make
arm-linux-gnueabihf-gcc irqAPP.c -o irqAPP
sudo cp irq.ko irqAPP /home/for/linux/nfs/rootfs/lib/modules/4.1.15/ -f# 串口
cd /lib/modules/4.1.15/
depmod
modprobe irq.ko
cat /proc/interrupts # 此時在列表最右一列中應當能看到KEY0./irqAPP /dev/timer
# 然后按下按鍵/松開按鍵,串口應有對應的輸出rmmod irq.k
cat /proc/interrupts # 此時應當看不到KEY0
5. 實驗:按鍵中斷+定時器消抖+工作隊列
有以下修改:
? ? ? ? 中斷設備結構體中增加了工作隊列結構體work_struct????????中斷處理函數key0_handler中增加了對工作隊列的調用schedule_work()
? ? ? ? 初始化函數keyio_init()中增加了對工作隊列的初始化INIT_WORK
? ? ? ? 增加了work處理函數key_work()
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/timer.h>
#include <linux/interrupt.h>
#include <linux/atomic.h>#define DEV_CNT 1 /* 設備號數量 */
#define DEV_NAME "timer" /* 設備名 */
#define KEY_NUM 1 /* 按鍵數量 */
#define KEY0_VALUE 0x01 /* 按鍵值(假定釋放為1,按下為0)*/
#define INVAKEY 0xFF /* 按鍵無效值 *//* 按鍵結構體 */
struct irq_keydesc{int gpio; /* io號 */int irqnum; /* 中斷號 */unsigned char value; /* 鍵值 (默認/空閑電平,比如 1) */char name[10]; /* 名字 */irqreturn_t (*handler) (int, void *); /* 中斷處理函數*/// struct tasklet_struct tasklet;struct work_struct work;
};/* 中斷設備結構體 */
struct irq{dev_t devid; // 設備號int major; // 主設備號int minor; // 次struct cdev cdev; // cdevstruct device *device; // 設備struct class *class; // 類struct device_node *nd; // 節點struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 個按鍵struct timer_list timer;// 定時器atomic_t releasekey; // 0表示按鍵釋放,1表示按鍵按下atomic_t keyvalue; // 第八位表示數據是否被讀取:每完成一次按鍵釋放,第八位就會置1,用戶態調用.read時會將其置0,表示數據被讀取// 其余七位表示按鍵的值
};
static struct irq key_irq;/* 打開/讀操作 */
static int irq_open(struct inode *inode, struct file *filp){filp->private_data = &key_irq;return 0;
}
static ssize_t irq_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 irq* dev = filp->private_data;keyvalue = (unsigned char)atomic_read(&dev->keyvalue);releasekey = (unsigned char)atomic_read(&dev->releasekey);if(releasekey){ // 如果按鍵釋放,此時可以讀取數據if(keyvalue & 0x80){ // 如果最高位為1,表示此時數據還未被讀取unsigned char out = keyvalue & 0x7F; // 將最高位置0(最高位為有效位,低7位為實際value)if(copy_to_user(buf, &out, sizeof(out))){ // 返回按鍵值給用戶態return -EFAULT;}ret = sizeof(out);} else {goto data_error;}atomic_set(&dev->releasekey, 0); // 釋放標志清零return ret;} else {goto data_error;}data_error:return -EINVAL;
}/* 操作集 */
static const struct file_operations key_fops = {.owner = THIS_MODULE,.open = irq_open,.read = irq_read,
};/* 中斷處理函數 */
static irqreturn_t key0_handler(int irq, void *dev_id){struct irq *dev = (struct irq*)dev_id;int current_level; // 保存中斷時的電平current_level = gpio_get_value(dev->irqkey[0].gpio);atomic_set(&dev->keyvalue, current_level); // 保存基準電平// tasklet_schedule(&dev->irqkey[0].tasklet); // 調度tasklet處理后續邏輯,避免中斷耗時schedule_work(&dev->irqkey[0].work);return IRQ_HANDLED;
}/* 定時器處理函數 */
static void timer_func(unsigned long arg){struct irq* dev = (struct irq*)arg;int current_value; // 保存定時器延時后的電平/* 讀取當前按鍵電平 */current_value = gpio_get_value(dev->irqkey[0].gpio);/* 若當前電平與中斷發生時讀取到的電平一致,則有效 */if (current_value == atomic_read(&dev->keyvalue)) {if (current_value == 1) { // 釋放printk("KEY0 release!\r\n");atomic_set(&dev->keyvalue, 0x80 | dev->irqkey[0].value); // 最高位置1,表示有效atomic_set(&dev->releasekey, 1); // 表示已釋放,此時用戶態可讀} else { // 按下printk("KEY0 Push!\r\n");atomic_set(&dev->keyvalue, current_value); // 這里0 表示按下// atomic_set(&dev->releasekey, 0); // 表示未釋放}}
}/* tasklet處理函數 */
static void key_tasklet(unsigned long data){struct irq* dev = (struct irq*)data;printk(" key_tasklet\r\n");dev->timer.data = data;mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20)); /* 20ms定時 */}
/* 工作隊列處理函數 */
static void key_work(struct work_struct *work){printk(" key_work\r\n");key_irq.timer.data = (unsigned long)&key_irq;mod_timer(&key_irq.timer, jiffies + msecs_to_jiffies(20)); /* 20ms定時 */
}/* 初始化按鍵 */
static int keyio_init(struct irq *dev){int ret = 0;int i = 0;/* 1. 按鍵初始化 */dev->nd = of_find_node_by_path("/key"); // 獲取設備節點if(dev->nd == NULL){ret = -EINVAL;goto fail_nd;}for(i=0; i<KEY_NUM; i++){dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i); // 獲取設備樹中 gpioif (dev->irqkey[i].gpio < 0) {pr_err("get gpio %d failed\n", i);ret = -EINVAL;goto fail_nd;}}for(i=0; i<KEY_NUM; i++){memset(dev->irqkey[i].name, 0, sizeof(dev->irqkey[i].name));sprintf(dev->irqkey[i].name, "KEY%d", i); // 命名ret = gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name); // 申請gpioif (ret) {pr_err("gpio_request %d failed\n", dev->irqkey[i].gpio);goto fail_gpio_req;}gpio_direction_input(dev->irqkey[i].gpio); // 設置io方向dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio);// 獲取中斷號if (dev->irqkey[i].irqnum < 0) {pr_err("gpio_to_irq failed for gpio %d\n", dev->irqkey[i].gpio);ret = dev->irqkey[i].irqnum;goto fail_gpio_req;}}dev->irqkey[0].handler = key0_handler; // 中斷處理函數dev->irqkey[0].value = KEY0_VALUE; // 例如:1 表示 release(高電平),0 表示按下(低電平)/* 2. 中斷初始化 */for(i=0; i<KEY_NUM; i++){ret = request_irq(dev->irqkey[i].irqnum,dev->irqkey[i].handler,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,dev->irqkey[i].name,&key_irq); /* dev_id 傳入結構體指針 */if(ret < 0){printk("irq %d request failed! ret=%d\n", dev->irqkey[i].irqnum, ret);while (--i >= 0) {free_irq(dev->irqkey[i].irqnum, &key_irq);}goto fail_irq;}// tasklet_init(&dev->irqkey[i].tasklet, key_tasklet, (unsigned long)dev);INIT_WORK(&dev->irqkey[i].work, key_work);}return 0;fail_irq:
fail_gpio_req:for (i = 0; i < KEY_NUM; i++) {if (gpio_is_valid(dev->irqkey[i].gpio))gpio_free(dev->irqkey[i].gpio);}
fail_nd:return ret;
}/* 驅動入口 */
static int __init key_init(void){int ret = 0;/* 1. 注冊字符設備驅動 */key_irq.devid = 0;alloc_chrdev_region(&key_irq.devid, 0, DEV_CNT, DEV_NAME);key_irq.major = MAJOR(key_irq.devid);key_irq.minor = MINOR(key_irq.devid);/* 2. 初始化 cdev */key_irq.cdev.owner = THIS_MODULE;cdev_init(&key_irq.cdev, &key_fops);/* 3. 添加 cdev */ret = cdev_add(&key_irq.cdev, key_irq.devid, DEV_CNT);if (ret) {pr_err("cdev_add failed\n");goto fail_cdev;}/* 4. 創建類 */key_irq.class = class_create(THIS_MODULE, DEV_NAME);if(IS_ERR(key_irq.class)){ret = PTR_ERR(key_irq.class);goto fail_class;}/* 5. 創建設備 */key_irq.device = device_create(key_irq.class, NULL, key_irq.devid, NULL, DEV_NAME);if(IS_ERR(key_irq.device)){ret = PTR_ERR(key_irq.device);goto fail_device;}/* 6. 初始化IO*/ret = keyio_init(&key_irq);if(ret < 0){goto fail_keyinit;}/* 7. 初始化定時器 */init_timer(&key_irq.timer);key_irq.timer.function = timer_func;key_irq.timer.data = (unsigned long)&key_irq; /* 初始化 timer.data 指向設備結構體 *//* 8. 初始化原子變量 */atomic_set(&key_irq.keyvalue, INVAKEY);atomic_set(&key_irq.releasekey, 0);return 0;fail_keyinit:device_destroy(key_irq.class, key_irq.devid);
fail_device:class_destroy(key_irq.class);
fail_class:cdev_del(&key_irq.cdev);
fail_cdev:unregister_chrdev_region(key_irq.devid, DEV_CNT);return ret;
}/* 驅動出口 */
static void __exit key_exit(void){int i=0;/* 釋放中斷 */for(i=0; i<KEY_NUM; i++){free_irq(key_irq.irqkey[i].irqnum, &key_irq);}/* 釋放io */for(i=0; i<KEY_NUM; i++){if (gpio_is_valid(key_irq.irqkey[i].gpio))gpio_free(key_irq.irqkey[i].gpio);}/* 刪除定時器 */del_timer_sync(&key_irq.timer);/* 注銷字符設備驅動 */cdev_del(&key_irq.cdev);unregister_chrdev_region(key_irq.devid, DEV_CNT);device_destroy(key_irq.class, key_irq.devid);class_destroy(key_irq.class);
}module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
連著兩個key_work應該就是發生了抖動,不過都被消抖攔住了↓
6. 附錄
視頻第13.7講的后半部分講了一下container_of函數的使用方法。
????????container_of通過指向結構體中某個成員的指針,反推出整個結構體的指針,并返回任意成員的地址:
// 需要#include <linux/kernel.h>container_of(ptr, type, member)
// prt: 已知的成員指針
// type: 結構體的定義類型
// member: 要獲得地址的成員名
// return: member的地址
如本次實驗中的結構體定義:
/* 按鍵結構體 */
struct irq_keydesc{int gpio; /* io號 */int irqnum; /* 中斷號 */unsigned char value; /* 鍵值 (默認/空閑電平,比如 1) */char name[10]; /* 名字 */irqreturn_t (*handler) (int, void *); /* 中斷處理函數*/// struct tasklet_struct tasklet;struct work_struct work;
};/* 中斷設備結構體 */
struct irq{dev_t devid; // 設備號int major; // 主設備號int minor; // 次struct cdev cdev; // cdevstruct device *device; // 設備struct class *class; // 類struct device_node *nd; // 節點struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 個按鍵struct timer_list timer;// 定時器atomic_t releasekey; // 0表示按鍵釋放,1表示按鍵按下atomic_t keyvalue; // 第八位表示數據是否被讀取:每完成一次按鍵釋放,第八位就會置1,用戶態調用.read時會將其置0,表示數據被讀取// 其余七位表示按鍵的值
};
調用該函數就能直接獲得任意成員地址:
// 工作隊列處理函數
static void key_work(struct work_struct *work_){struct irq* dev = container_of(work_, struct irq, work); // 獲得工作隊列work成員的地址
}