CLOSE

In this article will guide you through creating printing function in Stage 2 of a bootloader, specifically in VGA text mode (Mode 3), using C.

As we know that by default BIOS put us in VGA text mode (Mode 3) which operates with a 80x25 character display. Each character on the screen is represented by two bytes in memory: First byte for the ASCII character and Second one for the attribute (color). The memory for VGA text mode starts at address 0xB8000. As we are in protected mode, we don't have access to BIOS interrupts. We will directly write to the video memory.

Layout of Video Memory

1 Address Range: Video memory for text mode typically starts at the address 0xB8000 and spans 4,000 bytes (80 columns x 25 rows x 2 bytes per character).

2 Character Representation: Each character on the screen is represented by 2 bytes:

  • Byte1: The first byte is the ASCII code of the character.
  • Byte 2: The second byte is the attribute byte, which defines the foreground and background colors.

-: Visual Representation :-

Address    Content
0xB8000    ASCII of character 1 in row 1, column 1
0xB8001    Attribute of character 1 in row 1, column 1
0xB8002    ASCII of character 2 in row 1, column 2
0xB8003    Attribute of character 2 in row 1, column 2
...
0xB8FA0    ASCII of character 80 in row 25, column 80
0xB8FA1    Attribute of character 80 in row 25, column 80

Memory Address      | Screen Position
-------------------------------------
0xB8000 (ASCII)     | Row 1, Column 1
0xB8001 (Attr)      |
0xB8002 (ASCII)     | Row 1, Column 2
0xB8003 (Attr)      |
...                 | ...
0xB80A0 (ASCII)     | Row 2, Column 1
0xB80A1 (Attr)      |
...                 | ...
0xB8FA0 (ASCII)     | Row 25, Column 80
0xB8FA1 (Attr)      |

Attribute Byte

The attribute byte determines the colors of the text and background. It's an 8-bit value:

  • Bits 0-3: Foreground color (text color)
  • Bits 4-6: Background color
  • Bit 7: Blink attribute (1 = blinking, 0 = not blinking)

Foreground Color (Bits 0-3):

  • 0000: Black
  • 0001: Blue
  • 0010: Green
  • 0011: Cyan
  • 0100: Red
  • 0101: Magenta
  • 0110: Brown
  • 0111: Light Gray
  • 1000: Dark Gray
  • 1001: Light Blue
  • 1010: Light Green
  • 1011: Light Cyan
  • 1100: Light Red
  • 1101: Light Magenta
  • 1110: Yellow
  • 1111: White

Background Color (Bits 4-6):

  • 000: Black
  • 001: Blue
  • 010: Green
  • 011: Cyan
  • 100: Red
  • 101: Magenta
  • 110: Brown
  • 111: Light Gray

Example:

  • Attribute byte 0x1F:
    • Foreground: White (1111)
    • Background: Blue (001)

-: Code :-

Color Enumerations

Let's first make the enumerations for the foreground and background colors, which help make our code more readable and maintainable.

// Foreground colors
enum VGA_ForegroundColor {
    FG_BLACK        = 0x0,
    FG_BLUE         = 0x1,
    FG_GREEN        = 0x2,
    FG_CYAN         = 0x3,
    FG_RED          = 0x4,
    FG_MAGENTA      = 0x5,
    FG_BROWN        = 0x6,
    FG_LIGHT_GRAY   = 0x7,
    FG_DARK_GRAY    = 0x8,
    FG_LIGHT_BLUE   = 0x9,
    FG_LIGHT_GREEN  = 0xA,
    FG_LIGHT_CYAN   = 0xB,
    FG_LIGHT_RED    = 0xC,
    FG_LIGHT_MAGENTA= 0xD,
    FG_YELLOW       = 0xE,
    FG_WHITE        = 0xF
};

// Background colors
enum VGA_BackgroundColor {
    BG_BLACK        = 0x0 << 4,
    BG_BLUE         = 0x1 << 4,
    BG_GREEN        = 0x2 << 4,
    BG_CYAN         = 0x3 << 4,
    BG_RED          = 0x4 << 4,
    BG_MAGENTA      = 0x5 << 4,
    BG_BROWN        = 0x6 << 4,
    BG_LIGHT_GRAY   = 0x7 << 4
};

