Torque: Programming the TRS-80

This article is a hands-on introduction to the Torque assembler, showing how to write a program for the Z80 microprocessor in the TRS-80 Model 4 home computer (released 1983).

The TRS-80 Model 4P home computer

The TRS-80 computer has the ability to run programs written in BASIC, but in this article we’ll be programming the Z80 processor directly.

The language

Torque is a lightweight meta-assembler that gives you the tools to take an instruction set specification from a datasheet and turn it into an expressive and ergonomic programming language. The Torque language is bare-bones, providing only integers, bit sequences, labels, and macro expansion, but it’s general enough to be able to lever the right bits into the right order to write a program for any processor.

The main project page has downloads, source code, and the full user manual.

The computer

The TRS-80 is a mass-produced home computer originally released in 1977, and is built around the Zilog Z80 microprocessor. The TRS-80 Model 4 has 64 kilobytes of memory and a monochrome character-based CRT display. The contained Z80 processor runs at a clock speed of 4MHz and uses a byte-oriented variable-length instruction format.

The datasheet for the Z80 processor can be found here.

The program

The program we’ll be writing will draw a string of characters to the top-left corner of the screen. We’ll load address values into registers, call an instruction that copies a range of bytes from program memory into video memory, and then halt.

This is enough of a task to show off the various language features without getting bogged down by algorithms and implementation details. If you want to jump to the completed program listing, click here.

Getting started

The Z80 has an instruction set with 150 instructions, ranging from 1 to 4 bytes in length.

The first step will be to assemble a program containing just a single instruction in order to demonstrate the basic features of a Torque program. We’ll use the JP absolute jump instruction (called JP nn in the datasheet), using it to jump back to the beginning of the program.

The JP instruction

The datasheet lists the JP nn instruction on page 262. The instruction is three bytes long, with the first byte containing the instruction op-code and the final two bytes containing the absolute address to jump to. The address bytes are stored in little-endian order.

The JP instruction in the datasheet

This can be translated directly into the Torque language using multiple ‘word templates’, fixed-width sequences of bits into which other values can be packed. The syntax for the three word templates that represent the three bytes of the JP instruction is:

#1100_0011
#0000_0000
#0000_0000

This is an entire valid Torque program, assembling to a single JP instruction. Assemble it as a raw binary file with the following command:

tq program.tq output.hex --format=raw

A better way with macros

In the previous example, the instruction is an unreadable binary sequence and the address is hard-coded to be zero. We can do a lot better.

Macros provide a way to associate a name with a fragment of code, allowing us to reuse that fragment by invoking the macro name later on in the program. We can define a macro for the JP instruction from before, and then invoke it to insert the instruction into the program:

%JP
  #1100_0011
  #0000_0000
  #0000_0000 ;

JP
JP

The first four lines define the macro, ending with the semicolon character, and the final two lines invoke it twice, assembling to six bytes. But we’re still not quite where we want to be, we want to be able to pass any address to the instruction instead of having it hard-coded.

Breaking it down

There are a few different pieces to this instruction that we’ll need to break apart so that we can pack our own addresses in later on. We can separate the two-byte address sequence out into a new macro, called 16LE, and from that macro we can abstract each byte into a macro called BYTE:

%BYTE  #0000_0000      ;  ( 8-bit value                )
%16LE  BYTE BYTE       ;  ( 16-bit little-endian value )
%JP    #1100_0011 16LE ;

JP
JP

This code assembles to the same bytecode as before, but breaking up the code like this will allow us to pass in an address in the next step.

Passing in values

Macros don’t just have to expand to a static code fragment, they can also receive values as arguments. We can see this by making a small change to the BYTE macro from before:

%BYTE:n  #nnnn_nnnn ;

BYTE:0xC3
BYTE:0x01
BYTE:0x00

This new BYTE macro takes a single integer value as an argument, which is given the name n, and that value is packed into the n field of the word template each time the macro is invoked. Integers can be given in decimal, hexadecimal, or binary, as 29, 0x1D, or 0b11101.

Named fields in word templates work by searching for a macro with that name that expands to an integer, and then that integer is packed into the bits of the field. Arguments inside a macro definition are treated as regular macros, expanding to the value that was passed.

This program assembles to a single JP instruction, but instead of jumping to address zero it will jump to address 0x0001 (the instruction expects the address in little-endian order, where the low-order byte comes before the high-order byte).

We can put this all together to make a JP macro that accepts arbitrary addresses:

%BYTE:n    #nnnn_nnnn         ;
%16LE:H:L  BYTE:L BYTE:H      ;
%JP:H:L    BYTE:0xC3 16LE:H:L ;

JP:0x00:0x01
JP:0x00:0x02

This new JP macro allows us to pass in the high and low byte of an address in big-endian order, with the bytes flipped to little-endian order by the 16LE macro. We’re almost there, we just need to be able to pass in an address as a single 16-bit value instead of as two 8-bit halves.

Processing values with expressions

Expressions are a language feature that allow us to apply a sequence of operations to an integer at assembly time. We can use this to take a single large integer and automatically break it down into two 8-bit bytes.

Expressions are wrapped with square brackets and use a postfix notation, evaluating from left to right, and operator names are wrapped with angular brackets. When an integer (or a macro that expands to an integer) is evaluated, that integer is pushed to the expression stack. When an operator is evaluated, a pair of integers are popped from the stack, the operator is applied to those integers, and the result is pushed back onto the stack.

There must be exactly one integer left on the stack after the expression has been evaluated, which will be used as the value of the expression.

