C宏设置多个位

C macro to set multiple bits

我正在使用 C 语言开发微控制器。其中一部分涉及更改寄存器中的位值。我想出了一些宏来简化事情:

#define BIT_SET(addr, shift) (addr | (1 << shift))
#define BIT_RESET(addr, shift) (addr & ~(1 << shift))
#define BIT_GET(addr, shift) (addr & (1 << shift))
...
reg1 = BIT_SET(reg1, 3); //set bit 3
...

但是,现在我想制作一个宏,一次可以更改几个位。例如,可能希望将 10101010 的前 3 位更改为 110,从而得到数字 11001010.

我可以在 C 中执行此操作吗?还是我处理方法不对?

根据我的经验,为多个位设置位掩码要容易得多。因此,各个位用特定定义标识,其名称指示位的功能,如

#define ENABLE_T1  0x0001
#define ENABLE_T2  0x0002

然后这些与按位运算符一起使用来打开和关闭位。

unsigned short usReg1 = GetRegister(REG1);

// disable T1 and enable T2

usReg1 &= ~ ENABLE_T1;
usReg1 |= ENABLE_T2;

// enable both T1 and T2
usReg1 |= (ENABLE_T1 | ENABLE_T2);

// disable both T1 and T2
usReg1 &= ~(ENABLE_T1 | ENABLE_T2);

或测试位

// is T1 turned on
if (usReg1 & ENABLE_T1) {  

// is either T1 or T2 turned on
if (usReg1 & (ENABLE_T1 | ENABLE_T2)) {  

// is T1 turned on and T2 turned off
if ((usReg1 & (ENABLE_T1 | ENABLE_T2)) == ENABLE_T1) {  

// is either T1 or T2 turned on but not both
if ((usReg1 & (ENABLE_T1 | ENABLE_T2)) && ((usReg1 & (ENABLE_T1 | ENABLE_T2)) != (ENABLE_T1 | ENABLE_T2))) {

关于宏的注释

顺便说一句,为了安全起见,您需要确保在宏定义中使用括号来尽可能隔离参数。如果不使用括号显式强制将宏参数作为单个表达式求值,您将受制于运算符优先级规则,而这些规则可能并不总是您想要的(请参阅 https://en.cppreference.com/w/c/language/operator_precedence )。所以

#define BIT_SET(addr, shift) (addr | (1 << shift))

真的应该写成

#define BIT_SET(addr, shift) ((addr) | (1 << (shift)))

对于您的示例,由于位移运算符在优先级图表中的位置相当低,因此在大多数情况下它可能没问题,但对于其他宏,取决于运算符的优先级可能会导致缺陷,因为您永远不知道某人有多保守将在使用宏时。

您可以引入一个全角掩码来定义您要设置的位。 例如。设置前 3 位使用 0xe0,二进制为 11100000。

连同该掩码,提供也要以全角写入的值。
例如。前三位所需的二进制 110 为 0xc0。

然后需要一点操作魔法来只写入所需的位位置。

#define MULTIBIT_SET(addr, mask, value) (((addr)&((0xff)^(mask)))|((value)&(mask)))

此处 ((0xff)^(mask)) 反转掩码,因此与此操作会删除值中的现有位。
((value)&(mask)) 是值,但只在所需位置设置了位,即这可以防止在其他地方设置不需要的位。

总的来说,将两个部分进行或运算,可以得到您想要的结果。

这里有一个设计选择,我想提一下:
称之为偏执狂。如果有人告诉我 "I want to set only the first three bits, to this value which has bits elsewhere",那么我更愿意以这种方式确保安全,即删除 "elsewhere bits".
是的,我假设无意中设置位比不设置它们风险更大,这是一个品味问题。

你总是可以用 C 来做。:-)

如果您使用的是 C99 兼容的编译器,并且鉴于现在是 2019 年,我假设您是,您可以使用可变参数宏来使其工作,更具体地说,P99.

我有一个示例,其中我正在提取 P99 的相关组件,但它变得混乱,因为这是将 C 预处理器用作图灵完备语言的一些严肃用途。

这也滥用了括号;特别是,EXTRACT_VALUE x 扩展为 EXTRACT_VALUE (a, b) 因为我们传递了一个元组列表。

#include "p99.h"

#define EXTRACT_SHIFT(shift, value) (shift)
#define EXTRACT_VALUE(shift, value) (value)
#define MERGE(NAME, I, A, B) (A), (B)

#define SET_OR_CLEAR(target, x, i) \
    ((EXTRACT_VALUE x) ? \
        ((target) |= (1 << EXTRACT_SHIFT x)) : \
        ((target) &= ~(1 << EXTRACT_SHIFT x)))
#define SET_AND_CLEAR(target, pairs...) \
    P99_FOR(target, P99_NARG(pairs), MERGE, SET_OR_CLEAR, pairs)

// ...
reg1 = BIT_SET(reg3, 3); //set bit 3                                         
// ...
SET_AND_CLEAR(reg3, (7, 1), (6, 1), (5, 0)); // set bits 6 and 7, clear bit 5

您可能需要使用 -std=c99 进行编译。请注意,我使用了可变参数词 pairs 而不是 __VA_ARGS__,以试图明确我们需要 2 元组作为参数。

请注意,这不会屏蔽任何未设置或清除的输入寄存器。