3、多屏显示架构:Linux DRM/KMS框架、Android SurfaceFlinger原理、QNX Screen Graphics子系统

多屏显示架构,说白了就是系统怎么把画面送到不同的屏幕上。座舱里少则两块屏,多则五六块,每块屏的分辨率、刷新率、内容来源都不一样。我这些年调过的多屏系统,踩坑最多的就是架构选型阶段——选错了,后面性能怎么调都救不回来。

这一章我们拆开三个主流方案:Linux 的 DRM/KMS、Android 的 SurfaceFlinger、QNX 的 Screen Graphics。它们思路不同,但解决的核心问题是一样的——如何高效、低延迟地把多个应用画面合成并输出到多个显示器。

3.1 Linux DRM/KMS:最底层的多屏控制

DRM(Direct Rendering Manager)是 Linux 内核里的图形子系统。KMS(Kernel Mode Setting)负责设置显示模式——分辨率、刷新率、连接器状态。这两个东西合在一起,构成了 Linux 上多屏显示的基础。

我习惯把 DRM/KMS 看作“硬件的直接操作员”。它不帮你做合成,只给你提供操作硬件的接口。你要自己管理 framebuffer、自己处理合成、自己控制显示时序。

3.1.1 核心对象模型

DRM 用四个核心对象描述显示链路:

对象 含义 类比
CRTC 显示控制器,负责从内存读取画面并生成时序信号 投影仪的灯泡
Encoder 编码器,将数字信号转为特定接口格式(HDMI、LVDS、DSI) 信号转换器
Connector 物理连接器,代表实际的显示接口 插座
Plane 硬件图层,每个 plane 可以独立配置 framebuffer 和位置 幻灯片叠加层

多屏显示就是把这些对象串联起来。比如仪表屏接 LVDS,中控屏接 HDMI,每个链路都是一个独立的 CRTC + Encoder + Connector 组合。

关键点:DRM 的 plane 是硬件合成的基础。如果你有两层 UI(地图 + 导航浮层),用两个 plane 分别渲染,硬件自动合成,完全不需要 CPU 参与。我在项目中见过有人把所有内容画到一个 framebuffer 里,白白浪费了硬件能力。

3.1.2 多屏配置实战

用 DRM 驱动多屏,核心是调用 drmModeSetCrtc 为每个 CRTC 绑定一个 framebuffer。下面是一个简化示例:

// 枚举所有 connector
drmModeConnector *conn = drmModeGetConnector(fd, connector_id);
// 获取对应的 CRTC
drmModeCrtc *crtc = drmModeGetCrtc(fd, crtc_id);
// 创建 framebuffer
uint32_t fb_id;
drmModeAddFB(fd, width, height, depth, bpp, stride, bo_handle, &fb_id);
// 设置显示模式
drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0, &connector_id, 1, &conn->modes[0]);

嗯,这里要注意:每个 CRTC 只能绑定一个 connector。如果你想把同一个画面输出到两块屏,需要两个 CRTC 各自绑定一个 framebuffer,内容可以共享同一块内存。

我曾经踩过的坑:在 i.MX8 平台上,两个 CRTC 共享同一个 framebuffer 时,如果 buffer 在 GPU 端被更新,两个 CRTC 的读取时序不同步,会出现画面撕裂。解决办法是用双缓冲 + 垂直同步,或者用硬件 scatter/gather 引擎做独立拷贝。

3.1.3 Atomic API:事务性更新

DRM 的 Atomic API 是后来加入的,允许你把多个属性变更打包成一个原子操作。比如同时移动两个 plane 的位置、切换一个 CRTC 的显示模式,这些操作要么全部成功,要么全部回滚。

我建议所有新项目都用 Atomic API。它解决了传统 drmModeSetCrtc 的“中间态”问题——传统接口在设置过程中,屏幕可能会闪一下。

// Atomic 提交示例
drmModeAtomicReq *req = drmModeAtomicAlloc();
drmModeAtomicAddProperty(req, crtc_id, crtc->mode_prop_id, &mode_blob);
drmModeAtomicAddProperty(req, plane_id, plane->fb_prop_id, fb_id);
drmModeAtomicAddProperty(req, plane_id, plane->crtc_prop_id, crtc_id);
drmModeAtomicCommit(fd, req, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);

小技巧:Atomic 提交时加上 DRM_MODE_ATOMIC_NONBLOCK 标志,可以异步提交,不阻塞调用线程。但要注意,非阻塞模式下错误码不会立即返回,需要通过 event 回调获取。

