This continues my Part 1 notes on the input path (keyboard) and moves to the output path: how a single character travels through cgaputc into 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.ccgaputc(int c) (used by higher-level consputc / 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?

  1. Read cursor from the CRTC (0x0E/0x0F). Compute pos = col + 80*row.
  2. Apply the character:
    • \npos += 80 - pos % 80 (jump to next line start)
    • BACKSPACE → if possible --pos (move left one cell)
    • printable → write (c & 0xFF) | (attr << 8) to crt[pos++]
  3. Scroll if (pos/80) >= 24 (i.e., leaving the last visible row):
    memmove the visible window up, fix pos, memset the bottom line.
  4. Write new cursor back to CRTC (0x0E/0x0F).
  5. 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 of ushort *crt takes 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), col 24%8 = 0.

Clean the bottom line:

  • memset(void *dst, int value, size_t n): write the n bytes starting from dst as value & 0xFF
  • 4*8 - pos = 32 - 24 = 8 cells ⇒ 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).