20. Assembly language interface

Assembly language provides symbolic access to target machine instructions. You might need this control for specific instructions unrepresentable in C, precise hardware interaction, exception stack frame manipulation, exact timing sequences, or performance-critical routines.

20.1. Intrinsic functions

The compiler provides intrinsic functions (declared in the calypsi/intrinsics65816.h file) that resemble ordinary functions. Instead of a function call, an intrinsic generates a specific instruction sequence. For example, __disable_interrupts() emits machine instructions to disable normal interrupts. See Intrinsic functions for details.

20.2. Assembly functions

You can implement functions in assembly language and call them from C like any other function. An assembly function must adhere to the C calling convention, which dictates how values are passed to the called function and where the return value is placed.

Note

To simplify your assembly routine interface, consider using the simple_call calling convention.

You can choose between a separate assembly source file or inline assembly; each has pros and cons. Assembly functions offer better separation between C and assembly, aiding portability. However, function call overhead and adherence to calling conventions may be undesirable.

Minimal boilerplate assembly code is required to place the routine in a suitable section and declare public symbols. This assembly code resides in a separate file, which must be added to the build system.

Assembly language files typically use the .s or .asm extension; however, this varies due to the lack of standardization in assembly language itself.

The assembler provided by the Calypsi C compiler tool chain, similar to UNIX assemblers, uses directives starting with a dot. This avoids name clashes with instructions, whose naming conventions vary widely across targets, ensuring consistent directive names.

As a minimum, you must declare the section and export your function name using the .public directive:

              .section code
              .public myFunction    ; export myFunction
              .extern _Dp           ; pseudo direct page registers
myFunction:   clc
              adc     dp:_Zp
              rts

To call this function correctly, you must provide a prototype in C:

#include <stdint.h>

extern int8_t myFunction(int8_t a, int8_t b);

int caller(int8_t x) {
  return myFunction(5, x);
}

Generate skeleton code

The easiest way to generate an assembly source file is to have the compiler create it using the --assembly-source command-line option. A simplified C source file containing the desired functions and declarations can be used for this purpose.

You can provide desired function definitions with simple parameter uses to study how they are passed:

extern int intvar;
extern char charvar;

extern void externalFunction(int*);

int myFunction(int i, char c) {
  int local = i;
  intvar = i;
  charvar = c;
  externalFunction(&local);
  return local;
}

int main() {
  myFunction(intvar, charvar);
  return 0;
}
$ cc65816 skeleton.c --assembly-source=skeleton.s
; Generated by Calypsi ISO C compiler for 65816

            .rtmodel version,"1"
            .rtmodel codeModel,"large"
            .rtmodel dataModel,"small"
            .rtmodel core,"65816"
            .rtmodel huge,"0"
            .rtmodel target,"none-specified"
            .extern _Dp
            .extern _Vfp
            .extern charvar
            .extern externalFunction
            .extern intvar
;  extern int intvar;
;  extern char charvar;
;
;  extern void externalFunction(int*);
;
;  int myFunction(int i, char c) {
            .section farcode,text
            .public myFunction
myFunction: phy
;    int local = i;
            sta     1,s
;    intvar = i;
            sta     intvar
;    charvar = c;
            sep     #32
            lda     6,s
            sta     charvar
            rep     #32
;    externalFunction(&local);
            tsc
            inc     a
            jsl     long:externalFunction
;    return local;
            lda     1,s
;  }
            ply
            rtl
;
;  int main() {
            .section farcode,text
            .public main
main:
;    myFunction(intvar, charvar);
            lda     charvar
            pha
            lda     intvar
            jsl     long:myFunction
;    return 0;
            pla
            lda     ##0
;  }
            rtl

20.3. Calling convention

The default calling convention is complex in detail, but straightforward in most common scenarios. An alternative calling convention simple_call is also provided.

If parameters are passed on the stack, the caller is responsible for cleanup. The called function may use any register resource but must preserve certain registers, saving and restoring them before returning.

Simple calling convention

Use __attribute__((simple_call)) or __simple_call on a function declaration to enable the simple calling convention.

In this calling convention the first parameter is passed in register if it fits, the remaining parameters are passed on the stack.

Registers A, X, Y and pseudo registers _Dp[0-7] are destroyed by a function call. Pseudo registers _Dp[8-15] must be preserved.

Note

The pseudo registers are clobbered for consistency with the normal calling convention. If you implement an assembly or foreign language interface that do not use the _Dp style pseudo registers you can simply ignore this detail as it does not matter.

Table 20.1 first parameter register location

Register

Size

Types

A

8

char

C

16

int, short, tiny and near pointers

X and C

32

long, float, far and huge pointers

Table 20.2 Return values

Register

Size

Types

A

8

char, tiny pointer

C

16

int, short, tiny pointer

X and C

24

far pointer, farfunc pointer

X and C

32

long, float

Normal calling convention

Parameters are passed in the A accumulator, index register X and pseudo registers _Dp[0-7].

Registers A, X, Y and pseudo registers _Dp[0-7] are destroyed by a function call. Pseudo registers _Dp[8-15] must be preserved.

Note

The A register is the 8 bits accumulator. The same register in 16 bits version is called C. As the 65816 shares mnemonics with the 6502 and can switch modes, the term register name A is often used for the 16 bits version as well. Technically it should be named C when being the 16 bits version.

Table 20.3 Parameter registers

Register

Size

Types

A

8

char

C

16

int, short, tiny and near pointers

_Dp[0-1]

16

int, short, tiny and near pointers

_Dp[2-3]

16

int, short, tiny and near pointers

_Dp[4-5]

16

int, short, tiny and near pointers

_Dp[0-3]

32

long, float, far, far24 and huge pointers

_Dp[4-7]

32

long, float, far, far24 and huge pointers

X and C

32

long, float

Parameters are bound to register left to right on a first fit basis. If a parameter register has to be skipped over, it will be considered again for later parameters. Parameters that cannot be fit into registers are passed on the stack.

Note

The 16 bit register _Dp[2-3] is only used in the small data model. In the other data models this parameter is passed in _Dp[4-5] instead. The reason for this is that in the small data model it is possible to use plain 16 bits pointers. In the other data models the 16 bit register may need to be converted to a 24 bit register value.

Note

24 bit values are passed in three bytes, but the actual pseudo register covered is four bytes. The last byte is ignored and can be regarded as unused garbage. This is done in order to avoid data size shifts in the code.

Table 20.4 Return values

Register

Size

Types

A

8

char, tiny pointer

C

16

int, short, tiny pointer

X and C

24

far pointer, farfunc pointer

X and C

32

long, float

Index and data size

The 65816 is used in native mode with 16 bit registers. In some situations the runtime needs to switch to 8 bit register mode to implement the correct behavior. This is done automatically and the compiler will then switch back to 16 bits mode.

There are two flags in the CPU that control the width of registers, one for index registers and the other for the accumulator and data memory access. The index width is always kept at 16 bits by the runtime, only the accumulator/memory width is changed as needed.

A function call expects the width to be set to 16 bits and must return with the same setting. This means that if you write a function in assembly language, it will get called from a C function with 16 bit register width and you need to return that way. Inside your own routine you are free to switch register width as you may see fit.

Note

The compiler may generate code that reads 8 and 24 bit objects using 16 bit access, ignoring the extra byte. Writes are always done obeying the object width. If you are reading from hardware registers that requires reads to follow the width, you must use the volatile keyword.

It can be a good idea to group multiple 8 bit accesses next to each others as the compiler is able to reduce mode shifts back and forth if it detects it. However, you need to enable the optimizer for this to happen.

64 bit values

The above tables do not mention 64 bit values. A 64 bit value such as long long and long double is passed by reference, that is, as a pointer to the value. The size of this pointer is the same as the default pointer size and is either 16 bits or 24 bits depending on the data model used.

If a function returns a 64 bit value, the caller is responsible for allocating space. An extra ‘invisible’ parameter, a pointer to this space, is added to the function. The called function is expected to return this pointer.

Structure passing

Structure parameters are passed on the stack. Structure return values are handled like 64 bit values. The caller allocates space and adds an extra ‘invisible’ parameter (a pointer to that space) to the function call. The called function is expected to return this pointer.

20.4. Inline assembler

The inline assembler allows you to insert and interface assembly code slices within a C function. This avoids call overhead and can improve parameter adaptation. However, the optimizer must be more cautious, which may affect the performance of the surrounding C code.

Basic inline assembly

You can insert a slice of assembly code using an __asm block:

int counter;

void foo(char xx) {
  __asm(" sei\n"
        " inc counter\n"
        " bne skip\n"
        " inc counter+2\n"
        "skip: cli\n"
        );
}

Each line without a label requires at least one leading space and must be terminated by a newline character (\n).

The inline assembler supports the full assembly instruction set, including literal bytes, volatile operations, local labels, and register allocation for parameters and return values.

Goto labels and most assembler directives are currently not supported by the inline assembler. If you need better control with placement, use a separate assembly source file instead.

When inline assembly is inserted, the compiler adapts it to fit the generated C code. Variables can be passed as parameters, and a single result variable is supported. The compiler reasonably understands the inserted assembly, mixing it with C-generated code. Inline assembly is subject to low-level optimizations when the optimizer is enabled.

Volatile

An assembly block can be marked as volatile:

__asm volatile { ... }

This has the effect that all memory accesses in the assembly slice are treated as side effects. Otherwise the optimizer may remove reads from memory when the value read is not used.

Local labels

Local labels can be used and their names will not clash with C identifiers. When an inline assembly slice is inserted, local labels are converted to internal C labels, preventing name clashes with C identifiers.

External symbols

Inline assembly can refer to symbols defined outside its code slice, provided such symbols are visible at the C level within the same compilation unit.

Constraints

An inline assembly code slice can refer to C variables and return a value. The inline assembly construct optionally accepts three lists:

  1. Output variable: Specifies a C variable to represent the returned value and a register class where the assembly code block places it. The result is prefixed by = in its single-value list.

  2. Input expressions: Typically variables. The compiler evaluates the expression and places it in the specified register class.

  3. Clobbered registers: Any register resource clobbered by the inline assembly must be specified here.

Multiple entries in a list are comma-separated.

Register classes

A register class is a register resource that can represent either a single register or a set of equivalent registers. They are used for allocating parameters and determining the return value location.

The following register classes are defined:

Table 20.5 Register classes

Register class

Description

a

Accumulator A, 8 bit

c

Accumulator C (B and A combined), 16 bit

x

X index register

y

Y index register

dp8

An 8-bit direct page pseudo register

dp16

A 16-bit direct page pseudo register

dp32

A 32-bit direct page pseudo register

Note

Register classes are used internally during code generation. Internal code generator rules ensure safe register allocation by adhering to specific rules and invariants. Rather than attempting to diagnose inline assembly constraints or impose conservative limitations, the compiler trusts you. Overusing resources or violating internal invariants may lead to a register allocation error. In such cases, ease the register resources to find a working allocation.

Registers and constraints

The following code shows how constraints for an inline assembly code slice are defined:

char foo(int xx) {
  int out;
  __asm(" inx\n"
        " inx\n"
        " inx\n"
        " inx\n"
        " txa\n"
        : "=Kc" (out)
        : "Kx"  (xx)
        : "c", "x"
      );
  return out;
}

Currently, the only supported constraint is ‘K’, followed by a register class.

An empty list can be entered by using a colon character followed by nothing.

The first list is the optional output parameter, describing the C variable where the output is visible after the inline assembly slice executes. The inline assembly slice must leave the result in the specified register class; the compiler will automatically insert code to store this value in the C variable.

The second constraint specifies input variables and their register classes. The compiler ensures these variables are available in the specified register classes before passing control to the inline assembly code slice.

The third list specifies the actual registers clobbered by the inline assembly code slice. These must be register classes describing a single register.

Substitutions

A register class may specify a register resource with multiple alternatives. The register allocator selects the actual register used. You can refer to the register resource using substitutions, given by the %N syntax, where N is 0 for the result (if present), 1 for the first input, 2 for the second, and so on:

int foo(int xx, int *yy) {
  int out;
  __asm(" ldy %1\n"
        " iny\n"
        " iny\n"
        " lda (%2),y\n"
        " clc\n"
        " adc %1\n"
        " sta %0\n"
        : "=Kdp16"(out)
        : "Kdp16"(xx), "Kdp16"(yy)
        : "c", "y"
      );
  return out;
}

Note

If the output is empty then the input list starts with %0.

If you find substitutions using numbers to be unreadable, you can specify a symbol for each substitution:

int foo(int xx, int *yy) {
  int out;
  __asm(" ldy %[xx]\n"
        " iny\n"
        " iny\n"
        " lda (%[second]),y\n"
        " clc\n"
        " adc %[xx]\n"
        " sta %[result]\n"
        : [result] "=Kdp16"(out)
        : [xx] "Kdp16"(xx), [second] "Kdp16"(yy)
        : "c", "y"
      );
  return out;
}