Z88dk compiler suite#
Introduction#
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.
CRT-Preample#
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
Caution
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
Parameters#
The following pragma-define
instructions are used. Further information on these
settings can be found here.
pragma define |
description |
---|---|
|
Start position of the code in memory, this is |
|
Start position of variables and data in memory, we use |
|
Where to place the top of the stack ( |
|
Sets the stack size. |
|
Determines behavior on program exit. Default value is |
|
Determines behavior on program exit. Default value is |
|
Use the |
|
Maximum number of open files. We set this to |
Pragma-defines#
The majority of the parameters listed below are explained in more details here.
parameter |
description |
---|---|
|
Use the so-called embedded target, which is a generic target offering a great deal of flexibility. |
|
Which c-library to use. The |
|
Which model to use for the compilation. We set this to |
|
How many optimization cycles to be used. Set this to a high number of thorough optimization. We use at least |
|
Use aggressive peephole rules. |
|
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. |
Note
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.
1#include <stdio.h>
2
3/**
4 * Create reference to video memory
5 */
6__at (0x5000) char VIDMEM[];
7char* vidmem = VIDMEM;
8
9int main(void) {
10 sprintf(&vidmem[0x0000], "Hello world!");
11
12 while(0) {} // set infinite loop
13
14 return 0;
15}
1; signature, byte count, checksum
2DB 0x5E,0x00,0x00,0x00,0x00
3
4; name of the cartridge (11 bytes)
5DB "HELLOWORLD!"
6
7jp __Start
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 50.
Note
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.
Tip
The details of the cartridge validation process are found here.
This is easily achieved using the following Python script:
1import numpy as np
2
3def main():
4 with open('main.rom', mode='rb') as f:
5 data = bytearray(f.read())
6 f.close()
7
8 offset = 5
9 nrbytes = np.uint16(len(data) - offset)
10 checksum = np.sum(data[offset:], dtype=np.uint16)
11
12 print('Length: 0x%04X' % nrbytes)
13 print('Checksum: 0x%04X' % checksum)
14
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
20
21 # update main.rom
22 with open('main.rom', mode='wb') as f:
23 f.write(data)
24 f.close()
25
26if __name__ == '__main__':
27 main()