掩码操作:位与提取与位或设置

各位同学,今天我们来聊聊嵌入式开发中最基础、也最常用的两个操作——掩码提取掩码设置

说白了,就是用位与(&)把某些位“抠”出来,用位或(|)把某些位“写”进去。

我刚开始做单片机开发时,总觉得位操作很绕。后来踩了几个坑,才真正体会到:掩码操作是嵌入式工程师的“基本功”,就像厨师切菜一样,天天用,必须练熟。

一、位与(&)提取特定位

先问大家一个问题:你有一个32位的寄存器,想看看第3位到底是0还是1,怎么办?

答案就是——用位与操作,配合一个掩码。

掩码是什么?掩码就是一个“筛子”。你想看哪几位,就把掩码的那几位设成1,其他位设成0。然后跟原数做位与,结果中非掩码位全部被清零,只留下你关心的那几位。

1. 提取单个位

比如,我想提取变量 reg 的第3位(从0开始计数):

uint32_t reg = 0xAB;      // 二进制: 1010 1011
uint32_t mask = (1 << 3); // 掩码: 0000 1000
uint32_t bit3 = reg & mask; // 结果: 0000 1000 (非零,说明第3位是1)

这里有个小技巧:判断某位是否为1,可以直接用 if (reg & (1 << n)),不用非得把结果存下来。

我的习惯: 我一般把这种判断写成宏,方便复用:
#define GET_BIT(x, n)  ((x) & (1UL << (n)))
注意用 1UL 而不是 1,避免在16位MCU上出问题。

2. 提取多个连续位

有时候我们需要提取一个字段,比如寄存器中第4~7位(共4位)表示某个状态。这时候掩码就要覆盖这4位:

uint32_t reg = 0x3F4;      // 二进制: 0011 1111 0100
uint32_t mask = 0xF0;      // 掩码: 0000 1111 0000 (第4~7位)
uint32_t field = reg & mask; // 结果: 0011 0000 0000

但注意,这样提取出来的值还“待在原位”。如果你想把它右移到最低位,方便后续计算,需要再右移:

uint32_t field_val = (reg & mask) >> 4; // 结果: 0011 (即3)

嗯,这里有个坑——我见过不少新手直接拿 reg & mask 去比较,结果永远对不上。因为值还在高位,你拿它跟0~15比较,当然不对。

我曾经踩过的坑: 有一次调试SPI从机的状态寄存器,提取第2~5位时忘了右移,结果判断条件死活不成立。查了整整一下午,最后发现是位偏移没处理。从那以后,我写提取操作时一定会顺手把右移加上。

二、位或(|)设置特定位

提取搞定了,那怎么把某一位改成1?用位或。

原理很简单:你想把哪一位设成1,就把掩码的那一位写成1,其他位写成0,然后跟原数做位或。因为0或任何数都不变,1或任何数都得1。

1. 设置单个位

uint32_t reg = 0xAB;      // 二进制: 1010 1011
uint32_t mask = (1 << 2); // 掩码: 0000 0100
reg = reg | mask;          // 结果: 1010 1111 (第2位被置1)

更简洁的写法:reg |= (1 << 2);

2. 设置多个位

比如要把第4~7位全部设成1:

reg |= 0xF0;  // 0xF0 = 1111 0000

但更多时候,我们不是简单地把这些位全设成1,而是想写入一个特定的值。比如把第4~7位设成二进制1010(即10)。

这时候需要两步:

  1. 先清零:用位与把目标位清0
  2. 再设置:用位或把新值写进去
// 假设要把第4~7位设为 1010 (即10)
uint32_t reg = 0x3F4;
uint32_t clear_mask = ~0xF0;  // 清零掩码: 1111 0000 1111
uint32_t set_val = (10 << 4); // 新值左移到对应位置: 1010 0000

reg = (reg & clear_mask) | set_val;
// 结果: 原值 0011 1111 0100 → 0011 1010 0100
核心套路: 修改寄存器中的某几个位,永远是“先与后或”——先与清零,再或写入。这个顺序不能乱。

三、实战:GPIO输出寄存器操作

讲个实际例子。我在做STM32项目时,经常要操作GPIO的ODR寄存器(输出数据寄存器)。比如我想让PA5输出高电平,同时不影响其他引脚:

// 设置PA5为高电平
GPIOA->ODR |= (1 << 5);

// 设置PA5为低电平
GPIOA->ODR &= ~(1 << 5);

你看,设置高电平用位或,设置低电平用位与+取反。这两个操作在驱动LED、控制电机时天天用。

但要注意:如果多个任务同时操作同一个寄存器,位操作不是原子的。在RTOS环境下,可能需要加临界区保护。这个我们后面章节会讲。

四、知识体系总览

下面这张图总结了掩码操作的核心逻辑:

掩码操作核心逻辑 位与(&)提取特定位 掩码位为1 → 保留;掩码位为0 → 清零 位或(|)设置特定位 掩码位为1 → 置1;掩码位为0 → 不变 提取应用场景 • 读取寄存器某一位的状态 • 提取多bit字段(如ADC值) 设置应用场景 • 置位/清零GPIO引脚 • 写入寄存器字段(先与后或) 核心口诀:提取用 &,设置用 |,修改字段先与后或

五、常见错误与避坑

错误操作 后果 正确做法
直接用 reg & mask 比较数值 结果在高位,比较永远不对 先右移再比较
设置多位时只做或操作,忘了先清零 新旧值叠加,结果错误 先与清零,再或写入
掩码用 1 << 31 但变量是int 符号位扩展导致意外 1UL << 31
在中断和主循环中同时操作同一寄存器 读-改-写被中断打断,数据丢失 加临界区或使用原子操作
我曾经犯过的错: 有一次在中断服务函数里直接 GPIOA->ODR |= 0x20;,主循环也在操作同一个ODR。结果中断来了,主循环刚读完ODR还没写回去,中断就把ODR改了。等主循环写回去时,中断的设置被覆盖了。从那以后,我只要发现多个地方操作同一个寄存器,就会用 __disable_irq() 保护起来。

六、小结

掩码操作其实就两句话:

  • 想提取某几位:用位与,掩码那几位写1,其他写0
  • 想设置某几位:用位或,掩码那几位写1,其他写0
  • 想修改某几位:先与清零,再或写入

这些操作在寄存器配置、协议解析、状态机实现中无处不在。你练熟了,写代码会快很多,bug也会少很多。

好了,今天就聊到这里。记住:掩码是嵌入式工程师的“瑞士军刀”,随身带着,随时能用。


公众号:蓝海资料掘金营,微信deep3321