Skip to content

Instantly share code, notes, and snippets.

@russdill
Last active December 12, 2022 08:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save russdill/abbacfe5dba1ba4070584efe44f46a58 to your computer and use it in GitHub Desktop.
Save russdill/abbacfe5dba1ba4070584efe44f46a58 to your computer and use it in GitHub Desktop.
Example of AVR custom jump tables using inline assembler "asm goto" construct
#ifndef JUMP_TABLE_H
#define JUMP_TABLE_H
#define MAP_OUT
#define EVAL0(...) __VA_ARGS__
#define EVAL1(...) EVAL0 (EVAL0 (EVAL0 (__VA_ARGS__)))
#define EVAL2(...) EVAL1 (EVAL1 (EVAL1 (__VA_ARGS__)))
#define EVAL3(...) EVAL2 (EVAL2 (EVAL2 (__VA_ARGS__)))
#define EVAL4(...) EVAL3 (EVAL3 (EVAL3 (__VA_ARGS__)))
#define EVAL(...) EVAL4 (EVAL4 (EVAL4 (__VA_ARGS__)))
#define MAP_END(...)
#define MAP_GET_END() 0, MAP_END
#define MAP_NEXT0(item, next, ...) next MAP_OUT
#define MAP_NEXT1(item, next) MAP_NEXT0 (item, next, 0)
#define MAP_NEXT(item, next) MAP_NEXT1 (MAP_GET_END item, next)
#define MAP0(f, x, peek, ...) f(x) MAP_NEXT (peek, MAP1) (f, peek, __VA_ARGS__)
#define MAP1(f, x, peek, ...) f(x) MAP_NEXT (peek, MAP0) (f, peek, __VA_ARGS__)
#define MAP(f, ...) EVAL (MAP1 (f, __VA_ARGS__, (), 0))
#define __COMBINE(x, y) x ## y
#define __CALL(x, IDX, n...) x(IDX, n)
#define JUMP_TABLE1(IDX, IDX_TYPE, n...) __CALL(__COMBINE(JUMP_TABLE1_, IDX_TYPE), IDX, n)
#define JUMP_TABLE2(IDX, IDX_TYPE, n...) __CALL(__COMBINE(JUMP_TABLE2_, IDX_TYPE), IDX, n)
// 5 ins (10 bytes) + 2 bytes per target
#define RJMP_LABEL(x) "\trjmp\t%l[" #x "]\n"
/* Note, lo8(-1f)/hi8(-1f) is currently buggy, so subi/sbc can't be used.
* workaround is an alternate codepath with an extra instruciton */
#define JUMP_TABLE2_R30(...) \
asm goto( \
" ldi r31, pm_lo8(1f)\n" \
" add r30, r31\n" \
" ldi r31, pm_hi8(1f)\n" \
" adc r31, __zero_reg__\n" \
" ijmp\n" \
"1:\n" \
MAP(RJMP_LABEL, __VA_ARGS__) \
::: "r30", "r31" : __VA_ARGS__)
#define JUMP_TABLE2_MEM(IDX, ...) \
asm goto( \
" lds r30, %[idx]\n" \
" ldi r31, pm_lo8(1f)\n" \
" add r30, r31\n" \
" ldi r31, pm_hi8(1f)\n" \
" adc r31, __zero_reg__\n" \
" ijmp\n" \
"1:\n" \
MAP(RJMP_LABEL, __VA_ARGS__) \
:: [idx]"m"(IDX) : "r30", "r31" : __VA_ARGS__)
#define JUMP_TABLE2_REG(IDX, ...) \
asm goto( \
" mov r30, %[idx]\n" \
" ldi r31, pm_lo8(1f)\n" \
" add r30, r31\n" \
" ldi r31, pm_hi8(1f)\n" \
" adc r31, __zero_reg__\n" \
" ijmp\n" \
"1:\n" \
MAP(RJMP_LABEL, __VA_ARGS__) \
:: [idx]"r"(IDX) : "r30", "r31" : __VA_ARGS__)
// 10 ins (20 bytes) + 1 byte per near target (512 bytes), 3 bytes per far
#define BYTE_LABEL(x) "\t.byte (%l[" #x "] - . + 1)/2\n"
#if defined (__AVR_TINY__)
#define __LPM "ld"
#define __BASE "0x4000"
#else
#define __LPM "lpm"
#define __BASE "0x0"
#endif
/* Note, lo8(-1f)/hi8(-1f) is currently buggy, so subi/sbc can't be used.
* workaround is an alternate codepath with an extra instruciton */
#define JUMP_TABLE1_R30(...) \
asm goto( \
" ldi r31, lo8(1f + " __BASE ")\n" \
" add r30, r31\n" \
" ldi r31, hi8(1f + " __BASE ")\n" \
" adc r31, __zero_reg__\n" \
" " __LPM " r16, Z\n" \
" lsr r31\n" \
" ror r30\n" \
" add r30, r16\n" \
" adc r31, __zero_reg__\n" \
" ijmp\n" \
"1:\n" \
MAP(BYTE_LABEL, __VA_ARGS__) \
".align 1\n" \
::: "r16", "r30", "r31" : __VA_ARGS__)
#define JUMP_TABLE1_MEM(IDX, ...) \
asm goto( \
" lds r30, %[idx]\n" \
" ldi r31, lo8(1f + " __BASE ")\n" \
" add r30, r31\n" \
" ldi r31, hi8(1f + " __BASE ")\n" \
" adc r31, __zero_reg__\n" \
" " __LPM " r16, Z\n" \
" lsr r31\n" \
" ror r30\n" \
" add r30, r16\n" \
" adc r31, __zero_reg__\n" \
" ijmp\n" \
"1:\n" \
MAP(BYTE_LABEL, __VA_ARGS__) \
".align 1\n" \
:: [idx]"m"(IDX) : "r16", "r30", "r31" : __VA_ARGS__)
#define JUMP_TABLE1_REG(IDX, ...) \
asm goto( \
" mov r30, %[idx]\n" \
" ldi r31, lo8(1f + " __BASE ")\n" \
" add r30, r31\n" \
" ldi r31, hi8(1f + " __BASE ")\n" \
" adc r31, __zero_reg__\n" \
" " __LPM " r16, Z\n" \
" lsr r31\n" \
" ror r30\n" \
" add r30, r16\n" \
" adc r31, __zero_reg__\n" \
" ijmp\n" \
"1:\n" \
MAP(BYTE_LABEL, __VA_ARGS__) \
".align 1\n" \
:: [idx]"m"(IDX) : "r16", "r30", "r31" : __VA_ARGS__)
// 11 ins (22 bytes) + 2 bytes per target (gcc)
#if 0
" lds r20, lo8(idx)\n"
" ldi r21, high8(idx)\n"
" cpi r20, 0x0B\n"
" cpc r21, r17\n"
" brcs 1f\n"
" rjmp out\n"
"1: mov r30, r20\n"
" mov r31, r21\n"
" subi r30, lo8(-(jump_table/2))\n"
" sbci r31, hi8(-(jump_table/2))\n"
" ijmp\n"
#endif
#endif
@russdill
Copy link
Author

avr-gcc makes really inefficient jump tables. This allows use of more efficient jump tables in c code. There's a JUMP_TABLE2 version with 2 byte entries and a JUMP_TABLE1 version with 1 byte entries. The macro should give the index and then the jump targets starting with offset 0:

JUMP_TABLE1_MEM(state, target0, target1, target2, target3); // if state is a SRAM variable

target0:
    /*...*/
    goto end;
target1:
    /* ... */
    goto end;

/* ... */
end:

JUMP_TABLE1_REG(state, target0, target1, target2, target3); // if state is a local variable

You can include your own inline assembly lead-in by using the R30 variant and leaving the jump index in r30. This can be used to apply an offset or do range checking.

Note that the 1 byte entry version must have targets within 510 bytes of the entry. Optionally, a trampoline may be used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment