This continues my Part 1 notes on the input path (keyboard) and moves to the output path: how a single character travels through
cgaputcinto the VGA text buffer.
Series so far
- Part 1 — How the Keyboard Speaks (kbd.c, kbd.h)
0. Big Picture
- Goal: Print one character on a VGA text-mode console and keep the cursor in sync.
- Where:
console.c→cgaputc(int c)(used by higher-levelconsputc/printf). - Hardware: VGA/CGA text mode, 25×80 cells; each cell is 16 bits: [char:8 | attr:8].
- Cursor: Stored in the CRTC registers (ports 0x3D4/0x3D5; indices 0x0E for high bits of cursor location, 0x0F for low bits of cursor location).
1. The Core Routine (console.c)
// Print one char to CGA/VGA text console and move the hardware cursor.
static void cgaputc(int c) {
int pos;
// 1) Read current hardware cursor: pos = col + 80*row
outb(CRTPORT, 14); // select cursor high (index 0x0E)
pos = inb(CRTPORT + 1) << 8;
outb(CRTPORT, 15); // select cursor low (index 0x0F)
pos |= inb(CRTPORT + 1);
// 2) Handle character semantics
if (c == '\n') {
pos += 80 - pos % 80; // newline → next line start
} else if (c == BACKSPACE) {
if (pos > 0) --pos; // backspace → move left
} else {
crt[pos++] = (c & 0xff) | 0x0700; // write char with attribute 0x07
}
// Bounds check
if (pos < 0 || pos > 25 * 80) panic("pos under/overflow");
// 3) Scroll up if the cursor falls beyond the last visible row
if ((pos / 80) >= 24) {
memmove(crt, crt + 80, sizeof(crt[0]) * 23 * 80); // rows 1..23 → 0..22
pos -= 80; // pull cursor back
memset(crt + pos, 0, sizeof(crt[0]) * (24 * 80 - pos)); // clear last row
}
// 4) Update the hardware cursor location
outb(CRTPORT, 14); outb(CRTPORT + 1, pos >> 8);
outb(CRTPORT, 15); outb(CRTPORT + 1, pos);
// 5) Keep the cursor cell visually blank (clean caret)
crt[pos] = ' ' | 0x0700;
}
2. What Exactly Happens?
- Read cursor from the CRTC (
0x0E/0x0F). Computepos = col + 80*row. - Apply the character:
\n→pos += 80 - pos % 80(jump to next line start)BACKSPACE→ if possible--pos(move left one cell)- printable → write
(c & 0xFF) | (attr << 8)tocrt[pos++]
- Scroll if
(pos/80) >= 24(i.e., leaving the last visible row):memmovethe visible window up, fixpos,memsetthe bottom line. - Write new cursor back to CRTC (
0x0E/0x0F). - Blank the cursor cell:
crt[pos] = ' ' | attr<<8.
3. Walkthrough
To visualize the logic, shrink the screen to 4 rows × 8 columns (real is 25×80).
We show blanks as · and mark the cursor cell by [·]. For clarity, we also show the linear cursor index pos = col + 8*row.
3.1 Typing, newline (\n), and backspace
Start
row0: ········
row1: ········
row2: ········
row3: ········
cursor → row0 col0 [·] pos=0
Type Hello
row0: Hello···
row1: ········
row2: ········
row3: ········
cursor → row0 col5 [·] pos=5
Type \n (newline): pos += 8 - (pos % 8) = 8 - (5) = 3 → pos=8
row0: Hello···
row1: ········
row2: ········
row3: ········
cursor → row1 col0 [·] pos=8
Type World
row0: Hello···
row1: World···
row2: ········
row3: ········
cursor → row1 col5 [·] pos=13
Type BACKSPACE: pos-- → 12
row0: Hello···
row1: Worl ··· ← logical effect: cursor moved left; later we blank
row2: ········
row3: ········
cursor → row1 col4 [·] pos=12
Type !
row0: Hello···
row1: Worl!···
row2: ········
row3: ········
cursor → row1 col5 [·] pos=13
3.2 Natural wrap vs. explicit newline
Explicit newline: \n jumps to the next line start regardless of column.
Natural wrap: printing at the last column (col=7 here) then printing the next character advances pos to the next row col=0 without an extra blank.
row2: ABCDEFGH
cursor → row3 col0 [·] (pos went from 15 to 16; 16/8=2 → next row)
3.3 Scrolling (rolling the window)
Before scroll: cursor is at bottom row, last col.
row0: Hello···
row1: Worl!···
row2: 12345678
row3: ABCDEFG·
cursor → row3 col7 [·] pos = 3*8 + 7 = 31
Write the printable H:
- crt[pos++] = ‘H’ | 0x0700 writes at pos 31 (row3 col7), then pos becomes 32.
pos/8 = 32/8 = 4 >= 3+1→ true → scroll.
row3: ABCDEFGH (we just filled the last slot)
pos = 32
Shift row upwards:
memmove(void *dst, const void *src, size_t n):copy memory from[src, src + n)to[dst, dst + n)sizeof(ctr[0]): size ofushort *crttakes 2 bytes, 8 bits for ASCII code and 8 bits for attributes.
row0: Worl!···
row1: 12345678
row2: ABCDEFGH
row3: (stale, to be cleared)
pos = 32 (unchanged by memmove)
pos -= 8:
- Pull the cursor back one row to keep same column: New
pos = 32 - 8 = 24 - That is row
24/8 = 3(last row), col24%8= 0.
Clean the bottom line:
memset(void *dst, int value, size_t n): write thenbytes starting fromdstasvalue & 0xFF4*8 - pos = 32 - 24 = 8cells ⇒ clear row3 col 0..7 completely.
row0: Worl!···
row1: 12345678
row2: ABCDEFGH
row3: ········ (fresh, empty bottom line)
pos = 24 → row3 col0
Credits
- xv6 source (MIT 6.S081/6.828 lineage).