【嵌入式环境下linux内核及驱动学习笔记-(16)linux总线、设备、驱动模型之input框架】

目录

  • 1、Linux内核输入子系统概念导入
    • 1.1 输入设备工作机制
    • 1.2 运行框架
    • 1.3 分层思想
  • 2、驱动开发步骤
    • 2.1 在init()或probe()函数中
    • 2.2 在exit()或remove()函数中:
    • 2.3 上报事件
    • 2.4 input驱动要素导图
    • 2.5 input驱动的总结
  • 3、接口详述
    • 3.1数据结构
      • 3.1.1 struct input_dev
      • 3.1.2 struct input_event
      • 3.1.3 EVENT Type
      • 3.1.4 EVENT Code
      • 3.1.5 struct input_handle
      • 3.1.6 struct input_handler
      • 3.1.7 struct input_device_id
    • 3.2 函数
      • 3.2.1 input_allocate_device
      • 3.2.2 input_register_device
      • 3.2.3 input_free_device
      • 3.2.4 input_unregister_device
      • 3.2.5 input_event
      • 3.2.6 set_bit
      • 3.2.7 input_set_abs_params
      • 3.2.8 input_sync
      • 3.2.9 input_event 函数
      • 3.2.10 input_report_key
      • 3.2.11 input_report_rel
      • 3.2.12 input_report_abs
      • 3.2.13 input_set_bit_oparams
      • 3.2.14 input_alloc_absinfo
  • 4 、input驱动-按键实例
    • 4.1 设备树节点信息
    • 4.2 驱动代码
    • 4.3 应用程序测试代码
    • 4.4 Makefile
  • 5、mpu6050改用input框架驱动实例
    • 5.1 定时操作相关函数
      • 5.1.1 struct delayed_work
      • 5.1.2 struct work_struct
      • 5.1.3 INIT_DELAYED_WORK
      • 5.1.4 queue_delayed_work
      • 5.1.5 schedule_delayed_work
      • 5.1.6 schedule_timeout_interruptible
      • 5.1.7 cancel_delayed_work
    • 5.2 设备树
    • 5.3 用input框架实现mpu6050数值读取实例
      • 5.3.1 应用层测试代码
      • 5.3.2 驱动代码
      • 5.3.3 Makefile

1、Linux内核输入子系统概念导入

1.1 输入设备工作机制

\qquad 输入设备(如按键、键盘、触摸屏、鼠标等)是典型的字符设备,其一般的工作机制是:

  • 底层在按键、触摸等动作发送时产生一个中断(或驱动驱动通过Timer定时查询),然后CPU通过SPI、I2C或外部存储器总线读取键值、坐标等数据,并将它们放入一个缓冲区。
  • 字符设备驱动管理该缓冲区
  • 用户在应用层通过read()接口可以读取键值、坐标等数据。

1.2 运行框架

\qquad 根据这个过程,可以认为只是中断、读键值等是与设备有关,输入事件的缓冲区管理及字符设备驱动的ifle_operations接口则对输入设备是通用的。因此,内核设计了输入子系统。由核心层处理公共的工作。

Linux内核 输入子系统的运行框架如图:

在这里插入图片描述

1.3 分层思想

在这里插入图片描述
\qquad 上方的图从上到下有应用层,内核层,硬件层。对于应用层而言,用户主要操作的是设备文件如图上所示有

  • /ev/input/mouse0
  • /dev/input/mice

\qquad 对于内核空间,整个input驱动系统的框架,又分为三个功能层。如下:

  • 事件处理层:接收来自核心层上报的事件,并选择对应的handler(事件处理器 struct input_handler)去处理。内核维护着多个事件处理器对象,每个input_handler对象专门处理一类事件,所有产生同类事件的设备驱动共用同一个handler。
  • 设备驱动层:主要实现获取硬件设备的数据信息(包括触摸屏被按下、按下位置、鼠标移动、键盘按下等等),并转换为核心层定义的规范事件后提交给核心层,该层每个设备对应一个struct input_dev对象。
  • 核心层:负责连接设备驱动层和事件处理层,为设备驱动层提供输入设备驱动的接口(struct input_dev)以及输入设备驱动的注册函数(input_register_device),为事件处理层提供输入事件驱动的接口;通知事件处理层对事件进行处理。

\qquad 驱动框架,把所有来自硬件的数据统一成struct input_event。这样,从设备驱动开始,只要把数据整成event向上传递给了input核心层。在这个标准化的基础上,事件驱动层以及核心层都由系统编写完成,开发者只需要针对某个输入设备编写对应的input设备驱动程序就可以了,这样极大简化了驱动程序的编写工作量。

2、驱动开发步骤

2.1 在init()或probe()函数中

init或probe函数中:

  1. 创建struct input_dev对象input_allocate_device
  2. 设置事件类型以及相关参数set_bit
  3. 注册struct input_dev对象input_register_device

解释:
\qquad 如果在传统硬编码的驱动程序,则在init()函数里写下这三个步骤。如果是在platform等总线式驱动编程里,则在probe()函数里实现这三个步骤。

  • 在第1步里,用input_allocate_device去创建对应的输入设备对象struct input_dev。
  • 在第2步里,设置设备对象input_dev中的相关数据,用set_bit去设置相关事件类型
  • 在第3步里,必须要用input_register_device把创建的对象input_dev 注册到内核中去。

2.2 在exit()或remove()函数中:

  1. 注销struct input_dev对象input_unregister_device
  2. 销毁struct input_dev对象input_free_device

解释:
\qquad 同样,如果在硬编码的驱动程序里,在exit()函数中完成这两个步骤。如果是在platform等总线式驱动编程里,则在probe()函数里实现这两个操作。

2.3 上报事件

两种事件上报方式:

  1. 对有中断支持的输入设备:在其中断处理函数(上半部或下半部)中上报事件。
  2. 对无中断支持的输入设备:使用workqueue循环定时上报(struct delayed_work)

事件上报的主要函数如下:

  • input_event
  • input_report_abs
  • input_sync

2.4 input驱动要素导图

在这里插入图片描述

2.5 input驱动的总结

\qquad 这部分可以在阅读完其它部分后,再来看这个总结,会有更清晰的认识。

1、input子系统是一个针对输入类型硬件的高度标准化的驱动框架。
2、在充分的归纳驱动行为的基础上,把驱动由3层1事件来实现。 这3层从上到下就是(事件处理层、核心层、设备驱动层),而一个事件指的就是struct input_event 。
3、开发者只需要编写设备驱动层对应的驱动程序即可。事件处理层以及核心层的处理,由内核实现。而串起这些内核 处理的核心又是事件类型(EVENT TYPE)与事件码(EVENT CODE)。这个的指定,由开发者用input_event()函数去指定。
4、所以开发者在驱动中只做了简单的几件事:

  • 对应设备创建struct input_dev数据结构,代表设备对象。并把该数据结构注册到内核去。
  • 在中断或定时器的处理函数中上报设备产生的对象。用input_event函数来上报事件类型与事件码等。

5、使用者在应用层用文件标准IO操作,读出struct input_event事件数据。

在这里插入图片描述

3、接口详述

主要的接口API介绍:
初始化时涉及到的主要函数,写在init()或probe()函数中:

struct input_dev *input_allocate_device(void)//创建对象

static inline void set_bit(unsigned long nr , volatile unsigned long *m)//设置事件类型

void input_set_abs_params(struct input_dev *dev,unsigned int axis,int min,int max,int fuzz,int flat)

int input_register_device(struct input_dev *dev)//注册input设备到内核

退出时涉及到的主要函数,写在exit()或release()函数中:

void input_unregister_device(struct input_dev *dev)
void input_free_device(struct input_dev *dev)

上报事件:

/*上报事件*/
void input_event(struct input_dev *,unsigned int t,unsigned int c,int v)

void input_report_key(struct input_dev *,unsigned int c,int v) //上报按键事件
void input_report_abs(struct input_dev *,unsigned int c,int v)//上报绝对坐标事件
//上面这些函数只是把数据准备好,每调一次只准备一个值,一个分量。多次调用后才能完成准备。
void input_sync(struct input_dev *)//在上述这些函数完成数据分量的整理后,统一由本函数完成最后的完整上报。

应用层获得数据类型:

struct input_event {
    struct timeval time;       // 时间戳
    __u16 type;             // 事件类型
    __u16 code;             // 哪个分值
    __s32 value;            // 具体值      
};

\qquad 由于前面软件的分层概念已说过,linux把输入设备的数据都标准化到struct input_event中了。所以驱动把该对象传递给input核心,核心再上传,这样一步涉向上。最后应用层接收数据,也是接收该对像。

3.1数据结构

3.1.1 struct input_dev

头文件:include/linux/input.h

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)];
	unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];
	unsigned long relbit[BITS_TO_LONGS(REL_CNT)];
	unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];
	unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];
	unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];
	unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];
	unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];
	unsigned long swbit[BITS_TO_LONGS(SW_CNT)];

	unsigned int hint_events_per_packet;

	unsigned int keycodemax;
	unsigned int keycodesize;
	void *keycode;

	int (*setkeycode)(struct input_dev *dev,
			  const struct input_keymap_entry *ke,
			  unsigned int *old_keycode);
	int (*getkeycode)(struct input_dev *dev,
			  struct input_keymap_entry *ke);

	struct ff_device *ff;

	unsigned int repeat_key;
	struct timer_list timer;

	int rep[REP_CNT];

	struct input_mt *mt;

	struct input_absinfo *absinfo;

	unsigned long key[BITS_TO_LONGS(KEY_CNT)];
	unsigned long led[BITS_TO_LONGS(LED_CNT)];
	unsigned long snd[BITS_TO_LONGS(SND_CNT)];
	unsigned long sw[BITS_TO_LONGS(SW_CNT)];

	int (*open)(struct input_dev *dev);
	void (*close)(struct input_dev *dev);
	int (*flush)(struct input_dev *dev, struct file *file);
	int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);

	struct input_handle __rcu *grab;

	spinlock_t event_lock;
	struct mutex mutex;

	unsigned int users;
	bool going_away;

	struct device dev;

	struct list_head	h_list;
	struct list_head	node;

	unsigned int num_vals;
	unsigned int max_vals;
	struct input_value *vals;

	bool devres_managed;
};
#define to_input_dev(d) container_of(d, struct input_dev, dev)

这个结构体 input_dev 表示 Linux 输入子系统中的一个输入设备。它定义在 include/linux/input.h 头文件中。
input_dev 包含的主要成员有:

  • name、phys、uniq:设备的名称、物理位置标识符和唯一ID。
  • id:设备的ID信息,包含bustype(总线类型)、vendor(厂商ID)、product(产品ID)和version(版本)等。
  • propbit:设备支持的属性位场,如INPUT_PROP_POINTER、INPUT_PROP_DIRECT等。
  • evbit[]: 输入设备支持的事件类型,如EV_KEY, EV_REL等;
  • keybit[]: 输入设备支持的按键事件码,如KEY_A, KEY_B等;
  • relbit[]: 输入设备支持的相对坐标事件码,如REL_X, REL_Y等;
  • absbit[]: 输入设备支持的绝对坐标事件码,如ABS_X, ABS_Y等;
  • mscbit[]: 输入设备支持的杂项事件码;
  • ledbit[]: 输入设备支持的指示灯事件码;
  • sndbit[]: 输入设备支持的声音事件码;
  • ffbit[]: 输入设备支持的力反馈事件码;
  • swbit[]: 输入设备支持的开关事件码。
  • hint_events_per_packet:设备事件包的大小提示。
  • keycodemax、keycodesize、keycode:设备支持的按键SCAN CODE信息。
  • setkeycode、getkeycode:设置和获取设备的按键SCAN CODE的回调函数。
  • ff:设备的力反馈(Force Feedback)机制信息。
  • event:当有事件发生时,输入子系统调用的回调函数。
  • grab:当前grab该输入设备的handle。
  • dev:内嵌的设备结构体,由于input_dev也代表一个输入设备。
  • vals:一个值数组,为该设备保存读数值。
  • 还有其他各种位场和变量…
    从整体来看,input_dev结构体中包含了描述一个输入设备的各种信息,通过这些信息,输入子系统能够正确识别设备、读取设备值和实现设备的力反馈等功能。

