位操作的良好做法

时间:2018-11-02 12:45:08

标签: c standards bit

作为一名初学者C程序员,我想知道,什么是在设备中设置控制位的最佳易读和易懂的解决方案。有什么标准吗?有任何示例代码可以模仿吗? Google没有给出可靠的答案。

例如,我有一个控制块图: map

我看到的第一种方法是简单地设置所需的位。它需要在注释中进行一堆解释,而且似乎还不够专业。

DMA_base_ptr[DMA_CONTROL_OFFS] = 0b10001100;

我看到的第二种方法是创建一个位字段。我不确定这是否应该坚持,因为我从未遇到过以这种方式使用它(不同于我提到的第一种选择)。

struct DMA_control_block_struct
{ 
    unsigned int BYTE:1; 
    unsigned int HW:1; 
    // etc
} DMA_control_block_struct;

其中一个选项是否比另一个更好?有没有我看不到的选择?

任何建议将不胜感激

7 个答案:

答案 0 :(得分:40)

位字段的问题是C标准没有规定定义它们的顺序与实现它们的顺序相同。因此,您可能没有设置自己认为的位。

C standard的6.7.2.1p11节规定:

  

一个实现可以分配任何大的可寻址存储单元   足以容纳一个位域。如果还有足够的空间,则位域   紧跟在结构中另一个位域之后的是   打包到同一单元的相邻位中。 如果空间不足   仍然存在,是否放入不适合的位域   下一个单位或与相邻单位重叠的是   实现定义的。内位域的分配顺序   一个单位(高阶到低阶或低阶到高阶)是   实施定义。可寻址存储的对齐方式   未指定单位。

作为示例,请查看Linux上/usr/include/netinet/ip.h文件文件中struct iphdr的定义,该定义代表IP头:

struct iphdr
  {
#if __BYTE_ORDER == __LITTLE_ENDIAN
    unsigned int ihl:4;
    unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
    unsigned int version:4;
    unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif
    u_int8_t tos;
    ...

您可以在此处看到根据实现的不同,位域的放置顺序也不同。您也不应使用此特定检查,因为此行为取决于系统。该文件是系统的一部分,因此可以接受。其他系统可能以不同的方式实现这一目标。

所以不要使用位域。

执行此操作的最佳方法是设置所需的位。但是,为每个位定义命名常量并对您要设置的常量执行按位“或”运算将很有意义。例如:

const uint8_t BIT_BYTE =     0x1;
const uint8_t BIT_HW   =     0x2;
const uint8_t BIT_WORD =     0x4;
const uint8_t BIT_GO   =     0x8;
const uint8_t BIT_I_EN =     0x10;
const uint8_t BIT_REEN =     0x20;
const uint8_t BIT_WEEN =     0x40;
const uint8_t BIT_LEEN =     0x80;

DMA_base_ptr[DMA_CONTROL_OFFS] = BIT_LEEN | BIT_GO | BIT_WORD;

答案 1 :(得分:19)

其他答案已经涵盖了大多数内容,但是值得一提的是,即使您不能使用非标准的0b语法,也可以使用移位键来移动{{1} }按位数进行定位,即:

1

请注意最后一个数字如何与文档中的“位数”列匹配。

设置和清除位的用法不变:

#define DMA_BYTE  (1U << 0)
#define DMA_HW    (1U << 1)
#define DMA_WORD  (1U << 2)
#define DMA_GO    (1U << 3)
// …

答案 2 :(得分:18)

老式的C方法是定义一堆位:

#define WORD  0x04
#define GO    0x08
#define I_EN  0x10
#define LEEN  0x80

然后您的初始化将变为

DMA_base_ptr[DMA_CONTROL_OFFS] = WORD | GO | LEEN;

您可以使用|设置各个位:

DMA_base_ptr[DMA_CONTROL_OFFS] |= I_EN;

您可以使用&~清除各个位:

DMA_base_ptr[DMA_CONTROL_OFFS] &= ~GO;

您可以使用&测试单个位:

if(DMA_base_ptr[DMA_CONTROL_OFFS] & WORD) ...

但是绝对不要使用位域。它们有其用途,但是当外部规范定义这些位在某些地方时(如我所想的那样),则没有它们。

另请参阅20.7中的问题2.26C FAQ list

答案 3 :(得分:7)

位域没有标准。在这种情况下,映射和位操作取决于编译器。诸如record.save()之类的二进制值也未标准化。通常的方法是为每个位定义十六进制值。例如:

0b0000

要设置位时,可以使用:

#define BYTE (0x01)
#define HW   (0x02)
/*etc*/

或者您可以通过以下方式清除位:

DMA_base_ptr[DMA_CONTROL_OFFS] |= HW;

答案 4 :(得分:4)

现代C编译器可以很好地处理琐碎的内联函数-无需任何开销。我将使用所有抽象功能,以便用户无需操纵任何位或整数,并且不太可能滥用实现细节。

您当然可以使用常量而不是函数来获取实现细节,但是API应该是函数。如果您使用的是古老的编译器,这也允许使用宏而不是函数。

例如:

#include <stdbool.h>
#include <stdint.h>

typedef union DmaBase {
  volatile uint8_t u8[32];
} DmaBase;
static inline DmaBase *const dma1__base(void) { return (void*)0x12340000; }

// instead of DMA_CONTROL_OFFS
static inline volatile uint8_t *dma_CONTROL(DmaBase *base) { return &(base->u8[12]); }
// instead of constants etc
static inline uint8_t dma__BYTE(void) { return 0x01; }

inline bool dma_BYTE(DmaBase *base) { return *dma_CONTROL(base) & dma__BYTE(); }
inline void dma_set_BYTE(DmaBase *base, bool val) {
  if (val) *dma_CONTROL(base) |= dma__BYTE();
  else *dma_CONTROL(base) &= ~dma__BYTE();
}
inline bool dma1_BYTE(void) { return dma_BYTE(dma1__base()); }
inline void dma1_set_BYTE(bool val) { dma_set_BYTE(dma1__base(), val); }

此类代码应由机器生成:我使用gsl(具有0mq的名声)基于模板和一些列出寄存器详细信息的XML输入来生成代码。

答案 5 :(得分:-1)

尽管这里所有的恐惧贩子都在说什么,但您可以使用位域。您只需要知道您希望代码与之配合使用的编译器和系统ABI如何定义位域的“实现定义”方面即可。 pedant不必害怕,以粗体显示“实现定义”之类的字眼。

但是,到目前为止,其他人似乎仍然错过了内存映射的硬件设备的行为方式,这些方面在处理诸如C之类的高级语言以及此类语言提供的优化功能时可能会违反直觉。 。例如,硬件寄存器的每次读取或写入有时都可能会有副作用,即使写入时的位没有改变。同时,优化器可能难以区分所生成的代码何时实际在读取或写入寄存器的地址,并且即使描述寄存器的C对象被仔细地限定为volatile,也需要非常小心地控制当发生I / O时。

也许您将需要使用由编译器和系统定义的某些特定技术,以便正确处理内存映射的硬件设备。许多嵌入式系统就是这种情况。在某些情况下,编译器和系统供应商确实会使用位域,就像在某些情况下Linux一样。我建议您先阅读您的编译器手册。

您引用的位描述表似乎是针对Intel Avalon DMA控制器内核的控制寄存器的。 “读/写/清除”列提供了有关特定位在读取或写入时的行为的提示。该设备的状态寄存器有一个位示例,其中写入零将清除一个位值,但它可能不会回读与写入的值相同的值,即,写入寄存器可能会在设备中产生副作用,取决于DONE位的值。有趣的是,他们将SOFTWARERESET位记录为“ RW”,但随后将过程描述为向其写入1两次以触发重置,然后他们还警告在DMA传输处于活动状态时执行DMA软件重置可能会导致:永久总线锁定(直到下一次系统重置)。因此,除非万不得已,否则不应该写入SOFTWARERESET位。无论您如何描述寄存器,在C中管理复位都将需要一些仔细的编码。

关于标准,ISO / IEC很好地制作了一份称为“ ” ISO / IEC TR 18037”的“技术报告”,副标题为“ 支持嵌入式处理器的扩展” em>。它讨论了与使用C管理硬件寻址和设备I / O相关的许多问题,特别是针对您在问题中提到的位映射寄存器,它记录了许多宏和可通过包含文件获得的技术。致电<iohw.h>。如果您的编译器提供了这样的头文件,则您也许可以使用这些宏。

有TR 18037的草稿副本,最新的是TR 18037(2007),尽管它提供了比较干燥的阅读效果。但是,它确实包含<iohw.h>的示例实现。

QNX也许是真实<iohw.h>实现的一个很好的例子。 QNX文档提供了不错的概述(和一个示例,尽管我强烈建议对整数值使用enum而不是宏):QNX <iohw.h>

答案 6 :(得分:-3)

在声明变量以存储其值时,应确保将这些位初始化为已知的默认值。在C中,声明变量时,您只是在一个地址处保留一块内存,而该内存块的大小取决于其类型。如果不初始化变量,则可能会遇到未定义/意外的行为,因为该变量的值将受到声明该块之前该块中内存的值/状态的影响。通过将变量初始化为默认值,可以清除该内存块的现有状态并将其置于已知状态。

就可读性而言,应使用bit field存储该位的值。位字段使您可以将位的值存储在结构中。由于可以使用点表示法,因此这使组织起来更容易。另外,您应确保注释掉位字段的声明,以解释不同字段用作最佳实践的含义。我希望这回答了你的问题。 C编程祝您好运!