C编译器对于位域的处理是什么?

8
我正在从事一个嵌入式项目(PowerPC目标,Freescale Metrowerks Codewarrior编译器),其中寄存器是内存映射的,并以漂亮的位域定义,以便轻松地调整各个位标志。目前,我们正在使用此功能来清除中断标志和控制数据传输。虽然我还没有注意到任何错误,但我想知道这是否安全。是否有一种安全使用位域的方法,或者我需要将每个包装在DISABLE_INTERRUPTS ... ENABLE_INTERRUPTS中?
澄清一下:微控制器提供的头文件具有类似于以下结构的字段:
union {
        vuint16_t R;
        struct {
            vuint16_t MTM:1;        /* message buffer transmission mode */
            vuint16_t CHNLA:1;      /* channel assignement */
            vuint16_t CHNLB:1;      /* channel assignement */
            vuint16_t CCFE:1;       /* cycle counter filter enable */
            vuint16_t CCFMSK:6;     /* cycle counter filter mask */
            vuint16_t CCFVAL:6;     /* cycle counter filter value */
        } B;
    } MBCCFR;

我认为设置位域中的一个位不是原子操作,这个假设正确吗?编译器实际上为位域生成了什么样的代码?使用 R(原始)字段自己执行掩码可能会更容易记住该操作不是原子的(很容易忘记类似于CAN_A.IMASK1.B.BUF00M = 1这样的赋值操作不是原子操作)。
感谢您的建议。

1
我想问一下,vuint16_t 中的 v 是否代表 "volatile"? - JAB
是的,这些类型都是易变的,因为它们都是内存映射寄存器位置。还有对齐指令我没有提到,为了简洁起见。 - Adam Shiemke
避免使用位域(bitfields),而是使用掩码和移位(mask and shift)操作。 - starblue
6个回答

3

原子性取决于目标平台和编译器。例如,AVR-GCC会尝试检测位访问并在可能的情况下发出位设置或清除指令。请检查汇编输出以确保...

编辑:这里有一个关于PowerPC原子指令的资源,直接来自官方:

http://www.ibm.com/developerworks/library/pa-atom/


当它可以时,但并非所有地址都支持此操作,并且除非在编译时已知地址,否则无法执行此操作。 - nategoose
@nategoose 是的,最好像你所做的那样拼写出来,而不是像我一样暗示。 - Peter G.

3
假设设置位字段不是原子操作是正确的。C标准在位字段的实现上并不特别清晰,各种编译器也有不同的处理方式。
如果您只关心目标架构和编译器,可以反汇编一些对象代码。
通常情况下,您的代码将实现所需的结果,但效率远低于使用宏和移位的代码。话虽如此,如果您不关心性能,使用位字段可能更易读。
如果您担心未来的程序员(包括您自己)会感到困惑,可以为位编写一个原子性的设置器包装函数。

3

是的,你的假设是正确的,你不能假设原子性。在特定平台上,你可能会得到额外的保证,但无论如何都不能依赖它。

基本上,编译器会为你执行掩码和其他操作。他可能能够利用边角情况或特殊指令。如果你对效率感兴趣,可以查看编译器生成的汇编代码,通常很有启示性。作为经验法则,我认为现代编译器生成的代码与中等编程工作的效率相当。针对你的特定编译器进行深入的位操作可能会获得一些周期。


3
我认为使用位域来模拟硬件寄存器并不是一个好主意。
编译器处理位域的方式在很大程度上是实现定义的(包括如何处理跨越字节或字边界的字段、大小端问题以及确切地如何实现获取、设置和清除位)。请参阅C/C++: Force Bit Field Order and Alignment
为了验证寄存器访问是否按照您期望或需要的方式进行处理,您需要仔细研究编译器文档和/或查看生成的代码。我想,如果微处理器工具集提供的头文件使用它们,您可以假设大部分我的担忧都得到了解决。但是,我猜原子访问并不一定...
我认为最好使用执行显式读/修改/写操作并使用所需的位掩码处理硬件寄存器的这种位级访问操作的函数(或宏,如果必须)来处理这些类型的位级访问操作。
这些功能可以针对支持原子位级访问的架构进行修改(例如ARM Cortex M3的“位带定址”)。我不知道PowerPC是否支持类似于此的任何东西 - M3是我处理的唯一支持通用方式的处理器。即使是M3的位带也仅支持1位访问;如果您处理的字段宽度为6位,您必须回到读取/修改/写入场景。

