欢迎光临
我们一直在努力

位运算详解:嵌入式最常用的7种操作技巧

嵌入式开发天天和寄存器打交道,而寄存器操作的本质就是位运算。
这篇整理7种最常用的位操作,每种都有寄存器实战代码,看完就能用。


一、为什么嵌入式离不开位运算?

/* STM32让PA5输出高电平 */
GPIOA->BSRR = (1 << 5);

/* 读PA0输入 */
uint8_t val = (GPIOA->IDR >> 0) & 1;

/* 设置波特率分频 */
USART1->BRR = 625;

寄存器是32位的,每一位控制一个功能。你要操作某个bit,就必须用位运算。


二、7种基本位操作

操作1:置位(把某位设为1)

/* 把第n位置1 */
reg |= (1 << n);

/* 示例:把GPIOA的第5位置1(PA5输出高) */
GPIOA->ODR |= (1 << 5);

/* 同时置多个位 */
reg |= (1 << 3) | (1 << 5); /* 第3和第5位都置1 */


操作2:清零(把某位设为0)

/* 把第n位清零 */
reg &= ~(1 << n);

/* 示例:PA5输出低 */
GPIOA->ODR &= ~(1 << 5);

/* 同时清多个位 */
reg &= ~((1 << 3) | (1 << 5));

为什么用 ~(1<<n):先1<<n得到只有第n位为1的值,取反后第n位为0其他为1,&运算后只清除第n位。


操作3:取反(0变1,1变0)

/* 把第n位取反 */
reg ^= (1 << n);

/* 示例:LED翻转 */
GPIOA->ODR ^= (1 << 5); /* PA5状态翻转 */

XOR特性:x ^ 1 = ~x(取反),x ^ 0 = x(不变)。


操作4:判断某位是否为1

/* 判断第n位是否为1 */
if (reg & (1 << n)) {
/* 第n位是1 */
}

/* 示例:判断PA0是否为低电平(按键按下) */
if ((GPIOA->IDR & (1 << 0)) == 0) {
/* PA0=0,按键按下 */
}


操作5:读取连续多位

/* 读取第n到第m位(n>m)的值 */
uint32_t val = (reg >> m) & ((1 << (n – m + 1)) – 1);

/* 示例:读ADC数据的低12位 */
uint16_t adc = ADC1->DR & 0xFFF; /* 0xFFF = (1<<12)-1 */

示例:读寄存器的bit[7:4](4位):

uint8_t val = (reg >> 4) & 0x0F; /* 右移4位,再mask低4位 */


操作6:写入连续多位

/* 先清除,再写入 */
#define BIT_MASK(start, width) (~(((1 << width) – 1) << start))

/* 清除bit[start:start+width-1] */
reg &= BIT_MASK(start, width);
/* 写入新值 */
reg |= (new_value << start);

/* BIT_MASK(2, 4) 展开推导过程:
* 1 << 4 = 0b00010000 = 0x10
* 0x10 – 1 = 0b00001111 = 0x0F (得到4个1)
* 0x0F << 2 = 0b00111100 = 0x3C (移到bit2-5的位置)
* ~0x3C = 0b11000011 = 0xC3 (取反,bit2-5为0,其余为1)
* 所以 reg &= 0xC3 会清除bit2-5,其他位不变 */

/* 示例:设置GPIO模式,PA5在CRL寄存器的bit20-23 */
GPIOA->CRL &= ~(0x0F << 20); /* 清除PA5的配置位(注意是PA5不是PC5,因为是GPIOA) */
GPIOA->CRL |= (0x03 << 20); /* 写入推挽输出50MHz */


操作7:快速计算对齐值

/* 向上对齐到4的倍数 */
#define ALIGN4(x) (((x) + 3) & ~3)

/* 原理:加3是为了向上取整(13+3=16),& ~3是清除低2位实现4字节对齐
* ~3 = 0b…11111100,& ~3就是把低2位置0,相当于向下取整到4的倍数
* 13+3=16, 16 & ~3 = 16(已经是4的倍数)
* 14+3=17, 17 & ~3 = 16 */

