ASM Examples

Displaying Editor Output on Real Z80 Hardware
by Norbert of Norbsoft

Every editor in NorbSoft's zx-editor can export its work as a ready-to-assemble .asm file — raw defb bytes plus the constants you need to address them. That data is only half the story, though: to actually see it on a Z80 you need a small viewer program that loads the exported data into the right place in memory and tells the ZX Spectrum to draw it.

This section collects worked examples — one per editor — that do exactly that. Each example pairs an exported data file with a short viewer program, walks through how the viewer works, and shows you how to assemble and run the pair on an emulator.

Every example here is written for Pasmo with the --tapbas flag, producing a .tap file that auto-runs in an emulator such as Fuse with --auto-load — exactly the workflow described in the Z80 Assembly Introduction.

UDG: Displaying an Exported Sprite

[Back to top]

The UDG Sprite Editor exports a sprite as sprite_udg_data (the pixel bytes for each character), sprite_attr_data (one attribute byte per character) and a handful of equ constants describing the sprite's dimensions. udg_viewer.asm is a minimal companion program that includes that exported file and stamps the sprite into the centre of the screen by writing pixel and attribute bytes directly into screen memory — no ROM calls, so it works identically on a 48K or 128K machine regardless of which ROM is paged in.

Centring the Sprite

The exported file defines SPRITE_WIDTH and SPRITE_HEIGHT in characters. The viewer uses these, together with the screen's 32x24 character grid, to work out where the sprite's top-left character cell should land:

START_COL       equ (SCREEN_COLS - SPRITE_WIDTH) / 2   ; = 12
START_ROW       equ (SCREEN_ROWS - SPRITE_HEIGHT) / 2  ; = 10
END_COL         equ START_COL + SPRITE_WIDTH           ; = 19

Because SPRITE_WIDTH and SPRITE_HEIGHT come from whichever file the include line pulls in, the assembler works these three constants out for you — you never edit them by hand. For the sprite bundled with this example (7 characters wide, 3 tall) they come out to START_COL = 12, START_ROW = 10 and END_COL = 19, placing its top-left cell at character column 12, row 10. Export a sprite of your own with different dimensions and the assembled values — and the position it ends up centred at — will be different. END_COL always marks one column past the sprite's right edge, which the draw loop uses later to detect when it should wrap to the start of the next character row.

Screen Memory Addressing — the "Thirds" Trick

The ZX Spectrum's 6144-byte display file is notoriously not laid out row by row. It is split into three thirds of 2048 bytes (64 scan lines, or 8 character rows, each), and within a third the scan lines of each character row are interleaved in a fixed bit pattern. Calculating an arbitrary pixel address normally means juggling several groups of bits.

The viewer sidesteps almost all of that complexity by relying on one assumption: the whole sprite fits inside a single third. For the bundled 3-row-tall sprite, centring it lands it at character rows 10-12 — comfortably inside third 1 ($4800-$4FFF, covering character rows 8-15) — so the address of any pixel byte in the sprite reduces to a simple two-part formula:

  • High byte = $48 + scan-line-within-character (0-7)
  • Low byte = (character row − 8) × 32 + character column

That assumption isn't a coincidence specific to this one sprite — it holds for any sprite up to 8 character rows tall. Centring a sprite that size or smaller vertically on a 24-row screen always places it entirely within the middle third (rows 8-15), which is why PIXEL_BASE is hard-coded to $48. A taller sprite — possible with a narrow shape such as 2x10 or 1x21, since the editor's 21-character limit allows it — would straddle two thirds and need the fuller, bit-juggling general-purpose address calculation that this minimal viewer deliberately doesn't attempt.

The low byte formula above is no accident — it is also exactly how the linear, 768-byte attribute area at $5800 is addressed: $5800 + row × 32 + column can be rewritten as $5900 + (row − 8) × 32 + column, i.e. the same low byte with attribute high byte $59 instead of pixel high byte $48. The viewer computes that shared low byte once per character (into C) and reuses it for both the eight pixel-row writes and the single attribute write:

PIXEL_BASE      equ $48
ATTR_BASE       equ $59
ROW_OFFSET      equ START_ROW - 8

ROW_OFFSET is the sprite's top character row expressed as "row within the third", i.e. character row − 8 — the assembler works this out to 2 for the bundled sprite, but it will differ for a sprite of a different height or screen position. The draw loop counts this value up in register D as it moves down the sprite, so D always holds (character row − 8) directly — no subtraction needed at draw time.

The Draw Loop

With the addressing worked out, the rest of the program is a straightforward nested loop:

  1. IX walks through sprite_udg_data (8 pixel bytes per character) and IY walks through sprite_attr_data (1 byte per character).
  2. D/E track the current character's row-within-third and column. Each pass computes the shared low byte C = D × 32 + E by shifting D left five times and adding E.
  3. An inner loop writes the character's 8 pixel bytes to ($48+0, C) through ($48+7, C), incrementing the high byte H after each write — moving one scan line down the third for every byte.
  4. The character's attribute byte is then written to ($59, C).
  5. Finally E is incremented; once it reaches END_COL it resets to START_COL and D is incremented, moving the "cursor" to the start of the next character row.

