比特操纵良好做法

Kat*_*eva 34 c standards bit

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

例如,我有一个控制块映射: 地图

我看到的第一种方法是简单地设置所需的位.它在评论中需要一堆解释,似乎并不是那么专业.

DMA_base_ptr[DMA_CONTROL_OFFS] = 0b10001100;
Run Code Online (Sandbox Code Playgroud)

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

struct DMA_control_block_struct
{ 
    unsigned int BYTE:1; 
    unsigned int HW:1; 
    // etc
} DMA_control_block_struct;
Run Code Online (Sandbox Code Playgroud)

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

任何建议都将受到高度赞赏

dbu*_*ush 40

位字段的问题在于C标准没有规定它们的定义顺序与它们的实现顺序相同.所以你可能没有设置你认为你的位.

C标准第6.7.2.1.111节规定:

实现可以分配足够大的任何可寻址存储单元来保持位字段.如果剩余足够的空间,则紧跟在结构中的另一个位字段之后的位字段将被打包到相同单元的相邻位中. 如果剩余的空间不足,则是否将不适合的位域放入下一个单元或重叠相邻单元是实现定义的.单元内的位域分配顺序(高阶到低阶或低阶到高阶)是实现定义的. 未指定可寻址存储单元的对齐.

例如,struct iphdr从Linux上的/usr/include/netinet/ip.h文件文件中查看代表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;
    ...
Run Code Online (Sandbox Code Playgroud)

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

所以不要使用位域.

最好的方法是设置所需的位.但是,为每个位定义命名常量并对要设置的常量执行按位OR是有意义的.例如:

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;
Run Code Online (Sandbox Code Playgroud)

  • *这个文件是可以接受的,因为它是系统的一部分.*它也是"可接受的",因为Linux几乎*事实上*需要GCC来编译.即使字节序保持不变,另一个*编译器*也可以自由更改位字段的分配方式. (4认同)
  • 类Unix系统上的C编译器不仅要符合C标准,还要符合平台的ABI,这样它们就可以与平台的库互操作. (4认同)
  • 为什么不使用`enum`而不是定义潜在的ODR问题的常量变量? (2认同)

Ark*_*kku 19

其他答案已经涵盖了大部分内容,但值得一提的是,即使您不能使用非标准0b语法,也可以使用shift来1按位数将位移动到位,即:

#define DMA_BYTE  (1U << 0)
#define DMA_HW    (1U << 1)
#define DMA_WORD  (1U << 2)
#define DMA_GO    (1U << 3)
// …
Run Code Online (Sandbox Code Playgroud)

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

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

#define DMA_CONTROL_REG DMA_base_ptr[DMA_CONTROL_OFFS]

DMA_CONTROL_REG |= DMA_HW | DMA_WORD;    // set HW and WORD
DMA_CONTROL_REG &= ~(DMA_BYTE | DMA_GO); // clear BYTE and GO
Run Code Online (Sandbox Code Playgroud)

  • 对于初学者:宏中的括号如``#define DMA_BYTE(1U << 0)```非常重要 - 参见[this question](/sf/ask/503055311/ - 和 - 使用 - 的论点功能于括号中). (3认同)
  • @kasperd我认为关键是非初学者已经被这个咬了,因此学会了将括号放在他们的宏中.=) (2认同)

Ste*_*mit 18

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

#define WORD  0x04
#define GO    0x08
#define I_EN  0x10
#define LEEN  0x80
Run Code Online (Sandbox Code Playgroud)

然后你的初始化成为

DMA_base_ptr[DMA_CONTROL_OFFS] = WORD | GO | LEEN;
Run Code Online (Sandbox Code Playgroud)

您可以使用|以下方法设置各个位

DMA_base_ptr[DMA_CONTROL_OFFS] |= I_EN;
Run Code Online (Sandbox Code Playgroud)

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

DMA_base_ptr[DMA_CONTROL_OFFS] &= ~GO;
Run Code Online (Sandbox Code Playgroud)

您可以使用&以下方法测试各个位

if(DMA_base_ptr[DMA_CONTROL_OFFS] & WORD) ...
Run Code Online (Sandbox Code Playgroud)

但绝对不要使用位域.它们有它们的用途,但是当外部规范定义这些位在某些地方时,它们就没有了,正如我在这里假设的那样.

另请参阅C FAQ列表中的问题20.72.26.

  • 例如,我认为使用位字段匹配特定嵌入式平台上的硬件寄存器没有根本问题,因为在任何情况下代码往往都是固有的非可移植的(与特定设备相关联,通常是单个编译器).特别是多位字段的可读性和便利性的增益值得.(当然可能还有其他问题,例如代码大小或性能,需要检查,但我的观点是我不会自动忽略用于此用途的位字段.) (4认同)

小智 8

位字段没有标准.在这种情况下,映射和位操作依赖于编译器.诸如二进制值0b0000也不是标准化的.通常的做法是为每个位定义十六进制值.例如:

#define BYTE (0x01)
#define HW   (0x02)
/*etc*/
Run Code Online (Sandbox Code Playgroud)

如果要设置位,可以使用:

DMA_base_ptr[DMA_CONTROL_OFFS] |= HW;
Run Code Online (Sandbox Code Playgroud)

或者你可以清除位:

DMA_base_ptr[DMA_CONTROL_OFFS] &= ~HW;
Run Code Online (Sandbox Code Playgroud)


Rei*_*ica 5

现代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); }
Run Code Online (Sandbox Code Playgroud)

此类代码应由机器生成:我使用gsl(成帧数为0)以模板和一些XML输入(基于寄存器详细信息)为基础生成代码。

  • 也许我很奇怪,但是如果我正在处理诸如DMA控制之类的低级内容,我宁愿自己亲自查看这些位,也不愿将它们包装到“ bool”中,而实际上我无法读取或设置的内容比一次一个。(如果要提供真正的更高级别的API,那么(导出的)函数也应该比`set_BYTE`更高的级别。)至少在名称上。) (2认同)