Code status: maintained routines, executed in a bounded Z80 fixture. Checks cover all 768 character cells plus string and bounds cases against an independent bitmap oracle. The fixture supplies a synthetic font; it does not claim to test ROM startup.
Audit execution record · Scope and remaining checks
Overview
Print text directly to the display file by copying character data from the ROM character set at $3C00. Faster than using ROM routines and gives you full control over positioning. Essential for scores, messages, and UI text.
Include and call
Include characters.inc once. Give it a valid stack and the standard 48K font visible in ROM. print_char takes ASCII 32–127 in A, row 0–23 in B and column 0–31 in C. It preserves AF, BC, DE and HL. Invalid characters or positions do nothing. It writes eight bitmap bytes and leaves attributes alone: choose the cell colours separately.
print_string takes a zero-terminated string in HL and the same B/C position. It preserves BC, DE and HL, changes AF and clips at column 32. The string must remain readable until its terminator or the right edge; it does not wrap or clear old text.
Bounded character and string printing
; 48K Spectrum ROM visible at0..$3fff; caller provides stack and display ownership.
; print_char: A=ASCII32..127, B=row0..23, C=column0..31. Preserves AF/BC/DE/HL.
; Invalid character or position: no write. Uses the standard fixed font location.
; print_string: HL=null-terminated printable ASCII; B/C=start cell.
; Stops at the right edge without wrapping. Preserves BC/DE/HL, changes AF.
; Bitmap only: set attributes separately. No ROM routine call or IRQ requirement.
; =============================================================================
; CHARACTER PRINTING - ZX SPECTRUM
; Print characters using ROM character set
; Taught: Game 1 (Ink War), Unit 4
; =============================================================================
DISPLAY_FILE equ $4000 ; Screen memory
CHAR_SET equ $3C00 ; ROM character set (chars 0-127)
; Print single character
; Input: A = ASCII character (32-127)
; B = row (0-23), C = column (0-31)
print_char:
push bc
push de
push hl
push af
ld a,b
cp 24
jr nc,print_char_done
ld a,c
cp 32
jr nc,print_char_done
pop af
push af
cp 32
jr c,print_char_done
cp 128
jr nc,print_char_done
; Calculate character data address: CHAR_SET + char * 8
ld l, a
ld h, 0
add hl, hl ; * 2
add hl, hl ; * 4
add hl, hl ; * 8
ld de, CHAR_SET
add hl, de ; HL = character data source
push hl ; Save source address
; Calculate display file address
; The Spectrum's display is organised in thirds with interleaved lines
ld a, b ; A = row (0-23)
and %00011000 ; Which third (0, 8, or 16)?
add a, $40 ; Add display file base ($4000 high byte)
ld d, a ; D = high byte
ld a, b ; A = row
and %00000111 ; Line within character row (0-7)
rrca
rrca
rrca ; Move to bits 5-7
add a, c ; Add column
ld e, a ; E = low byte, DE = screen address
pop hl ; HL = character data
; Copy 8 bytes (8 pixel rows)
ld b, 8
character_scan:
ld a, (hl) ; Get character row
ld (de), a ; Write to screen
inc hl ; Next character row
inc d ; Next screen line (add 256)
djnz character_scan
print_char_done:
pop af
pop hl
pop de
pop bc
ret
; Print null-terminated string
; Input: HL = string address, B = row, C = column
print_string:
push bc
push hl
character_string_next:
ld a,b
cp 24
jr nc,character_string_done
ld a,c
cp 32
jr nc,character_string_done
ld a, (hl)
or a ; Check for null terminator
jr z, character_string_done
call print_char
inc hl
inc c ; Next column
jr character_string_next
character_string_done:
pop hl
pop bc
ret
; Example usage:
; ld hl, message
; ld b, 10 ; Row 10
; ld c, 5 ; Column 5
; call print_string
;
; message:
; defm "HELLO WORLD"
; defb 0 ld hl,message
ld b,10
ld c,5
call print_string
message: defm "HELLO WORLD"
defb 0Trade-offs
| Aspect | Cost |
|---|---|
| CPU | Not measured for this illustrative listing; include the caller and hardware scheduling costs |
| Memory | Assemble the include with your caller to obtain its size |
| Limitation | ROM charset only (use custom font for alternatives) |
When to use: Scores, labels, messages, any text display.
When to avoid: When you need custom graphics characters (use a custom charset instead).
Display File Address Calculation
The Spectrum’s display memory is non-linear. For character row R and column C:
High byte: $40 + (R AND $18)
Low byte: ((R AND 7) * 32) + C
= ((R AND 7) RRCA RRCA RRCA) + CThis interleaved layout was designed for TV signal generation, not programmer convenience!
The thirds layout
The display memory at $4000-$57FF is split into three 2 KB regions corresponding to the top, middle, and bottom thirds of the screen (64 pixel lines / 8 character rows each):
| Third | Address range | Character rows |
|---|---|---|
| Top | $4000-$47FF |
0-7 |
| Middle | $4800-$4FFF |
8-15 |
| Bottom | $5000-$57FF |
16-23 |
Within a third, adding 256 to the address moves down one pixel (the next scan line of the same character row). After 8 pixel rows, you’ve drawn one full character row; the next character row’s address is + $0020 (or + 32) into the third’s region.
This means horizontal scrolling within a third is fast (write to 8 contiguous addresses with +$0100 strides), but scrolling across third boundaries needs an explicit fix-up. Many games scroll only within one third (e.g. status bar in the bottom 8 rows, gameplay in middle, sky in top) to dodge this.
ROM Character Set
The character set at $3C00 contains:
$3C00is the addressing bias used with ASCII × 8; it is not the start of 128 stored glyphs.- Printable glyphs for codes 32–127 occupy
$3D00–$3FFF; the Spectrum glyphs differ from ASCII at some codes. - Each character is 8 bytes (8x8 pixels)
Related
Patterns: Attribute Writing, Numeric Display
Vault: ULA — display memory layout | ZX Spectrum