3、C语言基础复习:指针、结构体、位运算、回调函数——嵌入式开发中必须掌握的核心语法
说实话,很多刚入行的朋友问我:「嵌入式开发到底要学多深的C语言?」
我的回答很简单:指针玩得溜,结构体用得巧,位运算信手拈来,回调函数能看懂——这就够了。剩下的,都是在项目中慢慢磨出来的。
今天我就把这四个核心语法掰开揉碎了讲。嗯,都是我在实际项目中踩过的坑、总结的经验。
3.1 指针——嵌入式开发的灵魂
指针这东西,说白了就是「地址的搬运工」。你想想看,单片机里的寄存器、内存、外设地址,哪个不是通过指针来操作的?
3.1.1 指针的本质
指针就是一个变量,它存的是另一个变量的地址。就这么简单。
int a = 10;
int *p = &a; // p 存的是 a 的地址
*p = 20; // 通过 p 修改 a 的值
我在项目中遇到过最典型的场景:操作GPIO寄存器。比如STM32的GPIOB_ODR寄存器地址是0x40010C0C,你直接写:
#define GPIOB_ODR (*(volatile unsigned int *)0x40010C0C)
GPIOB_ODR |= (1 << 5); // 让PB5输出高电平
看到了吗?没有指针,你连寄存器都操作不了。
核心要点:嵌入式开发中,指针就是用来直接操作硬件地址的。别把它想得太玄乎。
3.1.2 指针与数组
数组名就是首元素的地址。这个特性在传递大量数据时特别有用。
uint8_t buffer[256];
void send_data(uint8_t *data, uint16_t len) {
for(uint16_t i = 0; i < len; i++) {
// 逐个发送 data[i]
}
}
send_data(buffer, 256); // 只传地址,不拷贝数据
我个人习惯在函数参数里用指针传递数组,而不是直接传数组本身。为什么?省内存、速度快。尤其在大数据量传输时,差别很明显。
避坑指南:我曾经在项目中犯过一个低级错误——用指针访问数组时越界了。结果程序跑着跑着就死机,查了两天才发现是野指针问题。记住:指针操作一定要确保在合法范围内。
3.1.3 函数指针
函数指针,说白了就是指向函数的指针。这在回调函数、中断服务程序里用得特别多。
void led_on(void) {
GPIOB_ODR |= (1 << 0);
}
void led_off(void) {
GPIOB_ODR &= ~(1 << 0);
}
void (*led_func)(void); // 声明一个函数指针
led_func = led_on; // 指向 led_on 函数
led_func(); // 调用 led_on()
嗯,这里要注意:函数指针的类型必须和指向的函数签名完全一致。否则编译器会报错,或者运行时出问题。
3.2 结构体——数据打包的艺术
结构体就是把多个不同类型的数据打包成一个整体。在嵌入式里,它最常见的用途就是描述硬件寄存器、协议帧、传感器数据。
3.2.1 结构体定义与使用
typedef struct {
uint8_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
} RTC_Time_t;
RTC_Time_t now = {2024, 3, 15, 10, 30, 0};
now.hour = 11; // 直接访问成员
我在项目中遇到过最经典的用法:用结构体映射硬件寄存器组。比如一个UART外设的寄存器:
typedef struct {
volatile uint32_t SR; // 状态寄存器
volatile uint32_t DR; // 数据寄存器
volatile uint32_t BRR; // 波特率寄存器
volatile uint32_t CR1; // 控制寄存器1
volatile uint32_t CR2; // 控制寄存器2
volatile uint32_t CR3; // 控制寄存器3
} USART_TypeDef;
#define USART1 ((USART_TypeDef *)0x40013800)
USART1->DR = 0x41; // 发送字符 'A'
你想想看,如果没有结构体,你得一个一个地计算偏移地址,多麻烦?
小技巧:结构体成员建议按4字节对齐,这样编译器不会在成员之间插入填充字节。如果你需要精确控制内存布局,可以用 __attribute__((packed)) 来取消对齐。
3.2.2 结构体指针
结构体指针在嵌入式里用得比结构体本身还多。为什么?因为传指针比传整个结构体快得多。
void print_time(RTC_Time_t *t) {
printf("%d-%d-%d %d:%d:%d\n",
t->year, t->month, t->day,
t->hour, t->minute, t->second);
}
print_time(&now); // 传地址,只占4字节
我个人习惯在函数参数里一律用结构体指针,除非结构体很小(比如只有一两个成员)。这样可以避免栈溢出——尤其是在中断服务程序里。
3.3 位运算——硬件操作的瑞士军刀
位运算在嵌入式里太常用了。设置寄存器、清除标志位、判断状态,都离不开它。
3.3.1 基本位运算操作
| 操作 | 运算符 | 示例 | 说明 |
|---|---|---|---|
| 按位与 | & | a & 0x0F | 保留低4位 |
| 按位或 | | | a | 0x80 | 置位第7位 |
| 按位异或 | ^ | a ^ 0xFF | 取反所有位 |
| 左移 | << | 1 << 3 | 得到0x08 |
| 右移 | >> | 0x80 >> 4 | 得到0x08 |
3.3.2 常用位运算技巧
// 置位某一位
REG |= (1 << 5); // 置位第5位
// 清零某一位
REG &= ~(1 << 5); // 清零第5位
// 判断某一位是否为1
if(REG & (1 << 5)) {
// 第5位为1
}
// 翻转某一位
REG ^= (1 << 5); // 翻转第5位
// 提取连续位
uint8_t value = (REG >> 4) & 0x07; // 提取第4-6位
核心要点:位运算的优先级容易搞混。我建议:拿不准就加括号。比如 REG & ~(1 << 5) 和 REG & ~1 << 5 结果完全不同。
我曾经在调试一个I2C驱动时,因为位运算优先级问题,折腾了整整一个下午。从那以后,我写位运算必加括号。
3.4 回调函数——解耦的利器
回调函数,说白了就是「你告诉我做什么,我到时候叫你」。在嵌入式里,它最常见的场景就是中断处理、定时器回调、协议栈事件处理。
3.4.1 回调函数的基本模式
// 定义回调函数类型
typedef void (*callback_t)(void *arg);
// 注册回调
void register_callback(callback_t cb, void *arg) {
// 保存回调函数指针和参数
g_callback = cb;
g_callback_arg = arg;
}
// 触发回调
void trigger_callback(void) {
if(g_callback != NULL) {
g_callback(g_callback_arg);
}
}
// 使用示例
void my_handler(void *arg) {
uint32_t *data = (uint32_t *)arg;
printf("Data: %d\n", *data);
}
uint32_t value = 100;
register_callback(my_handler, &value);
嗯,这里要注意:回调函数里不能做耗时操作。为什么?因为回调通常是在中断上下文里执行的,时间长了会影响系统实时性。
3.4.2 回调函数的实际应用
我在项目中用过最典型的回调:按键检测。按下按键时执行某个功能,松开时执行另一个功能。
typedef struct {
void (*press_cb)(void);
void (*release_cb)(void);
} Button_t;
Button_t btn = {
.press_cb = led_on,
.release_cb = led_off
};
// 在中断里调用
void EXTI0_IRQHandler(void) {
if(btn.press_cb != NULL) btn.press_cb();
}
小技巧:回调函数的参数建议用 void *,这样灵活性最高。你可以在回调里把它转换成任何类型。但要注意类型安全——我曾经因为类型转换错误,导致程序跑飞。
3.5 综合示例:用结构体+指针+位运算+回调实现LED控制
最后,我把这四个知识点串起来,写一个完整的例子。这个例子在阿里云IoT设备接入里很常见——控制一个LED灯。
#include <stdint.h>
// 1. 结构体:描述LED
typedef struct {
volatile uint32_t *port; // GPIO端口基地址
uint8_t pin; // 引脚号
void (*on_cb)(void); // 开灯回调
void (*off_cb)(void); // 关灯回调
} LED_t;
// 2. 位运算:控制LED
void led_set(LED_t *led, uint8_t state) {
if(state) {
*led->port |= (1 << led->pin); // 置位
if(led->on_cb) led->on_cb();
} else {
*led->port &= ~(1 << led->pin); // 清零
if(led->off_cb) led->off_cb();
}
}
// 3. 回调函数
void led_on_handler(void) {
printf("LED ON\n");
}
void led_off_handler(void) {
printf("LED OFF\n");
}
// 4. 使用
int main(void) {
// 假设GPIOB基地址为0x40010C0C
LED_t led = {
.port = (volatile uint32_t *)0x40010C0C,
.pin = 5,
.on_cb = led_on_handler,
.off_cb = led_off_handler
};
led_set(&led, 1); // 开灯
led_set(&led, 0); // 关灯
return 0;
}
你看,指针、结构体、位运算、回调函数,这四个知识点在这个例子里全用上了。说白了,嵌入式开发就是这些东西的组合。
最后提醒一句:我见过太多人把C语言学得很深,但一到实际项目就懵了。其实嵌入式C语言不需要你掌握所有语法,把这四个核心玩透,90%的场景都能应付。剩下的,遇到再查也不迟。
下一章,我会带你用这些知识,真正上手操作一个STM32的GPIO。到时候你会发现——原来硬件控制就这么简单。