MSX Assembly 101: 02 Printz
This post will continue my MSX Assembly 101 series, started here.
We are going to develop the routine printz
for displaying to the screen
0-terminated strings.
Control flow
Z80 CPU supports two types of jumps: jr
and jp
. There are also two commands
for working with subroutines: call
to call a subroutine and ret
to return
from it. Each of these command can take a condition, making control flow
transition conditional on some flag. For example, ret z
will return from the
subroutine, but only if the flag Z is set.
Flags
Z80 CPU has 6 flags, corresponding to the certain bits of the register f
. Out
of those only 4 can be used to create conditions for control flow transfer.
Flag | Name | Bit in f |
Condition for control flow (1/0)? |
---|---|---|---|
C | Carry | 0 | C / NC |
N | Add/Subtract | 1 | Not usable |
P/V | Parity/Overflow | 2 | PO (“parity odd”) / PE (“parity even”) |
H | Half Carry | 4 | Not usable |
Z | Zero | 6 | Z / NZ |
S | Sign | 7 | M (“minus”) / P (“plus”) |
Be careful with P/V flag; it behaves differently for arithmetic and logical operations. You need to read the documentation to understand what it really means.
Relative jumps with jr
While jp <address>
or jp <condition>,<address>
performs (possibly
conditional) jump to the given address, the instruction needs two bytes in a
machine code to store it (as addresses are 16-bit). The relative jump
instruction jr
solves this problem by using only one byte to store the offset
of jump destination from the jump instruction itself. However, jr
has some
important limitations comparing with jp
:
- It is a bit slower, as it required to calculate the jump destination.
- It uses only one byte for offset, so the range of possible jumps is limited. Actually, that byte contains offest minus two, so the possible offset values are in range from -126 to 129.
- Only C/NC and Z/NZ conditions are supported; PO/PE/P/M will not work.
Other control flow affecting commands
Not only jr
/jp
/call
/ret
commands affect control flow. There are four
more commands, not taking a condition. One of them is widely used, two are rare,
and one you never need in MSX.
Commonly-used instruction is djnz <address>
and it is often used for loops.
When CPU encounters this instruction, it decrements the register b
and
performs a jump if the result is not zero. You should keep in mind, that djnz
is similar to jr
and uses offset and records offset minus two, so the
offset can be in range from -126 to 129.
Typically djnz
is used along the following lines:
ld b, <number of loop repetitions>
@@loop:
<loop body>
djnz @@loop
First rarely used command is “restart”: RST <address>
. It behaves like
call <address>
but:
- takes only one byte in machine code, not three, and faster (11 cycles not 17);
- extremely limited, as the specified address can be only one of
00h
,08h
,10h
,18h
,20h
,28h
,30h
,38h
; this allows only calls of few BIOS routines; on MSX probably the most useful isrst 30h
for interslot call.
Second rarely used command is reti
to return from interrupt handler.
The similar retn
is used to return from nonmaskable interrupt handler;
however, in MSX there are no nonmaskable interrupts, so the command is not
useful.
Printing strings
We want to have a routine, printing a string, in a separate file, so it can be
easily reused. This is possible using the assembler’s .INCLUDE
directive.
Create the file printz-driver.asm
:
.PAGE 1
.ROM
.BIOS
ld hl,@@message
call printz
@@end:
jr @@end
@@message:
db "Hello, world!",0
.INCLUDE "printz.asm"
This program will load into the register pair hl
the address of “Hello,
world!” 0-terminated string and call (not yet defined) printz
to output it
to the screen.
Theoretically, there is MSX DOS (not BIOS!) function, which prints strings,
terminated by character $
. I cannot even start listing what is wrong with
this.
Local labels
Maybe it makes sense to mention here what does it mean for asMSX assembler if
a label’s name starts with @@
: it means that the label is local; in other
words is valid only to the next global label. Using local labels you can do
things like this:
a:
...
@@loop:
...
b:
...
@@loop:
...
In this case @@loop
is not really a duplicated label; it is local and before
b:
means the first of two, after it means the second. This approach really
helps when part of your code is in a separate file, which you want to .INCLUDE
somewhere else without caring if some of its internal labels will conflict with
labels you already have.
String-printing routine
Now create the file printz.asm
:
printz:
ld a,[hl]
and a
ret z
call CHPUT
inc hl
jr printz
It loads the character code from memory, pointed to by the register pair hl
,
into the accumulator register a
(with ld a,[hl]
), checks if it zero (and r
is bit-wise AND between the accumulator and the given register, and a
is 0
if and only if a
itself is 0; it was possible to use cp 0
instruction
to check a
for 0, but and a
takes less memory – 1 byte of machine code not
two – and faster – 4 cycles not 7), returns from the subroutine if the result
was zero (ret z
), otherwise calls BIOS routine to output the character,
advances hl
to point to the next character, and repeats everything.
Final result
Compile and run the program. You will see the string “Hello, world!” on the screen.
