这次折腾的目标很简单:不用 Windows 常驻软件,在 Linux 下分别控制 ASUS 水冷头上的 LCD
和 AORUS RTX 5090 MASTER ICE 的 Edge View 小屏。最终两条链路都在真机上跑通:水冷屏可以稳定显示任意图片;显卡屏可以写入静态图、文本和 GIF,
并能切回原厂 Chibi 动画。本文只记录协议原理、最小集成接口、验证方法和最容易踩的坑,
不绑定发行版、包管理器或后台服务框架。隐私说明:文中已移除用户名、IP、主机名、虚拟机名称、设备序列号、私有目录和 SSH 拓扑;
PCI/USB/I²C ID、公开硬件型号、开源项目地址等可复现的公开技术信息予以保留。
一、先说结论
两块看起来都叫“副屏”的设备,控制方式完全不同:
1 | Linux |
| 项目 | ASUS 水冷 LCD | AORUS Edge View |
|---|---|---|
| 总线 | USB | GPU 板载 I²C |
| Linux 入口 | PyUSB | /dev/i2c-*,由 NVIDIA 驱动导出 |
| 画布 | USB 握手报告 640×480 | 320×170 |
| 图像格式 | baseline JPEG | RGB565,小端 |
| 是否需要常驻 | 需要,停止推帧后 Live Screen 会超时 | 不需要,静态内容写完会保留 |
| 安全策略 | 校验两次控制响应 CRC | 只探测精确地址 0x61,成功后才允许写入 |
| 最终状态 | 恢复默认演示画面,由一个独占进程持续刷新 | 恢复原厂 Chibi 模式,写完即可退出 |
最绕的不是发图片,而是判断它到底属于哪一代协议。AORUS 目前的 Windows 软件里能找到一套0x76、RGB565 大端的新协议,但这张卡实际使用的是旧 GvLcdApi 协议,地址为 0x61。
沿着错误协议继续调,无论怎么换 Linux I²C adapter 都只会得到 EIO。
二、ASUS 水冷 LCD:USB 持续推送 JPEG
2.1 找到设备
先只做枚举,不写设备:
1 | lsusb |
本机对应的 USB ID 是:
1 | 0b05:1d64 ASUSTek Computer, Inc. |
后续程序固定匹配 VID/PID,不靠 USB 总线号或设备号,因为后两者在重启、重新插拔后会变化。
这款设备的 USB 握手返回了 640×480 画布,而 ASUS 当前相近零售型号的
官方页面
标注的是 480×480 原生面板。两者并不一定冲突:前者可能是控制器接收的传输画布,后者是物理面板分辨率,
中间由控制器缩放;也可能是外观相近但控制器不同的硬件版本。
不要只根据商品名硬编码分辨率,应以实际 USB 握手为准。
2.2 控制流程
这套控制流程来自 ASUS InfoHub 1.0.7 的主机端通信逻辑,并在设备固件 1.0.5 上验证。
实现只发送屏幕控制和图像数据,不包含任何固件更新操作。
完整的一次显示流程如下:
- 找到
0b05:1d64,必要时 reset。 - 设置 configuration,脱离占用接口的内核驱动并 claim interface 0。
- 向 OUT endpoint
0x02写入 36 字节握手包。 - 从 IN endpoint
0x81读取 36 字节响应,校验 CRC32。 - 发送 Live Screen 模式包,再读一次响应并校验 CRC32。
- 把输入图片裁剪或留边为
640×480RGB。 - 编码为非 progressive 的 baseline JPEG,quality 90,4:2:0。
- 在 JPEG 的
FFD9结束标记前插入FF,让 JPEG 总长度对齐到 32 字节。 - 加入帧元数据和传输头,持续写入 OUT endpoint。
固定握手包如下。把它拆成“命令头 + 27 个零字节 + CRC”来写,比复制一整行十六进制更容易检查长度:
1 | HANDSHAKE = bytes.fromhex("aa554b4c54" + ("00" * 27) + "44545b8c") |
控制响应使用固定 36 字节结构,最后 4 字节是大端 CRC:
1 | import zlib |
通用传输包的布局是:
1 | 5A A5 |
其中 CRC16 是 reflected CRC-16/IBM,也就是常见的 Modbus 形式:
1 | def crc16_modbus(data: bytes) -> int: |
切换屏幕模式的 payload 是 00 01 05,因此完整包应严格等于:
1 | 5a a5 00000003 2540 640b 000105 00 |
这里很适合写一个单元测试。它同时能抓出字节序、CRC 初值和 trailer 是否漏写:
1 | assert wrap_packet(bytes.fromhex("000105"), trailer=b"\x00").hex() == \ |
2.3 JPEG 为什么要在 EOI 前补齐
普通 JPEG 编码器不会保证输出长度是 32 的倍数,但控制器要求对齐。不能简单在文件尾追加垃圾,
因为设备侧可能按 JPEG 的结束标记截断;实际可用的做法是把 padding 放在最终 FFD9 前:
1 | def align_jpeg(jpeg: bytes) -> bytes: |
帧 payload 是:
1 | 00 00 00 00 00 00 |
传输包最后还要跟两个 00 字节:
1 | def make_frame_packet(jpeg: bytes) -> bytes: |
2.4 最小集成:一个独占写循环
单次发送确实会立即显示,但 Live Screen 模式在收不到后续帧时会超时。对静态图而言,重复编码没有意义;
最简单稳定的方式是先编码一次,再以 10 FPS 重复发送同一帧。
因此上层只需要实现一个很小的接口:
1 | image = prepare_image("picture.png", width=640, height=480) |
Python 示例只依赖 Pillow 和 PyUSB。为了不修改系统 Python,可以放在项目自己的 virtualenv 中;
如果用 Rust、C 或 Go 集成,则只需要等价的 libusb 和 JPEG 编码能力。
还要防止两个进程同时抢占 USB endpoint。可以在应用内部使用文件锁,也可以让调用方保证单实例;
锁文件放在哪里并不属于协议的一部分。进程退出时应释放 USB interface,异常路径也一样。
如果设备被直通给虚拟机,宿主机当然找不到它。要先决定由宿主机还是虚拟机拥有这个 USB 设备,
不能让两边同时控制。
三、AORUS RTX 5090 Edge View:GPU 板载 I²C
3.1 Windows 软件里找到的协议,为什么不能直接照搬
AORUS GeForce RTX 5090 MASTER ICE 32G 官方页面
说明 Edge View 可以显示 GPU 信息、文字、图片和 GIF,官方控制入口是 Windows 下的 GCC。
第一轮从当前 GCC 的 GvLcdExApi 路径中整理出了一套看起来很完整的新协议:
- I²C 地址
0x76 - 画布
320×170 - RGB565 大端
- 命令包含
0x15、0x10、0x16、0x21、0x24 - Windows DLL 通过 NvAPI internal I²C port 1 访问
问题是:Linux 下在 NVIDIA 导出的几个 adapter 上探测 0x76,全部返回 EIO。
这不是权限、i2c-dev 或 adapter 选择问题,而是协议代际选错了。社区项目
albancreton/aorus-master-linux
正好针对同型号显卡做了 clean-room 实现:该卡实际走旧 GvLcdApi 协议,地址是 0x61。
切换到旧协议后,状态探测立即返回:
1 | 4e 4e 4e 01 00 00 00 00 |
之后静态图一次写入成功,实机屏幕也确认生效。这里的教训是:
同一版厂商软件可以同时携带多代设备协议。找到一套“结构完整”的协议,并不等于当前硬件真的使用它。
应优先寻找同一 subsystem ID、同一板卡型号的实机证据。
3.2 Linux 下的 I²C 从哪里来
显卡副屏并不是主板 I²C 设备。NVIDIA 专有驱动会把显卡上的 I²C 端口注册到 Linux I²C framework;
NVIDIA 的 Linux 驱动文档
也描述了这一机制。
先加载字符设备模块:
1 | sudo modprobe i2c-dev |
然后按 adapter 名称找设备:
1 | for node in /sys/class/i2c-dev/i2c-*; do |
关键点是匹配类似:
1 | NVIDIA i2c adapter 1 at 1:00.0 |
而不是把 /dev/i2c-9 一类编号写死。I²C bus number 是内核动态分配的,重启、驱动变化或增加别的设备后都可能改变。
可靠工具应该遍历 /sys/class/i2c-dev/,按 name 前缀和目标 PCI BDF 选择。
3.3 不要直接 i2cdetect 扫整张显卡
显卡 I²C 上还可能挂 RGB 控制器、EEPROM、DDC 等设备。盲扫或对每个地址发 SMBus quick write
既没有必要,也不够安全:
- NVIDIA adapter 对 zero-length quick write 的支持并不可靠。
- 未连接的 DDC 端口也可能产生误 ACK。
- 扫描会碰到与 LCD 无关的控制器;这次明确避免写入 RGB 地址
0x71。
安全探测策略是只访问已知 LCD 地址 0x61,发送旧协议的状态命令 EB 03,读取 8 字节响应;
只有响应通过校验,工具才开放任何写操作。
推荐工作流:
1 | sudo aorus-lcd probe |
最后一条把屏幕恢复到原厂 Chibi 模式。
3.4 旧协议的数据结构
面板画布为 320×170,像素是小端 RGB565,逐行排列:
1 | def rgb888_to_rgb565_le(r: int, g: int, b: int) -> bytes: |
通用命令先放 opcode,再放固定 magic,最后补零到 256 字节:
1 | [opcode] [CB 55 AC 38] [parameters ...] [zero padding ...] |
一次静态图片上传的大致顺序是:
1 | F2 BEGIN |
已知模式:
| mode | 含义 |
|---|---|
| 0–2 | 内置模式 |
| 3 | 静态图片 |
| 4 | 文本 |
| 5 | GIF |
| 6 | Chibi |
静态图 framebuffer 目标地址为 0x01300000,文本为 0x01320000,GIF live buffer 为0x00000000。GIF 需要先切到 mode 5 再连续推帧;静态图片和文本则在上传完成后切换模式。
一次 320×170 静态图写入会产生数百次 I²C transaction。中途出错时应立即停止,而不是跳过错误继续写;
也不要把屏幕写入和显卡功耗、风扇、VBIOS 控制混在一个工具里。
3.5 最小集成边界
社区项目很新,且作者当时只在同一型号显卡上验证过。集成时建议固定到自己审过的 commit,
避免上游更新后发送给显卡的字节序列在无人复核的情况下改变。本文验证的是:
1 | b7f1fa8c21971306c937f609c1374632bd18a2dd |
最小实现只需要四个边界:
1 | find_adapter() 按 sysfs 名称找到 NVIDIA adapter,不硬编码 bus number |
Python 参考实现只依赖 Pillow 和 smbus2,也可以直接把这四层改写到已有程序中。静态图片写完后设备会保留内容,
所以不需要守护进程、定时任务或额外的系统集成。
OpenRGB 目前也没有完成对 Edge View LCD 的控制。相关讨论可见
OpenRGB issue #2363;
所以现阶段直接使用针对这张卡协议的专用小工具,反而更容易审计。
四、验证:不能只看命令退出码
我把验证拆成四层:
4.1 纯协议单元测试
ASUS 侧覆盖:
- 已知 payload length 对应的 CRC16。
- 固定 36 字节握手包自身的 CRC32。
- 模式包与抓包结果逐字节相等。
- JPEG 长度为 32 的倍数、SOI/EOI 未被破坏。
- payload 声明长度和实际布局一致。
AORUS 社区工具自带的 self-test 共 9 项,全部通过后才进行真机探测。
4.2 只读探测
- USB:确认 VID/PID、握手响应长度、CRC、固件字段、画布字段。
- I²C:只向
0x61发状态查询,并验证 8 字节响应。
4.3 真机写入和肉眼确认
两块屏都分别写入一张容易辨认的测试图。命令成功不代表物理屏真的刷新,因此以实际显示结果作为验收条件。
4.4 回滚和系统健康
- 水冷屏恢复默认演示画面,持续写入进程保持运行。
- 显卡屏切回 mode 6 原厂 Chibi,写入工具退出。
- 检查内核日志中没有 NVIDIA
Xid/NVRM错误。 - 确认显卡仍能进入空闲 P-State,温度、功耗和风扇行为正常。
这一步很重要:能写亮一次只证明协议大致正确;能够恢复、重启后状态合理、GPU 没产生错误,
才说明整个操作闭环了。
五、几个值得保留的工程习惯
5.1 设备编号永远不是身份
USB bus/device number、/dev/i2c-N 都会漂移。应该匹配:
- USB:VID/PID,必要时再加 interface 特征。
- I²C:sysfs adapter name + PCI BDF。
只有在一次性诊断输出里可以展示当前编号,不应把它写进持久配置。
5.2 先证明“这是我的设备”,再开放写入
写屏工具应有明确的 probe 阶段:
- 地址精确匹配。
- 响应长度精确匹配。
- magic、CRC 或状态字精确匹配。
- 任一检查失败就拒绝写入。
这比“扫描到有 ACK 就开始发包”安全得多。
5.3 把恢复动作当作功能的一部分
至少要保留:
- ASUS:能够停止写循环、重新握手,以及在设备已被其他进程占用时给出明确错误。
- AORUS:保留
mode 6恢复 Chibi;静态内容写完后直接释放 I²C 设备。
5.4 原生面板规格和协议画布要分开
商品页面写的是物理 LCD 规格,USB/I²C 握手或协议里写的是控制器接收格式。
它们可能相同,也可能经过缩放。代码应以实际设备响应为准,文章则把两类数字分别标注,
不能为了“看起来一致”强行改掉其中一个。
六、最终成果
这次两块副屏都不再依赖 Windows 常驻控制软件:
- ASUS 水冷 LCD:Linux 原生 USB 控制,图片预处理、握手、CRC、JPEG 对齐和持续推帧全部跑通。
- AORUS RTX 5090 Edge View:通过 NVIDIA I²C adapter 原生控制,静态图、文本、GIF 和原厂模式切换链路跑通。
- 两套工具都避免硬编码动态设备编号,并在写入前做设备级校验。
- 测试完成后,水冷屏恢复默认演示画面,显卡屏恢复 Chibi;显卡屏不需要任何常驻写入进程。
整个过程最有价值的结论不是某条十六进制命令,而是这条排障路径:
先确定总线,再确定设备身份,随后确定协议代际,最后才谈图像格式和上传性能。
如果顺序反过来,很容易对着一套完全正确、但不属于当前硬件的协议调上好几天。