2022年6月

键盘操作

按照上一章的路线,我们将实现键盘操作。
实现键盘操作的核心是编写键盘驱动KeyboardDriver,它是一个InterruptRoutine的子类,对应了键盘操作的中断服务例程。
类中的核心方法是routine,它是中断服务例程的定义。
和键盘相关的最重要的硬件是两个芯片。一个是 intel 8042 芯片,位于主板上,CPU 通过 I/O 端口直接和这个芯片通信,获得按键的扫描码或者发送各种键盘命令。另一个是 intel 8048 芯片或者其兼容芯片,位于键盘中,这个芯片主要作用是从键盘的硬件中得到被按的键所产生的扫描码,与 8042 通信,控制键盘本身。8042有两个端口,驱动中0x60叫数据端口,0x64命令端口。

#define __KEYBOARD_H__

#include "types.h"
#include "interrupts.h"
#include "port.h"

class KeyboardDriver : public InterruptRoutine
{
public:
    KeyboardDriver(InterruptManager *manager);
    ~KeyboardDriver();

    virtual uint32_t routine(uint32_t esp) override;

private:
    Port8Bit dataPort;
    Port8Bit commandPort;
};

首先实现构造函数和析构函数。构造函数涉及到了键盘的硬件编程。
我们将实现键盘的中断服务例程,在列表初始化中需要构造基类对象,并初始化键盘控制芯片的端口。键盘的中断向量号为0x21,命令端口和数据端口已经提到。
这里是键盘端口的详尽描述。

#include "keyboard.h"

KeyboardDriver::KeyboardDriver(InterruptManager *manager)
    : InterruptRoutine(0x01 + manager->getOffset(), manager),
      dataPort(0x60),
      commandPort(0x64)
{
    // 如果commandPort的最低位是1,则清除输出缓冲区
    while (commandPort.read() & 0x1)
    {
        dataPort.read();
    }
    // 开启键盘
    commandPort.write(0xae);
    // 准备读取commandPort
    commandPort.write(0x20);
    uint8_t status = (dataPort.read() | 0x01) & (~0x10);
    // 准备进行写入操作
    commandPort.write(0x60);
    // 写入status
    dataPort.write(status);
    // 清空键盘输出缓冲区,可以继续扫描输入
    dataPort.write(0xf4);
}

KeyboardDriver::~KeyboardDriver()
{
}

键盘的中断服务例程是读取键盘的输入并显示在屏幕上,这需要对每一个按键进行读取判断。当按下shift键时,输出大写字母,使用一个变量标记shift是否被按下即可。每一个按键都有扫描码和断码。实际上按下一个按键时,扫描码和断码会分别触发一次中断,而我们只需要一次中断来打印一个字符,因此只检测扫描码。

#include "keyboard.h"



KeyBoardDriver::KeyBoardDriver(InterruptManager* manager) 
    : InterruptHandler(0x01 + manager->HardwareInterruptOffset(), manager),
    //0X01+中断号 = 0x21也就是键盘的中断号
    dataport(0x60),
    //键盘数据,读写端口:0X60
    commandport(0x64)
    {
    //控制端口 0x64
    //从0x64端口来判断低位数据
    while (commandport.Read() & 0x1)
    //输入池为空,这是用的指令端口的低位数据,来判断输入池的状态
     {
        dataport.Read();
    }
    commandport.Write(0xae);
    //表示我们写完了,可以读了,激活键盘,前面的while操作,一直在清空键盘的数据,对键盘的command进行写操作
    commandport.Write(0x20);
    //,键盘使用的8420芯片,准备读取8420芯片的Command数据
    uint8_t status = (dataport.Read() | 1) & ~0x10;
    //|1的目的是开启键盘中断,&~0x10就是在确保一定能够开启键盘中断,第四位置为0
    commandport.Write(0x60);
    //准备写入
    dataport.Write(status);
    dataport.Write(0xf4);
    //8042在主板上, 8048在键盘上,写完之后给键盘的芯片说,我们写完了。清空输出池,回一个ACK确认帧
    }

KeyBoardDriver::~KeyBoardDriver() {}

void printf(const char*);

