4. 数据采集基础:传感器数据读取方式

数据采集是整个标定流程的起点。说白了,你后面所有的算法、模型、补偿曲线,都建立在原始数据之上。如果这一步没做好,后面全是白搭。

我个人习惯把数据采集拆成三个核心问题:怎么读?怎么拆?怎么对齐? 今天我们就一个一个来啃。

4.1 三种主流读取方式:串口、I2C、SPI

做嵌入式这么多年,我接触过的传感器接口无非就这三种。每种都有它的脾气,选错了接口,轻则丢数据,重则整个系统跑飞。

4.1.1 串口(UART)—— 最灵活,但最慢

串口的好处是简单。两根线(TX、RX),一收一发,全双工。我在项目中遇到过用串口接温湿度传感器的场景,波特率设到115200,基本够用。

但要注意:串口没有时钟线,收发双方得提前约定好波特率。一旦对不上,收到的就是乱码。

核心参数: 波特率、数据位(通常8位)、停止位(1位)、校验位(可选)
// 串口初始化示例(STM32 HAL库)
UART_HandleTypeDef huart1;
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
HAL_UART_Init(&huart1);

// 接收一帧数据(中断方式)
uint8_t rx_buffer[64];
HAL_UART_Receive_IT(&huart1, rx_buffer, 10);

4.1.2 I2C —— 多设备,两根线搞定

I2C用两根线(SCL、SDA)挂载多个设备,每个设备有独立地址。嗯,这里要注意:地址冲突是我踩过最多的坑。曾经有一次,两个传感器用了同一个地址,结果读出来的数据乱七八糟,排查了整整一下午。

I2C是半双工,速度一般(标准模式100kHz,快速模式400kHz)。适合读取配置寄存器、慢速传感器(如温度、湿度)。

// I2C读取传感器数据示例
uint8_t reg_addr = 0x00;  // 寄存器地址
uint8_t data[2];

HAL_I2C_Master_Transmit(&hi2c1, (SENSOR_ADDR << 1), ®_addr, 1, 100);
HAL_I2C_Master_Receive(&hi2c1, (SENSOR_ADDR << 1), data, 2, 100);

// data[0]和data[1]就是传感器原始值

4.1.3 SPI —— 快,但线多

SPI是速度之王。四根线(MOSI、MISO、SCLK、CS),全双工,速率轻松上MHz。我做过一个加速度计标定项目,用SPI读取数据,采样率能跑到10kHz以上。

但代价是:每多一个设备,就得多一根片选线。板子空间紧张的时候,这很要命。

我的选择原则:
- 低速、多设备 → I2C
- 高速、少设备 → SPI
- 远距离、简单协议 → 串口

4.2 数据帧解析 —— 从字节到物理量

传感器吐出来的是一堆字节,你得知道每个字节代表什么。这就是数据帧解析。

我曾经接手过一个项目,前任工程师把帧格式文档弄丢了。我对着示波器一比特一比特地猜,那种感觉……嗯,不想再来第二次。

4.2.1 常见帧结构

字段 长度 说明
帧头 1-2字节 固定值,如0xAA 0x55
数据长度 1字节 有效数据字节数
有效数据 N字节 传感器原始值
校验 1-2字节 CRC或累加和
帧尾 1字节 固定值,如0x0D 0x0A
// 帧解析伪代码
uint8_t parse_frame(uint8_t *buffer, int len) {
    // 1. 找帧头
    if (buffer[0] != 0xAA || buffer[1] != 0x55) return ERROR;
    
    // 2. 取数据长度
    int data_len = buffer[2];
    
    // 3. 校验
    uint8_t checksum = 0;
    for (int i = 0; i < 3 + data_len; i++) {
        checksum += buffer[i];
    }
    if (checksum != buffer[3 + data_len]) return ERROR;
    
    // 4. 提取有效数据
    memcpy(sensor_data, &buffer[3], data_len);
    
    return OK;
}
避坑指南: 我曾经遇到过帧头出现在有效数据中的情况。解决方案是:帧头用两个字节,并且确保有效数据中不会出现相同的组合。或者用转义字符。

4.3 时间戳对齐 —— 数据的时间灵魂

数据没有时间戳,就像人没有身份证。你只知道某个时刻传感器读数是100,但不知道是1秒前还是1分钟前,那这个数据就没法用。

4.3.1 为什么需要时间戳对齐?

多传感器融合时,每个传感器的采样时刻可能不同。比如IMU是100Hz,GPS是10Hz,摄像头是30Hz。如果不做时间戳对齐,你拿到的数据就是「时空错乱」的。

4.3.2 两种对齐策略

策略一:硬件时间戳

在传感器数据帧到达MCU时,立即记录当前系统时间。这是最准的方法。我习惯在中断服务函数里做这件事:

// 串口接收中断中打时间戳
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        uint32_t timestamp = HAL_GetTick();  // 毫秒级时间戳
        save_data_with_timestamp(rx_buffer, timestamp);
    }
}

策略二:软件插值对齐

如果硬件不支持,就用软件插值。比如两个传感器采样率不同,把慢速传感器的数据通过线性插值,对齐到快速传感器的时间轴上。

// 线性插值示例
float interpolate(float t, float t0, float v0, float t1, float v1) {
    return v0 + (v1 - v0) * (t - t0) / (t1 - t0);
}
我的经验: 能上硬件时间戳就别用软件。软件插值会引入误差,尤其是在传感器数据变化剧烈的时候。我曾经在无人机项目上吃过这个亏,插值后的数据导致姿态解算发散。

4.4 本章知识体系

下面这张图总结了我们刚才讲的内容。你可以把它当作数据采集的「路线图」:

数据采集基础:知识体系 传感器数据读取方式 串口 (UART) I2C SPI 数据帧解析 帧头/帧尾/校验 有效数据提取 字节序/位操作 时间戳对齐 硬件时间戳(推荐) 软件插值对齐

数据采集这件事,说难不难,说简单也不简单。关键是把每个环节想清楚:用什么接口读?读到的字节怎么拆?拆出来的数据怎么打上时间戳?

我个人建议,在开始写代码之前,先把这三个问题画在纸上。想清楚了再动手,能省掉后面80%的调试时间。

可可精品资料库