CLOSE

As of now we are reading the disk using BIOS functions which are normal bios functions and extended bios function. We used and tried both ways of the bios but the latest we are using is extended bios function for the disk read. Now we will try to read the disk and load the kernel using the direct communication with the disk using the ATA interface, which is an standard interface for the communication between the motherboard and the disk.

I have explained everything about the ATA in this article, do have a look: https://thejat.in/learn/real-mode-disk-operations-using-ataatapi

Things to Know Before Starting:

As we don't have any file system yet. We will be loading the kernel_entry only, which is of fixed size 1 KB. Rest of our process will be the same. We will load the dummy kernel of size 1 KB (2 sectors of size 512 bytes each) at the location 0xB000.

In the disk.img, the dummy kernel starts from the sector 60.

dd if=kernel/build/kernel.elf of=build/disk.img bs=512 seek=59 conv=notrunc

Code

1 Create Port I/O Functions:

In order to communicate with ATA devices, you need to read from and write to specific I/O ports.

In x86 architecture, the inb, inw, outb, and outw instructions are used to read from and write to I/O ports. We can create C functions to wrap these instructions.

  1. Read a Byte from an I/O Port:
  2. Read a Word from an I/O Port:
  3. Write a Byte to an I/O Port:
  4. Write a Word to an I/O Port:

We will write these functions in C file loaderutils.c and declaration in loaderutils.h.

port_io.h:

#ifndef PORT_IO_H
#define PORT_IO_H

// Declaration of functions
unsigned char inb(unsigned short port);
unsigned short inw(unsigned short port);
void outb(unsigned short port, unsigned char data);
void outw(unsigned short port, unsigned short data);

#endif // PORT_IO_H

port_io.c:

#include "port_io.h"

// Definition of functions
static inline unsigned char inb(unsigned short port) {
    unsigned char result;
    __asm__ __volatile__("inb %1, %0" : "=a"(result) : "dN"(port));
    return result;
}

static inline unsigned short inw(unsigned short port) {
    unsigned short result;
    __asm__ __volatile__("inw %1, %0" : "=a"(result) : "dN"(port));
    return result;
}

static inline void outb(unsigned short port, unsigned char data) {
    __asm__ __volatile__("outb %0, %1" : : "a"(data), "dN"(port));
}

static inline void outw(unsigned short port, unsigned short data) {
    __asm__ __volatile__("outw %0, %1" : : "a"(data), "dN"(port));
}

2 ATA Device Structures

Now some background. The ATA interface has two channels namely: Primary Channel and Secondary Channel.

Each Channel is capable of  connecting two devices which can be identified as the Master Device or Slave Device.

Thus overall ATA interface support up to four devices.

First we define some structures which would be needy:

ata_identify_t Structure:

This structure would store the information of the ATA devices. It represents the data returned by an ATA device in response to an identify command. This command is used to retrieve detailed information about the device, such as its capabilities, serial number, firmware version, and model number. The structure is typically defined to match the layout of the data returned by the device, which consists of a series of 16-bit words.

Structure of IDENTIFY DEVICE Data

WordBitsDescription
00-15General configuration information
10-15Number of logical cylinders
20-15Reserved
30-15Number of logical heads
4-50-15Retired
60-15Number of logical sectors per track
7-80-15Reserved for vendor
90-15Retired
10-190-15Serial number (ASCII, right justified)
20-210-15Retired
220-15Obsolete
23-260-15Firmware revision (ASCII)
27-460-15Model number (ASCII, left justified)
470-15Maximum number of sectors per interrupt
480-15Reserved
490-15Capabilities
500-15Capabilities
51-520-15Obsolete
530-15Field validity
54-580-15Number of logical cylinders, heads, sectors
590-15Number of sectors per interrupt
60-610-15Total number of user addressable sectors
620-15Obsolete
630-15Multiword DMA modes
640-15Advanced PIO modes
650-15Minimum multiword DMA transfer cycle time per word
660-15Manufacturer's recommended multiword DMA transfer cycle time
670-15Minimum PIO transfer cycle time without flow control
680-15Minimum PIO transfer cycle time with IORDY flow control
69-700-15Reserved
71-740-15Reserved for IDENTIFY PACKET DEVICE command
750-15Queue depth
76-790-15Serial ATA capabilities
800-15Major version number
810-15Minor version number
82-830-15Command set supported
84-850-15Command set supported
86-870-15Command set/feature enabled
880-15Ultra DMA modes
89-1270-15Reserved
1280-15Security status
129-1590-15Vendor specific
1600-15CFA power mode
161-1750-15Reserved for CompactFlash Association (CFA)
176-2540-15Reserved
2550-15Integrity word

