4. 飞控软件整体分层架构:硬件抽象层(HAL)、中间件层、应用层、地面站接口层

各位同学,今天我们聊聊飞控软件的整体分层。说实话,很多刚入行的朋友喜欢把代码全写在一个大循环里,觉得这样效率高。嗯,我年轻时也这么干过,结果项目后期改一个传感器驱动,整个系统都要重新编译测试,那叫一个痛苦。

分层架构,说白了就是「各司其职,互不干扰」。我习惯把飞控软件切成四层:硬件抽象层、中间件层、应用层、地面站接口层。每一层只关心自己的事,层与层之间通过标准接口通信。这样做的好处,你想想看——换一个IMU芯片,只需要改硬件抽象层,应用层的控制算法完全不用动。

核心原则:上层不关心底层怎么实现,底层不假设上层如何使用。这就是分层架构的精髓。

飞控软件四层架构 地面站接口层 MAVLink协议 · 参数管理 · 日志上传 · 遥控指令解析 应用层 姿态控制 · 位置控制 · 导航规划 · 状态机管理 安全保护 · 任务调度 · 故障诊断 中间件层 uORB消息总线 · 传感器融合(EKF) · 数据缓存 时间同步 · 任务间通信 · 健康监控 硬件抽象层 (HAL) SPI/I2C/UART驱动 · GPIO控制 · PWM输出 · ADC采集 传感器驱动(IMU/磁力计/气压计) · 执行器驱动(电机/舵机) 硬件平台

4.1 硬件抽象层(HAL)—— 把硬件「藏」起来

硬件抽象层是飞控软件的最底层,直接跟芯片和传感器打交道。我个人的习惯是,不管用STM32还是国产MCU,HAL层提供的API必须一模一样。比如读IMU数据,上层调用 hal_imu_read() 就行,至于底层是SPI还是I2C,上层完全不用管。

我在项目中遇到过最头疼的事:某次换了一款气压计,从MS5611换成BMP388。如果没有HAL层,你得满世界找调用了气压计驱动的地方,改几十个文件。有了HAL层,只需要重写 hal_baro_read() 这一个函数就行。

我的建议:HAL层要提供统一的错误码返回。比如读传感器超时返回 -1,数据无效返回 -2。这样上层做故障诊断时,逻辑可以统一处理。

/* 硬件抽象层接口示例 */
typedef struct {
    float gyro[3];    // 陀螺仪,单位 rad/s
    float accel[3];   // 加速度计,单位 m/s²
    float mag[3];     // 磁力计,单位 Gauss
    float baro;       // 气压计,单位 Pa
    float temp;       // 温度,单位 ℃
} hal_sensor_data_t;

/* 统一传感器读取接口 */
int hal_sensor_read(hal_sensor_data_t *data);
/* 返回 0 成功,负值表示错误码 */

4.2 中间件层 —— 飞控的「神经系统」

中间件层是飞控软件里最容易被低估的一层。说白了,它就是负责让各个模块之间能「说话」。我最早做飞控时,用的是全局变量传数据,结果调试时经常搞不清哪个模块改了哪个变量,bug查得头皮发麻。

后来我改用消息总线架构,类似PX4的uORB。每个模块只发布自己产生的数据,需要数据的模块去订阅。这样模块之间完全解耦,想加一个新传感器?写个驱动,把数据发布到总线上就行,控制模块自动就能收到。

中间件组件 职责 我踩过的坑
消息总线 模块间数据交换 消息队列深度不够导致丢数据
传感器融合 EKF/互补滤波 时间戳不同步导致发散
健康监控 心跳检测、异常上报 漏掉看门狗喂狗时机
时间同步 统一时间基准 不同传感器延时差异没补偿

注意:中间件层不要做太多「业务逻辑」。我曾经把控制算法也塞进中间件层,结果代码耦合得一塌糊涂。中间件只负责「传数据」和「做预处理」,真正的决策交给应用层。

4.3 应用层 —— 飞控的「大脑」

