8.1 位运算与寄存器掩码


8.1 位运算与寄存器掩码

本节摘要:六个位运算符(与、或、异或、取反、左移、右移)与 CPU 的逻辑指令一一对应,是 C 语言里"离硬件最近"的语法。掩码四则——查位、置位、清位、翻位——覆盖了寄存器配置、协议解析、标志管理的全部日常。本节从真值表演练到寄存器实战,顺带讲清移位的未定义边界与运算优先级这两个坑。

六个运算符:一条代码一条指令

unsigned a = 0x0C; /* 0000 1100 */ a & 0x02; /* 按位与:0000 0000 —— 全 1 才 1,用于"筛选" */ a | 0x10; /* 按位或:0001 1100 —— 有 1 就 1,用于"打开" */ a ^ 0x0F; /* 异或: 0000 0011 —— 不同才 1,用于"翻转""比较差异" */ ~a; /* 取反: 1111 0011 —— 0 换 1,1 换 0 */ a << 2; /* 左移: 0011 0000 —— 乘 4(位向高位搬) */ a >> 2; /* 右移: 0000 0011 —— 无符号数除 4 */

它们分别编译成 AND、OR、XOR、NOT、SHL、SHR 指令——写什么指令就是什么,零抽象损耗,这就是位运算在嵌入式与系统编程里地位崇高的原因。每位的运算结果用真值表一眼看全(以与、或、异或为例):

位A 位B 异或
0 0 0 0 0
0 1 0 1 1
1 0 0 1 1
1 1 1 1 0

掩码四则:查、置、清、翻

flags 是一个 8 位的状态字节,位编号 0 到 7:

/* 查:第 n 位是 0 还是 1(结果为 0 或非 0,别当 1 用) */ flags & (1u << n) /* 置:把第 n 位设为 1,其余不动 */ flags |= (1u << n); /* 清:把第 n 位设为 0,其余不动 */ flags &= ~(1u << n); /* 翻:第 n 位取反 */ flags ^= (1u << n);

注意写法模式:移位生成"只有目标位是 1"的掩码,再配合与或异或完成四种操作。置位用或(有 1 就 1)、清位用"反码与"(反码里目标位是 0,与运算把它清零而别处放行)、翻位用异或(不同才 1)。这一套是寄存器操作的字母表。

验证程序:

#include <stdio.h> int main(void) { unsigned flags = 0x00; flags |= (1u << 0) | (1u << 3); /* 打开第 0、3 位 */ printf("置位后 0x%02X\n", flags); /* 0x09 */ printf("第 3 位 = %u\n", (flags >> 3) & 1u); /* 1:先移到最低位再取 */ flags &= ~(1u << 0); /* 关掉第 0 位 */ printf("清位后 0x%02X\n", flags); /* 0x08 */ flags ^= 0xFF; /* 全部翻转 */ printf("翻转后 0x%02X\n", flags); /* 0xF7 */ return 0; }

图 1 掩码操作与寄存器配置对照

图 1 掩码操作与寄存器配置对照

实战:位标志与权限打包

位标志管理一组布尔状态,一个字节存 8 个开关:

#define FLAG_READY (1u << 0) #define FLAG_ERROR (1u << 1) #define FLAG_BUSY (1u << 2) #define FLAG_TIMEOUT (1u << 3) unsigned status = FLAG_READY; if (status & FLAG_ERROR) { /* 有错误 */ } status |= FLAG_BUSY; /* 标记忙 */ status &= ~FLAG_BUSY; /* 解除忙 */

多字段的打包与解包(协议与寄存器常见:把 3 个小字段压进一个 16 位值):

unsigned pack(unsigned type, unsigned len, unsigned ack) { return (type << 12) | (len << 2) | ack; /* type 4位 len 10位 ack 2位 */ } void unpack(unsigned v, unsigned *type, unsigned *len, unsigned *ack) { *type = (v >> 12) & 0xF; /* 右移到最低位 再用与掩码截取 */ *len = (v >> 2) & 0x3FF; /* 0x3FF = 10 个 1 */ *ack = v & 0x3; }

口诀:取字段 = 移到最低位再与截取;存字段 = 移到目标位置再或合并。加上第 7 章的位段 struct,C 提供了两种风格,位段写法可读、掩码写法可控(布局完全由你定),协议解析常用掩码、业务状态常用位段。

两个必踩的坑

移位的未定义边界:移位数大于等于位宽(x << 32 当 x 是 32 位)是未定义行为,不是"转一圈回来";有符号数的右移(算术移位补符号还是补 0)由实现定义——位运算一律用无符号类型,语义完全确定。

优先级陷阱:位运算的优先级低于比较运算!flags & MASK == 0 被解析成 flags & (MASK == 0),恒等于 flags & 0 即 0。正确写法必须加括号:(flags & MASK) == 0。这类错误编译器不报错、逻辑直接错——位运算表达式里的括号不是风格,是保命。

顺带一提 XOR 的三个妙用:不用临时变量交换两个整数(a^=b; b^=a; a^=b;);找出一组数中唯一出现一次的数(全部异或,成对抵消);简单加密(与密钥异或两次还原)。都是"不同才 1"性质的直接推论。

⚠️ 常见坑:读写硬件寄存器漏了 volatile。寄存器的值会被硬件异步改变,编译器优化可能把重复读取缓存进寄存器——寄存器地址必须声明 volatile,这是第 1 章讲过的"给优化器的封条"在嵌入式的主战场。

💡 关键直觉:位运算是"对内存里的每一个位做手术"。写掩码代码时脑中要有那排 0/1 方格图——查是"对准了看"、置是"点亮"、清是"遮住再熄"、翻是"全部取反目标位"。

本节要点回顾

  • 六个位运算符一一对应 CPU 逻辑指令:零抽象损耗,贴近硬件的最近通道。
  • 掩码四则:查用与、置用或、清用反码与、翻用异或;移位生成单兵掩码是核心手法。
  • 打包解包口诀:取字段先移后截,存字段先移后并。
  • 位运算一律用无符号:有符号右移与超宽移位都是实现定义或未定义。
  • 括号是保命符:位运算优先级低于比较,漏括号恒错。

下一节从位的尺度升到块的尺度:文件 IO 与它的三级缓冲。

常见疑问

问:左移与乘法哪个快? 左移几乎总是不慢于乘法,但现代编译器会把常量乘法自动转成移位加法组合,手写移位未必更快,可读性还更差。位运算的真正主场是按位语义(掩码、标志、字段),不是算术优化。

问:怎么数一个整数里有多少个 1? 经典手法是循环"v 与 v 减一"直到为零,循环次数恰是 1 的个数——每次与运算消去最低的一个 1。也有查表与分治法,原理都是位模式的性质。

问:位运算能用在浮点数上吗? 不能直接用。位运算符只接受整数类型;要看浮点的位模式,先经共用体或指针转换到整数视图(第 2 章与第 7 章的做法)。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U