Table of Contents

z88dk logo z88dk is a collection of software development tools that targets z80 machines. It consists of a C compiler, a set of libraries implementing the C standard library, an assembler / linker and a variety of utilities for profiling and generating executables in a number of formats. Development in C, assembly language or a mixture of the two is directly supported.

The name z88dk originates from the time when the project was founded and targetted only the Cambridge z88 portable. Today z88dk directly supports more than forty z80 targets with the level of library support for each target varying with interest shown by users. It is possible to add new targets with relative ease.

z88dk is known to run on a wide variety of platforms. Binary releases are available for Win32 and MacOS X and a source tarball is available to build the tools for other platforms.

There are a few things that make z88dk unique:

About the z88dk logo

Mandelbrot Twitter Client File Transfer
Click on the link on the right for the video YouTube
Ninjajar! 3D Globe Forest Raider Cherry

License

The Clarified Artistic License

The intent behind adoption of this license:

1. Any modifications to z88dk eg in the form of new targets, toolchain improvements, bug fixes, extra functionality etc should be fed back to the project so that every user can benefit.

2. You can use the compiler to generate products which are sold commercially and distributed for profit.

3. We totally indemnify ourselves against any damage caused by deployment of the kit or any product generated by the kit. We don't guarantee that the software is fit for any purpose in any way whatsoever.

Some modules have been imported into the project from other sources. They may be covered under other licenses that are documented with their source code. However in all cases, there is no restriction on generating software that can be sold commercially for profit.

What's New

z88dk has seen significant development in the past two years. Among the changes:

  • z80asm has been updated to support sections and modern operator syntax.
  • sdcc is being directly supported as an alternate C compiler. The choice between using sdcc or sccz80 as C compiler is as simple as using the appropriate switch on the command line.
  • A second C library has been introduced. To distinguish between the two C library options, the existing one has been named the “classic” library and the new one the “new” library. The new C library aims for as much C11 compliance as is reasonable for an 8-bit target and is intended to be similar to 32-bit libraries in its functionality. It is still under development but is already extensive consisting of more than 700 functions.
  • A data compression utility, zx7, and a utility to assist in profiling, ticks, has been included in the package.
  • New extended libraries have been added that support data compression and proportional font printing.

Impact on Code Generated by Earlier Versions of Z88DK

  • z80asm's library format has changed, meaning any libraries made using an older version of z80asm will need to be rebuilt. The error “xxx is not a library” will be emitted if the library format is old.
  • z80asm's mathematical operators have been modernized. For example, z80asm used to use ”:” as bitwise xor but this has now been replaced with the standard “^” for xor. The old operators are no longer supported so any assembly language code assembled with z80asm should be checked to ensure operators are updated.
  • z80asm is now section aware and with that new scoping keywords PUBLIC, EXTERN and GLOBAL have been introduced. These are intended to replace the older XLIB, XDEF, LIB and XREF however the old keywords will continue to work with warnings.
  • z80asm's labels are now case sensitive.

Benchmarks

Brief Compiler Descriptions

In all cases a simple compile is done to verify the programs generate correct results. Then the programs are compiled for a minimal target that eliminates as much unnecessary code as possible; this includes elimination of stdio and as many device drivers as possible. Total program size is recorded (this includes the CODE, DATA and BSS sections but does not include the stack) and the execution time is measured by ticks. Ticks is a command line z80 emulator that comes with z88dk and can measure execution time of program fragments exactly.

HITECH-C CPM v3.09

Hitech-C (CP/M-80) v 3.09
One of the most capable native C compilers for CP/M. Runs under CP/M 2.2 and implements a large subset of C89.

HITECH-C Z80 v7.50

The last z80 compiler from Hitech, cross compiles z80 code from MSDOS. Seems to be near complete compliance with C89.

SDCC

sdcc 3.5.5 #9392 (MINGW64)
sdcc is a current open source C cross compiler targetting several small CPUs including the z80. Its primary feature is that it supports a large subset of modern C standards (C89, C99, C11).

Z88DK/SCCZ80_CLASSIC

(nightly build 10/NOV/2015)
z88dk's native C compiler sccz80 using the classic C library in z88dk. sccz80 is a derivative of small C with most small C limitations eliminated. Its primary feature is a comprehensive C library written in assembly language.

Z88DK/SCCZ80_NEW

(nightly build 10/NOV/2015)
z88dk's native C compiler sccz80 using the new C library in z88dk. sccz80 is a derivative of small C with most small C limitations eliminated. Its primary feature is a comprehensive C library written in assembly language.

Z88DK/SDCC

(nightly build 10/NOV/2015)
sdcc 3.5.5 #9392 is used to translate C code with z88dk supplying its (new) C library and startup code for targets.

Dhrystone 2.1

Dhrystone was a common synthetic benchmark for measuring the integer performance of compilers in the 1980s until more modern benchmarks replaced it. It attempts to simulate typical programs by executing a set of statements statistically determined from common programs in the wild.

The benchmark package is available for download.

SIZE TIME DHRYSTONES/S DMIPS
Relative Bytes Relative Z80 Cycles Wall Clock @ 4MHz @ 4MHZ @ 4MHZ
Hitech-C CPM v3.09 1.11 7809 1.46 376,240,194 94 sec 212.63 0.1210
Hitech-C Z80 v7.50 1.00 7040 1.17 301,760,038 75 sec 265.11 0.1509
SDCC 1.03 7223 1.24 319,842,936 80 sec 250.12 0.1424
Z88DK/SCCZ80_CLASSIC
Z88DK/SCCZ80_NEW
Z88DK/SDCC 1.01 7136 1.00 257,822,927 64 sec 310.29 0.1766

Notes:

  • Hitech-C CPM v3.09 binary size is over-estimated as it will contain some stdio structures for cp/m.
  • Hitech-C Z80 v7.50 must be compiled with global optimizer set to two; higher causes the program to hang.

Dhrystone 1.1 is deprecated because optimizing compilers can eliminate redundant statements that were intended to add to execution time. However many z80-era compilers ran this benchmark so it is also available in the z88dk repository. Beginning at line 106 Dhry1.1 results can be found, at line 142 Dhry1.0 results and at line 394 a few results for the 6502.

Pi

Pi.c computes pi to 800 decimal places. It is based on an implementation found at crypto.stanford.edu.

Pi.c measures 32-bit integer math performance. The computation can make good use of ldiv() but not all compilers supply this function so the program is run with and without ldiv() for comparison purposes.

Z88DK's new C library has a fast integer math option so the table below shows results for it as well as the normal build using the small integer math option.

WITHOUT LDIV() WITH LDIV()
SIZE TIME SIZE TIME
Relative Bytes Relative Z80 Cycles Wall Clock @ 4MHZ Relative Bytes Relative Z80 Cycles Wall Clock @ 4MHZ
Hitech-C CPM v3.09 1.11 6740 3.76 5,465,797,961 22 min 46 sec
Hitech-C Z80 v7.50 1.04 6332 3.79 5,520,762,227 23 min 00 sec 1.07 6473 4.04 5,884,343,627 24 min 31 sec
SDCC 1.12 6805 6.11 8,892,037,196 37 min 03 sec
Z88DK/SCCZ80_CLASSIC 1.00 6076 3.70 5,391,413,260 22 min 28 sec
Z88DK/SCCZ80_NEW 1.01 6149 3.60 5,246,696,144 21 min 52 sec 1.02 6182 2.59 3,773,744,792 15 min 43 sec
Z88DK/SDCC 1.01 6154 3.63 5,285,278,076 22 min 01 sec 1.01 6165 2.60 3,786,981,324 15 min 47 sec
Z88DK/SCCZ80_NEW_FAST 1.18 7166 1.34 1,953,856,481 8 min 08 sec 1.18 7199 1.00 1,455,531,292 6 min 04 sec
Z88DK/SDCC_FAST 1.18 7171 1.37 1,990,813,171 8 min 18 sec 1.18 7182 1.01 1,467,142,582 6 min 07 sec

Notes:

  • The HITECH-C CPM v3.09 binary size is over-estimated as it will contain some stdio structures for cp/m.
  • Although HITECH-C Z80 v7.50 supplies ldiv(), it still performs two divisions to get quotient and remainder.
  • SDCC's performance is hurt by having its 32-bit math routines implemented in C.
  • Z88DK's fast integer math library is able to reduce most 32-bit divides to 16-bit divides. The loop unrolling option is not enabled.

Sieve of Eratosthenes (Prime Numbers)

Sieve.c finds all the prime numbers in [2,7999]. The algorithm is known as the Sieve of Eratosthenes.

This is a popular benchmark for small machine compilers because just about every compiler is able to compile it. As a benchmarking tool it's mainly measuring loop overhead.

SIZE TIME
Relative Bytes Relative Z80 Cycles Wall Clock @ 4MHZ
Hitech-C CPM v3.09 1.06 8629 1.24 4,547,538 1.137 sec
Hitech-C Z80 v7.50 1.00 8203 1.00 3,672,107 0.918 sec
SDCC 1.00 8200 1.13 4,150,710 1.038 sec
Z88DK/SCCZ80_CLASSIC 1.00 8209 1.45 5,325,739 1.331 sec
Z88DK/SCCZ80_NEW 1.01 8236 1.45 5,325,739 1.331 sec
Z88DK/SDCC 1.00 8175 1.01 3,691,568 0.923 sec

Notes:

  • The HITECH-C CPM v3.09 binary size is over-estimated as it will contain some stdio structures for cp/m.
  • Z88DK/SCCZ80 tries to generate small code by turning primitive compiler operations into subroutine calls. The additional call/ret overhead of these subroutine calls is significant in the small loop code and this is what hurts its performance in comparison to other compilers.

Whetstone 1.2

Whetstone is a synthetic floating point benchmark. The benchmark package is available for download.

Floating point performance depends strongly on the number of mantissa bits in the float type.

FLOAT TYPE SIZE TIME KWIPS MWIPS
Size Mantissa Relative Bytes Relative Z80 Cycles Wall Clock @4MHZ @ 4MHZ @ 4MHZ
Hitech-C CPM v3.09 32 24 1.39 7369 1.00 637,332,104 2 min 39 sec 6.276 0.006276
Hitech-C Z80 v7.50 32 24 1.30 6872 fail
SDCC 32 24 2.73 14463 4.43 2,821,354,806 11 min 45 sec 1.418 0.001418
Z88DK/SCCZ80_CLASSIC 48 40 1.01 5364 2.03 1,295,331,166 5 min 24 sec 3.088 0.003088
Z88DK/SCCZ80_NEW 48 40 1.00 5300 1.53 974,224,224 4 min 04 sec 4.106 0.004106
Z88DK/SDCC 32(48) 24(40) 1.12 5914 1.44 919,431,274 3 min 50 sec 4.351 0.004351

Notes:

  • Hitech-C CPM v3.09 produces two results with excessive error.
  • Hitech-C CPM v3.09 binary size is over-estimated as it will contain some stdio structures for cp/m.
  • Hitech-C Z80 v7.50 produces incorrect results on all optimization levels.
  • SDCC's peformance is hurt by a floating point package implemented in C.
  • Z88DK/SCCZ80_CLASSIC uses the genmath float library while the other Z88DK compiles use math48.
  • Z88DK/SDCC uses a 48-bit float internally but this is converted to 32-bit at the compiler-library interface since sdcc only understands a 32-bit float type.

PROGRAM SIZE

Program size is often more important than performance in the small 64k space available to standard z80 programs.

A selection of programs from z88dk's examples directory were compiled to compare binary sizes using the widely supported CP/M target.

backgammon.c (703 lines) clisp.c (1279 lines) eliza.c (352 lines) startrek.c (2153 lines)
Relative Bytes Relative Bytes Relative Bytes Relative Bytes
Hitech-C CPM v3.09 * * 1.68 12699 * *
Hitech-C Z80 v7.50 1.19 32186 2.00 15193 1.28 41038*
SDCC
Z88DK/SCCZ80_CLASSIC 1.00 27039 1.00 7578
Z88DK/SCCZ80_NEW 1.07 28888 1.13 8601
Z88DK/SDCC 1.00 30392 1.12 30155 1.10 8324 1.00 32053

Notes:

  • BACKGAMMON.C (Z88DK/SCCZ80_NEW, Z88DK/SDCC Pragmas to minimize crt, only %c%d enabled for printf, error strings reduced to minimum, –opt-code-size for sdcc)
  • CLISP.C (Hitech-C CPM v3.09 Does not allow switch on longs) (Hitech-C Z80 v7.50 Remove seed loop counter, enable long printf) (Z88DK/SCCZ80_CLASSIC Pragmas to minimize size, removed seed loop counter, added special functions for line editing) (Z88DK/SCCZ80_NEW, Z88DK/SDCC Pragmas to minimize crt, only %s%ld enabled for printf, error strings reduced to minimum, –opt-code-size for sdcc)
  • ELIZA.C (Hitech-C CPM v3.09 Simple strlwr() strstr() provided, "//" comments removed, “const” removed, EXIT_FAILURE defined, Constant type specifiers removed) (Hitech-C Z80 v7.50 Simple strlwr() provided) (Z88DK/SCCZ80_CLASSIC Pragmas to minimize size and USING_amalloc pragma for automatic malloc) (Z88DK/SCCZ80_NEW, Z88DK/SDCC Pragmas to eliminate exit stack and stdio heap, only %s enabled for printf, –opt-code-size for sdcc, automatic malloc)
  • STARTREK.C (Hitech-C CPM v3.09 Too many symbols for assembler) (Hitech-C Z80 v7.50 Program hangs may need lower optimization level, Typedef double_t, Remove seed loop counter) (Z88DK/SCCZ80_NEW, Z88DK/SDCC Pragmas to minimize crt, only %sdf enabled for printf, error strings reduced to minimum, –opt-code-size for sdcc)

Installation

The nightly build is the most current version. The package available for download from sourceforge is dated 10 Jan 2017.

The nightly build should be preferred unless you have a reason to install an older version of z88dk. The documentation on this page will apply to the nightly build.

NOTE: Some users have reported problems with usage because they have other unrelated programs installed named “zcc” or “z80asm” earlier in their paths. If you are having build or compile trouble, try putting z88dk/bin at the front of your path to see if the problems go away.

Windows

Download the latest nightly windows build and unzip it into the destination directory. This will create a tree rooted in a z88dk subdirectory.

Some environment variables will have to be defined. On Windows 8, this can be done from the Control Panel by selecting “User Accounts”. On the left side of the pop-up box you should find a link to “Change my environment variables”. Click that and add the following:

Variable Value
ZCCCFG {z88dk}\lib\config

Finally, add ”;{z88dk}\bin” to Path.

In the above ”{z88dk}” is the full path to the z88dk directory created.

To update the install simply delete the old one, download a new nightly build and unzip in the same location.

Any command prompt that is opened will be ready to compile using z88dk.

Linux / Unix

Download the latest nightly checked source package and unzip it:

  wget http://nightly.z88dk.org/z88dk-latest.tgz
  tar -xzf z88dk-latest.tgz

This will create a populated z88dk directory in the current working directory.

To succeed in building the 'z80svg' graphics tool you need the 'libxml2' library to be previously installed, although its absence will not prevent the rest of the kit from building.

Then enter:

  cd z88dk
  chmod 777 build.sh
  chmod 777 config.sh
  ./build.sh

You can run z88dk keeping it in the current location, all you need to do is to set the following environment variable:

Variable Value
ZCCCFG {z88dk}/lib/config

Supposing you have bash (most likely it is your system default shell) and you want to keep z88dk in your local user environment (AKA 'home directory'), you can configure it permanently in this way:

  vi ~/.bash_profile

Modify the configuration as follows:

  export PATH=${PATH}:${HOME}/z88dk/bin
  export ZCCCFG=${HOME}/z88dk/lib/config

A system install is not supported in this release.

To complete installation you may want to build sdcc, details below.

Otherwise, if you wish to install z88dk and merge it with your default system environment, then edit 'z88dk/Makefile' and set your preferred destination position (default is /usr/local), then type: make install (you still should add the two environment variables in the system settings).

MacOS X

The MacOS X build contains prebuilt binaries to simplify installation. Download the latest package and unzip to a directory:T

  wget http://nightly.z88dk.org/z88dk-osx-latest.tgz
  tar -xzf z88dk-osx-latest.tgz

You can run z88dk keeping it in the current position, all you need to do is to set the following environment variable:

Variable Value
ZCCCFG {z88dk}/lib/config

Supposing you have bash (most probably it is your system default shell) and you want to keep z88dk in your local user environment (AKA 'home directory'), you can configure it permanently in this way:

  vi ~/.bash_profile

Modify the configuration as follows:

  export PATH=${PATH}:${HOME}/z88dk/bin
  export ZCCCFG=${HOME}/z88dk/lib/config

To update the install simply delete the old one, download a new nightly build and unzip in the same location.

Verify the Install

To test if the install was successful, create the following program in a text editor and save as “test.c”

#include <stdio.h>
 
main()
{
   printf("Hello World !\n");
}

Open a terminal or command prompt, change to the directory where the test program was saved and compile the program with:

zcc +zx -vn test.c -o test -lndos

There should be no errors.

Installation of Support Tools (Optional)

These tools are supplied by third parties.

m4

M4 Manual (v1.4.14)
Notes on the M4 Macro Language

m4 is the standard macro processor used on Linux and Unix machines. It has now been adopted as an optional macro pre-processor in z88dk. The nightly build and latest windows package downloadable at sourceforge now include the m4 binary for windows machines so that m4 is now available to all installs.

zcc will pre-process source files ending in .m4 extension using m4. The intention is to supply a macro pre-processing facility for files ending in .c.m4 / .asm.m4 / .inc.m4 / .h.m4 extensions.

zcc will apply m4 and then immediately write a macro expanded file to its original source directory as .c / .asm / .inc or .h so that it is available in the current compile. .c and .asm expanded files are then processed as normal by zcc.

Note that files in the form *.ext.m4 will result in a new file *.ext generated in the same directory, overwriting any file of the same name there. Source files of the form *.ext.m4 should be understood to also reserve the name *.ext in the same directory.

m4 is also used by the new c library to generate its crts from macros.

sdcc

SDCC Main Page @ Sourceforge
SDCC Nightly Build Page
SDCC Manual

sdcc is an open source optimizing C compiler that can target the z80. A patched version is compatible with z88dk and can be invoked by zcc when the appropriate flag is selected on the command line. Besides making sdcc compatible with the z88dk toolchain, the patch also improves sdcc's generated code, addresses some of sdcc's code generation bugs and, being part of z88dk, grants sdcc access to z88dk's assembly language libraries and ready-made crts.

1. Windows

The z88dk nightly build for windows is now self-contained and includes the zsdcc binary in z88dk/bin. Separate installation of sdcc is no longer necessary. sdcc_z88dk_patch.zip will often contain a more recent windows build of zsdcc that can be copied into z88dk/bin.

2. Mac OSX

The z88dk nightly build for mac osx is now self-contained and includes the zsdcc binary in z88dk/bin. Separate installation of sdcc is no longer necessary.

3. Linux / Unix

Other users will have to apply the svn patch found in sdcc_z88dk_patch.zip and build sdcc from source.

A typical linux install process for sdcc would look like this:

  • svn checkout svn://svn.code.sf.net/p/sdcc/code/trunk sdcc-code This will check out the current development version of sdcc. If you already have the sdcc-code tree from a previous checkout you can instead perform an update.
  • copy “sdcc-z88dk.patch” from inside sdcc_z88dk_patch.zip into the sdcc-code directory
  • cd sdcc-code
  • patch -p0 < sdcc-z88dk.patch Apply the z88dk patch.
  • cd sdcc
  • ./configure Some additional packages may have to be installed to complete configuration. On Knoppix 7.6.0 these were flex, bison, gputils, libboost-dev.
  • make The build will fail when the non-z80 device libraries are compiled. This is expected and is the reason the z88dk patch has not been accepted into sdcc; the resulting binary is error-free for z80 compilation only.
  • cd bin
  • mv sdcc {z88dk}/bin/zsdcc Move the patched sdcc executable to {z88dk}/bin and rename it “zsdcc”.
  • cp sdcpp {z88dk}/bin/zsdcpp Copy the sdcc preprocessor to {z88dk}/bin and rename it “zsdcpp”.
  • cd ../.. Back to sdcc-code.
  • patch -Rp0 < sdcc-z88dk.patch Undo the patch. We will re-build sdcc from original source so that sdcc is available in its original form.

If you have an existing sdcc install or you don't want to continue with an install of sdcc you can stop here and verify the install was successful below. Optionally keeping the sdcc source tree in an unpatched state can allow you to update the zsdcc binary by repeating the steps above as sdcc itself is updated. Both z88dk and sdcc are active projects that see frequent updates.

To complete sdcc installation continue with these steps:

  • cd sdcc
  • make The build process should now complete.
  • sudo make install

VERIFY THE INSTALL

“zsdcc” is the name of the patched sdcc executable and “zsdcpp” is the name of the sdcc C preprocessor that z88dk will invoke.

