#3-integrate_outputs

Welcome back to the 3rd stage of my Operating system article series. In this article, I will discuss how to design Drivers for our operating system which function as a layer between the Kernel and the Hardware.

With this chapter will demonstrate how to show text on the console and write data to the serial port. Furthermore, we will write two drivers, which are the codes that act as layers between the kernel and the hardware, providing a higher level of abstraction than directly communicating with the hardware.

What is a Driver?

A driver, also known as a device driver, is a collection of files that instructs a piece of hardware on how to operate via interacting with a computer’s operating system.

For all this to be done, We have to configure two devices. They are,

  1. Framebuffer
  2. Serial Ports

Before talking about the above two devices, I’d like to give you a little introduction to hardware interaction.

How to Interact with the Hardware

Memory-mapped I/O and I/O ports are the two most common ways to interact with hardware.

If the hardware supports memory-mapped I/O, you can write to a specific memory address and the hardware will be updated.

If the hardware uses I/O ports then the assembly code instructions out and in must be used to communicate with the hardware. The instruction out takes two parameters: the address of the I/O port and the data to send.

The instruction in takes a single parameter, the address of the I/O port, and returns data from the hardware. One can think of I/O ports as communicating with hardware the same way as you communicate with a server using sockets. The cursor (the blinking rectangle) of the framebuffer is one example of hardware controlled via I/O ports on a PC.

Now let’s discuss how we can practically implement the output functions with the Framebuffer and Serial Port.

The Framebuffer

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

Writing text to the console via framebuffer

The text write console is performed by the frame buffer is completed by the I / O map of memory. The starting address of the memory mapping I / O of the frame buffer is 0x000b8000. The memory is divided into 16-bit unit, 16 bits determine characters, foreground, and background colors. The highest eight is the character’s ASCII value, bit 7–4 is the background, bit 3–0 is the foreground, as can be seen in the following figure:

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

The following table shows you available colors and their values.

The available colors and their values

On the console, the first cell corresponds to row zero, column zero. Using an ASCII table, we can see that ‘A’ corresponds to 65, or 0x41. As a result, the following assembly code instruction is used to write the character A with a green foreground (2) and a dark grey background (8) at position (0,0):

mov [0x000B8000], 0x4128

consequently, the second cell corresponds to row zero, column one, and its address is as follows:

0x000B8000 + 16 = 0x000B8010

Writing to the framebuffer can also be done in C by treating the address 0x000B8000 as a char pointer: char *fb = (char *) 0x000B8000. Then, writing A at (0,0) with a green foreground and a dark grey background becomes:

fb[0] = 'A';
fb[1] = 0x28;

The code below demonstrates how this can be wrapped into a function:

The above function can be called as follows:

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: 0 means row zero, column zero; 1 means row zero, column one; 80 means row one, column zero and so on.

Since the position is 16 bits large, and the out assembly code instruction argument is 8 bits, the position must be sent in two turns, first 8 bits then the next 8 bits. The framebuffer has two I/O ports, one for accepting the data, and one for describing the data being received. Port 0x3D4is the port that describes the data and port 0x3D5is for the data itself.

To set the cursor at row one, column zero (position 80 = 0x0050), one would use the following assembly code instructions:

The out assembly code instruction can’t be executed directly in C. Therefore it is a good idea to wrap out in a function in assembly code which can be accessed from C via the cdecl calling standard :

By storing this function in a file called io.s and also creating a header io.h, the out assembly code instruction can be conveniently accessed from C:

Now we can wrap the cursor moving functionality in a C function as follows:

You can now move the cursor by putting the following command in your main C function.

fb_move_cursor(600); // move the cursor to 600th position

Implementing the driver for the framebuffer

Now let’s create a driver to write a string to the console with proper cursor movements. We can reuse the C functions that we created for writing characters and moving the cursor for this.

Following is the kmain.c file configured to write string: “Welcome to YmeOS” to the console.

It is a good practice to move the driver to a separate header file as it will be more convenient to use in the future.

The Serial Ports

The serial ports will only be used for output, not input in this section. Serial ports are controlled I/O ports.

Configuring the Serial Port

