键盘驱动程序 键盘驱动程序是什么?

键盘驱动程序    键盘是如何工作的么工作的 , 当时是 Linux 0.11 是基于但不系统的 , 本文是 xv6 键盘驱动程序系统地讲述了键盘是如何工作的 。正如上面关于驱动程序的磁盘所说 , 它是硬件物理接口的包装 , 所以了解键盘驱动程序 , 或者首先了解键盘的一些物理接口 。
   键盘有两个芯片 , 一个是键盘编码器 i8048 , 另一个是键盘控制器 i8042 , 分别来看 。
   键盘编码器    键盘编码器位于键盘上 , 主要用于按下和弹起监控键 , 然后将两种状态编码发送到键盘控制器
   上述代码称为键盘扫描码 。有三种编码方法 。相应地 , 有三组键盘扫描码 。不要说每组键盘扫描码是如何编码的 。请参见以下链接 。今天的大多数键盘使用第二组键盘扫描码 , 但不排除使用第一组和第三组 。因此 , 为了兼容性 , 键盘控制器将转换为第一组扫描码 。当然 , 这是默认情况 。使用哪套扫描码以及控制器是否转换取决于硬件是否支持以及如何设置 。如果您感兴趣 , 请参阅文章末尾的链接 。
   所以第一套键盘扫描码还是要说的 , 按下一个键会弹起 , 所以每个键会有两个状态 , 即每个键对应两个扫描码 , 按下键时的编码称为通码() , 弹起时的编码称为断码()
   大多数键的通码和断码都是 8 位 1 字节 , 但有些操作控制键 , 如 Ctrl、Alt , 附加键如Insert , 如/ 、方向键等小键盘区域是 2 字节甚至多个字节 。多个字节的扫描码通常从 开始 。只有 一个键从 开始 。
   断码与通码的关系:断码通码 。二进制表示  , 所以可以这样理解断码和通码 。它们由 8 位比特组成 , 最高 7 位表示按钮状态 , 1 表示按下 , 0 表示弹起 。
   键盘控制器    键盘控制器(i8042) , 南桥芯片内 , 集成在南桥芯片上 。要接收键盘编码器发来的键盘扫描码 , 做一些处理(比如第二套扫描码转第一套) , 然后触发中断通知 CPU 读取扫描码
   4 8 键盘控制器bits 寄存器 , Status Register 和 Control Register , 两者共用一个端口 0x64 , 读的时候是状态寄存器 , 写的时候是控制寄存器 。Input Buffer 和 Output Buffer , 两者共用一个端口 0x60 , 读取时是输出缓冲器 , 写作时是输入缓冲器 。
   状态寄存器:    bit0:1 表示输出缓存器满 , CPU 读取后清零 。从编码器发送的扫描码放在这里 。
   bit1:1 表示输入缓存器满 , 读取后控制器清零 。
   控制寄存器:    通过写 0x64 端口向控制器发送命令 。请注意 , 它是向控制器本身发送命令 , 而不是向硬件设备键盘发送命令 。键盘的控制是通过控制器间接控制的 , 因此只需操作键盘即可 。
   命令控制器是将命令字节写入 0x64 端口 , 一般命令是一个字节 , 如果有两个字节 , 将第二个字节写入 0x60 端口 。因为要写 0x60 口表示缓存区 , 因此首先要判断缓存区是否为空 。
   例如 , 进入保护模式设置 时 , 首先判断输入缓存区是否为空 。如果是空的 , 说明控制器已经取走了数据 , 可以继续进行 。否则 , 如果不是空的 , 循环等待
   inb $0x64,%al # Wait for not busy 等待i8042空 缓冲区testb $0x2,%al jnz seta20.1
   再向 0x64 端口写入命令  , 表示准备写 Output 端口 , 然后写入 0x60 端口的字节将放入 Output 端口 。
   inb $0x64,%al # Wait for not busy 同上 testb $0x2,%al jnz seta20.2 movb $0xdf,%al # 0xdf -> port 0x60 向端口0x60写入0xdf , 打开A20 outb %al,$0x60
   首先判断输入缓存区是否为空 , 然后写入命令第二字节  , 这个字节将被送到 Output 端口 , 这个端口也是控制端口 , bit2 控制 开关 , 所以如果命令字节 表示关闭。
   关于键盘控制器说了这么多 , 只讲和 xv6 相关部分 , 其他部分同样感兴趣的链接见文末 。
   XV6    驱动程序是硬件物理接口的包装 , 键盘驱动程序也是如此 。其主要功能是将读取扫描码转换为计算机所需的信息 , 如字符、信号等 。xv6 在这方面实现的比较简单 , 只实现了字符转化 , 一些功能控制键 , 我们来看看 。
   端口号首先在 头文件中定义 , 控制键如 Ctrl , 特殊键如 UP , 以及最重要的映射表 , 看看普通情况下的映射表:
   static uchar normalmap[256] = { NO,0x1B,1 , 2 , 4 , 0x007 , 8 , 9-','=','b','t','q','w','e','r','t','y','u','i',// 0x10 'o','pn',NO,'a','s','d','f','g','h','j','k','l , // 0x20`',NO,'','z','x','c','v','b','n','m.NO,'*',// 0x30 NO,' ',NO,NO,NO,NO,NO,NO,NO,NO,NO,NO,NO,NO,NO,// 0x40-四 , 五 , 六 , .',NO,NO,NO,NO,// 0x50 [0x9C] 'n',// KP_Enter [0xB5] /' , / KP_Div [0xC8] KEY_UP,[0xD0] KEY_DN,[0xC9] KEY_PGUP,[0xD1] KEY_PGDN,[0xCB] KEY_LF,[0xCD] KEY_RT,[0x97] KEY_HOME,[0xCF] KEY_END,[0xD2] KEY_INS,[0xD3] KEY_DEL };
   键盘扫描码是键的代表 , 但不是我们想要的 , 我们想要的是键的意义 , 如数字键 1 代码是  , 显然不是我们想要的 , 我们想要数字 1 , 所以我们需要一个映射关系来转换 , 所有键映射关系集合在一起是上述映射表 。它是一个大数组 , 下标记是键的扫描码 , 内容是表达的意思 。
   以上是一般情况 , 当然也有不寻常的情况 , 比如按 Shift , CapsLock , Ctrl 等待控制键 。按下这些控制键后 , 按下其他键后表达的意义不同 , 因此需要另一个映射表 。这里没有列出 。太多了 。您可以直接参考代码 。例如 , 按下 Shift 键后按数字键 1 , 通码 应映射成 !而不是1 。
   有了这些理解 , 我们来看看 里面的源代码:
   int kbdgetc(void) { static uint shift; //shift用bit例如 , 记录控制键shift,ctrl static uchar *charcode[4] = { normalmap,shiftmap,ctlmap,ctlmap uint st,data,c; st = inb(KBSTATP); if((st & KBS_DIB) == 0) ///输出缓冲区未满 , 无法使用指令in读取 return -1; data = https://www.ykjsxy.net/shbaike/inb(KBDATAP); //从输出缓冲区阅读数据if(data == 0xE0){ //通码e0开头的键 shift |= E0ESC; //记录e0 return 0; } else if(data & 0x80){ //断码 , 表键弹起 // Key released data = (shift & E0ESC ? data : data & 0x7F); shift &= ~(shiftcode[data] | E0ESC); return 0; } else if(shift & E0ESC){ //紧接着0xE0后扫描码 // Last character was an E0 escape; or with 0x80 data |= 0x80; shift &= ~E0ESC; } shift |= shiftcode[data]; //记录控制键状态 , 如Shift,Ctrl,Alt shift ^= togglecode[data]; //记录控制键状态 , 如CapsLock , NumLock , ScrollLock c = charcode[shift & (CTL | SHIFT)][data]; //获取映射表的内容 , 即键的意义 if(shift & CAPSLOCK){ if('a' <= c && c <= 'z') c = 'A' - 'a'; else if('A' <= c && c <= 'Z') c = 'a' - 'A'; } return c; }
   该程序可视为一个极简主义的键盘驱动程序 , 也是键盘中断服务程序的主体键盘扫描码到所需信息的转换 。以下是仔细分析:
   前面说过很多映射表都有很多映射方法 , 怎么知道用哪一个?用哪一个要看有没有相应的控制键按下 , 所以要有东西记录控制键是否按下 。这个东西是变量。虽然变量名是  , 但并不意味着只记录 Shift 键状态 , 记录的信息如下:
   #define SHIFT (1<<0) #define CTL (1<<1) #define ALT (1<<2) #define CAPSLOCK (1<<3) #define NUMLOCK (1<<4) #define SCROLLLOCK (1<<5) #define E0ESC (1<<6) //通码断码E0开头
   从这种定义控制键的方式可以看出 , 使用 记录控制键的方式应该是使用位操作 。
   表示二维数组 , 可视为映射表的集合 , 根据 的记录信息选择映射表 , 以后使用时可以理解 。
   st = inb(KBSTATP); if((st & KBS_DIB) == 0) ///输出缓冲区为空 , 不能使用指令in读取 return -1; data = https://www.ykjsxy.net/shbaike/inb(KBDATAP); //从输出缓冲区阅读数据
   这些句子用来读取键盘扫描码 , 从键盘发送的扫描码放在输出缓冲区 。要读取扫描码 , 首先从状态寄存器读取当前状态到  , 然后用操作取出第 0 位 , 表示输出缓冲区的状态 。如果是 0  , 则表示输出缓冲区的寄存器为空 , 则无法读取并返回 -1 。如果为 1 表示输出缓冲区寄存器已满有内容 , 可以读取 , 所以接着从端口 0x60 输出
读取缓冲区的扫描码到。
   if(data =https://www.ykjsxy.net/shbaike/= 0xE0){ //通码e0开头的键 shift |= E0ESC; //记录e0 return 0; }
   若此扫描码为 0xE0 , 说明按下的键是特殊键 , 扫描码不止 8 字节 。这种情况可以直接返回 变量 , 等待下一个数据的到来进行具体处理
   else if(data & 0x80){ //断码 , 表键弹起 // Key released data = https://www.ykjsxy.net/shbaike/(shift & E0ESC ? data : data & 0x7F); shift &= ~(shiftcode[data] | E0ESC); return 0; }
   如果是 1  , 说明 7 位于 1 , 说明这个数据是断码 , 收到断码不需要额外做任何事情 。但是 , 如果断码是控制键的断码 , 则应清除控制键中记录的信息 。
   所以你必须知道哪个控制键是读出的  , 所以你有 映射:
   static uchar shiftcode[256] = { [0x1D] CTL,[0x2A] SHIFT,[0x36] SHIFT,[0x38] ALT,[0x9D] CTL,[0xB8] ALT,};
   【键盘驱动程序 键盘驱动程序是什么?】私下里 , 我认为这个定义是非常错误的 。我真的不明白一些控制键使用通码和一些断码 , 这导致 赋值句必须使用条件表达式 , 因为 映射 Shift 键不使用断码 , 因此必须转换为通码 。私下里 , 我认为这种映射非常混乱 , 导致 后面的一些句子的意义不太清楚 , 或者我们应该弥补映射关系 , 然后我们可以节省 赋值句子 , 使后面的句子写得更清楚 。当然 , 这不是关键 。只要理解这个过程的意义 。总之 , 如果 是一个断码 , 则无需做其他事情 。如果是控制键的断码 , 删除记录在 中的控制键信息 。
   else if(shift & E0ESC){ // Last character was an E0 escape; or with 0x80 data |= 0x80; shift &= ~E0ESC; }
   这种情况对应于 后面的键盘扫描码 。键盘扫描码有多个字节 , 都是成对存在的 , 也就是 的形式 。每次收到  , 都要清除 键中记录的 信息 。至于前面还是 xv6 设计的映射表与键盘上有许多相同意义的键有关 , xv6 用断码映射一些键的映射关系 , 如除号键 / 。
   shift |= shiftcode[data]; //记录控制键状态 , 如Shift,Ctrl,Alt shift ^= togglecode[data]; //记录控制键状态 , 如CapsLock , NumLock , ScrollLock
   这两句话记录了控制键的状态 , 分为两种情况和两种操作方法 。我们应该能够看到它们之间的区别 。实现组合键时 , Shift,Ctrl,Alt 需要按住才能生效 , 弹起后不再生效 。CapsLock 等控制键只需按下一次 , 即使弹起后也生效 。因此 , 模拟过程应该很容易理解一个使用或操作 , 一个使用不同或操作 。
   c = charcode[shift & (CTL | SHIFT)][data]; //获取映射表的内容 , 即键的意义 if(shift & CAPSLOCK){ //如果有 CapsLock 存在 if('a' <= c && c <= 'z //小写变大写 c = 'A' - 'a'; else if('A' <= c && c <= 'Z //大写变小写 c = 'a' - 'A'; }
   根据 中记录的控制键信息选择映射表 , 并根据 获取键盘扫描码的含义 。CapsLock 和 Shift 键的功能是相同的 , 所以如果 c 就是个普通 26 个英文字母字符的话 , 需要额外判断大小写 。
   关于 xv6 键盘驱动程序差不多 , 当然还有一些功能没有说 , 比如 Ctrl 组合键功能 , 键盘缓冲区等等 , 这涉及到另一份文件中的其他知识 , 让我们稍后详细介绍一下 。
   在这里 , 我们来谈谈一些常见的问题 。我们在第一个键盘:
   使用组合键时 , 需要先按下控制键 。键盘中断程序为这些控制键设置了标识() 。首先按下控制键 , 程序为控制键设置按下状态 , 然后检查这些标识是否有控制键 , 以便做出不同的操作 。
   组合键按钮有顺序 , 但弹起无顺序要求 。从上面的键处理程序可以看出 , 只有通码的键处理程序在做事 , 而断码的键处理程序直接返回到其他键 , 除了控制键的识别位置需要复位 。因此 , 在使用键盘控制输入时 , 重要的是按钮 , 而不是按钮弹起 , 所以只要按钮正确 , 如何弹起并不重要 。
   按键时 , 键盘中断会一直触发 , 如果是普通的字符键 , 电脑屏幕可能会一直打印一个字符 。如果是一些控制键 , 驱动程序可能会继续按下键 。当然 , 驱动程序是否记录最后一个按钮取决于具体的实现 , 大多数都没有记录 , xv6 也是如此 。触发键盘中断时 , 处理扫描码 。
   最后 , 综上所述 , 键盘驱动程序也用于包装键盘的物理接口 , 如读取状态、读取扫描码等 。键盘本身使用键盘扫描码 。每个键都有自己的键盘扫描码 。一个是按下通码表 , 另一个是弹出断码表 。键盘扫描码只是识别键的唯一方法 。键盘扫描码可以视为键的物理意义 , 但这不是我们想要的 。我们想要的是键所代表的逻辑意义 。因此 , 物理意义和逻辑需要转换 , 即映射表存在的意义 。
   键盘上有各种各样的键 , 也可以组合使用 , 它们代表的意义 , 功能也很杂 , xv6 只实现了其中的一部分 , 但它足以让我们理解它的本质 。无论你想通过按钮实现什么功能 , 或者只是简单地使用按钮所代表的逻辑意义 , 你都需要先获得按钮的扫描码 , 然后通过映射表转换为所需的信息 , 然后在其上写文章 。
   好了 , 这篇文章到此结束 。请批评和纠正任何错误 。也欢迎大家和我讨论交流 , 共同学习进步 。
   参考:
   https://www.win.tue.nl/~aeb/linux/kbd/scancodes-11.html
   https://wiki.osdev.org/"8042"_PS/2_Controller#Command_Register