外部设备键盘

键盘的本质

  • 键盘是一种计算机外部设备
  • 键盘与计算机的通信(数据交互) 需要借助中断完成

键盘中断服务程序

  • 使能主 8259A 引脚 IRQ1,让此引脚的值为0(这样才能接收键盘中断)
  • 编写中断服务程序, 并注册到中断向量表 (由于先前让主8259A设置成20号中断开始,因此键盘中断是0x21号中断)

image-20220726221509452

键盘工作原理

image-20220816211026066

注意一定要将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()
{ //读取0x60端口的数据并且将数据返回
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. 普通按键:1字节(按键扫描码)
  2. E0扩展按键:2字节(0xE0+按键扫描码)比如enter键有两个其中一个有E0前缀
  3. E1扩展按键:6字节(Break Pause)
1
2
3
4
5
6
7
typedef struct
{
byte ascii1; // no shift code 'a'
byte ascii2; // shift code 'A'
byte scode; // scan code 扫描码
byte kcode; // key code 虚拟键码
} KeyCode;

常规键盘使用中存在shift, capslock,NumLock这几个键,因此处理逻辑首先判断这几个特殊键是否同时被按下。利用三个变量标记cShift, cCapslock,cNumLock记录是否按下。另外引入E0变量,记录是否是扩展按键。

image-20220816220159778

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) )
{
/* Pause Key */
}
else if( KeyHandler(sc) )
{
/* Normal Key */
}
else
{
/* Unknown Key */
}
}

//pause按键
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);
// typedef struct
// {
// byte ascii1; // no shift code 'a'
// byte ascii2; // shift code 'A'
// byte scode; // scan code 扫描码
// byte kcode; // key code 虚拟键码
// } KeyCode;
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;
}
//MakeCode很关键 KeyCode转化为 动作|扫描码|虚拟键码|ASCII码
code = pressed | MakeCode(pkc, cShift, cCapsLock, cNumLock, E0);

StoreKeyCode(code);
//一定要赋值为0
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)
{ //数字键盘的扫描码除了'/'和'enter'是2字节剩下都是1字节
//数字键盘扫描码
static const byte cNumScanCode[] = {0x52, 0x53, 0x4F, 0x50, 0x51, 0x4B, 0x4C, 0x4D, 0x47, 0x48, 0x49, 0x35, 0x37, 0x4A, 0x4E, 0x1C};
//扫描码是否有前缀0xE0
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);
//是否带0xE0前缀且是数字小键盘的扫描码,满足上述两个数组的条件就是数字小键盘
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);//0x5E作为扫描码放入环形缓冲区
PutScanCode(0xDE);
}
}

return ret;
}

内核存储键盘编码

缓存最近8次按键编码,通过系统调用将编码任务扔给系统调用。设计一个8大小的环形缓冲区,最多仅仅能保留8个编码

image-20220816224612056

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;
//+1取余 环形的关键
gKCBuff.tail = (gKCBuff.tail + 1) % gKCBuff.max;
gKCBuff.count++;
}
else if( gKCBuff.count > 0 )//缓冲区已经满了
{
FetchKeyCode();//舍弃头部
StoreKeyCode(kc);//递归调用存入尾部
}
}

ReadKey

当shell请求用户输入时,用户是否正在输入字符?
大概率是用户不处于输入状态,shell任务应该处于阻塞状态

当多个任务请求用户输入时,哪个任务应该获得输入的字符?
每一个任务都应该获得输入的字符

解决方法:
定义一个全局的等待队列gKeyWait存储所有想要获取用户输入的任务
等待用户输入完毕时,唤醒所有gKeyWait的任务

image-20220905211444951

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 ) //&ret
{
uint kc = FetchKeyCode(); //拿到编码过后的键位信息

if( kc ) //真的拿到信息了
{
uint* ret = (uint*)param1;

*ret = kc; //获取的键位信息存储到ret

NotifyAll(kc); //拿到了键位信息则通知所有gKeyWait中的任务
}
else
{
Event* evt = CreateEvent(KeyEvent, (uint)&gKeyWait, param1, 0);
//当前任务 被调度进入 gKeyWait全局等待队列
EventSchedule(WAIT, evt);//KeySchedule
}
}
}
1
2
3
4
5
6
7
8
9
10
void KeyboardHandler()
{ //读取0x60端口的数据并且将数据返回
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

命令映射设计

将命令和命令入口进行映射(命令注册)

  • 用户输入命令后,查找命令入口并执行
  • 无法找到命令入口时,执行无效命令入口
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);
}
}

内核级命令

命令示例:获取系统物理内存容量

  1. 系统启动时,在实模式下获取物理内存容量(int 0x15)
  2. 实模式下再将物理内存容量存储到共享内存区
  3. 内核执行时,从共享内存区获取物理内存容量,可以存入全局变量中
  4. 实现一个0x80号中断 系统调用uint GetMemSize()
  5. shell任务通过GetMemSize实现获取命令mem并且打印容量大小