Basic VGA Driver

8 – Developing an Operating System – Tutorial – Episode 4 – Basic VGA Driver and printf

In previous episode, we learnt about switching to protected mode (32-bit), calling C code from assembly language, reading form hard disk, and accessing video memory to display message on the screen.

In this episode, we will create a basic video driver. Which will allow us to keep printing message on the screen by just calling a function, so that we do not have to worry about memory address every time. We are going to develop some of the following functionalities in our basic video driver:

  1. Get Cursor Position
  2. Set Cursor Position
  3. Clear Screen
  4. printf

This is very simple function, about printing on the screen. We are not stepping into graphics right now. We will step into graphics much later.

In earlier episode when we tried printing ‘X’ on the screen, it did but it printed on the screen at the beginning of the screen and not at the cursor position. Refer screenshot below:

First thing that we are going to do is get the cursor position, add the position into the video memory address that we have and print on the screen. To get a cursor position, we need to communicate with BIOS and get the position. We are going to write our code in C. To communicate with BIOS we need to write in low level language (Assembly), one of the beautiful thing about C language / compiler is that it allows us to write Assembly code in C language using __asm__ directive.

We will also do a bit of maintenance of a code so that we don’t have a single file containing all of the code. Let’s create a new file called ports.h in drivers folder.

unsigned char port_byte_in (unsigned short port);
void port_byte_out (unsigned short port, unsigned char data);

Below is the code for ports.c file in drivers folder:

#include "ports.h"

unsigned char port_byte_in (unsigned short port) {
    unsigned char result;
    __asm__("in %%dx, %%al" : "=a" (result) : "d" (port));
    return result;
}

void port_byte_out (unsigned short port, unsigned char data) {
    __asm__("out %%al, %%dx" : : "a" (data), "d" (port));
}

Remember, now we are in protected mode. In protected mode if we want to communicate with the hardware, only way that we can communicate is via Ports. We will understand about ports more in detail as we go further. If you want to read more about Ports right now you can visit – http://www.brokenthorn.com/Resources/OSDev7.html

Our kernel.c file will look like following:

#include "ports.h"

void main() {
	port_byte_out(0x3d4, 14); //read high byte of the cursor position
	int xposition = port_byte_in(0x3d5); //store data returned in 0x3d5 register to xposition variable
	xposition = xposition << 8; //convet into 8 bits

	port_byte_out(0x3d4, 15); //read low byte of the cursor position
	int yposition = port_byte_in(0x3d5); //store data returned in 0x3d5 register to yposition variable

	int position = xposition + yposition; //add xposition and yposition bytes
	position = position * 2; //data consist of 2 data - i.e. character, color of foreground and background

	char* video_memory = (char*) 0xb8000;
	video_memory[position] = 'X';
	video_memory[position + 1] = 0x0f; //black background on white foreground - 0 = black; f = white
}

If you are wondering how to figure out which address (0x3d5, 0x3d4) and index (14, 15) to use, refer the below table:

Register NamePortIndexMode 3h (80×25 Text Mode)
Mode Control0x3C00x100x0C
Overscan Register0x3C00x110x00
Color Plane Enable0x3C00x120x0F
Horizontal Panning0x3C00x130x08
Color Select0x3C00x140x00
Miscellaneous Output Register0x3C2N/A0x67
Clock Mode Register0x3C40x010x00
Character Select0x3C40x030x00
Memory Mode Register0x3C40x040x07
Mode Register0x3CE0x050x10
Miscellaneous Register0x3CE0x060x0E
Horizontal Total0x3D40x000x5F
Horizontal Display Enable End0x3D40x010x4F
Horizontal Blank Start0x3D40x020x50
Horizontal Blank End0x3D40x030x82
Horizontal Retrace Start0x3D40x040x55
Horizontal Retrace End0x3D40x050x81
Vertical Total0x3D40x060xBF
Overflow Register0x3D40x070x1F
Preset row scan0x3D40x080x00
Maximum Scan Line0x3D40x090x4F
Vertical Retrace Start0x3D40x100x9C
Vertical Retrace End0x3D40x110x8E
Vertical Display Enable End0x3D40x120x8F
Logical Width0x3D40x130x28
Underline Location0x3D40x140x1F
Vertical Blank Start0x3D40x150x96
Vertical Blank End0x3D40x160xB9
Mode Control0x3D40x170xA3

