pesets dot com. technology was a mistake

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:

  1. 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.
  2. 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.
  3. Statements — instructions, data or word. 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

Numeral

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:

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

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:

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:

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

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.

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

  1. Every source file on the command line is preprocessed and parsed into the same Program object, in order. Sections with the same name are merged; a section keeps the position of its first appearance.
  2. Each section's absolute base is computed: an explicit @offset sets it, otherwise it follows the previous section.
  3. Each symbol's absolute address is its section's base plus the length of all symbols declared before it in that section.
  4. All symbol and $symbol references are replaced with those absolute addresses.
  5. 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.