Well, there isn't a simple answer. It's a lot is more about philosophy than hard, Z80 specific advice, but let's look at some points:
The most important thing about Z80 is maybe to forget that it's a Z80 and take it as it is, an 8080 with some additions that sound nice but often don't work out as nice. Remember the index registers being clumsy? Yeah, exactly that. The Z80 was designed as an 8080 clone with most instruction set extensions being more targeted at sales than usage. So there's the reason why most CP/M software stayed with 8080 code, despite the Z80 having replaced it in new machines within less than a year.
The 8080 is well-defined and consistent (for most parts). It's an
- 8 bit
- accumulator based CPU with a
- single 16 bit memory pointer (HL)
- single 16 bit stack pointer
- single 16 bit program counter.
This sounds much like the 6800, doesn't it? Except for the second 16-bit accumulator and indexed addressing operations, that is. For both the 8080 makes up by adding two 8-bit pairs that can hold two additional pointers or up to 4 8-bit values.
It's these commonalities and differences any program needs to be built upon - after all, each and every CPU is built to support a certain coding style, the way the CPU is intended to be used. The Z80 assembler unified the mnemonics to a great extent. Its rather abstract angle simplifies first steps. In contrast 8080 mnemonics are more diverse, if not chatty, presenting more of the idea behind certain instructions. A good example is the already mentioned integrated use of the memory pointer. Where Zilog requires an abstract
LD B,(HL)
which suggests that there might be ways to use other pointers, Intel marks access via HL by using a pseudo register called M, as in
MOV B,M
All instructions that accept an 8 bit register as parameter will thus also accept a memory pointer in HL. Knowing that makes it clear why some combination in of Z80 parameters will work, others won't.
These are also a large part of the added instructions - when H/L/HL gets replaced by IX(DDh) and IY(FDh) by adding 1 or two bytes and 4 to 12 cycles.
Reading carefully through the instruction section of the original Programming Manual might be more useful than the later Assembler Manual or the Systems Manual, as its instruction section is still organized by encoding and internal working rather than alphabetic sequence.
With that knowledge it will be less confusing to identify which Z80 modifications can be helpful and which are folly.
In addition to above, it might be useful to go for some far more general assembly techniques, foremost among them avoiding premature optimization. This seems to be especially useful when reading your description for registers usage.
Don't care for input or output ahead or time.
Use registers always in the same order.
Don't care for optimization of parameter storage between function calls.
SP can be loaded into HL to address parameters on stack.
Stack is cheap.
Stack is 16 bits wide.
Push all registers at entry.
Use 16-bit pointers freely.
Freeing up a register temporarily is a simple PUSH/op/PULL.
Remember that all 16 bit registers can be added to.
Remember that adding 0FFFFh is the same as subtracting 0001h.
All registers used up?
Need more storage than registers offers?
Need some heap?
Set up a stack frame!
LD HL,-40 ; Stack space needed
ADD HL,SP
LD SP,HL
Now there's lots of space to store local data without any stack acrobatics
Or set up a stack frame using IX or IY (or both)
This is one of the few cases where IX/IY becomes really useful, as their implied 8-bit offset is now an fixed address into that stack frame (see below)
Stack frames may not resolve need for static storage or an independent heap, but does serve all needs for local storage.
Stack frames are as well a good way to access parameter passing on stack.
- Except parameter passing on stack is evil.
- Rather use explicit parameter blocks
- It's the same effort, but way better readable and less prone to incompatibilities.
Remember to provide enough stack space
Don't hesitate to use memory:
- These are 8 bit CPUs and memory is as fast as the CPU
- The only down side on a 8080/Z80 is its clumsy addressing
- IX/IY addressing does resolve a lot of this.
The mentioned stack frames show the importance of understanding the Z80 not as a single CPU design, but as a 8080 design plus a set of modification bolted on.
IX+n or IY+n addressing is a modification of the basic 8080 M addressing. Therefore all of these up to 256 local variables are (at 2 byte and 12 clock penalty) directly accessible in all basic instructions. No need to go through A.
After reading and throughout memorizing the 8080 Programming Manual, a table showing all instructions but making the 8080 to stand out may be helpful. I found this 8080/Z80 Instruction Set table very helpful:

It is not only functionally ordered, but also shows both mnemonics and makes thus Z80 only instructions stand out. Noticing them helps to think about added usefulness - or the lack thereof.
Now get your program working.
Only after that is reached should you start to optimize. This as well can be done in several steps, best done top down:
- Look for groups of functions that serve a purpose entered via a single point of entry.
- Have the stack frame created at entry for all of them.
- Do so with IX.
- Use IY for a second frame needed dynamically or only in some parts.
- Reorder functions within this group for commonalities.
- Reduce register saving between them (that goes bottom up).
- Reorganize register (and memory use).
- Reduce saving allocation again.
- Vary register usage for parameters.
- Inline routines that are called from a single place.
- Remember, a call is also a stack operation, needing 4 bytes of code and 2 bytes of stack.
etc. pp.
Oh, one more, not so secret, hint: 8080 supports all jumps as conditional; that includes CALL
and RET
.