Entering “zsdcc -v” and “zsdcpp –version” should print version information. The version information for zsdcc should begin with “ZSDCC is a modification of SDCC for Z88DK”. If that is not the case, the system is executing an older version of zsdcc from the sdcc/bin directory rather than the new version in z88dk/bin. The older version would have been installed by following an older version of these instructions. Find the zsdcc executable in sdcc/bin and remove it.

To verify that sdcc is usable from z88dk, try compiling sudoku.c for the cp/m target using sdcc:

zcc +cpm -vn -SO3 -clib=sdcc_iy --max-allocs-per-node200000 sudoku.c -o sudoku -create-app

clang+llvm

(recent and not quite ready)

LLVM + SDCC Toolchain
Clang 3.8 Manual
LLVM 3.8 Documentation

In order to compile using clang, you must also have sdcc installed.

Clang is a C front end that translates C to LLVM intermediate form and LLVM functions as the back end performing various optimizations on the way to generating the output. Clang+LLVM are well known projects in the open source community, probably made most famous by Apple which uses it as its C compiler.

LLVM as-is is not well suited for targeting small microprocessors. It would take a considerable amount of work to persuade it to generate as good code as the currently available non-LLVM z80 compilers. However, there is a fruitful shortcut that can be taken to exploit some of LLVM's strengths and that is using Clang to compile C to LLVM intermediate form and then using LLVM to perform optimizations and output as C. Then that C can be compiled by an existing z80 C compiler to a binary. In this case, sdcc will be used as the z80 C compiler because it best supports the modern C code that LLVM will be producing.

This process is not as wasteful as it sounds – there is some indication that a Clang/LLVM/SDCC sequence might produce better code. However, the real prize is that the LLVM toolchain can be used to compile other languages to C and this is an avenue z88dk would like to explore in the future.

1. Windows

2. Mac OSX

3. Linux / Unix

(installation instructions coming; linux/unix users can perhaps follow the instructions in the toolchain link above but rename the binaries to zclang and zllvm-cbe; compiles use the new c library with sdcc command line switches and ”-clib=clang_iy” or ”-clib=clang_ix”)

zcc +embedded -vn -SO3 -clib=clang_iy --max-allocs-per-node200000 test.c -o test -create-app

The Tools

  • ZCC is the front end of the toolchain. Most projects will be compiled with a single zcc invocation.
  • SCCZ80 is z88dk's native C compiler.
  • Z80ASM is a fully featured z80 assembler / linker / librarian that is section-aware.
  • APPMAKE processes the raw binaries generated by the toolchain to generate various output file formats. Output formats include tape files, disk files and intel hex format.
  • Z80NM is z80asm's companion archiver. z80nm can provide a listing of functions and data contained in a library or object file.
  • TICKS is a command line z80 emulator that runs z80 programs at the maximum speed possible on the host and counts execution time of a selected code block in z80 clock cycles. It is helpful as a profiler.
  • ZX7 is a PC-side lz77 data compression tool that has companion functions in the z80 library for decompression.
  • DZX7 is a PC-side decompressor counterpart for zx7.

These tools are not normally invoked by the user:

  • ZCPP is the C pre-processor invoked on C source prior to handoff to the C compiler (sccz80).
  • ZPRAGMA is used by the toolchain to process pragmas embedded in C source.
  • COPT is a peephole optimizer that transforms input assembler according to supplied substitution rules. It is used to optimize sccz80's output and to translate sdcc's output assembler to (nearly) Zilog syntax.

Quickstart Guide

These brief command line examples will get you compiling programs right away.

When compiling there is a choice between using the classic C library or the new C library.

Supported Targets

A compile line must specify a target. This allows the compiler to automatically select the correct libraries and drivers to use for the targeted machine. The set of supported targets differs between the two C libraries. If your machine is not directly supported, the generic target can be used to generate code. With the classic C library, ”+test” is the generic target and with the new C library the generic target is ”+embedded”.

List of Supported Targets (Classic C Library)

List of Supported Targets (New C Library)

  • cpm CPM 2.2 currently without disk i/o (the classic lib does implement disk i/o)
  • embedded Embedded z80 systems or generic z80 target
  • zx Sinclair ZX Spectrum

Compiling with SCCZ80

SCCZ80 is z88dk's native C compiler.

The Classic C Library

The classic C library is the library that has always shipped with z88dk. It has a simpler stdio model than the new C library and has many extended libraries that have not yet been ported to the new C library. The simpler stdio model allows it to generate small binaries for most C programs.

zcc +zx -vn test.c -o test -lndos -create-app
  • +zx Select the target machine (zx spectrum in this example). This causes the appropriate crt and libraries to be linked. ”+test” is a generic target.
  • -vn Verbose messages off. Omitting this option will cause zcc to detail each step taken during compilation.
  • -o filename Output filename.
  • -lndos Link the “nodos” library, required when no disk support is needed. A particular target with supported disk system can link to a library that connects that disk system to stdio.
  • -lm, -lmz (optional) Link against z88dk's genmath floating point library (”-lm”) or against the target's native floating point library (”-lmz”) if available.
  • -create-app (optional) Invoke APPMAKE with default parameters. The type of file generated will depend on the default type for the target. In this case, for the zx target, a tape file will be created in addition to the output binary.

The list of source files can contain any combination of C source (*.c), asm source (*.asm,*.opt), object files (*.o) or (nightly build) list files. List files are identified by a leading ”@” in their names and contain a list of source files, one file per line. zcc will read the contents of a list file and add listed files to the compile.

There must be exactly one main() function defined if an executable is to be produced.

Selected Command Line Options

  • -a Translate C source to assembler.
  • -c Generate an object file rather than an executable.
  • –c-code-in-asm (nightly build) Place C code as comments in output assembler.
  • -Ca.. Pass an option to the assembler. For example, "-Ca-D__SDCC"
  • -Ca-IXIY Instruct the assembler to swap IX and IY in assembled code.
  • -Cl.. Pass an option to the linker. For example, ”-Cl–split-bin”
  • -create-app Invoke APPMAKE with default parameters. The type of file generated from the output binary will depend on the default type defined by the target.
  • -crt0 filename Use a user-supplied crt.
  • -g Generate a global defines file that lists global labels and their assigned values as a sequence of defc constants.
  • -h Help.
  • -lname Link against the “name” library.
  • -Lpath Search path for libraries when linking.
  • –list (nightly build) Generate list files for the crt and each C source file compiled.
  • -m Generate a map file listing all labels and their values twice with the first half in alphabetical order and the second half in address order.
  • -notemp Intermediate files are left in the compile directory. Can be helpful for debugging.
  • -o outfile Change the output filename root.
  • -On Selects z88dk's peephole optimization level. Level 2 is the default and level 3 is the highest. Level 3 contains substitution rules that may reduce program size at the expense of speed.
  • -preserve Prevent the list of active pragmas stored in “zcc_opt.def” from being deleted prior to compiling. This is important when generating object files in a makefile. If your compile is a single line invocation of zcc this option is unnecessary.
  • -startup=n Quite often targets have several different crts available that can be selected by number. Differences between the crts can include different devices attached to stdin,stdout,stderr and different memory models.

Translating C to Assembler

zcc +zx -vn -a test.c

“test.c” will be translated to assembler in output file “test.opt” or “test.asm” depending on the optimization level.

Selection of the target machine ensures the include path is set, will enable headers specific to the target architecture, chooses default optimization (”-O2” for sccz80) but otherwise it has no effect on the code generated. If you just want to see the generated code for a standard C program, any target selection will do and the most generic in the classic C library is the ”+test” target.

The New C Library

The new C library aims to comply with a large subset of C11. It features an object oriented stdio, windowed terminals, proportional fonts and the ability to generate standalone & ROMable software.

zcc +zx -vn -clib=new test.c -o test
  • +zx Select the target machine (zx spectrum in this example). This causes the appropriate crt and libraries to be linked. ”+embedded” is the generic target.
  • -vn Verbose messages off. Omitting this option will cause zcc to detail each step taken during compilation.
  • -clib=new Selects the new C library version compatible with sccz80.
  • -o filename Output filename.
  • -lm (optional) Link against z88dk's math48 floating point library. Native floating point libraries are not yet available from the new clib.

The list of source files can contain any combination of C source (*.c), asm source (*.asm,*.opt), object files (*.o) or (nightly build) list files. List files are identified by a leading ”@” in their names and contain a list of source files, one file per line. zcc will read the contents of a list file and add listed files to the compile.

There must be exactly one main() function defined if an executable is to be produced.

Because the new C library is section aware, the generated output will be one or more raw binaries. The ”-create-app” option can no longer be used as there is insufficient information available to generate a default executable suitable for the target from multiple output binaries. Instead, APPMAKE can be invoked directly on the output binaries to generate a suitable output file.

Selected Command Line Options

  • -a Translate C source to assembler.
  • -c Generate an object file rather than an executable.
  • –c-code-in-asm (nightly build) Place C code as comments in output assembler.
  • -Ca.. Pass an option to the assembler. For example, "-Ca-D__SDCC"
  • -Ca-IXIY Instruct the assembler to swap IX and IY in assembled code.
  • -Cl.. Pass an option to the linker. For example, ”-Cl–split-bin”
  • -Cl–split-bin Causes z80asm to generate one output binary per section defined in the source. A directory listing afterward can tell you at a glance what is taking up the space in the final executable.
  • -g Generate a global defines file that lists global labels and their assigned values as a sequence of defc constants.
  • -h Help.
  • -lname Link against the “name” library.
  • -Lpath Search path for libraries when linking.
  • –list (nightly build) Generate list files for the crt and each C source file compiled.
  • -m Generate a map file listing all labels and their values twice with the first half in alphabetical order and the second half in address order.
  • -notemp Intermediate files are left in the compile directory. Can be helpful for debugging.
  • -o outfile Change the output filename root.
  • -On Selects z88dk's peephole optimization level. Level 2 is the default and level 3 is the highest. Level 3 contains substitution rules that may reduce program size at the expense of speed.
  • -preserve Prevent the list of active pragmas stored in “zcc_opt.def” from being deleted prior to compiling. This is important when generating object files in a makefile. If your compile is a single line invocation of zcc this option is unnecessary.
  • -startup=n Quite often targets have several different crts available that can be selected by number. Differences between the crts can include different devices attached to stdin,stdout,stderr and different memory models.

Translating C to Assembler

zcc +zx -vn -a -clib=new test.c

“test.c” will be translated to assembler in output file “test.opt” or “test.asm” depending on the ”-O” optimization level.

Selection of the target machine ensures the include path is set, will enable headers specific to the target architecture, chooses default optimization (”-O2” for sccz80) but otherwise it has no effect on the code generated. If you just want to see the generated code for a standard C program, any target selection will do and the most generic in the new C library is the ”+embedded” target.

Information on what occupies space in the final binary can be learned by adding ”–Cl-split-bin” to a compile line that generates a binary. The output will be one binary per defined section whose sizes will reveal what is occupying space.

Compiling with SDCC

SDCC is a third party C compiler that must be patched to work with z88dk. The patched executable “zsdcc”, aside from changes made to accommodate the z88dk backend, also fixes bugs connected with sdcc's peephole optimizer that allows zsdcc to produce better code than the current version of sdcc.

The temporary overview that was here gave some details on using sdcc in combination with the new c library for targetting embedded z80 systems. This information has been replaced by a much more complete discussion of the new c library's embedded target. Even if your intention is to compile for another target, this discussion is very insightful regarding how the new c library and its crts function. In terms of internal implementation, the embedded target is the base target with other targets applying variations to it.

Recent development has also allowed sdcc to be used in combination with the classic c library in z88dk. A subtopic is forming below.

See the sdcc optimization section for information on optimization levels for sdcc compiles.

See the compile sequence section for information on how z88dk creates binaries using sdcc.

See the reducing binary size section for tips on how to write compact and fast code for small micros like the z80. In particular, use of statics and unsigned types where possible will have a significant impact on code size and speed.

See mixing C and assembly language for more details on how C and assembly code can be effectively mixed in projects.

The Classic Library

Programs using the classic library can be compiled with sdcc. The following limitations must be respected:

  • Code that uses floating point can not be compiled with sdcc and the classic library
  • Library conversion is a work-in-progress and as such a small number of library functions may not be available.

A sample command line to compile with sdcc is:

zcc +zx -SO3 --max-allocs-per-node200000 --reserve-regs-iy -compiler=sdcc test.c

The New C Library

The new C library aims to comply with a large subset of C11. It features an object oriented stdio, windowed terminals, proportional fonts and the ability to generate standalone & ROMable software.

zcc +zx -vn -SO3 -clib=sdcc_ix --max-allocs-per-node200000 --reserve-regs-iy test.c -o test
  • +zx Select the target machine (zx spectrum in this example). This causes the appropriate crt and libraries to be linked. ”+embedded” is the generic target.
  • -vn Verbose messages off. Omitting this option will cause zcc to detail each step taken during compilation.
  • -SO3 Use the aggressive peephole rules supplied with z88dk. This can improve sdcc's generated code significantly.
  • -clib=sdcc_ix, -clib=sdcc_iy Selects the new C library and sdcc as compiler. The modified version of sdcc must be in the path. The new C library uses one index register and selection of “sdcc_ix” chooses the library version using ix. “sdcc_iy” is also available and has the library using iy. Because sdcc uses the ix register as a frame pointer, use of the “sdcc_iy” library can lead to smaller executables as the library will not have to preserve the ix register in calls. The choice is available because some targets do not allow the use of one index register or the other. Note that sdcc expects that iy is not modified by user-code so selection of the “sdcc_iy” library automatically causes ”–reserve-regs-iy” to be added to the compile line.
  • –max-allocs-per-nodeNNNNN (optional) Selects sdcc's optimization level. The default value is 3000; large values can increase compile time considerably with correspondingly better code generation. NNNNN=200000 is a reasonable upper bound.
  • –reserve-regs-iy (optional) sdcc is not allowed to use iy. sdcc usually generates better code with this option enabled so it can be worthwhile to try but primarily this option is available for targets that cannot easily allow use of more than one index register.
  • -o filename Output filename.
  • -lm (optional) Link against z88dk's math48 floating point library. Native floating point libraries are not yet available from the new clib.

The list of source files can contain any combination of C source (*.c), asm source (*.asm,*.opt), object files (*.o) or (nightly build) list files. List files are identified by a leading ”@” in their names and contain a list of source files, one file per line. zcc will read the contents of a list file and add listed files to the compile.

There must be exactly one main() function defined if an executable is to be produced.

Because the new C library is section aware, the generated output will be one or more raw binaries. The ”-create-app” option can no longer be used as there is insufficient information available to generate a default executable suitable for the target from multiple output binaries. Instead, APPMAKE can be invoked directly on the output binaries to generate a suitable output file.

Selected Command Line Options

  • -a Translate C source to assembler.
  • -c Generate an object file rather than an executable.
  • –c-code-in-asm (nightly build) Place C code as comments in output assembler.
  • -Ca.. Pass an option to the assembler. For example, "-Ca-D__SDCC"
  • -Ca-IXIY Instruct the assembler to swap IX and IY in assembled code.
  • -Cl.. Pass an option to the linker. For example, ”-Cl–split-bin”
  • -Cl–split-bin Causes z80asm to generate one output binary per section defined in the source. A directory listing afterward can tell you at a glance what is taking up the space in the final executable.
  • -D__SDCC_DISABLE_BUILTIN Disable sdcc's builtin functions. sdcc will sometimes inline a handful of common string functions by default (see the end of string.h).
  • –fsigned-char (zsdcc updated after v1.99) “char” means “signed char”. New versions of sdcc treat “char” as “unsigned char”.
  • –fverbose-asm When translating C to asm, includes code generator and peephole comments in the output asm source. Comments in function headers will tell if the compiler's optimization level (–max-allocs-per-node) was high enough to achieve optimal register assignment.
  • -g Generate a global defines file that lists global labels and their assigned values as a sequence of defc constants.
  • -h Help.
  • -lname Link against the “name” library.
  • -Lpath Search path for libraries when linking.
  • –list (nightly build) Generate list files for the crt and each C source file compiled.
  • -m Generate a map file listing all labels and their values twice with the first half in alphabetical order and the second half in address order.
  • –max-allocs-per-nodeNNNNN Selects code generation optimization level for sdcc. NNNNN is 3000 by default and 200000 is a good upper bound. Note there is no space between the option text and NNNNN.
  • -notemp Intermediate files are left in the compile directory. Can be helpful for debugging.
  • -o outfile Change the output filename root.
  • -On Selects z88dk's peephole optimization level. Under sdcc the purpose of the optimization rules is different. Level 0 will leave sdcc's output in its native asz80 format. z88dk cannot assemble this format. Level 1 translates sdcc's native format to more standard Zilog syntax. Level 2 changes sdcc's calls to its primitives to use a calling convention that saves extra bytes. The default is level 2.
  • –opt-code-size sdcc will call a subroutine to set up the frame pointer on entry to a function. (nightly build) programs using 64-bit integers will see significant code size reduction.
  • -preserve Prevent the list of active pragmas stored in “zcc_opt.def” from being deleted prior to compiling. This is important when generating object files in a makefile. If your compile is a single line invocation of zcc this option is unnecessary.
  • –reserve-regs-iy Prevent sdcc from using the iy register pair.
  • -SOn Selects the peephole optimization level for sdcc. Level 0 does not perform any code substitutions. Level 1 uses only the rules supplied by sdcc. Level 2 uses the sdcc rules and some rules that fix some of sdcc's code generation bugs. Level 3 is the highest level and in addition to level two, it introduces many new code substitution rules that can reduce code size and improve speed. Level 2 is the default.
  • -startup=n Quite often targets have several different crts available that can be selected by number. Differences between the crts can include different devices attached to stdin,stdout,stderr and different memory models.

In general any sdcc option can be appended to the compile line.

Translating C to Assembler

zcc +zx -vn -a -SO3 -clib=sdcc_ix --max-allocs-per-node200000 --reserve-regs-iy test.c
zcc +zx -vn -a -SO3 -clib=sdcc_iy --max-allocs-per-node200000 test.c
zcc +zx -vn -a -SO3 -clib=sdcc_iy --max-allocs-per-node200000 test.c --c-code-in-asm

“test.c” will be translated to assembler in output file “test.opt” or “test.asm” depending on the -O optimization level.

Selection of the target machine ensures the include path is set, will enable headers specific to the target architecture, chooses default optimization (”-O2 -SO2” for sdcc) but otherwise it has no effect on the code generated. If you just want to see the generated code for a standard C program, any target selection will do and the most generic in the new C library is the ”+embedded” target.

Information on what occupies space in the final binary can be learned by adding ”-Cl–split-bin” to a compile line that generates a binary. The output will be one binary per defined section whose sizes will reveal what is occupying space.

Overview

ZCC is the toolchain front-end. It takes as input a list of files consisting of any combination of C source (*.c), assembly language (*.asm,*.opt), object files (*.o) or (nightly build) list files, and compiles them into a single object file or into one or more binary executables. The steps taken to compile or assemble each input file is determined by each file's extension. A mandatory target selector (”+target”) allows zcc to read a configuration file that specifies which libraries to link against, which crt to use, what the include path is and various other defaults. In short, zcc acts like a magic bullet that will do what is needed to generate final binaries given any sort of input. The tools (compilers, assemblers, linkers) can also be invoked directly if that is preferable.

Z88DK Contains Two Independent C Libraries

The “classic” library is the one that has always shipped with z88dk. It is large and, in particular, contains a lot of extension libraries for things like graphics and sound. Its stdio model supports both input and output via character i/o functions supplied by the target. One disk device, one terminal and one network device can be supported in a single compile. stdio is compact and is easy to re-target. The classic library is not section aware yet so it generates code and data into a single binary blob that is intended to be run from RAM, with a few exceptions.

The “new” library is aiming for compliance with a large subset of C11. It is still under development but is already extensive, containing 700 functions and 30,000 lines of assembly code. It too contains extension libraries for things like sound, proportional fonts, data compression, container types, etc, but it does not yet contain many of the extension libraries found in the “classic” library. The stdio model is object-oriented and supports any number of attached devices in a single compile. The library supplies object-oriented driver base classes that can be derived from to write target-specific drivers. Currently the library contains terminal drivers that support line editing and terminal emulation using fixed width or proportional fonts. Writing new device drivers (and therefore targeting a new machine) involves writing code that responds to a small set of stdio messages and this can be done with or without the help of library code. The library is section aware and can produce code and data that is placed arbitrarily in memory or in multiple memory banks.

Over time features will homogenize between the two C libraries but they will remain independent. The simpler feature set of the “classic” C library can mean a smaller code footprint in many circumstances.

The selection of which C library to use is made on the compile line. Both libraries' functions are documented below.

Z88DK Supports Two C Compilers

sccz80 is z88dk's native C compiler. It makes optimization decisions locally. The peephole optimizer stage can further reduce code size by up to a third by performing simple text transformations on the assembly output of the compiler. The main feature of sccz80 is that it tries to generate small code by implementing most compiler actions through subroutine calls. The resulting code will be slower than some other compilers but it should also be smaller. This strategy pairs well with a library written in assembly language which provides a speed and size advantage compared to other z80 compilers. sccz80 does not reserve any registers for itself and accesses the stack frame through offsets from the stack pointer register.

