Build your own Operating System #3_integrate_outputs
In the previous article, we discussed about you can implement C language instead of Assembly language. Because C is more convenient for humans than Assembly. In this article, we are going to discuss about how we can integrate outputs as a frame and as a serial out.
Let’s start…
Hardware interaction
As you may already know, there are two different ways to interact with hardware.
- Memory-mapped I/O
- I/O ports
Memory-mapped I/O
If we are using Memory-mapped I/O, we can write data to a specific memory address and the hardware will be updated with the new data. One example for Memory-mapped I/O is, Framebuffer.
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.
Framebuffer
The framebuffer(Frame store) is a hardware device that is capable of displaying a buffer of memory on the screen. 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. The framebuffer has 80 columns and 25 rows, and the row and column indices start at 0 (so rows are labelled 0–24).
Writing text to the screen
Now we are going to write text to the screen using framebuilder. The starting address of the memory-mapped I/O for the framebuffer is 0x000B8000
. The memory is divided in to 16 bits and it determine the charactor, foreground colour and the background colour as bellow.
Bit: | 15 14 13 12 11 10 9 8 | 7 6 5 4 | 3 2 1 0 |
Content: | ASCII | FG | BG |
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:
fb[0] = 'A';
fb[1] = 0x28;
The following code shows how it can be done. (You have to call kmain
in the loader.s
file.)
/** 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)
}#define FB_GREEN 2
#define FB_DARK_GREY 8//call this function in loader.s
void kmain(){
fb_write_cell(0, 'A', FB_GREEN, FB_DARK_GREY);
}
Now you have successfully displayed ‘A’ in the screen using framebuilder.
Moving the cursor
The framebuffer’s cursor is controlled by two separate I/O ports. The cursor’s location is defined by 16-bit integers: 0 represents line 0, column 0; 1 represents row 0, column 1; 80 represents a row, zero columns, and so on. The framebuffer contains two I/O ports: one for receiving data and the other for summarizing the data that has been received.
Port 0x3D4
is the port that describes the data and port 0x3D5
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:
The out
assembly code instruction can’t be executed directly in C. So, we need to wrap out
in a function in assembly code which can be accessed from C via the cdecl calling standard.
Store this file(io.s
) in your root directory. And then create a io.h
file and save this code to conveniently access the Assembly out
code.
Now, insert this code to your kmain.c
file.
#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);
}
Then, you can move the cursor by calling fb_move_cursor
function inside the kmain
function.
Eg:-
void kmain(){
fb_move_cursor(10);
}
Now we have successfully created C functions for writing a character and moving the cursor. Now, try to write a C function to write a buffer to the screen using framebuffer by yourself. The following code will show you how I did it.
I can call this function in kmain.c
like this:
void kmain(){
char str[] = "Hello world..!!!";
fb_write(00, str, 16);
}
The Serial Ports
A serial port is an interface that use to communicate between hardware devices and although it is available on almost all motherboards. The serial port is easy to use, and, more importantly, it can be used as a logging utility in Bochs too. In this section, we will only use the serial ports for output, not input. The serial ports are completely controlled via I/O ports.
Configuring the Serial Port
First, we need to send configuration data to the serial port. 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)
Configuring the Line
In here, we need to configure, how data is being sent over the line. The serial port has an I/O port, which is a configuration line command port. First, the data transmission speed will be configured. The serial port features an internal clock with a 115200 Hz
frequency. Setting speed to a serial port implies sending divisions, such as transmission 2, at a rate of 115200/2 = 57600 Hz
.
Even if the divisor is a 16 bit number, we can only send 8 bits at a time. So, we need to send an instruction telling the serial port to first expect the highest 8 bits, then the lowest 8 bits. This is done by sending 0x80
to the line command port. An example is shown below:
#include "io.h"
#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)
#define SERIAL_LINE_ENABLE_DLAB 0x80
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 |
Descriptions for each of the above names.
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 code:
void serial_configure_line(unsigned short com)
{
outb(SERIAL_LINE_COMMAND_PORT(com), 0x03);
}
Configuring the Buffers
Whether receiving or transferring data, data is deposited in the buffer when sent over a serial connection. As a result, sending data to the serial port will cause it to be buffered before being sent over the wired network. If you send too much data too quickly, though, the buffer will fill up and the data will be lost. In other words, the buffer is a first-in, first-out (FIFO) queue. The following are the FIFO queue configuration bytes:
Bit: | 7 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Content: | lvl | bs | r | dma | clt | clr | e |
We use the value 0xC7 = 11000111
that will:
- Enables FIFO
- Clear both receiver and transmission FIFO queues
- Use 14 bytes as size of queue
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.
Bit: | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Content: | r | r | af | lb | ao2 | ao1 | rts | dtr |
In here, we don’t need to enable interrupts, because we won’t handle any received data. Therefore we use the configuration value 0x03 = 00000011
(RTS = 1 and DTS = 1) for now.
Writing Data to the Serial Port
Before writing to a serial port, the transmit FIFO queue has to be empty. The transmit FIFO queue is empty if bit 5 of the line status I/O port is equal to one.
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 (the same way as the out
assembly code instruction):
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]
in al, dx
ret
So, append the above code to the end of the io.s
file that you have already created. And then add the following statement to the end of the io.h
file.
unsigned char inb(unsigned short port);
I thought it’s better to define all the functions for the serial writing part, in a header file called serial_write.h
and include it into the kmain.c
file. And i wrote a custom function to write a serial buffer. So, after all the configurations, the serial_write.h
file will look like this:
Call the serial_write
function in kmain.c
:
void kmain(){
char str[] = "Hello world...!!!";
serial_write(0x3F8, str, 17);
}
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
Now you should be able to write to a serial port. After running the Bochs
Emulator, execute the command cat com1.out
and you will see your message.
You can download a completed code that I have created for integrating outputs for the OS from: here
Hope you have successfully integrated outputs to your OS and hope to catch you in the next article.
Thank you!