之编写4X4矩阵键盘驱动及基于QT GUI的键盘输入测试
【创龙AM4379 Cortex-A9试用体验】之编写4X4矩阵键盘驱动及基于QT GUI的键盘输入测试
在上篇试用报告《【创龙AM4379 Cortex-A9试用体验】之采用GPIO扩展口自定义键盘Input驱动+QT界面键盘输入测试》中,我们用4个按键模拟了数字1,数字2,以及TAB、ENTER键,使得通过GPIO的扩展口连接按键,结合上拉电阻实现了一般机械键盘的功能,但是,对按键较少的应用中,比如只需要输入0-9字符时,单个按键连接单个I/O口实现键盘功能还可以采用,但是如果我们需要一个功能相对较完整的数字键盘,如需具有0-9,小数点、空格键、TAB键、ENTER键、SHIFT键、退格键等等时,如果采用上一篇试用报告讲解的方法,就有点儿太浪费GPIO端口了,在工业应用中,GPIO口是非常宝贵的资源,我们在设计输入输出时,尽量会留一些备用接口,以备工业调试现场时一些为规划功能的扩展需要。我们这里采用单片机通常都会采用的行X列矩阵键盘,由行触发的中断,调用列的扫描方式,测试用户到底按下了哪个键。
如果在单片机的应用中,实现4X4矩阵键盘的功能就比较简单了,而对于Linux系统下的4X4键盘,我们必须为其编写符合Linux Input规范的驱动程序。鉴与TL-4379运行的是QT5.4.1版本的GUI,而有别于QT4, QT5的软键盘开发变化较大,我暂时还没有搞定QT5的软键盘移植,但是又想有输入,并且满足特殊行业禁止使用触摸屏的要求,我们这里就以4X4矩阵键盘为例,开发其在Linux下的Input驱动程序,并在QT GUI界面测试编写的矩阵键盘驱动程序的正确性。
1. 硬件搭建
为了完成本次测试,并为今后的开发板功能开发提供便利,特意从某宝上买了一块4X4键盘,尽量减少开发过程中繁琐的连线。4X4矩阵键盘如图所示:
硬件原理图,及我对每个按键的功能规划如图所示:
从AM4379参考手册中,GPIO用作矩阵键盘是,触发中断的引脚需接上拉电阻,如图所示:
矩阵键盘与TL-4379开发板的连接如图所示:
这里特别强调一下,AM437X与AM335X芯片在引脚定义上有较大不同,在AM335X中,每个GPIO都与一个特殊功能一一对应,而在AM437X中,一个特殊功能引脚可以对应对个GPIO,如下图所示:
对于主功能为spi2_sclk,其对应gpio3_24和gpio0_22,至于spi2_sclk对应哪个GPIO,则需要我们设置0x7或0x9模式,而对于spi4_d1其只对应一个GPIO,即gpio5_6。对应一个主功能引脚只对应一个GPIO的情况,我们可以通过Linux系统下的gpio_request等申请资源,设置输入或输出,而对于一个主功能对应两个GPIO的情况,我们必须通过重映射该引脚的寄存器设置。简便起见,我这里主要选择了一个主功能只对应一个GPIO引脚的I/O口。
硬件连接效果如图所示:
2. 4X4矩阵键盘原理
4X4矩阵键盘的原理如下:
以上述原理图为例,我们设置L1-L4对应的I/O为输入,并且配置为带中断的I/O口,R1-R4对应的I/O口为输出。在键值开始扫描之前,设置R1-R4输出为低电平,当用户按下某个键后,对对应的行中断线被拉低,触发下降沿中断,在中断处理函数中,依次设置R1为0,看看这一列中的哪一行为0输入,如果没有0输入,机械R2输出0,看看R2这一列哪一行输入为0,依次类推,直到确定触发中断的那个按钮所在的行和列,这样我们就可计算该键的键值KEB-VAL=4*ROW + COL。
3. 驱动程序开发
3.1 头文件
除了包含一般的头文件外,还要包含input.h头文件,该文件中包含了每个键盘按键对应的键码值。
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/fs.h>
#include<linux/init.h>
#include<linux/delay.h>
#include<linux/cdev.h>
#include<linux/miscdevice.h>
#include<linux/sched.h>
#include<linux/pm.h>
#include<linux/sysctl.h>
#include<linux/proc_fs.h>
#include<linux/irq.h>
#include<linux/device.h>
#include<linux/interrupt.h>
#include<linux/semaphore.h>
#include<linux/signal.h>
#include<linux/platform_device.h>
#include<linux/input.h>
#include<linux/gpio.h>
#include<asm/uaccess.h>
#include<linux/irq.h>
#include<asm/irq.h>
#include<linux/poll.h>
#include<linux/timer.h>
3.2 按键编号及中断编号定义
#defineGPIO_L1_PIN_NUM (3*32 + 14) /* gpio 3_14 */
#defineGPIO_L2_PIN_NUM (3*32 + 16) /* gpio 3_16 */
#defineGPIO_L3_PIN_NUM (4*32 + 3) /* gpio 4_3 */
#defineGPIO_L4_PIN_NUM (4*32 + 2) /* gpio 4_2 */
#defineGPIO_R1_PIN_NUM (3*32 + 17) /* gpio 3_17 */
#defineGPIO_R2_PIN_NUM (5*32 + 6) /* gpio 5_6 */
#defineGPIO_R3_PIN_NUM (5*32 + 4) /* gpio 5_4 */
#defineGPIO_R4_PIN_NUM (0*32 + 5) /* gpio 0_5 */
#defineL1_IRQ gpio_to_irq(GPIO_L1_PIN_NUM) /*KEY1对应的中断号*/
#defineL2_IRQ gpio_to_irq(GPIO_L2_PIN_NUM) /*KEY2对应的中断号*/
#defineL3_IRQ gpio_to_irq(GPIO_L3_PIN_NUM) /*KEY3对应的中断号*/
#defineL4_IRQ gpio_to_irq(GPIO_L4_PIN_NUM) /*KEY4对应的中断号*/
3.3 关键结构体及初始化
staticstruct input_dev *buttons_dev; /*输入设备结构体指针,作为全局变量为其他函数使用*/
staticstruct timer_list buttons_timer; /*定义消抖定时器*/
structbutton_irq_desc *button_irq_g; /*保存触发中断的那个结构体*/
unsignedint key_value = KEY_0; //全局按键码
structbutton_irq_desc {
int irq;
int pin;
int number;
char *name;
};
/*********************************************************
*行I/O端口定义
********************************************************/
staticunsigned long row_table [] = {
GPIO_R1_PIN_NUM,
GPIO_R2_PIN_NUM,
GPIO_R3_PIN_NUM ,
GPIO_R4_PIN_NUM,
};
/*********************************************************
*列I/O端口定义
********************************************************/
staticstruct button_irq_desc button_irqs [] = {
{-1, GPIO_L1_PIN_NUM, 0, "KEY1"},
{-1, GPIO_L2_PIN_NUM, 1, "KEY2"},
{-1,GPIO_L3_PIN_NUM, 2, "KEY3"},
{-1, GPIO_L4_PIN_NUM, 3, "KEY4"},
};
staticvolatile int ev_key = 0;
structtimer_list mytimer;
3.4 使能、禁止中断
在按键的扫描过程中,需要反复的拉低中断输入引脚,而这时我们只想读取引脚的状态,而无需让下降沿触发中断,所以在扫描码值时,我们关闭中断,扫描结束后,再次使能中断,为下一次按键读取做准备。
static voiddisable_irqs(void)
{
disable_irq(L1_IRQ);
disable_irq(L2_IRQ);
disable_irq(L3_IRQ);
disable_irq(L4_IRQ);
}
staticvoid enable_irqs(void)
{
enable_irq(L1_IRQ);
enable_irq(L2_IRQ);
enable_irq(L3_IRQ);
enable_irq(L4_IRQ);
}
3.5 按键扫描程序
根据第二节介绍的原理,形成按键扫描算法:
依次拉低某列的输入值
staticvoid buttons_kscan_reset(int row)
{
int i;
for(i=0; i < 4; i++)
{
if(i == row)
gpio_set_value(row_table,0); //行电平设置,第row列置低,其他置高
else
gpio_set_value(row_table, 1);
}
}
扫描实体:
static intbuttons_scan(void)
{
int i,j,k=0;
int column = 0; //列
int row = 0; //行
//printk("11111111111111111111111111111111111111/n");
disable_irqs();
for (i = 0; i < 4; i++) //列线置低
{
gpio_set_value(row_table, 0);
}
for (j = 0; j < 4; j++)
{
if(gpio_get_value(button_irqs[j].pin)== 0 ) //若某一列变低电平则保存该列值
{
column = j;
break;
}
}
if(j<4)
{
for ( j = 0; j < 4; j++)
{
buttons_kscan_reset(j); //0111 1011 1101 1110
ndelay(100);
if(gpio_get_value(button_irqs[column].pin) == 0) //扫描行
{
row = j;
break;
}
}
k = column * 4 + row;
ev_key = k; //第k个按键被按下
}
for (i = 0; i < 4; i++) //列线置低
{
gpio_set_value(row_table, 0);
}
//发送按键码
/* 松开 : 最后一个参数: 0-松开, 1-按下 */
switch(ev_key)
{
case 0:
key_value = KEY_1;
break;
case 1:
key_value = KEY_2;
break;
case 2:
key_value = KEY_3;
break;
case 3:
key_value = KEY_BACKSPACE;
break;
case 4:
key_value = KEY_4;
break;
case 5:
key_value = KEY_5;
break;
case 6:
key_value = KEY_6;
break;
case 7:
key_value = KEY_TAB;
break;
case 8:
key_value = KEY_7;
break;
case 9:
key_value = KEY_8;
break;
case 10:
key_value = KEY_9;
break;
case 11:
key_value = KEY_LEFTSHIFT;
break;
case 12:
key_value = KEY_0;
break;
case 13:
key_value = KEY_DOT;
break;
case 14:
key_value = KEY_SPACE;
break;
case 15:
key_value = KEY_ENTER;
break;
}
input_event(buttons_dev, EV_KEY,key_value, 1);
input_sync(buttons_dev);
del_timer(&mytimer);
enable_irqs();
return 0;
}
voidset_timer(void)
{
init_timer(&mytimer);
mytimer.expires = jiffies + 10;
mytimer.function = buttons_scan;
add_timer(&mytimer);
}
3.6 中断处理程序及定时器消抖
由于机械式按键的固有特性,我们必须通过硬件或软件的方式对按键抖动进行过滤处理,我们这里采用定时器避开按键10-20ms的抖动窗口前期,按键的扫描触发实际上是在消抖定时器的回调函数中执行的。
/*********************************************************
*中断程序,在其中调用定时器,
*进行定时及扫描。
********************************************************/
staticirqreturn_t buttons_interrupt(int irq, void *dev_id)
{
button_irq_g = (struct button_irq_desc*)dev_id;
mod_timer(&buttons_timer,jiffies+HZ/8); //设置去抖时间
return IRQ_RETVAL(IRQ_HANDLED);
}
staticvoid buttons_timer_function(unsigned long data)
{
int down;
disable_irqs();
down =!gpio_get_value(button_irq_g->pin);
if(down) //按下
{
set_timer();
}
else //松开
{
/* 松开 : 最后一个参数: 0-松开, 1-按下 */
unsigned int key_value = KEY_0;
switch(ev_key)
{
case 0:
key_value = KEY_1;
break;
case 1:
key_value = KEY_2;
break;
case 2:
key_value = KEY_3;
break;
case 3:
key_value =KEY_BACKSPACE;
break;
case 4:
key_value = KEY_4;
break;
case 5:
key_value = KEY_5;
break;
case 6:
key_value = KEY_6;
break;
case 7:
key_value = KEY_TAB;
break;
case 8:
key_value = KEY_7;
break;
case 9:
key_value = KEY_8;
break;
case 10:
key_value = KEY_9;
break;
case 11:
key_value =KEY_LEFTSHIFT;
break;
case 12:
key_value = KEY_0;
break;
case 13:
key_value = KEY_DOT;
break;
case 14:
key_value =KEY_SPACE;
break;
case 15:
key_value =KEY_ENTER;
break;
}
input_event(buttons_dev, EV_KEY,key_value, 0);
input_sync(buttons_dev);
}
enable_irqs();
}
3.7 初始化驱动模块
在驱动的初始化模块中,我们首先申请GPIO资源,设置行中断输入,列GPIO输出,然后动态分配Input设备描述结构体,设置该输入设备可向系统提交的按键值,最后注册该输入结构体。
static int__init dev_init(void)
{
int ret;
int i;
int err = 0;
/*设置中断号*/
button_irqs [0].irq = L1_IRQ;
button_irqs [1].irq = L2_IRQ;
button_irqs [2].irq = L3_IRQ;
button_irqs [3].irq = L4_IRQ;
/*申请中断引脚对应的GPIO*/
ret = gpio_request_one(button_irqs[0].pin,GPIOF_IN, "L1 IRQ"); /* 申请 IO ,为输入*/
if (ret < 0) {
printk(KERN_ERR "Failed torequest GPIO for L1\n");
}
ret = gpio_request_one(button_irqs[1].pin,GPIOF_IN, "L2 IRQ"); /* 申请 IO ,为输入*/
if (ret < 0) {
printk(KERN_ERR "Failed to requestGPIO for L2\n");
}
ret = gpio_request_one(button_irqs[2].pin,GPIOF_IN, "L3 IRQ"); /* 申请 IO ,为输入*/
if (ret < 0) {
printk(KERN_ERR "Failed torequest GPIO for L3\n");
}
ret = gpio_request_one(button_irqs[3].pin,GPIOF_IN, "L4 IRQ"); /* 申请 IO ,为输入*/
if (ret < 0) {
printk(KERN_ERR "Failed torequest GPIO for L4\n");
}
/*申请列引脚对应的GPIO*/
ret = gpio_request(row_table[0], "R1IRQ"); /* 申请 IO ,为输入*/
if (ret < 0) {
printk(KERN_ERR "Failed torequest GPIO for R1\n");
}
ret = gpio_request(row_table[1], "R2IRQ"); /* 申请 IO ,为输入*/
if (ret < 0) {
printk(KERN_ERR "Failed torequest GPIO for R2\n");
}
ret = gpio_request(row_table[2], "R3IRQ"); /* 申请 IO ,为输入*/
if (ret < 0) {
printk(KERN_ERR "Failed torequest GPIO for R3\n");
}
ret = gpio_request(row_table[3], "R4IRQ"); /* 申请 IO ,为输入*/
if (ret < 0) {
printk(KERN_ERR "Failed torequest GPIO for R4\n");
}
/* 设置中断引脚为输入 */
for(i=0;i<4;i++)
gpio_direction_input(button_irqs.pin);
/* 设置列引脚为输出 */
for(i=0;i<4;i++)
{
gpio_direction_output(row_table,0);
}
/* 为GPIO3_14,GPIO3_16,GPIO4_2,GPIO4_3申请中断 */
if(request_irq(button_irqs [0].irq,buttons_interrupt, IRQ_TYPE_EDGE_BOTH, "K1", &button_irqs[0]))
{
printk(KERN_ERR "Failed torequest IRQ for KEY1\n");
}
if(request_irq(button_irqs [1].irq,buttons_interrupt, IRQ_TYPE_EDGE_BOTH, "K2", &button_irqs[1]))
{
printk(KERN_ERR "Failed torequest IRQ for KEY2\n");
}
if(request_irq(button_irqs [2].irq,buttons_interrupt, IRQ_TYPE_EDGE_BOTH, "K3", &button_irqs[2]))
{
printk(KERN_ERR "Failed torequest IRQ for KEY3\n");
}
if(request_irq(button_irqs [3].irq,buttons_interrupt, IRQ_TYPE_EDGE_BOTH, "K4", &button_irqs[3]))
{
printk(KERN_ERR "Failed torequest IRQ for KEY4\n");
}
/* 1. 分配一个input_dev结构体 */
buttons_dev = input_allocate_device();;
/* 2. 设置 */
/* 2.1 能产生哪类事件 */
set_bit(EV_KEY, buttons_dev->evbit);
set_bit(EV_REP, buttons_dev->evbit);
/* 2.2 能产生这类操作里的哪些事件: 0,1,TAB,ENTER */
set_bit(KEY_0, buttons_dev->keybit);
set_bit(KEY_1, buttons_dev->keybit);
set_bit(KEY_2, buttons_dev->keybit);
set_bit(KEY_3, buttons_dev->keybit);
set_bit(KEY_4, buttons_dev->keybit);
set_bit(KEY_5, buttons_dev->keybit);
set_bit(KEY_6, buttons_dev->keybit);
set_bit(KEY_7, buttons_dev->keybit);
set_bit(KEY_8, buttons_dev->keybit);
set_bit(KEY_9, buttons_dev->keybit);
set_bit(KEY_BACKSPACE,buttons_dev->keybit);
set_bit(KEY_TAB, buttons_dev->keybit);
set_bit(KEY_LEFTSHIFT,buttons_dev->keybit);
set_bit(KEY_ENTER,buttons_dev->keybit);
set_bit(KEY_SPACE,buttons_dev->keybit);
set_bit(KEY_DOT, buttons_dev->keybit);
/* 3. 注册 */
input_register_device(buttons_dev);
/*消抖定时器*/
init_timer(&buttons_timer);
buttons_timer.function =buttons_timer_function;
add_timer(&buttons_timer);
return ret;
}
3.8 驱动模块退出函数
释放GPIO资源,释放申请的中断,及注销Input设备。
staticvoid __exit dev_exit(void)
{
int i;
/*卸载模块时,释放中断*/
for (i = 0; i <sizeof(button_irqs)/sizeof(button_irqs[0]); i++)
{
if (button_irqs.irq < 0)
{
continue;
}
free_irq(button_irqs.irq, (void*)&button_irqs);
}
/*卸载模块时,释放GPIO资源*/
for (i = 0; i < 4; i++)
{
gpio_free(button_irqs.pin);
}
for (i = 0; i < 4; i++)
{
gpio_free(row_table);
}
del_timer(&buttons_timer);
input_unregister_device(buttons_dev);
input_free_device(buttons_dev);
}
3.9 其他代码
module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("LZP");
3.10 编译驱动模块
执行命令:
makeARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
编译结果如图所示:
将驱动模块key_board_have_input.ko拷贝到NFS共享目录。
4. QT GUI测试
QT GUI程序我们还采用上一篇试用报告中的加法运算界面。
启动TL-4379开发板,执行如下命令:
mount -tnfs 192.168.1.103:/nfsshare /mnt/ -o nolock
cd /mnt
insmodkey_board_have_input.ko
加载驱动后,执行结果如图所示:
执行命令,启动QT GUI测试程序:
./qt_keyboard_test -plugin tslib:/dev/input/touchscreen0
执行结果如图所示:
在第一个输入框,通过4X4矩阵键盘,输入58,如图所示:
按TAB键,光标跳转到第二个输入框,输入97,如图所示:
按TAB键,是光标跳转到“=”按钮,按钮边沿出现一个黑色的矩形框,如图所示:
按下ENTER键,即第16个键,计算加法结果如图所示:
按下TAB键,光标跳转到第一个输入框,如图所示:
58被选中,我们按下右上角的BACKSPACE键,即退格键,将58这个数字删除,如图所示:
我们再次在第一输入框内输入小数点,和空格键,效果如图所示:
5. 小结
我们在上一篇试用报告的基础上,结合矩阵键盘的扫描原理,编写了4X4矩阵键盘的驱动程序,通过在QT GUI上做功能测试,基本达到了预期效果,实现了0-9,小数点、退格键、空格键、ENTER键和SHIFT键功能,对于我在上一篇试用报告中提到的那个煤矿瓦斯浓度检测仪表,我们采用TL-4379和4X4矩阵键盘,完全可以实现客户提出的功能要求。通过开发板的试用,挖掘开发板的各项功能的过程中,我也在不断的学习,这些看似基本的功能实现,对我实际工作中的方案制定有很多帮助,使我的方案设计中不再仅仅限于单片机,或Cortex-M3、Cortex-M4的这些中低端应用了,今天就写这么多,下篇报告再见!
大写的赞
谢谢关注,多多交流
小编 想请教个问题 请问你的上层qt应用程序是如何使用这个外接矩阵键盘的 是通过其他自带的键盘事件直接响应的吗?
方便的话 可以留下qq聊聊 谢谢 我的qq:312855150
下载我提供的驱动和QT程序源码,里面注释的比较详细