sdcc is an open source optimizing compiler than can target the z80. It is capable of global optimizations and contains a peephole optimizer stage that can perform simple code analysis while performing code substitutions. sdcc's compiler actions tend to be inlined which generates faster code but may also lead to larger code. Aside from its optimization capability, it is one of the few, if only, z80 C compilers aiming for comprehensive standards compliance with C89, C99 and some C11. sdcc implements a stack frame using the IX register as frame pointer whose value must be preserved. IY must also be preserved but sdcc can be instructed not to use IY with a compiler switch. The current sdcc has to be patched in order to be made compatible with z88dk.

The selection of which C compiler to use is made on the compile line with the ”-clib=” option. Only the “new” C library is compatible with sdcc at this time.

Z88DK Enables Integrated C and Assembly Language Development

C and assembly language are treated equally within z88dk. Projects are free to be written purely in C, purely in assembly language or any mixture of the two. Assembly language can be inlined within C code but it can also interact with C as a collection of standalone assembler subroutines. Indeed the C libraries are written in assembly language and supply C interface code so that they can be called directly from either C or asm.

C and Asm global variables can be directly accessed from both C and assembly language. C calls to assembly subroutines can use one of the three linkages supported by both C compilers (standard, fastcall and callee) with parameters passed in the fashion specified, either via stack or via register.

More details can be found here.

C Compiler Characteristics

Standards Compliance

SCCZ80

sccz80 is almost C89 compliant.

Missing Features

Structs cannot be assigned, passed by value to functions or returned by functions.

Passing structures around is an inefficient operation since their entire contents have to be duplicated on the stack. It is always preferable to pass pointers to the structures instead. However this is not quite the same semantic since a struct passed by value can be modified inside the function without changing the original struct outside the function. If this semantic is required you will have to supply alternate code to get the same effect that will most likely involve using memcpy() to make copies of structures before and after calls. The lack of support for struct assignment and parameters does affect a small number of C library functions such as div() and family. These functions have been modified to exchange pointers to structs rather than the structs themselves.

Multi-dimensional arrays are not directly supported.

This means, for example, arrays like “int a[10][20];” cannot be declared. So-called jagged arrays (arrays of arrays in which all indices prior to the last are actually pointers) are in fact supported and the a[x][y] syntax used to access them work fine.

One workaround for multi-dimensional arrays is to create a jagged array. For the example given this would involve creating an array of 10 pointers to int as in “int *a[10];” and assigning to each index a pointer to its own array of 20 ints. The a[x][y] syntax can then be used unchanged.

A second work-around is to declare a single dimension array of the required number of elements and then manually calculate the location of an element from its two indices. For this example one might declare “int a[200];” and then index the array as in “a[x*20+y];” If this looks cpu intensive to you, it is. This is how the compiler must implement multi-dimensional array indexing for you; jagged arrays have better performance at the expense of space.

Function pointers cannot be defined with parameters.

All function pointers must be declared using the style type (*f)(), for example double (*f)() and called as usual as in ”(f)(16384, 32768, 6192);” The consequence of this is that there is no parameter information carried with the function pointer. You must be careful to call via the function pointer using the correct number of parameters and the correct parameter types using explicit casting as required.

Since the compiler has no idea how many parameters a void function pointer is supposed to have, it will happily generate calls with too few or too many parameters. And since the compiler has no idea what the parameter types should be it cannot cast to the type expected by the function; instead it simply pushes the type of the parameter onto the stack. This can be disastrous if the parameter passed is a 4-byte long while the function expects a two-byte int; the target function will not read parameters properly from the stack. Explicitly casting that long parameter to the int expected by the function solves the problem.

Known Issues

sccz80 will sometimes demote longs to ints in mixed expressions.

To avoid this problem use explicit casting rather than rely on expected implicit conversions.

long a, c;
int b;
 
c = a + b;          // may result in improper demotion of long to int before the addition
c = a + (long)(b);  // will always work

It's a good coding practice to explicitly cast types where type conversion is required.

SDCC

sdcc can operate under C89, C99 and partial C11 compliance via command line switch. By default it operates under C99 compliance.

Missing Features

Structs cannot be assigned, passed by value to functions or returned by functions.

Passing structures around is an inefficient operation since their entire contents have to be duplicated on the stack. It is always preferable to pass pointers to the structures instead. However this is not quite the same semantic since a struct passed by value can be modified inside the function without changing the original struct outside the function. If this semantic is required you will have to supply alternate code to get the same effect that will most likely involve using memcpy() to make copies of structures before and after calls. The lack of support for struct assignment and parameters does affect a small number of C library functions such as div() and family. These functions have been modified to exchange pointers to structs rather than the structs themselves.

Variables must be declared at the beginning of a block.

C99 allows variables to be declared as needed in the middle of blocks (that is code enclosed in curly braces {…}). This language feature is not supported yet so all variable declarations must be hoisted to the top of a block as in C89. New: sdcc will now accept variable declarations in the initializer portion of for-loops.

Known Issues

–reserve-regs-iy

Use of this option can sometimes lead to buggy code generation when accessing global or static variables. Some peephole rules have been added at -SO2 level to correct this issue and this looks to have been successful.

The option will be ignored and the compiler will use iy if the stack frame is greater than 128 bytes in size. The size of the stack frame is determine by the size of local variables declared. One key difference is when this flag is active, the compiler will not expect iy's value to be preserved across function calls.

compiler sometimes fails when generating code for 64-bit integers

The issue can arise when the compiler tries to keep a 64-bit value in registers when it is reused in a following statement.

z = llabs(x);
lltoa(z, buffer, 10);

The compiler may fail with a fatal error when it tries to reuse “z” from the assignment in the following call to lltoa().

When this occurs, the error can be circumvented by inserting a dummy function call between the two statements. The dummy function “intrinsic_stub()” from <intrinsic.h> is ideal for this purpose as it results in no code being inserted between the statements.

sdcc critical language extension bugged for nmos z80s

sdcc implements a __critical { ... } block that will disable interrupts while the enclosed code runs. The interrupts are not simply disabled; instead the current interrupt state is read and then after the critical block, interrupts are only re-enabled if they were previously enabled. In order to learn the current interrupt state, the instruction “ld a,i” is executed. On cmos z80s this is a reliable way to find out if interrupts are enabled but on nmos z80s it isn't. So these critical sections are in fact unreliable on nmos z80s.

This issue has been fixed at the -SO2 optimization level. sdcc's inlined code to implement critical sections are replaced with library calls. Since the library knows whether the target is a cmos or nmos z80, it can provide the correct code to implement the desired behaviour.

clumsy access of static variables

Code generated to access globals and static variables can often be sub-optimal. Peephole rules have been added at -SO3 level to help mitigate this.

sdcc bug tracker

Data Types

The most common surprise to C programmers used to programming 32- and 64-bit machines is the reduced bit width of many types. The z80 processor is only 8-bit with some 16-bit characteristics so the bit widths of the built in data types are correspondingly reduced for best performance. Always keep this in mind while writing programs targetting small processors.

char int long long long float(2,4) double(2,4) void *
SCCZ80 8 16 32 n/a 48 48 16(5)
SDCC 8 16 32 64 32(3) 32(3) 16

1 Available in the nightly build only.

2 sccz80 does not distinguish between float and double and sdcc only supports the float type, however the library aliases float and double for sdcc so that source code is compatible between the two compilers. In user code, use of types “float_t” and “double_t” as defined in “math.h” and “stdlib.h” will eliminate compiler warnings and allow seemless compatibility of source between both compilers.

3 The floating point libraries supplied by z88dk operate on 48-bit floats which are converted at the compiler / library interface.

4 Targets may supply a native float library with variable characteristics. If a native library is selected on the compile line, conversions are inserted at the compiler / native interface to match the bit-width of float types expected by the compiler.

5 Pointer types are two bytes for addressing 64k of memory. sccz80 is capable of supporting three-byte pointer types for bankswitched systems but this feature is not generally taken advantage of by z88dk at this time.

Notes:

1. The C standard allows the implementation to decide whether char is signed or unsigned. Always make that decision explicitly in your code by using a “signed char” or an “unsigned char” if the type will be used as an arithmetic type. sdcc defaults to unsigned char which may be unexpected for many (–fsigned-char will change the default to signed).

2. Always prefer to use unsigned types whenever possible. Careless use if signed types can lead to superfluous sign extension code inserted by the compiler. The z80's instruction set is better suited to unsigned types.

New C code intended to be portable should use the data types defined in <stdint.h>. These data types explicitly define the bit width in their names. See int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t in particular.

Function Call Linkage

This is a technical subject probably only of interest to assembly language programmers and can safely be skipped.

Function call linkage refers to how the compiler communicates function parameters to- and return values from- a called function.

Parameter Passing

Both compilers support three calling conventions listed below.

1. Standard Linkage

The compiler pushes parameters onto the stack, calls the function and then clears the stack by popping the parameters off the stack.

2. Fastcall Linkage

The compiler passes a single parameter by register. The register used is always a subset of DEHL depending on the parameter bit width. So, for example, an integer would be passed in the HL register and a long in DEHL. sccz80's floats / doubles are 48-bit and are treated a little differently. They are passed via the “primary floating point accumulator”. In the classic C library this is six bytes of static memory labelled “fa”. In the new C library this is the registers BCDEHL' in the exx set. This means the classic C library's floating point implementation is not re-entrant whereas the new C library's is. sdcc's 64-bit long long type cannot be passed using fastcall linkage.

3. Callee linkage

The compiler pushes the parameters onto the stack, calls the function but the function is responsible for clearing the stack. This suits assembly language functions very well as they can pop parameters into suitable registers, execute and return. This calling convention can save hundreds of bytes in large programs over standard linkage.

Parameter Order

Next we discuss an uncomfortable point of departure between sdcc and sccz80. sdcc pushes parameters onto the stack in right-to-left order whereas sccz80 pushes parameters in left-to-right order. For C compilers the best method is right-to-left as it makes retrieving vararg parameters easy; in the left-to-right case, in order to retrieve the first vararg parameter, the function must know how many bytes were pushed onto the stack. The decision to use left-to-right order for sccz80 was made 35+ years ago and we are investigating whether it would be possible to change that so that both compilers use the same parameter order. This would help greatly with the plan to allow sdcc- and sccz80- generated object files to be linked together.

Return Values

Return values are held in a subset of DEHL depending on the return value's bit width. HL would hold an integer return value whereas DEHL would hold a long return value. sccz80's 48-bit float / double is treated differently with the classic library returning its value in the “primary floating point accumulator” which is six bytes of static memory at address “fa” and the new c library returning by registers BCDEHL' in the exx set. Note the same rules apply as for fastcall linkage where a single parameter is passed to a function via DEHL.

Return of sdcc's 64-bit long long type is handled specially. The compiler will pass a pointer to memory for the return value as the first parameter in the function call. This parameter is not listed in the function prototype. The called function must use that pointer to store the 64-bit value returned.

Notes

Calls through function pointers always use standard linkage.

All functions in the new C library make use of the quicker & smaller fastcall or callee linkage as do most functions in the classic C library. When C library functions are assigned to a function pointer, the function pointer is assigned an entry point that uses standard linkage.

Fastcall and callee linkage were originally added to sccz80 to efficiently call library functions written in assembler. sccz80 cannot generate fastcall or callee C code so only assembly language subroutines can be fastcall or callee. sdcc added these calling conventions for compatibility with z88dk's libraries. Unlike sccz80, sdcc is capable of generating fastcall C code. So if your C code takes just one parameter, sdcc will compile better linkage if the C function has the ”__z88dk_fastcall” decoration added to the end of the function signature. There is a noticeable improvement in the code generated by sdcc for fastcall functions particularly for small functions. Keep in mind that sdcc may have trouble calling fastcall functions by function pointer – it can only generate code for this if it is allowed to use the iy register. The C library has a different method for resolving this that allows all library functions, standard / fastcall / callee, to be called seemlessly via function pointer.

More details can be found in the Mixing C and Assembly topic.

Limitations

sdcc tends to generate faster code while sccz80 tends to generate smaller code particularly when dealing with longs, floats and statics and after code has been made small-uP friendly. For this reason we are working on making sdcc and sccz80 object code compatible so that portions of a C project can be compiled with both compilers and the result linked together. Although both C compilers are using the same library code this is not currently possible because of differences in the order that parameters are pushed on the stack for vararg functions and the incompatible float types that prevent floats from being communicated between sdcc- and sccz80- compiled functions.

So for the time being a project must be completely compiled with either sdcc or sccz80.

Makefiles

sccz80 produces binaries very quickly so the most convenient way to generate an executable is to simply list all the source files in a single zcc invocation and have it produce an executable from scratch every time.

sdcc, however, can take a long time to generate binaries when its optimization level is turned up (–max-allocs-per-node). It can save a great deal of time to have a makefile that generates object files from separate source files and then combines the lot into an executable. This way only C source that changes is re-compiled in each build step.

Makefiles are also a good way to automate the generation of the final output, which may not be limited to a single output binary. So even though sccz80 can generate binaries quickly, it can make sense to use a makefile with sccz80 to automate generation of the final output.

Object files can be generated using zcc with the ”-c” option added to the compile line. The generation of an executable can then be done by invoking zcc with a list of object files. This should not be new to anyone familiar with makefiles.

Pragmas

What is different in z88dk is that pragmas, generated by the compiler and the user, are used to select options in the crt.

An example of a compiler-generated pragma with the classic C library is sccz80's scan of printf format strings. sccz80 will keep track of what format specifiers are used and determine whether the compile can use a simple printf, medium printf or large printf implementation. This way the compiler can automatically reduce the size of the output binary by selecting the smallest printf implementation allowable.

An example of a user pragma that sets a crt option with the new C library is ”#pragma output CLIB_MALLOC_HEAP_SIZE = 2048”. This pragma will be seen by the crt which will reserve space for a 2k heap and automatically initialize it before main() is executed.

These pragmas are written to a file “zcc_opts.def” as assembler define directives during compilation. When an executable is generated, the crt is assembled as part of the last step of the compile and after “zcc_opts.def” is complete. The crt includes the “zcc_opts.def” file and uses the options specified to perform whatever initialization is necessary to implement the options.

Each time zcc is invoked, this “zcc_opts.def” file is deleted so that there is a clean compile on each invocation. However, when using a makefile the project is normally split into many source files that are individually compiled to object files. When each individual source file is compiled to an object file, the first step taken by zcc is to erase “zcc_opts.def” but this is not what you want to happen – the “zcc_opts.def” file should accumulate all options generated by all the source files. To allow that to happen, the ”-preserve” option should be specified on the zcc compile line to prevent “zcc_opts.def” from being deleted. When ”-preserve” is active any options encountered will be appended to “zcc_opts.def”.

Makefile Example

how to ensure zcc_opts.def does not grow indefinitely and is erased at an appropriate time.

Optimization Level

Both compilers support user-selected optimization levels.

sccz80

Peephole Optimizer

sccz80's output is passed through a peephole optimizer step that performs text substitutions on the compiler's output. There are three rule sets provided by z88dk and they are cumulative (ie applied one after the other). Which rule sets are applied is determined by the optimization level chosen on the compile line ”-On”.

  • -O0. Skip peephole optimizer.
  • -O1. Level 1 optimizations.
  • -O2. Level 2 optimizations.
  • -O3. Level 3 optimization that attempts to reduce code size at the expense of speed.

Example compile line:

zcc +zx -vn -O3 test.c -o test -lndos
zcc +zx -vn -O3 -clib=new test.c -o test

With ”-O3” selected, rule set #1 followed by rule set #2 followed by rule set #3 will be applied.

The default for all targets is ”-O2”.

sdcc

Code Generation

sdcc applies an optimization level during code generation that is supplied with the ”–max-allocs-per-node” flag. Larger numbers permit sdcc to perform deeper code analysis but this will also increase compile time considerably. The default is 3000 but a reasonable upper bound is probably 200000.

Another flag ”–opt-code-size” is intended to indicate to sdcc that small code is preferred. In the unpatched version of sdcc, this currently has little effect except to use a subroutine to set up stack frames inside functions and to prefer small code to clear up the stack after function calls. The impact on code size is small but present. In z88dk's version of sdcc, ”–opt-code-size” will also significantly reduce the size of code generated for handling 64-bit integers, sometimes by up to 50%. Recent changes also attempt to reduce code size of programs making heavy use of longs and floats where a 10% code size reduction is seen.

Example compile line:

zcc +cpm -vn -SO3 -clib=sdcc_iy --opt-code-size --max-allocs-per-node200000 test.c -o test

Peephole Optimizer

sdcc's output is passed through its own peephole optimizer step that performs text substitutions on the compiler's output. sdcc comes with some rules of its own but under z88dk we provide three different rule sets selected with ”-SOn” on the compile line.

  • -SO0. Skip peephole optimizer.
  • -SO1. Apply the rules provided by sdcc.
  • -SO2. Apply the rules provided by sdcc along with rules that fix some code generation bugs.
  • -SO3. Apply the rules provided by sdcc, the bugfix rules and a large set of aggressive rules provided by z88dk.

The default for all targets is ”-SO2”.

The SO3 rules have a significant impact on code size and speed and are regularly expanded as we check code generation for different programs. There are many hundreds of rules in the SO3 set so it is possible that errors may be present. If a program fails to run with SO3 enabled, try a compile at SO2 level to rule out the aggressive rules. A bug report in the z88dk forums would be appreciated if the SO3 rules are found to be at fault.

Example compile line:

zcc +zx -vn -SO3 -clib=sdcc_ix --reserve-regs-iy --max-allocs-per-node200000 test.c -o test

Z88dk Peephole Step

The output from the two steps above is passed through z88dk's peephole optimizer using sdcc-specific rules. There are several levels of rules which can be selected with ”-On” on the compile line. These rules are cumulative, meaning they are applied in sequence. With ”-O2” selected, the -O1 rules will be applied and then the -O2 rules afterward. The purpose of these rules is not to improve the code but instead to further process it.

  • -O0. Skip this step. In particular, sdcc's output will retain asz80's assembler syntax. This can be helpful when creating new peephole rules as sdcc's peephole optimizer operates on asz80 syntax input.
  • -O1. Translate assembler to (nearly) Zilog syntax. Replace sdcc areas with z88dk sections. Remove the redundant INITIALIZER area. Bring sdcc primitives into scope.
  • -O2. Change sdcc calls to its primitives to use callee linkage.

The default is ”-O2”. The minimum level must be ”-O1” for a program to be compilable by z88dk. The toolchain will accept source files ending in .s as using asz80 syntax and will perform the translation automatically during the compile.

Example compile line:

zcc +zx -vn -a -SO3 -O0 -clib=sdcc_ix --max-allocs-per-node200000 --reserve-regs-iy test.c

This line will translate the C to assembler and leave it in asz80 syntax.

Compile Sequence

zcc takes a list of files (*.c, *.o, *.asm, *.opt) and generates a single object file or binary executable as output. For each file, it determines from the file extension what steps need to be taken to generate an object file. When all files have been assembled into object files, they are collected together and either linked to form an executable or merged to form a single object file.

Seeing the steps taken for compiling C source to object file can take some of the mystery out of things.

sccz80

Example compile:

zcc +zx -vn -clib=new test.c -o test

“test.c” is taken through the following steps:

  • zcc reads configuration information from the target config file, in this case {z88dk}/lib/config/zx.cfg. This file contains various default flag settings such as crt to use, include path, library to use, default optimization level and so on. ”-clib=new” also causes the options under “CLIB new” to be read.
  • zcpp is run to process all c pre-processor directives. The output is a new C source file without pre-processor directives.
  • zpragma is run to process and remove any pragmas. The output is a C source file stripped of these pragmas and the file “zcc_opts.def” which contains information that needs to be communicated to the crt.
  • sccz80 is run to translate the C source to assembler. The output is an asm file containing the translated C source.
  • if the opt level is at least 1 (-O1) copt is run on the asm source using the level 1 peephole rules.
  • if the opt level is at least 2 (-O2) copt is run on the result using the level 2 peephole rules. (-O2 is the default)
  • if the opt level is at least 3 (-O3) copt is run on the result using the level 3 peephole rules.
  • z80asm is run to generate an object file from the asm source.

At this point if any other source files are included on the compile line, they are taken to an object file using the same or similar steps.

  • z80asm is invoked as linker with the crt listed as first file (in asm form) and a list of the object files following. The target library ”-lzx” and any libraries indicated by the user are added to the compile line. One or more binaries are output, one per section with independent ORG.

The crt includes the “zcc_opts.def” file which contains defined constants that indicate various options. The crt tests these defines to insert any asm code necessary to implement the options. Being listed first among the linker's list of files guarantees it is processed first and this also allows the crt to create the memory map.

sdcc

Example compile:

zcc +zx -vn -clib=sdcc_ix -SO3 --max-allocs-per-node200000 --reserve-regs-iy test.c -o test

