在C语言中如何设置/清除比特位?

4

如何写入单个位?我有一个变量,其值为1或0,我想将其值写入8位reg变量中的单个位。

我知道可以使用以下代码设置单个位:

reg |= mask; // mask is (1 << pin)

这会稍微清晰一些:

reg &= ~mask; // mask is (1 << pin)

有没有一行代码可以做到这一点,而不需要确定输入值是高还是低?

9
register 是 C 语言中的一个关键字,你不能将其用作变量名。 - Kerrek SB
2
我在问题中进行了更改:将“register”更改为“reg”。它需要同行批准。 - pablo1977
无法看到编辑队列,直接进行了更改。 - viraptor
6个回答

10

假设 value 的值为 01

REG = (REG & ~(1 << pin)) | (value << pin);

我使用REG代替register,因为如@KerrekSB在评论中指出的那样,register是一个C语言关键字。

这里的想法是我们计算一个具有指定位清除的REG值,然后根据value的不同设置该位。


1
在没有位移指令的体系结构上(如DEC alpha),我认为它需要循环,但是reg = a | b;本身并不意味着分支,这显然是不需要的。 - wildplasser
1
@wildpasser或MSP-430只能每次按位移动一个位。 - ouah
如果我没记错的话,第一台8088处理器还不能通过CX寄存器进行移位操作。(或者这只是6502处理器的限制?) - wildplasser
因为我正在使用MSP430,所以我会测试一下它是否比分支语句更昂贵。我没有意识到我只能一次移动一个位。不过我喜欢这个解决方案。"(1 << pin)"的值可以存储在数组中以加快速度。 - tylerjw
@tylerjw 如果pin是一个函数参数,那么这个假设就不成立了,因为参数并不是C常量。 - ouah
显示剩余6条评论

2

因为你将这个标签打上了 embedded,所以我认为最好的答案是:

if (set)
    reg |= mask; // mask is (1 << pin)
else
    reg &= ~mask; // mask is (1 << pin)

您可以将其包装在宏或内联函数中。原因是嵌入式架构(如AVR)具有位设置和位清除指令,并且与其他指令相比,分支的成本不高(例如,在具有推测执行的现代CPU上)。 GCC可以识别该if语句中的习惯用法并生成正确的指令。更复杂的版本(即使在现代x86上进行无分支测试)可能在嵌入式系统上不能组装到最佳指令。
确定最佳方式的最好方法是反汇编结果。您不必是专家(特别是在嵌入式环境中),就可以评估结果。

1
对于这些微控制器,您将需要使用正确的寄存器来执行重置和正确的寄存器来执行清除。类似于:if (set) reg_set = mask; else reg_clr = mask;。由于寄存器被定义为“volatile”对象,编译器没有权利自行执行此优化(即用reg_set / reg_clr替换reg)。 - ouah
Ouah 给出了很好的建议;实际上,你无法确定 reg |= mask 会导致位设置还是读取修改写入,这取决于 CPU 和编译器(我认为更可能会导致后者)。在这种情况下,始终反编译代码以查看实际发生的情况。 - Lundin

2

C语言中一个被忽视的特性是位域(bit packing),它非常适合嵌入式开发。你可以定义一个struct来单独访问每个位。

typedef struct
{
    unsigned char bit0 : 1;
    unsigned char bit1 : 1;
    unsigned char bit2 : 1;
    unsigned char bit3 : 1;
    unsigned char bit4 : 1;
    unsigned char bit5 : 1;
    unsigned char bit6 : 1;
    unsigned char bit7 : 1;
} T_BitArray;
< p > :1 告诉编译器您只希望每个变量的长度为1位。然后只需访问变量reg所在的地址,将其转换为位数组,然后逐个访问位。 < / p >
((T_BitArray *)&reg)->bit1 = value;

&reg是您变量的地址。 ((T_BitArray *)&reg) 是相同的地址,但现在编译器将其视为一个T_BitArray地址,((T_BitArray *)&reg)->bit1 提供访问第二位的方法。当然,最好使用比bit1更具描述性的名称。


+1 这是一个非常实用的抽象,我在嵌入式编译器供应商在外设驱动程序中使用过。 - Morten Jensen
1
这是100%不可移植和不安全的。在嵌入式系统中,使用位域始终是一个坏主意。在上面所称的位7并不一定是位7。它甚至可能没有分配到您希望的同一字节中。更多信息请参见此处 - Lundin
@Lundin 如果你禁用填充,这仍然是个坏主意吗? - MightyPork
大多数由@Lundin提出的问题是与实现相关的。我完全同意100%不可移植。问题在于:您的代码是否需要可移植性?我的许多工作都是为特定微控制器编写驱动程序,在那里我查找编译器如何打包位,并且通常提供足够的细节来保证我想要的结果。但仅适用于特定处理器! - DrRobotNinja
这是一种风险较高的访问寄存器位的方式。它可能适用于某个编译器版本和一组编译器标志,但很容易出现故障,导致难以发现的错误。 - bobc

1
//Through Macro we can do set resset Bit
#define set(a,n)   a|=(1<<n);
#define reset(a,n) a&=(0<<n);
//toggle bit value given by the user
#define toggle(a,n)  a^=(1<<n);

int a,n;
int main()
{
     printf("Set Reset particular Bit given by User ");
     scanf("%d %d",&a,&n);
     int b =set(a,n)   //same way we can call all the macro
     printf("%d",b);
     return 0;
}

你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

0

我认为你所问的是是否可以在不先读取所在字节的情况下执行单个位的写入指令。如果是这样,那么不,你不能这样做。这与C语言无关,只是微处理器没有寻址单个位的指令。即使在原始机器代码中,如果要设置一个位,您必须先读取它所在的字节,更改该位,然后将其写回。没有其他方法可以做到。


1
实际上,对于GPIO,许多微控制器都有特殊的寄存器,允许您在不需要先执行读取操作的情况下设置或清除位。 - ouah
是的,它被称为位带操作。 - Trevor Arjeski
很好。我猜测OP是在询问通用RAM。 - Lee Daniel Crocker
在ARM Cortex-M上,位带操作可能适用于片上RAM和外设寄存器。 - Clifford
不会给你负评,但是还有另外一种方式——许多微控制器都有端口的SET和CLEAR寄存器,我已经在答案中发布了。OP被标记为[embedded],所以我认为这是适用的。 - John U
我正在将此用于msp430目标上的GPIO端口。 - tylerjw

0

这是如何设置、清除和切换单个位的副本,我也会重新发布我的答案,因为还没有人提到SET和CLEAR寄存器:

由于这个标签是“嵌入式”,我假设您正在使用微控制器。上述所有建议都是有效的并且可行的(读取-修改-写入、联合、结构等)。
然而,在使用示波器进行调试时,我惊奇地发现,与直接将值写入微控制器的PORTnSET / PORTnCLEAR寄存器相比,这些方法在CPU周期方面有相当大的开销,这在有紧密循环/高频ISR切换引脚的情况下会产生真正的差异。
对于那些不熟悉的人:在我的例子中,微控制器具有反映输出引脚的通用引脚状态寄存器PORTn,因此执行PORTn |= BIT_TO_SET会导致对该寄存器进行读取-修改-写入操作。
然而,PORTnSET / PORTnCLEAR寄存器将“1”解释为“请将此位设置为1”(SET),将“0”解释为“请保持引脚不变”。因此,您最终会得到两个端口地址,具体取决于您是要设置还是清除位(并不总是方便),但反应更快,汇编代码更小。

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