In this article, we will learn how to program a basic video driver in C. Knowing that the characters to be displayed on the screen are mapped to a shared memory area between the graphics card and the OS, we will be able to print all kinds of characters at the desired coordinates. Our driver will support character printing, string printing, multi-line printing, and scrolling.
Before we begin, it is recommended that you read these previous articles:
- Boot Sector
- Interrupts
- Memory
- Stack
- IF-ELSE
- Functions
- Memory Segmentation
- Reading Data from Disk
- Entering Protected Mode 32bits
- Compilation, Linking, Stack Management, and Variables in C
- Pointers
- Kernel
- EntryPoint
- Gmake
- Gmake wildcards
- I/O Hardware
- Cross Compiler
As we explained in previous articles , each character on the screen is composed of an ASCII character plus its attribute. This implies that each character occupies 2 bytes in memory. Additionally, the characters are distributed in a matrix of 25x80 characters that make up the entire screen.
The video card is an MMIO device. To print characters on the screen, we must write to a certain region of memory shared between the video card and the OS. As we write to the memory, characters will be painted in the following positions of the matrix. The following table clearly shows the positions of the matrix, the associated character, the memory position, and its hexadecimal equivalent (very useful when we have the kernel loaded in Gdb and we are debugging).
NOTE: I have included position 4000 in the table despite being outside the matrix. I do this because it will be very useful to know this position when we debug the scrolling.
It should be noted that memory addresses are always counted byte by byte. Therefore, any operation that involves memory positions such as memory copying must always indicate the address in bytes. On the other hand, we have pointers that can be of any type we want. In this way, we can read/write from memory X by X, but I repeat that memory addresses are always in bytes.
To program the video driver, we will start from the files of previous articles and generate some new ones. But I leave here a compressed file of all of them for greater convenience.
Scrolling requires copying values from memory from one position to another. To do this, we will program the memory_copy function, defining the prototype and the function itself.
void memory_copy(short* source, short* dest, int no_bytes);
// Copy bytes from one place to another in blocks of short bytes until no_bytes is reached
void memory_copy(short* source, short* dest, int no_bytes) {
int i;
// copy short by short
for (i =0; i<no_bytes; i++) {
*(dest + i) = *(source + i);
}
}
Now the prototypes and functions of our driver.
#define VIDEO_ADDRESS 0xb8000
#define MAX_ROWS 25
#define MAX_COLS 80
#define WHITE_ON_BLACK 0x0f
#define RED_ON_WHITE 0xf4
/* Screen i/o ports */
#define REG_SCREEN_CTRL 0x3d4
#define REG_SCREEN_DATA 0x3d5
/* Public kernel API */
void clear_screen();
int get_offset(int row, int col);
void set_cursor_offset(int offset);
int get_cursor_offset();
int print_char(char character, int row, int col, char attribute_byte);
int get_offset_row(int offset);
int get_offset_col(int offset, int row);
void print_string(char *message, int row, int col);
int handle_scrolling(int cursor_offset);
#include "screen.h"
#include "ports.h"
#include "../kernel/util.h"
// Offset depends on the type of the video_memory pointer, if it is short: 1screenChar -> 1offset
void clear_screen() {
int screen_size = MAX_COLS * MAX_ROWS - 1;
int i;
short* video_memory = (short*) VIDEO_ADDRESS;
// Set each video memory address to '' value with WHITE_ON_BLACK attribute
short character_and_attribute_byte = 0 | WHITE_ON_BLACK << 8;
for (i = 0; i < screen_size; i++) {
video_memory[i] = character_and_attribute_byte;
}
// Set cursor to first offset after clear screen
set_cursor_offset(0);
}
// Convert input coordinates to video memory offset
int get_offset(int row, int col) {
return (row * MAX_COLS + col);
}
// Set cursor position to specified video_memory
void set_cursor_offset(int offset) {
port_byte_out(REG_SCREEN_CTRL, 14);
port_byte_out(REG_SCREEN_DATA, offset >> 8); // High byte
port_byte_out(REG_SCREEN_CTRL, 15);
port_byte_out(REG_SCREEN_DATA, (offset & 0xff)); // Low byte
}
// Get cursor position and return video_memory offset equivalent
int get_cursor_offset() {
port_byte_out(REG_SCREEN_CTRL, 14);
int offset = port_byte_in(REG_SCREEN_DATA) << 8; // High byte
port_byte_out(REG_SCREEN_CTRL, 15); // Low byte
offset += port_byte_in(REG_SCREEN_DATA);
return offset; /* Position * size of character cell */
}
int print_char(char character, int row, int col, char attribute_byte) {
short* video_memory = (short*) VIDEO_ADDRESS;
int offset;
if (!attribute_byte) {
attribute_byte = WHITE_ON_BLACK;
}
//short code variable improves printing chars performance, in that way we only write to video_memory one time
//not twice with char+attr
short character_and_attribute_byte = character | attribute_byte << 8;
if (row >= 0 && col >= 0) {
offset = get_offset(row, col);
} else {
offset = get_cursor_offset();
}
// If we see a newline character, set offset to the end of current row, so it will be advanced to the first col
// of the next row when offet is incremented.
if (character == '\n') {
int row = get_offset_row(offset);
offset = get_offset(row, MAX_COLS - 1);
} else {
//video_memory[offset] = character;
//video_memory[offset+1] = attribute_byte;
video_memory[offset] = character_and_attribute_byte;
}
// Update the offset to the next character cell
offset++;
// Make scrolling adjustment, we check if next char write will be done outside screen char matrix
offset = handle_scrolling(offset);
// Update the cursor position on the screen device .
set_cursor_offset(offset);
return offset;
}
void print_string(char *message, int row, int col) {
int offset;
/* Set cursor if col/row are negative */
if (row < 0 || col < 0 ) {
offset = get_cursor_offset();
row = get_offset_row(offset);
col = get_offset_col(offset, row);
}
/* Loop through message and print it */
int i = 0;
while (message[i] != 0) {
offset = print_char(message[i], row, col, WHITE_ON_BLACK);
i++;
/* Compute row/col for next iteration */
row = get_offset_row(offset);
col = get_offset_col(offset, row);
}
}
int get_offset_row(int offset) {
return offset / MAX_COLS;
}
int get_offset_col(int offset, int row) {
return (offset - (row * MAX_COLS));
}
/* Advance the text cursor, scrolling the video buffer if necessary. */
int handle_scrolling(int offset) {
// Are we writting inside screen char matrix?
if (offset < MAX_ROWS * MAX_COLS) {
return offset ;
}
// We are outside screen char matrix so copy row to inmediately above.
int i;
int row_length = (MAX_COLS-1) * 2;
// While we are working with memory addresses bear in mind that offset = 1short(2bytes), memory_pos = offset * 2
for (i = 1; i < MAX_ROWS - 1; i++) {
memory_copy((short*) (VIDEO_ADDRESS + (get_offset(i, 0) * 2)), (short*) (VIDEO_ADDRESS + (get_offset(i-1, 0) * 2)), row_length);
}
// Blank last line by setting all bytes to 0
short* last_line = (short*) (VIDEO_ADDRESS + (get_offset(MAX_ROWS-1, 0) * 2));
short character_and_attribute_byte = 0 | WHITE_ON_BLACK << 8;
for (i = 0; i < (MAX_COLS - 1); i++) {
last_line[i] = character_and_attribute_byte;
}
// Offset outside screen char matrix, move the offset back one row.
offset -= MAX_COLS ;
// Return the updated cursor position .
return offset ;
}
One of the most complex parts of the driver is possibly the one that controls new lines (\n).
if (character == '\n') {
int row = get_offset_row(offset);
offset = get_offset(row, MAX_COLS - 1);
} else {
video_memory[offset] = character_and_attribute_byte;
}
offset++;
When the ‘\n’ character is detected, the row number and offset (displacement from the video_memory base address) associated with the last column of that same row are obtained. As the offset is incremented a few lines below, we are now on the next line, and the next character that is written will be in that position.
The other function, which is a bit more complex, is scrolling. First, it checks if the offset where the next character will be written will be a position within the screen’s char matrix (25x80). If it is within the matrix, it returns and scrolling is not executed.
// Are we writting inside screen char matrix?
if (offset < MAX_ROWS * MAX_COLS) {
return offset ;
}
If we need to perform scrolling, we must copy the content of row N to row N-1. The memory_copy function requires a source/destination memory address and the number of bytes to copy. Remember that the offset corresponds to two bytes in RAM since the video_memory pointer has been defined as short (2 bytes: char+attr). Every time we write/read using this pointer, we are writing/reading 2 bytes. To calculate the memory offset from video_memory to a specific offset, we apply the formula: memory = offset * 2.
The loop that copies the rows starts from row number 1 (the second on the screen) up to the last row (MAX_ROWS - 1), and we copy its content using the memory_copy function.
int i;
int row_length = (MAX_COLS-1) * 2;
// While we are working with memory addresses bear in mind that offset = 1short(2bytes), memory_pos = offset * 2
for (i = 1; i < MAX_ROWS - 1; i++) {
memory_copy((short*) (VIDEO_ADDRESS + (get_offset(i, 0) * 2)), (short*) (VIDEO_ADDRESS + (get_offset(i-1, 0) * 2)), row_length);
}
- Source address: First position of the row get_offset(i, 0) * 2
- Destination address: First position of the previous row get_offset(i-1, 0) * 2
- Number of bytes: The size of a row (MAX_COLS-1) * 2
NOTE: Since we are working with memory positions, we must multiply all results by 2 to pass the memory_copy function the value of the memory position and not the offset.
As we are copying row N to row N - 1, the last row is repeated in the last and penultimate rows. Therefore, we must locate the last row and set it to 0.
short* last_line = (short*) (VIDEO_ADDRESS + (get_offset(MAX_ROWS-1, 0) * 2));
short character_and_attribute_byte = 0 | WHITE_ON_BLACK << 8;
for (i = 0; i < (MAX_COLS - 1); i++) {
last_line[i] = character_and_attribute_byte;
}
Scrolling is executed because it has been detected that the offset was in a position outside the screen matrix. Now that we have executed scrolling, we must set the offset to the first position of the last row.
// Offset outside screen char matrix, move the offset back one row.
offset -= MAX_COLS ;
Finally, the main code.
#include "../drivers/screen.h"
#include "util.h"
void main() {
clear_screen();
print_string("Welcome to MercuryOS v0.1b by Kr0m\n", 0, 0);
print_string("Line1\n", -1, -1);
print_string("Line2\n", -1, -1);
print_string("Line3\n", -1, -1);
print_string("Line4\n", -1, -1);
print_string("Line5\n", -1, -1);
print_string("Line6\n", -1, -1);
print_string("Line7\n", -1, -1);
print_string("Line8\n", -1, -1);
print_string("Line9\n", -1, -1);
print_string("Line10\n", -1, -1);
print_string("Line11\n", -1, -1);
print_string("Line12\n", -1, -1);
print_string("Line13\n", -1, -1);
print_string("Line14\n", -1, -1);
print_string("Line15\n", -1, -1);
print_string("Line16\n", -1, -1);
print_string("Line17\n", -1, -1);
print_string("Line18\n", -1, -1);
print_string("Line19\n", -1, -1);
print_string("Line20\n", -1, -1);
print_string("Line21\n", -1, -1);
print_string("Line22\n", -1, -1);
print_string("Line23\n", -1, -1);
print_string("Line24\n", -1, -1);
print_string("Line25\n", -1, -1);
print_string("Line26\n", -1, -1);
}
If we compile and load the kernel in Qemu, we can see the scrolling:
NOTE: The Makefile is customized for my cross-compiler and my tools under FreeBSD. It may be necessary to make some adjustments for it to work correctly.
I leave a video where I advance the execution through Gdb so that you can see how the code is executed. There are some glitches due to the graphic compositor, but the scrolling can be seen without problems: