Z80 Assembler

I got a bit of a shock when I began to learn Z80 assembler. It made me realise that there is a certain culture in the modern tech industry of fairly high quality introductory lessons. Most new things that pop up come included with a nice gentle introduction, linked with comprehensive documentation and a community of helpful users.

So when I googled for "Learning Z80 assembly" I expected to find a huge selection of gentle introduction lessons, followed by more advanced techniques, a thriving vibrant community etc. Instead I was shocked how little information there was out there. I figured perhaps I should go to contemporary sources, so I ordered an old book called "Programmign the Z80". Plenty of people wrote that this was the Z80 programming guide. However the book is not a gentle introduction to Z80 programming. A full half of the book is a detailed listing of every instruction and how it affects the internal state of the CPU - not how you might use those instructions to achieve something.

Part of the reason for this is a practical computer with a Z80 CPU is surrounded by periphals, and it is these periphals that give the computer it's character. The defining features of micro computers in the Z80 era were how much RAM they had, where the ROM was positioned in memory and how much space did it occupy, what graphics chip it had and what sound chip. So it was probably less of "Programming the Z80" and more "Programmign the Sinclair Spectrum". These computers were machine-code compatible, but not compatible in any other way that mattered. MSX attempted compatibility at the periphal level but it wasn't until IBM PC Compatible computers came on the scene that micro computers got enough compatibility that software could be run reliably on two machines made by different manufacturers.

Registers

Registers are like variables, except there is a limited number of them and you can not perform all instructions on all registers. The registers have been designed for particular uses and life gets difficult if you don't go with the flow. For example the instruction djnz * decrements the b register and jumps to * if b is not zero. This means that b is really handy as a counter for loops. You can loop using another register but you will likely end up with more instruction cycles being used. So a lot of Z80 programming is about knowing what the intended use of each register is and not fighting against that intention. The rest of Z80 programming is about knowing all the tricks of the trade that subvert those intentions for higher performance!

Because there are not an unlimited number of registers you can spend a lot of time juggling registers. You implement some clever use of the registers and then you find that the result is not in the right place to use it for your next operation, or that you have clobbered something you needed later. So you end up having to actually think and plan in advance how you are going to do something that would take you five lines of C (or one line of Perl).

Adding numbers

There are only four registers that can store the result of an addition: a, hl, ix and iy. The instructions that add only take one argument: the thing to add to one of those registers. For example 5 + 2 = 7 would be expressed by setting the a register to 5, setting another register to the value 2 (eg. b) and doing the instruction add a, b making a equal to 7:

ld a, 5
ld b, 2
add a, b            ; a is now 7

There is no add b, a instruction as b is not an "accumulator". Also interesting is that add hl, bc is 11 clock cycles, but add ix, bc is 15 clock cycles. So why would anyone want to use the ix register? Well, it turns out that ix and iy have capabilities that hl does not. Not all registers are created equal.

Subtraction

Strangely subtraction can only be performed on a:

ld a, 7
ld b, 2
sub b               ; a is now 5

Addressing

Look at the different ways addressing works:

ld hl, *            ; store the 8 bit value * into hl
ld (hl), *          ; store the value * into the address that hl points to
ld hl, **           ; store the 16 bit value ** into hl
ld hl, (**)         ; store the value at address ** into hl
ld (**), hl         ; store hl into the address at **