mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1587 字
4 分钟
设备驱动:键盘与 ATA 硬盘
2021-08-31

到目前为止,内核只能输出到屏幕,无法接收输入,也无法持久化数据。设备驱动是内核与硬件之间的桥梁,没有键盘驱动,内核就是个聋子;没有硬盘驱动,数据重启就消失。

设备驱动的动机#

没有设备驱动,操作系统就需要直接操作硬件寄存器,代码难以维护、无法扩展,可移植性也无从谈起。驱动程序将不同设备的硬件接口差异封装为统一的操作接口,通过中断处理响应硬件事件,管理设备共享和并发访问,并通过缓冲、请求队列等机制提高 I/O 效率。

理解了设备驱动的必要性,接下来看如何组织不同类型的驱动。

设备驱动架构#

操作系统根据访问方式将设备分为三类:

graph TD A[设备分类] --> B[字符设备] A --> C[块设备] A --> D[网络设备] B --> B1[键盘] B --> B2[鼠标] B --> B3[串口] B --> B4[按字节访问, 无缓冲] C --> C1[硬盘] C --> C2[SSD] C --> C3[USB存储] C --> C4[按块访问, 可随机读写] D --> D1[网卡] D --> D2[基于协议访问]
  • 字符设备:按字节流顺序访问,如键盘、鼠标、串口
  • 块设备:按固定大小的块访问,支持随机读写,如硬盘、SSD
  • 网络设备:通过网络协议栈访问,如网卡

设备抽象层#

内核通过统一的 device_t 结构来管理所有设备,无论字符设备还是块设备,都通过同一套接口访问:

typedef struct device {
char name[32]; /* 设备名称 */
device_type_t type; /* 设备类型:字符/块/网络 */
int major; /* 主设备号:标识驱动类型 */
int minor; /* 次设备号:标识具体设备实例 */
void *private; /* 设备私有数据 */
int refcount; /* 引用计数 */
/* 操作函数指针 */
int (*open)(struct device *dev);
int (*close)(struct device *dev);
ssize_t (*read)(struct device *dev, void *buf, size_t count, off_t offset);
ssize_t (*write)(struct device *dev, const void *buf, size_t count, off_t offset);
int (*ioctl)(struct device *dev, int cmd, void *arg);
} device_t;

device_t 中的 majorminor 字段构成设备号,用于唯一标识设备:主设备号标识设备类型或驱动程序,次设备号标识同类设备中的具体实例。例如 /dev/hda 的 major=3, minor=0(第一块 IDE 硬盘),/dev/keyboard 的 major=1, minor=0(键盘)。

先从最常用的输入设备开始,键盘。键盘是典型的字符设备,数据以字节流形式到达,按字节顺序访问,不支持随机访问,通常不使用缓存,适合交互式输入输出。

字符设备驱动:键盘#

键盘是最典型的字符设备,它通过中断向内核报告按键事件。以下是键盘驱动的完整工作流程:

sequenceDiagram participant User participant Keyboard participant IRQ participant ISR participant Buffer participant App User->>Keyboard: 按下键 Keyboard->>IRQ: 发送中断请求 IRQ->>ISR: 触发中断处理程序 ISR->>ISR: 读取扫描码 (0x60端口) ISR->>ISR: 转换为ASCII字符 ISR->>Buffer: 写入环形缓冲区 ISR->>App: signal(信号量唤醒) Note over App: 被唤醒 App->>Buffer: 读取字符 App->>User: 显示/处理字符

键盘中断处理

/* 键盘中断处理程序 */
void keyboard_isr(interrupt_frame_t *frame)
{
(void)frame;
uint8_t scancode = inb(0x60); /* 从0x60端口读取扫描码 */
/* 处理特殊键(Shift、Ctrl、Alt等) */
switch (scancode) {
case SCANCODE_LSHIFT:
case SCANCODE_RSHIFT:
keyboard_dev.state.shift = 1;
return;
case SCANCODE_LSHIFT_R:
case SCANCODE_RSHIFT_R:
keyboard_dev.state.shift = 0;
return;
case SCANCODE_CAPS:
keyboard_dev.state.caps_lock = !keyboard_dev.state.caps_lock;
return;
}
/* 忽略按键释放码(最高位为1) */
if (scancode & 0x80) {
return;
}
/* 转换扫描码为ASCII字符 */
char ch = scancode_to_ascii(scancode);
if (ch != 0) {
spinlock_acquire(&kb_lock);
/* 检查缓冲区是否满 */
if (keyboard_dev.count < KB_BUFFER_SIZE) {
/* 写入环形缓冲区 */
keyboard_dev.buffer[keyboard_dev.tail] = ch;
keyboard_dev.tail = (keyboard_dev.tail + 1) % KB_BUFFER_SIZE;
keyboard_dev.count++;
/* 唤醒等待的进程 */
sem_signal(&kb_sem);
}
spinlock_release(&kb_lock);
}
}

扫描码转换

键盘发送的是扫描码(scancode),需要转换为ASCII字符。驱动使用两张查找表,分别对应普通模式和Shift模式:

/* 普通模式扫描码表 */
static const char scancode_table[] = {
0, 0, '1', '2', '3', '4', '5', '6', /* 0x00-0x07 */
'7', '8', '9', '0', '-', '=', '\b', '\t', /* 0x08-0x0F */
'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', /* 0x10-0x17 */
'o', 'p', '[', ']', '\n', 0, 'a', 's', /* 0x18-0x1F */
'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', /* 0x20-0x27 */
'\'', '`', 0, '\\', 'z', 'x', 'c', 'v', /* 0x28-0x2F */
'b', 'n', 'm', ',', '.', '/', 0, '*', /* 0x30-0x37 */
0, ' ', 0, 0, 0, 0, 0, 0, /* 0x38-0x3F */
};
/* 扫描码转ASCII */
char scancode_to_ascii(uint8_t scancode)
{
/* 处理释放码 */
if (scancode & 0x80) {
return 0;
}
/* 根据Shift状态选择不同的转换表 */
if (keyboard_dev.state.shift) {
if (scancode < sizeof(scancode_table_shift)) {
char c = scancode_table_shift[scancode];
/* Caps Lock处理:大小写切换 */
if (keyboard_dev.state.caps_lock && c >= 'A' && c <= 'Z') {
c += 32; /* 转为小写 */
} else if (keyboard_dev.state.caps_lock && c >= 'a' && c <= 'z') {
c -= 32; /* 转为大写 */
}
return c;
}
} else {
if (scancode < sizeof(scancode_table)) {
char c = scancode_table[scancode];
if (keyboard_dev.state.caps_lock && c >= 'a' && c <= 'z') {
c -= 32; /* 转为大写 */
}
return c;
}
}
return 0;
}

阻塞式读取

键盘读取使用信号量实现阻塞式I/O:

/* 键盘设备读取(阻塞) */
static ssize_t keyboard_read(device_t *dev, void *buf, size_t count, off_t offset)
{
(void)offset;
keyboard_device_t *kb = (keyboard_device_t *)dev;
char *p = (char *)buf;
size_t read = 0;
while (read < count) {
/* 阻塞等待:ISR产生数据后signal */
sem_wait(&kb_sem);
spinlock_acquire(&kb_lock);
if (kb->count > 0) {
p[read++] = kb->buffer[kb->head];
kb->head = (kb->head + 1) % KB_BUFFER_SIZE;
kb->count--;
}
spinlock_release(&kb_lock);
}
return (ssize_t)read;
}

键盘驱动解决了输入问题,但数据重启就消失。要持久化数据,需要块设备驱动。块设备按固定大小的块访问(通常 512 字节或 4KB),支持随机访问,使用请求队列和缓存,适合存储设备。

块设备驱动:ATA 硬盘#

ATA(Advanced Technology Attachment)是常见的硬盘接口标准。驱动程序通过端口I/O与硬盘控制器通信。

ATA寄存器布局

/* ATA控制器寄存器(主控制器,I/O基地址0x1F0) */
#define ATA_REG_DATA 0x00 /* 数据寄存器(16位) */
#define ATA_REG_ERROR 0x01 /* 错误寄存器 */
#define ATA_REG_SECCOUNT 0x02 /* 扇区计数 */
#define ATA_REG_LBA0 0x03 /* LBA低字节 */
#define ATA_REG_LBA1 0x04 /* LBA中字节 */
#define ATA_REG_LBA2 0x05 /* LBA高字节 */
#define ATA_REG_HDDEVSEL 0x06 /* 驱动器/磁头选择 */
#define ATA_REG_COMMAND 0x07 /* 命令寄存器 */
#define ATA_REG_STATUS 0x07 /* 状态寄存器(与命令寄存器共用) */
/* 控制器基址(主控制器) */
#define ATA_PRIMARY_IO 0x1F0
#define ATA_PRIMARY_CTRL 0x3F6

状态寄存器

#define ATA_SR_BSY 0x80 /* 忙:设备正在执行命令 */
#define ATA_SR_DRDY 0x40 /* 设备就绪 */
#define ATA_SR_DF 0x20 /* 设备故障 */
#define ATA_SR_DSC 0x10 /* 介质改变 */
#define ATA_SR_DRQ 0x08 /* 数据请求:数据可读写 */
#define ATA_SR_CORR 0x04 /* 校正数据 */
#define ATA_SR_IDX 0x02 /* 索引 */
#define ATA_SR_ERR 0x01 /* 错误 */

设备检测与识别

/* 检测ATA设备 */
int ata_detect(uint16_t io_base, int is_master, ata_device_t *dev)
{
uint8_t status;
uint16_t identify[256];
/* 初始化结构 */
memset(dev, 0, sizeof(ata_device_t));
dev->io_base = io_base;
dev->is_master = is_master;
/* 选择驱动器 */
ata_write_reg(io_base, ATA_REG_HDDEVSEL,
ATA_LBA_MODE | (is_master ? 0 : 0x10) | 0xA0);
/* 等待就绪 */
for (volatile int i = 0; i < 10000; i++);
/* 检查状态 */
status = ata_read_reg(io_base, ATA_REG_STATUS);
if (status == 0xFF || status == 0x00) {
return -1; /* 设备不存在 */
}
/* 发送IDENTIFY命令 */
ata_write_reg(io_base, ATA_REG_COMMAND, ATA_CMD_IDENTIFY);
/* 等待数据就绪 */
if (ata_wait_ready(io_base, 1) != 0) {
return -1;
}
/* 读取IDENTIFY数据(256个字) */
for (int i = 0; i < 256; i++) {
identify[i] = inw(io_base + ATA_REG_DATA);
}
/* 解析设备信息 */
dev->lba_count = (uint64_t)identify[60] | ((uint64_t)identify[61] << 16);
/* 检查LBA48支持 */
if (identify[83] & 0x400) {
dev->lba48_supported = 1;
dev->lba_count = (uint64_t)identify[100] |
((uint64_t)identify[101] << 16) |
((uint64_t)identify[102] << 32) |
((uint64_t)identify[103] << 48);
}
/* 获取设备型号 */
for (int i = 0; i < 40; i += 2) {
dev->model[i] = identify[27 + i/2] >> 8;
dev->model[i + 1] = identify[27 + i/2] & 0xFF;
}
dev->model[40] = '\0';
return 0;
}

读取扇区流程

flowchart TD A[开始读取扇区] --> B[等待设备就绪] B --> C{检查状态} C -->|BSY=1| B C -->|错误| E[返回错误] C -->|DRDY=1| D[选择驱动器并设置LBA] D --> F[发送读取命令] F --> G[等待数据就绪] G --> H{检查DRQ} H -->|DRQ=0| G H -->|DRQ=1| I[读取512字节数据] I --> J[返回成功]

扇区读取实现

