← Back to Courses
module
40

Appendix X - Worked solutions

Every exercise in this course has one right answer in the sense that matters: a program that does what was asked. There is rarely only one way to write it, so treat what follows as a solution rather than the solution. If yours differs and it works, yours is fine.

Have a go first. These are here for when you are stuck, and for afterwards, when it is worth seeing how somebody else laid the same job out.

The "Change one thing" edits are not answered here, on purpose. Those work by making you guess before you look, and an answer in print would spoil the one part of each section that makes you commit to a prediction. You already have a better answer key than any appendix: make the edit and run it. The machine will tell you, immediately and without judgement.

Solutions appear here only once they have been assembled and run on the machine.

This half covers S3 to S16. The rest - S17 onwards, where the course takes the screen over and builds a game - is in Appendix X (continued).

S3. Your first program

3.1 - Print your name

Five characters, five copies of the same three instructions. Substitute your own letters.

        ORG 256

        LD A,'G'
        RST 8
        DEFB 158

        LD A,'A'
        RST 8
        DEFB 158

        LD A,'V'
        RST 8
        DEFB 158

        LD A,'I'
        RST 8
        DEFB 158

        LD A,'N'
        RST 8
        DEFB 158

        RET

What you should see: your name on the prompt line, followed by the usual register display.

The repetition is the point of the exercise rather than a flaw in the answer. S4 and S11 are where you stop writing it out by hand.

3.2 - Three words, three lines

The only new thing here is the pair of control codes between the words. Both are needed: 13 alone leaves you on the same line, 10 alone steps diagonally down the screen.

        ORG 256

        LD A,'O'
        RST 8
        DEFB 158

        LD A,'N'
        RST 8
        DEFB 158

        LD A,'E'
        RST 8
        DEFB 158

        LD A,13
        RST 8
        DEFB 158

        LD A,10
        RST 8
        DEFB 158

        LD A,'T'
        RST 8
        DEFB 158

        LD A,'W'
        RST 8
        DEFB 158

        LD A,'O'
        RST 8
        DEFB 158

        LD A,13
        RST 8
        DEFB 158

        LD A,10
        RST 8
        DEFB 158

        LD A,'S'
        RST 8
        DEFB 158

        LD A,'I'
        RST 8
        DEFB 158

        LD A,'X'
        RST 8
        DEFB 158

        LD A,13
        RST 8
        DEFB 158

        LD A,10
        RST 8
        DEFB 158

        RET

What you should see:

>ONE
TWO
SIX

The break after the last word is optional. It is in this listing so the register display starts on a line of its own rather than butting up against SIX.

3.3 - Sums

Each answer needs two additions: the sum itself, then 48 to turn the result into the character for that digit.

        ORG 256

        LD A,3
        ADD A,4
        ADD A,48
        RST 8
        DEFB 158

        LD A,13
        RST 8
        DEFB 158

        LD A,10
        RST 8
        DEFB 158

        LD A,6
        ADD A,2
        ADD A,48
        RST 8
        DEFB 158

        LD A,13
        RST 8
        DEFB 158

        LD A,10
        RST 8
        DEFB 158

        LD A,1
        ADD A,7
        ADD A,48
        RST 8
        DEFB 158

        LD A,13
        RST 8
        DEFB 158

        LD A,10
        RST 8
        DEFB 158

        RET

What you should see:

>7
8
8

The exercise says to keep the answers to single digits, and the three sums it gives you all land between 0 and 9. Try one that does not. Change the first sum to 7 + 5 and you get < rather than a number, because 7 + 5 is 12, 12 + 48 is 60, and 60 is the code for <. That is not a mistake in your code. Twelve is two digits and no single character stands for it. Splitting a larger number into digits you can print is a later section.

S4. Changing what happens next

4.1 - Three arrows

Three bytes, three labels, one jump over the lot, then fetch and print each in turn.

        ORG 256

        JP start

left:   DEFB 91
right:  DEFB 93
up:     DEFB 94

start:  LD A,(left)
        RST 8
        DEFB 158

        LD A,(right)
        RST 8
        DEFB 158

        LD A,(up)
        RST 8
        DEFB 158

        RET

What you should see: the three arrows side by side on the prompt line, left, right, up.

The three DEFB lines sit together and one JP clears all of them, which is the habit worth forming. Data in a block, jumped over once.

4.2 - A pattern that never stops

Two characters per pass instead of one. There is no way out of this loop, so press Restart when you have seen enough.

        ORG 256

loop:   LD A,'*'
        RST 8
        DEFB 158

        LD A,'.'
        RST 8
        DEFB 158

        JP loop

What you should see: *.*.*.*. repeating, filling the whole screen.

Now the second half of the exercise. Add a full line break to the end of the loop, before the JP:

        LD A,13
        RST 8
        DEFB 158

        LD A,10
        RST 8
        DEFB 158

What changes: instead of filling the screen it becomes a column two characters wide down the left edge, scrolling upwards for as long as you let it run. Each pass now ends by returning to column 0 and moving down, so the pattern never spreads sideways.

4.3 and 4.4

These two have no listing to give you. 4.3 asks you to read the listing for a program of your own, and 4.4 asks you to predict an address before you check it. Both are answered by your own program and the assembler, and an answer here would be answering a different question from the one you set yourself.

S5. Jumping without saying where

5.1 - Every jump relative

Exercise 4.1 has one jump in it, so this is 4.1 with JP changed to JR. That is the whole edit.

        ORG 256

        JR start

left:   DEFB 91
right:  DEFB 93
up:     DEFB 94

start:  LD A,(left)
        RST 8
        DEFB 158

        LD A,(right)
        RST 8
        DEFB 158

        LD A,(up)
        RST 8
        DEFB 158

        RET

What you should see: the same three arrows as before.

In the listing the jump reads 0100 18 03. Two bytes where 4.1 had three, and the 03 is the distance from 0102 to 0105, where start: landed.

5.2 - Find the boundary

Three lines are enough to test each direction. Forwards:

        ORG 256
        JR far
        DEFS 127
far:    RET

That assembles. Change the 127 to 128 and it does not:

error: relative jump out of range (128)

Backwards, with the label above the jump:

        ORG 256
back:   DEFS 126
        JR back

That assembles too, and 127 does not:

error: relative jump out of range (-129)

The answer: 127 forwards and 128 back, exactly as the section says.

Note the numbers you type are not the numbers in the errors. DEFS 127 is the largest that works forwards, but backwards it is DEFS 126 - because the jump itself and the two bytes it occupies sit inside the gap in one direction and outside it in the other. Read the distance in the error message and the asymmetry comes out right: +127 and -128.

5.3 - Read the offsets

One of each, in a program that still ends.

        ORG 256

        JR next         ; to the very next instruction

next:   JR fwd          ; forwards

back:   LD A,'B'
        RST 8
        DEFB 158
        JR out

fwd:    LD A,'F'
        RST 8
        DEFB 158
        JR back         ; backwards

out:    RET

What you should see: FB on the prompt line. The F prints first because the jumps go forward to fwd: before coming back to back:.

Working the offsets out by hand, each measured from the address of the instruction after the jump:

Jump At Next Target Offset Byte
JR next 0100 0102 0102 0 00
JR fwd 0102 0104 010a +6 06
JR out 0108 010a 0110 +6 06
JR back 010e 0110 0104 -12 f4

f4 is 244, and 244 - 256 is -12. That is how the Z80 writes a negative byte, and it is why a backward jump looks like a large number in the listing.

A jump to the very next instruction has an offset of zero and does nothing whatsoever. It is worth assembling once just to see 18 00 and understand that JR really is only ever "move PC by this much".

5.4 - Move it

        ORG 256

        LD A,'J'
        RST 8
        DEFB 158

        JR mid
        DEFB 118

mid:    LD A,'P'
        RST 8
        DEFB 158

        JP done
        DEFB 118

done:   RET

At ORG 256 this prints JP and returns normally, with PC reading 0769 like every well-behaved program so far.

Now change only the ORG to 512. The JP is the one that breaks - and the interesting part is that you cannot tell from the screen. It still prints JP, because both characters are printed before the broken jump is reached. The only evidence is PC, which now reads 020F instead of 0769.

