VGA 设定 首先简单看待调色板设定的相关程序,具体可以参考下面的链接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void set_palette (int start, int end, unsigned char *rgb) { int eflags = io_load_eflags(); io_cli(); io_out8(0x03c8 , start); for (int i = start; i <= end; i++) { io_out8(0x03c9 , rgb[0 ] / 4 ); io_out8(0x03c9 , rgb[1 ] / 4 ); io_out8(0x03c9 , rgb[2 ] / 4 ); rgb += 3 ; } io_store_eflags(eflags); }
GDT&IDT GDT “global segment descriptor table”,全局段号记录表。将段号记录在内存的某个地方,然后将内存的起始地址和有效设定个数放在CPU的GDTR的(global segment descriptor table register)特殊寄存器中。段寄存器是16位,但是由于cpu设定原因,低3位不可用,所以真正可以使用的位数为13位,即可以分为8192个段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #define ADR_GDT 0x00270000 #define LIMIT_GDT 0x0000ffff #define ADR_BOOTPACK 0x00280000 #define LIMIT_BOOTPACK 0x0007ffff #define AR_DATA32_RW 0x4092 #define AR_CODE32_ER 0x409a struct SegmentDescriptor *gdt = (struct SegmentDescriptor *)ADR_GDT; struct SegmentDescriptor { short limit_low, base_low; char base_mid, access_right; char limit_high, base_high; }; for (int i = 0 ; i <= LIMIT_GDT / 8 ; i++) { set_segmdesc(gdt + i, 0 , 0 , 0 ); } set_segmdesc(gdt + 1 , 0xffffffff , 0x00000000 , AR_DATA32_RW); set_segmdesc(gdt + 2 , LIMIT_BOOTPACK, ADR_BOOTPACK, AR_CODE32_ER); load_gdtr(LIMIT_GDT, ADR_GDT);
这里设定了两个段,第一个段从0x00000000到0xffffffff,设定了cpu能管理内存的大小。第二个段从0x00270000到0x0007ffff共512KB,
函数的最后一个变量代表段属性(access right),因为12位段属性的高4放在limit high中,所以程序里有意把ar当作如下16位处理:
“xxxx0000xxxxxxx”。
ar的高4位称为扩展访问权,由“GD00”构成,G值得是Gbit,D指段模式,为1表示32位模式,为0表示16位模式。
ar低8位:
00000000(0x00):未使用的记录表
10010010(0x92):系统专用,可读写,不可执行
10011010(0x9a):系统专用,可执行,可读不可写
11110010(0xf2):应用程序用,可读写,不可执行
11111010(0xfa):应用程序用,可执行,可读不可写
CPU处于系统模式还是应用模式,取决于执行中的应用程序是位于访问权为0x9a的段,还是位于访问权为0xfa的段。
IDT “interrput descriptor table “,中断记录表。IDT记录了0-255的中断号码与调用函数的关系。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #define ADR_IDT 0x0026f800 #define LIMIT_IDT 0x000007ff #define AR_INTGATE32 0x008e struct GateDescriptor *idt = (struct GateDescriptor *)ADR_IDT; struct GateDescriptor { short offset_low, selector; char dw_count, access_right; short offset_high; }; for (int i = 0 ; i <= LIMIT_IDT / 8 ; i++) { set_gatedesc(idt + i , 0, 0, 0) ; } load_idtr(LIMIT_IDT, ADR_IDT) ; set_gatedesc(idt + 0x21, (int ) asm_int_handler21, 2 * 8 , AR_INTGATE32); set_gatedesc(idt + 0x27, (int ) asm_int_handler27, 2 * 8 , AR_INTGATE32); set_gatedesc(idt + 0x2c, (int ) asm_int_handler2c, 2 * 8 , AR_INTGATE32);
PIC “programmable interrupt controller”,可编程中断控制器。在设计上CPU只能处理一个中断,PIC是连接着CPU的芯片,它将8个中断信号(interrupt request)集合成一个中断信号。当有一个中断信号进来,就将唯一的输出管脚设置为ON,并且通知CPU。一般有两个PIC,从PIC通过2号IRQ与主PIC相连(硬件设计)。
PIC寄存器 PIC的寄存器都是8位的。
IMR:”interrupt mask register”,中断屏蔽寄存器,如果某一位的值是1,则该位对应的IRQ信号被屏蔽。
ICW:”initial control word”,初始化控制数据。ICW共有4个,即存储了4个字节的数据。ICW1和ICW4 和 PIC与主板的配线方式、中断信号的电气信号有关,因此这里的电脑设定程序是一个固定值。ICW3是有关主-从连接设定,因为硬件已经不可更改,所以软件设定也是一致的(00000100)。
我们可以编程的中断信号只有ICW2,ICW2决定了IRQ以哪一号中断通知CPU。
注:中断发生之后,CPU命令PIC发送两个字节的数据,PIC利用信号线发送 “0xcd 0x?”的两个数据,从CPU看来,就是执行“INT 0x?”的指令。
我们可以设定的中断为“0x20 - 0x2f”,16个中断信号:IRQ0-16,由上述PIC发送。其中“0x00 - 0x1f”是CPU内部中断,不可以和外部中断冲突。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void init_pic (void ) { io_out8(PIC0_IMR, 0xff ); io_out8(PIC1_IMR, 0xff ); io_out8(PIC0_ICW1, 0x11 ); io_out8(PIC0_ICW2, 0x20 ); io_out8(PIC0_ICW3, 1 << 2 ); io_out8(PIC0_ICW4, 0x01 ); io_out8(PIC1_ICW1, 0x11 ); io_out8(PIC1_ICW2, 0x28 ); io_out8(PIC1_ICW3, 2 ); io_out8(PIC1_ICW4, 0x01 ); io_out8(PIC0_IMR, 0xfb ); io_out8(PIC1_IMR, 0xff ); }
中断处理程序 鼠标处理为例,鼠标中断处理程序:
1 2 3 4 5 6 7 void int_handler2c (int *esp) { io_out8(PIC1_OCW2, 0x64 ); io_out8(PIC0_OCW2, 0x62 ); unsigned char data = io_in8(PORT_KEYDAT); fifo8_put(&mousefifo, data); }
IRQ-12是从PIC的4号,因为主/从PIC的协调不能自动完成,因此需要首先通知从PIC,再通知主PIC完成鼠标的中断处理。
这里使用一个队列来处理不同的中断,因为中断通知完成之后,会处理屏幕信息比较耗时,如果一直等待处理完成再打开中断的话会导致系统不连贯,以及不能从网上接受数据的情况。
中断处理完成之后必须使用IRETD指令,所以需要使用汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 asm_int_handler2c: PUSH ES PUSH DS PUSHAD MOV EAX , ESP PUSH EAX MOV AX , SS MOV DS , AX MOV ES , AX CALL int_handler2c POP EAX POPAD POP DS POP ES IRETD
键/鼠初始化 键盘初始化: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #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 ; } } } void init_keyboard (void ) { wait_KBC_sendready(); io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE); wait_KBC_sendready(); io_out8(PORT_KEYDAT, KBC_MODE); }
在初始化程序中,一边确认是否可以往键盘控制电路发送信息,一边发送模式设定指令。模式设定的指令是0x60,利用鼠标模式的模式号码是0x47.
键盘初始化: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #define KEYCMD_SENDTO_MOUSE 0xd4 #define MOUSECMD_ENABLE 0xf4 void enable_mouse(struct FIFO32 * fifo , int data0 , struct MouseDec * mdec ) { mousefifo = fifo; mousedata0 = data0; wait_KBC_sendready() ; io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE) ; wait_KBC_sendready() ; io_out8(PORT_KEYDAT, MOUSECMD_ENABLE) ; mdec->phase = 0 ; }
发送鼠标激活指令,归根结底还是要向键盘控制器发送指令。
如果往键盘控制电路发送指令0xd4,下一个数据会自动发送给鼠标。
收到鼠标激活指令后,会给CPU发送0xfa中断,因此最后一行为了区分第一个鼠标中断信号和后续的鼠标中断,在接收到0xfa后,鼠标的phase就不会再是0。