22、指针与位操作:指针的位运算、通过指针操作寄存器、嵌入式中的指针应用
说实话,指针和位操作放在一起讲,是嵌入式开发里最硬核的部分之一。很多同学学C语言时觉得指针难,位运算也难,但把它们结合起来——嗯,这才是真正干活用的东西。
我个人习惯把这一章叫做「嵌入式开发的底层钥匙」。你想想看,单片机里的寄存器、内存映射、外设控制,哪一样离得开指针和位操作?
22.1 指针的位运算:不只是加减乘除
指针本身是个地址值,本质上就是个无符号整数。所以,整数的位运算也能用在指针上。但要注意,不是所有位运算都合法。
我举个例子:
uint32_t *p = (uint32_t *)0x40020000;
uint32_t *q = p + 1; // 指针加法,偏移4字节
uint32_t *r = p & 0xFFFFFFFC; // 指针位与,对齐操作
这里 p & 0xFFFFFFFC 就是把指针的低2位清零,强制对齐到4字节边界。我在项目中遇到过DMA传输地址没对齐导致硬件异常的情况,后来就是用这种位运算做的地址对齐修正。
22.2 通过指针操作寄存器:嵌入式开发的日常
嵌入式里没有「寄存器变量」这种说法。寄存器就是内存地址空间里的一块区域。你要读写它,就得用指针。
看一个典型的GPIO操作:
// 假设GPIOB的基地址是0x40020C00
#define GPIOB_BASE ((uint32_t)0x40020C00)
#define GPIOB_ODR (*(volatile uint32_t *)(GPIOB_BASE + 0x14))
#define GPIOB_BSRR (*(volatile uint32_t *)(GPIOB_BASE + 0x18))
// 设置PB5输出高电平
GPIOB_BSRR = (1 << 5);
// 读取PB3的电平状态
uint8_t pin_state = (GPIOB_IDR >> 3) & 0x01;
这里有个关键点:volatile 关键字。为什么必须加?因为寄存器值可能被硬件改变,编译器如果优化掉读取操作,你就拿不到真实数据了。我曾经踩过这个坑——调试一个UART接收程序,数据死活收不到,最后发现是忘了加 volatile,编译器把读取优化成了缓存值。
22.3 位域与指针:小心结构体对齐
C语言支持位域结构体,可以用来精确控制寄存器中的位段。但位域和指针结合时,有很多坑。
typedef struct {
uint32_t MODER0 : 2; // 位0-1
uint32_t MODER1 : 2; // 位2-3
uint32_t MODER2 : 2; // 位4-5
uint32_t reserved : 26;
} GPIO_MODER_Type;
GPIO_MODER_Type *p_moder = (GPIO_MODER_Type *)0x40020C00;
p_moder->MODER0 = 0x01; // 设置模式为输出
嗯,这里要注意:位域结构体的内存布局是编译器相关的。不同编译器、不同字节序下,位域的排列顺序可能不一样。我建议只在同一平台、同一编译器下使用,跨平台移植时要格外小心。
22.4 指针与位掩码:高效的状态管理
嵌入式里经常用位掩码来表示多个状态。指针配合位掩码,可以写出非常高效的代码。
// 状态寄存器地址
volatile uint32_t *status_reg = (uint32_t *)0x40020010;
// 定义位掩码
#define FLAG_TX_DONE (1 << 0)
#define FLAG_RX_READY (1 << 1)
#define FLAG_ERROR (1 << 2)
// 检查多个标志
uint32_t flags = *status_reg;
if (flags & (FLAG_TX_DONE | FLAG_RX_READY)) {
// 发送完成或接收就绪
}
// 清除某个标志(写1清除)
*status_reg = FLAG_ERROR;
这种写法在中断服务程序里特别常见。你想想看,一个寄存器里可能同时有多个中断标志,用位掩码一次判断,效率比逐个判断高得多。
22.5 嵌入式中的指针应用:实战案例
我挑一个典型的例子——SPI Flash驱动中的指针操作。
// SPI数据寄存器
#define SPI_DR (*(volatile uint32_t *)0x4001300C)
// 从Flash读取数据到内存缓冲区
void spi_flash_read(uint32_t addr, uint8_t *buf, uint32_t len) {
// 发送读取命令和地址
SPI_DR = 0x03; // 读取命令
SPI_DR = (addr >> 16) & 0xFF; // 地址高8位
SPI_DR = (addr >> 8) & 0xFF; // 地址中8位
SPI_DR = addr & 0xFF; // 地址低8位
// 读取数据
for (uint32_t i = 0; i < len; i++) {
SPI_DR = 0xFF; // 发送空字节,产生时钟
buf[i] = SPI_DR; // 读取接收到的数据
}
}
这里 buf 是个指针,指向用户提供的缓冲区。通过指针操作,我们可以把数据直接写入内存,不需要中间拷贝。这在性能敏感的场合特别重要。
22.6 知识体系总览
下面这张图是我整理的指针与位操作的知识结构,你看一眼就能明白它们之间的关系:
22.7 避坑指南:我踩过的那些雷
做嵌入式十几年,指针和位操作相关的坑我踩过不少。挑几个典型的说说:
- 忘记 volatile: 调试一个定时器中断,标志位死活不更新。查了两天,发现是编译器把读取优化成了寄存器变量。加上 volatile 就好了。
- 位域顺序搞反: 用位域结构体映射一个32位寄存器,在小端系统上调试通过,换到大端系统上全乱套了。后来我改用位掩码+移位操作,彻底解决了移植问题。
- 指针未对齐: 有一次用 DMA 传输数据,源地址是奇数地址,结果 DMA 直接报错。后来强制用
& ~3做对齐,问题解决。
公众号:蓝海资料掘金营,微信deep3321