4. 模型架构设计:分层架构原则

好,咱们进入模型架构设计这一节。说实话,这部分是我在飞控开发中踩坑最多的地方。早期我做项目时,代码写得很随意——算法和硬件操作混在一起,结果换个传感器就得重写一大片。后来我学乖了,开始用分层架构。

分层架构的核心思想,说白了就是「各司其职」。你想想看,飞控系统要处理的事情太多了:读传感器、跑控制算法、输出PWM、还要跟地面站通信。如果所有代码都搅在一起,调试起来简直要命。

4.1 三层架构:应用层、算法层、硬件抽象层

我个人习惯把飞控模型分成三层。嗯,这不是什么标准,但我在多个项目里试过,效果不错。

层级 职责 典型内容
应用层 任务调度、状态机、模式切换 起飞/降落逻辑、故障处理、用户指令解析
算法层 核心控制算法、状态估计 PID控制器、卡尔曼滤波、姿态解算
硬件抽象层 传感器驱动、执行器接口 IMU读取、PWM输出、GPS数据解析

应用层是飞控的大脑。它不关心你用的是MPU6050还是BMI088,也不管PID参数具体是多少。它只负责一件事:根据当前飞行模式,决定下一步该干什么。比如,接收到「降落」指令后,应用层就告诉算法层:「给我切换到降落模式」。

算法层是飞控的心脏。这里跑的是真正的控制逻辑。我在项目中遇到过一个问题:算法层里混了硬件相关的代码,导致仿真时跑得好好的,一上真机就崩。后来我强制规定:算法层的输入输出必须是标准化的物理量(比如角度用弧度,速度用米/秒),绝不出现原始寄存器值。

硬件抽象层是飞控的四肢。它负责把物理世界的信号变成算法层能理解的数据。你想想看,同样的加速度计,I2C接口和SPI接口的读取方式完全不同。但有了硬件抽象层,算法层根本不需要知道这些细节。

核心原则:每一层只能调用下一层的接口,不能跨层调用。应用层不能直接操作硬件寄存器,算法层不能直接调用传感器驱动。

4.2 模型接口定义

接口定义这件事,我吃过不少亏。曾经有个项目,团队里三个人各写各的,结果联调时发现接口对不上——有人传的是角度,有人传的是弧度,还有人传的是百分数。那场面,简直灾难。

所以我建议,在开始写代码之前,先把接口协议定死。怎么做?

  • 明确数据类型:所有物理量必须带单位。比如角度统一用弧度(float),不要混用度数和弧度。
  • 定义结构体:用结构体打包数据,而不是散装传参。比如姿态信息用一个 attitude_t 结构体,包含 roll、pitch、yaw 三个字段。
  • 接口函数命名规范:我习惯用 层名_功能_动作 的格式。比如 hal_imu_read()algo_attitude_update()
// 硬件抽象层接口示例
typedef struct {
    float ax;   // 加速度 X,单位 m/s²
    float ay;
    float az;
    float gx;   // 角速度 X,单位 rad/s
    float gy;
    float gz;
} imu_data_t;

// 算法层接口示例
typedef struct {
    float roll;   // 横滚角,单位 rad
    float pitch;  // 俯仰角,单位 rad
    float yaw;    // 偏航角,单位 rad
} attitude_t;

// 接口函数声明
hal_status_t hal_imu_read(imu_data_t *data);
algo_status_t algo_attitude_estimate(imu_data_t *imu, attitude_t *att);

小技巧:接口定义好后,先写一个空的桩函数(stub),让上层代码能编译通过。这样各层可以并行开发,不用等底层写完再测上层。

4.3 数据流与控制流分离

这个原则,我是在一次惨痛教训后才真正理解的。当时我做了一个飞控原型,数据采集和控制逻辑混在一起。结果调试时发现:控制周期不稳定,有时候快有时候慢。查了半天才发现,是数据采集阻塞了控制循环。

为什么会这样?因为数据流和控制流是两种完全不同的节奏。

  • 数据流:传感器数据、遥控器信号、GPS信息。这些数据是「流式」的,有快有慢,可能有延迟或丢包。
  • 控制流:状态机切换、任务调度、故障处理。这些是「事件驱动」的,需要确定性的响应时间。

我的做法是:用两个独立的循环来处理它们。

// 数据流循环:高频采集,不阻塞
void data_loop(void) {
    while(1) {
        hal_imu_read(&imu_data);      // 读IMU
        hal_gps_read(&gps_data);      // 读GPS
        hal_rc_read(&rc_data);        // 读遥控器
        // 数据放入缓冲区,供算法层使用
        data_buffer_update(&imu_data, &gps_data, &rc_data);
        delay_ms(1);  // 1ms周期
    }
}

// 控制流循环:低频控制,确定性执行
void control_loop(void) {
    while(1) {
        // 从缓冲区取最新数据
        data_buffer_get(&latest_data);
        // 运行状态机
        flight_state_machine(&state);
        // 执行控制算法
        algo_control_run(&latest_data, &state, &output);
        // 输出到执行器
        hal_actuator_set(&output);
        delay_ms(10);  // 10ms控制周期
    }
}

你看,数据流循环只管采集和缓存,控制流循环只管计算和输出。两者通过一个共享缓冲区交互。这样即使某个传感器偶尔卡一下,控制循环也不会受影响。

注意:共享缓冲区要加锁或使用无锁队列,防止数据竞争。我在项目中用过环形缓冲区(ring buffer),效果不错。

4.4 分层架构的SVG示意图

下面这张图是我画的分层架构示意图。你可以看到数据流是从下往上走(硬件→算法→应用),控制流是从上往下走(应用→算法→硬件)。两者在算法层交汇,但互不干扰。

应用层 状态机 · 任务调度 · 模式切换 算法层 PID控制 · 卡尔曼滤波 · 姿态解算 硬件抽象层 传感器驱动 · 执行器接口 · 通信协议 数据流 控制流 数据流从硬件到应用,控制流从应用到硬件

嗯,这张图其实是我在项目复盘时画的。当时团队里新人总搞不清数据该往哪流,有了这张图就清楚多了。

4.5 避坑指南

最后,分享几个我踩过的坑:

  • 不要过早优化:我曾经为了省几个字节的内存,把接口设计得很「精巧」,结果后来扩展时痛苦不堪。先保证清晰,再考虑优化。
  • 接口版本管理:接口一旦发布,就不要轻易改。如果非要改,用版本号区分。我吃过亏:改了一个接口函数名,结果忘了更新依赖它的模块,编译通过但运行结果全错。
  • 仿真与真机的一致性:算法层在仿真环境里跑得好好的,上真机就出问题。原因往往是硬件抽象层没有模拟真实硬件的延迟和噪声。所以,硬件抽象层要提供「仿真模式」和「真机模式」两种实现。

总结一下:分层架构不是银弹,但它能帮你把复杂系统拆解成可管理的小块。接口定义要早做、做细。数据流和控制流一定要分开,别混在一起。记住这些,你的飞控模型会好维护得多。


公众号:蓝海资料掘金营,微信deep3321