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
loop:
LD A,(HL)
OR A
JP Z, end
CALL print_char
INC HL
JP LOOP
end:
HALT
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)
loop:
INC HL
LD A,(HL)
CALL print_char
DJNZ loop
end:
HALT
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.