• Linux设备驱动之gpio-keys


    Linux设备驱动之gpio-keys

    前两个章节介绍了Linux字符设备和platform设备的注册,他们都是比较基础的,让大家理解Linux内核的设备驱动是如何注册、使用的。但在工作中,个人认为完全手写一个字符设备驱动的机会比较少,更多的都是基于前人的代码修修补补过三年。在内核驱动中,更多的会基于platform设备进行具体设备驱动的注册与使用,下面以内核原生的gpio-keys为例,向大家介绍一个简单的按键驱动是如何配置、工作的。

    dts配置

    前面有简单的介绍,设备树用于描述板端硬件信息,配合驱动进行使用的,Linux内核原生的gpio-keys驱动支持的dts如下:

    	gpio-keys {
    		compatible = "gpio-keys";	/* 用于匹配内核gpio-keys驱动 */
            autorepeat;					/* 标记是否自动重复该按键,想想常按的情况 */
    
            up {
                label = "GPIO Key UP";	/* 按键的标签 */
                linux,code = <103>;		/* 按键的键值,理解为键盘上的字母类 */
                gpios = <&gpio1 0 1>;	/* 使用的是哪个gpio */
            };
    
            down {
                label = "GPIO Key DOWN";
                linux,code = <108>;
                interrupts = <1 IRQ_TYPE_LEVEL_HIGH 7>;	/* 中断配置 */
            };
        };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    上面介绍到的dts配置,最后在驱动端都会进行解析,使用,那么驱动又具体是如何使用的呢,继续。

    gpio-keys的platform驱动

    Linux内核基本都可以看到类似以下的代码:

    [drivers/input/keyboard/gpio_keys.c]
    
    /* 设备驱动匹配信息 */
    static const struct of_device_id gpio_keys_of_match[] = {
            { .compatible = "gpio-keys", },
            { },
    };
    MODULE_DEVICE_TABLE(of, gpio_keys_of_match);
    
    static struct platform_driver gpio_keys_device_driver = {
            .probe          = gpio_keys_probe,	/* platform设备驱动匹配了,则调用probe函数 */
            .shutdown       = gpio_keys_shutdown,
            .driver         = {
                    .name   = "gpio-keys",
                    .pm     = &gpio_keys_pm_ops,
                    .of_match_table = gpio_keys_of_match,
                    .dev_groups     = gpio_keys_groups,
            }
    };
    
    static int __init gpio_keys_init(void)
    {
        	/* 注册platform driver */
            return platform_driver_register(&gpio_keys_device_driver);
    }
    
    static void __exit gpio_keys_exit(void)
    {
            platform_driver_unregister(&gpio_keys_device_driver);
    }
    
    /* late_initcall与modeule_init的宏作用一样,
     * 都是将函数添加到驱动段,方便开机启动加载驱动,
     * late_initcall与modeule_init的差异是加载
     * 的优先级不一致
     */
    late_initcall(gpio_keys_init);
    module_exit(gpio_keys_exit);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    在platform和设备树章节已经介绍,当设备驱动匹配时,将会调用到probe函数。下面看看gpio-keys的probe是怎样获取dts信息的。PS:dts是怎么解析的,platform device是什么时候注册的,后面其他章节我们再进行介绍。

    /*
     * Handlers for alternative sources of platform_data
     */
    
    /*
     * Translate properties into platform_data
     */
    static struct gpio_keys_platform_data *
    gpio_keys_get_devtree_pdata(struct device *dev)
    {
            struct gpio_keys_platform_data *pdata;
            struct gpio_keys_button *button;
            struct fwnode_handle *child;
            int nbuttons;
    
        	/* 查看当前的gpio-keys节点有多少个子节点,每个节点代表一个按键 */
            nbuttons = device_get_child_node_count(dev);
            if (nbuttons == 0)
                    return ERR_PTR(-ENODEV);
    
            pdata = devm_kzalloc(dev,
                                 sizeof(*pdata) + nbuttons * sizeof(*button),
                                 GFP_KERNEL);
            if (!pdata)
                    return ERR_PTR(-ENOMEM);
    
            button = (struct gpio_keys_button *)(pdata + 1);
    
            pdata->buttons = button;
            pdata->nbuttons = nbuttons;
    
        	/* 是否自动重复 */
            pdata->rep = device_property_read_bool(dev, "autorepeat");
    
        	/* 按键的标签 */
            device_property_read_string(dev, "label", &pdata->name);
    
            device_for_each_child_node(dev, child) {
                    if (is_of_node(child))
                            button->irq =
                                    irq_of_parse_and_map(to_of_node(child), 0);
    
                	/* 按键码值 */
                    if (fwnode_property_read_u32(child, "linux,code",
                                                 &button->code)) {
                            dev_err(dev, "Button without keycode\n");
                            fwnode_handle_put(child);
                            return ERR_PTR(-EINVAL);
                    }
    
                    fwnode_property_read_string(child, "label", &button->desc);
    
                	/* 输入的类型,一般设置为KEY */
                    if (fwnode_property_read_u32(child, "linux,input-type",
                                                 &button->type))
                            button->type = EV_KEY;
    
                	/* 该按键是否设置为唤醒源 */
                    button->wakeup =
                            fwnode_property_read_bool(child, "wakeup-source") ||
                            /* legacy name */
                            fwnode_property_read_bool(child, "gpio-key,wakeup");
    
                	/* 唤醒的状态 */
                    fwnode_property_read_u32(child, "wakeup-event-action",
                                             &button->wakeup_event_action);
    
                	/* 是否可休眠 */
                    button->can_disable =
                            fwnode_property_read_bool(child, "linux,can-disable");
    
                	/* 按键去抖配置 */
                    if (fwnode_property_read_u32(child, "debounce-interval",
                                             &button->debounce_interval))
                            button->debounce_interval = 5;
    
                    button++;
            }
    
            return pdata;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    在上面的代码可以看到,就是一个解析dts的操作,gpio_keys_get_devtree_pdata()将会进行以下操作:

    1. 确认gpio-keys节点存在多少个子节点,每个子节点代表一种按键功能;
    2. 获取”autorepeat“字段的信息赋值到pdata->rep,这个变量决定着是否允许重复发送事件;
    3. 获取各button的中断号、事件代码code、label、输入的类型、唤醒等信息;

    之后驱动会依据这些信息进行按键驱动的适配以及操作。下面通过gpio-keys驱动学习Linux input子系统。

    input子系统

    Linux input子系统是linux内核用于管理各种输入设备的部分,内核将给用户导出一套固定的硬件无关的input API,供用户空间程序使用。
    input系统分为三块:input core、input drivers和event handles。数据传输从底层硬件到input driver,再经过input core到event handles,最后到达用户空间。

    input core

    input子系统的core代码主要是input.c,该文件集成模块,模块的注册函数实现如下:

    static int __init input_init(void)
    {
    	int err;
    
    	/* 注册input类 */
    	err = class_register(&input_class);
    	if (err) {
    		pr_err("unable to register input_dev class\n");
    		return err;
    	}
    
    	/* 在/proc创建 bus/input/devices handlers */
    	err = input_proc_init();
    	if (err)
    		goto fail1;
    
    	/* 注册input字符设备 */
    	err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0),
    				     INPUT_MAX_CHAR_DEVICES, "input");
    	if (err) {
    		pr_err("unable to register char major %d", INPUT_MAJOR);
    		goto fail2;
    	}
    
    	return 0;
    
     fail2:	input_proc_exit();
     fail1:	class_unregister(&input_class);
    	return err;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    在input.c是input core的核心文件,input core主要是承上启下,为input drivers提供输入设备注册和操作接口,如input_register_device()函数;通知event handles对事件进行处理;在/proc下产生相应的设备信息。input core将会负责将input drivers和event handles联通,具体是如何完成这个操作的呢,继续看input drivers和event handles。

    相关结构体

    [linux/input.h]
    /*
     * The event structure itself
     */
    
    struct input_event {
            struct timeval time;	/* 输入事件时间 */
            __u16 type;			/* 类型 */
            __u16 code;			/* 事件代码 [linux/input-event-codes.h] */
            __s32 value;			/* 事件值(当type为EV_KEY时,value:0表示按键抬起,1表示按键按下) */
    };
    
    [linux/input-event-codes.h]
    /*
     * Event types
     */
    
    #define EV_SYN                  0x00	/* 所有input设备都具备的同步事件,主要是与client同步事件队列 */
    #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	/* 音频事件 */
    #define EV_REP                  0x14	/* 用于指定自动重复事件 */
    #define EV_FF                   0x15	/* 用于初始化具有力反馈功能的设备并使该设备反馈 */
    #define EV_PWR                  0x16	/* 电源管理 */
    #define EV_FF_STATUS            0x17	/* 用于接收力反馈设备状态 */
    #define EV_MAX                  0x1f
    #define EV_CNT                  (EV_MAX+1)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    gpio_keys_probe()

    继续回到gpio_keys_probe()函数,从dts中获取到button信息之后,将会通过调用devm_input_allocate_device()([input.c])函数创建input_dev设备。接着是通过gpio_keys_setup_key()函数,为各个button申请相应的GPIO、中断资源等。最后,通过input_register_device()函数,注册input设备。至此,可以看到input drivers注册完成。

    input handle

    input handle的相关操作在evdev.c实现,该文件恰好自己构成一个模块,先分析模块的注册函数,注册函数只是调用了input_register_handler()([input.c]),该函数主要是为系统中的输入设备注册一个新的输入处理程序,并将其附加到与该处理程序兼容的所有输入设备上。该模块将在input dev注册时,将会通过input core完成与input dev完成相应的connect,当event发生时,又将会处理相应的event事件并上报。

    input core、input drivers和event handles三块的简单介绍如上,下面,我们将带着问题去阅读代码,进一步了解Linux input子系统。

    FAQ

    input core是如何知道,当前有多少输入设备,而这些输入设备又分别是支持什么类型的事件?

    在介绍上面的input drivers的时候就有提到,在driver的probe函数中,将会通过input_register_device()函数注册input device,下面来分析一下该函数的实现。

    int input_register_device(struct input_dev *dev)
    {
    	/* 检查input dev支持的事件类型,注册device等 */
    	...
    
    	error = mutex_lock_interruptible(&input_mutex);
    	if (error)
    		goto err_device_del;
    
    	/* 将该input device添加到input_dev_list链表 */
    	list_add_tail(&dev->node, &input_dev_list);
    
    	/* 在linux input子系统中,一个input device的输入事件
    	 * 将会发送到系统中所有的event handles,所以这里从保存
    	 * event handles的全局链表input_handler_list中,逐个获
    	 * 取event handles添加input device */
    	list_for_each_entry(handler, &input_handler_list, node)
    		input_attach_handler(dev, handler);
    
    	/* 当注册input device成功,将会通过该函数唤醒profs的poll线程 */
    	input_wakeup_procfs_readers();
    
    	mutex_unlock(&input_mutex);
    
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    然后我们再回到这个问题,input core是如何知道有多少的input devices,显然,是在通过input_register_device()注册input device的时候,同时会把input device添加到全局链表input_dev_list[input.c]中,这样,input core通过枚举input_dev_list链表,就可以得到相应的input device。
    还剩一个问题,这些input device支持的事件类型和事件代码,又是从哪里填充的呢?
    上面在介绍input device的时候,我们是以gpio-keys为例,同样的,在这里我们继续以它为例,也即gpio_keys.c。
    在input device的probe()函数中,如上面介绍,将会通过gpio_keys_get_devtree_pdata()函数从dts中获取button的相应信息,接着通过gpio_keys_setup_key()将上面获取得到的信息填充到input dev,比如通过input_set_capability()函数设置input dev支持的事件类型等。
    这样,linux内核就知道,当前的input dev支持什么事件type以及code。

    当发生输入事件时,信息又是如何传递到应用层(用户空间)?

    继续以gpio-keys作为input dev例子进行这个问题的解答。
    回到gpio_keys_setup_key()函数,在该函数中,获取gpio之后,将会申请相应的中断,同时设置中断函数,中断函数有两个,分别是gpio_keys_gpio_isr()和gpio_keys_irq_isr(),将会根据gpio的信息相应的选择其中一个,以gpio_keys_gpio_isr()中断函数,当相应的gpio中断信号到来时,系统将会调用该函数,而在该函数中,又将存在以下代码:

    static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
    {
    	...
    	mod_delayed_work(system_wq,
    			 &bdata->work,
    			 msecs_to_jiffies(bdata->software_debounce));
    	...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    同时的,在gpio_keys_setup_key()函数中,是这样初始化bdata->work,所以,在中断函数中,设置延迟一段时间执行gpio_keys_gpio_work_func()函数。

    	INIT_DELAYED_WORK(&bdata->work, gpio_keys_gpio_work_func);
    
    • 1

    而gpio_keys_gpio_work_func()函数也很简单,主要是通过gpio_keys_gpio_report_event()函数报告相应的事件信息。

    static void gpio_keys_gpio_work_func(struct work_struct *work)
    {
    	struct gpio_button_data *bdata =
    		container_of(work, struct gpio_button_data, work.work);
    
    	gpio_keys_gpio_report_event(bdata);
    
    	if (bdata->button->wakeup)
    		pm_relax(bdata->input->dev.parent);
    }
    
    static void gpio_keys_gpio_report_event(struct gpio_button_data *bdata)
    {
    	const struct gpio_keys_button *button = bdata->button;
    	struct input_dev *input = bdata->input;
    	unsigned int type = button->type ?: EV_KEY;
    	int state;
    
    	/* 获取gpio的状态,是高电平还是低电平 */
    	state = gpiod_get_value_cansleep(bdata->gpiod);
    	if (state < 0) {
    		dev_err(input->dev.parent,
    			"failed to get gpio state: %d\n", state);
    		return;
    	}
    
    	/* 通过input_event()函数报告input事件 */
    	if (type == EV_ABS) {
    		if (state)
    			input_event(input, type, button->code, button->value);
    	} else {
    		input_event(input, type, *bdata->code, state);
    	}
    	/* 同步事件 */
    	input_sync(input);
    }
    
    void input_event(struct input_dev *dev,
    		 unsigned int type, unsigned int code, int value)
    {
    	unsigned long flags;
    
    	/* 先检查input dev是否支持该type,这个dev->evbit是在调用input_set_capability()时设置的 */
    	if (is_event_supported(type, dev->evbit, EV_MAX)) {
    
    		spin_lock_irqsave(&dev->event_lock, flags);
    		input_handle_event(dev, type, code, value);
    		spin_unlock_irqrestore(&dev->event_lock, flags);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    而在input_handle_event()函数中,将会先通过input_get_disposition()函数回去事件的相应信息。

    #define INPUT_IGNORE_EVENT	0		/* 忽略该事件 */
    #define INPUT_PASS_TO_HANDLERS	1	/* input handles处理该事件 */
    #define INPUT_PASS_TO_DEVICE	2	/* input device处理该事件 */
    #define INPUT_SLOT		4
    #define INPUT_FLUSH		8
    #define INPUT_PASS_TO_ALL	(INPUT_PASS_TO_HANDLERS | INPUT_PASS_TO_DEVICE)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    gpio-keys中,事件是由handles处理,所以在input_handle_event()函数中,将会将事件的信息填充到input dev的vals数组。至此,回到gpio_keys_gpio_report_event()函数,将事件信息填充到input dev的相应结构体之后,最后将会调用input_sync(input)函数同步事件。

    static inline void input_sync(struct input_dev *dev)
    {
    	input_event(dev, EV_SYN, SYN_REPORT, 0);
    }
    
    • 1
    • 2
    • 3
    • 4

    通过input_sync()函数的代码我们可以知道,最终是发送EV_SYN事件,code为SYN_REPORT,这样的一个事件代码,将会在input_handle_event()函数中调用input_pass_values()函数,激发input handles处理事件。
    而在input_pass_values()函数中,重要的也是以下部分:

    static void input_pass_values(struct input_dev *dev,
    			      struct input_value *vals, unsigned int count)
    {
    	...
    	/* 一般的,handle会为空,所以执行else部分代码 */
    	handle = rcu_dereference(dev->grab);
    	if (handle) {
    		count = input_to_handler(handle, vals, count);
    	} else {
    		/* 从input dev的dev->h_list链表获取event handles,
    		 * 上面就有提到,一个input dev的事件,将会发送到所有的handles */
    		list_for_each_entry_rcu(handle, &dev->h_list, d_node)
    			if (handle->open) {
    				count = input_to_handler(handle, vals, count);
    				if (!count)
    					break;
    			}
    	}
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    代码跟踪到这里,又多了一个疑问:dev->h_list这个链表是什么时候填充的?
    在input_register_device()函数中,通过调用input_attach_handler()函数,将event handles添加到dev->h_list,函数调用流程如下:

    	input_register_device()
    		input_attach_handler()
    			handler->connect(handler, dev, id)
    				evdev_connect()
    					input_register_handle()
    						list_add_rcu(&handle->d_node, &dev->h_list)/list_add_tail_rcu(&handle->d_node, &dev->h_list)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    了解到dev->h_list链表的填充过程之后,继续回到input_pass_values()函数,在该函数中,将会针对enable的event handle调用input_to_handler()函数,而在input_to_handler()函数重要的是调用handler的events函数—evdev_events()。

    /*
     * Pass incoming events to all connected clients.
     */
    static void evdev_events(struct input_handle *handle,
    			 const struct input_value *vals, unsigned int count)
    {
    	...
    
    	client = rcu_dereference(evdev->grab);
    
    	if (client)
    		evdev_pass_values(client, vals, count, ev_time);
    	else
    		/* 主要是通过client_list链表获取handle client进行事件处理,
    		 * 可以将client理解为一个用户层的接收者,在open event时创建 */
    		list_for_each_entry_rcu(client, &evdev->client_list, node)
    			evdev_pass_values(client, vals, count, ev_time);
    
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在evdev_pass_values()函数中,将会通过__pass_event()将event信息填充到client的buffer缓冲区,如果code是SYN_REPORT,将会调用kill_fasync(&client->fasync, SIGIO, POLL_IN)异步通知应用层,这样,input event传递到应用层,接着,用户程序就可以通过read函数读取event的详细信息。

  • 相关阅读:
    Java Properties的null值
    2022年湖北大学招生简章--成人高等教育高起专、专升本学历提升
    android10.0(Q) MTK 6765 user版本打开root权限
    波导芯的折射率大小决定了波导的最小横截面积
    用过Apifox这个API接口工具后,确实感觉postman有点鸡肋......
    unity中压缩文件与解压文件
    IDEA 的 debug 怎么实现?出于这个好奇心,我越挖越深
    在一张 24 GB 的消费级显卡上用 RLHF 微调 20B LLMs
    YOLO V8语义分割模型部署
    stm32和电机开发(从架构图到文档编写)
  • 原文地址:https://blog.csdn.net/weixin_41944449/article/details/133064355