Linux 按键有哪些实现方法
常用的按键实现方法
继点亮了LED 和 OLED之后,该讲讲我常用的键盘怎么实现的了。才入行的时候,想实现一个按键监测功能,对linux驱动也是一无所知。只会基础的在sys/class/gpios/目录下操作gpio,好一点的驱动呢,还支持在sys下配置edge(触发类型),支持上升沿、下降沿、或则双边沿。如果遇到以下不支持中断的gpio或则平台,真的一点办法都没有。
调试龙芯2K的时候,就因为它只有4个专用gpio支持边沿中断,而且都还不支持双边沿中断。直到后来发现了gpio_keys.c,当时居然都不知道有gpio_keys_poll.c驱动。后来慢慢了解到怎么使用串口实现输入子系统的按键等等。
也终于理解了输入子系统怎么实现按键的,按下、释放、长按等实现原理。
才疏学浅,非科班出身,分享的多是些实用型的技术,想要深入学习内核的我分享的内容可能不太适合。有讲的不对的地方,欢迎指出来。
gpio_keys.c配置方法
驱动位置:drivers/input/keyboard/gpio_keys.c
该驱动是利用gpio管脚可配置边沿中断的功能,分别在上升沿或则下降沿中断触发后再进行软件消抖,通过输入子系统上报输入事件的变化。
这一层其实只检测了按键按下和释放,长按实际上是输入子系统实现。
原理就是当产生按键pressed事件后,如果250ms内没有收到release,那么内核就会上报该键被持续按下,之后每35ms上报一次直到按键释放。
这就是长按的内核实现原理,和后面不同的驱动实现的原理也都是一样的。
内核配置中使能该驱动。
设备树配置:
文章讲的都是通过设备树配置,最早的通过板级文件配置的方式现在也几乎都淘汰了,以前的项目配置过,配置文件乱糟糟的一大堆,也不想再去翻看了。还是设备树好用,配置信息一目了然,层次结构也清晰。
#include <dt-bindings/input/input.h>
#include "m300.dtsi"
#include <dt-bindings/interrupt-controller/irq.h>
#include <generated/autoconf.h>
/{
gpio_keys: gpio_keys {
compatible = "gpio-keys";
autorepeat;
up {
label = "GPIO Key UP";
linux,code = <103>;
gpios = <&gpb 26 GPIO_ACTIVE_HIGH INGENIC_GPIO_NOBIAS>;
;
down {
label = "GPIO Key DOWN";
linux,code = <108>;
gpios = <&gpb 27 GPIO_ACTIVE_HIGH INGENIC_GPIO_NOBIAS>;
;
};
};
在gpio_keys分支下继续添加需要的按键信息即可, 其中autorepeat 这个属性是让按键支持长按效果。
gpio_keys_polled.c配置方法
polled,顾名思义就是轮训的意思,当年不知道有这个驱动,只会用上面这个驱动。所以配置龙芯按键驱动的时候,是修改了内核源码,当下降沿中断发生后,立即配置成上升沿中断,如此反复交替,基本能够实现按键的功能。
后来发现轮询的驱动方式后,不支持中断的gpio也就都不是问题了。
驱动位置:drivers/input/keyboard/gpio_keys_poll.c
这个驱动就没什么可讲的,就是把轮询的事情放在内核里面来完成,用户态程序按照输入子系统来编程即可。同样也是支持pressed、continue pressed、release,甚至在作为普通按键,我都建议统一成轮询的方式,可以减少中断的产生,降低上下文切换。
设备树配置:
gpio_keys {
compatible = "gpio-keys-polled";
#address-cells = <1>;
#size-cells = <0>;
autorepeat;
poll-interval = <100>;
up {
label = "UP"; /*键值196*/
linux,code = <103>;
gpios = <&pioA 0 1>;
debounce-interval = <5>;
};
down {
label = "DOWN"; /*键值195*/
linux,code = <108>;
gpios = <&pioA 1 1>;
debounce-interval = <5>;
#gpio-key,wakeup;
};
};
串口键盘驱动
当我们需要实现更多键值的键盘时候,以上两种实现方式可能就有点不适合了。这时候就需要对按键进行编码,例如USB键盘、蓝牙键盘等。这里介绍一种在嵌入式平台上的一种简单灵活的实现方式:
适用场景:
不排除还有一些CPU保留了beyboard接口,那样也可以支持扫描矩阵式键盘,但是在一般的嵌入式CPU上还是很少见到这类接口。如上图,我们可以通过一片FPGA、MCU或则是一些专用芯片,来实现按键的硬件检测;然后将检测到的按键事件,编码后通过串口发送到CPU。CPU通过stowaway驱动,处理键值再提交给输入子系统。
至于FPGA、MCU的按键检测方法,可以是IO直连的,也可以是矩阵扫描,消抖那些工作也都由这一层来完成。
驱动位置:drivers/input/keyboard/stowaway.c
驱动实现方式是将串口作为一个serio总线设备,利用linux内核提供的serio总线驱动,通过设置对应的串口,调用setport提供的函数将串口当做serio总线设备,在驱动里面需要按照serio总线设备的驱动框架来实现。
驱动的核心代码就是通过serio接收一个字节的键值编码,然后解析该键值,提交输入子系统该键值和按键状态。
以4个按键为例,为了让键值更有通用性,采用linux的标准input code对键值进行编码。键值定义如下:
按键 | 标准键值 | pressed | release |
---|---|---|---|
KEY_UP | d 103 | 0x67 | 0xe7 |
KEY_DOWN | d 108 | 0x6c | 0xec |
ENTER | d 28 | 0x1c | 0x9c |
CANCEL | d 1 | 0x01 | 0x81 |
按照编码规则,占用位宽为1字节,其中最高为用来标志按下或则释放,所以理论上最多支持128个按键。
对驱动代码作出如下修改:
//static unsigned char skbd_keycode[128] = {
// KEY_1, KEY_2, KEY_3, KEY_Z, KEY_4, KEY_5, KEY_6, KEY_7,
// 0, KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_GRAVE,
// KEY_X, KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_SPACE,
// KEY_CAPSLOCK, KEY_TAB, KEY_LEFTCTRL, 0, 0, 0, 0, 0,
// 0, 0, 0, KEY_LEFTALT, 0, 0, 0, 0,
// 0, 0, 0, 0, KEY_C, KEY_V, KEY_B, KEY_N,
// KEY_MINUS, KEY_EQUAL, KEY_BACKSPACE, KEY_HOME, KEY_8, KEY_9, KEY_0, KEY_ESC,
// KEY_LEFTBRACE, KEY_RIGHTBRACE, KEY_BACKSLASH, KEY_END, KEY_U, KEY_I, KEY_O, KEY_P,
// KEY_APOSTROPHE, KEY_ENTER, KEY_PAGEUP,0, KEY_J, KEY_K, KEY_L, KEY_SEMICOLON,
// KEY_SLASH, KEY_UP, KEY_PAGEDOWN, 0,KEY_M, KEY_COMMA, KEY_DOT, KEY_INSERT,
// KEY_DELETE, KEY_LEFT, KEY_DOWN, KEY_RIGHT, 0, 0, 0,
// KEY_LEFTSHIFT, KEY_RIGHTSHIFT, 0, 0, 0, 0, 0,
// 0, 0, 0, 0, 0, 0, 0, 0,
// 0, 0, 0, 0, 0, 0, 0, 0,
// 0, KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7,
// KEY_F8, KEY_F9, KEY_F10, KEY_F11, KEY_F12, 0, 0, 0
//};
static unsigned char skbd_keycode[] = {
KEY_UP, KEY_DOWN, KEY_ENTER, KEY_ESC};
struct skbd
{
//unsigned char keycode[128];
unsigned char keycode[4];
struct input_dev *dev;
struct serio *serio;
char phys[32];
};
static irqreturn_t skbd_interrupt(struct serio *serio, unsigned char data,
unsigned int flags)
{
struct skbd *skbd = serio_get_drvdata(serio);
struct input_dev *dev = skbd->dev;
// if (skbd->keycode[data & SKBD_KEY_MASK]) {
// input_report_key(dev, skbd->keycode[data & SKBD_KEY_MASK],
// !(data & SKBD_RELEASE));
// input_sync(dev);
// }
if (data & SKBD_KEY_MASK)
{
input_report_key(dev, data & SKBD_KEY_MASK,
!(data & SKBD_RELEASE));
input_sync(dev);
}
return IRQ_HANDLED;
}
源码中的键值定义和键值查找被我修改了一下,源码中的匹配规则我也不太清除,应该是有相应的映射关系,不过不重要了。改了之后的规则简单明了,有新增需求直接按照以上表格定义的规则添加键值即可。
驱动改好了,但是这时候驱动并不会被调用,烧写完成进系统后,设备目录下并没有出现对应的input/event节点,说明connect并没有被调用,也就是serio总线上并没有对应的serio设备。
此时串口还是作为一个tty设备存在,通过以下用户态代码,可以将tty设备设置成一个serio设备。该程序需要保持在后台运行,所以将程序启动为Deamon守护进程。代码如下:
#include <stdio.h> /*标准输入输出定义*/
#include <unistd.h> /*Unix标准函数定义*/
#include <stdlib.h>
#include <sys/types.h> /**/
#include <sys/stat.h> /**/
#include <sys/ioctl.h>
#include <fcntl.h> /*文件控制定义*/
#include <linux/fs.h>
#include <errno.h> /*错误号定义*/
#include <string.h>
#include <linux/fb.h>
#include <malloc.h>
#include <sys/mman.h> /**/
#include <termios.h> /*PPSIX终端控制定义*/
#include <linux/serio.h>
#define SERIO_ANG 0xff
#define SERIO_EGALAX 0x3f
#define SERIO_STOWAWAY 0x20
void Deamon()
{
pid_t pid = fork();
int size;
int i;
if (0 != pid)
{
exit(0);
}
setsid(); //创建新会话
pid = fork(); //
if (0 != pid)
{
exit(0);
}
chdir("/");
umask(0);
size = getdtablesize();
for (i = 0; i < size; i++)
{
close(i);
}
}
int main(int argc, char *argv[])
{
int ret;
int ldisc;
unsigned long type;
struct termios option;
int fd = -1;
Deamon();
//子进程工作
fd = open("/dev/ttyS2", O_RDWR | O_NONBLOCK | O_NOCTTY);
if (0 > fd)
{
perror("open");
return -1;
}
tcgetattr(fd, &option);
option.c_iflag = IGNPAR | IGNBRK;
option.c_cflag = HUPCL | CS8 | CREAD | CLOCAL | B9600;
option.c_cc[VMIN] = 1;
option.c_cc[VTIME] = 0;
cfsetispeed(&option, B9600);
cfsetospeed(&option, B9600);
ret = tcsetattr(fd, TCSANOW, &option);
if (0 > fd)
{
perror("TCSANOW");
return -1;
}
ldisc = N_MOUSE;
ret = ioctl(fd, TIOCSETD, &ldisc);
if (ret)
{
perror("TIOCSETD");
}
type = SERIO_STOWAWAY | (SERIO_ANG << 8) | (SERIO_ANG << 16);
ret = ioctl(fd, SPIOCSTYPE, &type);
if (ret)
{
perror("SPIOCSTYPE");
}
read(fd, NULL, 0);
return 0;
}
将以上代码修改成对应的串口号,编译后上板子执行,内核提示生成了对应的event节点:
[ 149.801033] input: Stowaway Keyboard as
/devices/platform/apb/10032000.serial/tty/ttyS2/serio2/input/input3
[ 149.811235] serio: Serial port ttyS2
这时候,就可以按照标准的input输入设备进行编程。
其他输入设备实现方式
linux还提供了一种更灵活的输入设备驱动实现方式,就是完全基于用户态的编程。
驱动路径:drivers/input/misc/uinput.c
该驱动可以用于自动测试脚本、外挂、VNC等网络应用;例如远端通过网络发送输入事件,本地通过uinput编程的方式,将事件通知到内核,内核再按照输入子系统的方式,把输入事件分发到各个检测模块。
这个目前之用在过一个仪表的远程桌面开发项目,了解的也不够深入。等有机会再探索一些,再来分享。