unsigned long evbit[BITS_TO_LONGS(EV_CNT)];语句的含义:

  • EV_CNT: 定义在<linux/input.h>头文件中,通常为32。它表示事件类型的最大值EV_MAX加1。
  • BITS_TO_LONGS(n)是一个计算宏,用于计算存储n个位所需的无符号长整型数量。
    • 因为每个无符号长整型(unsigned long)可以存储32个比特位。
  • 所以,BITS_TO_LONGS(EV_CNT)计算出所需的无符号长整型数量来存储EV_CNT个事件类型位。
  • 于是,evbit数组的大小就是BITS_TO_LONGS(EV_CNT)。每个元素用于存储32个事件类型的位信息。
    举例来说,如果EV_CNT为32:
  • 则BITS_TO_LONGS(32) = 32 / 32 = 1,计算结果为1
  • 所以evbit数组大小为1,evbit[0]用于存储32个事件类型的位
    如果EV_CNT为64:
  • 则BITS_TO_LONGS(64) = 64 / 32 = 2,计算结果为2
  • 所以evbit数组大小为2,evbit[0]和evbit[1]一起用于存储64个事件类型的位

解释位图的操作
input_dev对象中的evbit成员是一个位图(bitmap),用于标记该输入设备支持的事件类型。
当驱动调用set_bit(EV_KEY, input_dev->evbit);时,就是在evbit位图中设置EV_KEY类型事件对应的位。
例如,EV_KEY事件类型对应的位可能是第3位,那么set_bit(EV_KEY, input_dev->evbit);就相当于:

input_dev->evbit[0] |= (1 << 3);   设置input_dev->evbit的第3

这表示该输入设备支持EV_KEY类型的事件。
之后,输入子系统在解析该输入设备的事件时,会检查evbit位图中是否设置了对应事件类型的位,如果设置了,则确认设备支持该事件类型,并继续解析事件代码来判断具体事件。
例如,收到输入事件(EV_KEY, KEY_A, 1)时,输入子系统会先检查evbit位图是否设置了EV_KEY类型事件的位,如果设置了,则确认设备支持键盘事件,然后再根据KEY_A代码解析该事件为KEY_A按下。
如果evbit位图中没有设置EV_KEY事件类型对应的位,那么输入子系统将无法解析该事件,因为设备自身并不支持该类型事件。

3.1.2 struct input_event

头文件: /include/uapi/linux/input.h

#include <linux/input.h>
struct input_event {
	struct timeval time;
	__u16 type;
	__u16 code;
	__s32 value;
};

input_event 包含的主要成员有:

  • time:事件发生的时间,使用 timeval 结构表示。
  • type:事件的类型,可以是 EV_KEY(按键)、EV_REL(相对坐标)、EV_ABS(绝对坐标)等。
  • code:事件的代码,对于按键事件,这个值表示按键的SCAN CODE;对于坐标事件,这个值表示X/Y轴等。
  • value:事件的值,表示坐标值大小或是否是按键按下/弹起等。
    所以,input_event 结构体中包含了描述一个输入事件所有的信息:发生时间、事件类型、事件代码以及事件的值。
    在 Linux 输入子系统的工作流程中,无论输入事件来自于哪个输入设备,都会转换为 input_event 结构,并添加到输入事件队列中。然后系统会从输入事件队列中不断读取 input_event 事件,进行处理分发。

举个简单例子:
当有一个按键按下时,会产生一个 input_event 事件,其中:

  • type 设为 EV_KEY,表示这是一个按键事件
  • code 设为按键的SCAN CODE,例如KEY_A的SCAN CODE
  • value 设为 1,表示按键按下

当按键弹起时,会产生一个新的 input_event 事件,type和code与上一个事件相同,但value设为0,表示弹起。
此外,输入子系统还会根据时间戳time来判断事件的先后顺序和处理事件的频率等。
所以,input_event 结构体对描述一个输入事件来说,已包含了所有必须的信息。它是Linux输入子系统识别和处理各种输入事件的关键。
总之,input_event 和 input_dev 这两个结构体,是输入子系统的两大基石,前者描述输入事件,后者描述输入设备,二者共同实现了Linux下的输入处理机制。

3.1.3 EVENT Type

这些宏定义,会用在input_event函数的type参数 ,以及struct input_event的type成员
定义在/include/uapi/linux/input.h头文件里。

/*
 * Event types
 */

#define EV_SYN			0x00    同步事件,用于同步输入设备的时间轴。
#define EV_KEY			0x01    按键事件, comme PC键盘、触摸屏上的按键等。
#define EV_REL			0x02    相对运动事件,如鼠标移动。
#define EV_ABS			0x03    绝对坐标事件,如触摸屏、手写板等。
#define EV_MSC			0x04    杂项事件,不适合其他类型的事件。
#define EV_SW			0x05    开关事件,如笔记本电脑的盖子开关事件。
#define EV_LED			0x11    示灯事件,如Num Lock 指示灯状态变化。
#define EV_SND			0x12    声音事件,如蜂鸣器发出的声音信号。
#define EV_REP			0x14    重复事件,某事件连续多次产生,用来减少事件数量。
#define EV_FF			0x15    力回馈事件,如游戏手柄产生的振动事件。
#define EV_PWR			0x16    电源事件,如AC/Battery 状态变化事件。 
#define EV_FF_STATUS		0x17  力回馈设备状态事件。
#define EV_MAX			0x1f    定义事件类型的最大值。
#define EV_CNT			(EV_MAX+1)


3.1.4 EVENT Code

这些宏定义在 include/uapi/linux/input.h 头文件里。
主要用在input_event()函数的code 参数,以及struct input_event的code成员。以及关联着struct input_dev中的成员(如:evbit成员、keybit成员、relbit成员、absbit等)。这里只列出部分,详细的直接参考头文件里的详细定义。

Key 及 Button的事件码定义

#define KEY_RESERVED		0
#define KEY_ESC			1
#define KEY_1			2
#define KEY_2			3
#define KEY_3			4
#define KEY_4			5
#define KEY_5			6
#define KEY_6			7
#define KEY_7			8
#define KEY_8			9
#define KEY_9			10
#define KEY_0			11
#define KEY_MINUS		12
#define KEY_EQUAL		13
#define KEY_BACKSPACE		14
#define KEY_TAB			15
#define KEY_Q			16
#define KEY_W			17
#define KEY_E			18
#define KEY_R			19
#define KEY_T			20
#define KEY_Y			21
#define KEY_U			22
#define KEY_I			23
#define KEY_O			24
#define KEY_P			25
#define KEY_LEFTBRACE		26
#define KEY_RIGHTBRACE		27
#define KEY_ENTER		28
#define KEY_LEFTCTRL		29
#define KEY_A			30
#define KEY_S			31
#define KEY_D			32

...............

/* We avoid low common keys in module aliases so they don't get huge. */
#define KEY_MIN_INTERESTING	KEY_MUTE
#define KEY_MAX			0x2ff
#define KEY_CNT			(KEY_MAX+1)

相对轴的事件码定义

*
 * Relative axes
 */

#define REL_X			0x00
#define REL_Y			0x01
#define REL_Z			0x02
#define REL_RX			0x03
#define REL_RY			0x04
#define REL_RZ			0x05
#define REL_HWHEEL		0x06
#define REL_DIAL		0x07
#define REL_WHEEL		0x08
#define REL_MISC		0x09
#define REL_MAX			0x0f
#define REL_CNT			(REL_MAX+1)

绝对轴的事件码定义

/*
 * Absolute axes
 */

#define ABS_X			0x00               X轴坐标值
#define ABS_Y			0x01               Y轴坐标值
#define ABS_Z			0x02               Z轴坐标值
#define ABS_RX			0x03               X轴相对坐标(增量)
#define ABS_RY			0x04               Y轴相对坐标(增量)
#define ABS_RZ			0x05               Z轴相对坐标(增量)
#define ABS_THROTTLE		0x06           油门位置值
#define ABS_RUDDER		0x07               方向舵位置值 
#define ABS_WHEEL		0x08               轮坐标值
#define ABS_GAS			0x09               加速度值
#define ABS_BRAKE		0x0a               刹车值
#define ABS_HAT0X		0x10               帽子开关0的X轴值
#define ABS_HAT0Y		0x11               帽子开关0的Y轴值
#define ABS_HAT1X		0x12               帽子开关1的X轴值
#define ABS_HAT1Y		0x13               帽子开关1的Y轴值
#define ABS_HAT2X		0x14
#define ABS_HAT2Y		0x15
#define ABS_HAT3X		0x16
#define ABS_HAT3Y		0x17
#define ABS_PRESSURE		0x18          压力值
#define ABS_DISTANCE		0x19          距离值
#define ABS_TILT_X		0x1a              X轴倾斜值 
#define ABS_TILT_Y		0x1b              Y轴倾斜值
#define ABS_TOOL_WIDTH		0x1c          工具宽度值


#define ABS_VOLUME		0x20              音量值
#define ABS_MISC		0x28              其它杂项值

#define ABS_MT_SLOT		0x2f	    /* MT slot being modified */修改的多点触摸事件的插槽索引
#define ABS_MT_TOUCH_MAJOR	0x30	/* Major axis of touching ellipse */椭圆触摸区域的长轴
#define ABS_MT_TOUCH_MINOR	0x31	/* Minor axis (omit if circular) */椭圆触摸区域的短轴
#define ABS_MT_WIDTH_MAJOR	0x32	/* Major axis of approaching ellipse */逼近的椭圆触摸区域的长轴
#define ABS_MT_WIDTH_MINOR	0x33	/* Minor axis (omit if circular) */逼近的椭圆触摸区域的短轴
#define ABS_MT_ORIENTATION	0x34	/* Ellipse orientation */椭圆触摸区域的方向
#define ABS_MT_POSITION_X	0x35	/* Center X touch position */触摸中心的X坐标
#define ABS_MT_POSITION_Y	0x36	/* Center Y touch position */ 触摸中心的Y坐标
#define ABS_MT_TOOL_TYPE	0x37	/* Type of touching device */触摸设备的类型 
#define ABS_MT_BLOB_ID		0x38	/* Group a set of packets as a blob */把一组数据包分组为一个整体
#define ABS_MT_TRACKING_ID	0x39	/* Unique ID of initiated contact */初始化触摸点的唯一ID 
#define ABS_MT_PRESSURE		0x3a	/* Pressure on contact area */触摸区域的压力值 
#define ABS_MT_DISTANCE		0x3b	/* Contact hover distance */触摸悬停的距离
#define ABS_MT_TOOL_X		0x3c	/* Center X tool position */工具中心的X坐标
#define ABS_MT_TOOL_Y		0x3d	/* Center Y tool position */工具中心的Y坐标


