1

Due to some curiosity, I was trying to use some assembly with my Arduino MEGA 2560. I am not able to invoke a function from a c++ function pointer array.

When I uncomment the call function, func_a runs. However, when I almost reproduce the assembly, it doesnt work.

Example:

extern "C" void __attribute__ ((used, noinline, noreturn)) func_a();

typedef void (*volatile func_ptr)();

func_ptr tasks[MAX_TASKS] = {&func_a};

extern "C"
void __attribute__ ((used, noinline)) call(func_ptr *ptr) {
    (*ptr)();
}

void __attribute__ ((noreturn, used)) setup() {
    
    // call(tasks);

    asm volatile (
    "lds r26, (tasks)\n"
    "lds r27, (tasks + 1)\n"

    "ld r30, X+\n"
    "ld r31, X\n"

    "eijmp\n"
    );
}

Generated assembly recovered from avr-objdump

000001c6 <setup>:
 1c6:   a0 91 00 02     lds r26, 0x0200 ; 0x800200 <tasks>
 1ca:   b0 91 01 02     lds r27, 0x0201 ; 0x800201 <tasks+0x1>
 1ce:   ed 91           ld  r30, X+
 1d0:   fc 91           ld  r31, X
 1d2:   19 94           eijmp
 1d4:   08 95           ret

000001d6 <call>:
 1d6:   dc 01           movw    r26, r24
 1d8:   ed 91           ld  r30, X+
 1da:   fc 91           ld  r31, X
 1dc:   19 94           eijmp

An specific resources I should refer for things like this?

If it matters, I'm using platformio on a linux system for compilation.

3

It seems you got confused by the pointer indirections, which is confounded by the implicit indirections made by the compiler.

Here:

extern "C"
void __attribute__ ((used, noinline)) call(func_ptr *ptr) {
    (*ptr)();
}

The parameter ptr is not a function pointer: it's a pointer to a function pointer. You have to dereference it twice in order to get the function: (**ptr)();. Note that the compiler implicitly dereferences a function pointer if you call it like a function: that's why you get no error by explicitly dereferencing the pointer only once. In the generated assembly, the lines

ld  r30, X+
ld  r31, X

dereference ptr in order to get the address of the function to call.

You probably wanted to write

extern "C"
void __attribute__ ((used, noinline)) call(func_ptr ptr) {
    ptr();
}

And then use it as:

call(tasks[0]);

If you just call(tasks), the array identifier decays to pointer, and the compiler handles this as a shortcut for call(&tasks[0]);, which turns out to be compatible with your initial implementation of call().

Now, in your inline assembly you have

lds r26, (tasks)
lds r27, (tasks + 1)

The assembly treats tasks as a symbol representing the address of the tasks array. The linker replaces those symbols by the actual address (0x0200). Those instructions read the RAM at this and the following address. These slots of RAM hold the first item of the array, namely &func_a. So now you have this address in the X register pair. No need to read RAM again: you can movw r30, r26 and do the indirect jump. Or, better yet, load into Z to begin with:

lds r30, (tasks)
lds r31, (tasks + 1)
eijmp

See also Majenko's answer: here I am assuming EIND is somehow correctly set up to begin with. On an Uno, you would just use ijmp instead of eijmp and not worry about EIND.

2
  • That kindof eplained things. When I used (tasks) in the asm, I expected it to load the address of the first element of the array and not the value pointed to by it. Im not sure how to do that. Maybe I will have to understand the extended asm syntax. – darkspine Jun 19 '20 at 5:18
  • @darkspine: You can load the address of tasks with, e.g., ldi r26, lo8(tasks)\n ldi r27, hi8(tasks). Unlike lds, the ldi instruction does not access the RAM, as the address is known at build time and filled-in by the linker. – Edgar Bonet Jun 19 '20 at 7:33
2

I will start off by saying that I am no expert in AVR assembly, so I have no idea if any of this is actually correct. However from reading the various documents on the subject I have gleaned:

  • On the ATMega2560 EIJMP uses a combination of both the Z register and the EIND register to form a full address.

That means that as well as setting up the Z register for the lower 16 bits of the address you also have to set up the EIND register for the upper 5 bits of the address.

Indirect jump to the address pointed to by the Z (16 bits) Pointer Register in the Register File and the EIND Register in the I/O space. This instruction allows for indirect jumps to the entire 4M (words) Program memory space.

It then goes on to show an example:

ldi r16,$05 ; Set up EIND and Z-pointer 
out EIND,r16  
ldi r30,$00 
ldi r31,$10 
eijmp ; Jump to $051000

How you determine what the EIND register should be? Well, in general you don't. You wouldn't use EIJMP. From what I understand for functions outside the lower 64kwords of address space the compiler builds a "jump table" which is stored in the lower area of memory. You jump to the entry in that table with IJMP and that table then jumps to the final destination with EIJMP for you.

Doing this kind of jumping in assembly isn't really a nice thing to try and figure out. It's messy. Better to let the compiler do it for you and use C rather than assembly. The AVR-GCC guys put a lot of work into making function pointers work correctly (from what I have read it wasn't easy even for them), and you're just re-inventing the wheel.

At the very least you should implement your code 100% in C to begin with then look at the compiled output to see how the compiler thinks it should be done.

1
  • 1
    I switched to eijmp because thats what gcc was using. The problem turned out to be that tasks is dereferenced when using it in the asm. Also, thanks for the document, Its much more verbose than the ATMega2560 datasheet. – darkspine Jun 19 '20 at 5:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.