21. Assembler

The assembler enables writing assembly source files for C projects or standalone assembly language projects.

21.1. Overview

Unlike Standard C, there is no standard assembly language. The syntax for assembly instructions used by the 65816 assembler mostly follows what is outlined in guides made by the vendor of the instruction set.

Assembly source expressions are heavily inspired by C-style operators and precedence rules.

The assembler processes assembly source code using the C preprocessor, allowing C comments, include files, and conditional inclusion.

Directives are largely specific to the Calypsi C compiler tool chain, appearing consistent across products from Calypsi. Directive names begin with a leading dot, a common practice in many, but not all, assemblers.

Note

The dot prefix is used to prevent potential future conflicts where a mnemonic name might clash with a commonly used directive in the product line. This also makes directives stand out in the source code.

21.2. Syntax

Source file format

Assembly source lines follow the traditional format: a label field in the first column, followed by an instruction that may take one or more operands. Comments are preceded by a semicolon:

[label[:]]   [instruction  [operands]]  [; comment]

A label typically starts in the first column. If leading spaces are present, a label must be followed by a colon or enclosed in back quotes (see Symbol syntax below).

An instruction can be a mnemonic (target instruction name) or a directive. All directives begin with a leading dot as part of their name.

A label, a symbol describing a location, is defined by placing it in the first column of a source line, optionally followed by a colon.

Labels can also be declared by importing them with the .extern directive or by value using the .equlab directive.

Predefined words in instructions (mnemonics and operands) are case insensitive, while symbols are case sensitive. Some examples follow:

; Assembler comments start with a semicolon
;
Loop:         dex         ; assembler comment
              nop
              BNE  Loop

Predefined words are case-insensitive because both uppercase and lowercase assembly styles are commonly used.

Symbol syntax

Symbols are case sensitive and can be of arbitrary length. A symbol starts with a letter or underscore, followed by letters, underscores, and digits. Examples: _4, a_symbol, abc, Test1.

Any character is permitted in a symbol, provided the symbol is quoted with a back-tick. Examples: `another symbol` and `Table: 5`.

Intermixing symbols with the same name but different cases is possible, though considered poor style. Symbols in as65816 are case-sensitive primarily to support the C compiler, which also has case-sensitive symbols. This also encourages consistent mixed-case symbols in assembly source code, which is considered good practice.

Preprocessor

The assembler uses a full-featured C preprocessor for input source files. It provides standard C preprocessor features, including header file inclusion, macro expansions, conditional compilation, and C-style comments.

For an introduction with examples on using the C preprocessor, refer to Wikipedia.

See Predefined macros for available macros, most of which are also relevant to the assembler.

21.3. 65816 assembly

The use of direct page is quite central on the 65816 target. If you have an operand that cannot be solved at assembly time, the assembler will use the longer absolute addressing. You can prefix an operand with dp: to force the shorter direct page addressing, the following example taken from a list file shows how it works:

0001                              .extern foo
0002  000000 ad....               lda foo
0003  000003 a50a                 lda 10
0004  000005 a5..                 lda dp:foo

Loading individual parts of an address can be done using relocation operators, such as .word0 and .word2, see Relocation operators for more information. In many 65816 assemblers angle brackets are used for these (< and >). Here they are named somewhat more uniform with directives and a notation that should be fairly easy to read and understand.

0001                              .extern foo, bar
0002  000000 a9..                 lda     ##.word0 foo
0003  000002 85..                 sta     dp:bar
0004  000004 a9..                 lda     ##.word2 foo
0005  000006 85..                 sta     dp:bar+2

Immediate size

The 65816 can operate in 8-bit or 16-bit mode. There are two such modes, accumulator mode and index register mode, which can be set to either 8-bit or 16-bit independently.

Contrary to some 65816 assemblers, the Calypsi assembler does not attempt to keep track of the current runtime mode settings when it processes instructions. Thus, each instruction is treated independently of each other and from the actual mode settings. Properly matching instructions with runtime mode settings is the sole responsibility of the developer.

