4

New to Z80, coming from 6502, the instruction set appears hugely CISCY. And yet, I cannot find a simple method to inspect the PC (transfer PC to a general purpose register, e.g., HL). I need this to write 100% relocatable code and make subroutine calls, like so:

; HL contains my jump address
  LD   BC, HL    ; save the destination
here:
  LD   HL, PC    ; find my current location
  LD   DE, there - here ; determine distance to return address
  ADD  HL, DE    ; compute absolute return address
  PUSH HL        ; save return address to stack
  PUSH BC        ; call target to stack
  RET            ; jump to call target
there:
  ...            ; continuation of the program

This would allow me to make an indirect call and do so 100% relocatable, i.e., I can copy this code anywhere in memory and jump to it and it will work.

The only way I know how to inspect your own PC is to make a CALL and then inspect the stack. But then you need to know your location or else use a "scratch pad" region of RAM to jump to. For example,

LD $1234, $C9 ; write RET into a fixed scratchpad location
CALL $1234    ; call it

but now what? I can't even find a way to inspect my own SP! I see an LD SP, IX instruction but not the inverse! How can I even inspect my own SP to inspect the stack?? What if I have local variables on my stack frame?

I must be missing something.

Background:

I am building a Z80 computer from scratch. I have it all wired up with 64k RAM (even 128, with A16 tied to low right now) and a write-through ROM at $0000 to $2000, memory writes go to the RAM "under" the ROM. An Arduino Nano serves as the only IO device right now and communicates through IN and OUT operations one byte at a time. Serial line connected I will be able to interact with the machine. Including writing the EEPROM in place (so I don't have to take it out ever again, unless I "brick" it).

Initially there are very simple service routines just copy a sequence of IN to a memory range, copy a memory range to OUT, and copy one memory range to another, and jump to any address. When re-programming the EEPROM, I need to move the monitor code to a different place in RAM, load the new code to another place in RAM, check it, change a couple of jumpers to activate EEPROM write, then copy the new code to the ROM. Final goal would be a CP/M bootstrap.

Anyway, to be able to move the monitor code around, I need it to be 100% relocatable, and I want to make calls, to subroutines that then can return to where they have been called from.

13
  • 1
    The inverse of LD SP,HL is the following: LD HL,0 : ADD HL,SP (or IX/IY likewise). This code (with IX or IY) is actually used to reach local vars or parameters.
    – lvd
    Commented Aug 15, 2022 at 8:07
  • Also I feel like the best way to have a relocatable code is actually to relocate it before running, but not poisoning its every call or long jump with PC-relative mess :)
    – lvd
    Commented Aug 15, 2022 at 8:10
  • @lvd don't forget that there are no short jumps or relative ones at all, so each and every conditional has to be synthesised some way without using conditional ones. Doing all of that in ROM will be a truly Sisyphean task
    – Raffzahn
    Commented Aug 15, 2022 at 10:16
  • BTW: I like the thruout description what you need and why. +2 for that :))
    – Raffzahn
    Commented Aug 15, 2022 at 10:31
  • @Raffzahn It seems I haven't got your comment about short and relative jumps. Actually Z80 has both short relative and long absolute jumps -- that includes both conditionals and unconditionals. What I'm saying here about is that it is faster (for the code that will run in RAM) to relocate in advance at the loading stage than to suffer in runtime :)
    – lvd
    Commented Aug 15, 2022 at 11:14

3 Answers 3

8

Regarding your ultimate target, writing 100% relocatable code – it is impossible on Z80, at least in general.

However, if you have two bytes representing the following code POP HL : JP [HL] in ROM at the fixed address – you are lucky, just call it and know your PC in HL.

Another option is to have some guaranteed free RAM bytes at the known address where you can put that sequence and call it likewise.

As I said before, in general case, where any of the assumptions above are not true, there's simply no way to know your PC.

1
  • 2
    Regarding your "it is impossible" comment, I think you haven't thought through how clever some people can be. It would be quite easy to assemble every module of code at location zero and have all calls done through a shim that adjust the calling address based on where the module was loaded. Or you could go the BCPL global vector route where each module populates the vector based on its actual load address and all calls are through that vector (no direct calls allowed). People can be damn sneaky when they want to be :-)
    – paxdiablo
    Commented Aug 15, 2022 at 11:37
