First Z80 project after 6502, a simple looking piece of code giving me hard time.

print_char is a working function which prints contents of A to the screen.

My first issue is that the following code (and many variations of it) gives "index offset out of bound" error with VASM. Any idea on how assembler checks index offset on this occasion and picks up an error ?

Is there a better way to print a string ? On 6502 LDA was checking for affecting zero flag, this is not the case with Z80, so I am trying to or 0 and checking result against zero, but then using B register as or changes A. Doesn't look very elegant.

    ld A,(IY+msg)
    ld b,a
    or 0
    jp z, end
    ld a,b
    call print_char
    inc IY
    jp loop

msg: .asciiz "Hello World"

3 Answers 3


Since it's two questions, here are two answers:

Adressing Issue

It's a very BASIC 65xx to x80 transition error: Index Registers

Where 65xx CPUs use a 16 it base address (from memory) and an 8 bit index (from register), the x80s use 16 bit index register(s) and an (optional) 8 bit offset. The address of msg should be loaded into IY first (LD IY,msg) and then this register can simply be used to address the bytes to be loaded (LD A,(IY)).

Historically the x80s simply use one memory pointer, in classic 8008/8080 notation called M, residing in the register pair HL. All memory access that is not immediate or absolute had to go thru this. So what's a LD A,(HL)on a Z80 would be written MOV A,M for 8080 (*1). DE could be used to hold a second pointer to be exchanged with HL when needed. The Z80 did relax this a bit by adding IX/IY.

All addressing is done using such a pointer (and optionally on the Z80 a short offset). Similar the 6800 where the generic 16 bit index register IX was to be used.

When the 6500 series was designed by former Motorola engineers, they flipped the concept by putting the base address not in a register but in memory and adding a (short) 8 bit register as index. They even marketed it as an advantage, being a 'true' index now. Well, I guess that's debatable.

So, switching from 65xx programming to x80 programming, also includes the switch between the base address held in a register or in memory.

For all practical purpose, think of HL, IX, IY on the Z80 as pointers like in C. Just have them point somewhere, increment or decrement them, add or subtract a value and that's it. No complex addressing. That's as well the reason C compilers are much less work to be implemented on an x80 or 68xx CPU than on a 65xx. They fit the simple abstraction C makes of a CPU way better than the fine tuned 65xx structure.

*1 - Which is a way to avoid complex notation. For the 8008 it was even less ambiguous as LAM - for more history see this answer

Code Optimization

Is there a better way to print a string?

Depends on your environment.

On 6502 LDA was checking for affecting zero flag, this is not the case with Z80, so I am trying to or 0 and checking result against zero, but then using B register as or changes A. Doesn't look very elegant.

Not really sure why you're using B and OR 0 (*2). The most simple way would be simply ORing A to A as in OR A. Next HL might be more performant, as it doesn't need a prefix. In general, most Z80 features come with a price of a code prefix overhead and thus slower execution. So preferred pointer register is always HL, seconded by DE.

A straightened code might look like this:

    LD   HL,msg 
    LD   A,(HL)
    OR   A
    JP   Z, end
    CALL print_char
    INC  HL
    JP   LOOP

msg: .asciiz "Hello World"

This code is as well clean 8080 code, so will run on any x80 CPU.

Then again, and in this the Z80 is still better than C assumes, it allows to count and jump in one instruction, which makes length terminated strings way more applicable than zero terminated (*3). So a real Z80 like string handling would be like this:

    LD   HL,msg
    LD   B,(HL)
    INC  HL
    LD   A,(HL)
    CALL print_char
    DJNZ loop

msg .byte 11,"Hello World"

Now the inner loop is only 4 instructions and as fast as it can get.

In fact, this is another 65xx to Z80 mentality step to be taken. Zero delimited strings come (somewhat) natural on 65xx (*3), as they set the flags accordingly to a character loaded, while Z80 works best with length delimited, due the implied usage of B as counter register.

I'd strongly recommended doing that step and go full Z80 on this.

*2 - Here lies another benefit/pitfall when moving between these CPUs - the 8080 is way more orthogonal implementing all register to register variations than the 6502, including otherwise strange ones like ORing or ANDing A with A.

*3 - Which they are anyway. Length terminated strings do not have issues with any embedded data.

