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示意图
下面这张图是我画的分层架构示意图。你可以看到数据流是从下往上走(硬件→算法→应用),控制流是从上往下走(应用→算法→硬件)。两者在算法层交汇,但互不干扰。
嗯,这张图其实是我在项目复盘时画的。当时团队里新人总搞不清数据该往哪流,有了这张图就清楚多了。
4.5 避坑指南
最后,分享几个我踩过的坑:
- 不要过早优化:我曾经为了省几个字节的内存,把接口设计得很「精巧」,结果后来扩展时痛苦不堪。先保证清晰,再考虑优化。
- 接口版本管理:接口一旦发布,就不要轻易改。如果非要改,用版本号区分。我吃过亏:改了一个接口函数名,结果忘了更新依赖它的模块,编译通过但运行结果全错。
- 仿真与真机的一致性:算法层在仿真环境里跑得好好的,上真机就出问题。原因往往是硬件抽象层没有模拟真实硬件的延迟和噪声。所以,硬件抽象层要提供「仿真模式」和「真机模式」两种实现。
总结一下:分层架构不是银弹,但它能帮你把复杂系统拆解成可管理的小块。接口定义要早做、做细。数据流和控制流一定要分开,别混在一起。记住这些,你的飞控模型会好维护得多。
公众号:蓝海资料掘金营,微信deep3321