3

Am I missing something or is Z80 weird in its use of the B and C registers?

If I want to copy memory, I have this super cool LDIR instruction which copies (HL) to (DE) BC times and incrementing HL and DE in each iteration while decrementing BC and ending if DEC BC is Z. So, here in BC the B register is the high byte.

But the DJNZ loops will use the B register as a low byte.

This is a weird asymmetry. Why would they not have used the C register as the loop register then? Was that some holdover from previous 80xx CPUs?

2
  • 1
    Actually the question looks like 'why there's exactly X in (zp,X) and Y in (zp),Y addressing modes?' :)
    – lvd
    Aug 17, 2022 at 16:22
  • The last question: Yes: djnz seem to be supported by 8080, so Z80 had to use B for compatibility to 8080 because Z80 is intended to be compatible to 8080. Aug 19, 2022 at 20:05

3 Answers 3

13

Both memory and I/O instructions that use C as part of an address output C on the lower part of the address bus and B on the upper part (which will often be ignored by the outside system, but is output nonetheless). Allowing B, D, or H to be fed to the lower part of the address bus would have required either adding extra circuitry or adding an extra cycle to I/O instructions to route the data. If C is going to be used to identify an I/O port, B is the register that would be least likely to "get in the way" if used as a loop counter.

What's perhaps more curious is the decision to have LDI/LDIR/LDD/LDDR use BC as a counter instead of just using B. From a marketing standpoint, saying a chip can copy 65,535 bytes in a single instruction may be appealing, but using a 16-bit counter wastes two cycles per byte copied. Using an 8-bit inner-loop counter and then having application code use a second 8-bit "high byte" counter if necessary would yield better performance with less hardware.

1
  • 2
    Quite true. Also important to keep in mind that the Z80 was a quick design to keep the new company afloat until the real product, the Z8, could be delivered. 8080 compatibility was not only mandatory as sales argument, but it's internal structure was quite ... err ... compatible :)) This internal structure makes B always the high byte when used as BC. Likewise B was the only 8 bit register not otherwise assigned when filling the last single opcode spot (10h) with an integrated counter/branch.
    – Raffzahn
    Aug 16, 2022 at 20:50
7

In addition to Supercat's spot on technical description it might be helpful to look at requirements during ISA design:

Registers are Specialized

Z80 registers are specialized. Much like A, X and Y have different features on a 6502.

Inherited Basics

Most of this is inherited straight from the 8080:

  • A - General purpose accumulator
  • HL - Primary memory pointer
  • DE - Secondary memory pointer (XCHG/EX HL,DE)
  • DE/BC - Ìndex pointer (STAX, LDAX)

H, D and B are always the higher byte of any 16 bit value.

Faggin had to arrange any new features around this.

Finding a 16 bit Counter

Having a 16 bit counter for the repeated instructions will of course make them more versatile than only 8 bit. Doing so will require use of either one of the register pairs or the new IX/IY register. While it would have easy to use either, doing so would negating the advantage of the second register set for interrupt routines (*1), at least when they need to use any of the repeating instructions, like INIR/INDR, which are especially great during interrupts.

Of all 16 bit register pairs BC is the least versatile, so naturally the best to be used as counter for block operations.

Finding a 16 Bit I/O Register

Faggin put a great emphasis on improving the 8080 for embedded application (*2). Here the focus was faster, more versatile I/O and of course faster interrupts. Beside allowing to direct in-/output any 8 bit registers and fast block I/O, Faggin extended the I/O address space from 8 to 16 bit address size.

This could have been done by simply defining an IN and OUT instruction with immediate 16 bit address (*3). Then again, working with dynamic addresses would be a great plus for systems with several ports of the same kind, like multiple serial interfaces. So using a register pair it was. And like before BC was the optimal choice.

But Where to Put a Counter for Block IN/OUT

Those block instructions are a real great thing - even more for I/O where they can, supported by fast interrupt handling, save the need for a DMA controller. But this brings the need for a counter register. As before HL/DE are otherwise prioritised, so BC is again the only viable choice. Which conflicts with the use of BC as 16 bit counter. So what to do without screwing speed gained?

Since 8080 I/O was based on an 8 bit address space, a way out of this dilemma was to make the block I/O using only an 8 bit port address and use B as counter for up to 256 bytes to be transferred. Yes, this would mean that any system using block-I/O-instructions could not use the full 16 bit I/O address space. But I guess that was seen as a minor inconvenience - at least when keeping in mind that the main (and only) competition, Intel's 8080, had only 8 bit I/O address space anyway.

Isn't There One Opcode Left for a Relative Jump?

When looking at the new relative jumps (18h/29h/28h/30h/38h) shows one empty spot at 10h, where a jump never would be. Perfect for adding a simple loop mechanic: DJNZ. For this an 8 bit register is is not only enough, but the best choice. Most loops take less than 256 rounds - and if they have to, any overhead is minor, occurring only every 256 iterations (*4).

