嵌入式Linux驅動開發:i.MX6ULL按鍵中斷驅動(非阻塞IO)
概述
本文檔詳細介紹了在i.MX6ULL開發板上實現按鍵中斷驅動的完整過程。該驅動程序實現了非阻塞IO操作,允許用戶空間應用程序通過poll
系統調用高效地監控按鍵狀態變化,而無需進行忙等待。本文檔結合了提供的源代碼和設備樹文件,詳細解釋了驅動程序的各個組成部分及其工作原理。
源碼倉庫
- 倉庫地址: https://gitee.com/dream-cometrue/linux_driver_imx6ull
理論基礎
1. 非阻塞IO (Non-blocking I/O)
在傳統的阻塞IO模型中,當應用程序調用read
等系統調用時,如果數據不可用,進程會進入睡眠狀態,直到數據就緒。這在某些場景下是高效的,但在需要同時監控多個文件描述符或進行其他工作的場景下,會導致資源浪費。
非阻塞IO通過在open
系統調用時指定O_NONBLOCK
標志來實現。在這種模式下,如果read
調用時沒有數據可讀,系統調用會立即返回一個-EAGAIN
錯誤,而不是讓進程睡眠。這允許應用程序立即處理其他任務,或者使用poll
或select
系統調用來監控多個文件描述符的狀態。
2. poll
系統調用
poll
系統調用是實現非阻塞IO的核心機制。它允許應用程序在一個系統調用中監控多個文件描述符的讀、寫和異常事件。poll
會阻塞直到任何一個被監控的文件描述符就緒,或者超時。
在驅動程序中,poll
操作通過file_operations
結構體中的.poll
成員函數實現。驅動程序需要調用poll_wait
函數將當前進程添加到一個等待隊列中,并返回一個描述當前文件描述符狀態的掩碼。
3. 等待隊列 (Wait Queue)
等待隊列是Linux內核中用于進程同步的機制。它允許一個或多個進程在某個條件滿足之前進入睡眠狀態。當條件滿足時,另一個進程或中斷處理程序可以喚醒等待隊列中的所有進程。
在本驅動程序中,我們使用等待隊列來實現poll
功能。當應用程序調用poll
時,驅動程序會將當前進程添加到等待隊列中。當按鍵狀態發生變化時,中斷處理程序會通過定時器喚醒等待隊列中的進程。
4. 定時器 (Timer)
定時器用于在指定的時間后執行一段代碼。在本驅動程序中,定時器用于實現按鍵消抖。當按鍵中斷發生時,我們啟動一個20ms的定時器。在定時器超時后,我們讀取按鍵的實際狀態,以避免由于機械按鍵的抖動導致的誤觸發。
5. 原子變量 (Atomic Variables)
原子變量是內核中用于在多處理器系統中實現無鎖同步的機制。對原子變量的操作是不可分割的,保證了在并發訪問時的數據一致性。在本驅動程序中,我們使用原子變量keyvalue
和release
來在中斷上下文和進程上下文之間安全地傳遞按鍵值和釋放狀態。
設備樹 (Device Tree)
設備樹文件imx6ull-alientek-emmc.dts
定義了開發板上的硬件配置。與本驅動程序相關的部分是/key
節點:
key{compatible = "alientek,key";pinctrl-names = "default";pinctrl-0 = <&pinctrl_key>;states = "okay";key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;interrupt-parent = <&gpio1>;interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
};
compatible = "alientek,key"
: 指定了該節點的兼容性字符串,驅動程序會根據這個字符串來匹配設備。pinctrl-0 = <&pinctrl_key>
: 引用了iomuxc
節點中的pinctrl_key
子節點,用于配置GPIO1_IO18引腳的復用功能和電氣特性。key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>
: 指定了按鍵連接到gpio1
控制器的第18號引腳,且為高電平有效。interrupt-parent = <&gpio1>
: 指定了中斷控制器為gpio1
。interrupts = <18 IRQ_TYPE_EDGE_BOTH>
: 指定了中斷號為18,觸發類型為上升沿和下降沿(雙邊沿觸發)。
在驅動程序中,我們通過of_find_node_by_path("/key")
找到這個節點,并通過of_get_named_gpio
函數獲取按鍵的GPIO號。
驅動程序分析
1. 數據結構
struct key_desc
該結構體用于描述一個按鍵設備:
struct key_desc
{char name[10]; // 按鍵名稱int gpio; // GPIO號int irqnum; // 中斷號unsigned char value; // 按鍵值irqreturn_t (*handler)(int, void *); // 中斷處理函數
};
struct imx6uirq_dev
該結構體是驅動程序的核心,包含了所有必要的狀態信息:
struct imx6uirq_dev
{dev_t devid; // 設備號int major; // 主設備號int minor; // 次設備號struct cdev cdev; // 字符設備struct class *class; // 設備類struct device *device; // 設備struct device_node *key_nd; // 設備樹節點struct key_desc key[KEY_NUM]; // 按鍵描述數組struct timer_list timer; // 定時器atomic_t keyvalue; // 按鍵值(原子變量)atomic_t release; // 釋放狀態(原子變量)wait_queue_head_t r_wait; // 讀等待隊列
};
2. 字符設備操作
imx6uirq_open
該函數在應用程序打開設備文件時被調用。它將private_data
指針指向imx6uirq
全局變量,以便后續操作可以訪問驅動程序的狀態。
static int imx6uirq_open(struct inode *inode, struct file *filp)
{filp->private_data = &imx6uirq;return 0;
}
imx6uirq_release
該函數在應用程序關閉設備文件時被調用。在本驅動程序中,它不執行任何特定操作。
static int imx6uirq_release(struct inode *inode, struct file *filp)
{return 0;
}
imx6uirq_read
該函數實現了read
系統調用。它根據O_NONBLOCK
標志決定是阻塞還是非阻塞讀取。
- 如果指定了
O_NONBLOCK
標志,且沒有按鍵釋放事件,則立即返回-EAGAIN
。 - 否則,調用
wait_event_interruptible
將進程添加到等待隊列中,直到有按鍵釋放事件發生。 - 一旦有事件發生,將按鍵值復制到用戶空間緩沖區,并重置釋放狀態。
ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{struct imx6uirq_dev *dev = filp->private_data;u8 keyvalue, release;int ret = 0;if (filp->f_flags & O_NONBLOCK){if (atomic_read(&dev->release) == 0){return -EAGAIN;}}else{wait_event_interruptible(dev->r_wait, atomic_read(&dev->release));}keyvalue = atomic_read(&dev->keyvalue);release = atomic_read(&dev->release);if (release){ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));if (ret){ret = -EFAULT;goto fail_copy_user;}atomic_set(&dev->release, 0);}else{ret = -EAGAIN;goto fail_key_unrelease;}return sizeof(keyvalue);
}
imx6uirq_poll
該函數實現了poll
系統調用。它調用poll_wait
將當前進程添加到r_wait
等待隊列中,并檢查release
原子變量。如果release
為真,則返回POLLIN | POLLRDNORM
,表示文件描述符可讀。
static unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
{int mask = 0;struct imx6uirq_dev *dev = filp->private_data;poll_wait(filp, &dev->r_wait, wait);if (atomic_read(&dev->release)){mask = POLLIN | POLLRDNORM;}return mask;
}
3. 中斷處理
key0_handler
該函數是按鍵中斷的處理程序。它在按鍵狀態發生變化時被調用。由于中斷處理程序執行在中斷上下文中,不能進行睡眠操作,因此我們不能在這里直接讀取GPIO狀態。相反,我們啟動一個定時器,在定時器的回調函數中讀取GPIO狀態。
static irqreturn_t key0_handler(int irq, void *filp)
{struct imx6uirq_dev *dev = filp;dev->timer.data = (volatile long)filp;mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20));return IRQ_HANDLED;
}
timer_func
該函數是定時器的回調函數。它在定時器超時后被調用。在本函數中,我們讀取按鍵的GPIO狀態:
- 如果按鍵被按下(GPIO為低電平),則設置
keyvalue
為KEY0VALUE
。 - 如果按鍵被釋放(GPIO為高電平),則設置
release
為1,并喚醒等待隊列中的所有進程。
static void timer_func(unsigned long arg)
{struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;int value = 0;value = gpio_get_value(dev->key[0].gpio);if (value == 0){atomic_set(&dev->keyvalue, dev->key[0].value);}else{atomic_set(&dev->release, 1);}if (atomic_read(&dev->release)){wake_up_interruptible(&dev->r_wait);}
}
4. 驅動程序初始化和退出
imx6uirq_init
該函數在模塊加載時被調用。它完成了以下初始化工作:
- 分配設備號(動態或靜態)。
- 初始化字符設備,并將其添加到系統中。
- 創建設備類和設備文件。
- 調用
key_init1
函數初始化按鍵。 - 初始化定時器。
- 初始化原子變量和等待隊列。
static int __init imx6uirq_init(void)
{// ... (設備號分配、字符設備注冊、設備類和設備創建)ret = key_init1(&imx6uirq);if (ret < 0){goto fail_key_init;}timer1_init(&imx6uirq);atomic_set(&imx6uirq.keyvalue, KEYINVA);atomic_set(&imx6uirq.release, 0);init_waitqueue_head(&imx6uirq.r_wait);return 0;
}
key_init1
該函數負責初始化按鍵硬件。它通過設備樹API獲取按鍵的GPIO號,并請求GPIO、配置為輸入模式,然后獲取中斷號并注冊中斷處理程序。
int key_init1(struct imx6uirq_dev *dev)
{u8 ret = 0, i = 0;dev->key_nd = of_find_node_by_path("/key");if (dev->key_nd == NULL){ret = -EFAULT;goto fail_find_nd;}dev->key[i].handler = key0_handler;dev->key[i].value = KEY0VALUE;for (i = 0; i < KEY_NUM; i++){dev->key[i].gpio = of_get_named_gpio(dev->key_nd, "key-gpios", i);if (dev->key[i].gpio < 0){ret = -EFAULT;goto fail_get_gpio;}memset(dev->key[i].name, 0, sizeof(dev->key[i].name));sprintf(dev->key[i].name, "KEY%d", i);ret = gpio_request(dev->key[i].gpio, dev->key[i].name);if (ret){ret = -EFAULT;goto fail_gpio_req;}ret = gpio_direction_input(dev->key[i].gpio);if (ret){ret = -EFAULT;goto fail_gpio_dir;}dev->key[i].irqnum = gpio_to_irq(dev->key[i].gpio);ret = request_irq(dev->key[i].irqnum, dev->key[i].handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, dev->key[i].name, &imx6uirq);if (ret){ret = -EFAULT;goto fail_req_irq;}}return 0;
}
timer1_init
該函數初始化定時器,設置其回調函數為timer_func
。
void timer1_init(struct imx6uirq_dev *dev)
{init_timer(&dev->timer);dev->timer.function = timer_func;
}
imx6uirq_exit
該函數在模塊卸載時被調用。它負責釋放所有分配的資源,包括中斷、GPIO、設備文件、字符設備、設備類和設備號。
static void __exit imx6uirq_exit(void)
{u8 i = 0;for (i = 0; i < KEY_NUM; i++){free_irq(imx6uirq.key[i].irqnum, &imx6uirq);gpio_free(imx6uirq.key[i].gpio);}del_timer(&imx6uirq.timer);device_destroy(imx6uirq.class, imx6uirq.devid);class_destroy(imx6uirq.class);cdev_del(&imx6uirq.cdev);unregister_chrdev(imx6uirq.major, IMX6UIRQ_NAME);
}
用戶空間應用程序
用戶空間應用程序imx6uirqAPP.c
演示了如何使用poll
系統調用來監控按鍵狀態。
1. poll
的使用
應用程序創建一個pollfd
結構體,指定要監控的文件描述符、感興趣的事件(POLLIN
)和返回的事件。然后調用poll
函數,指定超時時間為500毫秒。
struct pollfd fds;
fds.fd = fd;
fds.events = POLLIN;
ret = poll(&fds, 1, 500);
2. 事件處理
poll
返回后,應用程序檢查revents
字段以確定發生了什么事件。如果POLLIN
事件發生,則調用read
函數讀取按鍵值。
if (ret > 0)
{if (fds.revents | POLLIN){unsigned char ch = 0;int ret = read(fd, &ch, sizeof(ch));if (ret >= 0){if (ch == KEY0VALUE){printf("User: key is pressing, ret is: %d\r\n", ret);}}}
}