3.2 Android SurfaceFlinger:合成与窗口管理

SurfaceFlinger 是 Android 显示系统的核心。它接收各个应用窗口的 Surface,合成后输出到显示设备。座舱里用 Android 做 IVI 系统很常见,SurfaceFlinger 的多屏能力直接决定了中控屏、副驾屏、后排屏的体验。

说白了,SurfaceFlinger 就是个“窗口合成器”。它不关心应用怎么画,只关心画完之后怎么叠、怎么送显。

3.2.1 合成流程

SurfaceFlinger 的工作流程可以概括为三步:

  1. 收集:从各个应用进程接收 Surface 的 buffer(通常是 GraphicBuffer)
  2. 合成:用 GPU 或 HWC(Hardware Composer)把多个 buffer 合成一张图
  3. 送显:通过 HWC 或直接写入 framebuffer 输出到屏幕

这里的关键是 HWC。HWC 是硬件合成器,它知道硬件有多少个 overlay plane。如果应用层数不超过硬件 plane 数,HWC 可以直接让硬件合成,GPU 完全不参与。如果超过了,SurfaceFlinger 才用 GPU 做 OpenGL 合成。

性能关键:尽量减少 GPU 合成的层数。我调过一个项目,副驾屏的导航层和媒体层本来可以走两个 hardware plane,但开发人员把两层画到了一个 Surface 里,导致 GPU 不得不参与合成。改回两层后,GPU 负载从 40% 降到了 5%。

3.2.2 多屏扩展:DisplayDevice 与 VirtualDisplay

Android 从 4.2 开始支持多屏。每个物理显示器对应一个 DisplayDevice 对象,SurfaceFlinger 为每个 DisplayDevice 维护独立的合成链。

VirtualDisplay 是 Android 的一个特色——它不输出到物理屏幕,而是输出到内存 buffer。座舱里常用 VirtualDisplay 做屏幕录制、投屏、或者把中控画面镜像到后排。

类型 用途 典型场景
Physical Display 直接输出到物理屏幕 仪表屏、中控屏、副驾屏
VirtualDisplay 输出到内存 buffer 投屏、录屏、多屏镜像

多屏配置在 display_ 开头的属性文件中定义。我记得有一次客户要求三屏独立显示,但硬件只有两个 display pipe。最后方案是让第三块屏走 USB DisplayLink 芯片,通过 VirtualDisplay 把内容传给 USB 桥接芯片——虽然延迟稍高,但满足了功能需求。

3.2.3 性能调优要点

  • 减少 layer 数量:每多一层,HWC 的匹配复杂度就增加一层。能合并的 UI 元素尽量合并。
  • 使用 BufferQueue 的异步模式:SurfaceFlinger 默认是同步的,应用生产 buffer 和 SF 消费 buffer 之间可能有等待。开启异步模式可以减少一帧延迟。
  • 监控 HWC 的 fallback:通过 dumpsys SurfaceFlinger 查看合成模式。如果看到大量 "GLES composition",说明硬件 plane 不够用,GPU 在兜底。

我曾经遇到的坑:某款车规级 SoC 的 HWC 只支持 4 个 overlay plane。我们当时有 5 个应用层(状态栏、导航、媒体、空调、语音浮窗),结果 HWC 回退到 GPU 合成,帧率直接掉到 30fps。最后把状态栏和语音浮窗合并到一个 Surface 里,才解决了问题。

3.3 QNX Screen Graphics:安全隔离下的多屏方案

QNX 是座舱里仪表和 ADAS 系统的首选。它的 Screen Graphics 子系统跟 Linux DRM 和 Android SurfaceFlinger 的思路都不一样——它把“安全”放在第一位。

Screen Graphics 的核心概念是 Screen Context。每个应用或服务拥有自己的 Screen Context,彼此隔离。一个 Context 崩溃了,不会影响其他 Context 的显示。

3.3.1 架构特点

  • 基于微内核:图形驱动运行在用户态,崩溃了可以重启,不会导致整个系统挂掉
  • 显式多屏管理:每个 display 是一个独立的 Screen 对象,应用可以指定输出到哪个 display
  • 硬件合成优先:Screen Graphics 会尽量利用硬件 overlay,跟 HWC 的思路类似

3.3.2 多屏编程模型

在 QNX 上创建多屏应用,核心是创建多个 Screen 窗口并绑定到不同的 display:

// 创建 screen context
screen_context_t ctx;
screen_create_context(&ctx, SCREEN_APPLICATION_CONTEXT);

// 获取 display 列表
screen_display_t *displays;
int num_displays;
screen_get_context_property_iv(ctx, SCREEN_PROPERTY_DISPLAYS, &num_displays);
displays = malloc(num_displays * sizeof(screen_display_t));
screen_get_context_property_pv(ctx, SCREEN_PROPERTY_DISPLAYS, (void**)displays);

// 为每个 display 创建窗口
for (int i = 0; i < num_displays; i++) {
    screen_window_t win;
    screen_create_window(&win, ctx);
    screen_set_window_property_pv(win, SCREEN_PROPERTY_DISPLAY, (void*)&displays[i]);
    // 设置窗口大小、位置、buffer 格式...
}

嗯,这里要注意:QNX 的窗口是“独占”的。一个窗口默认只属于一个 display,不能跨屏。如果你要做跨屏拖拽,需要自己实现坐标映射和 buffer 拷贝。

小技巧:QNX 支持 SCREEN_PROPERTY_ALTERNATE_DISPLAY 属性,允许窗口在多个 display 之间切换。但切换时会有短暂的闪烁,建议在切换前先做双缓冲同步。

3.3.3 安全隔离与性能的平衡

Screen Graphics 的隔离机制带来了性能开销。每次 buffer 跨进程传递都要经过微内核的 IPC,延迟比 Linux 的共享内存高一些。我测过一组数据:

操作 Linux DRM (共享内存) QNX Screen (IPC)
buffer 提交延迟 ~0.5ms ~1.2ms
合成延迟 (2层) ~1.0ms ~1.8ms
合成延迟 (4层) ~2.5ms ~3.5ms

但 QNX 的优势在于确定性。Linux 的延迟波动可能很大(调度延迟、中断延迟),而 QNX 的延迟几乎恒定。对于仪表这种需要严格时序的场景,QNX 的“慢但稳”比 Linux 的“快但抖”更可靠。

我的建议:如果项目对安全等级要求高(ASIL-B 及以上),选 QNX。如果追求极致性能且安全等级要求低(QM 或 ASIL-A),Linux DRM 更合适。Android 适合 IVI 这种需要丰富生态的场景,但要做好性能隔离。

3.4 三种架构对比总结

最后用一张表格收尾,方便你快速对比:

维度 Linux DRM/KMS Android SurfaceFlinger QNX Screen Graphics
架构层级 内核态 + 用户态 libdrm 用户态合成服务 用户态微内核服务
多屏支持 原生,每个 CRTC 一个屏 DisplayDevice + VirtualDisplay 原生,每个 display 独立
合成方式 用户自己管理(或借助 weston 等 compositor) HWC 硬件合成 + GPU 回退 硬件合成优先 + 软件回退
安全隔离 弱(依赖进程隔离) 中(SurfaceFlinger 崩溃影响所有窗口) 强(每个 Context 独立)
典型场景 仪表、中控(非 Android 方案) IVI 中控、副驾屏、后排娱乐 仪表、ADAS、HUD(高安全要求)
学习曲线 陡峭(需要理解 DRM 对象模型) 中等(Android 框架封装较好) 中等(QNX 文档质量高但生态小)

你想想看,座舱里的多屏系统,本质上就是在这三个方案里做选择,或者把它们组合起来。我见过不少混合方案:仪表跑 QNX + Screen Graphics,IVI 跑 Android + SurfaceFlinger,中间通过以太网或 PCIe 共享 buffer。这种方案兼顾了安全和生态,但调试复杂度也翻倍了。

嗯,这一章的内容就到这里。三种架构各有千秋,选型时一定要结合你的硬件能力、安全等级要求和团队技术栈来定。

多屏显示架构对比总览 Linux DRM/KMS Android SurfaceFlinger QNX Screen Graphics 核心对象 CRTC / Encoder Connector / Plane Atomic API 核心对象 Surface / GraphicBuffer HWC / DisplayDevice VirtualDisplay 核心对象 Screen Context Screen Display Screen Window 合成方式 用户态 Compositor (如 Weston / 自研) 合成方式 HWC 硬件合成 GPU OpenGL 回退 合成方式 硬件合成优先 软件合成回退 安全等级 QM ~ ASIL-A 安全等级 QM(需额外隔离) 安全等级 ASIL-B ~ ASIL-D 仪表 / 中控(非Android) IVI中控 / 副驾屏 / 后排娱乐 仪表 / ADAS / HUD