Skip to content

Place characters in a ZX80 display file

Draw counted labels and character pictures with the machine's ROM, with explicit bounds and display ownership.

A label, a room wall, a face made from several characters: a fixed font can do useful work. Give this routine a row of native screen codes and a cell position. It checks the request, then replaces that span. Your game owns the choice of pictures; the ROM supplies their dots.

Native ZX80 output from the reusable character-span caller.
Code198x native capture: 32×24 physical character positions, compressed rows. The stock PAL 4 KiB ROM runs the BASIC USR caller with 16 KiB RAM. Qualification places the assembled image at $6100 using debugger writes. It checks all three owned rows and their terminators; INPUT owns the following area. This is not a cassette loader or a 1 KiB footprint claim. Character designs belong to the machine's manufacturer; no firmware is distributed.

Include, set up, call

Copy characters.inc into your project and include it once. Reserve 24 private bytes at FC. Also define COLS=32, ROWS=22 and DF_END=$4010 as in the caller. The first maintained call places a six-character span; the full caller below shows allocations and inclusion:

Complete ZX80 caller

demo.asm, excerpt
case_0:
    ld hl,pictures
    ld (FC),hl
    ld a,2
    ld (FC+2),a
    ld a,0
    ld (FC+3),a
    ld a,6
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($6020),a

Stock-ROM USR bridge

caller.bas
10 CLS
20 PRINT "          "
30 PRINT "          "
40 PRINT "          "
50 LET A=USR(24832)
60 INPUT A

Put a source pointer at FC+0..1 (low byte first), column at +2, row at +3 and nonzero count at +4. Offset +7 is unused on this machine. Call character_span. Byte +5 returns 1 when accepted, 0 when rejected; +6 returns the number of character writes, or 0. Inputs +0..4 and +7 survive. Scratch/status +5..23 belong to the routine.

A request stays on one row. Zero counts, invalid starts and horizontal overruns reject before any display write. Sinclair requests also reject unallocated cells or unsafe source codes. This variant rejects rather than clips: shorten a label deliberately if that is the behaviour you want.

Keep the source long enough, readable, stable, non-wrapping and separate from anything the call can overwrite. Contexts are not re-entrant. AF/BC/DE/HL and flags are clobbered; IX/IY survive. Reserve four stack bytes for the direct call and its helpers, plus your caller and interrupts. The USR bridge adds eight saved-register bytes and returns BC=0 to BASIC. The routine changes no interrupt state or display mode.

The USR bridge assumes the assembled image is already at $6100. Reserve the context/results area at $6000 and the image through $63FF against BASIC growth before adapting it to a larger program. This small verification caller stays below that region; it does not install a RAMTOP policy or a tape loader for you.

Use the actual display

The ZX80 keeps a moving, newline-terminated display file, not a fixed-stride screen array. Read its current start from $400C and its exclusive DF-END from $4010. A row can contain fewer than 32 allocated cells.

A is screen code 38, not ASCII 65. Codes 0–63 name ROM characters; adding 128 selects their inverses. Bit 6 must stay clear. Byte 118 is a newline/HALT instruction in the display path: replacing it can destroy the picture.

The complete caller prints three ten-cell rows. A ten-cell run fits the second row; a 32-cell run rejects, even with 16 KiB attached. So do writes into an empty later row. The routine never expands the display file.

The caller uses 22 upper rows as a policy to leave input space; the display hardware has 24. A game that owns the entire valid display file can choose a different row limit. The include reads D_FILE afresh, walks to the row, preflights the whole span and then writes. PRINT, INPUT and editing can relocate or rebuild that file, so cached pointers need a clear lifetime.

The ZX80 normally blanks while computing and refreshes when the ROM waits for input. The ZX81 adds SLOW mode, which shares CPU time with display work; FAST suppresses that work. This component leaves that machinery alone. A standalone assembly game must provide its own refresh policy rather than assuming a free-running bitmap controller.

Connect cells, shapes and maths

At an 8×8 character grid, cell (2,3) begins at active pixel (16,24). That multiplication places a whole glyph; it does not let you change one of its dots. Border coordinates are a separate matter.

Block characters can supply a coarser drawing grid. With a genuine 2×2 block repertoire, coarse point (5,7) belongs to cell (2,3), quadrant (1,1): divide by two for the cell and take the remainders for the quadrant. You still need a lookup from the desired block mask to this ROM’s actual codes. This routine accepts those codes; it does not invent a universal semigraphics table.

Place a point develops local/grid coordinates. Move a glyph connects codes and stored shapes. A fixed font makes the distinction especially clear: a code can select a picture without giving the CPU writable picture memory. Use larger cell pictures or an approximation when the ROM lacks the shape; a high-resolution extension needs its own named configuration.

Read the reusable component and complete caller

Reusable character-span routine

characters.inc
; Define FC (24 private bytes). Source and request must not alias output.
; +0 source word, +2 x, +3 y, +4 count, +5 accepted, +6 writes,
; +7 Aquarius colour (ignored on Sinclair). Inputs +0..4 and +7 survive.
; AF/BC/DE/HL clobbered; IX/IY and interrupt state unchanged.
character_span:
    xor a
    ld (FC+5),a
    ld (FC+6),a
    ld a,(FC+2)
    cp COLS
    ret nc
    ld b,a
    ld a,COLS
    sub b
    ld b,a
    ld a,(FC+4)
    or a
    ret z
    cp b
    jr c,fc_count_fit
    ret nz
fc_count_fit:
    ld a,(FC+3)
    cp ROWS
    ret nc
; A current, ROM-owned, well-formed display file. Never allocate or extend it.
    ld hl,($400c)
    ld de,(DF_END)
    ld (FC+12),de
    ld a,(hl)
    cp $76
    ret nz
    inc hl
    ld a,(FC+3)
    ld b,a
    or a
    jr z,fc_row_ready
fc_skip_row:
    ld c,33
fc_scan:
    call fc_inside
    ret nc
    ld a,(hl)
    inc hl
    cp $76
    jr z,fc_newline
    dec c
    jr nz,fc_scan
    ret
fc_newline:
    djnz fc_skip_row
fc_row_ready:
    ld a,(FC+2)
    ld b,a
    or a
    jr z,fc_start_ready
fc_skip_column:
    call fc_inside
    ret nc
    ld a,(hl)
    cp $76
    ret z
    inc hl
    djnz fc_skip_column
fc_start_ready:
    ld (FC+10),hl
    ld a,(FC+4)
    ld b,a
    ld de,(FC)
fc_preflight:
    call fc_inside
    ret nc
    ld a,(hl)
    cp $76
    ret z
    ld a,(de)
    and $40
    ret nz
    inc de
    inc hl
    djnz fc_preflight
    ld hl,(FC+10)
    ld de,(FC)
    ld a,(FC+4)
    ld b,a
fc_copy:
    ld a,(de)
    ld (hl),a
    inc de
    inc hl
    djnz fc_copy
    ld a,(FC+4)
    ld (FC+6),a
    ld a,1
    ld (FC+5),a
    ret
; Carry means HL is below the exclusive end. Does not clobber BC/DE/HL.
fc_inside:
    ld a,(FC+13)
    cp h
    jr nz,fc_high
    ld a,(FC+12)
    cp l
fc_high:
    jr z,fc_equal
    ccf
    ret
fc_equal:
    or a
    ret
character_span_end:

Complete caller and configuration

demo.asm
ORG $6100
FC EQU $6000
COLS EQU 32
ROWS EQU 22
DF_END EQU $4010
start:
    push af
    push bc
    push de
    push hl
