第七天 - FIFO与鼠标控制
FIFO与鼠标控制
一、FIFO
1. 获取按键编码
- 实现当按下键盘时,会在屏幕显示按下键的编码
#define PORT_KEYDAT 0x0060
void inthandler21(int *esp)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
unsigned char data, s[4];
io_out8(PIC0_OCW2, 0x61); /* 通知PIC“IRQ-01”已经受理完毕 */
data = io_in8(PORT_KEYDAT);
sprintf(s, "%02X", data);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
return;
}
io_out8函数
- 通知PIC发生了 IRQ1中断,参数为0x61(0x60 + IRQ号码作为参数),PIC就会监听IRQ1中断的发生,如果没执行则PIC不再监听IRQ1的中断
2. 加快中断处理
- CPU处理键盘的中断的速度太慢,在中断程序处理过程中不能接受其他中断,需要等待键盘中断处理程序的结束,就会阻碍到其他事情(如鼠标移动,download速度等)的执行。
- 字符显示过程是需要大量的时间进行处理的(如逐个遍历位,并判断是否往VRAM中描画该像素,进行地址计算,然后执行内存写入指令
优化中断处理速度方案
- 先把按键的编码保存到变量中,CPU偶尔查看变量,如果变量有了数据则将其显示到屏幕中
#define PORT_KEYDAT 0x0060
struct KEYBUF keybuf;
void inthandler21(int *esp)
{
unsigned char data;
io_out8(PIC0_OCW2, 0x61); /* 通知PIC“IRQ-01”已经受理完毕 */
data = io_in8(PORT_KEYDAT);
if (keybuf.flag == 0) {
keybuf.data = data;
keybuf.flag = 1;
}
return;
}
- 使用 binfo结构体作为缓冲区来存储键盘的输入,使用 变量flag判断结构体是否有数据,如果为1则表示有数据,则不接受其他输入
HariMain分析
extern struct KEYBUF keybuf;
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
char s[40], mcursor[256];
int mx, my, i;
init_gdtidt();
init_pic();
io_sti(); /* 由于IDT / PIC的初始化已完成,因此请禁用CPU的中断。 */
io_out8(PIC0_IMR, 0xf9); /* 允许PIC1和键盘(11111001) */
io_out8(PIC1_IMR, 0xef); /* 允许鼠标(11101111) */
init_palette();
init_screen8(binfo->vram, binfo->scrnx, binfo->scrny);
mx = (binfo->scrnx - 16) / 2; /* 计算坐标在屏幕中心 */
my = (binfo->scrny - 28 - 16) / 2;
init_mouse_cursor8(mcursor, COL8_008484);
putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16);
sprintf(s, "(%d, %d)", mx, my);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s);
for (;;) {
io_cli();
if (keybuf.flag == 0) {
io_stihlt();
} else {
i = keybuf.data;
keybuf.flag = 0;
io_sti();
sprintf(s, "%02X", i);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
}
}
}
- io_cli指令屏蔽中断,然后检查缓冲区的flag,如果没有数据则表示没有键盘输入,执行io_stihlt函数,里面执行了STI 和HLT指令,取消中断屏蔽,这样子CPU可以继续相应PIC的通知
- 如果flag为1,表示有键盘输入,把key值保存在变量i中,将flag清空,然后使用STI指令开放中断,接着就对键盘获取的数据进行显示处理
还存在的问题
- 当按下右Ctrl键时,会产生两个字节的键码 “E0 1D",弹起键时又会产生两个字节的键码 "E0 9D"
- 键盘内部电路一次只能发送一个字节,所以一次按键需要两次中断去发送键码,会导致一个字节被丢弃
3. 制作FIFO缓冲区
-
用FIFO缓冲区可以解决上述的两次中断,第二次键值丢失的问题
-
struct KEYBUF{ unsigned char data[32]; int next; }; void inthandler21(int *esp) { unsigned char data; io_out8(PIC0_OCW2, 0x61); /* 通知PIC“IRQ-01”已经受理完毕 */ data = io_in8(PORT_KEYDAT); if (keybuf.next < 32) { keybuf.data[keybuf.next] = data; keybuf.next++; } return;
-
next 管理下一个存储位置的下标
-
取数据
-
for (;;) { io_cli(); if (keybuf.next == 0) { io_stihlt(); } else { i = keybuf.data[0]; keybuf.next--; for (j = 0; j < keybuf.next; j++) { keybuf.data[j] = keybuf.data[j + 1]; } io_sti(); sprintf(s, "%02X", i); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s); }
-
先进后出,从前往后取,取出一个数据,其他数据往前赋值
-
-
4. FIFO 缓冲区采用循环数组作为循环队列,避免数据前移操作
-
修改FIFO缓冲区, 避免在数据取出后无需进行数据前移操作
-
使用循环队列(循环数组的思想)
- 假设缓冲区的大小是32
-
当下一个数据写入的位置到达缓冲区最末尾时(32)时,强制性地将其设置为0,从头开始写,但如果数据读出速度不够快,会使得部分数据被强制覆盖
-
struct KEYBUF { unsigned char data[32]; int next_r, next_w, len; }; void inthandler21(int *esp) { unsigned char data; io_out8(PIC0_OCW2, 0x61); /* 通知PIC“IRQ-01”已经受理完毕 */ data = io_in8(PORT_KEYDAT); if (keybuf.len < 32) { keybuf.data[keybuf.next_w] = data; keybuf.len++; keybuf.next_w++; if (keybuf.next_w == 32) { keybuf.next_w = 0; } } return; }
-
-
读取数据时也是类似的操作
-
for (;;) { io_cli(); if (keybuf.len == 0) { io_stihlt(); } else { i = keybuf.data[keybuf.next_r]; keybuf.len--; keybuf.next_r++; if (keybuf.next_r == 32) { keybuf.next_r = 0; } io_sti(); sprintf(s, "%02X", i); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s); } }
-
5. FIFO缓冲区采用指针,实现大小可扩展
-
为了让缓冲区的大小可根据情况扩展
-
struct FIFO8 { unsigned char *buf; int p, q, size, free, flags; };
-
采用指针类型存储缓冲区的起始地址
-
free保存缓冲区中没有数据的字节数
-
p 保存下一个数据的写入地址
-
q 保存下一个数据的读出地址
-
-
缓冲区初始化操作
-
void fifo8_init(struct FIFO8 *fifo, int size, unsigned char *buf) /* 初始化 FIFO 缓冲区 */ { fifo->size = size; fifo->buf = buf; fifo->free = size; /* 缓冲区的大小 */ fifo->flags = 0; fifo->p = 0; /* 下一个数据写入位置 */ fifo->q = 0; /* 下一个数据读出位置 */ return; }
-
-
缓冲区写数据函数
-
int fifo8_put(struct FIFO8 *fifo, unsigned char data) /* 向 FIFO 传送数据并保存 */ { if (fifo->free == 0) { /* 空余没有了,溢出 */ fifo->flags |= FLAGS_OVERRUN; return -1; } fifo->buf[fifo->p] = data; fifo->p++; if (fifo->p == fifo->size) { fifo->p = 0; } fifo->free--; return 0; }
-
先判断是否有空余位置,用flags记录是否有溢出
-
否则就读取数据,然后根据调整相应的变量
-
-
缓冲区读取函数
-
int fifo8_get(struct FIFO8 *fifo) /* 从 FIFO 取得一个数据 */ { int data; if (fifo->free == fifo->size) { /* 如果缓冲区为空,则返回-1 */ return -1; } data = fifo->buf[fifo->q]; fifo->q++; if (fifo->q == fifo->size) { fifo->q = 0; } fifo->free++; return data; }
-
-
缓冲区状态查询函数
-
int fifo8_status(struct FIFO8 *fifo) /* 报告一下到底积攒了多少数据 */ { return fifo->size - fifo->free; }
-
-
中断处理函数使用FIFO缓冲区
-
void inthandler21(int *esp) { unsigned char data; io_out8(PIC0_OCW2, 0x61); /* 通知PIC“IRQ-01”已经受理完毕 */ data = io_in8(PORT_KEYDAT); fifo8_put(&keyfifo, data); return; }
-
-
MariMain函数中查询缓冲区状态,有数据则读取并显示
-
for (;;) { io_cli(); if (fifo8_status(&keyfifo) == 0) { io_stihlt(); } else { i = fifo8_get(&keyfifo); io_sti(); sprintf(s, "%02X", i); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s); }
-
二、鼠标控制
6. 鼠标控制
鼠标控制电路包含在键盘控制电路中 - bootpack.c
#define PORT_KEYDAT 0x0060
#define PORT_KEYSTA 0x0064
#define PORT_KEYCMD 0x0064
#define KEYSTA_SEND_NOTREADY 0x02
#define KEYCMD_WRITE_MODE 0x60
#define KBC_MODE 0x47
void wait_KBC_sendready(void)
{
/* 等待键盘控制电路准备完毕 */
for (;;) {
if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) {
break;
}
}
return;
}
void init_keyboard(void)
{
/* 初始化键盘控制电路 */
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);
wait_KBC_sendready();
io_out8(PORT_KEYDAT, KBC_MODE);
return;
}
- wait_KBC_sendready 函数让键盘控制电路 做好准备,等待控制指令到来
- 因为CPU的电路速度快于键盘控制电路速度,可能因为供过于求导致指令执行不完全出错。
- CPU不断查询从设备号码处读取的数据的倒数第二位的数据,当为0时表示键盘电路准备完毕
- init_keyboard 函数确认可否往键盘控制电路传送信息的同时发送模式设定指令
向键盘控制器发送指令
#define KEYCMD_SENDTO_MOUSE 0xd4
#define MOUSECMD_ENABLE 0xf4
void enable_mouse(void)
{
/* 激活鼠标 */
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);
wait_KBC_sendready();
io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);
return; /* 顺利的话,键盘控制其会返送回ACK(0xfa) */
}
- 激活鼠标
- 往键盘控制电路发送指令0xd4,下一个数据就会自动发给鼠标
- 鼠标收到激活指令后,就会发送0xfa指令给CPU,表明鼠标已经激活,
- 鼠标就会不断地产生鼠标中断
7、从鼠标接受数据
获取中断数据
struct FIFO8 mousefifo;
void inthandler2c(int *esp)
/* 来自PS/2鼠标的中断 */
{
unsigned char data;
io_out8(PIC1_OCW2, 0x64); /* 通知PIC1 IRQ-12的受理已经完成 */
io_out8(PIC0_OCW2, 0x62); /* 通知PIC0 IRQ-02的受理已经完成 */
data = io_in8(PORT_KEYDAT);
fifo8_put(&mousefifo, data);
return;
}
- 因为主从PIC的协调不能主动完成,所以在通知完从PIC后通知主PIC,否则主PIC就会忽略从PIC的下一个中断请求
获取鼠标数据
fifo8_init(&mousefifo, 128, mousebuf);
for (;;) {
io_cli();
if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {
io_stihlt();
} else {
if (fifo8_status(&keyfifo) != 0) {
i = fifo8_get(&keyfifo);
io_sti();
sprintf(s, "%02X", i);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
} else if (fifo8_status(&mousefifo) != 0) {
i = fifo8_get(&mousefifo);
io_sti();
sprintf(s, "%02X", i);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 47, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
}
}
}
- 鼠标是源源不断发送数据,所以缓冲区应设定大一些,大体逻辑与键盘的类似
- 判断键盘与鼠标的缓冲区是否为空,非空则先检查keyinfo显示,再检查mouseinfo数据
总结
#include "bootpack.h"
#include <stdio.h>
extern struct FIFO8 keyfifo, mousefifo;
void enable_mouse(void);
void init_keyboard(void);
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
char s[40], mcursor[256], keybuf[32], mousebuf[128];
int mx, my, i;
init_gdtidt();
init_pic();
io_sti(); /* 由于IDT / PIC的初始化已完成,因此请禁用CPU的中断。 */
fifo8_init(&keyfifo, 32, keybuf);
fifo8_init(&mousefifo, 128, mousebuf);
io_out8(PIC0_IMR, 0xf9); /* 允许PIC1和键盘(11111001) */
io_out8(PIC1_IMR, 0xef); /* 允许鼠标(11101111) */
init_keyboard();
init_palette();
init_screen8(binfo->vram, binfo->scrnx, binfo->scrny);
mx = (binfo->scrnx - 16) / 2;
my = (binfo->scrny - 28 - 16) / 2;
init_mouse_cursor8(mcursor, COL8_008484);
putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16);
sprintf(s, "(%d, %d)", mx, my);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s);
enable_mouse();
for (;;) {
io_cli();
if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {
io_stihlt();
} else {
if (fifo8_status(&keyfifo) != 0) {
i = fifo8_get(&keyfifo);
io_sti();
sprintf(s, "%02X", i);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
} else if (fifo8_status(&mousefifo) != 0) {
i = fifo8_get(&mousefifo);
io_sti();
sprintf(s, "%02X", i);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 47, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
}
}
}
}
#define PORT_KEYDAT 0x0060
#define PORT_KEYSTA 0x0064
#define PORT_KEYCMD 0x0064
#define KEYSTA_SEND_NOTREADY 0x02
#define KEYCMD_WRITE_MODE 0x60
#define KBC_MODE 0x47
void wait_KBC_sendready(void)
{
/* 等待键盘控制电路准备完毕 */
for (;;) {
if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) {
break;
}
}
return;
}
void init_keyboard(void)
{
/* 初始化键盘控制电路 */
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);
wait_KBC_sendready();
io_out8(PORT_KEYDAT, KBC_MODE);
return;
}
#define KEYCMD_SENDTO_MOUSE 0xd4
#define MOUSECMD_ENABLE 0xf4
void enable_mouse(void)
{
/* 激活鼠标 */
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);
wait_KBC_sendready();
io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);
return; /* 顺利的话,键盘控制其会返送回ACK(0xfa) */
}
- 初始化键盘和鼠标的缓冲区
- 通过 init_out8通知PIC监听中断请求
- 主循环中
- CPU不断查询键盘和鼠标缓冲区的状态,然后进行不同的处理
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现