C语言位运算笔记
- C语言进阶
- 13小时前
- 26热度
- 0评论
C 语言位运算是嵌入式开发的基础能力。对于 STM32 来说,GPIO、UART、SPI、定时器等外设最终都是通过寄存器中的 bit 位来控制的。掌握位运算,才能真正理解 HAL 库背后的底层逻辑,并具备直接操作寄存器的能力。
💡 核心结论
嵌入式里最常见的 4 个动作就是:
嵌入式里最常见的 4 个动作就是:
置位、清位、翻转、读位。
六大位运算符速览
| 运算符 | 名称 | 示例 | 说明 |
|---|---|---|---|
& |
按位与 | a & b |
同 1 为 1,否则为 0 |
| |
按位或 | a | b |
有 1 为 1,否则为 0 |
^ |
按位异或 | a ^ b |
不同为 1,相同为 0 |
~ |
按位取反 | ~a |
0 变 1,1 变 0 |
<< |
左移 | a << n |
各位左移 n 位,低位补 0 |
>> |
右移 | a >> n |
各位右移 n 位,无符号数高位补 0 |
容易混淆的符号
| 写法 | 含义 | 别混成 |
|---|---|---|
& |
按位与 | &&(逻辑与) |
\| |
按位或 | \|\|(逻辑或) |
~ |
按位取反 | !(逻辑非) |
^ |
按位异或 | 平方 |
<< |
左移 | <(小于) |
>> |
右移 | >(大于) |
x & y // 按位与
x && y // 逻辑与
x | y // 按位或
x || y // 逻辑或
~x // 按位取反
!x // 逻辑非
x ^ y // 按位异或,不是平方
💡 速记:位运算是“按每一位算”,逻辑运算是“按真假算”。另外,C 语言里没有平方运算符,
x^2 不是平方,平方应写成 x * x。
按位与 &:清零与检测
0b10101111
& 0b00001111
= 0b00001111
可以把它理解为:与 0 清零,与 1 保留。
// 读取 GPIOA->IDR 第 3 位引脚状态
uint32_t pin_state = GPIOA->IDR & (1 << 3);
// 清除 PA5 的模式位
GPIOA->MODER &= ~(0x3 << (2 * 5));
ℹ️ 使用场景:检测某一位是否为 1、清除某几位、配合掩码提取字段。
按位或 |:置位
0b10100000
| 0b00001111
= 0b10101111
可以把它理解为:或 1 置位,或 0 保留。
// 将 PA5 配置为输出模式
GPIOA->MODER |= (0x1 << (2 * 5));
// 使能 GPIOA 时钟
RCC->AHB1ENR |= (1 << 0);
按位异或 ^:翻转
0b10101010
^ 0b00001111
= 0b10100101
可以把它理解为:异或 1 翻转,异或 0 保留。
// 翻转 PA5 电平,实现 LED 闪烁
GPIOA->ODR ^= (1 << 5);
// 不用临时变量交换两个数
uint32_t a = 0xAA, b = 0x55;
a = a ^ b;
b = a ^ b;
a = a ^ b;
按位取反 ~:构造清零掩码
~0b00001111 = 0b11110000
它最常见的用途不是“单独用”,而是和 & 配合实现清位。
GPIOA->MODER &= ~(0x3 << (2 * 5));
⚠️ 注意:
~ 的结果宽度和变量类型有关。STM32 寄存器一般是 32 位,建议统一使用 uint32_t。
左移 <<:补 0,并且常常表示翻倍
1 << 0 // 0x01
1 << 3 // 0x08
1 << 7 // 0x80
左移就是把二进制整体往左挪,右边补 0。
对于无符号整数,左移 1 位可以近似理解为 乘 2,左移 n 位可以近似理解为乘 2^n。
3 << 1 // 6
3 << 2 // 12
它在 STM32 里最常见的用途,是构造第 n 位掩码:
#define BIT(n) (1UL << (n))
RCC->AHB1ENR |= BIT(0); // GPIOA
RCC->AHB1ENR |= BIT(1); // GPIOB
RCC->AHB1ENR |= BIT(2); // GPIOC
注意:
<<是移位运算符,不是比较大小的<。
右移 >>:移到低位,并且常常表示减半
0b10000000 >> 3 = 0b00010000
右移就是把二进制整体往右挪。对 uint32_t 这类无符号数来说,左边补 0。
对于无符号整数,右移 1 位可以近似理解为 除以 2,右移 n 位可以近似理解为除以 2^n。
8 >> 1 // 4
8 >> 2 // 2
右移常用于把高位字段移到低位,再配合掩码读取:
// 读取 GPIOA->MODER 中 PA5 的模式值
uint32_t mode = (GPIOA->MODER >> (2 * 5)) & 0x3;
⚠️ 注意:
>> 是移位运算符,不是比较大小的 >。寄存器操作建议使用无符号类型,行为更稳定。
STM32 最常用的四种位操作
1. 置位
REG |= (1 << n);
2. 清位
REG &= ~(1 << n);
3. 翻转位
REG ^= (1 << n);
4. 读取位
if (REG & (1 << n)) {
// 第 n 位为 1
}
📋 速记
|=:置 1&= ~:清 0^=:翻转& mask:检测
修改多位字段的标准写法
改连续几位字段时,最标准的写法就是:先清后写。
REG &= ~(MASK << POS); // 清掉目标字段
REG |= (VALUE << POS); // 写入新值
其中:
MASK:字段本身的掩码,例如 2 位字段就是0x3POS:字段起始位VALUE:要写进去的新值
例如把 PA5 配置为输出模式(MODER[11:10] = 01):
GPIOA->MODER &= ~(0x3 << (2 * 5));
GPIOA->MODER |= (0x1 << (2 * 5));
为什么不能直接写 |=
因为多位字段原来可能不是 0。若不先清零,旧值可能残留,结果就错了。
字段读取 / 写入标准模板
字段操作最好成对记忆:读字段 和 写字段。
// 读取字段
value = (REG >> POS) & MASK;
// 写入字段
REG &= ~(MASK << POS);
REG |= (VALUE << POS);
这样记最稳:读 = 右移再与,写 = 先清后写。
常见优先级陷阱
1. 位判断最好总是加括号
if (REG & (1 << n)) // 推荐
if ((REG & (1 << n)) != 0) // 更清晰
2. 不要写成下面这样
if (REG & 1 << n == 1) // 容易错
因为这里混合了 &、<<、==,可读性很差,也容易误判。
3. 字段写入也建议给移位加括号
REG |= (VALUE << POS); // 推荐
REG |= VALUE << POS; // 不推荐
⚠️ 经验法则:只要一行里同时出现位运算、移位、比较,基本都应该主动加括号,不要赌自己记得优先级。
最常用速查
| 操作 | 标准写法 |
|---|---|
| 置第 n 位 | REG \|= (1UL << n) |
| 清第 n 位 | REG &= ~(1UL << n) |
| 翻转第 n 位 | REG ^= (1UL << n) |
| 读第 n 位 | ((REG >> n) & 1UL) |
| 读多位字段 | ((REG >> POS) & MASK) |
| 写多位字段 | REG &= ~(MASK << POS); REG \|= (VAL << POS); |
记住这 3 点就够了
- 单个位:
置 1 / 清 0 / 翻转 / 读取 - 多位字段:一定是先清后写
- 位判断尽量加括号,寄存器操作尽量用
uint32_t