Develop Your Own x86 Operating System(OS) #4
You are diving deeper in to the process of developing your own x86 Operating System, as a result the process is getting more complex. In this step of article series, on developing your own Operating system, we will discuss the method to display text on the console and to write data to the serial port. Furthermore, we will create our first driver, code that acts as a layer between the kernel and the hardware.
If you haven’t read the second article of the series on using C programming language instead of assembly code as the programming language for the OS, you can read it from here as it is important, to understand what’s happening in this article.
This article will consist of two parts where the first part includes creating a driver for the framebuffer to be able to display text on the console and the second part shows how to create a driver for the serial port.
Interacting with the Hardware
Drivers act as a layer between the kernel and the hardware. A driver provides a software interface to hardware devices, enabling operating system to access hardware functions without needing to know precise details about the hardware being used. Instead of accessing a hardware device directly, an operating system loads the drivers and calls the specific functions in the driver software in order to execute specific tasks on the device. Each driver related to a device contains the device specific codes required to carry out the actions on the device.
There are normally two different ways to interact with the hardware as:
Memory-mapped I/O uses the same address space to address both memory and I/O devices. It means devices and RAM share same address space. If the hardware uses memory-mapped I/O then, you can write to a specific memory address and the hardware will be updated with the new data. An example for a hardware device that uses memory-mapped I/O is the framebuffer.
Ex :- If you write the value
0x410F to address
0x000B8000, you will see the letter A in white color on a black background
Alternatively referred to as I/O address, I/O port, and I/O port address, the input/output port, is a memory address used by software to communicate with hardware on the computer. If the hardware uses I/O ports then the assembly code instructions out and in must be used to communicate with the hardware. The “out” instruction takes two parameters, the address of the I/O port and the data to send while the “in” instruction takes only one parameter, the address of the I/O port, and returns data from the hardware. An example for a hardware controlled via I/O ports on a PC, is the cursor of the framebuffer.
When discussing about the memory-mapped I/O, the framebuffer was first mentioned. The framebuffer is a hardware device that is capable of displaying a buffer of memory on the screen. It is is a portion of random-access memory (RAM) containing a bitmap that drives a video display.
Writing text to the console via the framebuffer
This process is usually done with memory-mapped I/O. The starting address of the memory-mapped I/O for the framebuffer is 0x000B8000. The memory space is divided into 16 bit cells as shown in the following diagram:
According to the above diagram and ASCII table below:
and the provided table with available colors:
character A with a green foreground and dark grey background can be denoted as follows; 0x4128
A framebuffer may be thought of as computer memory organized as a two-dimensional array with each (x,y) addressable location corresponding to one pixel. The first cell corresponds to row zero, column zero on the console. As the starting address is 0x000B8000, in order to write the character A with a green foreground and dark grey background at place (0,0), the following assembly code instruction is used:
mov [0x000B8000], 0x4128
Now the second cell then corresponds to row zero, column one(0,1) and its address is therefore:
0x000B8000 + 16 = 0x000B8010
C language can also be used to write to the framebuffer. Here we have to consider the address 0x000B8000 as a char pointer,
char *fb = (char *) 0x000B8000;
Now writing character A at place (0,0) with green foreground and dark grey background becomes:
fb = 'A';
fb = 0x28;
This can be included in a function as follows:
The above function can be used in a code as follows:
You can update kmain.c file with above codes as follows:
And loader.s file as shown below to call the function saved in kmain.c file:
Now, use the “make run” command on terminal and you will see character A printed on the console.
Moving the Cursor
Moving the cursor of the framebuffer is done via two different I/O ports. The cursor’s position is determined with a 16 bits integer. The position is 16 bits large, and the out assembly code instruction argument is 8 bits. Hence, the position must be sent in two turns as first 8 bits then the next 8 bits. The framebuffer has two I/O ports, port 0x3D5 for accepting the data and port 0x3D4 for describing the data being received.
0 means row zero, column zero(0,0): 1 means row zero, column one(0,1): 80 means row one, column zero(1,0) and so on. To set the cursor at row one, column zero ((1,0)position 80 =0x0050), the following assembly code instructions can be used:
The out assembly code instruction can’t be executed directly in C. Therefore out assembly code instructions can be wrapped in a function in assembly code that can be accessed from C language using the cdecl calling standard(Discussed in the previous article) as shown below:
Now create a file called io.s in your working directory and save the above assembly code in it as follows:
As the next step create io.h file in your working directory and save the following code in it to access the out assembly code instruction can be from C:
Now update Makefile which we created in the previous article with just io.o as follows:
Moving the cursor can now be wrapped in a C function as follows:
Update kmain.c file as follows:
Now, use the “make run” command on terminal and you will see cursor on the console in 500th place on the console.
The driver should provide an interface, that the rest of the code in the OS will use for interacting with the framebuffer. A write function with the following declaration, explains the functionality that the interface should provide.
int write(char *buf, unsigned int len);
The write function writes the contents of the buffer buf of length len to the screen. The write function should automatically advance the cursor after a character has been written and scroll the screen if necessary. The following code can be used for that:
Now let’s combine all the above mentioned codes for the framebuffer into a one file.
First, let’s create header file framebuffer.h for the framebuffer to store C function declarations, as well as macro definitions by including the following code:
Then, create framebuffer.c file that contains combined C source codes of the framebuffer. Save the following code in framebuffer.c file:
Finally update your Makefile with just framebuffer.o as shown in the picture below:
Update your kmain.c file as follows to print “MYOS” to the console:
Now, use the “make run” command on terminal to see “MYOS” displayed on the console.
The Serial Ports
As mentioned at the beginning of the article this part shows how to create a driver for the serial port.
A serial port is an asynchronous port on the computer used to connect a serial device to the computer and capable of transmitting one bit at a time. The serial port is found on the back of the computer and is part of the motherboard. The serial port is easy to use, and it can be used as a logging utility in Bochs. Although a computer has support for multiple serial ports, here we will only make use of one of the ports as we will only use the serial ports for logging. Furthermore, we will only use the serial ports for output, not for input. The serial ports are completely controlled via I/O ports.
Configuring the Serial Port
The first data that is needed to be sent to the serial port is configuration data. In order for two hardware devices to be able to talk to each other they must agree upon a couple of things. These things include:
- 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)
Configuring the Line
Configuring the line means to configure 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.
At 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.
Ex: -Sending 4 results in a speed of 115200/4=28800Hz.
The divisor is a 16 bit number but we can only send 8 bits at a time. We must therefore send an instruction telling the serial port to first expect the highest 8 bits, then the lowest 8 bits. This is done by sending 0x80 to the line command port. Following code can be used for it:
Moreover, the way that data should be sent must be configured. This is also done through the line command port by sending a byte. The layout of the 8 bits looks like the following diagram:
Here we will use the most standard value 0x03, meaning a length of 8 bits, no parity bit, one stop bit and break control disabled. Using the below function his value is sent to the line command port:
Configuring the Buffers
When data is transmitted via the serial port it is placed in buffers in receiving and sending data. This way, data will be buffered if you send data to the serial port faster than it can send it over the wire. However, if you send too much data too fast the buffer will be full and data will be lost.
Buffers are FIFO(First In First Out)queues. The FIFO queue configuration byte looks like the following figure:
Here we use the value 0x07=11000111 that enables FIFO, clear both receiver and transmission FIFO queues and uses 14 bytes as the size of queue.
Following code can be used to configure the buffer with 0x07:
Configuring the Modem
The modem control register is used for very simple hardware flow control via the Ready To Transmit (RTS) and Data Terminal Ready (DTR) pins. When configuring the serial port we want RTS and DTR to be 1, with the meaning, we are ready to send data.
The modem configuration byte can be shown as in the following figure:
As we are not going to handle any received data, it is not needed to enable interrupts. Therefore we use the configuration value 0x03=00000011 (RTS = 1 and DTS = 1). Following function in C language can be used for this purpose:
Writing Data to the Serial Port
Writing data to the serial port is done via the data I/O port. However, before writing, the transmit FIFO queue has to be empty. The transmit FIFO queue is empty if bit 5 of the line status I/O port is equal to one.
Reading the contents of an I/O port is done via the in assembly code instruction. Just like the out instruction there is no way to use the in assembly code instruction from C. Therefore in assembly code instruction should be wrapped in a function in assembly code that can be accessed from C language using the cdecl calling standard as shown below:
Update the io.s file in your working directory and save the above assembly code in it as follows:
Now update the io.h file in your working directory with the following code to access the in assembly code instruction from C:
as shown in the figure:
Using the C function below we can check whether the transmit FIFO is empty:
As you can see 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.
The driver provide the functionality required to operate serial ports. A write function with the following declaration, explains the functionality that the driver should provide.
int serial_write(unsigned short com, char *buf, unsigned int len);
The following code represents serial_write:
Now let’s combine all the above mentioned codes for serial ports in to one file.
Just as in framebuffer section, let’s create header file serial_port.h for the serial port to store C function declarations, as well as macro definitions by including the following code:
So, now let’s create serial_port.c file that contains combined C source codes of the serial port. Save the following code in serial_port.c file:
Update your Makefile with just serial_port.o in it’s OBJECTS variable as shown in the figure below:
Now update your kmain.c file as follows:
Now we are needed to update Bochs configuration file bochsrc.txt in order to save the output from the first serial port. The com1 configuration instructs Bochs how to handle first serial port:
com1: enabled=1, mode=file, dev=com1.out
Now, use the “make run” command on terminal to operate your own operating system. You will see a file called com1.out in your working directory.
The output from serial port one will now be stored in the file com1.out. (Open com1.out with a text editor)
Now you have finished creating drivers for the framebuffer and the serial port. In the next article you will be presented with Segmentation in x86, which is accessing the memory through segments. You can read it from here.
Hope you understand steps in creating drivers for the framebuffer and the serial port. Let’s meet with the next article of Develop Your Own x86 Operating System(OS) series. Thank you so much for reading!!!!!!!!!!