/* 示例 */
int size = ALIGN4(13); /* = 16 */
int size = ALIGN4(20); /* = 20 */

应用:DMA传输要求数据对齐,协议帧长度对齐。


三、常用位运算宏封装

/* 置位 */
#define SET_BIT(reg, bit) ((reg) |= (1U << (bit)))

/* 清零 */
#define CLR_BIT(reg, bit) ((reg) &= ~(1U << (bit)))

/* 读取 */
#define GET_BIT(reg, bit) (((reg) >> (bit)) & 1U)

/* 翻转 */
#define TOGGLE_BIT(reg, bit) ((reg) ^= (1U << (bit)))

/* 读取多位 */
#define GET_BITS(reg, start, width) (((reg) >> (start)) & ((1U << (width)) – 1))


四、实战:STM32寄存器操作

4.1 配置GPIO为输出

/* 配置PC13为推挽输出,50MHz */
/* CRH寄存器bit[23:20]控制Pin13 */
GPIOC->CRH &= ~(0x0F << 20); /* 清零 */
GPIOC->CRH |= (0x03 << 20); /* 0011:推挽输出50MHz */

4.2 点亮LED

/* PC13输出低电平点亮LED */
GPIOC->ODR &= ~(1 << 13); /* 清零第13位 = 输出低 */

4.3 读取按键

/* 读取PA0状态 */
uint8_t key = (GPIOA->IDR >> 0) & 1; /* 0=按下,1=松开 */


五、位域(Bit-field)

5.1 定义

struct flags {
uint8_t enable : 1; /* 1 bit */
uint8_t mode : 3; /* 3 bits */
uint8_t priority : 2; /* 2 bits */
uint8_t reserved : 2; /* 2 bits,凑满1字节 */
};
/* sizeof = 1字节 */

5.2 优缺点

优点缺点
省内存 跨平台行为不一致
代码可读性好 不能取地址
编译器处理位操作 位序依赖实现(位序=位域中各字段的排列顺序,有的编译器从低位排,有的从高位排,不同编译器可能不同)

建议:位域在跨平台通信中不推荐用(字节序和对齐不确定),寄存器操作还是用位运算宏更安全。


六、常见踩坑

坑1:1<<31的符号问题

int x = (1 << 31); /* 1是int,左移31位 → 符号位变1,负数! */

/* 正确:用无符号 */
uint32_t x = (1U << 31); /* 加U,无符号,= 0x80000000 */

规则:位运算永远加U后缀(1U),避免符号位问题。

坑2:位运算优先级低于比较

if (reg & 0x01 == 0) /* 实际是 reg & (0x01 == 0) = reg & 0 = 0! */
/* 永远为真 */

/* 正确:加括号 */
if ((reg & 0x01) == 0)
/* 判断bit0是否为0 */

规则:位运算和比较混用时,位运算必须加括号。


七、总结

操作代码
置位 reg |= (1 << n)
清零 reg &= ~(1 << n)
取反 reg ^= (1 << n)
判断 if (reg & (1 << n))
读多位 (reg >> start) & mask
写多位 先清&=再写|=
对齐 (x + 3) & ~3

一句话总结:嵌入式位运算的核心就是"置位用或、清零用与非、取反用异或、判断用与",记住这四个口诀,90%的寄存器操作都能搞定。


C语言基础系列到这篇完结。下一篇开始新的系列——嵌入式核心概念。
下一篇预告:《中断详解:从原理到NVIC优先级嵌套》

如果这个系列对你有帮助,点赞 + 收藏 + 关注,这是我持续更新的动力!
有问题欢迎评论区交流,我会逐条回复。

作者:嵌入式阿蔡

赞(0)
未经允许不得转载:171主机测评 » 位运算详解:嵌入式最常用的7种操作技巧
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址