第七天 - 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);	/* 通知PICIRQ-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);	/* 通知PICIRQ-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不断查询键盘和鼠标缓冲区的状态,然后进行不同的处理

image-20230403002821984

posted @   bingekong  阅读(36)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示