The Q Assembler Language Reference
Farts.
Code structure
A source file is a flat sequence of lines. Each line may contain, in this order:
[.section] [@offset] [symbol:] [statement] [; comment]
The three levels of structure are:
-
Sections — named containers that determine where in the address space code and data end up. A section is opened by a token starting with
.and stays open until the next section token. -
Symbols — named blocks inside a section, opened by
name:. Everything that follows belongs to that symbol until the next symbol or section. A symbol is both a label and a unit of layout. -
Statements — instructions,
dataorword. Every statement must live inside a symbol, which in turn must live inside a section.
Anything can be written on one line or spread over many; the layout in the examples is a convention, not a requirement.
.data @0x100 ; open section .data, place it at absolute word address 0x100
answer: data 42 ; symbol `answer` holding one word of data
data 43 ; still part of `answer`
nice: word 0x45 ; new symbol `nice`
Lexical rules
-
Lines are split into tokens on whitespace. Commas are optional separators and are stripped from the end of each token;
add r1, r2, r3andadd r1 r2 r3are the same thing. -
Comments start at a token that begins with
;and run to the end of the line. The;must be preceded by whitespace —nop; commentis parsed as the mnemonicnop;and fails. -
Mnemonics and register names are case-insensitive (
ld r0, 5==LD R0, 5). Symbol and macro names are not. -
Strings are delimited by
"or'. A token may not start with a quote character unless the quote is closed on the same line. - There are no line-continuation characters, and a statement may not span lines.
Numeral
-
123— decimal, e.g.data 42 -
0x7B— hexadecimal, e.g.ld r0, 0xFF -
0b1111011— binary, e.g.ld r0, 0b1010
Negative literals are not supported (- is always an operator, see below).
Expressions
A single binary operation between two numerals is evaluated at assembly time. Supported operators are +, -, *, / (integer division), & and |.
st r0, @SIMIO_OFFSET + 1 ; address arithmetic on a -D macro
add r3, r0, VGI_OFFSET + 3
ld r1, 2 * 8
Rules and caveats:
-
Operators must be surrounded by whitespace.
2 * 8works;2*8is glued to the previous token and fails. -
Only one operation is evaluated.
1 + 2 + 3silently yields3— the trailing terms are dropped. Chain nothing; precompute in a#defineinstead. -
A leading
@still applies:@0x200 + 1is an absolute address,0x200 + 1is an immediate.
Sections
A token starting with . opens a section. Names are arbitrary — they carry no special meaning to qas. All sections are emitted into the binary.
Although sections' names are arbitrary, some are customarily used for specific contents. Such conventions include: - .text — program code (i. e. instructions); - .data — arbitrary data structures used by the program; - .bss — reserved memory space for variables used by the program (normally initialized w/ zeroes);
Sections with _llr suffixes are used by QSys LLR, so that symbols contained in them do not mix with user code.
A section may be given an absolute placement by following it with an @-prefixed address:
.text @0x50000 ; this section starts at word 0x50000
.data ; this section directly follows the previous one
.bss @0x53100 ; and this one is placed at a fixed address
- Offsets are word addresses, not byte addresses (QCPU is word-addressed; one word is 4 bytes).
- A section without an explicit offset is placed immediately after the previous section.
- The offset may be set only once per section name.
- The gap between a section's end and the next section's explicit offset is filled with zero words in the output image, so a distant offset produces a correspondingly large binary.
-
If an explicit offset falls inside the preceding section, assembly fails with
Section '.data' overlaps previous section. - Re-opening a section (in the same file or in another source file) appends to the section that already exists; it keeps its original position in the layout. This is what makes multi-file builds work — see Linking and memory layout.
Symbols
A token ending with : declares a symbol. Symbol names may contain A-Z, a-z, 0-9 and _.
A symbol includes every statement that follows it until the next symbol or section, and its length is the total length of those statements. Symbols are the least units at which references are resolved: a symbol name in an instruction always resolves to the address of the symbol's first word.
Symbol names are global across all sections and all source files on the command line. There is no duplicate check — if two symbols share a name, references resolve to whichever comes first in section order.
Instructions
An instruction is a mnemonic followed by zero or more arguments, e.g.
add r0, r1, 1
Argument forms
The shape of the arguments is what selects the addressing mode, so the sigils matter a great deal:
-
r0,sc,pc, ... —- register index. One of the 15 registers (see below). -
42,0x2A,0b101—- immediate value. A literal, embedded in the instruction word. -
@0x201— absolute address. A literal address, embedded as an address operand. -
symbol— absolute address. The address ofsymbol—- i. e. the instruction operates on the contents ofsymbol. -
$symbol— immediate value. The address ofsymbolas a number — i. e. the pointer itself.
The symbol / $symbol distinction is the most important one in the language:
ld r0, message ; R0 = the word stored at `message`
ld r0, $message ; R0 = the address of `message`
Registers are pc, sc, sr, ir, iv (indices 0x0–0x4) and r0…r9 (indices 0x5–0xE). sc (stack counter) and iv (interrupt vector) must be initialized by user code before use (see qcpu/isa).
Flavours
QCPU ISA calls its addressing modes flavours. qas never asks you to explicitly specify them: it deduces the flavour purely from the argument shapes, then encodes it into the top 3 bits of the opcode byte.
Argument shape Flavour Opcode byte Operand layout (bytes 1..3)
--------------- ------- ----------- -----------------------------------------
(none) N 0x00 | op 00 00 00
reg, reg, reg R 0x20 | op dst<<4|src1, src2<<4, 00
reg, reg, imm I 0x40 | op dst<<4|src1, 16-bit immediate
reg, imm S 0x60 | op dst<<4|imm[19:16], 20-bit immediate
addr Q 0x80 | op 24-bit address
reg, reg F 0xA0 | op dst<<4|src, 00 00
reg E 0xC0 | op dst<<4, 00 00
reg, addr A 0xE0 | op dst<<4|addr[19:16], 20-bit address
$symbol counts as an immediate (S / I), a bare symbol and @address count as an address (A / Q). So ld r0, $msg assembles to LDS and ld r0, msg to LDA.
Immediates and addresses are silently truncated to the width of their field — ld r0, 0xAAAAAAAA encodes 0xAAAAA. There is no range check and no warning.
Semantics worth knowing
These follow from the ISA, but they surprise people reading QCPU assembly for the first time:
-
ldbetween two registers is an indirect load, not a move.ld r3, r1means "load the word at the address held in R1 into R3". A register-to-register copy is writtenadd r0, r3, 0. -
sttakes the source first.st r4, variablestores R4 intovariable;st r0, r5stores R0 at the address held in R5. -
jal/retuse the stack, soscmust point at usable memory before the first call. -
lsh/rshshift the destination register in place:lsh r3, 8isr3 <<= 8.
BEQ/BNE/BGT/BLT only exist in R and I flavours, and their first argument is a register holding the branch target, not a label. The idiom is therefore:
ld r2, $loop ; load the loop vector into a register first
loop: ...
blt r2, r0, r4 ; if R0 < R4, jump to the address in R2
jmp and jal, in contrast, do take a label directly (Q flavour) or a register (E flavour).
qas deduces a flavour from argument shapes alone and does not check it against the opcode. It will happily assemble jmp r0, whatevs into JMPA, a combination that is not part of the ISA. Consult the instruction reference for what the hardware actually implements.
"data" statement
data emits a raw byte sequence. It accepts any number of arguments on the line, concatenated in order:
message: data "Hello, world!" 0xA
mixed: data 1, 2, "str"
file contents: data file:build/bitfont.gray
-
"text"/'text'— The ASCII bytes of the string, without a terminator. - numeral — Big-endian bytes, in the narrowest width that fits: 1, 2, 4 or 8 bytes.
-
file:PATH— The entire contents of the file, verbatim.
The result is padded with zero bytes to a whole number of words. data is a byte-oriented statement: it packs its arguments back to back and only then aligns them, so a numeral occupies as many bytes as it needs and the padding lands at the end of the whole sequence. data 42 therefore fills one word as 2A 00 00 00, and data 1, 2, "str" fills two as 01 02 73 74 72 00 00 00. Use word when you want a value laid out as a 32-bit quantity instead.
file: paths are resolved relative to the CWD of the qas process, not to the source file.
"word" statement
word emits one 32-bit big-endian word from one value:
message_len: word 66
counter: word 0
The statement takes a single argument by design — write one word per line, or use data for sequences. It accepts numerals only (including expressions); strings, $symbol and file: belong to data.
Preprocessor
Preprocessing runs per source file, before any parsing, in two passes: includes first, then macros.
Everything after #include or #define statement is considered a part of the statement, so the directive occupies the whole line: preprocessor directives are handled before the assembler ever sees the source, and ; comments are not part of their syntax. Put any explanation on a line of its own.
#include statement:
#include <macro_include.s>
#include "llr/simio.s"
#include macro_include.s
The #include directive is replaced with the full text of the file. The file is looked up in . followed by every -I path, in order. All three quoting styles behave identically.
#define statement:
#define MESSAGE_LENGTH 4
#define MOV(DST SRC) add DST, SRC, 0
The first form is object-like, the second is function-like.
- Definitions are removed from the source and applied as plain text substitution over the whole file.
-
Substitution is not token-aware: a macro named
R0orXwill also replace those characters inside other identifiers. -
Function-like macro parameters are separated by spaces, not commas:
#define MOV(DST SRC). Writing#define MOV(DST, SRC)makes the first parameter literallyDST,. -
At the call site both
MOV(r1, r2)andMOV(r1 r2)work: any stray commas that substitution leaves behind are cleaned up by the tokenizer. - Macros leak between source files on the same command line: a macro defined in the first file is still defined while the second is preprocessed. Do not rely on this, but be aware of it when names collide.
-D command line argument:
-D NAME=CONTENTS defines an object-like macro globally, before any file is read. A #define of the same name in a source file overrides it.
Inspecting the result
-p FILE writes the preprocessed source and stops. Useful when a macro does not expand the way you expect:
python3 qas.py -p /tmp/expanded.s main.s
Linking and memory layout
qas is a relocating assembler in the sense that it lays out symbols itself and patches all references at the end of the run. There is no separate linker step and no relocation at load time: the image produced is bound to one absolute address.
The process is:
-
Every source file on the command line is preprocessed and parsed into the same
Programobject, in order. Sections with the same name are merged; a section keeps the position of its first appearance. -
Each section's absolute base is computed: an explicit
@offsetsets it, otherwise it follows the previous section. - Each symbol's absolute address is its section's base plus the length of all symbols declared before it in that section.
-
All
symboland$symbolreferences are replaced with those absolute addresses. - Sections are rendered in order, zero-padding gaps between explicit offsets.
Because sections keep the position of their first appearance, the order in which the first mention of each section occurs decides the layout. When library code is passed as later source files, hoist its sections in the main file to pin them where you want:
.text @START_VECTOR
_start: ...
.data
message_text: data "..."
; forward declaration of the LLR runtime for linking purposes
.text_llr
.data_llr
.bss_llr
.bss @0x53100
stack: word 0
The resulting image starts at the offset of the first section — no padding is emitted in front of it. That offset must therefore match where the image is loaded.
Object files
--object serializes the whole intermediate Program (a Python pickle) instead of rendering a binary.
Output formats
Binary image (-o)
A flat, big-endian image, 4 bytes per word, starting at the first section's offset. This is what qsim loads and what a ROM/RAM model is initialized with.
Memory map (-m)
A text dump of the laid-out program, produced after reference resolution, so all addresses are final. For each section it prints the address range and length, then each symbol, then each word with its disassembly:
.text 0x50000..0x50006 (0x6)
0000050000: <_start> 0x50000..0x50004 (0x4)
0000050000: 69 15 31 00: LDS SC, 0x53100
0000050001: 69 55 00 06: LDS R0, 0x50006
0000050002: e9 65 00 48: LDA R1, @0x50048
0000050003: 90 05 00 c7: JALQ @0x500c7
0000050004: <endloop> 0x50004..0x50006 (0x2)
0000050004: 00 00 00 00: NOPN
0000050005: 8f 05 00 04: JMPQ @0x50004
Mnemonics in the map are printed as <opcode><flavour> (LDS is LD in S flavour). All numbers are word addresses. This is the fastest way to check what a piece of source actually assembled to.
Verilog header (-v + --verilog_entity)
An include-guarded header containing an initial block that preloads an array, one word per element:
`ifndef MEMORY_VH_
`define MEMORY_VH_
initial begin
%verilog_entity%[0] = 32'h90000103;
%verilog_entity%[1] = 32'h00000000;
...
end
`endif
--verilog_entity specifies the name of Verilog array words are loaded into.
Indices are relative to the start of the image, not absolute addresses.
To post a comment you need to login first.