Questions regarding Z80 assembly and Z88DK

582 views
Skip to first unread message

Richard Kiernan

unread,
Aug 21, 2022, 6:41:43 PM8/21/22
to RC2014-Z80
One of the objectives I have had with my RC2014 is to learn Z80 assembly language and while the in-line assembler in SCM has worked well so far, I've contemplated that as programs grow larger and more complex, I'll want a cross-compiler of some sort. I've managed to compile and set up Z88DK fine and tested it with a couple of C programs along with some basic assembly - took a bit of time to work out some of the conventions to get the code to assemble, but I got a result that worked as expected. I do have some questions, though, regarding this:

1) I'm compiling/assembling my code with -subtype=basic, which by default provides a hex file that starts at 0x9000. I can see the pragmas that should be defined in a C source file for SCM, but are there any equivalent statements that can be made in an assembly source file to do the same thing? I can define the pragmas on the CLI and it works, but I'm wondering if there's a more portable way to do this within the source file itself.

2) I've noticed that the code that I've assembled produces rather large binary files once the assembly is done and while I understand that the process is probably bringing in libraries from elsewhere, I'm wondering if this is in any way modular and whether I can choose to drop certain libraries for the sake of smaller binaries.

3) A more general question about Z80 assembly language - I'm looking at Z80 Assembly Language Programming by Leventhal and taking a look at some of the examples. I understand that these examples do need to be changed for valid address locations for the RC2014, but I was wondering if there was some way to assign data values to certain addresses prior to running the examples. Or whether I should be doing this at all or instead creating some sort of initialising subroutine to set the values in certain memory addresses prior to running the code, or using labels instead, for example.

Alan Cox

unread,
Aug 21, 2022, 7:35:15 PM8/21/22
to rc201...@googlegroups.com
> 3) A more general question about Z80 assembly language - I'm looking at Z80 Assembly Language Programming by Leventhal and taking a look at some of the examples. I understand that these examples do need to be changed for valid address locations for the RC2014, but I was wondering if there was some way to assign data values to certain addresses prior to running the examples. Or whether I should be doing this at all or instead creating some sort of initialising subroutine to set the values in certain memory addresses prior to running the code, or using labels instead, for example.

The conventional way to do this is to define those in the program not
as specific memory addresses. The syntax varies by assembler but you'd
usually have something like


fred:
dw 33 ; define word (dw, defw, .word .. assemblers never
can agree)

foo:
ld hl, (fred)
add hl,de
ld (fred),hl
ret

It's much much easier working with a CP/M setup as you can then use
ZMAC and VDE or similar on the Z80 machine to do the work rather than
uploading big hex files.

Phillip Stevens

unread,
Aug 21, 2022, 10:01:43 PM8/21/22
to RC2014-Z80
Richard wrote:
I've managed to compile and set up Z88DK fine and tested it with a couple of C programs along with some basic assembly - took a bit of time to work out some of the conventions to get the code to assemble, but I got a result that worked as expected.
 
If you've gotten that far, congratulations. That's the biggest step done.

I do have some questions, though, regarding this:

1) I'm compiling/assembling my code with -subtype=basic, which by default provides a hex file that starts at 0x9000. I can see the pragmas that should be defined in a C source file for SCM, but are there any equivalent statements that can be made in an assembly source file to do the same thing? I can define the pragmas on the CLI and it works, but I'm wondering if there's a more portable way to do this within the source file itself.

Yes there is. If you're working in assembly only then the recommended method is that all #pragma statements should be located into a single file, say zpragma.inc and an linker option added: -pragma-include:zpragma.inc.
Otherwise the #pragma statements can be located in a C file, as you'd have seen in examples or in other examples here.

This answer is taken from the wiki entry on pragmas. As usual the documentation is terse, but often good enough. Don't hesitate to ask if something is unclear.
 
2) I've noticed that the code that I've assembled produces rather large binary files once the assembly is done and while I understand that the process is probably bringing in libraries from elsewhere, I'm wondering if this is in any way modular and whether I can choose to drop certain libraries for the sake of smaller binaries.

If you're working in C, then which library routines are linked will depend on which function you call. It is completely modular, in that each library function is linked into the final binary only if it is needed.
You can minimise the size of the linked stdio printf() format conversion by using a #pragma to identify exactly which format conversions you need. The format for this is noted in the wiki.
 
3) A more general question about Z80 assembly language - I'm looking at Z80 Assembly Language Programming by Leventhal and taking a look at some of the examples. I understand that these examples do need to be changed for valid address locations for the RC2014, but I was wondering if there was some way to assign data values to certain addresses prior to running the examples. Or whether I should be doing this at all or instead creating some sort of initialising subroutine to set the values in certain memory addresses prior to running the code, or using labels instead, for example.

Yes you can and should use labels or directives, as Alan notes there are many directives understood by z88dk-z80asm. They are noted in the wiki here.
Fortunately, z88dk-z80asm is quite catholic in its understanding and accepts (for example) all of DW, DEFW, WORD to mean the same thing.

It can also understand the size and type of a FLOAT to generate the correct hex value (4 byte or 6 byte in different formats) depending on which library is being targeted.

 The overall wiki entry on z88dk-z80asm is worth a read too, as it is much more than just an assembler.

Once you get beyond single file projects, it is worth looking at the method used to define large amounts of constants in z88dk. The M4 tool is used to generate header files.
Three different files are produced from the configuration files. One for (private) assembly usage. One for assembly but definitions are made PUBLIC.
And, one as a C header file, that has constants referred to by the overall rc2014.h header file which generates headers with the specific decorations needed by each compiler, sccz80, sdcc, or, clang as required.

