Create New Operating System-Week 3

A.M.V.N.Attanayaka
9 min readAug 6, 2021

hello everyone, I am back this is our third week to creating own OS. lets start.

In this week we going to take about how to display text on the console as well as writing data to the serial port.

Interacting with the Hardware

There are usually two different ways to interact with the hardware, memory-mapped I/O and I/O ports.

  1. 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.
  2. If the hardware uses I/O ports then the assembly code instructions out and in must be used to communicate with the hardware.

The Framebuffer

The framebuffer is a hardware device that is capable of displaying a buffer of memory on the screen. It has 80 columns and 25 rows.

Writing Text on console screen

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:

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);

After writing those function, kmain.c file look like this:

you can see gray colour “A” in green background. After running “make run” command.

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 is the port that describes the data and port 0x3D5is for the data itself.

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:

make io.s file using “touch io.s” command.after creating write follow code on it.

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

create io.h file and write this command on it.

#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:

#include "io.h"    /* The I/O ports */
#define FB_COMMAND_PORT 0x3D4
#define FB_DATA_PORT 0x3D5
/* The I/O port commands */
#define FB_HIGH_BYTE_COMMAND 14
#define FB_LOW_BYTE_COMMAND 15
/** fb_move_cursor:
* Moves the cursor of the framebuffer to the given position
*
* @param pos The new position of the cursor
*/
void fb_move_cursor(unsigned short pos)
{
outb(FB_COMMAND_PORT, FB_HIGH_BYTE_COMMAND);
outb(FB_DATA_PORT, ((pos >> 8) & 0x00FF));
outb(FB_COMMAND_PORT, FB_LOW_BYTE_COMMAND);
outb(FB_DATA_PORT, pos & 0x00FF);
}

Also write io.o on Makefile

now run again ,now you can see cerser you provide place (I use 00 point)

The Driver

The serial port [30] 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. The serial port is easy to use, and, more importantly, it can be used as a logging utility in Bochs

  1. The first data that need 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)

2. 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.

sending 0x80 to the line command port. An example is shown below:

#include "io.h" /* io.h is implement in the section "Moving the cursor" */    /* The I/O ports */    /* All the I/O ports are calculated relative to the data port. This is because
* all serial ports (COM1, COM2, COM3, COM4) have their ports in the same
* order, but they start at different values.
*/
#define SERIAL_COM1_BASE 0x3F8 /* COM1 base port */ #define SERIAL_DATA_PORT(base) (base)
#define SERIAL_FIFO_COMMAND_PORT(base) (base + 2)
#define SERIAL_LINE_COMMAND_PORT(base) (base + 3)
#define SERIAL_MODEM_COMMAND_PORT(base) (base + 4)
#define SERIAL_LINE_STATUS_PORT(base) (base + 5)
/* The I/O port commands */ /* SERIAL_LINE_ENABLE_DLAB:
* Tells the serial port to expect first the highest 8 bits on the data port,
* then the lowest 8 bits will follow
*/
#define SERIAL_LINE_ENABLE_DLAB 0x80
/** serial_configure_baud_rate:
* Sets the speed of the data being sent. The default speed of a serial
* port is 115200 bits/s. The argument is a divisor of that number, hence
* the resulting speed becomes (115200 / divisor) bits/s.
*
* @param com The COM port to configure
* @param divisor The divisor
*/
void serial_configure_baud_rate(unsigned short com, unsigned short divisor)
{
outb(SERIAL_LINE_COMMAND_PORT(com),
SERIAL_LINE_ENABLE_DLAB);
outb(SERIAL_DATA_PORT(com),
(divisor >> 8) & 0x00FF);
outb(SERIAL_DATA_PORT(com),
divisor & 0x00FF);
}

The way that data should be sent must be configured. This is also done via the line command port by sending a byte. The layout of the 8 bits looks like the following:

Bit:     | 7 | 6 | 5 4 3 | 2 | 1 0 |
Content: | d | b | prty | s | dl |

A description for each name can be found in the table below:

We will use the mostly standard value 0x03 , meaning a length of 8 bits, no parity bit, one stop bit and break control disabled. This is sent to the line command port, as seen in the following example:

/** serial_configure_line:
* Configures the line of the given serial port. The port is set to have a
* data length of 8 bits, no parity bits, one stop bit and break control
* disabled.
*
* @param com The serial port to configure
*/
void serial_configure_line(unsigned short com)
{
/* Bit: | 7 | 6 | 5 4 3 | 2 | 1 0 |
* Content: | d | b | prty | s | dl |
* Value: | 0 | 0 | 0 0 0 | 0 | 1 1 | = 0x03
*/
outb(SERIAL_LINE_COMMAND_PORT(com), 0x03);
}

3. Configuring the Buffers

When data is transmitted via the serial port it is placed in buffers, both when receiving and sending data

the buffers are FIFO queues. The FIFO queue configuration byte looks like the following figure:

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

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 |

5. Writing Data to the Serial Port

Reading the contents of an I/O port is done via the in assembly code instruction. There is no way to use the in assembly code instruction from C, therefore it has to be wrapped,write this function on io.h 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
/* 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);

Checking if the transmit FIFO is empty can then be done from C:

#include "io.h"

/** serial_is_transmit_fifo_empty:
* Checks whether the transmit FIFO queue is empty or not for the given COM
* port.
*
* @param com The COM port
* @return 0 if the transmit FIFO queue is not empty
* 1 if the transmit FIFO queue is empty
*/
int serial_is_transmit_fifo_empty(unsigned int com)
{
/* 0x20 = 0010 0000 */
return inb(SERIAL_LINE_STATUS_PORT(com)) & 0x20;
}

6. Configuring Bochs

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

now you can run using make run command.The output from serial port one will now be stored in the file “com1.out” file,and also you can see it in terminal.

I hope you can make your first drive using this steps. and also you can see my git hub folder using this link.

hope to see you in week 4 artical wery soon.

Thank you!

--

--