C语言基本笔记(6)—— 位操作
在单片机软件开发中,位操作是硬件寄存器控制和资源优化的核心手段。
一、常用位操作
1. 基本位操作
-
置位(Set Bit):将某位设为1
PORT |= (1 << PIN); // 将PORT寄存器的第PIN位置1(例如:PIN=3 → 0b00001000)
-
清零(Clear Bit):将某位设为0
PORT &= ~(1 << PIN); // 将PORT寄存器的第PIN位清零(掩码取反后按位与)
-
取反(Toggle Bit):翻转某位的值
PORT ^= (1 << PIN); // 异或操作:0→1,1→0
-
判断位状态(Check Bit):检查某位是否为1
if (PORT & (1 << PIN)) { /* PIN位为1时的逻辑 */ }
2. 多比特操作
-
设置多个位:同时置位多个位
REG |= (BIT0 | BIT2 | BIT5); // 置位第0、2、5位
-
清除多个位:同时清零多个位
REG &= ~(BIT1 | BIT3); // 清零第1、3位
-
掩码替换值:替换某几位的值(如设置寄存器模式)
// 将REG的第2~4位(共3位)替换为0b101 REG = (REG & ~(0x7 << 2)) | (0x5 << 2);
3. 移位操作
- 左移/右移:用于位域提取或合并
uint8_t value = (data >> 3) & 0x0F; // 提取data的第3~6位(共4位)
二、代码技巧
1. 使用宏定义寄存器地址和位掩码
// 定义寄存器地址(假设PORTB地址为0x25)
#define PORTB (*(volatile uint8_t*)0x25)
// 定义位掩码
#define LED_PIN (1 << 5) // PB5
#define BUTTON_PIN (1 << 2) // PB2
// 使用示例
PORTB |= LED_PIN; // 点亮LED
if (PORTB & BUTTON_PIN) { /* 按钮按下 */ }
2. 位域结构体(用于寄存器映射)
typedef struct {
uint8_t mode : 2; // 低2位:模式配置
uint8_t enable : 1; // 第2位:使能位
uint8_t : 3; // 匿名位域填充(保留未用)
uint8_t error : 2; // 高2位:错误码
} ControlReg;
volatile ControlReg* pReg = (volatile ControlReg*)0x4000;
pReg->enable = 1; // 直接操作寄存器位
3. 联合体(Union)与位操作
结合联合体和结构体,实现灵活的数据访问:
typedef union {
uint16_t value; // 整体值
struct {
uint8_t low; // 低字节
uint8_t high; // 高字节
};
struct {
uint8_t bit0 : 1; // 按位访问
// ... 其他位定义
};
} DataReg;
DataReg reg;
reg.value = 0x1234;
reg.high = 0xAB; // 修改高字节
if (reg.bit0) { // 检查第0位
// ...
}
4. 内联函数或宏封装常用操作
// 宏封装置位和清零
#define SET_BIT(reg, bit) ((reg) |= (1 << (bit)))
#define CLEAR_BIT(reg, bit) ((reg) &= ~(1 << (bit)))
// 内联函数判断位状态
static inline bool IsBitSet(volatile uint8_t* reg, uint8_t bit) {
return (*reg & (1 << bit)) != 0;
}
三、注意事项
1. 原子操作
- 问题:在中断或多任务环境中,位操作可能被中断打断,导致数据不一致。
- 解决:
- 关闭中断保护关键操作:
- 使用原子操作指令(如C11的
_Atomic
,但需编译器支持)。
2. 可移植性
- 位域顺序:不同编译器对位域的存储顺序(大端/小端)可能不同,跨平台代码需谨慎。
- 寄存器映射:硬件寄存器地址可能因单片机型号不同而变化,需通过头文件统一管理。
3. 位域的内存布局
- 填充位:编译器可能在位域间插入填充位,导致结构体大小不符合预期。
- 示例:
struct { uint8_t a : 2; uint8_t b : 3; }; // 可能占用1字节(5位+3填充位)而非按预期紧凑排列。
4. 位序问题
- 硬件寄存器位序:某些外设的寄存器位序可能与直觉相反(如高位在前),需仔细查阅手册。
- 示例:SPI控制寄存器中,数据位可能从高位(MSB)开始传输。
5. 性能优化
- 频繁位操作:过多的位操作可能降低效率,可合并多次操作为一个整型赋值。
- 示例:
// 低效写法 PORT |= BIT0; PORT |= BIT1; // 高效写法 PORT |= (BIT0 | BIT1);
6. 硬件限制
- 只读/只写位:某些寄存器位可能只读或只写,误操作会导致未定义行为。
- 保留位:未使用的寄存器位通常需保持默认值,避免随意修改。
四、典型应用场景
1. GPIO控制
// 配置PB5为输出,并输出高电平
DDRB |= (1 << DDB5); // 设置方向为输出
PORTB |= (1 << PB5); // 输出高电平
2. 状态标志管理
// 使用uint8_t的每一位表示一个状态
uint8_t flags = 0;
#define FLAG_ERROR (1 << 0)
#define FLAG_READY (1 << 1)
flags |= FLAG_READY; // 设置就绪标志
if (flags & FLAG_ERROR) { ... } // 检查错误标志
3. 数据打包与解包
// 将温度(10位)和湿度(6位)打包为2字节
uint16_t pack_data(uint16_t temp, uint8_t humidity) {
return (temp & 0x3FF) | ((humidity & 0x3F) << 10);
}
合理使用位操作可显著提升单片机代码的效率和可维护性,但需结合硬件手册和实际场景谨慎设计。