表示具有联合和位字段的寄存器的问题

problem representing a register with unions and bit fields

我正在用 C++ 编写一个 NES 模拟器,我遇到了一个使用位字段表示寄存器的问题,这导致了一个非常严重的错误。我将内部地址寄存器表示为:

union
    {
        struct
        {
            uint16_t coarseX : 5;            // bit field type is uint16_t, same as reg type
            uint16_t coarseY : 5;
            uint16_t baseNametableAddressX : 1;
            uint16_t baseNametableAddressY : 1;
            uint16_t fineY : 3;
            uint16_t unused : 1;
        } bits;
        uint16_t reg;
    } addressT, addressV;   // temporary VRAM adddress register and VRAM address register

这样我就可以访问单个位域和整个寄存器。

最初我把寄存器写成:

union
    {
        struct
        {
            uint8_t coarseX : 5;             // bit field type is uint8_t, reg type is uint16_t
            uint8_t coarseY : 5;
            uint8_t baseNametableAddressX : 1;
            uint8_t baseNametableAddressY : 1;
            uint8_t fineY : 3;
            uint8_t unused : 1;
        } bits;
        uint16_t reg;
    } addressT, addressV;   // temporary VRAM adddress register and VRAM address register

当位域的类型(例如 coarseX)与寄存器 (reg) 的类型不同时,该错误是由位域行为引起的。 在这种情况下,当我增加一个字段(即 coarseX++)时,reg 成员被更新 "incorrectly",这意味着 reg 中的位模式没有反映由位字段表示的模式(或由位字段表示为我将它们布置在结构中)。 我知道编译器可以在 "allocation units" 中打包位字段,甚至可以插入填充,但为什么当我更改位字段的类型时行为会发生变化?

谁能解释一下为什么?

您用于位字段的类型是用于内部存储的类型。实际布局完全由实现定义。我想你的编译器将位域打包到存储单元中(“坏”示例中的 uint8_t),但不允许它们跨越存储单元边界。喜欢:

    uint8_t coarseX : 5;
// 3 bits remain (out of 8), not enough for coarseY. So these become padding,
// and next storage unit starts here
    uint8_t coarseY : 5;
    uint8_t baseNametableAddressX : 1;
    uint8_t baseNametableAddressY : 1;
// 1 bit remain. Again, too little.
    uint8_t fineY : 3;
    uint8_t unused : 1;

在“好”示例中,16 位足以容纳所有位域,因此编译器可以按照您需要的方式打包它们。有关详细信息,请参阅 https://en.cppreference.com/w/cpp/language/bit_field

还要记住,访问不活跃的联合成员是 C++ 中的 UB。因此,最好使用单个 uint16_t 字段和访问器(这不会阻止类型为 POD/trivial/standard-layout)。

你自己说的:

I know that the compiler can pack bit fields inside "allocation units", and may even insert padding, ...

这正是正在发生的事情。

uint8_t 有 8 位。结构中的前两个字段 coarseXcoarseY,每个字段有 5 位,不能连续放入内存中的单个字节中。编译器将 coarseX 存储在第一个字节中,然后必须将 coarseY 推入内存中的第二个字节,在内存中的 coarseXcoarseY 之间留下 3 个未使用的位您在寄存器中的值。

接下来的 3 个字段,coarseYbaseNametableAddressXbaseNametableAddressY,总共 7 位,因此它们适合第二个字节。

但是那个字节不能保存 fineYunused 字段,所以它们被推到内存中的第 3 个字节,在 baseNametableAddressYbaseNametableAddressY 之间的内存中留下 1 个未使用的位fineY 抵消了您在寄存器中的值。并且寄存器无法访问第 3 个字节!

因此,实际上,您的 struct 最终表现得就像您已这样声明它:

    union
    {
        struct
        {
            // byte 1
            uint8_t coarseX : 5;
            uint8_t padding1 : 3;

            // byte 2
            uint8_t coarseY : 5;
            uint8_t baseNametableAddressX : 1;
            uint8_t baseNametableAddressY : 1;
            uint8_t padding2 : 1;

            // byte 3!
            uint8_t fineY : 3;
            uint8_t unused : 1;
            uint8_t padding3 : 4;
        } bits;
        struct {
            uint16_t reg; // <-- 2 bytes!
            uint8_t padding4; // <-- ! 
        }
    } addressT, addressV;   // temporary 

使用 uint16_t 而不是 uint8_t,你不会 运行 解决这个问题,因为有足够的位分配给寄存器来保存所有的您正在定义的位。