Case Study: A Simple IDE Disk Driver

In this lesson, we look at a case study of an IDE disk driver.

We'll cover the following

To dig a little deeper here, let’s take a quick look at an actual device: an IDE disk driver“AT Attachment Interface for Disk Drives” by Lawrence J. Lamers. Reference number: ANSI X3.221, 1994. Available: ftp://ftp.t10.org/t13/project/d0791r4c-ATA-1.pdf. A rather dry document about device interfaces. Read it at your own peril.. We summarize the protocol as described in this reference“Hard Disk Driver” by Washington State Course Homepage. Available online at this site: http://eecs.wsu.edu/ ̃cs460/cs560/HDdriver.html. A nice summary of a simple IDE disk drive’s interface and how to build a device driver for it.; we’ll also peek at the xv6 source code“The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai Zeldovich. From: http://pdos.csail.mit.edu/6.828/2008/index.html. See ide.c for the IDE device driver, with a few more details therein. for a simple example of a working IDE driver.

Press + to interact
Control Register:
Address 0x3F6 = 0x08 (0000 1RE0): R=reset,
E=0 means "enable interrupt"
Command Block Registers:
Address 0x1F0 = Data Port
Address 0x1F1 = Error
Address 0x1F2 = Sector Count
Address 0x1F3 = LBA low byte
Address 0x1F4 = LBA mid byte
Address 0x1F5 = LBA hi byte
Address 0x1F6 = 1B1D TOP4LBA: B=LBA, D=drive
Address 0x1F7 = Command/status
Status Register (Address 0x1F7): 76543210
BUSY READY FAULT SEEK DRQ CORR IDDEX ERROR
Error Register (Address 0x1F1): (check when ERROR==1) 76543210
BBK UNC MC IDNF MCR ABRT T0NF AMNF
BBK = Bad Block
UNC = Uncorrectable data error
MC = Media Changed
IDNF = ID mark Not Found
MCR = Media Change Requested
ABRT = Command aborted
T0NF = Track 0 Not Found
AMNF = Address Mark Not Found

An IDE disk presents a simple interface to the system, consisting of four types of register: control, command block, status, and error. These registers are available by reading or writing to specific “I/O addresses” (such as 0x3F6 below) using (on x86) the in and out I/O instructions.

Basic protocol

The basic protocol to interact with the device is as follows, assuming it has already been initialized.

  • Wait for the drive to be ready. Read Status Register (0x1F7) until drive is READY and not BUSY.

  • Write parameters to command registers. Write the sector count, logical block address (LBA) of the sectors to be accessed, and drive number (master=0x00 or slave=0x10, as IDE permits just two drives) to command registers (0x1F2-0x1F6).

  • Start the I/O. by issuing read/write to the command register. Write READ—WRITE command to command register (0x1F7).

  • Data transfer (for writes): Wait until drive status is READY and DRQ (drive request for data); write data to data port.

  • Handle interrupts. In the simplest case, handle an interrupt for each sector transferred; more complex approaches allow batching and thus one final interrupt when the entire transfer is complete.

  • Error handling. After each operation, read the status register. If the ERROR bit is on, read the error register for details.

Press + to interact
static int ide_wait_ready() {
while (((int r = inb(0x1f7)) & IDE_BSY) || !(r & IDE_DRDY))
; // loop until drive isn’t busy
}
static void ide_start_request(struct buf *b) {
ide_wait_ready();
outb(0x3f6, 0); //generate interrupt
outb(0x1f2, 1); //how many sectors?
outb(0x1f3, b->sector & 0xff); //LBA goes here ...
outb(0x1f4, (b->sector >> 8) & 0xff); // ... and here
outb(0x1f5, (b->sector >> 16) & 0xff); // ... and here!
outb(0x1f6, 0xe0 | ((b->dev&1)<<4) | ((b->sector>>24)&0x0f));
if(b->flags & B_DIRTY){
outb(0x1f7, IDE_CMD_WRITE); // this is a WRITE
outsl(0x1f0, b->data, 512/4); // transfer data too!
} else {
outb(0x1f7, IDE_CMD_READ); // this is a READ (no data)
}
}
void ide_rw(struct buf *b) {
acquire(&ide_lock);
for (struct buf **pp = &ide_queue; *pp; pp=&(*pp)->qnext)
; // walk queue
*pp = b; // add request to end
if (ide_queue == b) // if q is empty
ide_start_request(b); // send req to disk
while ((b->flags & (B_VALID|B_DIRTY)) != B_VALID)
sleep(b, &ide_lock); // wait for completion
release(&ide_lock);
}
void ide_intr() {
struct buf *b;
acquire(&ide_lock);
if (!(b->flags & B_DIRTY) && ide_wait_ready() >= 0)
insl(0x1f0, b->data, 512/4); // if READ: get data
b->flags |= B_VALID;
b->flags &= ̃B_DIRTY;
wakeup(b); // wake waiting process
if ((ide_queue = b->qnext) != 0) // start next request
ide_start_request(ide_queue); // (if one exists)
release(&ide_lock);
}

xv6 IDE driver protocol

Most of this protocol is found in the xv6 IDE driver (given in the code widget above), which (after initialization) works through four primary functions.

  • The first is ide_rw(), which queues a request (if there are others pending), or issues it directly to the disk (via ide_start_request()); in either case, the routine waits for the request to complete and the calling process is put to sleep.

  • The second is ide_start_request(), which is used to send a request (and perhaps data, in the case of a write) to the disk; the in and out x86 instructions are called to read and write device registers, respectively.

  • The start request routine uses the third function, ide_wait_ready(), to ensure the drive is ready before issuing a request to it.

  • Finally, ide_intr() is invoked when an interrupt takes place; it reads data from the device (if the request is a read, not a write), wakes the process waiting for the I/O to complete, and (if there are more requests in the I/O queue), launches the next I/O via ide_start_request().

Get hands-on with 1400+ tech skills courses.