4. 传感器仿真与注入:GPS、IMU、气压计、视觉传感器

传感器仿真,说白了就是给飞控“喂假数据”。但假数据要假得真,假得有逻辑。我见过不少团队,仿真跑得飞起,一上真机就炸——原因往往出在传感器模型太理想化。

这一章,我们重点聊四个核心传感器:GPS、IMU、气压计、视觉传感器。怎么模拟?怎么注入故障?我把自己踩过的坑和经验都摊开来讲。

4.1 GPS 仿真:位置、速度、星数、精度因子

GPS 仿真其实不复杂。你只需要输出一组符合协议的数据。常用的协议是 NMEA 0183 或 UBX。仿真时,我习惯直接构造 NMEA 语句。

核心思路: 根据仿真时间戳和预设轨迹,反算出经纬度、海拔、地速、航向角。然后打包成 $GPGGA、$GPRMC 等语句。

举个例子,模拟一个悬停状态下的 GPS 输出:

// 伪代码:构造 GGA 语句
char gga[128];
sprintf(gga, "$GPGGA,%02d%02d%06.2f,%02d%%.4f,%c,%03d%.4f,%c,1,%02d,%.1f,%.1f,M,0.0,M,,*",
        hour, min, sec,
        lat_deg, lat_min, lat_ns,
        lon_deg, lon_min, lon_ew,
        sat_num, hdop, alt);
// 别忘了计算校验和

故障注入怎么做?

  • 丢星: 将卫星数 sat_num 设为 0,或直接停止输出 NMEA 语句。
  • 位置跳变: 在经纬度上叠加一个随机大偏移(比如 0.01 度)。
  • 精度恶化: 把 HDOP 值从 0.8 改成 5.0 以上。

我的经验: 曾经有一次,仿真时 GPS 一直正常,但实飞时发现飞机在楼宇间定位漂移。后来我在仿真里加入了“城市峡谷”场景——随机遮挡部分卫星,才把问题复现出来。所以,别只给完美信号。

4.2 IMU 仿真:加速度计与陀螺仪

IMU 是飞控最底层的传感器。仿真 IMU,本质上就是根据运动学模型计算比力和角速度。

我常用的方法:

  1. 从仿真引擎拿到当前加速度(含重力)和角速度。
  2. 加上传感器噪声(高斯白噪声 + 零偏)。
  3. 加上安装误差(小角度旋转矩阵)。
  4. 输出到飞控的 IMU 驱动接口。

噪声参数怎么设?我一般参考真实 IMU 的 datasheet:

参数 典型值(消费级) 典型值(工业级)
加速度计噪声密度 100 μg/√Hz 20 μg/√Hz
陀螺仪噪声密度 0.005 °/s/√Hz 0.001 °/s/√Hz
零偏稳定性 0.1 mg 0.01 mg

注意: 千万别忘了加重力分量!我见过有人直接把仿真加速度传给飞控,结果飞控以为飞机在自由落体,直接触发降落保护。嗯,那个画面挺尴尬的。

故障注入:

  • 饱和: 当角速度超过 ±2000°/s 时,输出钳位到最大值。
  • 死零: 强制输出全零。
  • 温漂: 随时间缓慢增加零偏值。

4.3 气压计仿真:高度与温漂

气压计用来测高度。仿真时,我通常用标准大气模型:

// 标准大气压模型(简化版)
float pressure_at_altitude(float alt_m) {
    float p0 = 101325.0f;  // 海平面气压 (Pa)
    float T0 = 288.15f;    // 海平面温度 (K)
    float L = 0.0065f;     // 温度递减率 (K/m)
    float g = 9.80665f;
    float M = 0.0289644f;  // 摩尔质量 (kg/mol)
    float R = 8.31432f;    // 通用气体常数

    float exponent = g * M / (R * L);
    return p0 * powf(1.0f - L * alt_m / T0, exponent);
}

然后加上噪声和量化误差。气压计的分辨率一般是 10cm 左右,别输出太精细的值。

