Brass Manual

Table of Contents

Command-Line Invocation

C:\>Brass source.ext [binary.ext [export.ext]] [-switch [-switch [-...]]]

Brass only needs one command-line argument - the filename of the source file you are assembling. You can optionally pass a filename for the binary to be output - it defaults to the filename of the source file with .bin on the end.

You can also specify a file name for the label export table (generate with the .export directive).This defaults to the source filename with tacked on the end.

Switches can be passed as a - dash followed by a single character. The available switches are:

dDebug mode - write a debug log file for Latenite.
eStrict error mode.
lWrite list file (followed by filename).
oDo not write headers on XML error log.
sForce case sensitivity on macros and labels.
tUse external table file (followed by filename).
xExport XML error log.

As far as the error log is concerned, it will write to the file name referenced in the environment variable. Latenite sets this for you, or you can manually set it with the command (on the Windows command-line):

SET ERROR_LOG=filename

Strict error mode stops after a single error. The default is to leave this off, so the assembler will skip over errors -this produces a useless binary, but it's easier to debug when you have a large list of errors to fix rather fixing than one at a time, recompiling, repeating.

Some examples:

Brass file.asmAssembles file.asm as file.bin
Brass file.asm "out"Assembles file.asm as out
Brass file.asm -s hello.exeAssembles file.asm as hello.exe (case sensitive).
Brass file.asm -l hello.lstAssembles file.asm as file.bin and writes listing to hello.lst.
Brass -t file.asmAssembles file.asm as file.bin using

Tested with:

Note that anything highlighed thus is in my test.cmd that I run after every modification to the source to check that I haven't broken anything (it reassembles them all in TASM and Brass, comparing the binaries to ensure they are identical).

Major Known Differences Between TASM 3.2 and Brass

Compilation Process

Brass is a two-pass assembler. In the first pass, each line of the source file is read and parsed. Any label definitions are added to the label list, macros are parsed and each line is translated by the macro preprocessor. No object code is produced until the second pass.

Due to this mode of operation, there are some important things to watch out for:

The assembler does clear/regenerate all macro definitions on each pass to prevent the following problem:

.org $0000

#ifdef bad_macro
.db $FF
.dw $FFFF


#define bad_macro

If the macros weren't cleared and regenerated, in the first pass bad_macro would not have been defined and _label would have had an address of $0002. In the second pass, bad_macro would have been defined, and _label would have had an address of $0001.

The second pass does not perform any macro replacement, just redefinition.

Source Code Syntax

The assembler searches through each line to try and work out what it is dealing with. It works in this order:

  1. If a line's first non-whitespace character is #, . or = then it is treated as an assembler directive.
  2. If the first non-whitespace character encountered is in column 0 (in other words, if the line starts with a non-whitespace character) then it is treated as a label.
  3. In all other cases, the line is treated as assembly code.

Lines can be split up using the \ backslash (for example, _l: ld a, 1 \ call do_something). This functionality is provided solely for backwards compatibility with TASM, and it is strongly recommended that you only use it with the #define directive.

All label names, string constants, .db lists &c are unlimited in length.


Labels can be used in expressions as an alternative to typing and manually calculating memory addresses. A label will, by default, be associated with the current value of the instruction pointer, but you can override this behaviour by using the .equ directive.

Labels that start with the current local label (which defaults to _ underscore) are only directly accessible by name within the current module. For example:

.module Renderer

    ld b, 64
    ld a, b
    call render_row
    djnz _loop
.module AI

    ld b, 32
    ld a, b
    call update_single_monster
    djnz _loop

The _loop labels are not confused because they are only declared in the local module's scope thanks to the leading underscore. They are, in fact, treated as Renderer._loop and AI._loop - so if I wanted to be very strange and jump into the rendering loop from my AI loop I could change the djnz instruction to djnz Renderer._loop.

Prefixing a label name with a : colon returns the label's page number rather than address. For example:

.page 0

    ld hl,function  ; hl = $F001
    ld a,:function  ; a = 4
    ld b,#          ; b = 0 (current page number)
