Let’s make your own operating system (#week 3)

Nimantha Gayan
6 min readAug 6, 2021

--

In the 2nd week we helped you how to use C instead of assembly code as the programming language for the OS. In this 3rd week we are going to discuss about how we can integrate outputs as a frame and as a serial out.

Framebuffer

A framebuffer (frame buffer, or sometimes framestore) is a portion of random-access memory (RAM) containing a bitmap that drives a video display. It is a memory buffer containing data representing all the pixels in a complete video frame. Modern video cards contain framebuffer circuitry in their cores. This circuitry converts an in-memory bitmap into a video signal that can be displayed on a computer monitor.

In computing, a screen buffer is a part of computer memory used by a computer application for the representation of the content to be shown on the computer display. The screen buffer may also be called the video buffer, the regeneration buffer, or regen buffer for short. Screen buffers should be distinguished from video memory. To this end, the term off-screen buffer is also used

Memory-mapped I/O

Memory-mapped Both memory and I/O devices are addressed using the same address space in I/O. The I/O devices’ memory and registers are mapped to (related with) address values. As a result, a memory address can correspond to either a part of physical RAM or the I/O device’s memory. As a result, the same CPU commands that are used to access memory may also be used to access devices.

You can write a specific memory location and the hardware will be updated with new data if the hardware uses the memory map’s I/O. The frame buffer is an example that will be covered in greater depth later. When the value 0x410f is sent to the address 0x000b8000, for example, the white letter A appears on a black background.

I/O ports

On the other hand, if we are using I/O ports, we can read or write data through a specific port. We will be using Assembly instructions in and out to communicate with the hardware using I/O ports. The cursor (the blinking rectangle) of the framebuffer is one example of hardware controlled via I/O ports on a PC.

Writing Text

Writing text to the console via the framebuffer is done with memory-mapped I/O. The starting address of the memory-mapped I/O for the framebuffer is 0x000B8000 [27]. The memory is divided into 16 bit cells, where the 16 bits determine both the character, the foreground color and the background color. The highest eight bits is the ASCII [28] value of the character, bit 7–4 the background and bit 3–0 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 available colors are shown in the following table:

ColorValueColorValueColorValueColorValueBlack0Red4Dark grey8Light red12Blue1Magenta5Light blue9Light magenta13Green2Brown6Light green10Light brown14Cyan3Light grey7Light cyan11White15

The first cell corresponds to row zero, column zero on the console. Using an ASCII table, one can see that A corresponds to 65 or 0x41. Therefore, to write the character A with a green foreground (2) and dark grey background (8) at place (0,0), the following assembly code instruction is used:

mov [0x000B8000], 0x4128

The second cell then corresponds to row zero, column one and its address is therefore:

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

fb[0] = ‘A’;
fb[1] = 0x28;

The following code shows how this can be wrapped into a function:

/** fb_write_cell:
* Writes a character with the given foreground and background to position i
* in the framebuffer.
*
* @param i The location in the framebuffer
* @param c The character
* @param fg The foreground color
* @param bg The background color
*/
void fb_write_cell(unsigned int i, char c, unsigned char fg, unsigned char bg)
{
fb[i] = c;
fb[i + 1] = ((fg & 0x0F) << 4) | (bg & 0x0F)
}

The function can then be used as follows:

#define FB_GREEN 2
#define FB_DARK_GREY 8

fb_write_cell(0, ‘A’, FB_GREEN, FB_DARK_GREY);

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 0x3D4 [29] is the port that describes the data and port 0x3D5 [29] is for the data itself.

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

out 0x3D4, 14 ; 14 tells the framebuffer to expect the highest 8 bits of the position
out 0x3D5, 0x00 ; sending the highest 8 bits of 0x0050
out 0x3D4, 15 ; 15 tells the framebuffer to expect the lowest 8 bits of the position
out 0x3D5, 0x50 ; sending the lowest 8 bits of 0x0050

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 [25]:

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

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:

#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 */

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

#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 */

The Driver

The driver should provide an interface that the rest of the code in the OS will use for interacting with the framebuffer. There is no right or wrong in what functionality the interface should provide, but a suggestion is to have a write function with the following declaration:

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.

Serial Ports

The serial port is an interface for communicating between hardware devices and although it is available on almost all motherboards, it is seldom exposed to the user in the form of a DE-9 connector nowadays.

Now create a c file called serial_port.c

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.

As final step type make run in the terminal then you can see the out put.

Thank you

--

--

Nimantha Gayan
Nimantha Gayan

Written by Nimantha Gayan

Software Engineering , University Of Kelaniya

No responses yet