uint32_t KeyBoardDriver::HandleInterrupt(uint32_t esp) {
    uint8_t key = dataport.Read();
    //从数据池中获得Key

    static bool shift = false;
    switch (key) {
     case 0x02: if (shift) printf("!"); else printf("1"); break;
    case 0x03: if (shift) printf("@"); else printf("2"); break;
    case 0x04: if (shift) printf("#"); else printf("3"); break;
    case 0x05: if (shift) printf("$"); else printf("4"); break;
    case 0x06: if (shift) printf("%"); else printf("5"); break;
    case 0x07: if (shift) printf("^"); else printf("6"); break;
    case 0x08: if (shift) printf("&"); else printf("7"); break;
    case 0x09: if (shift) printf("*"); else printf("8"); break;
    case 0x0A: if (shift) printf("("); else printf("9"); break;
    case 0x0B: if (shift) printf(")"); else printf("0"); break;

    case 0x10: if (shift) printf("Q"); else printf("q"); break;
    case 0x11: if (shift) printf("W"); else printf("w"); break;
    case 0x12: if (shift) printf("E"); else printf("e"); break;
    case 0x13: if (shift) printf("R"); else printf("r"); break;
    case 0x14: if (shift) printf("T"); else printf("t"); break;
    case 0x15: if (shift) printf("Y"); else printf("y"); break;
    case 0x16: if (shift) printf("U"); else printf("u"); break;
    case 0x17: if (shift) printf("I"); else printf("i"); break;
    case 0x18: if (shift) printf("O"); else printf("o"); break;
    case 0x19: if (shift) printf("P"); else printf("p"); break;

    case 0x1E: if (shift) printf("A"); else printf("a"); break;
    case 0x1F: if (shift) printf("S"); else printf("s"); break;
    case 0x20: if (shift) printf("D"); else printf("d"); break;
    case 0x21: if (shift) printf("F"); else printf("f"); break;
    case 0x22: if (shift) printf("G"); else printf("g"); break;
    case 0x23: if (shift) printf("H"); else printf("h"); break;
    case 0x24: if (shift) printf("J"); else printf("j"); break;
    case 0x25: if (shift) printf("K"); else printf("k"); break;
    case 0x26: if (shift) printf("L"); else printf("l"); break;

    case 0x2C: if (shift) printf("Z"); else printf("z"); break;
    case 0x2D: if (shift) printf("X"); else printf("x"); break;
    case 0x2E: if (shift) printf("C"); else printf("c"); break;
    case 0x2F: if (shift) printf("V"); else printf("v"); break;
    case 0x30: if (shift) printf("B"); else printf("b"); break;
    case 0x31: if (shift) printf("N"); else printf("n"); break;
    case 0x32: if (shift) printf("M"); else printf("m"); break;
    case 0x33: if (shift) printf("<"); else printf(","); break;
    case 0x34: if (shift) printf(">"); else printf("."); break;
    case 0x35: if (shift) printf("_"); else printf("-"); break;

    case 0x1C: printf("\n"); break;
    case 0x39: printf(" "); break;
    case 0x2A: case 0x36: shift = true; break;
    //按下shift
    case 0xAA: case 0xB6: shift = false; break;
    //弹起shift

    case 0x45: break; // NumLock键盘锁,不支持

    default:
        if (key < 0x80) {
            //为什么在这里要求<0x80呢,因为当按一个键的时候,其实是两次中断,一次按下的响应,一次抬起的相应
            //我们这里只处理按下去的一次
            char* foo = (char*)"KEYBOARD 0X00  ";
            const char* hex = "0123456789ABCDEF";
            foo[11] = hex[(key >> 4) & 0x0f];
            foo[12] = hex[key & 0x0f];
            printf((const char*)foo);
        }
    }
    
    return esp;
}

鼠标的私有成员和键盘略有不同,每次读取数据的时候读取的是一个数据流而不是一个扫描码。它有两种模式,分别是发送3字节流和4字节流,我们使用3字节流。简单来说,它包含了鼠标的位置(后2位)和状态信息(第1位),使用buffer[3]存储。然而触发中断时,只能读取一位信息,因此使用offset来记录当前读取的是哪一位的信息。除此之外,再使用一个buttons变量来记录鼠标当前的状态,如是否是按下状态等等。还有两个变量x和y用于记录鼠标当前的位置,这个位置将被初始化为屏幕的中心。

#ifndef __MOUSE_H__
#define __MOUSE_H__

#include "types.h"
#include "interrupts.h"
#include "port.h"

class MouseDriver : public InterruptRoutine
{
public:
    MouseDriver(InterruptManager *manager);
    ~MouseDriver();

    virtual uint32_t routine(uint32_t esp) override;

private:
    uint8_t offset = 0;
    uint8_t buttons = 0;
    uint8_t buffer[3];
    int8_t x, y;

    Port8Bit dataPort;
    Port8Bit commandPort;
};

#endif

鼠标的中断向量号是0x2C,使用和键盘相同的控制器,数据端口和命令端口和键盘一致。
构造函数和键盘类似。

#include "mouse.h"

MouseDriver::MouseDriver(InterruptManager *manager)
    : InterruptRoutine(0x0C + manager->getOffset(), manager),
      dataPort(0x60),
      commandPort(0x64),
      x(40), y(12)
{
    // 在屏幕上打印一个全白的字符表示鼠标
    uint16_t* videoMemory = (uint16_t*)0xb8000;
    videoMemory[y * 80 + x] = ((videoMemory[y * 80 + x] & 0xf000) >> 4) | 
                              ((videoMemory[y * 80 + x] & 0x0f00) << 4) |
                              (videoMemory[y * 80 + x] & 0x00ff);
    // 开启鼠标
    commandPort.write(0xa8);
    // 准备读取数据
    commandPort.write(0x20);
    // 设置状态,开启鼠标中断
    uint8_t status = (dataPort.read() | 0x02) & (~0x20);
    // 准备写入数据
    commandPort.write(0x60);
    // 写入鼠标状态
    dataPort.write(status);
    // 写入鼠标(而不是键盘)
    commandPort.write(0xd4);
    // 继续扫描输入
    dataPort.write(0xf4);
    // 读取数据流
    dataPort.read();
}

这段代码存在bug,后面将会看到移动鼠标时,鼠标轨迹不会消失。
将鼠标和键盘的源文件添加到Makefile的依赖中。
生成iso文件后的运行效果为
image.png
Image