故障注入:

  • 堵塞: 输出值卡在某个固定高度。
  • 温漂: 模拟阳光暴晒导致的气压计温度升高,输出高度缓慢下降。
  • 突变: 模拟开窗或气流扰动,瞬间跳变几米。

避坑指南: 我曾经在仿真里把气压计噪声设得太小,结果飞控的高度估计滤波器收敛得特别好。一上真机,高度波动大了一倍,滤波器直接发散。所以,噪声别省,往大了加一点,反而更安全。

4.4 视觉传感器仿真:光流与视觉里程计

视觉传感器仿真比较麻烦。简单场景下,我直接用理想模型:根据相机运动输出光流向量或位姿变化。

复杂场景下,我会用 Gazebo + 插件,或者 Unreal Engine 渲染。但说实话,大部分联调阶段,理想模型就够了。

光流仿真:

// 理想光流模型
// 输入:机体角速度 w, 地速 v, 高度 h
// 输出:x 方向光流 fx, y 方向光流 fy
fx = vx / h + wy;
fy = vy / h - wx;

视觉里程计仿真: 直接输出相对位姿变换(Δx, Δy, Δz, Δroll, Δpitch, Δyaw),加上漂移和丢帧。

故障注入:

  • 丢帧: 随机跳过 10%~50% 的输出。
  • 纹理缺失: 输出光流为零,模拟白墙或水面。
  • 尺度漂移: 单目视觉的典型问题,输出位移逐渐偏大或偏小。

4.5 传感器仿真整体架构

下面这张图,是我个人习惯的传感器仿真注入架构。你看一眼就明白了:

传感器仿真注入架构 仿真引擎 GPS 模型 NMEA / UBX IMU 模型 加速度+陀螺仪 气压计模型 标准大气 视觉模型 光流 / VO 故障注入层(丢星 / 饱和 / 堵塞 / 丢帧) 飞控(EKF / 控制器) 💡 说明:每个传感器模型独立运行,故障注入层可单独控制。我习惯用 YAML 配置文件定义故障场景,方便批量测试。 仿真时,所有传感器数据统一时间戳,避免飞控端时间对齐出错。

4.6 故障注入的统一管理

你想想看,如果每个传感器的故障注入逻辑都散落在代码里,后期维护会多痛苦?我建议统一管理。

我的做法是定义一个故障注入接口:

// 故障注入配置结构体
typedef struct {
    bool enable_gps_loss;
    bool enable_gps_jump;
    float gps_jump_range_deg;

    bool enable_imu_saturation;
    float imu_saturation_threshold;

    bool enable_baro_blockage;
    float baro_blockage_alt_m;

    bool enable_vision_drop;
    float vision_drop_rate;
} FaultConfig;

// 统一注入函数
void inject_sensor_fault(SensorData *data, FaultConfig *cfg, uint64_t timestamp);

这样,测试人员只需要改一个 JSON 或 YAML 文件,就能组合出各种故障场景。

核心原则: 故障注入要可重复、可配置、可观测。别搞随机不可控的故障,否则出了问题你都不知道是仿真 bug 还是飞控 bug。

4.7 时间同步:容易被忽略的细节

最后说一个我吃过亏的地方——时间同步。仿真时,四个传感器可能来自不同的线程或进程,时间戳如果不统一,飞控的 EKF 会直接崩掉。

我的建议:

  • 所有传感器模型使用同一个仿真时钟源。
  • 输出数据时,时间戳统一用仿真时间(uint64_t 微秒)。
  • 如果传感器有延迟(比如视觉处理延迟 50ms),在时间戳上主动减去延迟量。

警告: 别相信“反正仿真里时间同步没问题,实飞再调”。我见过一个团队,仿真里所有传感器时间戳都是 0,飞控居然也能跑。一上真机,EKF 直接发散。嗯,排查了三天才发现是时间戳没对齐。

好了,传感器仿真与注入的核心内容就这些。记住:仿真不是目的,验证才是。把故障注入做扎实,实飞时才能心里有底。