Here's the ata_identify_t structure which is packed to ensure there is no padding between its fields.

typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long int uint64_t;

typedef struct {
    uint16_t flags;                // General configuration (word 0)
    uint16_t unused1[9];           // Reserved fields (words 1-9)
    char     serial[20];           // Serial number (words 10-19)
    uint16_t unused2[3];           // Reserved fields (words 20-22)
    char     firmware[8];          // Firmware revision (words 23-26)
    char     model[40];            // Model number (words 27-46)
    uint16_t sectors_per_int;      // Number of sectors per interrupt on R/W multiple (word 47)
    uint16_t unused3;              // Reserved field (word 48)
    uint16_t capabilities[2];      // Capabilities (words 49-50)
    uint16_t unused4[2];           // Reserved fields (words 51-52)
    uint16_t valid_ext_data;       // Field validity (word 53)
    uint16_t unused5[5];           // Reserved fields (words 54-58)
    uint16_t size_of_rw_mult;      // Number of current sectors for R/W multiple (word 59)
    uint32_t sectors_28;           // Total number of user-addressable sectors (28-bit) (words 60-61)
    uint16_t unused6[38];          // Reserved fields (words 62-99)
    uint64_t sectors_48;           // Total number of user-addressable sectors (48-bit) (words 100-103)
    uint16_t unused7[152];         // Reserved fields (words 104-255)
} __attribute__((packed)) ata_identify_t;

Fields Description:

  1. flags: General configuration and status information of the device.
  2. unused1[9]: Reserved fields, not used in the current context.
  3. serial[20]: Serial number of the device, usually a 20-byte ASCII string.
  4. unused2[3]: Reserved fields, not used in the current context.
  5. firmware[8]: Firmware revision string, usually an 8-byte ASCII string.
  6. model[40]: Model number of the device, usually a 40-byte ASCII string.
  7. sectors_per_int: Number of sectors per interrupt for read/write multiple commands.
  8. unused3: Reserved field, not used in the current context.
  9. capabilities[2]: Capabilities of the device, indicating supported features.
  10. unused4[2]: Reserved fields, not used in the current context.
  11. valid_ext_data: Indicates which words are valid (fields are valid or not).
  12. unused5[5]: Reserved fields, not used in the current context.
  13. size_of_rw_mult: Number of current sectors for read/write multiple commands.
  14. sectors_28: Total number of user-addressable sectors using 28-bit addressing.
  15. unused6[38]: Reserved fields, not used in the current context.
  16. sectors_48: Total number of user-addressable sectors using 48-bit addressing.
  17. unused7[152]: Reserved fields, not used in the current context.

ata_device Structure:

The ata_device structure represents an ATA (Advanced Technology Attachment) device, such as a hard drive or an optical drive, and contains information and configuration parameters necessary to interact with the device. This structure typically includes fields for I/O base addresses, control ports, and detailed device identity information.

struct ata_device {
    int io_base;                  // Base I/O port address for the ATA device
    int control;                  // Control I/O port address
    int slave;                    // Indicates if the device is a slave (0 for master, 1 for slave)
    int is_atapi;                 // Indicates if the device is ATAPI (0 for ATA, 1 for ATAPI)
    ata_identify_t identity;      // Structure holding the identity information of the device
    unsigned int atapi_lba;       // Logical Block Addressing (LBA) for ATAPI devices
    unsigned int atapi_sector_size; // Sector size for ATAPI devices in bytes
    int is_device_connected;	// If the device is connected to this channel
};