Configuration data is the first thing that has to be transmitted to the serial port. Two hardware devices must agree on a few things in order to communicate with one other. Following information must included.

  • 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 configuring how data is transmitted across the line. The serial port has an I/O port for configuration, known as the line command port.

The speed at which data is sent will be determined first. The internal clock speed of the serial port is 115200 Hz. Setting the speed means sending a divisor to the serial port, for example sending 2 results in a speed of 115200 / 2 = 57600 Hz.

The divisor is a 16 bit number but we can only send 8 bits at a time. As a result, we must send an instruction to the serial port, instructing it to expect the highest 8 bits first, followed by the lowest 8 bits. This can be done by sending 0x80 to the line command port. The following function can be used to set the speed.

Furthermore, The method by which data should be sent must be configured. This is also done by sending a byte to the line command port. The layout of the 8 bits is as follows:

Bit: | 7 | 6 | 5 4 3 | 2 | 1 0 |
Content: | d | b | prty | s | dl |
d Enables (d = 1) or disables (d = 0) DLABb If break control is enabled (b = 1) or disabled (b = 0)prty The number of parity bits to uses The number of stop bits to use (s = 0 equals 1, s = 1 equals 1.5 or 2)dl Describes the length of the data

You can refer to this article on OSDev to get a better understanding of these values. However, We will use the most standard value 0x03, which means a length of 8 bits, no parity bit, one stop bit and break control disabled. This can be sent to the line command port, as seen in the following code:

Configuring the Buffers

When data is transmitted via the serial port it is placed in buffers, both when receiving and sending data. This way, if you send data to the serial port faster than it can send it over the wire, it will be buffered. However, if you send too much data too fast, the buffer will be full and data will be lost. In other words, the buffers function as FIFO queues. The FIFO queue configuration byte is depicted in the figure below:

Bit:     | 7 6 | 5  | 4 | 3   | 2   | 1   | 0 |
Content: | lvl | bs | r | dma | clt | clr | e |

Following are the meanings of the names mentioned above:

lvl    How many bytes should be stored in the FIFO buffersbs     If the buffers should be 16 or 64 bytes larger      Reserved for future usedma    How the serial port data should be accessedclt    Clear the transmission FIFO bufferclr    Clear the receiver FIFO buffere      If the FIFO buffer should be enabled or not

We’ll use the value 0xC7 = 11000111, which means:

  • FIFO is enabled,
  • FIFO queues for both the receiver and the transmission are cleared and,
  • The queue size is set to 14 bytes.

We can use the following code to configure the buffers with the above value.

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, RTS and DTR should be set to 1, indicating that we are ready to send data.

The modem configuration byte is shown in the following figure:

Bit:     | 7 | 6 | 5  | 4  | 3   | 2   | 1   | 0   |
Content: | r | r | af | lb | ao2 | ao1 | rts | dtr |

Following are the descriptions for the above-mentioned short terms.

r      Reserved
af Autoflow control enabled
lb Loopback mode (used for debugging serial ports)
ao2 Auxiliary output 2, used for receiving interrupts
ao1 Auxiliary output 1
rts Ready To Transmit
dtr Data Terminal Ready

We don’t need to enable interrupts because we won’t be dealing with any data that comes in. Therefore we use 0x03 = 00000011 (RTS = 1 and DTS = 1) as the configuration value. We can use the following C function to achieve that.

Write data to the serial port

Through the data I/O port, write data to the serial port. However, the FIFO queue must be empty before writing (you must complete all previous writes).
The FIFO queue is empty if bit 5 of the line status I/O port is set to 1. The IN assembly code instruction is used to read the contents of the I/O port.

In C, the in assembly code instruction isn’t available, therefore you’ll have to pack it (just like the OUT assembly code instruction):.

Configuring Bochs

You must edit the Bochs profile bochsrc.txt to store the output of the first serial port.
The COM1 setting specifies how the first serial port is handled by Bochs:

Serial port 1’s output is now saved in the file com1.out.
If you try to start OS by making a run after finishing this, you will get an error since we have yet to create the main function to execute our functions.
In the next article, we will deconstruct this.

References:

The little book about OS development (Erik Helin, Adam Renberg)

Thank you very much for reading!

Hope to see you again with “integrate_segmentation” chapter. Till then, STAY SAFE!!!

Happy reading …

-Thushara Samaraweera-

--

--