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
.here
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 float as necessary in expressions.
Other types can be converted to integer using the integer type
int.
.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 e
while a binary one is p
.
An underscore can be used between digits as a separator for better
readability. The following operations can be used:
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
, x
or z
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 x
prefix and
z
denotes a z85 encoded byte string. Spaces can be used between pairs of
hexadecimal digits as a separator for better readability.
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 label = binary('filename')
. Now if the labels
are listed to a file then there will be a z85 encoded definition for this
label.
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. Definition is done by collecting
key:value pairs separated by comma between braces {"key":"value",
:"default value"}.
Looking up a non-existing key is normally an error
unless a default value is given. An empty dictionary is {}.
This type is immutable. There are limitations what may be used as a
key but the value can be anything.
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)]
; Symbol accessible values. May be useful as a function return value too.
coords = {.x: 24, .y: 50}
ldx #coords.x
ldy #coords.y
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 |