如何在C ++类的内存结构中创建一个“空格”?


94

问题

低级别的裸机嵌入式环境中,我想在内存中以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标记可删除此警告...


26
你考虑char unused[12];了吗?
MM

3
我只是抑制警告。 [class.bit] / 1保证的行为uint32_t :192;
NathanOliver

3
@NathanOliver我也很高兴,但是似乎该警告不能抑制(使用GCC),或者我没有找到如何做的方法。而且,这仍然不是一个干净的方法(但是会很令人满意)。我设法找到正确的“ -W”标志,但没有设法仅将其应用于我自己的文件(我不希望用户为自己的工作删除这种警告)
J Faucher

3
顺便说一句,您可以写,:42*32而不是:1344
MM

1
尝试以此来抑制警告吗?gcc.gnu.org/onlinedocs/gcc/...
Hitobat

Answers:


36

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

    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;)
    ...
};

感谢你的回答。我已经考虑过了,但是它会在我的某些文件上添加200行(uint32_t :1344;在适当的位置),所以我宁愿不必这样做……
J Faucher

1
@JFaucher为您的行数要求添加了可能的解决方案。如果您有这样的要求,可以在问题中提及它们,以避免获得不符合要求的答案。
克利福德

感谢您的修改,对于未说明行数的问题,我们深表歉意。我的观点是,由于有很多行,我的代码已经很痛苦了,我宁愿避免添加太多代码。因此,我在问是否有人知道一种“干净”或“正式”的方式来避免使用相邻的匿名位域(即使该方法工作正常)。宏方法对我来说似乎很好。顺便说一句,在您的示例中,您没有36 * 32位的空间吗?
J Faucher '18

@JFaucher-已更正。由于寄存器数量众多,因此I / O寄存器映射文件必定很大-通常只需编写一次,并且维护不是问题,因为硬件是一个常数。除了通过“隐藏”寄存器之外,如果以后需要访问它们,则可以自己进行维护工作。您当然知道所有STM32设备都已经有供应商提供的寄存器映射头了吗?使用它会容易出错。
克利福德

2
我同意您的要求,并且公平地说,我认为我将采用您的答案中显示的两种方法之一。我只是想确保C ++在这样做之前不会提供更好的解决方案。我很清楚ST提供了这些标头,但是这些标头是建立在大量使用宏和按位运算的基础上的。我的项目是建立一个等效于这些标头的C ++,这些标头不易出错(使用枚举类,位域等)。这就是为什么我们使用脚本将CMSIS标头“转换”为C ++结构的原因(并在ST文件中发现了一些错误)
J Faucher

45

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名称空间,您将获得自动补全功能,并且不需要虚拟填充。甚至,更清楚的是发生了什么,因为您可以看到每个寄存器的地址,所以您完全不必依赖编译器的填充行为。


1
与从同一函数访问多个MMIO寄存器的结构相比,这可能优化得不够好。通过使用指向寄存器中基地址的指针,编译器可以使用具有立即位移的加载/存储指令,例如ldr r0, [r4, #16],而编译器更可能会因分别声明每个地址而错过该优化。GCC可能会将每个GPIO地址加载到一个单独的寄存器中。(从文字池中,尽管其中一些可以用Thumb编码表示为旋转后的立即数。)
Peter Cordes

4
原来我的担心是没有根据的;ARM GCC也确实以这种方式进行了优化。 godbolt.org/z/ztB7hi。但是请注意,您想要的static volatile uint32_t &MAP0_MODER不是inline。一个inline变量不编译。(static避免为指针提供任何静态存储,而volatile这正是MMIO想要避免的死存储消除或写/回读优化。)
Peter Cordes,

1
@PeterCordes:内联变量是C ++ 17的新功能。但是您是对的,static在这种情况下也做得很好。感谢您的提及volatile,我将其添加到我的答案中(并将内联更改为静态,因此适用于C ++ 17之前的版本)。
geza

2
这不是严格定义好的行为,请参见此Twitter线程也许这很有用
Shafik Yaghmour

1
@JFaucher:创建尽可能多的名称空间,并在该名称空间中使用独立的函数。因此,您将拥有GPIOA::togglePin()
geza

20

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

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

例:

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使用结构)。


感谢您的回答。但是,当用户获得自动完成功能(您将只显示正确的属性)时,我选择使用结构化方法来简化用户的工作,并且我不想“向”用户显示保留的插槽,因为在我的第一篇评论中指出。
J Faucher '18

static假设自动完成功能能够显示静态成员,则使上述地址成为结构的成员仍然可以实现。如果没有,它也可以是内联成员函数。
Phil1970 '18

@JFaucher我不是嵌入式系统人员,还没有测试过,但是通过声明保留成员为私有状态是否可以解决自动完成问题?(您可以在一个结构声明私有成员,你可以使用public:private:多次只要你想,拿到场的正确排序。)
纳撒尼尔

1
@Nathaniel:不是这样;如果类同时具有publicprivate非静态数据成员,则它不是标准布局类型,因此它不提供您所考虑的排序保证。(而且我很确定OP的用例确实需要标准布局类型。)
ruakh

