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在整个键盘驱动程序中,起着将上层通用 抽象层和底层硬件处理层粘和起来的关键作用。
键盘模式
键盘模式有4种, 在Linux 下你可以用kbd_mode -参数 来设置和显示你的模式:
1) Scancode mode (raw )raw模式:将键盘端口上读出的扫描码放入缓冲区
2) Keycode mode (mediumraw) mediumraw模式:将扫描码过滤为键盘码放入缓冲区
3) ASCII mode (XLATE ) XLATE模式:识别各种键盘码的组合,转换为TTY终端代码放入缓冲区
4) UTF-8 MODE (UNICODE) Unicode 模式:UNICODE模式基本上与XLATE相同,只不过可以通过数字小键盘间接输入UNICODE代码。
键盘模块的上层漫游:
键盘模块的最上层严格的说应该是TTY设备,这在以后描述。对于使用者而言,keyboard.c是我们的最上层。
在keyboard.c中,不涉及底层操作,也不涉及到任何体系结构,他主要负责:键盘初始化、键盘tasklet的挂入、按键盘后的处理、keymap的装入、scancode的转化、与TTY设备的通信。
Keyboard.c是一个大杂烩,包含了大多数keyboard的key的处理,因此代码才变的庞大和复杂,我们可以修改它,让他变的很小(但这样做并不会使空间节省,因为keyboard.c的许多代码是动态加载的)。
Keyboard.c 中的int __init kbd_init(void) 函数是键盘代码执行的开始点,但keyboard.c并非是一定 执行的,你必须先定义CONFIG_VT(Character devices ->nbsp;Virtual terminal)才有效,为什 么?因为kbd_init()是在tty_io.c 的 tty_init()中被调用的。
★Kbd_init()
在进行一些基本初始化后,执行kbd_init_hw(),此函数可以看做是一个统一的上层界面,对于不同的体系或 相同体系下不同的board,他们的kbd_init_hw()的实现代码是不同的(同过CONFIG_XXX_XXX来实现),他的实现代码就是操作硬 件。 kbd_init_hw()会:检查硬件及有效性、分配资源和中断、操作寄存器的实现函数调用。
初始化一完成,就把keyboard_tasklet放到CPU tasklet中(注意到keyboard_tasklet 是开放出来的,等下可以看到怎么使用它)。
现在就等你按键了,当有按键时,会调用键盘中断处理函数,一般都是体系下的keyboard_interrupt(),此函数会调用到keyboard.c中的handle_scancode() 。
★handle_scancode()
是我们的重点,这个函数最终决定了我们按下的key如何处理,在下面会分析他的流程,他主要做:与TTY的通信、keymap的装入、按键的处理。
handle_scancode()处理的结果就是把你按下的key发到不同的处理函数中,一般的,这些函数离最终的结果已经很近了。这其中,大多数的函数都会调用put_queue() 函数。
★put_queue()
工作原理就是利用TTY, 它将经过转换的键盘功能码用tty_insert_flip_char()放入到当前 TTY的flip buffer中,然后将缓冲区输出任务函数(flush_to_ldisc)添加到控制台任务队列(con_task_queue)并 激活控制台软中断执行该任务函数. flush_to_ldisc()翻转读写缓冲区,将缓冲区接收数据传递给tty终端规程的 n_tty_receive_buf()接收函数,n_tty_receive_buf()处理输入字符,将输出字符缓冲在终端的循环缓冲区 (read_buf)之中.用户通过tty规程的read_chan()读取read_buf中的字符.当多个进程同时读取同一终端时, 使用tty- >tomic_read信号灯来竞争读取权.