12 – Developing an Operating System – Tutorial – Episode 6 – ATA PIO Driver – OSDEV

In previous tutorial we learnt how we are reducing our kernel size and also using latest GCC compiler and making things better.

Our next milestone is to convert our current kernel into a 2nd stage loader and we will develop a new kernel. Why are we doing that? Currently we aare loading our Kernel at 0x1000 memory address. We don’t want to load our kernel at 0x1000 as our kernel becomes bigger, it will overwrite BIOS data area, video memory, etc… We want our kernel to load at 0x10000 memory address aka 1M memory address, so that it does not overwrite BIOS memory. Refer below image:

Why we are not loading our kernel at 0x10000 (1M) initially only? Well, we ara in 16-bit mode (Real mode) and we cannot access long memory address. So then question would be why we dont do it in loader.asm? Once we are in 32-bit mode (Protected mode) we cannot access interrupts. So we cannot access hard disk to read any data. Our approach will be to write ATA PIO driver which will read our new kernel (which we will develop later), and load it at 0x10000 (1M) memory address. This will make our current kernel as a 2nd stage loader. In our OS we have two stage of loader.. Bootloader -> 1st stage loader (Which executes C code) -> 2nd stage loader (Written in C, which will read HDD and load kernel at 0x10000).

In this tutorial, we will see how to write ATA PIO driver, For test purpose, we will read 1st sector of hard disk, print it on screen.. replace 1st sector of the hard disk with 0x01 and then again read 1st sector and print it on screen to make sure that read and write operations are working as expected.

What is ATA PIO driver?

ATA disk specification was written around older specification called ST506. ST506 was the first 5.25 inch hard disk introduced in 1980 by Shugart Technology (now Seagate Technology).

ATA has two modes PIO and DMA. PIO is the simplest mode of all but also the slowest.. With PIO we can achieve speed upto 16MB per second.

You can read more about ATA PIO Mode here – https://wiki.osdev.org/ATA_PIO_Mode

Let’s crack into writing ATA PIO driver.

Create ata folder inside drivers folder, and after that create ata.h file into ata folder. ata.h file will contain following two methods:

void read_sectors_ATA_PIO(uint32_t target_address, uint32_t LBA, uint8_t sector_count);
void write_sectors_ATA_PIO(uint32_t LBA, uint8_t sector_count, uint32_t* bytes);

Now create ata.c file, and in this file, we will implement both methods as follows:

#include<stdint.h>
#include "ata.h"
#include "../ports.h"

/*
 BSY: a 1 means that the controller is busy executing a command. No register should be accessed (except the digital output register) while this bit is set.
RDY: a 1 means that the controller is ready to accept a command, and the drive is spinning at correct speed..
WFT: a 1 means that the controller detected a write fault.
SKC: a 1 means that the read/write head is in position (seek completed).
DRQ: a 1 means that the controller is expecting data (for a write) or is sending data (for a read). Don't access the data register while this bit is 0.
COR: a 1 indicates that the controller had to correct data, by using the ECC bytes (error correction code: extra bytes at the end of the sector that allows to verify its integrity and, sometimes, to correct errors).
IDX: a 1 indicates the the controller retected the index mark (which is not a hole on hard-drives).
ERR: a 1 indicates that an error occured. An error code has been placed in the error register.
*/

#define STATUS_BSY 0x80
#define STATUS_RDY 0x40
#define STATUS_DRQ 0x08
#define STATUS_DF 0x20
#define STATUS_ERR 0x01

//This is really specific to out OS now, assuming ATA bus 0 master 
//Source - OsDev wiki
static void ATA_wait_BSY();
static void ATA_wait_DRQ();
void read_sectors_ATA_PIO(uint32_t target_address, uint32_t LBA, uint8_t sector_count)
{

	ATA_wait_BSY();
	port_byte_out(0x1F6,0xE0 | ((LBA >>24) & 0xF));
	port_byte_out(0x1F2,sector_count);
	port_byte_out(0x1F3, (uint8_t) LBA);
	port_byte_out(0x1F4, (uint8_t)(LBA >> 8));
	port_byte_out(0x1F5, (uint8_t)(LBA >> 16)); 
	port_byte_out(0x1F7,0x20); //Send the read command

	uint16_t *target = (uint16_t*) target_address;

	for (int j =0;j<sector_count;j++)
	{
		ATA_wait_BSY();
		ATA_wait_DRQ();
		for(int i=0;i<256;i++)
			target[i] = port_word_in(0x1F0);
		target+=256;
	}
}