done: really sits at 010F. Told the program would be at 512, the assembler wrote 020F, which is 256 too high, and that is where the program ended up. Take the real address and add 256 and you can predict the number before you run it.

This is worth keeping in mind well beyond this exercise. A jump to the wrong place does not always announce itself. Sometimes the output looks perfect and only the register line knows.

S6. Loops that stop

6.1 - Count the alphabet forwards

The backwards version with INC A in place of DEC A, and 'A' as the starting letter.

        ORG 256

        LD C,26
        LD A,'A'

loop:   RST 8
        DEFB 158       ; ZOUTC
        INC A
        DEC C
        JR NZ,loop

        RET

What you should see:

>ABCDEFGHIJKLMNOPQRSTUVWXYZ

A finishes at 5B, one past 'Z', for the same reason the backwards version finishes at 40: the last INC A runs after the last letter has printed.

DEC C stays immediately before the jump. That is the whole reason this works and the swapped version in "Change one thing" does not.

6.2 - A row of anything

The two numbers to edit are on the first two lines and nowhere else.

        ORG 256

        LD C,40         ; how many
        LD A,'-'        ; which character

loop:   RST 8
        DEFB 158       ; ZOUTC
        DEC C
        JR NZ,loop

        RET

What you should see: a row of dashes - and the fortieth one on a line of its own underneath.

That is not a bug in your loop. The text screen is 40 columns wide and the > prompt is sitting in the first of them, so only 39 dashes fit on that line and the last one wraps. Ask for 39 and you get one clean row. It is worth knowing now, because every later section that draws something has to think about where the edge of the screen is.

6.3 - Two loops

The exercise asks you to find a second register that survives a print. Test it rather than guess - three lines will do:

        ORG 256
        LD B,42
        LD C,7
        LD A,'X'
        RST 8
        DEFB 158
        RET

BC comes back as 2A07. 2A is 42 and 07 is 7, so both halves are intact and B is safe to count in.

Now nest one loop inside the other:

        ORG 256

        LD B,5          ; rows

row:    LD C,10         ; columns
        LD A,'*'

col:    RST 8
        DEFB 158       ; ZOUTC
        DEC C
        JR NZ,col

        LD A,13
        RST 8
        DEFB 158
        LD A,10
        RST 8
        DEFB 158

        DEC B
        JR NZ,row

        RET

What you should see:

>**********
**********
**********
**********
**********

Two things make this work. LD C,10 is inside the outer loop, so the column count is reloaded at the start of every row - put it above row: and you get one row and then four empty ones. And each DEC still sits immediately before the jump that tests it, with the two print calls for the line break in between doing no harm because they leave both counters alone.

6.4 - Read the flags

Z set - any result that comes out zero:

        ORG 256
        LD A,5
        SUB 5           ; 5 - 5 = 0
        RET

Flags read 01000010, with the Z column at 1.

Z clear - the same program with a different number:

        ORG 256
        LD A,5
        SUB 3           ; 5 - 3 = 2
        RET

Flags read 00000010. Only the Z column moved.

C set is the part that sends you looking. SUB is the instruction you want, and the carry flag is set by a subtraction that has to borrow - that is, one where you take a bigger number from a smaller one:

        ORG 256
        LD A,1
        SUB 2           ; 1 - 2 borrows
        RET

A comes back as FF and the flags read 10111011. The C column is 1, and so is S, because FF has its top bit set and the processor reads that as a negative number. 1 - 2 is -1, and FF is how a single byte writes -1.

Three programs, three different flag bytes, and each difference traceable to the one thing that changed. That is the useful habit here: change one number, watch one column move.

S7. Doing the same job from several places

7.1 - A newline subroutine

Two subroutines rather than one. print puts the character in A on screen, newline moves to the start of the next line.

        ORG 256

        LD A,'O'
        CALL print
        LD A,'N'
        CALL print
        LD A,'E'
        CALL print
        CALL newline

        LD A,'T'
        CALL print
        LD A,'W'
        CALL print
        LD A,'O'
        CALL print
        CALL newline

        LD A,'S'
        CALL print
        LD A,'I'
        CALL print
        LD A,'X'
        CALL print
        CALL newline

        RET

print:  RST 8
        DEFB 158       ; ZOUTC
        RET

newline: LD A,13
        RST 8
        DEFB 158
        LD A,10
        RST 8
        DEFB 158
        RET

What you should see:

>ONE
TWO
SIX

Compare it with S3's version of the same output. That one repeated RST 8 and DEFB 158 fifteen times. This one has them twice, in one place each, and every line break is a single line of source.

7.2 - Print a digit

        ORG 256

        LD A,7
        CALL digit
        LD A,0
        CALL digit
        LD A,9
        CALL digit

        RET

digit:  ADD A,48        ; 0-9 becomes '0'-'9'
        RST 8
        DEFB 158       ; ZOUTC
        RET

What you should see: 709, and A reading 39 at the end.

That 39 is the answer to the second half of the question. The subroutine leaves the character in A, not the number it was given - 39 is '9', not 9. Whether that matters depends entirely on the caller. Here it does not, because each call loads A fresh. In a loop that counted in A it would be a bug, and a quiet one, because the first pass would look perfect.

This is worth naming: a subroutine that alters a register the caller cares about has a cost, and the cost belongs in a comment at the top of it.

7.3 - A box

The straight pieces in the graphics range sit at the edges of the character square, so each side of the box needs its own code:

Piece Code
top-left corner 173
bottom-left corner 189
top rule 177
bottom rule 161
left rule 181
right rule 167
solid block 160

The two right-hand corners are the catch. The set has them, but they print their bar and no upright, so a rectangle built from them comes out with three sides. The solid block stands in perfectly well:

        ORG 256

        CALL newline    ; start at column 0

        LD A,173        ; top-left
        CALL print
        LD C,8
        LD A,177        ; top rule
        CALL row
        LD A,160        ; top-right
        CALL print
        CALL newline

        LD B,3          ; three middle rows

mid:    LD A,181        ; left rule
        CALL print
        LD C,8
        LD A,32         ; blank inside
        CALL row
        LD A,167        ; right rule
        CALL print
        CALL newline
        DEC B
        JR NZ,mid

        LD A,189        ; bottom-left
        CALL print
        LD C,8
        LD A,161        ; bottom rule
        CALL row
        LD A,160        ; bottom-right
        CALL print
        CALL newline

        RET

row:    RST 8           ; print A, C times
        DEFB 158
        DEC C
        JR NZ,row
        RET

print:  RST 8
        DEFB 158
        RET

newline: LD A,13
        RST 8
        DEFB 158
        LD A,10
        RST 8
        DEFB 158
        RET

What you should see: a rectangle ten characters wide and five tall, with slightly heavier blocks at the two right-hand corners.

If you would rather not hunt for the pieces at all, a frame made entirely of code 160 works and looks deliberate:

        LD C,10
        LD A,160
        CALL row

for the top and bottom, with a single 160 at each end of the middle rows.

row is the subroutine the exercise asks for, and it is worth looking at on its own. It prints whatever is in A, C times. It does not know or care what the character is, which is why the same four lines draw the top edge, the bottom edge and the blank interior.

7.4 - Watch the stack

Capture SP on the way down and keep each reading in a different register pair, so all three survive to the end.

        ORG 256

        LD HL,0
        ADD HL,SP       ; level 0, before any CALL
        LD B,H
        LD C,L

        CALL one
        RET

one:    LD HL,0
        ADD HL,SP       ; level 1
        LD D,H
        LD E,L
        CALL two
        RET

two:    CALL three
        RET

three:  LD HL,0
        ADD HL,SP       ; level 3
        RET

What you should see: BC = FCED, DE = FCEB, HL = FCE7.

FCED before any call, FCEB one call deep, FCE7 three calls deep. Two bytes a call, every time, which is the size of the address CALL puts there.

So level four is FCE5, and you can say so before running it. That is the whole point of the exercise: the stack is not mysterious, it is arithmetic.

7.5 - Break it deliberately

        ORG 256

        LD A,'*'
        CALL bad
        RET

bad:    PUSH BC
        RST 8
        DEFB 158       ; ZOUTC
        RET             ; no POP - RET takes BC off instead

What you should see: the asterisk prints, and then PC reads 3D41.