Fields Description:

  1. io_base: The base I/O port address for the ATA device. This is used to send commands to and receive data from the device.
  2. control: The control I/O port address. This port is used for control operations such as resetting the device or enabling/disabling interrupts.
  3. slave: Indicates whether the device is configured as a master or slave on the IDE channel. A value of 0 typically indicates a master device, while 1 indicates a slave device.
  4. is_atapi: Indicates whether the device is an ATAPI device (such as a CD/DVD drive) or a standard ATA hard drive. A value of 0 typically indicates an ATA device, while 1 indicates an ATAPI device.
  5. identity: An instance of the ata_identify_t structure, which holds detailed identity information about the device. This includes the device's serial number, model number, firmware revision, and other configuration details.
  6. atapi_lba: Logical Block Addressing (LBA) for ATAPI devices. This field is relevant for addressing blocks on ATAPI devices.
  7. atapi_sector_size: The sector size for ATAPI devices, in bytes. This field specifies the size of a data sector on the device.

3 Create Four ATA Device Structure

Create four ATA devices structure with their respective register configurations.

Primary Master Device:

static struct ata_device ata_primary_master = {
    .io_base = 0x1F0,
    .control = 0x3F6,
    .slave = 0
};
  • io_base: 0x1F0 (I/O base address for the primary IDE channel)
  • control: 0x3F6 (Control port address for the primary IDE channel)
  • slave: 0 (Indicates this is the master device)

Primary Slave Device:

static struct ata_device ata_primary_slave = {
    .io_base = 0x1F0,
    .control = 0x3F6,
    .slave = 1
};
  • io_base: 0x1F0 (I/O base address for the primary IDE channel)
  • control: 0x3F6 (Control port address for the primary IDE channel)
  • slave: 1 (Indicates this is the slave device)

Secondary Master Device:

static struct ata_device ata_secondary_master = {
    .io_base = 0x170,
    .control = 0x376,
    .slave = 0
};
  • io_base: 0x170 (I/O base address for the secondary IDE channel)
  • control: 0x376 (Control port address for the secondary IDE channel)
  • slave: 0 (Indicates this is the master device)

Secondary Slave Device:

static struct ata_device ata_secondary_slave = {
    .io_base = 0x170,
    .control = 0x376,
    .slave = 1
};
  • io_base: 0x170 (I/O base address for the secondary IDE channel)
  • control: 0x376 (Control port address for the secondary IDE channel)
  • slave: 1 (Indicates this is the slave device)

4 ATA Device Detect

Now we we will detect the connected devices to the ATA interface.

Soft Reset ATA Device:

First we will reset the ATA device. An ATA soft reset is a process used to reset ATA devices (such as hard drives or CD/DVD drives) without completely powering them off. This is typically done by writing specific values to the control registers of the ATA interface. The soft reset sequence is important for initializing the device or recovering from certain error states.

This is done by the following way:

  • Write to the Device Control Register: Write a specific value to the device control register to initiate the reset.
  • Wait: Introduce a delay to allow the device to reset.
  • Clear the Reset Bit: Write to the control register again to clear the reset bit.
  • Wait for the Device to Become Ready: Poll the status register to check if the device is ready.
#define ATA_REG_ALTSTATUS  0x0C

void ata_io_wait(struct ata_device * dev) {
	inb(dev->io_base + ATA_REG_ALTSTATUS);
	inb(dev->io_base + ATA_REG_ALTSTATUS);
	inb(dev->io_base + ATA_REG_ALTSTATUS);
	inb(dev->io_base + ATA_REG_ALTSTATUS);
}

// Function to perform an ATA soft reset
void ata_soft_reset(struct ata_device *dev) {
    // Write the reset bit (SRST) to the Device Control Register
    outb(dev->control, 0x04);
    // Wait for at least 4 microseconds
    ata_io_wait(dev);
    // Clear the reset bit
    outportb(dev->control, 0x00);
    
    // Wait for the device to become ready
    while ((inb(dev->io_base + 7) & 0x80) != 0) {
        // Wait while the BSY (busy) bit is set
    }

    // Optionally, wait for DRDY (device ready) bit to be set
    while ((inb(dev->io_base + 7) & 0x40) == 0) {
        // Wait until the DRDY bit is set
    }
}

