.png)
In S9 you read a typed line into a buffer and walked through it. That worked, but the text belonged to the user. This section is the other half: text that belongs to you - a message stored in your program and printed whenever you want it.
By the end you will have a subroutine that prints any message you hand it. It is four instructions long and you will use it in everything you write from here.
You have seen the mechanism already:
LD A,(HL)
HL holds an address, and the brackets say "the byte living there". HL
is not the data. It is a finger pointing at the data - a pointer.
This is called indirect addressing, and it is the third way of getting at a value. S10 had the other two:
LD A,65 ; immediate - the value is in the instruction
LD A,(2000) ; direct - the address is in the instruction
LD A,(HL) ; indirect - the address is in a register
Only the third one can change while the program runs, which is what makes it useful. Move the pointer and the same instruction reaches somewhere else.
It works in both directions:
LD (HL),A ; store A at the address HL holds
LD (HL),65 ; store a constant there - one instruction
That second form is worth noticing. In S10 you could not store a constant to a fixed address in one go, but through a pointer you can.
HL pairs with any of the 8-bit registers:
LD B,(HL)
LD (HL),C
BC and DE can be pointers too, but only with A:
LD A,(DE) ; fine
LD (BC),A ; fine
LD B,(DE) ; rejected - no such instruction
That asymmetry is not a rule with a reason behind it; it is simply which
combinations the Z80 was given. HL is the general-purpose pointer, and the
other two are there for when you need a second and a third.
Put a pointer and a counter together and you can write a run of bytes:
ORG 256
LD HL,35900 ; where to start
LD A,'A'
LD E,3 ; how many
next: LD (HL),A ; store one
INC HL ; move the pointer along
DEC E ; one fewer to go
JR NZ,next
RET
INC HL moves the pointer forward one byte. That pairing - do something,
INC HL, loop - is how every run of bytes in the machine gets read or
written.
Now the part that makes this practical.
You could build a message in memory a character at a time, and the old way of doing it looked like this:
LD HL,30000
LD A,'H'
LD (HL),A
INC HL
LD A,'I'
LD (HL),A
Six instructions for two letters, and a memory address you had to pick yourself and hope nothing else wanted.
You do not have to. DEFB puts bytes into your program where you write it,
and it takes a quoted string:
message: DEFB 'HELLO WORLD',0
The assembler lays out all eleven characters plus the terminating zero, and
message is the label for the first of them. Single or double quotes both
work, so DEFB "HELLO WORLD",0 is the same thing. No address to choose, nothing to
copy at run time, and the text is visible in your source where you can read
it.
The zero on the end is not decoration. It is how the printing loop knows where to stop, and it is the same convention the line-input call used in S9.
There is nothing clever going on. Assemble DEFB "Hello, world!",0 and those
fourteen bytes sit in your program in order:
48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 00
H e l l o , w o r l d ! end
A string is not a type or a structure. It is a run of character codes with a zero after them, and every "string operation" you will ever write is a loop over those bytes. Worth looking at once, so the word stops sounding like something more than it is.
Note the lower case. The display prints either case perfectly happily -
worth saying, because S8 told you that letter keys come back as capitals.
Those are two different paths and only the keyboard is restricted. You can
print Hello; you just cannot type a lower-case h and have your program
see one.
ORG 256
LD HL,message
print: LD A,(HL) ; fetch the next character
CP 0 ; terminator?
JR Z,done
RST 8
DEFB 158 ; ZOUTC - print it
INC HL ; step along
JR print
done: RET
message: DEFB 'HELLO WORLD',0
What you should see
>HELLO WORLD
Eleven characters from one loop of six instructions. Compare that with the program in S3, which printed five characters using fifteen.
Note that HL still points where it should after each print call. That is not
guaranteed - S8 showed that the keyboard scan destroys HL - but the print
call leaves it alone, which is what makes this loop possible at all.
The loop does not care which message it is given, so make it a subroutine and hand it one:
ORG 256
LD HL,msg1
CALL puts
LD HL,msg2
CALL puts
RET
; print the zero-terminated string at HL
puts: LD A,(HL)
CP 0
RET Z ; done - return straight out
RST 8
DEFB 158 ; ZOUTC
INC HL
JR puts
msg1: DEFB 'HELLO ',0
msg2: DEFB 'WORLD',0
What you should see
>HELLO WORLD
Two messages, one routine, and HL is how you tell it which. That is a
parameter - the first one in this course - and it is the normal way to pass an
address to a subroutine.
RET Z is new and does exactly what it looks like: return if the zero flag is
set. It saves a jump to a RET elsewhere.
Keep this routine. Copy it into everything. A title screen, a score
label, a game-over message - they are all LD HL,something and CALL puts.
,0 from the end of msg1. What gets printed, and where does it
stop? Work out why before you run it.CALL puts lines so msg2 prints first. Does anything else
need to change?puts to use BC as its pointer instead of HL. One line will not
assemble - which, and why?LD (HL),'X' immediately before CALL puts. What happens to your
message, and what does that tell you about where DEFB data lives?CALL each. Then count the
bytes you saved over printing them with LD A / RST 8 / DEFB 158
throughout.11.1 - Three messages. Define msg1, msg2 and msg3 and print them one
after another with three calls to puts. Then put a line break between them.
11.2 - Count to five in memory. Store the values 1 to 5 in consecutive bytes using a pointer and a loop. Then read them back and print each as a digit, remembering the 48 from S3.
11.3 - What does this do? Explain, line by line, what this leaves in A
and why the second load gets something different from the first:
LD HL,40000
LD A,(HL)
INC HL
LD A,(HL)
11.4 - Length of a message. Write a subroutine that takes a message in
HL and returns its length in A, not counting the terminator. Test it on
messages of different lengths.
11.5 - Print it backwards. Use 11.4 to find the end, then walk back with
DEC HL.
11.6 - Two pointers at once. Copy a message from one place to another,
byte by byte, using HL for the source and DE for the destination. You will
need LD A,(HL) and LD (DE),A, and to advance both. Then print the copy to
prove it worked.
| What you see | What it means |
|---|---|
| The message runs on into rubbish | No terminator, or the wrong one. The loop keeps printing until it happens to meet a zero. |
error: unable to resolve reference on an indirect load |
Probably LD B,(DE) or similar. BC and DE pair only with A. |
| The message prints once and the program stops | A RET Z doing its job in a place you did not intend, or the subroutine falling out early. |
| Text prints but the first character is wrong | HL was left pointing one byte past the label - check for an INC HL before the loop rather than inside it. |
| Nothing prints at all | HL never pointed at the message. A missing LD HL,message, or one loaded with (message) in brackets by mistake, which fetches the first two bytes instead of the address. |
(HL) means the byte it
points at, and INC HL moves it along.HL pairs with any 8-bit register. BC and DE pair only with A.LD (HL),n stores a constant through a pointer in one instruction.DEFB 'text',0 puts a message in your program where you can read it, with
no address to choose. The zero marks the end.HL alone, which is what makes a print loop work.puts - four instructions and a RET Z - prints any message you point it
at. Keep it.S12. The stack. Every RET you have written found its way back somewhere,
and every CALL remembered where it came from. Next you find out how - and
that the memory it uses is both shared and much smaller than you would
guess.