Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统

文章目录

  • 前言
  • 什么是Linux输入设备
  • input 输入子系统
    • input输入子系统简介
    • 输入子系统设备驱动层实现原理
    • input_dev、input_handler、input_handle
      • input_dev
      • input_handler
      • input_handle
    • 为什么用input_handle来关联input_dev和input_handler而不将input_dev和input_handler直接对应呢?
    • 与软件设计有关的API函数
  • 查看源码Documentation文档说明
    • 驱动说明rotary-encoder - a generic driver for GPIO connected devices
    • 设备树说明 Rotary encoder DT bindings
  • 添加Rotary_Encoder驱动
  • 查看driver和device挂载
    • 查看平台总线设备信息
    • rotary_encoder驱动源码
  • input输入子系统调试方法
    • devices查看input设备
    • 调试代码
  • 结语
    • 电路方案
    • 设备树 驱动关系
    • input输入子系统

前言

项目中需要使用旋转编码器作为控制器,之前在STM32中使用过EC11旋转编码器

STM32CubeMX EC11旋转编码器普通IO口外部中断+定时器实现

STM32CubeMX EC11旋转编码器开发心路历程(encode模式 外部中断模式 普通IO口模式 定时器模式探索)

所以最初在imx6平台上也是想要延续这种设计思路,但是考虑到实际过程中使用两个中断判断直接写驱动来读取会导致漏掉旋转步数

因此没有使用直接使用中断来判断的方法

准备选用EC11模块(实际上是通过一块arm控制,然后用i2c通信),使用带i2c的EC11旋转编码器模块,例如这种
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第1张图片

使用这种模块的话,只需要通过i2c读取就可以了,这样就不会漏掉旋转的步数

在验证过程中,忽略了一个很重要的事情,没有查找Linux源码中关于旋转编码器的说法
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第2张图片
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第3张图片

make menuconfig中可以搜索到Rotary_Encoder相关的内核驱动,于是,顺着思路,在官网也可以看到相关的解释说明
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第4张图片
现在,旋转编码器的软件方案朝着如何使用Linux内核源码驱动方向展开

虽然有了方向,但是无论是百度还是谷歌,都发现没有相关 的更加详细的资料,全部都是一些官方的资料,对于零基础的我还是需要走很多路

在之前,对于Linux驱动,只是通过模仿,写一些简单的驱动程序,没有涉及到添加一个官方驱动和修改设备树并且使用,如果这篇文章能够发出来,说明摸索到了一套完整的实现方法。

也算是全网首发旋转编码器内核源码驱动(包括设备树)的添加和使用吧

什么是Linux输入设备

老规矩,虽然基础可能在代码中我们接触不到,但是了解其工作原理,分层机制,才是通用的方法!对其他的驱动使用才能有启发作用。本文全程使用旋转编码器为例进行讲解。

Linux输入设备总类繁杂,常见的包括有按键、键盘、触摸屏、鼠标、摇杆等等,他们本身就是字符设备,而linux内核将这些设备的共同性抽象出来,简化驱动开发建立了一个input子系统。子系统共分为三层,如图所示。
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第5张图片

input 输入子系统

input输入子系统简介

输入设备(如按键,键盘,触摸屏,鼠标等)是典型的字符设备,其一般的工作机制是底层在按键,触摸等动作发生时产生一个中断(或驱动通过timer定时查询),然后cpu通过SPI,I2C或者外部存储器总线读取键值,坐标等数据,放一个缓冲区,字符设备驱动管理该缓冲区,而驱动的read()接口让用户可以读取键值,坐标等数据。

注意:这里有个关键,input输入子系统本质上也是内部使用中断对按键进行处理,所以对于旋转编码器EC11也是这样,跟之前自己使用中断去写一个杂项驱动是一样的本质,也是“中断”这个关键词,帮助我摸索到了使用的方法

这里插播之前的一篇文章,配合使用,对于rotary_encoder驱动源码理解更加顺畅

Linux嵌入式驱动开发14——中断的原理以及按键中断的实现(tasklet中断下文)

在Linux中,输入子系统是由输入子系统设备驱动层、输入子系统核心层(Input Core)和输入子系统事件处理层(Event Handler)组成。

其中设备驱动层提供对硬件各寄存器的读写访问和将底层硬件对用户输入访问的响应转换为标准的输入事件,再通过核心层提交给事件处理层;而核心层对下提供了设备驱动层的编程接口,对上又提供了事件处理层的编程接口;而事件处理层就为我们用户空间的应用程序提供了统一访问设备的接口和驱动层提交来的事件处理。所以这使得我们输入设备的驱动部分不在用关心对设备文件的操作,而是要关心对各硬件寄存器的操作和提交的输入事件。

  • 驱动层和硬件相关,直接捕捉和获取硬件设备的数据信息等(包括触摸屏被按下、按下位置、鼠标移动、键盘按下等等),然后将数据信息报告到核心层。
  • 核心层负责连接驱动层和事件处理层,设备驱动(device driver)和处理程序(handler)的注册需要通过核心层来完成,核心层接收来自驱动层的数据信息,并将数据信息选择对应的handler去处理,最终handler将数据复制到用户空间