%16LE:nn
  BYTE:[nn 0xff <and>]
  BYTE:[nn    8 <shr>] ;

This new 16LE macro takes a single integer called nn and breaks it down into two bytes. The first byte is created by ANDing the integer with 0xff, leaving the lower 8 bits. The second byte is created by right-shifting the integer by 8, leaving the upper 8 bits. A list of all available operators can be found here.

%BYTE:n   #nnnn_nnnn           ;
%16LE:nn  BYTE:[nn 0xff <and>]
          BYTE:[nn    8 <shr>] ;
%JP:nn    BYTE:0xC3 16LE:nn    ;

JP:0x0001
JP:0x0002

Combining the new 16LE macro with our code from before gives us a JP macro that can take a single address value and pack it into the instruction in the right order. Now that we’ve figured out the 16LE and BYTE macros, we can reuse them across the rest of the instruction set.

Addressing with labels

Finally, instead of using an integer literal as an address, we can have Torque automatically calculate the address value from a label.

JP:one
@one
JP:two
@two

Labels are treated as regular macros that expand to the address of the next word in the program.

Writing the program

We now know most of the Torque language, and can build up to the full program.

Drawing to the screen

Drawing characters to the screen is as straightforward as copying character bytes into video memory, which is memory-mapped on the TRS-80 Model 4 starting from address 0x3C00.

We can create a macro to associate the name $SCREEN with the screen address:

%$SCREEN  0x3C00 ;

Creating a string

We want to create a string as an array of bytes in memory so that we can copy it to the screen later on.

The TRS-80 character set is derived from the ASCII character set, sharing character codes 0x20 through 0x5A. This includes digits, uppercase characters, and most symbols, but notably excludes the lowercase characters.

As well as decimal, hexadecimal, and binary literals, we can also write integers using character literals. The value of a character literal is the Unicode code point of that character, and so is compatible with the ASCII and TRS-80 character sets.

This code will assemble to six bytes, with the value of each byte being the ASCII value of that character:

@string
  BYTE:'T'
  BYTE:'O'
  BYTE:'R'
  BYTE:'Q'
  BYTE:'U'
  BYTE:'E'

A more concise way of writing this would be to use a string literal. String literals are a special type of integer literal that can only be used as an argument to a macro invocation, and will cause the macro to be invoked once for every character in the string.

@string
  BYTE:"TORQUE"

Copying the string

For the sake of brevity, we’ll be using the LDIR instruction to copy the string to the screen. This will save us from having to dive into the complexities of the Z80 architecture.

The LDIR instruction will load a byte from the address stored in the HL register pair, save that byte to the address stored in the DE register pair, increment the addresses in both the HL and DE register pairs, and then decrement the loop counter stored in the BC register pair, repeating until the counter reaches zero.

All that we need to do in order to copy the string to the screen memory is to load the correct values into the HL, DE, and BC register pairs and execute the LDIR instruction.

%SET-HL:nn  BYTE:0x21 16LE:nn   ;  ( Load nn into HL             )
%SET-DE:nn  BYTE:0x11 16LE:nn   ;  ( Load nn into DE             )
%SET-BC:nn  BYTE:0x01 16LE:nn   ;  ( Load nn into BC             )
%LDIR       BYTE:0xED BYTE:0xB0 ;  ( Copy BC bytes from HL to DE )
%HALT       BYTE:0x76           ;  ( Stop the processor          )

SET-HL:string
SET-DE:$SCREEN
SET-BC:6
LDIR
HALT

@string
  BYTE:"TORQUE"

Calculating the string length

Instead of hardcoding the string length, we can calculate it automatically using an expression. This will allow us to change the string contents later on without breaking the program.

SET-BC:[end string <sub>]

@string
  BYTE:"TORQUE"
  @end

This works by subtracting the address at the end of the string from the address at the start of the string, resulting in the length of the string in bytes.

Abstracting the details

One final change we can make is to separate the ‘copy bytes’ code out into its own macro. This will allow us to copy strings around in the future without having to remember which values need to be loaded into which registers.

%COPY-BYTES:src:end:dest
  SET-BC:[end src <sub>]
  SET-HL:src
  SET-DE:dest
  LDIR ;

COPY-BYTES:string:end:$SCREEN

Bringing it all together

The completed Torque program is below. The |0x6000 syntax is used to store the program at address 0x6000, which will allow us to load it on a real TRS-80 computer.

%BYTE:n     #nnnn_nnnn           ;  ( 8-bit value                 )
%16LE:nn    BYTE:[nn 0xff <and>]
            BYTE:[nn    8 <shr>] ;  ( 16-bit little-endian value  )

%SET-HL:nn  BYTE:0x21 16LE:nn    ;  ( Load nn into HL             )
%SET-DE:nn  BYTE:0x11 16LE:nn    ;  ( Load nn into DE             )
%SET-BC:nn  BYTE:0x01 16LE:nn    ;  ( Load nn into BC             )
%LDIR       BYTE:0xED BYTE:0xB0  ;  ( Copy BC bytes from HL to DE )
%HALT       BYTE:0x76            ;  ( Stop the processor          )

%$SCREEN    0x3C00               ;

%COPY-BYTES:src:end:dest
  SET-BC:[end src <sub>]
  SET-HL:src SET-DE:dest LDIR    ;  ( Copy bytes from src to dest )


|0x6000
COPY-BYTES:string:end:$SCREEN
HALT

@string
  BYTE:"TORQUE" @end

This can be assembled with Torque using the following command, outputting a hex-encoded CP/M executable file compatible with the TRS-80 Model 4:

tq program.tq output.hex --format=cmd

Further reading