8-Bit Labs

Experiments in retro computers, assembly language and electronics

Learning Z80 Assembly on the RC2014

20 May 2023

I am reading Programming the Z80 by Rodnay Zaks to refresh my knowledge of Z80 assembly. I usually develop and compile the code on my laptop, but this time I wanted to experience writing and testing the code on my RC2014 computer running CP/M.

The assembler that ships with CP/M uses 8080 instructions, so instead I am using Z80ASM by SLR Systems. I edit the files using ZDE 1.6.

The book provides sample code like this for 8-bit addition,

LD  A,(ADR1)   ; LOAD OP1 INTO A
LD  HL,ADR2    ; LOAD ADDRESS OF OP2 INTO HL
ADD A,(HL)     ; ADD OP2 TO OP1
LD  (ADR3),A   ; SAVE THE RESULT RES AT ADR3

The problem with this code is that it isn’t complete and does not output to the console. I like to step through the code and inspect the registers in each step. In order to do so, I rewrite the code as follows on the RC2014 including labels for the memory locations.

        LD A,(OP1)     ; LOAD OP1 INTO A
        LD  HL,OP2     ; LOAD ADDRESS OF OP2 INTO HL
        ADD A,(HL)     ; ADD OP2 TO OP1
        LD  (RES),A    ; SAVE THE RESULT RES AT ADR3
        RET
OP1:    DB 32
OP2:    DB 10
RES:    DB 0

I then compile the program in 8BITADD.Z80. The assembler expects the Z80 file extension and in this case my code is on drive E: and the assembler is on drive I:. I use the /F option so that I get a program listing to help me understand the memory layout.

E>I:Z80ASM 8BITADD/F

Z80 ASSEMBLER Copyright (C) 1983 by SLR Systems Rel. 1.30 #F10268

 8BITADD/F
End of file Pass 1
End of file Pass 2
 0 Error(s) Detected.
 14 Absolute Bytes. 3 Symbols Detected.

The program listing that is produced contains,

Z80ASM SuperFast Relocating Macro Assembler                 Z80ASM 1.30 Page   1
8BITADD Z80

    1 0100  3A 010B             LD A,(OP1)     ; LOAD OP1 INTO A
    2 0103  21 010C             LD  HL,OP2     ; LOAD ADDRESS OF OP2 INTO HL
    3 0106  86                  ADD A,(HL)     ; ADD OP2 TO OP1
    4 0107  32 010D             LD  (RES),A    ; SAVE THE RESULT RES AT ADR3
    5 010A  C9                  RET
    6 010B  20          OP1:    DB 32
    7 010C  0A          OP2:    DB 10
    8 010D  00          RES:    DB 0
 0 Error(s) Detected.
 14 Absolute Bytes. 3 Symbols Detected.

I can run this and it works, but it doesn’t print anything to the console and I can’t see what it is doing. In order to do that, on CP/M, you can use the program DDT, the Dynamic Debugging Tool. The following is an example session. (Note that I am running Z-System, a Z80 CP/M clone, so I am using the command DDTZ but it operates the same.)

E>A:DDTZ 8BITADD.COM
DDTZ v2.7M by CB Falconer. CPU=Z80
Next  PC  Save
0180 0100 1

This loads 8BITADD.COM into memory. Notice that the program counter PC is 0x0100 which is the start of our program on line 1 of the listing. We can see our program in memory with the D command which will print 16 lines of memory starting at 0x0100. You can also give it ranges of memory. This program is 14 bytes, so we can just look at the first line.

-D0100,010F
0100 3A 0B 01 21 0C 01 86 32  0D 01 C9 20 0A 00 00 00 :..!...2... ....

Notice that the bytes on the first line match the bytes from our listing.

To step through the program one instruction at a time, use the command T.

-T
C0Z0M0E0I0 A=00 B=0000 D=0000 H=0000>03C3 S=D7B5>CC10 P=0100 LDA  010B*0103
C0Z0M0E0I0 A=20 B=0000 D=0000 H=0000>03C3 S=D7B5>CC10 P=0103 LXI  H,010C

Notice that the A register changed to 0x20 (hex for 32) and the program counter is now 0x0103 which is our second instruction. At the end of each line, we also see the instruction at the program counter disassembled in 8080 format.