0

我相信在PowerPC上这不是原子操作,但如果你的目标是单核系统,那么你可以这样做:

void update_reg_from_isr(unsigned * reg_addr, unsigned set, unsigned clear, unsigned toggle) {
   unsigned reg = *reg_addr;
   reg |= set;
   reg &= ~clear;
   reg ^= toggle;
   *reg_addr = reg;
}

void update_reg(unsigned * reg_addr, unsigned set, unsigned clear, unsigned toggle) {
   interrupts_block();
   update_reg_from_isr(reg_addr, set, clear, toggle);
   interrupts_enable();
}

我不记得powerpc的中断处理程序是否可中断,但如果可以的话,你应该始终使用第二个版本。

如果你的目标是多处理器系统,那么你应该创建锁(自旋锁),以保护对硬件寄存器等内容的访问,并在访问寄存器之前获取所需的锁,然后在更新寄存器(或寄存器)后立即释放锁。

我曾经读过如何在powerpc上实现锁定的方法--它涉及告诉处理器监视内存总线上的某个地址,同时你进行一些操作,然后在这些操作结束时回来检查监视地址是否已被另一个核心写入。如果没有,那么你的操作就成功了;如果有,则必须重新执行操作。这是为编译器、库和操作系统开发人员编写的文档。我不记得我在哪里找到它(可能是IBM.com上的某个地方),但稍微搜索一下应该就能找到它。它可能还包含有关如何进行原子位操作的信息。


哇,听起来像死锁的配方:处理器A开始临界区,查看地址。 处理器B开始临界区,查看地址。 处理器A完成写入。 处理器B完成写入。 处理器A查看地址。它已经改变了! 处理器A重新写入数据。 处理器B查看地址。它已经改变了! 处理器B重新写入数据。 无限循环继续进行。 - nmichaels
@Nathon:这比那复杂一些,也简单一些,但我已经有一段时间没读文档了。确实有东西防止了死锁情况的发生,同时比其他架构使用单个原子指令方法更灵活。如果另一个处理器在该处理器开始观察该地址后写入该地址,则可能导致最终对RAM的写入未发生。 - nategoose
1
在某些架构中,它被称为“加载链接/存储条件”。PPC将其称为lwarx(加载带索引的字)和stwcx.(存储带条件的索引字(并记录))。存储是有条件的,即没有其他人写入内存;一个处理器总是会获胜。此外,“保留”在上下文切换时被清除。无论哪种方式,您都不希望在硬件位字段上执行此操作... - tc.
@tc.: 你不直接在内存映射的硬件寄存器上执行操作的原因是因为它可能会在内存总线上发出_store_address_之前发生更改(其中_address_是保留地址{位域的地址})。这已经很久了。我记不清我以前写这个时是否考虑过这样做或者在其周围放置锁,但两者都不能防止硬件寄存器在总线外被更改(真正易失性)。 - nategoose
我没有做过任何硬件编程,但我的经验是孤立地使用原子访问很少有用。你可能需要更像“将地址放入寄存器A,将长度放入寄存器B,在寄存器C中调整位来启动DMA I/O”的东西。另一件事是,许多硬件寄存器只有单个字节,而PPC的load/store保留指令作用于整个字(或双字);问题中的位域仅为半字。 - tc.

0

这完全取决于架构和编译器是否对位域操作进行了原子化处理。我的个人经验告诉我:如果没有必要,就不要使用位域。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接