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 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.

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
- The main Torque project page
- Torque: Advanced loops on the Z80
- Torque: Programming the PIC10F200