.page 4
.org $F001
    push ix
    call _do_something
    pop ix

Reusable Labels

There is one more special sort of label. These are known as reusable labels. It is pretty likely that you will often need a loop label, and even with modules calling them _loop each time gets a bit painful.

A reusable label is made up of a sequence of + plus or - minus symbols. To avoid ambiguity with the addition and subtraction operators, you must surround them in {curly braces}. When you use the label in an expression, the label that is matched is the closest one behind you (in the case of -) or the closest one in front of you (in the case of +). I guess this really needs an example:

    ld b, 10

-: ; This is label (A)

    ld a, b
    call {+}    ; Calls label (B)
    djnz {-}    ; Loops back to label (A)

+: ; This is label (B)

    jr {+}      ; Jumps to label (C)

+: ; This is label (C)

    jp {-}      ; Jumps to label (A)

Pretty cool. If you need a little more flexibility, such as nesting loops, you can lengthen the names of the labels:

    ld b, 20
--:                 ; (A)
    push bc
    ld b, 30
-:                  ; (B)
    ; Do stuff
    djnz {-}        ; Loops to (B)

    pop bc
    djnz {--}       ; Loops to (A)

One possible pitfall is that it's the label closest to the current instruction pointer and not physical proximity in the source file that is used. Be careful of situations like the following:

.org 0
    call {+}    ; Nice surprise for the user!

.org 100
+:  ; Display a nice picture of a kitten.
    ld hl, kitten
    call display_picture
.org 50
+:  ; Burn out LCD
    ld a, lcd_cmd_burn
    out (lcd_data), a

This code will call the routine at 50, as it is the closest forwards label to the IP.

Variables in Label Names

In some instances, you'll need to give labels names that include a value to keep them individual - for example, when defining a for-loop. Surround the expression in curly braces in the label name - for example:

num = 3

    jr _label_3     ; Jump to the label above
    jr _label_{num} ; Same thing


In the first pass the line is replaced with a another version in which all predefined macros are swapped in. For example,

#define safe_call(address) push af \ call address \ pop af


    ; ...

    xor a

...will be assembled as:

    push af \ call _label \ pop af

    ; ...

    xor a


Comments are denoted with a ;, and run to the end of the line (as far as comments are concerned, the \ backslash does not count as the end of a line).

Numeric Constants

Numbers can be expressed in different 4 bases in Brass:

Hexadecimal (16)$h$FA or 9D94h
Decimal (10)d230 or 45d
Octal (8)@o@023 or 777o
Binary (2)%b%01010101 or 1111b

String Constants

There are two types of constant;

There are a number of escape sequences you can use:

\rCarriage return
\'Single quote
\"Double quote

Note that when using character constants you should not escape the double quote symbol and when using string quotes you should not escape the single quote symbol.


Brass has a fairly stupid expression parser, and as to maintain backwards compatibility with TASM's even more stupid expression parser it is strongly recommended to leave no trace of ambiguity in your expressions and to wrap (parentheses) around everything. In example, TASM would calculate 1+2*3+4*5 as ((((1+2)*3)+4)*5), giving you 65. Brass, however, would calculate it as 27. Be very careful, and to make sure your expression is evaluated correctly type it in as 1+(2*3)+(4*5).

Brass offers the following operators:

