第一部分:keyboard.h
首先,下面是头文件保护机制,防止同一个头文件被多次包含,避免编译错误。
#ifndef __KEYBOARD_H__
#define __KEYBOARD_H__
当然,在最后也要写上#endif。
然后要引入头文件:
type.h 提供了基本类型定义(如uint8_t、uint32_t等)。
port.h 用于与硬件I/O端口通信。
interrupt.h 提供了中断管理的基类和相关支持函数,下面出现的KeyboardDriver是interrupt.h中的InterruptHandler的子类。
#include "types.h"
#include "port.h"
#include "interrupts.h"
1. KeyboardEventHandler类
这是一个事件处理基类,定义了键盘按下和释放的事件接口。
键盘按下的时候如果是图形界面,没有输入的窗口,应该是什么都不做的。如果是在字符可以输入的地方按下键盘,是要在屏幕上输出东西的。
在不同情况下要有不同的键盘处理程序,为此设计专门处理这种情况的一个类。因此输出的功能不是通过下面的KeyboardDriver直接实现,而是通过另外的类中(指KeyboardEventHandler类)的handler来实现。
OnKeyDown和OnKeyUp是KeyboardEventHandler类中的虚函数,作用是为键盘按键的按下事件和松开事件提供处理。
OnKeyDown:
当键盘按下某个按键时,就会触发这个函数。处理键值(字符或控制键)输入,可能会涉及到:输出到屏幕,执行特定操作(如快捷键功能),在应用层面传递事件(如输入命令)。
OnKeyUp:
当键盘松开某个按键时,触发这个函数。处理按键释放的相关逻辑,例如:清楚某个状态(如松开Shift),执行某些释放时的操作(如松开一个功能键)。
在这次的实现中,实际上没有实现这么多的功能,只实现了OnKeyDown中,输出到屏幕的一个功能。这两个函数的具体的实现应该是在派生类里面实现,具体实现在kernel.cpp中。
那这里为什么不用纯虚函数呢?
纯虚函数需要依赖完整的 C++ 运行时支持,但在操作系统的早期开发阶段,通常没有完整的 C++ 运行时环境,因此不适合使用纯虚函数。通过空实现(即函数体为空),可以避免这个问题,同时保留派生类覆盖接口的灵活性。
完整代码:
class KeyboardEventHandler
{
public:
KeyboardEventHandler();
virtual void OnKeyDown(char){ }
virtual void OnKeyUp(char) { }
};
2. KeyboardDriver类
这是一个键盘驱动类,是InterruptHandler的子类,在这个类中实现键盘驱动逻辑,负责硬件通信和中断处理。
(1) 私有成员
首先,dataPort和commandPort是通信端口,我们是要和键盘通信,所以需要这些端口。
dataPort用于从键盘读取按键扫描码。
commandPort用于向键盘发送命令(例如初始化或改变键盘行为)。
另外,还要保存传入的KeyboardEventHandler指针,用于调用事件处理函数。
完整代码:
private:
Port8Bit dataPort;
Port8Bit commandPort;
KeyboardEventHandler * handler;
(2) 成员函数
首先要写一下构造函数和析构函数。
构造函数接受一个InterruptManager对象,用于注册键盘中断处理程序。通过KeyboardEventHandler * handler,关联键盘事件的具体处理逻辑。当然,构造函数初始化时还要初始化端口dataPort和commandPort。
下一个时HandleInterrupt函数,这是InterruptHandler类中的虚函数。因此需要在派生类中重写InterruptHandler的虚函数,用于处理键盘中断。每次中断发生时,会读取键盘数据,并通过KeyboardEventHandler调用OnKeyDown或OnKeyUp方法。
最后Active函数用于激活键盘中断。即配置键盘控制器,开启键盘中断。
完整代码:
public:
KeyboardDriver(InterruptManager * manager, KeyboardEventHandler * handler);
~KeyboardDriver();
uint32_t HandleInterrupt(uint32_t esp);
void Activate();
3. 完整代码
#ifndef __KEYBOARD_H__
#define __KEYBOARD_H__
#include "types.h"
#include "port.h"
#include "interrupts.h"
class KeyboardEventHandler
{
public:
KeyboardEventHandler();
virtual void OnKeyDown(char){ }
virtual void OnKeyUp(char) { }
};
class KeyboardDriver : public InterruptHandler
{
public:
KeyboardDriver(InterruptManager * manager, KeyboardEventHandler * handler);
~KeyboardDriver();
uint32_t HandleInterrupt(uint32_t esp);
void Activate();
private:
Port8Bit dataPort;
Port8Bit commandPort;
KeyboardEventHandler * handler;
};
#endif
第二部分:keyboard.cpp
首先,引入头文件和依赖函数的声明。
用于调试目的,在HandleInterrupt函数中打印扫描码和其他信息。
printf实现屏幕输出,printfHex用于以十六进制打印数据。
#include "keyboard.h"
void printf(const char *);
void printfHex(const uint8_t);
1. KeyboardEventHandler的构造函数
提供一个空的默认构造函数。因为KeyboardEventHandler是基类,需要派生类继承并重写OnKeyDown和OnKeyUp方法。
KeyboardEventHandler::KeyboardEventHandler() { }
2. KeyboardDriver的构造函数
首先,InterruptHandler有两个参数,(uint8_t interruptNumber, InterruptManager *interruptManager)。
第二句的InterruptHandler(0x21, manager)就是初始化了基类,因为是键盘中断,所以中断号就是0x21。即,0x21是键盘中断号,将键盘中断处理程序注册到InterruptManager。
另外还要初始化dataPort和commandPort。dataPort的端口号是0x60,用于读取扫描码和写入键盘命令;commandPort的端口号是0x64,用于发送命令到键盘控制器(8042)。
因为InterruptHandle没有默认构造函数,必须显式调用带参数的构造函数。
完整代码如下:
KeyboardDriver::KeyboardDriver(InterruptManager * manager, KeyboardEventHandler * handler)
: InterruptHandler(0x21, manager), dataPort(0x60), commandPort(0x64), handler(handler)
{
}
3. KeyboardDriver的析构函数
KeyboardDriver::~KeyboardDriver()
{
//析构函数不需要特殊的实现
}
4. HandleInterrupt函数
首先,要读取按键扫描码。因为我们按了键盘就会产生中断,这个键盘按了之后会向键盘控制器上输出了一个字符,因此我们需要将他读出来他才会继续产生下一个中断。(下一次按键盘的时候才会产生中断)
这里,调用dataPort.Read() 从0x60 端口获取按键扫描码。这一步非常关键,因为键盘中断发生时,如果不读取扫描码,键盘控制器会阻止后续按键事件。
uint8_t key = dataPort.Read();
因为上面已经写过KeyboardDriver类的构造函数,进行了初始化,因此正常在调用HandleInterrupt函数时,handler应该是初始化的状态,不能是空指针。如果handle没有被正确初始化,直接调用handle->OnKeyDown() 会导致未定义行为,通常是程序崩溃或异常。因此要进行检查,避免这个问题。
if(handler == 0)
return esp;
接着,来处理扫描码。其中,还对大写字母和小写字母进行了区分。在这里,定义bool变量来记录shift是按下还是抬起。按了shift就可以输出大写字母了。
static bool shift = false;
根据读到的扫描码来判断输入的字符是什么,接着继续来判断是否按下shift键,根据这两个判断条件来输出一个字符(区分大小写)。这里,调用handle -> OnKeyDown 通知按键事件。
在这里,对Shift键进行补充说明:
按下:0x2A(左Shift),0x36(右Shift)
松开:0xAA(左Shift),0xB6(右Shift)
当按键松开时,键盘会发送一个断码,其值为按下时的扫描码加0x80。因为我只想输出我输入的字符或暂未处理的其他字符的扫描码,并不想看到断码,因此我最后做了一个判断。0x80是断码的起始值,因此,如果小于0x80才会输出这个字符。
switch(key)
{
case 0x1E: if(shift) handler->OnKeyDown('A'); else handler->OnKeyDown('a'); break;
case 0x30: if(shift) handler->OnKeyDown('B'); else handler->OnKeyDown('b'); break;
case 0x2E: if(shift) handler->OnKeyDown('C'); else handler->OnKeyDown('c'); break;
case 0x20: if(shift) handler->OnKeyDown('D'); else handler->OnKeyDown('d'); break;
case 0x12: if(shift) handler->OnKeyDown('E'); else handler->OnKeyDown('e'); break;
case 0x21: if(shift) handler->OnKeyDown('F'); else handler->OnKeyDown('f'); break;
case 0x22: if(shift) handler->OnKeyDown('G'); else handler->OnKeyDown('g'); break;
case 0x23: if(shift) handler->OnKeyDown('H'); else handler->OnKeyDown('h'); break;
case 0x17: if(shift) handler->OnKeyDown('I'); else handler->OnKeyDown('i'); break;
case 0x24: if(shift) handler->OnKeyDown('J'); else handler->OnKeyDown('j'); break;
case 0x25: if(shift) handler->OnKeyDown('K'); else handler->OnKeyDown('k'); break;
case 0x26: if(shift) handler->OnKeyDown('L'); else handler->OnKeyDown('l'); break;
case 0x32: if(shift) handler->OnKeyDown('M'); else handler->OnKeyDown('m'); break;
case 0x31: if(shift) handler->OnKeyDown('N'); else handler->OnKeyDown('n'); break;
case 0x18: if(shift) handler->OnKeyDown('O'); else handler->OnKeyDown('o'); break;
case 0x19: if(shift) handler->OnKeyDown('P'); else handler->OnKeyDown('p'); break;
case 0x10: if(shift) handler->OnKeyDown('Q'); else handler->OnKeyDown('q'); break;
case 0x13: if(shift) handler->OnKeyDown('R'); else handler->OnKeyDown('r'); break;
case 0x1F: if(shift) handler->OnKeyDown('S'); else handler->OnKeyDown('s'); break;
case 0x14: if(shift) handler->OnKeyDown('T'); else handler->OnKeyDown('t'); break;
case 0x16: if(shift) handler->OnKeyDown('U'); else handler->OnKeyDown('u'); break;
case 0x2F: if(shift) handler->OnKeyDown('V'); else handler->OnKeyDown('v'); break;
case 0x11: if(shift) handler->OnKeyDown('W'); else handler->OnKeyDown('w'); break;
case 0x2D: if(shift) handler->OnKeyDown('X'); else handler->OnKeyDown('x'); break;
case 0x15: if(shift) handler->OnKeyDown('Y'); else handler->OnKeyDown('y'); break;
case 0x2C: if(shift) handler->OnKeyDown('Z'); else handler->OnKeyDown('z'); break;
case 0x2A: case 0x36: shift = true; break;
case 0xAA: case 0xB6: shift = false; break;
default:
if(key < 0x80)
{
printf(" keyboard0x");
printfHex(key);
}
break;
}
5. Activate() 函数
下面来写一下激活键盘中断函数。
commandPort是键盘控制器的命令端口,地址是0x64。
通过读取此端口,可以获取控制器的状态字节。
下面简单说明一下状态字节:
位0 :指示键盘缓冲区是否有数据可读(1 表示有数据)
位1 :指示键盘控制器是否忙碌(1 表示忙)
因此,commandPort.Read() & 0x1 就是检查状态字节的最低位。如果为1,表示缓冲区中有数据需要处理。
dataPort是键盘的数据端口,地址是0x60。dataPort.Read() 表示从缓冲区中读取一个字节,通常是键盘的输出数据或控制器的返回值。
这样做一个循环,就是为了清空缓冲区,确保启动时键盘控制器没有遗留的历史数据,避免后续操作受到影响。
while(commandPort.Read( ) & 0x1)
dataPort.Read();
下面通过commandPort来发送0xAE命令,0xAE命令就是打开键盘中断。
详细说明就是,commadPort.Write(0xAE)就是向命令端口0x64写入0xAE。其中0xAE的含义就是激活键盘中断,使 PS/2 键盘可以产生中断信号(IRQ1)。在未执行此命令之前,键盘即使接收到输入信号,也不会触发中断。
commandPort.Write(0xAE);
下面,向命令都端口0x64写入0x20。这个0x20的含义就是读取键盘控制器的状态字节到数据端口0x60。这个状态字节包含控制器的配置信息,例如键盘中断是否启用、时钟是否激活等。
commandPort.Write(0x20);
接着,通过dataPort.Read() 来读取commandPort.Write(0x20)指令后返回的状态字节,这一步获取当前控制器的状态信息。
并且,将这个状态字节进行修改。
|0x01表示将状态字节的最低位设置改为1,表示启用键盘中断。
&~0x10表示清楚第4位(位4表示禁用键盘时钟),确保键盘时钟是启用的。
最后将更新后的键盘控制器配置状态保存到status。
uint8_t status = (dataPort.Read() | 0x01) & ~0x10;
下面,向命令端口0x64写入0x60。这个命令的含义是:设置键盘控制器的状态字节。接下来的一个字节(通过数据端口写入)会更新控制器的配置。就是说,一个通过数据端口写入的字节就是状态。
commandPort.Write(0x60);
向数据端口0x60写入更新后的状态字节,将status应用到控制器中,启用键盘中断并打开键盘时钟。
dataPort.Write(status);
向数据端口0x60写入0xF4,这个命令的含义是启用键盘。在发送此命令后,键盘开始接受输入并扫描按键,使键盘开始正常工作。
dataPort.Write(0xF4);
6. 完整代码
#include "keyboard.h"
void printf(const char *);
void printfHex(const uint8_t);
KeyboardEventHandler::KeyboardEventHandler() { }
KeyboardDriver::KeyboardDriver(InterruptManager * manager, KeyboardEventHandler * handler)
: InterruptHandler(0x21, manager), dataPort(0x60), commandPort(0x64), handler(handler)
{
}
KeyboardDriver::~KeyboardDriver()
{
//析构函数不需要特殊的实现
}
uint32_t KeyboardDriver::HandleInterrupt(uint32_t esp)
{
uint8_t key = dataPort.Read();
if(handler == 0)
return esp;
static bool shift = false;
switch(key)
{
case 0x1E: if(shift) handler->OnKeyDown('A'); else handler->OnKeyDown('a'); break;
case 0x30: if(shift) handler->OnKeyDown('B'); else handler->OnKeyDown('b'); break;
case 0x2E: if(shift) handler->OnKeyDown('C'); else handler->OnKeyDown('c'); break;
case 0x20: if(shift) handler->OnKeyDown('D'); else handler->OnKeyDown('d'); break;
case 0x12: if(shift) handler->OnKeyDown('E'); else handler->OnKeyDown('e'); break;
case 0x21: if(shift) handler->OnKeyDown('F'); else handler->OnKeyDown('f'); break;
case 0x22: if(shift) handler->OnKeyDown('G'); else handler->OnKeyDown('g'); break;
case 0x23: if(shift) handler->OnKeyDown('H'); else handler->OnKeyDown('h'); break;
case 0x17: if(shift) handler->OnKeyDown('I'); else handler->OnKeyDown('i'); break;
case 0x24: if(shift) handler->OnKeyDown('J'); else handler->OnKeyDown('j'); break;
case 0x25: if(shift) handler->OnKeyDown('K'); else handler->OnKeyDown('k'); break;
case 0x26: if(shift) handler->OnKeyDown('L'); else handler->OnKeyDown('l'); break;
case 0x32: if(shift) handler->OnKeyDown('M'); else handler->OnKeyDown('m'); break;
case 0x31: if(shift) handler->OnKeyDown('N'); else handler->OnKeyDown('n'); break;
case 0x18: if(shift) handler->OnKeyDown('O'); else handler->OnKeyDown('o'); break;
case 0x19: if(shift) handler->OnKeyDown('P'); else handler->OnKeyDown('p'); break;
case 0x10: if(shift) handler->OnKeyDown('Q'); else handler->OnKeyDown('q'); break;
case 0x13: if(shift) handler->OnKeyDown('R'); else handler->OnKeyDown('r'); break;
case 0x1F: if(shift) handler->OnKeyDown('S'); else handler->OnKeyDown('s'); break;
case 0x14: if(shift) handler->OnKeyDown('T'); else handler->OnKeyDown('t'); break;
case 0x16: if(shift) handler->OnKeyDown('U'); else handler->OnKeyDown('u'); break;
case 0x2F: if(shift) handler->OnKeyDown('V'); else handler->OnKeyDown('v'); break;
case 0x11: if(shift) handler->OnKeyDown('W'); else handler->OnKeyDown('w'); break;
case 0x2D: if(shift) handler->OnKeyDown('X'); else handler->OnKeyDown('x'); break;
case 0x15: if(shift) handler->OnKeyDown('Y'); else handler->OnKeyDown('y'); break;
case 0x2C: if(shift) handler->OnKeyDown('Z'); else handler->OnKeyDown('z'); break;
case 0x2A: case 0x36: shift = true; break;
case 0xAA: case 0xB6: shift = false; break;
default:
if(key < 0x80)
{
printf(" keyboard0x");
printfHex(key);
}
break;
}
return esp;
}
void KeyboardDriver::Activate()
{
while(commandPort.Read( ) & 0x1)
dataPort.Read();
commandPort.Write(0xAE);
commandPort.Write(0x20);
uint8_t status = (dataPort.Read() | 0x01) & ~0x10;
commandPort.Write(0x60);
dataPort.Write(status);
dataPort.Write(0xF4);
}
第三部分:kernel.cpp
1. printf函数
不多做解释,就是在 VGA 文本模式下输出字符串到屏幕。
void printf(const int8_t * str)
{
static int16_t* VideoMemory = (short*)0xB8000;
static int8_t x = 0, y = 0;
for(int i = 0; str[i] != '\0'; ++i)
{
switch(str[i])
{
case '\n':
++y;
x = 0;
break;
default:
VideoMemory[80 * y + x] = (VideoMemory[80 * y + x] & 0xFF00) | str[i];
++x;
break;
}
if(x >= 80)
{
++y;
x = 0;
}
if(y >= 25)
{
for(y = 0; y < 25; ++y)
for(x = 0; x<80; ++x)
VideoMemory[80 * y + x] = (VideoMemory[80 * y + x] & 0xFF00) | str[i];
x = 0;
y = 0;
}
}
}
2. printfHex函数
这个函数的功能是:以十六进制格式输出单字节数字。
首先,将num的值复制到本地变量c。
然后声明一个静态字符数组msg,用于存储最终的十六进制字符串。
msg[0] 存储十六进制数的高位字符,msg[1] 存储十六进制数的低位字符,msg[2] 用于字符串的结束符 '\0'。
初始化只会执行一次,因此msg[3] 默认被初始化为{ '\0', '\0', '\0'}。
之后定义一个字符表hex,用于映射数字到十六进制字符。(比如,0对应 '0',1对应 '1',10 对应 'A' ,15对应'F' )通过数组索引的方式,可以快速获取任何一个十六进制字符。
下面来计算高位字符msg[0]。
c>>4:将c的二进制值右移4位,提取高4位。
&0xF:使用按位与操作,保留右移后的最低4位。
hex[ ]:使用计算出的高4位作为索引,从字符表中获取对应的十六进制字符。
最后,msg[0] 中保存的是高 4 位对应的十六进制字符。
计算低位字符msg[1]就比较简单,直接将c按位与操作,提取c的低4位后,使用低 4 位的值作为索引,从字符表中获取对应的十六进制字符就饿可以了。这样,msg[1]中保存的就是低 4 位对应的十六进制字符。
void printfHex(const uint8_t num)
{
uint8_t c = num;
static char msg[3] = {'0'};
const char * hex = "0123456789ABCDEF";
msg[0] = hex[(c >> 4) & 0xF];
msg[1] = hex[c & 0xF];
msg[2] = '\0';
printf(msg);
}
3. KeyboardEventHandler函数
这是在keyboard.h中,KeyboardEventHandler类。其中有写过虚函数:virtual void OnKeyDown(char){ },具体实现在这里。功能很简单,就是将输入的字符输出到屏幕。
class PrintfKeyboardEventHandler : public KeyboardEventHandler
{
public:
void OnKeyDown(char c)
{
char msg[2] = {'\0'};
msg[0] = c;
printf(msg);
}
};
4. 完整代码
其他的不多做解释,这里提供完整代码:
#include "types.h"
#include "gdt.h"
#include "interrupts.h"
#include "keyboard.h"
void printf(const int8_t * str)
{
static int16_t* VideoMemory = (short*)0xB8000;
static int8_t x = 0, y = 0;
for(int i = 0; str[i] != '\0'; ++i)
{
switch(str[i])
{
case '\n':
++y;
x = 0;
break;
default:
VideoMemory[80 * y + x] = (VideoMemory[80 * y + x] & 0xFF00) | str[i];
++x;
break;
}
if(x >= 80)
{
++y;
x = 0;
}
if(y >= 25)
{
for(y = 0; y < 25; ++y)
for(x = 0; x<80; ++x)
VideoMemory[80 * y + x] = (VideoMemory[80 * y + x] & 0xFF00) | str[i];
x = 0;
y = 0;
}
}
}
void printfHex(const uint8_t num)
{
uint8_t c = num;
static char msg[3] = {'0'};
const char * hex = "0123456789ABCDEF";
msg[0] = hex[(c >> 4) & 0xF];
msg[1] = hex[c & 0xF];
msg[2] = '\0';
printf(msg);
}
class PrintfKeyboardEventHandler : public KeyboardEventHandler
{
public:
void OnKeyDown(char c)
{
char msg[2] = {'\0'};
msg[0] = c;
printf(msg);
}
};
typedef void(*constructor)();
extern constructor start_ctors;
extern constructor end_ctors;
extern "C" void callConstructors()
{
for(constructor * i = &start_ctors; i != &end_ctors; ++i)
(*i)();
}
extern "C" void kernelMain(void * multiboo_structure, int32_t magicnumber)
{
printf("Hello MyOS world!\n");
printf("Hello MyOS world!\n");
GlobalDescriptorTable gdt;
InterruptManager interrupts(&gdt);
PrintfKeyboardEventHandler kbhandler;
KeyboardDriver keyboard(&interrupts, &kbhandler);
keyboard.Activate();
interrupts.Activate();
while(1);
}