2、嵌入式C语言基础回顾:指针、结构体、联合体、位域在固件中的应用

各位同学,咱们直接进入正题。这一章我打算带大家快速过一遍嵌入式C语言里几个最核心、也最容易出坑的概念——指针、结构体、联合体和位域。说实话,这些基础要是没打牢,后面写传感器固件状态机的时候,你会发现自己处处碰壁。

我个人习惯是,每接手一个新项目,第一件事就是先把底层数据结构的C语言功底夯实。你想想看,传感器芯片就那么点RAM和Flash,指针用不好,内存泄漏;结构体设计不合理,代码可读性差;联合体和位域不会用,寄存器操作能把你逼疯。好,咱们一个一个来。

2.1 指针:固件世界的“遥控器”

指针是什么?说白了,它就是变量的地址。在固件里,我们经常要直接操作硬件寄存器,这时候指针就是你的“遥控器”。

核心要点:指针变量存储的是内存地址,通过它可以间接访问该地址上的数据。

我在项目中遇到过一个问题:一个同事用指针操作DMA缓冲区,结果因为指针类型不匹配,导致数据错位,整个采集链路都乱了。嗯,这里要注意,指针的类型决定了它一次能“看”多少字节。

// 指针声明与使用示例
uint8_t data = 0xAB;
uint8_t *ptr = &data;  // ptr指向data的地址

// 通过指针修改数据
*ptr = 0xCD;  // 现在data的值变成了0xCD

// 指针运算:在固件中常用于遍历数组
uint32_t buffer[10];
uint32_t *p = buffer;
for(int i = 0; i < 10; i++) {
    *(p + i) = i * 2;  // 等价于 p[i] = i * 2
}

个人经验:我建议你在固件中尽量使用 uint32_t *uint8_t * 这类明确宽度的指针类型,别用 int *。不同编译器对 int 的解释可能不一样,移植性会出问题。

2.2 结构体:把数据“打包”管理

结构体就是把多个不同类型的数据组合成一个整体。在传感器固件里,一个传感器的配置参数、校准系数、状态标志,用结构体来管理再合适不过了。

// 传感器数据结构体定义
typedef struct {
    uint16_t raw_adc_value;    // 原始ADC值
    int32_t  temperature;      // 温度值,单位0.01°C
    uint8_t  status_flags;     // 状态标志位
    uint32_t timestamp;        // 时间戳
} sensor_data_t;

// 使用结构体
sensor_data_t sensor;
sensor.raw_adc_value = 0x0FFF;
sensor.temperature = 2567;  // 表示25.67°C

你可能会问,为什么不用一堆单独的变量?我告诉你,用结构体最大的好处是——你可以把整个传感器数据作为一个整体来传递。比如通过指针传参,效率极高。

// 通过指针传递结构体,避免拷贝
void process_sensor_data(sensor_data_t *data) {
    // 直接操作原始数据
    data->temperature = data->raw_adc_value * 100 / 4096;
}

避坑指南:我曾经因为结构体字节对齐问题,在I2C通信中吃了大亏。结构体成员之间可能有填充字节,导致你发送的数据和接收方解析的不一致。解决办法是使用 __attribute__((packed))#pragma pack(1) 来强制1字节对齐。

// 使用 packed 属性避免对齐填充
typedef struct __attribute__((packed)) {
    uint8_t  id;
    uint16_t value;
    uint32_t checksum;
} packed_sensor_frame_t;

2.3 联合体:同一块内存的“多副面孔”

联合体(union)允许你在同一块内存地址上存储不同类型的数据。说白了,就是同一块内存,你可以把它当uint32_t看,也可以把它当四个uint8_t看。

在传感器固件中,联合体最常见的用途就是解析通信协议帧。我记得有一次调试一个SPI接口的陀螺仪,数据手册上说寄存器是16位的,但实际读回来高低字节顺序和手册不一样。用联合体一拆,问题立马就定位了。

// 联合体:用于解析字节序或拆分数据
typedef union {
    uint32_t full_value;       // 当作32位整数
    uint8_t  bytes[4];         // 当作4个字节
    struct {
        uint16_t low_word;     // 低16位
        uint16_t high_word;    // 高16位
    } words;
} register_data_t;

// 实际应用:读取传感器寄存器
register_data_t reg;
reg.full_value = spi_read_register(0x10);  // 读取32位寄存器

// 直接访问各个字节
uint8_t lsb = reg.bytes[0];   // 最低有效字节
uint8_t msb = reg.bytes[3];   // 最高有效字节

关键点:联合体的大小等于其最大成员的大小。所有成员共享同一块内存起始地址。修改一个成员,其他成员的值也会跟着变(因为数据被覆盖了)。

2.4 位域:精确控制每一位

位域是C语言里一个非常“嵌入式”的特性。它允许你以位为单位来定义结构体成员。在传感器芯片中,寄存器通常只有16位或32位,每一位或每几位代表不同的控制功能。用位域来操作,代码可读性直接拉满。

// 传感器控制寄存器位域定义
typedef struct {
    uint32_t power_mode  : 2;   // bit0-1: 电源模式
    uint32_t range       : 3;   // bit2-4: 量程选择
    uint32_t filter_bw   : 2;   // bit5-6: 滤波器带宽
    uint32_t reserved    : 1;   // bit7:   保留位
    uint32_t int_enable  : 1;   // bit8:   中断使能
    uint32_t data_ready  : 1;   // bit9:   数据就绪标志
    uint32_t             : 22;  // bit10-31: 填充
} ctrl_reg_t;

// 使用位域操作寄存器
ctrl_reg_t ctrl;
ctrl.power_mode = 0x01;   // 设置为正常模式
ctrl.range = 0x02;        // 设置量程为±4g
ctrl.int_enable = 1;      // 使能中断

// 将位域结构体写入硬件寄存器
*(volatile uint32_t *)0x40002000 = *(uint32_t *)&ctrl;

注意:位域的布局(哪个位对应哪个成员)是编译器相关的。不同编译器可能从低位到高位,也可能从高位到低位。我曾经在移植代码时,就因为位域顺序问题,导致传感器初始化失败。解决办法是:在代码注释中明确标注位域对应的硬件位号,或者干脆用宏定义+移位操作来替代位域。

2.5 综合应用:传感器状态机中的数据结构

好了,咱们把上面这些知识点串起来。在传感器固件中,状态机需要管理各种状态和参数。下面是一个典型的状态机数据结构设计:

// 传感器状态枚举
typedef enum {
    STATE_INIT,       // 初始化
    STATE_IDLE,       // 空闲
    STATE_MEASURE,    // 测量中
    STATE_SLEEP,      // 休眠
    STATE_ERROR       // 错误
} sensor_state_t;

// 传感器配置参数(使用位域)
typedef struct {
    uint32_t sample_rate : 4;   // 采样率配置
    uint32_t resolution  : 2;   // 分辨率配置
    uint32_t filter_type : 2;   // 滤波器类型
    uint32_t             : 24;  // 保留
} sensor_config_t;

// 传感器完整状态(使用联合体解析寄存器)
typedef struct {
    sensor_state_t  current_state;     // 当前状态
    sensor_config_t config;            // 配置参数
    union {
        uint32_t status_reg;           // 状态寄存器原始值
        struct {
            uint32_t data_ready : 1;   // 数据就绪
            uint32_t over_range : 1;   // 超量程
            uint32_t error_flag : 1;   // 错误标志
            uint32_t            : 29;  // 保留
        } bits;
    } status;
    uint32_t (*read_func)(uint32_t reg);  // 函数指针:读取寄存器
} sensor_handle_t;

// 使用示例
sensor_handle_t sensor = {
    .current_state = STATE_INIT,
    .config.sample_rate = 0x05,   // 100Hz采样
    .read_func = spi_read_register
};

// 状态机中检查数据就绪
if(sensor.status.bits.data_ready) {
    // 读取数据...
    sensor.current_state = STATE_MEASURE;
}

我的建议:在固件中,尽量把硬件相关的操作(寄存器地址、位定义)封装在底层驱动中。上层状态机只通过结构体和函数指针来访问硬件。这样,换芯片型号时,你只需要改底层驱动,状态机逻辑基本不用动。这个习惯让我少加了多少班,你想想看。

好,这一章的内容就到这里。指针、结构体、联合体、位域,这四个东西在传感器固件里几乎天天用。你如果能把它们用熟、用对,后面写状态机的时候就会感觉“下笔如有神”。下一章咱们开始正式进入状态机设计,到时候这些数据结构都会派上大用场。