Look at BC in the same register line. It reads 3D41 too.

That is the whole lesson in one comparison. PUSH BC put the contents of BC on the stack. The RET took the top two bytes off and jumped to them, and the top two bytes were BC, so the processor went to address 3D41 - a place with no connection to your program at all. It did not know it had done anything unusual.

PUSH and POP come in pairs. When they do not, RET is where it shows up, and PC is where you see it.

S8. Reading the keyboard

8.1 - Echo until Escape

The wait-for-a-key call hands the code back in A, which is exactly where CP wants it and exactly where the print call wants it, so almost nothing has to be moved about.

        ORG 256

loop:   RST 8
        DEFB 156        ; ZKEYIN - wait for a key
        CP 27           ; Escape?
        JR Z,done

        RST 8
        DEFB 158        ; ZOUTC
        JR loop

done:   RET

What you should see: whatever you type appearing as you type it, and the prompt coming back the moment you press Escape. A finishes at 1B, which is 27 - the Escape you pressed, still sitting there.

Note that CP leaves A alone. That is the whole reason this is so short: the comparison does not disturb the character, so the same value goes straight on to the print call.

8.2 - Yes or no

Two comparisons, and a jump back to the top for anything that matches neither.

        ORG 256

again:  RST 8
        DEFB 156        ; ZKEYIN - wait for a key

        CP 'Y'
        JR Z,yes
        CP 'N'
        JR Z,no
        JR again        ; anything else - keep waiting

yes:    LD A,'Y'
        CALL print
        LD A,'E'
        CALL print
        LD A,'S'
        CALL print
        RET

no:     LD A,'N'
        CALL print
        LD A,'O'
        CALL print
        RET

print:  RST 8
        DEFB 158        ; ZOUTC
        RET

What you should see: nothing at all until you press Y or N. Press a few other keys first and they are silently ignored, which is what the exercise asked for.

The two subroutines are placed after the RET that ends the yes branch, so execution cannot fall into them - the S7 rule, still earning its keep.

8.3 - What code is that key?

This is the one that needs a condition the section did not give you. To peel tens off a number you have to keep asking "is there still ten or more left?", and CP with JR Z only answers "are these two equal?".

The condition you want is JR C, which jumps when the carry flag is set. CP does a subtraction it throws away, and a subtraction that has to borrow sets carry - so after CP 10, carry is set exactly when A was less than ten. That is the test.

        ORG 256

        RST 8
        DEFB 156        ; ZKEYIN - the code arrives in A

        LD C,0          ; count of tens

tens:   CP 10
        JR C,units      ; A is under ten - the rest is the units digit
        SUB 10
        INC C
        JR tens

units:  LD B,A          ; keep the units digit somewhere safe

        LD A,C          ; print the tens digit
        ADD A,48
        CALL print

        LD A,B          ; and the units
        ADD A,48
        CALL print

        RET

print:  RST 8
        DEFB 158        ; ZOUTC
        RET

What you should see: press A and you get 65. The register line shows BC as 0506 - 05 is the units digit and 06 the tens, which is a neat way to check the split worked before you trust the printing.

LD B,A is there because A has to carry two different things in turn. Peeling the tens off leaves the units in A; then A is needed for the tens digit; so the units go into B for safekeeping. That kind of shuffling is most of what arithmetic in assembly feels like.

Now the answers the exercise sends you looking for:

Key Code
Enter 13
Space 32
Escape 27
Left arrow 91
Right arrow 93
Up arrow 94
Down arrow 10

Three of the arrows are the interesting part. 91, 93 and 94 are the codes of the left, right and up arrow characters in Appendix I - so press the right arrow, print what you get, and a right arrow appears on screen. The key and the picture are the same number.

The down arrow is the surprise. There is no down-arrow character, and the key gives you 10, which is the line feed from S3. Print it and the cursor moves down a line instead of drawing anything. That is worth knowing before you build a game around the arrow keys.

8.4 - A menu

        ORG 256

again:  RST 8
        DEFB 156        ; ZKEYIN

        CP '1'
        JR Z,one
        CP '2'
        JR Z,two
        CP '3'
        JR Z,three
        JR again

one:    LD A,'A'
        JR show
two:    LD A,'B'
        JR show
three:  LD A,'C'

show:   RST 8
        DEFB 158        ; ZOUTC
        RET

What you should see: nothing for any other key, then A, B or C.

Note CP '1' and not CP 1. The key hands back the character code 49, not the number one. This is the S3 distinction between a number and the character for it, arriving from the other direction: here the conversion has already been done for you, which is why the section says never to add 48 to a key code.

The three branches all join at show, so the printing exists once. With three options that hardly matters; with ten it is the difference between a readable program and a wall.

8.5 - Prove the register problem

Two programs differing in one byte.

        ORG 256

        LD C,5

loop:   LD A,'*'
        RST 8
        DEFB 158        ; ZOUTC
        RST 8
        DEFB 156        ; ZKEYIN - waits, but leaves C alone
        DEC C
        JR NZ,loop

        RET

What you should see: five asterisks, one for each key you press, and C finishing at 00.

Now change that 156 to 181:

        RST 8
        DEFB 181        ; ZKSCAN - destroys C

What you should see: the whole screen filling with asterisks and no sign of stopping. Press Restart.

The count never reaches zero. Every lap, the keyboard scan puts its own value into C, the DEC C takes one off that, and the result is never zero - so the JR NZ is always taken. LD C,5 was overwritten before it was ever consulted.

LD A,'*' is inside the loop in both versions, and it has to be: call 156 hands the key it read back in A, so the character would be gone by the second pass otherwise. Two different calls, two different things to be careful of, and the only way to know which is which is to have it written down. That is what the section means by saying you cannot assume a register survives a call.

S9. Reading a whole line

Every program here uses the same two helpers, so they are written out once in 9.1 and referred to afterwards. puts prints a zero-terminated run of bytes - the walk from the section, turned into a subroutine. newline is the one from S7.

9.1 - Label the echo

The catch is where the label goes. Print You typed: before the read and it lands on the row the read copies, so it comes back as though the user typed it. It has to go after.

        ORG 256

        CALL newline    ; a clean row to read from
        LD DE,buffer
        RST 8
        DEFB 157        ; ZGETLN

        CALL newline    ; the label goes AFTER the read
        LD HL,message
        CALL puts

        CALL newline
        LD HL,buffer
        CALL puts

        RET

puts:   LD A,(HL)       ; print a zero-terminated run of bytes
        CP 0
        JR Z,pdone
        RST 8
        DEFB 158
        INC HL
        JR puts
pdone:  RET

newline: LD A,13
        RST 8
        DEFB 158
        LD A,10
        RST 8
        DEFB 158
        RET

message: DEFB 'Y','o','u',' ','t','y','p','e','d',':',0
buffer:  DEFS 64

What you should see: type HELLO and you get

You typed:
HELLO

Note message and buffer sitting together after the RET, and message ending in a zero so that puts knows where to stop. That zero is not decoration - it is the only thing marking the end.

9.2 - How long was it?

Count to the first space, not to the zero. Counting to the zero gives you the width of the screen row every time, however little was typed.

        LD HL,buffer
        LD C,0          ; characters counted
count:  LD A,(HL)
        CP 32           ; a space ends the typed text - counting to the
        JR Z,shown      ; zero would give the line width every time
        CP 0
        JR Z,shown
        INC C
        INC HL
        JR count

shown:  LD A,C
        CALL number
        RET

number: LD C,0          ; print A as two digits - from 8.3
tens:   CP 10
        JR C,units
        SUB 10
        INC C
        JR tens
units:  LD B,A
        LD A,C
        ADD A,48
        RST 8
        DEFB 158
        LD A,B
        ADD A,48
        RST 8
        DEFB 158
        RET

What you should see: type HELLO and you get 05.

Both tests are needed. The space catches a normal line; the zero catches the case where the typed text fills the row and there is no space to find.

9.3 - Backwards

"Find the end" is the whole exercise, and the end you want is the first space, for the reason 9.2 gives. Count the characters on the way out, then step back exactly that many times.

        LD HL,buffer    ; walk forward to the end, counting as we go
        LD C,0
