掩码操作:位与提取与位或设置
各位同学,今天我们来聊聊嵌入式开发中最基础、也最常用的两个操作——掩码提取和掩码设置。
说白了,就是用位与(&)把某些位“抠”出来,用位或(|)把某些位“写”进去。
我刚开始做单片机开发时,总觉得位操作很绕。后来踩了几个坑,才真正体会到:掩码操作是嵌入式工程师的“基本功”,就像厨师切菜一样,天天用,必须练熟。
一、位与(&)提取特定位
先问大家一个问题:你有一个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比较,当然不对。
二、位或(|)设置特定位
提取搞定了,那怎么把某一位改成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)。
这时候需要两步:
- 先清零:用位与把目标位清0
- 再设置:用位或把新值写进去
// 假设要把第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环境下,可能需要加临界区保护。这个我们后面章节会讲。
四、知识体系总览
下面这张图总结了掩码操作的核心逻辑:
五、常见错误与避坑
| 错误操作 | 后果 | 正确做法 |
|---|---|---|
直接用 reg & mask 比较数值 |
结果在高位,比较永远不对 | 先右移再比较 |
| 设置多位时只做或操作,忘了先清零 | 新旧值叠加,结果错误 | 先与清零,再或写入 |
掩码用 1 << 31 但变量是int |
符号位扩展导致意外 | 用 1UL << 31 |
| 在中断和主循环中同时操作同一寄存器 | 读-改-写被中断打断,数据丢失 | 加临界区或使用原子操作 |
GPIOA->ODR |= 0x20;,主循环也在操作同一个ODR。结果中断来了,主循环刚读完ODR还没写回去,中断就把ODR改了。等主循环写回去时,中断的设置被覆盖了。从那以后,我只要发现多个地方操作同一个寄存器,就会用 __disable_irq() 保护起来。
六、小结
掩码操作其实就两句话:
- 想提取某几位:用位与,掩码那几位写1,其他写0
- 想设置某几位:用位或,掩码那几位写1,其他写0
- 想修改某几位:先与清零,再或写入
这些操作在寄存器配置、协议解析、状态机实现中无处不在。你练熟了,写代码会快很多,bug也会少很多。
好了,今天就聊到这里。记住:掩码是嵌入式工程师的“瑞士军刀”,随身带着,随时能用。