Develop our own operating system! #part03
Integrate outputs
Last week we have developed our operating system to implement with C. But so far we haven’t see any obvious output form our operating system. So, this week as we promised we are going to display text on our console.
With this chapter will demonstrate how to show text on the console and write data to the serial port. And also we will write our first driver.
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.
This week first we create a driver for the framebuffer so that text will be shown on the console. The we demonstrate how to write a serial port driver. Bochs may log serial port output in a loop, thereby establishing a logging mechanism for the operating system.
Framebuffer is an area of memory used to hold the frame of data that is continuously being sent to the screen.
A serial port is a serial communication interface that allows information to be sent in or out one bit at a time.
How do we interact with those 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 with new data in that memory address. The framebuffer, For example, if you type 0x410F into the address 0x000B8000, you will see the letter A in white on a black backdrop.
If the hardware has I/O ports, the assembly code instructions out and in must be used to connect with it. The instruction out accepts two parameters: the I/O port address and the data to send. The instruction in accepts a single parameter, the I/O port address, and returns data from the hardware. The cursor (the blinking rectangle) is one example of hardware controlled via I/O ports on a PC.
Writing texts with framebuffer
As we mentioned before framebuffer is a hardware device that can show a memory buffer on the screen. It has 80 columns and 25 rows, with row and column indices beginning at 0.
Memory-mapped I/O is used to write text to the console via the framebuer. The framebuffer’s memory-mapped I/O begins at address 0x000B8000. The memory is split into 16 bit cells, with the 16 bits determining both the character and the foreground and background colors. As seen below, the highest eight bits are the ASCII value of the character, bit 7–4 is the background, and bit 3–0 is the foreground.
Bit: | 15 14 13 12 11 10 9 8 | 7 6 5 4 | 3 2 1 0 |
Content: | ASCII | FG | BG |
You can find the color code as follows.
On the console, the first cell corresponds to row zero, column zero. Using an ASCII table, it is possible to determine 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
As a result, the second cell corresponds to row zero, column one, and its address is,
0x000B8000 + 16 = 0x000B8010
You can use C language for the same implementation now. In C, you may write to the framebuffer by treating the address 0x000B8000 as a char pointer, char *fb = (char *) 0x000B8000. The framebuffer.c file will look as follows.
The header file of the framebuffer can be demonstrate as follows.
Now you can call these functions. This can be included to the kmain.c file inside the kmain function. Because that is the function that we called using loader.s file.
Move the cursor
The framebuffer’s cursor is moved via two separate I/O ports. The location of the cursor is defined by a 16-bit integer: 0 denotes row zero and column zero; 1 denotes row zero 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 turns, the first 8 bits followed by the following 8 bits. The framebuffer contains two I/O ports, one for receiving data and the other for describing the data received. The port that specifies the data is 0x3D4 , while the port that contains the data is 0x3D5.
The following assembly code instructions might be used to place the cursor to row one, column zero (position 80 = 0x0050): This code should be included to the io.s file.
global outb ; make the label outb visible outside this file
; outb - send a byte to an I/O port
; stack: [esp + 8] the data byte
; [esp + 4] the I/O port
; [esp ] return address
outb:
mov al, [esp + 8] ; move the data to be sent into the al register
mov dx, [esp + 4] ; move the address of the I/O port into the dx register
out dx, al ; send the data to the I/O port
ret ; return to the calling function
Then io.h file will contain following code. It is not necessary to create header file but when io.s has a header file the out
assembly code instruction can be conveniently accessed from C.
#ifndef INCLUDE_IO_H
#define INCLUDE_IO_H
/** outb:
* Sends the given data to the given I/O port. Defined in io.s
*
* @param port The I/O port to send the data to
* @param data The data to send to the I/O port
*/
void outb(unsigned short port, unsigned char data);
#endif /* INCLUDE_IO_H */
Now your framebuffer.c file needed to update as follows.
Also the framebuffer.h file need to be updated as follows.
The Serial Port
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)
Line Configuration
Configuring the line means determining how data is delivered across the line. The serial port include an I/O port for configuration, known as the line command port.
First, the data transmission speed will be determined. The internal clock of the serial port is set at 115200 Hz. Setting the speed involves sending a divisor to the serial port, such as 2, which results in 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. We will use the mostly standard value 0x03, meaning a length of 8 bits, no parity bit, one stop bit and break control disabled.
The method of how data should be transmitted must be configured. This is also accomplished by sending a byte over the line command port. The arrangement of the 8 bits is as follows:
Bit: | 7 | 6 | 5 4 3 | 2 | 1 0 |
Content: | d | b | prty | s | dl |
Buffer Configuration
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, it will be buffered. However, if you send too much data too quickly, the buffer will fill up and data will be lost. In other words, the buffers function as FIFO queues. The FIFO queue configuration byte is depicted in the image below:
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 queue
Setting Up the Modem
The Ready To Transmit (RTS) and Data Terminal Ready (DTR) pins of the modem control register are utilized for very basic hardware flow control. When establishing the serial port, RTS and DTR should be set to 1, indicating that we are ready to transfer data. As mentioned before we don’t need to activate interrupts because we won’t be dealing with any data that comes in. As a result, the configuration value 0x03 = 00000011 (RTS = 1 and DTS = 1) is used.
The modem configuration byte is depicted in the diagram below:
Bit: | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Content: | r | r | af | lb | ao2 | ao1 | rts | dtr |
Data Writing to the Serial Port
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 data I/O port is used to write data to the serial port. However, the transmit FIFO queue must be empty before writing. That means all previous writes must have finished. If bit 5 of the line status I/O port is set to one, the transmit FIFO queue is empty.
The in assembly code instruction is used to read the contents of an I/O port. Because there is no way to utilize the in assembly code instruction from C, it must be wrapped.
Include this code to your io.s file.
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
Now add this code to io.h file.
/** 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);
Now your final serial.h file should be same as follows.
And the serial.c file can be created as follows.
Configuring Bochs
To save the output from the first serial serial port the Bochs configuration file bochsrc.txt
must be updated. The com1
configuration instructs Bochs how to handle first serial port:
com1: enabled=1, mode=file, dev=com1.out
The output from serial port one will now be stored in the file com1.out
.
After all those steps you can run your operating system with make run command.
According to this repository, you will see KleinOS as the output with dark grey letters in green background. And congratulations! Form now onward you can display your text output in the console! So, till the next week…. Be motivated! Keep learning…!
Reference: The little book about OS development/Erik Helin, Adam Renberg
Written by,
R.A.W. Lalendra,
Undergraduate in Bsc(hons) Software Engineering,
University of Kelaniya Sri Lanka.