Stepping again,

-T
C0Z0M0E0I0 A=20 B=0000 D=0000 H=0000>03C3 S=D7B5>CC10 P=0103 LXI  H,010C*0106
C0Z0M0E0I0 A=20 B=0000 D=0000 H=010C>000A S=D7B5>CC10 P=0106 ADD  M

The H register now contains 010C which is the memory address of OP2. You can also see that after the >, the memory that the HL register is pointing to 0x000A. It shows two bytes at that location in little endian order.

-T
C0Z0M0E0I0 A=20 B=0000 D=0000 H=010C>000A S=D7B5>CC10 P=0106 ADD  M*0107
C0Z0M0E0I0 A=2A B=0000 D=0000 H=010C>000A S=D7B5>CC10 P=0107 STA  010D

This executed ADD A,(HL) which is add to A what is stored at the address pointed to by HL. HL is 0x010C and that memory location contains 0x0A so we expect the A register to switch from 0x20 to 0x2A which it did.

Next we store A to memory,

-T
C0Z0M0E0I0 A=2A B=0000 D=0000 H=010C>000A S=D7B5>CC10 P=0107 STA  010D*010A
C0Z0M0E0I0 A=2A B=0000 D=0000 H=010C>2A0A S=D7B5>CC10 P=010A RET

Notice that the high byte of memory where HL is pointing is now 0x2A which is what we stored. We can also confirm the result by dumping memory once again.

-D0100,010F
0100 3A 0B 01 21 0C 01 86 32  0D 01 C9 20 0A 2A 00 00 :..!...2... .*..

The third last byte now contains the result of the addition.

You may have noticed the C0Z0M0E0I0 at the start of each line. These are the flags. Addition didn’t overflow in our program, so the flags didn’t change. To see them change, let’s update the assembly so that the result of the addition is greater than 255.

        LD A,(OP1)     ; LOAD OP1 INTO A
        LD  HL,OP2     ; LOAD ADDRESS OF OP2 INTO HL
        ADD A,(HL)     ; ADD OP2 TO OP1
        LD  (RES),A    ; SAVE THE RESULT RES AT ADR3
        RET
OP1:    DB 222
OP2:    DB 81
RES:    DB 0

This is the debugging session up to the ADD.

E>A:DDTZ 8BITADD.COM
DDTZ v2.7M by CB Falconer. CPU=Z80
Next  PC  Save
0180 0100 1
-D0100,010F
0100 3A 0B 01 21 0C 01 86 32  0D 01 C9 DE 51 00 00 00 :..!...2....Q...
-t
C0Z0M0E0I0 A=00 B=0000 D=0000 H=0000>03C3 S=D7B5>CC10 P=0100 LDA  010B*0103
C0Z0M0E0I0 A=DE B=0000 D=0000 H=0000>03C3 S=D7B5>CC10 P=0103 LXI  H,010C
-T
C0Z0M0E0I0 A=DE B=0000 D=0000 H=0000>03C3 S=D7B5>CC10 P=0103 LXI  H,010C*0106
C0Z0M0E0I0 A=DE B=0000 D=0000 H=010C>0051 S=D7B5>CC10 P=0106 ADD  M
-T
C0Z0M0E0I0 A=DE B=0000 D=0000 H=010C>0051 S=D7B5>CC10 P=0106 ADD  M*0107
C1Z0M0E0I0 A=2F B=0000 D=0000 H=010C>0051 S=D7B5>CC10 P=0107 STA  010D
-

0xDE plus 0x51 is 0x012F which has overflowed, so the result is the low byte 0x2F in the A register. Also notice that the flags changed from C0Z0M0E0I0 to C1Z0M0E0I0, the Carry flag switched from 0 to 1 as expected.

Continuing the run, we see that 0x2F is then stored in memory.

-T
C1Z0M0E0I0 A=2F B=0000 D=0000 H=010C>0051 S=D7B5>CC10 P=0107 STA  010D*010A
C1Z0M0E0I0 A=2F B=0000 D=0000 H=010C>2F51 S=D7B5>CC10 P=010A RET
-D0100,010F
0100 3A 0B 01 21 0C 01 86 32  0D 01 C9 DE 51 2F 00 00 :..!...2....Q/..