输入子系统设备驱动层实现原理

在Linux中,Input设备用input_dev结构体描述,定义在input.h中。设备的驱动只需按照如下步骤就可实现了。

1).在驱动模块加载函数中设置Input设备支持input子系统的哪些事件;

2).将Input设备注册到input子系统中;

3).在Input设备发生输入操作时(如:键盘被按下/抬起、触摸屏被触摸/抬起/移动、鼠标被移动/单击/抬起时等),提交所发生的事件及对应的键值/坐标等状态。

input_dev、input_handler、input_handle

input_dev

struct input_dev {
      void *private;
      const char *name;
      const char *phys;
      const char *uniq;
      struct inBITS(KEY_MAX)];      //按键事件支持的子事件类型
      unsigned long relbit[NBITS(REL_MAX)];
      unsigned long absbit[NBITS(ABS_Mput_id id;      //与input_handler匹配用的id
      unsigned long evbit[NBITS(EV_MAX)];            //设备支持的事件类型
      unsigned long keybit[NAX)];      //绝对坐标事件支持的子事件类型
      unsigned long mscbit[NBITS(MSC_MAX)];
      unsigned long ledbit[NBITS(LED_MAX)];
      unsigned long sndbit[NBITS(SND_MAX)];
      unsigned long ffbit[NBITS(FF_MAX)];
      unsigned long swbit[NBITS(SW_MAX)];
      int ff_effects_max;
      unsigned int keycodemax;
      unsigned int keycodesize;
      void *keycode;
      unsigned int repeat_key;
      struct timer_list timer;
      struct pt_regs *regs;
      int state;
      int sync;
      int abs[ABS_MAX + 1];
      int rep[REP_MAX + 1];
      unsigned long key[NBITS(KEY_MAX)];
      unsigned long led[NBITS(LED_MAX)];
      unsigned long snd[NBITS(SND_MAX)];
      unsigned long sw[NBITS(SW_MAX)];
      int absmax[ABS_MAX + 1];      //绝对坐标事件的最大键值
      int absmin[ABS_MAX + 1];      //绝对坐标事件的最小键值
      int absfuzz[ABS_MAX + 1];
      int absflat[ABS_MAX + 1];
      int (*open)(struct input_dev *dev);
      void (*close)(struct input_dev *dev);
      int (*accept)(struct input_dev *dev, struct file *file);
      int (*flush)(struct input_dev *dev, struct file *file);
      int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);
      int (*upload_effect)(struct input_dev *dev, struct ff_effect *effect);
      int (*erase_effect)(struct input_dev *dev, int effect_id);
      struct input_handle *grab;      //当前占有该设备的handle
      struct mutex mutex;      /* serializes open and close operations */
      unsigned int users;            //打开该设备的用户量
      struct class_device cdev;
      struct device *dev;      /* will be removed soon */
      int dynalloc;      /* temporarily */
      struct list_head      h_list;      //该链表头用于链接该设备所关联的input_handle
      struct list_head      node;      //该链表头用于将设备链接到input_dev_list
};

Input_dev是一个很强大的结构体,它把所有的input设备(触摸屏、键盘、鼠标等)的信息都考虑到了,对于触摸屏来说只用到它里面的一部分而已,尤其是加粗的部分,注意该结构体中最后两行定义的两个list_head结构体,list_head在/linux/list.h中有定义,深入跟踪

struct list_head {
      struct list_head *next, *prev;
};

该结构体内部并没有定义数据而只定义了两个指向本身结构体的指针,预先说明一下:

★ 所有的input device在注册后会加入一个input_dev_list(输入 设备链表)。

★ 所有的eventhandler在注册后会加入一个input_handler_list(输入处理程序链表),这里的list_head主要的作用是作为input_dev_list和input_handler_list的一个节点来保存地址。

Input_dev_list和input_handler_list之间的对应关系由input_handle结构体桥接,具体后面说明。

input_handler

struct input_handler {
      void *private;
      void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
      struct input_handle* (*connect)(struct input_handler *handler, struct input_dev *dev, struct input_device_id *id);
      void (*disconnect)(struct input_handle *handle);
      const struct file_operations *fops;      //提供给用户对设备操作的函数指针
      int minor;
      char *name;
      struct input_device_id *id_table;      //与input_dev匹配用的id
      struct input_device_id *blacklist;      //标记的黑名单
      struct list_head      h_list;            //用于链接和该handler相关的handle
      struct list_head      node;            //用于将该handler链入input_handler_list
};

input_handler顾名思义,它是用来处理input_dev的一个结构体获取回来的数据,相关的处理函数在结构里内部都有定义,最后两行定义的list_head结构体作用同input_dev所定义的一样,这里不再说明。

注:input_device_id结构体在/linux/mod_devicetable.h中有定义

input_handle

