易失性和非易失性位域

volatile and non-volatile bitfields

我正在为 Cortex-M0 CPU 和 gcc 编写代码。我有以下结构:

struct {
    volatile unsigned flag1: 1;
    unsigned flag2: 1;

    unsigned foo; // something else accessed in main loop
} flags;

flag1 从 GPIO 中断处理程序和主循环中读取和写入。 flag2只在主循环中读写。

ISR 看起来像这样:

void handleIRQ(void) {
    if (!flags.flag1) {
        flags.flag1 = 1;
        // enable some hw timer
    }
}

主循环如下所示:

for (;;) {
    // disable IRQ
    if (flags.flag1) {
        // handle IRQ
        flags.flag1 = 0;
        // access (rw) flag2 many times
    }
    // wait for interrupt, enable IRQ
}

在主循环中访问 flag2 时,编译器是否会优化对它的访问,这样它就不会在每次用代码读取或写入时都被提取或存储到内存中?

我不清楚,因为要在 ISR 中设置 flag1,它需要加载整个 char,设置一点并将其存储回去。

只有一位的易失性标志意义不大——它甚至可能有害。编译器在实践中可能做的是分配两块内存,可能每块 32 位宽。因为易失性标志阻止它在同一分配区域内组合两个位,因为没有可用的位级访问指令。

When accessing flag2 in main loop, will the compilier optimize access to it so it won't be fetched or stored to memory every time it is read or written to in code?

这很难说,取决于有多少数据寄存器可用。反汇编代码看看。

总的来说,不推荐使用位域,因为它们在标准中的定义很差。在这种情况下,单个易失性位可能会导致分配额外的内存。

相反,您应该这样做:

volatile bool flag1;
bool flag2;

假设这些标志不是硬件寄存器的一部分,在这种情况下,代码从一开始就是不正确的,它们应该都是可变的。

根据我对 C11 标准的解读,为此使用位域是不合适的——即使它们都被声明为 volatile。以下摘自3.14 Memory location:

  1. Memory location
    Either an object of scalar type, or a maximal sequence of adjacent bit-fields all having nonzero width
  2. NOTE 1 Two threads of execution can update and access separate memory locations without interfering with each other.

  3. NOTE 2 It is not safe to concurrently update two non-atomic bit-fields in the same structure if all members declared between them are also (non-zero-length) bit-fields, no matter what the sizes of those intervening bit-fields happen to be.

volatile 没有例外。因此,如果两个执行线程(即主线程和 ISR)如果 ISR 将更新一个标志而主线程将更新另一个标志,则使用上述位域是不安全的。给出的解决方案是在两者之间添加一个大小为0的成员,以强制将它们放在不同的内存位置。但是话又说回来,这意味着两个标志都会消耗至少一个字节的内存,因此再次为它们使用非位字段 unsigned charbool 更简单:

struct {
    volatile bool flag1;
    bool flag2;

    unsigned foo; // something else accessed in main loop
} flags;

现在它们将被放置在不同的内存位置,并且可以在不相互干扰的情况下进行更新。


然而 flag1volatile 仍然是绝对必要的,因为否则 flag1 的更新将 无副作用 线程,并且编译器可以推断它只能将该字段保存在寄存器中 - 或者根本不需要更新任何内容。

但是,需要注意的是,在 C11 下,即使 volatile 的保证也可能不够:5.1.2.3p5:

When the processing of the abstract machine is interrupted by receipt of a signal, the values of objects that are neither lock-free atomic objects nor of type volatile sig_atomic_t are unspecified, as is the state of the floating-point environment. The value of any object modified by the handler that is neither a lock-free atomic object nor of type volatile sig_atomic_t becomes indeterminate when the handler exits, as does the state of the floating-point environment if it is modified by the handler and not restored to its original state.

因此,如果需要完全兼容,flag1 应该是 volatile _Atomic bool 类型的例子;甚至可以使用 _Atomic 位域。但是,这两者都需要 C11 编译器。

再一次,您可以查看编译器的手册,看它们是否保证对此类易失性对象的访问也保证是原子的。