This mainly affects the immediate addressing mode where the size of immediate operand depends on the active mode settings at runtime. The assembler can emit either 8-bit or 16-bit immediate instruction operands and you specify which by using either a single or double hash mark:

0001  000000 a23412                ldx     ##0x1234      ; 16-bit immediate
0002  000003 a9ab                  lda     #0xAB         ;  8-bit immediate

Addressing mode range

As mentioned there is a dp: facility that can be put before an address expression to specify that the direct page is intended. There are also abs: and long: that can be used in to describe 16 and 24 bits addressing to be used.

For expressions that can be solved in the assembler, the shortest possible addressing is assumed if no such prefix modifier is present.

0001  000000 b50a                 lda  dp:10,x
0002  000002 bd0a00               lda  abs:10,x
0003  000005 bf0a0000             lda  long:10,x
0004  000009 b50a                 lda  10,x
0005  00000b bd0010               lda  0x1000,x
0006  00000e bf001033             lda  0x331000,x

21.4. Sections

The assembly source file is divided into sections using the .section directive. Each section, an indivisible unit of code or data, is laid out in memory by the linker according to rules from a placement rules file.

If no .section directive is specified, the assembler defaults to a .section code in the source file, meaning you start in a section named code unless otherwise directed.

Multiple sections enable more flexible placement of code and data by the linker than a single section. A section name can be used multiple times, with each instance creating an individual section fragment.

All sections are relocatable due to the ELF file format and must be defined in the linker rules file.

Section kinds

The following table describes the supported section kinds. If unspecified, the section defaults to text.

Section kinds and modifiers are case-insensitive.

Section kind

Description

text

Executable code.

data

An initialized read/write data section in memory (RAM).

rodata

An initialized read-only data section in memory (ROM).

bss

“Block Started by Symbol”; holds zero initialized variables and variables not given an initializer value. The C runtime normally zero fill such area before calling main().

Section modifiers

Section modifiers describe further behavior and can be specified as positive or negative.

Section modifier

Description

noreorder

Maintains the relative order of section fragments with the same name within a translation unit.

reorder

Allows section fragments of the same name to be placed arbitrarily, regardless of other fragments with the same name. This is the default.

root

Always include this section fragment in the program. This is the default for object files.

noroot

Only include this section fragment in the program if someone refers to a label inside it. This is the default for library files.

Note

Section fragments with the same name and the noreorder modifier are combined by the linker into a single placement group, ensuring contiguous placement. The absence of noreorder (or explicit reorder) allows the linker to place each section fragment arbitrarily, regardless of other fragments with the same name.

Section alignment

The .align directive specifies an alignment in address units. It advances the location counter and inserts fillers if needed, ensuring the next location has the specified alignment.

; Ensure that "table" label is placed at an address that can be
; evenly divided by 4.
        .section data
        .align   4
table:  ...

21.5. Expressions

Numeric expressions operate in signed (2-complement) mode. A range check occurs based on value usage; exceeding the possible range results in an error. The Operators table lists standard operators, supplemented by specialized relocation and section operators (see Relocation operators and Section operators).

You can optionally use spaces between values and operators in an expression.

Table 21.1 Operators

Operator

Precedence

Purpose

~

9

Unary bit-wise not

!

9

Unary logical not

-

9

Unary negate

+

9

Unary plus

*

8

Multiply

/

8

Divide

%

8

Modulo

+

7

Add

-

7

Subtract

<<

6

Bit shift left

>>

6

Bit shift right

>

5

Greater than

<

5

Less than

>=

5

Greater than or equal

<=

5

Less than or equal

==

4

Equal

!=

4

Not equal

&

3

Bit-wise and

^

2

Bit-wise exclusive or

|

1

Bit-wise or

21.6. Numeric constants

Integer constants can be entered in decimal, binary, octal, or hexadecimal. Decimal integers are sequences of digits not starting with zero. Prefixes 0x indicate hexadecimal, 0b binary, and 0 (followed by digits) octal.

Character constants are also supported, replaced by their ASCII value.

Some examples:

table:     .byte   0x1ff         ; hexadecimal number
           .byte   077           ; octal number (corresponds to decimal 63)
           .byte   65            ; decimal 65
           .byte   'A'           ; ASCII 65 (decimal)
           .byte   0b1011        ; binary (corresponds to decimal 11)
           .byte   0             ; zero (actually octal, but it is written
                                 ;       the same way in decimal)

21.7. Location counter

The assembler converts source programs into machine code for the 65816 processor. Instructions occupy consecutive memory locations until the next .section directive or end of file. The current instruction address is accessed by a single period (.), often called “dot”.

The dot label, representing the current instruction location, is the location counter. It increments after each instruction, always holding the start address of the current instruction.

The location counter address is resolved by the linker but can be used in expressions. Short branches can use the location counter:

test:      ldx     #0
           lda     (table),y
           bpl     .+3           ; skip next instruction of positive
           dex

However, using labels or local labels is often preferred (see below).

21.8. Local labels

Local labels are useful for local branch destinations in assembly source files and come in two variants, dollar-postfix alphanumeric and plus/minus sign character labels.

Dollar postfix style

Local labels end with a single $; the name can be an identifier or numeric (e.g., loop$ or 3$). A local label is active only between two non-local labels and cannot be exported to the linker. They serve as temporary locations for short-distance branching, typically for skips or local loops.

loop$:        lda   (ptr),y
              beq   15$
              iny
              dex
              bne   loop$
              bra   50$
15$:          lda   #7

After a non-local label, you can no longer refer to any local label before it. Consider if the following code is added below the one above, the presence of the non-local label foo resets the local labels:

foo:          ldx   #5
              bne   loop$         ; error, loop$ above no longer visible

As an alternative, you can use ordinary labels related to the context and add a number to make it unique. What you do is mostly a matter of taste.

Sign style

Local labels can also be created using sequences of + or - characters. The entire label name must use the same character, and its length is used for matching. References to labels with minus characters go backwards to the closest match, while plus characters go forward to the closest match.

This allows easy determination of whether the destination label is before or after an instruction. Sign-style local labels can pass over non-local labels.

+             bne     +             ; this one goes to first + below
--
              beq     --            ; backward
+:            beq     ++++          ; goes over foobar
foobar:                             ; I am not in the way
              nop
++++:

Note

Sign-style local labels are only usable with branch-style instructions. They are not allowed in more elaborate operands where an ordinary label is allowed.

21.9. Directives

All directives start with a single dot character.

The following table summarizes the directives known to the assembler:

Directive

Purpose

.section section-name, argument-list

Generate code for given section

.equ expr

Define a symbol value

.equlab expr

Define a symbol value that corresponds to a memory location

.public symbol-list

Export symbols to the linker

.global symbol-list

Synonym to .public

.globl symbol-list

Synonym to .public

.pubweak symbol-list

Export weak symbols to the linker

.extern symbol-list

Import symbol from other module

.require symbol-list

Require symbol from other module

.incbin filepath

Include a binary file as data

.rtmodel symbol, string

Define a runtime model attribute

.byte expr-list

Emit 8 bit values

.word expr-list

Emit 16 bit values

.address expr-list

Emit 24 bit values

.long expr-list

Emit 32 bit values

.quad expr-list

Emit 64 bit values

.ascii text

Emit the given string

.asciz text

Emit the given string with a terminating 0 (C string)

.fillto address [, value]

Fill memory with value

.space count [, value]

Fill memory with value

.align value

Specify alignment

.macro parameter-list

Define a macro

.endm

End a macro definition

.end

Stop processing the source file

A symbol-list is a list of symbols separated by commas. An expr-list is a list of expressions separated by commas.