/* 读取扇区 */
int ata_read_sector(ata_device_t *dev, uint32_t lba, void *buf)
{
if (dev == NULL || buf == NULL) {
return -1;
}
spinlock_acquire(&ata_lock);
/* 1. 选择驱动器并设置LBA */
ata_write_reg(dev->io_base, ATA_REG_HDDEVSEL,
ATA_LBA_MODE | (dev->is_master ? 0 : 0x10) |
((lba >> 24) & 0x0F));
/* 2. 等待设备就绪 */
if (ata_wait_ready(dev->io_base, 0) != 0) {
spinlock_release(&ata_lock);
return -1;
}
/* 3. 设置LBA地址和扇区数 */
ata_write_reg(dev->io_base, ATA_REG_SECCOUNT, 1);
ata_write_reg(dev->io_base, ATA_REG_LBA0, lba & 0xFF);
ata_write_reg(dev->io_base, ATA_REG_LBA1, (lba >> 8) & 0xFF);
ata_write_reg(dev->io_base, ATA_REG_LBA2, (lba >> 16) & 0xFF);
/* 4. 发送读取命令 */
ata_write_reg(dev->io_base, ATA_REG_COMMAND, ATA_CMD_READ_PIO);
/* 5. 等待数据就绪 */
if (ata_wait_ready(dev->io_base, 1) != 0) {
spinlock_release(&ata_lock);
return -1;
}
/* 6. 读取数据(256个16位字 = 512字节) */
uint16_t *data = (uint16_t *)buf;
for (int i = 0; i < 256; i++) {
data[i] = inw(dev->io_base + ATA_REG_DATA);
}
spinlock_release(&ata_lock);
return 0;
}

等待设备就绪

/* 等待设备就绪(带超时) */
static int ata_wait_ready(uint16_t io_base, int check_drq)
{
uint32_t start = getTick();
uint8_t status;
while (1) {
status = ata_read_reg(io_base, ATA_REG_STATUS);
/* 检查错误 */
if (status & ATA_SR_ERR) {
return -1;
}
/* 检查设备故障 */
if (status & ATA_SR_DF) {
return -2;
}
/* 检查就绪 */
if ((status & ATA_SR_BSY) == 0) {
if (check_drq) {
if (status & ATA_SR_DRQ) {
return 0; /* 数据可读写 */
}
} else {
if (status & ATA_SR_DRDY) {
return 0; /* 设备就绪 */
}
}
}
/* 超时检查 */
if ((getTick() - start) * 10 > ATA_TIMEOUT) {
return -3;
}
}
}

有了键盘和硬盘两种驱动,还需要一个统一的框架来管理它们。

设备注册框架#

内核维护一个全局设备表,所有注册的驱动程序都会在其中登记:

#define MAX_DEVICES 32
static device_t *device_table[MAX_DEVICES];
static int device_count = 0;
static spinlock_t device_lock;
/* 注册设备 */
int register_device(device_t *dev)
{
if (dev == NULL) {
return -1;
}
spinlock_acquire(&device_lock);
/* 查找空槽 */
for (int i = 0; i < MAX_DEVICES; i++) {
if (device_table[i] == NULL) {
device_table[i] = dev;
dev->refcount = 0;
device_count++;
spinlock_release(&device_lock);
vga_printf("[Device] Registered: %s (major=%d, minor=%d)\n",
dev->name, dev->major, dev->minor);
return i;
}
}
spinlock_release(&device_lock);
vga_printf("[Device] ERROR: Device table full!\n");
return -1;
}
/* 按设备号查找设备 */
device_t *find_device(int major, int minor)
{
spinlock_acquire(&device_lock);
for (int i = 0; i < MAX_DEVICES; i++) {
device_t *dev = device_table[i];
if (dev != NULL && dev->major == major && dev->minor == minor) {
spinlock_release(&device_lock);
return dev;
}
}
spinlock_release(&device_lock);
return NULL;
}

设备初始化流程:

graph TD A[设备框架初始化] --> B[清空设备表] A --> C[初始化自旋锁] B --> D[键盘驱动初始化] C --> D D --> D1[初始化设备结构] D --> D2[注册中断处理程序] D --> D3[注册到设备表] D3 --> E[ATA驱动初始化] E --> E1[检测主控制器主设备] E --> E2[检测主控制器从设备] E --> E3[注册到设备表] E3 --> F[设备框架初始化完成]

代码实现#

文件结构#

13.kernel-drivers/
├── boot/
│ ├── mbr.S # MBR引导代码
│ └── loader.S # 加载器
├── kernel/
│ ├── include/
│ │ ├── device.h # 设备抽象接口
│ │ ├── keyboard.h # 键盘驱动接口
│ │ └── ata.h # ATA驱动接口
│ ├── drivers/
│ │ ├── device.c # 设备框架实现
│ │ ├── keyboard.c # 键盘驱动实现
│ │ └── ata.c # ATA驱动实现
│ └── kernel.c # 内核主函数
└── Makefile # 编译配置

键盘和 ATA 驱动各自维护私有数据结构:

/* 键盘设备结构 */
typedef struct {
device_t dev; /* 基础设备结构 */
uint8_t buffer[KB_BUFFER_SIZE]; /* 环形缓冲区 */
uint32_t head; /* 读指针 */
uint32_t tail; /* 写指针 */
uint32_t count; /* 缓冲区数据量 */
struct {
int shift; /* Shift键状态 */
int caps_lock; /* Caps Lock状态 */
int ctrl; /* Ctrl键状态 */
int alt; /* Alt键状态 */
} state;
} keyboard_device_t;
/* ATA设备结构 */
typedef struct {
block_device_t bdev; /* 块设备结构 */
uint16_t io_base; /* I/O基地址 */
uint16_t ctrl_base; /* 控制器基址 */
int is_master; /* 是否为主设备 */
uint64_t lba_count; /* LBA扇区总数 */
int lba48_supported; /* 是否支持LBA48 */
char model[41]; /* 设备型号 */
} ata_device_t;

键盘中断处理流程:

flowchart TD A[键盘按下] --> B[IRQ1中断] B --> C[keyboard_isr] C --> D[读取0x60端口扫描码] D --> E{处理特殊键} E -->|Shift/Ctrl/Alt| F[更新状态标志] E -->|普通键| G{是否释放码} G -->|是| H[忽略] G -->|否| I[转换为ASCII] I --> J[写入环形缓冲区] J --> K[发送信号量唤醒] F --> L[发送EOI] H --> L K --> L L --> M[中断返回]

ATA 读取扇区流程:

flowchart TD A[ata_read_sector] --> B[获取锁] B --> C[选择驱动器并设置LBA] C --> D[等待DRDY就绪] D --> E[设置LBA寄存器] E --> F[发送READ命令] F --> G[等待DRQ就绪] G --> H[读取512字节数据] H --> I[释放锁] I --> J[返回成功] D --> K{超时} K -->|是| L[释放锁并返回错误] G --> M{超时} M -->|是| L

环形缓冲区实现#

/* 键盘缓冲区 */
#define KB_BUFFER_SIZE 128
/* 中断处理程序中写入 */
if (keyboard_dev.count < KB_BUFFER_SIZE) {
keyboard_dev.buffer[keyboard_dev.tail] = ch;
keyboard_dev.tail = (keyboard_dev.tail + 1) % KB_BUFFER_SIZE;
keyboard_dev.count++;
sem_signal(&kb_sem);
}
/* 读取函数中读取 */
if (kb->count > 0) {
p[read++] = kb->buffer[kb->head];
kb->head = (kb->head + 1) % KB_BUFFER_SIZE;
kb->count--;
}

解析:环形缓冲区在中断处理程序(生产者)和读取进程(消费者)之间传递数据。headtail 指针通过取模运算实现循环,避免缓冲区溢出。

状态轮询等待#

static int ata_wait_ready(uint16_t io_base, int check_drq)
{
uint32_t start = getTick();
uint8_t status;
while (1) {
status = ata_read_reg(io_base, ATA_REG_STATUS);
if (status & ATA_SR_ERR) {
return -1; /* 错误 */
}
if ((status & ATA_SR_BSY) == 0) {
if (check_drq) {
if (status & ATA_SR_DRQ) {
return 0; /* 数据就绪 */
}
} else {
if (status & ATA_SR_DRDY) {
return 0; /* 设备就绪 */
}
}
}
/* 超时检查 */
if ((getTick() - start) * 10 > ATA_TIMEOUT) {
return -3;
}
}
}

解析:轮询 ATA 状态寄存器等待设备就绪,使用超时机制避免死锁,检查多种状态标志确保操作正确性。

自旋锁保护#

/* 设备注册时的锁保护 */
int register_device(device_t *dev)
{
spinlock_acquire(&device_lock);
for (int i = 0; i < MAX_DEVICES; i++) {
if (device_table[i] == NULL) {
device_table[i] = dev;
/* ... */
spinlock_release(&device_lock);
return i;
}
}
spinlock_release(&device_lock);
return -1;
}

解析:在多 CPU 环境下,设备表访问需要用自旋锁保护,防止竞态条件。自旋锁适用于短临界区,不适合可能导致阻塞的操作。

运行与验证#

编译运行#

cd 13.kernel-drivers
make clean
make all
make run

预期输出#

[Device] Framework initialized (max 32 devices)
[Keyboard] Driver initialized (IRQ1)
[Device] Registered: keyboard (major=1, minor=0)
[ATA] Detecting IDE devices...
Primary Master: Found
Model: QEMU HARDDISK
Sectors: 1048576 (512 MB)
LBA48: Yes
[Device] Registered: hda (major=3, minor=0)
Primary Slave: Not found
[ATA] Initialization complete

测试键盘驱动#

在QEMU中按键,应该能在VGA上看到字符输出。驱动会显示按键的扫描码和对应的ASCII字符:

Key pressed: 'a' (scancode=0x1E)
Key pressed: 'B' (scancode=0x30, shift)
Key pressed: 'C' (scancode=0x2E, caps_lock)

测试ATA驱动#

驱动会自动检测并识别硬盘,读取MBR(主引导记录)验证磁盘访问:

/* 测试代码:读取MBR */
uint8_t mbr[512];
block_read(&primary_master.bdev, 0, mbr);
if (mbr[510] == 0x55 && mbr[511] == 0xAA) {
vga_printf("MBR signature: 0x55AA (valid)\n");
} else {
vga_printf("MBR signature: invalid\n");
}

踩坑记录#

1. 键盘无响应#

原因

  • 键盘中断(IRQ1)未在IDT中注册
  • 中断处理程序未正确发送EOI
  • PS/2端口未正确初始化

解决方案

/* 确保注册中断处理程序 */
register_interrupt_handler(INT_IRQ1, keyboard_isr);
/* 检查EOI是否发送 */
void keyboard_isr(interrupt_frame_t *frame) {
/* ... 处理代码 ... */
outb(PIC1_CMD, PIC_EOI); /* 发送EOI */
}

2. 硬盘读取失败#

原因

  • 等待设备就绪超时
  • LBA地址设置错误
  • 设备不支持LBA48但尝试使用

解决方案

/* 增加超时时间 */
#define ATA_TIMEOUT 5000 /* 从1000ms增加到5000ms */
/* 检查LBA48支持后再使用 */
if (dev->lba48_supported && lba > 0x0FFFFFFF) {
/* 使用LBA48模式 */
} else {
/* 使用LBA28模式 */
}

3. 数据损坏或缓冲区溢出#

原因

  • 环形缓冲区未正确处理满的情况
  • 多线程访问未加锁保护
  • 读取时缓冲区被覆盖

解决方案

/* 在写入前检查缓冲区是否满 */
if (keyboard_dev.count < KB_BUFFER_SIZE) {
/* 安全写入 */
} else {
vga_printf("[Keyboard] Buffer overflow!\n");
}
/* 使用自旋锁保护缓冲区访问 */
spinlock_acquire(&kb_lock);
/* 读写操作 */
spinlock_release(&kb_lock);

小结#

本章从设备驱动的动机出发,构建了统一的设备抽象层:device_t 结构通过函数指针屏蔽字符设备和块设备的差异,主/次设备号提供唯一标识。键盘驱动展示了中断驱动的 I/O 模型,扫描码到 ASCII 的转换,以及环形缓冲区加信号量的阻塞读取;ATA 硬盘驱动展示了端口 I/O、LBA 寻址、状态轮询和扇区读写;设备注册框架则将两者纳入全局设备表统一管理。有了键盘驱动,内核能接收输入;有了硬盘驱动,内核能读写持久化数据。但按扇区读写硬盘仍然过于原始,下一章将在此基础上构建文件系统,让数据以文件和目录的形式组织起来。

参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

设备驱动:键盘与 ATA 硬盘
https://blog.souloss.com/posts/os/device-driver-keyboard-ata/
作者
Souloss
发布于
2021-08-31
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时