Build Your Own Operating System: #3

Vinojan Veerapathirathasan
7 min readAug 6, 2021

--

Play with `Outputs`

In this article, I am going to show you how to write data to the serial port and display text on the console. So, It is a third part of the process of implementing our own OS. We’ll also write our first driver, which will function as a layer between the kernel and the hardware, offering a higher level of abstraction than making contact with the hardware.

Interacting with the Hardware

Memory-mapped I/O and I/O ports are the two most common ways to interact with hardware. You can write to a particular memory address and the hardware will be updated with the new data if the hardware uses memory-mapped I/O. If the hardware supports I/O ports, assembly code instructions out and in must be utilized to connect with it. The instruction out takes two parameters that are the address of the I/O port and the data to send. The instruction accepts only one parameter, the I/O port address, and returns data from the hardware.

01. Construct a driver for the framebuffer to display text on the Console.

The framebuffer is a piece of hardware that can display a memory buffer on the screen.

1.1 Writing Text

Memory-mapped I/O is used to write text to the console via the frame buffer. 0x000B8000 is the starting address for the frame buffer’s memory-mapped I/O. The memory is divided into 16-bit cells, with each character, foreground color, and background color determined by the 16 bits. According to the shown diagram, you can identify respective bits for ASCII, the background, and the foreground:

On the console, the first cell corresponds to row zero, column zero. A corresponds to 65 or 0x41, according to an ASCII table. 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

As a result, the second cell belongs to row zero, column one, and its address is:

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, with a green foreground and a dark grey background, writing A at (0,0) becomes:

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

Let’s make this a function,

So, according to this function, we can write A at the place (0,0) with green foreground and dark grey background as follows,

1.2 Moving the Cursor

Two separate I/O ports are used to move the framebuffer’s pointer. A 16-bit integer is used to calculate the cursor’s position: 0 denotes row 0 and column 0; 1 denotes row 0 and column one; 80 denotes row one and column zero, and so on. Because the position is 16 bits long and the out assembly code instruction argument is 8 bits long, the position must be sent in two steps, the first 8 bits followed by the following 8 bits. The framebuffer contains two I/O ports, one for taking data and the other for describing it. The port that specifies the data is 0x3D4, while the port that contains the data is 0x3D5 .

The assembly code instructions to set the cursor at row one, column zero (position 80 = 0x0050)

In C, the out assembly code instruction cannot be directly executed. So, we can wrap out in a function in assembly code which can be accessed from C via the cdecl calling standard:

you can store this function in io.s and create header io.h , the following out assembly code instruction can be easily accessed from C .

Moving the cursor can now be wrapped in a C function:

1.3 The Driver

The frame buffer should be accessed using an interface provided by the driver, which will be used by the remainder of the OS code. The following 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.

int write(char *buf, unsigned int len);

02. Create a driver for the serial port

The serial port is a hardware-to-hardware communication interface that, while present on virtually all motherboards, is rarely accessible to the user in the form of a DE-9 connection today. The serial port is simple to operate, and it may also be used as a logging facility in Bochs. We will only use the serial ports for output, not input. The serial ports are completely controlled by I/O ports.

2.1 Configuring the Serial Port

The serial port will receive the first data as configuration data. Two hardware devices must agree on a few things before they may communicate with each other. Those are the speed used for sending data (bit or baud rate) if any error checking should be used for the data (parity bit, stop bits), and the number of bits that represent a unit of data (data bits).

2.2 Configuring the Line

The term “configuring the line” refers to the process of determining how data is transmitted across the line.

The data transmission speed will be configured first. The internal clock in the serial port is set to 115200 Hz. Setting the speed involves sending a divisor to the serial port, such as 2 for a speed of 115200 / 2 = 57600 Hz. We can only send 8 bits at a time since the divisor is a 16-bit integer. 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. Sending 0x80 to the line command port does this. The following is an example:

We’ll use the main standard value of 0x03, which has an 8-bit length, no parity bit, one stop bit, and no break control. As seen in the following example, this is transmitted to the line command port:

2.3 Configuring the Buffers

Data is placed in buffers when sent over the serial port, both while receiving and transmitting data. If you transmit data to the serial port quicker than the serial port can transfer it across the wire, the data will be buffered. If you send too much data too quickly, though, the buffer will fill up and data will be lost. The buffers, in other terms, are FIFO queues. The FIFO queue configuration byte appears to be as follows:

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

We use the value 0xC7 = 11000111 that:

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

2.4 Configuring the Modem

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 |

We don’t need to activate interrupts because we won’t be dealing with any data that comes in. As a result, we utilize the 0x03 = 00000011 configuration value (RTS = 1 and DTS = 1).

2.5 Writing Data to the Serial Port

The in assembly code instruction is used to read the contents of an I/O port. Because the in assembly code instruction cannot be used directly from C, it must be wrapped (in the same manner as the out assembly code instruction is wrapped):

global inb    ; inb - returns a byte from the given I/O port
; stack: [esp + 4] The address of the I/O port
; [esp ] The return address
inb:
mov dx, [esp + 4] ; move the address of the I/O port to the dx register
in al, dx ; read a byte from the I/O port and store it in the al register
ret ; return the read byte/* in file io.h */
/** inb:
* Read a byte from an I/O port.
*
* @param port The address of the I/O port
* @return The read byte
*/
unsigned char inb(unsigned short port);

2.6 Configuring Bochs

The Bochs configuration file bochsrc.txt must be changed to store the output from the first serial port. The com1 setting tells Bochs how to handle the first serial port:

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

Serial port one’s output will now be stored in the file com1.out.

Reference: The little book about OS development/Erik Helin, Adam Renberg

Additionally, you can see changes in my files after the integrate_outputs step here. Hope you all understand this blog and I will publish more articles to achieve our main goal “Develop our own OS”. Keep reading.

Thank you!!!

--

--

Vinojan Veerapathirathasan

Software Engineer at EL | Designer | Medium Writer | AI Enthusiast | Entrepreneur | Specializing in Modern Web Application Development