Directives in detail

.section

The .section directive takes the name of the section as the first argument. It can optionally be followed by a kind and modifiers to describe the section further.

; A code section named "code" (text)
        .section code

; A code section named "code" (text) that are not stored
; in relative order to other "code" section fragments in
; the same compilation unit.
        .section code, reorder

; A data section named "storage"
        .section storage, data

; A constant area in ROM that are always included in output,
; even when put in a library (provided that anything
; in the compilation unit is referenced).
       .section table, rodata, root

.equ

Introduces a new symbol and gives it a value. The symbol appears in the label column and may have an optional colon after it:

BufNo         .equ  7
BufSize:      .equ  2 + ContentSize

Any expression can be used as a value, however using external symbols is subject to certain limitations imposed by relocations in the ELF object file format. In the case of valid expressions with external symbols, the value will be resolved by the linker.

.equlab

Constants can also be defined with the .equlab directive. This is similar to the .equ directive, with the difference that it defines a label, which describes a location, rather than a plain number:

MyLocation:   .equlab  0xD7

Where this matters is in the interpretation of the debugging information. Labels defined using .equlab are treated the same as other location labels. The debugger will understand that a symbol defined using .equlab can be used as a location when generating disassembly listings, while symbols defined using .equ will not be used for locations in the disassembly listing.

.public

Exports a symbol in the current file and makes in visible to other modules. Symbols are local to the file being assembled by default.

For shared definitions, an alternative is to put them in an include file and define them using the .equ directive.

.global

Synonym to .public for compatibility with other assemblers.

.globl

Synonym to .public for compatibility with other assemblers.

.pubweak

A weak symbol is created using the .pubweak directive in a similar way to .public. The difference is that a weak symbol may exist in multiple copies. Of these potentially multiple copies, one is selected by the linker. If there is a non-weak symbol among the weak ones, it will be picked by the linker.

Weak symbols serve a couple of purposes. They can be used for library replaceable objects where you can override a default library object using a non-weak public symbol. They are also useful for tools that generate assembly code where an identical construct may be generated multiple times, though only one is needed in the end.

.extern

Imports a symbol that is defined in some other module and make it visible in the current source file.

.require

A required symbol can be specified with the .require directive. It works similar to the .extern directive, with the difference that you do not need to actually use the symbol in any expression, it is being pulled in by the linker regardless.

This is mostly of interest when building modular software using libraries.

The typical use of this is that you have initialization code somewhere else built up using section fragments with the no-reorder property. The no-reorder property ensures that the code fragments appear next to each other. A section fragment is only active if someone actually refers to it. In this case the .require directive can be used to refer to it without actually using it. The result is that code which relies on that initialization code fragment exists can request that such code fragment becomes active.

.incbin

Include a binary file as embedded data directly into the program. This is useful if you have binary data assets as you can include them in the program without having to convert them to suitable source code.

my_data:
              .incbin  "rawdata.bin"

.rtmodel

It is possible to define runtime model attributes using the .rtmodel directive. Such attributes are checked at link time to ensure object file consistency. The attribute is an identifier and its value is a string:

; activities suitable for winter
              .rtmodel season, "winter"

Another source file could specify season to have another value:

; activities suitable for summer
              .rtmodel season, "summer"

If you try to link these two modules together will result in an error message describing that runtime model attribute season has a mismatch.

Source files that do not define the season attribute can be linked with either. It is also possible to use the special * value which also means it works with either. It is an explicit way of saying that I aware of this attribute and it works with whatever value it has:

; I can work with either season
              .rtmodel season, "*"

Functionally it is equivalent to not having the attribute defined. The difference is more in the eye of the reader, you have actively considered the attribute and concluded it works with whatever interpretation there may be.

Note

A defined runtime attribute affects the entire compilation unit it appears in. If you need to have a more narrow scope for some runtime model attribute, you need to break up the source file into smaller pieces.

