Classic Homebrew - z88dk/z88dk GitHub Wiki

Homebrew hardware quickstart

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.

Setting up the project

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.

Z80 Machine, "Monitor/OS" already loaded

Memory map of the machine

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.

Monitor entry points

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)

Printing to the console

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.

Reading from the console (waiting)

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.

z80 Machine, no monitor

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

808x Machine

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.

Tuning options

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.

Switching compilers

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.

Custom CRT0

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.

Next Steps

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.

Why is the generated executable so big?

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 ""

It's still not small enough!

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.

⚠️ **GitHub.com Fallback** ⚠️