.png)
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).
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.
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.
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.
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.
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.
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.
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.
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.
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".
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
"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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
; 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
; 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.
; 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.
; 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.
; 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.
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.
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.
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.
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.
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:
HL = outer + 2 - the address of a pointer to a table.HL = tableB - the address of a table of pointers.HL = message3 - the address of some text.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".
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.
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.
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.
; ... 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.
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.
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.
; 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.
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.