fwd:    LD A,(HL)
        CP 32           ; a space ends the typed text
        JR Z,back
        CP 0
        JR Z,back
        INC C
        INC HL
        JR fwd

back:   LD A,C          ; nothing typed at all?
        CP 0
        JR Z,done

rev:    DEC HL          ; now step back exactly C times
        LD A,(HL)
        RST 8
        DEFB 158
        DEC C           ; last thing before the jump
        JR NZ,rev

done:   RET

What you should see: type STRESSED and you get DESSERTS.

Counting rather than comparing addresses is what keeps this inside what you know. HL holds a two-byte address and you have no way yet to ask whether two of those are equal, but you can certainly count to eight and back.

DEC HL comes before the fetch, not after. When the forward walk stops, HL is pointing at the space past the last character, so the first thing to do is step back onto the character itself.

9.4 - Just the letters

Two comparisons make a range. Below 'A' is out, and so is anything from the character after 'Z' upwards.

        LD HL,buffer
loop:   LD A,(HL)
        CP 0
        JR Z,done

        CP 65           ; below 'A'?
        JR C,skip
        CP 91           ; above 'Z'?
        JR NC,skip

        RST 8
        DEFB 158

skip:   INC HL
        JR loop

done:   RET

What you should see: type AB12cd EF and you get ABCDEF.

Two things worth noticing in that result. The digits and the space are gone, as asked. And the cd you typed in lower case came back as CD - the letters arrive as capitals, exactly as S8 said, so a filter for 65 to 90 catches everything a person typed rather than half of it.

CP 91 and not CP 90. The test is "is it above Z", and the smallest code that is above Z is 91. Off-by-one errors in range tests are the most common bug in this kind of loop, and the way to avoid them is to write the boundary you mean rather than the boundary you are thinking of.

9.5 - A command

        LD HL,buffer
        LD A,(HL)
        CP 'Y'          ; letter keys arrive as capitals - S8
        JR NZ,done
        INC HL
        LD A,(HL)
        CP 'E'          ; the second character too
        JR NZ,done

        LD A,'Y'
        RST 8
        DEFB 158
        LD A,'E'
        RST 8
        DEFB 158
        LD A,'S'
        RST 8
        DEFB 158

done:   RET

What you should see: type YES and you get YES. Type anything else and you get nothing.

Now count what that cost. Two characters took two fetches, two comparisons and two conditional jumps, and every one of them has to be written out by hand. Three characters is three of each; a word is one per letter; a list of ten commands is that again, ten times over. Nothing about it is difficult and all of it is tedious, which is the exact shape of a job that wants a loop and a table instead. That is a later section, and this is why it exists.

S10. Numbers bigger than a byte

Every line-drawing program here needs the four style bytes set first, so that block is written out in 10.1 and left implied afterwards.

10.1 - A different line

        ORG 256

        LD A,255
        LD (64424),A    ; solid line
        LD A,0
        LD (64425),A
        LD (64426),A
        LD (64427),A

        LD IX,0
        LD IY,0

        LD HL,100
        LD (64406),HL   ; end X = 100
        LD HL,200
        LD (64408),HL   ; end Y = 200

        RST 8
        DEFB 200        ; ZDRWTO

        RET

What you should see: a steep line from the bottom-left corner, twice as tall as it is wide.

That is the prediction to make before running it: 100 across and 200 up is a slope of two, so the line is steeper than the section's 45-degree one. And it goes up, not down, because 0,0 is the bottom left.

Two separate LD HL loads, because end X and end Y are different numbers now. The section's program got away with one because both were 200.

10.2 - A square

Four edges, four blocks that differ only in their numbers - which is exactly the shape that wants a subroutine.

        ORG 256

        LD A,255
        LD (64424),A
        LD A,0
        LD (64425),A
        LD (64426),A
        LD (64427),A

        LD IX,50        ; bottom edge
        LD IY,50
        LD HL,150
        LD DE,50
        CALL drawto

        LD IX,150       ; right edge
        LD IY,50
        LD HL,150
        LD DE,150
        CALL drawto

        LD IX,150       ; top edge
        LD IY,150
        LD HL,50
        LD DE,150
        CALL drawto

        LD IX,50        ; left edge
        LD IY,150
        LD HL,50
        LD DE,50
        CALL drawto

        RET

drawto: LD (64406),HL   ; end X, both bytes
        EX DE,HL        ; end Y was in DE - one byte to swap them
        LD (64408),HL
        RST 8
        DEFB 200        ; ZDRWTO
        RET

What you should see: a square from 50,50 to 150,150, drawn straight over the boot text.

The subroutine is the interesting part, and it is where EX DE,HL stops being a curiosity. The caller puts end X in HL and end Y in DE, because those are the two pairs you can swap in a single byte. drawto stores HL, swaps, and stores again. Copying with LD H,D / LD L,E would work and cost one byte more; nothing here needs DE afterwards, so the swap is free.

Each edge sets IX and IY again rather than trusting them to survive the call. That is the S8 habit applied to a different call: assume nothing you were not told.

10.3 - Three ways to move a pair

Three programs, each starting with DE = 1234h and HL = 7654h.

        EX DE,HL        ; DE = 7654h, HL = 1234h
        LD H,D
        LD L,E          ; DE = 1234h, HL = 1234h
        PUSH DE
        POP HL          ; DE = 1234h, HL = 1234h
Method Bytes DE afterwards
EX DE,HL 1 swapped - it holds what HL had
LD H,D / LD L,E 2 unchanged
PUSH DE / POP HL 2 unchanged

So the answer to the exercise is: only EX costs a single byte, and it is the only one of the three that does not leave DE alone. Two of them copy, one swaps, and knowing which you want is the whole of the decision.

The stack version is the one to be careful with. It works, and it reads pleasantly, but it moves SP twice and it is two bytes - so it earns its place only when you were pushing DE anyway for some other reason.

10.4 - Store and fetch

        ORG 256

        LD HL,4660      ; 1234h
        LD (store),HL   ; two bytes, low one first

        LD DE,(store)   ; read it back into a different pair

        LD A,(store)    ; the byte at the lower address
        LD B,A
        LD A,(store+1)  ; and the one after it
        LD C,A

        RET

store:  DEFS 2

What you should see: DE reads 1234 - the value survived the round trip into memory and back out into a different pair. And BC reads 3412.

That 3412 is the part worth staring at. B holds the byte at the lower address and it is 34, the bottom half of 1234h. C holds the byte after it and it is 12, the top half. The low byte really does go first, which is the little-endian order S4 introduced, seen here in your own storage rather than in a listing.

LD DE,(store) is worth noticing too. You have used LD HL,(addr); the same instruction exists for DE and BC, so a value can be fetched straight into whichever pair needs it.

10.5 - The overflow

Two lines, and the second one is wrong for exactly one reason.

        ORG 256

        LD A,255
        LD (64424),A
        LD A,0
        LD (64425),A
        LD (64426),A
        LD (64427),A

        LD HL,200       ; the line that is right
        LD (64406),HL
        LD HL,100
        LD (64408),HL
        LD IX,0
        LD IY,0
        RST 8
        DEFB 200

        LD HL,300       ; now leave a 1 in the high byte of end X
        LD (64406),HL
        LD A,200        ; and write only the low byte
        LD (64406),A    ; end X is 456, not 200

        LD HL,50        ; a different height so the two do not overlap
        LD (64408),HL
        LD IX,0
        LD IY,0
        RST 8
        DEFB 200

        LD HL,(64406)   ; what end X really held
        RET

What you should see: two lines from the bottom-left corner, one clearly steeper than the other. HL finishes at 01C8, which is 456.

The exercise's warning is the important part, and it is worth seeing why. Choose 200,200 for the good line and the bad one becomes 456,456 - and both of those sit on the same 45-degree line through the origin, so the two draw on top of each other and the screen shows one line. The bug is invisible.

That is not a quirk of this exercise. It is what makes the one-byte store dangerous: the wrong value often produces output that looks plausible, and the only thing that tells you the truth is reading the coordinate back.

S11. Text of your own

puts from the section does the printing in every one of these, so it is written out in 11.1 and taken as read afterwards.

11.1 - Three messages

        ORG 256

        LD HL,msg1
        CALL puts
        CALL newline
        LD HL,msg2
        CALL puts
        CALL newline
        LD HL,msg3
        CALL puts
        CALL newline

        RET