B ist predestined again. Not only as the sequence to decrement and test B is already needed for block-I/O, but it as well keeps C available for a variable I/O address. Essentially the DJNZ comes for free, as its mechanic is already part of all repeating I/O instructions.

Bottom Line: It's a compromise between various requirements while preferring flexible I/O


*1 - Speeding up an interrupt routine is the reason for the second register set to exist. By executing only two instructions (EX AF/EXX) the whole register set of any foreground process gets saved - that's why these also got two of the very rare single byte opcodes (80h/D9h).

*2 - After all, control applications, what we call nowadays 'Embedded', are what essentially all early micro processors were intended for. No matter if Motorola (6800), Intel (8080), MOS (6500) or Zilog (Z80), they all made their business not with PCs, not even with consoles, but control. Of the 5-10 billion 65xx cores, less then 20% ended up in such devices.

*3 - See below why usually only C is attributed as I/O address (in addition to 8080 compatibility).

*4 - Quite the same idea why the 6502 had only 8 bit wide index registers.

5
  • 2
    The 6502's use of 8-bit index registers not only saved hardware, but it also shaved a cycle off the time required for most indexed reads. Using 16-bit index registers would have made it necessary for the CPU to spend a cycle between fetching the upper byte of the constant adder and being able to perform the requested read. As it is, the 6502 can start performing a read while it adds 1 to the fetched upper byte of the address register, and then decide to either use the newly-read value or junk it.
    – supercat
    Aug 16, 2022 at 22:46
  • 2
    BTW, I wonder how much silicon would have been required to shave 4-5 cycles off the cost of (IX+d) or (IY+d) addressing in the cases where d was zero? It seems a real shame that in most code that uses IX and IY, the Z80 will spend five cycles on every access adding zero to every value. Alternatively, given that the Z80 uses a 4-bit ALU, ...
    – supercat
    Aug 17, 2022 at 16:17
  • 2
    @supercat, the very same 5-cycle penalty of adding sign-extended byte to 16-bit value also presents in every relative jumps insn like JR or DJNZ and even in LDIR-like insns (LDI is 16 clocks, LDIR is 16+5=21 clocks)
    – lvd
    Aug 17, 2022 at 16:25
  • ...I wonder how much it would have cost to have an opcode for indexed-mode addressing where one nybble of the displacement byte would select a register to use and a sign-extension bit, while the other represented a displacement of -16..+15. From what I understand of the internal architecture, this would with relatively minimal work make it practical to support (BC+d), (DE+d), (HL+d), and maybe (SP+d) and (PC+d) addressing modes in addition to those using IX and IY. If the prefix for displacement mode were only used for that purpose, the effective address calculation could overlap...
    – supercat
    Aug 17, 2022 at 16:26
  • ...with the main opcode fetch, at least when using a base other than PC or SP. @lvd: The five-cycle penalty with djnz is somewhat reasonable given that the instruction is generally used with a non-zero displacement. I would think it should have been possible to leverage the 16-bit ALU to shave a cycle off the time to handle the upper byte, but the design at least makes sense. As for LDIR, that instruction is just sad. I would think there would have been many ways to perform memory copies that were both faster and cheaper, or much faster for minimal extra cost.
    – supercat
    Aug 17, 2022 at 16:34
0

Several ways to have 16-bit-counted loops:

classical way:

 DEC   BC   ;actually any register pair here except SP
 LD    A,B
 OR    C
 JR/JP NZ,loop

advanced way:

 DEC  BC ;initial value for BC is somewhat weird
 INC  B
 DJNZ loop

Additional INC HL

 CPI         ; makes CP A,(HL), then HL=HL+1, BC=BC-1, then checks BC for zero
 JP  PE,loop ; note the condition!

or more straightforward nested approach:

 DJNZ  LOOP
 DEC   C
 JR/JP NZ,LOOP
3
  • Just your last "more straight-forward" approach, doesn't that nicely illustrate my initial "complaint", i.e., you use DJNZ decrementing B in the inner loop, then you decrement C in the outer loop. Isn`t that backwards? And doesn't this just destroy the usefulness for DJNZ in a rational 16 bit loop if you have to do DEC C JR Z, loop in the inner loop? Aug 18, 2022 at 6:20
  • 1
    @GuntherSchadow: In the nested-loop scenario, B and C wouldn't be a meaningful 16-bit value even if one used C for the inner loop, because if BC started holding 0280h, then after 127 loops it would hold 0201h, then 0100h, then 01FFh, and down to 0101h on the last iteration. So having B as the inner-loop index makes just as much sense as using C or any other register for that purpose.
    – supercat
    Aug 18, 2022 at 20:31
  • @GuntherSchadow just look up my examples, there's a 16bit loop using DJNZ. Also 'complaining' about 'weirdness' has almost no sense: every CPU has its own places of weirdness (just remember how weird is 6502!) and your task is to make that weirdness serve you, not complain about it )
    – lvd
    Aug 19, 2022 at 8:30

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .