C语言位运算笔记

C 语言位运算是嵌入式开发的基础能力。对于 STM32 来说,GPIO、UART、SPI、定时器等外设最终都是通过寄存器中的 bit 位来控制的。掌握位运算,才能真正理解 HAL 库背后的底层逻辑,并具备直接操作寄存器的能力。

💡 核心结论
嵌入式里最常见的 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 位字段就是 0x3
  • POS:字段起始位
  • 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. 单个位:置 1 / 清 0 / 翻转 / 读取
  2. 多位字段:一定是先清后写
  3. 位判断尽量加括号,寄存器操作尽量用 uint32_t