Linux 系统键盘驱动总体思路
- 采用层次型的结构
- 划分成两层,上层是个通用的键盘抽象,完成键盘中不依赖于底层具体硬件的功能;下层是硬件处理层,负责对硬件进行直接操作。
- 按键按下以中断方式处理,使用 tasklet 处理 bottom half, 扫描码的翻译在 top half 中完成, 字符的显示在 bottom half 中完成。
- 上层中 handle_scancode 是驱动的核心,它首先将扫描码转换成键码,接着根据shift, alt等扩展键的按下情况将键码转换成目标码,一般情况下是ASCII码,最后将该ASCII码放到终端设备的缓冲区中,并且调度一个tasklet负责 将其在显示器上回显出来。
- 下层定义了几个回调函数,供上层公共部分调用,包括:
- kbd_init_hw : 初始化硬件;
- kbd_translate: 扫描码转成键码,由上层的 handle_scancode 调用;
- kbd_unexpected_up:
- 中断处理负责扫描特殊键盘,确定哪个键被按下,并且拿到稳定的扫描码,然后调用内核导出函数 handle_scancode。
- 处理抖动问题
Linux键盘驱动简介
Linux中的大多数驱动程序都采用了层次型的体系结构,键盘驱动程序也不例外。 在Linux中,键盘驱动被划分成两层来实现。其中,上层是一个通用的键盘抽象 层,完成键盘驱动中不依赖于底层具体硬件的一些功能,并且负责为底层提供服 务;下层则是硬件处理层,与具体硬件密切相关,主要负责对硬件进行直接操作。
键盘驱动程序的上层公共部分都在driver/keyboard.c中。该文件中最重要的就 是内核用EXPORT_SYMBOL这个宏导出的handle_scancode函数。
handle_scancode 完成的功能是:首先将扫描码转换成键码,接着根据shift, alt等扩展键的按下情况将键码转换成目标码,一般情况下是ASCII码,最后将该 ASCII码放到终端设备的缓冲区中,并且调度一个tasklet负责将其在显示器上回 显出来。可以看出,这个函数完成的是键盘驱动程序中最核心的一些工作,而这 些核心的逻辑功能是不依赖于底层硬件的,所以可以将其独立出来,并且导出给 底层的硬件处理函数调用。
在这个文件中还定义了其它几个回调函数,它们由键盘驱动程序中的上层公共部 分调用,并由底层硬件处理函数实现。比如kbd_init_hw,kbd_translate,kbd_unexpected_up等等。其中kbd_translate由 handle_scancode 调用,负责将扫描码转换成键码;键盘驱动程序的底层硬件处 理部分则根据不同的硬件有不同的实现。例如PC平台上标准键盘的底层硬件处理 函数都集中在 driver/Pc_keyb.c 中。这个文件包括了键盘中断处理函数 keyboard_interrupt,扫描码到键码转换函数pckbd_translate等其他一些与底层硬件密切相关的函数。
在这种体系结构下,要添加一块特殊键盘到系统中就显得格外清晰。开发者只需 为其编写驱动程序中的底层硬件处理函数,就可以将该键盘驱动起来。一般说来, 底层硬件处理函数中最重要的工作就是在键盘中断处理中获取被按下键的扫描码, 并且以它为参数调用handle_scancode,该扫描码可以自己定义,但它必须唯一 地标识出被按下键在键盘上的位置。此外,开发者还需要提供对应的从自定义扫 描码到键码的转换函数kbd_translate。具体的键码转换,将目标码放到终端的 输入缓冲区,以及回显等工作都由handle_scancode负责完成。在此我们也可以 看出,内核导出函数handle_scancode在整个键盘驱动程序中,起着将上层通用 抽象层和底层硬件处理层粘和起来的关键作用。
应用实例
下面我们将以一个具体的应用实例来说明在嵌入式Linux系统中给一个特殊键盘编写驱动程序的具体过程。
硬件模块描述
本系统的构建选用了三星公司的S3C2410开发板作为硬件平台。特殊键盘的硬件模块主要由两个SN74hc164芯片和一个4行16列的矩阵扫描 电路构成。SN74hc164是一个8位的串形输入并形输出移位寄存器,它的内部由8个D触发器串联而成。其工作原理简单说来是这样 的,SN74hc164芯片在时钟CLK脉冲的上升沿将A,B引脚上的串形输入在8个时钟脉冲以后并行输出到输出引脚QA到QH。其真值表见图1所示。
两个SN74hc164芯片先串联后,将它们的CLK引脚和CLR引脚分别接到S3C2410开发板的GPB2和GPB4端口上,并且将第一个 SN74hc164芯片的A,B引脚接到开发板的GPB1端口上,这三个GPIO端口配置成输出端口。这样我们就借助于两个SN74hc164寄存器,实 现了只占用3个GPIO端口,给矩阵扫描电路的16列提供输入,从而既节约了成本,又避免了GPIO资源的浪费。但这同时也给键盘驱动程序的实现带来了一 定的麻烦,驱动程序首先要将SN74hc164驱动起来,然后才能对矩阵电路的16列进行控制。该矩阵电路的4个行引脚分别被接到S3C2410的 GPG6,GPG7,GPG8,GPG9端口上,并且这四个端口被配置成中断源。无键按下时直接读为高电位,使用时通过SN74hc164芯片先将键盘的 16列置低电位,任何一个键被按下,相应的行GPG端口就会有从高到低的电压跳变,从而触发一次中断。
软件模块描述
初始化部分。这部分包括硬件层和软件层上的初始化。在本例中,需要先对矩阵电路和SN74hc164芯片所使用到的GPIO端口作配置,以使CPU 可以对它们进行控制和访问。为了要将某个GPIO端口配置成输入输出或者是中断源,需要在对应的GPIO控制寄存器中设置正确的值,具体的值可以通过查阅 S3C2410开发板手册来获得。比如,为了将GPB1设置成SN74hc164的输入端,需要将GPBCON这个控制字中2,3两位设置成二进制的 01,为了将GPG6设置成电压低跳变中断源,需要将GPGCON中12,13两位设置成二进制的10。在完成了硬件初始化操作以后,就是软件层上的初始 化了。首先将键盘中断处理函数注册到系统,然后设置好一个定时器结构,以便在中断发生时将其挂到内核的定时器队列中去,该定时器将触发对键盘的扫描操作。 最后通过SN74hc164将矩阵电路的16列置零。
中断处理部分。如前所述,这部分软件应该完成的工作就是扫描特殊键盘,确定哪个键被按下,并且拿到稳定的扫描码,然后调用内核导出函数 handle_scancode。在这个应用中,该特殊键盘的布局与PC标准键盘的布局比较相似,所以我们直接将PC键盘上对应键的系统扫描码作为我们特 殊键盘上各个键的扫描码,同时我们将PC键盘驱动程序中扫描码到键码的转换函数pckbd_translate作为我们的kbd_translate函 数。
确定哪一个键被按下的算法如下。在中断到来时,我们已经可以根据中断号确定 被按下的键在哪一行,我们还需要确定被按下的键在哪一列。为此,我们先给串 联的两个SN74hc164芯片送一个CLR信号,清零,然后送16个1,使得特殊键盘的 列均为高电位,此时我们在键盘的行端口读到的都是高电位。在16个时钟脉冲下, 给 SN74hc164 芯片送入1个0和15个1,使得0在每一列上都唯一出现一次,于此同时在键盘行端口进行扫描。当被按下键所在列置0时,其所在行就会读到一个低电位。使用 这种“走0法”,我们就可以确定出键盘上哪个键被按下了。但是这种简单的扫描算法还不够,因为在这种类型的矩阵扫描键盘中,键的每次按下和抬起都会有 10~20ms(这段时间的长短由硬件特性决定)的毛刺抖动存在,如图2所示,所以为了获取稳定的按键信息,必须要想办法去掉这种抖动,才能避免将用户的 一次按键误当作几次按键来处理。去毛刺的一种常见的方法是在有键盘中断到达时,并不立即去扫描键盘,而是先等待一段时间,等跳过毛刺抖动以后再去扫描键 盘,其伪代码如下所示:
等待一段时间,跳过抖动;
扫描键盘;
if 键盘上没有键被按下
结束返回;
if 键盘上有键被按下
再次等待一段时间然后检查同样的键是否依然处于被按下状态;
if 同样的键任然是按下
将读到的扫描码返回;
else
直接返回;
这种解决方案固然可行,但是它使用了忙等的方法去毛刺,在忙等期间,系统做不了任何有用的工作。这对于计算资源本身就很有限的嵌入式Linux系统来说,是一种奢侈的浪费。本应用中,我们设计了一种适合嵌入式系统的去毛刺解决方案,使用效果良好。
由于 Linux 内核提供了定时器队列,所以我们可以使用这种机制来避免忙等, 提高系统的性能。当键盘上有键被按下时,键盘中断处理程序首先关闭中断源, 进入轮询模式,将一个 timerlist 对象挂入定时器队列以后就结束了。挂入内 核的定时器按时地被触发,它所触发的函数完成以下一些工作:先对整个键盘上 所有的键进行一次扫描,并且将扫描得到的结果保存到一个静态2维数组变量 snap_shot_matrix(16)(4) 中。该变量描述的是在本次键盘扫描的这个时刻,键 盘上所有键的按下情况。如果某个键没有被按下,即处于松开状态,那么将 snap_shot_matrix 中对应的值置为 0,如果某个键处于按下状态,那么将 snap_shot_matrix 中对应的值作自增1操作,若该值在自增 1 以后大于某个预 先指定的数,我们就可以认为这是一个稳定值,并且将另一个大小为 16*4 的 2 维数组变量 current_matrix 对应坐标中的值置 1,否则置 0。这个变量描述的 就是当前键盘上按键情况的稳定值了。也就是说我们首先把在本次扫描中得到的 采样数据作处理以后保存到 snap_shot_matrix 中,然后依据该变量中的值,过 滤得到 current_matrix,通过这样一个过程来做去毛刺处理。在得到了本次扫 描的稳定值 current_matrix 以后,我们将其与上次得到的稳定值 previous_matrix 作比较,从而确定与上次扫描时相比,此刻键盘上的按键情况 是否发生了变化,以及此刻键盘上是否有键按下。如果发现键盘上没有任何键被 按下,则打开键盘中断,再次切回到中断模式。如果键盘上有键被按下,并且是 不同于上次扫描到的被按下键,我们立刻调用按键处理函数 process_key,它会 调用键盘驱动中的上层函数 handle_scancode 。如果键盘上按下的键就是上次 按下的那个键,我们将递增一个计数器,当这个计数器达到某个指定值以后,我 们就启动所谓的 Auto repeat 功能,即用户一直按着某个键,驱动程序自动重 复产生键盘输入。该计数器在被按下键发生变化时置 0。但是只要键盘上仍然有 键处于被按下状态,我们就将当前读到的键盘稳定值 current_matrix 拷贝到 previous_matrix 中去,并且再次将前面描述的定时器对象挂到内核定时器队列中,过一段时间以后再次扫描整个键盘,直至键盘上没有键被按下。