All of the buildings, all of those cars
were once just a dream
in somebody's head
Mercy Street - Peter Gabriel


13 minutes read

Pic 1


Although the Zilog ZDS II tools do their job, they definitely show their age. The main bottleneck is the C Compiler which is stuck at C89 making compiling modern C code painful if not impossible. Even when modern C code is C89 compliant it’s still no guarantee to compile (a good example is LUA). The C compiler is rather slow as well.

A while ago I discovered someone wrote a Z80 family (including ez80) back-end for the LLVM/Clang compiler (for the TI84 Cemu project). So I went on a journey exploring to see if it’s possible to replace the old Zilog ZDS II, Windows based, toolchain with purely Linux based tools.

I’ve now come up to a point where I have the smallest possible C program (not even the famous “Hello World”) running on an ez80 and decided to document how I got this far.

Repo’s to consider

There are 2 repo’s on GitHub that I used to get to a working toolchain.

  1. jacobly0/llvm-project : This seems to be a fork from the original The LLVM Compiler Infrastructure where the Z80 back-end code is added. At the moment of writing, this repo contains release 14 of the LLVM toolchain.

  2. codebje/ez80-toolchain : This small repo contains a patch so that the Clang compiler will emit GAS (GNU ASsembler) compatible assembly code. With this patch, we can use the LLVM tools together with the GNU binutils tools which do support the Z80. This repo contains a Dockerfile that will clone and compile the full LLVM toolchain, applies the GAS patch, then downloads the binutils package, compiles and installs it in a Docker image. The patch is meant for Clang release 13 and needs small modifications to be applied for Clang release 14.

Not being a big fan of docker images for a toolchain I’d rather have locally on my file system I decided to write a simple bash script that does the following:

  • Defines a path to an install directory of my choice (by default, this will be in the same directory as this script but you can easily change this to your preference of course).
  • Clone the jacobly0/llvm-project git repo (this gives us the source code for the LLVM/Clang-14 compiler).
  • Apply the GAS patch (which I slightly modified to work for Clang release 14).
  • Creates a build directory inside llvm-project.
  • Creates the CMake make files for llvm-project.
  • Build and install Clang 14. Ninja is used to parallelize the build and will use all available threads of your CPU to speed things up.
  • Downloads the binutils-2.37 source code.
  • Compiles and installs the binutils-2.37 next to the previously compiled LLVM/Clang tools.
  • Performs a smoke test by compiling an absolute minimal C program and dumps the generated assembly code to the console for both unoptimized and optimized code.

Important to know is that for both the LLVM and the binutils tools, the target name is z80-none-elf.

This is a lengthy build! On my PC with a 12c/24t AMD 3900x CPU, the above took around 12 minutes. On my ageing Lenovo X270 laptop (Core i5, 2c/4t @ max 2.4GHz) this took around 6 hours…

Checking the generated code

This is the code used in the smoke test when building the toolchain.

int main(void)
	int	a;
	int	b;
	int	c;

	a = 1;
	b = 2;
	c = a + b;

	return c;
} /* end main */

Assuming the above code is saved as main.c in the bin directory of the toolchain (thus next to e.g. clang). Then we can see the generated assembly with the following command line (-S stops Clang when it has generated the assembly code and -nostdinc to prevent Clang to use the Clang header files, (later on we want to use eZ80 specific header files)):

./clang -target ez80-none-elf -S -nostdinc main.c

A main.s file is generated showing the following assembly:

	.section	.text,"ax",@progbits
	.assume	adl = 1
	.file	"main.c"
	.section	.text,"ax",@progbits
	.global	_main
	.type	_main,@function
	push	ix
	ld	ix, 0
	add	ix, sp
	ld	hl, -12
	add	hl, sp
	ld	sp, hl
	or	a, a
	sbc	hl, hl
	ld	de, 1
	ld	bc, 2
	ld	(ix - 3), hl
	ld	(ix - 6), de
	ld	(ix - 9), bc
	ld	hl, (ix - 6)
	ld	de, (ix - 9)
	add	hl, de
	ld	(ix - 12), hl
	ld	hl, (ix - 12)
	ld	iy, 12
	add	iy, sp
	ld	sp, iy
	pop	ix
	.section	.text,"ax",@progbits
	.local	.Lfunc_end0
	.size	_main, .Lfunc_end0-_main

	.ident	"clang version 14.0.0 ( b1303ec02a1932c74c306658339d3386d7f46b47)"

