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:
Control Register:Address 0x3F6 = 0x08 (0000 1RE0): R=reset,E=0 means "enable interrupt"Command Block Registers:Address 0x1F0 = Data PortAddress 0x1F1 = ErrorAddress 0x1F2 = Sector CountAddress 0x1F3 = LBA low byteAddress 0x1F4 = LBA mid byteAddress 0x1F5 = LBA hi byteAddress 0x1F6 = 1B1D TOP4LBA: B=LBA, D=driveAddress 0x1F7 = Command/statusStatus Register (Address 0x1F7): 76543210BUSY READY FAULT SEEK DRQ CORR IDDEX ERRORError Register (Address 0x1F1): (check when ERROR==1) 76543210BBK UNC MC IDNF MCR ABRT T0NF AMNFBBK = Bad BlockUNC = Uncorrectable data errorMC = Media ChangedIDNF = ID mark Not FoundMCR = Media Change RequestedABRT = Command abortedT0NF = Track 0 Not FoundAMNF = 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.
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 interruptoutb(0x1f2, 1); //how many sectors?outb(0x1f3, b->sector & 0xff); //LBA goes here ...outb(0x1f4, (b->sector >> 8) & 0xff); // ... and hereoutb(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 WRITEoutsl(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 endif (ide_queue == b) // if q is emptyide_start_request(b); // send req to diskwhile ((b->flags & (B_VALID|B_DIRTY)) != B_VALID)sleep(b, &ide_lock); // wait for completionrelease(&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 datab->flags |= B_VALID;b->flags &= ̃B_DIRTY;wakeup(b); // wake waiting processif ((ide_queue = b->qnext) != 0) // start next requestide_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 (viaide_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; thein
andout
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 viaide_start_request()
.
Get hands-on with 1400+ tech skills courses.