Build Your Own OS #3

Interacting with the Hardware

There are two different ways to interact with the hardware, memory-mapped I/O and I/O ports. 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. Frame-buffer is memory mapped I/O method.

The Frame buffer

The framebuffer is a hardware device that is capable of displaying a buffer of memory on the screen. The framebuffer has 80 columns and 25 rows, and the row and column indices start at 0 (so rows are labelled 0–24). 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 second cell then corresponds to row zero, column one and its address is therefore:

0x000B8000 + 16 = 0x000B8010
fb[0] = 'A';
fb[1] = 0x28;
#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);
/* 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);
#endif /* INCLUDE_IO_H */
#ifndef INCLUDE_FB_H
#define INCLUDE_FB_H
#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
#define FB_GREEN 2
#define FB_DARK_GREY 8
char *fb = (char *) 0x00B8000;/** 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*2] = c;
fb[i*2 + 1] = ((fg & 0x0F) << 4) | (bg & 0x0F);
}
/** 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);
}
int write(char *buf, unsigned int len)
{
unsigned int i = 0;
for ( i = 0; i < len; i++)
{
fb_write_cell(i, buf[i], FB_GREEN, FB_DARK_GREY);
}
return 0;
}
#endif

The 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. The serial port is easy to use, and, more importantly, it can be used as a logging utility in Bochs. If a computer has support for a serial port, then it usually has support for multiple serial ports, but we will only make use of one of the ports. This is because we will only use the serial ports for logging. Furthermore, 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

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)

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.

#ifndef INCLUDE_SERIAL_PORT_H
#define INCLUDE_SERIAL_PORT_H
#include “io.h” /* io.h is implement in the section “Moving the cursor” *//* The I/O port com1: enabled=1, mode=file, dev=com1.outs */
/* 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
void serial_configure_baud_rate(unsigned short com, unsigned short divisor) {
/* Tell 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
*/
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);
}
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
* data length of 8 bits, one stop bit, no parity bit, break control
* disabled and DLAB disabled
*/
outb(SERIAL_LINE_COMMAND_PORT(com), 0x03);
}
void serial_configure_fifo_buffer(unsigned short com) {
/* Bit: | 7 6 | 5 | 4 | 3 | 2 | 1 | 0 |
* Content: | lvl | bs | r | dma | clt | clr | e |
* Value: | 1 1 | 0 | 0 | 0 | 1 | 1 | 1 | = 0xC7
*/
outb(SERIAL_FIFO_COMMAND_PORT(com), 0xC7);
}
void serial_configure_modem(unsigned short com) {
/* Bit: | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
* Content: | r | r | af | lb | ao2 | ao1 | rts | dtr |
* Value: | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | = 0x03
*/
outb(SERIAL_MODEM_COMMAND_PORT(com), 0x03);
}
int serial_is_transmit_fifo_empty(unsigned short com) {
/* 0x20 = 0010 0000 */
return inb(SERIAL_LINE_STATUS_PORT(com)) & 0x20;
}
void serial_configure(unsigned short port, unsigned short baudRate) {
serial_configure_baud_rate(port, baudRate);
serial_configure_line(port);
serial_configure_fifo_buffer(port);
serial_configure_modem(port);
}
void serial_write_byte(unsigned short port, char byteData) {
outb(port, byteData);
}
int serial_write(unsigned short com, char *buf, unsigned int len) {
unsigned int indexToBuffer = 0;
while (indexToBuffer < len) {
if (serial_is_transmit_fifo_empty(com)) {
serial_write_byte(com, buf[indexToBuffer]);
indexToBuffer++;
}
}
return 0;
}
#endif
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
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
#include “io.h”
#include “fb.h”
#include “serial_port.h”
void run(){
char c[] = “Welcome”;
write(c, 9);
serial_write(SERIAL_COM1_BASE, c, 7);
}
global loader ; the entry symbol for ELFMAGIC_NUMBER equ 0x1BADB002 ; define the magic number constant
FLAGS equ 0x0 ; multiboot flags
CHECKSUM equ -MAGIC_NUMBER ; calculate the checksum
; (magic number + checksum + flags should equal 0)
KERNEL_STACK_SIZE equ 4096 ; size of stack in bytes
section .bss
align 4 ; align at 4 bytes
kernel_stack: ; label points to beginning of memory
resb KERNEL_STACK_SIZE ; reserve stack for the kernel
section .text: ; start of the text (code) section
align 4 ; the code must be 4 byte aligned
dd MAGIC_NUMBER ; write the magic number to the machine code,
dd FLAGS ; the flags,
dd CHECKSUM ; and the checksum
loader: ; the loader label (defined as entry point in linker script)
mov eax, 0xCAFEBABE ; place the number 0xCAFEBABE in the register eax
mov esp, kernel_stack + KERNEL_STACK_SIZE ; point esp to the start of the
; stack (end of memory area)
; Calling C code
extern run
call run
.loop:
jmp .loop ; loop forever
OBJECTS = loader.o kmain.o io.o
CC = gcc
CFLAGS = -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector \
-nostartfiles -nodefaultlibs -Wall -Wextra -Werror -c
LDFLAGS = -T link.ld -m elf_i386
AS = nasm
ASFLAGS = -f elf
all: kernel.elfkernel.elf: $(OBJECTS)
ld $(LDFLAGS) $(OBJECTS) -o kernel.elf
baseOS.iso: kernel.elf
cp kernel.elf iso/boot/kernel.elf
genisoimage -R \
-b boot/grub/stage2_eltorito \
-no-emul-boot \
-boot-load-size 4 \
-A os \
-input-charset utf8 \
-quiet \
-boot-info-table \
-o baseOS.iso \
iso
run: baseOS.iso
bochs -f bochsrc.txt -q
%.o: %.c
$(CC) $(CFLAGS) $< -o $@
%.o: %.s
$(AS) $(ASFLAGS) $< -o $@
clean:
rm -rf *.o kernel.elf baseOS.iso
megs: 32
display_library: sdl
romimage: file=/usr/share/bochs/BIOS-bochs-latest
vgaromimage: file=/usr/share/bochs/VGABIOS-lgpl-latest
ata0-master: type=cdrom, path=baseOS.iso, status=inserted
boot: cdrom
log: bochslog.txt
clock: sync=realtime, time0=local
cpu: count=1, ips=1000000
com1: enabled=1, mode=file, dev=com1.out

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store