上节实现了对键盘中断服务子程序的处理和修改优化了中断程序,但只是简单的在中断服务子程序中记录断码或通码,缓冲区使用效率不高。
实现鼠标中断处理、优化中断缓存。pc中8259A中断控制器连接模型如下:
1.鼠标发送中断信号的数据线在从8259A芯片的IRQ4信号线,为了接收鼠标中断信号,我们在初始化中断控制芯片时,必须启用该信号线,同时,从8259A芯片是通过主8259A的IRQ2信号线连接在一起的,所以,也必须同时启动主8259A芯片的IRQ2信号线。故init8259A:初始化中断控制器中如下代码修改如下:
mov al, 11111001b ;CPU只接收主8259A, IRQ1管线发送的信号,其他管线发送信号一概忽略
out 0x21, al ;IRQ1对应的是键盘产生的中断
call io_delay
mov al, 11101111b ;IRQ4 允许鼠标中断
out 0xa1, al ;鼠标是通过从8259A的IRQ4管线向CPU发送信号
call io_delay
2.鼠标电路对应的一个端口是 0x64, 通过读取这个端口的数据来检测鼠标电路的状态,内核会从这个端口读入一个字节的数据,如果该字节的第二个比特位为0,那表明鼠标电路可以接受来自内核的命令,因此,在给鼠标电路发送数据前,内核需要反复从0x64端口读取数据,并检测读到数据的第二个比特位,直到该比特位为0时,才发送控制信息。
3.修改kernel.s 如下:
;全局描述符结构 8字节
; byte7 byte6 byte5 byte4 byte3 byte2 byte1 byte0
; byte6低四位和 byte1 byte0 表示段偏移上限
; byte7 byte4 byte3 byte2 表示段基址
;定义全局描述符数据结构
;3 表示有3个参数分别用 %1、%2、%3引用参数
;%1:段基址 %2:段偏移上限 %3:段属性
%macro GDescriptor 3
dw %2 & 0xffff
dw %1 & 0xffff
db (%1>>16) & 0xff
dw ((%2>>8) & 0x0f00) | (%3 & 0xf0ff)
db (%1>>24) & 0xff
%endmacro
DA_32 EQU 4000h ; 32 位段
DA_C EQU 98h ; 存在的只执行代码段属性值
DA_DRW EQU 92h ; 存在的可读写数据段属性值
DA_DRWA EQU 93h ; 存在的已访问可读写数据段类型值
;中断描述符表
;Gate selecotor, offset, DCount, Attr
%macro Gate 4
dw (%2 & 0xffff)
dw %1
dw (%3 & 0x1f) | ((%4 << 8) & 0xff00)
dw ((%2>>16) & 0xffff)
%endmacro
DA_386IGate EQU 8Eh ; 中断调用门
org 0x9000
jmp entry
[SECTION .gdt]
;定义全局描述符 段基址 段偏移上限 段属性
LABEL_GDT: GDescriptor 0, 0, 0
LABEL_DESC_CODE: GDescriptor 0, SegCodeLen-1, DA_C+DA_32
LABEL_DESC_VIDEO: GDescriptor 0xb8000, 0xffff, DA_DRW
LABEL_DESC_STACK: GDescriptor 0, STACK_TOP-1, DA_DRWA+DA_32
LABEL_DESC_VRAM: GDescriptor 0, 0xffffffff, DA_DRW
;gdt 表大小
GdtLen equ $-LABEL_GDT
;gdt表偏移上限和基地址
GdtPtr dw GdtLen-1
dd 0
;cpu开机进入实模式时使用的段寄存器 cs,ds,es,ss 和偏移地址组成内存地址,内存地址=段寄存器 * 16 + 偏移地址
;保护模式中段寄存器保存的是gdt 描述表中各个描述符的偏移,也叫选择子
SelectorCode32 EQU LABEL_DESC_CODE-LABEL_GDT
SelectorVideo EQU LABEL_DESC_VIDEO-LABEL_GDT
SelectorStack EQU LABEL_DESC_STACK-LABEL_GDT
SelectorVRAM EQU LABEL_DESC_VRAM-LABEL_GDT
;中断描述符表
LABEL_IDT:
%rep 0x21
Gate SelectorCode32, SpuriousHandler,0, DA_386IGate
%endrep
;键盘中断向量(8259A 键盘中断向量0x20,IRQ1 是键盘中断请求,0x20 + IRQ[n] = 0x21
.0x21:
Gate SelectorCode32, KeyboardHandler,0, DA_386IGate
%rep 10
Gate SelectorCode32, SpuriousHandler,0, DA_386IGate
%endrep
;从中断控制器8259A 中断向量0x28,IRQ4 是鼠标中断请求,0x28 + IRQ[n] = 0x2c
.0x2c:
Gate SelectorCode32, MouseHandler,0, DA_386IGate
IdtLen equ $ - LABEL_IDT
IdtPtr dw IdtLen - 1
dd 0
[SECTION .s16]
[BITS 16]
entry:
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov sp,0x100
;设置屏幕色彩模式
mov al,0x13
mov ah,0
int 0x10
;设置LABEL_DESC_CODE描述符段基址
mov eax,0
mov ax,cs
shl eax,4
add eax,SEG_CODE32
mov word [LABEL_DESC_CODE+2],ax
shr eax,16
mov [LABEL_DESC_CODE+4],al
mov [LABEL_DESC_CODE+7],ah
;设置栈空间
xor eax,eax
mov ax,cs
shl eax,4
add eax,LABEL_STACK
mov word [LABEL_DESC_STACK+2],ax
shr eax,16
mov byte [LABEL_DESC_STACK+4],al
mov byte [LABEL_DESC_STACK+7],ah
mov eax,0
mov ax,ds
shl eax,4
add eax,LABEL_GDT
mov dword [GdtPtr+2],eax
;设置GDTR 寄存器
lgdt [GdtPtr]
cli ;关闭可可屏蔽中断,如键盘中断
in al,0x92
or al,0x02
out 0x92,al
mov eax,cr0
or eax,1
mov cr0,eax
call init8259A
;加载中断描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_IDT
mov dword [IdtPtr + 2], eax
lidt [IdtPtr]
sti ;恢复中断
jmp dword SelectorCode32:0
;初始化8259A中断控制器
init8259A:
mov al, 0x11 ;向主8259A发送ICW1
out 0x20, al
call io_delay
out 0xa0, al ;向从8259A发送ICW1
call io_delay
;20h 分解成ICW2 是, ICW2[0,1,2] = 0, 这是强制要求的,
;也就是ICW2的值不能是0x21,0x22之类,只要前三位是0就行
;整个ICW2 = 0x20,这样的话,当主8259A对应的IRQ0管线向CPU发送信号时,
;CPU根据0x20这个值去查找要执行的代码,IRQ1管线向CPU发送信号时,
;CPU根据0x21这个值去查找要执行的代码,依次类推
mov al, 0x20 ;向主8259A发送ICW2
out 0x21, al ;
;
call io_delay
mov al, 0x28 ;向从8259A发送ICW2
out 0xa1, al
call io_delay
;04h 分解成ICW3 相当于ICW[2] = 1,
;这表示从8259A通过主IRQ2管线连接到主8259A控制器
mov al, 0x04 ; 向主8259A发送ICW3
out 0x21, al
call io_delay
mov al, 0x02 ;向从8259A 发送 ICW3
out 0xa1, al
call io_delay
mov al, 0x02
out 0x21, al
call io_delay
out 0xa1, al
call io_delay
;还需要再向两个芯片分别发送一个字节,叫OCW(operation control word),
;一个OCW是一字节数据, 也就是8bit,每一bit设置作用是,当OCW[i] = 1 时,
;屏蔽对应的IRQ(i)管线的信号,例如OCW[0]=1, 那么IRQ0管线的信号将不会被CPU接收,以此类推
;
mov al, 11111001b ;CPU只接收主8259A, IRQ1管线发送的信号,其他管线发送信号一概忽略
out 0x21, al ;IRQ1对应的是键盘产生的中断
call io_delay
mov al, 11101111b ;IRQ4 允许鼠标中断
out 0xa1, al ;鼠标是通过从8259A的IRQ4管线向CPU发送信号
call io_delay
ret
io_delay:
nop
nop
nop
nop
ret
[SECTION .s32]
[BITS 32]
SEG_CODE32:
mov ax,SelectorStack
mov ss,ax
mov esp,STACK_TOP
mov ax,SelectorVRAM
mov ds,ax
call init_main
fin:
hlt
jmp fin
;8259A中断控制器
LABEL_8259A:
SpuriousHandler equ LABEL_8259A - $$
iretd
;键盘中断程序
LabelKeyboardHandler:
KeyboardHandler equ LabelKeyboardHandler - $$
push es
push ds
pushad
mov eax, esp
push eax
call int_keyboard
pop eax
mov esp, eax
popad
pop ds
pop es
iretd
;鼠标中断程序
LabelMouseHandler:
MouseHandler equ LabelMouseHandler - $$
push es
push ds
pushad
mov eax, esp
push eax
call int_mouse
pop eax
mov esp, eax
popad
pop ds
pop es
iretd
;导入io操作函数模块
%include "io.s"
;导入C语言编写的功能模块
%include "os.s"
;32位模式代码长度
SegCodeLen EQU $-SEG_CODE32
[SECTION .gs]
ALIGN 32
[BITS 32]
LABEL_STACK:
times 1024 db 0
STACK_TOP EQU $ - LABEL_STACK
4.修改os.c如下:
// !compile method
// clang -m32 -c os.c -o os.o
// objconv -fnasm os.o -o os.s
//
#include "io.h"
#include "ascii_font.h"
//定义调色板颜色
#define COL8_000000 0
#define COL8_FF0000 1
#define COL8_00FF00 2
#define COL8_FFFF00 3
#define COL8_0000FF 4
#define COL8_FF00FF 5
#define COL8_00FFFF 6
#define COL8_FFFFFF 7
#define COL8_C6C6C6 8
#define COL8_840000 9
#define COL8_008400 10
#define COL8_848400 11
#define COL8_000084 12
#define COL8_840084 13
#define COL8_008484 14
#define COL8_848484 15
//屏幕宽度
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 200
//定义键盘缓冲数据区
#define KEYBUF_LEN 32
struct _KeyBuf{
//缓冲区数据长度
unsigned char buf[KEYBUF_LEN];
//下一数据读/写索引
int next_r, next_w;
//有效数据长度
int len;
}KeyBuf;
struct _KeyBuf keybuf = {{0},0,0,0};
void initPallet();
/**
*绘制矩形
*x 矩形左上角x坐标
*y 矩形左上角y坐标
*width 宽度
*height 高度
*colIndex pallet_color 类型调色板颜色索引,即矩形颜色
*/
void fillRect(int x,int y,int width,int height,char colIndex);
//绘制桌面背景
void drawBackground();
/**
*绘制字体
*@param addr 绘制的起始显存地址
*@param x 绘制的x坐标
*@param y 绘制的y坐标
*@param col 绘制颜色
*@param pch 绘制的字符数组8*16,每一行共8位,共16行
*@param screenWidth 屏幕宽度
*/
void putChar(char *addr,int x,int y,char col,unsigned char *ch,int screenWidth);
/*
*初始化鼠标指针
*@param vram 绘制的起始显存地址
*@param x 绘制鼠标指针最左上角x坐标
*@param y 绘制鼠标指针最左上角y坐标
*@param bc 绘制的矩形填充颜色,和背景色一样将能看到鼠标指针
*/
void init_mouse_cursor(char *vram,int x,int y,char bc);
/*
*char 类型数据转换为16进制字符数据
*@param val 待转化为16进制的数值
*@param arr 保存16进制字符串数据的数组
*/
void char2HexStr(unsigned char val,char *arr);
//初始化鼠标硬件
void init_mouse();
int num = 0;
//操作系统C语言入口函数--可以指定为其他
void init_main() {
io_sti();
initPallet();
drawBackground();
init_mouse_cursor((char *)0xa0000,100,100,COL8_008484);
init_mouse();
for(; ;){
if(keybuf.len==0){
io_hlt();
}
else{
io_cli();
static char arr[4] = {'0','x'};
unsigned char *ascii = ascii_array;
for(int t=0;t> 4;
if(tmp>=10){
arr[2] = 'a'+tmp-10;
}
else{
arr[2] = '0'+tmp;
}
tmp = val & 0x0f;
if(tmp>=10){
arr[3] = 'a'+tmp-10;
}
else{
arr[3] = '0'+tmp;
}
}
/*
*8259A 键盘中断调用
*
*/
void int_keyboard(char *index){
//0x20是8259A控制端口
//0x21对应的是键盘的中断向量。当键盘中断被CPU执行后,下次键盘再向CPU发送信号时,
//CPU就不会接收,要想让CPU再次接收信号,必须向主PIC的端口再次发送键盘中断的中断向量号
io_out8(0x20,0x21);
//读取8259A 0x60端口键盘扫描码
char data = io_in8(0x60);
if(keybuf.len
鼠标电路被激活后将给CPU发送中断信号显示mouse图形,加载运行floppy.img 效果如下:
1.随着功能的丰富,os.c 文件也越来越大会导致修改的难度增加,我们先对os.c 文件分离,考虑到我们OS 是一个简单小巧,文件不会太多。故把os.c 文件中相关申明、类型定义部分提取到os.h 文件中。修改后os.h 文件如下:
//*******************************相关数据类型声明*************************
//定义调色板颜色
#define COL8_000000 0
#define COL8_FF0000 1
#define COL8_00FF00 2
#define COL8_FFFF00 3
#define COL8_0000FF 4
#define COL8_FF00FF 5
#define COL8_00FFFF 6
#define COL8_FFFFFF 7
#define COL8_C6C6C6 8
#define COL8_840000 9
#define COL8_008400 10
#define COL8_848400 11
#define COL8_000084 12
#define COL8_840084 13
#define COL8_008484 14
#define COL8_848484 15
//屏幕宽度
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 200
//定义缓冲区
typedef struct _FIFO8{
//指向缓冲区
char* buf;
//r:读索引,w:写索引
//len:存储数据长度
int r, w, size, len, flag;
}FIFO8;
//*******************************函数声明*************************
//初始化调色板
void initPallet();
/**
*绘制矩形
*x 矩形左上角x坐标
*y 矩形左上角y坐标
*width 宽度
*height 高度
*colIndex pallet_color 类型调色板颜色索引,即矩形颜色
*/
void fillRect(int x,int y,int width,int height,char colIndex);
//绘制桌面背景
void drawBackground();
/**
*绘制字体
*@param addr 绘制的起始显存地址
*@param x 绘制的x坐标
*@param y 绘制的y坐标
*@param col 绘制颜色
*@param pch 绘制的字符数组8*16,每一行共8位,共16行
*@param screenWidth 屏幕宽度
*/
void putChar(char *addr,int x,int y,char col,unsigned char *ch,int screenWidth);
/*
*初始化鼠标指针
*@param vram 绘制的起始显存地址
*@param x 绘制鼠标指针最左上角x坐标
*@param y 绘制鼠标指针最左上角y坐标
*@param bc 绘制的矩形填充颜色,和背景色一样将能看到鼠标指针
*/
void init_mouse_cursor(char *vram,int x,int y,char bc);
/*
*char 类型数据转换为16进制字符数据
*@param val 待转化为16进制的数值
*@param arr 保存16进制字符串数据的数组
*/
void char2HexStr(unsigned char val,char *arr);
//初始化鼠标硬件
void init_mouse();
//缓存初始化
void fifo8_init(FIFO8 *fifo,int size,char *buf);
//缓冲区存放数据
int fifo8_put(FIFO8 *fifo,char data);
//缓冲区读取数据
int fifo8_get(FIFO8 *fifo);
这样我们只需要在os.c 文件中 #include "os.h"
,os.c 文件中写相关实现逻辑和屏蔽不需要暴露的功能!
2.os.c文件如下:
// !compile method
// clang -m32 -c os.c -o os.o
// objconv -fnasm os.o -o os.s
//
#include "os.h"
#include "io.h"
#include "ascii_font.h"
static char keybuf[32];
static char mousebuf[128];
static FIFO8 keybufInfo;
static FIFO8 mousebufInfo;
static int num = 0;
//操作系统C语言入口函数--可以指定为其他
void init_main() {
io_sti();
initPallet();
drawBackground();
fifo8_init(&keybufInfo,32,keybuf);
fifo8_init(&mousebufInfo,128,mousebuf);
init_mouse_cursor((char *)0xa0000,100,100,COL8_008484);
init_mouse();
for(; ;){
if(keybufInfo.len>0){
io_cli();
static char arr[4] = {'0','x'};
unsigned char *ascii = ascii_array;
for(int t=0;t0){
io_cli();
static char arr[4] = {'0','x'};
unsigned char *ascii = ascii_array;
for(int t=0;t> 4;
if(tmp>=10){
arr[2] = 'a'+tmp-10;
}
else{
arr[2] = '0'+tmp;
}
tmp = val & 0x0f;
if(tmp>=10){
arr[3] = 'a'+tmp-10;
}
else{
arr[3] = '0'+tmp;
}
}
/*
*8259A 键盘中断调用
*
*/
void int_keyboard(char *index){
//0x20是8259A控制端口
//0x21对应的是键盘的中断向量。当键盘中断被CPU执行后,下次键盘再向CPU发送信号时,
//CPU就不会接收,要想让CPU再次接收信号,必须向主PIC的端口再次发送键盘中断的中断向量号
io_out8(0x20,0x21);
//读取8259A 0x60端口键盘扫描码
char data = io_in8(0x60);
fifo8_put(&keybufInfo,data);
}
#define PORT_KEYDAT 0x60
#define PORT_KEYSTA 0x64
#define PORT_KEYCMD 0x64
#define KEYCMD_WRITE_MODE 0x60
#define KBC_MODE 0x47
//鼠标电路对应的一个端口是 0x64, 通过读取这个端口的数据来检测鼠标电路的状态,
//内核会从这个端口读入一个字节的数据,如果该字节的第二个比特位为0,那表明鼠标电路可以
//接受来自内核的命令,因此,在给鼠标电路发送数据前,内核需要反复从0x64端口读取数据,
//并检测读到数据的第二个比特位,直到该比特位为0时,才发送控制信息
void waitKBCReady(){
for( ; ;){
if((io_in8(PORT_KEYSTA) & 0x02)==0){
break;
}
}
}
#define KEYCMD_SENDTO_MOUSE 0xd4
#define MOUSECMD_ENABLE 0xf4
//初始化键盘控制电路,鼠标控制电路是连接在键盘控制电路上,通过键盘电路实现初始化
void init_mouse(){
waitKBCReady();
//0x60让键盘电路进入数据接受状态
io_out8(PORT_KEYCMD,KEYCMD_WRITE_MODE);
waitKBCReady();
//数据0x47要求键盘电路启动鼠标模式,这样鼠标硬件所产生的数据信息,通过键盘电路端口0x60就可读到
io_out8(PORT_KEYDAT,KBC_MODE);
waitKBCReady();
io_out8(PORT_KEYCMD,KEYCMD_SENDTO_MOUSE);
waitKBCReady();
//0xf4数据激活鼠标电路,激活后将会给CPU发送中断信号
io_out8(PORT_KEYDAT,MOUSECMD_ENABLE);
}
/*
*8259A 鼠标中断调用
*
*/
void int_mouse(char *index){
//当中断处理后,要想再次接收中断信号,就必须向中断控制器发送一个字节的数据
io_out8(0x20,0x20);
io_out8(0xa0,0x20);
//读取鼠标数据
char data = io_in8(0x60);
fifo8_put(&mousebufInfo,data);
}
void fifo8_init(FIFO8 *fifo, int size,char *buf){
fifo->buf = buf;
fifo->r = 0;
fifo->w = 0;
fifo->size = size;
fifo->len = 0;
fifo->flag = 0;
}
int fifo8_put(FIFO8 *fifo,char data){
if (fifo->len == fifo->size) {
return -1;
}
fifo->buf[fifo->w] = data;
fifo->w++;
if (fifo->w == fifo->size) {
fifo->w = 0;
}
fifo->len++;
return 0;
}
int fifo8_get(FIFO8 *fifo) {
if (fifo->len == 0) {
return -1;
}
int data = fifo->buf[fifo->r];
fifo->r++;
if (fifo->r == fifo->size) {
fifo->r = 0;
}
fifo->len--;
return data;
}
中断处理后,要想再次接收中断信号,必须向中断控制器发送一个字节的数据,这个字节数据叫OCW2, OCW2[0-2] 用来表示中断的优先级,OCW2[3-4]这两位必须设置为0,OCW[5]这一位称之为End of Interrupt, 这一位设置为1,表示当前中断处理结束,控制器可以继续调用中断函数处理到来的中断信号,要想下一次继续处理中断信号,这一位必须设置为1,OCW2[6-7]设置为0即可,我们代码中发送OCW2时的数值是0x20,也就是仅仅把OCW[5]设置为1。
运行内核文件后屏幕上会显示0xfa,是鼠标被激活时传送过来的。
点击键盘a 字符,界面会出现0x1e、0x9e,再把鼠标放入虚拟机后移动鼠标屏幕上回出现一串信息,效果如下:
至此我们的键盘鼠标中断处理完成,最后鼠标发送的数据,需要连续三个字节一起解读。