Q2 Assembler

The Q2 assembler (q2asm) is written in Rust and available on Github: q2asm.

The Q2 assembler reads the source file and generates outputs with the following extensions:


q2asm supports the following directives:

Address Modes

The Q2 is a single-address architecture, meaning all instructions have a single operand representing an address. The Q2 supports four address modes to determine this address:

For convenience, q2asm supports two additional address modes to support 12-bit immediates:

These immediate address modes require that an additional word is stored at the end of the current page. This word is emitted either at the end of the program or when an ".align" or ".org" directive is encountered.

The Zero Page

The zero page is the first 128 words of memory. The operand of each instruction can either be in the same page as the instruction or in the zero page (depending on whether the zero-page bit in the instruction is set). This makes the zero page convenient for storing frequently used constants and for values that can be used across pages. Another way to look at the zero page is as if the Q2 had 128 general-purpose registers.

A Simple Example

To demonstrate a typical Q2 program, here is a simple example to compute the largest Fibonacci number that fits in 12-bits.

; Comments start with a ';' and continue to the end of the line.

; Here we use ".def" to give symbolic names to some memory locations.
; These will be used as zero-page addresses.
.def  x0    0
.def  x1    1
.def  x2    2
.def  x3    3

  .org  0x800     ; The entry point to our program will be 0x800.

; Call the "fib" function and output the result.

  ; The following performs a (page-local) call.
  jal   fib       ; "jal" (jump and link) is a psuedo-instruction,
                  ; which is equivalent to "lea $+2" followed by "jmp fib".
                  ; Note that '$' references the current address.
                  ; Here the "lea" loads A with the current address plus 2,
                  ; which is right after the jmp.

  ; The fib function will return here with the result in A.

  sta   @#0xFFF   ; Store the result to 0xFFF, which is the output device.
                  ; Since 0xFFF is neither in the zero page nor the
                  ; current page, we use "#" to ask the assembler to
                  ; insert 0xFFF at the end of the current page and give
                  ; us a pointer to it, and then we access it indirectly
                  ; with "@".

  jmp   $         ; Loop forever (this causes the simulator to halt).

; The "fib" function
; Labels start at the beginning of the line and end with a ":".
; Note that a label is like a ".def" that gets set to the current address.
  sta   =x3       ; Store A to =x3. This is used as the return address.
  lea   =1        ; Set A = 1 (the effective address of "=1" is simply "1")
  sta   =x0       ; Store 1 at =x0
  sta   =x1       ; Store 1 at =x1
  lda   =x0       ; Load A with the value at =x0
  add   =x1       ; Now A = =x0 + =x1
  jfc   fib_cont  ; If the add did not overflow (no carry), jump.
                  ; If the add overflowed, we're done.
  lda   =x1       ; Load A with =x1 (the result of the function)
  jmp   @=x3      ; Return by jumping to the address at =x3

  sta   =x2       ; Store =x0 + =x1 to =x2
  lda   =x1       ; Move x1 to x0 and then =x2 to =x1
  sta   =x0
  lda   =x2
  sta   =x1
  jmp   fib_loop  ; Loop back to determine the next number in the sequence.

Common Functions

Here are small examples to demonstrate the instruction set and how to accomplish some common tasks.


The Q2 ALU only supports 4 operations: load, nor, add, and shift-right. Other operations can be derived from these.

; NOT: A = ~A
  nor   #0

; OR: A = A | v
  nor   v
  nor   #0

; AND: A = A & v
  nor   #0
  sta   =x0
  lda   v
  nor   #0
  nor   =x0

; Negate: A = -A
  nor   #0
  add   #1

; Subtract: A = A - v
  nor   #0
  add   v
  nor   #0

; Decrement A
  add   #-1

Other arithmetic functions require slightly more code:

Function Calls

Despite not directly supporting function calls, it is relatively easy to implement them using the accumulator as a link register:

  lea   $+2   ; Save the return address in A.
  jmp   func  ; Jump to the function (same page or zero page).

Note that this assumes that the return address ("$+2", which stands for the current address plus 2), is in the same page as the "lea" instruction. The callee is responsible for saving the return address. Here we show saving the return address in the zero page (if you need recursion, use a stack instead):

  sta   =x0   ; Save the return address in the zero page.
  ; ...
  jmp   @=x0  ; Return (indirect jump through =x0)

A function pointer can be stored in the zero page to allow calling the function from anywhere:

  lea   $+2
  jmp   @=func_ptr

Often, functions that we want to call are not in the same page nor are they available in the zero page. To call any function, one option is:

  lea   $+3   ; Save the return address in A.
  jmp   @$+1  ; Indirect jump to the next address.
  .dw   func  ; Pointer to the function to call.

Since this sequence is so common, q2asm supports a jal pseudo-instruction. jal stands for "jump and link" and is equivalent to "lea $+2" followed by "jmp target". Combining jal with immediate addressing makes calling functions outside of the current page easy and relatively efficient:

  jal   @#func

Some architectures, such as the PDP-8, have a special instruction to jump to a subroutine by storing the return address immediately before the first word of the subroutine. Such an approach obviously wouldn't work with a ROM (which is why it wasn't considered for the Q2 architecture), but it is possible to do something similar on the Q2, if desired.


There is no dedicated stack pointer, but it is easy to use a word in the zero page as a stack pointer. Here we assume that "=sp" is the stack pointer and it is initialized to the stack (likely 0xFFE). To push the accumulator on to the stack:

  sta   @=sp    ; Store A to the stack
  lda   =sp     ; Set sp = sp - 1
  add   #-1
  sta   =sp

To pop the accumulator off of the stack:

  lea   =1      ; Set sp = sp + 1
  add   =sp
  sta   =sp
  lda   @=sp    ; Load A from the stack

To pop a value off the stack and return to it (useful since stacks are frequently used for storing return values):

  lea   =1
  add   =sp
  sta   =sp     ; Set sp = sp + 1
  lda   @=sp    ; A = return address
  sta   =x1     ; Now x1 = return address
  jmp   @=x1    ; Jump to the return address

Halt and No-operation

The Q2 does not support halt, but a similar effect can be achieved using an infinite loop. This instruction will cause the simulator to exit and the hardware to loop forever:

  jmp   $

There is no fully generic way to create a no-operation instruction, but depending on the situation, it is possible to achieve a similar effect. The closest thing to a generic no-operation is jumping to the next instruction. Unfortunately, this will not work if the next instruction is on a different page:

  jmp   $+1

If the contents of the flag can be discarded, adding zero to the accumulator can be used:

  add   #0
Another approach would be to store to a location of memory that is not used (either on the same page or in the zero page):
  sta   unused