&Bitwise AND%1010&%1100%1000
|Bitwise OR%0011|%1001%1011
^Bitwise XOR%1101^%0110%1011
!Boolean NOT!01
~Bitwise NOT (One's complement)~%1011%0100
&&Boolean AND1&&11
||Boolean OR0||11
<<Bitwise shift left%0011<<1%0110
>>Bitwise shift right%0100>>2%0001
Two's complement (negate)5+14
<Less than4<30
>Greater than35>121
<=Less than or equal to32<=(30+2)1
>=Greater than or equal to'p'>='q'0
$Instruction pointer$$9D93
#Current page#0

Any boolean operators treat zero as false and any nonzero to be true.

The ternary operator is a very useful one and can be used as an inline conditional. It takes this form:

(boolean expression)?(returned if true):(returned if false)

If the expression returns true (nonzero) then the the value just after the ? is returned - if the expression returns false (zero) then the value after the : is returned.

Case Sensitivity

Brass is, by default, case insensitive. This means that the label CaseSensitive is the same as the label casesensitive. For compatibility with some source code that takes advantage of a case insensitive assembler, you can switch Brass into a case insensitive mode. This only affects label/module names and macro definitions, though. Assembler directives are always case insensitive, as are Z80 instructions.

Enviroment Variables

The macro preprocessor also substitutes enviroment variables. This can be very useful in an IDE such as Latenite, which passes information to the build scripts in the form of environment variables.

Include environment variables by surrounding them with '[%' and '%]'. For example:

debug_lev   .equ    [%debug_level%]

You could set the variable debug_level on the command line by typing SET debug_level=4 (or whichever value you so wish). These are handled by the preprocessor, and work all over the place - inside string constants, as #define arguments... wherever they are needed, really. If you wish to use a string, you can escape it by surrounding the environment variable name with $dollar signs$, like this:

.echo "Your path variable is [%$PATH$%]\n"

Assembler Directives

Please note that any directives highlighted thus are Brass specific and will not work in TASM. Some TASM directives are ignored by Brass (such as the .nolist and .list directives - Brass doesn't generate any sort of listing file).

General Directives


.org address

Forces the instruction pointer to a new location. Defaults to zero. The output binary is made up of all the bytes from the lowest address ever written to to the highest address ever written to, so watch out that you don't do anything before issuing the .org directive.

.include (also #include)

.include filename

Includes and assembles a file at the current location. You can nest .include statements as deep as you like, just make sure that you don't go into an infinite loop.

Double quotes are optional, but recommended.


.echo expression

This outputs a line of text or the result of an expression to the output console. For example:

    ld (hl), a
    inc hl

.echo "The routine _routine is "
.echo _end_of_routine - _routine
.echo " bytes in size.\n"

Unlike TASM, you can specify multiple arguments by splitting up expressions with commas - for example:

.echo "The routine _routine is ", _end_of_routine - _routine, " bytes in size.\n"



Stops the current source file from assembling (the behaviour is slightly different to TASM in that it only stops the current file from assembling - think of it like PHP's return; after you have loaded a file using require();).


.addinstr instruction args opcode size rule class [shift [or]]

Solely included for backwards compatibility with TASM, this allows you to manually add an instruction to the Z80 instruction set. The rest of the line should follow the format as demonstrated in the TASM table files. (See TASMTABS.HTM from TASM's zip file for more information).



.locallabelchar character

This redefines the current character used to denote a local label (defaults to _). You can make it any single character you like, but is strongly recommended you leave it as a standard, non-alphanumeric, non-operator symbol.


.module name

Tells the assembler which module you are currently in (to limit the scope of local labels). Defaults to noname, passing a blank name to .module will also reset it to noname.

.equ (also =)

label .equ value

This assigns a label with a particular value, so you can then use the label in expressions rather than the constant each time. For example:

size_of_array .equ 30
and_factor = %00001111

    ; ...

    ld b, size_of_array
    ld a, (hl)
    call do_something
    and and_factor
    ld (hl), a
    inc hl
    djnz _loop


.export label [, label [, label [, ...]]]

This tells Brass to add a label name to the label export file. An example would be:

.org $100
    .dw $FAFF
.export start_of_code, end_of_code

This would produce the following label export file:

start_of_code   .equ    $0100
end_of_code     .equ    $0102


.var size, name

This is another way to declare a label, designed to make adding variables which point to some location of safe RAM easier. The size argument is in bytes - 1 declares a byte, 2 a word, 324 a 324 byte region. The name is just any old label name (local label rules still apply!) This directive makes very little sense alone, you need .varloc for it to be of any use...


.varloc location, [size]

This directive is to used with the directive .var to create a bunch of labels which point to variables in areas of memory without you having to manually calculate the offsets in memory yourself. For example, you might currently use:

.define safe_ram $CED5
me_x   .equ    safe_ram+0
me_y   .equ    safe_ram+1
me_dx  .equ    safe_ram+2   ; 2 bytes
me_dy  .equ    safe_ram+4   ; 2 bytes
me_s   .equ    safe_ram+6

...which is pretty rubbish. A better solution would be to make each one an offset of the previous (eg me_dy .equ me_dx+2) but that is still rubbish, as you can't rearrange them. The easiest way is to use .var, like this:

.define safe_ram $CED5
.varloc safe_ram, 128 ; We have 128 bytes of safe RAM here
.var 1, me_x    ; $CED5
.var 1, me_y    ; $CED6
.var 2, me_dx   ; $CED7
.var 2, me_dy   ; $CED9
.var 1, me_s    ; $CEDB

The only reason to declare a size is as an extra precaution; if you overfill your current area of RAM, Brass will warn you for each new variable that it's going into uncharted territory. Specifying a size of 0 (or no size at all) will stop Brass from displaying the warnings.

Naturally, you can redefine .varloc as many times as you wish - .var will just use the last defined variable table location..varloc defaults to location 0.


.db (also .byte and .text) and .dw (also .word)

.db expression [, expression [, expression [, ...]]]

Defines bytes (.db, .byte, .text) or words (.dw, .word).

You can specify a comma-delimited list of expressions. Unlike most places expressions are used, you can include strings. Here are a few examples:

.db 1, 2, 3, 4
.dw $CAFE, $BABE
.db "This is a string", '!', $20, "Here's a \"capital\" E: ", 'e'+('A'-'a'), 0

.incbin (also #incbin)

.incbin filename [, rle] [, label=size] [, start=index] [, end=index] [, rule=expression]

Inserts a binary file straight into the output - no assembling is done. This can be useful to hold data (such as sprites or large amounts of text) in an external file in a raw format, rather than having to expand to multiple .db statements.

A more novel use of .incbin would be to use it to load a preassembled chunk of code into your program. By using the .export directive you could also get it to export the correct label addresses as you assemble the original block.

Setting the flag RLE causes the binary to be RLE compressed using the current RLE mode settings.

Specifying a label name, followed by =size, creates a new label containing the size of the original binary (uncompressed).

.incbin "readme.txt", rle, uncompressed = size

compressed = file_end - file_start

.echo "README.TXT compressed from "
.echo uncompressed
.echo "b to "
.echo compressed
.echo "b ("
.echo (compressed * 100) / uncompressed
.echo "%)\n"

The start and end flags allow you to specify the range of data inside the file that you want to include (zero-indexed). For example, a start = 256 would ignore the first 256 bytes of data in the file. end points to the last byte you want included. start=1, end=3 would include bytes 1, 2 and 3 into the final binary. By combining them with a size label, you could do things like this:

.incbin "hello.txt", start=1, end=total-2, total=size

...which would strip out the first and last byte from the binary.

Last of all is the rule field. This works like the .asciimap directive - for each byte of the binary, an expression is evaluated to translate the byte in some way. The special string {*}represents the byte being translated. For example, the rule rule={*}+1 would add 1 to each byte.


The above rule would perform rot13 on all alphabetic characters. How useful. Note that if you use the $ symbol as the current program counter in your rule then it will be set to the program counter at the start location the binary is loaded into. It is not updated during the translation (as this would cause all sorts of madness).


.rlemode run_indicator [, value_first]

Sets the current RLE mode - first, the byte value used to represent a run (defaults to $91), followed by a flag to set whether the value or the length is written first after the run indicator (defaults to true).

; FILE.BIN contains the string "ABCDDDDDEFG"
.rlemode $91, 1
.incbin "file.bin" ; .db 'A','B','C',$91,'D',5,'E','F','G'
.rlemode $F0, 0
.incbin "file.bin" ; .db 'A','B','C',$F0,5,'D','E','F','G'
.rlemode $00
.incbin "file.bin" ; .db 'A','B','C',$00,'D',5,'E','F','G'


.block size

Advances the program counter by the specified size. This can be useful to allocate space, for example:

string_buffer: .block 256


.chk label

Calculates an 8-bit checksum made up of the sum of all the data between the current instruction pointer and the specified label. Does not produce the same results as TASM, so this is not a recommended directive.

    .db 1
    .db 4
    .db 54
    .chk _label ; Has a value of 59 with Brass, 51 in TASM.

The least significant byte is all that is used.


.fill count [, value]

Outputs count bytes, with the value value. If value is not specified, 255 is substituted. For example, .fill 3, $BB is equivalent to .db $BB, $BB, $BB


.fillw count [, value]

Performs the same operation as .fill except that it outputs words rather than bytes.

.dbrnd and .dwrnd

.dbrnd count, min, max

Outputs count random bytes or words between min and max. For example, you could generate a random 4-character string of letters for one of those annoying website "I am not a machine, honest!" verification things:

.dbrnd 4, 'A', 'Z'

Use .dwrnd if you want to output words.


.asc expression [, expression [, expression [, ...]]]

This performs virtually the same operation as .db with the exception that each byte defined is modified using the current ASCII translation table (declared using .asciimap).


.asciimap start, [end], rule

Defines an ASCII mapping table. In English, this is a special table that can be used to translate strings from the ASCII you're dealing with on your PC to any special variations on the theme on the Z80 device you are assembling to. For example, the TI-83 Plus calculator has a θ symbol where the '[' is normally. Using an ASCII mapping table, you could automatically make any strings defined using the .asc directive handle this oddity. Another possibility would be a font where A-Z is actually 0-25 rather than the usual 65-90.

The first two arguments define the range of characters you are translating, inclusive. You can miss out the second one if you are only redefining a single character. The final argument is a special rule telling Brass how to perform the translation. It is a standard expression, where the special code {*} specifies the current character being translated. Here are a couple of examples:

.asciimap 'a', 'z', {*}+('A'-'a')   ; Force all strings UPPERCASE
.asciimap $00, $FF, {*}             ; Reset to standard mapping
.asciimap ' ', '_'                  ; Turn spaces into underscores
.asciimap 'A', 'Z', {*}+1           ; Make each letter in the range A⇒Z one bigger (A→B, B→C &c)
.asciimap 128, 255, {*}&%01111111   ; Clear the most significant bit (limit to 7-bit ASCII).

.dbsin, .dbcos, .dwsin and .dwcos

.dbsin angles_in_circle, amplitude_of_wave, start_angle, end_angle, angle_step, DC_offset

Use this directive to define trigonometric tables.

First of all, you need to define the number of angles in a complete circle (cycle of the wave). For example, there are 360° in a circle, so to create a table which uses our degrees scale, use 360. A more sensible value to use would be 256, so a complete sinusoidal wave would fit into 256 angles.

Next you need to specify the amplitude of the wave. To use the range of a byte, 127 seems sensible, for example.

The next 3 arguments are used to denote which angles to generate the table from as a range. For example, to output the angles 0-179 for a half wave (when using a 360° table), you would specify 0, 179, 1. You could, for example, skip every other angle by using 0, 179, 2, or run backwards with 179, 0, -1 (note ordering of arguments!)

Last of all is the DC offset applied to your wave. 0 centres it around 0, a positive value shifts the wave up and a negative value shifts the wave down.

It might be clearer to see some pseudo-code for the way the table is generated:

for angle is start_angle to end_angle step angle_step
    output_value(DC_offset +
        amplitude_of_wave *
            sin((angle / angles_in_circle) * 2π)
next angle

The .dbsin and .dwsin directives generate a sine table, .dbcos and .dwcos generate a cosine table. Needless to say, the .db* versions output bytes, the .dw* versions output words.


.define (also #define)

.define name[(argument [, argument [, ...]])] replacement

Defines a new (TASM-style) macro. Macros are basic find-and-replace operations, and as such lines of code are modified before being assembled. Because of this, you can do some silly things with macros; for example make XOR perform an AND operation, or call the accumulator fred.

The simplest macro will take one thing and replace it with another, such as:

.define size_of_byte 8
    ; ...
    ld a, size_of_byte

When the macro preprocessor sees the line ld a, size_of_byte it will get to work and replace it with ld a, 8.

It is important to realise that this is handled by the preprocessor - long before the actual code is even sent to the main assembler. As far as the actual assembler is concerned, that line never was never ld a, size_of_byte - it was always ld a, 8. The preprocessor only runs at the start of the first pass - this is why you cannot forward-reference macros. The reason for this is that one macro can affect another.

To give the macros a little more power, it is possible to define a macro that takes some arguments. The arguments are a comma-delimited list of argument names, and any instance of them in the replacement section of the macro will be substituted by the value passed when the macro is called. For example:

.define big_endian(value) .db value >> 8 \ .db value & $FF
    ; ...

This would assemble as .db $F001 >> 8 \ .db $F001 & $FF, displaying $F0, $01 in a hex editor, rather than the usual $01, $F0.

Multiple arguments aren't much more difficult:

.define call_on_success(test, success) call test \ or a \ call z, success
    ; ...
    call_on_success(open_file, read_file)
    ; ...

    ld a, (hl)  ; (hl) contains 0 if file exists, 1 if it doesn't.

    ; This will not get called if open_file fails (returns non-zero).

One special case macro is one where you don't give it any replacement and no arguments, such as .define FISHCAKES. In this case, the macro replaces itself with itself (so FISHCAKES becomes FISHCAKES), not nothing. However, a test to see if the macro exists through .ifdef FISHCAKES will still be true.

Another difference between TASM and Brass is that Brass has a more advanced macro system. A single macro name (such as call_on_success above) can have multiple replacements, and the correct one is identified by the replacement signature.

A replacement signature is the internal representation of the argument list in a macro. By default, each argument is treated as a wildcard, but by surrounding it with {} curly braces you can force it to be a particular string, for example:

.define my_macro(label) call label                              ; Signature of *
.define my_macro(label, variable) ld a,variable \ call label    ; Signature of *,*
.define my_macro(label, {a}) call label                         ; Signature of *,a
.define my_macro({0}, variable) call something \ ld a,variable  ; Signature of 0,*

The advantage of this is that you can create multiple macros - one being a general case macro, the others being specific cases where you can apply optimisations. Here's an example - let's say you had a function called sqr_root that you wanted to wrap in a macro for some reason. Here's the TASM approach:

#define sqrt(var) ld a,var\ call sqr_root

    sqrt(43)    ; Generates ld a,43\ call sqr_root
    sqrt(0)     ; Generates ld a,0\ call sqr_root (could be xor a!)
    sqrt(a)     ; Generates ld a,a\ call sqr_root (oh dear)

The Brass version would be:

.define sqrt(var) ld a,var\ call sqr_root
.define sqrt({0}) xor a\ call sqr_root
.define sqrt({a}) call sqr_root

    sqrt(43)    ; Generates ld a,43\ call sqr_root
    sqrt(0)     ; Generates xor a\ call sqr_root
    sqrt(a)     ; Generates call sqr_root

To make this sort of thing easier for yourself, it's a good idea to create a list of useful macros that handle the basic cases for you - for example, as a ld a,* replacement:

.define ld_a(var) ld a,var
.define ld_a({0}) xor a
.define ld_a({a})
; Now we have a sensible ld a,* macro replacement, use it to build the rest:
.define sqrt(var) ld_a(var)\ call sqr_root
.define cbrt(var) ld_a(var)\ call cube_root

Another possible use of this is to be able to assign defaults to arguments (Ion's sprite routine springs to mind - how often do you use it to display a non-8x8 sprite?)

; Assume ld_a()/ld_b() macros defined as above.
; _display is a function to display the number in 'a' in a number of bases:
; 'b' specifies which base we want to print it in.

.define display_in_base(var, base) ld_a(var)\ ld_b(var)\ call _display
.define display_in_base(var)       ld_a(var)\ ld b,10\ call _display

display_in_base(43,2)   ; Display 43 in base 2.
display_in_base(65,16)  ; Display 65 in base 16.
display_in_base(124)    ; Display 124 in base 10 (default).

.defcont (also #defcont)

.defcont replacement

In TASM, this is a way to get around the 255 column limit and also to split your .define statements onto multiple lines. It tacks replacement onto the end of the last defined macro. For example:

.define big_macro(arg) ld a, arg
.defcont \ push af
.defcont \ call blort
.defcont \ cp arg \ ret nz
.defcont \ inc a \ call
.defcont blort

That defines the same macro as this:

.define big_macro(arg) ld a, arg \ push af \ call blort \ cp arg \ ret nz \ inc a \ call blort

If you want to confuse people, don't forget to stick other code between the .define and the .defcont. Or not...



.binarymode mode

This directive specifies the format of the output binary. Mode can be one of the following:

RawPlain, unformatted binary. (Default)
IntelIntel hex format.
IntelWordIntel hex word address format.
MOSMOS Technology hex format.
MotorolaMotorola hex format.
TI73TI-73 binary (*.73?)
TI82TI-82 binary (*.83?)
TI83TI-83 binary (*.83?)
TI8XTI-83+ binary (*.8x?)
TI85TI-85 binary (*.85?)
TI86TI-86 binary (*.86?)


.variablename name

For binary modes that support a variable name, such as the TI output formats, you can use this directive to specify the variable name. It defaults to the filename (minus extension) of the source file.


.tivariabletype type

Specify a variable type if outputting a TI binary. This defaults to an edit-locked program for the current platform (note that it's not always the same on all platforms - so $06 on a TI-83 or $12 on a TI-85).


.binaryrange start, end

Force the output binary to span the declared range, rather than just between the lowest and highest memory addresses written to overall.


.binaryfill value

Specifies the value used when a byte is left undefined in the output binary (defaults to $FF).


.if (also #if)

.if expression

If the expression evaluates to true (non zero) then the following block of code is assembled until another conditional statement is hit. If it evaluates to false (zero) then the following block of code is skipped until another conditional statement is hit.

.else (also #else)


If a preceding conditional statement evaluated to false, the following block of code will be assembled instead. If it evaluated to true, the following block of code will be skipped.

.endif (also #endif)


The current top-level conditional is cleared, and the following code is assembled as normal. An example of these 3 statements could be:

age = 19

.if age < 18
    .echo "You are below the legal drinking age.\n"
    .echo "Here, have a pint.\n"

.elseif (also #elseif)

.elseif expression

If the preceding if statement failed, this expression is evaluated. It works a bit like a second if statement to replace the first if it doesn't work:

#if age > 300
.echo "Sorry, we don't serve spirits.\n"
#elseif age < 18
.echo "You are below the legal drinking age.\n"
#elseif age < 21
.echo "Can I see some ID, please?\n"
.echo "Here, have a pint.\n"


.ifdef (also #ifdef)

.ifdef macro

Works in the same manner as .if, except rather than evaluate an expression it continues assembling if the macro macro exists, and skips assembling if the macro macro does not exist.

.ifndef (also #ifndef)

.ifndef macro

Is an inverted version of .ifdef in that it assembles if the macro macro does not exist.

.elseifdef (also #elseifdef) and .elseifndef (also #elseifndef)

.elseifdef macro

The same as .elseif except that it uses the .ifdef or .ifndef conditionals:

#define X marks_the_spot

#ifdef X
.echo "X is defined.\n"
#elseifdef Y
.echo "Y is defined.\n"
.echo "Neither X nor Y are defined.\n"

Assembler Flow Control


.for label, start, end, [step]

This directive is used to assemble a block of code multiple times. For example; unrolling loops or defining blocks of data programmatically.

A label is created and set to the value of start. The code is assembled between the .for directive and the matching .loop, and the value of the label is adjusted by step at the end of each loop. If it goes beyond the value of end, the label is freed and the assembler carries on.



This directive terminates the last defined .for loop.

.for i, 0, 7
.db 1<<i

; This assembles as:
.db %00000001
.db %00000010
.db %00000100
.db %00001000
.db %00010000
.db %00100000
.db %01000000
.db %10000000

Using Brass's conditionals, you can use it to assemble slightly different blocks of code on each loop. For example, a sprite routine that would need to do the same shifting operations but in one case AND the mask then OR the sprite data:

.for i, 1, 2
    call _shift_sprite
_sprite_loop_{i}:   ; To ensure different label names each loop
    ld a,(de)
    .if i == 1
        and (hl)
        or (hl)
    ld (hl),a
    inc hl
    inc de
    call _update_pointers
    djnz _sprite_loop_{i}

Naturally, for-loops can be nested.

.for y,0,7
    .for x,0,7
        .db x+y*2

File Operations


.fopen handle, filename

This opens the file specified by filename and creates a file handle which can be used to perform any of the below file operations with.


.fclose handle

This closes the file handle handle. All open file handles are automatically closed for you on each pass.


.fsize handle, label

This gets the size of the file handle and stores the result in the label label.


.fread handle, label

This reads a byte (.fread) or word (.freadw) from the file handle and stores the result in the label label. The position of the pointer in the file stream is shunted along to point to the next byte/word.

; Open the file 'hello.txt' and include the data, stripping out the letter 'e':

.fopen fhnd, "hello.txt"        ; fhnd is our handle
.fsize fhnd, hello_size         ; hello_size = size of "hello.txt" in bytes

.for i, 1, hello_size           ; Go through each byte...
    .fread fhnd, chr            ; Read a byte and store it as "chr"
    .if chr!='e' && chr!='E'    ; is it an "e"?
        .db chr                 ; No, so output it.

.fclose fhnd                    ; Close our file handle.


.fpeek handle, label

This reads a byte (.fpeek) or word (.fpeekw) from the file handle and stores the result in the label label. The file pointer is not updated.


.fpos handle, label

Returns the file pointer position in file handle and stores the result in label.


.fseek handle, address

Sets the file pointer position in file handle to position address (in bytes).

; Open the file 'hello.txt' and include the data, reversing it.

.fopen fhnd, "hello.txt"        ; fhnd is our handle
.fsize fhnd, hello_size         ; hello_size = size of "hello.txt" in bytes

.for i, hello_size - 1, 0, -1   ; Go through each byte, backwards.
    .fseek fhnd, i
    .fpeek fhnd, chr
    .db chr

.fclose fhnd                    ; Close our file handle.



.defpage number, offset [, size [, origin]]

This defines a binary page for the output. By default, the output is configured to be a single, 0-offsetted 64KB page (with a page number of 0), which is usually enough for simple programs/platforms. Breaking up your program into a series of pages is usually the way around the 64KB addressable memory limit of the Z80 CPU. How your device pages different areas of memory is entirely device specific, but these routines try to help you.

Let us suppose our mythical device uses 8KB pages. It has 16KB total memory; the first 8KB of memory is fixed as page 0; the last 8KB is swappable by writing the required page number to port $40. We could set up the paging like this:

.binaryrange $0000, $5FFF

.defpage 0, $0000, $2000, $0000 ; $2000 is 8KB
.defpage 1, $2000, $2000, $2000 ; Page #1 is  8KB into the output
.defpage 2, $4000, $2000, $2000 ; Page #2 is 16KB into the output

.page 0 ; This is page 0.

; Swap in page #1:
    ld a,1
    out ($40),a
    call $2000 ; C will now be 1.

    ld a,2
    out ($40),a
    call $2000 ; C will now be 2.

.page 1 ; Page 1...
    ld c,1

.page 2 ; Page 2...
    ld c,2

I'm sorry if this seems a little confusing, I'll try to explain it as best as I can.

For our imaginary device, there is only memory addresses $0000 up to $3FFF. The first $2000 bytes are always occupied by page 0. The $2000-$3FFF range will either be page 1 or page 2; regardless of which page is loaded in there, the range of addresses is $2000 to $3FFF. We cannot store binary files in this manner, sadly, which means that when we output the file we put page 2 after page 1. The addresses of page 2 are still calculated as if they were in the $2000-$3FFF range, even though the data is stored at $4000-$5FFF. We assume that the ROM burner or emulator will be able to work out how to arrange the file on the media correctly.


.page number

Switch to assembling on a particular page.