位操作怎么写才不出错?置位、清位、翻转、取位一次讲清
一句话 : 寄存器位操作只有四个基本动作------置位用
|=、清位用&= ~、翻转用^=、取位用"先移位再与 1"。本文给出可直接复制的宏模板、一张对照表,以及四个最容易写错的组合。
适合谁读 :刚开始配置寄存器、见到|=、&= ~、^=分不清该用哪个的嵌入式新手。
先做这一步:把四个宏放进工程
下面 5 行宏覆盖了位操作 90% 的场景。先复制进工程,或核对你们项目里的写法是否一致:
c
#define BIT(n) (1UL << (n))
#define BIT_SET(reg, n) ((reg) |= BIT(n))
#define BIT_CLR(reg, n) ((reg) &= ~BIT(n))
#define BIT_TGL(reg, n) ((reg) ^= BIT(n))
#define BIT_GET(reg, n) (((reg) >> (n)) & 1UL)
判断标准:编译零警告;对某个变量依次 SET 后 GET 得 1、CLR 后 GET 得 0、TGL 后 GET 再翻回去;单步调试观察,只有目标位变化,其他位不动。
失败时下一步 :任何一步不符合预期,先查两处------宏里漏没漏括号、1 写没写成 1UL(原因见下文"坑 2"和"坑 3")。
一张表讲清四个动作
| 想做的事 | 正确写法 | 直观理解 | 最常见的错误写法 |
|---|---|---|---|
| 置 1(置位) | `reg | = BIT(3)` | "或上"这一位,其余位不动 |
| 清 0(清位) | reg &= ~BIT(3) |
取反得到"除这位全是 1"的掩码再"与" | reg &= 0(整个寄存器清零) |
| 取反(翻转) | reg ^= BIT(3) |
异或:和 1 异或的位翻转 | 想清零时误用 ^=(这位是翻转,不是清零) |
| 读某位 | (reg >> 3) & 1 |
先右移到位 0,再和 1 相与 | (reg & BIT(3)) == 1(结果是 8 不是 1,永远不成立) |
记住一个对应关系就够了:| 管置 1,& 管清 0,^ 管翻转,>> + & 管读。
为什么是这三个运算符
| 的性质:任何位和 1 相或变 1,和 0 相或不变。所以 reg | 0b1000 只改第 3 位。
& 的性质:任何位和 1 相与不变,和 0 相与变 0。想只清第 3 位,掩码必须构造出来:~BIT(3) 就是"除了第 3 位全是 1"的数。
^ 的性质:相同出 0,不同出 1。和 1 异或的位被翻转,和 0 异或的位保持原样------所以它是"翻转"专用,不能当清零用。
取低位 的口诀是先移位,再掩码:
c
uint32_t low3 = reg & 0x7; /* 取低 3 位 */
uint32_t bits76 = (reg >> 6) & 0x3; /* 取第 6~7 位:先移到 0 位再截取 */
反过来"先掩码再移位"也能写,但掩码值得手算,错一次就得查半天。
四个真实场景里最容易踩的坑
坑 1:判断某位时写 == 1
c
if ((reg & BIT(3)) == 1) /* 永远不成立! */
reg & BIT(3) 的结果要么是 0,要么是 8(即 BIT(3) 本身),永远不会是 1。正确写法二选一:
c
if (reg & BIT(3)) /* 直接当布尔值用 */
if ((reg & BIT(3)) == BIT(3)) /* 或者跟掩码本身比 */
坑 2:1 << 31 是未定义行为
1 的类型是有符号 int。C 标准规定,有符号数左移溢出是未定义行为 ,1 << 31 在不同编译器上可能得到不同结果,还可能触发编译警告。
所以 BIT 宏里必须写 1UL------把字面量变成无符号长整型,移到第 31 位才有确定结果。这不是风格问题,是标准规定。
坑 3:宏里漏括号
c
#define BIT(n) 1UL << n /* 危险写法 */
uint32_t x = BIT(2) * 8; /*你以为 32,实际 65536*/
乘法优先级高于移位,BIT(2) * 8 被展开成 1UL << 2 * 8,编译器算成 1UL << 16。结果差了 2048 倍,而且编译器不给任何警告。
所以宏定义里每个参数、每次移位都要包括号,这是开头那 5 行宏写满括号的原因。
坑 4:配置多个位时用 = 覆盖
c
reg = BIT(0) | BIT(4); /* 其他位全被清零了 */
想同时打开两位,用 |= 把这两位"或上去";或者先读、改、再写回:
c
reg |= (BIT(0) | BIT(4));
还有一个进阶注意点:读-改-写不是原子的 。如果中断和主循环会同时改同一个寄存器,|= 中间被打断就可能丢掉一方的改动。这类寄存器要么在临界区里改,要么用很多单片机都提供的"原子置位/清位"专用寄存器------写一次只影响目标位,天生没有读-改-写问题。
可复现的最小验证
下面这段代码在 PC 上就能编译运行(gcc / clang / 任意编译器均可),五个输出全对上,说明宏写对了;搬到单片机上对真实寄存器使用,行为一致:
c
#include <stdio.h>
#include <stdint.h>
#define BIT(n) (1UL << (n))
#define BIT_SET(reg, n) ((reg) |= BIT(n))
#define BIT_CLR(reg, n) ((reg) &= ~BIT(n))
#define BIT_TGL(reg, n) ((reg) ^= BIT(n))
#define BIT_GET(reg, n) (((reg) >> (n)) & 1UL)
int main(void) {
uint32_t reg = 0x00;
BIT_SET(reg, 3); printf("SET : 0x%02X\n", (unsigned)reg);
BIT_SET(reg, 0); printf("SET2 : 0x%02X\n", (unsigned)reg);
BIT_CLR(reg, 3); printf("CLR : 0x%02X\n", (unsigned)reg);
BIT_TGL(reg, 7); printf("TGL : 0x%02X\n", (unsigned)reg);
printf("GET3 : %u\n", (unsigned)BIT_GET(reg, 3));
return 0;
}
预期输出:
text
SET : 0x08
SET2 : 0x09
CLR : 0x01
TGL : 0x81
GET3 : 0
成功判据 :SET2 那行是 0x09 而不是 0x01------说明第二次置位没有破坏第 3 位。失败时下一步 :任何一行对不上,优先检查宏的括号和 1UL,再检查是不是把 |= 写成了 =。
实测对比 :reg = BIT(3) 后再 reg = BIT(0),第 3 位丢失;换成 reg |= BIT(n) 连续置位,所有位保留。坑 3 的例子(BIT(2) * 8 差 2048 倍)在无括号宏上必现。
有用的话点个收藏 ,下次写寄存器配置直接抄宏。有问题欢迎评论区交流。
相关文章 :volatile 什么时候必须加?三种必须和两种白加------位操作的变量被中断共享时,还得配合 volatile 才可靠;开漏和推挽有什么区别?上拉电阻到底怎么选------位操作最终落到 GPIO 输出寄存器,输出结构决定你置 1 清 0 之后引脚的真实电平。