1. 嵌入式C语言进阶:指针、关键字与寄存器映射
大家好,我是你们这趟实战之旅的领路人。咱们第一节课,不急着敲板子、点灯。先把C语言这把刀磨快。嵌入式开发,说白了就是跟寄存器、内存、硬件引脚打交道。C语言玩不转,后面移植RTOS、调低功耗,处处是坑。
我个人习惯,拿到一个新平台,第一件事不是看原理图,而是先看芯片头文件里的寄存器定义。那里藏着硬件工程师和软件工程师的“契约”。今天我们就来拆解这份契约里的几个核心语法。
1.1 指针:硬件的“遥控器”
指针是什么?很多人背过“指针就是地址”。但在我眼里,指针就是一根直接连到硬件寄存器的导线。你通过指针写数据,就是在给硬件发指令。
看个最经典的例子,操作GPIO输出高电平:
// 假设GPIOB端口的输出数据寄存器地址为 0x40010C0C
#define GPIOB_ODR (*(volatile unsigned int *)0x40010C0C)
void LED_On(void) {
GPIOB_ODR |= (1 << 5); // 将PB5引脚置高
}
这里 (volatile unsigned int *)0x40010C0C 是什么意思?
- 0x40010C0C:一个赤裸裸的物理地址。
- (unsigned int *):把它强制转换成指向32位整数的指针。
- volatile:告诉编译器,这个地址的值随时可能被硬件改变,别给我优化掉。
- 最外层的 *:解引用,就是通过这个指针去操作那个地址里的内容。
1.2 volatile与const:编译器的“紧箍咒”
这两个关键字,面试常考,项目里也常栽跟头。
volatile:别瞎优化!
编译器为了效率,会把变量值暂存在CPU寄存器里。但硬件寄存器的值,或者中断服务程序里修改的变量,随时会变。编译器如果用了“缓存”的值,程序就错了。
我曾经调试一个串口接收程序,数据死活收不全。查了两天,发现接收标志位没加volatile。编译器觉得那个变量在循环里没被修改,就直接从寄存器里读旧值,根本不理会硬件更新的内存。加上volatile,问题秒解。
const:别乱改!
const在嵌入式里有两个妙用:
- 定义只读参数表: 比如正弦波表、校准系数,放在Flash里省RAM。
- 修饰函数参数: 告诉调用者,这个函数不会修改你传进来的数据。比如:
void UART_Send(const unsigned char *data, int len);
const和volatile可以同时使用。比如读取硬件状态寄存器:
// 状态寄存器,硬件会改,但软件只读
#define USART_SR (*(const volatile unsigned int *)0x40013800)
嗯,这里要注意,const修饰的是指针指向的内容,不是指针本身。硬件可以改,但你的代码不能写它。
1.3 位操作:寄存器的“手术刀”
寄存器操作,无非就是置位、清零、读取、翻转。C语言提供了&、|、^、~、<<、>>这几把手术刀。
我整理了一个常用操作速查表:
| 操作 | 表达式 | 说明 |
|---|---|---|
| 置位某位 | REG |= (1 << n); |
将第n位写1,其他位不变 |
| 清零某位 | REG &= ~(1 << n); |
将第n位写0,其他位不变 |
| 读取某位 | bit = (REG >> n) & 1; |
获取第n位的值(0或1) |
| 翻转某位 | REG ^= (1 << n); |
将第n位取反 |
| 连续位赋值 | REG &= ~(0xF << 4); |
先清零bit[7:4],再写入新值 |
REG |= (value << 4);,结果value的某些位和原来的位叠加了,配置全乱套。
1.4 函数指针:驱动架构的“灵魂”
函数指针,就是指向函数的指针。在驱动里,它最大的用处是实现“回调”和“多态”。
你想想看,如果我们要写一个I2C驱动,它可能挂载不同的设备:温度传感器、加速度计、EEPROM。每个设备的读写流程不一样,但驱动框架是一样的。这时候,函数指针就派上用场了。
// 定义一个I2C设备结构体
typedef struct {
void (*init)(void); // 设备初始化函数指针
int (*read)(uint8_t reg, uint8_t *buf, int len); // 读函数指针
int (*write)(uint8_t reg, uint8_t *buf, int len);// 写函数指针
} i2c_device_t;
// 温度传感器的具体实现
static void temp_init(void) { /* 配置温度传感器 */ }
static int temp_read(uint8_t reg, uint8_t *buf, int len) { /* 读取温度 */ }
static int temp_write(uint8_t reg, uint8_t *buf, int len) { /* 写配置 */ }
// 实例化一个温度传感器设备
i2c_device_t temp_sensor = {
.init = temp_init,
.read = temp_read,
.write = temp_write
};
// 驱动层统一调用
void i2c_driver_handle(i2c_device_t *dev) {
dev->init();
dev->read(0x00, buffer, 2);
}
这样做的好处是,驱动层代码完全不用改。换一个传感器,只需要换一个结构体实例。这就是面向对象思想在C语言里的体现。
1.5 知识体系总览
为了让大家有个整体印象,我画了张图,把今天讲的内容串起来:
1.6 寄存器映射实战:点亮一盏灯
理论说完了,咱们来点实际的。以STM32F103为例,点亮PC13引脚上的LED。整个过程就是:找到寄存器地址 → 用指针操作它。
// 1. 定义寄存器基地址和偏移
#define RCC_BASE 0x40021000
#define RCC_APB2ENR (*(volatile unsigned int *)(RCC_BASE + 0x18))
#define GPIOC_BASE 0x40011000
#define GPIOC_CRH (*(volatile unsigned int *)(GPIOC_BASE + 0x04))
#define GPIOC_ODR (*(volatile unsigned int *)(GPIOC_BASE + 0x0C))
// 2. 初始化函数
void LED_Init(void) {
// 使能GPIOC时钟(APB2总线上的外设)
RCC_APB2ENR |= (1 << 4);
// 配置PC13为推挽输出,最大速度50MHz
// CRH寄存器控制高8位引脚(Pin8~Pin15)
// PC13对应CRH的bit[23:20],设置为0010 (0x2)
GPIOC_CRH &= ~(0xF << 20); // 先清零
GPIOC_CRH |= (0x2 << 20); // 设置为推挽输出
}
// 3. 控制LED
void LED_Toggle(void) {
GPIOC_ODR ^= (1 << 13); // 翻转PC13
}