Skip to content

Call a drawing routine

Draw twice with one routine, follow CALL and RET, and check what a routine needs and changes.

  • Sinclair ZX Spectrum
  • Assembly

Our loop draws one character. To draw another, we could copy its instructions. Instead, we will put the loop in a routine and call it twice, with different destinations. Both pictures use the same drawing code and the same eight pattern bytes.

Give the routine a job

The routine is named draw_character. Its caller prepares HL with the address of eight pattern bytes and DE with a screen destination, then calls it:

Z80 assembly
 ld hl,patterns
 ld de,$4000
 call draw_character

call draw_character saves the address of the instruction after the call, then transfers execution to the routine. Inside it, the familiar loop draws eight rows. A new instruction, ret, returns to the saved address.

The main program prepares another destination and calls the same label again. This time RET returns after the second call. A routine does not have one fixed place to return to: each call supplies its own return address.

What CALL keeps for us

The saved address lives on the stack, an area of RAM used to keep return addresses and other temporary values. SP, the stack pointer, identifies the top of that stack.

On the Z80, CALL stores a two-byte return address and reduces SP by two. RET reads that address and increases SP by two. The browser and the local tape loader use the ROM to establish a working stack before our program starts.

CALL and RET do not automatically preserve HL, DE, A or B. If a routine changes those registers, the caller sees the changed values after it returns.

State the contract

A routine’s contract tells its caller what to supply and what to expect afterwards. Here is ours:

Part of the contract draw_character
Pattern input HL points to eight readable pattern bytes.
Destination input DE is $4000–$401F, the top row of a cell in the first character row.
Environment A valid return stack; the caller has chosen the cell’s colours.
Work Write eight bitmap rows, advancing the destination by $100 each time.
Changed registers HL advances by 8; D advances by 8 while E stays unchanged; B becomes 0; A holds the last pattern byte. Flags may change.
Return Execution resumes after the calling instruction; balanced CALL/RET restores SP to its value before CALL.

The routine sets B to 8 itself, so B is not an input the caller must prepare. It does not set attributes or clear other screen memory. The main program sets bright white ink on black paper for the first three cells, covering the destinations we will try.

Follow both calls

Run the program and replay its recorded events. Next event reveals one call, bitmap write or return. Through next return advances through one routine invocation. The register table shows actual values immediately before and after the selected event.

The program runs on the emulator first; replay inspects that recording. It stops at hold, so the endless hold loop is not part of the recording. If an edit prevents that stopping point being reached, a bounded partial trace is labelled as such.

One routine, two calls

Run the program. Replay the first call and its writes, then its return. Where does the second call send the same eight patterns?

Explain this experiment

Both calls enter draw_character. The caller supplies a fresh HL and DE each time. The routine draws eight rows, then RET resumes after the call that invoked it.

Optionally import the character you kept in lesson 3, in this tab.

Source · view and edit
ZX Spectrum

Edit the source, then assemble.

Edits are saved in this browser. Keep a downloaded copy of work you want to keep.

Follow the recorded execution

The program has already run. Replay shows actual calls, returns and bitmap writes up to hold. Light and dark show stored 1 and 0 bits; grey means no write replayed yet.

Run your program to capture its execution.

Each cell is eight pixels wide. The replay covers the first three cells.

Registers around the selected event
RegisterBeforeAfter
HL——
DE——
B——
A——
SP——

    The finished machine state

    Run to inspect the first three screen cells.

    Actual bitmap bytes, left to right
    RowCell 1Cell 2Cell 3
    1———
    2———
    3———
    4———
    5———
    6———
    7———
    8———

    Your edits remain in the same editor between experiments. Use my saved character imports the eight pattern bytes explicitly kept in lesson 3 in this tab; it leaves the routine and callers intact.

    A return is not a reset

    At the first return, HL is eight bytes past patterns. That is useful evidence: the routine consumed all eight source bytes. But it means the caller must reload HL before drawing that same picture again.

    Likewise, DE has advanced beyond the eight destinations just written. Reloading DE chooses the next character’s position. Passing new inputs is what makes one piece of code useful in different places.

    If both calls use DE = $4000, will two characters appear side by side?

    Show the explanation

    No. Both invocations write the same eight screen bytes. The second drawing replaces the first. The replay still records two calls and two returns, but both groups of writes use $4000, $4100, through $4700.

    Could we replace CALL with JP because both transfer execution to a label?

    Show the explanation

    Not while relying on this routine’s RET. JP does not save a return address. RET would use whatever address was already on the stack, not the instruction after that JP. CALL and RET form a pair because they agree on how the return address is stored and recovered.

    Keep code and data separate

    The main program’s hold loop appears before the routine. That prevents normal execution from falling into it after the second return. The routine ends with RET, and its pattern table follows it. Neither the caller nor the routine should continue into those data bytes as instructions.

    The complete starting program
    draw-routine.asm
    ; Call one drawing routine with two different destinations.
     org 32768
    start:
     ld a,0
     out ($FE),a
     ld a,71
     ld ($5800),a
     ld ($5801),a
     ld ($5802),a
     ld hl,patterns
     ld de,$4000
     call draw_character
    second:
     ld hl,patterns
     ld de,$4001
     call draw_character
    hold:
     jr hold
    ; In: HL points to eight bytes; DE points to row 1 of a top-row cell.
    ; Out: HL += 8, D += 8, E unchanged, B = 0, A = last pattern.
    ; Changes flags; balanced CALL/RET restores SP. Does not set colours.
    draw_character:
     ld b,8
    row:
     ld a,(hl)
     ld (de),a
     inc hl
     inc d
     djnz row
     ret
    patterns:
     defb %00011000
     defb %00111100
     defb %01111110
     defb %11011011
     defb %11111111
     defb %00111100
     defb %01100110
     defb %01000010
     end start

    Use Download your source to save draw-routine.asm. Build locally with Asm198x:

    Shell
    asm198x --dialect pasmo --cpu z80 --tapbas draw-routine.asm -o draw-routine.tap

    Or with Pasmo:

    Shell
    pasmo --tapbas draw-routine.asm draw-routine.tap

    Load the tape in a fresh 48K Spectrum as in lesson 1. Check both characters. If only the second is wrong, inspect the inputs prepared for the second call before changing a routine that already drew the first correctly.

    We now have a drawing operation we can call with a chosen position. That gives us a useful basis for moving something in response to input.

    Sources

    Zilog, Z80 CPU User Manual, UM008011-0816, instruction entries “CALL nn”, “RET”, “JP nn”, “INC ss”, “INC r” and “DJNZ, e”. Sinclair Research, ZX Spectrum BASIC Programming, chapter 24, display-file and attribute layout.

    Run it here

    This unit's program, built for you.

    Program
    draw-routine.tap
    Machine
    ZX Spectrum 48K (PAL)

    Download draw-routine.tap