puts:   LD A,(HL)
        CP 0
        RET Z
        RST 8
        DEFB 158
        INC HL
        JR puts

newline: LD A,13
        RST 8
        DEFB 158
        LD A,10
        RST 8
        DEFB 158
        RET

msg1:   DEFB 'ONE',0
msg2:   DEFB 'TWO',0
msg3:   DEFB 'THREE',0

What you should see:

>ONE
TWO
THREE

Three messages, six lines of main program. Take the CALL newline lines out and you get ONETWOTHREE on one row, which is the answer to the first half of the exercise.

11.2 - Count to five in memory

Two loops and one pointer each way: one to write the numbers, one to read them back.

        ORG 256

        LD HL,store     ; write 1 to 5 through a pointer
        LD A,1
        LD E,5

fill:   LD (HL),A
        INC HL
        INC A
        DEC E
        JR NZ,fill

        LD HL,store     ; now read them back and print each as a digit
        LD E,5

show:   LD A,(HL)
        ADD A,48        ; the number becomes its character - S3
        RST 8
        DEFB 158
        INC HL
        DEC E
        JR NZ,show

        RET

store:  DEFS 5

What you should see: 12345.

E is the counter rather than C for no reason except that C is free for something else; either works, and both survive the print call. INC A inside the first loop is what makes the stored values 1 to 5 rather than five copies of the same number.

The ADD A,48 in the second loop is the whole reason the exercise exists. The bytes in memory are 1 to 5. What goes on screen is 49 to 53. They are not the same thing and the loop has to convert.

11.3 - What does this do?

        LD HL,40000
        LD A,(HL)
        INC HL
        LD A,(HL)

Line by line: HL is loaded with the number 40000, which is an address rather than a value. LD A,(HL) fetches the byte living at 40000. INC HL makes HL hold 40001 instead. The second LD A,(HL) fetches the byte at 40001, overwriting the first one - so A finishes holding the second byte and the first is gone unless you kept it.

What you should see when you run it: keep both bytes, in B and C say, and they come back as FF and FF. The same probe over the next two bytes gives FF there too.

That is the part worth understanding. The two loads read two different addresses, which is what INC HL did - but reading different addresses does not mean getting different values. That stretch of memory has nothing in it, and nothing in it reads as FF.

Now point the same four lines at your own text:

        LD HL,msg
        LD A,(HL)
        LD B,A
        INC HL
        LD A,(HL)
        LD C,A
        RET

msg:    DEFB 'HELLO',0

BC comes back as 4845 - 48 is H and 45 is E. Same four lines, same mechanism, and now the difference is obvious because there is actually something there to differ.

11.4 - Length of a message

; length of the zero-terminated string at HL, returned in A. HL preserved.
strlen: PUSH HL
        LD C,0
slen:   LD A,(HL)
        CP 0
        JR Z,slend
        INC C
        INC HL
        JR slen
slend:  LD A,C
        POP HL
        RET

What you should see: DEFB 'HI',0 gives 2, and DEFB 'HELLO WORLD',0 gives 11. Neither counts the zero.

The PUSH HL / POP HL pair is the part worth copying. Walking a string moves the pointer, and a caller that asked only for a length will not expect its pointer to have moved - so the routine puts it back. That costs two bytes and saves the caller from a bug it would struggle to find. 11.5 depends on it directly.

LD A,C at the end is there because the exercise asked for the answer in A. Counting in C and copying at the end is easier than counting in A, which has to keep fetching the character.

11.5 - Print it backwards

Get the length, step forward that many times to reach the terminator, then walk back.

        ORG 256

        LD HL,msg
        CALL strlen     ; A = length, HL still at the start
        LD C,A          ; how many to print
        CP 0
        RET Z

        LD B,A          ; step HL forward to the terminator
adv:    INC HL
        DEC B
        JR NZ,adv

rev:    DEC HL          ; then walk back, printing as we go
        LD A,(HL)
        RST 8
        DEFB 158
        DEC C
        JR NZ,rev

        RET

What you should see: STRESSED comes out as DESSERTS.

Two counters, and they are doing different jobs. B walks the pointer to the end. C counts the characters on the way back. Both start as the length, and using one for both would leave you at zero before the printing began.

CP 0 / RET Z right after strlen is not decoration. An empty message would otherwise send DEC B round 256 times and print whatever it found.

11.6 - Two pointers at once

        ORG 256

        LD HL,source    ; HL reads, DE writes
        LD DE,dest

copy:   LD A,(HL)
        LD (DE),A       ; the terminator gets copied too
        CP 0
        JR Z,copied
        INC HL
        INC DE
        JR copy

copied: LD HL,dest      ; print the copy to prove it worked
        CALL puts
        RET

source: DEFB 'COPIED OK',0
dest:   DEFS 32

What you should see: COPIED OK, printed from dest rather than from source.

The order of those four lines in the loop is the whole exercise. The byte is stored before the test for zero, so the terminator gets copied along with everything else - and without it dest would have no end and puts would run off into whatever follows. Test first and you copy the text but not its terminator, which is a bug that works right up until it does not.

This is also the first program with two pointers moving at once, and it is why DE exists. HL does the reading because LD A,(HL) is the general form; DE does the writing because LD (DE),A is one of the two places DE is allowed to point. Neither could be a plain LD A,(addr), because both addresses change on every pass.

S12. The stack

12.1 - Watch it move

SP cannot be read directly, so every reading here goes through the ADD HL,SP trick from the section, and each one is parked in a different pair so they all survive to the end.

        ORG 256

        LD HL,0
        ADD HL,SP       ; SP before any call
        LD B,H
        LD C,L

        CALL sub
        RET

sub:    LD HL,0
        ADD HL,SP       ; SP inside the subroutine
        LD D,H
        LD E,L
        RET

What you should see: BC = FCED and DE = FCEB. Two bytes lower inside the call, which is the return address the CALL put there.

Now add a PUSH AF at the top of sub and a POP AF before its RET, and take the reading between them:

What changes: DE reads FCE9. Four bytes below where you started - two for the call, two for the push. Nothing mysterious happens on the stack; it is arithmetic you can do on paper before you run it.

12.2 - A safe subroutine

        ORG 256

        LD HL,1111h     ; three known values
        LD DE,2222h
        LD A,33h

        CALL safe

        LD B,A          ; keep A where the register line shows it
        RET

; uses HL, DE and AF, and gives all three back
safe:   PUSH AF
        PUSH DE
        PUSH HL

        LD HL,0         ; scribble on all three
        LD DE,0
        LD A,0

        POP HL          ; reverse order coming off
        POP DE
        POP AF
        RET

What you should see: HL = 1111, DE = 2222, and A = 33 - all three exactly as they went in, despite the routine setting every one of them to zero in the middle.

Three known values chosen to be instantly recognisable is the whole trick to testing this. If they came back as 1111, 2222 and 33 then nothing was lost; if any one of them is 0000 you know which push is missing, and if two of them have traded places you know the pops are in the wrong order.

LD B,A at the end is only there because the register display shows BC but not A separately from the rest of the run - A is on the display, but parking a copy in B makes the before-and-after comparison easier to read.

12.3 - Wrong order on purpose

Take 12.2 and reverse two of the pops:

unsafe: PUSH AF
        PUSH DE
        PUSH HL

        LD HL,0
        LD DE,0
        LD A,0

        POP DE          ; WRONG - these two are swapped
        POP HL
        POP AF
        RET

What you should see: HL = 2222 and DE = 1111. The two values have traded places. A is still 33, because AF was pushed first and popped last and its position in the order was never disturbed.

This is predictable on paper, and predicting it before running is the exercise. HL went on last, so it is on top; the first POP takes it, whatever register you name. Naming DE does not fetch the DE value - it fetches whatever is on top and puts it in DE. The stack has no idea what any of those bytes were for.

That is why this bug is nastier than it looks. Nothing fails, nothing errors, and the program runs to completion with two registers quietly holding each other's contents.

12.4 - Find the wall

This one has no listing to print, because the answer is the measurement and the measurement is yours to make. The program shape is the only part worth giving:

        ORG 256

        LD HL,1234h     ; a sentinel, so you can tell a clean run
        LD B,<pushes>

fill:   PUSH HL
        DEC B
        JR NZ,fill

        LD HL,0
        LD B,<pushes>

drain:  POP HL
        DEC B
        JR NZ,drain

        RET

Each push is two bytes, so the byte count is twice <pushes>. A clean run comes back with HL reading 1234 - the sentinel went on the stack, came off again, and the program returned normally.

Two pieces of advice, both of which will save you time.

Write the count in one place. You are going to change it a dozen times, and a program with the number in two places will eventually have two different numbers in it and waste a run.

Look at the whole screen, not just your sentinel. "Did my program come back with 1234?" is the obvious test and it is not sufficient. There is a range of depths where the answer is yes and the machine is nevertheless in trouble, and the only sign of it is something appearing on screen after your program has finished. Check what the screen looks like below your register line, every time.

12.5 - Protect a call you did not write

The wrapper is six instructions around the call, and it turns something unusable into something you can put in a loop.

        ORG 256

        LD C,5          ; a counter the keyboard scan would destroy

loop:   LD A,'*'
        RST 8
        DEFB 158        ; ZOUTC

        CALL getkey     ; the wrapper, not the raw call

        DEC C
        JR NZ,loop

        RET

; read the keyboard - key code in A, everything else exactly as found
getkey: PUSH BC
        PUSH DE
        PUSH HL
        RST 8
        DEFB 181        ; ZKSCAN - destroys BC, DE and HL
        POP HL
        POP DE
        POP BC
        RET

What you should see: exactly five asterisks, C finishing at 00, and DE and HL back at the values every other program leaves them at.

Take the wrapper out and call 181 directly in that loop and the screen fills with asterisks and never stops - which is S8's exercise 8.5, and the reason this one exists. The counter is reset by the call on every pass, so it never reaches zero.

A is deliberately not protected. It is the one register the call is there to give you something in, so saving and restoring it would throw away the answer. That is the general shape of a wrapper: push everything the call damages except what you actually wanted from it.

Six extra bytes, and a call that could not previously be used inside a counted loop now can. This is the single most useful thing in the section.

S13. Arrays and lookup tables

13.1 - Reach the fifth

        ORG 256

        LD HL,eight
        LD BC,4         ; the fifth element is at index 4
        ADD HL,BC
        LD A,(HL)       ; should be 55

        RET

eight:  DEFB 11,22,33,44,55,66,77,88

What you should see: A = 37, which is 55.

Values chosen so that a wrong answer is obviously wrong. With 11, 22, 33 and so on, an off-by-one lands on 44 or 66 and you can see immediately which way you went. An array of 1, 2, 3, 4, 5 would have told you far less.

LD BC,4 and not LD B,4. That is the whole trap of the section and the reason to write the pair form every single time.

13.2 - Sum of ten

        ORG 256

        LD HL,ten
        LD B,10
        LD A,0

sum:    ADD A,(HL)
        INC HL
        DJNZ sum

        RET

ten:    DEFB 1,2,3,4,5,6,7,8,9,10

What you should see: A = 37, which is 55 - and 1+2+…+10 is 55.

Now the second half. Change the data to ten thirties, so the total should be 300:

ten:    DEFB 30,30,30,30,30,30,30,30,30,30

What you get instead: A = 2C, which is 44. 300 does not fit in a byte, so it wrapped: 300 - 256 is 44.

Nothing warns you about this. The loop ran ten times, added ten values correctly, and handed back an answer that is wrong by exactly 256. If you need totals past 255 the running total has to live in a register pair, and adding a byte to a pair is a later problem than this section.

13.3 - Digits without arithmetic

; a number 0-9 in A becomes its character, by table rather than arithmetic.
digit:  LD C,A
        LD B,0
        LD HL,digits
        ADD HL,BC
        LD A,(HL)
        RET

digits: DEFB '0','1','2','3','4','5','6','7','8','9'

What you should see: calling it with 7 and then 0 prints 70.

And the comment the exercise asks for: ADD A,48 is the right answer here. One instruction against six, and no table to keep in step with anything. The table is not better; it is a demonstration.

What the table is better at is a mapping with no formula behind it. Digits happen to be consecutive, so arithmetic works. Sprite frame addresses, colour numbers for a palette, the letters on a keyboard row, the sequence a monster walks in - none of those have a formula, and for those the table is the only sensible answer. Reach for arithmetic when the mapping is arithmetic and a table when it is not.

13.4 - A grid

; value at row D, column E of a three-wide grid.
; No multiply instruction, so row*3 is done by adding three, D times.
at:     LD HL,grid
        LD A,D
        CP 0
        JR Z,addcol

rowlp:  LD BC,3
        ADD HL,BC
        DEC A
        JR NZ,rowlp

addcol: LD C,E
        LD B,0
        ADD HL,BC
        LD A,(HL)
        RET

grid:   DEFB 1,2,3
        DEFB 4,5,6
        DEFB 7,8,9

What you should see: row 2, column 1 gives A = 08.

Nine instructions, one of them a loop. The CP 0 / JR Z at the top is not optional: row 0 must skip the loop entirely, and without that test DEC A on zero would send it round 256 times.

13.5 - The same grid, cheaper

; the same fetch, with rows four bytes wide. row*4 is two ADD HL,HL and no
; loop at all.
at:     LD L,D
        LD H,0          ; HL = the row number
        ADD HL,HL       ; row * 2
        ADD HL,HL       ; row * 4
        LD BC,grid
        ADD HL,BC       ; base + row*4
        LD C,E
        LD B,0
        ADD HL,BC       ; + column
        LD A,(HL)
        RET

grid:   DEFB 1,2,3,0    ; the fourth byte of each row is wasted
        DEFB 4,5,6,0
        DEFB 7,8,9,0

What you should see: the same A = 08.

Now the comparison the exercise asks for. Nine instructions became six, the loop and its guard disappeared, and the number of instructions no longer depends on which row you asked for - row 0 and row 7 cost exactly the same. The three-wide version gets slower the further down the grid you go.

The price is three wasted bytes in a nine-byte grid. In a game loop that runs this fetch hundreds of times a frame, that is not a close decision. Put the four-wide version in the game and keep the three-wide one for a table you read once at start-up.

13.6 - Directions

; direction 0-3 in A becomes a screen offset in A. No comparisons at all.
offset: LD C,A
        LD B,0
        LD HL,dirs
        ADD HL,BC
        LD A,(HL)
        RET

; a screen row is 32 cells, so up is -32 and down is +32. As single bytes,
; -32 is 224 and -1 is 255.
dirs:   DEFB 224,32,255,1       ; up, down, left, right

What you should see: direction 3 gives 1, and direction 0 gives E0, which is 224 - the byte that means -32.

Compare that with what it replaces: four CP and four JR Z and four separate loads, growing every time you add a direction. This is five instructions and a four-byte table, and adding diagonals means adding four bytes and changing no code at all.

One trap worth meeting here. The obvious way to check both answers at once is to park the first in B and the second in C - and it does not work, because the routine uses BC for its own index and wipes whatever was in it. Put them in D and E instead. It is the S12 lesson turning up unannounced: a subroutine that uses a register is a subroutine you cannot store anything in that register across.

S14. Tables of addresses

14.1 - Follow one pointer

        ORG 256

        LD HL,(ptr)     ; the one-instruction way
        LD A,(HL)
        LD C,A          ; keep it - the print call gives C back

        LD HL,ptr       ; the five-instruction way
        LD E,(HL)
        INC HL
        LD D,(HL)
        EX DE,HL
        LD A,(HL)

        CP C            ; same byte?
        LD A,'Y'
        JR Z,show
        LD A,'N'

show:   RST 8
        DEFB 158        ; ZOUTC
        RET

data:   DEFB 42
ptr:    DEFW data

What you should see: >Y.

The exercise says to satisfy yourself the two agree, and the cheapest way to do that is to make the program say so rather than reading two register lines and comparing them by eye. C is the place to park the first result, for the same reason it held the counter in S6: the print call gives it back.

Reading them both into A and comparing with CP is also the first time in the course that a program checks its own work, which is a habit worth having.

