如何在 AVR C 中从它的内存地址调用函数?



我正在编写一个函数:

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

这是 Atmel Studio 的C++。如果要相信前面的问题,简单的答案是写"address();"这一行。这不可能是正确的。在不更改此函数的标头的情况下,如何调用位于给定地址的函数?

对于所有支持标准 c++ 编译的微控制器,答案应该是与系统无关的。

执行此操作的常用方法是为参数提供正确的类型。然后你可以立即调用它:

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

但是,由于您编写了"不更改此函数的标头 [...]">,因此您需要将无符号整数强制转换为函数指针:

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

但这并不安全,也不是 portabel,因为它假设uint32_t可以强制转换为函数指针。这不一定是真的:">[...]所有微控制器的系统无关 [...]"。函数指针的宽度可以超过 32 位。指针通常可能包含比纯地址更多的内容,例如包括内存空间的选择器,具体取决于系统的体系结构。


如果从链接器脚本获取地址,则可能已按如下方式声明它:

extern const uint32_t ext_func;

并且喜欢这样使用它:

callFunctionAt(ext_func);

但您可以将声明更改为:

extern void ext_func();

并直接或间接地调用它:

ext_func();
callFunctionAt(&ext_func);

链接器中的定义可以保持原样,因为链接器对类型一无所知。

没有通用的方法。 这取决于您使用的编译器。 在下文中,我将假设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)();
}

这仅适用于闪存高达 128KiB 的设备!原因是avr-g++中的地址长度为16位,参见根据avr-gcc ABI的void*布局。这意味着在闪存> 128KiB 的设备上使用标量地址通常不起作用,例如,当您在 ATmega2560 上发出callFunctionAt (0x30000)时。

在此类设备上,EICALL指令使用的寄存器Z中的16位地址由EIND特殊功能寄存器中的值扩展,并且输入main不得更改EIND。 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);
}

如果要在特定单词地址调用函数,例如0x0"重置">1μC,则可以

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

但这是否有效取决于 Vector 表的位置,或者更具体地说,启动代码初始化EIND的方式。 始终有效的方法是使用符号并在使用以下代码链接时-Wl,--defsym,func=0定义它:

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

与直接使用0x0相比,最大的区别在于编译器将使用符号修饰符包装符号funcgs直接使用0x0时不会这样做:

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

如果地址超出了建议链接器生成存根EIJMP范围,则需要这样做。

1这不会重置硬件。 强制重置的最佳方法是让看门狗计时器 (WDT) 为您发出重置。


方法

另一种情况是,当您需要类的非静态方法的地址时,因为在这种情况下您还需要一个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);
}

callFunctionAt的第二个参数指定您想要的方法(给定原型),但您还需要一个对象(或指向一个对象的指针)来应用它。avr-g++在获取方法地址时将使用gs(前提是无法内联以下调用),因此它也适用于所有 AVR 设备。

根据评论,我认为您是在询问微控制器调用的工作原理。 你能编译你的程序来查看汇编文件吗? 我建议您阅读其中之一。
编译后的每个功能都转换为CPU可以执行的指令(加载到寄存器,添加到寄存器等)。
因此,您的void foo(int x) {statements;}编译为简单的 CPU 指令,每当您在程序中调用foo(x)时,您都会转向与foo相关的指令 - 您正在调用子例程。
据我所知,AVR 中有一个 CALL 函数来调用子例程,子例程的名称是执行程序跳转和调用下一个指令的标签。 我认为当您阅读一些 AVR 组装教程时,您可以澄清您的疑问。
当CPU调用我编写的函数时,看看CPU到底做了什么很有趣(至少对我来说),但它需要知道指令的作用。您在 AVR 中进行开发,因此您可以在此 PDF 中阅读一组说明,并与您的程序集文件进行比较。

最新更新