< back to index

Using 6502 assembly within Millfork programs

There are two ways to include raw assembly code in your Millfork programs:

Assembly syntax

Millfork inline assembly uses the same three-letter opcodes as most other 6502 assemblers. Indexing syntax is also the same. Only instructions available on the current CPU architecture are available.

Work in progress: Currently, RMBx/SMBx/BBRx/BBSx and some extra 65CE02/HuC6280/65816 instructions are not supported yet.

Undocumented instructions are supported using various opcodes.

Labels have to be followed by a colon and they can optionally be on a separate line. Indentation is not important:

first:  INC x
second: 
        INC y
INC z

Label names have to start with a letter and can contain digits, underscores and letters. This means than they cannot start with a period like in many other assemblers. Similarly, anonymous labels designated with + or - are also not supported.

Assembly can refer to variables and constants defined in Millfork, but you need to be careful with using absolute vs immediate addressing:

const byte fiveConstant = 5
byte fiveVariable = 5

byte ten() {
    byte result
    asm {
        LDA #fiveConstant
        CLC
        ADC fiveVariable
        STA result
    }
    return result
}

Any assembly opcode can be prefixed with ?, which allows the optimizer change it or elide it if needed. Opcodes without that prefix will be always compiled as written.

The '!' prefix marks the statement as volatile, which means it will be a subject to certain, but not all optimizations, in order to preserve its semantics.

You can insert macros into assembly, by prefixing them with + and using the same syntax as in Millfork:

macro void run(byte x) {
    output = x
}

byte output @$c000

void main () {
    byte a
    a = 7
    asm {
        + run(a)
    }
}

You can insert raw bytes into your assembly using the array syntax:

[ $EA, $EA ]
"this is a string to print" apple2
["this is a string to print but this time it's zero-terminated so it will actually work" apple2, 0]
[for x,0,until,8 [x]]

Assembly functions

Assembly functions can be declared as macro or not.

A macro assembly function is inserted into the calling function like an inline assembly block, and therefore usually it shouldn't end with RTS or RTI.

A non-macro assembly function should end with RTS, JMP or RTI as appropriate, or it should be an external function.

For both macro and non-macro assembly functions, the return type can be any valid return type, like for Millfork functions.
If the size of the return type is one byte, then the result is passed via the accumulator.
If the size of the return type is two bytes, then the low byte of the result is passed via the accumulator and the high byte of the result is passed via the X register.

Assembly function parameters

An assembly function can have parameters. They differ from what is used by Millfork functions.

Macro assembly functions can have the following parameter types:

For example, if you have:

macro asm void increase(byte ref v, byte const inc) {
    LDA v
    CLC
    ADC #inc
    STA v
}

and call increase(score, 10), the entire call will compile into:

LDA score
CLC
ADC #10
STA score

Non-macro functions can only have their parameters passed via registers:

Macro assembly functions can have maximum one parameter passed via a register.

External functions

An external function should be declared with a defined memory address and the extern keyword instead of the body:

asm void putchar(byte a) @$FFD2 extern

Safe assembly

Since assembly gives the programmer unlimited access to all machine features, certain assumptions about the code may be broken. In order to make assembly cooperate with the rest of the Millfork code, it should abide to the following rules:

The above list is not exhaustive.