“test.c” is taken through the following steps:

  • zcc reads configuration information from the target config file, in this case {z88dk}/lib/config/zx.cfg. This file contains various default flag settings such as crt to use, include path, library to use, default optimization level and so on. ”-clib=sdcc_ix” also causes the options under “CLIB sdcc_ix” to be read.
  • zsdcpp is run to process all c pre-processor directives. The output is a new C source file without pre-processor directives.
  • zpragma is run to process and remove any pragmas. The output is a C source file stripped of these pragmas and the file “zcc_opts.def” which contains information that needs to be communicated to the crt.
  • zsdcc is invoked with options not understood by zcc (”–max-allocs-per-node200000 –reserve-regs-iy”) to translate the C source to assembler and to run sdcc's peephole optimizer step. The peephole optimizer uses one of three rulesets. If “SO=0” no peephole rules are applied, if “SO=1” the rules that come with sdcc are applied, if “SO=2” the rules that come with sdcc are applied along with a few rules that fix code generation bugs, if “SO=3” the rules with sdcc are run along with the bugfix rules and a new set of rules that aim to improve code size and speed. The default is SO=2 but the compile line above selects “SO=3”. The output is an asm file in asz80 format.
  • if the O level is at least 1 (”-O1”) copt is run using sdcc rule set 1. This translates the asz80 syntax to (more or less) standard Zilog syntax, translates sdcc AREA designations into z80asm SECTIONs, and eliminates the INITIALIZER area created by sdcc. The output is an asm file (*.opt) in z80asm format.
  • if the O level is at least 2 (”-O2”) copt is run using sdcc rule set 2. This replaces all sdcc calls to its primitives with callee linkage. The output is an asm file. (-O2 is the default)
  • z80asm is run to generate an object file from the asm source.

At this point if any other source files are included on the compile line, they are taken to an object file using the same or similar steps.

  • z80asm is invoked as linker with the crt listed as first file (in asm form) and a list of the object files following. The target library ”-lzx” and any libraries indicated by the user are added to the compile line. One or more binaries are output, one per section with independent ORG.

The crt includes the “zcc_opts.def” file which contains defined constants that indicate various options. The crt tests these defines to insert any asm code necessary to implement the options. Being listed first among the linker's list of files guarantees it is processed first and this also allows the crt to create the memory map.

Note that sdcc is only used to translate C to assembler and its backend is not used to generate libraries, object files or executables. Instead sdcc's output is translated to standard Zilog syntax for consumption by z88dk's back end. This means inline assembler is assembled by z80asm and must use standard Zilog syntax. Inlined assembly can be enclosed in ”#asm” / ”#endasm” or "__asm" / "__endasm;" blocks.

Reducing Binary Size

  1. Ensure that the minimal crt required is selected. Targets normally supply more than one crt option that can be selected by number on the compile line with ”-startup=n”. These crts vary in options that can consume different amounts of memory. In particular, if your program does not use stdin, stdout or stderr, choose a crt that does not instantiate any devices at startup.
  2. Opt out of stdio if it's not needed. Use of printf and scanf implies that terminal i/o drivers are required that implement line editing, windows, terminal emulation and so on. This is a lot of extra code that is not always required for all projects. Most embedded applications provide their own i/o subroutines and communicate directly with devices. In these cases, a full-blown stdio implementation is wasted. By selecting a crt that does not instantiate terminal devices, programs will not have that extra code included. Keep in mind that they can still use functions from the sprintf and sscanf families to operate on buffers read from or written to devices. They can also use memstreams to do file i/o to memory buffers.
  3. Modify the crt to change font. If the program doesn't use the default font supplied by the crt, change it so that the default font is not stored as part of the binary.
  4. Configure the library and crt. Configure the library to choose a speed and space compromise suitable to your project. In particular, opt out of individual printf and scanf converters that your program does not use. Disable unused options in the crt that occupy memory space in the resulting binary. In particular, eliminate/resize the malloc heap and stdio heap if they are not needed.
  5. Use the library code. It's written in assembly language and takes advantage of callee and fastcall linkage. If you think your similar c or asm code is better suited to the task and will perform faster or will be smaller, test it.
  6. Assign data to appropriate sections. This applies to rom targets. Non-zero initial data must have a copy stored in rom so that the crt can initialize ram at startup. Consider two different declarations of a large array holding the text of a book. One is done with 'char book[] = ”…”;' and the other with 'char *book = ”…”;'. The array implies that the book data is modifiable so it is assigned to the DATA section and two copies will be present at runtime – the stored copy in rom and the active copy in ram. The second declaration stores the book text in a string constant. String constants are read-only so it will be assigned to CODE/RODATA and at runtime only one copy of the string will exist in rom, freeing up ram in comparison to the other declaration. Judicious use of the const qualifier can also affect whether data is stored in the DATA section or the CODE/RODATA section. Keep in mind that the stored DATA section can be compressed so if there is more ram than rom available, it may be preferable to store in the DATA section even though two copies would be present at runtime (the rom would contain a much smaller compressed copy).
  7. Use static variables when reasonable. The z80 is not very good at stack-relative addressing. Neither of the two main methods (using an index register set to sp and offsetting from that, computing offsets from sp using hl) leads to particularly compact or efficient z80 code. A large improvement in code size and speed can be had from changing local variables to statics. Keep in mind that doing this means functions will no longer be reentrant. For sdcc, unsigned char variables and frequently accessed small variables can be an exception to this advice.
  8. Avoid long lists of function parameters. Function parameters are located on the stack and, like the local variables mentioned in the last point, cannot be efficiently addressed by the z80. If long lists are unavoidable, chances are the function is also long. In these circumstances it can make sense to copy function parameters into local static variables before being used by the function.
  9. Use unsigned types when possible. Promotion of signed types to larger bit widths, which will quite often be done by the compiler, will usually involve insertion of sign extension code. Furthermore, the z80 instruction set is better suited to unsigned types.
  10. Use types of appropriate size. The z80 can do 8-bit and 16-bit arithmetic efficiently. 32-bit arithmetic involves many more cycles.
  11. Prefer unsigned char. (sdcc only) sdcc is particularly good at generating compact code when using char types.
  12. Demote larger types to smaller types as soon as possible. If the program performs operations on large types and then stores results into smaller types, try to demote the larger types to the smaller type and carry out the operations on that.
  13. Compute things once and store the result. It's not uncommon for modern code to repeatedly recompute expressions that evaluate to the same value. A common place where this is done is in the conditional of loops. Removing redundant calculations will not only speed up code, but it will give the compilers a better chance at generating better code.
  14. Try not to intermingle independent operations. Order code so that everything to do with variable A is done before using variable B. The z80 has a limited number of registers so having to access A and B in an intermingled manner may force the compiler to save intermediate results.
  15. Condense multiple simple statements into one statement. For example, “i–; if (i == 0) break;” may occasionally produce better code if condensed to “if (–i == 0) break;”
  16. Declare most frequently used local variables last. (sccz80 only) sccz80 will optimize access to local variables near the top of the stack using pushes and pops.

C Library Reference (Classic)

C Library Reference (New)

Introduction

The new C library aims to implement as large a subset of C11 as is reasonable on an 8-bit target. The library does not confine itself to the standard and adds many non-standard functions drawn from BSD and GNU, as well as libraries aiming to support text, graphics and sound among other things. The library is still under development but it is already extensive featuring more than 700 functions.

The library is unique in several ways:

  • It is written in assembly language. The C compilers are currently generating code that is 3-5x slower and larger than human written assembly language. By supplying an extensive library written in assembly language where most execution time is spent, compiled code can approach the performance of assembly language and can be several times smaller than it would be otherwise.
  • It is extensive. Most z80 C libraries only implement a minimal subset of the C standard with many noticeable omissions. Additionally, the non-standard libraries supplied by z88dk are things likely not seen before by many z80 programmers; it includes data compression, proportional fonts, music synthesis and some C++ STL containers.
  • It is designed to be able to generate standalone code. The library is not dependent on any existing operating system to implement any features. C code can be compiled, stored in ROM and run at boot-up without any fuss. Of course the library can also use an existing operating system if one is present.
  • It is highly configurable. Library build time options can select between fast+large and slow+small implementations of integer arithmetic, number↔ascii conversion, etc and can individually include or exclude printf and scanf converters to reduce final binary size. Pragmas in C source can communicate various crt options at compile time; among these are selecting heap size, RAM or ROM model, code origin, stack location.
  • It is designed with bankswitched memory in mind. Library code is assigned to sections at fine granularity which allows library code to be placed flexibly in any memory region. By default the crts generate CODE, DATA and BSS blocks but this can be changed to store code and data in any user-defined memory sections. Some library functions are extended to support bankswitched memory; an example is the malloc library which has been generalized to allocate memory out of multiple heaps, any of which could be located in different memory banks.
  • It is intended to be highly portable. In keeping with z88dk's goal of directly supporting any z80 target, the library can be ported to any target by providing a small number of customized functions. Because the library is more complicated under the hood, porting is not quite as easy as with z88dk's classic library and does take some expertise.
  • It is reentrant. All code is written to be reentrant with few exceptions (these would include functions like strtok() which are non-reentrant by definition and a few functions that are unavoidably self-modifying like the tritone music player).
  • It is easy to use. If your particular z80 machine is a supported target, you can begin compiling immediately. If it is not, you can use the generic “embedded” target to compile with.

Some information on the library:

  • All functions take advantage of callee and fastcall linkage. These linkages can reduce binary size by several hundred bytes in large programs.
  • The library is written to use one index register (IX). This makes it more compatible with some known z80 targets that reserve one index register for themselves. An assembler option (-Ca-IXIY) can switch IX for IY when the library is built.
  • The library takes advantage of the z80 architecture. The exx set is used to carry out parallel tasks within the library, to hold a 48-bit float and to speed up integer math, among other things. On occasion, IX and IY are split into their 8-bit halves. These instructions are undocumented but are reliable on all z80 variants. Because it does make it more difficult to port the library to some targets, use of the z80's extra registers has not been done haphazardly.
  • All functions are available from assembly language. The c library is written in assembly language and has both assembly and c entry points.
  • Stdio is object oriented. This allows sophisticated drivers to be implemented using code inheritance from the library. The base classes currently contain code to implement terminals and serial devices.

The library was written to be compatible with sdcc; some effort has been made to improve sdcc's output when used with z88dk:

  • sdcc uses the z88dk libraries. The z88dk libraries are more complete, faster and smaller.
  • As mentioned above, all functions take advantage of callee and fastcall linkage. They also include attributes that inform the compiler when registers are unchanged by functions. These are new features in sdcc that are not used by the sdcc-supplied libraries.
  • Assembly output from sdcc is post-processed so that sdcc's calls to its primitives also use callee linkage.
  • More than 600 peephole optimizer rules have been added to correct a couple of code generation bugs and to reduce code size further. To enable these rules add ”-SO3” to the compile line.

Notable omissions:

  • Disk i/o is not integrated yet. This also means opening and closing files is not complete and disk related C11 functions are not present.
  • Multi-byte characters will likely never be supported.
  • C11 threading is not integrated yet. The library config file contains options to enable locking on files – leave those disabled.

Library Configuration

Each target has a library configuration file that selects among various options when the target's library is built. The defaults are suitable for most projects but if you would like to experiment with generating smaller or faster code the options can be edited and the library re-built. Because the config file belongs to the target, only that target's library is affected.

All information concerning a particular port is found in z88dk/libsrc/_DEVELOPMENT/target. There are currently three targets implemented: cpm, embedded and zx (zx spectrum). We will use the “embedded” target for discussion purposes since it's a generic target suitable for any z80 machine.

Two library configuration files along with backups with default settings can be found in the embedded subdirectory:

The asm files contain the active configuration.

clib_cfg.asm

Contains general library settings.

CLIB_OPT_MULTITHREAD Leave disabled. The library does not support multi-threading yet.
CLIB_OPT_IMATH Choose between small and fast integer library.
CLIB_OPT_IMATH_FAST If the fast integer library is selected you can enable leading zero elimination and loop unrolling. Loops are unrolled a maximum of eight times. The LIA-1 option is not fully supported by the compilers at this time.
CLIB_OPT_IMATH_SELECT Choose between small and fast integer shifting.
CLIB_OPT_TXT2NUM Enable specialized functions for ascii → binary, octal, decimal and hex conversion.
CLIB_OPT_TXT2NUM_SELECT For enabled specialized functions choose between small and fast implementations.
CLIB_OPT_NUM2TXT Enable specialized functions for binary, octal, decimal and hex → ascii conversion.
CLIB_OPT_NUM2TXT_SELECT For enabled specialized functions choose between small and fast implementations.
CLIB_OPT_STDIO Set this to one if you want stdio to check the validity of a FILE* before using it.
CLIB_OPT_PRINTF Individually enable or disable printf converters; this can help to reduce program size1.
CLIB_OPT_SCANF Individually enable or disable scanf converters; this can help to reduce program size2.
CLIB_OPT_FASTCOPY Options to increase the speed of memcpy and memset using unrolled ldir; some other library code will also benefit.
CLIB_OPT_STRTOD Enable parsing of nan/inf strings and hex floats in form -0xh.hhhhp-dd
CLIB_OPT_SORT Selects the sorting algorithm used by qsort(). The default is shellsort3.
CLIB_OPT_SORT_QSORT Various quicksort algorithm options.
CLIB_OPT_ERROR Determines how detailed error strings are; reducing detail can help save a few bytes.

1 All printf float converters ”%aefg” are disabled by default. stdlib's dtoa() family of functions can also be used to convert floats to text.

2 scanf does not support ”%aefg” at this time. stdlib's strtod() and atof() can be used to convert text to floats.

3 Shellsort is currently non-reentrant.

clib_target_cfg.asm

Contains settings for target-specific portions of the library. Options will vary according to architecture but these two will always be present:

clock_freq The target's cpu clock rate in Hz1.
z80_cpu_info Indicate whether a CMOS or NMOS cpu is used2.

1 The cpu clock rate is used to generate precise delays and to generate tones in the sound library.

2 NMOS z80s have a bug that doesn't allow the current maskable interrupt state to be reliably determined with the “ld a,i” instruction. If an NMOS z80 is indicated the library will build with more robust code to determine that information. If the generated code should be correct for all z80s, NMOS should be chosen.

Rebuilding the Library

Once the library's configuration has been edited, the target's library must be rebuilt in order for changes to take effect.

Windows

  • Navigate to {z88dk}/libsrc/_DEVELOPMENT.
  • Run Winmake
    • Winmake” lists all targets
    • Winmake all” builds all target libraries
    • Winmake {target}” builds a specific target's libraries

To rebuild the embedded library, “Winmake embedded” should be run.

Non-Windows

  • Navigate to {z88dk}/libsrc/_DEVELOPMENT.
  • Invoke the Makefile with suitable target specified.
    • make all” builds all target libraries
    • make TARGET={target}” builds a specific target's libraries

To rebuild the embedded library, “make TARGET=embedded” should be run.

Crt

The crt is the startup code that runs before calling main(). It is responsible for setting the memory map, instantiating device drivers on stdin/stdout/stderr, initializing the bss and data sections and calling any initialization code prior to calling main(). On return from main() it is responsible for closing open files, resetting the stack and preparing to return to the host.

In combination with the crt, the memory map and crt configuration completely determine the program's execution environment at compile time. The memory map defines what goes where and the crt configuration sets defaults such as code origin and heap size.

The library supplies crts, memory maps and crt configurations for supported targets. Usually a non-trivial target will have multiple crt choices that differ in what devices are instantiated on stdin, stdout, stderr and possibly more than one memory map if the target supports ram-resident programs, rom-resident programs or bankswitched memory. Suggested combinations are condensed into a startup option selected on the compile line (”-startup=n”). A default startup is chosen if no startup is specified. In previous compile examples on this page, no startup was specified so a default was chosen by z88dk that would be considered most common for the target.

The startups and crts are target-specific of course so details should be gathered from the target's wiki entry. We will look at the embedded target in some detail so that the options available are tangibly explained.

embedded_crt.asm

The specific crt used in the compile is found from the target's _crt.asm file. For the embedded target this is target/embedded/embedded_crt.asm. The file is just a switch on the startup value specified on the compile line, eg “zcc +embedded -vn -startup=0 ….”. At the top of the file, if startup was not defined on the compile line, a default is selected for you (2 in this case). An important value is -1 which allows the user to supply his own crt file.

For each startup value, a memory model is selected by number (__MMAP=n), a crt configuation is chosen (__CRTDEF=n) and a real crt.asm file is included from the target's startup directory.

Let's choose startup=0. This selects the “ram model” for the embedded target; the reason for the name will become evident shortly.

This sets up the following:

  • __CRTDEF = 0 selects crt configuration number zero.
  • __MMAP = 0 selects memory map number zero.
  • startup/embedded_crt_0.asm is the start-up code.

The actual start-up code contains static data structure definitions to satisfy stdio which is both difficult to read and difficult to understand so the original macro file it was generated from is preferable to refer to: startup/embedded_crt_rom.m4.

Of particular interest are lines 32-43 which list statically instantiated devices. The order of instantiation determines the file descriptor each instantiated driver will be associated with. The embedded target is a general one so there are no drivers instantiated.

Lines 45 and up contains the first start-up assembly code. The embedded start-up code accommodates two cases. The first occurs when the code ORG is zero, in which case the crt sets up the z80 restarts in the bottom 100 bytes of memory. The second occurs when the code ORG is not zero, in which case the restart page is not set up. If you follow the startup code you will see how the crt performs initialization before it calls main() and what it does on return from main().

crt configuration

The crt configuration defines properties of the execution environment. The value of the __CRTDEF variable selects a configuration from a number of options in the target's “crt_target_defaults.inc”. For the embedded target this is target/embedded/crt_target_defaults.inc.

For __CRTDEF = 0 the following defaults are set:

CRT OPTION crt_target_defaults.inc meaning
CRT_ORG_CODE 0 ORG 0
CRT_ORG_DATA 0 data section appends to code section
CRT_ORG_BSS 0 bss section appends to data section
CRT_MODEL 0 ram model in effect
REGISTER_SP 0 crt will set SP=0
CRT_STACK_SIZE 512 max stack size is 512 bytes
CRT_INITIALIZE_BSS 0 crt will not initialize BSS
CRT_ENABLE_COMMANDLINE 0 no command line
CRT_ENABLE_RESTART 0 do not restart on program exit; infinite loop or return to host if code org != 0
CRT_ENABLE_CLOSE 1 close files on exit
CRT_ENABLE_RST 0 no user supplied rst vectors
CRT_ENABLE_NMI 0 no user supplied nmi
CLIB_EXIT_STACK_SIZE 2 two functions can be registered with atexit()
CLIB_QUICKEXIT_STACK_SIZE 0 no functions can be registered with at_quick_exit()
CLIB_MALLOC_HEAP_SIZE -1 heap is automatically created and will lie between the end of BSS and the stack
CLIB_STDIO_HEAP_SIZE 128 size of stdio heap used to allocate FILE* and driver structures
CLIB_BALLOC_TABLE_SIZE 0 no block allocator table
CLIB_FOPEN_MAX 0 max number of open FILE* = 0
CLIB_OPEN_MAX 0 max number of file descriptors = 0

As will be seen in the next section, the memory maps supplied by z88dk organize the output binary into CODE, DATA and BSS sections. CODE contains read-only z80 code and read-only data. DATA contains self-modifying code and initially non-zero variable data. BSS holds space for variables that are zero on startup.

The crt options are explained below:

  • CRT_ORG_CODE The address of the CODE section.
  • CRT_ORG_DATA The address of the DATA section. If 0 or -1, the DATA section immediately follows the CODE section.
  • CRT_ORG_BSS The address of the BSS section. If 0 or -1, the BSS section immediately follows the DATA section.

The final executable will consist of one binary for each section with its own ORG address. There will always be a CODE binary, a DATA binary will exist separately from CODE if its org is non-zero and a BSS binary will exist separately from DATA if its org is non-zero.

  • CRT_MODEL 0 = RAM model, 1 = ROM model, 2 = compressed ROM model.
    • RAM model: The crt will not initialize the data and bss sections on startup. Normally this is coupled with zero ORGs specified for the DATA and BSS sections. In this arrangement the output is a single binary containing CODE followed by the initialized variables in DATA followed by the zeroed variables in BSS. Because the crt does not initialize DATA and BSS, the executable can only be run once with correct initial state. This is the most common model used for systems running programs from ram and this is why it is called the RAM model. In systems that are sensitive to large binaries (maybe long loading times or limited storage medium), the BSS section can be cut off by specifying a BSS ORG address of -1. The special -1 value is recognized by z80asm to mean the section appends to the previous one but a separate binary should be produced. The output will then be the CODE+DATA binary and a BSS binary. The CODE+DATA binary can be used as the executable as long as RAM occupied by BSS is zeroed prior to execution. You can get the crt to do this zero-initialization by setting CRT_INITIALIZE_BSS to 1.
    • ROM model: The output binary is destined for rom. In this case an ORG address for DATA in RAM must be specified (if not an error will be emitted). If BSS's ORG is zero, the crt will modify this to -1 so that a separate BSS binary will be generated. The output will be three separate binaries: CODE, DATA and BSS. In order for the crt to initialize properly, it needs to have a stored copy of the DATA section in ROM, and it expects to find it immediately following the CODE section. So the final executable that is stored in ROM should consist of the CODE+DATA sections put together. From a windows command prompt this can be done with “copy /B name_CODE.bin+name_DATA.bin rom.bin”. This file “rom.bin” is the executable that should be stored on ROM. On startup the crt will copy the stored DATA section into ram, zero the BSS section and call main. Note that the output BSS binary is only useful to indicate how much ram it occupies. The total ram space required is the size of the DATA binary plus the BSS binary.
    • Compressed ROM model: Again the output binary is destined for rom. Everything mentioned for the rom model applies here except the stored data section will be compressed. Once the three binaries are generated (CODE, DATA, BSS), the DATA section should be compressed with zx7. “zx7 name_DATA.bin” will generate a compressed file “name_DATA.bin.zx7”. This file should be appended to the CODE binary and the result is what should be stored in ROM. The crt will decompress the stored DATA section into RAM and zero the BSS section before calling main. It's always preferable to use the compressed ROM model over the plain ROM model as the final rom image will be smaller. Note that the output BSS binary is only useful to indicate how much ram it occupies. The total ram space required is the size of the uncompressed DATA binary plus the BSS binary.
  • REGISTER_SP The crt will set the stack pointer to the indicated value before doing anything else. 0 indicates the top of ram and -1 is a special value that indicates the crt should not change the stack pointer. This makes sense on some hosts which will set the stack pointer prior to starting the C program.
  • CRT_STACK_SIZE The maximum stack size. This is only referenced if the heap is automatically created. See CLIB_MALLOC_HEAP_SIZE.
  • CRT_INITIALIZE_BSS If non-zero the crt will zero the BSS section before calling main. If one of the rom models is selected this always happens regardless.
  • CRT_ENABLE_COMMANDLINE If non-zero the crt will generate argc and argv. Targets that don't have a command line get values that indicate an empty command line.
  • CRT_ENABLE_RESTART If non-zero the crt will restart the program if it exits. This is especially helpful for a standalone program launched from ROM. Note that RAM model programs will not have their bss and data sections properly initialized.
  • CRT_ENABLE_RST Some crts are intended to generate code for address 0 and will fill in the z80 restarts. In these crts, code can be inserted at the z80 rst locations to jump to user supplied subroutines. The CRT_ENABLE_RST value is eight bits with one bit corresponding to each rst location. Rst 0 (bit 0) is ignored. Rst 0×08 corresponds to bit 1 and so on up to Rst 0×38 at bit 7. If any of these bits is set, a jump is inserted at the rst location to a user subroutine called “_z80_rst_xxh” where xx is the restart location in hex. The rst subroutine can be written in C or assembly. Note that rst 38h corresponds to the im1 interrupt routine. The same mechanism applies – you can implement an im1 service routine by supplying the subroutine “_z80_rst_38h” and setting bit 7 of CRT_ENABLE_RST. Don't forget that an interrupt service routine needs to preserve register values and terminate with “ei; reti”. Where CRT_ENABLE_RST has zero bits, the crt will insert “ret” except for the im1 entry point where “ei; reti” will be inserted.
  • CRT_ENABLE_NMI In crts intended to generate code for address 0, a non-zero value will cause the crt to insert “jp _z80_nmi” at address 0×66 to execute a user-supplied routine on nmi. Otherwise the crt inserts “retn”. The nmi service routine must preserve register values and terminate with “retn”.
  • CLIB_EXIT_STACK_SIZE The maximum number of functions that can be registered by atexit(). The C standard calls for 32 but most programs don't use any so this is usually reduced by the target config.
  • CLIB_QUICKEXIT_STACK_SIZE The maximum number of functions that can be registered by atquickexit(). The C standard calls for 32 but most programs don't use any so this is usually reduced by the target config.
  • CLIB_MALLOC_HEAP_SIZE The size of the heap from which memory is allocated using malloc(). If zero, no heap is present. If -1, the heap is automatically initialized to occupy the area between the end of the BSS section and the bottom of the stack. The maximum stack size is assumed to be CRT_STACK_SIZE bytes (above). (Note: if the max heap size computed is negative, which can happen if the stack is below the BSS section, the program will immediately exit without any indication). Any other positive value larger than 14 bytes will cause a heap of that size to be create in the BSS section.
  • CLIB_STDIO_HEAP_SIZE The size of stdio's heap. This heap is used to allocate driver structures when files are opened and FILE* when memstreams are opened. If set to 0, the stdio heap will only be large enough to accommodate stdin, stdout, stderr if they are present.
  • CLIB_BALLOC_TABLE_SIZE The queue table for the block memory allocator. If zero there is no table.
  • CLIB_FOPEN_MAX Max number of FILE* that can be simultaneously open. This includes stdin, stdout, stderr if they are present. If < =0, only FILE* for stdin, stdout, stderr will be created if demanded by crt options. If -1, FILE* lists won't be created unless stdin, stdout or stderr exist.
  • CLIB_OPEN_MAX Size of the fd table and indicates how many files can be simultaneously open. If 0, only space for stdin, stdout, stderr will be made if demanded by crt options.

As seen in the table above, the library chooses sensible defaults suitable for the target but your program can override these defaults using pragma embedded in your C source.

pragma overrides

Pragmas embedded in the C source can override the crt configuration. Pragmas can be located in any C source file in your project but it's best to keep them confined either to your main.c or to a dedicated file in projects that use makefiles and consist of many source files.

Overriding is done by name. An example will illustrate:

#include <stdio.h>
#include <stdlib.h>
 
#pragma output CRT_ORG_CODE          = 30000  // org 30000
#pragma output CLIB_MALLOC_HEAP_SIZE = 4096   // 4k heap needed
 
main()
{
   ...
}

The code origin is moved to address 30000 and the heap size is made 4096 bytes. As described in the last section, the heap will be created in the BSS section.

If you find that you are overriding many defaults you may want to edit the target defaults to something more suitable for your projects so you can do away with these pragmas.

memory map

The memory map is defined in the target's “memory_model.inc”. For the embedded target this is target/embedded/memory_model.inc which includes crt_memory_model.inc. If you recall, the selection of “startup=0” on the compile line as discussed above set the variable __MMAP = 0. This selects the one and only memory map defined in the memory model file. This memory map is almost universally used and would only need to be different for bankswitched targets. The model sets up the standard CODE/DATA/BSS sections.

user initialization and exit code

The crts create two sections that allow programs to place initialization and cleanup code into the crt. The intialization code is run just before main() is called and the exit code is run just after registered atexit() functions are called but before files are closed.

The section names are “code_crt_init” and “code_crt_exit”.

This example shows how the clib initializes the user heap.

; some leading underscores removed from labels
; so as not to disturb wiki formatting

SECTION bss_alloc_malloc
   
malloc_block:             defs clib_malloc_heap_size
   
SECTION code_crt_init
   
ld hl,malloc_block
ld bc,clib_malloc_heap_size
   
EXTERN asm_heap_init
call asm_heap_init

First the memory region reserved for the heap is placed into section bss_alloc_malloc (this will be part of the program's BSS section). Then the initialization code is placed into section code_crt_init.

User-Supplied Crt

You don't have to use the library-supplied crts nor do the crts have to be as sophisticated as the above. You may prefer to have something very simple or something with a different memory map. If the compile line contains ”-startup=-1” a local file “crt.asm” will be taken as the crt. The crt must set up the environment and call _main at minimum. If no memory map is set up, the output will be a single binary blob with CODE, DATA, BSS items mixed in the order the linker encounters them. Ideas can be taken from the library's crts, specifically have a look at the m4 macros in the target's startup subdirectory which are the crts before devices are instantiated (these files end in *.m4).

Assembly Language

The entire library is accessible from assembly language. The assembly entry points for functions are prefixed with “asm_” and the register interface for each function is documented in the source code rooted in {z88dk}/libsrc/_DEVELOPMENT. Here is a brief example that makes use of stdlib's dtoa() function to convert a double to ascii text:

; assumes math48 is the math library linked

SECTION code_user

EXTERN asm_dtoa

exx
ld bc,$490F      ; AC' = pi
ld de,$DAA2      ; math48 uses BCDEHL to hold a double
ld hl,$2182
exx

ld c,0           ; no flags
ld de,-1         ; max precision
ld hl,buffer     ; destination buffer

call asm_dtoa    ; write double in ddd.ddd format to buffer

...

SECTION bss_user

buffer:   defs 32

asm_dtoa is located in {z88dk}/libsrc/_DEVELOPMENT/stdlib/z80. The comments detail input parameters, output parameters and registers modified. The C documentation can also be consulted for more details.

Vararg functions such as printf expect their parameters to be pushed onto the stack. In these sorts of cases, the asm caller must use standard C linkage to call the function. Here is a brief example that uses printf:

; assumes sccz80 is the compiler
; L->R parameter order, varargs require A to be loaded with num words pushed onto stack

SECTION code_user

EXTERN asm_printf

ld hl,fmt       ; format string
push hl
ld hl,100       ; 100 dollars (16-bit integer)
push hl
ld a,2          ; sccz80 only, number of words pushed
call asm_printf
pop af
pop af          ; clear stack

...

SECTION rodata_user

fmt:  defm "You win %d dollars.\n"
      defb 0

The equivalent C is:

printf("You win %d dollars.\n", 100);

If this is a project compiled with sccz80 or if this is an assembly language project linked against the sccz80 library, then printf is expecting its parameters to be pushed in left-to-right order and the 'A' register must be loaded with the number of 16-bit words pushed.

On the other hand if the project is compiled with sdcc or if this is an assembly language project linked against the sdcc library, then printf is expecting its parameters to be pushed in right-to-left order and nothing needs to be loaded into 'A'.

More details can be found in the Mixing C and Assembly topic. Most library functions are not vararg and asm parameters will be passed via register rather than stack.

You must also be aware that the new c library employs sections. Sections are destination containers that hold code and/or data and can have an ORG address associated with them. All assembly language written should be assigned to a section so that the linker can know where to place it in memory.

The crts previously discussed create three basic sections: CODE, DATA and BSS. These large sections hold many small ones, including some sections designated for user code:

  • code_user assign executable code to this section
  • rodata_user assign read-only data to this section
  • smc_user assign self-modifying code to this section
  • data_user assign non-zero initial data to this section
  • bss_user assign zero initial data to this section

By assigning your assembly code to the correct sections, the linker will be able to create ROMable software with your code.

You can also create your own sections. There's no magic incantation, simply start using it and assign a name as in:

SECTION my_section

start:

    ld hl,2
    ....
    ret

Since this section is not in the crts' memory map, all data or code assigned to it will be output as a separate binary when the project is assembled. The name of the binary will be “outputname_my_section.bin”. If no ORG is assigned to the section anywhere in your project, it is assigned an ORG of 0. (Note that un-ORGed sections may be appended to previously defined sections in the same source file; this is how memory maps are built with z80asm).

These details are best described in the Mixing C and Assembly topic.

Header Listing

Function Listing

Library In Depth

Creating a Target

There are two C libraries in z88dk that exist independently of each other. This topic is concerned with creating a new target for the new C library only.

To make changes to an existing target, you can simply add a new CRT. Adding a new CRT allows you to change the startup code, change the memory map, change the drivers instantiated for stdio, and change the default CRT options (these are things like code origin, heap size and so on).

If your target is completely new you can follow the steps below for creating a new target. In addition to new CRTs, a new target defines what is included in the C library, can define architecture dependent functions and header files, and can define its own set of device drivers.

Adding CRTs to an Existing Target

This forum posting contains details for adding a CRT to the simple embedded target and can serve as instructions until the topic is properly filled out.

Creating a New Target

1. ZCC Configuration File

As with all grand projects, the first step in creating a target is settling on a name. The name will appear in the compile line “zcc +yourname …. test.c -o test” so keeping it short will save a little typing but it must be long enough that it is descriptive of your target and that it won't collide with names for already existing targets in z88dk.

The list of current targets can be found in z88dk/lib/config where the name of each cfg file corresponds to a target name. For purposes of this discussion we will create a target called “temp”.

To make zcc aware of the target a new file “temp.cfg” must be added to this directory:

z88dk/lib/config/temp.cfg

#
# Target configuration file for z88dk
#

# Asm file which contains the startup code (without suffix)
CRT0		 DESTDIR/lib/temp_crt0

# Any default options you want - these are options to zcc which are fed
# through to compiler, assembler etc as necessary
OPTIONS		 -v -O2 -SO2 -I. -DZ80 -DTEMP -D__TEMP__ -D__TEMP -M -clib=default

CLIB     default -ltemp_clib -lndos
CLIB     new -D__SCCZ80 -Ca-D__SCCZ80 -Cl-D__SCCZ80 -nostdlib -IDESTDIR/include/_DEVELOPMENT/sccz80 -Ca-IDESTDIR/libsrc/_DEVELOPMENT/target/temp -ltemp -LDESTDIR/libsrc/_DEVELOPMENT/lib/sccz80 -Cl-IDESTDIR/libsrc/_DEVELOPMENT/target/temp -crt0=DESTDIR/libsrc/_DEVELOPMENT/target/temp/temp_crt
CLIB     sdcc_ix -compiler=sdcc -D__SDCC -D__SDCC_IX -Ca-D__SDCC -Ca-D__SDCC_IX -Cl-D__SDCC -Cl-D__SDCC_IX -nostdlib -IDESTDIR/include/_DEVELOPMENT/sdcc -Ca-IDESTDIR/libsrc/_DEVELOPMENT/target/temp -ltemp -LDESTDIR/libsrc/_DEVELOPMENT/lib/sdcc_ix -Cl-IDESTDIR/libsrc/_DEVELOPMENT/target/temp -crt0=DESTDIR/libsrc/_DEVELOPMENT/target/temp/temp_crt
CLIB     sdcc_iy -compiler=sdcc --reserve-regs-iy -D__SDCC -D__SDCC_IY -Ca-D__SDCC -Ca-D__SDCC_IY -Cl-D__SDCC -Cl-D__SDCC_IY -nostdlib -IDESTDIR/include/_DEVELOPMENT/sdcc -Ca-IDESTDIR/libsrc/_DEVELOPMENT/target/temp -ltemp -LDESTDIR/libsrc/_DEVELOPMENT/lib/sdcc_iy -Cl-IDESTDIR/libsrc/_DEVELOPMENT/target/temp -crt0=DESTDIR/libsrc/_DEVELOPMENT/target/temp/temp_crt
  • CRT0 holds the name of the crt file used by the classic C library. This is ignored when the new C library is used.
  • OPTIONS lists the default compile line options. These can be overridden and augmented by the following CLIBs.
  • CLIB lists options that are added to the compile when ”-clib=???” appears on the compile line. For compiles using the new C library, ”-clib=new”, ”-clib=sdcc_ix” or ”-clib=sdcc_iy” are used. The first one sets up a compile using sccz80 while the latter two set up compiles using sdcc with the distinction being which index register the C library uses.

Together, OPTIONS and CLIB define a few constants which can be tested in user C or asm code, determine the default optimization levels, select the CRT to use, and set up the library and include paths.

In the file above, the target name used is “temp” so for your own target, you'll want to replace all instances of “temp” and “TEMP” with your target's name.

2. Create the Target Directory Structure and Add Initial Contents

All information concerning a target for the new C library is located in z88dk/libsrc/_DEVELOPMENT/target. Unfortunately the CVS view of the repository on the internet tends to show dead directories mixed with current ones and this is something that will have to be coped with. The list of currently supported targets are cpm, embedded and zx and you can see that each of these have their own directory here. m is a pseudo-target used to compile the floating point library.

Create a new directory for your target using its config name. In this example, that's “z88dk/libsrc/_DEVELOPMENT/target/temp”.

Inside the new temp directory, unzip this package of files and directories to initially populate the target. Spend a few minutes renaming files so that “temp” is replaced by your target name. Then open files in the temp (your name) directory and temp/startup startup directory and replace instances of “temp” or “TEMP” with your target's name.

Here is a brief description of each file and subdirectory:

d driver contains the target's device drivers
d library contains list files specifying the contents of the target's C library
f library/temp_sccz80.lst lists contents of the target's C library when sccz80 is used as compiler
f library/temp_sdcc_ix.lst lists contents of the target's C library when sdcc is used as compiler and the library uses ix
f library/temp_sdcc_iy.lst lists contents of the target's C library when sdcc is used as compiler and the library uses iy
d startup contains the target's CRTs
f startup/temp_crt_0.asm CRT #0 generated from temp_crt_0.m4
f startup/temp_crt_0.m4 Human readable and editable CRT macro that is expanded by m4
f clib_cfg.asm selects library build-time options for the target's C library
f clib_target_cfg.asm selects library build-time options for the target-specific portion of the C library
f clib_target_constants.inc target-specific global constants defined at program compile time
f clib_target_variables.inc target-specific (memory occupying) variables defined at program compile time
f clib_target_defaults.inc crt options defined at program compile time
f memory_model.inc memory map defined at program compile time
f temp_crt.asm defines which crt to use at program compile time
f temp_crt.opt must include temp_crt.asm
3. Determine the Contents of the Target's C Library

When the target's C library is built, the list of files assembled into the library are read from the target's library subdirectory. In the present example that is z88dk/libsrc/_DEVELOPMENT/target/temp/library. Three libraries are built corresponding to the three list files found here: sccz80, sdcc_ix and sdcc_iy. The sccz80 library is used when sccz80 is chosen as compiler. The sdcc_ix and sdcc_iy libraries are chosen when sdcc is the compiler and are selected between by either ”-clib=sdcc_ix” or ”-clib=sdcc_iy” on the compile line. The difference between the two is which index register the C library uses. “sdcc_ix” corresponds to the library using ix and “sdcc_iy” corresponds to the library using iy. It's always preferable to use the “sdcc_iy” version of the library because this gives sdcc sole use of ix for its frame pointer while the library uses iy. If “sdcc_ix” is selected, sdcc and the library must share ix which means the library must insert extra code to preserve the ix register when it is used. This means the “sdcc_iy” compile will be smaller. The choice is present because some targets reserve one of the index registers for themselves.

The new C library's source code is located just above the target directories in z88dk/libsrc/_DEVELOPMENT. Just to rule out the few dead directories, the currently valid source code directories are listed below.

adt Abstract data types: arrays, vectors, stacks, queues, lists
alloc Memory allocators: balloc (block allocator), heap (malloc), obstack
arch Architecture-specific code
compress Data compression
ctype Character classification
drivers Base classes for writing device drivers
error Exit subroutines used by the C library to set errno
fcntl I/O using file descriptors
font Fixed width bitmap font definitions and fzx proportional fonts
input Direct hardware access to keyboard, joystick, mouse
inttypes Known names for some functions operating on largest integer type as defined by C standard
l Library subroutines and compiler primitives
locale A few functions defining locale-specific character comparison
math Integer and float math functions
network Network related
setjmp Long jumps for C
sound Music and sound effects for 1-bit devices
stdio Standard C I/O using FILE*
stdlib Standard C library includes number ↔ text conversion, random numbers, sorting, etc
string Standard C string and raw memory manipulation
temp/sp1 Software sprites for bitmapped displays
threads (Incomplete) Mutex, critical section, spinlock, call_once
z80 Z80 related includes precise delay, port I/O, IM2 ISRs, interrupt state

Each source directory is structured to contain list files listing all source files, a z80 directory containing the asm source code, and a c directory containing preamble code for the C interface that gathers parameters from the stack and jumps into the asm implementation.

While a large portion of the library will use an 8080 subset of the instruction set, the library is written to be as compact and fast as reasonable and does use all z80 features, including the EXX set when appropriate. This does cause problems for some targets and the solution that will be supplied in z88dk is to offer alternate implementations for some functions in a subdirectory other than z80. But those alternate implementations are not present at this time so if your target cannot allow use of the EXX set, you will have to either supply alternate implementations for affected functions or you will have to exclude some of the functions from your target's library. If you have such a difficult target it's probably best to inquire about porting in the z88dk forums.

Next it's time to list what is in your target's C library. Three files have already been provided in z88dk/libsrc/_DEVELOPMENT/target/temp/library that include all of the C library minus target-specific code. For example, the file “temp_sccz80.lst” contains this:

library/temp_sccz80.lst

@adt/adt_sccz80.lst
@alloc/alloc_sccz80.lst
@compress/compress_sccz80.lst
@ctype/ctype_sccz80.lst
@drivers/drivers.lst
@error/error.lst
@fcntl/fcntl_sccz80.lst
@font/font_4x8/fonts.lst
@font/font_8x8/fonts.lst
@font/fzx/fzx_sccz80.lst
@inttypes/inttypes_sccz80.lst
@l/l.lst
@l/sccz80.lst
@locale/locale.lst
@math/math_integer.lst
@math/math_float_sccz80.lst
@network/network_sccz80.lst
@setjmp/setjmp_sccz80.lst
@stdio/stdio_sccz80.lst
@stdlib/stdlib_sccz80.lst
@string/string_sccz80.lst
@threads/threads_sccz80.lst
@z80/z80_sccz80.lst

The paths are relative to the source code base directory z88dk/libsrc/_DEVELOPMENT. The ”@” symbol means the indicated file is a list file rather than an asm file. You can modify these files to exclude portions of the library as you like or to be more choosy about which functions make it into the library by replacing list files with a more specific list of functions. But unless your target is restricted by being unable to use certain registers or you want to purposely exclude some functionality, there is no reason to restrict what goes into the target's C library. Targets without bitmapped displays may want to exclude the font related things and I suppose this will reduce library size and library build time.

There are some functions that will not work without further defines or code supplied for the target. These include functions in font/fzx (a single character putchar must be written to complete support for proportional fonts), input (the C library defines standard functions for direct access to keyboards / joysticks / mice hardware but this must be written for each target), sound (targets must define how the state of a 1-bit speaker is toggled) and temp/sp1 (bitmap software sprite engine requires much customization to suit display resolution). If you would like to add these features you can ask how in the z88dk forums.

4. Set the Default Library Configuration

The C library allows the user to customize it for speed and size. These customizations are specified in the target's clib_cfg.asm file located in z88dk/libsrc/_DEVELOPMENT/target/temp. The selections already supplied are typical but if you would like to change them you can. In particular, printf has the float converters %aefg disabled so that compiles don't involve the float library. When they are enabled, any use of printf drags in the float library and the compile line must include ”-lm”. scanf does not yet support %aefg but stdlib does include atof() and strtod() which can be used for ascii → float conversion. I'd suggest using %[ to scan a float string and then convert that using one of these functions.

Keep the clib_cfg.bak file consistent as it serves as backup if the user makes his own customizations later.

5. Set the Target Library Configuration

The target library configuration serves the same function as the regular library configuration but it applies to target-specific code only. The target-specific customizations are found in clib_target_cfg.asm. The file provided defines two things that all targets should define: the z80 clock rate in Hz and some bit flags that define whether the z80 is nmos or cmos. The latter is important because the nmos z80 has a bug that makes the determination of interrupt state (enabled or disabled) more difficult. The library can be compiled to use the simpler cmos code or the more complicated nmos code as needed. If your code must run on all manner of z80s, choose nmos.

If you will be writing target-specific library code, this is the place to put any required configuration information that will be available when the library code is built. As an example, you can have a look at the zx target's clib_target_cfg.asm file. It has the same architecture information at the top but it also has more defines that customize the sp1 sprite library and define how 1-bit sound is generated.

Keep the clib_target_cfg.bak file consistent as it serves as backup if the user makes his own customizations later.

6. Build the Z80 Library

The z80 libraries are built from z88dk/libsrc/_DEVELOPMENT using either Winmake (windows) or the Makefile (non-windows).

First add the new “temp” target to Winmake by changing this line:

z88dk/libsrc/_DEVELOPMENT/Winmake.bat

set alltargets= embedded cpm m temp zx 

This list of targets must have a space before the first target name and a space after the last target name. Because this is a .bat file, Windows makes it difficult to edit it. To edit, right click on “Winmake.bat” and choose “Send To” from the context menu. Then select a text editor from the options.

Next add the new “temp” target to the Makefile by changing this line:

z88dk/libsrc/_DEVELOPMENT/Makefile

TARGET ?= embedded zx m cpm temp

Let's build the target library. From z88dk/libsrc/_DEVELOPMENT, run “Winmake temp” (windows) or “make TARGET=temp” (non-windows).

Three libraries should be built without error and they should appear as temp.lib in z88dk/libsrc/_DEVELOPMENT/lib/sccz80, lib/sdcc_ix and lib/sdcc_iy. It may take 5-10 minutes to finish.

7. Create the CRT(s)

CRT stands for C Runtime. It is a short bit of code that sets up the execution environment before main() is called. It will take care of things like initializing the heap, parsing the command line, setting the stack pointer's initial value and, on program exit, running the exit stack and preparing to return to the host.

In z88dk, two other entities help define the environment.

  • The CRT Options are a collection of defines that let the user program customize its environment at compile time. The C library defines defaults which can be optionally overridden by the target and they in turn can be optionally overridden by pragmas embedded in a program's C source. Some of these CRT options are implemented by the C library but some are the responsibility of the CRT.
  • The memory map determines where the linker places code and data in memory. z88dk defines a default memory map that consists of CODE, DATA and BSS sections. For most targets this default memory map will be adequate.

Combinations of CRT, CRT Options and memory map are summarized by a single numerical startup value. The user will specify a startup in the compile line:

zcc +embedded -vn -startup=0 -SO3 -clib=sdcc_iy --max-allocs-per-node200000 test.c -o test

If a startup is not specified, you will be providing a default value. This startup value will be used in a switch to enable the relevant CRT, CRT Options and memory map.

Implied here is that targets can have any number of CRTs defined for them. In this instruction just one CRT will be created but you can create as many as are needed to suit different types of output.

CRTs are actually defined as m4 macros. As will be seen in the device driver section, this makes it easy to create stdin, stdout, and stderr. So the m4 macro is what is edited by people but the expanded macro – an associated .asm file – is what is used in the compile as the active crt. This means all CRTs come in pairs: an .m4 created by a person and an .asm which is the m4 expansion of that file. These pairs are named and numbered as in “temp_crt_0.m4” and “temp_crt_0.asm”, here corresponding to CRT #0 (recall “temp” is the name of the target). Some of the targets supplied by z88dk actually have one m4 file associated with several .asm files but we've since figured out this is a maintenance problem so having a 1:1 relationship between the two is highly recommended.

To build CRTs m4 must be installed. m4 is the common macro processor found in Unix and Linux so only Windows users are likely to have to install it separately.

CRTs are defined in the target's startup directory, in this case z88dk/libsrc/_DEVELOPMENT/target/temp/startup. A simple CRT was included in the bundle of files used to initially populate the target. You can edit this file as it is discussed below to create your target's first CRT.




z88dk/libsrc/_DEVELOPMENT/target/temp/startup/temp_crt_0.m4

dnl############################################################
dnl##           TEMP_CRT_0.M4 - EXAMPLE TARGET               ##
dnl############################################################
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;                  temp standalone target                   ;;
;;      generated by target/temp/startup/temp_crt_0.m4       ;;
;;                                                           ;;
;;                  flat 64k address space                   ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CRT AND CLIB CONFIGURATION ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

include "../crt_defaults.inc"
include "crt_target_defaults.inc"
include "../crt_rules.inc"

This is where the final state of the CRT options is determined. The CRT options are a list of defined constants that indicate options to the C library and the CRT. It is the responsibility of the CRT to implement some of these options, as appropriate, for the target. All options were briefly described earlier under crt configuration and it will be mentioned here where CRT options will apply.

First the default settings of all CRT options are included. Then the settings overridden by the target are included. Finally, rules are executed to determine the final state of those options. The rules are simple: they give highest priority to pragmas in the C source, next priority to the target overrides and lowest priority to the default settings.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SET UP MEMORY MODEL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

include "memory_model.inc"

The memory map is defined here by communicating to the linker where the various sections are assigned in memory.

Applicable CRT options consumed by the memory model:

__crt_org_code sets the address of the CODE section
__crt_org_data sets the address of the DATA section
__crt_org_bss sets the address of the BSS section
__crt_model chooses between ram model, rom model or compressed rom model

If you make your own memory maps it will be up to you to apply these options there.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GLOBAL SYMBOLS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

include "../clib_constants.inc"
include "clib_target_constants.inc"

Defines various global constants required by the C library.

  • “clib_constants” correspond to general library defines like error numbers and ioctls.
  • “clib_target_constants” correspond to target-specific defines like device error numbers.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INSTANTIATE DRIVERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; No device drivers available yet.
; sprintf/sscanf + memstreams can still be used

include(../../clib_instantiate_begin.m4)
include(../../clib_instantiate_end.m4)

Device drivers are assigned to FILEs and file descriptors between the “begin” and “end” macros. This target does not have any device drivers yet so nothing is instantiated here.

The first actual code begins here:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; STARTUP ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

SECTION CODE

PUBLIC __Start, __Exit

EXTERN _main

__Start:

   nop

The “nop” is in place to control the name of the output file. The final output is one or more binaries corresponding to non-empty sections with independent ORG addresses. The binary filenames are created from the output filename with the section name concatenated. For example, if the output filename is “test” and the first non-empty section is “CODE” then one of the binaries will be named “test_CODE.bin”.

In the CRT written here, it is possible that no code will be generated until after the “code_crt_main” section below. This means the output filename may be “test_code_crt_main.bin” when it's expected the filename will be “test_CODE.bin”. The “nop” is guaranteeing that the CODE section will not be empty. If you add code to this section you can eliminate the “nop”.

   ; set stack address
   ; (optional)

Applicable CRT options:

__register_sp if -1 the user is asking not to change SP else the user is supplying an initial stack pointer value.

Some hosts will set the stack pointer before starting a machine code program so it can make sense not to alter the stack pointer value.

See lines 110-116 of this CRT for an example.

   ; parse command line
   ; (optional)

Applicable CRT options:

__crt_enable_commandline if non-zero the user is requesting the command line to be parsed and argc + argv to be generated.

If the target does not have a means to communicate a command line, a course of action you can take is to generate an empty command line. See lines 118-142 of this CRT for an example.

The library supplies two functions to help parse command lines:

  • l_command_line_parse This function is intended for parsing command lines from a region in memory that is likely to be overwritten during program execution. An example is the cp/m target which leaves the command line at address 0×80 which also happens to be the location of the primary FCB. The first time a file is read or written from disk, the command line will be overwritten. What this function does is copy the words to the stack as they are parsed so that the command line will be available throughout program execution. See lines 110-164 of this CRT for an example.
  • l_command_line_parse_in_place This function is intended for parsing command lines from a region in memory that will not be overwritten during program execution. The command line will be parsed into words in place, with terminating NUL bytes written into the command line as needed. See lines 162-194 of this CRT for an example. At the time of writing this CRT is only partially implemented.

Both these functions stop parsing when a file re-director (”<” or ”>”) is encountered. In the future this part of the command line will be interpretted to redirect i/o to files. Neither function currently groups quoted strings into a single argument. This will also change in a future update.

   ; initialize data section

   include "../clib_init_data.inc"

   ; initialize bss section

   include "../clib_init_bss.inc"

The C library will zero the BSS section and initialize the DATA section here. If the ram model is active, neither initialization will take place.

SECTION code_crt_init          ; user and library initialization

This is a point where the library or user can insert initialization code. For example, if a heap is created the library will insert heap initialization code here.

SECTION code_crt_main

   ; call user program
   
   call _main                  ; hl = return status

   ; run registered exit() functions

   IF __clib_exit_stack_size > 0
   
      EXTERN asm_exit
      jp asm_exit              ; exit function jumps to __Exit
   
   ENDIF

main() is called and if exit() functions can be registered, they are executed.

__Exit:

   ; abort(), exit(), quick_exit() arrive here and can be called from anywhere in the program
   ; this means the stack may be unbalanced

   ; hl = return status

   push hl

'hl' holds the return value. You can choose to save it or throw it away. This code saves it.

SECTION code_crt_exit          ; user and library cleanup

This is a point where the library or use can insert clean-up code.

SECTION code_crt_return

   ; close files
   
   include "../clib_close.inc"

   pop hl                      ; hl = return status

The C library closes open files and then the return value is recovered.

   ; exit program

   jr ASMPC                    ; infinite loop (ASMPC means current address)

At this point you must decided what to do about a program exit. In this example, an infinite loop is entered.

Applicable CRT options:

__crt_enable_restart if non-zero the user is indicating the program should restart on exit.

If the program exits to the host, the original stack pointer must be recovered so that the return address is available. At ”__Start” the host's original stack pointer can be saved and then at program exit, this value can be recovered and then “ret” executed to return. A good place to save the stack pointer value is in section BSS_UNINITIALIZED which is described below.

If the program is to be restarted, keep in mind that the ram model does not initialize the BSS or DATA sections. This means ram model programs can only execute once with correct initial state. The CRT will always initialize BSS and DATA for rom model programs so there is no issue with restarting those. This is discussed in more detail here. Other issues to keep in mind include what to do about command line parsing on second runs (the command line can be modified by the program; one suggestion is to generate an empty command line for second runs) and resetting the stack (the stack can be unbalanced at program exit so the stack pointer should be reset to an appropriate address before restart). You may need to add a ”__Restart” label to define a point to restart the program and you may have to restructure the example CRT somewhat.

On program exit, consider what should happen to interrupts.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; RUNTIME VARS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

SECTION BSS_UNINITIALIZED

; place any uninitialized data here (eg saved stack pointer)
; bss and data section initialization will not touch it

Data defined in section “BSS_UNINITIALIZED” will not be zeroed by the CRT's BSS initialization code and it will persist across program restarts.

include "../clib_variables.inc"
include "clib_target_variables.inc"

Definitions of global library data that occupies memory space.

  • “clib_variables.inc” adds general global variables including the heap, exit stacks and thread id.
  • “clib_target_variables.inc” adds global variables for target-specific library code.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIB STUBS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

include "../clib_stubs.inc"

Labels defined for incomplete portions of the library that need to be present for successful compiles.

NOTE FOR CRTS INTENDED FOR ADDRESS 0x0000

The example CRT was written assuming an ORG address that is not zero. If the ORG address will be zero, the CRT must fill in the z80 restarts and the nmi entry point at 0×66.

Applicable CRT options:

__crt_enable_rst the lower eight bits indicate which rst locations are to be implemented by user code.
__crt_enable_nmi non-zero indicates the nmi will be implemented by user code.

Further discussion of these CRT options can be found here.

The embedded target has a CRT which will generate the z80 restarts if the ORG address is 0. The CRT is written to apply to both 0 ORG and non-zero ORG so there is conditional code present for the 0 ORG case. You can see how the restarts were implemented by examining that code.




Congratulations on completing your first CRT. Now it's time to generate the .asm file that will be used in compiles:

From z88dk/libsrc/_DEVELOPMENT/target/temp/startup:

m4 temp_crt_0.m4 > temp_crt_0.asm

8. Define the Default CRT Options

The CRT options overridden by the target during compilation are defined in z88dk/libsrc/_DEVELOPMENT/target/temp/crt_target_defaults.inc. Although only CRT options that the target actually overrides need to be listed here, in reality all the CRT options are listed here in order to document them for each compile.

As for CRTs, there can be any number of different CRT options settings in this file and they are distinguished by the constant ”__CRTDEF”. This file is a simple switch based on the value of __CRTDEF.

The example creates a single CRT option selection for ”__CRTDEF = 0” that will be used with the CRT created in the last section. You can make changes to these options as required for your target. The options are more completely described in the crt configuration topic.

IF __CRTDEF = 0

   ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   ;; temp ram model ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

   defc TAR__crt_org_code              = 32768
   defc TAR__crt_org_data              = 0
   defc TAR__crt_org_bss               = 0

   defc TAR__crt_model                 = 0
   
   defc TAR__register_sp               = 0
   defc TAR__crt_stack_size            = 512
      
   defc TAR__crt_initialize_bss        = 0
   
   defc TAR__crt_enable_commandline    = 0
   defc TAR__crt_enable_restart        = 0
   defc TAR__crt_enable_close          = 1
   
   defc TAR__crt_enable_rst            = 0
   defc TAR__crt_enable_nmi            = 0
   
   ; clib defaults
   
   defc TAR__clib_exit_stack_size      = 2
   defc TAR__clib_quickexit_stack_size = 0
   
   defc TAR__clib_malloc_heap_size     = -1
   defc TAR__clib_stdio_heap_size      = 128
   
   defc TAR__clib_balloc_table_size    = 0
   
   defc TAR__clib_fopen_max            = 0
   defc TAR__clib_open_max             = 0

ENDIF

For a minimum binary size you will also want to set TAR__clib_exit_stack_size = 0 (no functions can be registered with atexit()), TAR__clib_malloc_heap_size = 0 (the heap will not be created) and TAR__clib_stdio_heap_size = 0 (no files can be opened; for a target without drivers this affects whether memstreams can be created).

The 0 values for DATA and BSS origin mean they will append to the CODE section. The ram model is in effect (TAR__crt_model = 0) so these settings mean the output will be a single binary “name_CODE.bin” containing the CODE,DATA,BSS sections concatenated together and the DATA,BSS sections will not be initialized by the CRT (they will hold their initial values in the binary image). If your output is destined for ROM, you will have to change to the compressed ROM model and set the DATA section's ORG to the first RAM address. The CRT will have to ensure that the stack pointer points into RAM as well.

If TAR__clib_malloc_heap_size = -1 then the heap will be automatically located and sized such that it occupies the space between the end of the BSS section and the bottom of the stack. The value of TAR__crt_stack_size is used to indicate the maximum size of the stack when the heap size is calculated at runtime. Should a negative size be computed for the heap (size = SP - stack_size - BSS_END - 14), the program will simply exit just after starting up without any indication. The other way to automatically create a heap is to specify a size > 14 bytes. Such a heap will be allocated in the BSS section and it will be automatically initialized by the CRT. The heap can also be dynamically created at runtime or statically created at a fixed address as was done in this example C program.

These options can, of course, be overridden by pragmas inserted by the user into C source.

Keep the crt_target_defaults.bak file consistent as it serves as backup if the user makes his own customizations later.

9. Define the Default Memory Map

The memory map instructs the linker where to place code and data in memory. z80asm defines containers called “sections” to which code and data can be added from any source file in the project. In the linking stage these sections are sequenced according to the memory map and then one or more binaries are produced as output, depending on whether the memory map creates disjoint regions in memory. There is more discussion of this in the mixing C and assembly language topic.

The C library defines sections for each module in the library. For example strings, stdlib and stdio all have their own sections. This was done to assist users in assigning library code to different regions in memory at a fine grain level. But perhaps a more interesting application is that by including ”-Cl–split-bin” on the compile line, every section is output in its own binary file. This allows users to find out at-a-glance what is occupying the memory in output binaries.

Small talk aside, as with CRTs and CRT options, many memory maps can be defined and these are identified by the label ”__MMAP”. A default memory map is defined by the C library and this memory map is what is used by this example target:

z88dk/libsrc/_DEVELOPMENT/target/temp/memory_model.inc

IF __MMAP = 0

   ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   ;; standard CODE/DATA/BSS memory map ;;;;;;;;;;;;;;;;;;;;;;;
   ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

   INCLUDE "../crt_memory_model.inc"

   ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   
ENDIF

The include actually defining the memory map is found one directory above: z88dk/libsrc/_DEVELOPMENT/target/crt_memory_model.inc

Three major sections are defined (CODE, DATA, BSS) with optional ORG addresses and many smaller sections are sequenced such that they merge into these bigger ones.

This memory map is suitable for most targets and will work for programs destined for RAM or ROM. You may need a different one if you do something non-trivial with bankswitched memory or if you have a target that defines non-traditional memory regions.

It's unlikely you will have to create your own memory map but you can by adding another __MMAP case.

Keep the crt_memory_model.bak file consistent as it serves as backup if the user makes his own customizations later.

10. Startup

At compile time, the runtime environment is initialized and defined by the CRT, the CRT options and the memory map. These three parameters are summarized by a single startup value on the compile line.

zcc +embedded -vn -startup=0 -SO3 -clib=sdcc_iy --max-allocs-per-node200000 test.c -o test

In this example the user has selected startup #0, the meaning of which is defined in the master crt file.

z88dk/libsrc/_DEVELOPMENT/target/temp/temp_crt.asm

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; SELECT CRT0 FROM -STARTUP=N COMMANDLINE OPTION ;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

INCLUDE "zcc_opt.def"

IFNDEF startup

   ; startup undefined so select a default
   
   defc startup = 0

ENDIF


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; user supplied crt ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

IF startup = -1

   INCLUDE "crt.asm"

ENDIF

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ram model ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

IF startup = 0

   ; generic temp startup

   IFNDEF __CRTDEF
   
      defc __CRTDEF = 0
   
   ENDIF
   
   IFNDEF __MMAP
   
      defc __MMAP = 0
   
   ENDIF

   INCLUDE "startup/temp_crt_0.asm"

ENDIF

The include of “zcc_opt.def” pulls in any pragmas defined in the C source. A default startup value is defined if the user didn't specify one and then, for startup=0, which CRT, CRT options and memory map to use are defined. Modify this as you like or add more startup cases as needed.

The file temp_crt.opt in the same directory must include temp_crt.asm.

THE TARGET IS NOW FUNCTIONAL

Try a simple compile:

#include <stdio.h>

unsigned char buffer[100];

main()
{
   sprintf(buffer, "Hello World!\n")
}

zcc +temp -vn -clib=new test.c -o test

The output will be one or more binaries depending on the options you defined but for a ram model compile, there will be one binary file generated “test_CODE.bin”.

To use printf and scanf, device drivers will have to be written and instantiated on stdin and stdout. See the Device Drivers section below.

11. Optionally Add Target-Specific Library Code to Your Target
12. Cooperating with Third Party Libraries
13. Creating a ROM Resident Monitor, Operating System or Shell
14. Bankswitched Memory

Device Drivers

Device drivers are functions assigned to file descriptors that implement a small set of device-independent stdio messages for communicating with some sort of i/o device. By implementing that set of messages, the stdio library will be able to perform i/o using the device. For targets, these drivers can be instantiated on stdin, stdout and/or stderr in the CRT to allow printf and scanf to function normally. You can also invent other streams at driver instantiation time in the CRT. For example, the cp/m target defines additional streams (stdpun, stdrdr, stdlst) that are connected to devices defined by cp/m. You could also define multiple terminal windows and assign different streams to each so that you could print output into two or more independent windows on screen. But we are getting a little bit ahead of ourselves.

There are three broad categories of device drivers defined by the C library: character, terminal and disk.

  1. Character refers to devices that generate output or receive input one character at a time. This would include things like printers or serial ports.
  2. Terminal refers to a console where an input stream and an output stream are tied together so that the user can enter editable text. Normally a terminal has some kind of output screen with dimensions measured in characters or pixels but this isn't a requirement. You could have input coming from a serial device and output delivered to another serial device with the terminal driver interpretting backspace characters mediating in between.
  3. Disk refers to devices that do i/o in blocks or sectors. The library caches disk sectors to improve runtime performance on real devices. Unfortunately the disk portion of the library is not ready so it is unavailable.

These categorizations are artificial because you can write a driver that implements the stdio messages using your own code without caring what kind of device is attached. The categorizations come into play if you use library code to help implement a driver. There are three types of library driver code that you can inherit from while implementing your driver and, yes, they are character, terminal and disk.

Before getting started, let's lay out some of the reasons behind the driver model used by the C library.

  • The C library must be a complete implementation. z88dk aims to be a complete C implementation and similar in functionality to C compilers found on 32-bit machines as far as that can be taken. This means stdio must work in a device independent manner and file descriptors, defined by POSIX and not the C standard, must be present. Stdio must be able to connect to any sort of i/o device.
  • Many devices must be supported in the same compile in a space efficient manner. Some targets have a dozen or more different disk devices that programs may want to copy between. They can have a half dozen different display modes and the program may want to have terminals using more than one display mode at once.
  • Device drivers should not be a burden to implement. The library should provide code to take care of complex behaviours required by some types of devices. Drivers should be writable using library code to implement most functionality with the author only supplying a small amount of custom code to complete it.
  • Stdio should be reasonably fast without consuming too much memory.

We've solved these problems by making the drivers object oriented.

  • Code inheritance from the library allows complex drivers to be written using mainly library code.
  • Space efficiency is achieved through code reuse aided by the message-passing method used to implement the object model. The message-passing method allows all open terminals to share the same library code and all disk devices to share the same disk code.
  • Achieving reasonable speed without consuming a lot of memory is more difficult since the strategy used in standard C implementations is to create large buffers in user-space for each open FILE in order to service character-at-a-time i/o. Extra buffers are hard to accommodate in a small 64k (or less) address space. Instead, on the input side, z88dk's stdio pushes a state machine to the driver that indicates what input it will accept from the driver. Then only the driver's buffers are used for i/o, if it needs buffering. On the output side, z88dk tries to send strings of characters instead of individual chars.

Deliberately, stdio is not the only means to read/write to the screen or devices inside the library. More direct and low level functions are always present so that the user has the choice to use something simpler if speed or program size is a concern. If you are the originator of a target, it is up to you to supply this functionality and normally the drivers would be implemented in terms of these low level functions themselves. The stdio implementation in z88dk is written in assembly language so it is smaller than those available from other z80 C compilers but 64k is a restrictive environment for C programs nevertheless. For the cp/m target the option is always there to do i/o directly through bdos. Similarly for the zx target, low level functions are available for output to the screen or for reading keys directly from the hardware. You can bypass device drivers entirely and you can still use stdio by confining your program to the sprintf/sscanf families or memstreams and doing i/o directly from buffers in memory. However things are more convenient and fun to program with a proper stdio implementation present, especially if it's easily affordable in the user program and target.

Directory Structure

The target's device drivers are located in z88dk/libsrc/_DEVELOPMENT/target/TARGETNAME/driver.

d driver contains the target's device drivers
f driver/driver.lst lists source files of all drivers
d driver/character contains character device drivers
d driver/character/cpm_00_input_reader directory holding “cpm_00_input_reader” driver's message implementation
f driver/character/cpm_00_input_reader.asm device driver “cpm_00_input_reader”
f driver/character/cpm_00_input_reader.lst lists all source files related to “cpm_00_input_reader”
f driver/character/cpm_00_input_reader.m4 macro for statically instantiating the “cpm_00_input_reader” driver in the CRT
d driver/disk contains disk device drivers
d driver/terminal contains terminal device drivers
d driver/raw contains raw device drivers

The files corresponding to an example “cpm_00_input_reader” driver are in the un-highlighted portion above. The driver has a directory where its message functions reside. The driver itself is the .asm file, the .lst file lists all source files connected to the driver and the .m4 is the instantiation macro for the CRT.

The name “cpm_00_input_reader” is composed of three parts: “cpm” is the target name (this is a real driver for the cp/m target), “00_input” indicates it derives from the library's “character_00_input” base class and “reader” is a name connected to the device being driven.

When creating your device driver, copy this structure and place files in the relevant location. Once the driver is written, its list file should be included in the master list driver/driver.lst. If this is the first driver for the target, be sure that this master list file is included in the target's libraries. It must be listed in all three list files in the directory target/TARGETNAME/library.

The driver code itself should be placed in the most appropriate code section:

  • code_driver
  • code_driver_character_input
  • code_driver_character_output
  • code_driver_terminal_input
  • code_driver_terminal_output

Once the device driver is ready, the target's libraries will need to be rebuilt by running “Winmake TARGETNAME” or “make TARGET=TARGETNAME” from z88dk/libsrc/_DEVELOPMENT.

The cp/m target's driver directory can be used as a model.

The Raw Device Driver

On entry to a device driver, IX points at a file descriptor data structure (aka FDSTRUCT). This data structure contains fields used by the library at the top but it can be extended by any number of bytes by your driver. What is stored there is up to you.

FDSTRUCT
Offset Name Purpose
0 JP jump instruction (195)
1..2 driver address of driver
3 flags F0D0 0TTT (F=1 if filter, D=0 if stdio handles unget, TTT=ioctl message category)
4 ref reference count
5 mode 0TXC BAWR (mode byte supplied to open)
6 ioctl_f0 bit flags automatically managed by ioctl
7 ioctl_f1 bit flags automatically managed by ioctl
8..13 mutex used by library to block other threads
14..? ddata your driver's data

An FDSTRUCT is created dynamically when open() is called but it can also be created statically by instantiating the driver in the CRT. Dynamically opening files will not be available in the library until disk i/o is complete so in the current z88dk, static instantiation is the only means to instantiate drivers.

Some of the flag bits managed by the library in the upper portion of the FDSTRUCT impact on your driver and you will determine what those flag settings are when the driver is instantiated.

In flags (offset 3)

  • F=0. A set bit indicates the driver is a filter. Filters forward the stream to another driver after processing it somehow (for example, character set conversion or stream decompression might be done by a filter). Filters are not fully implemented yet.
  • D=0. One stdio message requires the driver to implement an unget character. If this bit is set, the driver indicates that it will implement ungetc otherwise stdio will take care of it.
  • TTT. The ioctl message category: 000=n/a, 001=terminal input, 010=terminal output, 011=character input, 100=character output. The library defines many ioctls to modify driver behaviour and the ioctl() function is written to reject ioctls not applicable to the driver in order to simplify driver code. If the driver declares itself an input terminal, attempts by the user to send output terminal ioctls will result in an error. Type 000 drivers will not receive any library ioctls.

In mode (offset 5).

  • RW bits indicate if the driver is open for read and/or write. These bits are tested on every read() and write() call but at the FILE* level they are only test when the FILE is opened.
  • BA bits indicate if the driver is open in binary mode and append mode. These bits can be used or ignored by your driver. When open() is implemented, the driver will have an opportunity to reject opens if the mode bits are not supported.

In ioctl_f0 and ioctl_f1 (offsets 6 and 7). In an effort to reduce binary size, ioctl() will automatically manage bit flags corresponding to bits 13..3 of these two bytes. ioctl() supplies commands to set, reset or read these bit flags but before it makes changes it will ask your driver if the changes are acceptable. If your driver implements runtime options, the bit flags are a cheap way to keep track of them. Bits 15..14 and 2..0 cannot be altered by ioctl().

ddata. Any amount of per-file ram data that your driver requires can be appended here.

Stdio communicates with the driver by sending messages to it. The message type is held in the “A” register while other registers will hold parameters. Drivers are therefore structured like switch statements, testing the value of “A” and jumping to implementation code if a match is found. Not all stdio messages have to be implemented. If your driver only does output it can ignore input related messages. If you don't care to support ioctls you can ignore those messages too. Ignoring a message will normally mean exiting from the end of your switch statement via the library's enotsup_zc function. “enotsup_zc” will set errno=ENOTSUP, hl=0 and carry flag set. The carry flag being set indicates an error to the caller. At FILE* level this will place the FILE structure in an error state that will prevent any further i/o until the error is cleared. At POSIX level, the caller will only be informed that an error occurred; further i/o is not actually prevented so if you want that behaviour you will have to implement that in your driver.

STDIO MESSAGES
A= Type Request
STDIO_MSG_PUTC Out Write a single char to the stream multiple times.
IN: E'=char code, BC'=HL=number of chars to output > 0
OUT: HL=num chars successfully written, carry set if error
STDIO_MSG_WRIT Out Write a buffer to the stream.
IN: HL'=void *buffer, BC'=HL=length of buffer > 0
OUT: HL'=void *buffer + num chars written, HL=num chars successfully written, carry set if error
STDIO_MSG_GETC In Read a single char from the stream.
IN: none
OUT: A=HL=char, if error: carry set and HL=0 for general error or HL=-1 for eof
STDIO_MSG_READ In Read multiple characters from the stream into a buffer.
IN: DE'=void *buffer, BC'=HL=length of buffer > 0
OUT: DE'=void *buffer + num chars read, BC=num chars successfully read, carry set if error with HL=0 general or HL=-1 eof
STDIO_MSG_EATC In Read chars from the stream until stopped by the state machine.
IN: HL'=bool (*qualify)(char c), HL=length of buffer >= 0
OUT: BC=num chars read from stream, HL=next unconsumed char, carry set if error with HL=0 general or HL=-1 eof
RESERVED: EXX set is owned by the qualify function
NOTE: Characters are read from the stream until either the buffer is filled or the qualify function disqualifies a char.
qualify() is called by setting A=char; exx; call l_jphl; exx, if carry is set the char is rejected.
The char returned in HL is a peek at the next char in the stream; it must be ungot by either the driver or stdio.
STDIO_MSG_SEEK In/Out Move file pointer to a new position.
IN: C'=C=STDIO_SEEK_SET (0);STDIO_SEEK_CUR (1);STDIO_SEEK_END (2), DEHL=signed 32-bit offset
OUT: DEHL=new file position, carry set if error with HL=0 general or HL=-1 eof, BC' = 0 or num chars sought forward from current
STDIO_MSG_ICTL In/Out Program sends an ioctl() to driver.
IN: DE=ioctl request, BC=first parameter, HL=va_arg* (must mind L→R sccz80 or R→L sdcc order on stack)
OUT: carry reset if ok with HL=return value!=-1, carry set if error
RESERVED: EXX set must not be altered
STDIO_MSG_FLSH In/Out Flush any buffers (terminals clear input buffers too).
IN: none
OUT: carry set on error
STDIO_MSG_CLOS In/Out The file is being closed.
IN: none
OUT: carry set on error (likely ignored)
NOTE: A flush message is always sent ahead of close. Deallocate any resources this driver has allocated.

These messages, error numbers and library ioctls are defined in z88dk/libsrc/_DEVELOPMENT/target/clib_constants.inc which is included into every CRT.

While writing device drivers or any other code with z88dk, it can be very helpful to be familiar with the library code supplied by z88dk. Using it can keep binary sizes down and speed up development time substantially. The root source directory is z88dk/libsrc/_DEVELOPMENT. Many of the subdirectories will have familiar names as they implement portions of the C standard. Two other important subdirectories might be “l” which contains many small subroutines and “error” which provides exit points for functions. These exit points can be used to balance the stack and return after optionally setting errno and a return value appropriately.

Given the above, a driver might be structured like this:

SECTION code_driver
SECTION code_driver_character_input

PUBLIC character_00_input

EXTERN STDIO_MSG_EATC, STDIO_MSG_READ, STDIO_MSG_SEEK
EXTERN STDIO_MSG_FLSH, STDIO_MSG_CLOS, STDIO_MSG_GETC
EXTERN STDIO_MSG_ICTL

EXTERN character_00_input_stdio_msg_getc, character_00_input_stdio_msg_eatc
EXTERN character_00_input_stdio_msg_read, character_00_input_stdio_msg_seek
EXTERN character_00_input_stdio_msg_ictl, error_znc, error_enotsup_zc

character_00_input:

   cp STDIO_MSG_GETC
   jp z, character_00_input_stdio_msg_getc   ;; jump to function implementing getc

   cp STDIO_MSG_EATC
   jp z, character_00_input_stdio_msg_eatc   ;; jump to function implementing eatc
   
   cp STDIO_MSG_READ
   jp z, character_00_input_stdio_msg_read   ;; jump to function implementing read
   
   cp STDIO_MSG_SEEK
   jp z, character_00_input_stdio_msg_seek   ;; jump to function implementing seek

   cp STDIO_MSG_FLSH
   jp z, error_znc                           ;; do nothing and report no error
   
   cp STDIO_MSG_ICTL
   jp z, character_00_input_stdio_msg_ictl   ;; jump to function implementing ioctl
   
   cp STDIO_MSG_CLOS
   jp z, error_znc                           ;; do nothing and report no error
   
   jp error_enotsup_zc                       ;; hl = 0 puts FILE stream in error state

Drivers are allowed to change any registers except ix and iy unless otherwise documented.

The above is an abstract base class driver implemented in the library. The entire driver can be found in z88dk/libsrc/_DEVELOPMENT/drivers/character/character00/input.

If your driver properly ignores messages that do not apply and implements messages from the list above that do apply, your driver is finished. This is the .asm file mentioned in the last section. The message functions are placed in the driver's subdirectory and the list file will list all of the driver's source files. The last piece is the .m4 macro that will allow the driver to be instantiated as a file in the CRT. This is discussed in the Driver Instantiation topic below.

The Character Device Driver

The library supplies code to implement character device drivers. Character device drivers are intended for performing i/o on devices like serial or parallel ports that generate or consume a stream of individual characters. Since the library's character driver base classes change stdio's multiple character messages to single character messages, it is likely a raw device driver will have higher throughput if it is able to read or write multiple bytes at a time. On the other hand, with the aid of the library base classes, your driver will automatically (and optionally) do CRLF conversions and can be written in as little as a dozen lines of code.

There are three character device drivers your driver can inherit from: character_00_input, character_00_input_bin, and character_00_output. If your driver will do both input and output on the same file descriptor, you can inherit from both an input driver and an output driver. The difference between character_00_input and character_00_input_bin is that the former can have an option set that will do CRLF conversion whereas the latter cannot. The latter driver corresponds to opening files in binary “b” mode. On the output side, character_00_output can also be configured to do CRLF conversion but since the code for binary mode is not impacted by additional code for CRLF, a separate binary driver is unnecessary.

Driver Inheritance -- How it Works

The Terminal Device Driver

The Disk Device Driver

IOCTLs for Drivers

Driver Instantiation in the CRT

Interrupts

I/O

C++ STL Containers

Integer Math

Floating Point Math

Memory Allocation

Tutorials

Examples

These example programs are intended to help familiarize you with some of the library's features. Some of the programs have been made more complicated than necessary to illustrate various points.

  • BEEPFX is a sound effect generator for targets with 1-bit sound devices. The program plays all available sound effects and then accepts user input to play effects by number. A windows program is available to make new effects and it is explained how they can be imported into your project.
  • CHESS is one of the shortest chess programs in the world. Straightforward modifications are made to allow a program written for a 32-bit machine to run on a z80.
  • CLISP is a lisp interpreter written in C. The output binary is destined for a 16k ROM which presents challenges as the binary is larger than 16k. The solution chosen is to run the program from RAM but have a compressed copy stored in ROM that is decompressed in RAM at power up. Details on the ROM model and data compression are discussed.
  • PASSWORD QUERY. Stdin has the username stuffed into its input buffer so that a following scanf primes the input with the last username entered. Password mode is set so that asterisks are printed while the password is entered.
  • PI computes pi to 800 digits. The library is configured to speed up execution almost four times.
  • SP1_EX1 sp1 software sprite engine example for the zx spectrum target. Mixes C and external asm data, applies crt and library configurations to minimize code size, customizes sp1 for a different display area, moves the library's malloc heap to a known location in memory. A bonus program adds collision detection and a few sound effects.
  • SUDOKU SOLVER solves sudoku puzzles. The more sophisticated version uses p_list_t (a doubly-linked list type from the library), makes use of obstack for memory allocation, and creates a new SECTION to hold data that is assembled as a separate binary file.

Mixing C and Assembly Language

The compilers translate C code into assembler. These translated assembler files are treated no differently from hand-written assembly code consumed by the assembler. In particular, just like any other assembly language input, assembly code can interact with variables and functions defined in the translated C.

This section gives a compact overview of the assembly language environment supported by z80asm and then describes how assembly language code and C code can interact with each other.

Files and Scope: PUBLIC, EXTERN, GLOBAL

The basic translation unit is the file. All symbols within the same file are implicitly local, meaning they are not visible to asm code in other files. To make symbols publicly visible, they must be declared PUBLIC. To reference a symbol defined in another file, it must be declared EXTERN. Extern symbol references are resolved at link time.

FILE 1: “asm_strcpy.asm”

PUBLIC asm_strcpy

asm_strcpy:

   push de
   xor a

loop:

   cp (hl)
   ldi
   jr nz, loop
   
   pop hl
   dec de
   ret

FILE 2: “Hello.asm”

PUBLIC Hello

EXTERN asm_strcpy

Hello:

   ; enter : de = destination string pointer

   ld hl,hello_s
   jp asm_strcpy

hello_s:

   defm "Hello"
   defb 0

FILE 3: “World.asm”

PUBLIC World

World:

   ; de = destination string pointer

   ld hl,world_s
   jp asm_strcpy

world_s:

   defm "World"
   defb 0

FILE #1 exports the symbol “asm_strcpy”. The other symbol “loop” is local and cannot be seen outside the file.

FILE #2 exports the symbol “Hello”. It declares an external reference to “asm_strcpy”. “hello_s” is a local symbol. The code jumps to “asm_strcpy” which is declared EXTERN, so the assembler will resolve this reference at link time when it has access to all PUBLIC symbols.

FILE #3 exports the symbol “World”. The symbol “world_s” is local. The assembler will complain about an unresolved reference in “asm_strcpy”. Since it's (mistakenly) not declared EXTERN, the assembler expects the label to be defined locally.

To assemble these files they can be listed in the same z80asm invocation:

z80asm -b -o=program asm_strcpy.asm Hello.asm World.asm

or they can be assembled into object files individually and then linked together:

z80asm -Mo -o=asm_strcpy asm_strcpy.asm
z80asm -Mo -o=Hello Hello.asm
z80asm -Mo -o=World World.asm
z80asm -b -o=program asm_strcpy.o Hello.o World.o

z80asm also supplies the GLOBAL scope operator for compatibility with sdcc. GLOBAL indicates that the symbol may be declared locally or it may be declared externally. If the symbol is declared locally, it is exported so that GLOBAL behaves like PUBLIC. If the symbol is not declared locally, GLOBAL behaves like EXTERN so that it is assumed to be declared externally.

Code and Data Placement in Memory: SECTIONs

A section is a named container with an optional ORG address that can hold assembly code and data. The programmer can direct code and data into specific sections with suitable section directives embedded in the assembly source. On assembly, the output will be composed of one binary for each section with its own ORG address. Sections without ORG addresses append themselves to the last section defined in text order.

The best way to understand sections is to see some examples.

SECTION code
org 0

PUBLIC main

EXTERN display, input

main:

   call initialize

loop:

   call display
   call input
   jr loop


SECTION data
org 32768

PUBLIC maze

maze:

   defm "XOXOXXXXXOXXXO"
   defm "XOXOOOOOOOOOOO"
   defm "XOXXXXXOXXXXXO"
   ...


SECTION bss

PUBLIC workspace, _score

workspace:  defs 128
_score   :  defs 2


SECTION code

PUBLIC calculate

EXTERN l_mult

calculate:

    ld hl,(_score)
    ld de,10
    call l_mult
    ret

In the above source, three sections are defined: “code”, “data” and “bss”. As can be seen sections are open, meaning they can be added to from anywhere. The “calculate” function at the end of the example is placed in the same “code” section as the “main” code at the start of the example. In fact “calculate” will immediately follow “main” in memory. This also applies to source code spread across multiple files.

The “code” section has been assigned an ORG of 0 and the “data” section has been assigned an ORG of 32768. However “bss” has not been assigned an ORG. It will be appended to the previously defined section in the file (“data” in this example). If there were no such previous section, it would have behaved as if assigned an ORG of 0.

On assembly, two binary files will be generated: “name_code.bin” and “name_data.bin” where “name” is the name of the output file. The “bss” section, not having an ORG, will be part of “name_data.bin”. The “code” section had an ORG of 0 so “name_code.bin” should be loaded at address 0 in memory. Similarly “data” had an ORG of 32768 so “name_data.bin” should be loaded at address 32768.

“ORG -1” has special meaning. It indicates that the section will follow the previous one in address order but it will be output as a separate binary.

Notice that because sections can append to previously defined ones if not given an ORG address, the order that they are encountered during assembly determines where the linker will place them in memory. A sane project will define a memory map, which is simply a listing of sections in the order that they should appear in memory; by necessity this memory map must be present in the first file seen by the linker. Within z88dk this memory map is defined in the crt with the (unassembled) crt.asm file appearing first in the list of object files being linked to form the executable. An example of the typical memory map defined in crts appears in the next section.

Sections give programmers the power to place code and data at specific places in memory. These are some applications:

  • Programs destined for ROM must have their read-only (code) portion stored in ROM but their variables must be stored in RAM. The read-only portion of asm code can be assigned to a CODE section and the variables used by the asm code can be assigned to a DATA section. ORGing the CODE section to ROM and the data section to the RAM area solves the problem.
  • Programs using bankswitched memory must assign code and data to specific memory banks that are often fixed in size. There is normally address overlap as well with different banks occupying the same address range. A section can be created for each bank and these sections can have any ORG, including overlapping values. z80asm does not enforce size constraints on sections so it is up to the programmer to verify that sections do not exceed their maximum size when assembled.
  • Sometimes data must be aligned on certain byte boundaries. A section can be created with an ORG address set to a byte-aligned boundary and any data placed in it will have the desired alignment.

Crt Memory Map

The new C library makes extensive use of sections. Sections are defined at a fine-grain level, normally one per logical module. There is a section for string functions, a section for stdlib, a section for byte arrays, and so on. This fine-grained section assignment gives the programmer more control over where library functions are located.

When compiling C programs, the library supplies crts that are responsible for initializing the C environment before starting main(). They are also guaranteed to be the first file listed in the final linking step when an executable is made. This allows them to define the memory map for the compile.

The memory map is simply a list of sections that informs the linker what order to place code and data in memory. Most crts divide memory into three main sections: CODE, DATA and BSS.

The memory map is defined right at the beginning of the crt before any code or data is seen. A typical crt contains a prologue similar to this:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;                zx spectrum if2 cartridge                  ;;
;;     generated by target/zx/startup/zx_crt_if2.m4          ;;
;;                                                           ;;
;;   16k ROM in 0-16k area, ram placement per pragmas        ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CRT AND CLIB CONFIGURATION ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

define CONFIG_ZX_IF2

include "../crt_defaults.inc"
include "crt_target_defaults.inc"
include "../crt_rules.inc"

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SET UP MEMORY MODEL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

include "memory_model.inc"
...

and the memory model looks like this:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

SECTION CODE

org __crt_org_code

section code_crt_init
section code_crt_main
section code_crt_exit
section code_crt_return
section code_crt_common

include "../../clib_code.inc"
include "../../clib_rodata.inc"

section code_lib
section rodata_lib

section code_compiler
section rodata_compiler

section code_user
section rodata_user

SECTION CODE_END

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

SECTION DATA

IF __crt_org_data

   org __crt_org_data

ELSE

   IF __crt_model
      
      "DATA section address must be specified for rom models"
   
   ENDIF

ENDIF

defb 0

include "../../clib_smc.inc"
section smc_compiler
section smc_lib
section smc_user

include "../../clib_data.inc"
section data_compiler
section data_lib
section data_user

SECTION DATA_END

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

SECTION BSS

IF __crt_org_bss

   org __crt_org_bss

ELSE

   IF __crt_model

      org -1

   ENDIF

ENDIF

defb 0

section BSS_UNINITIALIZED
include "../../clib_bss.inc"
section bss_compiler
section bss_lib
section bss_user

SECTION BSS_END

;; end memory model ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

The includes are aggregates of the fine-grained sections defined by the new C library. They can be examined in {z88dk}/libsrc/_DEVELOPMENT.

There are three main sections defined: “CODE”, “DATA” and “BSS”. Each of these may have an independent ORG address. If “DATA” does not have an independent ORG address, it will be appended to “CODE”. Similarly if “BSS” does not have an independent ORG address, it will be appended to “DATA”. Recall that any section with an independent address will be output in a separate binary file on assembly.

The smaller sections that follow “CODE”, “DATA” and “BSS” never have an independent ORG so they will always be absorbed as part of “CODE”, “DATA” or “BSS”. This is what it means to define a memory map – all sections are sequenced and their placement in memory is defined.

Some sections have been set aside to hold user code and data (code_user, rodata_user, smc_user, data_user, bss_user). Properly placing any user assembly code into these sections allows the compiler to generate binaries destined for ROMs. User assembly code is not confined to using these defined sections; by simply declaring new sections with their own ORG, a compile will output those sections as independent binary files. This has many applications as well.

What Happens If There Aren't Any Sections?

A no-name section is active when an asm file is opened. If no sections are defined at all in asm source files, all asm code and data will be placed into this no-name section and the project will assemble as expected into a single binary file. If the project imports code that has been assigned to sections, those sections, assuming no ORG address, will simply append to the no-name section.

In short, the asm code will assemble as if the assembler knew nothing about sections.

Note that the classic C library is not yet section aware so its code and data is defined in the no-name section.

C Translation to Assembly

The compilers translate C code to assembly language and at this point no distinction is made among any assembly input to z80asm. This means any assembly code in a project can interact with the translated C as if it were any ordinary assembly code.

Global Names (Variables and Functions)

The same scoping rules apply to the translated C code as to the ordinary assembly code: labels are local to a file unless they are declared PUBLIC and any external labels are only visible inside the file if they are declared EXTERN. This maps directly to C where global variables and functions are declared PUBLIC by the compiler inside the translated C and all other labels within the translated C are invisible outside the asm file. The C code can access external variables by using the standard C “extern” keyword which causes the compiler to declare those variables as EXTERN in the translated asm.

The only piece of information missing is how C mangles function and variable names. It's simple: all names are preceded with a leading underscore when the C is translated to assembler. There is one exception under sccz80 where functions can be given the “LIB” attribute in declarations and this prevents the leading underscore from being added but let's ignore that for now and see an example to solidify these ideas.

This example contains a small C program and a separate assembly subroutine that interact through global variables and functions. We've gone out of our way to avoid dealing with parameters passed to functions which is discussed in the next topic.

File: “main.c”

#include <stdio.h>
 
int a[5] = {0,1,2,3,4};
 
void negate_odd_a(void)
{
   int i;
 
   for (i=0; i!=5; ++i)
      if (a[i] & 0x01) a=-a;          // negate odd numbers in array a[]
 
   return;
}
 
extern void neg_and_triple_a(void);   // implemented elsewhere
 
main()
{
   int i;
 
   neg_and_triple_a();                // call to asm subroutine
 
   for (i=0; i!=5; ++i)
      printf("a[%d]=%d\n", i, a[i]);
 
   return 0;
}

File: “nata.asm”

SECTION code_user

PUBLIC _neg_and_triple_a  ; export C name "neg_and_triple_a"

EXTERN _a                 ; access global C variable "a"
EXTERN _negate_odd_a      ; access global C function "negate_odd_a"

_neg_and_triple_a:

   call _negate_odd_a     ; call C function "negate_odd_a()"

   ; triple contents of array a[]

   ld b,5               ; array a[] has five members
   ld hl,_a             ; hl = &a[0]

loop:

   push hl              ; save address in array a[]

   ld e,(hl)
   inc hl
   ld d,(hl)            ; de = int member of a[]

   ld l,e
   ld h,d
   add hl,hl
   add hl,de
   ex de,hl             ; de = int * 3

   pop hl               ; hl = address in array a[] of int read

   ld (hl),e
   inc hl
   ld (hl),d            ; replace int with its value tripled
   inc hl               ; hl points to next array entry

   djnz loop            ; do it for all five members of array a[]
   ret

Both files are listed in the compile line (or they are individually assembled to object files and linked in a final linking step):

zcc +zx -vn -clib=new main.c nata.asm -o main

The C code contains three global names that will be exported as PUBLIC when the C is translated to asm:

  • int a[5] is declared “PUBLIC _a”
  • void negate_odd_a(void) is declared “PUBLIC _negate_odd_a”
  • main() is declared “PUBLIC _main”

The C code imports one name by declaring it “extern”:

  • extern void neg_and_triple_a(void) is declared “EXTERN _neg_and_triple_a”

The asm code exports one global name:

  • PUBLIC _neg_and_triple_a

The asm code imports two global names:

  • EXTERN _a which is the address of C's “int a[5]”
  • EXTERN _negate_odd_a which is the address of C's “void negate_odd_a(void)”

Tracing execution from main(), C calls function “neg_and_triple_a()” which has been declared extern. This means the linker must find it outside the translated C asm file. And it does find it in “_neg_and_triple_a” exported from file “nata.asm”. So the C function calls the assembly subroutine “_neg_and_triple_a”.

The assembly subroutine “_neg_and_triple_a” calls function “_negate_odd_a” which has been declared EXTERN in “nata.asm”. This means the linker must find it outside the file “nata.asm”. And it does – the name “_negate_odd_a” is exported from the C program by “void negate_odd_a(void){}” so the asm subroutine calls the C function “negate_odd_a()”.

The C function “negate_odd_a()” negates all odd members of global array a[].

On return, the asm subroutine “_neg_and_triple_a” then triples all members of the array a[]. It gets the address of the first element of “int a[5]” by declaring the name “_a” EXTERN. The C programs exports the address of the array a[] when it is translated to asm by declaring it “PUBLIC _a”.

The asm subroutine then returns to main(). main() prints the elements of a[] to screen.

The result:

a[0]=0
a[1]=-3
a[2]=6
a[3]=-9
a[4]=12

As you can see, global names are very simply shared between C and asm.

In C, apparently global names can be declared “static”. The static keyword used in these cases actually means do not make the associated name global. Here are a few examples:

static int a[5];
 
static void negate_odd_a(void)
{
...
}

These names will not be made PUBLIC in the translated C and they will not be visible outside the C file where they are defined.

Local variables in functions can also be declared static. In these cases the static keyword means something entirely different – the variable is assigned permanent storage in memory rather than temporary storage on the stack. This is discussed in the next section.

Functions

The address of a function is indicated by its associated label. That label is shared by declaring it PUBLIC and accessed from another file by declaring it EXTERN. This has already been discussed in the previous section.

What has not been discussed is how parameters and return values are communicated with functions. An overview was given earlier in the documentation which should be reviewed before proceeding as that information will not be rehashed here.

Return Value

The return value from functions is communicated via a subset of registers DEHL. If the result is char (8-bit), it is returned in the L register. If the result is int or pointer (16-bit), it is returned in HL. If the result is long (32-bit), it is returned in DEHL.

Neither compiler supports returning structs.

In sdcc, floats/doubles are 32-bit and are communicated by DEHL as usual. In sccz80, floats/doubles are 48-bit and are treated differently. In the new C library, floats/doubles are returned in BCDEHL in the exx set. In the classic C library, floats/doubles are returned via the “primary floating point accumulator” which is 6-bytes of static memory at address “fa”. Because the classic C library uses static memory to communicate float values, float computations are not reentrant with the classic C library.

Parameters

Both C compilers support parameter passing using three different linkages:

  • Standard. The parameter values are pushed onto the stack, the function is called, and then the stack is repaired by popping the values off. The function called collects parameter values from the stack without disturbing the stack pointer on return.
  • Fastcall. One parameter is passed to the function via register. The rules are exactly the same as for the return value already discussed. A subset of DEHL holds the parameter value (L for an 8-bit value, HL for a 16-bit value, DEHL for a 32-bit value). Neither compiler supports passing structs as a parameter. Floats/doubles under sdcc are 32-bit and are communicated through DEHL. sccz80 uses a 48-bit float/double type which is treated differently. Under the new C library, the float/double value is communicated by BCDEHL in the exx set. Under the classic C library, the float/double value is communicated through the “primary floating point accumulator” which is six bytes of memory at address “fa”. Because of this, floating point computations are not reentrant under the classic C library.
  • Callee. The parameter values are pushed onto the stack and the function is called. The callee is expected to repair the stack. This is especially convenient for assembly subroutines as the act of repairing the stack (removing the pushed parameters) is the same as popping the parameters into appropriate registers as required by the subroutine. Because the caller no longer repairs the stack, callee linkage can save hundreds of bytes in long programs.

Whether the C compiler generates standard, fastcall, or callee linkage for a particular function depends on the function's prototype. The prototype supplies attributes identifying which linkage to use.

Some examples:

// SCCZ80
 
int    __FASTCALL__ abs(int i);                                 // sccz80 fastcall linkage
double __CALLEE__   strtod(const char *nptr, char **endptr);    // sccz80 callee linkage
int                 printf(const char *format, ...);            // sccz80 standard linkage (note: vararg! see below)
 
// SDCC
 
int    abs(int i) __z88dk_fastcall;                             // sdcc fastcall linkage
double strtod(const char *nptr, char **endptr) __z88dk_callee;  // sdcc callee linkage
int    printf(const char *format, ...);                         // sdcc standard linkage

Note: Under sccz80, vararg functions use standard linkage but the caller must also set the A register equal to the number of 16-bit words pushed onto the stack.

With no decorations added, both compilers use standard linkage to call a function. However sccz80 and sdcc differ on the attribute decorations used to indicate fastcall and callee linkage.

That's not where the differences end. sdcc pushes its parameters on the stack in right-to-left order whereas sccz80 pushes in left-to-right order. Right-to-left order is most appropriate for C compilers because accessing the first parameter in a vararg parameter list is trivial. That's not the case for left-to-right order where it must be known how many parameters are pushed on the stack to calculate where the first vararg parameter is located (see note above). This error was made 35 years ago with Ron Cain's original small-C compiler and since then it has infected dozens of small-C derived compilers since.

These complications are taken care of automatically by the C compilers when C code is compiled and the library makes these details invisible. C code, if accepted by both compilers, can be compiled by either compiler without any changes to the source.

What this does affect is assembly code. An assembly subroutine can be called by the C compiler if it is given an extern function prototype. The extern prototype specifies what linkage to use and the C compilers will generate appropriate calls using the linkage specified. The assembly subroutine can then collect parameters according to the linkage contract.

There are three difficulties with this:

  • 1. Linkage Attributes are Different for the Two Compilers. This can be solved by supplying different prototypes depending on which compiler is doing the compiling.
#ifdef __SDCC
extern char *itoa(int num, char *buf, int radix) __z88dk_callee;
#endif
 
#ifdef __SCCZ80
extern char __CALLEE__ *itoa(int num, char *buf, int radix);
#endif
  • 2. Calls Through Function Pointers Must Use Standard Linkage. Under z88dk all calls through function pointers use standard linkage. This means if a function is implemented using another linkage and function pointer calls are to be allowed, the implementation must also offer a second entry point for standard linkage. This is resolved with a little macro magic.
#ifdef __SDCC
extern char *itoa(int num, char *buf, int radix);
extern char *itoa_callee(int num, char *buf, int radix) __z88dk_callee;
#define itoa(a,b,c) itoa_callee(a,b,c)
#endif
 
#ifdef __SCCZ80
extern char            *itoa(int num, char *buf, int radix);
extern char __CALLEE__ *itoa_callee(int num, char *buf, int radix);
#define itoa(a,b,c) itoa_callee(a,b,c)
#endif

Two functions are declared, one with standard linkage and another with callee linkage with name augmented with “_callee”. The implementation must provide both entry points. The magic is in the following #define. If the function is invoked with parameters, it is substituted with a call to the _callee entry point. When function pointers are assigned, they are assigned the name of the function only without parameters so that the substitution is not done and the function pointer will be assigned the address of the plain standard linkage function.

// BEFORE MACRO SUBSTITUTION BY ZCPP
 
p = itoa(10000, p, 16);
f = itoa;
p = (f)(20000, p, 10);
 
// AFTER MACRO SUBSTITUTION BY ZCPP
 
p = itoa_callee(10000, p, 16);   // normal invocation uses callee linkage
f = itoa;                        // function pointer assigned standard linkage
p = (f)(20000, p, 10);           // function pointer calls use standard linkage
  • 3. Callee and Standard Linkage Have Parameters Pushed in Different Order for the Two Compilers. This can be solved by supplying different asm code to collect parameters depending on which compiler is active.
; char *itoa(int num, char *buf, int radix)
; (callee linkage from sdcc or sccz80)
 
SECTION code_user
 
PUBLIC _itoa, _itoa_callee, asm_itoa
 
_itoa:
 
  ; standard linkage entry point
 
IFDEF __SDCC
 
   pop af      ; return address
   pop hl      ; hl = int num
   pop de      ; de = char *buf
   pop bc      ; bc = int radix
 
   ; restore stack
 
   push bc
   push de
   push hl
   push af     ; return address
 
ENDIF
 
IFDEF __SCCZ80
 
   pop af      ; return address
   pop bc      ; bc = int radix
   pop de      ; de = char *buf
   pop hl      ; hl = int num
 
   ; restore stack
 
   push hl
   push de
   push bc
   push af     ; return address
 
ENDIF
 
   jr asm_itoa
 
_itoa_callee:
 
   ; callee entry point
 
IFDEF __SDCC
 
   pop af      ; return address
   pop hl      ; hl = int num
   pop de      ; de = char *buf
   pop bc      ; bc = int radix
   push af     ; return address
 
ENDIF
 
IFDEF __SCCZ80
 
   pop hl      ; return address
   pop bc      ; bc = int radix
   pop de      ; de = char *buf
   ex (sp),hl  ; hl = int num
 
ENDIF
 
   ; params have been removed from the stack per callee contract
 
asm_itoa:
 
   ; assembly entry point with register API
 
   ; bc = radix
   ; de = buf
   ; hl = num
   ; stack = return address
 
   ;; implementation here
 
   ;; return char* in HL here
 
   ret

This code snippet, in combination with the C function prototypes in point 2, supplies an entry point for function pointers (_itoa), an entry point for callee linkage (_itoa_callee) and an assembly language entry point with parameters in registers (asm_itoa). All that and it supports both C compilers equally.

The above example is nearly how the new C library is written to accommodate both C compilers. When libraries are discussed later it will be revealed that the basic unit of code is the file so that if a certain library function is required by the program, the entire file's contents that it is found in will be pulled out of the library and attached to the program. For this reason, it is desirable to compose libraries of many minimally sized files normally one subroutine per file. So in the example above, the new C library would separate the asm implementation (asm_itoa), the standard linkage (_itoa with a jump to asm_itoa) and the callee linkage (_itoa_callee with a jump to asm_itoa) into three separate files. The new C library also has two different header files, one for sdcc and one for sccz80, generated from a single prototype header file using macros and m4 to automatically build them.

Reserved Registers

sccz80 does not reserve any registers for itself. Assembly language subroutines can use whatever registers they want that are allowed by the target.

sdcc, however, expects the two index registers ix and iy to be unchanged. Assembly language subroutines must preserve those registers if they are modified. If the C code is compiled with ”–reserve-regs-iy” then sdcc will not use iy and iy is safe to modify. Take note of this known bug associated with ”–reserve-regs-iy”.

The Stack Frame

Inside a C function, a stack frame is constructed. The stack frame is an area on the stack that contains the parameters passed to the function and the local non-static variables declared inside the function. sccz80 and sdcc differ in how the stack frame is formatted and how it is addressed.

Until this point the concern has been on how C code can call assembly language subroutines and how the assembly language subroutines gather passed parameters and return a value. With this section is going to examine how to go the other way with assembly language calling a C subroutine.

……

Inlined Assembly

Assembly language can be inlined in C code if it is surrounded by ”#asm” and ”#endasm” tags. Note that sdcc's back end is not used by z88dk so all assembly language instructions, inlined assembly included, is processed by z80asm and therefore use standard Zilog mnemonics.

The C compilers ignore inlined assembly code and simply generate code around it. Inlined assembly can access any variables and functions defined in the local file and can make use of PUBLIC and EXTERN to export and import names as discussed before. They can also access local variables declared on the stack as described in the last section.

Inlined assembly is often misused in the z80 community. It's meant to interject a small amount of assembly code into an otherwise C function. But quite often C functions are declared with their entire bodies written in assembly language using inlined assembler. The preferred way to implement assembly subroutines called from C was described earlier: prototypes are supplied in a header file to tell the C compiler how to call the asm functions and then a separate asm file contains the asm implementation.

  • C prologue can change over time or across compilers
  • cannot place code in arbitrary section
  • separation of C and asm is cleaner organizationally and increases portability
  • separation makes it easy to create libraries of asm routines later
  • can easily supply an asm interface
  • compilers may have moved data into a register

Links

Magazine Articles

Videos

 
temp/front.txt · Last modified: 2017/02/06 17:07 by aralbrec
 
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki