11.建立中断机制

简介

1.我们绘制了鼠标指针,但是都是固定在特定位置,不能移动。为了能响应键盘和鼠标事件,我们需要了解中断控制器、建立中断机制!

2.8259A芯片是一个中断管理芯片。中断的来源除了来自于硬件自身的NMI中断和来自于软件的INT n指令造成的软件中断之外,还有来自于外部硬件设备的中断,这些中断是可屏蔽的。这些中断也都通过可编程中断控制器PIC(Programmable Interrupt Controller)进行控制,并传递给CPU。

11.建立中断机制_第1张图片

一个8259A芯片的可以接最多8个中断源,但由于可以将2个或多个8259A芯片级连(cascade),并且最多可以级连到9个,所以最多可以接64个中断源。如今绝大多数的PC都拥有两个8259A,这样 最多可以接收15个中断源。

在一个8259A芯片有如下几个内部寄存器
Interrupt Mask Register (IMR)。
Interrupt Request Register (IRR)。
In Service Register (ISR)。

IMR被用作过滤被屏蔽的中断,IRR被用作暂时放置未被进一步处理的Interrupt,当一个Interrupt正在被CPU处理时,此中断被放置在ISR中。
除了这几个寄存器之外,8259A还有一个单元叫做Priority Resolver,当多个中断同时发生时,Priority Resolver根据它们的优先级,将高优先级者优先传递给CPU。

3.8259A工作原理
当一个中断请求从IR0到IR7中的某根线到达IMR时,IMR首先判断此IR是否被屏蔽,如果被屏蔽,则此中断请求被丢弃;否则,则将其放入IRR中。

在此中断请求不能进行下一步处理之前,它一直被放在IRR中。一旦发现处理中断的时机已到,Priority Resolver将从所有被放置于IRR中的中断中挑选出一个优先级最高的中断,将其传递给CPU去处理。IR号越低的中断优先级别越高,比如IR0的优先级别是最高的。

8259A通过发送一个INTR(Interrupt Request)信号给CPU,通知CPU有一个中断到达。CPU收到这个信号后,会暂停执行下一条指令,然后发送一个INTA(Interrupt Acknowledge)信号给8259A。8259A收到这个信号之后,马上将ISR中对应此中断请求的Bit设置,同时IRR中相应的bit会被reset。比如,如果当前的中断请求是IR3的话,那么ISR中的bit-3就会被设置,IRR中IR3对应的bit就会被reset。这表示此中断请求正在被CPU处理,而不是正在等待CPU处理。

随后,CPU会再次发送一个INTA信号给8259A,要求它告诉CPU此中断请求的中断向量是什么,这是一个从0到255的一个数。8259A根据被设置的起始向量号(起始向量号通过中断控制字ICW2被初始化)加上中断请求号计算出中断向量号,并将其放置在Data Bus上。比如被初始化的起始向量号为8,当前的中断请求为IR3,则计算出的中断向量为8+3=11。

CPU从Data Bus上得到这个中断向量之后,就去IDT中找到相应的中断服务程序ISR,并调用它。如果8259A的End of Interrupt (EOI)通知被设定位人工模式,那么当ISR处理完该处理的事情之后,应该发送一个EOI给8259A。
8259A得到EOI通知之后,ISR寄存器中对应于此中断请求的Bit会被Reset。
如果8259A的End of Interrupt (EOI)通知被设定位自动模式,那么在第2个INTA信号收到后,8259A ISR寄存器中对应于此中断请求的Bit就会被Reset。

在此期间,如果又有新的中断请求到达,并被放置于IRR中,如果这些新的中断请求中有比在ISR寄存中放置的所有中断优先级别还高的话,那么这些高优先级别的中断请求将会被马上按照上述过程进行处理;否则,这些中断将会被放在IRR中,直到ISR中高优先级别的中断被处理结束,也就是说知道ISR寄存器中高优先级别的bit被Reset为止。

4.8259As的口令
程序员可以向8259A写两种命令字:
Initialization Command Word (ICW);这种命令字被用作对8259A芯片的初始化。
Operation Command Word (OCW):这种命令被用来向8259A发布命令,以对其进行控制。OCW可以在8259A被初始化之后的任何时候被使用。

任何时候,只要向某一个8259A的第一个端口(0x20 for Master,and 0xA0 for Slave)写入的命令的bit-4(从0算起)为1,那么这个8259A就认为这是一个ICW1;而一旦一个8259A收到一个ICW1,它就认为一个初始化序列开始了。

我们需要对指定端口发送1字节的数据,这个一字节(8 bit)的数据,我们称之为ICW(initialization control word).主8259A对应的端口地址是20h,21h, 从8259A对应的端口是A0h和A1h. 对端口发送数据时,顺序是固定的。
往端口20h(主片)或A0h(从片)发送ICW1
往端口21h(主片)或A1h(从片)发送ICW2
往端口21h(主片)或A1h(从片)发送ICW3
往端口20h(主片)或A0h(从片)发送ICW4

ICW的结构和意义:
ICW1[0…7]:
ICW1[0] 设置为1表示需要发送ICW4,0表示不需要发送ICW4.
ICW1[1] 设置为1表示单个8259, 0表示级联8259
ICW1[2] 设置为1表示4字节中断向量,0表示8字节中断向量
ICW1[3] 设置为1表示中断形式是水平触发,0表示边沿触发
ICW1[4] 必须设置为1
ICW1[5,6,7] 必须设置为0

ICW2[0…7]:
ICW2[0,1,2] 对于80X86架构必须设置为0
ICW2[3…7]: 80X86中断向量

ICW3[0…7]:(主片)
ICW3[0] 设置为1, IR0级联从片,0无从片
ICW3[1] 设置为1, IR1级联从片,0无从片
ICW3[2] 设置为1, IR2级联从片,0无从片
ICW3[3] 设置为1, IR3级联从片,0无从片
ICW3[4] 设置为1, IR4级联从片,0无从片
ICW3[5] 设置为1, IR5级联从片,0无从片
ICW3[6] 设置为1, IR6级联从片,0无从片

ICW3[0…7]:(从片):
ICW3[0,1,2] 从片连接主片的IR号
ICw3[3…7] 必须是0

ICW4[0…7]:
ICW4[0] 设置为1,表示x86模式,0表示MCS 80/85模式
ICW4[1] 设置为1,自动EOI;0 正常EOI
ICW4[2,3] 表示主从缓冲模式
ICW4[4] 1表示SFNM模式; 0 sequential 模式
ICW4[5,6,7] 设置为0

目标

1.实现键盘中断事件,修改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 & 0FFFFh)
		dw  %1
		dw  (%3 & 1Fh) | ((%4 << 8) & 0FF00h)
		dw  ((%2>>16) & 0FFFFh)
	%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  255
		Gate  SelectorCode32, SpuriousHandler,0, DA_386IGate
	%endrep

	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]

    jmp dword SelectorCode32:0



;初始化8259A中断控制器
init8259A:
	mov  al, 011h		;向主8259A发送ICW1
	out  20h, al
	call io_delay

	out 0A0h, 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, 020h		;向主8259A发送ICW2
	out 021h, al		;
						;
	call io_delay

	mov  al, 028h		;向从8259A发送ICW2
	out  0A1h, al
	call io_delay

	;04h 分解成ICW3 相当于ICW[2] = 1, 
	;这表示从8259A通过主IRQ2管线连接到主8259A控制器
	mov  al, 004h		; 向主8259A发送ICW3 
	out  021h, al
	call io_delay

	mov  al, 002h		;向从8259A 发送 ICW3 
	out  0A1h, al
	call io_delay

	mov  al, 002h
	out  021h, al
	call io_delay

	out  0A1h, al
	call io_delay


	;还需要再向两个芯片分别发送一个字节,叫OCW(operation control word), 
	;一个OCW是一字节数据,	也就是8bit,每一bit设置作用是,当OCW[i] = 1 时,
	;屏蔽对应的IRQ(i)管线的信号,例如OCW[0]=1, 那么IRQ0管线的信号将不会被CPU接收,以此类推
	;
	mov  al, 11111101b	;CPU只接收主8259A, IRQ1管线发送的信号,其他管线发送信号一概忽略
	out  021h, al		;IRQ1对应的是键盘产生的中断
	call io_delay

	mov  al, 11111111b	;CPU忽略所有来自从8259A芯片的信号
	out  0A1h, 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 

	;恢复中断
	sti

   
    call init_main 

fin:    
    hlt
    jmp fin


LABEL_8259A:
	SpuriousHandler  equ LABEL_8259A - $$
	call int_8259A
	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

2.修改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


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);



//操作系统C语言入口函数--可以指定为其他
void init_main() {

    initPallet();
	drawBackground();

	init_mouse_cursor((char *)0xa0000,100,100,COL8_008484);
	
    for(; ;){
        io_hlt();
    }
    
}

void initPallet(){
    //定义调色板
    static unsigned char table_rgb[16*3] = {
    
        0x00,  0x00,  0x00,		/*  0:黑色*/
        0xff,  0x00,  0x00,		/*  1:亮红*/
        0x00,  0xff,  0x00,		/*  2:亮绿*/
        0xff,  0xff,  0x00,		/*  3:亮黄*/
        0x00,  0x00,  0xff,		/*  4:亮蓝*/
        0xff,  0x00,  0xff,		/*  5:亮紫*/
        0x00,  0xff,  0xff,		/*  6:浅亮蓝*/
        0xff,  0xff,  0xff,		/*  7:白色*/
        0xc6,  0xc6,  0xc6,		/*  8:亮灰*/
        0x84,  0x00,  0x00,		/*  9:暗红*/
        0x00,  0x84,  0x00,		/* 10:暗绿*/
        0x84,  0x84,  0x00,		/* 11:暗黄*/
        0x00,  0x00,  0x84,		/* 12:暗青*/
        0x84,  0x00,  0x84,		/* 13:暗紫*/
        0x00,  0x84,  0x84,		/* 14:浅灰蓝*/
        0x84,  0x84,  0x84,		/* 15:暗灰*/
        
    };
    
	unsigned char *rgb = table_rgb;
	int flag = io_readFlag();
    io_cli();
    io_out8(0x03c8, 0);
	for(int i=0;i<16;i++){
		io_out8(0x03c9,rgb[0] / 4);
        io_out8(0x03c9,rgb[1] / 4);
        io_out8(0x03c9,rgb[2] / 4);
    	rgb += 3;
	}
	io_writeFlag(flag);
}


void fillRect(int x,int y,int width,int height,char colIndex){
    char *vram = (char *)0xa0000;
    for(int i=y;i<=y+height;i++){
        for(int j=x;j<=x+width;j++){
            vram[i*SCREEN_WIDTH+j] = colIndex;
        }
    }
}


void drawBackground(){
	fillRect(0,0,SCREEN_WIDTH-1,SCREEN_HEIGHT-29, COL8_008484);
    fillRect(0,SCREEN_HEIGHT-28,SCREEN_WIDTH-1,28, COL8_848484);


	fillRect(0,SCREEN_HEIGHT-27,SCREEN_WIDTH,1, COL8_848484);
	fillRect(0,SCREEN_HEIGHT-26,SCREEN_WIDTH,25, COL8_C6C6C6);
	
	fillRect(3,SCREEN_HEIGHT-24,56,1, COL8_FFFFFF);
	fillRect(2,SCREEN_HEIGHT-24,1,20, COL8_FFFFFF);

	fillRect(3,SCREEN_HEIGHT-4,56,1, COL8_848484);
	fillRect(59,SCREEN_HEIGHT-23,1,19, COL8_848484);

	fillRect(2,SCREEN_HEIGHT-3,57,0, COL8_000000);
	fillRect(60,SCREEN_HEIGHT-24,0,19, COL8_000000);

	fillRect(SCREEN_WIDTH-47,SCREEN_HEIGHT-24,43,1, COL8_848484);
	fillRect(SCREEN_WIDTH-47,SCREEN_HEIGHT-23,0,19, COL8_848484);

	fillRect(SCREEN_WIDTH-47,SCREEN_HEIGHT-3,43,0, COL8_FFFFFF);
	fillRect(SCREEN_WIDTH-3,SCREEN_HEIGHT-24,0,21, COL8_FFFFFF);
}


void putChar(char *addr,int x,int y,char col,unsigned char *pch,int screenWidth){
	
	for(int i=0;i<16;i++){
		char ch = pch[i];
		int off = (y+i)*screenWidth;
		
		//显示的字形最左边的是低地址,右侧的是高地址。例如:0x80,则高地址部分显示在内存的低地址,
		//最低位的应该偏移7
		if((ch & 0x01) != 0){
			addr[off+x+7] = col;
		}
		if((ch & 0x02) != 0){
			addr[off+x+6] = col;
		}
		if((ch & 0x04) != 0){
			addr[off+x+5] = col;
		}
		if((ch & 0x08) != 0){
			addr[off+x+4] = col;
		}	
		if((ch & 0x10) != 0){
			addr[off+x+3] = col;
		}
		if((ch & 0x20) != 0){
			addr[off+x+2] = col;
		}
		if((ch & 0x40) != 0){
			addr[off+x+1] = col;
		}	
		if((ch & 0x80) != 0){
			addr[off+x+0] = col;
		}
	}
}


void init_mouse_cursor(char *vram,int x,int y,char bc){
	//16*16 Mouse 
    //鼠标指针点阵
	static char cursor[16][16] = {
	 "*...............",
	 "**..............",
	 "*O*.............",
	 "*OO*............",
	 "*OOO*...........",
	 "*OOOO*..........",
	 "*OOOOO*.........",
	 "*OOOOOO*........",
	 "*OOOOOOO*.......",
	 "*OOOO*****......",
	 "*OO*O*..........",
	 "*O*.*O*.........",
	 "**..*O*.........",
	 "*....*O*........",
	 ".....*O*........",
	 "......*........."
	};
	
	for (int i = 0; i < 16; i++) {
		for (int j = 0; j < 16; j++) {
			int off = (i+y)*SCREEN_WIDTH+x+j;
			if (cursor[i][j] == '*') {
				vram[off] = COL8_000000;
			}
			if (cursor[i][j] == 'O') {
				vram[off] = COL8_FFFFFF;
			}
			if (cursor[i][j] == '.') {
				vram[off] = bc;
			}
		}
	}

}

/*
 *8259A中断调用
 *
 */
void int_8259A(char *index){

	unsigned char *ascii = ascii_array;
	
	for(int i=0x20;i<=0x7f;i++){
		int x = (i-0x20)%32*10;
		int y = (i-0x20)/32*20;
		putChar((char *)0xa0000,x,y,COL8_FFFFFF,ascii+(i-0x20)*16,SCREEN_WIDTH);
		
	}
}

3.生成floppy.img 文件,加载运行结果如下:
11.建立中断机制_第2张图片
点击键盘任意按键如下图:
11.建立中断机制_第3张图片
至此,我们编写的键盘中断机制完成

你可能感兴趣的:(自制操作系统)