Full picture
12

Text Controller

12.1 Overview

The tilemap's layer0 and layer1 (see Tilemap Memory) are enough to implement a functional text terminal or console output. However, implementing this entirely in software on an 8-bit system can be non-trivial. Cursor position calculations may require multiplications, modulo operations, and additional logic for scrolling and wrapping.

The text controller simplifies this process by providing:

  • A hardware cursor
  • Automatic scrolling
  • Newline handling
  • Additional helper functions for text display

Like other controllers, the Text Controller can be mapped into the device bank starting at address 0xA0 on the I/O port. Its device index is 0.

12.2 Hardware Cursor

The text controller includes a customizable hardware cursor. The cursor is a tile (8x12 px) that can display any character from Font Memory, selected via the CURSOR_CHARACTER register.

Its colors are configured via CURSOR_COLORS, which works like layer1 entries:

  • Upper nibble (4-bit) = background color index
  • Lower nibble (4-bit) = foreground color index

Both indices refer to the first 16 entries in Palette Memory.

Name Description Offset Access
CURSOR_Y Set the cursor vertical position. 0x01 R/W
CURSOR_X Set the cursor horizontal position. 0x02 R/W
CURSOR_BLINK_TIMING Blinking speed for the cursor. Controls how many frames cursor is visible/invisible. 0x06 R/W
CURSOR_CHARACTER Character glyph to use for displaying the cursor 0x07 R/W
CURSOR_COLORS Cursor colors, formatted like CURRENT_COLOR 0x08 R/W

Cursor Blinking

Blink timing is set with CURSOR_BLINK_TIMING, in frames:

  • For example, 30 = visible for 30 frames (~500 ms at 60 Hz), hidden for 30 frames
  • 0 is a special value meaning always hidden
  • 255 (0xFF) is a special value meaning always visible

When hidden, the character beneath remains visible, making the cursor behave like an overlay on top of layer0, similar to a sprite in graphics mode.

Position Tracking

The cursor's current column and row (in character units, not pixels) are stored in CURSOR_X and CURSOR_Y.

These registers are read-write, so for example, setting CURSOR_X to 1 moves the cursor to the second column.

Save & Restore

Two bits in CTRL make it easy to temporarily reposition the cursor:

  • SAVE_CURSOR stores current X and Y
  • RESTORE_CURSOR, restores stored X and Y

Only the position is saved, the cursor's character and colors are unaffected.

12.3 Character Output

Writing any 8-bit value to PRINT_CHAR displays the corresponding glyph (from Font Memory) at the cursor's position, using the colors set in CURRENT_COLOR.

Internally, the character code is written to layer0, the CURRENT_COLOR value is written to layer1, with the offset calculated from the cursor position and current scroll values.

After writing, cursor X increments. If X passes the last column (80 for 640x480 mode, 40 for 320x240 mode), it will wrap to start of next line, or wrap to the top of the screen if it was already on the last line.

Auto-Scroll

Hardware auto-scrolling is also supported. Two control bits of the CTRL register modify this behavior:

  • AUTO_SCROLL_X: when set, reaching the last visible column scrolls horizontally instead of wrapping (cursor X stays the same)
  • AUTO_SCROLL_Y: when set, reaching the last line scrolls vertically

If vertical scrolling occurs with AUTO_SCROLL_Y enabled, the SCYF flag in CTRL is set, letting the CPU know it may need to clear the new line.

Scroll offsets are stored in SCROLL_X and SCROLL_Y. These are read-write, so they can also be written manually, but must not exceed the boundaries:

  • 80x40 mode: X ≤ 79, Y ≤ 39
  • 40x20 mode: X ≤ 39, Y ≤ 19

The cursor position is not affected by scroll settings.

In most cases, to simulate a terminal, AUTO_SCROLL_X is disabled while AUTO_SCROLL_Y is enabled.

12.4 Newline

Go to Next Line

Values written to PRINT_CHAR are not interpreted as ASCII, they are simply written to layer0 as-is. While the default Font Memory uses an ASCII-like layout, this is purely conventional.

Implementing a newline in software would require the following:

  1. Reading CURSOR_Y
  2. Incrementing it and setting CURSOR_X to 0
  3. If already on last line:
    • If vertical scroll is enabled: increment SCROLL_Y (wrap if needed), set SCYF flag
    • Else: wrap CURSOR_Y to 0

This can get costly on an 8-bit CPU. To simplify this, the DO_NL bit in CTRL performs the entire sequence:

  1. Increment Y, set X = 0
  2. If Y ≤ max visible row: done
  3. Else:
    • If vertical scroll enabled: increment SCROLL_Y (wrap if needed), set SCYF
    • Else: set Y = 0

Deferred Wrap

