前情提要
上一节我们实现了锁与信号量,这一节我们就可以实现键盘驱动了,访问键盘输入的数据也属于临界区资源,所以需要锁的存在。
一、键盘简介
之前的 ps/2 键盘使用的是中断驱动的,在当时,按下键盘就会触发中断,引导操作系统去处理这个按键行文。但是当今的usb键盘,使用的是轮询机制,cpu会定时访问键盘看有没有按下键盘。
我个人认为这是cpu技术的进步导致的,在之前,cpu的频率比较低,使用轮询可能会导致漏掉用户按键的行为。但是在今天,cpu的主频已经非常高了,处理一个按键行为就触发中断,这个开销太大了,而且轮询的频率也上来了,现在每秒访问几千次对电脑一点影响都没有,所以现在大多采用了轮询机制。
不过据说中断驱动的还是比较快,现在一些电竞主板还是支持ps/2的接口,这个未经论证。
1.1、键盘的通码与断码
键盘的状态要么是按下,要么是弹起,因此一个键便有两个编码,按键按下时的编码叫做通码,键盘上的触电接通了电路,使硬件产生了一个编码,故此通码叫makecode。按键在被按住不松手时会持续产生相同的码,直到按键被松开时才终止,因此按键被松开弹起时产生的编码叫断码,也就是电路被断开了,不再持续产生码了,故断码也称为breakcode。
无论是按下键,或是松开键,当键的状态改变后,键盘中的8048芯片把按键对应的扫描码(通码或断码)发送到主板上的8042芯片,由8042处理后保存在自己的寄存器中,然后向8259A发送中断信号,这样处理器便去执行键盘中断处理程序,将8042处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理。
1.2、键盘扫描码
键的扫描码是由键盘中的键盘编码器决定的,不同的编码方案便是不同的键盘扫描码,也就是说,相同的键在不同的编码方案下产生的通码和断码也是不同的。
根据不同的编码方案,键盘扫描码有三套,分别称为scan code set 1、scan code set 2、scan code set 3。
其中scan code set 1是XT键盘用的扫描码,这个历史就比较久远了。scan code set 2是AT键盘的扫描码,这个键盘和我们当今的键盘也不是很一样,但是已经比较接近了。scan code set 3是IBM PS/2系列高端计算机所用的键盘上,IBM蓝色巨人现在都凉了,这个键盘也就很少看到了。
第二套键盘扫描码几乎是目前所使用的键盘的标准,因此大多数键盘向8042发送的扫描码都是第二套扫描码。但是难免有别的键盘,所以才会出现8042这个芯片,这个芯片做一个中间层,为了兼容第一套键盘扫描码对应的中断处理程序,不管键盘用的是何种键盘扫描码,当键盘将扫描码发送到8042后,都由8042转换成第一套扫描码,我们再从8042中读取扫描码。
这里我们给出常用键位的扫描码(这里的扫描码就是通码,加0x80
就是断码)
按键 | 扫描码 | 按键 | 扫描码 | 按键 | 扫描码 |
---|---|---|---|---|---|
Esc | 0x01 | F1 | 0x3B | F2 | 0x3C |
F3 | 0x3D | F4 | 0x3E | F5 | 0x3F |
F6 | 0x40 | F7 | 0x41 | F8 | 0x42 |
F9 | 0x43 | F10 | 0x44 | F11 | 0x57 |
F12 | 0x58 | PrintSc | 0x37 | ScrollLk | 0x46 |
Pause/Brk | 0x45 | ` | 0x29 | 1 | 0x02 |
2 | 0x03 | 3 | 0x04 | 4 | 0x05 |
5 | 0x06 | 6 | 0x07 | 7 | 0x08 |
8 | 0x09 | 9 | 0x0A | 0 | 0x0B |
- | 0x0C | = | 0x0D | Backspace | 0x0E |
Tab | 0x0F | Q | 0x10 | W | 0x11 |
E | 0x12 | R | 0x13 | T | 0x14 |
Y | 0x15 | U | 0x16 | I | 0x17 |
O | 0x18 | P | 0x19 | [ | 0x1A |
] | 0x1B | | | 0x2B | CapsLock | 0x3A |
A | 0x1E | S | 0x1F | D | 0x20 |
F | 0x21 | G | 0x22 | H | 0x23 |
J | 0x24 | K | 0x25 | L | 0x26 |
; | 0x27 | ' | 0x28 | Enter | 0x1C |
Shift左 | 0x2A | Z | 0x2C | X | 0x2D |
C | 0x2E | V | 0x2F | B | 0x30 |
N | 0x31 | M | 0x32 | , | 0x33 |
. | 0x34 | / | 0x35 | Shift右 | 0x36 |
Ctrl左 | 0x1D | Win左 | 0xE0 | Alt左 | 0x38 |
Space | 0x39 | Alt右 | 0xE038 | Win右 | 0xE0 |
Menu | 0xE0 | Ctrl右 | 0xE01D |
问:为什么会有通码和断码,通码不就够了嘛
**答:**如果按一个组合键的话,比如ctrl+a,是先按下ctrl,再按a,再松开ctrl,再松开a。如果没有断码,我们无法判断ctrl是否松开。
1.3、键盘的芯片
和键盘相关的芯片只有8042和8048,它们都是独立的处理器,都有自己的寄存器和内存。Intel 8048芯片或兼容芯片位于键盘中,它是键盘编码器,Intel 8042芯片或兼容芯片被集成在主板上的南桥芯片中,它是键盘控制器,也就是键盘的IO接口,因此它是8048的代理,也是前面所得到的处理器和键盘的"中间层"。我们只需要学习8042就够了
他的端口如下
寄存器 | 端口 | 读写 |
---|---|---|
Output Buffer(输出缓冲区) | 0x60 | 读 |
Input Buffer(输入缓冲区) | 0x60 | 写 |
Status Register(状态寄存器) | 0x64 | 读 |
Control Register(控制寄存器) | 0x64 | 写 |
状态寄存器8位宽度的寄存器,只读,反映8048和8042的内部工作状态。各位意义如下。
(1)位0:置1时表示输出缓冲区寄存器已满,处理器通过in指令读取后该位自动置0。
(2)位1:置1时表示输入缓冲区寄存器已满,8042将值读取后该位自动置0。
(3)位2:系统标志位,最初加电时为0,自检通过后置为1。
(4)位3:置1时,表示输入缓冲区中的内容是命令,置0时,输入缓冲区中的内容是普通数据。
(5)位4:置1时表示键盘启用,置0时表示键盘禁用。
(6)位5:置1时表示发送超时。
(7)位6:置1时表示接收超时。
(8)位7:来自8048的数据在奇偶校验时出错。
8位宽度的寄存器,只写,用于写入命令控制字。每个位都可以设置一种工作方式,意义如下。
(1)位0:置1时启用键盘中断。
(2)位1:置1时启用鼠标中断。
(3)位2:设置状态寄存器的位2。
(4)位3:置1时,状态寄存器的位4无效。
(5)位4:置1时禁止键盘。
(6)位5:置1时禁止鼠标。
(7)位6:将第二套键盘扫描码转换为第一套键盘扫描码。
(8)位7:保留位,默认为0。
二、环形队列
键盘中断的数据是放在队列中的,等待其他线程的读取。如果我们之前做过关于软件相关的工作,很容易理解这个概念,就是buffer,缓冲区。因为我们是一直在输入的,所以这里设计成了环形队列。
我们看一下环形队列的数据结构
c
#define bufsize 256
/* 环形队列 */
struct ioqueue {
// 生产者消费者问题
struct lock lock;
// 生产者,缓冲区不满时就继续往里面放数据
struct task_struct* producer;
// 消费者,缓冲区不空时就继续从往里面拿数据
struct task_struct* consumer;
char buf[bufsize]; // 缓冲区大小
int32_t head; // 队首,数据往队首处写入
int32_t tail; // 队尾,数据从队尾处读出
};
这个就很明朗了。一个生产者一个消费者,生产者向buf中添加数据,消费者从buf中取出数据,为了防止buf中的数据出错,生产者和消费者同时只能有一个可以访问到buf。如果buf中数据满了,生产者就不能放了,此时阻塞生产者,如果buf中数据为空,消费者就不能拿了,此时阻塞消费者。
我们看一下具体的实现
c
/* 初始化io队列ioq */
void ioqueue_init(struct ioqueue* ioq) {
lock_init(&ioq->lock); // 初始化io队列的锁
ioq->producer = ioq->consumer = NULL; // 生产者和消费者置空
ioq->head = ioq->tail = 0; // 队列的首尾指针指向缓冲区数组第0个位置
}
/* 返回pos在缓冲区中的下一个位置值 */
static inline int32_t next_pos(int32_t pos) {
return (pos + 1) % bufsize;
}
/* 判断队列是否已满 */
bool ioq_full(struct ioqueue* ioq) {
return next_pos(ioq->head) == ioq->tail;
}
/* 判断队列是否已空 */
bool ioq_empty(struct ioqueue* ioq) {
return ioq->head == ioq->tail;
}
/* 使当前生产者或消费者在此缓冲区上等待 */
static void ioq_wait(struct task_struct** waiter) {
// 二级指针不为空,指向的pcb指针地址为空
ASSERT(*waiter == NULL && waiter != NULL);
*waiter = running_thread();
thread_block(TASK_BLOCKED);
}
/* 唤醒waiter */
static void wakeup(struct task_struct** waiter) {
// 二级指针指向不为空
ASSERT(*waiter != NULL);
thread_unblock(*waiter);
*waiter = NULL;
}
/* 消费者从ioq队列中获取一个字符 */
char ioq_getchar(struct ioqueue* ioq) {
// 若缓冲区(队列)为空,把消费者ioq->consumer记为当前线程自己,等待生产者唤醒
while (ioq_empty(ioq)) {
lock_acquire(&ioq->lock);
ioq_wait(&ioq->consumer);
lock_release(&ioq->lock);
}
char byte = ioq->buf[ioq->tail]; // 从缓冲区中取出
ioq->tail = next_pos(ioq->tail); // 把读游标移到下一位置
if (ioq->producer != NULL) {
wakeup(&ioq->producer); // 唤醒生产者
}
return byte;
}
/* 生产者往ioq队列中写入一个字符byte */
void ioq_putchar(struct ioqueue* ioq, char byte) {
// 若缓冲区(队列)已经满了,把生产者ioq->producer记为自己,等待消费者线程唤醒自己
while (ioq_full(ioq)) {
lock_acquire(&ioq->lock);
ioq_wait(&ioq->producer);
lock_release(&ioq->lock);
}
ioq->buf[ioq->head] = byte; // 把字节放入缓冲区中
ioq->head = next_pos(ioq->head); // 把写游标移到下一位置
if (ioq->consumer != NULL) {
wakeup(&ioq->consumer); // 唤醒消费者
}
}
我们看一下后面两个函数,wait
和wakeup
,这两个函数,这两个函数传入的是一个pcb指针的地址,所以这里是一个二级指针。所以无论是阻塞还是解除阻塞都是取这个二级指针的地址,也就得到了pcb指针。这里对于不熟悉指针的人来说可能会有点扰。
三、键盘驱动
c
#define KBD_BUF_PORT 0x60 // 键盘buffer寄存器端口号为0x60
/* 用转义字符定义部分控制字符 */
#define esc '\033' // 八进制表示字符,也可以用十六进制'\x1b'
#define backspace '\b'
#define tab '\t'
#define enter '\r'
#define delete '\177' // 八进制表示字符,十六进制为'\x7f'
/* 以上不可见字符一律定义为0 */
#define char_invisible 0
#define ctrl_l_char char_invisible
#define ctrl_r_char char_invisible
#define shift_l_char char_invisible
#define shift_r_char char_invisible
#define alt_l_char char_invisible
#define alt_r_char char_invisible
#define caps_lock_char char_invisible
/* 定义控制字符的通码和断码 */
#define shift_l_make 0x2a
#define shift_r_make 0x36
#define alt_l_make 0x38
#define alt_r_make 0xe038
#define alt_r_break 0xe0b8
#define ctrl_l_make 0x1d
#define ctrl_r_make 0xe01d
#define ctrl_r_break 0xe09d
#define caps_lock_make 0x3a
struct ioqueue kbd_buf; // 定义键盘缓冲区
/* 定义以下变量记录相应键是否按下的状态,
* ext_scancode用于记录makecode是否以0xe0开头 */
static bool ctrl_status, shift_status, alt_status, caps_lock_status, ext_scancode;
/* 以通码make_code为索引的二维数组 */
static char keymap[][2] = {
/* 扫描码 未与shift组合 与shift组合*/
/* ---------------------------------- */
/* 0x00 */ {0, 0},
/* 0x01 */ {esc, esc},
/* 0x02 */ {'1', '!'},
/* 0x03 */ {'2', '@'},
/* 0x04 */ {'3', '#'},
/* 0x05 */ {'4', '$'},
/* 0x06 */ {'5', '%'},
/* 0x07 */ {'6', '^'},
/* 0x08 */ {'7', '&'},
/* 0x09 */ {'8', '*'},
/* 0x0A */ {'9', '('},
/* 0x0B */ {'0', ')'},
/* 0x0C */ {'-', '_'},
/* 0x0D */ {'=', '+'},
/* 0x0E */ {backspace, backspace},
/* 0x0F */ {tab, tab},
/* 0x10 */ {'q', 'Q'},
/* 0x11 */ {'w', 'W'},
/* 0x12 */ {'e', 'E'},
/* 0x13 */ {'r', 'R'},
/* 0x14 */ {'t', 'T'},
/* 0x15 */ {'y', 'Y'},
/* 0x16 */ {'u', 'U'},
/* 0x17 */ {'i', 'I'},
/* 0x18 */ {'o', 'O'},
/* 0x19 */ {'p', 'P'},
/* 0x1A */ {'[', '{'},
/* 0x1B */ {']', '}'},
/* 0x1C */ {enter, enter},
/* 0x1D */ {ctrl_l_char, ctrl_l_char},
/* 0x1E */ {'a', 'A'},
/* 0x1F */ {'s', 'S'},
/* 0x20 */ {'d', 'D'},
/* 0x21 */ {'f', 'F'},
/* 0x22 */ {'g', 'G'},
/* 0x23 */ {'h', 'H'},
/* 0x24 */ {'j', 'J'},
/* 0x25 */ {'k', 'K'},
/* 0x26 */ {'l', 'L'},
/* 0x27 */ {';', ':'},
/* 0x28 */ {'\'', '"'},
/* 0x29 */ {'`', '~'},
/* 0x2A */ {shift_l_char, shift_l_char},
/* 0x2B */ {'\\', '|'},
/* 0x2C */ {'z', 'Z'},
/* 0x2D */ {'x', 'X'},
/* 0x2E */ {'c', 'C'},
/* 0x2F */ {'v', 'V'},
/* 0x30 */ {'b', 'B'},
/* 0x31 */ {'n', 'N'},
/* 0x32 */ {'m', 'M'},
/* 0x33 */ {',', '<'},
/* 0x34 */ {'.', '>'},
/* 0x35 */ {'/', '?'},
/* 0x36 */ {shift_r_char, shift_r_char},
/* 0x37 */ {'*', '*'},
/* 0x38 */ {alt_l_char, alt_l_char},
/* 0x39 */ {' ', ' '},
/* 0x3A */ {caps_lock_char, caps_lock_char}
/*其它按键暂不处理*/
};
/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {
/* 这次中断发生前的上一次中断,以下任意三个键是否有按下 */
bool ctrl_down_last = ctrl_status;
bool shift_down_last = shift_status;
bool caps_lock_last = caps_lock_status;
uint16_t scancode = inb(KBD_BUF_PORT);
// 若扫描码是e0开头的, 结束此次中断处理函数,等待下一个扫描码进来
if (scancode == 0xe0) {
ext_scancode = true; // 打开e0标记
return;
}
// 如果上次是以0xe0开头,将扫描码合并
if (ext_scancode) {
scancode = ((0xe000) | scancode);
ext_scancode = false; // 关闭e0标记
}
// 若是断码(按键弹起时产生的扫描码)
if ((scancode & 0x0080) != 0) {
// 获得相应的通码
uint16_t make_code = (scancode &= 0xff7f);
// 若是任意以下三个键弹起了,将状态置为false
if (make_code == ctrl_l_make || make_code == ctrl_r_make) {
ctrl_status = false;
}
else if (make_code == shift_l_make || make_code == shift_r_make) {
shift_status = false;
}
else if (make_code == alt_l_make || make_code == alt_r_make) {
alt_status = false;
}
// 若是其他非控制键位,不需要处理,那些键位我们只需要知道通码
return;
}
// 若是通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_code
else if ((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make)) {
// keymap的二维索引
bool shift = false;
// 按下的键不是字母
if ((scancode < 0x0e) || (scancode == 0x29) || \
(scancode == 0x1a) || (scancode == 0x1b) || \
(scancode == 0x2b) || (scancode == 0x27) || \
(scancode == 0x28) || (scancode == 0x33) || \
(scancode == 0x34) || (scancode == 0x35)) {
if (shift_down_last) {
shift = true;
}
}
// 如果按下的键是字母,需要和CapsLock配合
else {
if (shift_down_last && caps_lock_last) { // 如果shift和capslock同时按下
shift = false;
}
else if (shift_down_last || caps_lock_last) { // 如果shift和capslock任意被按下
shift = true;
}
else {
shift = false;
}
}
// 将扫描码的高字节置0,主要是针对高字节是e0的扫描码.
uint8_t index = (scancode &= 0x00ff);
// 在数组中找到对应的字符
char cur_char = keymap[index][shift];
// 如果cur_char不为0,也就是ascii码为除'\0'外的字符就加入键盘缓冲区中
if (cur_char) {
// 如果ctrl按下,且输入的字符为'l'或者'u',那就保存为 cur_char-'a',主要是'a'前面26位没啥用
if ((ctrl_down_last && cur_char == 'l') || (ctrl_down_last && cur_char == 'u')) {
cur_char -= 'a';
}
// 如果缓冲区未满,就将其加入缓冲区
if (!ioq_full(&kbd_buf)) {
ioq_putchar(&kbd_buf, cur_char);
}
return;
}
// 记录本次是否按下了下面几类控制键之一,供下次键入时判断组合键
if (scancode == ctrl_l_make || scancode == ctrl_r_make) {
ctrl_status = true;
}
else if (scancode == shift_l_make || scancode == shift_r_make) {
shift_status = true;
}
else if (scancode == alt_l_make || scancode == alt_r_make) {
alt_status = true;
}
// 这里注意,大写的锁定键是取反
else if (scancode == caps_lock_make) {
caps_lock_status = !caps_lock_status;
}
}
else {
put_str("unknown key\n");
}
}
/* 键盘初始化 */
void keyboard_init() {
put_str("keyboard init start\n");
ioqueue_init(&kbd_buf);
register_handler(0x21, intr_keyboard_handler);
put_str("keyboard init done\n");
}
键盘驱动就稍显复杂一点,主要是涉及到了shift
,ctrl
,alt
,caplock
这些个控制键,这些键位是否按下所表示的通码断码是不一样的。这里就是处理字符,相信大家看代码就可以看明白。
四、仿真
我们创建一个线程,键盘输入什么,打印什么
结束语
本节我们编写了键盘驱动以及其使用的环形队列数据结构。下一节我们将实现一个用户进程,即特权级为3的进程。
老规矩,代码地址为 https://github.com/lyajpunov/os