struct input_handle {
      void *private;
      int open;      //记录设备打开次数
      char *name;
      struct input_dev *dev;      //指向所属的input_dev
      struct input_handler *handler;      //指向所属的input_handler
      struct list_head      d_node;            //用于链入所指向的input_dev的handle链表
      struct list_head      h_node;            //用于链入所指向的input_handler的handle链表
};

可以看到input_handle中拥有指向input_dev和input_handler的指针

★ 即input_handle是用来关联input_dev和input_handler。

为什么用input_handle来关联input_dev和input_handler而不将input_dev和input_handler直接对应呢?

因为一个device可以对应多个handler,而一个handler也可处理多个device。就如一个触摸屏设备可以对应event handler也可以对应tseve handler。
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第6张图片

与软件设计有关的API函数

分配一个输入设备

Struct input_dev *input_allocate_device*(void); 

注册一个输入设备

Int input_register_device(struct input_dev *dev);

驱动实现-事件支持

Set_bit告诉inout子系统它支持哪些事件 
Set_bit(EV_KEY,button_dev.evbit) 
Struct input_dev中有两个成员,一个是evbit;一个是keybit.分别用来表示设备所支持的事件类型和按键类型。

事件类型

Linux中输入设备的事件类型有(这里只列出了常用的一些,更多请看linux/input.h中):EV_SYN 0x00 同步事件
EV_KEY 0x01 按键事件 
EV_REL 0x02 相对坐标 
EV_ABS 0x03 绝对坐标 
EV_MSC 0x04 其它 
EV_LED 0x11 LED 
EV_SND 0x12 声音 
EV_REP 0x14 Repeat 
EV_FF 0x15 力反馈

驱动实现-报告事件

Void input_event(struct input_dev *dev,unsigned int type,unsigned int code,int value);//报告指定type,code的输入事件 
Void input_report_key(struct input_dev *dev,unsigned int code,int value);//报告键值 
Void input_report_rel(struct input_dev *dev,unsigned int code,int value);//报告相对坐标 
Void input_report_abs(struct input_dev *dev,unsigned int code,int value);//报告绝对坐标 
Void input_sync(struct input_dev *dev);//报告同步事件 
在触摸屏驱动设计中,一次坐标及按下状态的整个报告过程如下: 
Input_report_abs(input_dev,ABS_X,x);//X坐标 
Input_report_abs(input_dev,ABS_Y,y);//Y坐标 
Input_report_abs(input_dev,ABS_PRESSURE,pres);//压力 
input_sync(struct input_dev *dev);//同步 

释放与注销设备

Void input_free_device(struct input_dev *dev); 
Void input_unregister_device(struct input_dev *);

对于上面这些关键的结构体,目的不是吃透,而是熟悉,明白input输入子系统常用的结构体是哪些,哪些结构体出现意味着什么,有一个熟悉的感觉就可以了。

有了一定的input输入子系统的基础之后,我们来看官方的文档。进行后面是实践操作。

查看源码Documentation文档说明

对于一个驱动,最好的说明就是官方的文档,在Linux的Documentation文件夹中,我们直接搜索ratary就可以找到驱动的说明还有设备树的说明
在这里插入图片描述
目录如图所示:
在这里插入图片描述
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第7张图片

驱动说明rotary-encoder - a generic driver for GPIO connected devices

对于这两个txt的说明文档,我也贴在了这里

rotary-encoder - a generic driver for GPIO connected devices
Daniel Mack <daniel@caiaq.de>, Feb 2009

0. Function
-----------

Rotary encoders are devices which are connected to the CPU or other
peripherals with two wires. The outputs are phase-shifted by 90 degrees
and by triggering on falling and rising edges, the turn direction can
be determined.

Some encoders have both outputs low in stable states, whereas others also have
a stable state with both outputs high (half-period mode).

The phase diagram of these two outputs look like this:

                  _____       _____       _____
                 |     |     |     |     |     |
  Channel A  ____|     |_____|     |_____|     |____

                 :  :  :  :  :  :  :  :  :  :  :  :
            __       _____       _____       _____
              |     |     |     |     |     |     |
  Channel B   |_____|     |_____|     |_____|     |__

                 :  :  :  :  :  :  :  :  :  :  :  :
  Event          a  b  c  d  a  b  c  d  a  b  c  d

                |<-------->|
	          one step

                |<-->|
	          one step (half-period mode)

For more information, please see
	http://en.wikipedia.org/wiki/Rotary_encoder


1. Events / state machine
-------------------------

In half-period mode, state a) and c) above are used to determine the
rotational direction based on the last stable state. Events are reported in
states b) and d) given that the new stable state is different from the last
(i.e. the rotation was not reversed half-way).

Otherwise, the following apply:

a) Rising edge on channel A, channel B in low state
	This state is used to recognize a clockwise turn

b) Rising edge on channel B, channel A in high state
	When entering this state, the encoder is put into 'armed' state,
	meaning that there it has seen half the way of a one-step transition.

c) Falling edge on channel A, channel B in high state
	This state is used to recognize a counter-clockwise turn

d) Falling edge on channel B, channel A in low state
	Parking position. If the encoder enters this state, a full transition
	should have happened, unless it flipped back on half the way. The
	'armed' state tells us about that.

2. Platform requirements
------------------------

As there is no hardware dependent call in this driver, the platform it is
used with must support gpiolib. Another requirement is that IRQs must be
able to fire on both edges.


3. Board integration
--------------------

To use this driver in your system, register a platform_device with the
name 'rotary-encoder' and associate the IRQs and some specific platform
data with it.

struct rotary_encoder_platform_data is declared in
include/linux/rotary-encoder.h and needs to be filled with the number of
steps the encoder has and can carry information about externally inverted
signals (because of an inverting buffer or other reasons). The encoder
can be set up to deliver input information as either an absolute or relative
axes. For relative axes the input event returns +/-1 for each step. For
absolute axes the position of the encoder can either roll over between zero
and the number of steps or will clamp at the maximum and zero depending on
the configuration.

Because GPIO to IRQ mapping is platform specific, this information must
be given in separately to the driver. See the example below.

---------<snip>---------

/* board support file example */

#include 
#include 

#define GPIO_ROTARY_A 1
#define GPIO_ROTARY_B 2

static struct rotary_encoder_platform_data my_rotary_encoder_info = {
	.steps		= 24,
	.axis		= ABS_X,
	.relative_axis	= false,
	.rollover	= false,
	.gpio_a		= GPIO_ROTARY_A,
	.gpio_b		= GPIO_ROTARY_B,
	.inverted_a	= 0,
	.inverted_b	= 0,
	.half_period	= false,
};

static struct platform_device rotary_encoder_device = {
	.name		= "rotary-encoder",
	.id		= 0,
	.dev		= {
		.platform_data = &my_rotary_encoder_info,
	}
};


文档描述了内核源码的使用方法,这里的/* board support file example */给我造成了很大的迷惑,最后发现根本用不上,但是文档中的每一个字都要仔细的阅读,首先开头就说明了本驱动适用于通用GPIO,也就是说不需要特殊的硬件资源来进行实现。

第一节讲了旋转编码器的原理,先略过不是本章节的重点,第二节里面原文

2.Platform requirements
As there is no hardware dependent call in this driver, the platform it is used with must support gpiolib. Another requirement is that IRQs must be able to fire on both edges.

这段话是平台总线的申请条件,首先是必须要支持gpiolib,其次是GPIO需要支持中断的上升沿下降沿判断!

这两个条件其实还是比较好满足的,我的板子Linux源码版本是Linux4.1.15,选用的GPI也是普通GPIO口。

这里需要注意的是选择好IO之后,一定要去查看本IO能不能服用为普通GPIO,例如我要使用板子上的EIM_A16和EIM_A20口
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第8张图片

然后去GPIO子系统中去寻找,GPIO的定义部分在imx6q-pinfun.h文件中
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第9张图片

在这里的引脚定义中,可以看到可以服用为普通GPIO,也就是一会儿我们在设备中定义pinctrl子系统时候需要使用MX6QDL_PAD_EIM_A16__GPIO2_IO22来对应GPIO子系统的操作

第三节通过原文内容我们可以看到

To use this driver in your system, register a platform_device with the name ‘rotary-encoder’ and associate the IRQs and some specific platform data with it.

这里的重点是设备树节点的名称一定要是rotary-encoder,这样才可以和驱动中的match的驱动名字匹配上,以免driver和device匹配不上,还有就是关键的 associate the IRQs and some specific platform data with it,也就是要把中断匹配上,这里说明了设备树的添加方法和注意事项,接下来就是设备树。

设备树说明 Rotary encoder DT bindings

Rotary encoder DT bindings

Required properties:
- gpios: a spec for two GPIOs to be used

Optional properties:
- linux,axis: the input subsystem axis to map to this rotary encoder.
  Defaults to 0 (ABS_X / REL_X)
- rotary-encoder,steps: Number of steps in a full turnaround of the
  encoder. Only relevant for absolute axis. Defaults to 24 which is a
  typical value for such devices.
- rotary-encoder,relative-axis: register a relative axis rather than an
  absolute one. Relative axis will only generate +1/-1 events on the input
  device, hence no steps need to be passed.
- rotary-encoder,rollover: Automatic rollove when the rotary value becomes
  greater than the specified steps or smaller than 0. For absolute axis only.
- rotary-encoder,half-period: Makes the driver work on half-period mode.

See Documentation/input/rotary-encoder.txt for more information.

Example:

		rotary@0 {
			compatible = "rotary-encoder";
			gpios = <&gpio 19 1>, <&gpio 20 0>; /* GPIO19 is inverted */
			linux,axis = <0>; /* REL_X */
			rotary-encoder,relative-axis;
		};

		rotary@1 {
			compatible = "rotary-encoder";
			gpios = <&gpio 21 0>, <&gpio 22 0>;
			linux,axis = <1>; /* ABS_Y */
			rotary-encoder,steps = <24>;
			rotary-encoder,rollover;
		};

对于设备树代码的说明,也是很详细,每一项设置的说明都有

其中,ABS_X、REL_X 起初让我困惑不少,但是如果仔细看前面的input输入子系统介绍,REL_X 其实是输入设备的一种描述,相对轴,输出一个相对的值,对于旋转编码器来说也就是正转+1,反转-1

我探索过程中很大的困难都是设备树文件带来的,以及这个设备树例程的不熟悉

需要用到的GPIO为

MX6QDL_PAD_EIM_A16__GPIO2_IO22	
MX6QDL_PAD_EIM_A20__GPIO2_IO18	

通过文档,我们最直观得到的信息就是要首先在imx6qdl-saresd.dtsi文件,添加进来例程中的设备树,加进来设备的节点信息

对照例程设备树,首先要修改的就是gpios的写法,和自己的设备树对应上,我的显然就是 gpios = <&gpio2 22 1>, <&gpio2 18 1>;

	rotary_encoder: rotary_encoder {
		compatible = "rotary-encoder";
		gpios = <&gpio2 22 1>, <&gpio2 18 1>;			
		linux,axis = <0>; 								/* 0:REL_X  1:ABS_Y */
		rotary-encoder,relative-axis;					// 相对轴只会在输入上生成 +1/-1 事件设备
	};

下面就是例程中没有的信息了,需要自己根据理解来进行添加

一个必不可少的就是pinctrl的定义

添加pinctrl子系统,这里添加引脚直接复制前面在imx6q-pinfun.h文件找到的GPIO名称添加在iomuxc节点内
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第10张图片
注意:在添加过程中,一定要确保引脚没有被其他设备初始化并使用!

这样,我们最基本的信息添加完毕。起初我也以为这样直接使用驱动就可以了,但现实是没有办法正常识别到旋转编码器。

回头继续研究说明文档,以及研究input输入子系统的驱动源码,发现事情没有这么简单

Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第11张图片
在源码中可以看到获取中断号的函数,这才反应过来,原来忽略掉了之前一直在反复提到的IRQ,也就是中断在设备树节点中的添加。

找到了问题,下一步就是对设备树节点进一步进行添加和配置,因为是添加新的额外配置信息,所以,在imx6q-c-sabresd.dts中继续添加,以防止出现一些问题。

&rotary_encoder {
	pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_rotary>;
	gpios = <&gpio2 22 1>, <&gpio2 18 1>;				// 测试任意GPIO
	interrupt-parent = <&gpio2>;
    fsl,user;
    status = "okay";	
};

这里主要就是添加了中断的父类信息interrupt-parent = <&gpio2>;

现在设备树就完成了配置

添加Rotary_Encoder驱动

按照在make menuconfig中的位置提示,启用内核源码中的驱动程序
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第12张图片
驱动和设备树搞定后,编译Linux源码,然后就是下载了,下载进去之后直接测试,其实是不会有任何反应的,一方面是因为没有应用程序的调用,另一方面也不能保证驱动正常的运行。

如果不能直接使用,那么就要把驱动提取出来,编译程ko文件,安装到Linux中。

查看driver和device挂载

检查驱动能否正常运行,首先是查看有没有在平台总线上注册了driver和device,还有就是设备树节点有没有正确添加,最后就是驱动的probe函数是否正常运行,因为probe函数正常运行意味着driver和device匹配上了,驱动也可以正常使用。

驱动跑起来了,具体怎么用,还得通过应用程序来进行测试,这个都是后话了,先来测试驱动能不能正常跑起来。

查看平台总线设备信息

在最熟悉的/dev目录下先来看看驱动的运行情况,/dev/input/by-path目录下查看设备信息

cd /dev/input/by-path

看到了挂载在平台总线上的设备节点
在这里插入图片描述
驱动模块加载完成以后到/sys/bus/platform/drivers/目录下查看驱动是否存在,我们在
rotary-encoder.c 中设置rotary_encoder_driver (platform_driver 类型)的name 字段为“rotary-encoder”,因此会在
/sys/bus/platform/drivers/目录下存在名为“rotary-encoder”这个文件,结果如图所示:

ls /sys/bus/platform/devices/

Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第13张图片

同理,在/sys/bus/platform/devices/目录下也存在rotary的设备文件,也就是设备树中rotary_encoder这
个节点,如图

ls /sys/bus/platform/devices/

Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第14张图片

到了这一步,发现驱动和模块都存在,下面只要能够成功进入probe函数,就代表匹配成功了,也就是驱动有了正常工作的前提条件

在源码中加入一个打印信息
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第15张图片
这时候会发现无论怎么操作,都没有办法触发probe函数 (后面经过实验发现是起初我设备树没有写对,所以没有办法正确的触发probe函数和启动),所以后面的很多步骤是默认写错了设备树进行的测试和验证。

既然没有办法正确的从Linux源码中启动驱动,那么只能用到最原始的方法,把驱动编译成ko模块继续进行调试

rotary_encoder驱动源码

把rotary_encoder驱动源码提取出来,然后单独编译程ko模块,里面添加进去printk的打印

驱动源码还是非常有必要进行读一读,尤其是probe函数,在函数中,我们可以看到和设备树之间的关系,以及运行的步骤

