Skip to content

Character Printing

Print ASCII characters to the screen using the ROM character set. Direct display file access for fast text rendering.

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

characters.inc
; 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
Z80 assembly
    ld hl,message
    ld b,10
    ld c,5
    call print_string
message: defm "HELLO WORLD"
         defb 0

Trade-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:

Code
High byte: $40 + (R AND $18)
Low byte:  ((R AND 7) * 32) + C
         = ((R AND 7) RRCA RRCA RRCA) + C

This 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:

  • $3C00 is 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)

Patterns: Attribute Writing, Numeric Display

Vault: ULA — display memory layout | ZX Spectrum