*4 - As inherited from its real ancestor 6800 and spiritual ancestor PDP.

  • Thanks a lot for the explanation, it makes sense now. And yes, OR A is way more elegant. That mess with B was me trying to preserve A's value while doing OR 0, naturally OR A removes this need.
    – Charles
    Mar 24, 2021 at 16:10
  • 1
    Another trick is to have a function begin and end with ex hl,(sp), in which case the message must immediately follow the call to the print function, but the calling code wouldn't need to load HL.
    – supercat
    Mar 24, 2021 at 19:19
  • 1
    Now that strings are length-encoded, a natural step woud be automatic length assignment through some type of macro, otherwise it is way too error-prone.
    – lvd
    Mar 25, 2021 at 8:12
  • 1
    My memory is evidently failing - I was sure I had to deal with that when using an Intel assembler, having learnt Zilog first. It must have been 6800 that's the other way around... Mar 25, 2021 at 16:25
  • 1
    @TobySpeight Well, we all grow oder ... err .. wiser :)) 6800 uses a single operand syntax (like 6502, or 68k). What uses that wired s,d syntax is the GNU Assembler, as it grew out of an AT&T assembler. Until recently it forced this as well onto x86 programmers. Maybe that's where you picked it up.
    – Raffzahn
    Mar 25, 2021 at 16:43

LD A, (IY+msg) looks fishy; on the Z80 IX and IY are 16-bit registers and the in-opcode offset is 8-bit. Sort of the opposite of absolute indexed addressing mode on the 6502. So you'd idiomatically load IY with the address of msg and then ld A,(IY+0). And if you're not using the offset, you might then consider (HL) instead for a more compact and faster program.

So I'm guessing that your code fails because msg sits at least partly outside of the address range [-128, 127] of whatever you seeded IX with.

  • 4
    An addition - the 8-bit offset is signed value.
    – Vlad
    Mar 24, 2021 at 15:09
  • 1
    @Vlad ugh, you're right, I'd forgotten about that. It affects my second paragraph, so: edited. Thanks!
    – Tommy
    Mar 24, 2021 at 15:23
  • Moving msg closer to the index resolved compiler error, thank you !
    – Charles
    Mar 24, 2021 at 16:07

A handy way of outputting a string on the Z80 is to use something like:

  ex (sp),hl
  ld a,(hl)
  call putchar
  inc hl
  ld a,(hl)
  or a
  jr nz,primmlp
  ex (sp),hl

The message to be output must immediately follow the call instruction, be at least one character long, and be followed by a zero byte and then the code that should be executed after it finishes. This function will fetch the return address, output bytes there until it sees a zero byte, update the return address to that of the zero byte, and return. Note that while although return address will be one the address of the trailing zero byte rather than the next "real" instruction, letting the zero byte execute as a NOP is faster than adding another INC HL between the LD A,(HL) and the EX (SP),HL instructions to prevent such execution.

  • 1
    1. No need to do ex [sp],hl twice if HL is not to be saved: just pop hl and finally jp [hl] without ret. 2. Basically intermixing text messages with code is not very good style.
    – lvd
    Mar 25, 2021 at 8:16
  • @lvd: 1. Replacing the second EX (sp),HL with POP HL would make execution slightly faster if the client code doesn't care about HL, but using the EX means that there will be no need for client code to save HL if it does matter. 2. This approach saves three or five bytes at each call site, depending upon whether the client would need to maintain the HL value. While interleaving code and data is not without downsides, writing something like "RST PRIMM / DB "Hello there",0" is easier to write and read than "PUSH HL / LD HL,HelloThere / RST PRIMM / POP HL" and then...
    – supercat
    Mar 25, 2021 at 15:07
  • ...having to place the text of the message somewhere else. Even if an assembler allows interleaving content for code and data segments, doing so will make it necessary for the assembler and linker to do more work. Further, on some assemblers, if one switches to a data segment and wants to switch back to a code segment, one will have to know which code segment one was using and make certain one switches back to the right code segment. Using the PRIMM technique (which, credit be given, I first saw in the Commodore 128 Kernal even though it works better on the Z80) avoids such issues.
    – supercat
    Mar 25, 2021 at 15:10
  • 1
    'easier to write and read' -- unless you have hundreds of such messages scattered along and mixed with the code, and then want to change some of them. Assembler considerations could be valid in 1980ies but not now. In most cases there is no even need in artificial segmenting the code and splitting compilation into assembling and linking, as the PCs now are blazingly fast and have TONS of memory.
    – lvd
    Mar 25, 2021 at 17:03
  • @lvd: Are you suggesting that it's easier to have a section of the source code which holds all the messages than to place messages in source code near the places where they are used? That may facilitate some tasks like internationalization, but in general it's more convenient to have messages located in source near the point of use. On some platforms, segregating code and data may offer performance advantages, and it certainly makes disassemblers easier to use, but on the Z80, unless one is interested in having people disassemble one's code, this approach has mainly upsides.
    – supercat
    Mar 25, 2021 at 17:37

You must log in to answer this question.

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