学习中。。
分类: LINUX
2013-12-10 16:57:02
1.1、引言
通常,矩阵键盘的工作方式有三种,即:程序查询、定时查询和键盘中断扫描。键盘工作方式的选取应根据实际应用系统中CPU工作的忙、闲情况而定。
此次矩阵键盘的应用场合是数字电桥系统。一者,电桥系统对按键的响应速度、准确性都有较高的要求。二者,按键相关代码嵌入在linux系统中,linux系统中的其它程序(如LCD显示)对CPU的占用量相对较大,基本排除程序查询方式的使用。若采用中断扫描方式在组合按键时有一定的局限性,即不能组合同列的按键,否则无法响应中断。
相比以上两种方式,定时扫描在嵌入式系统中的应用最为广范。
linux源码中提供了input子系统来完成所有输入设备的抽象。Input子系统由设备驱动层、输入子系统核心层、事件处理层三部分组成。信息的传递由下图所示。
l 设备驱动层:将底层的硬件输入转化为统一事件形式,向输入核心层(Input Core)汇报。
l 输入子系统核心:承上启下。为驱动层提供输入设备注册与操作接口,如:input_register_device;通知事件处理层对事件进行处理;在/Proc下产生相应的设备信息等。
l 事件处理层:主要是和用户空间交互。(Linux中在用户空间将所有的设备都当做文件来处理,由于在一般的驱动程序中都有提供fops接口,以及在/dev下生成相应的设备文件nod,这些操作在输入子系统中由事件处理层完成)。
l 用户空间:一般包括C程序实现的应用程序和Qt程序。
图2 PS/2兼容键盘的数据流
从图2可以看出驱动程序员所要完成的是硬件相关部分(具体是输入设备驱动程序模块)。
设备驱动程序主要完成键盘的扫描,如果有按键按下或弹起,则通过input core完成两件事件:
l 报告事件
l 传递键码
本电路采用6*7矩阵键盘,共39个按键,如图3所示。其中行包括KEY_LINE[1:6],分别连接GPB5~GPB10作为电平输出引脚。列包括KEY_ROW[1:7],分别连接GPG5~GPG7,GPG11~GPG14作为电平输入脚,且使用内部上拉置高。
图3 矩阵键盘原理图
说明:上图中各列行上拉电阻,并未画出。若无外部上啦,一般应用场合采用内部上拉电阻也可。
系统初始化时将列设为输入,弱上拉;第1行输出低电平,其余行输出高电平(假设扫描顺序按标号由低到高);启动内核定时器。
定时间隔到,查询各列输入电平情况,从而确定第1行上各个按键状态与上次扫描时本行各按键状态比较。若有按键状态变化,此时的按键坐标为button(line(1),row(x)),将按键键码,状态等信息保存,报告事件到input core层(也可将每次扫描结果全部汇报,由应用程序决定按键状态是否改变)。同时将第2行输出低电平,其余行输出高电平。
定时间隔到,查询各列输入电平情况,从而确定第2行上各个按键状态与上次扫描时本行各按键状态比较。若有按键变化,此时的按键坐标为button(line(2),row(x)),将按键键码,状态等信息保存,报告事件到input core层。同时将第3行输出低电平,其余行输出高电平。
如此周期循环进行,即可方便将所有按键变化扫描到,并向input core层汇报,键码转换后供用户程序调用。
说明:为什么不在定时间隔到的时候一次性将6行都扫描了?
因为在定时间隔到来时,按上面方法一次将6行扫描,很可能会出现硬件不能及时响应指令,而造成扫描不到按键变化的现象(特别是在系统时钟较高的情况)。比如:第1行扫描完成后,将第2行置高,其余行置低来扫描低二行,这句话就两条C语言指令:拉低第1行,置高低2行。接下来马上扫描列的电平,但其实在上面两条指令结束时,硬件上并不一定完成这两条指令预期的动作。即第1行线路真的已经为0,第2行线路真的为1码?如果有键按着,此时列的数据寄存器里的值已经根据行电平的变化而变化了吗?其实这些是难以保证的,按键的阻抗,GPIO的阻性、容性都会滞后电平变化。所以难以保证一次性将所有行扫描完成这种方式的准确性和稳定性。
不像单片机定时中断处理函数,内核定时器调度的函数几乎肯定不会在注册这些函数的进程正在执行时运行。换言之,这些函数会异步的运行。
linux内核提供了定时器API
struct timer_list {
/*.............*/
unsigned long expires; //期望定时器执行的jiffies计数值
viod (*function)(unsigned long); //被调度函数(或函数集)指针
unsigned long data; //被调度函数传入参数
}
static struct timer_list scantimer;
接口函数:
viod init_timer(struct timer_list * timer) //初始化定时器节点
void add_timer(struct timer_list * timer) //将定时器节点加入定时队列,定时开始
viod mod_timer(struct timer_list * timer,unsigned long expires) //修改定时器到期时间
同时定义每个按键的数据结构
#define BUTTON_DOWN 1
#define BURRON_UP 0
struct button_desc {
int num; //按键编号,用来定位按键
int code; //按键键码,需遵循PS/2键码(input.h中已定义)
char status; //按键状态:BUTTON_DOWN 或 BUTTON_UP
};
struct button_desc pre_button[38]; //按键的上一次状态
struct button_desc curr_button[38]; //按键的当前状态
定义pre_button[38]和pre_button[38]的是为了1.2.2节提到的两次按键比较。若变换则传递键码及报告事件。
正如1.2.1节所述:input core层起了承上启下的作用,有为驱动层提供输入设备注册与操作接口的功能。
用于报告EV_KEY,EV_REL,EV_ABS事件的函数分别为:
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)
目前用到的是第一个函数,用于向事件层报告按键事件,函数的第一参数是值具体的设备;第二个参数指的是上节讲到的键码;第三个参数为按键状态值,1表示按下,0表示弹起。
例1:
input_report_key(button_dev, KEY_Z, 1); //报告’z’键按下
例2: input_report_key(button_dev, KEY_SLASH, 1); //报告’shift’键按下
input_report_key(button_dev, KEY_Z, 1); //报告’z’键按下
别忘了要让input core知道什么时候一次报告结束:
input_sync(struct input_dev *dev) //输入同步,用于告诉input core子系统报告结束。
在例1,例2最后必须加上input_sync(button_dev)才是一次有效的报告。
上面讲到input_report_key()用来报告EV_KEY事件,为什么可以这个EV_KEY甚至具体的一个键码会支持呢?
驱动实现——初始化(事件支持):
用set_bit()告诉input输入子系统支持哪些事件,哪些按键。例如:
set_bit(EV_KEY,button_dev.evbit) (其中button_dev是struct input_dev类型)
set_bit(KEY_1,button_dev.evbit)
struct input_dev中有两个成员为:
evbit:事件类型(包括EV_RST,EV_REL,EV_MSC,EV_KEY,EV_ABS,EV_REP等)
keybit:按键类型(当事件类型为EV_KEY时包括KEY_1,KEY_Z,BTN_LEFT,
BTN_0,BTN_1,BTN_MIDDLE等)。
最后注册设备:input_register_device(button_dev); //注册input设备
点击(此处)折叠或打开
上述方案已通过实际验证,现将驱动程序和测试程序贴出供大家参考:
驱动源码(可直接使用):
driver.rar
应用程序如下: