Calling functions

It seems good to have the caller push and pop function arguments:

caller:
    ld hl, 0x0001
    push hl
    ld hl, 0x0002
    push hl
    call callee
    pop hl
    pop hl

Then the callee can extract those arguments:

callee:
    ld hl, 0x0002
    add hl, sp
    ld e, (hl)
    inc hl
    ld d, (hl)              ; de==0x0002
    inc hl
    ld c, (hl)
    inc hl
    ld b, (hl)              ; bc==0x0001
    ...

The ld hl, 0x0002 and add hl, sp is for skipping the return address on the stack. This is because the "call" itself will push the return address after your params (closer to the top of the stack). You don't have to extract the arguments at the beginning of the function. If an argument is only needed later on you can do the add hl, sp dance later also.

Pass by register

If you are in tight control of the caller and callee then it might be better to pass arguments via registers:

caller:
    ld bc, 0x0001
    ld de, 0x0002
    push bc
    push de
    call callee
    pop de
    pop bc

callee:
    ; can just go ahead and use bc and de

Because the caller has pushed de and bc those values in the caller are backed-up and restored. If the caller doesn't need those values after the call they can just not push them.

Slower methods

Surprisingly using the indexed registers is slower:

callee:
    ld ix, 0x0002
    add ix, sp
    ld e, (ix+0)
    ld d, (ix+1)
    ld c, (ix+2)
    ld b, (ix+3)

Returning values

I usually just set a value in hl and return.

Variables

You can specify locations in memory to serve as variables:

save_variable:
    ld de, 0x1234
    ld (0x0008), de         ; save 0x1234 at memory 0x0008 and 0x0009

Or create lables for your variables so the code is more readable:

var_myfoo: equ 0x0006       ; the myfoo variable is located here
var_mybar: equ 0x0008       ; remember to leave space is prev is 16 bit
save_variable:
    ld de, 0x1234
    ld (var_myfoo), de

Copying memory

Tables of data

Very often you want a table of data in memory, like an array of structs.

Length of tables

It is useful for your tables to be 256 items in length so you can use an 8 bit register as an index and it will auto-wrap. Alternatively you can mask the index, meaning your table can be 2, 4, 8, 16 .. etc items long. In general is is simpler to have sizes be a power of 2.

Width of tables

When you have a table index and you want to convert an index into a memory address it is convenient if your table width (record length) is 2, 4, 8, 16 .. etc in size. for example if your record length is 8, the address of the entry is (base + (idx * 8)). For example:

base_addr: equ 0x0100       ; base address of table
main:
    ld de                   ; make a backup of de
    ld hl, 0x0005           ; record index (record nr. 6)
    call get_record_addr
    pop de                  ; restore de
    ; hl converted into address

get_record_addr:
    add hl, hl              ; multiply x2
    add hl, hl              ; multiply x4
    add hl, hl              ; multiply x8
    ld de, table_base_addr  ; load the table base address
    add hl, de              ; hl is now the memory addr of idx 5
    ret

Jump tables

For my OS project I wanted to have programs call into kernel routines without knowing their memory address. For this