| 关键词: 端口 IDT 键盘 指令 一个 nbsp 寄存器 偏移 处理 port |
在我之前的文章 让我们编写一个内核 ,讲述了如何搭建一个初级的x86内核,该内核使用GRUB启动,运行于保护模式,能在屏幕上输出字符串。 今天,我将为该内核添加键盘驱动,这样它能够获取键盘上a-z和0-9字符输入并且输出到屏幕。 本文使用的源代码可以在我的 GirHub仓库-mkeykernel 中获取。 我们使用I/O端口来与I/O设备交互。这些端口只是x86 I/O总线上的具体地址,仅此而已。对该端口的读/写操作通过处理器内建的特定指令完成。 端口读写read_port: mov edx, [esp + 4] in al, dx ret write_port: mov edx, [esp + 4] mov al, [esp + 4 + 4] out dx, al ret I/O端口的访问使用x86指令集中的in和out指令。 在read_port中,端口号被视为参数。当编译器调用你的函数时,它将所有的参数压入栈中。参数使用栈指针复制到寄存器edx中。寄存器dx是edx的低16位。这里的in指令读取dx指定端口并将结果输出到al中。寄存器al是eax的低8位。如果你还记得大学课程,函数的返回值存放于eax寄存器中。因此,read_port可以读取I/O端口数据。 write_port非常类似。这里有两个参数:端口号和待写入数据。out指令将数据写入端口中。 中断在我们开始编写任何设备驱动前,我们需要了解处理器是如何知道设备执行了一个事件。 最简单的方法是 轮询 —始终保持对设备状态的监测。这样显然效率太低且不实用。因此 中断 机制被引入。中断是硬件或软件发送给处理器的一个信号,代表一个事件。使用中断,我们可以避免轮询,仅当我们关注的指定中断触发时才响应。 可编程中断控制器(PIC)器件或芯片确保x86成为支持中断驱动的架构。该器件或芯片负责管理硬件中断并将中断发送至相应系统中断。 当硬件设备上执行指定操作时,它会向与PIC芯片连接的指定中断引脚上发送一个中断请求(IRQ) 脉冲。接着,PIC将接收到的IRQ转换成系统中断,并发送消息中止CPU当前执行的任何工作。接下来,由内核负责处理这些中断。 如果没有PIC,我们就必须轮询系统中的所有设备来查看它们中是否有事件发生。 下面以键盘为例。键盘依靠0×60和0×64 I/O端口工作。端口0×60输出数据(按了什么键),端口0×64输出状态。但是,你必须确切知道什么时候去读取这些端口。 此处使用中断很简单。当按下一个键时,键盘会在IRQ1中断线上发送一个信号给PIC。PIC在初始化期间存储了一个偏移量。该设备将输入线号叠加偏移量就形成 中断号。接着,处理器查询称为 中断描述符表(IDT) 的特定数据结构给出此中断号相对应的中断处理程序入口地址。 接着,上述地址的事件处理代码会被执行。 建立IDT表struct IDT_entry{ unsigned short int offset_lowerbits; unsigned short int selector; unsigned char zero; unsigned char type_attr; unsigned short int offset_higherbits; }; struct IDT_entry IDT[IDT_SIZE]; void idt_init(void) { unsigned long keyboard_address; unsigned long idt_address; unsigned long idt_ptr[2]; /* populate IDT entry of keyboard's interrupt */ keyboard_address = (unsigned long)keyboard_handler; IDT[0x21].offset_lowerbits = keyboard_address & 0xffff; IDT[0x21].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ IDT[0x21].zero = 0; IDT[0x21].type_attr = 0x8e; /* INTERRUPT_GATE */ IDT[0x21].offset_higherbits = (keyboard_address & 0xffff0000) >> 16; /* Ports * PIC1 PIC2 *Command 0x20 0xA0 *Data 0x21 0xA1 */ /* ICW1 - begin initialization */ write_port(0x20 , 0x11); write_port(0xA0 , 0x11); /* ICW2 - remap offset address of IDT */ /* * In x86 protected mode, we have to remap the PICs beyond 0x20 because * Intel have designated the first 32 interrupts as "reserved" for cpu exceptions */ write_port(0x21 , 0x20); write_port(0xA1 , 0x28); /* ICW3 - setup cascading */ write_port(0x21 , 0x00); write_port(0xA1 , 0x00); /* ICW4 - environment info */ write_port(0x21 , 0x01); write_port(0xA1 , 0x01); /* Initialization finished */ /* mask interrupts */ write_port(0x21 , 0xff); write_port(0xA1 , 0xff); /* fill the IDT descriptor */ idt_address = (unsigned long)IDT ; idt_ptr[0] = (sizeof (struct IDT_entry) * IDT_SIZE) + ((idt_address & 0xffff) << 16); idt_ptr[1] = idt_address >> 16 ; load_idt(idt_ptr); } IDT依靠IDT_entry组成的结构体数组实现。文章稍后会讨论键盘中断是如何映射到中断处理程序。首先,我们来了解PIC是如何工作的。 现在的x86系统有2块PIC芯片,每一块有 8条输入线 。我们称其为PIC1和PIC2。PIC1接收IRQ0到IRQ7,PIC2接收IRQ8至IRQ15。PIC1使用0×20作为命令端口,0×21作为数据。PIC2使用0xA0作为命令端口,0xA1作为数据端口。 PIC初始化使用8位命令字,该命令字叫做 初始化命令字(ICW) 。这些命令字的具体每一位语法参考 此链接 。 在保护模式下,你需要发送给这两个PIC的第一条命令是初始化命令 ICW1 (0×11)。该指令让PIC等待数据端口的其他3条初始化字。 这些指令告诉PIC以下信息: *它的偏移向量。(ICW2) *PIC是如何作为主/从设备的。(ICW3) *给出环境相关的附加信息。(ICW4) 第二条初始化指令是ICW2,该指令会被写入每一个PIC的数据端口。该指令设置PIC的偏移量。该偏移量和输入线的数据相加可以得到中断号。 PIC允许彼此之间输出到输入的级联。级联的建立使用ICW3,每一位代表相应IRQ的级联状态。至于现在,我们不使用级联并且所有位均设为0。 ICW4设置附加环境参数。我们仅设置大部分低位来告诉PIC我们运行于80×86模式。 Tang ta dang!! PIC初始化完成。 每一个PIC内部有一个8位寄存器叫做 中断屏蔽寄存器(IMR) 。该寄存器中存放进入PIC的IRQ线的位图。当一位被置位时,PIC会忽略相应的请求。这意味着我们可以通过设置IMR中的数值的第n位为0或1来启用和禁止第n跳IRQ线。从数据端口读取的数据返回值存放在IMR寄存器中,向其中写入可设置寄存器。此处我们的代码中,在PIC初始化后,我们设置所有位为1,因此所有IRQ线都被禁用。我们稍后会启用键盘中断对应的线。至于现在,我们禁用所有的中断!! 假设IRQ线开启,PIC可以通过IRQ线接收信号,并叠加偏移量转换成中断号。现在,我们需要填写IDT,这样键盘的中断号才能映射到我们编写的键盘处理程序的地址。 那么键盘处理函数地址应该与IDT中哪一个中断号映射? 键盘使用IRQ1。IRQ1是PIC1的输入线。我们已经将PIC1的偏移量初始化为0×20(参见ICW2)。为查找中断号,将1加上偏移量0×20,即0×21。所以在IDT中,键盘终端处理程序地址必须映射到0×21号中断。 那么,接下来的任务就是填写IDT中的0×21中断。 我们要将该中断映射到我们在汇编文件中编写的键盘处理函数。 每一个IDT条目由64位组成。在IDT中断条目中,我们没有将中断处理程序地址作为一个整体存储。我们把它分成两个16位部分。低16位存储在IDT条目的前16位,高16位存储在IDT条目的最后16位。这样做的目的是为了保持与286兼容。你可以在很多地方看到Intel类似的巧妙设计!! 在IDT条目中,我们还必须设置类型——这样做是为了捕获中断。我们还需给出内核代码段偏移量。GRUB引导为我们建立一个 GDT 。每一项GDT条目占8个字节,内核代码描述符是第二个段;所以它的偏移量是0×08(更多相关描述对本文过于冗余)。中断门由0x8e表示。中间剩余8位必须全部填充为0。这样,我们就填充了与键盘中断相对应的IDT条目。 一旦IDT中需要映射完成,我们需要告诉CPU IDT的位置。 该操作通过lidt汇编指令完成。lidt指令有一个操作数。该操作数必须是一个指向描述IDT描述符结构的指针。 该描述符十分简单。它包含了IDT的字节空间和地址。我使用了一个数组来打包该数值。你也可以使用一个结构体来填写该数值。 我们在变量idt_ptr中存放了指针,然后使用load_idt()函数传递指针给lidt指令。 load_idt: mov edx, [esp + 4] lidt [edx] sti ret 此外,load_idt()函数使用sti指令启用中断。 一旦IDT被建立并加载,我们可以使用前面讨论过的中断屏蔽启用键盘的IRQ线。 void kb_init(void) { /* 0xFD is 11111101 - enables only IRQ1 (keyboard)*/ write_port(0x21 , 0xFD); } 键盘终端处理函数既然我们已经通过IDT的0×21中断条目成功将键盘中断映射到键盘处理函数。 那么,每次你按下键盘上的一个键,键盘处理函数必定会被调用。 keyboard_handler: call keyboard_handler_main iretd 键盘处理函数仅仅是调用了另一个C函数并使用iret类指令返回。我们本可以在此处编写整个中断处理程序,但是相对汇编而言编写C代码要更容易——所以这里采用函数调用。 当从中断处理程序返回中断之前的程序时,应该使用iret/iretd替换ret指令。这些指令会将中断调用前入栈的标志寄存器值出栈。 void keyboard_handler_main(void) { unsigned char status; char keycode; /* write EOI */ write_port(0x20, 0x20); status = read_port(KEYBOARD_STATUS_PORT); /* Lowest bit of status will be set if buffer is not empty */ if (status & 0x01) { keycode = read_port(KEYBOARD_DATA_PORT); if(keycode < 0) return; vidptr[current_loc++] = keyboard_map[keycode]; vidptr[current_loc++] = 0x07; } } 首先,我们通过向PIC通用端口写入指令发送EOI信号。只有这样,PIC才允许更多的中断请求。这里我们必须读取两个端口——0×60数据端口和0×64命令/状态端口。 我们首先读取0×64端口获取状态。如果状态的最低位是0,这意味着缓冲区是空的,没有可读取数据。否则,我们要读取0×60数据端口。该端口将给出我们按下的键盘键码。每一个键码对应于键盘上的一个按键。keyboard_map.h头中定义了一个简单的字符数组来完成键码到对应字符的映射。该字符在屏幕上的输出与之前那篇文章中使用的技术相同。 在本篇文章中,为了简洁,我仅仅处理了小写字母a-z和数字0-9。你可以轻松扩展到其他特殊字符,ALT,SHIFT,CAPS LOCK。你可以根据状态端口输出知道按键是否被按下或释放,并执行所需操作。你也可以将任意按键组合映射到特定功能,例如关机等。 你可以编译内核,在实际机器或虚拟机(QEMU)上运行,和 之前的文章 ( 内核仓库)一样。 开始输入!! |
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|