linux4.6 EC11旋转编码器的驱动

最近项目使用了旋转编码器EC11,遍查内核,发现并没有它的驱动,查了查CSDN,终于找到一篇有用的。根据自己的需要和对最基础的gpio_key.c的理解,我改写出了一份EC11的专用驱动。

感谢下面博主的启发,有了这位高人的指点,我才有信心改写成功。并决定向他学习,将自己成功的代码与大家分享。

https://blog.csdn.net/aifei7320/article/details/50037689

1.了解ec11旋转编码器

参考链接:https://www.yiboard.com/thread-1001-1-1.html、https://www.jianshu.com/p/41fa67ecb248

网上相关的介绍很多,读完以上内容,我对自己使用的产品有了基本的认识:

首先,知道它电路的连接使用,参考说明书上面的电路图。该连电阻连电阻,该接电容接电容,才能正确出波形。

接着,知道它是一定位一脉冲式,通过示波器观察到转动一格,A、B都有完整的脉冲产生。(两定位的驱动没有想)

最后,顺时钟转的波形和逆时钟转的波形。

关于一圈多少格,个人认为,如果说转够一圈有什么特殊波形的话,这个可以留意一下,但是我产品没有,此特性忽略。

此处应该有图

2.分析驱动实现

有了上述不同旋转方向的波形的图片以后,开始思考实现驱动的方案和效果,先初步想一下,然后再参考别人的linux代码、C语言代码进行补充修正。

2.1  驱动的方案

提供的方案有以下

1)记录信号A、B两个的下降沿(或上升沿)出现的时间,然后相减的正负结果,暴露出A与B谁先出现,从而知道是左旋还是右旋。

要点:A与B的中断都要关注,全局变量记录中断出现时间。

2)以其中一个信号作为时钟线(中断线),以上升沿(或下降沿)中断触发,读取另外一根(信号线)的电平状态,从而知左旋还是右旋。

要点:A与B仅选择关注一个中断,不需要全局变量记录。

因为驱动基于gpio_key.c,它是中断发送然后检测电平的方式。参考了其他博主,都是第二种方式,有成功的例子,所以采用了第二种方式。

另外这里考虑另外一个问题,那就是按钮值的处理。有的ec11除了左旋、右旋还可以像普通按钮按下,也就是它有3个值。我研究了手头有按钮功能的按键,发现按钮事件对A、B信号影响不大,它另外通过引脚C输出,波形表现是平时高电平,按键按下变成低,松开又成高。所以按钮事件交给驱动gpio_key.c处理就好,不予关注。而左旋、右旋事件交给驱动gpio_ec11.c(在gpio_key.c基础上修改而成,不影响gpio_key.c驱动)。参考我后面的设备树描述,这个就好懂了。

总结一下要做的工作就是在上,改写出旋转功能(这个驱动不负责按钮部分所以不是旋转按钮)的驱动gpio_ec11.c。

2.2  预计效果

最后驱动要实现的功能是,左旋的时候,输出一个按键值。右旋的时候,输出另外一个按键值。

怎么说呢,效果就是左旋一格相当于按键A完成一次按下松开动作,右旋一格相当于按键B完成一次按下松开动作。

3.驱动编辑

因为修改部分多,而且很多变量类型修改的细节,这里仅展示重点讲解,详情参考提供的源代码

3.1 相关头文件

因为不能影响到gpio_key.c,所以需要创建新的结构体数据。按照参考链接1博主的做法,在struct gpio_keys_button 中增加成员,并修改变量类型。

gpio_ec11.h ——修改自gpio_key.h

修改部分:

#ifndef _GPIO_EC11_H
#define _GPIO_EC11_H

……

struct gpio_ec11_button {  /*重要结构体名字换掉*/
    unsigned int code;  /*没有用到,为了适应旧架构就保留没删除*/
    unsigned int leftcode;  /*记录左旋键值*/
    unsigned int rightcode;  /*记录右旋键值*/
    int gpio;  /*旋转编码器A引脚的gpio号*/
    int subgpio;  /*旋转编码器B引脚的gpio号*/

 ……

};

/*原来struct gpio_keys_button部分都相应改成struct gpio_ec11_button,为减少工作量,变量名不变*/

struct gpio_ec11_platform_data {
    struct gpio_ec11_button *buttons; 

 ……

};

总结,这样修改的主要是保证改的新驱动不会影响就驱动的使用,没换变量名是为了尽可能少改。

3.2 相关源文件

既然要修改源文件,那就得先把基础文件gpio_key.c理解清楚。

采用了最笨的办法,就是在里面的主要功能函数里,首加“printk("%s start\n",__FUNCTION__);”,“尾加printk("%s end\n",__FUNCTION__);” ,再配合按键的使用观察内核打印信息,基本摸清楚驱动调用顺序。最后,搜几篇gpio_key.c驱动详解之类的文章,对每个功能函数加深了解,最后着手改程序。我觉得高手可能直接看代码能理清,emmm~反正我不行,不管什么手段办法,摸清楚原来的框架是必须的。

gpio_ec11.c ——修改自gpio_key.c

…… //25h

#include   //20190408替换成改的头文件

…… //35h

struct gpio_ec11_data {
    const struct gpio_ec11_button *button;  //20190408
 ……
};

struct gpio_ec11_drvdata {
    const struct gpio_ec11_platform_data *pdata;  //20190408
 ……
    struct gpio_ec11_data data[0];    //20190408
};

到这里,所有重要的数据结构体就都定义了,然后就是替换程序中原来旧数据类型,如struct gpio_keys_drvdata等的地方了。这一步工程量挺大的,在实验中发现可以借助GCC编译器找。改完直接编译,到时候报出很多的错,然后根据错误精准修改,不到半小时就能改完吧。

不建议一次改很多编译。我是先改了数据类型(此时顶着的是gpio_key的名头),后来实验成功了,才将很多函数名都改了。这是为了避免内核冲突。

重点修改地方的介绍:

3.2.1  设置中断类型

我在实验用gpio_ec11.c的时候,发现它经常不对,后来调试发现gpio_key.c就时灵时不灵。也就是说,新驱动不行的毛病是打娘胎里来的。只好先研究gpio_key.c为什么时灵时不灵。然后就发现,是程序里中断类型(上升沿、下降沿、高电平和低电平触发天剑)设置不对。我不知道为什么这个驱动对中断类型变量有声明,但是没有通过解析设备树赋值给变量,内情以后再研究,自己先加了这个:

static struct gpio_ec11_platform_data *
gpio_keys_get_devtree_pdata(struct device *dev)
{

 ……
    for_each_available_child_of_node(node, pp) {
        //enum of_gpio_flags flags;  /*这个变量值只声明未定义知道是不是原作者忘了……*/  
        button = &pdata->buttons[i++];

        button->gpio = of_get_gpio_flags(pp, 0, &button->irq_flags);  /*时钟的注册*/        
        ……

        if (of_property_read_u32(pp, "irq,flags", &button->irq_flags))
            button->irq_flags = IRQF_TRIGGER_FALLING;  /*默认为下降沿触发20190410*/
         //printk("button->irq_flags=%d",button->irq_flags);

 ……

注意: button->irq_flags的赋值一定要在of_get_gpio_flags()函数之后,实验中发现此函数会修改button->irq_flag归0

总结:像这类改数据值的能在定义处改就改。千万别在使用的地方改,要改很多处,而且二次修改时万一漏了一处,调试很浪费时间。

3.2.2  注册gpio与键值

static struct gpio_ec11_platform_data *gpio_keys_get_devtree_pdata(struct device *dev)
{

 ……

    pdata->rep = !!of_get_property(node, "autorepeat", NULL);

 ……
    for_each_available_child_of_node(node, pp) {
    ……

        button->gpio = of_get_gpio_flags(pp, 0, &button->irq_flags);  /*时钟的注册,管中断gpio叫时钟信号了,仿照某帖子*/        
        button->subgpio = of_get_gpio_flags(pp, 1, &button->irq_flags);  /*信号的注册*/

  ……

        if(button->gpio == button->subgpio) {  /*必要的错误判断,照着button->gpio模板加,详情参考源代码*/
            dev_err(dev, "ERROR:the ec11 pin A——GPIO %d,same as pin B——GPIO %d\n",button->gpio,button->subgpio);
            error = -button->gpio;
            return ERR_PTR(error);
        }

  ……

        if (of_property_read_u32(pp, "left,code", &button->leftcode)) {
            dev_err(dev, "Button without leftcode: 0x%x\n",button->gpio);
            return ERR_PTR(-EINVAL);
        }  /*注册左旋和右旋的按键值*/
        if (of_property_read_u32(pp, "right,code", &button->rightcode)) {
            dev_err(dev, "Button without rightcode: 0x%x\n", button->subgpio);
            return ERR_PTR(-EINVAL);
        }

  ……

        if (of_property_read_u32(pp, "debounce-interval",&button->debounce_interval))
            button->debounce_interval = 5;  /*赋值防抖动时间,实验中发现这个时间最少是10ms,写的小了没什么用,不知为何*/

……

3.2.2  中断处理函数

最重要的当属中断处理函数,这里整理一下函数调用顺序:

当满足中断要求时,函数的调用顺序:

 

/*中断处理函数 传说的顶部*/
static irqreturn_t gpio_ec11_gpio_isr(int irq, void *dev_id)

{

 ……

 /*函数不卡在这,先return IRQ_HANDLED,再经过bdata->software_debounce(ms)延时,执行&bdata->work*/

 mod_delayed_work(system_wq,
              &bdata->work,  // ——> void gpio_keys_gpio_work_func(struct work_struct *work)
              msecs_to_jiffies(bdata->software_debounce));

 ……

 return IRQ_HANDLED;

}

注意:根据函数理解,执行函数gpio_ec11_gpio_isr,到gpio_keys_gpio_work_func的延时为bdata->software_debounce。

实验中通过current_kernel_time()函数打印时间,发现这个延时最少也为10ms。

/*&bdata->work 的实现 传说的底部??*/
static void gpio_keys_gpio_work_func(struct work_struct *work)
{
    struct gpio_ec11_data *bdata =
        container_of(work, struct gpio_ec11_data, work.work);

    gpio_keys_gpio_report_event(bdata);

    if (bdata->button->wakeup)
        pm_relax(bdata->input->dev.parent);
}
 

/*上报按键事件  20190408 背景:下降沿触发*/
static void gpio_keys_gpio_report_event(struct gpio_ec11_data *bdata)
{
    const struct gpio_ec11_button *button = bdata->button;
    struct input_dev *input = bdata->input;
    unsigned int type = button->type ?: EV_KEY;
    int state = gpio_get_value_cansleep(button->gpio);  /*取得时钟线的状态*/
    int dir = 0;  /*旋转方向的变量*/
    ts = current_kernel_time();
    if (state == 1){  /*中断发生后经过一个延时抖动再读时钟线的电平,正常肯定是0,1立刻返回*/
        printk("sec=%ld,nsec=%ld\n",ts.tv_sec, ts.tv_nsec);        
        return;
    }
    if (state < 0) {
        dev_err(input->dev.parent, "failed to get gpio state\n");
        return;
    }

    //state = (state ? 1 : 0) ^ button->active_low;
    
    if(state != state2)/*中断发生时取得的信号线的状态,联合时钟线判断旋转方向*/
        dir = 1;//button->code = bdata->button->rightcode;
    else
        dir = 2;//button->code = bdata->button->leftcode;
    printk("dir=%d~~~state=%d,stateother=%d,~~~~~",dir,state,state2);
    printk("start sec=%ld,nsec=%ld\n",ts.tv_sec, ts.tv_nsec);
    /*上报时间,第一次上报按键按下,第二次上报按键松开。缺点:对active_low进行研究,先忽略了*/
    if(dir == 1){
        input_event(input, type, button->rightcode,1);
        input_sync(input);
        input_event(input, type, button->rightcode,0);
        input_sync(input);printk("input_sync\n");
    }
    else if(dir == 2){
        input_event(input, type, button->leftcode,1);  //!!state
        input_sync(input);
        input_event(input, type, button->leftcode,0);
        input_sync(input);printk("input_sync\n");
    }
}

详情参考上传的资源中。

为什么采用一个中断,但是上报两次事件的方式?

如果把触发方式改成边沿触发,道理上讲可以由程序一一对应上报。但是试验中发现,旋转开关矩形低脉冲的时间有时候比较短,本来按键状态检测就会出错,这样错误概率会高。

为什么信号线电平要在中断顶部读取,而不是中断底部读取?

这个根据实验现象调整的。主要是AB信号下降沿的时差不确定性引起。虽然会拖慢中断释放时间,但是只读取,正真的处理还在中断底部,所以能接受。

时钟下降沿之后,上升沿之前,也就是它低电平的时期,信号线的状态会发生一次变化。要想有正确结果,得读变化前的值。但是,随你怎么设置,中断顶部到中断底部最少就得10ms,那么信号线在时钟线下降沿后 <10ms内变化的,统统得到错误的方向,只好在时钟下降沿也就是中断顶部立刻读取。

此处应该有图(以后再配)

驱动的缺陷是什么?

很遗憾它现在依然会错,

错误情况1,时钟线多了一个脉冲,但是信号线无变化。导致会多报一次事件,但是旋转方向保持了一致,就是明明转了一格,但是系统输出两次按键事件。这个也会发生时钟线无变化,但是信号线多了一个脉冲,但是因为中断触发在时钟线上,所以不会产生影响。

此处真有图(以后再配)

错误情况2,旋转方向报告错误。原因是时钟线抖动引起。时钟线低电平时期,它在信号线变化之后,忽然产生了抖动。这个抖动后,触发中断,在读取时钟线电平时,它刚好还是个低就误判成一个脉冲了。导致多报一次事件,第二次报的是反向。

解决办法:

打算从电路方向着手了。再议。

 

总结:此次调试ec11前后花了一个周的时间大概,初代代码2天完成,后来的时间全在调试上。总结一下:

一是,要先从原理了解清楚。比如,就得先知道旋钮波形,推测判断手段,自己写实现很方便,读懂别人文章也容易。

二是,理清输入框架。本人觉悟很低,每次都得先摸清楚功能函数执行顺序,才能了解到框架。以后加强框架观察能力。

三是,实验配合调试。这次调试遇到很多想不到的错,比如内核没有中断方式的设置,延时设小了没有用,这些原因导致的错误,靠猜还真是难,就得靠示波器,配合内核打印来看。

四是,一步一个脚印的调试。必须保证准备条件的正确。我之前中断错了。努力的改中断处理,就是浪费时间。应该按照事件的发生顺序一步步调。

五是,适当请求帮助。感谢领导,给我提出打印程序执行时间这个好办法。辛苦了我的头发陪我思考。

 

设备树描述:

        gpio_keys {
            compatible = "gpio-keys";
            #address-cells = <0x1>;
            #size-cells = <0x0>;
            /*autorepeat;*/

            button@0 {
                label = "test";
                linux,code = <0x3D>;
                debounce-interval = <0x15>;
                irq,flags = <0x3>;
                gpios = <0x6 0x32 0x0>;
            };    
            button@1 {
                label = "test1";
                linux,code = <0x3f>;
                debounce-interval = <0x15>;
                irq,flags = <0x2>;
                gpios = <0x6 0x33 0x0>;
            };
           
            button@2 {
                label = "ec11 button";  /*ec11的按钮,如果有按钮功能的话*/
                linux,code = <0x3>;
                debounce-interval = <0x15>;
                gpios = <0x6 0x38 0x0>;
            };
           
        };

        gpio_ec11 {
            compatible = "gpio-ec11";
            #address-cells = <0x1>;
            #size-cells = <0x0>;
            /*autorepeat;*/

            button@1 {
                label = "EC11";
                linux,code = <0x3D>;
                left,code = <0x3E>;
                right,code = <0x3f>;
                irq,flags = <0x2>;  /*中断方式设置*/
                debounce-interval = <0x5>;
                gpios = <0x6 0x38 0x0 0x6 0x39 0x0>;
            };
        };

C语言测试程序(还不会上传,先贴图了):

input1.c

/*这是旋转按钮的测试函数。尤其适合在没有显示时,通过串口控制台进行测试。
*屏幕会打印出按下还是松开的状态,
*和输入按键的值,这个值是设备树中描述的值,不是实际效果值。
*比如:在输入框按下按键,上面出现了数字1,1是实际效果值,而它的设备树描述值却是2。
*本实验输出的是 ————描述值
*附:
*怎么知道按键效果和键值的对应关系?
*参阅网址:http://www.verysource.com/code/10989174_1/vcomkeycode.h.html
*怎么知道按键事件的文件路径?
*参阅开机打印信息。比如某实验开机时关于矩阵键盘的信息如下:
*input: amba:matrix_keys as /devices/soc0/amba/amba:matrix_keys/input/input0
*通过上述就知道,开机后/dev/input/event0是按键事件的相关文件。
*假如输入input: amba:matrix_keys as /devices/soc0/amba/amba:matrix_keys/input/input1,就变成了/dev/input/event1
*/       
#include  
#include  
#include  
#include  
#include  
#include  
#include  
#include  
#include
 
struct input_event event;
int main()
{  
        int keys_fd,rc;           
        if ((keys_fd = open("/dev/input/event1", O_RDWR, 0)) <= 0){
            printf("open failed:%d\n", keys_fd);
    }    
     printf("ok");
    while (1)
        {
            if (read(keys_fd, &event, sizeof (event)) != sizeof (event))
            break;

                if (event.type == EV_KEY){
            printf("event.code=%d  %s\n",event.code&0xff,(event.value) ? "Pressed" : "Released");
                        if (event.code == KEY_ESC)
            break;
            }
        }

}

使用方法,先用gcc 编译,然后执行,左右转时界面打印不同的值,效果如下:

root@linaro-ubuntu-desktop:~# cd /home/linaro/driver_test/

root@linaro-ubuntu-desktop:/home/linaro/driver_test# gcc -o ./put1 ./input1.c
root@linaro-ubuntu-desktop:/home/linaro/driver_test# ./put1
okevent.code=62  Pressed
event.code=62  Released
event.code=62  Pressed
event.code=62  Released
event.code=62  Pressed
event.code=62  Released
event.code=63  Pressed
event.code=63  Released

介于大家总是看不到我的相关头文件,懒人如我,打算粘贴在这里了。我的ubuntu系统上传不是很友好,没办法设置0积分下载。就只能怎么着了吧。编译时候,建议和.c放同一目录,引用变为,#include "gpio_ec11.h"。有基础的应该懂吧,不解释了。文件名如下:

gpio_ec11.h

内容:

#ifndef _GPIO_EC11_H
#define _GPIO_EC11_H

struct device;
struct gpio_desc;

/**
 * struct gpio_ec11_button - configuration parameters
 * @leftcode:        ec11 left direction input event code (KEY_*, SW_*)
 * @rightcode:        ec11 right direction input event code (KEY_*, SW_*)
 * @gpio:        %-1 if this key does not support gpio
 * @gpio:        %-1 if this key does not support gpio
 * @active_low:        %true indicates that button is considered
 *            depressed when gpio is low
 * @desc:        label that will be attached to button's gpio
 * @type:        input event type (%EV_KEY, %EV_SW, %EV_ABS)
 * @wakeup:        configure the button as a wake-up source
 * @debounce_interval:    debounce ticks interval in msecs
 * @can_disable:    %true indicates that userspace is allowed to
 *            disable button via sysfs
 * @value:        axis value for %EV_ABS
 * @irq:        Irq number in case of interrupt keys
 * @gpiod:        GPIO descriptor
 */
struct gpio_ec11_button {
    unsigned int code;
    unsigned int leftcode;  /*记录左旋键值*/
    unsigned int rightcode;  /*记录右旋键值*/
    int gpio;  /*旋转编码器A引脚的gpio号*/
    int subgpio;  /*旋转编码器B引脚的gpio号*/
    int active_low;
    const char *desc;
    unsigned int type;
    int wakeup;
    int debounce_interval;
    bool can_disable;
    int value;
    unsigned int irq;
    unsigned int irq_flags;
    struct gpio_desc *gpiod;
};

/**
 * struct gpio_ec11_platform_data - platform data for gpio_ec11 driver
 * @buttons:        pointer to array of &gpio_keys_button structures
 *            describing buttons attached to the device
 * @nbuttons:        number of elements in @buttons array
 * @poll_interval:    polling interval in msecs - for polling driver only
 * @rep:        enable input subsystem auto repeat
 * @enable:        platform hook for enabling the device
 * @disable:        platform hook for disabling the device
 * @name:        input device name
 */
struct gpio_ec11_platform_data {
    struct gpio_ec11_button *buttons;
    int nbuttons;
    unsigned int poll_interval;
    unsigned int rep:1;
    int (*enable)(struct device *dev);
    void (*disable)(struct device *dev);
    const char *name;
};

#endif

 

 

 

 

 

 

你可能感兴趣的:(Linux系统,驱动,旋转按钮,ec11)