The outer loop runs SPRITE_CHARS times — 21 for the bundled sprite, but whatever your own exported file defines — once per character in the sprite, counted down in B via djnz.

Border Flags and the Halt Loop

Two out (254), a calls bracket the drawing work, changing the border colour to give an at-a-glance progress indicator with no text output required: the border turns red the instant the program starts running, and white once every character has been stamped to the screen. After that the program parks itself in a tight halt / jr halt_loop pair — there is nothing left to do, and halt lets the CPU idle efficiently until the next interrupt.

The Exported Data: udg_7_x_3.asm

Before looking at the viewer, here is the file it includes — the .asm the UDG Sprite Editor's Export ASM produced for the bundled 7x3-character sprite. This is the same shape of file the editor generates for any sprite you export: a header comment summarising its dimensions, the equ constants the viewer relies on, the raw pixel bytes for each character (sprite_udg_data), and one attribute byte per character (sprite_attr_data).

; ZX Spectrum UDG Sprite Data
; Generated by ZX Spectrum UDG Editor
;
; Sprite dimensions: 7x3 characters (56x24 pixels)
; Total characters: 21
; UDG data size: 168 bytes
; Attribute data size: 21 bytes

; -----------------------------------------------------------------------------
; Constants
; -----------------------------------------------------------------------------
SPRITE_WIDTH    equ 7          ; Width in characters
SPRITE_HEIGHT   equ 3          ; Height in characters
SPRITE_CHARS    equ 21         ; Total characters
UDG_BYTES       equ 168        ; Bytes of UDG pixel data
FIRST_UDG_CHAR  equ 144         ; First UDG character code

; -----------------------------------------------------------------------------
; UDG Pixel Data (8 bytes per character, MSB=leftmost pixel)
; -----------------------------------------------------------------------------
sprite_udg_data:
        defb $00,$00,$00,$00,$00,$00,$00,$00  ; char 0 (row 0, col 0)
        defb $10,$10,$10,$10,$10,$10,$10,$10  ; char 1 (row 0, col 1)
        defb $00,$00,$00,$00,$00,$00,$00,$00  ; char 2 (row 0, col 2)
        defb $08,$10,$20,$20,$40,$80,$00,$00  ; char 3 (row 0, col 3)
        defb $00,$00,$00,$00,$00,$00,$00,$00  ; char 4 (row 0, col 4)
        defb $08,$10,$20,$41,$82,$42,$34,$0C  ; char 5 (row 0, col 5)
        defb $00,$00,$00,$00,$00,$00,$00,$00  ; char 6 (row 0, col 6)
        defb $00,$00,$00,$FF,$00,$00,$00,$00  ; char 7 (row 1, col 0)
        defb $00,$00,$00,$00,$00,$00,$00,$00  ; char 8 (row 1, col 1)
        defb $00,$3C,$42,$52,$42,$22,$1C,$00  ; char 9 (row 1, col 2)
        defb $00,$00,$00,$00,$00,$00,$00,$00  ; char 10 (row 1, col 3)
        defb $10,$10,$10,$FF,$10,$10,$10,$10  ; char 11 (row 1, col 4)
        defb $00,$00,$00,$00,$00,$00,$00,$00  ; char 12 (row 1, col 5)
        defb $00,$1C,$62,$42,$22,$22,$1C,$00  ; char 13 (row 1, col 6)
        defb $00,$00,$00,$00,$00,$00,$00,$00  ; char 14 (row 2, col 0)
        defb $10,$10,$12,$10,$10,$90,$10,$10  ; char 15 (row 2, col 1)
        defb $00,$00,$00,$00,$00,$00,$00,$00  ; char 16 (row 2, col 2)
        defb $81,$42,$24,$18,$18,$24,$42,$81  ; char 17 (row 2, col 3)
        defb $00,$00,$00,$00,$00,$00,$00,$00  ; char 18 (row 2, col 4)
        defb $00,$40,$60,$20,$38,$18,$30,$00  ; char 19 (row 2, col 5)
        defb $00,$00,$00,$00,$00,$00,$00,$00  ; char 20 (row 2, col 6)

; -----------------------------------------------------------------------------
; Attribute Data (1 byte per character)
; Format: FBPPPIII where F=flash, B=bright, P=paper(0-7), I=ink(0-7)
; -----------------------------------------------------------------------------
sprite_attr_data:
        defb $0F,$38,$0F,$22,$0F,$2A,$0F  ; row 0
        defb $3A,$0F,$22,$0F,$28,$0F,$35  ; row 1
        defb $0F,$79,$0F,$27,$0F,$2E,$0F  ; row 2

Two details worth pointing out: FIRST_UDG_CHAR equ 144 is the character code you'd print through the ROM to place this sprite's first character on screen — the viewer below bypasses the ROM entirely and pokes bytes straight into screen memory, so it never touches that constant, but your own programs may well want it. And the trailing ; char N (row R, col C) / ; row N comments on each defb line are generated purely so a human can find their place in the data; the assembler skips over them, and (as the corrected comments in the viewer below remind us) nothing checks that they stay accurate if the data they describe ever changes.

