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 数据流方向
- 输入阶段:传感器数据通过总线进入数据管理模块。数据管理模块做校验、去噪、时间戳对齐。
- 处理阶段:核心处理层从数据管理模块取数据,进行融合计算。比如,把GPS数据和惯性导航数据融合,得到更精确的位置信息。
- 生成阶段:符号生成模块根据处理后的数据,生成画面元素。比如,根据空速和高度生成PFD上的速度带和高度带。
- 输出阶段:渲染引擎模块把符号渲染到帧缓冲区,然后输出到显示器。
注意:数据流不能有环路。我曾经见过一个设计,告警模块的输出又反馈回数据管理模块,结果导致数据不断循环,系统负载飙升。如果确实需要反馈,一定要加防抖和限幅机制。
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、内存等。到时候我会分享一些选型时的踩坑经验,敬请期待。