14.2 - Pick and print

        ORG 256

        LD A,(which)    ; the index comes from memory
        ADD A,A
        LD L,A
        LD H,0
        LD DE,table
        ADD HL,DE

        LD E,(HL)
        INC HL
        LD D,(HL)
        EX DE,HL

        CALL puts
        RET

puts:   LD A,(HL)
        CP 0
        RET Z
        RST 8
        DEFB 158        ; ZOUTC
        INC HL
        JR puts

which:  DEFB 1

message1: DEFB "Ready",0
message2: DEFB "Set",0
message3: DEFB "Go!",0

table:  DEFW message1
        DEFW message2
        DEFW message3

What you should see: >Set.

The only change from the section's program is the first line: LD A,(which) instead of LD A,1. That is the whole point of the exercise. The index is now a byte of data, and a byte of data is something the rest of a program can write - which is how a menu remembers what the player chose, and how a monster remembers which way it was walking.

14.3 - Ask which

        ORG 256

        RST 8
        DEFB 156        ; ZKEYIN - wait for a key
        SUB '1'         ; '1','2','3' become 0,1,2

        ADD A,A
        LD L,A
        LD H,0
        LD DE,table
        ADD HL,DE

        LD E,(HL)
        INC HL
        LD D,(HL)
        EX DE,HL

        CALL puts
        RET

puts:   LD A,(HL)
        CP 0
        RET Z
        RST 8
        DEFB 158        ; ZOUTC
        INC HL
        JR puts

message1: DEFB "Ready",0
message2: DEFB "Set",0
message3: DEFB "Go!",0

table:  DEFW message1
        DEFW message2
        DEFW message3

What you should see: press 2 and you get >Set. 1 gives Ready and 3 gives Go!.

SUB '1' is doing the work that in S8 would have been three CPs and three JR Zs. A key code is a number, and a number one subtraction away from being an index.

Count the instructions: fifteen, and it stays fifteen with thirty messages in the table.

14.4 - Bounds

        ORG 256

        RST 8
        DEFB 156        ; ZKEYIN - wait for a key
        SUB '1'         ; '1','2','3' become 0,1,2

        CP 3            ; 0,1,2 are the only valid indexes
        RET NC          ; anything else - print nothing at all

        ADD A,A
        LD L,A
        LD H,0
        LD DE,table
        ADD HL,DE

        LD E,(HL)
        INC HL
        LD D,(HL)
        EX DE,HL

        CALL puts
        RET

puts:   LD A,(HL)
        CP 0
        RET Z
        RST 8
        DEFB 158        ; ZOUTC
        INC HL
        JR puts

message1: DEFB "Ready",0
message2: DEFB "Set",0
message3: DEFB "Go!",0

table:  DEFW message1
        DEFW message2
        DEFW message3

What you should see: press 2 and you get >Set. Press 9 and you get nothing at all, with A reading 08 in the register line.

Two instructions bought the safety, and there is something genuinely clever hiding in them. CP 3 / RET NC looks like it only guards the top end - and it guards the bottom too. Press 0 and SUB '1' gives 255 rather than -1, because a byte has no sign; 255 is not below 3, so the same test throws it out. Press a letter and you get something in the tens or hundreds, thrown out the same way.

So one comparison covers both ends, which it would not do if these were signed numbers. That is the first time the course's "a byte just wraps" fact has worked in your favour rather than against you.

RET NC is the conditional return - the same family as RET Z in puts. It is worth preferring to a JR over the whole body: the guard reads as one line and the body does not have to know it is there.

14.5 - Two levels deep

        ORG 256

        LD HL,outer     ; step 1: the address of the outer pointer
        LD DE,2         ; entry 1, two bytes per entry
        ADD HL,DE
        LD E,(HL)
        INC HL
        LD D,(HL)
        EX DE,HL        ; HL = the address of the inner table

        LD E,(HL)       ; step 2: entry 0 of the inner table
        INC HL
        LD D,(HL)
        EX DE,HL        ; HL = the address of the message

        CALL puts
        RET

puts:   LD A,(HL)
        CP 0
        RET Z
        RST 8
        DEFB 158        ; ZOUTC
        INC HL
        JR puts

message1: DEFB "Ready",0
message2: DEFB "Set",0
message3: DEFB "Go!",0
message4: DEFB "Stop",0

tableA: DEFW message1
        DEFW message2
tableB: DEFW message3
        DEFW message4

outer:  DEFW tableA
        DEFW tableB

What you should see: >Go!.

The exercise asks you to say what HL holds after each step, so here it is written out:

  1. HL = outer + 2 - the address of a pointer to a table.
  2. HL = tableB - the address of a table of pointers.
  3. HL = message3 - the address of some text.
  4. HL walks the text.

Notice that the second block of four instructions is identical to the first apart from the offset. Following a pointer is always the same four instructions, whatever is on the other end of it, and that is what makes two levels no harder than one. Three levels would be the same four instructions again.

This shape turns up in a real game as "the level, then the row, then the tile".

14.6 - A table of routines

        ORG 256

        LD A,1          ; which routine
        ADD A,A
        LD L,A
        LD H,0
        LD DE,table
        ADD HL,DE

        LD E,(HL)
        INC HL
        LD D,(HL)
        EX DE,HL        ; HL = the address of the routine

        JP (HL)         ; go there - its RET ends the program

one:    LD A,'A'
        RST 8
        DEFB 158        ; ZOUTC
        RET

two:    LD A,'B'
        RST 8
        DEFB 158        ; ZOUTC
        RET

three:  LD A,'C'
        RST 8
        DEFB 158        ; ZOUTC
        RET

table:  DEFW one
        DEFW two
        DEFW three

What you should see: >B.

Everything above the JP (HL) is exercise 14.2 unchanged - the same six instructions and the same four-instruction follow. Only the last line differs, and only the table contents differ. A table of addresses does not care whether the addresses point at text or at code.

One thing to understand before you build on this. JP (HL) is a jump, not a call: it puts nothing on the stack. So the RET at the end of one, two or three does not come back here - it uses the return address that was already on the stack when your program started, and the program ends. That is exactly what is wanted in this listing, and exactly not what you want in a game, where the dispatched routine has to hand control back to the main loop.

Two ways to fix it when you get there: put a CALL in front by dispatching through a subroutine that ends JP (HL) itself, or - simpler to read - keep the routine's own RET and reach it with a CALL to a two-line stub. Either way the rule is the one from S7: whatever jumps away has to be matched by something that comes back.

You have now written the dispatch mechanism a game's input handling is built on. Press a key, subtract a base, bounds-check it, index a table, jump. Five steps and no comparisons, and adding a control means adding two bytes.

S16. Writing to video memory

S15 sets no exercises, so this follows on from S14.

Every listing here uses the same two helper routines, given in full in 16.3 and referred to afterwards: eight sends the eight bytes at HL to the data port, and same sends the byte in A eight times. Writing them out saves a great deal of repetition once you are setting up two patterns and two colour entries.

16.1 - Your own shape

        ORG 256

        LD A,10h        ; pattern 34 at 0110h
        OUT (9),A
        LD A,41h
        OUT (9),A
        LD HL,arrow
        LD B,8
send:   LD A,(HL)
        OUT (8),A
        INC HL
        DEC B
        JR NZ,send

        LD A,10h        ; its colours at 2110h
        OUT (9),A
        LD A,61h
        OUT (9),A
        LD A,70h        ; cyan shape, transparent around it
        LD B,8
col:    OUT (8),A
        DEC B
        JR NZ,col

        RET

arrow:  DEFB 00011000b
        DEFB 00111100b
        DEFB 01111110b
        DEFB 11011011b
        DEFB 10011001b
        DEFB 00011000b
        DEFB 00011000b
        DEFB 00011000b

What you should see: a cyan arrow in the second row, two cells in.

The shape is the section's program with eight different bytes and one different colour, which is the point: once the two address setups are right, designing a shape is drawing on paper and reading off the rows. Note how the arrow is readable in the source. Write it as 18h, 3Ch, 7Eh, DBh, 99h, 18h, 18h, 18h and you will never spot the mistake when you make one.

16.2 - A row of them

        ; ... the pattern and colour writes from 16.1, then:

        LD A,40h        ; row 2 starts at cell 64 - 3840h
        OUT (9),A
        LD A,78h        ; 40h + 38h
        OUT (9),A
        LD A,34         ; the pattern number, thirty-two times
        LD B,32
