This is the manual for 64tass, the multi pass optimizing macro assembler for the 65xx series of processors. Key features:
linkerwith section support
Contrary how the length of this document suggests 64tass can be used with just basic 6502 assembly knowledge in simple ways like any other assembler. If some advanced functionality is needed then this document can serve as a reference.
This is a development version. Features or syntax may change as a result
of corrections in non-backwards compatible ways in some rare cases. It's
difficult to get everything right
first time.
Project page: https://sourceforge.net/projects/tass64/
The page hosts the latest and older versions with sources and a bug and a feature request tracker.
64tass is a command line assembler, the source can be written in any text
editor. As a minimum the source filename must be given on the command line. The
command line option is highly recommended if the source is Unicode or
ASCII.
-a
64tass -a src.asm
There are also some useful parameters which are described later.
For comfortable compiling I use such Makefile
s (for make):
demo.prg: source.asm macros.asm pic.drp music.bin 64tass -C -a -B -i source.asm -o demo.tmp pucrunch -ffast -x 2048 demo.tmp >demo.prg
This way demo.prg
is recreated by compiling source.asm
whenever source.asm
, macros.asm
, pic.drp
or music.bin
had changed.
Of course it's not much harder to create something similar for win32 (make.bat), however this will always compile and compress:
64tass.exe -C -a -B -i source.asm -o demo.tmp pucrunch.exe -ffast -x 2048 demo.tmp >demo.prg
Here's a slightly more advanced Makefile example with default action as testing in VICE, clean target for removal of temporary files and compressing using an intermediate temporary file:
all: demo.prg x64 -autostartprgmode 1 -autostart-warp +truedrive +cart $< demo.prg: demo.tmp pucrunch -ffast -x 2048 $< >$@ demo.tmp: source.asm macros.asm pic.drp music.bin 64tass -C -a -B -i $< -o $@ .INTERMEDIATE: demo.tmp .PHONY: all clean clean: $(RM) demo.prg demo.tmp
It's useful to add a basic header to your source files like the one below, so that the resulting file is directly runnable without additional compression:
* = $0801 .word (+), 2005 ;pointer, line number .null $9e, format("%4d", start);will be sys 4096 + .word 0 ;basic line end * = $1000 start rts
A frequently coming up question is, how to automatically allocate
memory, without hacks like *=*+1? Sure
there's .byte and friends for variables with initial values
but what about zero page, or RAM outside of program area? The solution
is to not use an initial value by using
or not
giving a fill byte value to ?.fill.
* = $02 p1 .addr ? ;a zero page pointer temp .fill 10 ;a 10 byte temporary area
Space allocated this way is not saved in the output as there's no data to save at those addresses.
What about some code running on zero page for speed? It needs to be relocated, and the length must be known to copy it there. Here's an example:
ldx #size(zpcode)-1;calculate length
- lda zpcode,x
sta wrbyte,x
dex ;install to zero page
bpl -
jsr wrbyte
rts
;code continues here but is compiled to run from $02
zpcode .logical $02
wrbyte sta $ffff ;quick byte writer at $02
inc wrbyte+1
bne +
inc wrbyte+2
+ rts
.endlogical
The assembler supports lists and tuples, which does not seems interesting at first as it sound like something which is only useful when heavy scripting is involved. But as normal arithmetic operations also apply on all their elements at once, this could spare quite some typing and repetition.
Let's take a simple example of a low/high byte jump table of return
addresses, this usually involves some unnecessary copy/pasting to create a pair
of tables with constructs like >(label-1).
jumpcmd lda hibytes,x ; selected routine in X register
pha
lda lobytes,x ; push address to stack
pha
rts ; jump, rts will increase pc by one!
; Build a list of jump addresses minus 1
_ := (cmd_p, cmd_c, cmd_m, cmd_s, cmd_r, cmd_l, cmd_e)-1
lobytes .byte <_ ; low bytes of jump addresses
hibytes .byte >_ ; high bytes
There are some other tips below in the descriptions.
Integer constants can be entered as decimal digits of arbitrary length. An underscore can be used between digits as a separator for better readability of long numbers. The following operations are accepted:
x + y | add x to y | 2 + 2 is 4
|
x - y | subtract y from x | 4 - 1 is 3
|
x * y | multiply x with y | 2 * 3 is 6
|
x / y | integer divide x by y | 7 / 2 is 3
|
x % y | integer modulo of x divided by y | 5 % 2 is 1
|
x ** y | x raised to power of y | 2 ** 4 is 16
|
-x | negated value | -2 is -2
|
+x | unchanged | +2 is 2
|
~x | -x - 1 | ~3 is -4
|
x | y | bitwise or | 2 | 6 is 6
|
x ^ y | bitwise xor | 2 ^ 6 is 4
|
x & y | bitwise and | 2 & 6 is 2
|
x << y | logical shift left | 1 << 3 is 8
|
x >> y | arithmetic shift right | -8 >> 3 is -1
|
Integers are automatically promoted to floats as necessary in expressions.
Other types can be converted to integer using the integer type
int.
Integer division is a floor division (rounding down) so 7 / 4
is 1 and not 1.75. If ceiling division is required (rounding up) that
can be done by negating both the divident and the result. Typically it's done like 0 - -5 / 4 which results in 2.
.byte 23 ; as unsigned
.char -23 ; as signed
; using negative integers as immediate values
ldx #-3 ; works as '#-' is signed immediate
num = -3
ldx #+num ; needs explicit '#+' for signed 8 bits
lda #((bitmap >> 10) & $0f) | ((screen >> 6) & $f0)
sta $d018
Bit string constants can be entered in hexadecimal form with a leading dollar sign or in binary with a leading percent sign. An underscore can be used between digits as a separator for better readability of long numbers. The following operations are accepted:
~x | invert bits | ~%101 is ~%101
|
y .. x | concatenate bits | $a .. $b is $ab
|
y x n | repeat | %101 x 3 is %101101101
|
x[n] | extract bit(s) | $a[1] is %1
|
x[s] | slice bits | $1234[4:8] is $3
|
x | y | bitwise or | ~$2 | $6 is ~$0
|
x ^ y | bitwise xor | ~$2 ^ $6 is ~$4
|
x & y | bitwise and | ~$2 & $6 is $4
|
x << y | bitwise shift left | $0f << 4 is $0f0
|
x >> y | bitwise shift right | ~$f4 >> 4 is ~$f
|
Length of bit string constants are defined in bits and is calculated from the number of bit digits used including leading zeros.
Bit strings are automatically promoted to integer or floating point as necessary in expressions. The higher bits are extended with zeros or ones as needed.
Bit strings support indexing and slicing. This is explained in detail
in section Slicing and indexing
.
Other types can be converted to bit string using the bit string type bits.
.byte $33 ; 8 bits in hexadecimal
.byte %00011111 ; 8 bits in binary
.text $1234 ; $34, $12 (little endian)
lda $01
and #~$07 ; 8 bits even after inversion
ora #$05
sta $01
lda $d015
and #~%00100000 ;clear a bit
sta $d015
Floating point constants have a radix point in them and optionally an
exponent. A decimal exponent is
while a binary one is e
.
An underscore can be used between digits as a separator for better
readability. The following operations can be used:
p
x + y | add x to y | 2.2 + 2.2 is 4.4
|
x - y | subtract y from x | 4.1 - 1.1 is 3.0
|
x * y | multiply x with y | 1.5 * 3 is 4.5
|
x / y | integer divide x by y | 7.0 / 2.0 is 3.5
|
x % y | integer modulo of x divided by y | 5.0 % 2.0 is 1.0
|
x ** y | x raised to power of y | 2.0 ** -1 is 0.5
|
-x | negated value | -2.0 is -2.0
|
+x | unchanged | +2.0 is 2.0
|
~x | almost -x | ~2.1 is almost -2.1
|
x | y | bitwise or | 2.5 | 6.5 is 6.5
|
x ^ y | bitwise xor | 2.5 ^ 6.5 is 4.0
|
x & y | bitwise and | 2.5 & 6.5 is 2.5
|
x << y | logical shift left | 1.0 << 3.0 is 8.0
|
x >> y | arithmetic shift right | -8.0 >> 4 is -0.5
|
As usual comparing floating point numbers for (non) equality is a bad idea due to rounding errors.
The only predefined constant is pi.
Floating point numbers are automatically truncated to integer as necessary.
Other types can be converted to floating point by using the type float.
Fixed point conversion can be done by using the shift operators. For example
an 8.16 fixed point number can be calculated as (3.14 << 16) & $ffffff.
The binary operators operate like if the floating point number would be a fixed
point one. This is the reason for the strange definition of inversion.
.byte 3.66e1 ; 36.6, truncated to 36
.byte $1.8p4 ; 4:4 fixed point number (1.5)
.sint 12.2p8 ; 8:8 fixed point number (12.2)
Character strings are enclosed in single or double quotes and can hold any Unicode character.
Operations like indexing or slicing are always done on the original representation. The current encoding is only applied when it's used in expressions as numeric constants or in context of text data directives.
Doubling the quotes inside string literals escapes them and results in a single quote.
y .. x | concatenate strings | "a" .. "b" is "ab"
|
y in x | is substring of | "b" in "abc" is true
|
a x n | repeat | "ab" x 3 is "ababab"
|
a[i] | character from start | "abc"[1] is "b"
|
a[-i] | character from end | "abc"[-1] is "c"
|
a[:] | no change | "abc"[:] is "abc"
|
a[s:] | cut off start | "abc"[1:] is "bc"
|
a[:-s] | cut off end | "abc"[:-1] is "ab"
|
a[s] | reverse | "abc"[::-1] is "cba"
|
Character strings are converted to integers, byte and bit strings as necessary using the current
encoding and escape rules. For example when using a sane encoding "z"-"a" is
25.
Other types can be converted to character strings by using the type
str or by using the repr and format
functions.
Character strings support indexing and slicing. This is explained in detail
in section Slicing and indexing
.
mystr = "oeU" ; character string constant .text 'it''s' ; it's .word "ab"+1 ; conversion result is "bb" usually .text "text"[:2] ; "te" .text "text"[2:] ; "xt" .text "text"[:-1] ; "tex" .text "reverse"[::-1]; "esrever"
Byte strings are like character strings, but hold bytes instead of characters.
Quoted character strings prefixing by
, b
, l
, n
, p
, s
or xz
characters can be used to create byte strings. The resulting byte
string contains what .text, .shiftl,
.null, .ptext and .shift would
create. Direct hexadecimal entry can be done using the
prefix and
x
denotes a z85 encoded byte string. Spaces can be used between pairs of
hexadecimal digits as a separator for better readability.
z
y .. x | concatenate strings | x"12" .. x"34" is x"1234"
|
y in x | is substring of | x"34" in x"1234" is true
|
a x n | repeat | x"ab" x 3 is x"ababab"
|
a[i] | byte from start | x"abcd12"[1] is x"cd"
|
a[-i] | byte from end | x"abcd"[-1] is x"cd"
|
a[:] | no change | x"abcd"[:] is x"abcd"
|
a[s:] | cut off start | x"abcdef"[1:] is x"cdef"
|
a[:-s] | cut off end | x"abcdef"[:-1] is x"abcd"
|
a[s] | reverse | x"abcdef"[::-1] is x"efcdab"
|
Byte strings support indexing and slicing. This is explained in detail
in section Slicing and indexing
.
Other types can be converted to byte strings by using the type bytes.
.enc "screen" ;use screen encoding
mystr = b"oeU" ;convert text to bytes, like .text
.enc "none" ;normal encoding
.text mystr ;text as originally encoded
.text s"p1" ;convert to bytes like .shift
.text l"p2" ;convert to bytes like .shiftl
.text n"p3" ;convert to bytes like .null
.text p"p4" ;convert to bytes like .ptext
Binary data may be embedded in source code by using hexadecimal byte
strings. This is more compact than using .byte followed by a lot
of numbers. As expected 1 byte becomes 2 characters.
.text x"fce2" ;2 bytes: $fc and $e2 (big endian)
If readability is not a concern then the more compact z85 encoding may be used which encodes 4 bytes into 5 characters. Data lengths not a multiple of 4 are handled by omitting leading zeros in the last group.
.text z"FiUj*2M$hf";8 bytes: 80 40 20 10 08 04 02 01
For data lengths of multiple of 4 bytes any z85 encoder will do. Otherwise the
simplest way to encode a binary file into a z85 string is to create a source file
which reads it using the line
. Now if the labels
are listed to a file then there will be a z85 encoded definition for this
label.
label = binary('filename')
Lists and tuples can hold a collection of values. Lists are defined from
values separated by comma between square brackets [1, 2, 3], an
empty list is []. Tuples are similar but are enclosed in
parentheses instead. An empty tuple is (), a single element tuple
is (4,) to differentiate from normal numeric expression
parentheses. When nested they function similar to an array. Both
types are immutable.
y .. x | concatenate lists | [1] .. [2] is [1, 2]
|
y in x | is member of list | 2 in [1, 2, 3] is true
|
a x n | repeat | [1, 2] x 2 is [1, 2, 1, 2]
|
a[i] | element from start | ("1", 2)[1] is 2
|
a[-i] | element from end | ("1", 2, 3)[-1] is 3
|
a[:] | no change | (1, 2, 3)[:] is (1, 2, 3)
|
a[s:] | cut off start | (1, 2, 3)[1:] is (2, 3)
|
a[:-s] | cut off end | (1, 2.0, 3)[:-1] is (1, 2.0)
|
a[s] | reverse | (1, 2, 3)[::-1] is (3, 2, 1)
|
*a | convert to arguments | format("%d: %s", *mylist)
|
... op a | left fold | ... + (1, 2, 3) is ((1+2)+3)
|
a op ... | right fold | (1, 2, 3) - ... is (1-(2-3))
|
Arithmetic operations are applied on the all elements recursively,
therefore [1, 2] + 1 is [2, 3], and abs([1,
-1]) is [1, 1].
Arithmetic operations between lists are applied one by one on their
elements, so [1, 2] + [3, 4] is [4, 6].
When lists form an array and columns/rows are missing the smaller array is
stretched to fill in the gaps if possible, so [[1], [2]] * [3, 4]
is [[3, 4], [6, 8]].
Lists and tuples support indexing and slicing. This is explained in detail
in section Slicing and indexing
.
mylist = [1, 2, "whatever"] mytuple = (cmd_e, cmd_g) mylist = ("e", cmd_e, "g", cmd_g, "i", cmd_i) keys .text mylist[::2] ; keys ("e", "g", "i") call_l .byte <mylist[1::2]-1; routines (<cmd_e-1, <cmd_g-1, <cmd_i-1) call_h .byte >mylist[1::2]-1; routines (>cmd_e-1, >cmd_g-1, >cmd_i-1)
Although lists elements of variables can't be changed using indexing (at the moment) the same effect can be achieved by combining slicing and concatenation:
lst := lst[:2] .. [4] .. lst[3:]; same as lst[2] := 4 would be
Folding is done on pair of elements either forward (left) or reverse (right). The list must contain at least one element. Here are some folding examples:
minimum = size([part1, part2, part3]) <? ... maximum = size([part1, part2, part3]) >? ... sum = size([part1, part2, part3]) + ... xorall = list_of_numbers ^ ... join = list_of_strings .. ... allbits = sprites.(left, middle, right).bits | ... all = [true, true, true, true] && ... any = [false, false, false, true] || ...
The range(start, end, step) built-in function can be used to
create lists of integers in a range with a given step value. At least the end
must be given, the start defaults to 0 and the step to 1. Sounds not very
useful, so here are a few examples:
;Bitmask table, 8 bits from left to right
.byte %10000000 >> range(8)
;Classic 256 byte single period sinus table with values of 0–255.
.byte 128 + 127.5 * sin(range(256) * pi / 128)
;Screen row address tables
_ := $400 + range(0, 1000, 40)
scrlo .byte <_
scrhi .byte >_
Dictionaries hold key and value pairs normally but can be used as sets too if simple values are used. In the latter case the values are the keys for themselves.
A dictionary is defined with coma separated values between curly brackets. An
empty one is {}. Key and value pairs are separated with colon,
like { <key>:<value> }. A default value for missing
items is can be defined by leaving out the key before the colon, like {
:<default> }. Simple value don't use a colon {
<value> }.
Looking up a non-existing key is an error unless a default value is given. Dictionaries are immutable. There are limitations what may be used as a key but the value can be anything. As the keys are used for lookups these must be unique.
y .. x | combine dictionaries | {1:2, 3:4} .. {2:3, 3:1} is {1:2, 2:3, 3:1}
|
x[i] | value lookup | {"1":2}["1"] is 2
|
x.i | symbol lookup | {.ONE:1, .TWO:2}.ONE is 1
|
y in x | is a key | 1 in {1:2} is true
|
; Simple lookup
.text {1:"one", 2:"two"}[2]; "two"
; 16 element "fader" table 1->15->12->11->0
.byte {1:15, 15:12, 12:11, :0}[range(16)]
; Variables can be used to build dictionaries incrementally.
md := {1:2}
md ..= {3:4}
The keys can be symbols as well, this allows simple definition of data structures or enumerations.
; Symbol accessible values. May be useful as a function return value too.
coords = {.x: 24, .y: 50}
ldx #coords.x
ldy #coords.y
; Simple enumeration where red = 0, green = 1, blue = 2
colors = dict(.(red, green, blue), range(3))
lda #color.green
; Enumerate register bits as %1, %10, %100, ...
irqbits = dict(.(ta, tb, tod, serial, flag), %1 << range(5))
and #irqbits.flag
Code holds the result of compilation in binary and other enclosed objects. In an arithmetic operation it's used as the numeric address of the memory where it starts. The compiled content remains static even if later parts of the source overwrite the same memory area.
Indexing and slicing of code to access the compiled content might be implemented differently in future releases. Use this feature at your own risk for now, you might need to update your code later.
a.b | b member of a | label.locallabel
|
.b in a | if a has symbol b | .locallabel in label
|
a[i] | element from start | label[1]
|
a[-i] | element from end | label[-1]
|
a[:] | copy as tuple | label[:]
|
a[s:] | cut off start, as tuple | label[1:]
|
a[:-s] | cut off end, as tuple | label[:-1]
|
a[s] | reverse, as tuple | label[::-1]
|
mydata .word 1, 4, 3 mycode .block local lda #0 .endblock ldx #size(mydata) ;6 bytes (3*2) ldx #len(mydata) ;3 elements ldx #mycode[0] ;lda instruction, $a9 ldx #mydata[1] ;2nd element, 4 jmp mycode.local ;address of local label
Addressing modes are used for determining addressing modes of instructions.
For indexing there must be no white space between the comma and the register letter, otherwise the indexing operator is not recognized. On the other hand put a space between the comma and a single letter symbol in a list to avoid it being recognized as an operator.
# | immediate |
#+ | signed immediate |
#- | signed immediate |
( ) | indirect |
[ ] | long indirect |
,b | data bank indexed |
,d | direct page indexed |
,k | program bank indexed |
,r | data stack pointer indexed |
,s | stack pointer indexed |
,x | x register indexed |
,y | y register indexed |
,z | z register indexed |
Parentheses are used for indirection and square brackets for long indirection. These operations are only available after instructions and functions to not interfere with their normal use in expressions.
Several addressing mode operators can be combined together. Currently the complexity is limited to 4 operators. This is enough to describe all addressing modes of the supported CPUs.
# | immediate | lda #$12
|
#+ | signed immediate | lda #+127
|
#- | signed immediate | lda #-128
|
#addr,#addr | move | mvp #5,#6
|
addr | direct or relative | lda $12 lda $1234 bne $1234
|
bit,addr | direct page bit | rmb 5,$12
|
bit,addr,addr | direct page bit relative jump | bbs 5,$12,$1234
|
(addr) | indirect | lda ($12) jmp ($1234)
|
(addr),y | indirect y indexed | lda ($12),y
|
(addr),z | indirect z indexed | lda ($12),z
|
(addr,x) | x indexed indirect | lda ($12,x) jmp ($1234,x)
|
[addr] | long indirect | lda [$12] jmp [$1234]
|
[addr],y | long indirect y indexed | lda [$12],y
|
#addr,b | data bank indexed | lda #0,b
|
#addr,b,x | data bank x indexed | lda #0,b,x
|
#addr,b,y | data bank y indexed | lda #0,b,y
|
#addr,d | direct page indexed | lda #0,d
|
#addr,d,x | direct page x indexed | lda #0,d,x
|
#addr,d,y | direct page y indexed | ldx #0,d,y
|
(#addr,d) | direct page indirect | lda (#$12,d)
|
(#addr,d,x) | direct page x indexed indirect | lda (#$12,d,x)
|
(#addr,d),y | direct page indirect y indexed | lda (#$12,d),y
|
(#addr,d),z | direct page indirect z indexed | lda (#$12,d),z
|
[#addr,d] | direct page long indirect | lda [#$12,d]
|
[#addr,d],y | direct page long indirect y indexed | lda [#$12,d],y
|
#addr,k | program bank indexed | jsr #0,k
|
(#addr,k,x) | program bank x indexed indirect | jmp (#$1234,k,x)
|
#addr,r | data stack indexed | lda #1,r
|
(#addr,r),y | data stack indexed indirect y indexed | lda (#$12,r),y
|
#addr,s | stack indexed | lda #1,s
|
(#addr,s),y | stack indexed indirect y indexed | lda (#$12,s),y
|
addr,x | x indexed | lda $12,x
|
addr,y | y indexed | lda $12,y
|
Direct page, data bank, program bank indexed and long addressing modes
of instructions are intelligently chosen based on the instruction type,
the address ranges set up by .dpage, .databank
and the current program counter address. Therefore the
,
,d
and ,b
indexing is only used in very special cases.
,k
The immediate direct page indexed
addressing
mode is usable for direct page access. The 8 bit constant is a
direct offset from the start of actual direct page. Alternatively it may
be written as #0,d
.
0,d
The immediate data bank indexed
addressing
mode is usable for data bank access. The 16 bit constant is a direct
offset from the start of actual data bank. Alternatively it may be
written as #0,b
.
0,b
The immediate program bank indexed
addressing mode
is usable for program bank jumps, branches and calls. The 16 bit constant
is a direct offset from the start of actual program bank. Alternatively it may
be written as #0,k
.
0,k
The immediate stack indexed
and data stack indexed
#0,s
accept 8 bit constants as an offset from the
start of (data) stack. These are sometimes written without the immediate
notation, but this makes it more clear what's going on. For the same reason the
move instructions are written with an immediate addressing mode #0,r
as well.
#0,#0
The immediate (#) addressing mode expects unsigned values of byte or
word size. Therefore it only accepts constants of 1 byte or in range 0–255
or 2 bytes or in range 0–65535.
The signed immediate (#+ and #-) addressing
mode is to allow signed numbers to be used as immediate constants. It accepts
a single byte or an integer in range −128–127, or two bytes or an integer of
−32768–32767.
The use of signed immediate (like #-3) is seamless, but
it needs to be explicitly written out for variables or expressions
(#+variable). In case the unsigned variant is needed but the
expression starts with a negation then it needs to be put into parentheses
(#(-variable)) or else it'll change the address mode to
signed.
Normally addressing mode operators are used in expressions right after instructions. They can also be used for defining stack variable symbols when using a 65816, or to force a specific addressing mode.
param = #1,s ;define a stack variable const = #1 ;immediate constant lda #0,b ;always "absolute" lda $0000 lda param ;results in lda #$01,s lda param+1 ;results in lda #$02,s lda (param),y ;results in lda (#$01,s),y ldx const ;results in ldx #$01 lda #-2 ;negative constant, $fe
There's a special value for uninitialized memory, it's represented by a
question mark. Whenever it's used to generate data it creates a hole
where the previous content of memory is visible.
Uninitialized memory holes without previous content are not saved unless it's really necessary for the output format, in that case it's replaced with zeros.
It's not just data generation statements (e.g. .byte) that can
create uninitialized memory, but .fill, .align or address manipulation as
well.
* = $200 ;bytes as necessary .word ? ;2 bytes .fill 10 ;10 bytes .align 64 ;bytes as necessary
There are two predefined boolean constant variables, true and false.
Booleans are created by comparison operators (<,
<=, !=, ==, >=, >),
logical operators (&&, ||, ^^,
!), the membership operator (in) and the
all and any functions.
Normally in numeric expressions true is 1 and
false is 0, unless the
command line option was
used.
-Wstrict-bool
Other types can be converted to boolean by using the type bool.
bits | At least one non-zero bit |
bool | When true |
bytes | At least one non-zero byte |
code | Address is non-zero |
float | Not 0.0
|
int | Not zero |
str | At least one non-zero byte after translation |
The various types mentioned earlier have predefined names. These can used for conversions or type checks.
address | Address type |
bits | Bit string type |
bool | Boolean type |
bytes | Byte string type |
code | Code type |
dict | Dictionary type |
float | Floating point type |
gap | Uninitialized memory type |
int | Integer type |
list | List type |
str | Character string type |
symbol | Symbol type |
tuple | Tuple type |
type | Type type |
Bit and byte string conversions can take a second parameter to specify an exact size. Values which can fit in shorter space will be padded but longer ones give an error.
Dictionaries can be built from a single iterable of key and value pairs, or from two iterables where the keys come from the first and the values from the second parameter.
.cerror type(var) != str, "Not a string!"
.text str(year) ; convert to string
Symbols are used to reference objects. Regularly named, anonymous and local symbols are supported. These can be constant or re-definable.
Scopes are where symbols are stored and looked up. The global scope is always defined and it can contain any number of nested scopes.
Symbols must be uniquely named in a scope, therefore in big programs it's hard to come up with useful and easy to type names. That's why local and anonymous symbols exists. And grouping certain related symbols into a scope makes sense sometimes too.
Scopes are usually created by .proc and .block
directives, but there are a few other ways. Symbols in a scope can be accessed
by using the dot operator, which is applied between the name of the scope and the symbol (e.g. myconsts.math.pi).
Regular symbol names are starting with a letter and containing
letters, numbers and underscores. Unicode letters are allowed if the
command line option was used.
There's no restriction on the length of symbol names.
-a
Care must be taken to not use duplicate names in the same scope when the symbol is used as a constant as there can be only one definition for them.
Duplicate names in parent scopes are not a problem and this gives the
ability to override names defined in lower scopes. However this can just as well lead to mistakes if
a lower scoped symbol with the same name was meant so there's a
command line option to warn if such ambiguity exists.
-Wshadow
Case sensitivity can be enabled with the
command line option, otherwise all
symbols are matched case insensitive.
-C
For case insensitive matching it's possible to check for consistent symbol name use
with the
command
line option.
-Wcase-symbol
A regular symbol is looked up first in the current scope, then in lower scopes until the global scope is reached.
f .block
g .block
n nop ;jump here
.endblock
.endblock
jsr f.g.n ;reference from a scope
f.x = 3 ;create x in scope f with value 3
Local symbols have their own scope between two regularly named code symbols and are assigned to the code symbol above them.
Therefore they're easy to reuse without explicit scope declaration directives.
Not all regularly named symbols can be scope boundaries just plain code symbol ones
without anything or an opcode after them (no macros!). Symbols defined as procedures, blocks,
macros, functions, structures and unions are ignored. Also symbols defined by
.var, := or = don't apply, and there are a few more
exceptions, so stick to using plain code labels.
The name must start with an underscore (_), otherwise the same character
restrictions apply as for regular symbols. There's no restriction on the length of the name.
Care must be taken to not use the duplicate names in the same scope when the symbol is used as a constant.
A local symbol is only looked up in it's own scope and nowhere else.
incr inc ac
bne _skip
inc ac+1
_skip rts
decr lda ac
bne _skip
dec ac+1
_skip dec ac ;symbol reused here
jmp incr._skip ;this works too, but is not advised
Anonymous symbols don't have a unique name and are always called as a single plus or minus sign. They are also called as forward (+)
and backward (-) references.
When referencing them
means the first backward,
-
means the second backwards and so on. It's the same for forward, but with
--
. In expressions it may be necessary to put them into brackets.
+
ldy #4
- ldx #0
- txa
cmp #3
bcc +
adc #44
+ sta $400,x
inx
bne -
dey
bne --
Excessive nesting or long distance references create poorly readable code. It's also very easy to copy-paste a few lines of code with these references into a code fragment already containing similar references. The result is usually a long debugging session to find out what went wrong.
These references are also useful in segments, but this can create a nice trap when segments are copied into the code with their internal references.
bne +
#somemakro ;let's hope that this segment does
+ nop ;not contain forward references...
Anonymous symbols are looked up first in the current scope, then in lower scopes until the global scope is reached.
Anonymous labels within conditionally assembled code are counted even if the code itself is not compiled and the label won't get defined. This ensures that anonymous labels are always at the same "distance" independent of the conditions in between.
Constant symbols can be created with the equal sign. These are not re-definable. Forward referencing of them is allowed as they retain the objects over compilation passes.
Symbols in front of code or certain assembler directives are created as constant symbols too. They are bound to the object following them.
Re-definable symbols can be created by the .var directive
or := construct. These are also called as variables. They
don't carry their content over from the previous pass therefore it's not
possible to use them before their definition.
If the variable already exists in the current scope it'll get updated.
If an existing variable needs to be updated in a parent scope then the
::= variable reassign operator is able to do that.
Variables can be conditionally defined using the :?= construct.
If the variable was defined already then the original value is retained
otherwise a new one is created with this value.
WIDTH = 40 ;a constant lda #WIDTH ;lda #$28 variabl .var 1 ;a variable var2 := 1 ;another variable variabl .var variabl + 1;update it verbosely var2 += 1 ;compound assignment (add one) var3 :?= 5 ;assign 5 if undefined
The
symbol denotes the current program
counter value. When accessed it's value is the program counter at the
beginning of the line. Assigning to it changes the program counter and
the compiling offset.
*
Built-in functions are pre-assigned to the symbols listed below. If you reuse these symbols in a scope for other purposes then they become inaccessible, or can perform a different function.
Built-in functions can be assigned to symbols (e.g. sinus = sin), and
the new name can be used as the original function. They can even be passed as
parameters to functions.
floor(-4.8) is -5.0round(4.8) is 5.0ceil(1.1) is 2.0trunc(-1.9) is -1frac(1.1) is 0.1sqrt(16.0) is 4.0cbrt(27.0) is 3.0log10(100.0) is 2.0log(1) is 0.0exp(0) is 1.0pow(2.0, 3.0) is 8.0sin(0.0) is 0.0asin(0.0) is 0.0sinh(0.0) is 0.0cos(0.0) is 1.0acos(1.0) is 0.0cosh(0.0) is 1.0tan(0.0) is 0.0atan(0.0) is 0.0tanh(0.0) is 0.0rad(0.0) is 0.0deg(0.0) is 0.0hypot(4.0, 3.0) is 5.0atan2(0.0, 3.0) is 0.0abs(-1) is 1sign(-5) is -1These functions return byte strings of various lengths for signed numbers, unsigned numbers and addresses.
The naming of functions is not a coincidence and they return the bytes what the data directives with the same names normally emit.
byte(0) is x"00" and
char(-1) is x"ff"word(1024) is x"0004" and
sint(-1) is x"ffff"long(123456) is x"40E201" and
lint(-1) is x"ffffff"dword(123456789) is x"15CD5B07" and
dint(-1) is x"ffffffff"addr(start) is x"0d08"rta(4096) is x"ff0f"all.
| all bits set or no bits at all | all($f) is true
|
| all characters non-zero or empty string | all("c") is true
|
| all bytes non-zero or no bytes | all(x"ac24") is true
|
| all elements true or empty list | all([true, true, false]) is false
|
Only booleans in a list are accepted with the
command line option.-Wstrict-bool
any.
| at least one bit set | any(~$f) is false
|
| at least one non-zero character | any("c") is true
|
| at least one non-zero byte | any(x"ac24") is true
|
| at least one true element | any([true, true, false]) is true
|
Only booleans in a list are accepted with the
command line option.-Wstrict-bool
This function reads the content of a binary file as a byte string. It also accepts optional offset and length parameters.
| Read everything | binary(name)
|
| Skip starting bytes | binary(name, offset)
|
| Some bytes from offset | binary(name, offset, length)
|
sid = binary("music.sid"); read in the SID file as bytes offs := sid[[$7, $6]] ; data offset (big endian) load := sid[[$9, $8]] ; load address (big endian) init = sid[[$b, $a]] ; init address (big endian) play = sid[[$d, $c]] ; play address (big endian) ; if load address is zero then it's the first 2 bytes of data .if load == 0 load := sid[offs:offs+2] ; load address (little endian) offs += 2 ; skip load address bytes .endif * = load ; set pc to load address .text sid[offs:] ; dump music data
The format function converts a list of values into a character string.
The converted values are inserted in place of the % sign. Optional
conversion flags and minimum field length may follow, before the conversion
type character. These flags can be used:
# | alternate form (-$a, ~$a, -%10, ~%10, -10.)
|
* | width/precision from list |
. | precision |
0 | pad with zeros |
- | left adjusted (default right) |
| blank when positive or minus sign |
+ | sign even if positive |
~ | binary and hexadecimal as bits |
The following conversion types are implemented:
b | binary |
c | Unicode character |
d | decimal |
e E | exponential float (uppercase) |
f F | floating point (uppercase) |
g G | exponential/floating point |
s | string |
r | representation |
x X | hexadecimal (uppercase) |
% | percent sign |
.text format("%#04x bytes left", 1000); $03e8 bytes left
| bit string | length in bits | len($034) is 12
|
| character string | number of characters | len("abc") is 3
|
| byte string | number of bytes | len(x"abcd23") is 3
|
| tuple, list | number of elements | len([1, 2, 3]) is 3
|
| dictionary | number of elements | len({1:2, 3:4]) is 2
|
| code | number of elements | len(label)
|
The sequence does not change across compilations and is the same every time.
Different sequences can be generated by seeding with .seed.
floating point number 0.0 <= x < 1.0 | random()
|
integer in range of 0 <= x < e | random(e)
|
integer in range of s <= x < e | random(s, a)
|
integer in range of s <= x < e, step t | random(s, a, t)
|
.seed 1234 ; default is boring, seed the generator
.byte random(256); a pseudo random byte (0–255)
.byte random([16] x 8); 8 pseudo random bytes (0–15)
integers from 0 to e-1 | range(e)
|
integers from s to e-1 | range(s, a)
|
integers from s to e (not including e), step t | range(s, a, t)
|
.byte range(16) ; 0, 1, ..., 14, 15
.char range(-5, 6); -5, -4, ..., 4, 5
mylist = range(10, 0, -2); [10, 8, 6, 4, 2]
.warn repr(var) ; pretty print value, for debugging
var .word 0, 0, 0 ldx #size(var) ; 6 bytes var2 = var + 2 ; start 2 bytes later ldx #size(var2) ; what remains is 4 bytes
If the original list contains further lists then these must be all of the same length. In this case the order of lists is determined by comparing their elements from the start until a difference is found. The sort is stable.
; sort IRQ routines by their raster lines sorted = sort([(60, irq1), (50, irq2)]) lines .byte sorted[:, 0] ; 50, 60 irqs .addr sorted[:, 1] ; irq2, irq1
The following operators are available. Not all are defined for all types of arguments and their meaning might slightly vary depending on the type.
- | negative | + | positive |
! | not | ~ | invert |
* | convert to arguments | ^ | decimal string |
The
decimal string operator will be changed to mean the
bank byte soon. Please update your sources to use ^format("%d", xxx) instead!
This is done to be in line with it's use in most other assemblers.
+ | add | - | subtract |
* | multiply | / | divide |
% | modulo | ** | raise to power |
| | binary or | ^ | binary xor |
& | binary and | << | shift left |
>> | shift right | . | member |
.. | concat | x | repeat |
in | contains | !in | excludes |
Spacing must be used for the
and x
operators or else they won't
be recognized as such. For example the expression in
should be written as [1,2]x2
instead.
[1,2]x 2
Parenthesis (( )) can be used to override operator precedence.
Don't forget that they also denote indirect addressing mode for certain
opcodes.
lda #(4+2)*3
Traditional comparison operators give false or true depending on the result.
The compare operator (<=>) gives −1 for less, 0 for equal and 1 for more.
<=> | compare | ||
== | equals | != | not equal |
< | less than | >= | more than or equals |
> | more than | <= | less than or equals |
=== | identical | !== | not identical |
These unary operators extract 8 or 16 bits. Usually they are used to get parts of a memory address.
< | lower byte | > | higher byte |
<> | lower word | >` | higher word |
>< | lower byte swapped word | ` | bank byte |
lda #<label ; low byte of address
ldy #>label ; high byte of address
jsr $ab1e
ldx #<>source ; word extraction
ldy #<>dest
lda #size(source)-1
mvn #`source, #`dest; bank extraction
Please note that these prefix operators are not strongly binding like negation or inversion. Instead they apply to the whole expression to the right. This may be unexpected but is required for compatibility with old sources which expect this behaviour.
lda #<label+10 ;This is <(label+10) and not (<label)+10
;The check below is wrong and should be written as (>start) != (>end)
.cerror >start != >end;Effectively this is >(start != (>end))
Boolean conditional operators give false or true or one of the operands as the result.
x || y | if x is true then x otherwise y
|
x ^^ y | if both false or true then false otherwise x || y
|
x && y | if x is true then y otherwise x
|
!x | if x is true then false otherwise true
|
c ? x : y | if c is true then x otherwise y
|
c ?? x : y | if c is true then x otherwise y (broadcasting)
|
x <? y | if x is smaller then x otherwise y
|
x >? y | if x is greater then x otherwise y
|
;Silly example for 1=>"simple", 2=>"advanced", else "normal"
.text MODE == 1 && "simple" || MODE == 2 && "advanced" || "normal"
.text MODE == 1 ? "simple" : MODE == 2 ? "advanced" : "normal"
;Limit result to 0 .. 8
light .byte 0 >? range(-16, 101)/6 <? 8
Please note that these are not short circuiting operations and both sides are calculated even if thrown away later.
With the
command line option
booleans are required as arguments and only the -Wstrict-bool
operator may return something else.
?
Special addressing length forcing operators in front of an expression can be used to make sure the expected addressing mode is used. Only applicable when used directly at the mnemonic.
@b | to force 8 bit address |
@w | to force 16 bit address |
@l | to force 24 bit address (65816) |
lda @w $0000 ; force the use of 2 byte absolute addressing
bne @b label ; prevent upgrade to beq+jmp with long branches in use
lda @w #$00 ; use 2 bytes independent of accumulator size
These assignment operators are short hands for updating variables. Constants can't be changed of course.
The variables on the left must be defined beforehand by
or :=
.
.var
Compound assignment operators can modify variables defined in parent scopes as well.
+= | add | -= | subtract |
*= | multiply | /= | divide |
%= | modulo | **= | raise to power |
|= | binary or | ^= | binary xor |
&= | binary and | ||= | logical or |
&&= | logical and | <<= | shift left |
>>= | shift right | ..= | concat |
<?= | smaller | >?= | greater |
x= | repeat | .= | member |
v += 1 ; same as 'v ::= v + 1'
Lists, character strings, byte strings and bit strings support various
slicing and indexing possibilities through the [] operator.
Indexing elements with positive integers is zero based. Negative indexes
are transformed to positive by adding the number of elements to them, therefore
−1 is the last element. Indexing with list of integers is possible as well so
[1, 2, 3][(-1, 0, 1)] is [3, 1, 2].
Slicing is an operation when parts of sequence is extracted from a start
position to an end position with a step value. These parameters are separated
with colons enclosed in square brackets and are all optional. Their default
values are [start:maximum:step=1]. Negative start and end characters are
converted to positive internally by adding the length of string to them.
Negative step operates in reverse direction, non-single steps will jump over
elements.
This is quite powerful and therefore a few examples will be given here:
a[x]
"abcd"[1] results in "b".a[-x]
"abcd"[-2] results in "c".a[:to]
to. So
[10,20,30,40][:-1] results in [10,20,30].a[from:]
from. So
[10,20,30,40][-2:] results in [30,40].a[from:to]
fromand stopping before
to. The two end positions can be positive or negative indexes. So
[10,20,30,40][1:-1] results in [20,30].a[:]
a[::-1]
"abcd"[::-1] is "dcba".a[from:to:step]
stepth element starting from
fromand stopping before
to. So
"abcdef"[1:4:2] results in "bd".
The fromand
tocan be omitted in case it starts from the beginning or end at the end. If the
stepis negative then it's done in reverse.
a[list]
"abcd"[[1,3]] will be "bd".The fun start with nested lists and tuples, as these can be used to create a matrix. The examples will be given for a two dimensional matrix for easier understanding, but this also works in higher dimensions.
a[x]
[(1,2),(3,4)] matrix [0] will give the first row which is (1,2)a[from:to]
[(1,2),(3,4),(5,6),(7,8)] matrix [1:3] will give [(3,4),(5,6)]a[x]
[(1,2),(3,4)] matrix [:,0] will give the first column of all rows which is [1,3]a[:,from:to]
[(1,2,3,4),(5,6,7,8)] matrix [:,1:3] will give [(2,3),(6,7)]And it works for list of indexes, negative indexes, stepped ranges, reversing, etc. on all axes in too many ways to show all possibilities.
Basically it's just the indexing and slicing applied on nested constructs, where each nesting level is separated by a comma.
Two counters are used while assembling.
The compile offset is where the data and code ends up in memory (or in image file).
The program counter is what labels get set to and what the special star label refers to.
Normally both are the same (code is compiled to the location it runs from) but it does not need to be.
;Offset ;PC ;Hex ;Monitor ;Source
* = $0800
.0800 label1
.logical $1000
.0800 1000 label2
* = $1200
.0a00 1200 label3
.endlogical
.0a00 label4
Popular in old TASM code where this was the only way to create relocated code, otherwise it's use is not recommended as there are easier to use alternatives below.
;Offset ;PC ;Hex ;Monitor ;Source
* = $1000
.1000 ea nop nop
.offs 100
.1065 1001 ea nop nop
Changes the program counter only, the compile offset is not changed. When finished all continues where it was left off before.
The naming is not logical at all for relocated code, but that's how it was named in old 6502tass.
It's used for code copied to it's proper location at runtime. Can be nested of course.
;Offset ;PC ;Hex ;Monitor ;Source
* = $1000
.logical $300
.1000 0300 a9 80 lda #$80 drive lda #$80
.1002 0302 85 00 sta $00 sta $00
.1004 0304 4c 00 03 jmp $0300 jmp drive
.endlogical
Changes the program counter to the expression (if given) and discards the result of compilation. This is useful to define structures to fixed addresses.
.virtual $d400 ; base address
sid .block
freq .word ? ; frequency
pulsew .word ? ; pulse width
control .byte ? ; control
ad .byte ? ; attack/decay
sr .byte ? ; sustain/release
.endblock
.endvirtual
Or to define stack "allocated" variables on 65816.
.virtual #1,s
p1 .addr ? ; at #1,s
tmp .byte ? ; at #3,s
.endvirtual
lda (p1),y ; lda ($01,s),y
Alignment is about constraining data/code placement in memory.
The processor architecture doesn't have hard constraints on instruction or data placement still pages (256 bytes) come up quite often in instruction cycle times tables. Or even in errata like the indirect JMP bug which happens only if the word of the vector is crossing such page.
Other components like video chips can only display object if placed at an address divisible by 64 for example.
For code half of an address table might be spared if it's known that all the addresses have the same high bytes. Or if all interrupt routines are on the same page then it's enough to change the low byte of the vector when selecting another one.
Now it shouldn't come as a surprise that the following directives are mainly concerned about how dividing the program counter address gives a certain remainder.
The divisor in this context is called the alignment interval and is usually a number which is a power of two. Quite often 256, so that's the default.
The remainder is called offset and is by default 0. Negative offsets are a convenience feature and are internally corrected by adding the interval to it.
An interval sized memory area is called a page. It's boundary is at it's start. If data spans more than one page it's known as a page boundary cross.
Having a non-zero offset effectively shifts the boundary of a page in memory
further up or down (if negative). An interval of 256 with offset of 8 gives page
boundaries of $1008, $1108 or $1208 for
example.
If the alignment is not good enough some alignment directives might try to
correct it by adding padding. This is by default uninitialized (skip forward) but may be a fixed byte
or anything more complex similarly to what the .fill directive accepts.
When alignment is done within named structures then it's relative to the start of the structure. This means the structure layout will always be the same independent of which address it's instantiated at. Anonymous structures do not change the way the alignment works.
The
command line option can be used to emit
warnings on where and how much padding was necessary for alignment.-Walign
This directive is a passive assertion and checks for a page difference or page crossing.
By default or with a negative interval parameter it verifies that the start and end directives are on the same page. This is what's needed to guard relative branches against jumping across pages:
ldx #3
.page ;now this will execute
- dex ;in 14 cycles for sure
bne -
.endpage
With a positive size parameter it verifies that there's no page cross in the memory range between the directives. This is what's needed to guard against indexed access page cross cycle penalties:
* = $10c0 .page 256 table .fill $40 ;table within the same page .endpage ;different page here but no crossing
Normally a page check results in an error but the
command line option can
reduce it into a warning.
-Wno-error=page
Once this directive reports an error it's time to rearrange the source in a way that the check passes. Or alternatively the alignment directives below can be used to avoid violating the assertion.
This directive is useful when code/data needs to be placed exactly to a page boundary. If that's not already the case sufficient padding is added until the next one is reached.
.align $40 ;sprite bitmap (64 byte aligned)
sprite .fill 63
.align $400 ;screen memory (1024 byte aligned)
screen .fill 1000
.align $400, ?, -8;sprite pointers (last 8 bytes)
spritep .fill 8
.align ; page sized buffer at page boundary
sendbuf .fill 256 ;to avoid indexing penalty cycles
Often the start address is not important only avoiding the page boundary matters.
This often can be achieved without any padding at all. If padding is necessary then this directive works
the same as .align including alignment within structures.
It's typically used to place tables so that absolute indexed read accesses won't suffer page crossing cycle penalties.
.alignblk ;avoid page cross
table .byte 0, 1, 2, 3, 4, 5, 6, 7
.endalignblk
lda table,x ;no cycles wasted on access
In case the stronger guarantee of having both the start and the end directives in the same page is required then the alignment interval needs to be given as a negative number (e.g. −256). This may be necessary for aligning code with relative branches.
If the block size varies based on its memory location then doing the alignment may become impossible.
Using .alignblk in the middle of
executable code is usually problematic as the alignment is done there as well.
This directive can do the alignment padding outside of the execution flow.
rts
.alignpageind pageblk;add alignment padding here
wait ldx #3
pageblk .page ;now this will execute
- dex ;in 14 cycles for sure
bne -
.endpage
By default and with a negative interval it tries to avoids page differences.
With positive intervals page crosses. Same as the .page assertion block.
It is assumed that the padding inserted will move the target block as if it'd be right in front of it. If this isn't the case the alignment will fail.
If the block size varies based on its memory location then doing the alignment may become impossible.
This directive tries to align the target to a page boundary. If not already on one then sufficient padding will be added until the next one is reached.
;Align "pos" to page boundary. It must cased to avoid vio page cssesults in an error but the b>, 6e aligned) to a paget>]].align < sizn varies baseef="#d_align"> ;Align "pos" to pagnd. If thura/a> screen- Ends al>If the age block indn clnment interne option can beet loc/u>[[
datulk" href="#d_r page cre botanchneragesuapnus;2/u> == .al ;now this wilroptinment atule as well. This direignment bw thoonank">.databankand the current program counter aotack index place tablf_pow"Scle penalower wordalled/the nextindex place tablf_pow"> - Arc sieade> is
$40x"fp>If thei>;no cyclwilllt;offs>;nogram b>.byte < - ==
.abranches.s">x"ff0f"p>It's typically Špan> waiible.;bal>[, <fi> member <4>The star label The
# aom" href="#tr> <>Thu .pagebounpenaltyIt is assumait ldx < bound to thebank"> ="k">.pan>3,fill>[, Jock. < ;Alignwill execut "c"