My First Z80 Program

I wrote a Z80 assembly program for my RC2014 Zed computer. I wanted to get a complete CP/M experience, so I used the CP/M 2.2 ASM assembler, LOAD relocator, and the DDT debugger.

I did cheat a little. Although I used the vintage RED editor for some text input, I also wrote some on my Linux laptop, and transferred to the RC2014 Zed via XMODEM.

After cutting and pasting a Z80 assembly “hello world” program into a file, and getting the very basics of assembly, loading and running down, I decide to write the equivalent of this C program:

#include <stdio.h>
int
main(int ac, char **av)
{
    int h;
    char *array[4] = {
        "item 1 Item 1\r\n",
        "Second item SECOND\r\n",
        "THIRD THIRD\r\n",
        "four, fourth item 4th item\r\n",
    };
    for (h = 0; h < 4; ++h) {
        printf(array[h]);
    }
    return 0;
}

That is, print strings by walking an array of pointers. Doing this would get me more Z80 assembly experience, in particular, doing double indirection through an array, and doing a loop.

This is what I came up with.

	org     0100h
BDOS:    equ     05h
WRTLINE: equ     09h ; DE contains .String
LF:      equ     0Ah
CR:      equ     0Dh

mvi h,00h
TOP
	lxi b,array ; bc has address of array in it
	mov a,c
	add h
	mov c,a
	ldax b
; a has lower byte of address of item 1 in it
	mov e,a

	inr c ; what happens when c holds 0xff and you inr it? b should get incrementd, c zeroed.
	ldax b
; a has high byte of address of item 1 in it
	mov d,a

	push h ; save HL on stack
; de has address item1 in it
	mvi     c,WRTLINE
	call    BDOS

	pop h ; get HL back off stack
; increment H
	mov a,h
	adi 2
	mov h,a
; loop termination
	cpi 8
	jnz TOP

	ret

item1: db CR,LF,'Item 1 Item 1',CR,LF,'$'
item2: db CR,LF,'Second item SECOND',CR,LF,'$'
item3: db CR,LF,'THIRD THIRD',CR,LF,'$'
item4: db CR,LF,'four, fourth item 4th item',CR,LF,'$'
array:
dw item1
dw item2
dw item3
dw item4

Observations

  • Z80 instruction set is quirky. You can only do “compares” against the A register, among other non-orthogonalities. I’m guessing this derives from keeping the instruction set small, for densely encoded instructions, but gee whiz.
  • CP/M’s ED text editor is unfathomable even for line-oriented editors.
  • You can end up with a program that works “by magic”. At one point during development, I had a wrong address for a string, but the program appeared to print the string anyway, I think because CP/M just skipped some leading non-printable characters on output.
  • CPUs without a way to protect the operating system from stray writes end up running extremely fragile systems. I had some bad crashes from sending incorrect pointers to the “write to console” BDOS call.
  • The old CP/M manuals exhibit a strange style. I get a disjoint feeling from reading them. On the first read, I would think that something was undocumented, only to find it in “the wrong section” or finding it concealed in a pile of other text. I haven’t felt this way about manuals since learning Unix troff back in the late 80s by reading what passed for a manual for it.
  • I’m beginning to understand why programs coded in assembly have odd failures. There’s only 1 “load value from memory address in a register” instruct, LDAX. It only loads a byte. Addresses are 2 bytes. If you want to do double-indirection, and I did, you have to load a 2-byte-address one byte at a time. That means a LDAX from some address, and an LDAX from some address + 1. Without a carry, the second LDAX, from some address + 1 will underflow. Your program might want to load one byte of an address from 0x10ff, and a second byte from 0x1100. Since LDAX only loads a byte at a time, you should load the byte from 0x1100 first, then the byte from 0x1100 - 1, instead of loading the byte from 0x10ff, then the byte from 0x10ff + 1. Because the registers hold 8-bit values, if you do the address increment as I did above, with inr c, I think it’s possible to overflow the C register and load the 2nd byte from 0x1000.
  • I see why Windows doesn’t have a TTY device to this very day. CP/M ran on such underpowered hardware that it just let user programs do a lot of terminal control. “DOS style” text files, that have a carriage return/line feed combination at the end of every line of text are actually “CP/M style” text files.