Writing your own Operating System: Dealing with devices + drivers

So through this article I’m going to explain you about controlling inputs and outputs of the computer using drivers. After all an OS which is unable to control any hardware with it, is an utter useless operating system that nobody wants to have.
Moving on in this article I’ll display some text on the display and write something to the serial port. We do these things by creating a driver. A driver is a chuck of code that behaves as a layer between the kernel and the hardware, providing a comparatively sophisticated abstraction than communicating directly with the hardware.
First, let’s start by creating a driver for the frame buffer — which is responsible for displaying 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 whatever the color you want.
I will use black(0) , on a Light red(12) background.
//0111(bg) 0001(fg) 0100 0001(A)
Bit: | 0 1 0 1 1 1 1 0 | 0 0 0 0 | 0 0 1 1 |
Content: | ASCII(122) | FG(0) | BG (12) |
So, if you write the value 0xC07A to address 0x000B8000
, you will see the letter z in black color on a light red background.
You can do this by adding this piece of code to your loader.s.
mov [0x000B8000], 0xC07A
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 multiple characters you simply have 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 have done it!
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.
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.