Как вызвать функцию с адреса памяти в AVR C? - PullRequest
0 голосов
/ 06 февраля 2020

Я пишу функцию:

void callFunctionAt(uint32_t address){
  //There is a void at address, how do I run it?
}

Это в C ++ Atmel Studio. Если верить предыдущим вопросам, простой ответ - написать строку «address ();». Это не может быть правильным. Без изменения заголовка этой функции, как можно вызвать функцию, расположенную по указанному адресу?

Ответ должен быть системным c для всех микроконтроллеров, которые поддерживают стандартную компиляцию c ++.

Ответы [ 3 ]

4 голосов
/ 06 февраля 2020

Обычный способ сделать это - дать аргументу правильный тип. Затем вы можете вызвать его сразу же:

void callFunctionAt(void (*address)()) {
  address();
}

Однако, поскольку вы написали "Без изменения заголовка этой функции [...]" , вам нужно привести целое число без знака в указателе функции:

void callFunctionAt(uint32_t address) {
  void (*f)() = reinterpret_cast<void (*f)()>(address);
  f();
}

Но это небезопасно и не портабельно, поскольку предполагает, что uint32_t может быть приведен к указателю функции. И это не должно быть правдой: "[...] системная агенция c для всех микроконтроллеров [...]" . Указатели на функции могут иметь ширину, отличную от 32 бит. Обычно указатели могут содержать не только чистый адрес, например включать селектор для областей памяти, в зависимости от архитектуры системы.


Если вы получили адрес из сценария компоновщика, возможно, вы объявили это так:

extern const uint32_t ext_func;

И нравится использовать это так:

callFunctionAt(ext_func);

Но вы можете изменить объявление на:

extern void ext_func();

И вызвать его напрямую или косвенно:

ext_func();

callFunctionAt(&ext_func);

Определение в компоновщике может остаться без изменений, поскольку компоновщик ничего не знает о типах.

3 голосов
/ 06 февраля 2020

Нет универсального c способа. Это зависит от того, какой компилятор вы используете. В дальнейшем я приму avr-g++, потому что он распространен и находится в свободном доступе.

Спойлер: На AVR это сложнее, чем на большинстве других машин.

Предположим, у вас действительно есть uint32_t адрес, который будет байтовым адресом. Функциональные указатели в avr-g++ на самом деле являются адресами слов, где слово имеет 16 битов. Следовательно, вам нужно сначала разделить адрес байта на 2, чтобы получить адрес слова; затем приведите его к указателю на функцию и вызовите его:

#include <stdint.h>

typedef void (*func_t)(void);

void callFunctionAt (uint32_t byte_address)
{
    func_t func = (func_t) (byte_address >> 1);
    func();
}

Если вы начали с адреса слова, вы можете вызывать его без лишних слов:

void callFunctionAt (uint32_t address)
{
    ((func_t) word_address)();
}

Это будет работать только на устройствах с объемом памяти до 128 КБ fla sh! Причина в том, что адреса в avr-g++ имеют длину 16 бит, ср. макет void* согласно avr-g cc ABI . Это означает, что использование скалярных адресов на устройствах с fla sh> 128 КБ вообще не будет работать , например, когда вы запускаете callFunctionAt (0x30000) на ATmega2560.

На таких устройствах 16 -битный адрес в регистре Z, используемый инструкцией EICALL, расширяется на значение, содержащееся в регистре специальной функции EIND, и вы не должны изменять EIND после ввода main. В документации avr-g ++ об этом ясно сказано .

Важным моментом здесь является то, как вы получаете адрес. Во-первых, чтобы правильно вызвать и передать его, используйте указатель на функцию:

typedef void (*func_t)(void);

void callFunctionAt (func_t address)
{
    address();
}

void func (void);

void call_func()
{
    func_t addr = func;
    callFunctionAt (addr);
}

Я использую аргумент void в объявлении, потому что именно так вы и поступите в C. Или, если вам не нравится typedef:

void callFunctionAt (void (*address)(void))
{
    address();
}

void func (void);

void call_func ()
{
    void (*addr)(void) = func;
    callFunctionAt (addr);
}

Если вы хотите вызвать функцию по указанному адресу c слова, например, 0x0 для «сброса» 1 µ C, вы могли бы

void call_0x0()
{
    callFunctionAt ((func_t) 0x0);
}

, но будет ли это работать, зависит от того, где находится ваша векторная таблица, или, более конкретно, от того, как EIND был инициализирован кодом запуска. То, что всегда будет работать, - это использование символа и определение его с помощью -Wl,--defsym,func=0 при связывании со следующим кодом:

extern "C" void func();

void call_func ()
{
    void (*addr)(void) = func;
    callFunctionAt (addr);
}

Большая разница по сравнению с использованием 0x0 непосредственно в том, что компилятор будет переносить символ func с модификатором символа gs, который он не будет делать при непосредственном использовании 0x0:

_Z9call_funcv:
    ldi r24,lo8(gs(func))
    ldi r25,hi8(gs(func))
    jmp _Z14callFunctionAtPFvvE

Это необходимо, если адрес выходит за рамки EIJMP, чтобы рекомендовать компоновщику сгенерировать заглушка.

1 Это не приведет к сбросу аппаратного обеспечения. Лучший подход к принудительному сбросу - позволить сторожевому таймеру (WDT) выполнить сброс для вас.


Methods

Еще одна ситуация, когда вам нужен адрес не статичного c метода класса, потому что вам также нужен указатель this в этом случае:

class A
{
    int a = 1;
public:
    int method1 () { return a += 1; }
    int method2 () { return a += 2; }
};

void callFunctionAt (A *b, int (A::*f)())
{
    A a;
    (a.*f)();
    (b->*f)();
}

void call_method ()
{
    A a;
    callFunctionAt (&a, &A::method1);
    callFunctionAt (&a, &A::method2);
}

2-й аргумент callFunctionAt указывает, какой метод (из данного прототипа) вы хотите, но вам также нужен объект (или указатель на него) для его применения. avr-g++ будет использовать gs при получении адреса метода (при условии, что следующие вызовы не могут быть встроены), поэтому он также будет работать для всех устройств AVR.

0 голосов
/ 06 февраля 2020

Основываясь на комментариях, я думаю, что вы спрашиваете о том, как функционируют вызовы микроконтроллера. Не могли бы вы скомпилировать вашу программу, чтобы увидеть файлы сборки? Я бы порекомендовал вам прочитать один из них.
Каждая функция после компиляции переводится в инструкции, которые может выполнять процессор (загрузка в регистр, добавление в регистр и т. Д. c.).
Итак, ваша компиляция void foo(int x) {statements;} к простым инструкциям процессора и всякий раз, когда вы вызываете foo(x) в своей программе, вы переходите к инструкциям, связанным с foo - вы вызываете подпрограмму.
Насколько я помню, существует CALL функция в AVR для вызова подпрограмм, а имя подпрограммы является меткой, на которой выполняется переход программы и вызов следующей инструкции по адресу. Я думаю, что вы можете прояснить свои сомнения, прочитав некоторые руководства по сборке AVR.
Интересно (по крайней мере, для меня) видеть, что именно делает процессор, когда он вызывает функцию, которую я написал, но для этого нужно знать, что делают инструкции. Вы разрабатываете в AVR, поэтому есть набор инструкций , о которых вы можете прочитать в этом PDF-файле и сравнить с файлами сборки.

...