2、系统架构设计:显示系统总体架构、模块化设计原则、数据流与接口定义

好,咱们进入第二章。系统架构设计,说白了就是搭骨架。你想想看,一个航电显示系统,要处理雷达数据、导航信息、发动机参数,还得把画面流畅地渲染出来。骨架搭不好,后面全是坑。我个人习惯,在画第一张架构图之前,先想清楚三个问题:数据从哪来?到哪去?中间怎么处理?

2.1 显示系统总体架构

先看总体架构。我把它分成三个层次:数据采集层核心处理层显示输出层。每一层各司其职,互不干扰。

  • 数据采集层:负责从各类传感器、总线(ARINC 429、AFDX、CAN)获取原始数据。我在项目中遇到过,有些老飞机还在用429总线,速率只有100kbps,数据量一大就容易丢包。所以这一层必须做数据校验和缓冲。
  • 核心处理层:这是大脑。负责数据融合、符号生成、告警逻辑。说白了,就是把原始数据变成飞行员能看懂的画面元素。
  • 显示输出层:负责最终的图形渲染和屏幕驱动。嗯,这里要注意,不同显示器的分辨率、刷新率、色彩深度都不一样,驱动层必须做适配。

重要原则:每一层之间必须通过明确定义的接口通信,不能跨层调用。我曾经见过一个项目,处理层直接去读显示层的帧缓冲区,结果调试时画面乱跳,查了三天才发现是耦合问题。

2.2 模块化设计原则

模块化设计,我总结了四个字:高内聚,低耦合。但光说理论没用,咱们看具体怎么做。

2.2.1 功能模块划分

我建议把系统拆成这几个模块:

模块名称 职责 典型接口
数据管理模块 数据采集、校验、缓存 getSensorData()
符号生成模块 生成PFD、ND、EICAS等画面元素 generateSymbol()
告警管理模块 告警逻辑判断、优先级排序 checkAlert()
渲染引擎模块 图形渲染、抗锯齿、帧率控制 renderFrame()
配置管理模块 加载飞机型号、显示布局等配置 loadConfig()

每个模块只做一件事,并且把这件事做好。举个例子,符号生成模块只负责生成几何图形,它不关心数据怎么来的,也不关心画面怎么渲染。这样,如果以后要换渲染引擎,只需要改渲染引擎模块,其他模块完全不动。

个人经验:模块划分时,别贪大求全。我刚开始做设计时,总想把所有功能塞进一个模块里,结果代码越写越乱,改一个功能要动十几个文件。后来学乖了,宁可多拆几个模块,也别让一个模块太臃肿。

2.2.2 接口设计原则

接口设计,说白了就是模块之间的「契约」。我习惯用C语言的结构体来定义接口,清晰明了。

/* 数据管理模块接口 */
typedef struct {
    uint32_t sensor_id;
    float    value;
    uint8_t  quality;  /* 0-100, 数据质量 */
    uint32_t timestamp;
} SensorData_t;

/* 符号生成模块接口 */
typedef struct {
    uint16_t symbol_id;
    float    pos_x;
    float    pos_y;
    uint32_t color;
    float    size;
} Symbol_t;

为什么要这么定义?因为接口一旦确定,各个模块就可以并行开发了。我在项目中遇到过,两个团队因为接口没对齐,联调时发现数据格式对不上,白白浪费了两周时间。所以,接口定义一定要在编码开始前就定下来,并且写进文档里。

2.3 数据流与接口定义

数据流,就是数据在系统里怎么流动的。我画了一张典型的数据流图,咱们一步步看。

2.3.1 数据流方向

  1. 输入阶段:传感器数据通过总线进入数据管理模块。数据管理模块做校验、去噪、时间戳对齐。
  2. 处理阶段:核心处理层从数据管理模块取数据,进行融合计算。比如,把GPS数据和惯性导航数据融合,得到更精确的位置信息。
  3. 生成阶段:符号生成模块根据处理后的数据,生成画面元素。比如,根据空速和高度生成PFD上的速度带和高度带。
  4. 输出阶段:渲染引擎模块把符号渲染到帧缓冲区,然后输出到显示器。

注意:数据流不能有环路。我曾经见过一个设计,告警模块的输出又反馈回数据管理模块,结果导致数据不断循环,系统负载飙升。如果确实需要反馈,一定要加防抖和限幅机制。

2.3.2 关键接口定义

我挑几个关键接口,给大家看看具体怎么定义。

接口1:数据管理模块 → 符号生成模块

/* 获取当前帧的所有传感器数据 */
int32_t GetFrameData(SensorData_t *buffer, uint32_t *count);
/* 参数说明:
 * buffer: 输出缓冲区,存放传感器数据
 * count: 输入时表示缓冲区大小,输出时表示实际数据数量
 * 返回值: 0表示成功,-1表示失败
 */

接口2:符号生成模块 → 渲染引擎模块

/* 提交一帧的符号列表 */
int32_t SubmitSymbolList(const Symbol_t *symbols, uint32_t count);
/* 参数说明:
 * symbols: 符号列表
 * count: 符号数量
 * 返回值: 0表示成功,-1表示失败
 */

为什么要用指针传递数据?因为航电系统对实时性要求极高,拷贝数据会浪费CPU周期。用指针传递,效率更高。但要注意,指针指向的内存必须由调用方管理,接收方不能释放。

2.3.3 数据流时序

数据流不是乱跑的,它有严格的时序。我建议用状态机来管理数据流。

typedef enum {
    STATE_IDLE,       /* 空闲 */
    STATE_ACQUIRE,    /* 采集数据 */
    STATE_PROCESS,    /* 处理数据 */
    STATE_GENERATE,   /* 生成符号 */
    STATE_RENDER,     /* 渲染输出 */
    STATE_SYNC        /* 帧同步 */
} DataFlowState_t;

每个状态都有明确的进入条件和退出条件。比如,从STATE_ACQUIRE到STATE_PROCESS,必须等数据采集完成并且校验通过。这样做的好处是,系统行为是可预测的,不会出现数据还没采集完就开始处理的情况。

避坑指南:我曾经在项目中遇到一个问题,数据流时序没控制好,导致画面出现撕裂。后来加了帧同步机制,在STATE_SYNC状态等待垂直消隐信号,问题就解决了。所以,时序控制一定要做细。

2.4 小结

这一章咱们讲了系统架构设计的三个核心:总体架构、模块化设计、数据流与接口。说白了,就是要把系统拆成可管理的模块,定义好模块之间的通信方式,然后让数据按照既定的时序流动。你想想看,只要这三件事做好了,后面的开发就会顺畅很多。

下一章,咱们会深入讲显示系统的硬件选型,包括处理器、GPU、内存等。到时候我会分享一些选型时的踩坑经验,敬请期待。