如何在C++类内存结构中创建"spacer"?

How do I create a "spacer" in a C++ class memory structure?

本文关键字:创建 spacer 结构 内存 C++      更新时间:2023-10-16

问题

低级裸机嵌入式上下文中,我想在内存中创建一个空白空间,在C++结构中,没有任何名称,以禁止用户访问此类内存位置。

现在,我已经通过在位域中放置一个丑陋的uint32_t :96;位域来实现它,该位域可以方便地代替三个单词,但它会引发GCC的警告(位域太大而无法容纳uint32_t),这是非常合法的。

虽然它工作正常,但当您想分发带有数百个警告的库时,它不是很干净......

我该如何正确执行此操作?

为什么首先会出现问题?

我正在从事的项目包括定义整个微控制器系列(STMicroelectronics STM32)不同外设的存储器结构。为此,结果是一个类,其中包含多个结构的联合,这些结构定义所有寄存器,具体取决于目标微控制器。

一个非常简单的外设的简单示例如下:通用输入/输出 (GPIO)

union
{
struct
{
GPIO_MAP0_MODER;
GPIO_MAP0_OTYPER;
GPIO_MAP0_OSPEEDR;
GPIO_MAP0_PUPDR;
GPIO_MAP0_IDR;
GPIO_MAP0_ODR;
GPIO_MAP0_BSRR;
GPIO_MAP0_LCKR;
GPIO_MAP0_AFR;
GPIO_MAP0_BRR;
GPIO_MAP0_ASCR;
};
struct
{
GPIO_MAP1_CRL;
GPIO_MAP1_CRH;
GPIO_MAP1_IDR;
GPIO_MAP1_ODR;
GPIO_MAP1_BSRR;
GPIO_MAP1_BRR;
GPIO_MAP1_LCKR;
uint32_t :32;
GPIO_MAP1_AFRL;
GPIO_MAP1_AFRH;
uint32_t :64;
};
struct
{
uint32_t :192;
GPIO_MAP2_BSRRL;
GPIO_MAP2_BSRRH;
uint32_t :160;
};
};

其中所有GPIO_MAPx_YYY都是宏,定义为uint32_t :32或寄存器类型(专用结构)。

在这里,您可以看到运行良好的uint32_t :192;,但它会触发警告。

到目前为止,我所考虑的:

我可能已经用几个uint32_t :32;(这里 6 个)替换了它,但我有一些极端情况,我有uint32_t :1344;(42)(等等)。所以我宁愿不在其他 8k 行之上添加大约一百行,即使结构生成是脚本的。

确切的警告消息如下所示:width of 'sool::ll::GPIO::<anonymous union>::<anonymous struct>::<anonymous>' exceeds its type(我只是喜欢它的阴暗)。

我宁愿通过简单地删除警告来解决这个问题,而是使用

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-WTheRightFlag"
/* My code */
#pragma GCC diagnostic pop

可能是一个解决方案...如果我找到TheRightFlag.但是,正如此线程中指出的,gcc/cp/class.c这个悲伤的代码部分:

warning_at (DECL_SOURCE_LOCATION (field), 0,
"width of %qD exceeds its type", field);

这告诉我们没有-Wxxx标志可以删除此警告......

C++式的方式怎么样?

namespace GPIO {
static volatile uint32_t &MAP0_MODER = *reinterpret_cast<uint32_t*>(0x4000);
static volatile uint32_t &MAP0_OTYPER = *reinterpret_cast<uint32_t*>(0x4004);
}
int main() {
GPIO::MAP0_MODER = 42;
}

由于命名空间GPIO,您可以获得自动完成,并且不需要虚拟填充。甚至,它更清楚发生了什么,因为您可以看到每个寄存器的地址,您根本不需要依赖编译器的填充行为。

使用多个相邻的匿名位域。 所以代替:

uint32_t :160;

例如,您将拥有:

uint32_t :32;
uint32_t :32;
uint32_t :32;
uint32_t :32;
uint32_t :32;

每个要匿名的寄存器一个。

如果要填充较大的空间,则使用宏重复单个 32 位空间可能会更清晰且更不容易出错。 例如,给定:

#define REPEAT_2(a) a a
#define REPEAT_4(a) REPEAT_2(a) REPEAT_2(a)
#define REPEAT_8(a) REPEAT_4(a) REPEAT_4(a)
#define REPEAT_16(a) REPEAT_8(a) REPEAT_8(a)
#define REPEAT_32(a) REPEAT_16(a) REPEAT_16(a)

然后可以这样添加一个 1344(42 * 32 位)空格:

struct
{
...
REPEAT_32(uint32_t :32;) 
REPEAT_8(uint32_t :32;) 
REPEAT_2(uint32_t :32;)
...
};

在嵌入式系统领域,您可以通过使用结构或定义指向寄存器地址的指针来对硬件进行建模。

不建议按结构建模,因为允许编译器在成员之间添加填充以进行对齐(尽管嵌入式系统的许多编译器都有用于打包结构的编译指示)。

例:

uint16_t * const UART1 = (uint16_t *)(0x40000);
const unsigned int UART_STATUS_OFFSET = 1U;
const unsigned int UART_TRANSMIT_REGISTER = 2U;
uint16_t * const UART1_STATUS_REGISTER = (UART1 + UART_STATUS_OFFSET);
uint16_t * const UART1_TRANSMIT_REGISTER = (UART1 + UART_TRANSMIT_REGISTER);

您也可以使用数组表示法:

uint16_t status = UART1[UART_STATUS_OFFSET];  

如果您必须使用结构,恕我直言,跳过地址的最佳方法是定义一个成员而不是访问它:

struct UART1
{
uint16_t status;
uint16_t reserved1; // Transmit register
uint16_t receive_register;
};

在我们的一个项目中,我们同时拥有来自不同供应商的常量和结构(供应商 1 使用常量,而供应商 2 使用结构)。

geza说得对,你真的不想为此使用类。

但是,如果你坚持,添加n字节宽度的未使用成员的最佳方法是这样做:

char unused[n];

如果添加特定于实现的杂注以防止向类的成员添加任意填充,则可以这样做。


对于 GNU C/C++(gcc、clang 和其他支持相同扩展的内容),放置属性的有效位置之一是:

#include <stddef.h>
#include <stdint.h>
#include <assert.h>  // for C11 static_assert, so this is valid C as well as C++
struct __attribute__((packed)) GPIO {
volatile uint32_t a;
char unused[3];
volatile uint32_t b;
};
static_assert(offsetof(struct GPIO, b) == 7, "wrong GPIO struct layout");

(Godbolt 编译器资源管理器上的示例显示offsetof(GPIO, b)= 7 字节。

扩展@Clifford和@Adam Kotwasinski的答案:

#define REP10(a)        a a a a a a a a a a
#define REP1034(a)      REP10(REP10(REP10(a))) REP10(a a a) a a a a
struct foo {
int before;
REP1034(unsigned int :32;)
int after;
};
int main(void){
struct foo bar;
return 0;
}

要扩展 Clifford 的答案,您可以随时宏化匿名位字段。

所以而不是

uint32_t :160;

#define EMPTY_32_1 
uint32_t :32
#define EMPTY_32_2 
uint32_t :32;      // I guess this also can be replaced with uint64_t :64
uint32_t :32
#define EMPTY_32_3 
uint32_t :32;     
uint32_t :32;     
uint32_t :32
#define EMPTY_UINT32(N) EMPTY_32_ ## N

然后像这样使用它

struct A {
EMPTY_UINT32(3);
/* which resolves to EMPTY_32_3, which then resolves to real declarations */
}

不幸的是,您需要与您:(的字节数一样多的EMPTY_32_X变体 尽管如此,它仍然允许您在结构中使用单个声明。

将大型间隔符定义为 32 位组。

#define M_32(x)   M_2(M_16(x))
#define M_16(x)   M_2(M_8(x))
#define M_8(x)    M_2(M_4(x))
#define M_4(x)    M_2(M_2(x))
#define M_2(x)    x x
#define SPACER int : 32;
struct {
M_32(SPACER) M_8(SPACER) M_4(SPACER)
};

我认为引入更多的结构是有益的;这反过来又可以解决垫片的问题。

命名变体

虽然平面命名空间很好,但问题是你最终会得到一个杂乱无章的字段集合,并且没有简单的方法来将所有相关字段传递在一起。此外,通过在匿名联合中使用匿名结构,不能传递对结构本身的引用,也不能将它们用作模板参数。

因此,作为第一步,我会考虑打破struct

// GpioMap0.h
#pragma once
// #includes
namespace Gpio {
struct Map0 {
GPIO_MAP0_MODER;
GPIO_MAP0_OTYPER;
GPIO_MAP0_OSPEEDR;
GPIO_MAP0_PUPDR;
GPIO_MAP0_IDR;
GPIO_MAP0_ODR;
GPIO_MAP0_BSRR;
GPIO_MAP0_LCKR;
GPIO_MAP0_AFR;
GPIO_MAP0_BRR;
GPIO_MAP0_ASCR;
};
} // namespace Gpio
// GpioMap1.h
#pragma once
// #includes
namespace Gpio {
struct Map1 {
// fields
};
} // namespace Gpio
// ... others headers ...

最后,全局标头:

// Gpio.h
#pragma once
#include "GpioMap0.h"
#include "GpioMap1.h"
// ... other headers ...
namespace Gpio {
union Gpio {
Map0 map0;
Map1 map1;
// ... others ...
};
} // namespace Gpio

现在,我可以编写一个void special_map0(Gpio:: Map0 volatile& map);,并一目了然地快速概述所有可用的体系结构。

简单垫片

通过将定义拆分为多个标头,标头单独更易于管理。

因此,我完全满足您的要求的最初方法是坚持重复std::uint32_t:32;。是的,它在现有的 8k 行中添加了几行 100 行,但由于每个标头都单独较小,因此可能没有那么糟糕。

不过,如果您愿意考虑更多奇特的解决方案......

介绍 $。

一个鲜为人知的事实是,$是C++标识符的可行字符;它甚至是一个可行的起始字符(与数字不同)。

源代码中出现的$可能会引起人们的注意,$$$$肯定会在代码审查期间引起注意。这是您可以轻松利用的功能:

#define GPIO_RESERVED(Index_, N_) std::uint32_t $$$$##Index_[N_];
struct Map3 {
GPIO_RESERVED(0, 6);
GPIO_MAP2_BSRRL;
GPIO_MAP2_BSRRH;
GPIO_RESERVED(1, 5);
};

您甚至可以将一个简单的"lint"放在一起作为预提交钩子或在您的 CI 中,它在提交的C++代码中查找$$$$并拒绝此类提交。

虽然我同意结构不应该用于MCU I/O端口访问,但原始问题可以这样回答:

struct __attribute__((packed)) test {
char member1;
char member2;
volatile struct __attribute__((packed))
{
private:
volatile char spacer_bytes[7];
}  spacer;
char member3;
char member4;
};

您可能需要将__attribute__((packed))替换为#pragma pack或类似语法,具体取决于编译器语法。

在结构中混合使用私有和公共成员通常会导致C++标准不再保证内存布局。 但是,如果结构的所有非静态成员都是私有的,则它仍被视为 POD/标准布局,嵌入它们的结构也是如此。

出于某种原因,如果匿名结构的成员是私有的,gcc 会生成警告,所以我不得不给它起一个名字。或者,将其包装到另一个匿名结构中也可以摆脱警告(这可能是一个错误)。

请注意,spacer成员本身不是私有的,因此仍可以通过以下方式访问数据:

(char*)(void*)&testobj.spacer;

然而,这样的表达看起来像一个明显的黑客,希望没有真正充分的理由就不会被使用,更不用说作为一个错误了。

反解决方案。

不要这样做:混合使用私有和公共字段。

也许带有计数器的宏来生成 uniqie 变量名称会很有用?

#define CONCAT_IMPL( x, y ) x##y
#define MACRO_CONCAT( x, y ) CONCAT_IMPL( x, y )
#define RESERVED MACRO_CONCAT(Reserved_var, __COUNTER__) 

struct {
GPIO_MAP1_CRL;
GPIO_MAP1_CRH;
GPIO_MAP1_IDR;
GPIO_MAP1_ODR;
GPIO_MAP1_BSRR;
GPIO_MAP1_BRR;
GPIO_MAP1_LCKR;
private:
char RESERVED[4];
public:
GPIO_MAP1_AFRL;
GPIO_MAP1_AFRH;
private:
char RESERVED[8];
};