← Back to Courses
module
11

Text of your own

Why this matters

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.

A register pair as a pointer

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.

Writing through a pointer

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.

Filling memory with a loop

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.

Text you declare

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.

What that actually looks like in memory

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.

Printing it

        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 subroutine to keep

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.

Change one thing

  • Remove the ,0 from the end of msg1. What gets printed, and where does it stop? Work out why before you run it.
  • Swap the two CALL puts lines so msg2 prints first. Does anything else need to change?
  • Change puts to use BC as its pointer instead of HL. One line will not assemble - which, and why?
  • Put LD (HL),'X' immediately before CALL puts. What happens to your message, and what does that tell you about where DEFB data lives?
  • Add a third message and print all three with one CALL each. Then count the bytes you saved over printing them with LD A / RST 8 / DEFB 158 throughout.

Exercises

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.

When it goes wrong

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.

Summary

  • A register pair holding an address is a pointer. (HL) means the byte it points at, and INC HL moves it along.
  • Addresses stop being abstract once you're moving a pointer through them. Appendix IV - The memory map lays out what lives where across the whole 64K, byte by byte, if you want to see it - there's a spreadsheet version alongside it for scrolling through.
  • 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.
  • Call 158 leaves 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.

Next

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.

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.