一、簡介
和pincttl、gpio等子系統一樣,INPUT子系統專門用於處理一類事件——輸入事件。對於輸入設備(包括按鍵、觸摸屏等),用户只需要上報事件、傳入信息,由INPUT子系統來處理事件。
《開發指南》原話:
input子系統分為input驅動層、input核心層、input事件處理層,最終給用户空間提供可訪問的設備節點,input子系統框架如圖58.1.1.1所示:
我們編寫驅動程序時只需要關注中間的驅動層、核心層和事件層。這三個層的分工如下:
驅動層:輸入設備的具體驅動程序,比如按鍵驅動程序,向內核層報告輸入內容。
核心層:承上啓下,為驅動層提供輸入設備註冊和操作接口。通知事件層對輸入事件進行處理。
事件層:主要和用户空間進行交互。
二、驅動函數
2.1 input類
打開文件/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/input/input.c,能看到很多熟悉的東西:
struct class input_class = { // 定義一個input類
.name = "input",
.devnode = input_devnode,
};
…………
// 驅動入口
static int __init input_init(void)
{
int err;
err = class_register(&input_class); // 註冊類
…………
// 註冊字符設備驅動
err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0), // 設備號INPUT_MAJOR=13
INPUT_MAX_CHAR_DEVICES, "input"); // 設備數量INPUT_MAX_CHAR_DEVICES=1024
…………
return 0;
…………
}
// 驅動出口
static void __exit input_exit(void)
{
input_proc_exit();
unregister_chrdev_region(MKDEV(INPUT_MAJOR, 0), // 註銷字符設備驅動
INPUT_MAX_CHAR_DEVICES);
class_unregister(&input_class); // 註銷類
}
subsys_initcall(input_init);
module_exit(input_exit);
input.c中的代碼和之前幾次實驗寫的代碼類似,都有註冊設備、class等。因此在使用input子系統處理輸入設備時,就不需要再進行以上步驟,只需要向系統註冊一個input設備即可。
2.2 input_dev結構體
使用input_dev結構體來表示一個input設備,定義如下:
// 定義在include/linux/input.h
// 這裏我直接搬了《開發指南》的示例代碼58.1.2.2中的註釋
// 註釋後面的“對應XXX部分”指的是對應文件include/uapi/linux/input.h中的對應註釋,直接ctrl+f搜索即可
struct input_dev {
const char *name;
const char *phys;
const char *uniq;
struct input_id id;
unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; // 事件類型的位圖 對應Event types部分
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; // 按鍵值的位圖 對應Keys and buttons部分
unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; // 相對座標的位圖 對應Relative axes部分
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; // 絕對座標的位圖 對應Absolute axes部分
unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; // 雜項事件的位圖 對應Misc events部分
unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; // LED相關的位圖 對應LEDs部分
unsigned long sndbit[BITS_TO_LONGS(SND_CNT)]; // sound有關的位圖 對應Sounds部分
unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; // 壓力反饋的位圖 對應Values describing the status of a force-feedback effect部分
unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; // 開關狀態的位圖 對應Switch部分
…………
};
以事件類型evbit為例,其取值可以為以下內容:
// 從include/uapi/linux/input.h的174行起
/* 這裏的註釋來自《開發指南》的示例代碼58.1.2.3
* Event types
*/
#define EV_SYN 0x00 /* 同步事件 */
#define EV_KEY 0x01 /* 按鍵事件 */
#define EV_REL 0x02 /* 相對座標事件 如鼠標的座標都是計算相對上一時刻的位移 */
#define EV_ABS 0x03 /* 絕對座標事件 如觸摸屏,觸摸的是屏幕座標系中的某個位置 */
#define EV_MSC 0x04 /* 雜項(其他)事件 */
#define EV_SW 0x05 /* 開關事件 */
#define EV_LED 0x11 /* LED */
#define EV_SND 0x12 /* sound(聲音) */
#define EV_REP 0x14 /* 重複事件 如長按鍵盤上的鍵,會打出很長一串*/
#define EV_FF 0x15 /* 壓力事件 */
#define EV_PWR 0x16 /* 電源事件 */
#define EV_FF_STATUS 0x17 /* 壓力狀態事件 */
2.3 申請/釋放 input_dev
// 申請:
struct input_dev *input_allocate_device(void)
// return:申請到的input_dev
// 這只是一個空的內核對象,需要後面註冊時填入參數
// 釋放:
void input_free_device(struct input_dev *dev)
// dev:需要釋放的input_dev
2.4 註冊/註銷 input_dev
申請到input_dev以後,需要初始化事件類型evbit和事件值(如本次使用按鍵,就需要初始化keybit)。經過註冊,/sys/class/input/就能找到對應的設備了,同時/dev/input/下也會出現“eventX”(X=0….n),這個/dev/input/eventX就是對應的input設備文件。
// 註冊:
int input_register_device(struct input_dev *dev)
// dev: 要註冊的input_dev
// return:0則成功,負值則失敗
// 註銷:
void input_unregister_device(struct input_dev *dev)
// dev:要註銷的input_dev
2.5 事件上報——input_event函數
事件發生後 需要通知給內核,同時還需要將事件值一併上報,如發生按鍵事件時按鍵的值等等。
// 定義在drivers/input/input.c
void input_event(struct input_dev *dev, // 需要上報的input_dev
unsigned int type, // 上報的事件類型evbit,具體值詳見2.2
unsigned int code, // 事件碼
int value); // 事件值
linux針對一些具體事件提供包裝好的上報函數,但本質還是在調用input_event:
// 定義在include/linux/input.h
static inline void input_report_key(struct input_dev *dev, unsigned int code, int value)
static inline void input_report_rel(struct input_dev *dev, unsigned int code, int value)
static inline void input_report_abs(struct input_dev *dev, unsigned int code, int value)
static inline void input_report_ff_status(struct input_dev *dev, unsigned int code, int value)
static inline void input_report_switch(struct input_dev *dev, unsigned int code, int value)
上報事件後還需要用input_sync函數來告訴Linux內核上報結束。input_sync函數本質是上報一個同步事件EV_SYN:(詳見3.8)
static inline void input_sync(struct input_dev *dev){ // dev:要上報同步事件的input_dev
input_event(dev, EV_SYN, SYN_REPORT, 0);
}
綜上,事件上報的示例代碼:(來自《開發指南》示例代碼58.1.2.7)
/* 用於按鍵消抖的定時器服務函數 */
void timer_function(unsigned long arg){
unsigned char value;
value = gpio_get_value(keydesc->gpio); /* 讀取IO值 */
if(value == 0){ /* 按下按鍵 */
/* 上報按鍵值=1 */
input_report_key(inputdev, KEY_0, 1); /* 最後一個參數1,按下 */
input_sync(inputdev); /* 同步事件 */
} else { /* 按鍵鬆開 */
/* 上報按鍵值=0 */
input_report_key(inputdev, KEY_0, 0); /* 最後一個參數0,鬆開 */
input_sync(inputdev); /* 同步事件 */
}
}
2.6 input_event結構體
各種事件(如按鍵,觸摸屏,鼠標等等)都是使用input_event()來上報,這就需要一個統一的結構體,能表示所有輸入事件。linux內核使用input_event結構體表示所有輸入事件:
// 定義在include/uapi/linux/input.h
struct input_event {
struct timeval time;
__u16 type; // 事件類型,如按鍵事件EV_KEY unsigned short 16位
__u16 code; // 事件碼,如KEY_0 unsigned short 16位
__s32 value; // 具體的值,如按鍵的狀態0/1 int 32位
};
結構體中的type、code、value即為input_event上報的type、code、value。
input_event結構體中,成員timeval的定義為:
// 定義在time.h
struct timeval
{
__kernel_time_t tv_sec; // Seconds. 秒 long類型 32位
__kernel_suseconds_t tv_usec; // Microseconds. 微秒 long類型 32位
};
對input_event數據的具體分析可見3.5。
三、實驗
本次實驗在中斷代碼基礎上修改而來。
3.1 文件結構
21_INPUT (工作區)
├── .vscode
│ ├── c_cpp_properties.json
│ └── settings.json
├── 21_input.code-workspace
├── Makefile
├── keyinput.c
└── keyinputAPP.c
3.2 Makefile
CFLAGS_MODULE += -w
KERNELDIR := /.../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 := keyinput.o # 編譯文件
build: kernel_modules # 編譯模塊
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
3.3 設備樹
依照中斷這一節的寫法,在arch/arm/boot/dts/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>;
};
然後編譯並放置到tftp路徑下,並重啓開發板加載設備樹:
make dtbs # 編譯
cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /.../tftpboot/ # 編譯出的dtb文件放置到tftp路徑下
重啓開發板
3.4 驅動代碼
#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/input.h>
#define KEYINPUT_CNT 1 /* 設備號數量 */
#define KEYINPUT_NAME "keyinput" /* 設備名 */
#define KEY_NUM 1 /* 按鍵數量 */
#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;
};
/* keyinput設備結構體 */
struct keyinput_dev_struct{
struct device_node *nd; // 節點
struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 個按鍵
struct timer_list timer;// 定時器
struct input_dev *inputdev; // 輸入設備結構體input_dev
};
static struct keyinput_dev_struct keyinput_dev;
/* 中斷處理函數 */
static irqreturn_t key0_handler(int irq, void *dev_id){
struct keyinput_dev_struct *dev = (struct keyinput_dev_struct*)dev_id;
int current_level; // 保存中斷時的電平
current_level = gpio_get_value(dev->irqkey[0].gpio);
tasklet_schedule(&dev->irqkey[0].tasklet); // 調度tasklet處理後續邏輯,避免中斷耗時
return IRQ_HANDLED;
}
/* 定時器處理函數 */
static void timer_func(unsigned long arg){
int value = 0;
struct keyinput_dev_struct* dev = (struct keyinput_dev_struct*)arg;
value = gpio_get_value(dev->irqkey[0].gpio);
if(value == 0){ // 按下
input_event(dev->inputdev, EV_KEY, KEY_0, 1); // 上報 按鍵事件EV_KEY,按鍵為KEY_0,按鍵值為1
input_sync(dev->inputdev);
}else if(value == 1){ // 釋放
input_event(dev->inputdev, EV_KEY, KEY_0, 0); // 上報 按鍵事件EV_KEY,按鍵為KEY_0,按鍵值為0
input_sync(dev->inputdev);
}
}
/* tasklet處理函數 */
static void key_tasklet(unsigned long data){
struct keyinput_dev_struct* dev = (struct keyinput_dev_struct*)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 keyinput_dev_struct *dev){
int ret = 0;
int i = 0;
/* 1. 按鍵初始化 */
dev->nd = of_find_node_by_path("/key"); // 獲取設備節點
if(dev->nd == NULL){
printk("獲取設備節點of_find_node_by_path failed!!\r\n");
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); // 獲取設備樹中 gpio
if (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); // 申請gpio
if (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; // 中斷處理函數
/* 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,
&keyinput_dev); /* dev_id 傳入結構體指針 */
if(ret < 0){
printk("irq %d request failed! ret=%d\r\n", dev->irqkey[i].irqnum, ret);
while (--i >= 0) {
free_irq(dev->irqkey[i].irqnum, &keyinput_dev);
}
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;
/* 初始化IO*/
ret = keyio_init(&keyinput_dev);
if(ret < 0){
goto fail_keyinit;
}
/* 初始化定時器 */
init_timer(&keyinput_dev.timer);
keyinput_dev.timer.function = timer_func;
keyinput_dev.timer.data = (unsigned long)&keyinput_dev; /* 初始化 timer.data 指向設備結構體 */
keyinput_dev.inputdev = input_allocate_device(); // 申請input_dev
if(keyinput_dev.inputdev == NULL){
printk("申請input_dev失敗!\r\n");
ret = -EINVAL;
goto fail_keyinit;
}
keyinput_dev.inputdev->name = KEYINPUT_NAME;
__set_bit(EV_KEY, keyinput_dev.inputdev->evbit); // 按鍵事件
__set_bit(EV_REP, keyinput_dev.inputdev->evbit); // 重複事件
__set_bit(KEY_0, keyinput_dev.inputdev->keybit); // 另按鍵對應KEY_0
ret = input_register_device(keyinput_dev.inputdev); // 註冊input_dev
if(ret){
goto fail_input_register;
}
return 0;
fail_input_register:
input_free_device(keyinput_dev.inputdev); // 釋放input_dev
fail_keyinit:
return ret;
}
/* 驅動出口 */
static void __exit key_exit(void){
int i=0;
/* 釋放中斷 */
for(i=0; i<KEY_NUM; i++){
free_irq(keyinput_dev.irqkey[i].irqnum, &keyinput_dev);
}
/* 釋放io */
for(i=0; i<KEY_NUM; i++){
if (gpio_is_valid(keyinput_dev.irqkey[i].gpio))
gpio_free(keyinput_dev.irqkey[i].gpio);
}
/* 刪除定時器 */
del_timer_sync(&keyinput_dev.timer);
/* 註銷input_dev */
input_unregister_device(keyinput_dev.inputdev);
input_free_device(keyinput_dev.inputdev);
}
module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
3.5 input_event上報數據格式
# VSCODE終端
make
sudo cp keyinput.ko /.../nfs/rootfs/lib/modules/4.1.15/
# 串口
ls /dev/input/ # 此時應當輸出:event0 mice
depmod
modprobe keyinput.ko # 此時應有輸出:input: keyinput as /devices/virtual/input/input1
ls /dev/input/ # 此時應當輸出:event0 event1 mice
hexdump /dev/input/event1 # 查看event1的原始數據。按下和鬆開都會輸出兩行數據,長按會輸出連續的數據
這些輸出的數據就是上面2.6提到的input_event的內容。input_event的成員從前到後依次為:
秒tv_sec(32位),微秒tv_usec(32位),事件類型type(16位),事件碼code(16位),值value(32位)。因此上面這張圖的內容可以翻譯為:
編號 秒 微秒 事件類型 事件碼 值
key_tasklet(此時按下按鍵)
0000000 b638 0002 751e 0009 0001 000b 0001 0000
0000010 b638 0002 751e 0009 0000 0000 0000 0000
key_tasklet(此時釋放按鍵)
0000020 b638 0002 49ee 000b 0001 000b 0000 0000
0000030 b638 0002 49ee 000b 0000 0000 0000 0000
key_tasklet(按下)
0000040 b639 0002 e139 0004 0001 000b 0001 0000
0000050 b639 0002 e139 0004 0000 0000 0000 0000
key_tasklet(釋放)
0000060 b639 0002 b1c0 0008 0001 000b 0002 0000
0000070 b639 0002 b1c0 0008 0000 0000 0001 0000
key_tasklet(開始長按)
0000080 b639 0002 4e01 0009 0001 000b 0002 0000
0000090 b639 0002 4e01 0009 0000 0000 0001 0000
00000a0 b639 0002 ea40 0009 0001 000b 0002 0000
00000b0 b639 0002 ea40 0009 0000 0000 0001 0000
00000c0 b639 0002 8684 000a 0001 000b 0002 0000
…………
事件類型:0001為按鍵事件EV_KEY;0000為同步事件EV_SYN
事件碼: 000b即為十進制11,為KEY_0;0000為KEY_RESERVED,表示佔位/無效(定義在input.h)
值:即為上報的值,本次實驗中定義0表示釋放、1表示按下。
看彈幕在爭大小端的事。查了一下,x86/ARM都是小端存儲,只是打印方式的問題導致低位在前高位在後。
每次事件上報後面都會跟一個同步事件EV_SYN,具體分析在3.8中。
3.6 應用程序代碼
#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>
#include <linux/input.h>
#define LEDOFF 0
#define LEDON 1
/*
* @description : main主程序
* @param - argc : argv數組元素個數
* @param - argv : 具體參數
* @return : 0 成功; else失敗
* 調用 ./keyintAPP /dev/input/event1
*/
static struct input_event inputevent;
int main(int argc, char *argv[]){
if(argc != 2){
printf("Error Usage!\r\n");
return -1;
}
int fd, err;
char *filename;
filename = argv[1];
fd = open(filename, O_RDWR);
if(fd <0){
printf("file %s open failed!\r\n",filename);
return -1;
}
while(1){
err = read(fd, &inputevent, sizeof(inputevent));
if(err > 0){ // 讀取成功
switch(inputevent.type){
case EV_KEY:
if(inputevent.code < BTN_MISC){ // 在input.h中可以看到,從0到BTN_MISC以前的編號都是KEY
printf("=KEY%d %s=\r\n",inputevent.code, inputevent.value?"press":"release");
} else {
// 可以根據自己的code值進行編寫
}
break;
case EV_SYN:
printf(" SYN EVENT\r\n");
break;
default: // 剩下的懶得寫了
break;
}
} else {
printf("read failed!\r\n");
}
}
return 0;
}
3.7 測試
# VSCODE終端
make
arm-linux-gnueabihf-gcc keyinputAPP.c -o keyinputAPP
sudo cp keyinputAPP keyinput.ko /.../nfs/rootfs/lib/modules/4.1.15/
# 串口
depmod
modprobe keyinput.ko # 此時應有輸出:input: keyinput as /devices/virtual/input/input1
./keyinputAPP /dev/input/event1 # 按下按鍵應有對應的輸出
3.8 各種問題
input_sync的問題:
如2.5中所述,input_event上報事件後還要跟一個input_sync同步事件。input_sync定義如下:
static inline void input_sync(struct input_dev *dev)
{
input_event(dev, EV_SYN, SYN_REPORT, 0);
}
// 這裏的EV_SYN、SYN_REPORT都是0(定義在input.h),也就是3.5中看到的事件碼和值都是0的SYN事件
input_event()只是把事件寫入input緩衝,而input_sync()會產生一個同步事件EV_SYN/ SYN_REPORT,此時就會將緩衝裏的事件送到用户態。
可以試一下,把按下或者釋放後面的input_sync去掉一個、保留一個,會發現只有在執行input_sync時用户態才read到事件。
CPU佔用問題:
讓應用程序後台運行,用ps查看會發現cpu佔用很低。用户態進程是阻塞睡眠的。
長按的問題:
當按下按鍵時,邊沿觸發了中斷。之後長按,因為沒有邊沿,因此不會再觸發中斷。但因為打開了重複事件EV_REP,input子系統在長按時會按照一定頻率不斷產生EV_KEY事件。