Console and Terminal Emulation (ANSI Escape Sequences)
VGA & Framebuffers already covers drawing a character or a pixel to the screen; a console is the layer most kernels build directly on top of that, interpreting a stream of bytes containing embedded escape sequences for color, cursor movement, and scrolling, the same interface any ordinary terminal program (vim, less, a shell prompt) already expects to be able to write to without modification.
The general shape of an escape sequence
Section titled “The general shape of an escape sequence”Every ANSI escape sequence begins with the single byte 0x1B (ESC), and the most common family, CSI (Control Sequence Introducer) sequences, follows it with [, then zero or more numeric parameters separated by ;, then a single letter identifying the command.
ESC [ 3 1 ; 1 m ^^^^^^^ ^ params command letter (m = SGR, Select Graphic Rendition)ESC[31;1m sets bold, foreground red; ESC[2J (no parameters, command J) clears the entire screen; ESC[10;5H moves the cursor to row 10, column 5. SGR (m) is the family a console spends the most time on in practice, since it’s what a program uses for every color and text-attribute change, with parameters both for the 16 classic colors (30-37 foreground, 40-47 background) and, on a console that supports it, extended 256-color and 24-bit RGB variants through further parameter sub-sequences.
Parsing bytes that arrive in pieces
Section titled “Parsing bytes that arrive in pieces”A console can’t assume an entire escape sequence arrives in a single read from whatever’s feeding it output, one byte at a time from a slow producer, or split arbitrarily across two separate writes are both normal, so parsing has to be a state machine that resumes correctly across calls rather than a single function that expects a complete sequence in one buffer.
enum console_state { STATE_NORMAL, STATE_ESCAPE, STATE_CSI };
void console_putchar(uint8_t c) { static enum console_state state = STATE_NORMAL; static int params[8], param_count = 0;
switch (state) { case STATE_NORMAL: if (c == 0x1B) { state = STATE_ESCAPE; return; } draw_char(c); return; case STATE_ESCAPE: if (c == '[') { state = STATE_CSI; param_count = 0; params[0] = 0; return; } state = STATE_NORMAL; return; case STATE_CSI: if (c >= '0' && c <= '9') { params[param_count] = params[param_count] * 10 + (c - '0'); return; } if (c == ';') { param_count++; params[param_count] = 0; return; } apply_csi_command(c, params, param_count + 1); // c is the command letter state = STATE_NORMAL; return; }}Each byte moves the state machine forward exactly one step regardless of when it arrives relative to the rest of its sequence, accumulating numeric parameters as digits arrive and only actually executing a command once the terminating letter is seen; a byte that doesn’t fit the current state (an unexpected character in the middle of a CSI sequence, for instance) resets back to STATE_NORMAL rather than leaving the parser stuck partway through a sequence that will never complete correctly.
Scrolling
Section titled “Scrolling”Scrolling the console up by one line, the operation that happens on essentially every newline once the cursor reaches the bottom row, is implemented as a memory copy: every row from the second down to the last is copied up by one row’s worth of bytes, and the newly exposed bottom row is cleared to the current background color, all without touching any row that didn’t actually need to move.
void console_scroll(void) { memmove(video_mem, video_mem + ROW_BYTES, (ROWS - 1) * ROW_BYTES); memset(video_mem + (ROWS - 1) * ROW_BYTES, 0, ROW_BYTES);}This is considerably cheaper than redrawing the entire screen character by character, since it’s a bulk copy of already-formatted pixel or character-cell data rather than a re-render of anything: the rows being kept don’t need their color or content recomputed at all, only relocated, and on a linear framebuffer this reduces to exactly the kind of large sequential memory move hardware handles efficiently.
A serial console needs the same parser, in reverse
Section titled “A serial console needs the same parser, in reverse”A console driven over a serial port rather than a framebuffer needs the identical escape-sequence handling, but running in the opposite direction from what this article covers so far: instead of the kernel emitting sequences for a host terminal to interpret, it’s the kernel receiving sequences (cursor keys, for instance, which a terminal encodes as their own multi-byte ESC[ sequences rather than a single character) from whatever terminal emulator is attached to the other end of the serial line, requiring the same state-machine parsing described above, applied to input instead of output.
Implementation notes
Section titled “Implementation notes”A console implementation has to decide, deliberately, which of the many escape sequences accumulated by decades of terminal history it actually supports: implementing the common VT100/ANSI subset (cursor movement, SGR colors, screen clearing) covers the overwhelming majority of real-world usage, while obscure sequences from specific historical terminal types are safe to simply ignore, parsed enough to be consumed without crashing the state machine but not acted on. A program that queries the console’s capabilities (via TERM in a hosted environment, though a hobby kernel typically has no equivalent negotiation and instead hardcodes an assumed feature set) and receives an escape sequence back it wasn’t expecting to have to handle is a common source of garbled output, which is why sticking to a well-known, narrow subset rather than inventing custom sequences keeps a console’s output predictable across whatever terminal software actually connects to it.
References
Section titled “References”- ^ ECMA International, ECMA-48: Control Functions for Coded Character Sets: the formal specification behind ANSI/VT100-style escape sequences.
See also
Section titled “See also”- VGA & Framebuffers: the drawing layer this console’s escape-sequence interpreter sits on top of.
- Serial Port: where a console needs the same parsing applied to sequences arriving from the opposite direction.