第3章:传感器数据采集——加速度计、陀螺仪与CAN总线解析
大家好,我是你们的嵌入式讲师。今天咱们聊聊黑匣子最核心的一环——数据采集。
说白了,黑匣子就是个“超级记录仪”。它得把车子的各种状态忠实地记下来。加速度计、陀螺仪、CAN总线,这三样东西,就是它的“眼睛”和“耳朵”。
我个人习惯把这一章分成三块来讲:加速度计驱动、陀螺仪数据读取、CAN总线解析。咱们一个一个来。
3.1 加速度计驱动开发(SPI/I2C接口)
加速度计,测的是“加速度”。别小看它,碰撞瞬间的冲击、急刹车、颠簸路面,全靠它记录。
市面上常见的加速度计芯片,比如ADXL345、MPU6050(带陀螺仪),接口无非两种:SPI 和 I2C。
我个人更偏爱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;
}
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);
}
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.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表示未刹车
}
}
3.3.2 转向角与刹车状态
转向角信号,通常范围是-780°到+780°(方向盘转两圈多)。解析时注意偏移量。
刹车状态,一般就一个bit。但有些车会有“刹车踏板行程”信号,是个模拟量。黑匣子最好都记录下来,方便事故分析。
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里。