Goals
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.
-
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. -
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
_main:
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
ret
.section .text,"ax",@progbits
.local .Lfunc_end0
.Lfunc_end0:
.size _main, .Lfunc_end0-_main
.ident "clang version 14.0.0 (https://github.com/jacobly0/llvm-project.git 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
_main:
ld hl, 3
ret
.section .text,"ax",@progbits
.local .Lfunc_end0
.Lfunc_end0:
.size _main, .Lfunc_end0-_main
.ident "clang version 14.0.0 (https://github.com/jacobly0/llvm-project.git 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 theez80-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...
:
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 thevectors24.asm
file)init_params_l92.asm
(which includes theez80l92.inc
file)cstartup.asm
zsldevinit.asm
(I usedzsldevinitdummy.asm
instead since I don’t use theZSL
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 <https://bitbucket.org/cocoacrumbselectronics/ez80-llvm-toolchain/src/master/>
OPT=-O1
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 -Map=executable.map \
./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 */
SECTIONS
{
. = 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 -Map=executable.map
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)
.reset 0x0000000000000000 0x6b ./build/vectors16.o
0x0000000000000000 _reset
0x000000000000006b end_of_reset = .
0x0000000000000100 . = 0x100
0x0000000000000100 start_of_ivects = .
.ivects 0x0000000000000100 0x120
*(.ivects)
.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)
.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)
.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)
.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)
.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.
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.
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.
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
.
Conclusion
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.