void write_sectors_ATA_PIO(uint32_t LBA, uint8_t sector_count, uint32_t* bytes)
{
	ATA_wait_BSY();
	port_byte_out(0x1F6,0xE0 | ((LBA >>24) & 0xF));
	port_byte_out(0x1F2,sector_count);
	port_byte_out(0x1F3, (uint8_t) LBA);
	port_byte_out(0x1F4, (uint8_t)(LBA >> 8));
	port_byte_out(0x1F5, (uint8_t)(LBA >> 16)); 
	port_byte_out(0x1F7,0x30); //Send the write command

	for (int j =0;j<sector_count;j++)
	{
		ATA_wait_BSY();
		ATA_wait_DRQ();
		for(int i=0;i<256;i++)
		{
			port_long_out(0x1F0, bytes[i]);
		}
	}
}

static void ATA_wait_BSY()   //Wait for bsy to be 0
{
	while(port_byte_in(0x1F7)&STATUS_BSY);
}
static void ATA_wait_DRQ()  //Wait fot drq to be 1
{
	while(!(port_byte_in(0x1F7)&STATUS_RDY));
}

In the code, we have ATA_wait_BSY method. This method we use to check on the status of hard disk operation. Hard disk is a mechanical device, head movement takes a time. ATA_wait_BSY is one of the method which we will use to make sure we execute next method only when hard disk is not busy.

We will now read 1st sector, write 0 to 1st sector and again read 1st sector. This are small testing methods for us to make sure read, write is working as expected. This code we will write in our kernel.c file:

#include "drivers/screen.h"
#include "utils/utils.h"
#include "drivers/ata/ata.h"

void kmain(void) {
    clear();
    
    printf("Welcome to Learn OS.\r\n");
    printf("Memory address of kmain in: 0x%x\r\n\r\n", kmain);

    printf("reading...\r\n");

    uint32_t* target;

    read_sectors_ATA_PIO(target, 0x0, 1);
    
    int i;
    i = 0;
    while(i < 128)
    {
        printf("%x ", target[i] & 0xFF);
        printf("%x ", (target[i] >> 8) & 0xFF);
        i++;
    }

    printf("\r\n");
    printf("writing 0...\r\n");
    char bwrite[512];
    for(i = 0; i < 512; i++)
    {
        bwrite[i] = 0x0;
    }
    write_sectors_ATA_PIO(0x0, 2, bwrite);


    printf("reading...\r\n");
    read_sectors_ATA_PIO(target, 0x0, 1);
    
    i = 0;
    while(i < 128)
    {
        printf("%x ", target[i] & 0xFF);
        printf("%x ", (target[i] >> 8) & 0xFF);
        i++;
    }
}

Another thing is, we have also re-written our printf method entirely, as well as have added many functionalities in screen.c file. Screen.c file as follows:

#include "screen.h"
#include "ports.h"
#include "../utils/utils.h"

int CURRENT_CURSOR_POSITION = 0;

void set_cursor_position(int offset);
int get_cursor_position();

int get_current_cursor_row();
int get_current_cursor_column();
int get_offset(int col, int row);

void clear() {
    char* video_memory = (char*) VIDEO_ADDRESS;
    int row = 0;
    for(row = 0; row < TOTAL_COLS * TOTAL_ROWS; row++)
    {
        video_memory[row * 2] = ' ';
        video_memory[row * 2 + 1] = STANDARD_MSG_COLOR;
    }
    CURRENT_CURSOR_POSITION = 0x00;
    set_cursor_position(CURRENT_CURSOR_POSITION);
}

void printf(const char* format, ...) {
    uint8_t **arg = (uint8_t **) &format;
    uint8_t c;
    uint8_t buf[20];

    arg++;

    while((c = *format++) != 0) {
        if (c != '%')
            putchar (c);
        else {
            uint8_t *p, *p2;
            int pad0 = 0, pad = 0;

            c = *format++;
            if (c == '0') {
                pad0 = 1;
                c = *format++;
            }

            if (c >= '0' && c <= '9') {
                pad = c - '0';
                c = *format++;
            }

            switch (c) {
            case 'X':
            case 'd':
            case 'u':
            case 'x':
                itoa (buf, c, *((int *) arg++));
                p = buf;
                goto string;
                break;
            case 's':
                p = *arg++;
                if (! p)
                p = (uint8_t*)"(null)";
    string:
                for (p2 = p; *p2; p2++);
                for (; p2 < p + pad; p2++)
                putchar (pad0 ? '0' : ' ');
                while (*p)
                putchar (*p++);
                break;
            default:
                putchar (*((int *) arg++));
                break;
            }
            }
        }
}

