Не могу вызывать указатели функций С++ из встроенной сборки

Из-за некоторого любопытства я попытался использовать какую-то сборку с моей Arduino MEGA 2560. Я не могу вызвать функцию из массива указателей функций С++.

Когда я раскомментирую функцию вызова, запускается func_a. Однако, когда я почти воспроизвожу сборку, она не работает.

Пример:

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"
    );
}

Сгенерированная сборка, восстановленная из 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

Конкретные ресурсы, на которые я должен ссылаться в подобных случаях?

Если это имеет значение, я использую для компиляции platformio в системе Linux.

, 👍1


2 ответа


Лучший ответ:

3

Кажется, вы запутались в косвенных указателях, которые смешиваются с неявными косвенными обращениями, сделанными компилятором.

Здесь:

внешняя "C"
void __attribute__ ((используется, noinline)) call(func_ptr *ptr) {
(*указатель)();
}

Параметр ptr не является указателем на функцию: это указатель на указатель функции. Вы должны разыменовать его дважды, чтобы получить функция: (**ptr)();. Обратите внимание, что компилятор неявно разыменовывает указатель на функцию, если вы называете его как функцию: вот почему вы не получаете ошибку, явно разыменовав указатель только один раз. в сгенерированная сборка, строки

ld  r30, X+
ld  r31, X

разыменовать ptr, чтобы получить адрес вызываемой функции.

Возможно, вы хотели написать

внешняя "C"
void __attribute__ ((используется, noinline)) call(func_ptr ptr) {
указатель();
}

А затем используйте его как:

вызов(задачи[0]);

Если вы просто вызов(задачи), идентификатор массива превращается в указатель, и компилятор обрабатывает это как ярлык для call(&tasks[0]);, который оказывается совместимым с вашей первоначальной реализацией call().

Теперь во встроенной сборке у вас есть

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

Сборка обрабатывает tasks как символ, представляющий адрес массив tasks. Компоновщик заменяет эти символы фактическими адрес (0x0200). Эти инструкции читают ОЗУ в этом и следующий адрес. Эти слоты оперативной памяти содержат первый элемент массива, а именно &func_a. Итак, теперь у вас есть этот адрес в паре регистров X. Не нужно снова читать ОЗУ: вы можете movw r30, r26 и сделать непрямое Прыгать. Или, что еще лучше, для начала загрузите в Z:

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

См. также ответ Маженко: здесь я предполагаю, что EIND каким-то образом правильно настроить для начала. На Uno вы просто использовали бы ijmp вместо eijmp и не беспокойтесь о EIND.

,

Такого рода объяснение вещей. Когда я использовал (tasks) в asm, я ожидал, что он загрузит адрес первого элемента массива, а не значение, на которое он указывает. Я не уверен, как это сделать. Возможно, мне придется понять расширенный синтаксис ассемблера., @darkspine

@darkspine: Вы можете загрузить адрес задач, например, ldi r26, lo8(tasks)\n ldi r27, hi8(tasks). В отличие от lds, инструкция ldi не обращается к оперативной памяти, поскольку адрес известен во время сборки и заполняется компоновщиком., @Edgar Bonet


2

Начну с того, что я не эксперт в сборке AVR, поэтому понятия не имею, правильно ли что-то из этого. Однако, прочитав различные документы по этому вопросу, я нашла:

  • На ATMega2560 EIJMP использует комбинацию регистра Z и регистра EIND для формирования полного адреса.

Это означает, что наряду с настройкой регистра Z для младших 16 бит адреса вам также необходимо настроить регистр EIND для старших 5 битов адреса.

Косвенный переход к адресу, на который указывает регистр указателя Z (16 бит) в файле регистров и регистр EIND в пространстве ввода-вывода. Эта инструкция допускает непрямые переходы ко всему пространству памяти программы объемом 4М (слов).

Затем следует пример:

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

Как определить, каким должен быть регистр EIND? Ну, в общем, нет. Вы бы не использовали EIJMP. Из того, что я понимаю, для функций за пределами нижних 64 тыс. слов адресного пространства компилятор строит «таблицу переходов»; который хранится в нижней области памяти. Вы переходите к записи в этой таблице с помощью IJMP, а затем эта таблица переходит к конечному пункту назначения с помощью EIJMP для вас.

Выполнение таких прыжков в ассемблере не очень хорошая вещь, чтобы пытаться понять. Это грязно. Лучше позволить компилятору сделать это за вас и использовать C, а не ассемблер. Ребята из AVR-GCC приложили немало усилий, чтобы указатели на функции работали правильно (судя по тому, что я читал, это было непросто даже для них), а вы просто заново изобретаете велосипед.

По крайней мере, вы должны для начала реализовать свой код на 100 % на C, а затем посмотреть на скомпилированный вывод, чтобы увидеть, как, по мнению компилятора, это нужно сделать.

,

Я переключился на eijmp, потому что это то, что использовал gcc. Проблема оказалась в том, что задачи разыменовываются при использовании в ассемблер. Кроме того, спасибо за документ, он намного более подробный, чем техническое описание ATMega2560., @darkspine