提取出来之后,首先得把之前Linux内核中的驱动取消掉,不然就会冲突

取消掉之前的驱动,然后才能编译程模块后正常运行

rotary_encoder.c驱动

/*
 * rotary_encoder.c
 *
 * (c) 2009 Daniel Mack 
 * Copyright (C) 2011 Johan Hovold 
 *
 * state machine code inspired by code from Tim Ruetz
 *
 * A generic driver for rotary encoders connected to GPIO lines.
 * See file:Documentation/input/rotary-encoder.txt for more information
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 

#define DRV_NAME "rotary-encoder" 


/*Ori*/
struct rotary_encoder {
	struct input_dev *input;
	const struct rotary_encoder_platform_data *pdata;

	unsigned int axis;
	unsigned int pos;

	unsigned int irq_a;
	unsigned int irq_b;

	bool armed;
	unsigned char dir;	/* 0 - clockwise, 1 - CCW */

	char last_stable;
};

static int rotary_encoder_get_state(const struct rotary_encoder_platform_data *pdata)
{
	int a = !!gpio_get_value(pdata->gpio_a);
	int b = !!gpio_get_value(pdata->gpio_b);

	a ^= pdata->inverted_a;
	b ^= pdata->inverted_b;

	return ((a << 1) | b);
}

static void rotary_encoder_report_event(struct rotary_encoder *encoder)
{
	const struct rotary_encoder_platform_data *pdata = encoder->pdata;

	if (pdata->relative_axis) {
		input_report_rel(encoder->input,
				 pdata->axis, encoder->dir ? -1 : 1);
	} else {
		unsigned int pos = encoder->pos;

		if (encoder->dir) {
			/* turning counter-clockwise */
			if (pdata->rollover)
				pos += pdata->steps;
			if (pos)
				pos--;
		} else {
			/* turning clockwise */
			if (pdata->rollover || pos < pdata->steps)
				pos++;
		}

		printk("pos value is : %d\n", pos);
		
		if (pdata->rollover)
			pos %= pdata->steps;

		encoder->pos = pos;
		input_report_abs(encoder->input, pdata->axis, encoder->pos);
	}

	input_sync(encoder->input);
}

static irqreturn_t rotary_encoder_irq(int irq, void *dev_id)
{
	struct rotary_encoder *encoder = dev_id;
	int state;

	state = rotary_encoder_get_state(encoder->pdata);

	switch (state) {
	case 0x0:
		if (encoder->armed) {
			rotary_encoder_report_event(encoder);
			encoder->armed = false;
		}
		break;

	case 0x1:
	case 0x2:
		if (encoder->armed)
			encoder->dir = state - 1;
		break;

	case 0x3:
		encoder->armed = true;
		break;
	}

	return IRQ_HANDLED;
}

static irqreturn_t rotary_encoder_half_period_irq(int irq, void *dev_id)
{
	struct rotary_encoder *encoder = dev_id;
	int state;

	state = rotary_encoder_get_state(encoder->pdata);

	switch (state) {
	case 0x00:
	case 0x03:
		if (state != encoder->last_stable) {
			rotary_encoder_report_event(encoder);
			encoder->last_stable = state;
		}
		break;

	case 0x01:
	case 0x02:
		encoder->dir = (encoder->last_stable + state) & 0x01;
		break;
	}

	return IRQ_HANDLED;
}

#ifdef CONFIG_OF
static const struct of_device_id rotary_encoder_of_match[] = {
	{ .compatible = "rotary-encoder", },
	{ },
};
MODULE_DEVICE_TABLE(of, rotary_encoder_of_match);

static struct rotary_encoder_platform_data *rotary_encoder_parse_dt(struct device *dev)
{
	const struct of_device_id *of_id =
				of_match_device(rotary_encoder_of_match, dev);
	struct device_node *np = dev->of_node;
	struct rotary_encoder_platform_data *pdata;
	enum of_gpio_flags flags;

	if (!of_id || !np)
		return NULL;

	pdata = kzalloc(sizeof(struct rotary_encoder_platform_data),
			GFP_KERNEL);
	if (!pdata)
		return ERR_PTR(-ENOMEM);

	of_property_read_u32(np, "rotary-encoder,steps", &pdata->steps);
	of_property_read_u32(np, "linux,axis", &pdata->axis);

	pdata->gpio_a = of_get_gpio_flags(np, 0, &flags);
	pdata->inverted_a = flags & OF_GPIO_ACTIVE_LOW;

	pdata->gpio_b = of_get_gpio_flags(np, 1, &flags);
	pdata->inverted_b = flags & OF_GPIO_ACTIVE_LOW;

	pdata->relative_axis = !!of_get_property(np,
					"rotary-encoder,relative-axis", NULL);
	pdata->rollover = !!of_get_property(np,
					"rotary-encoder,rollover", NULL);
	pdata->half_period = !!of_get_property(np,
					"rotary-encoder,half-period", NULL);

	return pdata;
}
#else
static inline struct rotary_encoder_platform_data *
rotary_encoder_parse_dt(struct device *dev)
{
	return NULL;
}
#endif

