ZYB ARTICLES REPOS

通过PCI IDE Controller以DMA和LBA48读取ATA硬盘

仅考虑ATA硬盘的情况,一个IDE接口能接Master、Slave两个DRIVE。一个PC机上通常有两个IDE接口(IDE0, IDE1或ATA0, ATA1),通常称通道0、1。对于同一个IDE通道的两个DRIVE,共享同一组寄存器,它们之间的区分是通过Device寄存器的第4个bit位来实现的。0为Master,1为Slave。读硬盘有ATA_CMD_READ_PIO == 0x20ATA_CMD_READ_PIO_EXT == 0x24, ATA_CMD_READ_DMA == 0xC8, ATA_CMD_READ_DMA_EXT == 0x25几种方式:

网上的教程基本是LBA28+PIOLBA28+DMA的方式读硬盘,LBA48+PIO的很少,LBA48+DMA基本没有。本文就将详述LBA48+DMA读硬盘的方式,其它读硬盘的方式和读硬盘所需要的背景知识请参考网上其它的文档。

定义

以下代码用到的宏定义如下,更为详细的IDE寄存器说明请参考文档《AT Attachment with Packet Interface - 6》。

// 驱动器0,1的命令和控制寄存器地址
#define ATA_CHL0_CMD_BASE 0x1F0
#define ATA_CHL1_CMD_BASE 0x170
#define ATA_CHL0_CTL_BASE 0x3F6
#define ATA_CHL1_CTL_BASE 0x376

#define ATA_DATA 0

#define ATA_FEATURES 1

#define ATA_ERR 1
#define ATA_ERR_BB 0x80
#define ATA_ERR_ECC 0x40
#define ATA_ERR_ID 0x10
#define ATA_ERR_AC 0x04
#define ATA_ERR_TK 0x02
#define ATA_ERR_DM 0x01

#define ATA_NSECTOR 2

#define ATA_LBAL 3
#define ATA_LBAM 4
#define ATA_LBAH 5

// DEVICE寄存器
// bit7: Obsolete
// bit6: L 如果为1,LBA Mode
// bit5: Obsolete
// bit4: DRIVE
// bit[0, 3] HS 如果L为0就是磁头号Head Number,如果L为1,则为LBA的24-27位
#define ATA_LBA48_DEVSEL(dev) (0x40 | ((dev & 0x01) << 4))
#define ATA_DEVICE 6

#define ATA_CMD 7

// bit7: BSY Busy. If BSY==1, no other bits in the register are valid
// bit6: DRDY Drive Ready.
// bit5: DF/SE Device Fault / Stream Error
// bit4: # Command dependent. (formerly DSC bit)
// bit3: DRQ Data Request. (ready to transfer data)
// bit2: - Obsolete
// bit1: - Obsolete
// bit0: ERR
#define ATA_STATUS 7              /* controller status */
#define ATA_STATUS_BSY 0x80       /* controller busy */
#define ATA_STATUS_RDY 0x40       /* drive ready */
#define ATA_STATUS_WF 0x20        /* write fault */
#define ATA_STATUS_SEEK_CMPT 0x10 /* seek complete */
#define ATA_STATUS_DRQ 0x08       /* data transfer request */
#define ATA_STATUS_CRD 0x04       /* correct data */
#define ATA_STATUS_IDX 0x02       /* index pulse */
#define ATA_STATUS_ERR 0x01       /* error */

#define ATA_CMD_IDLE 0x00
#define ATA_CMD_RECALIBRATE 0x10
#define ATA_CMD_READ_PIO 0x20     /* read data */
#define ATA_CMD_READ_PIO_EXT 0x24 /* read data (LBA-48 bit)*/
#define ATA_CMD_READ_DMA 0xC8
#define ATA_CMD_READ_DMA_EXT 0x25 /* read data DMA LBA48 */
#define ATA_CMD_WRITE_PIO 0x30
#define ATA_CMD_WRITE_PIO_EXT 0x34
#define ATA_CMD_WRITE_DMA 0xCA
#define ATA_CMD_WRITE_DMA_EXT 0X35
#define ATA_CMD_READ_VERIFY 0x40
#define ATA_CMD_FORMAT 0x50
#define ATA_CMD_SEEK 0x70
#define ATA_CMD_DIAG 0x90
#define ATA_CMD_SPECIFY 0x91
#define ATA_CMD_IDENTIFY_PACKET 0xA1
#define ATA_CMD_IDENTIFY 0xEC

