Writing your own Operating System: Drivers

Hasini Samarathunga
7 min readAug 6, 2021

This is the third article in a series of articles explaining the development of an x86 Operating System step-by-step. If you seem lost I recommend going back to my first article.

In this article, I will explain how to display text on the console and how to write data to the serial port. We do this by creating a driver. A driver is a code that acts as a layer between the kernel and the hardware, providing a higher abstraction than communicating directly with the hardware.

First, let’s start by creating a drive for the frame buffer to display text on the console.

The Framebuffer

The frame buffer is a hardware device that is capable of displaying a buffer of memory on the screen. The frame buffer has 80 columns and 25 rows, and the row and column indices start at 0 (so rows are labeled 0–24).

The frame buffer uses memory-mapped I/O then you can write to a specific memory address and the hardware will be updated with the new data. The starting address of the memory-mapped I/O for the framebuffer is 0x000B8000 . The memory is divided into 16-bit cells, where the 16 bits determine both the character, the foreground color, and the background color.

Bit:     | 15 14 13 12 11 10 9 8 | 7 6 5 4 | 3 2 1 0 |
Content: | ASCII | FG | BG |

Here you can see all the available colors. You can use any color you want.

I will use blue(1) as it’s my favorite color, on a Light Grey(7) background.

So, if you write the value 0x7141 to address 0x000B8000, you will see the letter A in cyan color on a black background.

You can do this by adding this piece of code to your loader.s.

mov [0x000B8000], 0x7141

Writing to the frame buffer can also be done in C by treating the address 0x000B8000 as a char pointer.

char *fb = (char *) 0x000B8000

And using this function below.

Moving the Cursor

So far we have only been able to write one single character. But if you want to write more than that you simply need to move the curse to the next location on the frame buffer.

Moving the cursor of the frame buffer is done through two different I/O ports. Therefore, we need to use the assembly code instructions out and in to communicate with the hardware.

Now we can’t directly execute the out assembly code instruction in C. So we wrap it in a function in assembly code which can be accessed from C through the cdecl calling standard.

Create a file called io.s and the header file called io.h.

Here, we created the io.h and io.s but How we will link it to Make file? You just need to add one word in the Makefile and you are all set.

Lastly, add the c function to your framebuffer.c file and “make run”.

Now if you see this blinking line, you are good to go!

The Driver

The driver should provide an interface that the rest of the code in the OS will use for interacting with the frame buffer. There is no right or wrong in what functionality the interface should provide. You can write your own C function for this. This is my version of it.

Now you can enter anything for the buffer and it will be displayed on the console. Like this

You can of course have your fun with this. You can write anything you want. The trick is in moving the curser where ever you want. Here’s a small Haiku I wrote on the console.

The Serial Ports

Now let’s see how to create a driver for the serial port. The serial port is an interface for communicating between hardware devices. If a computer has support for a serial port, then it usually has support for multiple serial ports, but we are only making use of one of the ports. This is because we will only be using the serial ports for logging and only for output. Then Bochs can store output from the serial port in a file, effectively creating a logging mechanism for the operating system.

The serial ports are also controlled through I/O ports. These are the steps to set up a Serial Port.

1) Configuring the Serial Port

The very first data that need to be sent to the serial port is configuration data. For two hardware devices to be able to communicate with each other they must agree upon a couple of things.

  • The speed used for sending data (bit or baud rate)
  • If any error checking should be used for the data (parity bit, stop bits)
  • The number of bits that represent a unit of data (data bits)

2) Configuring the Line

This means configuring how data is being sent over the line. The serial port has an I/O port, the line command port, that is used for configuration. First, the speed for sending data will be set. The serial port has an internal clock that runs at 115200 Hz. Setting the speed means sending a divisor to the serial port.

3) Configuring the Buffers

When receiving and sending data through the serial port it is placed in buffers. This is because if you send data to the serial port faster than it can send it over the wire, it will be buffered. The buffers are FIFO queues.

We use the value 0xC7 = 11000111 as the FIFO queue configuration byte so that it,

  • Enables FIFO
  • Clear both receiver and transmission FIFO queues
  • Use 14 bytes as the size of the queue

4) Configuring the Modem

The modem control register is used for very simple hardware flow control through the Ready To Transmit (RTS) and Data Terminal Ready (DTR) pins. When configuring the serial port we want RTS and DTR to be 1, which means that we are ready to send data.

So now you are probably thinking— how we are gonna use all THAT in our coding?

Here’s how. You can find the completed serial.c and serial.h file below. Read the comments for more details about each configuration.

Writing Data to the Serial Port

Writing data to the serial port is done through the data I/O port. But before writing, we have to make sure, the transmit FIFO queue is empty (all previous writes must have finished). The transmit FIFO queue is empty if bit 5 of the line status I/O port is equal to one. If you scroll back up and check the serial.c and serial.h files, you’ll see I have already added a function to check this. Writing to a serial port means spinning as long as the transmit FIFO queue isn’t empty, and then writing the data to the data I/O port.

Reading the contents of an I/O port is done via the in assembly code instruction. And this is done in the same way as theout assembly code we did earlier.

And the header file:

Configuring Bochs

The Bochs configuration file bochsrc.txt must be modified to save the output from the serial port. The com1 configuration instructs Bochs on how to handle the first serial port. Add this to the bochsrc.txt file.

com1: enabled=1, mode=file, dev=com1.out

The output from serial port one will now be stored in the file com1.out.

The Driver

Now let’s implement a write function for the serial port similar to the write function in the driver for the frame buffer.

To wrap it all up we finish by implementing a printf-like function. It takes an additional argument to decide to which device to write the output (frame buffer or serial). So the final kmain.c file will look like this.

Hope you found it easier to follow these steps and create your very first drive! You can obtain the complete code from my GitHub below.

Hope to see you in the next article as well!

Thank you!

Reference : Helin, E., & Renberg, A. (2015). The little book about OS development

--

--