我正在为Cortex-M0 CPU和gcc编写代码。我有以下结构:
struct {
volatile unsigned flag1: 1;
unsigned flag2: 1;
unsigned foo; // something else accessed in main loop
} flags;
从GPIO中断处理程序和主循环读取和写入 flag1
。 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
,设置一下并将其存回。
答案 0 :(得分:9)
我对C11标准的解读是,使用位域是不合适的 - 即使它们都被声明为volatile
。以下摘录来自3.14 Memory location:
- 内存位置
标量类型的对象,或者具有非零宽度的相邻位域的最大序列注意1 两个执行线程可以更新和访问单独的内存位置,而不会相互干扰。
- 醇>
注意2 同时更新同一结构中的两个非原子位字段是不安全的 在它们之间声明的成员也是(非零长度)位域,无论它们的大小如何 介入的位域恰好是。
volatile
没有例外。因此,如果两个执行线程(即主要和ISR)如果ISR将更新一个标志而主要将更新另一个标志,则使用上述位域是不安全的。给出的解决方案是在其间添加大小为0的成员以强制它们放置在不同的存储位置。但话说再说一遍,这意味着两个标志都会占用至少一个字节的内存,因此对它们使用非位字段unsigned char
或bool
再简单一些:
struct {
volatile bool flag1;
bool flag2;
unsigned foo; // something else accessed in main loop
} flags;
现在,它们将被放置在不同的存储位置,并且可以在不相互干扰的情况下进行更新。
然而,volatile
flag1
仍然是绝对必要的,因为对flag1
的更新将在主线程和编译器中无副作用可以推断它只能将该字段保存在寄存器中 - 或者根本不需要更新任何内容。
但是,需要注意的是,在 C11 下,即使volatile
的保证可能还不够:5.1.2.3p5:
当通过接收信号中断抽象机器的处理时,未指定既不是无锁原子对象也不是类型volatile sig_atomic_t的对象的值,浮点环境的状态也是如此。处理程序修改的既不是无锁原子对象也不是volatile sig_atomic_t类型的任何对象的值在处理程序退出时变得不确定,如果由处理程序修改并且未恢复,则浮点环境的状态也是如此到原来的状态。
因此,如果需要完全兼容性,flag1
应该是类型volatile _Atomic bool
;甚至可以使用_Atomic
位域。但是,这两个都需要C11编译器。
然后,您可以检查编译器的手册,如果它们保证对这些易失性对象的访问也保证是原子的。
答案 1 :(得分:4)
只有一位的易失性标志并不是那么有意义 - 甚至可能有害。编译器在实践中可能会做的是分配两个内存块,可能每个32位宽。因为volatile标志阻止它组合同一个分配区域内的两个位,因为没有可用的位级访问指令。
当在主循环中访问flag2时,编译器是否会优化对它的访问,以便每次在代码中读取或写入时都不会将其提取或存储到内存中?
这很难说,取决于有多少数据寄存器可用。反汇编代码,看看。
总的来说,不建议使用位域,因为它们的标准定义很差。在这种情况下,单独的volatile位可能会导致分配额外的内存。
相反,你应该这样做:
volatile bool flag1;
bool flag2;
假设这些标志不是硬件寄存器的一部分,在这种情况下,代码从一开始就是不正确的,它们都应该是易失性的。