#define ATA_CTL 0
#define ATA_CTL_HOB 0x80        /* high order byte (LBA-48bit) */
#define ATA_CTL_NOECC 0x40      /* disable ecc retry */
#define ATA_CTL_EIGHTHEADS 0x08 /* more than 8 heads */
#define ATA_CTL_SRST 0x04       /* soft reset controller */
#define ATA_CTL_NIEN 0x02       /* disable interrupts */

#define ATA_GET_CHL(dev) (0) /* only support channel 0 */
#define ATA_GET_DEV(dev) (0) /* only support one hard disk */

#define REG_CMD_BASE(dev, offset) (ATA_GET_CHL(dev) ? (ATA_CHL1_CMD_BASE + offset) : (ATA_CHL0_CMD_BASE + offset))
#define REG_CTL_BASE(dev, offset) (ATA_GET_CHL(dev) ? (ATA_CHL1_CTL_BASE + offset) : (ATA_CHL0_CTL_BASE + offset))

#define REG_DATA(dev) REG_CMD_BASE(dev, ATA_DATA)
#define REG_ERR(dev) REG_CMD_BASE(dev, ATA_ERR)
#define REG_NSECTOR(dev) REG_CMD_BASE(dev, ATA_NSECTOR)
#define REG_LBAL(dev) REG_CMD_BASE(dev, ATA_LBAL)
#define REG_LBAM(dev) REG_CMD_BASE(dev, ATA_LBAM)
#define REG_LBAH(dev) REG_CMD_BASE(dev, ATA_LBAH)
#define REG_DEVICE(dev) REG_CMD_BASE(dev, ATA_DEVICE)
#define REG_STATUS(dev) REG_CMD_BASE(dev, ATA_STATUS)
#define REG_FEATURES(dev) REG_CMD_BASE(dev, ATA_FEATURES)

#define REG_CMD(dev) REG_CMD_BASE(dev, ATA_CMD)
#define REG_CTL(dev) REG_CTL_BASE(dev, ATA_CTL)

判断是否支持LBA48+DMA

在用LBA48+DMA的方式读硬盘前,先要确定硬盘是否支持,方法就是通过向硬盘发送ATA_CMD_IDENTIFY == 0xEC命令,让硬盘返回其identify数据。通过解析该数据,就可以判断了。

使用IDENTIFY命令步骤:

  1. 选择DRIVE构造命令,发送到Device寄存器(选择master发送: 0x00, 选择slave发送: 0x40)
  2. 发送IDENTIFY(0xEC)命令到该通道的命令寄存器

检查status寄存器:

  1. 若为0,就认为没有IDE
  2. 等到status的BSY位清除
  3. 等到status的DRQ位或ERR位设置