That’s a lot of code which in the end just returns the value 3 in the HL register pair. Let’s check if the LLVM optimizer can help with the following command line:

./clang -target ez80-none-elf -S -nostdinc -O1 main.c

The generated code is a lot shorter this time. The toolchain saw that the return value could be calculated at compile time and now simply returns 3 in the HL register pair:

	.section	.text,"ax",@progbits
	.assume	adl = 1
	.file	"main.c"
	.section	.text,"ax",@progbits
	.global	_main
	.type	_main,@function
	ld	hl, 3
	.section	.text,"ax",@progbits
	.local	.Lfunc_end0
	.size	_main, .Lfunc_end0-_main

	.ident	"clang version 14.0.0 ( b1303ec02a1932c74c306658339d3386d7f46b47)"

Generating object code

We could feed the above assembly code into the ez80 assembler (ez80-none-elf-as) ourself but we can instruct Clang to do this step itself.

In our bin directory, the eZ80 binutils tools are present, starting with a ez80-none-elf prefix (e.g. ez80-none-elf-as which is the assembler). Since we specified ez80-none-elf as target for the Clang compiler, the Clang compiler will generate eZ80 assembly code and call the binutils tools with the ez80-none-elf prefix instead of the default binutils tools (typically stored in /usr/bin/) that would generate the Intel X86_x64 assembly code.

Use the following command line:

./clang -target ez80-none-elf -Wa,-march=ez80+full -nostdinc -O1 -c main.c
  • -Wa,-march=ez80+full passes the command line option -march=ez80+full to the ez80-none-elf-as assembler.
  • -c stops Clang when it has generated the object code and prevents it to attempt the linking step.

You should now see a main.o file appearing next to the main.c file, which contains the object code. We can check its contents with the objdump tool from binutils. Try out the following command line:

./ez80-none-elf-objdump -d main.o
$ ./ez80-none-elf-objdump -d main.o

main.o:     file format elf32-z80

Disassembly of section .text:

00000000 <_main>:
   0:	21 03 00 00       	ld hl,0x0003
   4:	c9                	ret

Which is indeed the content of the optimized main function.

The linking step

Next to the object code for the main function, we need code to setup the eZ80 as well (e.g. reset and interrupt vectors, Control Registers and Bus Mode Registers for the external memory, etc.) and all functionality a C runtime normally provides (e.g. printf). Luckily, since my main function is so simple, we only need the code which sets up the eZ80 and then calls the main function.

To get familiar with how the Zilog ZDS II IDE sets everything up, I created a simple project with the same main() function. And then let it generate the make files for me. This also had the advantage that I get the correct values for e.g. Control Registers and Bus Mode Registers for the external memory. We will need these for the final linking step.

Under the Project menu select Export Makefile...:

Export makefile.

A dialog box pops up asking for a filename (e.g. makefile) and a location where you want to store them. In reality, 2 files will be created. makefile.mak and makefile.linkcmd.

Both files give us valuable information about defines that have been set for this project and all the files that are included in the final executable.

Deciphering the makefile.linkcmd told me that I need at least these files to create the executable:

  • main.c
  • vectors16.asm (if I would have used the eZ80F91 variant, I would need the vectors24.asm file)
  • init_params_l92.asm (which includes the file)
  • cstartup.asm
  • zsldevinit.asm (I used zsldevinitdummy.asm instead since I don’t use the ZSL at this moment)

With the exception of main.c of course, all other files can be found in the ZDSII_eZ80Acclaim!_5.3.4 installation directory.

I copied the needed files into a crt directory, following the same layout as the ZDS II installation. I needed to perform a few modifications to the files as well so that their syntax was compatible with the ez80-none-elf-as assembler.

My final test build script looks like this:

TOOLCHAINDIR=$(pwd)/../ez80-none-elf/bin # This is the toolchain that is build using <>

mkdir ./build > /dev/null 2>&1
mkdir ./list > /dev/null 2>&1

# Compiling main.c
$TOOLCHAINDIR/clang -target ez80-none-elf $OPT -Wa,-march=ez80+full -nostdinc main.c -c -o ./build/main.o

