外部设备键盘
键盘的本质
- 键盘是一种计算机外部设备
- 键盘与计算机的通信(数据交互) 需要借助中断完成
键盘中断服务程序
- 使能主 8259A 引脚 IRQ1,让此引脚的值为0(这样才能接收键盘中断)
- 编写中断服务程序, 并注册到中断向量表 (由于先前让主8259A设置成20号中断开始,因此键盘中断是0x21号中断)
键盘工作原理
注意一定要将8042缓冲区的键位信息的数据给读取完才可以存储下一个摁键信息。
因此键盘中断服务程序一定要同段端口0x60去获取8042缓冲区的内容。而缓冲区存储的是键盘扫描码。
扫描码指的是硬件电路对键位的编码makecode(摁下键时的扫描码)+0x80=breakcode(释放键时的扫描码)
注意C语言通过ax存储函数返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ; ; byte ReadPort(ushort port) ; ReadPort: push ebp mov ebp, esp xor eax, eax mov dx, [ebp + 8] ; 参数port赋值给dx寄存器 in al, dx ; C语言函数返回值存储在ax中, 端口中读取的数据保存在ax寄存器当中 nop nop nop leave ret
|
1 2 3 4 5 6 7 8 9 10
| void KeyboardHandler() { byte sc = ReadPort(0x60); PutScanCode(sc); NotifyKeyCode(); SendEOI(MASTER_EOI_PORT); }
|
每个键盘按键对应的扫描码与键盘厂商相关,不同厂商的扫描码可能不同,
而虚拟键码是软件层面(操作系统层面)识别的(键盘按键统一的标准编码),每一个按键对应一个虚拟键盘key code。
因此我们只需要建立不同的扫描码到虚拟键码的映射关系就可以适应不同厂商生产的键盘了。
而ASCII码是常用字符的统一编码。与按键无关。例如同一个键码键盘上的’A’既可以对应大写A也可以对应a
键盘驱动映射设计
建立扫描码,虚拟键码。ASCII码之间的映射。
解析扫描码,处理常用组合键
用4字节表示键盘操作结果:动作|扫描码|虚拟键码|ASCII码
1 2
| 按下a: 0x011E4161 释放a: 0x001E4161 按下A: 0x011E4141 释放A: 0x001E4141
|
扫描码可能不止1个字节,扫描码有多少个字节那么按下就会执行多少次键盘中断处理函数
- 普通按键:1字节(按键扫描码)
- E0扩展按键:2字节(0xE0+按键扫描码)比如enter键有两个其中一个有E0前缀
- E1扩展按键:6字节(Break Pause)
1 2 3 4 5 6 7
| typedef struct { byte ascii1; byte ascii2; byte scode; byte kcode; } KeyCode;
|
常规键盘使用中存在shift, capslock,NumLock这几个键,因此处理逻辑首先判断这几个特殊键是否同时被按下。利用三个变量标记cShift, cCapslock,cNumLock记录是否按下。另外引入E0变量,记录是否是扩展按键。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| void PutScanCode(byte sc) { if( PauseHandler(sc) ) { } else if( KeyHandler(sc) ) { } else { } }
static uint PauseHandler(byte sc) { static int cPause = 0; uint ret = ( (sc == 0xE1) || cPause ); if( ret ) { static byte cPauseCode[] = {0xE1, 0x1D, 0x45, 0xE1, 0x9D, 0xC5}; byte* pcc = AddrOff(cPauseCode, cPause); if( sc == *pcc ) { cPause++; } else { cPause = 0; ret = 0; } if( cPause == Dim(cPauseCode) ) { cPause = 0; PutScanCode(0x5E); PutScanCode(0xDE); } } return ret; }
static uint KeyHandler(byte sc) { static int cShift = 0; static int cCapsLock = 0; static int cNumLock = 0; static int E0 = 0; uint ret = 0; if( sc == 0xE0 ) { E0 = 1; ret = 1; } else { uint pressed = KeyType(sc); KeyCode* pkc = NULL; if( !pressed ) { sc = sc - 0x80; } pkc = AddrOff(gKeyMap, sc); if( ret = !!pkc->scode ) { uint code = 0; if( IsShift(sc) ) { cShift = pressed; } else if( IsCapsLock(sc) && pressed ) { cCapsLock = !cCapsLock; } else if( IsNumLock(sc) && pressed ) { cNumLock = !cNumLock; } code = pressed | MakeCode(pkc, cShift, cCapsLock, cNumLock, E0); StoreKeyCode(code); E0 = 0; } } return ret; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| static uint IsNumPadKey(byte sc, int E0) { static const byte cNumScanCode[] = {0x52, 0x53, 0x4F, 0x50, 0x51, 0x4B, 0x4C, 0x4D, 0x47, 0x48, 0x49, 0x35, 0x37, 0x4A, 0x4E, 0x1C}; static const byte cNumE0[Dim(cNumScanCode)] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1 };
uint ret = 0; int i = 0; for(i=0; i<Dim(cNumScanCode); i++) { byte* pc = AddrOff(cNumScanCode, i); byte* pe = AddrOff(cNumE0, i); if( (sc == *pc) && (E0 == *pe) ) { ret = 1; break; } } return ret; }
|
Pause键的处理
Pause按下到释放完整扫描码为0xE1,0x1D,0x45,0xE1,0x9D,0xC5
因此0xE1,0x1D,0x45-->0x5E
因为0x5E映射之前尚未使用
0xE1,0x9D,0xC5-->0xDE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| static uint PauseHandler(byte sc) { static int cPause = 0; uint ret = ( (sc == 0xE1) || cPause ); if( ret ) { static byte cPauseCode[] = {0xE1, 0x1D, 0x45, 0xE1, 0x9D, 0xC5}; byte* pcc = AddrOff(cPauseCode, cPause);
if( sc == *pcc ) { cPause++; } else { cPause = 0; ret = 0; } if( cPause == Dim(cPauseCode) ) { cPause = 0; PutScanCode(0x5E); PutScanCode(0xDE); } } return ret; }
|
内核存储键盘编码
缓存最近8次按键编码,通过系统调用将编码任务扔给系统调用。设计一个8大小的环形缓冲区,最多仅仅能保留8个编码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| static uint FetchKeyCode() { uint ret = 0; if( gKCBuff.count > 0 ) { uint* p = AddrOff(gKCBuff.buff, gKCBuff.head);
ret = *p; gKCBuff.head = (gKCBuff.head + 1) % gKCBuff.max; gKCBuff.count--; } return ret; } static void StoreKeyCode(uint kc) { uint* p = NULL; if( gKCBuff.count < gKCBuff.max ) { p = AddrOff(gKCBuff.buff, gKCBuff.tail); *p = kc; gKCBuff.tail = (gKCBuff.tail + 1) % gKCBuff.max; gKCBuff.count++; } else if( gKCBuff.count > 0 ) { FetchKeyCode(); StoreKeyCode(kc); } }
|
ReadKey
当shell请求用户输入时,用户是否正在输入字符?
大概率是用户不处于输入状态,shell任务应该处于阻塞状态
当多个任务请求用户输入时,哪个任务应该获得输入的字符?
每一个任务都应该获得输入的字符
解决方法:
定义一个全局的等待队列gKeyWait存储所有想要获取用户输入的任务
等待用户输入完毕时,唤醒所有gKeyWait的任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| void KeyCallHandler(uint cmd, uint param1, uint param2) { if( param1 ) { uint kc = FetchKeyCode(); if( kc ) { uint* ret = (uint*)param1; *ret = kc; NotifyAll(kc); } else { Event* evt = CreateEvent(KeyEvent, (uint)&gKeyWait, param1, 0); EventSchedule(WAIT, evt); } } }
|
1 2 3 4 5 6 7 8 9 10
| void KeyboardHandler() { byte sc = ReadPort(0x60); PutScanCode(sc); NotifyKeyCode(); SendEOI(MASTER_EOI_PORT); }
|
shell任务
总体式样:
支持删除键和回车键
- backspace:修改已输入命令中的字符
- enter:确认命令输入完毕
命令类型
缓存用户输入
- 定义一个全局字符缓冲区gKBuf用于存储用户输入的字符
- 定义全局变量gKIndex用于记录新字符子啊缓冲区中的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| static void Handle(char ch, byte vk) { if( ch ) { PrintChar(ch); gKBuf[gKIndex++] = ch; } else { switch(vk) { case KEY_ENTER: EnterHandler(); break; case KEY_BACKSPACE: BSHandler(); break; default: break; } } }
|
命令映射设计
将命令和命令入口进行映射(命令注册)
- 用户输入命令后,查找命令入口并执行
- 无法找到命令入口时,执行无效命令入口
1 2 3 4 5 6
| typedef struct { ListNode header; const char* cmd; void (*run)(); } CmdRun;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| static void AddCmdEntry(const char* cmd, void(*run)()) { CmdRun* cr = (CmdRun*)Malloc(sizeof(CmdRun)); if( cr && cmd && run ) { cr->cmd = cmd; cr->run = run; List_Add(&gCmdList, (ListNode*)cr); } else { Free(cr); } }
|
内核级命令
命令示例:获取系统物理内存容量
- 系统启动时,在实模式下获取物理内存容量(int 0x15)
- 实模式下再将物理内存容量存储到共享内存区
- 内核执行时,从共享内存区获取物理内存容量,可以存入全局变量中
- 实现一个0x80号中断 系统调用uint GetMemSize()
- shell任务通过GetMemSize实现获取命令mem并且打印容量大小