操作系统——实现简单的键盘驱动

第一部分: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);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值