// Combine foreground and background colors
#define VGA_COLOR(fg, bg) ((fg) | (bg))

Constants

Declare some constants which we will use a lot of times in our code, such that in change at one place reflects at every place it is used.

#define SCREEN_WIDTH 80		// Mode 3 Screen Width | Column
#define SCREEN_HEIGHT 25	// Mode 3 Screen Height | Row
#define VIDEO_MEMORY 0xB8000

Print Char to Screen at Particular Position

To print a character at a specific position, we need to calculate the offset in video memory based on the row and column.

  • Calculate Offset:
    • Offset = (row * 80 + column)
      • 80 represents the number of columns per row.
  • Access Video Memory:
    • Start from the base address 0xB8000.
    • Add the calculated offset to get the memory location where the character and its attribute are stored.
unsigned short *videoMem = (unsigned short*) VIDEO_MEMORY;
unsigned char attribute = VGA_COLOR(FG_WHITE, BG_BLACK);

void print_char_at(char c, int row, int col) {
    
    // Get Offset by row and column
    int offset = row * SCREEN_WIDTH + col;

    videoMem[offset] = (attribute << 8) | c;

}

// Usage:-
// Row = 0
//Column = 0
print_char_at('J', 0, 0);

Visualization Example:

Let's visualize printing the character 'A' at position (10, 20) with white text on a blue background.

1 Calculate Offset:

  • Row = 10, Column = 20
  • Offset = (10 * 80 + 20)
    • Offset = 800 + 20
    • Offset = 820 (unsigned short) = 820*2 = 1640 (unsigned char)

2 Memory Address:

  • Video Memory Base: 0xB8000
  • Memory Address = 0xB8000 + 820
    • Memory Address = 0xB8640

3 Data Representation (unsigned short):

  • ASCII Code ('A') = 0x41 (decimal 65).
  • Attribute (FG_WHITE | BG_BLUE) = 0x1F (assuming white text on blue background).

4 Video Memory Layout:

Memory Address      | Content (unsigned short)
-----------------------------------------------
0xB8640             | 0x1F41

In memory:

  • The lower byte (0x41) represents the ASCII character ('A').
  • The higher byte (0x1F) represents the attribute byte (FG_WHITE | BG_BLUE).

Printing String

int row = 0;
int column = 0;

void boot_print(char *str) {
    while (*str) {
        if (*str == '\n') {
            // Move to the next line and reset column
            row++;
            column = 0;
        } else {
            // Print character at current position
            print_char_at(*str, row, column);
            column++;
            if (column >= SCREEN_WIDTH) {
                // Move to the next line if the end of the current line is reached
                column = 0;
                row++;
            }
        }

        // Scroll the screen if necessary
        if (row >= SCREEN_HEIGHT) {
        	// Either scroll or do wraparound
            boot_scroll();
            row = SCREEN_HEIGHT - 1;
        }

        str++;
    }
}

Scroll Window

We need to handle the case where the text reaches the bottom of the screen.

When the cursor reaches the end of the screen, then we have two solutions of printing the next character.

  1. Wraparound: Start printing from the top of the screen again.
  2. Scrolling: Shift all rows up by one, clear the last row, and continue printing on the last row.

We will make use of Scrolling screen. In the scrolling option, the screen scrolls up by one row when the bottom is reached, making space for new text at the bottom:

void boot_scroll() {
    // Scroll up by copying each line up one row
    for (int i = 0; i < SCREEN_HEIGHT - 1; ++i) {
        for (int j = 0; j < SCREEN_WIDTH; ++j) {
            videoMem[i * SCREEN_WIDTH + j] = videoMem[(i + 1) * SCREEN_WIDTH + j];
        }
    }

    // Clear the last line
    for (int j = 0; j < SCREEN_WIDTH; ++j) {
        videoMem[(SCREEN_HEIGHT - 1) * SCREEN_WIDTH + j] = (attribute << 8) | ' ';
    }
}
  • Scroll Up: Copies each row of the screen to the row above it.
  • Clear Last Line: Clears the last line after scrolling by filling it with spaces.

Clear Screen

void clear_screen() {
    unsigned short blank = (attribute << 8) | ' ';
    for (int row = 0; row < SCREEN_HEIGHT; ++row) {
        for (int column = 0; column < SCREEN_WIDTH; ++column) {
            videoMem[row * SCREEN_WIDTH + column] = blank;
        }
    }

// Reset global row and column to 0.    
    row = 0;
    column = 0;
}

Print Hexadecimal

void boot_print_hex(unsigned int value) {
    char hex_chars[] = "0123456789ABCDEF";
    char hex_str[9]; // 8 hex digits + null terminator
    hex_str[8] = '\0'; // Null terminator

    for (int i = 7; i >= 0; --i) {
        hex_str[i] = hex_chars[value & 0xF];
        value >>= 4;
    }

    boot_print(hex_str);
}

Step-by-Step Breakdown

  • Initialize Hex Characters: The string hex_chars contains all possible hexadecimal characters (0-9 and A-F).
    • char hex_chars[] = “0123456789ABCDEF”;
  • Prepare Hex String: hex_str is an array of 9 characters, enough to hold 8 hex digits and a null terminator.
    • char hex_str[9];
  • Null Terminator: The last element of hex_str is set to \0 to ensure it is a valid C string.
    • hex_str[8] = ‘\0’; // Null terminator
  • Convert to Hex: A loop iterates over each nibble (4 bits) of the input integer, starting from the least significant nibble. For each iteration:
    • The current value of value is bitwise ANDed with 0xF to isolate the last 4 bits (one hex digit).
    • The corresponding hex character is placed in the hex_str array.
    • value is right-shifted by 4 bits to process the next nibble.
  • Print the Hex String: The scrolling_print function is called to print the hex_str to the screen.

Visualization:

1. Initialize Hex Characters

char hex_chars[] = "0123456789ABCDEF";

2. Prepare Hex String

char hex_str[9];
hex_str[8] = '\0'; // Null terminator

3. Convert to Hex

Let's say the input value is 0x1234ABCD.

  • Initialize hex_str to contain only the null terminator.
hex_str = "00000000\0"
  • Start the loop to process each nibble from least significant to most significant:

First Iteration (i = 7):

  • Isolate the last nibble: value & 0xF0xD (13 in decimal).
  • hex_chars[13]D
  • Update hex_str[7] to D.
  • Right shift value: 0x1234ABCD >> 40x01234ABC
hex_str = "0000000D\0"

Second Iteration (i = 6):

  • Isolate the last nibble: value & 0xF0xC (12 in decimal).
  • hex_chars[12]C
  • Update hex_str[6] to C.
  • Right shift value: 0x01234ABC >> 40x001234AB
hex_str = "000000CD\0"

Third Iteration (i = 5):

  • Isolate the last nibble: value & 0xF0xB (11 in decimal).
  • hex_chars[11]B
  • Update hex_str[5] to B.
  • Right shift value: 0x001234AB >> 40x0001234A
hex_str = "00000BCD\0"

Fourth Iteration (i = 4):

  • Isolate the last nibble: value & 0xF0xA (10 in decimal).
  • hex_chars[10]A
  • Update hex_str[4] to A.
  • Right shift value: 0x0001234A >> 40x00001234
hex_str = "0000ABCD\0"

Fifth Iteration (i = 3):

  • Isolate the last nibble: value & 0xF0x4.
  • hex_chars[4]4
  • Update hex_str[3] to 4.
  • Right shift value: 0x00001234 >> 40x00000123
hex_str = "0004ABCD\0"

Sixth Iteration (i = 2):

  • Isolate the last nibble: value & 0xF0x3.
  • hex_chars[3]3
  • Update hex_str[2] to 3.
  • Right shift value: 0x00000123 >> 40x00000012
hex_str = "0034ABCD\0"

Seventh Iteration (i = 1):

  • Isolate the last nibble: value & 0xF0x2.
  • hex_chars[2]2
  • Update hex_str[1] to 2.
  • Right shift value: 0x00000012 >> 40x00000001
hex_str = "0234ABCD\0"

Eighth Iteration (i = 0):

  • Isolate the last nibble: value & 0xF0x1.
  • hex_chars[1]1
  • Update hex_str[0] to 1.
  • Right shift value: 0x00000001 >> 40x00000000
hex_str = "1234ABCD\0"