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:
ld hl,patterns
ld de,$4000
call draw_charactercall 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
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.
| Register | Before | After |
|---|---|---|
| HL | — | — |
| DE | — | — |
| B | — | — |
| A | — | — |
| SP | — | — |
The finished machine state
Run to inspect the first three screen cells.
| Row | Cell 1 | Cell 2 | Cell 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
; 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 startUse Download your source to save draw-routine.asm. Build locally with Asm198x:
asm198x --dialect pasmo --cpu z80 --tapbas draw-routine.asm -o draw-routine.tapOr with Pasmo:
pasmo --tapbas draw-routine.asm draw-routine.tapLoad 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)