位操作良好实践

Bit manipulations good practices

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

比如我有一个控制块图:

我看到的第一种方法是简单地设置所需的位。评论里要一堆解释,好像不是很专业

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;

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

任何建议将不胜感激

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

C standard 的第 6.7.2.1p11 节指出:

An implementation may allocate any addressable storage unit large enough to hold a bit- field. If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit. If insufficient space remains, whether a bit-field that does not fit is put into the next unit or overlaps adjacent units is implementation-defined. The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined. The alignment of the addressable storage unit is unspecified.

举个例子,看看 Linux 上的 /usr/include/netinet/ip.h 文件中 struct iphdr 的定义,它代表一个 IP header:

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;

老派的 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 and 2.26 in the C FAQ list

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

就可读性而言,您应该使用bit field 来存储位的值。位字段使您能够将位的值存储在结构中。这使得组织起来更容易,因为您可以使用点符号。此外,您应该确保对位字段的声明进行注释,以解释不同字段的用途,作为最佳实践。我希望这回答了你的问题。祝你 C 编程顺利!

位域没有标准。在这种情况下,映射和位操作取决于编译器。 0b0000 等二进制值也未标准化。通常的做法是为每一位定义十​​六进制值。例如:

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

当你想设置位时,你可以使用:

DMA_base_ptr[DMA_CONTROL_OFFS] |= HW;

或者您可以使用以下方法清除位:

DMA_base_ptr[DMA_CONTROL_OFFS] &= ~HW;

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

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

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

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

#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

现代 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 输入列出寄存器的详细信息。

你可以使用 bit-fields,尽管这里的所有 fear-mongers 都在说什么。您只需要知道您希望代码使用的编译器和系统 ABI 如何定义 bit-fields 的 "implementation defined" 方面。不要被书呆子把 "implementation defined" 加粗的字吓跑。

然而,到目前为止其他人似乎遗漏了 memory-mapped 硬件设备在处理 higher-level 语言时可能表现的各个方面 counter-intuitive C 和此类语言提供的优化功能。例如,即使在写入时未更改位,有时每次读取或写入硬件寄存器也可能有 side-effects。同时,优化器可能很难判断生成的代码何时实际读取或写入寄存器地址,甚至当描述寄存器的 C object 仔细限定为 volatile 时,非常小心需要控制什么时候出现I/O。

也许您需要使用编译器和系统定义的某些特定技术才能正确操作 memory-mapped 硬件设备。许多嵌入式系统就是这种情况。在某些情况下,编译器和系统供应商确实会使用 bit-fields,就像 Linux 在某些情况下所做的那样。我建议先阅读您的编译器手册。

您引用的位描述 table 似乎是针对 Intel Avalon DMA 控制器内核的控制寄存器。 "read/write/clear" 列给出了有关特定位在读取或写入时的行为方式的提示。该设备的状态寄存器有一个位示例,其中写入零将清除位值,但它可能不会读回与写入相同的值——即写入寄存器可能有一个 side-effect设备,取决于 DONE 位的值。有趣的是,他们将 SOFTWARERESET 位记录为 "RW",但随后将过程描述为两次向其写入 1 以触发重置,然后他们还警告 Executing a DMA software reset when a DMA transfer is激活可能导致永久总线锁定(直到下一次系统复位)。因此,除非万不得已,否则不应写入 SOFTWARERESET 位。 无论您如何描述寄存器,在 C 中管理复位都需要仔细编码。

至于标准,ISO/IEC 制定了一个 "technical report",称为 "ISO/IEC TR 18037",副标题为 "Extensions to support embedded processors"。它讨论了一些与使用 C 管理硬件​​寻址和设备 I/O 相关的问题,特别是针对你在问题中提到的 bit-mapped 寄存器的种类,它记录了一些宏和技术,可通过他们称为 <iohw.h> 的包含文件。如果您的编译器提供了这样的 header 文件,那么您也许可以使用这些宏。

有 TR 18037 的草稿副本可用,最新的是 TR 18037(2007),尽管它提供了相当枯燥的阅读。但是它确实包含 <iohw.h>.

的示例实现

也许 real-world <iohw.h> 实施的一个很好的例子是在 QNX 中。 QNX 文档提供了一个不错的概述(还有一个例子,尽管我强烈建议使用 enums 来表示整数值,而不是宏):QNX <iohw.h>