# Compiling minimal set of .asm files to setup the ez80L92 CPU
$TOOLCHAINDIR/ez80-none-elf-as -march=ez80+full \
    -a=./list/vectors16.lst \
    ./crt/src/boot/common/vectors16.asm \
    -o ./build/vectors16.o

$TOOLCHAINDIR/ez80-none-elf-as -march=ez80+full \
    -a=./list/init_params_l92.lst \
    ./crt/src/boot/eZ80L92/init_params_l92.asm \
    -I ./crt/include/zilog \
    -o ./build/init_params_l92.o

$TOOLCHAINDIR/ez80-none-elf-as -march=ez80+full \
    -a=./list/cstartup.lst \
    ./crt/src/boot/common/cstartup.asm \
    -o ./build/cstartup.o

$TOOLCHAINDIR/ez80-none-elf-as -march=ez80+full \
    -a=./list/zsldevinitdummy.lst \
    ./crt/src/boot/common/zsldevinitdummy.asm \
    -o ./build/zsldevinitdummy.o

# Linking step
$TOOLCHAINDIR/ez80-none-elf-ld -T linkerScript.ld \
    ./build/vectors16.o \
    ./build/init_params_l92.o \
    ./build/cstartup.o \
    ./build/zsldevinitdummy.o \
    ./build/main.o \
    --oformat ihex \
    -o executable.hex

The end result is an executable.hex file that can be loaded onto the simulator or a real eZ80L92 CPU with the ZDS II IDE.

One file that is not yet explained is the linkerScript.ld. In short this script contains various defines that are needed by the assembly files such as the Control Registers and Bus Mode Registers values for the external memory which I copied from the ZDS II make files.

/* on CWS 1: CS0 = Flash 8 Mb */
__CS0_LBR_INIT_PARAM = 0x20;
__CS0_UBR_INIT_PARAM = 0x2f;
__CS0_CTL_INIT_PARAM = 0x88;
__CS0_BMC_INIT_PARAM = 0x01;
/* on CWS 1: CS1 = SRAM 2 Mb */
__CS1_LBR_INIT_PARAM = 0x00;
__CS1_UBR_INIT_PARAM = 0x1f;
__CS1_CTL_INIT_PARAM = 0x08;
__CS1_BMC_INIT_PARAM = 0x01;
/* on CWS 1: CS2 = Not Used */
__CS2_LBR_INIT_PARAM = 0x80;
__CS2_UBR_INIT_PARAM = 0xbf;
__CS2_CTL_INIT_PARAM = 0x28;
__CS2_BMC_INIT_PARAM = 0x01;
/* on CWS 1: CS3 = Not Used */
__CS3_LBR_INIT_PARAM = 0x03;
__CS3_UBR_INIT_PARAM = 0x03;
__CS3_CTL_INIT_PARAM = 0x18;
__CS3_BMC_INIT_PARAM = 0x84;

The other reason we need the linkerScript.ld file is to provide the memory layout. Below an example how the memory layout looks like for my eZ80L92 prototype board::

START_OF_RAM    = 0x000000;
RAM_SIZE        = 0x200000;     /* 2Mb, 2.097.152 bytes */
FLASH_SIZE      = 0x800000;     /* 8Mb, 8.388.608 bytes */

    . = 0x000000;
    start_of_reset = . ;
    .reset : { *(.reset) }
    end_of_reset = . ;

    . = 0x000100;
    start_of_ivects = . ;
    .ivects : { *(.ivects) }
    end_of_ivects = . ;

    start_of_startup = . ;
    .startup : { *(.startup) }
    end_of_startup = . ;

    start_of_text = . ;
    .text : { *(.text) }
    end_of_text = . ;

    start_of_data = . ;
    .data : { *(.data) }
    end_of_data = . ;

    start_of_bss = . ;
    .bss : { *(.bss) }
    end_of_bss = . ;

__low_romdata       = start_of_data;
__low_data          = start_of_data;
__len_data          = end_of_data - __low_data;
__low_bss           = start_of_bss;
__len_bss           = end_of_bss - __low_bss;
__stack             = START_OF_RAM + RAM_SIZE;
__low_romcode       = start_of_text;
__low_code          = start_of_text;
__len_code          = end_of_text - __low_code;
__copy_code_to_ram  = 0;

During linking, the option is specified which will generate a useful map file as well:

Memory Configuration

Name             Origin             Length             Attributes
*default*        0x0000000000000000 0xffffffffffffffff

