Create or remove constants to be used during assembly. Defines can be set to equal a value, given as the second operand, or they can just be defined to exist. The #defcont
preop continues the value expression of the last define on a new line--this might be useful for making more complicated defines pretty. The #undefine
preop and its alias #undef
make it so the constant is no longer defined.
#define progStart $9D00
#defcont + $95 ; continued the define statement
.org progStart-2 ; evaluates to $9D93
#undefine progStart ; could have also used #undef
#ifdef progStart
.echo "this won't print"
#endif
Performs a first pass of the contents of the file pointed to by the path in its argument. An included file usually has useful macros, defines, and equates that can be used in your source during the second pass (or later in the first pass if you included the file at the top of your source).
#include "ti83plus.inc"
; code
You can also include black-and-white bitmaps (.bmp image files) as follows:
#include "artwork.bmp"
The bitmap must be black-and-white (1-bit grayscale) and the bitmap header can't include the color space information, so there are a lot of restrictions with this. Also, the format prefers 8xN data and will pad the data with 0's to make it wide enough to fit in a byte, even if it's 4xN like this:
60 ; 0 1 1 0 0 0 0 0 <- padding on the right F0 ; 1 1 1 1 0 0 0 0 F0 ; 1 1 1 1 0 0 0 0 60 ; 0 1 1 0 0 0 0 0 F0 ; 1 1 1 1 0 0 0 0 F0 ; 1 1 1 1 0 0 0 0 90 ; 1 0 0 1 0 0 0 0 90 ; 1 0 0 1 0 0 0 0
Here's how I created the above bitmap using GIMP, but you could probably find many other editors that output the bitmap format that SPASM accepts.
Here's how an assembled 8x16 sprite looks in bytecode:
3C ; 0 0 1 1 1 1 0 0 42 ; 0 1 0 0 0 0 1 0 6A ; 0 1 1 0 1 0 1 0 42 ; 0 1 0 0 0 0 1 0 3C ; 0 0 1 1 1 1 0 0 18 ; 0 0 0 1 1 0 0 0 66 ; 0 1 1 0 0 1 1 0 A5 ; 1 0 1 0 0 1 0 1 A5 ; 1 0 1 0 0 1 0 1 A5 ; 1 0 1 0 0 1 0 1 A5 ; 1 0 1 0 0 1 0 1 3C ; 0 0 1 1 1 1 0 0 24 ; 0 0 1 0 0 1 0 0 24 ; 0 0 1 0 0 1 0 0 24 ; 0 0 1 0 0 1 0 0 66 ; 0 1 1 0 0 1 1 0
The #if
and #elif
preops allow you to control what parts of the source code get assembled based on the boolean value of their operands. The #ifdef
and #ifndef
preops work the same except they resolve to true when their operand has been defined or has not been defined respectively. The #else
, and #endif
preops define the true blocks and false blocks that get assembled or ignored as a result of the #if
boolean tests.
For example, assembling the code below with spasm -DDEBUG test.z80 test.bin
will include the call to debug_prt
. Assembling it with spasm test.z80 test.bin
will ignore the call and simply ret
.
#ifdef DEBUG
call debug_prt
.db "this is a debug print",0
ret ; gets assembled if DEBUG is defined
#else
ret ; only gets assembled during production
#endif
There are infinite ways to construct complicated examples of if-skeletons, so I won't show them all but you can figure out where is best for you. Here's just one more, an example of an include guard:
tester.inc:
#ifndef TESTER_INC
#define TESTER_INC
test: ret
#endif
Even if you put #include "tester.inc"
several places in your code, it will only be included one time because after the first time TESTER_INC
will be defined and the #ifndef
will resolve to false, skipping the whole file.
The assembler ignores the block of code between #comment
and #endcomment
, allowing for large and unformatted comments.
#comment ____ this isn't mine / /\ /___/ \ / /\ /\ / / \/ \ / / /\ \ / / / \ \ / / /\ \ \ / / / \ \ \ /___/___/____\ \ \ / / \ \ /\ /___/____________\___\/ \ \ \ \ \ / \___\_____________\___\/ #endcomment
Creates a macro function that performs blind string replacements of its arguments before the code is assembled. These can be simple or arbitrarily complex.
#macro ntstr(value) ; create a null-terminated string
.db value,0
#endmacro
ntstr("hello") ; .db "hello",0
Used for importing binaries directly into the source code. Can be used for sprites and look-up tables. In the case below, msg is an ASCII-encoded file, so #import msg
is effectively the same as .db "super cool message thingy"
and indeed the message prints as expected.
#include "ti83plus.inc"
.org $9D93
.db t2ByteTok,tAsmCmp
bcall(_ClrLCDFull)
ld HL, 0
ld (CurRow), HL
ld HL, Message
bcall(_PutS) ; Displays the string
bcall(_NewLine)
ret
Message:
#import "msg"
.db 0
msg:
super cool message thingy
These are valid preops according to the SPASM source code but they don't do anything.
Place bytes, words, or longs (double-words) into code memory at the current location. The .byte
, .word
, and .long
directives are aliases for .db
, .dw
, and .dl
, respectively.
.db 3, $F5, 17
.db "null-terminated string",0
.byte 3, $F5, 17 ; same thing
.dw 65535, $C0DE
.word 65535, $C0DE ; same thing
.dl $DEADBEEF
.long $DEADBEEF ; same thing
These determine what portions of the code should be included in a listing file, which is intended to allow you to compare assembly to bytecode. Usually put .nolist
before includes and defines and .list
before the code segment
.nolist
#include "file.inc"
#define VALUE
.list
ret
The above yields the following listing file, where you can see ret
has been assembled as C9
:
6 00:0000 C9 - - - ret
This tells the assembler to set the program counter to the two-byte address passed as its argument. The actual address that labels correspond to are all calculated based on the value in PC. Therefore, .org
can be used to set the position of certain blocks of code in memory. It should also be used at the beginning of the program to tell the assembler where in memory the code will be loaded and executed from (9D93
for Ti calculators). Can also be used to leave space between code blocks in a larger program
.org $9D93 ; PC <- $9D93
Fills a region of memory starting at the current value of PC with a constant value, and increments PC to start at the next byte after the fill block. The first argument specifies the number of bytes to fill. The optional second argument specifies the value to fill with. If no second argument is given the region is filled with 0. The .fill
and .block
directives are aliases for each other, so they do the exact same thing
.fill 256, $AB ; fills 256 bytes with $AB
.fill 10 ; fills 10 bytes with 0
.block 256, $AB
.block 10 ; the same thing
Specifies the end of the source code. Can be used to place a text segment at the end for documentation or an appendix. Everything else after the (This should be the functionality of .end
directive will be ignored..end
but as of the latest version of SPASM-ng it ignores the directive completely. Good practice to leave it in for readability)
.org $9D93
ret
.end
.db "this shouldn't get assembled", 0
.db "but it does as of current version :(", 0
Creates a new opcode and takes the following parameters:
.addinstr mnemonic args instr_data size [class extended end_data]
mnemonic
is a word like "add" that indicates what the instruction does.args
is a comma-separated list of the argumentsinstr_data
is the hex code that the opcode assembles to (Forret
this would beC9
)size
is the number of bytes the instruction takes up in memory (can be greater than length ofinstr_data
+end_data
)class
andextended
are not used and can be omitted unless you want to specifyend_data
end_data
is a single hex byte that will be placed afterinstr_data
in the assembled bytecode
SPASM's .addinstr
code supports instr_data
up to 8 bytes in size, but due to a conflict with SPASM's pass-one parser, literals of size greater than 4 bytes are not possible so the instr_data
field is limited to 4 bytes. However, as shown in the example below, 5 bytes are possible by specifying the single-byte end_data
to be placed after a max-length instr_data
segment.
.nolist
.addinstr swap A,B A8A8A8 3
.addinstr supret "" C9C9C9C9 5 cls ext C9
.list
swap A,B
supret
The above code produces the following listing:
6 00:0000 A8 A8 A8 - swap A,B
7 00:0004 C9 C9 C9 C9
C9 - - - supret
Here are some useful instructions:
.addinstr swap A,B A8A8A8 3 ; XOR swap algorithm .addinstr swap A,C A9A9A9 3 .addinstr swap A,D AAAAAA 3 .addinstr swap A,E ABABAB 3 .addinstr swap A,H ACACAC 3 .addinstr swap A,L ADADAD 3 .addinstr clr A AF 1 ; A xor A = 0
They're saved in a file called extops.inc which you can include in your code and use like so:
#include "extops.inc"
clr A ; not in the standard opcode set
ret
Print a value or string to a file or stderr during assembly.
.echo "Writing to stderr"
.echo > rel/path "Writing to this file"
.echo >> rel/path "Appending to this file"
.echo > /home/abs/path "Writing to absolute path file"
.echo >> /home/abs/path "Appending to absolute path file"
Print a value or string to stderr during assembly.
.error "err_string"
Create an equate for a constant value. Each place in the source code where ALIAS
is found, the constant value is substituted. Equates can be used in expressions, and likewise, expressions can be used as the constant value for an alias as long as the value is predetermined at assembly-time.
ALIAS .equ 64
PLUS1 .equ 1+ALIAS
ld A, PLUS1
Shows the string value of a define, so no text replacement is performed before printing the value (see apple
below). Does not work with equates.
#define progStart $9D95
#define apple progStart-2
.show apple
The above code prints APPLE: progStart-2
in the command line during assembly.
Create multiple defines in one line. Each name=expr
pair will create a define with __name
evaluating to expr
. Several pairs can be chained together.
.option apple=56,orange=tacos,bagel="flamingo"
.show __apple
.show __orange
.show __flamingo
The above code prints the following during assembly:
__APPLE: 56
__ORANGE: tacos
__BAGEL: "flamingo"
Moves the program counter to the specified 2-byte absolute address. Also moves the out_ptr
which controls where the file ends to the value specified, so without the last line below, the program would end after $9E20
, but with it the program ends at $9E40
as intended.
.list
.org $9E00
.block 64,1
.seek $9E20 ; place PC into center of .block segment
.db $FF
.seek $9E40 ; sets the end of the program properly
Make sure to read carefully when looking at the listing file, since the addresses at the left are not always in order when using .seek
. Check the addresses of lines 4 and 5.
1 00:0000 - - - - .list 2 00:0000 - - - - .org $9E00 3 00:9E00 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 .block 64,1 4 00:9E40 - - - - .seek $9E20 5 00:9E20 FF - - - .db $FF 6 00:9E21 - - - - .seek $9E40
However, a hexdump
of the assembled binary shows that the operation was indeed performed properly--that is, an $ff
appears in the middle of the .block
segment.
00000000 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 00000010 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 00000020 ff 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 00000030 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
Can be used to set ADL to 0 or 1 (only useful for eZ80 programming). Takes arguments in the following format:
.assume name=value
But I believe .assume ADL={0 or 1}
is the only way this is used at the moment. From the SPASM-ng github page: "This tells the assembler to assume that ADL mode is on. ADL mode means the processor is using 24-bit register pairs and memory addresses, which is [the] calculator's default"