当前版本实现了DMA传输模式, 并且在bochs, VirtualBox及VMware下都测试通过, 目前只有ATA硬盘读写操作使用的是DMA模式, ATAPI光盘使用的还是PIO模式, 读者有兴趣的话, 可以自行修改驱动来实现ATAPI的DMA传输模式。DMA传输模式是直接在内存与磁盘之间传输数据, 所以理论上速度比PIO模式要快, 这点在 VirtualBox与VMware下得到了很好的验证。

    页面导航:
项目下载地址:

    v2.4.0版本的项目地址:

    github.com地址:https://github.com/zenglong/zenglOX (只包含git提交上来的源代码)

    Dropbox地址:点此进入Dropbox网盘  该版本位于zenglOX_v2.4.0的文件夹中。

    文件夹里的zip压缩包为源代码,readme.txt为版本的简单说明。pci_ide.cc文件为需要修改的bochs源文件(下面会进行说明)。

    文件夹中的DMA_program.pdf为开发DMA驱动的主要参考手册,Intel_ICH.pdf为Intel的ICH控制器的官方手册,Intel_PIIX_PIIX3.pdf为Intel的PIIX与PIIX3控制器的官方手册,ICH与PIIX是两类不同型号的IDE控制器芯片,它们位于主板上,是系统内存与ATA/ATAPI驱动器之间的最主要的控制部件。这两类芯片的PDF手册是作为扩展阅读的,因为作者写的DMA驱动是基于DMA_program.pdf手册写的一个比较通用的驱动程式,和芯片相关的特殊代码比较少。

    Google Drive地址:点此进入Google Drive云端硬盘 对应也是zenglOX_v2.4.0的文件夹。

    sourceforge地址:https://sourceforge.net/projects/zenglox/files  对应也是zenglOX_v2.4.0的文件夹。

    Dropbox与Google Drive如果正常途径访问不了,则需要使用代理访问。

    要了解网盘中pci_ide.cc文件的具体使用方法,请参考下面的readme说明文档。

readme说明文档:

    以下是网盘中readme.txt文件的内容(在github的commit message里也可以看到同样的内容),该文档详细的说明了运行步骤,注意事项:

zenglOX v2.4.0 DMA(Direct Memory Access)

当前版本实现了DMA传输模式, 并且在bochs, VirtualBox及VMware下都测试通过,
目前只有ATA硬盘读写操作使用的是DMA模式, ATAPI光盘使用的还是PIO模式,
读者有兴趣的话, 可以自行修改驱动来实现ATAPI的DMA传输模式。

DMA传输模式是直接在内存与磁盘之间传输数据, 所以理论上速度比PIO模式要快, 这点在
VirtualBox与VMware下得到了很好的验证。

但是在bochs下, 如果你不修改它的源代码的话, 默认情况下, DMA表现的速度比PIO模式还要
慢, 主要是它的代码里, 和DMA读写操作相关的定时器的值, 设置的太大了。

因此作者对其定时器值做了些修改, 并放到sourceforge对应版本的网盘里,
文件为pci_ide.cc , 假设你的bochs源码的路径为/mnt/zenglOX/bochs-2.6 ,
那么就将该pci_ide.cc文件放置到/mnt/zenglOX/bochs-2.6/iodev/目录下, 覆盖掉
该目录下原来的pci_ide.cc文件。

(我这里是对之前一直在用的bochs-2.6的版本进行的修改,
请确保你和我是一样的版本, 在之前zenglOX v0.0.1的网盘里, 我提供过bochs-2.6
的源代码, 并在zenglOX v0.0.1及zenglOX v2.0.0的文章里介绍过bochs-2.6的编译过程
, 目前bochs的编译过程请以zenglOX v2.0.0版本为主, 因为该版本的编译过程中
加入了E1000网卡与PCI总线的支持, 如果你之前已经按照zenglOX v2.0.0版本的要求,
编译过bochs了, 那么在替换了pci_ide.cc文件后, 就只需执行make和make install命令
即可(请用root权限), 这样它就只会去编译修改过的文件, 其他没修改过的文件就可以跳过编译)

pci_ide.cc文件里, 主要修改了第397行的代码:

//bx_pc_system.activate_timer(BX_PIDE_THIS s.bmdma[channel].timer_index, 1000, 0);
bx_pc_system.activate_timer(BX_PIDE_THIS s.bmdma[channel].timer_index, 1, 0);

也就是将activate_timer函数的第二个参数由1000改为1 , 这样每次触发DMA读写操作时,
就不用等那么久了(原来的定时器间隔大概是1000 usecond的时间)

读者可以在修改pci_ide.cc之前, 先运行一下zenglOX, 看下默认情况下原始的DMA
读写速度, 然后修改pci_ide.cc并编译安装bochs后, 再看下DMA的读写速度情况,
你就会发现他们在速度上的差异了。

在VirtualBox与VMware下则不存在这种情况, 他们的DMA速度明显比PIO模式要快。

zenglOX当前版本中, 和DMA相关的驱动代码主要集中在新增的zlox_ide.c文件里。

需要参考sourceforge网盘里的DMA_program.pdf手册, 才能理解代码的含义。

在前一个版本的说明文档里, 作者曾说过DMA驱动很难写, 原因就是作者只有PDF
手册可以作为参考, 没有实际的源代码可供研究, 作者根据手册里的说明写出来的
驱动程式在一开始时, 遇到了很多问题, 最后都是通过调试bochs源代码, 分析其中IDE
控制器, ATA驱动器相关的代码, 在充分理解其工作方式后, 才写出一个比较通用的DMA驱动程式的。