Now if we compile the code and execute, we will be able to see X on the screen and at a correct position. i.e. where cursor is blinking.

Now let’s go further and create a very basic utility for screen part, so that we can start working on more important stuffs.

Let’s create a basic screen driver. We will create screen.h file inside drivers folder.

#define VIDEO_ADDRESS 0xb8000
#define TOTAL_ROWS 25
#define TOTAL_COLS 80
#define STANDARD_MSG_COLOR 0x0f //black bg, white foreground
#define ERROR_MSG_COLOR 0xf4 //red bg, white background

#define REG_SCREEN_CTRL 0x3d4
#define REG_SCREEN_DATA 0x3d5

void clear();
void printf(char* str);

and below is code for screen.c file inside drivers folder.

#include "screen.h"
#include "ports.h"

int CURRENT_CURSOR_POSITION = 0;

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

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] = 0x0f;
    }
    CURRENT_CURSOR_POSITION = 0x00;
    set_cursor_position(CURRENT_CURSOR_POSITION);
}

void printf(char *str) {
    char* video_memory = (char*) VIDEO_ADDRESS;
    int pos = 0;
    int cursorPosition = get_cursor_position();
    while(str[pos] != 0)
    {
        video_memory[cursorPosition] = str[pos++];
        video_memory[cursorPosition + 1] = 0x0f;
        cursorPosition = cursorPosition + 2;
    }
    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);
}

And we will make our kernel.c file sweet and simple:

#include "drivers/screen.h"

void main() {
    clear();

	char str[] = "Welcome to Learn OS. ";
    printf(str);

	char str1[] = "This message has been printed using printf.";
    printf(str1);
}

With this, we are making our kernel bigger.. if you compile the code, you will notice that once we convert our kernel object file to elf32 file, kernel size increases up to 128MB. In boot.asm we are reading sectors from hard disk, so far we were reading only 1 sector from hard disk. Now 1 sector is not enough as our kernel size has increased. Even though size has increased by a huge number, actual kernel still occupies less space, but more then 1 sector size. So we are going to read 2 sectors now. In boot.asm file, look for load_kernel section, in that section we are reading sectors (line number 54). Let’s change that to 2. Code will look like following:

load_kernel:
     mov  bx, MSG_LOAD_KERNEL
     call print
     call print_nl

     mov  bx, KERNEL_OFFSET ;read from disk and store in 0x1000
     mov  dh, 2 ;read 2 sectors from HDD or bootable disk
     mov  dl, [BOOT_DRIVE]
     call disk_load
     ret

also let’s modify our compile.bat file:

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

echo "compile boot.asm"
fasm boot.asm

echo "compile loader.asm"
fasm loader.asm

echo "compile kernel.c"
wsl gcc -m32 -ffreestanding drivers/ports.h drivers/screen.h kernel.c drivers/ports.c drivers/screen.c -o kernel.o

echo "Producing elf file"
wsl objcopy kernel.o -O elf32-i386 kernel.elf

echo "Linking files"
wsl /usr/local/i386elfgcc/bin/i386-elf-ld -o kernel.bin -Ttext 0x1000 loader.o kernel.elf --oformat binary

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

echo "Launching QEMU"
qemu-system-x86_64 os_image.bin

Now, if we execute compile.bat file. It will execute the code and we will see output on the screen as below:

Basic VGA Driver

CONGRATULATIONS!!!! We have written our first driver.

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

That’s it for this post, next we will write a PCI IDE Controller driver to perform read and write operations on hard disk. We will also write our first File System into hard disk or look into a way to call Interrupts from Protected mode.

About the Author

Leave a Reply

Your email address will not be published.