为什么通过引用写入 bitfield-uint union 会产生错误的汇编指令?

Why does writing to a bitfield-uint union by reference yield wrong assembly instruction?

首先,一些背景:

在我的嵌入式系统 (STM32 ARM Cortex-M4) 中编写传感器驱动程序时出现此问题。

编译器:ARM NONE EABI GCC 7.2.1

表示传感器内部控制寄存器的最佳解决方案是使用带位域的联合,按照这些思路

enum FlagA {
   kFlagA_OFF,
   kFlagA_ON,
};

enum FlagB {
   kFlagB_OFF,
   kFlagB_ON,
};

enum OptsA {
   kOptsA_A,
   kOptsA_B,
   .
   .
   .
   kOptsA_G  // = 7
};

union ControlReg {
    struct {
        uint16_t  RESERVED1 : 1;
        FlagA     flag_a    : 1;
        uint16_t  RESERVED2 : 7;
        OptsA     opts_a    : 3;
        FlagB     flag_b    : 1;
        uint16_t  RESERVED3 : 3;
    } u;
    uint16_t reg;
};

这允许我单独寻址寄存器的位(例如ctrl_reg.u.flag_a = kFlagA_OFF;),并且它允许我一次设置整个寄存器的值(例如ctrl_reg.reg = 0xbeef;)。

问题:

当尝试使用通过函数调用从传感器获取的值填充寄存器时,通过指针传递联合,然后仅更新寄存器的 opts_a 部分,然后再将其写回传感器(如下所示),编译器生成错误的 bitfield insert 汇编指令。

ControlReg ctrl_reg;
readRegister(&ctrl_reg.reg);

ctrl_reg.opts_a = kOptsA_B;  // <-- line of interest

writeRegister(ctrl_reg.reg);

产量

ldrb.w r3, [sp, #13]
bfi r3, r8, #1, #3   ;incorrectly writes to bits 1, 2, 3
strb.w r3, [sp, #13]

但是,当我使用中间变量时:

uint16_t reg_val = 0;
readRegister(&reg_val);

ControlReg ctrl_reg;
ctrl_reg.reg = reg_val;
ctrl_reg.opts_a = kOptsA_B;  // <-- line of interest

writeRegister(ctrl_reg.reg);

它产生了正确的指令:

bfi r7, r8, #9, #3   ;sets the proper bits 9, 10, 11

readRegister 函数没有做任何奇怪的事情,只是在指针处写入内存

void readRegister(uint16_t* out) {
   uint8_t data_in[3];
   ...
   *out = (data_in[0] << 8) | data_in[1];
}

为什么编译器不正确设置位域插入指令的起始位?

编译器生成的代码 100% 正确

void foo(ControlReg *reg)
{
    reg -> opts_a = kOptsA_B;
}

void foo1(ControlReg *reg)
{
    volatile ControlReg reg1;

    reg1.opts_a = kOptsA_B;
}

foo:
        movs    r2, #1
        ldrb    r3, [r0, #1]    @ zero_extendqisi2
        bfi     r3, r2, #1, #3
        strb    r3, [r0, #1]
        bx      lr
foo1:
        movs    r2, #1
        sub     sp, sp, #8
        ldrh    r3, [sp, #4]
        bfi     r3, r2, #9, #3
        strh    r3, [sp, #4]    @ movhi
        add     sp, sp, #8
        bx      lr

正如您在函数 'foo' 中看到的那样,它只加载一个字节(联合的第二个字节)并且该字段存储在该字节的 1 到 3 位中。

正如您在函数 'foo1' 中看到的那样,它加载半字(整个结构)并且字段存储在半字的 9 到 11 位中。

不要试图在编译器中查找错误,因为它们几乎总是在您的代码中。

PS

您不需要命名结构和填充位域

typedef union {
    struct {
        uint16_t : 1;
        uint16_t flag_a    : 1;
        uint16_t : 7;
        uint16_t opts_a    : 3;
        uint16_t flag_b    : 1;
        uint16_t : 3;
    };
    uint16_t reg;
}ControlReg ;

编辑

但是如果你想确保整个结构(联合)被修改,只需使函数参数 volatile

void foo(volatile ControlReg *reg)
{
    reg -> opts_a = kOptsA_B;
}

foo:
        movs    r2, #1
        ldrh    r3, [r0]
        bfi     r3, r2, #9, #3
        strh    r3, [r0]        @ movhi
        bx      lr

我不喜欢位域,尤其是当您的目标是可移植性时。 C 留下了很多关于它们的未指定或实现定义的东西,这比大多数人似乎意识到的要多,并且对于标准对它们的要求与某些实现的行为恰好相反,存在一些非常常见的误解。然而,如果您仅为特定应用程序编写代码,并且针对目标平台的单个特定 C 实现,那么这基本上没有实际意义。

在任何情况下,C 都不允许符合规范的实现与符合规范的代码行为不一致。在您的情况下,在函数 readRegister() 中通过指针设置 ctrl_reg.reg 与通过赋值设置它同样有效。这样做后,分配给 ctrl_reg.u.opts_a 是有效的,结果应该从 ctrl_reg.u 正确读回。也允许事后读取ctrl_reg.reg,会反映出修改的结果。

但是,您正在对标准不支持的位域布局做出假设。您的编译器将保持一致,但您需要仔细验证布局是否确实符合您的预期,否则在两个联合成员之间来回切换将不会产生您想要的结果。

然而,在 ctrl_reg.reg 中存储值的方式对于分配给位域的效果并不重要。您的编译器不需要为这两种情况生成相同的程序集,但是如果两个程序之间没有其他差异并且它们没有执行未定义的行为,那么它们 需要生成相同的程序集相同输入的可观察行为。