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 知识体系总览

下面这张图,是我自己总结的位操作知识体系。你可以把它当成一张「地图」,学完这章后对照着看看,哪里还不熟。

位操作核心 六大运算符 三大操作模式 实战应用 & 位与 | 位或 ^ 异或 ~ 取反 << 左移 >> 右移 置位(|) 清零(&~) 翻转(^) 提取(&>>) GPIO配置 时钟使能 中断控制 状态读取 ⚠️ 注意事项 优先级问题 · 掩码设计 · 读-改-写 · 位域兼容性

10.6 写在最后

位操作这东西,看着简单,用好了是真功夫。我见过有人用位操作实现了一个超精简的状态机,整个逻辑就几十行代码,跑起来又快又稳。

记住一句话:嵌入式开发里,位操作不是选择题,是必答题。 你躲不开它,不如把它练成肌肉记忆。

下次写寄存器配置的时候,试着不用现成的HAL库,自己手写位操作。相信我,写个三五次,你就再也不想回去用那些臃肿的库函数了。


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