void putchar(char* str)
{
    char* video_memory = (char*) VIDEO_ADDRESS;

    int cursorPosition = get_cursor_position();
    int row = get_current_cursor_row(cursorPosition);
    int column = get_current_cursor_column(cursorPosition);

    if(str == '\n')
    {
        cursorPosition = get_offset(column, row + 1);
    }
    else if(str == '\r')
    {
        cursorPosition = get_offset(0, row);
    }
    else if(str == '\t')
    {
        cursorPosition = get_offset(column + 4, row);
    }
    else
    {
        video_memory[cursorPosition] = str;
        video_memory[cursorPosition + 1] = STANDARD_MSG_COLOR;
        cursorPosition = cursorPosition + 2;
    }

    //check are we on last row?
    if(cursorPosition >= TOTAL_ROWS * TOTAL_COLS * 2)
    {
        int i;
        /*shift the content by 1 line up*/
        for(i = 1; i < TOTAL_ROWS; i++)
        {
            memcopy(get_offset(0, i) + VIDEO_ADDRESS, get_offset(0, i - 1) + VIDEO_ADDRESS, TOTAL_COLS * 2);
        }

        /*create one blank line at the end*/
        char *last_line = get_offset(0, TOTAL_ROWS - 1) + VIDEO_ADDRESS;
        for(i = 0; i < TOTAL_COLS * 2; i++)
        {
            last_line[i] = 0;
        }

        cursorPosition = get_offset(0, TOTAL_ROWS - 1); /*TODO: revisit this.. last line cursor not visible*/
    }
    set_cursor_position(cursorPosition);
}

int get_cursor_position() {
    port_byte_out(REG_SCREEN_CTRL, 14);
    int position = port_byte_in(REG_SCREEN_DATA) << 8;

    port_byte_out(REG_SCREEN_CTRL, 15);
    position += port_byte_in(REG_SCREEN_DATA);

    return position * 2;
}

void set_cursor_position(int offset)
{
    offset /= 2;
    port_byte_out(REG_SCREEN_CTRL, 14);
    port_byte_out(REG_SCREEN_DATA, offset >> 8);

    port_byte_out(REG_SCREEN_CTRL, 15);
    port_byte_out(REG_SCREEN_DATA, offset & 0xff);
}

int get_current_cursor_row(int cursorPosition) {
    return cursorPosition / (2 * TOTAL_COLS);
}

int get_current_cursor_column(int cursorPosition) {
    return (cursorPosition - (get_current_cursor_row(cursorPosition) * 2 * TOTAL_COLS)) / 2;
}

int get_offset(int col, int row) { return 2 * (row * TOTAL_COLS + col); }

void itoa(uint8_t *buf, uint32_t base, uint32_t d) {
   uint8_t *p = buf;
   uint8_t *p1, *p2;
   uint32_t ud = d;
   uint32_t divisor = 10;

   if(base == 'd' && d < 0) {
       *p++ = '-';
       buf++;
       ud = -d;
   } else
     if (base == 'x')
         divisor = 16;

   do{
       uint32_t remainder = ud % divisor;
       *p++ = (remainder < 10) ? remainder + '0' : remainder + 'a' - 10;
   } while (ud /= divisor);

   *p = 0;
   p1 = buf;
   p2 = p - 1;
   while (p1 < p2) {
     uint8_t tmp = *p1;
     *p1 = *p2;
     *p2 = tmp;
     p1++;
     p2--;
   }
}

Next, we update our compile.bat file to include ata files.

echo off
echo "clean all binaries"
del *.bin
del *.o
del *.elf
del *.objdump
del *.log

echo "compile boot.asm"
fasm boot.asm

echo "compile kernel.c"
wsl gcc -g -m32 -ffreestanding -c *.c drivers/*.c utils/*.c drivers/pci/*.c drivers/ata/*.c -nostartfiles -nostdlib

echo "link all c object files"
wsl ld -m elf_i386 -nostdlib -T linker.ld *.o -o kernel.elf

echo "compile loader.asm"
fasm loader.asm

echo "Link loader and kernel"
wsl ld -m elf_i386 -nostdlib -T linker.ld loader.o kernel.elf -o kernel_full.elf

echo "objdump loader file"
objdump -phxd loader.o > loader.objdump

echo "objdump kernel file"
objdump -phxd kernel_full.elf > kernel.objdump

echo "Producing elf file"
wsl objcopy kernel_full.elf -O binary kernel.bin

echo "Creating image...."
type boot.bin kernel.bin > os_image.bin

echo "Launching QEMU"
qemu-system-x86_64 os_image.bin
rem bochs -f bochsconfig.conf
rem bochsdbg -f bochsconfig.conf

Now let’s execute compile.bat, and that will compile our code and execute it in QEMU. You should see following output:

ATA PIO osdev

Source code available at Github: https://github.com/dhavalhirdhav/LearnOS/

About the Author

Leave a Reply

Your email address will not be published.