static int rotary_encoder_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	const struct rotary_encoder_platform_data *pdata = dev_get_platdata(dev);
	struct rotary_encoder *encoder;
	struct input_dev *input;
	irq_handler_t handler;
	int err;

	printk("rotary-encoder probe started\n");

	if (!pdata) {
		pdata = rotary_encoder_parse_dt(dev);
		if (IS_ERR(pdata))
			return PTR_ERR(pdata);

		if (!pdata) {
			dev_err(dev, "missing platform data\n");
			return -EINVAL;
		}
	}

	/*申请input_dev*/
	encoder = kzalloc(sizeof(struct rotary_encoder), GFP_KERNEL);
	input = input_allocate_device();							// 为新添加的输入设备分配内存,如果成功,将返回input_dev *的指针结构
	if (!encoder || !input) {
		err = -ENOMEM;
		goto exit_free_mem;
	}

	encoder->input = input;
	encoder->pdata = pdata;

	input->name = pdev->name;
	input->id.bustype = BUS_HOST;
	input->dev.parent = dev;

	if (pdata->relative_axis) {
		input->evbit[0] = BIT_MASK(EV_REL);
		input->relbit[0] = BIT_MASK(pdata->axis);

	} else {
		input->evbit[0] = BIT_MASK(EV_ABS);
		input_set_abs_params(encoder->input,
				     pdata->axis, 0, pdata->steps, 0, 1);
	}

	/* request the GPIOs 申请GPIO */
	err = gpio_request_one(pdata->gpio_a, GPIOF_IN, dev_name(dev));
	if (err) {
		dev_err(dev, "unable to request GPIO %d\n", pdata->gpio_a);
		goto exit_free_mem;
	}
	
	/*
     * 申请GPIO
	 * gpio_request_one使用GPIO_IN,后面就不需要自己再额外调用gpio_direction_input或者gpio_direction_output
	 */
	err = gpio_request_one(pdata->gpio_b, GPIOF_IN, dev_name(dev));		// 申请、配置、释放
	if (err) {
		dev_err(dev, "unable to request GPIO %d\n", pdata->gpio_b);
		goto exit_free_gpio_a;
	}

	encoder->irq_a = gpio_to_irq(pdata->gpio_a);						// 获取gpio对应的中断号
	encoder->irq_b = gpio_to_irq(pdata->gpio_b);						// 获取gpio对应的中断号

	/* request the IRQs 申请中断 */
	if (pdata->half_period) {
		handler = &rotary_encoder_half_period_irq;
		encoder->last_stable = rotary_encoder_get_state(pdata);
	} else {
		handler = &rotary_encoder_irq;
	}

	/* 申请中断函数request_irq 中断号,中断处理函数,中断标志 */
	err = request_irq(encoder->irq_a, handler,
			  IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
			  DRV_NAME, encoder);				
	if (err) {
		dev_err(dev, "unable to request IRQ %d\n", encoder->irq_a);
		goto exit_free_gpio_b;
	}
		
	err = request_irq(encoder->irq_b, handler,
			  IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
			  DRV_NAME, encoder);
	if (err) {
		dev_err(dev, "unable to request IRQ %d\n", encoder->irq_b);
		goto exit_free_irq_a;
	}

	/* 注册输入设备 */
	err = input_register_device(input);							
	if (err) {
		dev_err(dev, "failed to register input device\n");
		goto exit_free_irq_b;
	}

	platform_set_drvdata(pdev, encoder);

	printk("rotary-encoder probe success!!!\n");

	return 0;

exit_free_irq_b:
	free_irq(encoder->irq_b, encoder);
exit_free_irq_a:
	free_irq(encoder->irq_a, encoder);
exit_free_gpio_b:
	gpio_free(pdata->gpio_b);
exit_free_gpio_a:
	gpio_free(pdata->gpio_a);
exit_free_mem:
	input_free_device(input);
	kfree(encoder);
	if (!dev_get_platdata(&pdev->dev))
		kfree(pdata);

	return err;
}

static int rotary_encoder_remove(struct platform_device *pdev)
{
	struct rotary_encoder *encoder = platform_get_drvdata(pdev);
	const struct rotary_encoder_platform_data *pdata = encoder->pdata;

	free_irq(encoder->irq_a, encoder);
	free_irq(encoder->irq_b, encoder);
	gpio_free(pdata->gpio_a);
	gpio_free(pdata->gpio_b);

	input_unregister_device(encoder->input);
	kfree(encoder);

	if (!dev_get_platdata(&pdev->dev))
		kfree(pdata);
 
	printk("rotary_encoder_remove OK!!!\n");

	return 0;
}

static struct platform_driver rotary_encoder_driver = {
	
	.probe		= rotary_encoder_probe,
	.remove		= rotary_encoder_remove,
	.driver		= {
		.name	= DRV_NAME,
		.of_match_table = of_match_ptr(rotary_encoder_of_match),
	}
};

module_platform_driver(rotary_encoder_driver);

