最近项目使用了旋转编码器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
…… //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