3

Beside this being a duplicate and in addition to LVD's spot on answer maybe a few remarks regarding your problem:

Preface:

As he already mentions, Z80 (or better 8080) code is not made for being relocatable. That's something that didn't come to mind when designing it. Everything, even branches, is absolute.

It's the reason why the TPA in CP/M starts at 0100h and MP/M running more than one program needs them to be paged in at address 0000h (*1). CP/M Programs are not relocated when loaded, but simply read in.

The Z80 relaxes this a bit by introducing relative jumps for unconditional, carry and zero, but only with a signed 8 bit offset, so still leaving all other to be relocated.

Do It Yourself

If an 8080 program has to be relocated, then it must be done by an internal or external relocator. Much like on a 6502 (*2). Yes, within limits 6502 code can be made relocatable, but it's a dirty job. But not as dirty as on an x80. Having relative branches does help quite a bit :))

The limited case of a relocator is quite similar to what Apple DOS did. Have

  1. a table of all target addresses that need to be patched (in your case all within the ROM size)
  2. a table of code regions to be patched (to avoid patching data)
  3. Walk through all code in the marked code regions looking for
    • 11xxx010b
    • 11xxx100b
    • 11000011b
    • 11001001b (11xx1001b for 8080)
    • (11001011b only 8080)
  4. Add to the next two bytes whatever the relocation distance is.

Done. The last step is eased if the base address of the ROM would be 0000h, right?

  • Writing a relocator stub moving your monitor to its target address in RAM might be the solution.

Interlude

As already mentioned, any attempt to make 8080 code relocatable without relocation is an extreme fruitless job. The x80 does not have any kind of relative addressing, so

  • All JMP need to be either direct via LD HL,adr/JMP [HL] pairs or via a table LD HL,table+x/JMP [HL]
  • All CALL need to be emulated by some way of going through some call gate
  • Some derivative way to do conditional jumps has to be developed, as these are, on an x80, always absolute 16-bit addresses.

The last may in fact be the trap you took when coming from a 6500 - assuming that the x80 offers relative branches in all cases. Surprise, it doesn't and the Z80 only offers very limited relief by adding relative (8-bit signed offset) versions for unconditional jump and jumps on Carry or Zero set or cleared. While this may help in short code sections, it will not really eradicate the need for relocation of long jumps. Absolute jumps are still needed for longer distance and/or conditions based on other flags than Zero and Carry.

So for anything other than very short cases, making relocatable code for an x80 is quite a task

Still, let's look at ways to get at least your own address.


Know Yourself

Doesn't that ROM always start at address 0000h after RESET? If so, wouldn't that make every address as it is during compile?

  • It always starts at 0000h

Remember Yourself

Now, if you insist on not relocating the ROM, but wasting code on runtime retargetting, then why not store the new, relocated base address to be used for later action? When resetting, the ROM will be at address 0000h, right? So pick some RAM location to store that base value and have all jumps (and other ROM access) use it.

  • Use a RAM location to keep track of the new ROM address.

Know It Yourself

Next, while there is no way to access the PC directly, there's always the 'Known RTS' trick like used on an Apple II by I/O cards to determinate their position/slot they are in. It's based on jumping to a known return instruction in Monitor-ROM (JSR $FF58), thus delivering the PC on the stack. Accessing the stack pointer on an 8080 is done by simply adding it to HL (which also answers your question about SP (*3)). The address is stored at SP-2, so

CALL  KNOWN_RET
LD    HL,$FFFE
ADD   HL,SP
LD    HL,[HL]

will get you your return address.

Since it's your ROM, you may place that RET at any location you like. See below at Know Where for some idea.

  • Create your own Known-RTS as part of your ROM

Know It Faster

Of course, since the ROM code is all yours, it's possible to directly return the address from a function located at a known address. Here the mentioned

POP    HL
JP     [HL]

will do the trick; best of all, it's only two bytes (E1h, E9h).

  • Have a fixed 'Get-My-Addr' function as part of your ROM instead of a known RTS

Know Where

