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:
- ".q2p" - The programming file used with q2prog to load the program onto the Q2 (from a Raspberry Pi).
- ".lst" - A listing file, which is a human-readable output to see the machine code that was generated.
- ".hex" - A hex output to be used with the Verilog model for simulating the program.
Directives
q2asm supports the following directives:
- .align - Aligns the next address to the specified word boundary (128 by default).
- .bss - Reserves the specified number of words.
- .def - Defines a constant.
- .dw - Inserts one or more words.
- .include - Includes the contents of another file.
- .org - Sets the next address (origin).
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:
- x - Relative (a value in the same 128-word page).
- =x - Zero-page (a value in the first 128 words; the zero page).
- @x - Indirect (an address in the same 128-word page to a word).
- @=x - Indirect through the zero page (an address in the zero page to a word).
For convenience, q2asm supports two additional address modes to support 12-bit immediates:
- #x - Immediate. The assembler will store "x" at the end of the current page and replace the operand with a relative address to reference "x".
- @#x - Immediate indirect. The assembler will store "x" at the end of the current page and replace the operand with an indirect address to reference a value at "x".
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. fib: 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 fib_loop: 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 fib_cont: 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.
Arithmetic
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):
func: 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.
Stacks
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 #0Another 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