Delay Mechanism (ata_io_wait):

Its purpose is to introduce a delay or wait period after sending commands or performing operations on an ATA device.

  • The function ata_io_wait uses four consecutive inb (input from port) operations to read from the ATA device's Alternate Status register (ATA_REG_ALTSTATUS). Each inb operation introduces a small delay because reading from an I/O port typically takes a certain amount of time, which can be enough to synchronize with the ATA device's internal operations.

Soft Reset Function (ata_soft_reset):

  • Write Reset Bit: The outb function writes 0x04 to the device control register to initiate the reset.
  • Delay: Calls the ata_io_wait function to introduce the delays or wait periods during ATA operations. This is often employed to ensure that the ATA device has enough time to process commands or to wait for status changes.
  • Clear Reset Bit: The outb function writes 0x00 to clear the reset bit.
  • Wait for Ready: The inb function reads the status register and waits until the BSY (busy) bit is cleared, indicating the device is ready. It optionally waits for the DRDY (device ready) bit to be set.

Select Drive/Head:

  • Drive Select: Specifies which ATA drive (master or slave) is being accessed.
  • Head Select: For devices with multiple heads (e.g., CHS addressing mode), specifies which head is being accessed.

Now, the time is to select the drive (master or slave). Since we are using the LBA not CHS.

#define ATA_REG_HDDEVSEL   0x06

outb(dev->io_base + ATA_REG_HDDEVSEL, 0xA0 | dev->slave << 4);
  • dev->io_base is the base I/O port address for the ATA device (e.g., 0x1F0 for the primary ATA channel).
  • dev->io_base + ATA_REG_HDDEVSEL computes the I/O port address for the Drive/Head Device Select register (e.g., 0x1F0 + 0x06 = 0x1F6 for the primary ATA channel) (0x170 + 0x06 = 0x176 for the secondary ATA channel).
  • The value being written is 0xA0 | dev->slave << 4.
    • 0xA0 is a base value that sets specific bits in the Drive/Head Device Select register. The bits are:
      • Bit 7 (1): Indicates that the device is ready for use.
      • Bit 6 (0): Reserved and usually set to 0.
      • Bit 5 (1): Indicates LBA mode if set.
      • Bit 4 (0): Reserved and usually set to 0.
    • dev->slave << 4:
      • dev->slave is a field that indicates whether the device is a master (0) or slave (1).
      • Shifting dev->slave left by 4 (dev->slave << 4) moves the slave bit to the correct position in the Drive/Head Device Select register.
        • If dev->slave is 0 (master), this results in 0x00 (0000 0000).
        • If dev->slave is 1 (slave), this results in 0x10 (0001 0000).
    • Combining the Values: 0xA0 | dev->slave << 4
      • For a master device (dev->slave == 0): 0xA0 | 0x00 results in 0xA0.
      • For a slave device (dev->slave == 1): 0xA0 | 0x10 results in 0xB0.
Master Device: Writing 0xA0 to the Drive/Head Device Select register.

Sets the device to master.
Enables LBA mode.
Indicates the device is ready for use.
Slave Device: Writing 0xB0 to the Drive/Head Device Select register.

Sets the device to slave.
Enables LBA mode.
Indicates the device is ready for use.

Status Wait:

After selecting the device, need to wait for the ATA device to become ready by checking the status register until the device is no longer busy (BSY).

#define ATA_REG_STATUS     0x07
#define ATA_SR_BSY     0x80