.byte

Define one or more 8 bit values expressed as a list of comma separated expressions.

.word

Define one or more 16 bit values expressed as a list of comma separated expressions.

.address

Defines one or more 24 bit values expressed as a list of comma separated expressions.

.long

Define one or more 32 bit values expressed as a list of comma separated expressions.

.quad

Define one or more 64 bit values expressed as a list of comma separated expressions.

.ascii

Take a string arguments and emit the bytes.

.asciz

Take a string arguments and emit the bytes followed by a terminating zero byte.

.fillto

Take an address offset and an optional filler value. The address value is relative to the start of the current section fragment. Emits the filler value until the location counter is equal address offset.

If the current section is bss, the filler value cannot be non-zero.

.space

Take an count value and an optional filler value. Emits count number of filler values in the output. If the current section is bss, the filler value cannot be non-zero and it only advances the location counter by count.

.align

Emit zero fillers to bring the location counter in alignment with the specified alignment argument. To long word align a table, use:

              .align 4
table         .byte  1,2,3,4

Note

The .align directive also have the effect of applying alignment on the section itself in the object file which forces the linker to place the section according to the alignment. If there are multiple .align directives with different alignment values in a section, the assembler will determine the overall alignment needed and emit that as the alignment of the section.

.end

End the source file and stop processing any further input. This directive is optional and processing will otherwise stop when the input file has been fully consumed.

.macro

Define a macro, see Macro language.

.endm

End a macro definition.

.argdelim

This directive defines delimiter characters that can be used include commas in an argument, see Macro language.

21.10. Relocations

The assembler and the C compiler produces relocatable output, which means that the program output does not have fixed addresses. The linker is responsible for placing sections at suitable memory addresses.

The compiler may need to refer to addresses that are unknown at compiler time. Such references are represented by relocations which are part of the object file. The linker processes the relocations after section placement to finalize the program.

Relocations are automatically generated by the assembler and C compiler when it encounters expressions or values that depend on final section placement.

Relocation operators

In certain contexts some additional prefix operators are available. They can be seen as relocation operators as they can be used to optionally introduce a relocation.

As they are relocations, they allow the operator to be executed at link time when the final addresses are known. However, they have the limitation that they must appear at the top level of the expression.

On the 65816 you often need to load word parts of an address. This can be done with the .word0 and .word2 operators. There are also .byte0, .byte1, .byte2 operators that return individual bytes of an expression at link time.

; Set up a pointer
             lda   #.byte0 table
             sta   zp:ptr
             lda   #.byte1 table
             sta   zp:ptr+1

Note

Relocation operators have high precedence like other unary prefix operators. If you want to access an address with an offset, you need to surround the expression by parentheses.

.byte0

The .byte0 operator gives the first byte (count starts from 0) and is allowed to be resolved at link time.

.byte1

The .byte1 operator gives the second byte (count starts from 0) and is allowed to be resolved at link time.

.byte2

The .byte2 operator gives the third byte (count starts from 0) and is allowed to be resolved at link time.

.word0

The .word0 operator gives the lower 16 bits word and is allowed to be resolved at link time.

.word2

The .word2 operator gives the upper 16 bits word and is allowed to be resolved at link time.

.tiny

This relocation gives the address on a symbol in the Tiny area, which corresponds to the direct page. Typically it is given an expression to be resolved at link time. The expression is an address that is converted to a relative offset to the linker defined direct page base symbol DirectPageStart:

0001                              .extern foo
0002  000000 a9..                 lda     dp: .tiny (foo+2)

.near

This relocation gives the relative address of a symbol in the Near area, which corresponds to the 16 bits absolute area. Typically it is given an expression to be resolved at link time. The expression is an address that is converted to a relative offset to the linker defined Near base symbol NearBaseAddress:

0001                              .extern foo
0002  000000 a9....               lda     abs: .near (foo+2)

.kbank