u16 identify[256];
void ata_read_identify(int dev) {  // 这里所用的dev是逻辑编号 ATA0、ATA1下的Master、Salve的dev分别为0,1,2,3
    outb(ATA_CTL_NIEN, REG_CTL(dev));                   // 在读IDENTIFY的时候禁用硬盘中断
    outb(0x00 | ((dev & 0x01) << 4), REG_DEVICE(dev));  // 根据文档P113,这里不用指定bit5, bit7,直接指示DRIVE就行
    outb(ATA_CMD_IDENTIFY, REG_CMD(dev));

    while (1) {
        u8 status = inb(REG_STATUS(dev));
        printk("hard disk status: %x %x\n", status, REG_STATUS(dev));
        if (status == 0) {
            panic("no ata device");
        }
        if ((status & ATA_STATUS_BSY) == 0 && (status & ATA_STATUS_DRQ) != 0) {
            break;
        }
    }

    insw(REG_DATA(dev), identify, SECT_SIZE / sizeof(u16));

    // 第49个word的第8个bit位表示是否支持DMA
    // 第83个word的第10个bit位表示是否支持LBA48,为1表示支持。
    // 第100~103个word的八个字节表示user的LBA最大值
    if ((identify[49] & (1 << 8)) != 0) {
        printk("support DMA\n");
    }

    if ((identify[83] & (1 << 10)) != 0) {
        printk("support LBA48\n");

        u64 lba = *(u64 *)(identify + 100);
        printk("hard disk size: %u MB\n", (lba * 512) >> 20);
    }
}

如果不想用这种一真读STATUS寄存器的方式来判断硬盘是否把identify数据准备好的话,可以用中断通知的方式。要使用中断通知,只需要把outb(ATA_CTL_NIEN, REG_CTL(dev));改成outb(0x00, REG_CTL(dev)); 即可。

LBA48读取

用LBA48读取硬盘,跟LBA28类似。顺序是:

  1. 等待硬盘的REG_STATUS寄存器READY
  2. 视在硬盘读完后是否触发中断,发送不同的命令字到REG_CTL。若需要中断,发送0x00;若不需要,发送ATA_CTL_NIEN
  3. 写命令到REG_DEVICE寄存器,选择操作硬盘的Master还是Slave驱动器。同时要设置L比特位为1,也就是第6个比特位。
  4. 写扇区数高字节(扇区最大总数为两个字节,也就是65535)
  5. 写LBA48地址的第3个字节到REG_LBAL寄存器
  6. 写LBA48地址的第4个字节到REG_LBAM寄存器
  7. 写LBA48地址的第5个字节到REG_LBAH寄存器
  8. 写扇区数低字节
  9. 写LBA48地址的第0个字节到REG_LBAL寄存器
  10. 写LBA48地址的第1个字节到REG_LBAM寄存器
  11. 写LBA48地址的第2个字节到REG_LBAH寄存器

DMA读取

DMA读取最主要的是步骤如下,涉及PCI的参见后文:

  1. 停止该PCI设备的DMA
  2. 配置描述表,这样硬件才知道往哪DMA数据
  3. 清除PCI状态寄存器(不是硬盘的REG_STATUS寄存器)上的中断和错误位(写1清除)
  4. 按LBA48读硬盘的流程,初始化所有硬盘寄存器,注意要开启中断通知
  5. 向硬盘的REG_CMD寄存器发送ATA_CMD_READ_DMA_EXT命令
  6. 将该PCI设备的PCI_COMMAND寄存器的第2个比特位,也就是PCI_COMMAND_MASTER位置1,让该PCI设备可以主控PCI总线
  7. 向该PCI设备的bus_cmd寄存器(不是PCI_COMMAND寄存器)发送PCI_IDE_CMD_WRITE == 0x08(操作系统读硬盘对硬盘来说是写出)和PCI_IDE_CMD_START == 0x01指令,开启DMA。
  8. 不要忘记在中断函数中将PCI_COMMANDPCI_COMMAND_MASTER位清0
// ATA_CMD_READ_DMA_EXT
void ata_dma_read_ext(int dev, uint64_t pos, uint16_t count, void *dest) {
    // 停止DMA
    outb(PCI_IDE_CMD_STOP, ide_pci_controller.bus_cmd);

    // 配置描述符表
    unsigned long dest_paddr = va2pa(dest);
    ide_pci_controller.prdt[0].phys_addr = dest_paddr;
    ide_pci_controller.prdt[0].byte_count = SECT_SIZE;
    ide_pci_controller.prdt[0].reserved = 0;
    ide_pci_controller.prdt[0].eot = 1;
    outl(va2pa(ide_pci_controller.prdt), ide_pci_controller.bus_prdt);

    printk("paddr: %x prdt: %x %x prdte %x %x\n", dest_paddr, ide_pci_controller.prdt, va2pa(ide_pci_controller.prdt),
           ide_pci_controller.prdt[0].phys_addr, *(((unsigned int *)ide_pci_controller.prdt) + 1));

    // 清除中断位和错误位
    // 这里清除的方式是是设置1后清除
    outb(PCI_IDE_STATUS_INTR | PCI_IDE_STATUS_ERR, ide_pci_controller.bus_status);

    // 等待硬盘不BUSY
    while (inb(REG_STATUS(dev)) & ATA_STATUS_BSY) {
        nop();
    }

    // 不再设置nIEN,DMA需要中断
    outb(0x00, REG_CTL(dev));

    // 选择DRIVE
    outb(ATA_LBA48_DEVSEL(dev), REG_DEVICE(dev));

    // 先写扇区数的高字节
    outb((count >> 8) & 0xFF, REG_NSECTOR(dev));

    // 接着写LBA48,高三个字节
    outb((pos >> 24) & 0xFF, REG_LBAL(dev));
    outb((pos >> 32) & 0xFF, REG_LBAM(dev));
    outb((pos >> 40) & 0xFF, REG_LBAH(dev));

    // 再写扇区数的低字节
    outb((count >> 0) & 0xFF, REG_NSECTOR(dev));

    // 接着写LBA48,低三个字节
    outb((pos >> 0) & 0xFF, REG_LBAL(dev));
    outb((pos >> 8) & 0xFF, REG_LBAM(dev));
    outb((pos >> 16) & 0xFF, REG_LBAH(dev));

    // 等待硬盘READY
    while (inb(REG_STATUS(dev)) & ATA_STATUS_RDY == 0) {
        nop();
    }

    outb(ATA_CMD_READ_DMA_EXT, REG_CMD(dev));

    // 这一句非常重要,如果不加这一句
    // 在qemu中用DMA的方式读数据就会读不到数据,而只触是发中断,然后寄存器(Bus Master IDE Status
    // Register)的值会一直是5 也就是INTERRUPT和和ACTIVE位是1,正常应该是4,也就是只有INTERRUPT位为1
    // 在bochs中则加不加这一句不会有影响,都能正常读到数据
    unsigned int v = pci_read_config_word(pci_cmd(ide_pci_controller.pci, PCI_COMMAND));
    printk(" ide pci command %04x\n", v);
    pci_write_config_word(v | PCI_COMMAND_MASTER, pci_cmd(ide_pci_controller.pci, PCI_COMMAND));

    // 指定DMA操作为读取硬盘操作,内核用DMA读取,对硬盘而言是写出
    // 并设置DMA的开始位,开始DMA
    outb(PCI_IDE_CMD_WRITE | PCI_IDE_CMD_START, ide_pci_controller.bus_cmd);
}

其中PRDT的每一项定义如下

需要指出的是phys_addr,不能跨64KB物理页

typedef struct prdte {
    uint32_t phys_addr;
    uint32_t byte_count : 16;
    uint32_t reserved : 15;
    uint32_t eot : 1;
} prdte_t;

另外这里需要特别提醒不要忘记如下操作:

    unsigned int v = pci_read_config_word(pci_cmd(ide_pci_controller.pci, PCI_COMMAND));
    printk(" ide pci command %04x\n", v);
    pci_write_config_word(v | PCI_COMMAND_MASTER, pci_cmd(ide_pci_controller.pci, PCI_COMMAND));

否则,会出现在bochs能读到数据,在qemu上就读不到硬盘数据的情况!!!

以下是通过LBA48+DMA读取0号扇区的情况,qemu和bochs都能顺利读到。

PCI设备

每个PCI设备都有一组寄存器如下,读取它们的方法请参考其它文档。

/*
 * 31                        16 15                         0
 * +---------------------------+---------------------------+ 00H
 * |         Device ID         |          Vendor ID        |
 * +---------------------------+---------------------------+ 04H
 * |           Status          |          Command          |
 * +-----------------------------------------+-------------+ 08H
 * |  Class Code |  Subclass   |   Prog IF   |  Revision   |
 * +-------------+-------------+-------------+-------------+ 0CH
 * |    BIST     | Header Type |LatencyTimer |CacheLineSize|           // BITS: built-in self test
 * +-------------+-------------+-------------+-------------+ 10H
 * |                Base Address Register 0                |
 * +-------------------------------------------------------+ 14H
 * |                Base Address Register 1                |
 * +-------------------------------------------------------+ 18H
 * |                Base Address Register 2                |
 * +-------------------------------------------------------+ 1CH
 * |                Base Address Register 3                |
 * +-------------------------------------------------------+ 20H
 * |                Base Address Register 4                |
 * +-------------------------------------------------------+ 24H
 * |                Base Address Register 5                |
 * +-------------------------------------------------------+ 28H
 * |                  CardBus CIS Pointer                  |
 * +---------------------------+---------------------------+ 2CH
 * |        System ID          |        Subsystem ID       |
 * +---------------------------+---------------------------+ 30H
 * |               Expansion ROM Base Address              |
 * +-----------------------------------------+-------------+ 34H
 * |/////////////////////////////////////////|Capabilities |
 * |/////////////////////////////////////////|Pointer      |
 * +-----------------------------------------+-------------+ 38H
 * |///////////////////////////////////////////////////////|
 * +-------------+------------+-------------+--------------+ 3CH
 * | Max Latency | Min Grant  |Interrupt PIN|Interrupt Line|
 * +-------------+------------+-------------+--------------+ 40H
 */

其中PCI_COMMAND就是图中的Command寄存器。bus_cmd寄存器是Base Address Register 4

从`Base Address Register 4开始有3个寄存器。

偏移 名字 长度 通道
0x00 bus_cmd 1 IDE总线命令寄存器
0x02 bus_status 1 IDE总线状态寄存器
0x04 bus_prdt 4 就是PRDT的物理地址
0x08 bus_cmd 1 IDE总线命令寄存器
0x0A bus_status 1 IDE总线状态寄存器
0x0C bus_prdt 4 就是PRDT的物理地址

帮助读写PCI设备寄存器的C函数如下:

#define PCI_ADDR 0xCF8  // CONFIG_ADDRESS
#define PCI_DATA 0xCFC  // CONFIG_DATA

#define PCI_VENDORID 0x00
#define PCI_DEVICEID 0x02
#define PCI_COMMAND 0x04
#define PCI_BAR0 0x10
#define PCI_BAR1 0x14
#define PCI_BAR2 0x18
#define PCI_BAR3 0x1C
#define PCI_BAR4 0x20
#define PCI_BAR5 0x24

int pci_read_config_byte(int reg) {
    outl(PCI_CONFIG_CMD(reg), PCI_ADDR);
    return inb(PCI_DATA + (PCI_GET_CMD_REG(reg) & 3));
}

int pci_read_config_word(int reg) {
    outl(PCI_CONFIG_CMD(reg), PCI_ADDR);
    return inw(PCI_DATA + (PCI_GET_CMD_REG(reg) & 2));
}

int pci_read_config_long(int reg) {
    outl(PCI_CONFIG_CMD(reg), PCI_ADDR);
    return inl(PCI_DATA);
}

void pci_write_config_byte(int value, int reg) {
    outl(PCI_CONFIG_CMD(reg), PCI_ADDR);
    outb(value & 0xFF, PCI_DATA + (reg & 3));
}

void pci_write_config_word(int value, int reg) {
    outl(PCI_CONFIG_CMD(reg), PCI_ADDR);
    outw(value & 0xFFFF, PCI_DATA + (reg & 2));
}

void pci_write_config_long(int value, int reg) {
    outl(PCI_CONFIG_CMD(reg), PCI_ADDR);
    outl(value, PCI_DATA);
}