#define ABS_MAX			0x3f 绝对坐标事件代码的最大值
#define ABS_CNT			(ABS_MAX+1) 绝对坐标事件代码的数量

这些定义代表Linux内核输入子系统中绝对坐标事件类型(EV_ABS)对应的事件代码。
这些绝对坐标事件代码用来表示各种输入设备的坐标数据或类似数据,例如:

  • ABS_X/Y: 触摸屏、手写板坐标
  • ABS_Z: 3D鼠标的Z轴值
  • ABS_THROTTLE/RUDDER: 飞行摇杆油门/方向舵值
  • ABS_WHEEL: 鼠标滚轮坐标值
  • ABS_PRESSURE: 手写板/绘图板的笔压值
  • ABS_TILT_X/Y: 倾斜板的倾斜值
  • ABS_TOOL_WIDTH: 手写笔或绘图笔的宽度值

3.1.5 struct input_handle

#include <linux/input.h>
struct input_handle {

	void *private;

	int open;
	const char *name;

	struct input_dev *dev;
	struct input_handler *handler;

	struct list_head	d_node;
	struct list_head	h_node;
};

这个结构体表示input子系统中的一个input handle。它包含以下几个字段:

  • private: 私有数据,由input handle的创建者使用
  • open: 是否打开,0表示关闭,非0表示打开
  • name: input handle的名称
  • dev: 关联的input device
  • handler: 关联的input handler
  • d_node: 链接到dev->h_list,组成dev的input handle列表
  • h_node: 链接到handler->h_list,组成handler的input handle列表
    所以这个结构体把一个input handle跟它的input device和input handler关联起来,并通过两个链表将它们串联起来。
    input子系统通过注册input handler来检测和处理input event。每个input handler可以监听一个或多个input device。当input device产生event时,与之关联的input handle会调用对应的input handler来处理这个event。
    所以输入事件的传递路径是:
    input device -> input handle -> input handler
    input handle充当中间层,将input device和input handler联系起来。

3.1.6 struct input_handler

#include <linux/input.h>
struct input_handler {

	void *private;

	void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
	void (*events)(struct input_handle *handle,
		       const struct input_value *vals, unsigned int count);
	bool (*filter)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
	bool (*match)(struct input_handler *handler, struct input_dev *dev);
	int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);
	void (*disconnect)(struct input_handle *handle);
	void (*start)(struct input_handle *handle);

	bool legacy_minors;
	int minor;
	const char *name;

	const struct input_device_id *id_table;

	struct list_head	h_list;
	struct list_head	node;
};

它包含以下几个字段:

  • private: 私有数据,由input handler的创建者使用
  • event: 处理单个input event的回调函数
  • events: 处理input event数组的回调函数
  • filter: 过滤input event的回调函数,如果返回true,event会被handler处理
  • match: 判断一个input device是否匹配这个handler的回调函数
  • connect: 当一个匹配的input device被添加时调用的回调函数,用来建立handle
  • disconnect: 当一个handle被删除时调用的回调函数
  • start: 当第一个handle被添加时调用的回调函数
  • legacy_minors: 是否使用旧的input minor number
  • minor: input handler的minor number
  • name: input handler的名称
  • id_table: 与这个handler匹配的input device ID表
  • h_list: 链接到该handler所有的input handle
  • node: 将handler链接到input_handler_list
    所以一个input handler通过上述各种回调函数来检测和处理input事件。它通过id_table和match函数来匹配适合它处理的input device。一旦匹配,就会通过connect函数来建立一个input handle与该device的连接。它还维护了一个h_list来管理所有与它关联的input handle。

3.1.7 struct input_device_id

include/linux/mod_devicetable.h

struct input_device_id {

	kernel_ulong_t flags;

	__u16 bustype;
	__u16 vendor;
	__u16 product;
	__u16 version;

	kernel_ulong_t evbit[INPUT_DEVICE_ID_EV_MAX / BITS_PER_LONG + 1];
	kernel_ulong_t keybit[INPUT_DEVICE_ID_KEY_MAX / BITS_PER_LONG + 1];
	kernel_ulong_t relbit[INPUT_DEVICE_ID_REL_MAX / BITS_PER_LONG + 1];
	kernel_ulong_t absbit[INPUT_DEVICE_ID_ABS_MAX / BITS_PER_LONG + 1];
	kernel_ulong_t mscbit[INPUT_DEVICE_ID_MSC_MAX / BITS_PER_LONG + 1];
	kernel_ulong_t ledbit[INPUT_DEVICE_ID_LED_MAX / BITS_PER_LONG + 1];
	kernel_ulong_t sndbit[INPUT_DEVICE_ID_SND_MAX / BITS_PER_LONG + 1];
	kernel_ulong_t ffbit[INPUT_DEVICE_ID_FF_MAX / BITS_PER_LONG + 1];
	kernel_ulong_t swbit[INPUT_DEVICE_ID_SW_MAX / BITS_PER_LONG + 1];

	kernel_ulong_t driver_info;
};

这个结构体表示input设备的ID信息,用来匹配input handler和input device。它包含以下几个字段:

  • flags: 标志位,如INPUT_DEVICE_ID_MATCH_BUS等
  • bustype: 总线类型,如BUS_USB、BUS_HIL等
  • vendor: 设备厂商ID
  • product: 产品ID
  • version: 产品版本
  • evbit: 支持的event type位域
  • keybit: 支持的key位域
  • relbit: 支持的relative axis位域
  • absbit: 支持的absolute axis位域
  • mscbit: 支持的misc event位域
  • ledbit: 支持的led位域
  • sndbit: 支持的sound event位域
  • ffbit: 支持的force feedback事件位域
  • swbit: 支持的switch事件位域
  • driver_info: 供驱动使用的可选信息
    input子系统和input handler可以通过比较input device的ID信息与handler的id_table来判断一个input device是否 matches 一个handler。
    举个例子,一个handler的id_table可以这样定义:
static const struct input_device_id my_input_table[] = {
    {
        .flags = INPUT_DEVICE_ID_MATCH_BUS,
        .bustype = BUS_USB,
        .vendor = 0x1234,
        .product = 0x5678,
    }, 
}

这样这个handler就只会匹配厂商ID为0x1234、产品ID为0x5678的USB输入设备。
所以通过将各个input handler的id_table设计得错落不重叠,可以使不同的input device匹配到不同的handler进行处理。

3.2 函数

3.2.1 input_allocate_device

input_allocate_device函数是input子系统提供的函数,用于分配一个新的input device对象。
原型:

#include <<linux/input.h>>
struct input_dev *input_allocate_device(void);

它只有一个void类型的形参,并返回一个新的struct input_dev对象。

用法:
当一个驱动程序检测到一个输入设备时,它需要先通过input_allocate_device()函数分配一个input_dev对象,然后设置这个对象的各个字段来描述这个输入设备,最后通过input_register_device()函数向input子系统注册这个设备。
举个例子:

struct input_dev *input_dev;

input_dev = input_allocate_device();
if (!input_dev) 
    return -ENOMEM;

input_dev->name = "Foo";    // 设置name为"Foo"
input_dev->phys = "foo/input0";    // 设置物理ID
input_dev->id.bustype = BUS_HOST;    // 设置总线类型
...     

input_register_device(input_dev); // 注册该设备
这个例子展示了如何分配一个新input device对象,设置它的字段,然后注册该设备到input子系统。
另外需要注意,调用input_register_device()成功注册设备后,内核会采取input_dev对象的所有权,驱动不需要也不应该再释放这个对象。所以驱动在调用input_allocate_device()后需要牢记这个对象,而在调用input_register_device()成功注册后就可以忘记这个对象了。

3.2.2 input_register_device

input_register_device()函数用来向input子系统注册一个input device对象。

#include <linux/input.h>
int __must_check input_register_device(struct input_dev *);

  • dev: 要注册的input_dev对象
  • 返回值: 成功返回0,失败返回负值的错误码

用法:
当一个驱动程序通过input_allocate_device()函数分配一个新的input_dev对象并设置完毕后,需要调用input_register_device()函数来注册该设备,否则input子系统不会知道该设备的存在,也就不会对其产生的输入事件进行处理。
registration的工作主要是将该input_dev对象添加到input_dev_list链表中,并为其分配一个input minor number,以便应用程序可以通过该minor number来访问该输入设备。
举个例子:

struct input_dev *input_dev;

input_dev = input_allocate_device();
/* 设置input_dev各个字段 ... */ 

if (input_register_device(input_dev)) {
    /* 注册失败 */ 
    input_free_device(input_dev);
    /* 错误处理 */
}

如代码所示,如果注册失败,需要通过input_free_device()释放input_dev对象;如果注册成功,则无需释放该对象,内核会自动释放。
需要注意,注册成功后,驱动程序应该保留input_dev->dev.parent指向的设备对象的引用,以免该对象过早被销毁。
另外,一个驱动至多只能注册一个input device对象。如果需要注册多个,需要为每个设备分别编写一个独立的驱动。

3.2.3 input_free_device

input_free_device()函数用来释放一个input device对象。

#include <linux/input.h>
void input_free_device(struct input_dev *dev);

参数

  • dev: 要释放的input_dev对象

用法:
当注册一个input device对象失败时,需要通过input_free_device()函数释放该对象以防内存泄露。

示例:*

struct input_dev *input_dev;

input_dev = input_allocate_device();
/* 设置input_dev各个字段 ... */ 

if (input_register_device(input_dev)) {
    /* 注册失败 */ 
    input_free_device(input_dev);
    /* 错误处理 */ 
}

如代码所示,如果注册失败需要调用input_free_device()释放input_dev对象。
需要注意,如果registration成功,则不需要也不应该调用input_free_device(),因为内核会自己管理这个input_dev对象的生命周期。
只有在对象分配后注册before未成功的情况下,才需要手动调用input_free_device()释放input device对象。

3.2.4 input_unregister_device

input_unregister_device()函数用来从input子系统注销一个已经注册的input device对象。

#include <linux/input.h>
void input_unregister_device(struct input_dev *);

参数

  • dev: 要注销的input_dev对象

用法:
当一个输入设备被移除时,驱动程序需要调用input_unregister_device()函数来注销对应的input device对象,以通知input子系统 stop 处理该设备产生的输入事件。
示例:

if (!input_dev->dev.parent) {    /* 检查设备对象是否仍存在 */
    input_unregister_device(input_dev);
}

如代码所示,在设备对象被删除之前,驱动需要调用input_unregister_device()注销对应的input_dev对象。
成功调用input_unregister_device()后,input子系统会从其管理的input_dev_list中移除该对象,并释放相关资源。

3.2.5 input_event

input_event()函数用来通过已注册的input device对象向input子系统提交一个输入事件。

#include <linux/input.h>
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value);

参数:

  • dev: 已注册的input_dev对象
  • type: 事件类型,如EV_KEY、EV_REL、EV_ABS等
  • code: 事件代码,如KEY_A、REL_X、ABS_Y等
  • value: 事件值

用法:
当检测到输入设备产生一个输入事件时,驱动程序需要通过input_event()函数提交该事件到input子系统,以便让相关的应用程序获得该事件的通知。
示例:

if (get_usb_keycode(&usb_dev, &scancode)) {
    input_event(input_dev, EV_KEY, scancode, 1);
    input_sync(input_dev);
} 

如代码所示,当检测到USB键盘产生一个键盘事件时,驱动通过input_event()提交一个EV_KEY类型的事件,键码为scancode,值为1(表示按下), 然后调用input_sync()同步事件。
需要注意,仅调用input_event()是不够的,还需要及时调用input_sync()来同步事件,否则事件不会被处理。
另外,input_event()可以被调用在中断上下文,因此需要确保其执行时间尽量短,避免延迟中断响应。

3.2.6 set_bit

set_bit()函数用来给一个长整型变量中的某一位设置1。
原型:

static inline void set_bit(unsigned long nr, volatile unsigned long *m)

参数:

  • nr: 要设置的位的索引号,0-63
  • m: 长整型变量的地址

该函数定义在<asm/bitops.h>头文件中。
返回值: 无
用法:
set_bit()函数通常用来标记某种状态。它通过设置一个长整型变量中的某一位来表示一个布尔值。
示例:

unsigned long flags;

set_bit(0, &flags);  /* 设置flags的第0位 */
set_bit(3, &flags);  /* 设置flags的第3位 */ 

如上代码所示,设置了flags变量的第0和第3位。

此后,可以使用test_bit()函数来测试某位是否置位,即检查对应状态是否置TRUE。

if (test_bit(0, &flags)) 
    /* flags的第0位已置位,值为TRUE */  
if (!test_bit(2, &flags)) 
    /* flags的第2位未置位,值为FALSE */

常见的使用场景有:

  1. 标记中断是否使能(enable_irq()函数内部使用);
  2. 表示进程的状态(如是否暂停等);
  3. 记录设备的状态(如通信线路是否繁忙);
  4. 等等。
    总之,当需要使用一位来表示一个布尔值或某种状态时,set_bit()函数就很有用。配合test_bit()可以方便地检查状态。
    需要注意的是,set_bit()函数适用于长整型(unsigned long)变量,如果需要操作位图(bitmap)等,需要使用其他位操作函数。

3.2.7 input_set_abs_params

input_set_abs_params()函数用于为输入设备的一个绝对坐标轴设置参数。

#include <linux/input.h>
void input_set_abs_params(struct input_dev *dev, unsigned int axis,
			  int min, int max, int fuzz, int flat);

参数:

  • dev: 输入设备结构体指针
  • axis: 要设置的参数对应的轴,如ABS_X, ABS_Y等
  • min: 轴的最小值
  • max: 轴的最大值
  • fuzz: 轴的噪声值,用于过滤小幅度的波动
  • flat: 轴的死区值,位于该范围内的输入事件会被忽略

用法:
input_set_abs_params()函数通常在输入设备的probe()函数中调用,用于为设备的一个绝对坐标轴设置相关参数,包括:

  • 轴的最小值min和最大值max,表示轴的输入范围;
  • 轴的噪声值fuzz,用于过滤小幅度的Axes数据波动;
  • 轴的死区值flat,位于该范围内的输入事件会被忽略;

设置这些参数后,内核输入子系统会根据它们来判断和处理该轴的输入事件:

  • 大于min且小于max的事件会被认为是有效事件;
  • 小于min或大于max的事件会被忽略;
  • 位于min与min + flat之间的事件会被忽略
  • 位于max - flat与max之间的事件也会被忽略
  • 相邻两个事件之差小于fuzz的会被看作噪声而被过滤。
    所以,该函数用于为输入设备的一个绝对坐标轴设置坐标范围、噪声过滤值和死区范围等参数。内核输入子系统会根据这些参数来判断该轴的输入事件是否有效及是否应被忽略或过滤。

举例来说,对于一触摸屏,我们可以设置:

input_set_abs_params(tp_dev, ABS_X, 0, 1024, 10, 40); 
input_set_abs_params(tp_dev, ABS_Y, 0, 768, 10, 40);

这会设置x轴和y轴的范围(0-1024, 0-768),x/y轴的噪声过滤值为10,死区范围为40。
内核会据此来判断触摸屏的x/y坐标输入事件,实现对噪点和震动的过滤,提高触摸精度。
所以,input_set_abs_params()函数为Linux输入子系统提供了定制绝对坐标轴参数的方法。通过设置坐标范围、噪声过滤和死区值等,我们可以定制内核如何判断和处理该轴的输入事件,这在输入设备的开发中是非常有用的功能。


3.2.8 input_sync

input_sync()函数用来同步先前通过input_event()提交的输入事件。

#include <linux/input.h>
static inline void input_sync(struct input_dev *dev)
{
	input_event(dev, EV_SYN, SYN_REPORT, 0);
}

参数

  • dev: 对应的input_dev对象

用法:
驱动程序在通过input_event()提交一系列输入事件后,需要调用input_sync()来同步这些事件,否则事件不会被送往应用程序。
input_sync()的实现很简单,它只是提交了一个EV_SYN类型的SYN_REPORT事件,值为0。但这个事件起到同步前面事件的作用。
示例:

input_event(input_dev, EV_KEY, KEY_A, 1);
input_event(input_dev, EV_KEY, KEY_B, 1);
input_sync(input_dev);  /* 同步事件 */

如代码所示,两次事件提交后调用input_sync()来同步,否则这两个事件不会被处理。
所以,input_sync()函数的使用时机是在提交一系列输入事件后,在事件序列结束处调用,用来同步整个事件序列,使其被送往应用程序处理。
如果不调用input_sync(),先前提交的事件不会产生任何效果,这点开发者需要注意。
另外,input_sync()可以在中断上下文调用,但也应该保证尽量短时间执行,避免影响中断响应。

3.2.9 input_event 函数

input_event()函数用来向input子系统报告一个输入事件。
原型:

#include <linux/input.h>
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value)

**参数 **

  • dev: input_dev对象,即输入设备
  • type: 事件类型,如EV_KEY、EV_REL等 具体参见上面的EVENT TYPE 解释 定义在include/uapi/linux/input.h头文件里
  • code: 事件代码,如KEY_A、REL_X等 具体参见上面的EVENT Code 解释 定义在include/uapi/linux/input.h头文件里
  • value: 事件值,如1(按下)、0(弹起)等
    该函数定义在<linux/input.h>头文件中。
    用法:
    input_event()函数由输入驱动调用,用来向输入子系统报告外围输入设备产生的各种事件,如按键、相对运动、绝对坐标等事件。
    输入子系统会根据事件类型和代码,将输入事件映射为系统事件(如Linux下的KEY_A等),然后交由相应的上层软件进行处理。
    示例:
input_event(input_dev, EV_KEY, KEY_A, 1);    /* A键按下 */ 
input_event(input_dev, EV_KEY, KEY_A, 0);    /* A键弹起 */
input_event(input_dev, EV_REL, REL_X, -5);   /* X轴左移5个单位 */ 

如上代码所示:
报告了一个A键按下和弹起事件,以及X轴左移5的相对移动事件。
输入子系统会解析这3个事件,并将EV_KEY类型的A键事件映射为KEY_A相应事件,从而通知上层软件A键的变化;将REL_X事件交由鼠标驱动处理,达到鼠标左移5像素的效果。
所以,input_event()函数是输入驱动与输入子系统沟通的桥梁,驱动通过调用该函数将外设产生的各种输入事件报告给输入子系统,让其将这些事件转化为系统事件后交由上层软件进行处理。
这是实现一个输入驱动的关键,没有input_event()函数,驱动就无法将输入事件传达给操作系统。
另外,需要注意的是,在调用input_event()前,必须已通过input_register_device()函数注册对应的输入设备input_dev,否则会引起内核异常。

3.2.10 input_report_key

input_report_key()函数用来方便地提交一个键盘事件。

static inline void input_report_key(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_KEY, code, !!value);
}

参数

  • dev: 对应的input_dev对象
  • code: 要提交的键码,如KEY_A、KEY_B等
  • value: 事件值,0表示释放,1表示按下
    该函数定义在<linux/input.h>头文件中。
    用法:
    当检测到键盘的某个键被按下或释放时,可以调用input_report_key()函数方便地提交一个EV_KEY类型的键盘事件。
    该函数的实现很简单,它只是将value非0的值转换为1,然后调用input_event()提交一个EV_KEY事件。
    示例:
input_report_key(input_dev, KEY_A, 1);    /* A键按下 */
...
input_report_key(input_dev, KEY_A, 0);    /* A键释放 */
input_sync(input_dev);            /* 同步事件 */

如代码所示,按下A键时value为1,释放A键时value为0,然后调用input_sync()同步事件。
所以,input_report_key()函数的使用时机是在检测到键盘按键事件时,用来方便地提交一个EV_KEY类型的键盘事件到input子系统。使用后同样需要调用input_sync()来同步事件。
相比直接调用input_event(),input_report_key()可以更清晰地表示键盘事件的按下和释放,使用更方便,所以是首选的输入键盘事件的接口。

3.2.11 input_report_rel

input_report_rel()函数用来方便地提交一个相对轴事件。

static inline void input_report_rel(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_REL, code, value);
}

参数

  • dev: 对应的input_dev对象
  • code: 要提交的相对轴,如REL_X、REL_Y等
  • value: 相对轴的值
    该函数定义在<linux/input.h>头文件中。
    用法:
    当检测到鼠标或触控板的相对轴的值变化时,可以调用input_report_rel()函数方便地提交一个EV_REL类型的相对轴事件。
    该函数的实现很简单,它只是调用input_event()提交一个EV_REL事件。
    示例:
input_report_rel(input_dev, REL_X, -5);    /* X轴减少5 */
input_report_rel(input_dev, REL_Y, 10);    /* Y轴增加10 */ 
input_sync(input_dev);            /* 同步事件 */

如代码所示,检测到X轴减少5、Y轴增加10时,调用input_report_rel()提交两个EV_REL事件,然后input_sync()同步。
所以,input_report_rel()函数的使用时机是在检测到相对轴输入设备的相对轴值变化时,用来方便地提交EV_REL类型的相对轴事件到input子系统。使用后同样需要调用input_sync()来同步事件。

3.2.12 input_report_abs

input_report_abs()函数用来方便地提交一个绝对轴事件。

static inline void input_report_abs(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_ABS, code, value);
}

参数

  • dev: 对应的input_dev对象
  • code: 要提交的绝对轴,如ABS_X、ABS_Y、ABS_PRESSURE等
  • value: 绝对轴的值
    该函数定义在<linux/input.h>头文件中。
    用法:
    当检测到触摸屏或触控板的绝对轴的值变化时,可以调用input_report_abs()函数方便地提交一个EV_ABS类型的绝对轴事件。
    该函数的实现很简单,它只是调用input_event()提交一个EV_ABS事件。
    示例:
input_report_abs(input_dev, ABS_X, 100);   /* X轴位置为100 */
input_report_abs(input_dev, ABS_Y, 200);   /* Y轴位置为200 */
input_report_abs(input_dev, ABS_PRESSURE, 50); /* pressure为50 */  
input_sync(input_dev);              /* 同步事件 */ 

如代码所示,检测到X轴为100、Y轴为200、pressure为50时,调用input_report_abs()提交三个EV_ABS事件,然后input_sync()同步。
所以,input_report_abs()函数的使用时机是在检测到绝对轴输入设备的绝对轴值变化时,用来方便地提交EV_ABS类型的绝对轴事件到input子系统。使用后同样需要调用input_sync()来同步事件。

3.2.13 input_set_bit_oparams

input_set_abs_params()函数用来设置一个绝对轴的范围参数。

#include <linux/input.h>
void input_set_abs_params(struct input_dev *dev, unsigned int axis,
			  int min, int max, int fuzz, int flat);

参数

  • dev: 对应的input_dev对象
  • axis: 要设置的参数对应的绝对轴,如ABS_X、ABS_Y等
  • min: 最小值
  • max: 最大值
  • fuzz: 误差范围值
  • flat: 滑动后轴值变化量

用法:
在注册一个绝对轴输入设备前,需要通过input_set_abs_params()函数设置每个绝对轴的参数,如最小值、最大值、误差范围等,这些参数对标定绝对轴输入非常重要。
示例:

input_set_abs_params(input_dev, ABS_X, 0, 1023, 0, 0); 
input_set_abs_params(input_dev, ABS_Y, 0, 767, 0, 0);
input_set_abs_params(input_dev, ABS_PRESSURE, 0, 255, 0, 0);
input_register_device(input_dev);

如代码所示,在注册input_dev之前设置了ABS_X、ABS_Y和ABS_PRESSURE三个绝对轴的参数,然后再注册该设备。
如果不设置这些参数,input子系统将使用默认值,这可能导致无法正确处理输入事件。
所以,input_set_abs_params()函数的使用时机是在注册绝对轴输入设备前,用来设置每个绝对轴的重要参数,如最小值、最大值、误差范围等。
正确设置这些参数对于标定绝对轴输入非常重要,否则无法识别和处理精确的输入事件。
另外,这些参数可以在注册后 runtime 重新调整,以实现标定、校准等功能。

3.2.14 input_alloc_absinfo

input_alloc_absinfo()函数用来为一个input_dev对象分配absinfo数组。
原型:

#include <linux/input.h>
void input_alloc_absinfo(struct input_dev *dev);

参数

  • dev: 对应的input_dev对象

用法:
对于支持绝对轴的输入设备,input子系统会为其维护一个absinfo数组,每个元素对应一个绝对轴,用来保存相关参数信息。
调用input_alloc_absinfo()函数可以为一个input_dev对象分配这个absinfo数组。如果该对象已经拥有一个absinfo数组,则此函数不会执行任何操作。
一般来说,驱动不需要直接调用input_alloc_absinfo(),因为:

  1. input_register_device()在成功注册一个input_dev对象时,会自动为其分配absinfo数组;
  2. input_allocate_device()在分配一个新的input_dev对象时,也会自动分配absinfo数组。
    所以,除非驱动在注册input_dev对象前就需要访问其absinfo数组,否则不需要显式调用input_alloc_absinfo()。
    示例:
struct input_dev *input_dev;

input_dev = input_allocate_device();  /* 会自动分配absinfo数组 */

input_set_abs_params(input_dev, ABS_X, 0, 1024, 0, 0); 
input_set_abs_params(input_dev, ABS_Y, 0, 768, 0, 0);

input_register_device(input_dev);   /* 也会分配absinfo数组 */ 

如上代码所示,input_allocate_device()和input_register_device()都会自动分配absinfo数组,所以驱动不需要显式调用input_alloc_absinfo()。

4 、input驱动-按键实例

根据input子系统的框架,对比硬编码方式所编写的按键驱动(见前几章),input框架的驱动结构将会变得简洁很多。
1、首先,硬编码方式下的file_operations结构体所关联的所有open、read、write等操作函数都不需要了,系统已帮我们完成。
2、不需要创建设备号,不需要创建设备文件。

4.1 设备树节点信息

在这里插入图片描述

在这里插入图片描述

4.2 驱动代码

/*************************************************************************
	> File Name: key-input.c
	> Author: maohm
	> Created Time: Sun 11 Jun 2023 09:37:19 AM CST
 ************************************************************************/

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/cdev.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/input.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/uaccess.h>


struct key_dev_t{
    struct input_dev *pidev;
    int gpio;
    int irqno;
};

struct key_dev_t *pgdev = NULL;


irqreturn_t  key_irq_handle(int no , void *arg){
    struct key_dev_t *pdev =(struct key_dev_t *)arg;
    int status1 = gpio_get_value(pdev->gpio);
    printk("driver:gpio -> %d , value = %d \n", pdev->gpio, status1);
    /*中断处理程序里面实现对事件类型及事件代码和值的提交。*/
    if (status1){
        input_event(pdev->pidev , EV_KEY , KEY_2 , 0);
    }else{
        input_event(pdev->pidev, EV_KEY , KEY_2 ,1);
        
    }
    /*完成事件的同步,发送事件给input核心*/
    input_sync(pdev->pidev);
    
    return IRQ_HANDLED;

}

int __init key2_init(void){
    int ret = 0;

    /*读取设备树节点信息*/
    struct device_node *pnode = NULL  ;  //设备树结点变量
    pnode = of_find_node_by_path("/key2_node");
    if (!pnode){
        printk("driver:fine node key2_node failed\n");
        return -1;
    }
    /*创建全局的数据结构变量*/
    pgdev = (struct key_dev_t*)kmalloc(sizeof(struct key_dev_t),GFP_KERNEL);
    if (!pgdev){
        printk("driver: kmalloc for struct key_dev_t failed\n");
        return -1;
    }

    /*读取设备树属性*/
    pgdev->gpio = of_get_named_gpio(pnode , "key2_gpio", 0);
    pgdev->irqno = irq_of_parse_and_map(pnode,0);

    /*创建设备对象input_dev,并注册*/
    pgdev->pidev = input_allocate_device();

    set_bit(EV_KEY , pgdev->pidev->evbit);  //设置事件类型
    set_bit(KEY_2 , pgdev->pidev->keybit);  //设置事件码

    ret = input_register_device(pgdev->pidev);

    ret = request_irq(pgdev->irqno, key_irq_handle , IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING , "key2" , pgdev);

    if (ret){
        printk("driver:request_irq failed\n");
        input_unregister_device(pgdev->pidev);
        input_free_device(pgdev->pidev);
        kfree(pgdev);
        pgdev = NULL;
        return -1;
    }
    return 0;
    
}

void __exit key2_exit(void){
    free_irq(pgdev->irqno , pgdev);
    input_unregister_device(pgdev->pidev);
    input_free_device(pgdev->pidev);
    kfree(pgdev);
    pgdev = NULL;

}

module_init(key2_init);
module_exit(key2_exit);
MODULE_LICENSE("GPL");


4.3 应用程序测试代码

/*************************************************************************
	> File Name: read-key.c
	> Author: maohm
	> Created Time: Sun 11 Jun 2023 04:28:48 PM CST
 ************************************************************************/

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>


int main(int argc , char **argv){
    int fd = -1;
    struct input_event evt;

    if (argc < 2){
        printf("app:Argument is too few . \n");
        return -1;
    }

    //打开设备文件,该文件需到/dev/input/目录中去查找
    fd = open(argv[1] , O_RDONLY);
    if (fd<0){
        printf("app:open %s failed.\n",argv[1]);
        return -1;

    }

    while (1){
        
        read(fd, &evt , sizeof(evt));
        printf("app :  event type is %d\n", (int)evt.type);
        printf("app : event code is %d\n",  (int )evt.code);
        printf("app : event value is %d \n ",(int)evt.value);
    }
    close(fd);
    fd = -1;
    return 0;
}

4.4 Makefile



CUR_DIR := $(shell pwd)



ifeq ($(filename),)

ifeq ($(KERNELRELEASE), )

ifeq ($(ARCH),arm)
KERNEL_DIR := /home/mao/linux/linux-3.14

ROOTFS_DIR := /opt/4412/rootfs

else

KERNEL_DIR := /lib/modules/$(shell uname -r)/build

endif


all :
	$(MAKE) -C  $(KERNEL_DIR) M=$(CUR_DIR) modules

install:
	#$(MAKE) -C $(KERNEL_DIR) M=$(CUR_DIR)	INSTALL_MOD_PATH=$(ROOTFS_DIR) modules_install
	cp *.ko $(ROOTFS_DIR)/drv -rf
clean :
	make -C  $(KERNEL_DIR) M=$(CUR_DIR) clean
	


else

obj-m += key-input.o



endif

else

ifeq  ($(ARCH),arm)
GCC_DIR := ~/linux/toolchain/gcc-4.6.4/bin/arm-none-linux-gnueabi-
ROOTFSDIR := /opt/4412/rootfs/work
else
GCC_DIR = /usr/bin/
ROOTFSDIR = $(shell pwd)/work/
endif

all:
	$(GCC_DIR)gcc $(filename).c -o $(filename).elf 
	sudo mv -f  $(filename).elf $(ROOTFSDIR)
	cp -rf ./load.sh $(ROOTFSDIR)

endif

5、mpu6050改用input框架驱动实例

5.1 定时操作相关函数

5.1.1 struct delayed_work

struct delayed_work结构体用来表示一个延迟工作(delayed work),它用于Linux内核定时执行某个工作的场景。
定义在<linux/workqueue.h>头文件中,如下:

struct delayed_work {
    struct work_struct work;
    struct timer_list timer;
};

它包含两个成员:

  • work: 一个正常的work对象,用于实际执行的工作
  • timer: 一个定时器,用于实现延迟执行

主要操作函数有:

  • INIT_DELAYED_WORK(): 初始化一个delayed_work对象
  • queue_delayed_work(): 将延迟工作入队,等待定时器超时后执行
  • cancel_delayed_work(): 取消一个延迟工作
    用法:
    delayed_work对象用于在指定延迟后自动执行某个工作。步骤如下:
  1. 定义并初始化一个delayed_work对象;
struct delayed_work my_work;
INIT_DELAYED_WORK(&my_work, my_work_func);
  1. 调用queue_delayed_work(),指定延迟时间和工作队列,将其入队;
queue_delayed_work(wq, &my_work, msecs_to_jiffies(2000));
  1. 等待定时器超时;
  2. 核心自动执行my_work对象中的work,进入my_work_func工作函数;
  3. 如果需要取消该延迟工作,调用cancel_delayed_work()。

示例:

void my_work_func(struct work_struct *work)
{
    pr_info("工作函数被调用!\n");
}

/* 延迟2秒执行my_work_func() */ 
queue_delayed_work(wq, &my_work, msecs_to_jiffies(2000));

/* 1秒后取消该延迟工作 */  
schedule_timeout_interruptible(msecs_to_jiffies(1000));  
cancel_delayed_work(&my_work);

delayed_work机制使内核可以在指定的延迟后自动执行某段工作,这在需要定时操作或延迟触发的场景下非常有用。
所以,struct delayed_work用于表示一个需要延迟后执行的工作,包含定时器和实际工作两个部分;通过queue_delayed_work()入队并等待定时器超时后自动执行工作,cancel_delayed_work()可以取消一个延迟工作。
它实现了Linux内核的延迟作业机制,在需要定时或延迟执行某个操作的场景下非常实用。

5.1.2 struct work_struct

work_struct结构体用来表示一个工作(work)。它包含一个工作函数和相关的数据,用于 Linux 内核实现工作队列(workqueue)机制。

#include <linux/workqueue.h>
struct work_struct {
	atomic_long_t data;
	struct list_head entry;
	work_func_t func;
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

typedef void (*work_func_t)(struct work_struct *work);

主要成员:

  • data:工作数据,其访问需要互斥
  • entry:用于将该work项加入列表
  • func:工作函数指针,实际要执行的函数typedef void (*work_func_t)(struct work_struct *work);
  • lockdep_map:用于lock验证,调试使用

工作队列机制的主要步骤:

  1. 定义一个work_struct,如:
struct work_struct my_work;
  1. 初始化并指定工作函数,如:
INIT_WORK(&my_work, my_work_func);
  1. 将该work入队,入用户态或内核空间的工作队列,如:
queue_work(my_wq, &my_work);  /* 入用户态工作队列 */
queue_work(system_wq, &my_work); /* 入内核工作队列 */
  1. 内核会提取工作队列中的work,并调用其指定的工作函数;
  2. 如果需要取消一个未执行的work,调用cancel_work()。

示例:

void my_work_func(struct work_struct *work)  
{
    pr_info("工作函数被调用!\n");
}

struct work_struct my_work;
INIT_WORK(&my_work, my_work_func);

/* 将work入queue,之后会自动执行my_work_func() */
queue_work(my_wq, &my_work);  
work_struct实现了异步工作执行的机制:可以将需要运行的工作函数入队,稍后由内核自动提取并执行。
这避免了直接在调用线程中运行该函数可能带来的睡眠或调度延迟等问题。

5.1.3 INIT_DELAYED_WORK

#include <linux/workqueue.h>
#define INIT_DELAYED_WORK(_work, _func)	__INIT_DELAYED_WORK(_work, _func, 0)

功能伪代码为:

#define INIT_DELAYED_WORK(_work, _func)                        \
    do {                                    \
        INIT_WORK(&(_work)->work, (_func));            \
        init_timer(&(_work)->timer);                    \
        (_work)->timer.function = delayed_work_timer_fn;    \
        (_work)->timer.data = (_work);            \
    } while (0)

它接受两个参数:

  • _work: delayed_work对象的指针
  • _func: work对象中实际要执行的函数。
  • _func函数指针的原型为: void (*_func)(struct work_struct *work);它指向的函数接受一个struct work_struct *work参数,返回类型为void。

该宏的作用是:

  1. 调用INIT_WORK()宏初始化_work->work,指定_func工作函数;
  2. 调用init_timer()初始化_work->timer定时器;
  3. 将_work->timer定时器的回调函数设置为delayed_work_timer_fn();
  4. 将_work对象指针作为_work->timer定时器的data;

之后,可以调用queue_delayed_work()将该delayed_work入队,等待定时器超时后work对象会自动执行_func工作函数。
举例:

void my_work_func(struct work_struct *work)
{
    pr_info("工作函数!\n");
}

struct delayed_work my_work;
INIT_DELAYED_WORK(&my_work, my_work_func);  /* 初始化my_work */

queue_delayed_work(wq, &my_work, msecs_to_jiffies(2000));   /* 延迟2秒入队 */

如上,我们定义了一个delayed_work对象my_work,然后调用INIT_DELAYED_WORK宏初始化它,指定工作函数my_work_func。
之后,通过queue_delayed_work()将其入队,2秒后my_work_func()函数会被自动调用。

5.1.4 queue_delayed_work

#include <linux/workqueue.h>
static inline bool queue_delayed_work(struct workqueue_struct *wq,
				      struct delayed_work *dwork,
				      unsigned long delay)
{
	return queue_delayed_work_on(WORK_CPU_UNBOUND, wq, dwork, delay);
}

参数:

  • wq: 工作队列(workqueue)对象
  • dwork: delayed_work对象
  • delay: 延迟时间,以jiffies为单位

返回值:

  • true: 入队成功
  • false: 已有相同的delayed_work在队列中,本次入队被忽略

用法:
queue_delayed_work()函数将一个延迟工作dwork入队,指定要延迟的时间delay后自动执行。步骤如下:

  1. dwork必须已经通过INIT_DELAYED_WORK()或类似函数初始化过;
  2. 调用queue_delayed_work(),传入工作队列wq、延迟工作dwork和延迟时间delay;
  3. dwork被添加到工作队列wq的延迟工作列表中,等待delay时间;
  4. delay时间后,dwork被自动取出并执行;
  5. dwork中的work被调用,进入初始化时指定的工作函数;
  6. 如果在delay时间内需要取消该延迟工作,调用cancel_delayed_work()。
    示例:
void my_work_func(struct work_struct *work) 
{
    pr_info("工作函数被调用!\n");
}

struct delayed_work my_work;
INIT_DELAYED_WORK(&my_work, my_work_func);

queue_delayed_work(wq, &my_work, msecs_to_jiffies(2000));  /* 延迟2秒入队 */

上述代码初始化了一个延迟工作my_work,指定其工作函数为my_work_func,然后通过queue_delayed_work()将其入队,延迟2秒后my_work_func()会被调用。


5.1.5 schedule_delayed_work

schedule_delayed_work()函数用来为一个延迟工作(delayed_work)安排延迟执行时间。

#include <linux/workqueue.h>
static inline bool schedule_delayed_work(struct delayed_work *dwork,
					 unsigned long delay)
{
	return queue_delayed_work(system_wq, dwork, delay);
}

参数:

  • dwork: 待延迟的延迟工作对象
  • delay: 延迟时间,以jiffies为单位

返回值:

  • true: 延迟工作成功入队
  • false: 已有相同的delayed_work在队列中,本次操作被忽略

用法:
schedule_delayed_work()函数用于将一个延迟工作dwork入队system_wq内核工作队列,指定delay时间后被执行。

具体步骤如下:

  1. dwork必须已经通过INIT_DELAYED_WORK()初始化;
  2. 调用schedule_delayed_work(),传入延迟工作dwork和延迟时间delay;
  3. dwork被添加到system_wq内核工作队列的延迟工作列表,等待delay时间;
  4. delay时间后,dwork会被自动取出并执行;
  5. dwork中的定时器回调函数delayed_work_timer_fn()被调用,进而执行其中的work,调用INIT_DELAYED_WORK()初始化时指定的工作函数;
  6. 如果需要在这段时间内取消该延迟工作,调用cancel_delayed_work(dwork)。
    该函数实际上是对queue_delayed_work(system_wq, dwork, delay)的封装,它将延迟工作dwork添加到系统内核工作队列system_wq,并指定延迟时间delay。

示例:

void my_work_func(struct work_struct *work)  
{
    pr_info("工作函数被调用!\n");
}

struct delayed_work my_work;
INIT_DELAYED_WORK(&my_work, my_work_func);  

schedule_delayed_work(&my_work, msecs_to_jiffies(2000));
/* 延迟2秒将my_work添加到system_wq并执行 */

如上,我们初始化了一个延迟工作my_work,并通过schedule_delayed_work()函数将其添加到system_wq内核工作队列,2秒后my_work_func()会被调用。


5.1.6 schedule_timeout_interruptible

schedule_timeout_interruptible()函数用来让出CPU,进入睡眠状态一定时间或直到被中断为止。

#include <linux/sched.h>
signed long schedule_timeout_interruptible(signed long timeout);

参数:

  • timeout: 睡眠时间,以jiffies为单位。如果为0,则让出CPU但不睡眠;如果为MAX_SCHEDULE_TIMEOUT,则一直睡眠直到被唤醒。

返回值:

  • 剩余时间:如果被信号中断唤醒,返回剩余未睡眠的时间;
  • 0: 如果时间已到或被非信号中断唤醒;
  • -ERESTARTSYS: 如果被信号中断唤醒且该信号的默认动作是重新运行系统调用。

机制
schedule_timeout_interruptible()函数用于让出CPU,使调用线程进入睡眠状态一定时间。用法如下:

  1. 调用该函数,传入需要睡眠的时间timeout;
  2. 当前线程会释放CPU,进入睡眠状态;
  3. 如果在timeout时间内没有被中断唤醒,线程会自动苏醒;
  4. 如果在timeout时间内收到信号并被中断唤醒,函数会提前返回,返回剩余未睡眠的时间;
  5. 被非信号中断唤醒时,返回0;
  6. 如果信号的默认动作是重新运行系统调用,返回-ERESTARTSYS。

该函数用于主动让出CPU,进入睡眠一段时间。常见用途有:

  1. 实现定时等待;
  2. 进程等待某事件发生或条件达成;
  3. 等待其他进程完成某任务;
  4. 等等。

示例:

timeout = schedule_timeout_interruptible(msecs_to_jiffies(2000));
/* 睡眠2秒,或者被信号中断唤醒 */  

if (timeout > 0) 
    printk("提前被中断唤醒,剩余时间:%ld jiffies\n", timeout);
else 
    printk("2秒时间已到或被非信号中断唤醒\n");

如上,我们让调用线程睡眠2秒,如果在这期间收到信号并被中断唤醒,就打印剩余时间并提前返回,否则就打印睡眠已完成的提示。


5.1.7 cancel_delayed_work

cancel_delayed_work()函数用来取消一个延迟工作(delayed_work)。

#include <linux/workqueue.h>
bool cancel_delayed_work(struct delayed_work *dwork);

参数:

  • dwork: 待取消的延迟工作对象

返回值:

  • true: 延迟工作成功取消
  • false: 延迟工作已完成或正在执行,无法取消

用法:
当一个延迟工作入队后,会在指定的延迟时间后被自动执行。如果在这段延迟时间内需要取消该工作,可以调用cancel_delayed_work()函数。

步骤如下:

  1. 延迟工作dwork必须已通过queue_delayed_work()函数入队;
  2. 调用cancel_delayed_work(dwork)函数取消该延迟工作;
  3. 如果取消成功(返回true),则dwork不会被执行;
  4. 如果返回false,表示dwork已超时执行或正在执行,无法取消。

示例:

void my_work_func(struct work_struct *work) 
{
    pr_info("工作函数!\n");
}

struct delayed_work my_work;
INIT_DELAYED_WORK(&my_work, my_work_func);  

queue_delayed_work(wq, &my_work, msecs_to_jiffies(2000));

/* 1秒后取消该延迟工作 */  
schedule_timeout_interruptible(msecs_to_jiffies(1000));  
if (cancel_delayed_work(&my_work))
    pr_info("取消成功!\n");
else 
    pr_info("无法取消!\n"); 

如上,我们初始化了一个延迟工作my_work, 2秒后会执行工作函数my_work_func。
但我们在1秒后调用cancel_delayed_work取消该工作,此时返回true表示成功取消。
如果我们的取消函数调用时间 > 2秒,就会返回false,表示my_work已超时执行,无法取消。

5.2 设备树

接下来的代码,涉及到设备树的操作。对应的设备树节点如下。
在这里插入图片描述

5.3 用input框架实现mpu6050数值读取实例

\qquad 利用上一章的【嵌入式环境下linux内核及驱动学习笔记-(15-1)例程】的第3.2节的实例。采用input框架来改写这个实例。

\qquad 由于这个实例是读取mpu6050输出的6个值。分别是X、Y、Z加整度和角速度,以及温度值。由于这些值是一次性输出,因此在INPUT框架中考虑的事件类型与事件码时,就不能用键事件EV_KEY了。正好可以用EV_ABS绝对值事件。绝对值事件。
\qquad 由于mpu6050的数值是一直在产生,如果读的频率远小于数据产生的速度,则可以不用中断方式触发事件处理,而是采用定时任务的方式。

\qquad 另外,从框架的结构上看,这个驱动同时使用了i2c驱动框架–用于mpu6050数据的读取;使用了input驱动框架–用于将数据事件提交。因此这个程序实际结合了这两个驱动框架的优势。即实现了总线式框架的driver与设备的分离,又不用开发者自已实现file_operations的操作函数。所以整个驱动实现变得很简洁,高效。

5.3.1 应用层测试代码

/*************************************************************************
	> File Name: show-mpu.c
	> Author: maohm
	> Created Time: Sun 11 Jun 2023 05:08:04 PM CST
 ************************************************************************/

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/input.h>
#include <unistd.h>
#include <fcntl.h>


int main(int argc , char **argv){
    int fd = -1;
    struct input_event evt;
    if(argc < 2){
        printf("app : Argument is too few .\n");
        return -1;
    }

    /*open device file */
    fd = open(argv[1] ,  O_RDONLY );
    if (fd < 0){
        printf("app : open %s failed.\n",argv[1]);
        return -1;
    }

    /*read mpu6050*/
    while (1){
        read(fd,&evt,sizeof(evt));
    
        if (evt.type == EV_ABS){
            switch(evt.code){
                case ABS_X:
                    printf("app : Accel-x: %d \n" , evt.value);
                    break;
                case ABS_Y:
                    printf("app : Accel-y: %d \n" , evt.value);
                    break;
                case ABS_Z:
                    printf("app : Accel-z: %d \n" , evt.value);
                    break;

                case ABS_RX:
                    printf("app : Gyro-x: %d \n" , evt.value);
                    break;
                case ABS_RY:
                    printf("app : Gyro-y: %d \n" , evt.value);
                    break;
                case ABS_RZ:
                    printf("app : Gyro-z: %d \n" , evt.value);
                    break;
                case ABS_MISC:
                    printf("app : Temp: %d \n" , evt.value);
                    break;
            }
        }


    }
    close(fd);
    fd = -1;
    return 0;
}

./show-mpu.elf /dev/input/event1

5.3.2 驱动代码

/*mpu6050-input.c*/
/*本驱动程序,实际是由两个框架组成的,一个是i2c设备驱动框架,一个是input驱动框架。
* 由于是i2c驱动框架,则__init() __exit() XX_probe()  XX_remove()的型式就确定了,并进行了
* 设备树的匹配。
* 又由于使用了input驱动框架,所以在probe里就无需去创建设备号dev_t,创建struct cdev,
* create_deivce()创建设备文件节点等事项。而是直接在probe()创建input_dev,并注册后就可以了。*/

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/poll.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/io.h>
#include <asm/uaccess.h>
#include <asm/atomic.h>
#include <linux/of.h>
#include <linux/input.h>




/**以下为mpu6050中的各个寄存器地址标号*/

#define  SMPLRT_DIV  0x19   //采样率分配器配置寄存器
#define  CONFIG  0x1A       //配置FSYNC 和 DLPF 寄存器
#define  GYRO_CONFIG  0x1B  //配置陀螺仪的寄存器
#define  ACCEL_CONFIG  0x1C //配置加速度计的寄存器

#define  ACCEL_XOUT_H  0x3B  //X向加速度值的高8位
#define  ACCEL_XOUT_L  0x3C  //X向加速度值的低8位
#define  ACCEL_YOUT_H  0x3D  //Y向加速度值的高8位
#define  ACCEL_YOUT_L  0x3E  //Y向加速度值的低8位
#define  ACCEL_ZOUT_H  0x3F  //Z向加速度值的高8位
#define  ACCEL_ZOUT_L  0x40  //Z向加速度值的低8位
#define  TEMP_OUT_H  0x41    //温度的高8位
#define  TEMP_OUT_L  0x42    //温度的氏8位
#define  GYRO_XOUT_H  0x43   //x轴角速度的高8位
#define  GYRO_XOUT_L  0x44   //x轴角速度的低8位
#define  GYRO_YOUT_H  0x45   //Y轴角速度的高8位
#define  GYRO_YOUT_L  0x46   //Y轴角速度的低8位
#define  GYRO_ZOUT_H  0x47   //Z轴角速度的高8位
#define  GYRO_ZOUT_L  0x48   //Z轴角速度的低8位

#define  PWR_MGMT_1    0x6B  //电源管理寄存器



struct  mpu6050_dev        //自定义数据结构,
{
    struct input_dev *pinput;  //代表一个input设备对象,用于input驱动框架
    struct  i2c_client  *pclient;    //clinet设备结构体
    struct delayed_work work; //延时工作结构体

};

struct  mpu6050_dev  *pgmydev  =  NULL;



/****************************************************/
/*直接驱动i2c实现读一个字节的操作函数*/
int mpu6050_read_byte(struct i2c_client *pclt , unsigned char reg){
    int ret = 0;
    char txbuf[1] = {reg};
    struct i2c_msg msg[2] ={
        {pclt->addr , 0 , 1, txbuf},      //发送i2c地址,发送寄存器地址标号
        {pclt->addr ,I2C_M_RD , 1, txbuf}, //发送地址及读标志,读数据到rxbuf中
    };
    ret = i2c_transfer(pclt->adapter , msg , ARRAY_SIZE(msg));
    if (ret < 0){
        printk("driver: ret = %d, in mpu6050_read_byte\n",ret);
        return ret;
    }
    return txbuf[0];
}
/*直接驱动i2c实现写一个字节的操作函数*/
int mpu6050_write_byte(struct i2c_client *pclt , unsigned char reg , unsigned char val){
    int ret = 0;
    char txbuf[2] = {reg , val} ; //寄存器标号,寄存器值
    struct i2c_msg msg[1] = {
        {pclt->addr , 0 , 2 , txbuf},    
    };

    ret = i2c_transfer(pclt->adapter , msg , ARRAY_SIZE(msg));
    if (ret <0){
        printk("driver: ret= %d , in mpu6050_write_byte\n",ret);
        return ret;
    }
    return 0;
}

/*定时器的回调函数,在这里去上报input_event事件数据*/
void mpu6050_work_func(struct work_struct *pwk){

    /*这里函数的参数struct work_struct *pwk是在mpu6050_work_func做为回调被调用时传递进来的。
    * 而这个pwk指向的结构体又是在INIT_DELAYED_WORK()时创建的,这个pwk指针指向的结构体变量
    * 是struct delayed_work work对象 中的第一个成员。
    * 这也就意味着,pwk的指针就是我们创建pgmydev结构体成员work的指针,所以才有下面这个
    * container_of函数第一个参数,用(struct delayed_work*)对pwk进行强转*/
    struct mpu6050_dev *pmydev = container_of((struct delayed_work *)pwk , struct mpu6050_dev, work);

    unsigned short ax = 0;
    unsigned short ay = 0;
    unsigned short az = 0;
    unsigned short gx = 0;
    unsigned short gy = 0;
    unsigned short gz = 0;
    unsigned short temp = 0;



    //以下为从mpu6050中读取值,并把值合成事件提交给内核层
    ax = mpu6050_read_byte(pmydev->pclient,ACCEL_XOUT_L);
    ax += mpu6050_read_byte(pmydev->pclient,ACCEL_XOUT_H) << 8;
    input_report_abs(pmydev->pinput , ABS_X , ax);

    ay = mpu6050_read_byte(pmydev->pclient,ACCEL_YOUT_L);
    ay += mpu6050_read_byte(pmydev->pclient,ACCEL_YOUT_H) << 8;
    input_report_abs(pmydev->pinput , ABS_Y , ay);
   
    az = mpu6050_read_byte(pmydev->pclient,ACCEL_ZOUT_L);
    az += mpu6050_read_byte(pmydev->pclient,ACCEL_ZOUT_H) << 8;
    input_report_abs(pmydev->pinput , ABS_Z , az);

    gx = mpu6050_read_byte(pmydev->pclient, GYRO_XOUT_L);
    gx += mpu6050_read_byte(pmydev->pclient, GYRO_XOUT_H) << 8;
    input_report_abs(pmydev->pinput , ABS_RX, gx);
    

    gy = mpu6050_read_byte(pmydev->pclient, GYRO_YOUT_L);
    gy += mpu6050_read_byte(pmydev->pclient, GYRO_YOUT_H) << 8;
    input_report_abs(pmydev->pinput , ABS_RY, gy);

    gz = mpu6050_read_byte(pmydev->pclient, GYRO_ZOUT_L);
    gz += mpu6050_read_byte(pmydev->pclient, GYRO_ZOUT_H) << 8;
    input_report_abs(pmydev->pinput , ABS_RZ, gz);

    temp = mpu6050_read_byte(pmydev->pclient , TEMP_OUT_L);
    temp += mpu6050_read_byte(pmydev->pclient , TEMP_OUT_H) << 8;
    input_report_abs(pmydev->pinput , ABS_MISC, temp);

    //提交上述所有事件。
    input_sync(pmydev->pinput);
    //再次让延时工作在1秒后启动
    schedule_delayed_work(&pgmydev->work , msecs_to_jiffies(1000));
}




/*mpu6050的初始化设置函数*/
void init_mpu6050(struct i2c_client *pclt){
    mpu6050_write_byte(pclt ,PWR_MGMT_1,0x00);
    mpu6050_write_byte(pclt,SMPLRT_DIV,0X07);
    mpu6050_write_byte(pclt,CONFIG,0x06);
    mpu6050_write_byte(pclt,GYRO_CONFIG,0xF8);
    mpu6050_write_byte(pclt,ACCEL_CONFIG,0x19);
}




/*重要的驱动设备构造函数与驱动初始化函数*/
int mpu6050_probe(struct i2c_client *pclt , const struct i2c_device_id *pid){
    int ret = 0;
    pgmydev =  (struct  mpu6050_dev*)kmalloc(sizeof(struct mpu6050_dev), GFP_KERNEL);
    if (!pgmydev){
        printk("driver:kmalloc failed\n");
        return -1;
    }

    memset(pgmydev , 0 , sizeof(struct mpu6050_dev));
    
    pgmydev->pclient = pclt;  //获得匹配好的i2c设备指针。

    init_mpu6050(pgmydev->pclient);
    
    /*创建一个struct input_dev结构体*/
    pgmydev->pinput = input_allocate_device();
    /*设定input_dev的事件类型为EV_ABS绝对值事件*/
    set_bit(EV_ABS , pgmydev->pinput->evbit);
    /*分别把7个绝对值事件码及最大最小等参数写入input_dev中的成员absinfo数组里*/
    input_set_abs_params(pgmydev->pinput , ABS_X , -32768 , 32768 , 0, 0);
    input_set_abs_params(pgmydev->pinput , ABS_Y , -32768 , 32768 , 0, 0);
    input_set_abs_params(pgmydev->pinput , ABS_Z , -32768 , 32768 , 0, 0);
    input_set_abs_params(pgmydev->pinput , ABS_RX , -32768 , 32768 , 0, 0);
    input_set_abs_params(pgmydev->pinput , ABS_RY , -32768 , 32768 , 0, 0);
    input_set_abs_params(pgmydev->pinput , ABS_RZ , -32768 , 32768 , 0, 0);
    input_set_abs_params(pgmydev->pinput , ABS_MISC , -32768 , 32768 , 0, 0);
    /*注册input_dev设备到内核中*/
    ret = input_register_device(pgmydev->pinput);

    if(ret){
        printk("driver: input_register_device failed\n");
        input_free_device(pgmydev->pinput);
        pgmydev->pinput = NULL;
        kfree(pgmydev);
        pgmydev = NULL;
        return -1;
    }

    /*初始化延时工作对象,并启动延时工作*/
    INIT_DELAYED_WORK(&pgmydev->work , mpu6050_work_func);
    schedule_delayed_work(&pgmydev->work , msecs_to_jiffies(1000));

    return 0;

}

/*驱动的移除函数*/
int mpu6050_remove(struct i2c_client *plct){
    cancel_delayed_work(&pgmydev->work);
    input_unregister_device(pgmydev->pinput);
    input_free_device(pgmydev->pinput);
    pgmydev->pinput = NULL;
    kfree(pgmydev);
    pgmydev = NULL;
    return 0;
}


struct i2c_device_id  mpu6050_ids[]={
    {"mpu6050-1" , 0},
    {},
};

struct of_device_id mpu6050_dts[]={
    {.compatible = "invensense,mpu6050"}, 
    {},
};

/*驱动对象的数据结构体,代表了驱动对象,挂接到驱动链表中*/
struct i2c_driver mpu6050_driver = {
    .driver ={
        .name = "mpu6050-1",
        .owner = THIS_MODULE,
        .of_match_table = mpu6050_dts,
    },
    .probe = mpu6050_probe,
    .remove = mpu6050_remove,
    .id_table = mpu6050_ids,
};

#if 0
int __intit  mpu6050_driver_init(void){
    i2c_add_driver(mpu6050_driver);
}

void __exit mpu6050_driver_exit(void){
    i2c_del_driver(mpu6050_driver);
}
module_init(mpu6050_driver_init);
module_exit(mpu6050_driver_exit);
#else
module_i2c_driver(mpu6050_driver);

#endif

MODULE_LICENSE("GPL");

insmod /dev/mpu6050-input.ko

5.3.3 Makefile



CUR_DIR := $(shell pwd)



ifeq ($(filename),)

ifeq ($(KERNELRELEASE), )

ifeq ($(ARCH),arm)
KERNEL_DIR := /home/mao/linux/linux-3.14

ROOTFS_DIR := /opt/4412/rootfs

else

KERNEL_DIR := /lib/modules/$(shell uname -r)/build

endif


all :
	$(MAKE) -C  $(KERNEL_DIR) M=$(CUR_DIR) modules

install:
	#$(MAKE) -C $(KERNEL_DIR) M=$(CUR_DIR)	INSTALL_MOD_PATH=$(ROOTFS_DIR) modules_install
	cp *.ko $(ROOTFS_DIR)/drv -rf
clean :
	make -C  $(KERNEL_DIR) M=$(CUR_DIR) clean
	


else

obj-m += mpu6050-input.o



endif

else

ifeq  ($(ARCH),arm)
GCC_DIR := ~/linux/toolchain/gcc-4.6.4/bin/arm-none-linux-gnueabi-
ROOTFSDIR := /opt/4412/rootfs/work
else
GCC_DIR = /usr/bin/
ROOTFSDIR = $(shell pwd)/work/
endif

all:
	$(GCC_DIR)gcc $(filename).c -o $(filename).elf 
	sudo mv -f  $(filename).elf $(ROOTFSDIR)
	cp -rf ./load.sh $(ROOTFSDIR)

endif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/27863.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LVS负载均衡群集部署——DR直接路由模式

这里写目录标题 一 、 LVS-DR 工作原理二、数据包流向分析三、LVS-DR 模式的特点四、ARP问题4.1 问题一&#xff1a;IP地址冲突4.2 问题二&#xff1a;第二次再有访问请求 五、部署LVS-DR集群5.1 配置Tomcat 多实例服务器5.2 配置web节点服务器配置web1节点服务器配置Nginx七层…

Flutter进阶篇-布局(Layout)原理

1、约束、尺寸、位置 overrideWidget build(BuildContext context) {return Scaffold(body: LayoutBuilder(builder: (context, constraints) {print("body约束:" constraints.toString());return Container(color: Colors.black,width: 300,height: 300,child: L…

MATLAB | 绘图复刻(九) | 泰勒图及组合泰勒图

有粉丝问我这个图咋画&#xff1a; 我一看&#xff0c;这不就泰勒图嘛&#xff0c;就fileexchange上搜了一下泰勒图绘制代码&#xff0c;但是有的代码比较新的版本运行要改很多地方&#xff0c;有的代码需要包含一些压缩包没并没有的别人写的函数&#xff0c;于是我干脆自己写了…

【MySQL 数据库】11、学习 MySQL 中的【锁】

目录 一、锁的概述与分类二、全局锁&#xff08;全库数据备份&#xff09;三、表级锁(1) 表锁(2) 元数据锁&#xff08;Meta Data Lock&#xff09;(3) 意向锁 四、行级锁(1) 行锁(2) 间隙锁&临键锁 一、锁的概述与分类 锁是计算机协调多个进程或线程并发访问某一资源的机…

国产Gauss 分布式数据库概述

一、前言 GaussDB 是华为2023年6月7日发布新一代分布式数据库&#xff0c;采用share-nothing架构&#xff0c;数据自动分片&#xff0c;通过GTM-Lite技术实现事务强一致&#xff0c;无中心节点性能瓶颈&#xff0c;是华为基于openGauss自主创新研发的一款分布式关系型数据库&am…

STM32CubeMX | 44 - 使用GPIO点亮单总线RGBLED

一、单总线RGBLED 1. 硬件连接 在DragonFly上有四个全彩灯相连: 其中RGB_LED连接到STM32的PB9引脚。 2. 单总线通信协议 单总线通信协议中,表示bit0和bit1的码型如下: 时序值如下: 驱动一个单总线RGBLED只需要传输24bit颜色数据即可(MSB,高位优先),格式如下(注意…

C++设计模式 - 创建型模式之工厂模式

文章目录 C设计模式 - 创建型模式之工厂模式接口和针对接口编程 1. 简单工厂模式适用场合UML代码示例 2. 工厂方法模式适用场合UML代码示例 3. 抽象工厂模式适用场合UML代码示例 总结 C设计模式 - 创建型模式之工厂模式 工厂模式属于创建型模式&#xff0c;大致可以分为三类&a…

力扣 209. 长度最小的子数组

一、题目描述 给定一个含有 n 个正整数的数组和一个正整数 target。 找出该数组中满足其和大于等于 target 的长度最小的连续子数组&#xff0c;并返回其长度。如果不存在符合条件的子数组&#xff0c;返回 0。 示例 1&#xff1a; 输入&#xff1a;target 7, nums [2,3,1…

Uni-app学习从0到1开发一个app——(3)简单小工程内容介绍

文章目录 工程文件 看看一个标准的hello微信小程序工程文件的组成和作用。 工程文件 可以参考官方教程&#xff1a;传送门 之前的文章有详细的开发环境介绍&#xff0c;传送门Uni-app学习从0到1开发一个app——(2)windowns环境搭配&#xff0c;这里我们先建一个简单的示例微信…

C++教程(06)——变量类型

C 变量类型 变量其实只不过是程序可操作的存储区的名称。C 中每个变量都有指定的类型&#xff0c;类型决定了变量存储的大小和布局&#xff0c;该范围内的值都可以存储在内存中&#xff0c;运算符可应用于变量上。 变量的名称可以由字母、数字和下划线字符组成。它必须以字母…

华为OD机试真题B卷 Java 实现【自守数】,附详细解题思路

一、题目描述 自守数是指一个数的平方的尾数等于该数自身的自然数。例如&#xff1a;252 625&#xff0c;762 5776&#xff0c;93762 87909376。 请求出n(包括n)以内的自守数的个数。 数据范围&#xff1a; 1≤n≤10000 二、输入描述 int型整数。 三、输出描述 n以内…

大话Stable-Diffusion-Webui-动手开发一个简单的stable-diffusion-webui(一)

文章目录 写在前面整体效果开发所需环境开发工具需要具备的知识Node安装更改npm包安装的目录设置npm镜像vscode安装创建vue项目代码编写项目先体验注意写在前面 stable-diffusion-webui(以下简称sd)项目通过FastAPI对外提供了一系列的api用于开发者二次开发或者集成到自己的…

005Mybatis返回值(ResultMap 一对多,多对多)

属性 id 应该总是指定一个或多个可以唯一标识结果的属性。 虽然&#xff0c;即使不指定这个属性&#xff0c;MyBatis 仍然可以工作&#xff0c;但是会产生严重的性能问题。 只需要指定可以唯一标识结果的最少属性。显然&#xff0c;你可以选择主键&#xff08;复合主键也可以…

MixQuery系列(一):多数据源混合查询引擎调研

背景 存储情况 当前的存储引擎可谓百花齐放,层出不穷。为什么会这样了?因为不存在One for all的存储,不同的存储总有不同的存储的优劣和适用场景。因此,在实际的业务场景中,不同特点的数据会存储到不同的存储引擎里。 业务挑战 然而异构的存储和数据源,却给分析查询带…

Vue中如何进行状态持久化(LocalStorage、SessionStorage)

Vue中如何进行状态持久化&#xff08;LocalStorage、SessionStorage&#xff09;&#xff1f; 在Vue应用中&#xff0c;通常需要将一些状态进行持久化&#xff0c;以便在用户关闭浏览器或刷新页面后&#xff0c;仍能保留之前的状态。常见的持久化方式包括LocalStorage和Sessio…

【软件环境安装部署】华为云服务器下 Docker 安装 MongoDB 以及 SpringBoot 整合 MongoDB 开发使用

文章目录 安装测试 MongoDB拉取镜像创建和启动容器登录mongo容器&#xff0c;并进入到【admin】数据库创建一个用户&#xff0c;mongo 默认没有用户连接mongo数据库测试数据库&#xff0c;插入一条语句测试数据库&#xff0c;查询刚才插入的语句查看所有数据库开放指定端口开放…

华为存储IA篇仿真器搭建

设备清单 编号 设备名 数量 备注 01 Windows系统主机 1台 为VMware提供安装位置 02 VMware软件 1份 提供存储仿真器的部署环境 03 仿真器文件 1份 用于部署estor虚拟机 【注意】&#xff1a;暂无注意事项 一、下载安装文件并配置虚拟机设备清单 1.1…

动态测试数据处理

分类 动态测试数据&#xff1a; 1、确定性数据&#xff1a;能够用明确的数学表达式进行描述的数据称为确定性数据。 Ⅰ、周期数据 Ⅱ、非周期数据 2、随机性数据&#xff1a;无法用明确的数学表达式表述&#xff1b;若在一个…

Qt详解实现TCP文件传输例子(文件下载和上传)附源码

网络通信我们用的很频繁&#xff0c;如文字&#xff0c;语音&#xff0c;文件&#xff0c;图片等&#xff0c;这个些传输方式都差不多 QT文件传输主要考验对传输的控制&#xff0c;还是需要点逻辑的&#xff0c;文件传输的大致框架如下 先看一下简单例子实现的效果&#xff08…

【伏羲八卦图】(PythonMatlab实现)

目录 1 与达尔文对话 2 与老子对话 2.1 Python实现 2.2 Matlab实现 1 与达尔文对话 140年前&#xff0c;1858年7月1日&#xff0c;达尔文在英伦岛发表了自己有关自然选择的杰出论文。他提出&#xff0c;生物的发展规律是物竞天择。经过物竞&#xff0c;自然界选择并存留最具…