如何在嵌入式系统上创建指向寄存器的constexpr指针



我希望能够配置一个类,以便能够访问其成员函数中的硬件。让我们假设我们有一个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值,我们就不能在模板中使用它们,也不能直接使用它们。这意味着,我们只有运行时变量,这些变量无法再优化,并且总是需要ramflash中的空间。这对于非常小的嵌入式系统来说是不可接受的。

如果真的是这样的话,唯一的工作方法就是使用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样式#定义,用于和模板方法进行比较。

最新更新