int ata_status_wait(struct ata_device * dev, int timeout) {
    int status;
    if (timeout > 0) {
        int i = 0;
        while ((status = inportb(dev->io_base + ATA_REG_STATUS)) & ATA_SR_BSY && (i < timeout)) i++;
    } else {
        while ((status = inportb(dev->io_base + ATA_REG_STATUS)) & ATA_SR_BSY);
    }
    return status;
}
  • Functionality
    • Initialize status Variable:
      • The function initializes an integer variable status to hold the current status register value.
    • Check for Timeout:
      • The function checks if the timeout parameter is greater than 0.
      • If timeout is greater than 0, it uses a while loop to wait for the BSY flag to clear, with a maximum number of iterations specified by timeout.
      • If timeout is less than or equal to 0, it uses a while loop to wait indefinitely until the BSY flag clears.
    • Loop Until Not Busy or Timeout Reached:
      • The while loop continuously reads the status register using inportb(dev->io_base + ATA_REG_STATUS).
      • It checks the BSY flag (status & ATA_SR_BSY). If the BSY flag is set (device is busy), it continues looping.
      • If timeout is greater than 0, the loop also checks if the current iteration count (i) is less than timeout. If the iteration count reaches timeout, the loop exits.

Detect if Device is Connected, if connected whether it is ATA || ATAPI || SATA:

#define ATA_REG_LBA1       0x04
#define ATA_REG_LBA2       0x05

unsigned char cl = inportb(dev->io_base + ATA_REG_LBA1); /* CYL_LO */
unsigned char ch = inportb(dev->io_base + ATA_REG_LBA2); /* CYL_HI */

if (cl == 0xFF && ch == 0xFF) {
    // No device is connected
    // Handle it
}
if ((cl == 0x00 && ch == 0x00) ||
    (cl == 0x3C && ch == 0xC3)) {
    // Device is ATA
    // Print ATA Device Information
} else if ((cl == 0x14 && ch == 0xEB) ||
           (cl == 0x69 && ch == 0x96)) {
    // Device is ATAPI
    // Print ATAPI Device Information
}
  • Reading Values:
    • The values of the lower (cl) and higher (ch) cylinder registers are read using inportb.
  • Checking for No Device:
    • If both cl and ch are 0xFF, this typically indicates that there is no device present at the specified I/O address. The function returns 0.
  • Checking for ATA Devices:
    • If cl and ch are (0x00, 0x00) or (0x3C, 0xC3), these values indicate the presence of a regular ATA device.
  • Checking for ATAPI Devices:
    • If cl and ch are (0x14, 0xEB) or (0x69, 0x96), these values indicate the presence of an ATAPI device (such as a CD-ROM drive).

Print ATA Device Information:

// ATA Device
outport

Initialize ATAPI if there is any:

If in the previous step we encounters the connected device is ATAPI. Then here we initialize it.

#include <port_io.h>

#define ATA_PRIMARY_IO_BASE 0x1F0
#define ATA_COMMAND_REGISTER 0x07
#define ATA_READ_SECTORS 0x20

void ata_read_sector() {

	unsigned int lba = 59;
	int sector_count = 26;

	unsigned short *buffer = (unsigned short *)0xB000;

	// Select the drive and head
	outb(ATA_PRIMARY_IO_BASE + 6, 0xE0 | ((lba >> 24) & 0x0F)); 
	// Set the sector count to 1
	outb(ATA_PRIMARY_IO_BASE + 2, 1);  
	// Set the sector number
	outb(ATA_PRIMARY_IO_BASE + 3, lba); 
	// Set the cylinder low
	outb(ATA_PRIMARY_IO_BASE + 4, lba >> 8); 
	// Set the cylinder high
	outb(ATA_PRIMARY_IO_BASE + 5, lba >> 16); 
	// Send the read command
	outb(ATA_PRIMARY_IO_BASE + ATA_COMMAND_REGISTER, ATA_READ_SECTORS);

	// Wait for the device to be ready
	while (!(inb(ATA_PRIMARY_IO_BASE + 7) & 0x08));

	// Read data from the data register
	for (int sector = 0; sector< sector_count; sector++){
		for (int i = 0; i < 256; i++) {
			((unsigned short *)buffer)[(sector*256) + i] = inw(ATA_PRIMARY_IO_BASE);
		}
	}
}