In default mode, reaching the last column causes an immediate wrap to the start of the next line. This can cause the "newline glitch": printing exactly 80 characters followed by a newline results in two lines being skipped instead of one.

This is reproducible easy with the following example:

  1. Start at cursor at (0,0)
  2. Print 80 characters → wraps cursor to (0,1)
  3. Print newline → cursor moves to (0,2)

Many terminals instead ignore the newline if the cursor already wrapped.

To support this, the WAIT_AND_WRAP bit in CTRL delays wrapping. After reaching the last column, cursor stays out of bounds in X until next action:

  • If next action is DO_NL: newline is ignored, cursor wraps to next line only
  • If next action is PRINT_CHAR: wraps first, then prints character
  • If next action is writing to CURSOR_Y: Y changes, X stays out of bounds

12.5 Registers Summary

Name Description Base-relative address Access
PRINT_CHAR Write a character to the current cursor location 0x00 WO
CURSOR_Y Set the cursor vertical position. 0x01 R/W
CURSOR_X Set the cursor horizontal position. 0x02 R/W
SCROLL_Y Vertical scroll of the text layer. 0x03 R/W
SCROLL_X Horizontal scroll of the text layer. 0x04 R/W
CURRENT_COLOR Current text color for printed characters 0x05 R/W
CURSOR_BLINK_TIMING Blinking speed for the cursor. Controls how many frames cursor is visible/invisible. 0x06 R/W
CURSOR_CHARACTER Character glyph to use for displaying the cursor 0x07 R/W
CURSOR_COLORS Cursor colors, formatted like CURRENT_COLOR 0x08 R/W
CTRL Text controller control register with flags for behavior and commands 0x09 varies

12.6 Registers

CURSOR_Y (0x01)
Set the cursor vertical position.
CURSOR_Y
7 6 5 4 3 2 1 0
0 0 0 0 0 0 0 0
Reset
CURSOR_Y
Cursor Y position. Writing moves the cursor vertically. (R/W)
CURSOR_X (0x02)
Set the cursor horizontal position.
CURSOR_X
7 6 5 4 3 2 1 0
0 0 0 0 0 0 0 0
Reset
CURSOR_X
Cursor X position. Writing moves the cursor horizontally. (R/W)
SCROLL_Y (0x03)
Vertical scroll of the text layer.
OFFSET_Y
7 6 5 4 3 2 1 0
0 0 0 0 0 0 0 0
Reset
OFFSET_Y
Vertical scroll offset or absolute Y position. (R/W)
SCROLL_X (0x04)
Horizontal scroll of the text layer.
OFFSET_X
7 6 5 4 3 2 1 0
0 0 0 0 0 0 0 0
Reset
OFFSET_X
Horizontal scroll offset or absolute X position. (R/W)
CURRENT_COLOR (0x05)
Current text color for printed characters
FG_COLOR
BG_COLOR
7 6 5 4 3 2 1 0
0 0 0 0 0 0 0 0
Reset
FG_COLOR
Foreground color, taken from the first 16 entries of the global palette (R/W)
BG_COLOR
Background color, taken from the first 16 entries of the global palette (R/W)
CURSOR_CHARACTER (0x07)
Character glyph to use for displaying the cursor
CURSOR_CHAR
7 6 5 4 3 2 1 0
0 0 0 0 0 0 0 0
Reset
CURSOR_CHAR
ASCII character to be displayed as the cursor (R/W)
CURSOR_COLORS (0x08)
Cursor colors, formatted like CURRENT_COLOR
FG_COLOR
BG_COLOR
7 6 5 4 3 2 1 0
0 0 0 0 0 0 0 0
Reset
FG_COLOR
Foreground color of the cursor, taken from the first 16 entries of the global palette (R/W)
BG_COLOR
Background color of the cursor, taken from the first 16 entries of the global palette (R/W)
CTRL (0x09)
Text controller control register with flags for behavior and commands
SAVE_CURSOR
RESTORE_CURSOR
AUTO_SCROLL_X
AUTO_SCROLL_Y
WAIT_AND_WRAP
(reserved)
SCYF / DO_NL
7 6 5 4 3 2 1 0
0 0 0 0 0 0 0 0
Reset
SAVE_CURSOR
Save current cursor position (WO)
RESTORE_CURSOR
Restore cursor to last saved position (WO)
AUTO_SCROLL_X
If 1, automatically scroll horizontally when reaching line end; if 0, wrap (RW)
AUTO_SCROLL_Y
If 1, automatically scroll vertically when reaching bottom; if 0, wrap (RW)
WAIT_AND_WRAP
If 1, wait until space is available and wrap; otherwise skip (RW)
SCYF / DO_NL
On read, indicates that an automatic Y scroll occurred after printing a character or going to a new line.
On write, manually insert a newline, triggers same effect as line wrap. (R/W)