LBA HDD Access via PIO

Every operating system will eventually find a need for reliable, long-term storage. There are only a handful of commonly used storage devices:

  • Floppy

  • Flash media

  • CD-ROM

  • Hard drive

Hard drives are by far the most widely used mechanism for data storage, and this tutorial will familiarize you with a practical method for accessing them. In the past, a method known as CHS was used. With CHS, you specified the cylinder, head, and sector where your data was located. The problem with this method is that the number of cylinders that could be addressed was rather limited. To solve this problem, a new method for accessing hard drives was created: Linear Block Addressing (LBA). With LBA, you simply specify the address of the block you want to access. Blocks are 512-byte chunks of data, so the first 512 bytes of data on the disk are in block 0, the next 512 bytes are in block 1, etc. This is clearly superior to having to calculate and specify three separate bits of information, as with CHS. However, there is one hitch with LBA. There are two forms of LBA, which are slightly different: LBA28 and LBA48. LBA28 uses 28 bits to specify the block address, and LBA48 uses 48 bits. Most drives support LBA28, but not all drives support LBA48. In particular, the Bochs emulator supports LBA28, and not LBA48. This isn't a serious problem, but something to be aware of.(Bochs 2.3.7 added support for LBA48 - noted on 2011-01-28) Now that you know how LBA works, it's time to see the actual methods involved.

To read a sector using LBA28:

  1. Send a NULL byte to port 0x1F1: outb(0x1F1, 0x00);

  2. Send a sector count to port 0x1F2: outb(0x1F2, 0x01);

  3. Send the low 8 bits of the block address to port 0x1F3: outb(0x1F3, (unsigned char)addr);

  4. Send the next 8 bits of the block address to port 0x1F4: outb(0x1F4, (unsigned char)(addr >> 8);

  5. Send the next 8 bits of the block address to port 0x1F5: outb(0x1F5, (unsigned char)(addr >> 16);

  6. Send the drive indicator, some magic bits, and highest 4 bits of the block address to port 0x1F6: outb(0x1F6, 0xE0 | (drive << 4) | ((addr >> 24) & 0x0F));

  7. Send the command (0x20) to port 0x1F7: outb(0x1F7, 0x20);

To write a sector using LBA28:

Do all the same as above, but send 0x30 for the command byte instead of 0x20: outb(0x1F7, 0x30);

To read a sector using LBA48:

  1. Send two NULL bytes to port 0x1F1: outb(0x1F1, 0x00); outb(0x1F1, 0x00);

  2. Send a 16-bit sector count to port 0x1F2: outb(0x1F2, 0x00); outb(0x1F2, 0x01);

  3. Send bits 24-31 to port 0x1F3: outb(0x1F3, (unsigned char)(addr >> 24));

  4. Send bits 0-7 to port 0x1F3: outb(0x1F3, (unsigned char)addr);

  5. Send bits 32-39 to port 0x1F4: outb(0x1F4, (unsigned char)(addr >> 32));

  6. Send bits 8-15 to port 0x1F4: outb(0x1F4, (unsigned char)(addr >> 8));

  7. Send bits 40-47 to port 0x1F5: outb(0x1F5, (unsigned char)(addr >> 40));

  8. Send bits 16-23 to port 0x1F5: outb(0x1F5, (unsigned char)(addr >> 16));

  9. Send the drive indicator and some magic bits to port 0x1F6: outb(0x1F6, 0x40 | (drive << 4));

  10. Send the command (0x24) to port 0x1F7: outb(0x1F7, 0x24);

To write a sector using LBA48:

Do all the same as above, but send 0x34 for the command byte, instead of 0x24: outb(0x1F7, 0x34);



Once you've done all this, you just have to wait for the drive to signal that it's ready:

while (!(inb(0x1F7) & 0x08)) {}

And then read/write your data from/to port 0x1F0:

// for read:

for (idx = 0; idx < 256; idx++)

{

tmpword = inw(0x1F0);

buffer[idx * 2] = (unsigned char)tmpword;

buffer[idx * 2 + 1] = (unsigned char)(tmpword >> 8);

}

// for write:

for (idx = 0; idx < 256; idx++)

{

tmpword = buffer[8 + idx * 2] | (buffer[8 + idx * 2 + 1] << 8);

outw(0x1F0, tmpword);

}

Of course, all of this is useless if you don't know what drives you actually have hooked up. Each IDE controller can handle 2 drives, and most computers have 2 IDE controllers. The primary controller, which is the one I have been dealing with thus-far has its registers located from port 0x1F0 to port 0x1F7. The secondary controller has its registers in ports 0x170-0x177. Detecting whether controllers are present is fairly easy:

  1. Write a magic value to the low LBA port for that controller (0x1F3 for the primary controller, 0x173 for the secondary): outb(0x1F3, 0x88);

  2. Read back from the same port, and see if what you read is what you wrote. If it is, that controller exists.

Now, you have to detect which drives are present on each controller. To do this, you simply select the appropriate drive with the drive/head select register (0x1F6 for the primary controller, 0x176 for the secondary controller), wait a small amount of time (I wait 1/250th of a second), and then read the status register and see if the busy bit is set:

outb(0x1F6, 0xA0); // use 0xB0 instead of 0xA0 to test the second drive on the controller

sleep(1); // wait 1/250th of a second

tmpword = inb(0x1F7); // read the status port

if (tmpword & 0x40) // see if the busy bit is set

{

printf("Primary master exists\n");

}

And that about wraps it up. Note that I haven't actually tested my LBA48 code, because I'm stuck with Bochs, which only supports LBA28. It should work, according to the ATA specification.

If any of this is inaccurate or unclear, just email me at marsdragon88@NOSPAM.gmail.com.



--Dragoniz3r

Related

Report issues via Bona Fide feedback.

2001 - 2024 © Bona Fide OS Development | The Goal | Contributors | How To Help |