case_0:
    ld hl,pictures
    ld (FC),hl
    ld a,2
    ld (FC+2),a
    ld a,0
    ld (FC+3),a
    ld a,6
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($6020),a
case_1:
    ld hl,pictures
    ld (FC),hl
    ld a,0
    ld (FC+2),a
    ld a,1
    ld (FC+3),a
    ld a,10
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($6021),a
case_2:
    ld hl,pictures
    ld (FC),hl
    ld a,9
    ld (FC+2),a
    ld a,2
    ld (FC+3),a
    ld a,1
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($6022),a
case_3:
    ld hl,pictures
    ld (FC),hl
    ld a,0
    ld (FC+2),a
    ld a,21
    ld (FC+3),a
    ld a,1
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($6023),a
case_4:
    ld hl,pictures
    ld (FC),hl
    ld a,32
    ld (FC+2),a
    ld a,0
    ld (FC+3),a
    ld a,1
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($6024),a
case_5:
    ld hl,pictures
    ld (FC),hl
    ld a,0
    ld (FC+2),a
    ld a,22
    ld (FC+3),a
    ld a,1
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($6025),a
case_6:
    ld hl,pictures
    ld (FC),hl
    ld a,0
    ld (FC+2),a
    ld a,0
    ld (FC+3),a
    ld a,0
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($6026),a
case_7:
    ld hl,pictures
    ld (FC),hl
    ld a,31
    ld (FC+2),a
    ld a,0
    ld (FC+3),a
    ld a,2
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($6027),a
case_8:
    ld hl,pictures
    ld (FC),hl
    ld a,255
    ld (FC+2),a
    ld a,0
    ld (FC+3),a
    ld a,2
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($6028),a
case_9:
    ld hl,pictures
    ld (FC),hl
    ld a,0
    ld (FC+2),a
    ld a,255
    ld (FC+3),a
    ld a,1
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($6029),a
case_10:
    ld hl,pictures
    ld (FC),hl
    ld a,0
    ld (FC+2),a
    ld a,0
    ld (FC+3),a
    ld a,255
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($602a),a
case_11:
    ld hl,pictures
    ld (FC),hl
    ld a,9
    ld (FC+2),a
    ld a,2
    ld (FC+3),a
    ld a,2
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($602b),a
case_12:
    ld hl,pictures
    ld (FC),hl
    ld a,0
    ld (FC+2),a
    ld a,1
    ld (FC+3),a
    ld a,32
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($602c),a
case_13:
    ld hl,bad_picture
    ld (FC),hl
    ld a,0
    ld (FC+2),a
    ld a,2
    ld (FC+3),a
    ld a,2
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($602d),a
case_14:
    ld hl,inverse_picture
    ld (FC),hl
    ld a,0
    ld (FC+2),a
    ld a,2
    ld (FC+3),a
    ld a,1
    ld (FC+4),a
    ld a,0
    ld (FC+7),a
    call character_span
    ld a,(FC+5)
    or 128
    ld ($602e),a
    ld a,239
    ld ($603f),a
    pop hl
    pop de
    pop bc
    pop af
    ld bc,0
    ret
    INCLUDE "characters.inc"
pictures:
    db 38,39,40,41,42,43,38,39,40,41,42,43,38,39,40,41,42,43,38,39,40,41,42,43,38,39,40,41,42,43,38,39,40,41,42,43,38,39,40,41,42,43
bad_picture:
    db 118,38
inverse_picture:
    db 166

Pay for the work

The component is 164 code bytes, excluding caller and source, with 24 reserved context bytes. A six-character label needs six source bytes and six destination writes. Sinclair also walks preceding rows and checks the target span before copying. Those are storage/work counts, not measured cycles or a promise to fit one frame.

PRINT is simpler when the ROM owns formatting, placement and scrolling. Direct spans suit a game that owns its cells. Cached row pointers or a trusted fast path can reduce work, but their validity depends on allocation lifetime and validated inputs. Clipping, transparent cells, proportional text and arbitrary-size lettering each change the contract.

What was verified

The stock PAL 4 KiB ROM runs the BASIC USR caller with 16 KiB RAM. Qualification places the assembled image at $6100 using debugger writes. It checks all three owned rows and their terminators; INPUT owns the following area. This is not a cassette loader or a 1 KiB footprint claim.

Three builds per machine complete every call, including boundary and rejection cases. Completed wrong-stride controls fail the same memory and visible-output checks. The second control bypasses source-token validation; the independent newline preflight still prevents a count-only overflow.

Native pixel comparisons cover the caller-owned rectangle, with one global alignment for native frame padding. They do not claim that moving input/cursor rows stay unchanged. Retained CI checks source/image/memory hashes, call markers and independent destinations. Fresh execution needs separately supplied firmware. These observations do not establish original-hardware timing, tear-free updates or OS coexistence.