Source: udg_viewer.asm

This is the complete viewer. Its include line names the exported sprite data file it expects to find alongside it — bundled here as udg_7_x_3.asm, a 7x3-character sprite, simply because that's the name this particular export was given. When you export your own sprite from the UDG Sprite Editor it will be saved under whatever filename you choose, so you'd change this line to match. Whatever it's called, that file needs to define SPRITE_WIDTH, SPRITE_HEIGHT, SPRITE_CHARS, sprite_udg_data and sprite_attr_data — exactly what the editor's ASM export produces, for any sprite up to the 21-character limit.

; udg_viewer.asm
;
; Displays the 7x3-character UDG sprite from udg_7_x_3.asm centred on
; the ZX Spectrum screen (32x24 characters). Pixel and attribute bytes
; are written directly to screen memory — no ROM calls, works on 48K
; or 128K regardless of which ROM is active.
;
; Assemble:  pasmo --tapbas udg_viewer.asm udg_viewer.tap
; Run:       fuse --auto-load udg_viewer.tap

        org     $8000

        include "udg_7_x_3.asm"

; -----------------------------------------------------------------------------
; Layout: centre the SPRITE_WIDTH x SPRITE_HEIGHT sprite on the 32x24
; character screen.
; -----------------------------------------------------------------------------
SCREEN_COLS     equ 32
SCREEN_ROWS     equ 24
START_COL       equ (SCREEN_COLS - SPRITE_WIDTH) / 2   ; = 12
START_ROW       equ (SCREEN_ROWS - SPRITE_HEIGHT) / 2  ; = 10
END_COL         equ START_COL + SPRITE_WIDTH           ; = 19

; The sprite spans character rows START_ROW..START_ROW+2 (10-12), which
; fall entirely within screen "third" 1 ($4800-$4FFF). That keeps the
; pixel-address arithmetic simple: high byte = $48 + scan line, low byte
; = (row - 8) * 32 + column. Attribute memory is linear, so its address
; is just $5800 + row*32 + column = $5900 + (row - 8)*32 + column — the
; same low byte with a different high byte ($59).
PIXEL_BASE      equ $48
ATTR_BASE       equ $59
ROW_OFFSET      equ START_ROW - 8

start:
        ; Red border = code is running
        ld      a, 2
        out     (254), a

        ld      ix, sprite_udg_data     ; IX -> next character's pixel data
        ld      iy, sprite_attr_data    ; IY -> next character's attribute byte
        ld      d, ROW_OFFSET           ; D = row within third (2..4)
        ld      e, START_COL            ; E = character column (12..18)
        ld      b, SPRITE_CHARS         ; 21 characters to draw

draw_loop:
        push    bc

        ; Low byte of address = (row within third) * 32 + column.
        ; Shared by the pixel rows (high byte $48+Y) and the attribute
        ; byte (high byte $59) — see comment above.
        ld      a, d
        add     a, a
        add     a, a
        add     a, a
        add     a, a
        add     a, a            ; A = D * 32
        add     a, e
        ld      c, a            ; C = low byte (constant for this character)

        ; Stamp the 8 scan lines of this character into screen memory
        ld      b, 8
        ld      h, PIXEL_BASE
draw_scan:
        ld      l, c
        ld      a, (ix)
        ld      (hl), a
        inc     ix
        inc     h               ; advance to next scan line
        djnz    draw_scan

        ; Write this character's attribute byte
        ld      h, ATTR_BASE
        ld      l, c
        ld      a, (iy)
        ld      (hl), a
        inc     iy

        ; Move to next character position; wrap column back at END_COL
        inc     e
        ld      a, e
        cp      END_COL
        jr      nz, next_char
        ld      e, START_COL
        inc     d
next_char:
        pop     bc
        djnz    draw_loop

        ; White border = display ready
        ld      a, 7
        out     (254), a

halt_loop:
        halt
        jr      halt_loop

        end     start

Assembling and Running

To try this with your own sprite: export it from the UDG Sprite Editor, place the resulting .asm file in the same folder as udg_viewer.asm, and edit the include line — currently include "udg_7_x_3.asm", the name of the sprite bundled with this example — to match whatever filename your export was saved as. Everything else (SPRITE_WIDTH, SPRITE_HEIGHT, SPRITE_CHARS and every constant derived from them) is recalculated automatically by the assembler from your file's contents. Then assemble and run it exactly as described in the Z80 Assembly Introduction:

pasmo --tapbas udg_viewer.asm udg_viewer.tap && fuse --tape udg_viewer.tap --auto-load

The --tapbas flag wraps the assembled machine code in a small BASIC loader so the resulting .tap auto-runs when loaded; --auto-load tells Fuse to insert and run the tape image immediately on startup. You should see the border flash red, the sprite appear centred on a black screen, and the border settle to white once drawing is complete.