Не могу вызывать указатели функций С++ из встроенной сборки
Из-за некоторого любопытства я попытался использовать какую-то сборку с моей 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.
@darkspine, 👍1
2 ответа
Лучший ответ:
Кажется, вы запутались в косвенных указателях, которые смешиваются с неявными косвенными обращениями, сделанными компилятором.
Здесь:
внешняя "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
.
Начну с того, что я не эксперт в сборке 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
- Запустить Timer1 в ATmega2560 со сборкой
- Загрузка кода в mega 2560
- Конечный автомат C++ / Wpmf-конверсия
- использование ссылок на SFR в встроенном ассемблере gcc
- Активация определенного макроса в классе из main.cpp
- C++ против языка Arduino?
- Как использовать SPI на Arduino?
- Какие накладные расходы и другие соображения существуют при использовании структуры по сравнению с классом?
Такого рода объяснение вещей. Когда я использовал (tasks) в asm, я ожидал, что он загрузит адрес первого элемента массива, а не значение, на которое он указывает. Я не уверен, как это сделать. Возможно, мне придется понять расширенный синтаксис ассемблера., @darkspine
@darkspine: Вы можете загрузить адрес
задач
, например,ldi r26, lo8(tasks)\n ldi r27, hi8(tasks)
. В отличие от lds, инструкция ldi не обращается к оперативной памяти, поскольку адрес известен во время сборки и заполняется компоновщиком., @Edgar Bonet