-
Notifications
You must be signed in to change notification settings - Fork 0
vga text mode
How a freestanding kernel paints characters on screen by writing bytes straight into memory — no driver, no syscall, no BIOS.
When the BIOS hands control to your boot sector, the display is already in VGA text mode 3: an 80-column by 25-row grid of character cells, 16 colors. The screen is not something you "draw" pixel by pixel — instead, a region of physical memory is the screen. Write a byte there and a glyph appears. This is the simplest possible output device, and it is why MyOS-Simple can print "Hello, World!" from a kernel that contains no libc, no printf, and no graphics code at all.
The text-mode framebuffer lives at physical address 0xB8000. It is memory-mapped, not an I/O port — you reach it with an ordinary pointer dereference, not with inb/outb. (Contrast this with the PS/2 keyboard, which is read through I/O ports.)
#define VIDEO_MEMORY 0xb8000— kernel.c:12, and identically shell.c:13.
Because the project runs in flat 32-bit protected mode with an identity-mapped GDT, the linear address 0xB8000 maps directly to that physical address, so the pointer "just works."
Each of the 80×25 = 2000 cells is two bytes:
| Byte | Meaning |
|---|---|
| 0 (even) | The ASCII character code |
| 1 (odd) | The attribute (color) byte |
So the whole screen is 80 * 25 * 2 = 4000 bytes. The offset of cell (x, y) is:
int offset = (y * 80 + x) * 2;
video[offset] = c; // character
video[offset + 1] = attr; // color— putchar_at, kernel.c:51-56 (and shell.c:44-49).
💡 Tidbit: The
(y * 80 + x) * 2formula is the text-mode equivalent of a stride calculation in a pixel framebuffer —80is the row stride in cells and2is the bytes-per-cell. Every framebuffer you ever touch, graphical or textual, computes addresses this same way.
The pointer is declared volatile so the compiler never caches or reorders the writes — every store must actually reach video memory:
volatile char* video = (volatile char*)VIDEO_MEMORY;— kernel.c:52.
The attribute byte packs a background color in the high nibble and a foreground color in the low nibble:
bit: 7 6 5 4 3 2 1 0
[B] [ BG ] [ FG ]
make_color builds one by hand:
char make_color(char bg, char fg) {
return (bg << 4) | fg;
}— kernel.c:46-48. Equivalently, attribute = (background << 4) | foreground.
The low nibble (foreground) can be any of 16 colors 0x0–0xF:
| Code | Color | Code | Color |
|---|---|---|---|
0x0 |
BLACK | 0x8 |
DARK_GRAY |
0x1 |
BLUE | 0x9 |
LIGHT_BLUE |
0x2 |
GREEN | 0xA |
LIGHT_GREEN |
0x3 |
CYAN | 0xB |
LIGHT_CYAN |
0x4 |
RED | 0xC |
LIGHT_RED |
0x5 |
MAGENTA | 0xD |
LIGHT_MAGENTA |
0x6 |
BROWN | 0xE |
YELLOW |
0x7 |
LIGHT_GRAY | 0xF |
WHITE |
These are defined as macros in kernel.c:15-30. A combined constant like WHITE_ON_BLACK is simply 0x0F (kernel.c:33) — black background nibble 0, white foreground nibble F.
The kernel even renders a live swatch of all 16 by drawing space characters whose background equals their foreground:
for (int i = 0; i < 16; i++) {
putchar_at(' ', 13 + i*2, 23, make_color(i, i));
putchar_at(' ', 13 + i*2 + 1, 23, make_color(i, i));
}— kernel.c:293-296.
⚠️ Caveat: In standard VGA text mode, bit 7 of the attribute byte (the top bit of the background nibble) is the blink attribute by default, not an extra background-color bit. That is why the background is effectively limited to 8 colors (0x0–0x7) while the foreground gets all 16: setting a "background" of0x8–0xFinstead makes the cell blink. Blink can be disabled via a VGA register to free that bit for backgrounds, but MyOS-Simple never touches it, so high background codes will blink.
There is no "clear" instruction — you blank the screen by writing a space into every cell with a chosen attribute:
void clear_screen_color(char bg_color) {
volatile char* video = (volatile char*)VIDEO_MEMORY;
char attr = make_color(bg_color, WHITE);
for (int i = 0; i < 80 * 25 * 2; i += 2) {
video[i] = ' ';
video[i + 1] = attr;
}
}— kernel.c:73-80. The shell's clear_screen is the same loop with a fixed WHITE_ON_BLACK attribute (shell.c:51-59), and it also resets the software cursor to (0, 0).
VGA text mode has no scroll hardware that the project uses, so the shell scrolls by hand: when the cursor passes the bottom row it copies rows 1–24 up into rows 0–23, then blanks the last row.
void scroll_screen() {
volatile char* video = (volatile char*)VIDEO_MEMORY;
// Move all lines up by one
for (int y = 0; y < 24; y++) {
for (int x = 0; x < 80; x++) {
int src_offset = ((y + 1) * 80 + x) * 2;
int dst_offset = (y * 80 + x) * 2;
video[dst_offset] = video[src_offset];
video[dst_offset + 1] = video[src_offset + 1];
}
}
// Clear the last line
for (int x = 0; x < 80; x++) {
int offset = (24 * 80 + x) * 2;
video[offset] = ' ';
video[offset + 1] = WHITE_ON_BLACK;
}
}— shell.c:61-80. It is triggered from putchar when cursor_y >= 25 (shell.c:101-104).
There is no hardware text cursor management in this project. The shell tracks a software cursor with two integers (cursor_x, cursor_y — shell.c:33-34) and advances them inside putchar (shell.c:82-105). The color demo draws a literal underscore glyph to look like a cursor and erases it by overwriting with a space:
// Show cursor
putchar_at('_', cursor_x, cursor_y, WHITE_ON_BLACK);
...
// Clear cursor
putchar_at(' ', cursor_x, cursor_y, WHITE_ON_BLACK);— kernel.c:304 and kernel.c:311. The real blinking hardware cursor (controlled via CRTC registers on ports 0x3D4/0x3D5) is never programmed.
💡 Tidbit: Because output is just memory writes, the kernel can update any cell at any time without "moving" a cursor first — the live keyboard-modifier status line (
show_keyboard_status,kernel.c:205-250) and the cursor underscore are both drawn directly at fixed coordinates while the input loop runs.
- PS/2 Keyboard & the 8042 Controller — the input counterpart, read via I/O ports rather than memory
-
Protected Mode — the 32-bit flat environment that makes
0xB8000directly addressable - Global Descriptor Table — the identity-mapped segments behind that flat address space
-
Freestanding C — why there is no
printfand you write bytes by hand - Stage 2: C in Protected Mode — where the color demo lives
- Stage 3: Interactive Shell — where scrolling and the software cursor are used
-
Memory Map — where
0xB8000sits relative to the kernel and stack - I/O Ports Reference — the ports VGA text mode does not use for the framebuffer (and the CRTC ports it could)
- Home
Stages
- 1 · Assembly boot
- 2 · C protected mode
- 3 · Interactive shell
- 4 · Clock / processes / calc
- 5 · Stabilized release
Concepts — boot
Concepts — protected mode
Concepts — hardware
Concepts — OS services
Reference
- Memory map
- I/O ports
- GDT descriptor format
- Scancode tables
- Command reference
- Toolchain & build
- Glossary
Guides