Linker script and memory map

                0x0000000000000000                START_OF_RAM = 0x0
                0x0000000000200000                RAM_SIZE = 0x200000
                0x0000000000800000                FLASH_SIZE = 0x800000
                0x0000000000000000                . = 0x0
                0x0000000000000000                start_of_reset = .

.reset          0x0000000000000000       0x6b
 .reset         0x0000000000000000       0x6b ./build/vectors16.o
                0x0000000000000000                _reset
                0x000000000000006b                end_of_reset = .
                0x0000000000000100                . = 0x100
                0x0000000000000100                start_of_ivects = .

.ivects         0x0000000000000100      0x120
 .ivects        0x0000000000000100      0x120 ./build/vectors16.o
                0x0000000000000100                __vector_table
                0x0000000000000160                __1st_jump_table
                0x0000000000000220                end_of_ivects = .
                0x0000000000000220                start_of_startup = .

.startup        0x0000000000000220      0x188
 .startup       0x0000000000000220       0x6c ./build/vectors16.o
                0x0000000000000220                __nvectors
                0x0000000000000222                __default_nmi_handler
                0x0000000000000224                __default_mi_handler
                0x0000000000000227                _init_default_vectors
                0x0000000000000227                __init_default_vectors
                0x0000000000000256                __set_vector
                0x0000000000000256                _set_vector
 .startup       0x000000000000028c       0xce ./build/init_params_l92.o
                0x000000000000028c                __init
                0x0000000000000350                __exit
                0x0000000000000350                _abort
                0x0000000000000350                _exit
                0x0000000000000356                _SysClkFreq
 .startup       0x000000000000035a       0x4e ./build/cstartup.o
                0x000000000000035a                __c_startup
                0x00000000000003a8                end_of_startup = .
                0x00000000000003a8                start_of_text = .

.text           0x00000000000003a8       0x3d
 .text          0x00000000000003a8        0x0 ./build/vectors16.o
 .text          0x00000000000003a8        0x0 ./build/init_params_l92.o
 .text          0x00000000000003a8        0x0 ./build/cstartup.o
 .text          0x00000000000003a8        0x1 ./build/zsldevinitdummy.o
                0x00000000000003a8                _close_periphdevice
                0x00000000000003a8                __close_periphdevice
                0x00000000000003a8                _open_periphdevice
                0x00000000000003a8                __open_periphdevice
 .text          0x00000000000003a9       0x3c ./build/main.o
                0x00000000000003a9                _main
                0x00000000000003e5                end_of_text = .
                0x00000000000003e5                start_of_data = .

.ivjmptbl       0x00000000000003e5       0xc0
 .ivjmptbl      0x00000000000003e5       0xc0 ./build/vectors16.o
                0x00000000000003e5                __2nd_jump_table

.data           0x00000000000004a5        0x0
 .data          0x00000000000004a5        0x0 ./build/vectors16.o
 .data          0x00000000000004a5        0x0 ./build/init_params_l92.o
 .data          0x00000000000004a5        0x0 ./build/cstartup.o
 .data          0x00000000000004a5        0x0 ./build/zsldevinitdummy.o
 .data          0x00000000000004a5        0x0 ./build/main.o
                0x00000000000004a5                end_of_data = .
                0x00000000000004a5                start_of_bss = .

.bss            0x00000000000004a5        0x0
 .bss           0x00000000000004a5        0x0 ./build/vectors16.o
 .bss           0x00000000000004a5        0x0 ./build/init_params_l92.o
 .bss           0x00000000000004a5        0x0 ./build/cstartup.o
 .bss           0x00000000000004a5        0x0 ./build/zsldevinitdummy.o
 .bss           0x00000000000004a5        0x0 ./build/main.o
                0x00000000000004a5                end_of_bss = .
                0x00000000000003e5                __low_romdata = start_of_data
                0x00000000000003e5                __low_data = start_of_data
                0x00000000000000c0                __len_data = (end_of_data - __low_data)
                0x00000000000004a5                __low_bss = start_of_bss
                0x0000000000000000                __len_bss = (end_of_bss - __low_bss)
                0x0000000000200000                __stack = (START_OF_RAM + RAM_SIZE)
                0x00000000000003a8                __low_romcode = start_of_text
                0x00000000000003a8                __low_code = start_of_text
                0x000000000000003d                __len_code = (end_of_text - __low_code)
                0x0000000000000000                __copy_code_to_ram = 0x0
                0x0000000000000020                __CS0_LBR_INIT_PARAM = 0x20
                0x000000000000002f                __CS0_UBR_INIT_PARAM = 0x2f
                0x0000000000000088                __CS0_CTL_INIT_PARAM = 0x88
                0x0000000000000001                __CS0_BMC_INIT_PARAM = 0x1
                0x0000000000000000                __CS1_LBR_INIT_PARAM = 0x0
                0x000000000000001f                __CS1_UBR_INIT_PARAM = 0x1f
                0x0000000000000008                __CS1_CTL_INIT_PARAM = 0x8
                0x0000000000000001                __CS1_BMC_INIT_PARAM = 0x1
                0x0000000000000080                __CS2_LBR_INIT_PARAM = 0x80
                0x00000000000000bf                __CS2_UBR_INIT_PARAM = 0xbf
                0x0000000000000028                __CS2_CTL_INIT_PARAM = 0x28
                0x0000000000000001                __CS2_BMC_INIT_PARAM = 0x1
                0x0000000000000003                __CS3_LBR_INIT_PARAM = 0x3
                0x0000000000000003                __CS3_UBR_INIT_PARAM = 0x3
                0x0000000000000018                __CS3_CTL_INIT_PARAM = 0x18
                0x0000000000000084                __CS3_BMC_INIT_PARAM = 0x84
                0x0000000001312d00                _SYS_CLK_FREQ = 0x1312d00
LOAD ./build/vectors16.o
LOAD ./build/init_params_l92.o
LOAD ./build/cstartup.o
LOAD ./build/zsldevinitdummy.o
LOAD ./build/main.o
OUTPUT(executable.hex ihex)

data            0x0000000000000000        0x3
 data           0x0000000000000000        0x3 ./build/cstartup.o
                0x0000000000000000                _errno

.comment        0x0000000000000000       0x6e
 .comment       0x0000000000000000       0x6e ./build/main.o

We can easily see here that the start of the _main function is located at address 0x3A9.

I have no doubt that the above linkerScript.ld file will evolve over time when I start adding more and more functionality and have a full conversion of the C Runtime Library.

Testing the final executable

For this, I have no other choice for now than go back to Windows and use the ZDS II IDE 😞.

In the picture below, You can see the IDE with the simple project I initially used to create the makefile files and figure out the values for the Control Registers and Bus Mode Registers, etc. Instead of running on real hardware, I’m using the simulator here (works a lot faster and serves its purpose quite well here).

Pressing the Reset button brings us to this window where I also opened the Memory, Registers and Disassembly windows:


It’s important to realize, that the source code window does not show you the real source code since the downloaded HEX file doesn’t contain any debug info!

Right clicking in the Memory windows opens a small pop-up menu (as can be seen in the picture above) which allows us to load a file (bin, hex or text) straight into memory. Using a HEX file is the most interesting for us since the memory address to load are part of the HEX file.

Clicking on the '...' opens a new dialog box to select a file.
Make sure the 'Hex' option is selected.

After downloading the hex file, I manually set the PC (Program Counter) register to 000000 in the register window, since we want to start from that address.

Setting PC to 000000.

In the Dissasembly window, I enter address 3A9 since this is the start of our main function (we found the start address of the main function in the map file). I set a breakpoint at address 0x3A9 as well.

Setting a breakpoint at the start of the main() function.

Now, I simply hit the Go button and the simulator will start from address 0x0 and stop at address 0x3A9. Hitting the Step Over button once brings us to address 0x3AD which is the end of the main() function and we can nicely see that the return value, stored in the HL register, is the expected 3.

The return value, stored in HL, is the expected 3.


This blog post explains the absolute minimum toolchain setup required to get the smallest possible C code compiled and running on an eZ80 CPU using LLVM/Clang combined with the binutils tools.

A lot of work still needs to be done to fully implement all C runtime functionality one would expect and the functionality of the ZSL library as well. Luckily, all source code is provided by Zilog in the ZDSII_eZ80Acclaim!_5.3.4 directory. But they will need manual modification to get them compiled/assembled with this new toolchain.

Other sources of inspiration for the runtime library code could be the aforementioned TI84 Cemu project).

I think this exercise shows potential to provide more modern tools to this processor family.

Recent posts

See more