我希望能够配置一个类,以便能够访问其成员函数中的硬件。让我们假设我们有一个avr设备,在那里我们可以访问像PORTA = 0x00;
这样的硬件,它将0x00
写入io内存空间。这个问题适用于所有类型的嵌入式内存io访问,而不是特定于avr。
但是,如果我现在想使用一个可以参数化的类,C++似乎已经关闭了所有的门,因为似乎不可能再定义任何类型的指针类型并给它们一个constexpr值。
在以前的一些编译器版本中,我们能够运行这样的代码,如:constexpr对avr端口地址的引用
但现在所有尝试为constexpr值的指针赋值的操作都失败了,因为在这种情况下不能再使用reinterpret_cast
。
作为一个肮脏的黑客,我尝试并失败了:
struct ONE
{
static constexpr volatile uint8_t* helper=nullptr;
static constexpr volatile uint8_t* portc=&helper[100];
};
失败:
x.cpp:6:57: error: arithmetic involving a null pointer in '0'
6 | static constexpr volatile uint8_t* portc=&helper[100];
同样失败:
// for AVR PORTB is defined in io.h like:
#define PORTB (*(volatile uint8_t *)((0x05) + 0x20))
constexpr volatile uint8_t* ptr=&PORTB;
失败:
x.cpp: In function 'int main()':
x.cpp:15:37: error: 'reinterpret_cast<volatile uint8_t* {aka volatile unsigned char*}>(56)' is not a constant expression
15 | constexpr volatile uint8_t* ptr=&PORTB;
这直接让我找到了interpret_ cast<挥发性uint8_t*>(37(';不是常量表达式。但也没有任何解决方案!
我的目标非常简单:编写一些可以配置为使用特定寄存器的类,如:
template < volatile uint8_t* REG>
class X
{
public:
X() { REG = 0x02; }
};
如果我们不能再将指针值定义为constexpr值,我们就不能在模板中使用它们,也不能直接使用它们。这意味着,我们只有运行时变量,这些变量无法再优化,并且总是需要ram和flash中的空间。这对于非常小的嵌入式系统来说是不可接受的。
如果真的是这样的话,唯一的工作方法就是使用c-macross?我不敢相信我的代码不会再工作了。。。如果不使用C宏,将来将永远无法工作。
我目前使用avr-g++ (Fedora 10.2.0-1.fc33) 10.2.0
,但从我的所有读数来看,如果在C++17模式中使用,这似乎是正确的行为。
我认为这在C++17中根本不可能。但是,如果您愿意切换到C++20,有一个可能的解决方法。与其直接伪造constexpr
指针,不如创建一个自定义类型来表示它,当留下constexpr
上下文时,该类型将在第一时间转换为指针:
#include <cstdint>
template <typename T>
struct fixed_ptr
{
std::uintptr_t m_value;
inline constexpr explicit fixed_ptr(std::uintptr_t p) :
m_value { p }
{}
inline operator T * () const
{ return reinterpret_cast<T *>(m_value); }
};
然后,与其直接为模板参数指定类型,不如用一个概念来约束它:
#include <utility>
#include <cstdint>
template <typename T, typename U>
concept pointerish = std::is_same_v<U &, decltype(*std::declval<T>())>;
template <pointerish<volatile std::uint8_t> auto REG>
class X {
public:
X() { *REG = 0x02; }
};
volatile std::uint8_t x;
auto p = X<fixed_ptr<volatile std::uint8_t>(5)>();
auto q = X<&x>();
在-O1
中,应该没有在直接指针上使用类包装器的开销:内联将处理它
现在,如何处理现有的预处理器宏?您不能将地址强制转换回uintptr_t
,因为它构成了reinterpret_cast
,而这在constexpr上下文中是被禁止的。您可以在头文件上运行gcc -E -dM
来提取宏,作为额外的构建步骤:
echo '#include <avr/io.h>' |
gcc -E -dM - |
sed -ne '
s!#define (PORT[A-Za-z0-9_]*) ( ** *( *(.*) **)(.*))!DEF_PORT(1, 2, 3)!p'
> avr-io.hh
然后,您可以使用生成的文件创建自己的C++兼容头,如下所示:
#define DEF_PORT(name, type, addr)
constinit fixed_ptr<type> name = addr;
#include "avr-io.hh"
#undef DEF_PORT
但是,如果你不想添加构建步骤,那么总会有一些肮脏的预处理器伎俩来拯救你:
#define volatile ), (
#define UNWRAP_ADDR2__(x)
#define UNWRAP_ADDR1__(x) UNWRAP_ADDR2__ x
#define UNWRAP_ADDR0__(x, y) y
#define UNWRAP_ADDR(x) UNWRAP_ADDR1__(UNWRAP_ADDR0__ x)
constexpr std::uintptr_t ADDR_PORTA = UNWRAP_ADDR(PORTA);
constexpr std::uintptr_t ADDR_PORTB = UNWRAP_ADDR(PORTB);
#undef volatile
在这里,我们利用了寄存器宏定义的形式为(*(volatile XXX *)(YYY))
的事实。将volatile
定义为), (
会将寄存器宏拆分为UNWRAP_ADDR0__
的两个宏参数,这就去掉了包含解引用运算符的"第一个参数",而UNWRAP_ADDR1__
和UNWRAP_ADDR2__
则去掉了类型转换的其余部分。您只剩下数字地址。
下面是一个使用lambda作为端口地址的模板示例。
模板可以定义为:
template <GetPortAdr PORT_ADR>
struct Register
{
// Call lambda to get the address and de-reference the pointer.
static inline void set() { *PORT_ADR() = 2; }
};
其中类型GetPortAdr将被定义为函数指针:
typedef volatile uint8_t * (*GetPortAdr)();
假设HAL或供应商提供端口定义:
#define PORTA(*(volatile uint8_t *)(0x05000020))
这可以像这样实例化和使用:
Register<[](){return &PORTA;}> port_a;
port_a.set();
丑陋的lambda相关语法增加了混乱,但供应商提供的PORTA#定义可以在不受干扰的情况下使用。
2个注意事项:
- c++20是必需的,否则我会收到声明
lambda-expression in template-argument only available with '-std=c++20'
的错误 - 至少需要-O1的优化,否则会产生消耗闪存的膨胀组件。使用-O1,生成的代码等效于常规的C样式转换
https://godbolt.org/z/5aqTnGa68示例显示了一个C样式#定义,用于和模板方法进行比较。