第十三篇:实现io缓冲区和键盘驱动
环形缓冲区
计划将键盘的输入存入一个io缓冲区中,以备消费者获取。
因为缓冲区大小固定,所以采用环形队列的数据结构。
环形队列结构如下:
#define bufsize 64
//环形队列
struct ioqueue {
//生产者消费者问题
struct lock lock;
//此项用于记录哪个生产者在此缓冲区上睡眠
struct task_struct* producer;
//此项用于记录哪个消费者在此缓冲区上睡眠
struct task_struct* consumer;
//缓冲区
uint8_t buf[bufsize];
//队首
int32_t head;
//队尾
int32_t tail;
};
相关函数如下:
//初始化io队列
void ioqueue_init(struct ioqueue* ioq) {
lock_init(&ioq->lock);
ioq->producer = NULL;
ioq->consumer = NULL;
ioq->head = 0;
ioq->tail = 0;
}
//返回pos在缓冲区的下一个位置
int32_t next_pos(uint32_t pos) {
return (pos + 1) % bufsize;
}
//判断队列是否已满
bool ioq_full(struct ioqueue* ioq) {
ASSERT(get_intr_status() == INTR_OFF);
return next_pos(ioq->head) == ioq->tail;
}
//判断队列是否已空
bool ioq_empty(struct ioqueue* ioq) {
ASSERT(get_intr_status() == INTR_OFF);
return ioq->head == ioq->tail;
}
//使当前生产者或消费者在缓冲区上等待
void ioq_wait(struct task_struct** waiter) {
ASSERT(*waiter == NULL && waiter != NULL);
*waiter = running_thread();
thread_block(TASK_BLOCKED);
}
//唤醒waiter
void wakeup(struct task_struct** waiter) {
ASSERT(*waiter != NULL);
thread_unblock(*waiter);
*waiter = NULL;
}
//消费者从ioq队列中获取一个字符
uint8_t ioq_getchar(struct ioqueue* ioq) {
//确保在关中断情况下运行
ASSERT(get_intr_status() == INTR_OFF);
//如果队列为空,消费者就阻塞在缓冲区上
//目的是让生产者知道要通知哪个消费者
while (ioq_empty(ioq)) {
lock_acquire(&ioq->lock);
ioq_wait(&ioq->consumer);
lock_release(&ioq->lock);
}
//获取一字节的数据
uint8_t byte = ioq->buf[ioq->tail];
ioq->tail = next_pos(ioq->tail);
//唤醒生产者
if (ioq->producer != NULL) {
wakeup(&ioq->producer);
}
return byte;
}
//生产者往ioq队列中写入一个字符
void ioq_putchar(struct ioqueue* ioq, uint8_t byte) {
//确保在关中断情况下运行
ASSERT(get_intr_status() == INTR_OFF);
//如果队列满,生产者就阻塞在缓冲区上
//目的是让消费者知道要唤醒哪个生产者
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);
}
}
键盘驱动
键盘驱动用于将8042缓冲寄存器中的扫描码转换为ASCII码,以备上层模块使用。
- 键盘扫描码共有三套,当有键盘敲击时,8048会将扫描码发给8042
- 为了兼容性,8042同意将扫描码转为第一套扫描码,并将其存储在自身的缓冲寄存器中
- 随后8042向8259a发送中断信号,随后键盘中断处理程序会读取扫描码并进行处理
三个中断代理的关系如下:
扫描码相关定义如下:
#define KBD_BUF_PORT 0x60 //键盘buffer寄存器端口号为0x60
//用转移字符定义部分控制字符(8进制)
#define esc '\033'
#define backspace '\b'
#define tab '\t'
#define enter '\r'
#define delete '\0177'
//以下控制字符不可见,一律定义为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开头
bool ctrl_status, shift_status, alt_status, caps_lock_status, ext_scancode;
//以通码make_code为索引的二维数组
static char keymap[][2] = {
{0, 0},
{esc, esc},
{'1', '!'},
{'2', '@'},
{'3', '#'},
{'4', '$'},
{'5', '%'},
{'6', '^'},
{'7', '&'},
{'8', '*'},
{'9', '('},
{'0', ')'},
{'-', '_'},
{'=', '+'},
{backspace, backspace},
{tab, tab},
{'q', 'Q'},
{'w', 'W'},
{'e', 'E'},
{'r', 'R'},
{'t', 'T'},
{'y', 'Y'},
{'u', 'U'},
{'i', 'I'},
{'o', 'O'},
{'p', 'P'},
{'[', '{'},
{']', '}'},
{enter, enter},
{ctrl_l_char, ctrl_l_char},
{'a', 'A'},
{'s', 'S'},
{'d', 'D'},
{'f', 'F'},
{'g', 'G'},
{'h', 'H'},
{'j', 'J'},
{'k', 'K'},
{'l', 'L'},
{';', ':'},
{'\'', '"'},
{'`', '~'},
{shift_l_char, shift_l_char},
{'\\', '|'},
{'z', 'Z'},
{'x', 'X'},
{'c', 'C'},
{'v', 'V'},
{'b', 'B'},
{'n', 'N'},
{'m', 'M'},
{',', '<'},
{'.', '>'},
{'/', '?'},
{shift_r_char, shift_r_char},
{'*', '*'},
{alt_l_char, alt_l_char},
{' ', ' '},
{caps_lock_char, caps_lock_char}
//其他键暂不处理
};
键盘中断处理函数如下:
//键盘中断处理程序
void intr_keyboard_handler(void) {
//这次中断前的中断,以下三个键是否有按下
bool ctrl_down_last = ctrl_status;
bool shift_down_last = shift_status;
bool caps_lock_last = caps_lock_status;
bool break_code;
uint16_t scancode = inb(KBD_BUF_PORT);
//如果此次扫描码为0xeo,则表示扩展,直接返回
if (scancode == 0xe0) {
ext_scancode = true;
return;
}
//如果上次扫描码为0xe0,则合并
if (ext_scancode == true) {
scancode = (0xe000 | scancode);
ext_scancode = false;
}
//判断是否为断码
break_code = ((scancode & 0x0080) != 0);
//如果是断码
if (break_code) {
//通过断码得到其扫描码
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;
}
//若为通码
if ((scancode > 0x00 && scancode < 0x3b) || \
(scancode == alt_r_make) || \
(scancode == ctrl_r_make)) {
//用来判断是否与shift组合,将来用于在一维数组中索引对应的字符
bool shift = false;
//如果是表示两个字符的键,例如'0'~'9'
if ((scancode < 0x0e) || (scancode == 0x29) || \
(scancode == 0x1a) || (scancode == 0x1b) || \
(scancode == 0x2b) || (scancode == 0x27) || \
(scancode == 0x28) || (scancode == 0x33) || \
(scancode == 0x34) || (scancode == 0x35)) {
//表示两个字符的键的具体表示只与shift的状态有关
if (shift_down_last) shift = true;
} else {
//如果是字母键,只有shift和caps按下其中一个才表示大写
shift = shift_down_last ^ caps_lock_last;
}
//一律当作非扩展处理
uint8_t index = (scancode &= 0x00ff);
uint8_t cur_char = keymap[index][shift];
//只打印ascii码不为0的键
if (cur_char) {
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 ^= 1;
}
} else {
put_str("unknown key\n");
}
}
在开启键盘中断前,还需要初始化io队列,并注册键盘中断处理函数
初始化函数如下:
//键盘初始化
void keyboard_init(void) {
put_str("keyboard init start\n");
ioqueue_init(&kbd_buf);
register_handler(0x21, intr_keyboard_handler);
put_str("keyboard init done\n");
}