Skip to content

Instantly share code, notes, and snippets.

@Konamiman
Last active February 1, 2024 03:22
Show Gist options
  • Save Konamiman/af5645b9998c802753023cf1be8a2970 to your computer and use it in GitHub Desktop.
Save Konamiman/af5645b9998c802753023cf1be8a2970 to your computer and use it in GitHub Desktop.
[SDCC] Interfacing with Z80 assembler code

SDCC - Interfacing with Z80 assembler code

The basics

When writing code to be compiled with SDCC targetting Z80, assembler code fragments can be inserted in the C functions by enclosing them between the __asm and __endasm; tags:

void DoNotDisturb()
{
  __asm
  di
  __endasm;

  DoSomething();

  __asm
  ei
  halt
  __endasm;
}

SDCC 3.2 introduced the __asm__ keyword, that allows to specify the assembler code inside a string:

void DoNotDisturb()
{
  __asm__ ("di");

  DoSomething();

  __asm__ ("ei\nhalt");
}

Adding __naked to the function definition will cause the compiler to not generate the ret statement at the end of the function, you will usually use it when the entire body of the function is written in assembler:

void TerminateProgram() __naked
{
  __asm
  ld c,#0
  jp #5
  __endasm;
}

Assembler code must preserve the value of IX, all other registers can be used freely.

Return values

The return value of a C function is passed to the caller as follows:

  • Functions that return char: in the L register
  • Functions that return int or a pointer: in the HL registers
  • Functions that return long: in the DEHL registers
char GetMagicNumber() __naked
{
  __asm
  ld l,#34
  ret
  __endasm;
}

int GetMagicYear() __naked
{
  __asm
  ld hl,#1987
  ret
  __endasm;
}

long GetReallyStrongPassword() __naked
{
  __asm
  ld de,#0x1234
  ld hl,#0x5678
  ret
  __endasm;
}

Getting function parameters

Parameters for the function are pushed to the stack before the function is called. They are pushed from right to left, so the leftmost parameter is the first one found when going up in the stack:

char SumTwoChars(char x, char y) __naked
{
    __asm
    ld iy,#2
    add iy,sp ;Bypass the return address of the function 

    ld l,(iy)   ;x
    ld a,1(iy)  ;y

    add l
    ld l,a      ;return value

    ret
    __endasm;
}

int SumCharAndInt(char x, int y) __naked
{
    __asm
    ld iy,#2
    add iy,sp

    ld e,(iy)   ;x
    ld d,#0

    ld l,1(iy)  ;y (low)
    ld h,2(iy)  ;y (high)

    add hl,de   ;return value

    ret
    __endasm;
}

long SumCharIntAndLong(char x, int y, long z) __naked
{
    __asm
    ld iy,#2
    add iy,sp

    ld c,(iy)   ;x
    ld b,#0
    ld l,1(iy)  ;y (low)
    ld h,2(iy)  ;y (high)
    add hl,bc   ;x+y

    ld a,l
    add 3(iy)   ;z (lower)
    ld l,a
    ld a,h
    adc 4(iy)
    ld h,a
    ld a,#0
    adc 5(iy)
    ld e,a
    ld a,#0
    adc 6(iy)   ;z (higher)
    ld d,a

    ret     ;return value = DEHL
    __endasm;
}

Calling other C functions

It is possible to call other C functions from assembler code. Just push the parameters that the function expects, call the function assembler name (it's the C name prepended with _), pop the parameters to restore the stack state, and act on the return value as appropriate:

int SumTwo(int x, int y)
{
    return x+y;
}

int SumThree(int x, int y, int z) __naked
{
    __asm

    ld iy,#2
    add iy,sp

    ld l,2(iy)
    ld h,3(iy)
    push hl     ;y for SumTwo
    ld l,(iy)
    ld h,1(iy)
    push hl     ;x for SumTwo

    call _SumTwo  ;Return value in HL

    pop af      ;x
    pop af      ;y

    ld iy,#2
    add iy,sp

    ld e,4(iy)
    ld d,5(iy)  ;z
    add hl,de

    ret

    __endasm;
}

It is possible to define pure assembler functions intended to be called exclusively from assembler code. In this case the standard parameter passing rules can be overriden:

//DO NOT call this function from C code!
int SumHLDE() __naked
{
    __asm
    add hl,de
    ret
    __endasm;
}

int SumThree(int x, int y, int z) __naked
{
    __asm

    ld iy,#2
    add iy,sp

    ld l,2(iy)
    ld h,3(iy)
    ld e,(iy)
    ld d,1(iy)

    call _SumHLDE

    ld e,4(iy)
    ld d,5(iy)  ;z
    add hl,de

    ret

    __endasm;
}

Assembler syntax

The Z80 assembler supplied with SDCC uses a pretty much standard syntax for the assembler source code except for the following:

  • Decimal numeric constants must be preceded with #
  • Hexadecimal numeric constants must be preceded with #0x
  • The syntax for offsets when using index registers is n(ix), where in other assemblers it's usually (ix+n)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment