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。到时候你会发现——原来硬件控制就这么简单。