MODULE_ALIAS("platform:" DRV_NAME);
MODULE_DESCRIPTION("GPIO rotary encoder driver");
MODULE_AUTHOR("Daniel Mack , Johan Hovold");
MODULE_LICENSE("GPL v2");

Makefile代码

# 开发板Linux内核的实际路径 
# KDIR变量
KDIR:=/work/linux-4.1.15

#  获取当前目录
PWD:=$(shell pwd)

# obj-m表示将 chrdevbase.c这个文件 编译为 chrdevbase.ko模块。
obj-m += rotary_encoder.o

# 编译成模块
all:
	make -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean

make编译之后,通过scp发送到开发板中

直接把单独的编译程模块在开发板中安装,可以看到成功打印出了信息,独立出来驱动之后的操作证明了驱动本身没有问题,也可以正确的匹配driver和device
在这里插入图片描述
但是目前驱动还是没有办法使用,使用方法还得继续探索!

先来分析这里的输出,可以发现,这里输出了一行不是我们添加的代码,也就是

input: rotary_encoder as /devices/soc0/rotary_encoder/input/input6

通过这一行代码,也就引出来了input子系统如何进行调试和使用

input输入子系统调试方法

在之前,用过了一些常用的open或者read都无动于衷后,看到驱动打印出来的input: rotary_encoder as /devices/soc0/rotary_encoder/input/input6信息,才反应过来这是一个input输入设备,与uart串口设备是不同的,rotary_encoder挂载到输入子系统后,input输入设备是有自己的使用方法的。

devices查看input设备

对于输入设备,首先,查看/proc/bus/input/devices文件

nano /proc/bus/input/devices

这时候,现在可以看到两个设备(现在是还没有加载rotary_encoder模块的情况)

在文档中我们需要注意的就是

Name 设备名字
Handlers 设备节点

Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第16张图片

在我们注册旋转编码器设备驱动后,再一次查看文档,可以发现多了一个设备的信息

Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第17张图片
看到了Name正是我们的旋转编码器Name=“rotary_encoder”,设备号是Handlers=event2

这时候,我们再次打开/dev/input
在这里插入图片描述
可以看到旋转编码器作为一个输入设备挂载在了input上,并且设备节点的名字变成了event2

调试代码

现在驱动算是彻底的准备好了,接下来就要迎接应用程序的调用了,应用程序input_app.c代码如下

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
       struct input_event in_ev = {0};
       int fd = -1;

       /* 校验传参 */
       if (2 != argc) {
              fprintf(stderr, "usage: %s \n", argv[0]);
              exit(-1);
       }

       /* 打开文件 */
       if (0 > (fd = open(argv[1], O_RDONLY))) {
              perror("open error");
              exit(-1);
       }

       for ( ; ; ) {

              /* 循环读取数据 */
              if (sizeof(struct input_event) !=
                read(fd, &in_ev, sizeof(struct input_event))) {
                     
                     perror("read error");
                     exit(-1);
              }

              printf("type:%d code:%d value:%d\n",
                in_ev.type, in_ev.code, in_ev.value);
       }
}

编译应用程序

$CC input_app.c -o input_app

得到交叉编译好的可执行文件,发送到开发板

使用方法为

./input_app /dev/input/event2

转动旋转编码器就可以看到开始输出旋转的信息
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第18张图片
验证成功之后,就可以通过内核编译进去旋转编码器的驱动了,编译进Linux源码之后,重启,查看,可以看到输入设备,现在彻底成功了!成功了!
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第19张图片

在这里插入图片描述

结语

电路方案

内核源码能够正常使用,也就可以不用之前设想的i2c模块方案了,为了保险起见,使用了接口复用的方法

Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第20张图片
为了防止电阻焊接错误,将焊盘进行重叠。
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第21张图片

设备树 驱动关系

经过了四天的折腾,总算是有了结果

对于旋转编码器这个模块,虽然Linux源码支持,但是网络上并没有实战教程,也都是转载的官方文档,对于开发老手来说,自然是不需要教程的,但是对于初次开发的新手来说,还是需要一个详细的教程,从而引出对于input输入子系统的学习,以及对于Linux分层的理解。

对于一个陌生的驱动,最重要的就是驱动和设备树,在设备树中添加正确的节点信息并且和驱动匹配

在早些时候需要使用match的代码编写,现在的话就是在probe中正确的使用match函数
Linux 输入设备调试详解(零基础开发)Rotary_Encoder旋转编码器驱动(EC11)通用GPIO为例 挂载input输入子系统_第22张图片
也就是i一定要明白设备树和驱动为什么可以匹配在一起,然后工作。

input输入子系统

能够正确的摸索出来,离不开对于input输入子系统的学习还有平台总线的学习。

虽然是不常用的旋转编码器模块,但是归根到底,还是input输入子系统,并且通过平台总线的方法进行挂载,所以对于基础的学习是非常的重要!

Linux越抽象,也就越要保持清醒,万变不离其宗,掌握好平台总线的方法,注册这一类设备就可以快速的定位问题。

你可能感兴趣的:(i.MX6,linux,EC11,旋转编码器,input输入子系统)