因此, 如果没有bochs的源代码的话, 这个驱动程式估计还写不出来(PDF手册里的
说明信息并不是完全正确的, 有些说明信息甚至有非常致命的错误, 只有通过调试
bochs源代码才能分析出来, 因此PDF手册也只能作为一种大概的参考)

当前版本只是增加了DMA驱动, 并修复了一些小BUG, 除此之外, 没作别的大的改动,
zenglOX源代码的编译运行过程和之前v2.3.0版本的方式一样, 也没有增加新的命令行程式。

时间: 2014年11月15日
作者: zenglong
官网: www.zengl.com
 

readme的补充说明:

    下面假设读者已经按照zenglOX v2.0.0版本的要求,对bochs进行了重新配置和编译安装,从而让bochs支持PCI总线和E1000网卡了,并且假设读者将pci_ide.cc文件下载到了Downloads目录下,那么就可以直接按照下面的命令对bochs修改源码和进行编译安装:

[email protected]:~$ cd Downloads/
[email protected]:~/Downloads$ sudo cp pci_ide.cc /mnt/zenglOX/bochs-2.6/iodev/ 
[sudo] password for zengl: 
[email protected]:~/Downloads$ vim /mnt/zenglOX/bochs-2.6/iodev/pci_ide.cc 
[email protected]:~/Downloads$ cd /mnt/zenglOX/bochs-build/
[email protected]:/mnt/zenglOX/bochs-build$ sudo make
[email protected]:/mnt/zenglOX/bochs-build$ sudo make install


    这里注意使用sudo指令切换到root权限,在将pci_ide.cc文件拷贝到iodev目录后,可以使用vim工具打开该文件,通过前面readme介绍的第397行的代码来判断是否拷贝成功了,接着就可以在bochs-build目录内执行make和make install命令来进行编译安装了(同样在root权限下),这里bochs源代码的路径都以zenglOX v2.0.0版本中的路径为标准,在make编译时,只会对修改过的pci_ide.cc文件进行编译,其他没修改过的文件则会跳过编译。

    zenglOX v2.4.0的源代码的编译运行过程,与之前v2.3.0的一样,假设读者将zenglOX_v2.4.0.zip压缩包下载到了Downloads目录中,那么就可以输入以下命令来进行编译:

[email protected]:~/Downloads$ unzip -d zenglOX_v2.4.0 zenglOX_v2.4.0.zip
[email protected]:~/Downloads$ cd zenglOX_v2.4.0/
[email protected]:~/Downloads/zenglOX_v2.4.0$ make
[email protected]:~/Downloads/zenglOX_v2.4.0$ make iso
[email protected]:~/Downloads/zenglOX_v2.4.0$ chmod +x startBochs 
[email protected]:~/Downloads/zenglOX_v2.4.0$ ifconfig -a
eth0      Link encap:以太网  硬件地址 08:00:27:07:a0:9a  
          inet 地址:10.0.2.15  广播:10.0.2.255  掩码:255.255.255.0
.....................................................
[email protected]:~/Downloads/zenglOX_v2.4.0$ vim bochsrc.txt 
[email protected]:~/Downloads/zenglOX_v2.4.0$ sudo ./startBochs


    上面先通过unzip命令解压源代码,接着cd进入源代码的根目录,在zenglOX_v2.4.0的目录内,输入make命令来编译生成initrd.img(即ram disk镜像)和zenglOX.bin(ELF可执行格式的内核文件),再输入make iso命令生成zenglOX.iso的光盘镜像,使用chmod命令给startBochs脚本设置可执行属性,在运行startBochs之前,我们需要先通过ifconfig命令来查看当前系统中是否存在eth0网络接口,如果当前系统中不存在eth0的网络接口,而是存在eth1之类的接口,则需要编辑bochsrc.txt配置文件,将配置文件里e1000的ethdev参数设置为eth1之类的系统中实际存在的接口名,保存bochsrc.txt文件后,在root权限下运行startBochs脚本。(有关e1000网卡配置相关的信息,请参考zenglOX v2.0.0版本对应的官方文章)

    startBochs脚本运行时,会停下来等待gdb调试器的远程连接,可以另开一个终端,在终端里输入如下命令:

[email protected]:~/Downloads$ cd zenglOX_v2.4.0/
[email protected]:~/Downloads/zenglOX_v2.4.0$ gdb -q zenglOX.bin
Reading symbols from /home/zengl/Downloads/zenglOX_v2.4.0/zenglOX.bin...done.
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x0000fff0 in ?? ()
(gdb) c
Continuing.
Remote connection closed
(gdb) q
[email protected]:~/Downloads/zenglOX_v2.4.0$ 


    上面通过gdb来加载zenglOX.bin的符号信息,在gdb命令行内输入target remote localhost:1234来连接上面Bochs打开的1234端口,从而建立远程连接,你可以输入c命令继续执行虚拟机,也可以输入b命令设置断点等。

    由于zenglOX v2.4.0源代码里的hd.img磁盘镜像文件默认是没有分区过的,如果你不想重新分区格式化的话,可以将之前v2.3.0版本中(前提是你对该版本进行运行测试过了),已经分区格式化过的hd.img文件拷贝到zenglOX v2.4.0的源代码根目录下,这样,zengOX启动后,就可以在mount加载磁盘目录后,直接运行zengl之类的程式来进行测试了。(fdisk磁盘分区工具,format磁盘格式化工具的用法请参考zenglOX v1.5.0版本对应的官方文章。至于mount工具,isoget工具以及zengl脚本程式的使用方法,则请参考上一篇zenglOX v2.3.0版本对应的官方文章)

   在前面编译生成了zenglOX.iso的光盘镜像后,可以将该光盘镜像文件加载到VirtualBox或VMware里进行测试。(在之前zenglOX v1.0.0版本对应的官方文章里,我们介绍过如何将zenglOX.iso镜像挂接到VirtualBox里,此外,在zenglOX v1.4.0与v1.4.1版本对应的官方文章里,我们介绍过如何在VMware里新建一个虚拟机来挂载和测试zenglOX.iso镜像)

    当前版本的zenglOX系统运行后,你会看到DMA的相关提示信息:


图1

    可以看到ATA硬盘驱动器将使用DMA来传输数据,ATAPI的光盘驱动器还是用的PIO模式,上面我在VirtualBox虚拟机里添加了两个磁盘,一个2G大小,另一个512M大小,它们在DMA传输模式下,运行效果和预期的相一致,当然,同一时间,只能mount加载和使用其中的一个磁盘。

Makefile文件解析:

    由于当前版本只增加了zlox_ide.c的源文件(之前readme里提到过,该文件里存储的是和DMA相关的驱动程式),因此,zenglOX源代码根目录中的Makefile文件里只需把该模块相关的文件加进来即可:

#makefile for zenglOX

....................................................

DEPS = zlox_common.h zlox_monitor.h zlox_descriptor_tables.h zlox_isr.h zlox_time.h zlox_kheap.h \
		zlox_paging.h zlox_ordered_array.h zlox_initrd.h zlox_fs.h zlox_multiboot.h zlox_task.h \
		zlox_keyboard.h zlox_elf.h zlox_ata.h zlox_iso.h zlox_zenglfs.h zlox_vga.h zlox_pci.h \
		zlox_e1000.h zlox_network.h zlox_ps2.h zlox_uheap.h zlox_ide.h
OBJS = zlox_boot.o zlox_kernel.o zlox_common.o zlox_monitor.o zlox_descriptor_tables.o \
		zlox_gdt.o zlox_interrupt.o zlox_isr.o zlox_time.o zlox_kheap.o zlox_paging.o \
		zlox_ordered_array.o zlox_initrd.o zlox_fs.o zlox_task.o zlox_process.o zlox_syscall.o \
		zlox_keyboard.o zlox_elf.o zlox_shutdown.o zlox_ata.o zlox_iso.o zlox_zenglfs.o zlox_vga.o \
		zlox_pci.o zlox_e1000.o zlox_network.o zlox_ps2.o zlox_uheap.o zlox_ide.o

INITRD_IMG = build_initrd_img/initrd.img

....................................................


    上面的DEPS变量里增加了一个zlox_ide.h的头文件,OBJS变量里则增加了一个zlox_ide.o的模块文件,除此之外,makefile文件的其他代码都没做任何改变。

源码简析:

    DMA驱动程式的代码主要集中在zlox_ide.c文件里,要理解该文件中的代码,就需要结合网盘里的DMA_program.pdf手册来进行说明。

    在该PDF手册的第2页,提到了一个Physical Region Descriptor Table(物理区域描述符表),该表中可以存储多个Physical Region Descriptor(物理区域描述符),每个描述符里,都存储了一个对应的物理内存区域的基址信息和该区域的尺寸大小信息,当进行DMA写磁盘操作前,数据需要先写入到该物理内存区域中,当DMA传输正式开启时,该区域内的数据就会被传输到磁盘里。当进行DMA读磁盘的操作时,ATA驱动器传递过来的数据会被存储在该物理内存区域里,用户需要在DMA传输结束后,将该物理内存区域里的数据提取出来。

    物理内存区域描述符的结构如下图所示(DMA_program.pdf手册的第2页的底部):


图2

    从上图可以看到,每个描述符为8个字节,头4个字节用于指定内存区域的起始物理内存地址(位0是保留的,始终为0),后4个字节的位1到位15用于指明该内存区域的尺寸大小(以字节为单位),内存区域的尺寸大小(Byte Count)不能超过64K字节的大小,当Byte Count为0时,则表示该区域为64K的大小。在后4个字节的最高位即上图中的EOT位,用于表示该描述符是否为表中的最后一个描述符,DMA传输时,会从第一个描述符开始处理,一直到最后一个描述符被处理完为止。

    在zenglOX的当前版本里,只在表中设置了一个描述符,创建描述符和创建物理内存区域相关的代码位于zlox_ide.c文件的zlox_ide_probe函数中:

// 通过PCI配置空间, 检测IDE控制器是否支持Bus Master, 如果支持,则对
// Bus Master IDE的status状态寄存器, Descriptor Table Pointer Register即物理内存区域描述符表指针寄存器
// 进行配置, 从而为DMA传输作准备
ZLOX_SINT32 zlox_ide_probe()
{
	.....................................................
	ZLOX_UINT32 prdt_phy = 0;
	ZLOX_UINT32 prdt_u32 = zlox_kmalloc_ap(8, &prdt_phy);
	ZLOX_UINT32 pr_mem_buf_phy = 0;
	ide_pr_mem_buf = (ZLOX_UINT8 *)zlox_kmalloc_ap(2048, &pr_mem_buf_phy);
	ZLOX_UINT32 * prdt_ptr = (ZLOX_UINT32 *)prdt_u32;
	prdt_ptr[0] = pr_mem_buf_phy;
	//prdt_ptr[1] = (2048 | 0x80000000); //如果将物理内存区域的大小设置为2048的话, 则bochs下运行时会出现问题
	prdt_ptr[1] = (1024 | 0x80000000);
	zlox_outl(ide_reg_base + 0x4, prdt_phy);

	prdt_phy = 0;
	prdt_u32 = zlox_kmalloc_ap(8, &prdt_phy);
	pr_mem_buf_phy = 0;
	ide_pr_mem_buf2 = (ZLOX_UINT8 *)zlox_kmalloc_ap(2048, &pr_mem_buf_phy);
	prdt_ptr = (ZLOX_UINT32 *)prdt_u32;
	prdt_ptr[0] = pr_mem_buf_phy;
	//prdt_ptr[1] = (2048 | 0x80000000);
	prdt_ptr[1] = (1024 | 0x80000000);
	zlox_outl(ide_reg_base + 0xC, prdt_phy);
	.....................................................
}


    上面的代码中,使用zlox_kmalloc_ap函数分别为两个channel通道各分配了一个描述符,以及描述符所对应的物理内存区域。ide_pr_mem_buf为第一个通道的物理内存区域,ide_pr_mem_buf2则为第二个通道的物理内存区域。虽然物理内存区域被分配了2048字节的大小,但是在设置时,目前只能设置为1024字节的大小,因为设置为2048字节的话,在bochs下运行时会出现问题。(在之前介绍ATA驱动及ATAPI驱动相关的文章里,我们介绍过channel通道的概念,每个channel通道上还可以接一个master主设备与一个slave从设备)

    在DMA_program.pdf手册的第3页到第4页,详细介绍了Bus Master IDE的各种寄存器,主要是三个寄存器:Bus Master IDE Command(命令寄存器),Bus Master IDE Status(状态寄存器),以及Descriptor Table Pointer(物理内存区域描述符表的指针寄存器)。

    这三个寄存器都是存储在IDE控制器的芯片里的,并且第一个通道和第二个通道都对应有一组这样的寄存器,用于分别对这两个通道进行DMA的读写控制。

    刚才我们介绍了创建描述符表的代码,这些被创建的描述符表的指针信息,需要加载到对应通道的Descriptor Table Pointer寄存器中,才能生效。

    在DMA_program.pdf手册的第3页,介绍了这些寄存器的偏移值:

Offset from
Base Address
Register Register
Access
00h Bus Master IDE Command register Primary
第一个通道(主通道)对应的Bus Master命令寄存器
R/W
01h Device Specific  
02h Bus Master IDE Status register Primary
主通道对应的Bus Master状态寄存器
RWC
03h Device Specific  
04h-07h Bus Master IDE PRD Table Address Primary
主通道对应的Bus Master物理内存区域描述符表
指针寄存器
R/W
08h Bus Master IDE Command register Secondary
第二个通道(从通道)对应的Bus Master命令寄存器
R/W
09h Device Specific  
0Ah Bus Master IDE Status register Secondary
从通道对应的Bus Master状态寄存器
RWC
0Bh Device Specific  
0Ch-0Fh Bus Master IDE PRD Table Address Secondary
从通道对应的Bus Master物理内存区域描述符表
指针寄存器
R/W

    上表中,左侧第一列表示寄存器的Offset偏移值(这些偏移值需要配合下面要介绍的Base基地址才能得出完整的I/O地址值),第二列表示Offset对应的寄存器名(Device Specific部分不用去关注),最后一列表示寄存器的读写权限,R/W表示该寄存器可读写,RWC表示寄存器可读写(但是如果要将该寄存器里的二进制位清零,则需要向对应的二进制位写入1,而不是写入0)。

    在介绍各寄存器里的二进制位含义之前,我们先看下如何获取Base基地址的值。

    在DMA_program.pdf手册的第6页(即最后一页)的底部,介绍了该Base值位于PCI配置空间的0x20的偏移位置处,并且还介绍了该PCI配置空间所对应的PCI设备的特征信息:当PCI配置空间里的Class Code值为0x01,Subclass值为0x01,以及Prog IF部分的最高位为1时,就表示对应的PCI设备是一个IDE控制器,并且该控制器具有DMA总线传输功能,因此就有了zlox_ide.c文件中的zlox_ide_probe函数里的如下代码:

ZLOX_SINT32 zlox_ide_probe()
{
	for(ZLOX_UINT32 i = 0; i < pci_devconf_lst.count ;i++)
	{
		if(pci_devconf_lst.ptr[i].cfg_hdr.class_code == 0x01 &&
			pci_devconf_lst.ptr[i].cfg_hdr.sub_class == 0x01 &&
			((pci_devconf_lst.ptr[i].cfg_hdr.prog_if & 0x80) != 0))
		{
			//if(pci_devconf_lst.ptr[i].cfg_hdr.vend_id != 0x8086 || 
			//	pci_devconf_lst.ptr[i].cfg_hdr.dev_id != 0x7111)
			//	return -1;

			ide_reg_base = pci_devconf_lst.ptr[i].cfg_hdr.bars[4];
			if((ide_reg_base & 0x1) == 0)
			{
				return -1;
			}
			else
				ide_reg_base = ide_reg_base & (~0x03);
			................................................
			................................................
		}
}


    如果读者对PCI及PCI配置空间的概念,不是很清楚的话,可以参考zenglOX v2.0.0版本对应的官方文章,上面代码中,获取到的ide_reg_base基地址需要将最低两位清零,因为它的位0用于表示该地址是否是一个I/O地址类型,位1则是一个保留位,详情请参考 http://wiki.osdev.org/PCI

    有了基地址后,只要再加上前面Bus Master寄存器表格里的偏移值,就可以得到所需寄存器的I/O端口地址了,通过这些I/O端口就可以对寄存器进行读写操作了。

    下面我们来看下Bus Master各寄存器里的二进制位的含义。

    DMA_program.pdf手册的第3页介绍了Bus Master IDE Command命令寄存器的二进制结构,如下表所示:

Bit Description
7:4 Reserved. Must return 0 on reads.
保留位,读取时必须返回0
3 Read or Write Control: This bit sets the direction of the bus master transfer: when set to zero,
PCI bus master reads are performed. When set to one, PCI bus master writes are performed. This
bit must NOT be changed when the bus master function is active.
读写控制位,这里的英文说明有问题,应该是当执行READ DMA即DMA读操作时,该二进制位需要
设置为1,当执行WRITE DMA即DMA写操作时,该二进制位需要被清0,该二进制位必须在向ATA
驱动器发送具体的读写命令之前进行设置。
2:1 Reserved. Must return 0 on reads.
保留位,读取时必须返回0
0 Start/Stop Bus Master: DMA传输的启动和停止位,当向该二进制位写入1时,就会立即触发DMA的
传输操作,如果在传输的过程中写入0的话,就会中止DMA的传输,并且所有状态信息都会被丢失,因此,
一般是在DMA传输后(可以通过状态寄存器里的中断位或Active位来判断传输是否结束),才向该位写入0 。

    DMA_program.pdf手册的第4页介绍了Bus Master IDE status状态寄存器的二进制结构,如下表所示:

Bit Description
7 Simplex only: This read-only bit indicates whether or not both bus master channels (primary
and secondary) can be operated at the same time. If the bit is a '0', then the channels operate
independently and can be used at the same time. If the bit is a '1', then only one channel may be
used at a time.
该二进制位(只读)用于判断主从两个通道是否可以同时执行DMA总线操作,如果为0,则表示两个
通道的操作是独立的,可以同时执行总线操作,当为1时,则同一时间内,只能有一个通道可以被用于
执行总线操作。
6 Drive 1 DMA Capable: This read/write bit is set by device dependent code (BIOS or device
driver) to indicate that drive 1 for this channel is capable of DMA transfers, and that the
controller has been initialized for optimum performance.
该二进制位用于判断Drive 1即通道上的slave从设备是否具备DMA的传输能力,这通常是由设备相关的代码
(例如BIOS或设备驱动程式)来进行设置,在bochs下,一开始它的BIOS不会对其进行设置,因此,需要我们的驱动程式
来将其设置为1。
5 Drive 0 DMA Capable: This read/write bit is set by device dependent code (BIOS or device
driver) to indicate that drive 0 for this channel is capable of DMA transfers, and that the
controller has been initialized for optimum performance.
用于判断Drive 0即通道上的master主设备是否具备DMA的传输能力,驱动程式中也会对其进行设置。
4:3 Reserved. Must return 0 on reads.
保留位,读取时必须返回0
2 Interrupt: This bit is set by the rising edge of the IDE interrupt line. This bit is cleared when a
'1' is written to it by software. Software can use this bit to determine if an IDE device has
asserted its interrupt line. When this bit is read as a one, all data transfered from the drive is
visible in system memory.
中断位:通过该二进制位,可以检测通道上的IDE设备(例如ATA驱动器)是否发送了中断信号。
驱动程式里可以通过向该位写入1来将其清零。当通道中出现中断信号时,即该位被设置为1时,
就表示DMA操作执行完毕,可以配合下面的Error错误位,来判断DMA传输过程中,是否发生了错误。
1 Error: This bit is set when the controller encounters an error in transferring data to/from
memory. The exact error condition is bus specific and can be determined in a bus specific
manner. This bit is cleared when a '1' is written to it by software.
错误位:当DMA数据传输过程中,发生错误时,该二进制位就会被设置。具体的出错原因,可以
通过PCI配置空间里的Status状态寄存器的值,以及ATA驱动器的状态寄存器的值,来进行判断。
0 Bus Master IDE Active: This bit is set when the Start bit is written to the Command register.
This bit is cleared when the last transfer for a region is performed, where EOT for that region is
set in the region descriptor. It is also cleared when the Start bit is cleared in the Command
register. When this bit is read as a zero, all data transfered from the drive during the previous
bus master command is visible in system memory, unless the bus master command was aborted.
通过该Active二进制位,可以判断DMA总线操作是否开始,以及总线操作是否执行完毕。
当该位被设置为1时,则表示DMA操作已经开始。当该位被清零时,则表示DMA传输结束。
可以配合上面的Error错误位,来判断传输过程中,是否发生了错误。

    DMA_program.pdf手册的第4页的底部,介绍了Descriptor Table Pointer(物理内存区域描述符表指针寄存器)的二进制结构,如下表所示:

Bit Description
31:2 Base address of Descriptor table. Corresponds to A[31:2]
描述符表的物理内存地址。
1:0 reserved
保留位

    驱动程式中,需要将创建的描述符表的物理内存基址,设置到该寄存器中,才能生效。

    在DMA_program.pdf手册的第5页,给出了执行DMA操作的通用步骤:

1) Software prepares a PRD Table in system memory. Each PRD is 8 bytes long and consists of an
address pointer to the starting address and the transfer count of the memory buffer to be
transferred. In any given PRD Table, two consecutive PRDs are offset by 8-bytes and are aligned
on a 4-byte boundary.
1):驱动程式需要在系统内存里,分配一个PRD表即物理内存区域描述符表,
表中的每一项都为8字节尺寸的描述符,还要为描述符分配对应的物理内存区域。
并且,描述符的起始物理地址,需要按照4字节来对齐。

2) Software provides the starting address of the PRD Table by loading the PRD Table Pointer
Register . The direction of the data transfer is specified by setting the Read/Write Control bit.
Clear the Interrupt bit and Error bit in the Status register.
2):驱动程式需要将PRD表的起始物理内存地址,加载到PRD表指针寄存器中。
每次DMA读写操作之前,需要设置Bus Master命令寄存器里的读写控制位,以控制DMA数据传输的方向,
在开启传输之前,还需要将Bus Master状态寄存器里的中断位及错误位清零。

3) Software issues the appropriate DMA transfer command to the disk device.
3):驱动程式向ATA磁盘设备发送相应的DMA读写命令(例如READ DMA命令或WRITE DMA命令等)

