引言
本章节我们将实现硬盘驱动,为后面的文件系统做准备
基础认知
- 早期,硬盘控制器和硬盘是分开的
- 后来,硬盘控制器和硬盘合二为一
- 于是,有了高大上的名字:Integrated Driver Electronics(IDE)
- 后来,有了别名 ATA,然后是 ATA/IDE
- 最初的硬盘是并口传输数据,所以 IDE 常指并口硬盘接口
- 再后来有了串口硬盘接口,名为:Serial ATA,缩写:SATA
- 关于硬盘的相关的概念我们就不做深入研究了,感兴趣的可以自己搜索
虚拟硬盘设置
- 我们的开发环境都是 bochs 虚拟的,所以硬盘也是虚拟出来的,实际上我们的系统就是存储在虚拟硬盘中的
- 虚拟硬盘的创建非常简单,使用下面的命令就可以创建一个大小为 10M 的 a.img 虚拟硬盘(不同 bximage 版本可能命令有所差异,可以通过 bximage --help 命令获取使用帮助)
bximage -func=create a.img -hd=10 -imgmode="flat" sectsize=512 -q
- 这条指令我们老早就实现过了,就写在 “Build.py” 脚本中,当执行这条指令后,会看到打印信息中包含下面信息
The following line should appear in your bochsrc: ata0-master: type=disk, path="a.img", mode=flat
- 所以 .bochsrc 配置文件中会包含下面内容
ata0-master: type=disk, path="a.img", mode=flat
- 不过呢,今天,我们的 .bochsrc 配置文件还需要增加一点东西
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
BIOS 留下的遗产
- BIOS 运行后会遍历系统中的硬件,并记录相关信息,其中,硬盘数量记录在 0x475 地址处
- 至于为什么是 0x475,咱就别关心了
- 打印出硬盘数量
printk("Hard disk:%d\n", *((U08*)0x475));
- 说到这里,突然间想起来之前实现过读取物理内存容量,但是当前工程中忘记加上去了,现在把这个功能加进来,新的代码见:loader.asm、inc.asm。注意在实模式下调用 call detect_memory, 顺着 detect_memory 函数把之前相关代码全部拷贝过来就行
- 不过现在代码稍微改动一下,我们把物理内存容量的读取结果放到固定的共享内存处
MEM_SIZE_ADDR equ SHARE_START_ADDR + 32
- 内核 “share.h” 文件中添加一下这个共享地址,见:share.h
#define MEM_SIZE_ADDR (SHARE_START_ADDR + 32)
- 打印出物理内存容量,见:main.c
printk("Mem:%x\n", *((U32*)MEM_SIZE_ADDR));
- 成果展示:
硬盘控制器
- 通过固定的 I/O 端口对硬盘进行控制操作
- 操作的本质就是读写硬盘控制器中的寄存器
- 在硬盘控制器中,有两种类型的寄存器:命令块寄存器和控制块寄存器
- 硬盘 I/O 端口及寄存器表
Master I/O | Slave I/O | 读 | 写 | 类型 | 作用 |
0x1F0 | 0x170 | Data | Data | 命令块寄存器 | 数据读写寄存器,大小为 16 bit,每次读写 1 个 WORD,反复循环,直到读写完所有数据 |
0x1F1 | 0x171 | Error | Features | 命令块寄存器 | 读时当做错误寄存器,写时当做额外参数寄存器 |
0x1F2 | 0x172 | Sector Count | Sector Count | 命令块寄存器 | 指定读取或写入的扇区数 |
0x1F3 | 0x173 | LBA Low | LBA Low | 命令块寄存器 | 扇区号低 8 位 |
0x1F4 | 0x174 | LBA Mid | LBA Mid | 命令块寄存器 | 扇区号中8 位 |
0x1F5 | 0x175 | LBA High | LBA High | 命令块寄存器 | 扇区号高 8 位 |
0x1F6 | 0x176 | Device | Device | 命令块寄存器 | LBA 地址的前 4 位(占用 Device 寄存器的低 4 位)高 4 位是 Device 相关设置位 |
0x1F7 | 0x177 | Status | Command | 命令块寄存器 | 读取时返回硬盘状态,写入时当做命令处理 |
0x3F6 | 0x376 | Alternate Staus | Device Control | 控制块寄存器 |
- 从上面表中,我们看到 I/O 端口分为 Master 和 Slave 两种,代表主从两块硬盘,目前我么仅有一块硬盘,所以我们只需要关注 Master I/O 那一列就可以了
- 可能是为了省端口吧,从表中我们可以看到,同一个端口在读和写时可能表示不同的含义,比如端口 0x1F7, 读时表示 Status (状态),写的时候代表着 Command(命令)
- 关键寄存器 LBA
- LBA(Logical Block Addressing):逻辑块地址,是一种线性寻址方式。通俗的将讲就是给扇区编号(从 0 ~ n),想要访问哪个扇区,直接指定扇区编号就可以了
- 与 LBA 并列的还有一种叫做 CHS 的东西,根据硬盘的物理结构,需要通过柱面位置(C)、磁头位置(H)以及扇区偏移(S)确定扇区位置,即 C、H、S 决定扇区地址,然而这种方式使用起来太麻烦,我们就不做深入研究了,还是使用 LBA 的方式吧,谁让这种方式简单呢
- LBA 寄存器中存放着扇区号,那么我们能访问的最大扇区号是多少呢?
- LBA 寄存器有高中低三个,所以能拼出一个 24(3*8) 位的数值,其访问扇区编号就是 24 位寻址吗?
- 其实并不是,Device 寄存器的低 4 位也是用来存放扇区编号的,所以扇区编号是 28 位寻址
- 关键寄存器 Device
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
1 | 1 | 1 | 0 | HS3 | HS2 | HS1 | HS0 |
- 低 4 位与 3 个 LBA 寄存器共拼成 28 位,用于扇区寻址
- bit4 代表着想要操作的是主硬盘还是从硬盘,目前我们只有一块硬盘,所以为 0
- bit6 = 1: 使用 LBA 寻址方式
- bit7 和 bit5 我们不使用,置 1 即可
- 关键寄存器 Command
- 获取硬盘信息:0xEC
- 读取扇区数据:0x20
- 数据写入扇区:0x30
- 关键寄存器 Status
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
BUSY | DRDY | DFSE | DSC | DRQ | - | - | ERR |
- 使用 bit7 和 bit 4 就可以判断硬盘是被是否就绪
- BUSY = 0:设备不忙,BUSY = 1:表示是正忙
- DRQ 表示是否可以传输数据,=1:表示可以传输数据(即可有进行读写操作)
端口读写
- 在实现硬盘驱动程序之前,我们先实现一下端口的读写操作接口函数吧,主要是硬盘驱动也需要多次进行端口读写操作,封装成函数更好一些
; U08 ReadPort(U16 port) ; 从 port 端口读一个字节数据 ReadPort: push ebp mov ebp, esp xor eax, eax ; eax = 0; mov dx, [ebp + 8] ; port in al, dx ; 读 dx 端口数据到 al 寄存器中 leave ret ; void WritePort(U16 port, U08 data) ; 将一个字节的 data 数据写到 port 端口中 WritePort: push ebp mov ebp, esp xor eax, eax ; eax = 0; mov dx, [ebp + 8] ; port mov al, [ebp + 12] ; data out dx, al ; 将 al 寄存器中的值写入 dx 端口中 leave ret
- 前面键盘驱动中的键值读取当时是通过内嵌汇编方式实现端口读写的,现在可以优化一下了
// asm volatile("inb $0x60, %%al; nop;" :"=a"(key)); key = ReadPort(0x60);
- 虽然我们已经实现了端口读写接口函数,但是,还不够,我们专门为硬盘实现其特殊的连续端口读写接口函数,还有一点需要注意的是,硬盘端口数据读写是以双字节为单位的(唯有 Data 寄存器是 16 位的,其余还是 8 位)
; void ReadPortW(U16 port, U16* buf, U32 n) ; 从 port 端口连续读取 n 个 WORD (双字节)数据到 buf 中 ReadPortW: push ebp mov ebp, esp mov edx, [ebp + 8] ; port mov edi, [ebp + 12] ; buf mov ecx, [ebp + 16] ; n cld ; 自增 rep insw ; rep: 重复指令,重复次数由 ecx 寄存器确定 leave ret ; void WritePortW(U16 port, U16* buf, U32 n) ; 将 buf 中的连续 n 个 WORD (双字节)数据写到 port 端口中 WritePortW: push ebp mov ebp, esp mov edx, [ebp + 8] ; port mov esi, [ebp + 12] ; buf mov ecx, [ebp + 16] ; n cld ; 自增 rep outsw ; rep: 重复指令,重复次数由 ecx 寄存器确定 leave ret
初步实现硬盘驱动
- 为硬盘创建单独的驱动文件:“hd.c” 和 “hd.h”,接下来的硬盘驱动代码就写在这两个文件中,当然了,新增源文件后需要改动对应目录下的 “BUILD.json” 配置文件
- 所谓的硬盘驱动无外乎实现硬盘的读写接口函数,下面是给出的硬盘扇区读写接口函数,看起来并没有啥复杂的地方,很好理解,首先通过相关的端口向控制器寄存器中写入相关参数,然后通知硬盘控制器接下来是要读取数据还是写入数据,最后就是利用我们前面专门为硬盘实现的端口连续读写接口函数来读写数据了
// 读 E_RET HardDiskRead(U32 sn, U08* buf) { WritePort(REG_NSECTOR, 1); // 设置要读取的扇区数,固定每次只读取一个扇区 WritePort(REG_LBA_LOW, (sn&0xFF)); // 设置 LBA Low, 扇区号低 8 位 WritePort(REG_LBA_MID, ((sn>>8)&0xFF)); // 设置 LBA Min, 扇区号中 8 位 WritePort(REG_LBA_HIGH, ((sn>>16)&0xFF)); // 设置 LBA High, 扇区号高 8 位 WritePort(REG_DEVICE, (((sn>>24)&0x0F) | 0xE0)); // 设置 Device, bit4 ~ bit7 为 1110(0xE0),表示 LBA 模式,低 4 位与LBA low Mid High 共拼成 28 位扇区地址寻址 WritePort(REG_COMMAND, ATA_READ); // 读扇区命令 // 等待硬盘设备 Ready {volatile U32 i = 999; while(i--);} ReadPortW(REG_DATA, (U16 *)buf, SEC_SIZE>>1); return E_ERR; } // 写 E_RET HardDiskWrite(U32 sn, U08* buf) { WritePort(REG_NSECTOR, 1); // 设置要读取的扇区数,固定每次只读取一个扇区 WritePort(REG_LBA_LOW, (sn&0xFF)); // 设置 LBA Low, 扇区号低 8 位 WritePort(REG_LBA_MID, ((sn>>8)&0xFF)); // 设置 LBA Min, 扇区号中 8 位 WritePort(REG_LBA_HIGH, ((sn>>16)&0xFF)); // 设置 LBA High, 扇区号高 8 位 WritePort(REG_DEVICE, (((sn>>24)&0x0F) | 0xE0)); // 设置 Device, bit4 ~ bit7 为 1110(0xE0),表示 LBA 模式,低 4 位与LBA low Mid High 共拼成 28 位扇区地址寻址 WritePort(REG_COMMAND, ATA_WRITE); // 写扇区命令 // 等待硬盘设备 Ready {volatile U32 i = 999; while(i--);} WritePortW(REG_DATA, (U16 *)buf, SEC_SIZE>>1); return E_OK; } • 找个地方测试一下读写接口是否可用,就放到 “main.c” 中吧,关键代码如下 buf_t[101] =0xAB; buf_t[102] =0xCD; HardDiskWrite(300, buf_t); buf_t[101] =0x0; buf_t[102] =0x0; HardDiskRead(300, buf_t); printk("%x %x\n", buf_t[101], buf_t[102]);
驱动优化
- 硬盘读写接口函数确实已经成功实现了,但是并不完善,比如我们在等待设备就绪时使用的是 while 等待固定的时间,这个时间可能远大于硬盘实际就绪时间,可以通过读硬盘 status 寄存器状态来优化就绪等待
static E_STATUS WaitReady(void) { U32 i = 0; U08 status = 0; for(i = 0; i < 500; i++) { status = ReadPort(REG_STATUS); // bi7:BUSY = 0 且 bit4:DRQ = 1 时表示硬盘设备已就绪,可以进行读写操作 if((!(status & STATUS_BUSY)) && (status & STATUS_DRQ)) return E_READY; } return E_NOT_READY; }
- 通过命令 0xEC 可以获得一些硬盘相关信息,比如扇区总数
U32 GetHardDiskSectors(void) { static U32 ret = 0; U16* buf = NULL; // 扇区数只读取一次就可以了 if(ret) return ret; WritePort(REG_COMMAND, ATA_INFO); // 读硬盘信息 // 申请一个扇区大小的内存空间用于存放读取到的硬盘信息 // 因为硬盘数据是以双字节为基本单位,申请到的内存就相当于数组 U16 buf[SEC_SIZE/2] // buf[61] 为硬盘扇区数的高 16 位,buf[60] 为硬盘扇区数的低 16 位 buf = (U16 *)Malloc(SEC_SIZE); if(NULL == buf) return 0; // 等待硬盘设备就绪 if(E_NOT_READY == WaitReady()) return 0; ReadPortW(REG_DATA, (U16 *)buf, SEC_SIZE>>1); ret = (buf[61]<<16) | buf[60]; Free(buf); return ret; } • 有了扇区总数后我们就可以对硬盘读写时传入的扇区号进行合法性检查了,只有传入的扇区号小于扇区总数才可以继续读写扇区 // 读 E_RET HardDiskRead(U32 sn, U08* buf) { // 检查参数合法性 if(sn >= GetHardDiskSectors() || NULL == buf) return E_ERR; ... } // 写 E_RET HardDiskWrite(U32 sn, U08* buf) { // 检查参数合法性 if(sn >= GetHardDiskSectors() || NULL == buf) return E_ERR; ... }
- 打印一下读到的硬盘扇区总数,该值乘以一个扇区大小 512 字节后应该等于 a.img 的大小,如果不等于,那肯定是扇区数读取函数实现的有问题
printk("Sectors:%x\n",GetHardDiskSectors());
- 本次改动的代码见:hd.c、hd.h、main.c,需要注意的是,由于 GetHardDiskSectors() 函数中使用的动态内存分配,所以 GetHardDiskSectors() 函数必须在 MemInit() 之后调用,别问我为啥要强调这个,因为我被坑了,看来在没有动态内存初始化的情况下就调用 VolatileMemAlloc() 是有问题的,有空再优化一下 VolatileMemAlloc() 函数吧,为啥等有空,因为我现在懒得优化