Classic Homebrew - z88dk/z88dk GitHub Wiki
Note
This page refers to the classic target. Equivalent document for newlib can be found under the newlib embedded target
Building a toolchain for your homebrew hardware using z88dk allows you to take advantage of the z88dk's highly optimised standard library and hardware support.
Although the process of adding a new target using the standard method is fairly easy, the sheer amount of code in the standard libraries can appear overwhelming initially, and building on windows requires some additional third party tools.
From observations of some projects, it seems a common way to bring up hardware when using other toolchains is to "slam everything into a single file and link together". This adhoc method also works with z88dk but is perhaps not obvious from the documentation.
This tutorial will demonstrate how to create a working program and toolchain for various types of homebrew.
In all cases, we'll be using the classic library since it offers the widest CPU support and is the simplest to retarget.
Tradition dicates that we should start off with "helloworld", so lets create one:
#include <stdio.h>
int main()
{
printf("Hello from z88dk!\n");
}
Lets compile it:
zcc +z80 -clib=classic world.c -create-app -m
That should complete without errors and leave the following files in your directory:
a.bin
a_DATA.bin
a.map
a.rom
The first two files are intermediate files. a.map
contains a listing of all the symbols and their addresses and a.rom
is binary containing the result of the compilation.
At this stage there's no point attempting to run the generated code - it will most likely crash or hang your machine. Let's go on, and try to make something that is runnable.
Let's assume that the monitor uses the first 8k of memory, from $0000
to $1fff
with programs allowed to run from address $2000
.
We need to adjust our compilation options so that the program is compiled to $2000
. This is easy to do with the option -pragma-define:CRT_ORG_CODE=
option. So let's do it:
zcc +z80 -clib=classic world.c -pragma-define:CRT_ORG_CODE=0x2000 -create-app -m
Once again we'll have the 4 files produced, and this time, let's have a peek into the .rom
file generated.
z88dk-dis -o 0x2000 -x a.map a.rom
start:
ld hl,$ffc0 ;[2000] 21 c0 ff
add hl,sp ;[2003] 39
ld sp,hl ;[2004] f9
call crt0_init_bss ;[2005] cd 16 20
ld (exitsp),sp ;[2008] ed 73 8b 27
call _main ;[200c] cd 40 21
cleanup:
...
We can see that a sensible symbolic disassembly is achieved which means that we're compiling to the correct address. Try changing the value of the -o option and you'll see the labels on the left no longer appear.
The "Monitor/OS" for this machine exposes a couple of functions that permit reading and writing to the console. What that console is (serial, crt, TCP/IP connection) is of no concern to us. We just have the following entry points that we could call:
-
$1000
- print character to console (character in a) -
$1003
- wait for console (returns a=chaacter read from keyboard)
The classic library supports switching between multiple implementions of the console driver. By default, the calls to putchar()
, fputc(c, stdout)
end up in a function call named fputc_cons_native()
. We know the address of the monitor routine that prints a character to the console, so let's connect the library up to it.
Create a new file, call it monitor.c
with the following contents:
#include <stdio.h>
int fputc_cons_native(char c) __naked
{
__asm
pop bc ;return address
pop hl ;character to print in l
push hl
push bc
ld a,l
call $1000
__endasm;
}
And now compile that along with world.c:
zcc +z80 -clib=classic world.c monitor.c -pragma-define:CRT_ORG_CODE=0x2000 -create-app -m
You should now be able to transfer the .rom
onto your homebrew hardware, execute it and see Hello from z88dk
printed on the console.
We can also add a routine to read from the keyboard. Let's update our driver program first of all:
#include <stdio.h>
int main()
{
printf("Hello from z88dk!\n");
while ( 1 ) {
int c = getchar();
printf("<%c>=%d ", c,c);
}
}
We also need to add a function to monitor.c
to handle reading the keyboard, the internal routine used by z88dk is int fgetc_cons()
:
int fgetc_cons() __naked
{
__asm
call $1003
ld l,a ;Return the result in hl
ld h,0
__endasm;
}
If you now compile and run the target as well as seeing the Hello from z88dk!
banner you'll also get the character and ascii value of each key pressed printed to the screen.
If everything works correctly, then congratulations you've successfully got your homebrew computer working with z88dk.
If you've got a monitor program, then bringing up a target is easy - there's really not much to do, but if all you've got is a 16kb ROM that starts execution from address $0000
, with RAM located from $4000
to $ffff
then surely it's much harder?
Let's find out. Let's assume the hardware provides a blocking output and input on port 1, that is:
- Console output
out (1),a
where a is the character - Console input
in a,(1)
So we'd have the following files:
world.c:
#include <stdio.h>
int main()
{
printf("Hello from z88dk!\n");
while ( 1 ) {
int c = getchar();
printf("<%c>=%d ", c,c);
}
}
And, let's write the binding in pure assembler this time, so we have hardware.asm
:
SECTION code_user
PUBLIC fputc_cons_native
PUBLIC _fputc_cons_native
PUBLIC fgetc_cons
PUBLIC _fgetc_cons
fputc_cons_native:
_fputc_cons_native:
pop bc ;return address
pop hl ;character to print in l
push hl
push bc
ld a,l
out (1),a
ret
fgetc_cons:
_fgetc_cons:
in a,(1)
ld l,a ;Return the result in hl
ld h,0
ret
The exporting of the symbols with and without the _
prefix ensures that these routines, which overwrite library routines, can be reached by both sccz80 and sdcc compiled code.
And then we can compile with:
zcc +z80 -clib=classic world.c hardware.asm -pragma-define:CRT_ORG_BSS=0x4000 -pragma-define:REGISTER_SP=0x0000 -create-app -m
Note that this time, we've not specified a CRT_ORG_CODE
address since the default is $0000
, but we've had to specify CRT_ORG_BSS
to point to the start of RAM. We've also provided REGISTER_SP
to set the sp
register up to last part of memory.
You can now burn the binary to an EPROM, and see your program running on your homebrew machine.
Should you require an Intel Hex output, just add the option -Cz--ihex
:
zcc +z80 -clib=classic world.c monitor.c -pragma-define:CRT_ORG_CODE=0x2000 -create-app -m -Cz--ihex
To pad to an eprom size, add the option -Cz--romsize=NNN
, for example, to create a 16k ROM:
zcc +z80 -clib=classic world.c monitor.c -pragma-define:CRT_ORG_CODE=0x2000 -create-app -m -Cz--ihex -CZ--romsize=16384
Follow the steps for the z80, but change the compile line so instead of -clib=classic
, the line is -clib=8080
or -clib=8085
as appropriate.
For this tutorial we've mostly relied on the default configurations for various bits of behaviour, the standard crt0 is fairly flexible and can be configured using pragmas.
This tutorial has used the default sccz80
compiler. You can switch to using sdcc by adding the command line option -compiler=sdcc
however if you do, you should ensure that hardware/firmware file is either written in assembler or compiled itself with sccz80.
The crt0 code is the first code that runs. It's possible that you may want to customise it to include a jump table at the start (for example if you are writing the monitor program!). In this case you will need to override the crt0 that's used by default.
Thankfully, there's an option for that:
zcc +z80 -clib=classic -crt0=crt0.asm ....
The -crt0 will force z88dk to use the file named crt0.asm
that's in the current directory. It's probably easiest to copy and then customise the crt0 that's located at lib/target/z80/classic/z80_crt0.asm
.
The next steps are detailed in the retargetting page and involve adding the target to the library so that the hardware bindings can be shared between projects and not copied between them.
By default, z88dk tries to make things "just work", so with the default configuration there may be extraneous things that your application doesn't need, but another one might.
For example, in this tutorial, we've been using printf()
without any format parameters. This means that a lot of unnecessary formatters are dragged in by default. To remove them, add the following to world.c: #pragma printf ""
On the pragmas page there's some tips to do with this, the easiest is to replace the call to printf()
with a call to printk()
and add the pragma: -pragma-define:CRT_ENABLE_STDIO=0
If you want to go further, remove the #pragma printf
line and replace the call to printk()
with a call to puts_cons()
With these few changes we've gone just under 2k, to under 200 bytes. There's more tuning options that we've not applied here that can reduce the size even more.