应用层是飞控软件里最「聪明」的一层。姿态控制、位置控制、导航规划、状态机管理,全在这里。我习惯把应用层拆成几个独立的任务(Task),每个任务跑一个固定频率。

举个例子,姿态控制任务跑 400Hz,位置控制任务跑 100Hz,导航规划跑 10Hz。为什么这样设计?你想想看,姿态变化最快,需要高频控制;位置变化慢一些,频率可以低一点;导航规划是宏观决策,10Hz 足够了。

我曾经犯过一个错误:把所有控制逻辑写在一个大循环里,结果高频的姿态控制被低频的导航任务阻塞,飞控直接炸机。嗯,从那以后我坚持用任务优先级+独立栈空间的设计。

/* 应用层任务调度示例 */
void task_attitude_control(void *arg) {
    /* 400Hz 姿态控制 */
    while(1) {
        hal_sensor_data_t sensor;
        hal_sensor_read(&sensor);           // 读传感器
        attitude_ekf_update(&sensor);       // 姿态解算
        attitude_pid_control();             // PID控制
        hal_motor_output(control_output);   // 输出PWM
        os_delay_us(2500);                  // 2.5ms周期
    }
}

void task_position_control(void *arg) {
    /* 100Hz 位置控制 */
    while(1) {
        position_ekf_update();
        position_pid_control();
        os_delay_ms(10);
    }
}

4.4 地面站接口层 —— 飞控的「窗户」

地面站接口层负责飞控和地面站之间的通信。说白了,就是让地面上的工程师能看到飞机在干什么,也能给飞机下指令。我一般用MAVLink协议,它是无人机行业的事实标准。

这一层要处理的事情挺杂的:参数读写、日志上传、遥控指令解析、心跳包维护。我建议把地面站接口层做成一个独立的任务,不要跟控制任务混在一起。为什么?因为地面站通信有时候会丢包、会重传,这些网络问题不应该影响飞控的实时性。

避坑指南:我曾经把日志上传功能直接放在控制任务里,结果日志文件太大,上传时占用了大量CPU时间,导致控制周期抖动。后来我把日志上传放到一个低优先级任务里,问题就解决了。

地面站接口层还有一个重要功能:参数管理。飞控的参数成百上千,PID系数、传感器校准值、飞行模式配置等等。我习惯用参数ID来索引,而不是用字符串名字。字符串比较太慢了,在嵌入式系统里能省则省。

/* 参数管理接口示例 */
typedef struct {
    uint16_t param_id;      // 参数ID
    float    value;         // 参数值
    uint8_t  type;          // 参数类型
} param_t;

int param_set(uint16_t id, float value);
int param_get(uint16_t id, float *value);
/* 地面站通过MAVLink协议调用这些接口 */

4.5 层与层之间的「规矩」

分层架构最怕什么?最怕层与层之间「乱搞」。我见过有人直接在应用层调用HAL层的SPI读写函数,美其名曰「提高效率」。结果呢?换了一个MCU,所有代码都得重写。

我定了几条规矩,大家可以参考:

  • 单向依赖:上层可以调用下层,下层绝不能调用上层。应用层可以调中间件层,中间件层不能反过来调应用层。
  • 接口稳定:层与层之间的接口一旦确定,尽量不要改。改接口意味着所有上层代码都要跟着改,成本很高。
  • 数据封装:不要直接暴露内部数据结构。比如HAL层的传感器数据结构,应该通过函数返回,而不是让上层直接访问全局变量。

总结一下:分层架构不是为了好看,是为了「可维护」和「可移植」。我做了十几年飞控,换过七八种MCU、几十种传感器,每次都能快速适配,靠的就是严格的分层设计。你想想看,如果每次换硬件都要重写整个飞控,那得多痛苦?

好了,这一章的内容就到这里。分层架构是飞控软件的骨架,骨架搭好了,后面的工作才能顺利进行。下一章我们会深入中间件层的消息总线设计,那是飞控软件里最精彩的部分之一。


专注资料整理