我在为 AVR 微控制器编程时也遇到了这个问题。 Avr-libc 有头文件(包含在<avr/io.h> 中,通过定义macros such as 为每个微控制器提供寄存器布局:
#define TCNT1 (*(volatile uint16_t *)(0x84))
这允许像使用普通变量一样使用TCNT1,并且任何读取和写入都会自动定向到内存地址0x84。但是,它还包含一个(隐式)reinterpret_cast,它可以防止在常量表达式中使用这个“变量”的地址。而且由于这个宏是由 avr-libc 定义的,因此更改它以删除强制转换并不是一个真正的选择(并且自己重新定义这些宏是可行的,但随后需要为所有不同的 AVR 芯片定义它们,复制来自 avr-libc 的信息) .
由于 Shafik 在此处建议的折叠技巧似乎不再适用于 gcc 7 及更高版本,因此我一直在寻找另一种解决方案。
仔细查看 avr-libc 头文件,turns out they have two modes:
- 通常,它们会定义类似变量的宏,如上所示。
- 在汇编器内部使用时(或包含在 _SFR_ASM_COMPAT 中定义时),它们定义只包含地址的宏,例如:
#define TCNT1 (0x84)
乍一看后者似乎很有用,因为您可以在包含<avr/io.h> 之前设置_SFR_ASM_COMPAT 并简单地使用intptr_t 常量并直接使用地址,而不是通过指针。但是,由于您只能包含一次 avr-libc 标头(iow,只有 TCNT1 作为变量类宏或地址),因此此技巧仅适用于不包含任何其他文件的源文件那将需要类似变量的宏。在实践中,这似乎不太可能(尽管也许您可以在 .h 文件中声明 constexpr(类?)变量并在 .cpp 文件中分配一个不包含其他内容的值?)。
无论如何,我找到了another trick by Krister Walfridsson,它将这些寄存器定义为 C++ 头文件中的外部变量,然后使用汇编程序 .S 文件将它们定义并定位在固定位置。然后您可以简单地获取这些全局符号的地址,这在 constexpr 表达式中是有效的。为了使这项工作有效,这个全局符号必须与原始寄存器宏具有不同的名称,以防止两者之间发生冲突。
例如在您的 C++ 代码中,您将拥有:
extern volatile uint16_t TCNT1_SYMBOL;
struct foo {
static constexpr volatile uint16_t* ptr = &TCNT1_SYMBOL;
};
然后您在项目中包含一个 .S 文件,其中包含:
#include <avr/io.h>
.global TCNT1_SYMBOL
TCNT1_SYMBOL = TCNT1
在写这篇文章时,我意识到上面的内容不仅限于 AVR-libc 案例,还可以应用于这里提出的更一般的问题。在这种情况下,您可以得到一个如下所示的 C++ 文件:
extern char MY_PTR_SYMBOL;
struct foo {
static constexpr const void* ptr = &MY_PTR_SYMBOL;
};
auto main() -> int {
return 0;
}
还有一个 .S 文件,看起来像:
.global MY_PTR_SYMBOL
MY_PTR_SYMBOL = 0x1
这是它的样子:https://godbolt.org/z/vAfaS6(不过,我不知道如何让编译器资源管理器将 cpp 和 .S 文件链接在一起
这种方法有相当多的样板,但似乎在 gcc 和 clang 版本中可靠地工作。请注意,这种方法看起来类似于使用链接器命令行选项或链接器脚本将符号放置在某个内存地址的类似方法,但该方法高度不可移植且难以集成到构建过程中,而上面建议的方法更便携只需在构建中添加一个 .S 文件即可。