ZSM4 Z80/Z180/Z280 Macro-Assembler

There are no Z80-hosted Z280 assemblers (or Z280-hosted, for that matter) that I know of. Only a PRE280 pre-processor is available for CP/M that can be used to expand Z280 mnemonics into a series of DB statements for use with a Z80 assembler. It does the job rather well, but being a pre-processor means it suffers from certain deficiencies:

  • Assembly becomes a two-step process, and therefore slow.
  • It does not process INCLUDE files, so you have to feed them by hand to PRE280 and manually modify the original (or resulting) source file to include the modified INCLUDE file instead.
  • It can get confused by MACROs.
  • It does not know about the size of arguments, so it cannot select automatically e.g. the right type of instruction involving the IX/IY index registers.
  • It does not process correctly PC-relative addressing expressions such as $+10.

In addition, the source code for PRE280 is not to be found anywhere.

Here is my version of a Z280 assembler; the goal was to make it compatible with the de-facto standard M80 and RMAC, and that means producing relocatable output, supporting conditional assembly, MACROs, and the established set of pseudo-operators.

In order to avoid starting from scratch, I looked around for the source code of a free Z80 assembler that could be used as starting point. After considering the available options out there (there are actually very few), I decided to use ZSM from the CP/M UK Users Group. ZSM was originally known as Z80ASM and written by Michael G. Lehman back in 1977, and later modified by Ray Halls in 1982 and Neil Harrison in 1983. The last released version was 2.8.

After releasing the first beta version, I learned that Ian Waters, Chris Maxfields and Miguel Garcia had made further changes to ZSM 2.8; the latest version being 3.2 and available available via GitHub.

There were some interesting modifications:

  • An ASSERT pseudo-op.
  • LESS signed comparison operator.
  • Date/time displayed on listing if running under CP/M 3.x

All other modifications were rather minor, mostly attempts to correct the know bugs and/or deficiencies of the assembler, and in some cases changed the syntax of expressions in an unacceptable way.

After reviewing the changes, I decided to incorporate the idea of the LESS operator and the inclusion of the assembly date and time in the listing (but using a different format, and so it works under MP/M too). The ASSERT pseudo-operator can be easily implemented with a MACRO and therefore was not added (as the assembler has already become considerably large, is better to limit the number of extra bells-and-whistles to a minimum.)

Below is a summary of my modifications:

  • Fixed many bugs that used to crash the assembler when it encountered invalid syntax.
  • Syntax checking is now much more strict.
  • Optimized code, removed redundancies. Faster symbol table searches.
  • The main assembly routine is no longer a monolithic piece of code with thousands of jumps around, but was restructured into a set of routines that are called from, and return to, the main thread. This not only saved a lot of bytes, but made the addition of further features and enhancements much easier.
  • Replaced the simple but very slow add-in-a-loop multiply routine by a faster one. Same for the division routine.
  • The expression evaluation routine is now smarter: symbol names, register names, opcodes and conditional codes no longer conflict with each other.
  • Opcode table restructured: entries are now grouped by number of characters and sorted alphabetically in order to allow for binary searches.
  • The assembler now produces Microsoft REL object files instead of Intel HEX files.
  • Source was split into different modules for easier port to other Z80 OSes, in particular to UZI180 and RSX180.
  • Abandoned 8080 executable compatibility in favor of the more efficient Z80 instructions.
  • Added support for the extra Z180 instructions, and for the complete set of Z280 instructions. New .Z80, .Z180 and .Z280 pseudo-operators allow selecting target processor type. Z80 "undocumented" instructions that use half-index registers are now supported in .Z80 mode.
  • Fixed processing of character expressions (e.g. statements like DW '''a' now work correctly).
  • Where single-quotes were allowed, double-quotes are now also allowed, e.g. DEFB "string".
  • Labels are allowed to start with $. Dots, ? and @ are also allowed in labels.
  • Public labels can now be defined using double-colons :: and external references with ##, again for compatibility with M80.
  • Spaces and/or TABs are now allowed before/after commas, brackets, etc. Originally, the assembler would happily generate wrong object code in cases such as ld a,( hl ) (note the spaces around hl) without complain!!!
  • Free-format is now allowed (i.e. labels do not have to start at column 1, and instructions do not have to be preceded by a blank).
  • Expanded operators are no longer delimited by a dot, but instead by a space or TAB. This was done in order to allow dots in label names and pseudo-operators, and for compatibility with M80.
  • Expression evaluation now honors operator precedence and relocation rules. In addition, brackets are now allowed, e.g. LD A,(IX+2*(k+8)), etc.
  • Added REL-related pseudo-operators: ASEG, CSEG, DSEG, COMMON, PUBLIC, GLOBAL, ENTRY, EXT and EXTRN.
  • Added a new pseudo-operator .EVEN to align code and/or data to word boundary (may become useful when writing programs for the Z280 CPU); .ODD does the opposite (aligns code/data to odd byte address).
  • Added DC pseudo-operator that sets the MSBit of the last character of a string.
  • Added support for INCLUDE files. Can be nested up to a level of 5.
  • Added more flavors of IF conditionals: IFT, IFF, IF1, IF2, IFDEF, IFNDEF, IFB, IFNB, IFIDN, IFDIF.
  • Added support for MACROs, including REPT, IRP and IRPC.
  • Added new LIST options: MACROS (list all lines of MACRO expansions), XMACROS (list only the lines that generate object code), NOMACROS (don't list MACRO expansions). The default is LIST XMACROS.
  • Added more pseudo-operators: ASET, DEFL (same as ASET), DEFM (same as DEFB), DEFC (same as DC), DEFZ, .PHASE, .DEPHASE, .RADIX, .PRINTX, .COMMENT, .REQUEST, RQST (same as .REQUEST), SUBTTL, NAME, IDENT, .LIST (for compatibility with M80, same as LIST ON), .XLIST (same as LIST OFF), .LALL (same as LIST MACROS), .SALL (same as LIST NOMACROS), .XALL (same as LIST XMACROS), .SFCOND (same as LIST NOCOND) and .LFCOND (same as LIST COND).
  • Added LESS relational operator from ZSM 2.9.
  • Added NUL operator.
  • Date and time of assembly is now output to the listing, if the functionality is supported by the OS.
  • The PAGE pseudo-operator can be optionally followed by an expression that sets the page size.
  • Listing output format is now more M80-like, with error code on first column. Since the source line section now starts at a tab boundary, TAB expansion and compression is no longer needed. The TABS and NOTABS LIST options were therefore removed.
  • Long error messages are now a compile-time option.
  • All generated code bytes are now shown on the assembly listing, instead of just the first 4 bytes.
  • Default source file extension is now MAC.
  • Changed the command line syntax. When called without a command, the assembler enters an interactive command mode where several files can be assembled without having to exit to CP/M.

You can consult the full documentation here (PDF file.)

Operation under CP/M

The assembler is initiated by typing:

ZSM [command]

where command is optional and has the following format:

[relfile],[prnfile]=srcfile[/option][/option...]

If no command is specified, the assembler will enter an interactive mode and prompt for the next command to be executed.

relfile, prnfile and srcfile are valid file specifications and may contain a drive specification and extension. The default extensions are as follows:

relfileREL
prnfilePRN
srcfileMAC

The following device names can be used as well in place of a file specification (normally to redirect the output of the listing):

LPT: or LST:line printer
CON: or TTY:user's console

Options are a single char preceded by a slash. Some options require an argument. If several options are specified, each one needs to be preceded by a slash.

The available options are:

/L Force generation of a listing file
/Dsymbol[=value] Define symbol, optionally assigning a value to it. The value is a numeric constant following the standard format (i.e. nnnn, nnnnH, etc.) If no value is specified, 0 is assumed.
/Sn Set the maximum symbol name length in the REL file. Allowed value of n is 5..8. Default value is 6.
/M Initialize block data areas defined by DS to zeros. If the option is not specified, the space is not guaranteed to contain zeros.
/U Treat all undefined symbols as externals
/Zn Sets the initial CPU type to Z80, Z180 or Z280 (n=0, 1 or 2 respectively.)

For example, assembling the file TEST.MAC:

ZSM4 =TEST/L

This expects the file TEST.MAC on the current drive, will write the object code file TEST.REL and the listing file TEST.PRN both to current drive.

ZSM4 b:ldiskio,tty:=diskio.280/Dloader=1/U

This expects the file DISKIO.280 on the current drive, will define the LOADER symbol as 1, all undefined symbols will be declared External, will write the object code file LDISKIO.REL to drive B:, and will output the listing to the console.

Operation under RSX180

ZSM4 is built with a default task name of ...MAC. If desided, a different name can be specified in the INStall command, for example:

INS ZSM4/TASK=...ZSM

The task is also built with a default memory increment of 8000 bytes, which is enough for assembling small to medium-sized files. However, for large files specifying a larger memory increment via the INStall command is not necessary, since the assembler can request automatically more memory from the system if/as it becomes necessary. If the request is not granted, and error will be displayed.

File specifications follow the standard RSX180 convention:

dev:[directory]filename.ext;vers

Note that the square brackets above are part of the syntax and therefore required when specifying a directory. Only filename is required, all other fields are optional and default to the following values:

dev SY0: (the user's login device)
directory the current user's directory
ext MAC
vers highest (latest) version of the file.

If the task has been installed, it can be invoked by entering its installed task name:

MAC [command]

where command is optional and has the following format:

[objfile],[lstfile]=srcfile[/option][/option...]

If no command is specified, the assembler will enter an interactive mode and prompt for the next command to be executed.

objfile, lstfile and srcfile are valid file specifications and may contain device and directory specifications, extension and version. The default extensions are as follows:

objfileOBJ
lstfileLST
srcfileMAC

Options are a single char preceded by a slash. Some options require an argument. If several options are specified, each one needs to be preceded by a slash.

The available options are:

/L Force generation of a listing file
/Dsymbol[=value] Define symbol, optionally assigning a value to it. The value is a numeric constant following the standard format (i.e. nnnn, nnnnH, etc.) If no value is specified, 0 is assumed.
/Sn Set the maximum symbol name length in the REL file. Allowed value of n is 5..8. Default value is 6.
/M Initialize block data areas defined by DS to zeros. If the option is not specified, the space is not guaranteed to contain zeros.
/U Treat all undefined symbols as externals
/Zn Sets the initial CPU type to Z80, Z180 or Z280 (n=0, 1 or 2 respectively.)

For example, assembling the latest version of the file TEST.MAC:

MAC =TEST/L

This expects the file TEST.MAC on the current directory of the user's login device, will write the object code file TEST.OBJ and the listing file TEST.LST both to the current directory.

MAC du2:[output]ldiskio,ti:=diskio.280;5/Dloader=1/U

This expects the file DISKIO.280;5 on the current directory of SY:, will define the LOADER symbol as 1, all undefined symbols will be declared External, will write the object code file LDISKIO.OBJ to the directory OUTPUT on device DU2:, and will output the listing to the user's terminal.

When not installed, the task can be invoked by using the RUN command. For example, assuming the task is named ZSM4.TSK and is located on the LB:[SYSTEM] directory:

RUN $ZSM4

If no command line is present, the assembler will enter interactive mode and prompt for a command line:

MAC>

ZSM4 also supports indirect command files under RSX180 in the standard way:

MAC @filespec

where filespec is a valid command file specification. If no extension is specified, CMD is assumed. The command file simply contains a list of commands to be executed by ZSM4, one per line, and can in turn invoke additional command files up to a nesting level of 3.

Operation under Linux or Windows

The CP/M version of ZSM4 can be used as a cross-assembler on a Linux or Windows system by using John Elliott's zxcc program. Zxcc emulates both a Z80 and a CP/M environment in a way such that a regular CP/M application can be run from the Linux or Windows command prompt as if it was a native application.

Since ZSM accepts source files with both CP/M and Unix end-line conventions, no conversion to the CP/M format is necessary.

Downloads

You can find the binary and sources in the downloads page. The sources are also on GitHub.

Last modified 29-Apr-2019.