4) Engage the bus master function by writing a '1' to the Start bit in the Bus Master IDE Command
Register for the appropriate channel.
4):在向ATA设备发送DMA读写命令后,需要将当前通道对应的Bus Master命令寄存器里的Start/Stop Bus
Master位设置为1,从而正式开启DMA的传输操作。

5) The controller transfers data to/from memory responding to DMA requests from the IDE device.
5):IDE控制器就会通过DMA总线,将数据从IDE设备传输到物理内存区域中,或者从物理内存区域
传输到IDE设备中。

6) At the end of the transfer the IDE device signals an interrupt.
6):DMA传输结束后,IDE设备就会发送中断信号。

7) In response to the interrupt, software resets the Start/Stop bit in the command register. It then
reads the controller status and then the drive status to determine if the transfer completed
successfully.
7):驱动程式可以通过Bus Master Status状态寄存器里的中断位来判断是否产生了中断信号,
当产生了中断信号时,则说明数据传输完毕了。此外,VirtualBox下,当ATA与ATAPI设备位于
同一个通道中时,可能会出现收不到中断信号的情况,这种情况下,可以通过状态寄存器里的
Bus Master IDE Active位来判断数据是否传输完毕了,最后,再配合状态寄存器的Error错误
位,就可以检测数据传输过程中是否发生了错误。

    zenglOX里的zlox_ide.c文件中的代码,就是根据上面的步骤来写的:

.......................................................

extern ZLOX_PCI_DEVCONF_LST pci_devconf_lst;
ZLOX_UINT32 ide_reg_base = 0;
ZLOX_UINT8 * ide_pr_mem_buf = ZLOX_NULL;
ZLOX_UINT8 * ide_pr_mem_buf2 = ZLOX_NULL;
ZLOX_UINT32 ide_pci_index;
ZLOX_BOOL ide_can_dma = ZLOX_FALSE;

// 通过PCI配置空间, 检测IDE控制器是否支持Bus Master, 如果支持,则对
// Bus Master IDE的status状态寄存器, Descriptor Table Pointer Register即物理内存区域描述符表指针寄存器
// 进行配置, 从而为DMA传输作准备
ZLOX_SINT32 zlox_ide_probe()
{
	for(ZLOX_UINT32 i = 0; i < pci_devconf_lst.count ;i++)
	{
		if(pci_devconf_lst.ptr[i].cfg_hdr.class_code == 0x01 &&
			pci_devconf_lst.ptr[i].cfg_hdr.sub_class == 0x01 &&
			((pci_devconf_lst.ptr[i].cfg_hdr.prog_if & 0x80) != 0))
		{
			//if(pci_devconf_lst.ptr[i].cfg_hdr.vend_id != 0x8086 || 
			//	pci_devconf_lst.ptr[i].cfg_hdr.dev_id != 0x7111)
			//	return -1;

			ide_reg_base = pci_devconf_lst.ptr[i].cfg_hdr.bars[4];
			if((ide_reg_base & 0x1) == 0)
			{
				return -1;
			}
			else
				ide_reg_base = ide_reg_base & (~0x03);
			ZLOX_UINT32 ide_reg_status = zlox_inb(ide_reg_base + 0x2);
			zlox_outb(ide_reg_base + 0x2, ide_reg_status | 0x60);
			ide_reg_status = zlox_inb(ide_reg_base + 0xa);
			zlox_outb(ide_reg_base + 0xa, ide_reg_status | 0x60);

			ZLOX_UINT16 cr = zlox_pci_reg_inw(pci_devconf_lst.ptr[i].cfg_addr, ZLOX_PCI_REG_COMMAND);
			if (!(cr & ZLOX_PCI_COMMAND_MAST_EN))
				zlox_pci_reg_outw(pci_devconf_lst.ptr[i].cfg_addr, ZLOX_PCI_REG_COMMAND, cr | ZLOX_PCI_COMMAND_MAST_EN);

			ZLOX_UINT32 prdt_phy = 0;
			ZLOX_UINT32 prdt_u32 = zlox_kmalloc_ap(8, &prdt_phy);
			ZLOX_UINT32 pr_mem_buf_phy = 0;
			ide_pr_mem_buf = (ZLOX_UINT8 *)zlox_kmalloc_ap(2048, &pr_mem_buf_phy);
			ZLOX_UINT32 * prdt_ptr = (ZLOX_UINT32 *)prdt_u32;
			prdt_ptr[0] = pr_mem_buf_phy;
			//prdt_ptr[1] = (2048 | 0x80000000); //如果将物理内存区域的大小设置为2048的话, 则bochs下运行时会出现问题
			prdt_ptr[1] = (1024 | 0x80000000);
			zlox_outl(ide_reg_base + 0x4, prdt_phy);

			prdt_phy = 0;
			prdt_u32 = zlox_kmalloc_ap(8, &prdt_phy);
			pr_mem_buf_phy = 0;
			ide_pr_mem_buf2 = (ZLOX_UINT8 *)zlox_kmalloc_ap(2048, &pr_mem_buf_phy);
			prdt_ptr = (ZLOX_UINT32 *)prdt_u32;
			prdt_ptr[0] = pr_mem_buf_phy;
			//prdt_ptr[1] = (2048 | 0x80000000);
			prdt_ptr[1] = (1024 | 0x80000000);
			zlox_outl(ide_reg_base + 0xC, prdt_phy);

			ide_pci_index = i;
			ide_can_dma = ZLOX_TRUE;
			return 0;
		}
	}
	return -1;
}

// 在发送ATA磁盘读写命令之前, 需要先将Bus Master IDE Command命令寄存器的读写位进行设置, 让IDE控制器知道
// 当前是读操作还是写操作, 另外还需要将Bus Master IDE Status状态寄存器里的中断位与错误位清零
ZLOX_SINT32 zlox_ide_before_send_command(ZLOX_UINT8 direction, ZLOX_UINT16 bus)
{
	if(ide_can_dma == ZLOX_FALSE)
		return -1;
	ZLOX_UINT32 cmd_port;
	if(bus == ZLOX_ATA_BUS_PRIMARY)
		cmd_port = ide_reg_base + 0x0;
	else
		cmd_port = ide_reg_base + 0x8;
	if(direction == 0)
		zlox_outb(cmd_port, 0x08);
	else
		zlox_outb(cmd_port, 0);
	ZLOX_UINT32 ide_reg_status;
	ZLOX_UINT16 status_port;
	if(bus == ZLOX_ATA_BUS_PRIMARY)
		status_port = ide_reg_base + 0x2;
	else
		status_port = ide_reg_base + 0xa;
	ide_reg_status = zlox_inb(status_port);
	ide_reg_status |= 0x6; // 只有向中断位和Error错误位写入1, 才能将其清零
	zlox_outb(status_port, ide_reg_status);
	return 0;
}

// 通过将Bus Master IDE Command命令寄存器的位0即Start/Stop Bus Master位设置为1, 就可以触发DMA总线读写操作
ZLOX_SINT32 zlox_ide_start_dma(ZLOX_UINT16 bus)
{
	if(ide_can_dma == ZLOX_FALSE)
		return -1;
	ZLOX_UINT16 cmd_port;
	if(bus == ZLOX_ATA_BUS_PRIMARY)
		cmd_port = ide_reg_base + 0x0;
	else
		cmd_port = ide_reg_base + 0x08;
	ZLOX_UINT32 ide_reg_command = zlox_inb(cmd_port);
	zlox_outb(cmd_port, ide_reg_command | 0x1);
	return 0;
}

// 在DMA总线读写操作完毕后, 需要通过Bus Master IDE status状态寄存器来检测中断位 或 Bus Master IDE Active位,
// 当中断位为1时(即ATA驱动器发送了中断信号), 则说明读写操作完成, 当然也有可能是发生了错误而中断了传输, 要判断是否发生错误
// 可以通过状态寄存器的Error错误位来进行检测
// 此外, 由于VirtualBox下, 当ATA与ATAPI驱动位于同一个channel通道时, 因为它们共用一个中断信号, 因此会导致对ATAPI读操作后, ATA驱动无法
// 检测到中断信号的情况, 这种情况下, 我们就需要通过Bus Master IDE status状态寄存器的Bus Master IDE Active位来进行检测了,
// 当Active位为0时, 则说明数据传输完毕, 或者发生错误而中断了传输, 同样也可以配合Error错误位来进行判断
ZLOX_SINT32 zlox_ide_after_send_command(ZLOX_UINT16 bus)
{
	if(ide_can_dma == ZLOX_FALSE)
		return -1;
	ZLOX_UINT8 ide_reg_status = 0;
	for(;;)
	{
		if(bus == ZLOX_ATA_BUS_PRIMARY)
			ide_reg_status = zlox_inb(ide_reg_base + 0x2);
		else
			ide_reg_status = zlox_inb(ide_reg_base + 0xa);
		if(((ide_reg_status & 0x4) != 0) || ((ide_reg_status & 0x1) == 0))
		{
			if((ide_reg_status & 0x2) != 0)
			{
				if(bus == ZLOX_ATA_BUS_PRIMARY)
					zlox_outb(ide_reg_base + 0x0, 0);
				else
					zlox_outb(ide_reg_base + 0x8, 0);
				ZLOX_UINT16 status = pci_devconf_lst.ptr[ide_pci_index].cfg_hdr.status;
				zlox_monitor_write("ide error , pci status: ");
				zlox_monitor_write_hex((ZLOX_UINT32)status);
				zlox_monitor_write("\n");
				zlox_monitor_write("ide error , pci dev_id: ");
				zlox_monitor_write_hex((ZLOX_UINT32)pci_devconf_lst.ptr[ide_pci_index].cfg_hdr.dev_id);
				zlox_monitor_write("\n");
				return -1;
			}
			if(bus == ZLOX_ATA_BUS_PRIMARY)
				zlox_outb(ide_reg_base + 0x0, 0);
			else
				zlox_outb(ide_reg_base + 0x8, 0);
			return 1;
		}
		else
			asm volatile ("pause");
	}
	return 0;
}

// 从物理内存区域中获取DMA传输的数据
ZLOX_SINT32 zlox_ide_get_buffer_data(ZLOX_UINT8 * buffer, ZLOX_UINT32 len, ZLOX_UINT16 bus)
{
	if(ide_can_dma == ZLOX_FALSE)
		return -1;
	if(bus == ZLOX_ATA_BUS_PRIMARY)
		zlox_memcpy(buffer, ide_pr_mem_buf, len);
	else
		zlox_memcpy(buffer, ide_pr_mem_buf2, len);
	return 0;
}

// 要写入ATA磁盘的数据, 需要先写入到物理内存区域中, 稍后再通过DMA总线传输到磁盘里
ZLOX_SINT32 zlox_ide_set_buffer_data(ZLOX_UINT8 * buffer, ZLOX_UINT32 len, ZLOX_UINT16 bus)
{
	if(ide_can_dma == ZLOX_FALSE)
		return -1;
	if(bus == ZLOX_ATA_BUS_PRIMARY)
		zlox_memcpy(ide_pr_mem_buf, buffer, len);
	else
		zlox_memcpy(ide_pr_mem_buf2, buffer, len);
	return 0;
}


    此外,zlox_ata.c文件里,在执行读写磁盘的操作时,就会调用上面zlox_ide.c文件中的函数,来实现DMA的数据传输:

// Read/Write From ATA Drive, direction == 0 is read, direction == 1 is write
ZLOX_SINT32 zlox_ide_ata_access(ZLOX_UINT8 direction, ZLOX_UINT8 ide_index, ZLOX_UINT32 lba, 
                             ZLOX_UINT8 numsects, ZLOX_UINT8 * buffer)
{
	....................................................

	// (V) Select the command and send it;
	if (lba_mode == 0 && direction == 0)
		cmd = ide_can_dma ? 0xC8 : 0x20; // READ DMA OR READ SECTOR(S)
	if (lba_mode == 1 && direction == 0) 
		cmd = ide_can_dma ? 0xC8 : 0x20; // READ DMA OR READ SECTOR(S)
	if (lba_mode == 2 && direction == 0) 
		cmd = ide_can_dma ? 0x25 : 0x24; // READ DMA EXT or READ SECTOR(S) EXT command (support from ATA-ATAPI-6)
	if (lba_mode == 0 && direction == 1)
		cmd = ide_can_dma ? 0xCA : 0x30; // WRITE DMA or WRITE SECTOR(S) command
	if (lba_mode == 1 && direction == 1)
		cmd = ide_can_dma ? 0xCA : 0x30; // WRITE DMA or WRITE SECTOR(S) command
	if (lba_mode == 2 && direction == 1) 
		cmd = ide_can_dma ? 0x35 : 0x34; // WRITE DMA EXT or WRITE SECTOR(S) EXT command (support from ATA-ATAPI-6)
	
	if(ide_can_dma)
	{
		zlox_ide_before_send_command(direction, bus);
		if(direction == 1)
			zlox_ide_set_buffer_data(buffer, numsects * ZLOX_ATA_SECTOR_SIZE, bus);
	}

	zlox_outb (ZLOX_ATA_COMMAND (bus), cmd);

	if(ide_can_dma)
		zlox_ide_start_dma(bus);

	if(ide_can_dma)
	{
		// DMA Read.
		if (direction == 0)
		{
			if(zlox_ide_after_send_command(bus) == -1)
			{
				status = zlox_inb (ZLOX_ATA_COMMAND (bus));
				if (status & 0x1) {
					ZLOX_UINT8 error = zlox_inb (ZLOX_ATA_FEATURES (bus));
					zlox_monitor_write("ata error , error reg: ");
					zlox_monitor_write_hex((ZLOX_UINT32)error);
					zlox_monitor_write("\n");
					return -1;
				}
				return -1;
			}
			zlox_ide_get_buffer_data(buffer, numsects * ZLOX_ATA_SECTOR_SIZE, bus);
		}
		else // DMA Write.
		{
			if(zlox_ide_after_send_command(bus) == -1)
			{
				status = zlox_inb (ZLOX_ATA_COMMAND (bus));
				if (status & 0x1) {
					ZLOX_UINT8 error = zlox_inb (ZLOX_ATA_FEATURES (bus));
					zlox_monitor_write("ata error , error reg: ");
					zlox_monitor_write_hex((ZLOX_UINT32)error);
					zlox_monitor_write("\n");
					return -1;
				}
				return -1;
			}
		}
	}
	else
	{
		// PIO Read.
		if (direction == 0)
		{
			....................................................
		}
		else // PIO Write.
		{
			....................................................
		}
	}

	zlox_outb(ZLOX_ATA_DCR(bus), control_orig);
	return 0;
}


    上面函数中出现的ide_can_dma全局变量,是zlox_ide.c文件里定义的变量,用于表示当前的系统环境下,是否支持DMA传输,如果支持,则会向ATA驱动器发送0xC8(即READ DMA命令)或者发送0xCA(即WRITE DMA命令)等。在向ATA驱动器发送命令之前,还需要先通过zlox_ide.c文件中定义的zlox_ide_before_send_command函数,来设置Bus Master Command命令寄存器里的读写控制位(同时将状态寄存器里的中断位与错误位清零),如果是WRITE DMA之类的写入操作,还要通过zlox_ide_set_buffer_data函数将需要写入的数据,先写入到物理内存区域里。

    接着,在向ATA驱动器发送了READ DMA或WRITE DMA之类的命令后,就可以通过zlox_ide_start_dma函数来开启DMA传输了。在开启DMA传输后,需要通过zlox_ide_after_send_command函数来循环判断传输是否结束,并检测传输过程中是否发生了错误。

    最后,DMA传输结束后,如果是READ DMA之类的读磁盘的操作,还需要通过zlox_ide_get_buffer_data函数,将物理内存区域里的数据给获取出来。

    有关ATA读写命令的详情,请参考zenglOX v1.1.0版本对应的网盘里的 ATA-ATAPI-5.pdf 或者 ATA-ATAPI-6.pdf手册。

    以上就是zenglOX v2.4.0版本的主要内容。没有讲解到的代码,请自行调试分析。

    OK,就到这里,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: zenglOX v3.0.0与v3.0.1 GUI窗口界面

上一篇: zenglOX v2.3.0 移植zengl嵌入式脚本语言

相关文章

zenglOX v3.0.2 PNG(Portable Network Graphics) and BUG Fix

zenglOX v0.0.11 ELF format(ELF可执行文件格式)与execve系统调用

zenglOX v0.0.9 User Mode(用户模式)

zenglOX v1.4.0与v1.4.1 通过ATA驱动读写硬盘里的数据, BUG修复, VirtualBox与VMware的调试功能

zenglOX v2.2.0 ee(easy editor)文本编辑器 C标准库函数 uheap(单独的用户堆空间) atapi驱动BUG zenglfs文件系统BUG 分页BUG 堆算法BUG等修复

zenglOX v0.0.1 开发自己的操作系统