* Writing programs for CP/M and ZCN            -*- outline -*-
...by Russell Marks.
* (Lack of) Copyright
This document is public domain. You can do whatever you want with it.
* Introduction
** What this file is
This is a guide to writing programs in Z80 machine code for CP/M and
ZCN. It's intended for Z80 programmers who are new to CP/M, and the
ZCN section is intended for those new to ZCN. It may also be useful as
a reference for those already familiar with CP/M and/or ZCN.
It covers, in a reasonable amount of detail:
- Fundamental ideas behind CP/M's operation
- The BDOS functions
- zcnlib, my library of PD Z80 routines for ZCN and CP/M generally
** ZCN? What's that?
In case you're not reading this as part of the ZCN documentation, I
should mention that ZCN is a CP/M clone I wrote for the Amstrad NC100,
an obscure but surprisingly good Z80-based A4 `notepad'. The machine
wasn't designed to run CP/M by any means, but the hardware is *just*
flexible enough to be able to run a specially-written clone, and
that's what ZCN is.
** What this guide leaves out
This guide avoids describing in any detail:
- the BIOS.
- use of allocation and DPB info (particularly using them to find free
   disk space in CP/M 2.2).
- the internals of CP/M.
- RSXs etc.
Most programs shouldn't need these things.
If anyone wants to add sections on any of these things, that'd be
great - especially the last two, as I don't know much about them. (But
please send me your modified copy, if you can, to save any possible
duplicated effort.)
[What's that you're saying? I don't know much about the internals of
CP/M but I wrote a CP/M clone!? Well, I suppose strictly speaking ZCN
isn't a CP/M clone. It's a completely different OS that just happens
to have the same syscall interface, etc. So, I know a little of the
CP/M internals, but nothing like as much as a seasoned CP/M hacker
would. If this still doesn't make sense, consider:
- I used CP/M for the first time in May 1994.
- I started writing ZCN in late July.
- I wrote my first CP/M program in m/c in September (yes, really).
Admittedly, this *is* a rather odd way of going about things... :-)]
* CP/M
** Fundamental ideas
CP/M is a single-tasking OS - there is only one program running at a
time. This program is allowed to use all the available memory. This
memory available to programs is called the transient program area, or
TPA. This starts at 100h - all programs are run with (effectively) a
`CALL 100h' instruction. You should end your program with either a RET
or a jump to 0000h.
*** Memory organisation
Here's a memory map:
            ___________________
           |                   |
           |        BIOS       |  Don't overwrite the BIOS or BDOS!
           |___________________|
           |                   |  Together these are called the FDOS,
           |        BDOS       |  but this term is very rarely used.
     FBASE |___________________|
           |                   |
           |        CCP        |  You can overwrite this if you want
     CBASE |___________________|
           |                   |  <-- your stack grows down from here
           |                   |
           |                   |
           |        TPA        |
           |                   |
           |                   |
     0100h |___________________|  <-- program loaded in here
           |                   |
           | SYSTEM PARAMETERS |  More about this below
           |___________________|
     0000h
This is the usual layout for CP/M 2.2. CP/M 3 (known as "CP/M+") uses
a different memory bank to hold most of the BDOS and BIOS, to give a
larger TPA. To your program though, it'll look much the same.
The BDOS is what you'll call to do most things - output text, get
input, read/write files, etc. The BIOS provides some very low-level
services which the BDOS uses. The CCP is the `shell' - basically, it
reads in the command line and loads/executes the relevant command.
Note that if you overwrite the CCP, you MUST jump to 0000h to exit the
program rather than using a RET. This causes a `warm boot' which
reloads the CCP on those systems which require it.
FBASE is the address at 0006h. CBASE is FBASE-0800H.
NB: You should assume that *all* systems have a separate CCP when
deciding whether you need to do a warm boot or not. This way, your
program will run correctly on all CP/M systems. The best policy is
this: if in doubt, do a warm boot.
(If you're wondering why some CP/M-like systems have a separate CCP
and some don't - well, that's a good question. :-))
*** The Zero Page
The `system parameters' section, or the `zero page', is the most
important from a programming point of view. The significant things in
this are:
0000h - `JP 0' does a warm boot
0005h - `CALL 5' calls the BDOS
005Ch - an FCB constructed from the 1st arg
006Ch - an FCB constructed from the 2nd arg
0080h - length of command tail in bytes
0081h - command tail
The `command tail' is the command line minus the actual command name -
so if I typed `zde foo.txt' the command tail would be ` foo.txt'. Yes,
it even includes any spaces I type after the command name - if I put
two spaces between `zde' and `foo.txt' there would be two spaces at
the start of the command tail.
The command tail is always forced to be all-caps by the CCP, so watch
out for that.
FCBs are explained next:
*** File Control Blocks (FCBs)
FCBs are the closest thing CP/M has to file handles. They contain the
filename, current position in the file, and other such information.
They're used extensively by the BDOS for file operations, so it's
important to know what they are and how they work.
(ZCN users might also want to look at `stdio.z' in zcnlib, which
provides a much nicer way to read/write files, modelled after C's
stdio. It's described later on.)
The usual 33-byte FCB, used for reading files sequentially, is:
Offset      Name        Length      Description
------      ----        ------      -----------
0           DR          1      Drive the file is on
1           F1-F8       8      Filename - the part before the `.'
9           T1-T3       3      Extension (or `filetype') - the part after
12          EX          1      Current extent
13          S1-S2       2      For internal system use
15          RC          1      System use; number of records in extent EX
16          D0-DN       16      For internal system use
32          CR          1      Current record in extent EX
For random-access to be possible, you need a 36-byte FCB which
includes these:
33    R0-R2      3      Random-access record number (little-endian)
The `name' column contains the abbreviated names for each field.
DR is 0 for `current drive', 1 for A:, 2 for B:, etc.
F1-F8 and T1-T3 constitute the filename. To give an example; the
filename `wibble.com' would have F1-F8 as "WIBBLE__" and T1-T3 as
"COM", where the two underscores represent spaces.
[The FCBs provided by the system at 005Ch and 006Ch have DR and
F1-F8/T1-T3 already filled in with the file details in the 1st and 2nd
args in the command tail. The one at 005Ch can be used as is, but if
you want to use the one at 006Ch, it must be copied somewhere else
before using either.]
EX should be in the range 0-31 inclusive. It should be set to zero
before any file operations. It represents which 16k section of the
file is currently being addressed. (0 is the 1st 16k, 1 the 2nd, etc.)
CR should be in the range 0-127 inclusive. It should also be set to
zero before any file operations. It points to the current record in
the extent EX. A record is always 128 bytes long - this is the only
unit you can read and write in CP/M.
The BDOS sequential read/write functions update CR and EX to point to
the next record automatically, so once you've zeroed them out and
opened/created the file you can forget about them. The maximum file
size you can read/write with sequential file operations is 512k.
R0/R1/R2 take over the role of EX and CR when you use random-access on
a file. The usual way to use these is to zero R0/R1/R2 before any file
operations and then just use R0/R1 as a 16-bit record address, with R0
the least significant. Note that the BDOS random-access functions do
NOT update R0/R1/R2 to point to the next record - if you want this,
you must do it yourself. The maximum file size you can read/write with
random-access file operations using R0/R1 is 8192k (8Mb).
*** The `DMA'
The DMA is a 128-byte buffer which read/write operations use to read
to or write from. The DMA is also used by some other BDOS functions
such as `search for first'. Its address is initially 0080h, but this
can be changed with a BDOS call.
Note that 0080h clashes with the command tail, so you must either read
that or relocate the DMA before any operations which modify the DMA.
*** User numbers
[This section is adapted from the `zselx' README. It's here for those
not sufficiently familiar with CP/M to know what user numbers are.]
The term `user' in this file means `user area' or `user number'. If
you don't know what that means, check your CP/M manual. They're a
little bit like directories on other OS's. For those people without a
CP/M manual, here's the definition from the CP/M 2.2 manual:
"user number: Number assigned to files in the disk directory so that
different users need only deal with their own files and have their own
directories, even though they are all working from the same disk. In
CP/M, files can be divided into 16 user groups."
On a single-user system, then, they can be used rather like
directories. They're numbered from 0 to 15, with 0 the default user.
(You can use the `user n' command to switch to user n.)
** BDOS functions
You call the BDOS with `CALL 5'. Before doing this however, you need
to specify the BDOS function number, args, etc. in the relevant
registers. The list of BDOS functions below should tell you what you
need to know. Note that all of AF/BC/DE/HL are liable to be corrupted
on exit, except where stated otherwise. (A register which `corrupts'
is one modified by the function which does not have its previous value
restored.) IX/IY, the high bit of R, and the alternate register set
(AF', BC', DE', and HL') will always remain intact, as CP/M is written
in 8080 which didn't have these, and even Z80 CP/M or BDOS clones
conform to these requirements.
There are several different BDOS functions, described below. The
descriptions are written as though instructing the BDOS what to do
when the function is called. Sorry if this looks a little odd.
Only CP/M 2.2 functions are described, except for functions 3/4/7/8
which have different effects on CP/M 2.2 and 3. (Another exception is
function 46, which I considered useful enough to include for CP/M 3
and ZCN users.)
Note that those functions which support wildcards only support the `?'
wildcard, not the `*' one. The FCBs provided by the system have any
`*'s converted - this is the only place in CP/M where the `*' wildcard
is actually recognised!
(NB: ZCN allows `*' in wildcards everywhere, but it's probably unique
in this respect, and you shouldn't ever depend on this behaviour
unless your program is ZCN-specific.)
0 - System Reset
entry: C=0
exit:  none (doesn't return)
Warm boot. Same effect as `JP 0'.
1 - Console Input
entry: C=1
exit:  A=char pressed
Wait until a key is pressed, echo it, and return the ASCII value in A.
2 - Console Output
entry: C=2, E=char to output
exit:  none
Output char in E.
3 - Reader Input
entry: C=3
exit:  A=char read
Read a char from the reader device. Wait until a char is ready before
returning. On CP/M 3, read from AUX:. On ZCN, read from the serial
port.
4 - Punch Output
entry: C=4, E=char to output
exit:  none
Send char in E to punch device. On CP/M 3, send to AUX:. On ZCN, send
to the serial port.
5 - List Output
entry: C=5, E=char to output
exit:  none
Print char in E.
6 - Direct Console I/O
entry: C=6, E=FFh (for input) or char to output
exit:  if E was FFh; A=0 if no char ready, else A=char input
If E isn't FFh, output char in E to console. Otherwise, return A=0 if
no char is waiting to be read, or A=char input. This function is the
only way to get input without echo via the BDOS.
The CP/M 2.2 manual says "Function 6 must not be used in conjunction
with other console I/O functions". I don't know how true this is in
general. It's certainly not the case on ZCN.
[Functions 7 and 8 have different meanings in CP/M 2.2 and 3.]
7 - CP/M 2.2: Get I/O Byte; CP/M 3 and ZCN: AUX:/serial input status
entry: C=7
CP/M 2.2: Return A=IOBYTE.
CP/M 3 and ZCN: Return A=0 if no chars waiting, else A=FFh.
8 - CP/M 2.2: Set I/O Byte; CP/M 3 and ZCN: AUX:/serial output status
entry: C=8
CP/M 2.2: Set IOBYTE=E.
CP/M 3 and ZCN: Return A=0 if can't write now, else A=FFh.
9 - Print String
entry: C=9, DE=string address
exit:  none
Output string at DE to console. The string is terminated by a `$'
character. There is no way of outputting a literal `$' character with
this function.
10 - Read Console Buffer
entry: C=10, DE=buffer address
exit:  none
Read a string from console into buffer at DE. The buffer has this
format:
      DE+0 - entry: max number of chars to allow at DE+2
      DE+1 - exit:  number of chars following
      DE+2 - exit:  string starts here
The string is not terminated by any character, and the CR typed at the
console to terminate input is omitted. The string is output as it is
typed in, *including the CR*.
11 - Get Console Status
entry: C=11
exit:  A=0 if no char ready, else A=FFh
Return A=0 if no char ready, else return A=FFh.
12 - Return Version Number
entry: C=12
exit:  HL=version
Return H=0 (for CP/M, as opposed to MP/M where it returns H=1) and L
equals 0 for CP/M 1.x, else the version number in BCD. For example,
CP/M 2.2 returns L=22h.
To find a ZCN version number is a little more complicated, as ZCN
pretends to be CP/M 2.2 as far as this function is concerned. See the
`ZCN' section below for details.
13 - Reset Disk System
entry: C=13
exit:  none
Set all disks to read/write, make drive A: current, set DMA address to
0080h. You *must* do this after a disk is changed (this can only apply
to removeable media like floppies, of course) before accessing it, or
corruption can result.
(Corruption is unlikely, as most drives can detect disk changes, which
CP/M then notices and makes the disk read-only which this call resets;
however, it *is* a possibility on some systems.)
This call is unnecessary on ZCN, but you must use it to have the
slightest chance of your program working on other CP/M systems.
14 - Select Disk
entry: C=14, E=drive
exit:  none
Set current drive to value in E, where E=0 for A:, 1 for B:, etc.
15 - Open File
entry: C=15, DE=FCB address
exit:  A=directory code
Open existing file for reading. Return A in range 0 to 3 inclusive if
it worked, else A=FFh, usually meaning `file not found'.
16 - Close File
entry: C=16, DE=FCB address
exit:  A=directory code
Close file. (You don't need to do this if you only read from it.)
Return A in range 0 to 3 inclusive if it worked, else A=FFh.
The CP/M 2.2 manual says the file "need not be closed" if it was only
read from. Not closing such files saves doing an unnecessary write to
the directory area, so it's common practice. However, it is *vital* to
correctly close files you wrote to. (ZCN is an unusual case - its file
I/O is stateless, and you never need to close files. It's a Bad Thing
to depend on this, though.)
17 - Search For First
entry: C=17, DE=FCB address
exit:  A=directory code
Search for the first file matching the drive/name in the FCB, which
may contain a wildcard (and usually will). Return A in range 0 to 3
inclusive if a file matched, else A=FFh. If a file matched, the
matching directory entry is put at dma_address+32*A. The format of a
directory entry is difficult to describe without having to describe
the entire CP/M disk format - suffice it to say that the filename is
at offsets 1 to 11 inclusive, the same as an FCB, and that the top bit
of each may be set to indicate a file attribute (see the `set file
attributes' description for what they mean).
If this call succeeds, you should then call the `search for next'
function to read any other matching filenames.
The wildcards supported by this call are slightly more flexible than
in the rest of CP/M, and deserve special mention:
As well as allowing `?' in the filename to match any possible char at
that position, this function allows the `dr' field to be a `?', which
matches (confusingly) any user number. In that case, the byte at
offset 0 in the dir entry is the user number; if `dr' isn't `?', then
the byte contains the drive number, again the same as an FCB.
The other wildcard extension allowed by this function is that if `ex'
contains a `?', all extents of the file are considered to match,
rather than just the 0th. This can be used to work out the size of
each file as you go through the files, but for now at least, that's
outside the scope of this document.
18 - Search For Next
entry: C=18
exit:  A=directory code
Find the next file matching the FCB passed to `search for first'.
Return A in range 0 to 3 inclusive if a file matched, else A=FFh. This
function also puts a dir. entry at dma_address+32*A if successful.
The most recent BDOS call before calling this function *must* have
been either `search for first' or a previous `search for next'.
19 - Delete File
entry: C=19, DE=FCB address
exit:  A=directory code
Delete any files matching the FCB (you can use wildcards). Return A in
range 0 to 3 inclusive if it worked, else A=FFh.
20 - Read Sequential
entry: C=20, DE=FCB address
exit:  A=directory code
Read a record from the file and advance cr (and ex if needed). Return
A=0 if it worked, else A>0.
21 - Write Sequential
entry: C=21, DE=FCB address
exit:  A=directory code
Write a record to the file and advance cr (and ex if needed). Return
A=0 if it worked, else A>0. Failure means that either the disk is
full, or the directory is (i.e. no more dir. entries are left).
22 - Make File
entry: C=22, DE=FCB address
exit:  A=directory code
Create new file for writing to. Return A in range 0 to 3 inclusive if
it worked, else A=FFh. Failure means that no more dir. entries are
left.
Be warned that the results of creating a file with the same name as a
file which already exists are undefined! (This is techspeak for "you
don't *want* to know what the results are, just don't do it...".) To
create a file deleting any existing file of the same name first,
simply attempt to delete the file you want to open before creating the
new file. It doesn't matter whether the deletion worked or not - if it
worked, the existing file was deleted, if it didn't, there wasn't one
to delete anyway!
(Under ZCN this is what actually happens when you call the `make file'
function itself; but this is mind-bogglingly non-standard and you
should not depend on this behaviour, at all, ever. Really. I mean it.)
You don't have to subsequently open the file created with this
function before writing to it; it's automatically opened.
23 - Rename File
entry: C=23, DE=FCB address
exit:  A=directory code
Rename the file specified in the first 16 bytes of the FCB to the name
specified in the second 16 bytes of the FCB. (The 2nd 16 bytes should
be in the format of bytes 0..15 of a normal FCB.) Return A in range 0
to 3 inclusive if it worked, else A=FFh.
24 - Return Login Vector
entry: C=24
exit:  HL=login vector
Return login vector in hl. Bit 0 of L corresponds to A: and bit 7 of
H to P:.
The login vector is a bitmap indicating which drives have been
accessed and have valid buffer info etc. - if a given drive has/does,
the relevant bit will be 1, else 0. For the drive to count as having
been accessed you need not have read anything from it, but simply have
to have selected the drive since the last warm boot or reset.
It then follows that one way to get a login vector which shows which
drives exist on the system is to select all drives from A: to P: in
succession, then call this function. The returned bitmap should
indicate which drives are valid. (Note: I haven't actually *tested*
this, but it seems reasonable.)
25 - Return Current Disk
entry: C=25
exit:  A=current disk number
Return current default disk in A, with 0=A:, 1=B:, etc.
26 - Set DMA Address
entry: C=26, DE=128-byte area of memory to use as DMA buffer
exit:  none
Set address of DMA buffer (used mainly for read/write ops) to DE.
27 - Get Addr (Alloc)
entry: C=27
exit:  HL=alloc addr
Return address of allocation `vector' in hl.
(What this means is complicated, so I'll quote the CP/M 2.2 manual:)
"An allocation vector is maintained in main memory for each on-line
disk drive. Various system programs use the information provided by
the allocation vector to determine the amount of remaining storage...
Function 27 returns the base address of the allocation vector for the
currently selected disk drive. However, the allocation information
might be invalid if the selected disk has been marked Read-Only."
It further notes that the function is "not normally used by
application programs". If it *is* used, it tends to be used to work
out the free disk space. (This is distinctly unpleasant, and how it's
done will not be described here.) On CP/M 3 and ZCN, function 46 is a
better way of doing this.
I don't think this function returns meaningful results with CP/M 3, or
at least not with certain versions of it. It certainly doesn't return
anything meaningful under ZCN, which is very different to CP/M
internally and doesn't use (or need) the allocation stuff.
28 - Write Protect Disk
entry: C=28
exit:  none
Disallow further write operations on current disk until warm boot or a
call to `reset disk system'.
If you use this function, do not assume that it will definitely work -
there are some CP/M-like systems on which it won't. It's probably best
to not use it at all.
29 - Get R/O Vector
entry: C=29
exit:  HL=R/O bitmap
Return R/O bitmap in HL. This is in the same format as that returned
by `return login vector', except it indicates which drives are
read-only.
30 - Set File Attributes
entry: C=30, DE=FCB address
exit:  A=directory code
Set the file's attributes based on the top bits of the filename. The
top bits of T1 and T2 are used respectively to set `read-only' and
`system' status for the file. The other top bits of the filename may
be used too - those for T3 and F5-F8 are reserved, but the F1-F4 bits
may be used by programs for whatever purpose they want. (However, T3's
top bit is used as the `archive' attribute on CP/M 3.)
ZCN does not support file attributes (mainly due to the problem with
0066h), and this call will have no effect other than claiming to have
succeeded.
31 - Get Addr (Disk Parms)
entry: C=31
exit:  HL=address of DPB
Return the address of the BIOS disk parameter block in hl. Again, ZCN
will not return meaningful results. What the DPB is will not be
described here. The 2.2 manual says "Normally, application programs
will not require this facility".
32 - Set/Get User Code
entry: C=32, E=FFh (for `get') or user number to make current
exit:  if E=FFh, A=current user number; else none
If E=FFh, return current user number in A, else set current user
number to E.
33 - Read Random
entry: C=33, DE=FCB address
exit:  A=error code
Read record pointed to by r0/r1/r2. Return A=0 if it worked, else A>0.
Unlike the sequential function, this does not advance the file pointer
(r0/r1/r2 in this case).
Note that this function only really uses r0/r1 - r2 is largely ignored
but must be zero.
This function also converts the current r0/r1 value to a sequential
file pointer and puts this in cr/ex, but I would *STRONGLY* advise not
relying on this, and not mixing random and sequential operations on
the same file. For one thing, sequential ops only support far smaller
files (512k rather than 8Mb), so `interesting' corruption could
result for large files.
Possible error codes returned in A when it's non-zero are:
1      reading unwritten data
3     cannot close current extent
4     seek to unwritten extent
6     seek past physical end of disk
34 - Write Random
entry: C=34, DE=FCB address
exit:  A=error code
Write record pointed to by r0/r1/r2. Return A=0 if it worked, else
A>0. Unlike the sequential function, this does not advance the file
pointer (r0/r1/r2 in this case).
Note that this function only really uses r0/r1 - r2 is largely ignored
but must be zero.
This function also converts the current r0/r1 value to a sequential
file pointer and puts this in cr/ex, but I would *STRONGLY* advise not
relying on this, and not mixing random and sequential operations on
the same file. For one thing, sequential ops only support far smaller
files (512k rather than 8Mb), so `interesting' corruption could
result for large files.
Possible error codes returned in A when it's non-zero are:
1      reading unwritten data
3     cannot close current extent
4     seek to unwritten extent
5     file cannot be extended
6     seek past physical end of disk
35 - Compute File Size
entry: C=35, DE=FCB address
exit:  none (but r0/r1/r2 altered)
Return size of file in r0/r1/r2. The size is in records, and r0 is the
least significant byte.
Note that as well as being useful for directory listing programs etc.,
this function provides a nice easy way to append to an existing file
using `write random' after r0/r1/r2 are set.
On some CP/M systems, the file must be opened before calling this
function. However, no mention of this is made in the 2.2 manual, and
it only happens on one system I know of - a CP/M emulator - so I
reckon it's just a bug in that emulator.
36 - Set Random Record
entry: C=36, DE=FCB address
exit:  none (but r0/r1/r2 altered)
Set random-access position in r0/r1/r2 from current sequential file
position in cr/ex.
37 - Reset Drive
entry: C=37, DE=drive bitmap
exit:  A=0 (the 2.2 manual says this is "to maintain compatibility with MP/M")
Reset drives flagged in bitmap. The bitmap is in the same format as
that returned by `return login vector', except it indicates which
drives to reset.
[Functions 38 and 39 are undefined under CP/M 2.2.]
40 - Write Random with Zero Fill
entry: C=40, DE=FCB address
exit:  A=error code
Like `write random', but fills unwritten areas which have been
allocated to the file with zeroes.
On ZCN, this is identical to `write random' - that is, it doesn't
actually do the zero fill.
[Functions >=41 are undefined under CP/M 2.2.]
46 - Get Free Disk Space (CP/M 3 and ZCN only)
entry: E=drive number (0=A:, 1=B:, etc.)
exit:  none (but dma_address+0 to dma_address+2 = number of free records)
Return free disk space in records, in 3-byte number at DMA address.
The byte at offset 0 in the DMA is the least significant.
*** MP/M? What's that?
MP/M is mentioned a couple of times above, so I thought it reasonable
to give a quick description of it. As I understand it (never having
seen an MP/M system) MP/M was a multi-tasking multi-user version of
CP/M. I'm not actually certain if it multi-tasked; it may have instead
used multiple processors. But anyway, that's what it was.
** Processor issues - writing 8080-compatible code in Z80
CP/M is designed to run on an Intel 8080 CPU. Now, Z80s can run 8080
code, so that's not a problem. But every time you write a CP/M program
in Z80, you risk it not running on 8080s, 8085s, and even V20/V30s
(which usually run as 8086-compatibles but can also run 8080 code).
Since all of these can run CP/M, you may want to write a program in
Z80 but still let it run on 8080s etc. This is perfectly possible, as
the Z80 instruction set is simply an extension of the 8080 one. You
have to give up quite a lot of useful stuff though.
If you really do want your code to run on an 8080, you have to avoid
using the alternate register set and IX/IY/I/R (which don't exist on
the 8080), and avoid all these instructions:
- ldir/lddr and all similar block ops
- djnz and all other relative jumps
- ld rr,(nn) and ld (nn),rr for rr=bc,de,sp (hl is ok)
- ld a,(rr) and ld (rr),a for rr=bc,de (hl is ok)
- neg
- all 16-bit adc/sbc ops (yep, no 16-bit subtraction on an 8080)
- all bit-shift ops except rla/rra/rlca/rrca
- all bit/set/res ops
To do 16-bit subtraction, you could two's complement the number you
want to subtract (invert the bits and add one) then add it. This is
pretty painful and slow, but it works. One problem with it is that it
doesn't give sbc-like flag results. Doing it with 8-bit subtractions
fixes that, and is probably quicker anyway. To give an example of the
latter, this code should be exactly equivalent to `sbc hl,de' (apart
from needing to use the stack :-)) and work on an 8080:
push de
push af
ld a,l
sbc a,e
ld l,a
ld a,h
sbc a,d
ld h,a
pop de
ld a,d
pop de
I haven't tested it though, so caveat emptor and all that. You can
omit the weird push/pop stuff if you don't care about A getting
clobbered.
There are a few more `missing' instructions which shouldn't matter for
generic CP/M code. But for completeness' sake, I'll list those too:
- reti/retn
- in and out r,(c) for all r
- im0/im1/im2
Looking at it another way, here's the 8080 instruction set using Z80
mnemonics:
00      nop
01      ld bc,NN
02     
03      inc bc
04      inc b
05      dec b
06      ld b,N
07      rlca
08     
09      add hl,bc
0A     
0B      dec bc
0C      inc c
0D      dec c
0E      ld c,N
0F      rrca
10     
11      ld de,NN
12     
13      inc de
14      inc d
15      dec d
16      ld d,N
17      rla
18     
19      add hl,de
1A     
1B      dec de
1C      inc e
1D      dec e
1E      ld e,N
1F      rra
20     
21      ld hl,NN
22      ld (NN),hl
23      inc hl
24      inc h
25      dec h
26      ld h,N
27      daa
28     
29      add hl,hl
2A      ld hl,(NN)
2B      dec hl
2C      inc l
2D      dec l
2E      ld l,N
2F      cpl
30     
31      ld sp,NN
32      ld (NN),a
33      inc sp
34      inc (hl)
35      dec (hl)
36      ld (hl),N
37      scf
38     
39      add hl,sp
3A      ld a,(NN)
3B      dec sp
3C      inc a
3D      dec a
3E      ld a,N
3F      ccf
40      ld b,b
41      ld b,c
42      ld b,d
43      ld b,e
44      ld b,h
45      ld b,l
46      ld b,(hl)
47      ld b,a
48      ld c,b
49      ld c,c
4A      ld c,d
4B      ld c,e
4C      ld c,h
4D      ld c,l
4E      ld c,(hl)
4F      ld c,a
50      ld d,b
51      ld d,c
52      ld d,d
53      ld d,e
54      ld d,h
55      ld d,l
56      ld d,(hl)
57      ld d,a
58      ld e,b
59      ld e,c
5A      ld e,d
5B      ld e,e
5C      ld e,h
5D      ld e,l
5E      ld e,(hl)
5F      ld e,a
60      ld h,b
61      ld h,c
62      ld h,d
63      ld h,e
64      ld h,h
65      ld h,l
66      ld h,(hl)
67      ld h,a
68      ld l,b
69      ld l,c
6A      ld l,d
6B      ld l,e
6C      ld l,h
6D      ld l,l
6E      ld l,(hl)
6F      ld l,a
70      ld (hl),b
71      ld (hl),c
72      ld (hl),d
73      ld (hl),e
74      ld (hl),h
75      ld (hl),l
76      halt
77      ld (hl),a
78      ld a,b
79      ld a,c
7A      ld a,d
7B      ld a,e
7C      ld a,h
7D      ld a,l
7E      ld a,(hl)
7F      ld a,a
80      add a,b
81      add a,c
82      add a,d
83      add a,e
84      add a,h
85      add a,l
86      add a,(hl)
87      add a,a
88      adc a,b
89      adc a,c
8A      adc a,d
8B      adc a,e
8C      adc a,h
8D      adc a,l
8E      adc a,(hl)
8F      adc a,a
90      sub b
91      sub c
92      sub d
93      sub e
94      sub h
95      sub l
96      sub (hl)
97      sub a
98      sbc a,b
99      sbc a,c
9A      sbc a,d
9B      sbc a,e
9C      sbc a,h
9D      sbc a,l
9E      sbc a,(hl)
9F      sbc a,a
A0      and b
A1      and c
A2      and d
A3      and e
A4      and h
A5      and l
A6      and (hl)
A7      and a
A8      xor b
A9      xor c
AA      xor d
AB      xor e
AC      xor h
AD      xor l
AE      xor (hl)
AF      xor a
B0      or b
B1      or c
B2      or d
B3      or e
B4      or h
B5      or l
B6      or (hl)
B7      or a
B8      cp b
B9      cp c
BA      cp d
BB      cp e
BC      cp h
BD      cp l
BE      cp (hl)
BF      cp a
C0      ret nz
C1      pop bc
C2      jp nz,NN
C3      jp NN
C4      call nz,NN
C5      push bc
C6      add a,N
C7      rst 0
C8      ret z
C9      ret
CA      jp z,NN
CB
CC      call z,NN
CD      call NN
CE      adc a,N
CF      rst 8
D0      ret nc
D1      pop de
D2      jp nc,NN
D3      out (N),a
D4      call nc,NN
D5      push de
D6      sub N
D7      rst 16
D8      ret c
D9     
DA      jp c,NN
DB      in a,(N)
DC      call c,NN
DD
DE      sbc a,N
DF      rst 24
E0      ret po
E1      pop hl
E2      jp po,NN
E3      ex (sp),hl
E4      call po,NN
E5      push hl
E6      and N
E7      rst 32
E8      ret pe
E9      jp (hl)
EA      jp pe,NN
EB      ex de,hl
EC      call pe,NN
ED
EE      xor N
EF      rst 40
F0      ret p
F1      pop af
F2      jp p,NN
F3      di
F4      call p,NN
F5      push af
F6      or N
F7      rst 48
F8      ret m
F9      ld sp,hl
FA      jp m,NN
FB      ei
FC      call m,NN
FD
FE      cp N
FF      rst 56
As for whether you should go to the trouble of making your code
8080-friendly - well, it depends. There haven't been many popular
8080-based CP/M boxes in the UK, so my biased view is that it's not
usually worth the hassle. :-)
It should be possible to write a filter which converts normal Z80
assembly to 8080-compatible Z80 assembly. That would certainly be a
better way of doing things. (Though you'd still have to avoid IX/IY,
etc.) I've written such a filter in awk, called z8080, but since I
don't have an 8080-based machine to test the output on I can't be
certain how well it works. (If you're reading this as part of the ZCN
distribution, a copy is included - see `z8080.txt' for details.)
* ZCN
** What the NC100 has that generic CP/M machines don't
Essentially, it has graphics, sound, a fully readable keyboard, memory
paging hardware, and a real-time clock chip. See `zcn.txt' for details
of the first three. The memory paging is described later in this file.
The RTC's time/date can be read/written via ZCN-specific BDOS
functions, also described later on.
** Testing for ZCN
You should not use ANY NC100-specific or ZCN-specific features without
first testing that you're running under ZCN. The easiest way to do
this is to check, right at the start of the program, that the value at
0066h is F7h. If it is, you're running ZCN.
For a completely ZCN-specific program which would not work at all
under generic CP/M, it's reasonable to simply die if ZCN isn't
present, like this:
      ld a,(066h)
      cp 0f7h
      jp nz,0        (or just "ret nz" if the stack is `empty')
You may wish to be a bit less brutal about it and output an error
message, though.
** The memory paging hardware
The NC100 pages memory in 16k pages, with the 64k address space making
up 4 slots into which any part of RAM, ROM or PCMCIA card memory can
be paged. (A PCMCIA memory card is just memory, after all, even if ZCN
solely uses it as a disk (with the exception of `bigrun').)
Memory is paged by OUTing to port 10h for the 0000-3FFFh slot, 11h for
the 4000-7FFFh slot, 12h for the 8000-BFFFh slot, and 13h for the
C000-FFFFh slot. The values to OUT depend on the type of memory and
which 16k page is to be paged in. The low six bits of the value are
the 16k page number, with 0 being the first 16k, 1 the next, etc. The
top two bits should match the type of memory being used - 00b for ROM,
01b for RAM, or 10b for PCMCIA memory.
So, for example, to page the first 16k of PCMCIA memory into the
4000-7FFFh slot, I'd use:
      ld a,080h
      out (011h),a
ZCN does something similar to this to read/write files on the `disk',
though this is obviously transparent to your program.
I'd recommend not using the memory paging hardware unless you REALLY
have to. It's sooo easy to swap out an interrupt routine, the stack,
your code... and with a one-bit error you could even corrupt your
memory card! And don't forget that whatever you've paged in can
`instantly' switch back to the normal RAM if the machine is turned off
and on after your paging operation. (ZCN protects against this for
paging operations *it* does, but your program can't.)
If you really do want to use paging for some reason, be sure to only
use 4000-7FFFh. 0000-3FFFh contains the NMI routine (well, the jump to
the real routine at least), and 8000-BFFFh and C000-FFFFh between them
contain all of ZCN.
** ZCN-specific BDOS functions
There are several BDOS functions specific to ZCN. These are listed
here. Many of these were quick single-purpose hacks so I could support
something in a user program rather than having to add another internal
command, so don't be surprised if some of them seem rather strange.
128 - ZCN version number
entry: C=128
exit:  HL=version number
Return ZCN version number in hl. This is not in the same format as the
value returned by CP/M's `return version number'! H is the major
revision number, and L is the minor. So v0.1 gave hl=0001h, and a v2.3
would give hl=0203h.
129 - Set whether interrupts are `tight' or not
entry: C=129, E=1 to use tight ints or 0 for normal.
exit:  none
Set whether to use `tight' interrupts or not. The tight interrupts
mode is designed for use by games, as it's pretty hairy running the
NC100 with interrupts off. The big plus of TI mode is that it lets you
directly read the keyboard `bytemap' constructed by ZCN, allowing you
to read multiple keys pressed at once.
Historically, the TI mode gave a minimal interrupt too, which reduced
the interrupt burden and meant your code effectively ran faster. But
an optimisation made to ZCN since sped the normal interrupt mode up
massively, leaving the TI mode actually *slower* because of the
requirement to support multiple arbitrary keypresses at once. This
sounds perverse, but I'm afraid that's the way it has to be. See
`zcn.txt' for further details.
130 - Return address of keyboard bytemap
entry: none
exit:  HL=address of keyboard bytemap
Return the address of ZCN's keyboard bytemap. The contents are
effectively only readable in `tight ints' mode. See `zcn.txt' for
details.
131 - Return address of the 1/100th-second strobe byte
entry: none
exit:  HL=address of strobe byte
Return the address of ZCN's strobe byte. This alternates between 0 and
255 exactly 100 times a second. Note that waiting for this value to
change is NOT the same as using `halt', as `halt' waits for any
interrupt, so any serial interrupt would stop that earlier than
desired. However, for most games etc., `halt' is sufficient - the
strobe byte is largely a relic of a workaround for a problem since
solved.
132 - Return console in/out assignments
entry: none
exit:  H=input device, L=output device
Return console input/output devices in hl. H is the input device, L
the output device. The values are 0 for the normal console (the
built-in screen), 1 for the serial port, or (for output only) 2 for
the printer. (In the latter case output also appears on the screen.)
There is no way to set the redirections from user programs. Sorry.
133 - Get time from RTC
entry: DE=buffer address
exit:  none (but buffer filled)
Return the current time/date according to the real-time clock. The
buffer pointed to by de should be six bytes long. The returned buffer
is filled like this:
      de+0 de+1 de+2 de+3 de+4 de+5
       yy   mm   dd   hh   mm   ss
The values are in BCD. The year is specified as an offset from 1990.
(Dates before 1st Jan 1990 are not supported, nor are dates after 31st
Dec 2099.) Note that this year offset isn't *strictly* a BCD value for
years in the range 2090-2099, as the high nibble is then 10.
Bear in mind that the clock is still running when this measurement is
taken, so to be sure of getting the true time/date you should call
this twice and use the `highest' result, i.e. the later time/date of
the two. See `time.z' for an example of how to do this.
(Note: I don't think you need to do this any more, as I worked out how
to lock the time/date output from the RTC, but it's best to do it
anyway to be sure.)
134 - Set RTC time
entry: DE=buffer address
exit:  none
Set the time/date on the real-time clock. Uses the same format buffer
as `get time from RTC'.
135 - Check drive exists and is ZCN format
entry: E=drive num. (0=current, 1=A:, etc.)
exit:  c if ok, nc otherwise
Check if drive exists and is in ZCN format.
136 - Read 128 bytes from a data block
entry: DE=table address
exit:  A=0 if ok, else 255
Read a record from a data block. Return A=0 if ok, else 255. The
format of the table is as follows:
      de+0      byte      block number (lowest being 0)
      de+1      byte      128-byte record number in block (0-7)
      de+2      byte      drive (0=A:, 1=B:, etc.)
      de+3      word      address to read 128 bytes to
You cannot read the boot block or any system blocks with this
function.
137 - Write 128 bytes to a data block
entry: DE=table address
exit:  A=0 if ok, else 255
Write a record to a data block. Return A=0 if ok, else 255. The
format of the table is as given in the description of the previous
function.
You cannot write the boot block or any system blocks with this
function.
138 - Read 128 bytes from boot block and/or system blocks
entry: DE=table address
exit:  none
Read a record from boot block and/or system blocks. (In fact, it
directly reads from anywhere in the first 16k of the logical drive.)
This routine does no error checking at all - it doesn't even check if
there's a card in the slot! The format of the table is:
      de+0      byte      drive (0=A:, 1=B:, etc.)
      de+1      word      byte offset from start of drive (0-16383)
      de+3      word      address to read 128 bytes to
Using this and the other raw read routine, it is possible to write a
routine to read any block. See the source to `zdbe' for a routine
which does this.
139 - Write 128 bytes to boot block and/or system blocks
entry: DE=table address
exit:  none
Write a record to boot block and/or system blocks. (In fact, it
directly writes to anywhere in the first 16k of the logical drive.)
This routine does no error checking at all - it doesn't even check if
there's a card in the slot! The format of the table is as given in the
description of the previous function.
Using this and the other raw write routine, it is possible to write a
routine to write any block. See the source to `zdbe' for a routine
which does this.
140 - Get bytemap of used/unused data blocks
entry: D=drive num. (0=A:, etc.)
exit:  HL=address of bytemap (where 1=unused, 0=used)
Get a bytemap of data block use for drive d. Return pointer to buffer
in hl. Some other BDOS functions use the buffer which the data is
returned in, so you should either finish using it or copy it elsewhere
before calling the BDOS again.
The bytemap doesn't include the boot block or any system blocks (which
are always used, by definition) - it only covers the data blocks. How
many data blocks there are must be worked out by looking at the boot
block (the data there is readable with function 138, and is described
in `zcn.txt'). See the source to `defrag' for how to do this.
141 - Set/unset serial port up for mouse      (requires ZCN >=0.4)
entry: E=0 for normal serial operation, non-zero for mouse,
       D=log2(baudrate/150) (e.g. 3=1200, 4=2400, 5=4800, etc.)
       (NB: D need not be specified if you're `turning off' the mouse.)
exit:  A=0 if ok, or A=FFh if unsupported (i.e. if ZCN version <0.4)
Setup the serial port for use with a Microsoft-compatible serial
mouse, or return it to normal serial operation. The mouse baud rate
etc. is stored separately from the normal one, so calling this with
E=0 to `turn off' the mouse restores all the normal serial settings,
i.e. the baud rate.
You shouldn't need to use this directly - instead, you should use the
routines in zcnlib's `mouse.z', described later.
142 - Set font base address                 (requires ZCN >=1.2)
entry: DE=font base address
exit:  A=0 if ok, or A=FFh if unsupported (i.e. if ZCN version <1.2)
Sets ZCN's font base address to DE. This is where the bitmaps for the
characters printed by ZCN are held. (Though strictly speaking it's 192
bytes less, as there are no bitmaps for chars 0..31.) The displayable
chars are those in the range 32..255, with the exception of 7Fh (127)
and F7h (247). (The latter exception is to help a bit with the 0066h
problem.)
The normal font address is restored when your program exits, but you
can restore it before that (if needed) by calling this routine with
DE=EA00h.
The font is made up of N 6-byte bitmaps (ZCN itself provides 96 to
cover the ASCII character set (with the last char ignored), but up to
224 are possible), as you might expect. However, the format is
slightly unusual - for normal text output, the 4-bit-wide bitmap in
the most significant nibble must equal that in the least significant
one. (This lets ZCN display text a bit quicker.)
Interestingly though, this gives you an easy way to have double-width
graphics and the like - just treat the bitmap as 8x6, and output the
relevant char twice to get your bitmap. :-)
If you merely want to add a few graphics chars to the normal ZCN font,
just copy the ZCN font bitmaps at EAC0h down into TPA, copy your new
chars onto the end, and use that. (The ZCN support in `cpmtris' works
like this.)
143 - Set user area to 255               (requires ZCN >=1.2)
entry:      none
exit: none
It's not possible to get to user area 255 from a program, when using
the normal CP/M BDOS function for setting the user area. This function
gives you a way to do it.

** Using ZCN's zcnlib library
I wrote ZCN as a CP/M clone. That's fine as far as it goes, but CP/M
is a bit primitive. I eventually decided that I'd like a higher-level
interface to ZCN. With the limited memory available, I opted to
write a library of useful routines rather than a new system-call
interface, and zcnlib was the result.
Zcnlib is public domain, and you can do anything you want with it. See
the zcnlib README for details. It consists of various source files,
each of which covers some abstract task, e.g. graphics drawing, file
I/O, etc. The routines in each are described here. The routines in
some files require routines in other files - these dependancies, where
they exist, are noted at the start of the description of the file.
Though they're intended for use on ZCN only, most of the more generic
routines should work on any Z80-based CP/M box, and the really generic
ones (maths etc.) should work on *any* Z80-based system. (That's
excluding pathological cases like the ZX81, where you can't reasonably
use IX, IY or the alternate register set, which would render (say) the
32-bit int routines unusable.)
Before I start describing the routines, there are several things you
should be aware of:
- In entry/exit conditions, capital letters represent registers, while
lowercase ones represent flag status. For example, `C' is the C
register, while `c' is the carry flag. In descriptions, I tend to use
lowercase for everything and let context resolve ambiguities.
- Sometimes I may say that a certain flag has a certain value on exit
from a routine, but then go on to say that F (the flags register)
corrupts. What I mean in this case is that flags not specified as
having a meaningful value corrupt. In all other cases, if I say a
register corrupts I really mean it. :-)
- All registers which don't return values, and which don't corrupt,
aren't touched by the routine and are preserved. Sometimes I mention
this explicitly, to make it clear which registers remain intact.
- Following on from the above, most routines preserve IY and the
alternate register set. Here is a (hopefully exhaustive) list of
exceptions:
in graph.z - ftri corrupts IY.
in int32.z - mul32/smul32/div32/sdiv32 and the number I/O routines
             corrupt IY and all alternates except AF'.
Quite a few routines also preserve IX. Here's a list of exceptions to
that:
in args.z - makeargv.
in graph.z - draw8x8, save16x8, rstr8x8.
in graph2.z - ftri.
in int32.z - all routines.
in mouse.z - mouseon, mouseoff, mstat.
in qsort.z - qsort uses IX as addr of compare routine.
in sqrt.z - intsqrt.
in stdio.z - all routines.
- An `asciiz' string (these are used by some routines) is zero or more
ascii characters followed by a zero byte - that is, a NUL, a byte with
all bits zero.
Covering the source files in alphabetical order:
*** args.z
A clone of C's argc/argv. Requires string and ctype. In case you're
not familiar with C, here's an example. Take the command-line:
      foo bar baz
(`foo' is the command, `bar' and `baz' are args.)
Here, argc is 3, and there are three elements in the argv array -
argv[0] is "foo", argv[1] is "bar", argv[2] is "baz". (In CP/M, there
is no way of knowing the way the command was run, so argv[0] will
really be "", the null string. Also, the cmdline is lowercased from
the all-caps copy the system provides.)
To use this argc/argv method of reading the command-line (well,
command tail) you call `makeargv' as early as possible in your
program. Then you can read the byte at `argc' (you can read this as a
word if that's more convenient) and the `argv' array - the routine
`getargv' returns hl=argv[a]. As you might expect, the strings in the
argv array are asciiz.
makeargv
entry: none (but cmdline at 80h-ffh must be intact)
exit:  AF/BC/DE/HL/IX corrupt, cmdline corrupt
Assign correct values to argc/argv based on command tail at 0080h.
After calling this routine, you can do whatever you want with the data
at 0080-00FFh, argc/argv aren't affected.
There is no way of `quoting' arguments, i.e. to put one or more spaces
in an argument. Spaces always separate arguments, no matter what.
getargv
entry: A=argv element to look up
exit:  HL=addr of argv[A], all other registers preserved
Return the address of the specified element of the argv array.
*** conio.z
Console I/O routines, many similar to those in DOS C compilers (or
rather, the libraries that come with them).
putchar
entry: A=char to output
exit:  F corrupt
Output char in A. If A=10 (LF), this is converted to CR/LF.
putbyte
entry: A=char to output
exit:  F corrupt
As for `putchar', but doesn't translate LF into CR/LF.
getchar (and getch)
entry: none
exit:  A=char input, F corrupt
Input char into A. It waits until a key is pressed, and does not echo
it. (Also, it doesn't translate the char at all, which is unexpected
when compared with putchar, but is probably what you want.)
kbhit
entry: none
exit:  c if key pressed, nc if not; AF/BC/DE/HL corrupt
Report if any key is waiting to be read, but do not read it. This is
similar to the common DOS C function.
*** ctype.z
Clones of C's `ctype' routines for testing if a char is upper/lower
case, etc.
These routines work in the sanest possible way - i.e. the `toupper'
only tries to uppercase lowercase chars. If you're thinking, "well
what on earth *else* would it do?" you haven't seen some of the more
`interesting' C library implementations. :-/
Note that these only work for ascii. If you're expecting `toupper' to
work for umlauts, say... dream on. :-)
All routines work on the char in A. The conversion routines (`to...')
return the modified char in A, too. The testing routines (`is...')
return nc if the test failed, else c. All registers not used for
returning results are preserved by all routines.
Having made that clear, there's no point doing the usual entry/exit
stuff for each routine - I'll simply list what they test/do.
isalpha - is char a letter?
isupper - is char an uppercase letter?
islower - is char a lowercase letter?
isdigit - is char a (decimal) digit?
isxdigit - is char a hex digit? (is that a hexit? :-))
isalnum - is char alphanumeric?
isspace - is char whitespace?
(For the purposes of `isspace', whitespace is defined as any of space,
FF, CR, LF, HT (tab) or even VT. This is the way it's defined in C, so
I did the same. If you're wondering (like I did) why VT was included,
check out the values of the chars on an ascii chart...)
isprint - is char printable? (i.e. in range 32<=A<=126)
isgraph - is char printable and not space?
iscntrl - is char a control char? (i.e. in range A<32)
isascii - is char an ascii char? (i.e. in range A<128)
toupper - convert char to uppercase
tolower - convert char to lowercase
toascii - strip top bit of char to make it 7-bit ascii
ispunct - is char punctuation? (i.e. ascii, but not space or alphanumeric)
*** getopt.z
A clone of Unix's getopt routine for parsing command-line options.
Requires args (and thus string and ctype). Note that *this is not
re-entrant*, so you may want to put a `ret' (C9h) at 0100h when
starting up, to prevent people using `!!' (or a zero-length .COM file)
to re-run the program.
For the uninitiated, getopt parses Unix-style cmdline options, which
work like this:
- an option-setting argument starts with the `-' char. Then each char
  after that in the arg sets an option, unless the option takes an
  arg:
- options can take an arg, in which case the next arg is absorbed by
  that, and option processing continues on the next arg after. (The
  option's arg is put in the string at `optarg' on return from
  getopt.)
- the 1st arg which *doesn't* start with `-', and which isn't an
  option's arg, terminates option processing. (Traditionally, these
  remaining args tend to be filenames, but they can be whatever you
  want.) Most Unix getopts allow a `--' arg to terminate option
  processing too; this is not supported by this implementation (not
  yet, anyway).
This probably makes it sound like you call getopt once, and it sorts
everything out, right? Well, it doesn't quite work like that. You call
getopt once for each option on the cmdline, until they run out and
getopt returns "-1" (really 255 in this Z80 version). getopt also
signals bad options, etc., similarly: a `?' is returned when an
unknown option is found (the bad option letter is at `optopt'), and a
`:' is returned when an option's arg is missing.
When getopt returns 255, the index into argv of the first non-option
arg (if there is one) is in the byte at optind. If this equals argc,
there are no options left; if it's argc-1, there's one option left;
and so on.
When calling getopt, you pass an asciiz string describing the options.
For options which don't take args you just put the option letter in
the string; for options which do take args you put in the option
letter followed by `:'.
Here's a simple example which has `a' and `c' as simple options, and
`b' as an option which takes an arg. It just displays any options
given, ignores any arg to the `-b' option (!) and silently exits on
error. In addition to getopt, args, etc., it requires conio for
the putchar routine.
      org 0100h
     
      call makeargv
     
      optloop:
      ld hl,optstr
      call getopt
     
      cp 255
      ret z       ;exit at end of options
     
      call putchar      ;show option (or error) char
     
      cp ':'            ;missing option arg
      ret z
      cp '?'            ;unknown option
      ret z
      jr optloop
     
      ;option string
      optstr:     defb 'ab:c',0
So, after all that, describing what getopt does is simple... :-)
getopt
entry: HL=addr of option string
exit:  A=option (or error, etc.), F/BC/DE/HL corrupt
Parse command-line options. A clone of Unix's getopt routine.
*** graph.z
Most of the graphics routines. Note that all the line and
shape-drawing routines use the routine set by a call to `pixstyle' to
draw the pixels.
pixstyle
entry: HL=addr of pixel draw routine to use
exit:  none
Set the routine which all graphics routines use to draw a pixel (which
defaults to `pset'). The routines defined in graph.z which may be used
for this purpose are pset, preset, pxor, and versions of those
routines which don't check to see if the pixel to draw is onscreen
(which are therefore faster and more dangerous) called fastpset,
fastpres and fastpxor.
The pixel drawing routine must have these entry/exit conditions:
entry: DE=x pos, C=y pos
exit:  AF/BC/DE/HL corrupt
pos2addr
entry: DE=x pos, C=y pos
exit:  HL=addr on screen, C=mask with pixel set at pixel position,
      AF/B/DE corrupt
Convert pixel position to address/mask. This routine is primarily
intended for internal use, but feel free to use it directly.
pset, preset, pxor, fastpset, fastpres, fastpxor
entry: DE=x pos, C=y pos
exit:  AF/BC/DE/HL corrupt
Set/reset/xor the pixel at (de,c).
pfillpat
entry: DE=x pos, C=y pos
exit:  AF/BC/DE/HL corrupt
Set/reset the pixel at (de,c) according to current fill pattern (as
selected with `setfill').
setfill
entry: HL=addr of fill bitmap
exit:  none
Set current fill pattern used by `pfillpat' to the 8x8 bitmap at hl.
The format of the bitmap is 8 bytes, one for each line, with the top
line first and bit 7 leftmost. Predefined bitmaps you can use are
`patblack', `patdgrey', `patmgrey', `patlgrey', and `patwhite', which
are respectively black, dark grey, grey, light grey, and white.
`patblack' is the default pattern.
hline
entry: (DE,C) and (HL,C) = endpoints of line
exit:  AF/BC/DE/HL corrupt
Draw horizontal line from (de,c) to (hl,c). Faster than the more
general `drawline'.
vline
entry: (DE,C) and (DE,B) = endpoints of line
exit:  AF/BC/DE/HL corrupt
Draw vertical line from (de,c) to (de,b). Faster than the more general
`drawline'.
drawline
entry: (DE,C) and (HL,B) = endpoints of line
exit:  AF/BC/DE/HL corrupt
Draw a line from (de,c) to (hl,b).
rect
entry: (DE,C) and (HL,B) are co-ords of opposing corners
exit:  AF/BC/DE/HL corrupt
Draw a rectangle (outline) from (de,c) to (hl,b). Usually (de,c) will
be the top-left corner and (hl,b) the bottom-right, but they can
actually be any two diagonally-opposing corners.
frect
entry: (DE,C) and (HL,B) are co-ords of opposing corners
exit:  AF/BC/DE/HL corrupt
As `rect', but draw a filled rectangle.
clrscrn
entry: none
exit:  F/B/DE/HL corrupt
A fast clear screen routine. It's three times faster than the
`obvious' implementation using LDIR.
However, and I know this sounds bizarre, there is a chance of it
corrupting data in a small part (about 0.5%) of the serial input
buffer. Usually this won't be a problem, but I mention it here just in
case it is. You can avoid this problem completely by calling the
routine with interrupts disabled.
pget
entry: (DE,C) = pixel to get
exit:  A=FFh if `on' (black), 0 if `off' (white);
      F/BC/DE/HL corrupt
Get the current state of a pixel on the screen, and return it in A.
flood
entry: (DE,C) = seed pixel to start filling from
exit:  AF/BC/DE/HL corrupt
Fills in black all pixels inside a continuous black boundary, moving
outwards from (de,c) which must be inside the shape to fill. (This
method is called a floodfill, which gives the routine its name.)
This routine needs a *large* amount of usable stack space. I think the
worst case requires 5k.
draw8x8
entry: (DE,C) = where to draw the bitmap, IX=addr of bitmap
exit:  AF/BC/DE/HL/IX corrupt
Draw an 8x8 bitmap at (de,c). This (and other bitmap-drawing routines
here) is really only intended to support the mouse pointer drawing
required by mouse.z, but feel free to use it/them for other purposes.
The 8x8 bitmap should be in the same format as that specified in the
description of the `setfill' routine.
save16x8
entry: (DE,C) = where to save from, IX=addr of buffer to copy to
exit:  AF/BC/DE/HL/IX corrupt
Copy 8x8 and surrounding area (hence 16x8) from (DE,C) to IX. You'd
usually do this before drawing a bitmap there if you wanted to be able
to restore the original background later, e.g. if you wanted a
`sprite'.
rstr16x8
entry: (DE,C) = where to restore to, IX=addr of buffer to copy from
exit:  AF/BC/DE/HL/IX corrupt
Restore a 16x8 bitmap saved by `save16x8'.
*** graph2.z
More complicated graphics routines. Requires graph, maths and sqrt.
circle
entry: (DE,C) = centre, B=radius
exit:  AF/BC/DE/HL corrupt
Draw a circle centre (de,c) radius b. The square-root method is used
and two pixels are drawn for each pixel line, so that the circles tend
to break up at the top and bottom. For a better circle, you could try
drawing a filled circle with `fcircle' and `undraw' a filled circle
slightly smaller.
fcircle
entry: (DE,C) = centre, B=radius
exit:  AF/BC/DE/HL corrupt
As `circle', but draw a filled circle. As you might imagine, *this*
circle doesn't break up. :-)
ftri
entry: (DE,C) (HL,B) (IX,A) = co-ords of vertices
exit:  AF/BC/DE/HL/IX/IY corrupt
Draw a filled triangle with vertices (de,c), (hl,b), and (ix,a). This
is a rather complicated operation, and this routine is appropriately
slow, I'm afraid.
The triangle is only an approximation, and gets inaccurate when large.
The inaccuracy isn't major - about 1 pixel out per 480 pixels across -
but it can look strange because of the way the triangle is drawn. The
routine can sometimes `miss out' a pixel row when pxor is being used
as the pixel draw routine and one of the triangle's edges is vertical.
(In fact, it draws it twice, so the edge disappears and is `missing'.)
*** int32.z
A collection of maths routines which work on 32-bit integers. They're
not necessarily terribly good or anything, I just hacked them up so I
could write an fixed-point integer mandelbrot program (zcnbrot). But
they *do* work. Many of the routines use undocumented instructions,
which may not work under (incomplete) Z80 emulators.
Mul32/div32 work for unsigned numbers only. Smul32/sdiv32 are wrappers
around mul32/div32 which work for signed numbers, but they're slower
than mul32/div32, so only use them if you really need signed ops.
Note also that flags are almost certainly not meaningful for
mul32/div32/smul32/sdiv32; carry should be right for add32/sub32, but
don't expect any other flags to be.
As with the similar routines in maths.z, the number I/O routines (such
as atoi32 and dispdec32) only deal with unsigned numbers. It shouldn't
be too hard to get them to work with signed ones - see utils/expr.z in
ZCN for example code.
32-bit args to the routines are passed in one or both of ix/hl and
de/bc. ix and de are the most significant words of these args.
Some of these routines corrupt a LOT of registers, including IY and
the alternates in some cases, so check those `foo/bar/baz corrupt'
listings carefully!
swap32
entry: IXHL and DEBC = numbers to swap
exit:  IXHL and DEBC swapped, other registers preserved
Swap two 32-bit numbers.
iszero32
entry: IXHL = number
exit:  z if true, else nz; A corrupt
Test if a 32-bit number is zero.
inc32
entry: IXHL = number
exit:  IXHL = number+1, AF corrupt
Increment a 32-bit number.
dec32
entry: IXHL = number
exit:  IXHL = number-1, AF corrupt
Decrement a 32-bit number.
add32
entry: IXHL = num1, DEBC = num2
exit:  carry is correct, other flags corrupt, other regs preserved
Add debc to ixhl.
sub32
entry: IXHL = num1, DEBC = num2
exit:  carry is correct, other flags corrupt, other regs preserved
Subtract debc from ixhl.
mul32
entry: IXHL = unsigned num1, DEBC = unsigned num2
exit:  IXHL = num1*num2, AF/BC/DE/IY/BC'/DE'/HL' corrupt
Multiply ixhl by debc. ixhl and debc must be unsigned.
div32
entry: IXHL = unsigned num1, DEBC = unsigned num2
exit:  IXHL = num1/num2, DEBC = num1 mod num2;
      AF/IY/BC'/DE'/HL' corrupt
Divide ixhl by debc, and put remainder in debc. ixhl and debc must be
unsigned.
smul32
entry: IXHL = num1, DEBC = num2
exit:  IXHL = num1*num2, AF/BC/DE/IY/BC'/DE'/HL' corrupt
Multiply (signed) ixhl by debc.
sdiv32
entry: IXHL = num1, DEBC = num2
exit:  IXHL = num1/num2, DEBC = num1 mod num2;
      AF/IY/BC'/DE'/HL' corrupt
Divide (signed) ixhl by debc, and put remainder in debc.
abs32
entry: IXHL = num1
exit:  IXHL = abs(num1), AF/BC/DE corrupt
If num1 is negative, make it positive.
neg32
entry: IXHL = num1
exit:  IXHL = -num1, AF/BC/DE corrupt
Negate num1.
sgn32
entry: IXHL = num1
exit:  nc if num1>=0, else c; A corrupt
IXHL = num1*num2, AF/BC/DE/IY/BC'/DE'/HL' corrupt
Return nc if num1 is positive or zero, else c.
itoa32
entry: IXHL=number to convert
exit:  DE=addr of ascii number in internal buffer, `$' terminated;
      AF/BC/HL/IX/IY/BC'/DE'/HL' corrupt
Convert 32-bit number in ixhl to ascii.
itoabase32
entry: IXHL=number to convert, B=base to use
exit:  DE=addr of ascii number in internal buffer, `$' terminated;
      AF/BC/HL/IX/IY/BC'/DE'/HL' corrupt
As `itoa32', but supports any base in range 2 to 36 rather than just
decimal.
dispdec32
entry: IXHL=number to print
exit:  AF/BC/DE/HL/IX/IY/BC'/DE'/HL' corrupt
Output 32-bit number in ixhl as decimal.
atoi32
entry: HL=addr of decimal ascii number
exit:  IXHL=actual number, AF/BC/DE/IY/BC'/DE'/HL' corrupt
Convert number in ascii to a 32-bit integer, and return that in ixhl.
The number should be terminated by any non-digit.
atoibase32
entry: HL=addr of ascii number, B=base
exit:  IXHL=actual number, AF/BC/DE/IY/BC'/DE'/HL' corrupt
Convert number in ascii in given base to a 32-bit integer, and return
that in ixhl. The number should be terminated by any char which is not
a digit in the given base.
*** maths.z
Integer maths routines such as fast multiply/divide, and number I/O
and conversion routines. Note that atoi/itoa (and routines which use
them) deal with *unsigned* numbers only.
disphex
entry: HL=number to print
exit:  AF corrupt
Output the number in hl as a 4-digit hex number.
hexbyte
entry: A=number to print
exit:  AF corrupt
Output the number in a as a 2-digit hex number.
multiply
entry: HL=num1, DE=num2
exit:  HL=num1*num2, AF/BC/DE corrupt
Multiply hl by de and return in hl.
divide
entry: HL=num1, DE=num2
exit:  HL=num1/num2, DE=num1 mod num2, AF/BC corrupt
Divide num1 by num2 and return result in hl and remainder in de.
itoa
entry: DE=number to convert
exit:  DE=addr of ascii number in internal buffer, `$' terminated;
      AF/BC/HL corrupt
Convert number in de to ascii.
itoabase
entry: DE=number to convert, B=base to use
exit:  DE=addr of ascii number in internal buffer, `$' terminated;
      AF/BC/HL corrupt
As `itoa', but supports any base in range 2 to 36 rather than just
decimal.
dispdec
entry: DE=number to print
exit:  AF/BC/DE/HL corrupt
Output number in de as decimal.
atoi
entry: HL=addr of decimal ascii number
exit:  HL=actual number, AF/BC/DE corrupt
Convert number in ascii to an integer, and return that in hl. The
number should be terminated by any non-digit.
atoibase
entry: HL=addr of ascii number, B=base
exit:  HL=actual number, AF/BC/DE corrupt
Convert number in ascii in given base to an integer, and return that
in hl. The number should be terminated by any char which is not a
digit in the given base.
*** mouse.z
A driver for microsoft-mouse-compatible serial mice. Requires graph.
See `msdemo.z' for an example of how to use this.
minit
entry: none
exit:  AF/BC/DE/HL corrupt
Initialise mouse. There's currently no way of being sure if there's
actually a mouse plugged in or not... :-(
You must call this routine before using any other mouse routines.
Before calling this routine, you may want to set (mfixp) to something
other than 0 for a slower-moving mouse. An explanation: There are
2^(mfixp) sub-pixel positions, so the bigger (mfixp) gets, the less
sensitive (slower) the mouse movement gets. It must be >=0 and <=7. 0
or 1 are generally ok, and 4 is good for high detail stuff.
muninit
entry: none
exit:  AF/BC/DE/HL corrupt
Uninitialise mouse. You must call this before exiting if you called
`minit'.
mouseon
entry: none
exit:  AF/BC/DE/HL/IX corrupt
Turn on mouse pointer. The pointer is redrawn each time you call
`mstat'. Call `mouseoff' to disable the pointer before drawing
anything onscreen yourself (and, of course, `mouseon' afterwards).
mouseoff
entry: none
exit:  AF/BC/DE/HL/IX corrupt
Turn off mouse pointer.
mevents
entry: none
exit:  AF/BC/DE/HL corrupt
Handle any mouse events pending. Call this reasonably often, certainly
before calling `mstat'.
mstat
entry: none
exit:  (DE,C) = mouse pointer co-ords, A=mouse buttons (bit 1 set if
      left pressed, bit 0 set if right); F/B/HL/IX corrupt
Get mouse status. Call `mevents' before calling this.
*** qsort.z
A simple generic sort routine like C's qsort. Requires maths.
WARNING! WARNING! There's a warning going on. It's still going on.
It's still a warning. This is a warning announcement. (Uh, yeah,
thanks Hol.) There must be enough room at the end of the array for an
extra element, which is used when swapping elements in the array. This
sucks unpleasantly large, hairy rocks, but I couldn't think of a
better solution. (Well, I did think of one, but it'd have been much
slower.)
Another warning - sorting zero-length arrays is a Bad Thing. Results
are undefined... because I don't want to frighten you. :-)
By the way, the sort routine isn't really a quicksort, it's just an
exchange sort. If I remember my A-level CompSci correctly, it's not
all that much better than (gak!) bubble sort for some cases, but it's
certainly the easiest and most intuitive sort to write and understand.
And since this is assembly, that's a good thing in my book. Fewer
bugs. :-) Like the man said, "When in doubt, use brute force".
Technical details for the interested who don't know how an exchange
sort is better than a bubble sort: While the number of comparisons is
the same, it almost always massively reduces the number of exchange
ops. So `exchange sort' is a rather stupid name for it really. :-) As
for how the sort works, it's like this:
      for n=0 to nmemb-1
        find smallest element (from elements n..nmemb-1)
        exchange that with the nth element
      next
(It should really be `for n=0 to nmemb-2', but that causes problems
with 1-element arrays, of course!)
If you have, say, a 100-element array with the smallest element at the
end, exchange sort will do 98 fewer exchanges than bubble sort to get
it to the right place! With each exchange involving three block
copies, this is a major saving. And for the kind of relatively small
arrays you'll be sorting in the restricted Z80 address space, exchange
sort even compares reasonably well with the famous `fast' sorting
algorithms, quicksort and shell-sort (which only get much faster than
other sorting methods when you have a large array to sort).
But enough of this rubbish, and on with the description:
qsort
entry: HL=array base, BC=no. entries in array, DE=size of an element,
      IX=addr of element compare routine
exit:  AF/BC/DE/HL corrupt
Sorts an array. You must provide a routine to compare elements, which
should conform to this description:
entry: DE=element1, HL=element2
exit:  c  if element1 > element2;
       nc if element1 < element2;
       carry state doesn't matter if they're equal, return whatever's
      most convenient;
       AF/BC/DE/HL corrupt, if you like, but no others
*** rand.z
A simple pseudo-random number generator. Requires routines from maths.
srand
entry: none
exit:  AF/HL corrupt
Set random number seed from the refresh register R (which should be
relatively random). The seed is a 4-byte number at `seed'. If you have
a better source for a seed (the RTC is a good choice on ZCN), be sure
to use it - with the R register as a seed, there are only 128 possible
random number sequences!
(Yes, Z80 pedants, there are strictly speaking 256 sequences, but as
the Z80 only increments the low 7 bits of R, it's effectively only
128.)
rand
entry: HL=range size
exit:  HL=random number in range 0 to range_size-1 inclusive;
      AF/BC/DE corrupt
Return a random number in the given range in hl. This is exactly
equivalent to `rand()%range_size' in C.
rand16
entry: none
exit:  HL=random number, AF/BC/DE corrupt
Return a 16-bit random number (i.e. in the range 0 to 65535
inclusive). This is faster than `rand'.
*** sqrt.z
An integer square-root routine.
intsqrt
entry: HL=number
exit:  HL=sqrt (only L significant), AF/BC/DE/IX corrupt
Return integer square-root of number. Only the integer part of the
sqrt is given, so for example sqrt(99) is given as 9.
*** stdio.z
File I/O routines based on a subset of C's "stdio"; rather easier to
use than CP/M's 128-byte records and FCBs. On the other hand, they're
(necessarily) slower than the native I/O, so there's no free lunch.
You'll have to make up your own mind which I/O interface you prefer.
As for my opinion... For bulk I/O, like copying a file, CP/M's I/O is
faster and almost as easy. This probably applies to most binary I/O,
actually. But for text files, CP/M is a total pain, and these routines
are *much* better.
fopen
entry: HL=address of asciiz filename
        (however, it can actually end in a space if you want),
       A=0 to read text, 1 to write text, 2 to read binary, or 3 to
        write binary.
exit:  nc if couldn't open, else c if ok;
       if ok, HL=file handle;
       always, AF/BC/DE/IX corrupt
Open a file. A indicates the file mode. Binary files are read/written
without any conversion, while in text files CRs are stripped when
reading and added (before LFs) when writing. This gives a C-like feel
to file ops. Also, a ^Z character is treated as ending a text file (as
required in CP/M), and a ^Z is written when a text file is closed.
fopenr/fopenw/fopenrb/fopenwb
entry: HL=address of asciiz filename
        (however, it can actually end in a space if you want),
exit:  nc if couldn't open, else c if ok;
       if ok, HL=file handle;
       always, AF/BC/DE/IX corrupt
These routines set A to the relevant value and call fopen. It's
usually more mnemonic to call these than using fopen directly and
actually bothering to set A yourself.
fopenfcb
entry: HL=addr of FCB, A=file mode (as for fopen)
exit:  nc if couldn't open, else c if ok;
       if ok, HL=file handle;
       always, AF/BC/DE/IX corrupt
Open file specified in the FCB. This can sometimes be easier to use
than `fopen', such as when opening a file using the FCBs provided by
CP/M at 005Ch and 006Ch.
The FCB pointed to by HL is not used or modified (simply copied), and
only the first 12 bytes are relevant.
makefn83
entry: HL=address of asciiz filename, DE=address of FCB
exit:  AF/BC/DE/HL corrupt
Convert an asciiz filename at hl to FCB format name at de.
fclose
entry: HL=file handle
exit:  nc if file write error when closing, else c;
      AF/BC/DE/HL/IX corrupt
Close the file. Call this for *ALL* files you opened with fopen etc.,
not just ones you wrote to - if you don't close a file, the file
handle remains allocated. (And there are only 3, so this can be a
problem!)
fread
entry: HL=file handle, DE=address to read bytes at, BC=no. bytes
exit:  BC=no. bytes actually read, AF/DE/HL/IX corrupt
Read (up to) a certain number of bytes from a file. (Note that the
carry status is not used to signal an error here!) This is implemented
in terms of fgetc, and is simply a convenience routine - do not expect
it to be any faster than multiple calls to fgetc.
fwrite
entry: HL=file handle, DE=address to write bytes from, BC=no. bytes
exit:  BC=no. bytes actually written, AF/DE/HL/IX corrupt
Write (up to) a certain number of bytes to a file. (Note that the
carry status is not used to signal an error here!) This is implemented
in terms of fputc, and is simply a convenience routine - do not expect
it to be any faster than multiple calls to fputc.
fgetc
entry: HL=file handle
exit:  if c, ok and A=char from file - if nc, error reading or EOF;
      F/BC/DE/HL/IX corrupt
Get a char from file into A. If reading in text mode, CRs are dropped.
fputc
entry: HL=file handle, A=char to write
exit:  c if ok, else nc if write error (usually means disk is full);
      F/BC/DE/HL/IX corrupt
Write the char in A to the file. If writing in text mode, CRs are
added before any LFs written.
fseek
entry: HL=file handle, CDE=offset in file to seek to, in bytes (C is MSB)
exit:  AF/BC/DE/HL/IX corrupt
Seek to a new position in the file. Note that for the purposes of
fseek/ftell *only*, text files are treated exactly the same as binary
files are.
ftell
entry: HL=file handle
exit:  CDE=current offset in file, in bytes (C is MSB);
      AF/B/HL/IX corrupt
Return current position in file. Note that for the purposes of
fseek/ftell *only*, text files are treated exactly the same as binary
files are.
fgets
entry: HL=file handle, DE=addr to read line in at, BC=max bytes to read
exit:  AF/BC/DE/HL/IX corrupt
Get a line from the file, up to the max. length given in bc. Carry is
not used to signal an error - a zero-length returned string indicates
error reading or EOF.
The string will contain an LF if it was shorter than the max. length;
you can remove this with strchop from `string.z' if need be.
fputs
entry: HL=file handle, DE=addr of line to write
exit:  c if ok, else nc if error writing; AF/BC/DE/HL/IX corrupt
Write a line to the file. This writes exactly the string at de
(subject to any text-mode conversions), and doesn't add any LF.
*** string.z
Routines for manipulating C-like asciiz strings. Mostly based on
equivalent C library routines.
strlen
entry: HL=addr of string
exit:  BC=length of string, excluding trailing NUL;
       HL=addr of trailing NUL;
       AF corrupt
Return length of string (and address of trailing NUL).
strstr
entry: HL=`needle' string, DE=`haystack' string
exit:  HL=addr of first occurance of `needle', or 0 if none
Find `needle' in `haystack', and return address in hl.
strcmp
entry: HL=string1, DE=string2
exit:  c if they match, else nc; AF/DE/HL corrupt
Compare strings at hl and de. Unlike the C function, this only tests
for equality.
strncmp
entry: HL=string1, DE=string2, BC=no. bytes to compare
exit:  c if they match, else nc; AF/BC/DE/HL corrupt
Compare bc bytes of strings at hl and de. Tests for equality only.
strcpy
entry: HL=destination, DE=source
exit:  HL and DE both point to the NUL in each copy; AF corrupt
Copy string from de to hl.
strcat
entry: HL=destination, DE=source
exit:  HL and DE both point to the NUL in each copy; AF/BC corrupt
Add string from de onto the end of string at hl.
strncpy
entry: HL=destination, DE=source, BC=no. bytes to copy
exit:  F/BC/DE/HL corrupt
Copy bc bytes from de to hl. Does not add any NUL.
strchr
entry: HL=string, E=char to search for
exit:  HL=address of first occurrence of char in string, or 0 if none;
      AF corrupt
Find leftmost occurrence of char in string.
strrchr
entry: HL=string, E=char to search for
exit:  HL=address of last occurrence of char in string, or 0 if none;
      AF corrupt
Find rightmost occurrence of char in string.
strprint
entry: HL=addr of string
exit:  AF/HL corrupt
Print string at hl. Does not add any CR/LF.
ilprint
entry: none (see description)
exit:  AF/DE/HL corrupt
Print a string inlined in the code of the caller. To use, do something
like:
      call ilprint
      defb 'Hello world',0
The return address is altered such that execution continues after the
inlined string.
strchop
entry: HL=string
exit:  AF/BC/HL corrupt
Remove any trailing LF from the string. This is like the Perl `chop'
command. This can be useful for strings read with stdio's fgets
routine.
* Contacting the author
I'm repeating this info from the ZCN docs in case you got this
separate from ZCN, which it is distributed with:
No email address at the moment I'm afraid. :-(
Postal address:
            Russell Marks,
            3 Rapley Close,
            Camberley,
            Surrey,
            GU15 4ER,
            United Kingdom.
If you insist on abbreviating my name, please use "R. J. Marks".