row:    OUT (8),A
        DJNZ row

        RET

What you should see: thirty-two cyan arrows straight across the third row of the screen, over the top of the Insert disc in drive 0 and line.

Six instructions for a whole row. The address goes in once and the VDP walks itself along, so the loop body is a single OUT - and DJNZ from S13 is exactly the right instruction for it.

Row 2 was chosen because it is inside the top eight rows. Row 1 would have worked too; row 9 would not, for the reason S17 is about.

The text underneath is gone, and it does not come back until MOS next prints in those cells. That is worth knowing rather than worrying about: you are writing to the same memory the ROM is using, because at this point in the course it is still the ROM's screen.

16.3 - Two shapes

        ORG 256

        LD A,10h        ; pattern 34 at 0110h - the arrow
        OUT (9),A
        LD A,41h
        OUT (9),A
        LD HL,arrow
        CALL eight

        LD A,18h        ; pattern 35 at 0118h - the diamond
        OUT (9),A
        LD A,41h
        OUT (9),A
        LD HL,diamond
        CALL eight

        LD A,10h        ; pattern 34's colours at 2110h
        OUT (9),A
        LD A,61h
        OUT (9),A
        LD A,70h        ; cyan
        CALL same

        LD A,18h        ; pattern 35's colours at 2118h
        OUT (9),A
        LD A,61h
        OUT (9),A
        LD A,0E0h       ; grey
        CALL same

        LD A,40h        ; row 2 - 3840h
        OUT (9),A
        LD A,78h
        OUT (9),A
        LD B,16         ; sixteen pairs
pair:   LD A,34
        OUT (8),A
        LD A,35
        OUT (8),A
        DJNZ pair

        RET

; send the eight bytes at HL to the data port
eight:  LD B,8
e1:     LD A,(HL)
        OUT (8),A
        INC HL
        DJNZ e1
        RET

; send the byte in A to the data port eight times
same:   LD B,8
s1:     OUT (8),A
        DJNZ s1
        RET

arrow:  DEFB 00011000b
        DEFB 00111100b
        DEFB 01111110b
        DEFB 11011011b
        DEFB 10011001b
        DEFB 00011000b
        DEFB 00011000b
        DEFB 00011000b

diamond: DEFB 00011000b
        DEFB 00111100b
        DEFB 01111110b
        DEFB 11111111b
        DEFB 11111111b
        DEFB 01111110b
        DEFB 00111100b
        DEFB 00011000b

What you should see: cyan arrows and grey diamonds alternating all the way across the third row.

Pattern 35's bytes go at 35 * 8 = 280 = 0118h and its colours at 2118h. Notice that the second address byte is 41h for both patterns and 61h for both colour entries - only the low byte moved, because both patterns sit in the same 256-byte page. That is normal when you keep your tiles together, and it saves thinking about the high part at all.

The two subroutines are the real lesson. Without them this listing is four copies of the same loop with different labels, and the second time you write that out you will get one of them wrong.

16.4 - A striped cell

        ORG 256

        LD A,10h        ; pattern 34 at 0110h
        OUT (9),A
        LD A,41h
        OUT (9),A
        LD A,10101010b
        LD B,8
send:   OUT (8),A
        DJNZ send

        LD A,10h        ; its colours at 2110h
        OUT (9),A
        LD A,61h
        OUT (9),A
        LD A,9Fh        ; 9 for the 1 bits, F for the 0 bits
        LD B,8
col:    OUT (8),A
        DJNZ col

        RET

What you should see: a cell of four light red and four white vertical stripes, one pixel wide each.

And the answer the exercise wants: the leftmost stripe is light red. The leftmost pixel of 10101010b is bit 7, which is a 1, and light red is colour 9, the high nibble. So the high nibble colours the 1 bits and the low nibble the 0 bits, exactly as the section says - and now you have seen it rather than taken it on trust.

This is also the cheapest possible test of a colour byte. One pattern of alternating bits shows you both nibbles at once, side by side, which is much easier to read than a shape where one of the two colours only appears round the edges.

16.5 - Find a letter in the pattern table

; The cell at row 5, column 0 is cell 160, so pattern 160, at 160*8 = 0500h.
; A read address is the same two bytes as a write with no 40h added.
        ORG 256

        LD A,00h        ; low byte of 0500h
        OUT (9),A
        LD A,05h        ; high part, no 40h - this is a read
        OUT (9),A

        LD B,0          ; give the VDP a moment
wait:   DJNZ wait

        LD HL,buf       ; fetch the eight bytes first, print afterwards
        LD B,8
rd:     IN A,(8)
        LD (HL),A
        INC HL
        DJNZ rd

        ; --- turn them into eight lines of text
        LD HL,buf       ; source
        LD DE,out       ; destination
        LD C,8          ; rows to do
rowlp:  LD A,(HL)
        LD B,8          ; bits to do
bitlp:  CP 128          ; is the top bit set?
        JR C,zero
        PUSH AF
        LD A,'#'
        JR put
zero:   PUSH AF
        LD A,'.'
put:    LD (DE),A
        INC DE
        POP AF
        ADD A,A         ; next bit up into the top
        DJNZ bitlp
        LD A,13
        LD (DE),A
        INC DE
        LD A,10
        LD (DE),A
        INC DE
        INC HL
        DEC C
        JR NZ,rowlp
        LD A,0
        LD (DE),A       ; terminate the text

        LD HL,out
puts:   LD A,(HL)
        CP 0
        RET Z
        RST 8
        DEFB 158        ; ZOUTC
        INC HL
        JR puts

buf:    DEFS 8
out:    DEFS 81

What you should see

........
.#####..
...#....
...#...#
...#...#
...#...#
...#...#
........

The T of TATUNG, looking back at you.

Three things in this listing are worth more than the result.

Testing a bit with only what you know. CP 128 sets the carry if A is below 128, which is another way of saying the top bit is clear. ADD A,A from S14 doubles the byte, which moves the next bit up into the top. Eight of each and you have walked the whole byte. There are single instructions for this and you will meet them later; you did not need them.

Reading first, printing second. The eight INs happen before any printing at all, into buf. Mixing them would mean trusting RST 8 not to disturb the VDP's address counter between reads, and there is no reason to find out the hard way. Get the data, then use it.

LD (DE),A. Two pointers are needed here - one walking the bytes, one walking the text being built - and the Z80 will store A through DE as well as through HL. Without that you would be swapping HL back and forth with EX DE,HL twice a character.

The marks down the right-hand edge are not a bug. A cell is eight pixels wide, the text is 40 columns across a 256-pixel screen, and 256 does not divide by 40 into whole pixels - so this cell holds the T plus the left stroke of the A beside it. Seeing that is worth as much as seeing the letter.

16.6 - Read it back the short way

        ORG 256

        LD A,10h        ; write pattern 34 at 0110h
        OUT (9),A
        LD A,41h        ; 40h + 01h - a write
        OUT (9),A
        LD A,10110101b  ; a byte that could not be mistaken for FF or 00
        LD B,8
send:   OUT (8),A
        DJNZ send

        LD A,10h        ; now read the first of those bytes back
        OUT (9),A
        LD A,01h        ; high part with NO 40h - a read
        OUT (9),A

        LD B,0          ; in case the VDP needs a moment
wait:   DJNZ wait

        IN A,(8)
        CP 10110101b    ; the same byte?
        LD A,'Y'
        JR Z,show
        LD A,'N'

show:   RST 8
        DEFB 158        ; ZOUTC
        RET

What you should see: >Y.

The choice of 10110101b matters. Write FFh or 00h and a Y proves nothing, because those are the values you would get from a bus that told you nothing at all. Pick a byte with an awkward pattern and a correct read is the only way to produce it.

The LD B,0 / DJNZ pair is a do-nothing loop that runs 256 times. It is there as insurance rather than necessity - take it out on this machine and the read still works - but it costs nothing and it is the first thing to reach for if a read ever hands you the wrong byte.

Being able to read VRAM back matters more than it looks. Everything you put on the screen from here on is invisible to you unless you can ask what is there: this is the tool you will use to find out why a tile is wrong, and 16.5 is the tool you will use to look at it.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.