我正在写一个函数:
void callFunctionAt(uint32_t address){
//There is a void at address, how do I run it?
}
这是在 Atmel Studio 的 C++ 中。如果要相信前面的问题,简单的答案是写“address();”行。这不可能是正确的。如果不更改此函数的标头,如何调用位于给定地址的函数?
对于所有支持标准 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();
}
但这不安全也不可移植,因为它假定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 位长,参见。void*
按照avr-gcc ABI的布局。 这意味着在闪存 > 128KiB 的设备上使用标量地址通常不起作用,例如当您callFunctionAt (0x30000)
在 ATmega2560 上发布时。
Z
在此类设备上,指令使用的寄存器中的 16 位地址由特殊功能寄存器EICALL
中保存的值扩展,输入后不得更改。avr-g++ 文档对此很清楚。EIND
EIND
main
这里的关键点是您如何获取地址。首先,为了正确调用和传递它,请使用函数指针:
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);
}
但这是否有效取决于您的向量表所在的位置,或者更具体地说,取决于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) 为您发出复位。
另一种情况是当您想要类的非静态方法的地址时,因为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 在调用我编写的函数时究竟做了什么很有趣(至少对我而言),但它需要知道指令是做什么的。您在 AVR 中进行开发,因此您可以在此 PDF 中阅读一组说明并与您的程序集文件进行比较。