4、数据采集软件框架:RTOS基础、传感器驱动与时间戳
这一章,我们聊聊无人机数据采集的软件骨架。说白了,就是怎么让飞控板上的代码有条不紊地干活,同时把传感器数据整得明明白白。
我见过不少团队,硬件选型挺牛,传感器也高级,但软件架构一塌糊涂。结果呢?数据乱跳,时间对不上,飞起来心里发虚。嗯,咱们今天就把这块地基打牢。
4.1 实时操作系统(RTOS)基础
无人机不是手机。你不能让一个高优先级的任务被低优先级的任务堵住。比如,电机控制必须准时,GPS数据晚来几毫秒可能问题不大,但姿态控制要是晚了——炸机。
所以,我们需要RTOS。它跟Windows、Linux那种“大家排队等”的思路不一样。RTOS的核心是:谁急谁先跑。
RTOS在无人机中的三大作用:
- 任务调度:按优先级分配CPU时间,确保关键任务不被饿死。
- 中断管理:传感器数据来了,立刻响应,不拖沓。
- 资源同步:多个任务同时访问同一个数据时,不出乱子。
我个人习惯用FreeRTOS,轻量、开源、资料多。当然,也有用uC/OS、RT-Thread的。选哪个不重要,关键是理解它的调度机制。
4.1.1 任务优先级与调度策略
RTOS里,每个任务都有一个优先级。数值越小,优先级越高(或者反过来,看具体实现)。调度器会保证:高优先级任务就绪时,低优先级任务立刻让路。
举个例子,我做过一个四旋翼项目,任务优先级是这样分配的:
| 任务名称 | 优先级 | 执行频率 | 说明 |
|---|---|---|---|
| 姿态控制 | 最高 | 1 kHz | 电机PWM输出,延迟不能超过1ms |
| 传感器读取 | 高 | 1 kHz | IMU数据采集,与姿态控制同步 |
| GPS处理 | 中 | 10 Hz | 位置更新,可以容忍几十毫秒延迟 |
| 数据记录 | 低 | 100 Hz | 写SD卡,慢一点没关系 |
你看,姿态控制和传感器读取绑在一起,优先级最高。GPS慢一点没关系,数据记录最不着急。这样设计,飞控的实时性就有保障了。
小技巧: 我曾经踩过一个坑——把传感器读取和姿态控制放在同一个任务里。结果传感器初始化卡了一下,姿态控制直接超时。后来我改成两个独立任务,用队列传递数据,问题就解决了。
4.1.2 任务间通信:队列与信号量
RTOS里,任务之间不能直接访问对方的变量。为啥?因为可能同时读写,数据就乱了。我们需要用队列(Queue)或信号量(Semaphore)来通信。
- 队列:一个任务往里面放数据,另一个任务取数据。先进先出,适合传感器数据流。
- 信号量:一个任务发个“我干完了”的信号,另一个任务收到后开始干活。适合同步。
你想想看,IMU数据每秒1000次往队列里塞,姿态控制任务每秒1000次从队列里取。只要队列深度够,就不会丢数据。
// FreeRTOS 队列示例
QueueHandle_t imuQueue;
// 传感器读取任务
void vSensorTask(void *pvParameters) {
imu_data_t data;
while(1) {
read_imu(&data);
xQueueSend(imuQueue, &data, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(1)); // 1ms周期
}
}
// 姿态控制任务
void vControlTask(void *pvParameters) {
imu_data_t data;
while(1) {
if(xQueueReceive(imuQueue, &data, portMAX_DELAY) == pdTRUE) {
attitude_control(&data);
}
}
}
嗯,这里要注意:队列的等待时间别设成无限阻塞(portMAX_DELAY),万一传感器挂了,整个任务就卡死了。我一般设个超时,比如10ms,超时了就报错。
4.2 传感器驱动开发
传感器驱动,说白了就是让MCU跟传感器芯片“说话”。常见的接口有I2C、SPI、UART。无人机里,IMU(MPU6050、BMI088)多用SPI或I2C,气压计(MS5611)用I2C,GPS用UART。
我开发驱动时,习惯分三层:
- 硬件抽象层(HAL):封装MCU的SPI/I2C读写函数。
- 芯片驱动层:实现传感器的寄存器读写、初始化、数据读取。
- 应用接口层:提供校准、滤波、单位转换等高级功能。
这样做的好处是,换MCU时只改HAL层,芯片驱动不用动。
4.2.1 驱动开发的关键步骤
以BMI088(6轴IMU)为例,我讲讲驱动开发的流程:
- 第一步:读数据手册。别偷懒。搞清楚寄存器地址、通信时序、初始化序列。
- 第二步:实现读写函数。用SPI或I2C读写单个寄存器。
- 第三步:验证芯片ID。读WHO_AM_I寄存器,确认通信正常。
- 第四步:配置传感器。设置量程、滤波器带宽、输出数据速率。
- 第五步:读取数据。连续读取加速度和角速度寄存器。
避坑指南: 我曾经在BMI088驱动上栽过跟头。数据手册说SPI模式是“模式0”,但我没注意它要求时钟极性是“空闲低电平”。结果读出来的数据全是0xFF。查了两天才发现是SPI模式配错了。所以,一定要用逻辑分析仪抓波形,确认时序对不对。
4.2.2 驱动代码示例(BMI088 SPI)
// BMI088 加速度计初始化
uint8_t bmi088_accel_init(void) {
uint8_t chip_id;
// 1. 读取芯片ID
spi_read_reg(BMI088_ACCEL_CHIP_ID_ADDR, &chip_id, 1);
if(chip_id != BMI088_ACCEL_CHIP_ID) {
return BMI088_ERR_CHIP_ID;
}
// 2. 软复位
spi_write_reg(BMI088_ACCEL_SOFTRESET_ADDR, 0xB6);
vTaskDelay(pdMS_TO_TICKS(10));
// 3. 配置量程 ±24g,ODR 1600Hz
spi_write_reg(BMI088_ACCEL_RANGE_ADDR, 0x03); // ±24g
spi_write_reg(BMI088_ACCEL_ODR_ADDR, 0xAC); // 1600Hz
// 4. 使能传感器
spi_write_reg(BMI088_ACCEL_PWR_CTRL_ADDR, 0x04);
return BMI088_OK;
}
// 读取加速度数据
void bmi088_accel_read(float *accel_x, float *accel_y, float *accel_z) {
uint8_t buf[6];
int16_t raw[3];
// 连续读取6个字节(X、Y、Z各2字节)
spi_read_reg(BMI088_ACCEL_X_LSB_ADDR, buf, 6);
// 合并为16位有符号数
raw[0] = (int16_t)(buf[1] << 8 | buf[0]);
raw[1] = (int16_t)(buf[3] << 8 | buf[2]);
raw[2] = (int16_t)(buf[5] << 8 | buf[4]);
// 转换为物理单位(m/s²),±24g量程下 LSB = 0.001464 g
*accel_x = raw[0] * 0.001464f * 9.80665f;
*accel_y = raw[1] * 0.001464f * 9.80665f;
*accel_z = raw[2] * 0.001464f * 9.80665f;
}
4.3 数据同步与时间戳
这是数据采集里最容易被忽视、但后果最严重的问题。你想想看,IMU数据是1000Hz,GPS是10Hz,气压计是100Hz。如果每个数据的时间基准不一样,融合出来的姿态和位置就是错的。
我见过一个案例:某团队做无人机测绘,拍的照片和GPS时间戳差了200ms。结果生成的3D模型全是歪的。嗯,这就是时间不同步的代价。
4.3.1 时间戳的生成策略
在RTOS环境下,时间戳通常用系统滴答定时器(SysTick)生成。每个数据读取时,记录当前的tick值,换算成微秒或毫秒。
我推荐的做法是:
- 统一时间基准:所有传感器都用同一个定时器计数。
- 在中断里打时间戳:数据就绪中断触发时,立刻记录时间,而不是等任务处理时再记。
- 时间戳与数据绑定:把时间戳和传感器数据打包成一个结构体,一起传递。
// 带时间戳的IMU数据结构体
typedef struct {
uint64_t timestamp_us; // 微秒级时间戳
float accel_x;
float accel_y;
float accel_z;
float gyro_x;
float gyro_y;
float gyro_z;
} imu_data_ts_t;
// SPI中断服务函数(假设数据就绪引脚触发中断)
void EXTI_IRQHandler(void) {
uint64_t now = get_system_us(); // 立即记录时间
imu_data_ts_t data;
data.timestamp_us = now;
// 读取传感器数据
bmi088_accel_read(&data.accel_x, &data.accel_y, &data.accel_z);
bmi088_gyro_read(&data.gyro_x, &data.gyro_y, &data.gyro_z);
// 放入队列
xQueueSendFromISR(imuQueue, &data, NULL);
}
经验之谈: 我习惯用64位微秒时间戳。32位毫秒时间戳在49天后会溢出,无人机飞不了那么久,但万一你跑个长航时测试呢?64位微秒可以跑到几十万年,省心。
4.3.2 多传感器时间对齐
不同传感器的数据速率不一样,怎么对齐?我常用的方法是:
- 插值法:以最高频率的传感器(通常是IMU)为基准,对其他传感器数据做线性插值,算出它在同一时刻的值。
- 最近邻法:找到时间戳最接近的那个数据,直接拿来用。简单,但精度稍差。
- 硬件同步:用MCU的定时器输出PWM,同时触发多个传感器的数据采集。这是最准的,但硬件上要支持。
我个人偏好插值法。精度够,实现也不复杂。你想想看,GPS是10Hz,两个数据之间隔了100ms。IMU在这100ms里跑了100个数据。用IMU的时间戳做基准,对GPS位置做线性插值,就能得到每个IMU时刻对应的位置。
4.3.3 时间同步的常见坑
我曾经踩过的坑:
- 中断优先级:时间戳记录的中断优先级必须最高。否则,高优先级中断嵌套时,时间戳会偏大。
- 定时器溢出:32位定时器在72MHz下,每59秒溢出一次。必须做溢出处理,否则时间戳会回绕。
- 任务调度延迟:不要在任务里打时间戳。任务可能被高优先级任务抢占,时间戳就不准了。
4.4 本章知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以看到,RTOS是骨架,传感器驱动是血肉,时间戳是神经——三者缺一不可。
好了,这一章的内容就这些。RTOS怎么搭、驱动怎么写、时间戳怎么打,都是实打实的工程经验。你把这些搞明白了,数据采集的软件框架就算立住了。