10、位操作与嵌入式应用:位与、位或、位异或、取反、左移右移,寄存器位操作实战
位操作,说白了就是直接跟芯片的「脑细胞」对话。
我刚开始做嵌入式那会儿,总觉得这东西就是几个逻辑符号来回折腾,没啥了不起。直到有一次调试一个电机驱动板,死活转不起来,最后发现是寄存器的一个控制位写反了。嗯,从那以后,我再也不敢小看这几个运算符了。
10.1 位操作的基础:六脉神剑
C语言里位操作就六个运算符,我习惯叫它们「六脉神剑」。你想想看,单片机里所有外设——GPIO、定时器、串口、ADC——它们的控制核心,全落在这六个符号上。
| 运算符 | 名称 | 口诀 |
|---|---|---|
| & | 位与 | 全1则1,有0则0 |
| | | 位或 | 有1则1,全0则0 |
| ^ | 位异或 | 相同为0,不同为1 |
| ~ | 取反 | 0变1,1变0 |
| << | 左移 | 左移n位,低位补0 |
| >> | 右移 | 右移n位,高位补0(无符号) |
我个人习惯把这六个操作分成两组:逻辑组(与、或、异或、取反)和移位组(左移、右移)。逻辑组负责「改值」,移位组负责「定位」。
核心心法:
- 想清零某几位?用 & 配合掩码。
- 想置位某几位?用 | 配合掩码。
- 想翻转某几位?用 ^ 配合掩码。
- 想提取某几位?用 & 配合掩码,再右移。
10.2 寄存器位操作实战:从理论到代码
嵌入式里没有「变量」这个概念,只有「寄存器地址」。我见过不少新手直接写 REG = 0x1234,这其实很危险——你一次把整个寄存器都改了,其他位的状态全丢了。
正确的做法是:读-改-写三步走。
10.2.1 置位某一位(或某几位)
假设我们要把寄存器 GPIOA->ODR 的第5位置1,其他位不变。
// 方法一:直接写(不推荐)
GPIOA->ODR |= 0x0020; // 0x0020 = (1 << 5)
// 方法二:用宏定义(推荐)
#define PIN5_MASK (1 << 5)
GPIOA->ODR |= PIN5_MASK;
为什么推荐用宏?我在项目中遇到过一件事:有个同事把 1 << 5 写成了 1 << 6,查了两天才发现。用宏定义后,一眼就能看出是操作第5脚。
10.2.2 清零某一位
// 清零第5位
GPIOA->ODR &= ~(1 << 5);
这里有个坑:~ 的优先级比 &= 高,但比 << 低。所以 ~(1 << 5) 是先移位再取反,结果正确。我曾经见过有人写成 ~1 << 5,那意思就完全变了——先取反1得到0xFFFFFFFE,再左移5位,结果完全不对。
避坑指南: 位操作里,括号是你的好朋友。拿不准优先级的时候,加括号准没错。
10.2.3 翻转某一位
// 翻转第5位
GPIOA->ODR ^= (1 << 5);
这个在按键消抖、LED闪烁里特别常用。每次执行,第5位就变一次,像翻书一样。
10.2.4 提取某几位
假设一个8位寄存器,bit[3:0]存放了ADC的通道号,bit[7:4]存放了采样值。我们要提取采样值:
uint8_t reg_val = 0xA5; // 假设读到的值
uint8_t sample = (reg_val >> 4) & 0x0F; // 提取高4位
uint8_t channel = reg_val & 0x0F; // 提取低4位
先右移把目标位对齐到低位,再 & 掩码把其他位清掉。这个套路我用了十年,从来没出过问题。
10.3 实战案例:配置一个GPIO输出
我们拿STM32来举个完整的例子。配置PA5为推挽输出,速度50MHz。
// 1. 开启GPIOA时钟(RCC寄存器)
RCC->AHB1ENR |= (1 << 0); // 置位bit0,使能GPIOA时钟
// 2. 配置模式寄存器(MODER)
// PA5对应MODER的bit[11:10],设为01(输出模式)
GPIOA->MODER &= ~(0x3 << 10); // 先清零bit[11:10]
GPIOA->MODER |= (0x1 << 10); // 再置为01
// 3. 配置输出类型(OTYPER)
// PA5对应OTYPER的bit5,设为0(推挽输出)
GPIOA->OTYPER &= ~(1 << 5);
// 4. 配置输出速度(OSPEEDR)
// PA5对应OSPEEDR的bit[11:10],设为10(高速50MHz)
GPIOA->OSPEEDR &= ~(0x3 << 10);
GPIOA->OSPEEDR |= (0x2 << 10);
// 5. 输出高电平
GPIOA->ODR |= (1 << 5);
你看,整个配置过程全是位操作。没有加减乘除,没有浮点运算,就是 &、|、<< 这三个符号来回用。
我的小技巧: 写寄存器配置时,先画个二进制草图。把每一位标出来,再写代码。这样不容易漏位,也不容易写错位置。
10.4 位操作的高级技巧:位域与联合体
C语言里有个冷门但好用的东西——位域。它允许你按位定义结构体成员。
typedef struct {
uint8_t channel : 4; // 低4位
uint8_t sample : 4; // 高4位
} ADC_Result_t;
ADC_Result_t result;
result.channel = 3;
result.sample = 10;
不过要小心:位域在内存布局上跟编译器有关。不同芯片、不同编译器,位域的排列顺序可能不一样。我一般在跨平台代码里不用位域,老老实实用 & 和 >> 手动提取。
10.5 知识体系总览
下面这张图,是我自己总结的位操作知识体系。你可以把它当成一张「地图」,学完这章后对照着看看,哪里还不熟。
10.6 写在最后
位操作这东西,看着简单,用好了是真功夫。我见过有人用位操作实现了一个超精简的状态机,整个逻辑就几十行代码,跑起来又快又稳。
记住一句话:嵌入式开发里,位操作不是选择题,是必答题。 你躲不开它,不如把它练成肌肉记忆。
下次写寄存器配置的时候,试着不用现成的HAL库,自己手写位操作。相信我,写个三五次,你就再也不想回去用那些臃肿的库函数了。
公众号:蓝海资料掘金营,微信deep3321