Z88dk compiler suite#


The P2000T uses a Z80 processor and one can program for the P2000T using Z80 assembly. Although this yields the fastest code (assuming the programmer is proficient), programming in assembly can be sluggish and inefficient. For good reasons, people started using higher level programming languages. For the P2000T, this means BASIC for most people. Programming in BASIC replaces lots of the complexity of assembly by providing a myriad of handy routines, yet in its execution is relatively slow.

The C programming language offers in that sense the best of both world. It allows for relatively quick code development but without a huge sacrifice in terms of speed as C code is in the end compiled to assembly. Furthermore, in the compilation process, the compiler attempts to optimize the code.

There are no native C compilers for the P2000T, but one can program for the P2000T on a modern computing and use a so-called cross-compilation procedure wherein C-code is compiled on a x86 (or similar) processor for the Z80. The Z88dk compiler suite offers all the tools needed for this process.

The Z88dk compiler suite caters to a lot of different systems using a Z80. Since all these systems use different memory lay-outs, it remains up to the programmer to supply the right parameters to the compiler to ensure a functioning program. On this page, we provide a short explanation on how to supply these parameters to the compiler and showcase the compilation of a simple Hello World cartridge program.


All P2000T cartridges require a 16 byte header. We can ensure this 16 byte header is present by creating a file called crt_preamble.asm. The contents of this file is placed before any other code.

; signature, byte count, checksum
DB 0x5E,0x00,0x00,0x00,0x00

; name of the cartridge (11 bytes)
DB 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00

jp __Start


This is a rather bare-bones header. It has a general start-byte 0x5E and it performs no checksum test. It is recommended to change this accordingly after full compilation of the cartridge.

Compiler instructions#

To compile for the P2000T, we need to provide a number of parameters and pragma defines to the compiler which are documented here. A typical parameter listing will look as shown below.

zcc \
+embedded -clib=sdcc_iy \
main.c \
-startup=1 \
-pragma-define:CRT_ORG_CODE=0x1000 \
-pragma-define:CRT_ORG_DATA=0x6200 \
-pragma-define:REGISTER_SP=0x9FFF \
-pragma-define:CRT_ON_EXIT=0x9FFF \
-pragma-define:REGISTER_SP= \
-pragma-define:CRT_ENABLE_EIDI= \
-pragma-define:CRT_INCLUDE_PREAMBLE=1 \
-pragma-define:CLIB_FOPEN_MAX=0 \
--max-allocs-per-node2000 \
-SO3 -bn main.bin \
-create-app -m


The following pragma-define instructions are used. Further information on these settings can be found here.

pragma define



Start position of the code in memory, this is 0x1000 for P2000T cartridges.


Start position of variables and data in memory, we use 0x6200 to avoid .


Where to place the top of the stack (-1 does not touch the stack).


Sets the stack size.


Determines behavior on program exit. Default value is 0x10001, which is suitable for cartridges.


Determines behavior on program exit. Default value is 0x13, which is suitable for cartridges.


Use the crt_preamble.asm file, we need this for cartridges.


Maximum number of open files. We set this to -1 as we do not use file pointers.


The majority of the parameters listed below are explained in more details here.




Use the so-called embedded target, which is a generic target offering a great deal of flexibility.


Which c-library to use. The sdcc library is preferred. The _iy specification states that the library uses one index register iy, leaves ix alone for sdcc to use as its frame pointer, and forbids sdcc from using iy.


Which model to use for the compilation. We set this to -startup=1 for a cartridge.


How many optimization cycles to be used. Set this to a high number of thorough optimization. We use at least 2000.


Use aggressive peephole rules.

-bn main.bin

Name of the binary file to create.


Generates a complete rom image from the output binaries.


Generate a map file listing all defined symbols with their values. This is useful to analyze the memory positions of all variables and routines.


When creating your own (mixed) assembly routines, avoid using ix and iy as these register may clash with the library or the frame pointer. Use push and pop or even exx instructions instead.

Hello world example#

A simple Hello World program can be created using three files:

  • main.c

  • crt_preamble.asm

  • Makefile

The first two files contain the source code, the latter file contains the compilation instructions.

Listing 1 main.c#
 1#include <stdio.h>
 4 * Create reference to video memory
 5 */
 6__at (0x5000) char VIDMEM[];
 7char* vidmem = VIDMEM;
 9int main(void) {
10    sprintf(&vidmem[0x0000], "Hello world!");
12    while(0) {} // set infinite loop
14    return 0;
Listing 2 crt_preamble.asm#
1; signature, byte count, checksum
2DB 0x5E,0x00,0x00,0x00,0x00
4; name of the cartridge (11 bytes)
7jp __Start
Listing 3 Makefile#
 1main.bin main.map main.rom: main.c
 2	zcc \
 3	+embedded -clib=sdcc_iy \
 4	main.c \
 5	-startup=2 \
 6	-pragma-define:CRT_ORG_CODE=0x1000 \
 7	-pragma-define:CRT_ORG_DATA=0x6500 \
 8	-pragma-define:REGISTER_SP=0x9FFF \
 9	-pragma-define:CRT_STACK_SIZE=256 \
10	-pragma-define:CRT_INCLUDE_PREAMBLE=1 \
11	-pragma-define:CLIB_FOPEN_MAX=0 \
12	--max-allocs-per-node2000 \
13	-SO3 -bn main.bin \
14	-create-app -m

All these three files reside in the same folder and the compilation can be executed by means of the Z88dk Docker image.

On Linux (or WSL), this is as simple as running the following one-liner.

docker run -v `pwd`:/src/ -it z88dk/z88dk make

Using Bash for Window, one can use

winpty docker run -v `pwd | sed 's/\//\/\//g'`://src/ -it z88dk/z88dk make

Upon compilation, a number of files will be created. The file that contains the binary data for the cartridge is called main.rom. One can directly open this file in a P2000T emulator such as M2000. The result of this program is as seen in Figure 45.


Fig. 45 Screenshot of the hello world program.#


The file main.rom is fairly large (~2.1kb). The reason is that we have made use of the function sprintf. We could have used a much simpler routine where we directly assign the bytes to video memory and safe upon a lot of space. The intention is however to show an example where we have made use of a handy function of the C library and when printing a lot of text to the screen, the disadvantage of this library being fairly large is outweighed by its ease of use.

Modifying header#

Although we have managed to create a working cartridge, we use a cartridge header which does not perform any checksum test. This can be changed by calculating the checksum and modifying the header.


The details of the cartridge validation process are found here.

This is easily achieved using the following Python script:

Listing 4 modheader.py#
 1import numpy as np
 3def main():
 4    with open('main.rom', mode='rb') as f:
 5        data = bytearray(f.read())
 6        f.close()
 8    offset = 5
 9    nrbytes = np.uint16(len(data) - offset)
10    checksum = np.sum(data[offset:], dtype=np.uint16)
12    print('Length:   0x%04X' % nrbytes)
13    print('Checksum: 0x%04X' % checksum)
15    # remember that Z80 is little endian (LSB first)
16    data[0x01] = nrbytes & 0xFF
17    data[0x02] = (nrbytes >> 8) & 0xFF
18    data[0x03] = (~(checksum & 0xFF) + 1) & 0xFF
19    data[0x04] = ~((checksum >> 8) & 0xFF) & 0xFF
21    # update main.rom
22    with open('main.rom', mode='wb') as f:
23        f.write(data)
24        f.close()
26if __name__ == '__main__':
27    main()