1
别忘volatile了那些声明,BTW,用于内存映射的I / O寄存器。
彼得·科德斯

13

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个字节。)


9

扩展@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;
}

根据评论的更多要求,我在回答中纳入了您建议的一种变体。贷方到期的贷方。
克利福德

7

要扩展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字节一样多的变体:(尽管如此,它允许您在结构中具有单个声明。


5
通过使用Boost CPP宏,我认为您可以使用递归来避免手动创建所有必要的宏。
彼得·科德斯

3
您可以级联它们(达到预处理器递归限制,但这通常足够了)。所以,#define EMPTY_32_2 EMPTY_32_1; EMPTY_32_1#define EMPTY_32_3 EMPTY_32_2; EMPTY_32_1
Miral

也许是@PeterCordes,但是标签表明也许需要C和C ++兼容性。
克利福德

2
C和C ++使用相同的C预处理程序;除了可能为C提供必要的boost标头之外,我没有看到其他问题。他们确实将CPP宏的内容放在单独的标头中。
彼得·科德斯

1

将大间隔符定义为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)
};

1

我认为引入更多的结构将是有益的。进而可以解决隔离物的问题。

命名变体

虽然平面名称空间不错,但问题是您最终会杂乱地收集字段,而没有简单的方法将所有相关字段一起传递。此外,通过在匿名联合中使用匿名结构,您不能传递对结构本身的引用,也不能将其用作模板参数。

因此,作为第一步,我将考虑突破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行中增加了几百条线,但是由于每个标头分别较小,因此可能不会那么糟。

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

介绍$。

鲜为人知的事实$是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);
};

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


1
请记住,OP的特定用例用于向编译器描述内存映射的I / O寄存器。它从来没有有意义的价值,整个结构复制。(GPIO_MAP0_MODER大概每个成员都喜欢volatile。)不过,以前匿名成员的引用或模板参数用法也可能有用。当然,对于填充结构的一般情况。但是用例说明了OP为何将其保留为匿名。
彼得·科德斯

如果$$$padding##Index_[N_];在自动完成或调试时出现了字段名称,则可能使字段名称更易解释。(或者zz$$$padding按照GPIO...名称进行排序,因为根据OP,本练习的重点是自动完成内存映射的I / O位置名称。)
Peter Cordes

@PeterCordes:我再次扫描了答案以进行检查,但是再也没有提到复制的内容。不过,我确实忘记了volatile参考文献中的限定词,该词已被更正。至于命名;我将其交给OP。有很多变体(填充,保留,...),甚至自动完成的“最佳”前缀也可能取决于手头的IDE,尽管我很欣赏调整排序的想法。
Matthieu M.

我指的是“ 并且不是将所有相关字段一起传递的简单方法 ”,这听起来像是结构分配,而其余句子则是关于命名工会的结构成员。
彼得·科德斯

1
@PeterCordes:我正在考虑通过引用传递,如下所述。我发现尴尬的是,OP的结构阻止它们创建“模块”,这些模块可以被静态证明只能访问特定的体系结构(通过引用特定的struct),并且union最终以甚至特定于体系结构的位传播到各处。可以不在乎别人。
Matthieu M.

0

尽管我同意不应将结构用于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 /标准布局,嵌入它们的结构也将被视为POD /标准布局。

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

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

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

但是,这样的表达看起来很明显,希望在没有充分理由的情况下不能使用,更不用说是错误了。


1
用户不能在名称中任何地方包含双下划线的名称空间中声明标识符(在C ++中,或仅在C开头);这样做会使代码格式错误。这些名称是为实现保留的,因此从理论上讲,它们可能以可怕的微妙和反复无常的方式与您的名称冲突。无论如何,如果编译器包含代码,则没有义务保留它们。这样的名称并不是获取供您自己使用的“内部”名称的快速方法。
underscore_d

谢谢,修复它。
杰克·怀特

-1

反解。

不要这样做:混合私有和公共领域。

带有计数器以生成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];
};


3
好。如果没人介意,我会把答案留给不做的事情。
罗伯特·安德鲁杰克

4
@NicHartley考虑到答案的数量,我们接近一个“研究”问题。在研究中,僵局的知识仍然是知识,它可以避免他人走错路。勇敢者+1。
Oliv '18

1
@Oliv而且我-1'是因为OP需要一些东西,这个答案违反了要求,因此这是一个错误的答案。在任何评论中,我都没有对这个人做出任何正面或负面的价值判断,只是对答案做出了判断。我认为我们都可以同意这是不好的。关于这个人的话对这个网站来说是题外话。(尽管IMO愿意花一些时间来提出一个想法的人做的对的,即使这个想法没有成功)
Fund Monica的诉讼

2
是的,这是错误的答案。但是我担心有些人可能会得出相同的想法。由于评论和链接,我才学到了一些东西,这对我来说并不是负面的。
罗伯特·安德鲁杰克
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.