Since the ROM is yours, it should be fairly easy to put it at a location that always will be accessible. More so, if it's always at the same visible ROM address in all versions; then it doesn't matter if a newer version accesses the code from an older one, or vice versa.

Being for an x80 CPU, I assume your Z80 ROM starts at address 0000h with a 3 byte jump to your reset routine. So why not take the same road Kildall did for CP/M, using the following 5 bytes for some important fixed values/entry points? In your case above, the routine to get a caller's address:

JMP    RESET_CODE
POP    HL
JP     [HL]

Now your ROM may do a CALL 0003h at any point and will get the address after that call returned in HL.

Since this address is the same for all of your ROM images, no address fiddling is to be done.

  • Locate the 'Get-My-Addr' function at a fixed address.

Ask Someone Who Knows

Last but maybe not least, why not ask someone who could look it up, like the mentioned ATmega328? It already monitors the address lines of the Z80 to see if it gets addresses as part of an IN or OUT instruction, by sampling IORQ, right? It could also do so whenever a a code fetch is signalled by M1 being active, except now recording address lines instead.

Unless you want to relocate your ROM on byte borders, it might be enough to just copy the top 8 bits or even less (*4), so maybe only three or four.

A follow up IN (after setting it up by what ever protocol you're using) will report the top 1..8 bits of the actual ROM code location.

  • Let Hardware do the Job

*1 - In CP/M 3 only the first bank must start at 0000h as there are no multiple programs supported.

*2 - That's why there are Master Disks for Apple DOS, they contain a relocator on Sector $A/$B of track 0, loaded into $1B00. The relocator has a dedicated table of all addresses to be patched when moving DOS to a different address.

*3 - Which also answers your question how to access SP: Add it to HL :)) I always found this to be an unexpectedly decent solution, as it allows applying an offset right away. Of course, I assume it has been selected due to being symmetric with the remaining 16-bit adds, while a move would have been a new addition - SPHL being an outlier anyway.

*4 - When that ROM gets moved only by a multiple of its own size, the address patch would be reduced to ORing to the upper byte, as by default all values will have zero bits in these positions which differ after relocation.

2
  • CALL KNOWN_RET LD HL,$FFFE ADD HL,SP LD HL,[HL] -- this is possible but only when any interrupts are not happening. Also better way to do it is CALL KNOWN_RET : DEC SP : DEC SP : POP rp
    – lvd
    Commented Aug 15, 2022 at 11:17
  • @lvd true, except an interrupt between the DEC SP could kill it as well. Then again in a system with (cyclic) interrupts, one would rather go LD HL,-2, ADD HL,SP, HALT, LD HL,[HL]. wouldn't it? SCNR :))
    – Raffzahn
    Commented Aug 15, 2022 at 11:27
2

Haven’t coded for Z80 for years but I would try simply this:

getpc: pop hl
       push hl
       ret

now whenever you call getpc it should load hl with pc (or pc+3?) and continue with your code.

As others suggest you can use jmp (hl) so the same will become:

getpc: pop hl
       jmp (hl)

In case you can not have such subroutine and you are building new HW you might want to add HW for this. Simply add 16 bit LATCH register to address bus, write it with some IO port and read with 2 8bit ports ... so 1 or 2 address decoder and one 2x 8bit LATCH...

Another option is to use memory paging where you can have such subroutine at fixed position at some RAM/ROM page.

6
  • 2
    It's just POP HL : JP [HL]
    – lvd
    Commented Aug 15, 2022 at 8:07
  • @Spektre JMP [HL] is an 8080 instruction (PCHL), so quite basic.
    – Raffzahn
    Commented Aug 15, 2022 at 9:35
  • @lvd yes +1 completely forgot Z80 have jmp (hl) ... your version is better :) (i commented again as the notification string got corrupted god know why)
    – Spektre
    Commented Aug 15, 2022 at 9:40
  • This requires that I have an absolute address for getpc., but true, better than trying to get the SP into HL and do an indirect read from there. Commented Aug 15, 2022 at 12:22
  • @GuntherSchadow yes to overcome that you might add some HW , RAM/ROM paging or exploit some interrupt ... I edited in some stuff
    – Spektre
    Commented Aug 16, 2022 at 12:03

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