Typically, for each target (eg +rc2014) each device comes with its own definitions and constants, and these are located by a config_target.m4 entry into the right address range.

Hope that's useful.

Cheers, Phillip

Richard Kiernan

unread,
Aug 22, 2022, 9:58:51 AM8/22/22
to RC2014-Z80
OK, that was very helpful. I understand how the pragmas fit into this and I think I understand the idea behind using labels/directives to assign values; if I'm understanding right, this would be equivalent to declaring/defining variables (int, char, short, float, etc.) in C, where referencing the variable name itself produces its memory address (like if you were to use the & address operator in C) and referencing the variable name in brackets produces the value assigned to that address. I can understand this for single assignments (although I gather that you can assign more than one value of the same size in the same label by separating the values with commas), but is there a way to produce an array of a given size? Would it be correct to use defs, then relative addressing for each element of that block of bytes?

I think I've answered the questions I had about larger assembly-only binaries than expected reading about the --no-crt command line switch, which also requires manual assignment of the memory map and a separate step to make the HEX file, which is fair enough. But it does raise another question. If we take the example of a program from the first chapter of Leventhal, using the --no-crt option, I'd write it like this:

"zmap.asm"
SECTION CODE
org 0x8000

SECTION DATA
org 0xa000

"test.asm"
    SECTION CODE    
    PUBLIC start

start:
    ld a,(foo)
    ld b,a
    ld a,(bar)
    add a,b
    ld (baz),a
    ret

    SECTION DATA

foo:    defb 3
bar:    defb 4
baz:    defb 0

This allows me to easily locate the input and output values for the program, with foo being assigned to $a000, bar to $a001 and baz to $a002. I'm wondering if there's a way to do something similar when using the crt, since I've tried setting the org parameter, but this doesn't seem to work for me.

Phillip Stevens

unread,
Aug 22, 2022, 10:10:33 PM8/22/22
to RC2014-Z80
Richard wrote:
If I'm understanding right, this would be equivalent to declaring/defining variables (int, char, short, float, etc.) in C, where referencing the variable name itself produces its memory address (like if you were to use the & address operator in C) and referencing the variable name in brackets produces the value assigned to that address.

Yes, Zilog were quite switched on to make their mnemonics like that. They are quite consistent throughout their Z80 opcode table.

I can understand this for single assignments (although I gather that you can assign more than one value of the same size in the same label by separating the values with commas), but is there a way to produce an array of a given size? Would it be correct to use defs, then relative addressing for each element of that block of bytes?

Yes, you can use relative addressing for an array, based on DEFS. Here's an example of this for the ACIA Serial Tx Buffer. Note the ALIGN and SECTION directives to place the array into the correct address range (an assumption to optimise driver performance). ALIGN can be used to either provide a relative alignment, or an absolute alignment where the value provided is a 16-bit address.

I think I've answered the questions I had about larger assembly-only binaries than expected reading about the --no-crt command line switch, which also requires manual assignment of the memory map and a separate step to make the HEX file, which is fair enough. But it does raise another question. If we take the example of a program from the first chapter of Leventhal, using the --no-crt option, I'd write it like this:

"zmap.asm"
SECTION CODE
org 0x8000

SECTION DATA
org 0xa000

Yes, using maps is the right way to do this. There is a default map for all the z80 newlib targets, but it is easy to provide your own based on a configuration switch.
 
"test.asm"
    SECTION CODE    
    PUBLIC start

start:
    ld a,(foo)
    ld b,a
    ld a,(bar)
    add a,b
    ld (baz),a
    ret

    SECTION DATA

foo:    defb 3
bar:    defb 4
baz:    defb 0

This allows me to easily locate the input and output values for the program, with foo being assigned to $a000, bar to $a001 and baz to $a002. I'm wondering if there's a way to do something similar when using the crt, since I've tried setting the org parameter, but this doesn't seem to work for me.

You can either put your code or data into a predefined SECTION, defined in the default memory model, or you can add your own memory model to the end of the existing one. Note that the CRT will assume that BSS SECTIONS are zeroed (initialised to zero) at startup, unless the label is placed into the uninitialised SECTION. The C compilers will automatically place uninitialised variables into BSS.

When linking there are warnings for the memory spaces configured, where code or rodata grows beyond 0x8000 as this is the expected end of ROM (but not for RAM subtypes like basic that don't use ROM.).

Note that the ORG of 0x9000 for the +rc2014 -subtype=basic is historical, as this allows space for the hexload BASIC program to be loaded  below an eventual assembly or C program.
It is quite ok to move the org down to 0x8400, if you need more space.

Cheers, Phillip


Douglas Miller

unread,
Aug 22, 2022, 10:36:40 PM8/22/22
to RC2014-Z80
Note that using labels in assembly language is pretty standard across the whole industry, and not specific nor related to Zilog's CPU architecture or mnemonics. In typical Z80 assembly languages, the following would result in "3" being in the A register:

foo      equ      3
...
         ld       a,foo

while the following also results in "3" being in the A register but also means that "3" is in the *memory location* labeled "foo":

foo:    defb    3
...
        ld      a,(foo)

but beware, the Z80 instruction set is not completely symmetrical - for example, "ld b,(foo)" is not a valid Z80 instruction.

The first example takes up two bytes in the program, while the second example takes up four bytes and costs extra cycles to execute. One case where you need to use the second example is where 'foo' is actually a variable that is modified as the program runs. If 'foo' is a constant, then the first example is probably the better one. Of course, the second example is just one method to get the contents of 'foo' into a register, there are others.

In "C" parlance, the first example is more like using a "#define foo 3" whereas the second is more like "char foo = 3;".


Reply all
Reply to author
Forward
0 new messages