text:
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:
|Twitter Client
|File Transfer
|3D Globe
|Forest Raider Cherry
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.
z88dk has seen significant development in the past two years. Among the changes:
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 (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.
The last z80 compiler from Hitech, cross compiles z80 code from MSDOS. Seems to be near complete compliance with C89.
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).
(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.
(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.
(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 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.
|Z80 Cycles
|Wall Clock @ 4MHz
|Hitech-C CPM v3.09
|94 sec
|Hitech-C Z80 v7.50
|75 sec
|80 sec
|64 sec
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.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.
|Z80 Cycles
|Wall Clock @ 4MHZ
|Z80 Cycles
|Wall Clock @ 4MHZ
|Hitech-C CPM v3.09
|22 min 46 sec
|Hitech-C Z80 v7.50
|23 min 00 sec
|24 min 31 sec
|37 min 03 sec
|22 min 28 sec
|21 min 52 sec
|15 min 43 sec
|22 min 01 sec
|15 min 47 sec
|8 min 08 sec
|6 min 04 sec
|8 min 18 sec
|6 min 07 sec
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.
|Z80 Cycles
|Wall Clock @ 4MHZ
|Hitech-C CPM v3.09
|1.137 sec
|Hitech-C Z80 v7.50
|0.918 sec
|1.038 sec
|1.331 sec
|1.331 sec
|0.923 sec
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.
|Z80 Cycles
|Wall Clock @4MHZ
|Hitech-C CPM v3.09
|2 min 39 sec
|Hitech-C Z80 v7.50
|11 min 45 sec
|5 min 24 sec
|4 min 04 sec
|3 min 50 sec
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)
|Hitech-C CPM v3.09
|Hitech-C Z80 v7.50
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.
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:
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.
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 ./build.sh
You can run z88dk keeping it in the current location, all you need to do is to set the following environment variable:
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).
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:
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.
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.
These tools are supplied by third parties.
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 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
NOTE: It appears recent updates to sdcc have introduced a number of bugs in the z80 target. To avoid them, check out revision #9958.
Other users will have to apply the svn patch found in z88dk/src/zsdcc and build sdcc from source.
A typical linux install process for sdcc would look like this:
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:
"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
(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
These tools are not normally invoked by the user:
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.
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)
SCCZ80 is z88dk's native C compiler.
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
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.
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 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
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.
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.
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.
Programs using the classic library can be compiled with sdcc. The following limitations must be respected:
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 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
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.
In general any sdcc option can be appended to the compile line.
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.
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.
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.
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 either the "-clib=" (new library) or "-compiler=" (classic library) options.
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.
sccz80 is almost C89 compliant.
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.
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 can operate under C89, C99 and partial C11 compliance via command line switch. By default it operates under C99 compliance.
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.
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.
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.
|long long
|void *
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.
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.
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.
Both compilers support three calling conventions listed below.
The compiler pushes parameters onto the stack, calls the function and then clears the stack by popping the parameters off the stack.
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.
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.
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 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.
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. sdcc added these calling conventions for compatibility with z88dk's libraries. sccz80 is capable of generating callee code. Both sccz80 and sdcc are 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.
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.
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.
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".
how to ensure zcc_opts.def does not grow indefinitely and is erased at an appropriate time.
Both compilers support user-selected optimization levels.
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".
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 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
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.
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
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.
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.
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.
Example compile:
zcc +zx -vn -clib=new test.c -o test
"test.c" is taken through the following steps:
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.
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.
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:
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.
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.
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:
Some information on the library:
The library was written to be compatible with sdcc; some effort has been made to improve sdcc's output when used with z88dk:
Notable omissions:
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.
Contains general library settings.
|Leave disabled. The library does not support multi-threading yet.
|Choose between small and fast integer library.
|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.
|Choose between small and fast integer shifting.
|Enable specialized functions for ascii → binary, octal, decimal and hex conversion.
|For enabled specialized functions choose between small and fast implementations.
|Enable specialized functions for binary, octal, decimal and hex → ascii conversion.
|For enabled specialized functions choose between small and fast implementations.
|Set this to one if you want stdio to check the validity of a FILE* before using it.
|Individually enable or disable printf converters; this can help to reduce program size1.
|Individually enable or disable scanf converters; this can help to reduce program size2.
|Options to increase the speed of memcpy and memset using unrolled ldir; some other library code will also benefit.
|Enable parsing of nan/inf strings and hex floats in form -0xh.hhhhp-dd
|Selects the sorting algorithm used by qsort(). The default is shellsort3.
|Various quicksort algorithm options.
|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.
Contains settings for target-specific portions of the library. Options will vary according to architecture but these two will always be present:
|The target's cpu clock rate in Hz1.
|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.
Once the library's configuration has been edited, the target's library must be rebuilt in order for changes to take effect.
To rebuild the embedded library, "Winmake embedded" should be run.
To rebuild the embedded library, "make TARGET=embedded" should be run.
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.
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:
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().
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:
|data section appends to code section
|bss section appends to data section
|ram model in effect
|crt will set SP=0
|max stack size is 512 bytes
|crt will not initialize BSS
|no command line
|do not restart on program exit; infinite loop or return to host if code org != 0
|close files on exit
|no user supplied rst vectors
|no user supplied nmi
|two functions can be registered with atexit()
|no functions can be registered with at_quick_exit()
|heap is automatically created and will lie between the end of BSS and the stack
|size of stdio heap used to allocate FILE* and driver structures
|no block allocator table
|max number of open FILE* = 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:
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.
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.
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.
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.
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.
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).
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:
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.
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.
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.
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:
# # 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
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.
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:
|contains the target's device drivers
|contains list files specifying the contents of the target's C library
|lists contents of the target's C library when sccz80 is used as compiler
|lists contents of the target's C library when sdcc is used as compiler and the library uses ix
|lists contents of the target's C library when sdcc is used as compiler and the library uses iy
|contains the target's CRTs
|CRT #0 generated from temp_crt_0.m4
|Human readable and editable CRT macro that is expanded by m4
|selects library build-time options for the target's C library
|selects library build-time options for the target-specific portion of the C library
|target-specific global constants defined at program compile time
|target-specific (memory occupying) variables defined at program compile time
|crt options defined at program compile time
|memory map defined at program compile time
|defines which crt to use at program compile time
|must include temp_crt.asm
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.
|Abstract data types: arrays, vectors, stacks, queues, lists
|Memory allocators: balloc (block allocator), heap (malloc), obstack
|Architecture-specific code
|Data compression
|Character classification
|Base classes for writing device drivers
|Exit subroutines used by the C library to set errno
|I/O using file descriptors
|Fixed width bitmap font definitions and fzx proportional fonts
|Direct hardware access to keyboard, joystick, mouse
|Known names for some functions operating on largest integer type as defined by C standard
|Library subroutines and compiler primitives
|A few functions defining locale-specific character comparison
|Integer and float math functions
|Network related
|Long jumps for C
|Music and sound effects for 1-bit devices
|Standard C I/O using FILE*
|Standard C library includes number ↔ text conversion, random numbers, sorting, etc
|Standard C string and raw memory manipulation
|Software sprites for bitmapped displays
|(Incomplete) Mutex, critical section, spinlock, call_once
|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:
@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.
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.
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.
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:
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:
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.
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.
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.
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:
|sets the address of the CODE section
|sets the address of the DATA section
|sets the address of the BSS section
|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.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; 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:
|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:
|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:
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:
|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 STUBS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; include "../clib_stubs.inc"
Labels defined for incomplete portions of the library that need to be present for successful compiles.
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:
|the lower eight bits indicate which rst locations are to be implemented by user code.
|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
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.
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:
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.
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.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; 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.
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.
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.
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.
We've solved these problems by making the drivers object oriented.
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.
The target's device drivers are located in z88dk/libsrc/_DEVELOPMENT/target/TARGETNAME/driver.
|contains the target's device drivers
|lists source files of all drivers
|contains character device drivers
|directory holding "cpm_00_input_reader" driver's message implementation
|device driver "cpm_00_input_reader"
|lists all source files related to "cpm_00_input_reader"
|macro for statically instantiating the "cpm_00_input_reader" driver in the CRT
|contains disk device drivers
|contains terminal device drivers
|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:
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.
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.
|jump instruction (195)
|address of driver
|F0D0 0TTT (F=1 if filter, D=0 if stdio handles unget, TTT=ioctl message category)
|reference count
|0TXC BAWR (mode byte supplied to open)
|bit flags automatically managed by ioctl
|bit flags automatically managed by ioctl
|used by library to block other threads
|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)
In mode (offset 5).
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.
|Write a single char to the stream multiple times.
|E'=char code, BC'=HL=number of chars to output > 0
|HL=num chars successfully written, carry set if error
|Write a buffer to the stream.
|HL'=void *buffer, BC'=HL=length of buffer > 0
|HL'=void *buffer + num chars written, HL=num chars successfully written, carry set if error
|Read a single char from the stream.
|A=HL=char, if error: carry set and HL=0 for general error or HL=-1 for eof
|Read multiple characters from the stream into a buffer.
|DE'=void *buffer, BC'=HL=length of buffer > 0
|DE'=void *buffer + num chars read, BC=num chars successfully read, carry set if error with HL=0 general or HL=-1 eof
|Read chars from the stream until stopped by the state machine.
|HL'=bool (*qualify)(char c), HL=length of buffer >= 0
|BC=num chars read from stream, HL=next unconsumed char, carry set if error with HL=0 general or HL=-1 eof
|EXX set is owned by the qualify function
|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.
|Move file pointer to a new position.
|C'=C=STDIO_SEEK_SET (0);STDIO_SEEK_CUR (1);STDIO_SEEK_END (2), DEHL=signed 32-bit offset
|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
|Program sends an ioctl() to driver.
|DE=ioctl request, BC=first parameter, HL=va_arg* (must mind L→R sccz80 or R→L sdcc order on stack)
|carry reset if ok with HL=return value!=-1, carry set if error
|EXX set must not be altered
|Flush any buffers (terminals clear input buffers too).
|carry set on error
|The file is being closed.
|carry set on error (likely ignored)
|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 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.
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.
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.
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.
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:
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.
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.
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.
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:
The C code imports one name by declaring it "extern":
The asm code exports one global name:
The asm code imports two global names:
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.
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.
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.
Both C compilers support parameter passing using three different linkages:
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:
#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
#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
; 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.
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".
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.
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.

subset: commoncrawl