第3章:传感器数据采集——加速度计、陀螺仪与CAN总线解析

大家好,我是你们的嵌入式讲师。今天咱们聊聊黑匣子最核心的一环——数据采集。

说白了,黑匣子就是个“超级记录仪”。它得把车子的各种状态忠实地记下来。加速度计、陀螺仪、CAN总线,这三样东西,就是它的“眼睛”和“耳朵”。

我个人习惯把这一章分成三块来讲:加速度计驱动陀螺仪数据读取CAN总线解析。咱们一个一个来。

第3章:传感器数据采集知识体系 加速度计驱动 SPI / I2C 接口 初始化配置 数据读取与校验 量程与滤波 陀螺仪数据读取 角速度测量 零偏校准 温度补偿 数据融合基础 CAN总线解析 车速信号提取 转向角解析 刹车状态判断 DBC规则应用 数据融合 时间同步

3.1 加速度计驱动开发(SPI/I2C接口)

加速度计,测的是“加速度”。别小看它,碰撞瞬间的冲击、急刹车、颠簸路面,全靠它记录。

市面上常见的加速度计芯片,比如ADXL345、MPU6050(带陀螺仪),接口无非两种:SPII2C

我个人更偏爱SPI。为什么?速度快,而且没有I2C那种“从机地址冲突”的麻烦。我在项目中遇到过好几次,I2C总线上挂了两颗同样地址的传感器,结果死活读不到数据。后来全改成SPI了,清静。

3.1.1 SPI接口驱动要点

SPI驱动,说白了就是四根线:CS、SCLK、MOSI、MISO。写驱动时,我习惯先搞定底层读写函数:

// SPI 读写一个字节
uint8_t spi_read_write(uint8_t data) {
    // 等待发送缓冲区空
    while(!(SPI->SR & SPI_SR_TXE));
    SPI->DR = data;
    // 等待接收缓冲区非空
    while(!(SPI->SR & SPI_SR_RXNE));
    return SPI->DR;
}

// 读取加速度计寄存器
uint8_t accel_read_reg(uint8_t reg) {
    uint8_t val;
    // CS 拉低
    GPIO_ResetBits(CS_PORT, CS_PIN);
    // 发送读命令(通常最高位置1)
    spi_read_write(reg | 0x80);
    // 读取数据
    val = spi_read_write(0x00);
    // CS 拉高
    GPIO_SetBits(CS_PORT, CS_PIN);
    return val;
}
小技巧: 很多加速度计芯片要求读取寄存器时,先发送“读命令”(寄存器地址 | 0x80)。写操作则不需要。这个细节容易忘,我当年就因为这个浪费了半天。

3.1.2 I2C接口驱动要点

I2C驱动,两根线:SCL、SDA。代码量比SPI大一点,但胜在引脚少。

// I2C 读取加速度计数据
HAL_StatusTypeDef accel_i2c_read(uint8_t dev_addr, uint8_t reg, uint8_t *data, uint16_t len) {
    // 发送设备地址 + 写位
    // 发送寄存器地址
    // 重新发送起始条件
    // 发送设备地址 + 读位
    // 读取数据
    // 发送停止条件
    return HAL_I2C_Mem_Read(&hi2c1, dev_addr, reg, I2C_MEMADD_SIZE_8BIT, data, len, 100);
}
注意: I2C的从机地址通常是7位或10位。比如ADXL345的7位地址是0x53(默认)。但有些开发板会把地址左移一位,变成0xA6。这个坑我踩过,读出来的数据全是0xFF,还以为是芯片坏了。

3.2 陀螺仪数据读取

陀螺仪测的是“角速度”。车子转弯、侧倾、翻滚,都靠它感知。

你想想看,如果只有加速度计,车子在匀速转弯时,加速度计是测不出转向的。但陀螺仪可以。所以黑匣子里,加速度计和陀螺仪通常是成对出现的。

3.2.1 原始数据读取

以MPU6050为例,它内部集成了加速度计和陀螺仪。读取陀螺仪数据的流程很简单:

// 初始化MPU6050
void mpu6050_init(void) {
    // 唤醒芯片(写0x00到电源管理寄存器)
    i2c_write_reg(MPU6050_ADDR, 0x6B, 0x00);
    // 设置陀螺仪量程为±2000°/s
    i2c_write_reg(MPU6050_ADDR, 0x1B, 0x18);
    // 设置加速度计量程为±16g
    i2c_write_reg(MPU6050_ADDR, 0x1C, 0x18);
}

// 读取陀螺仪数据(三轴)
void mpu6050_read_gyro(int16_t *gx, int16_t *gy, int16_t *gz) {
    uint8_t buf[6];
    i2c_read_regs(MPU6050_ADDR, 0x43, buf, 6);
    *gx = (buf[0] << 8) | buf[1];
    *gy = (buf[2] << 8) | buf[3];
    *gz = (buf[4] << 8) | buf[5];
}
关键点: 原始数据是16位有符号整数。要转换成实际的角速度(°/s),需要除以灵敏度。比如±2000°/s量程下,灵敏度是16.4 LSB/°/s。所以:
实际角速度 = 原始值 / 16.4

3.2.2 零偏校准

陀螺仪有个“通病”——静止时输出不为零。这叫零偏。

我曾经在一个项目中,没做零偏校准,结果车子停着的时候,黑匣子记录的数据显示车子在“缓慢自转”。你说这数据谁敢用?

解决办法很简单:上电后,让车子静止几秒钟,采集100组数据取平均,作为零偏值。之后每次读数都减去这个值。

// 零偏校准
int16_t gyro_offset[3] = {0};
void gyro_calibrate(void) {
    int32_t sum[3] = {0};
    for(int i = 0; i < 100; i++) {
        int16_t gx, gy, gz;
        mpu6050_read_gyro(&gx, &gy, &gz);
        sum[0] += gx;
        sum[1] += gy;
        sum[2] += gz;
        delay_ms(10);
    }
    gyro_offset[0] = sum[0] / 100;
    gyro_offset[1] = sum[1] / 100;
    gyro_offset[2] = sum[2] / 100;
}

3.3 CAN总线数据解析

CAN总线,是汽车的中枢神经。车速、转向角、刹车状态,这些关键信号都在上面跑。

黑匣子要做的,就是“听”总线上的数据,然后解析出来。嗯,这里要注意:CAN总线上的数据是“广播”的,谁都可以收。但你要知道每个信号在哪个ID、哪个字节、哪个位。

3.3.1 车速信号提取

车速信号,通常是一个16位无符号整数,单位是km/h。比如某车型的DBC定义:

信号名 CAN ID 起始位 长度 因子 偏移
VehicleSpeed 0x1A1 8 16 0.01 0
SteeringAngle 0x1A2 0 16 0.1 -500
BrakeStatus 0x1A3 0 1 1 0

解析代码很简单:

// CAN消息接收回调
void CAN_Receive_Callback(uint32_t id, uint8_t *data, uint8_t len) {
    if(id == 0x1A1) {
        // 车速:起始位8,长度16,小端模式
        uint16_t raw = (data[1] << 8) | data[2];
        float speed = raw * 0.01f;  // 单位 km/h
        // 存储到黑匣子缓冲区
    }
    else if(id == 0x1A2) {
        // 转向角:起始位0,长度16
        uint16_t raw = (data[0] << 8) | data[1];
        float angle = raw * 0.1f - 500.0f;  // 单位 度
    }
    else if(id == 0x1A3) {
        // 刹车状态:起始位0,长度1
        uint8_t brake = data[0] & 0x01;
        // 1表示刹车,0表示未刹车
    }
}
避坑指南: 我曾经遇到过一个问题——车速信号解析出来总是偏大。查了半天,发现是字节序搞反了。CAN总线数据通常是“大端模式”,但有些ECU会用“小端模式”。一定要看DBC文件里的定义,别想当然。

3.3.2 转向角与刹车状态

转向角信号,通常范围是-780°到+780°(方向盘转两圈多)。解析时注意偏移量。

刹车状态,一般就一个bit。但有些车会有“刹车踏板行程”信号,是个模拟量。黑匣子最好都记录下来,方便事故分析。

我的习惯: 所有CAN信号解析出来后,我都会加一个“合理性检查”。比如车速不能超过300km/h,转向角不能超过800°。如果超出范围,就标记为“无效数据”。这样能过滤掉一些总线上的干扰。

3.4 数据融合与时间戳

加速度计、陀螺仪、CAN数据,它们的采样频率可能不一样。加速度计和陀螺仪可以跑到1kHz,CAN数据通常100Hz左右。

黑匣子记录时,必须给每一帧数据打上统一的时间戳。我建议用微秒级的时间戳,从系统上电开始计数。

// 数据帧结构体
typedef struct {
    uint32_t timestamp_us;  // 时间戳
    uint8_t  sensor_type;   // 0:加速度, 1:陀螺仪, 2:CAN
    union {
        struct { float ax, ay, az; } accel;
        struct { float gx, gy, gz; } gyro;
        struct { float speed, angle; uint8_t brake; } can;
    } data;
} blackbox_frame_t;

这样,事后分析时,就能精确还原事故前几秒的车辆状态。嗯,这才是黑匣子的价值所在。


好了,这一章的内容就到这里。传感器数据采集,是黑匣子的根基。驱动写不好,后面全是白搭。下一章咱们聊聊数据存储——怎么把海量数据高效地写进Flash里。