This relocation is used for jmp and jsr inside the current 64K program bank. It takes an address and strips out the lower 16 bits, in the same way as .word0 does. In addition it checks that the destination is within the same program bank (K register) as the instruction itself.

0000  020034 4c....               jmp     .kbank localLabel

Section operators

Section operators makes it possible to get hold of where a section is placed in memory.

Section names must be known to the assembler. If you want to refer to a section that is not used otherwise in the current assembly source file, simply declare it without inserting any code below it.

;;; Forward declaration
              .section elsewhere

              .section code
              ...

All these operators are resolved by the linker as placement is not known before link time. An error is given if a section spans multiple memories.

Table 21.2 Section operators

Operator

Precedence

Purpose

.sectionStart sectionName

9

The first address of the given section.

.sectionEnd sectionName

9

The last address of the given section.

.sectionSize sectionName

9

The size of the given section.

Note

Section size corresponds to 1 + end - start.

Warning

If you allow the linker to intermix different sections in the same allocation range, these operators will base their values on the first and last section fragment of the given name. Different sections that are interleaved inside are silently included in the address range given by these operators.

21.11. Macro language

The .macro directive allows you to generate new commands that can create assembler output. A simple example follows:

foo           .macro  a, b
              .byte   \a
              .word   \b - 1
              .long   0
              .endm

This creates a new macro named foo which takes two arguments a and b. To use an argument inside the macro, prefix the parameter name with a backslash \.

Rules for argument substitutions

When looking for argument substitutions, the longest match is favored. This means if you have parameters called a and aa, substituting the longer name is always tried before shorter names. As there is no way to explicitly specify the end of a parameter name inside the body, a parameter may accidently try to match characters that comes after the parameter. A good rule of thumb is to make use of space to separate entities whenever possible. This also tends to improve readability.

Use of local labels

Each macro expansion will create a new unique context for local labels inside the macro body. Any previous local label context is restored after the macro is expanded. Thus, local labels inside a macro will not clash or interfere with any local labels surrounding the use of the macro.

This also works when using nested macro expansions. If a macro uses another macro inside its body, that inner macro expansion will have its own private local label context, and the previous context of the outer macro expansion will be restored when the inner macro has been expanded.

Thus, you are able to use the same label name inside a macro and in the code that uses it without any clash:

waitreg       .macro  reg
1$:           de\reg
              bne     1$
              .endm

              ldx     #100
              waitreg x
1$:           lda     #0

Which would create the following list file:

###############################################################################
#                                                                             #
# Calypsi assembler for 65816                                    version 5.16 #
#                                                       14/Apr/2026  16:42:19 #
# Command line: example/macro-local.s -l                                      #
#                                                                             #
###############################################################################

0001                  waitreg       .macro  reg
0002                  1$:           de\reg
0003                                bne     1$
0004                                .endm
0005
0006  000000 a264                   ldx     #100
0007                                waitreg x
    \ 000002 ca       `1$`:       dex
    \ 000003 d0fd                 bne     `1$`
0008  000005 a900     1$:           lda     #0

##########################
#                        #
# Memory sizes (decimal) #
#                        #
##########################

Executable  (Text): 7 bytes

Arguments with comma

Arguments to a macro are comma separated. This poses a problem in a situation where you want an argument to contain a comma character. The .argdelim directive defines a start and stop character that can be used to create an argument that contains a comma character.

              .argdelim <>
access        .macro  arg1, arg2
              ...
              .endm

              access  0, <2,a>

Here arg1 is bound to 0 and arg2 is bound to the value 2,a.

The delimiter can be either one or two characters and you can pick any suitable character combination. By default there are no delimiter characters defined.

If a single character combination is not suitable, you can use two characters, e.g. <- and -> which would be defined as follows:

              .argdelim <-->
access        .macro  arg1, arg2
              ...
              .endm

              access  0, <-2,a->

All delimiter characters are stripped and arg2 is bound to 2,a here as well.