Yunxi Box 3.0 Pro Hacks: From Restricted to Rooted

背景

手里有台闲置多年的云犀 Box 3.0 Pro 触屏导播一体机，这台设备支持双路 1080P  HDMI 输入、内置抖音、淘宝、快手直播及 RTMP 推流直播功能。但我们之前的使用场景比较特殊，有些平台以前没法方便地进行 RTMP 推流，更多还是采用电脑连接多张 USB 采集卡和 OBS Studio 虚拟摄像头的方案，因此这个设备闲置多年无人问津。

最近，我正好在开发类似定位的硬件产品，于是开始重新审视这台设备。一方面，希望学习和了解其软硬件方案；另一方面，也期望通过软件改造，让其独特的硬件能在其他场景中发挥作用。

硬件

拧下藏在背面防滑垫下的螺丝，就能轻松打开后盖。主板的设计相对简洁，元器件只布局在一面，并且采用了核心模组 + 底板的形式。

对于这种 PCB 空间充裕的项目来说，使用核心板或核心模组确实有很多优势，一方面高频的 RF 部分、高速的 DDR 内存、存储等最复杂的设计都集成在核心模组上，大大减少了硬件设计和调试的工作量，有助于产品更快推向市场；另一方面底板可以采用更低的层数和更简单的工艺，将高多层板限制在最小面积，有效控制成本。

从拆解图中可以看到云犀 Box 3.0 Pro 的主要芯片方案：

核心模组：移远 (Quectel) SC60 模组，搭载高通骁龙 625 (MSM8953) SoC，2GB RAM 和 16GB eMMC。

以太网：亚信 (ASIX) AX88179 USB 3.0 to Gigabit Ethernet

MIPI-DSI to HDMI Tx 桥接：龙讯 (Lontium) LT8912B

HDMI Rx to MIPI-CSI 桥接：东芝 (Toshiba) TC358840XBG *2

电源管理：德州仪器 (TI) BQ25890

USB HUB：创惟 (Genesys Logic) GL3523

起初，我猜测它采用了最常见的瑞芯微 RK3399 方案，但拆机后才发现是高通方案。仔细想想，考虑到直播设备可能需要通过移动网络进行推流，具有基带优势的高通平台就有了很大的吸引力，选择高通方案也就合情合理了。

对于其核心功能 HDMI 采集，云犀 Box 使用了两颗东芝 TC358840XBG 桥接芯片来实现两路信号采集。TC358840XBG 虽然经典，但也有些局限，它需要 Dual Link MIPI-CSI-2 一共 8 Lane 才能实现 4K 30fps 的视频采集（两个 Link 分别采集 4K 画面的左右两半），像云犀 Box 这样使用 Single Link CSI-2 则只能支持到 1080p 采集。相比之下，现在更新的芯片，例如龙讯的 LT6911UXE，其 MIPI 每条 Lane 的最高速率可达 2.5Gbps，仅用一个 4-Lane 的 MIPI 端口就能支持 4K 60fps 采集，若要进行 4K 采集方案会简洁不少。不过 TC358840XBG 有开源驱动可用，对个人开发者友好，而 LT6911UXE 的公开资料目前非常稀少。

在硬件设计上，可以感受到 MSM8953 的外设限制给设计带来了一些挑战。由于 MSM8953 没有内置以太网 MAC，也没有 PCIe 接口，要实现有线网络接入，基本只能依赖 USB 网卡。然而，MSM8953 只有一个 USB 3.0 接口。这就带来了一系列连锁反应：一方面，为了同时支持一个 USB 网卡和一个外部 USB 3.0 Type-A 接口，必须引入 USB HUB 芯片 (GL3523)。另一方面，主要作 Device 用的 Type-C 接口和作 Host 用的 USB HUB 需要使用 USB 开关芯片进行信号物理连接的切换，对应主板上的 PI3USB302 和 TS3USB221A。这意味着一旦系统切换到 Host 模式（连接网卡和 Type-A 口），Type-C 接口就完全被断开了，依赖这个接口的 ADB、Fastboot 等功能也就无法使用了。

软件

从设备界面可以看出云犀 Box 运行的是安卓系统。相比于纯 Linux 方案，采用安卓系统能利用其对硬件的高度抽象简化开发，并且可以借助基于 Java 的成熟应用开发生态大幅提升应用开发效率。

不过这个安卓系统被深度限制了，只能运行官方的云犀 APP，无法直接启动任意应用，没有地方可以打开 ADB，下拉通知栏被锁定，系统设置等标准安卓界面也都被隐藏了。看来想通过简单的系统界面操作来解除限制并不可行。

于是制定了下一步计划：利用高通平台的 9008 紧急下载模式 (EDL) 来提取设备 eMMC 存储中的 system 分区镜像，然后挂载镜像进行修改，替换内置 App，最后重新烧录修改后的镜像。

尝试后发现，主板上预留的按键 S2 正好连接到 MSM8953 的 force_boot_from_usb 引脚 (GPIO37)。在上电时按住此键，设备就能进入 9008 模式。利用 Qualcomm Premium Tool 工具导出 system 分区镜像，之后就可以在 PC 上自由地挂载和修改这个镜像了。

由于云犀 APP 可以启动抖音、快手、淘宝直播这几个特定的第三方应用，我便将淘宝直播对应的 APK 文件替换成了修改了包名的 QuickShortcutMaker APK，进行偷梁换柱。将修改后的 system.img 通过 9008 模式刷回设备，重启后看到桌面上原 “淘宝直播” 的图标都变成了 QuickShortcutMaker 的图标。打开 QuickShortcutMaker，就可以自由地启动系统内安装的任何应用和系统组件了，包括被隐藏的安卓原生设置。
为了获得完整的系统控制权，Root 必不可少。使用 Magisk 对设备的 boot 分区镜像进行修补，为系统加入 su 命令，刷入后这个设备就顺利获得了 Root 权限。

之前担心的 USB 问题还是出现了。虽然在设置中成功开启了 USB 调试选项，但将 Type-C 口连接到电脑后仍然不能看到 ADB 设备。说明此时 USB 控制器正处于 Host 模式，用于内置的 USB 网卡和 Type-A 口。

但是想要自己强制切换模式就比较棘手了，一条可能可行但未经验证的路径是一方面在硬件上，割断 USB 切换芯片的切换控制信号线，使得 USB 信号物理上始终通向 Type-C 接口；另一方面在软件上通过将 /sys/kernel/debug/msm_otg/mode 设置为 peripheral ，使 USB 控制器工作在 Device 模式。

不过偶然发现按下主板上另一个预留按键 S3 后，USB 调试就意外地出现了，应该是厂商预留的调试手段。这下省去了我们自己进行复杂软硬件修改的麻烦。

后来为了梳理切换 USB 模式的过程，从 boot.img 中提取并反编译了设备树。在设备树的 usb_detect 节点中，发现 qcom,gpio-usbdetect 相比 Example ，还额外定义了一个 key-gpio，其 GPIO 编号与 vol_up 键对应，板上的 S3 按键正是对应此 vol_up 。因此厂商应该是修改了内核中的 gpio-usbdetect 驱动，使得按下 S3 按键也能触发中断，并执行 USB 开关切换以及 USB Role (Host/Peripheral) 的切换逻辑。

Copy
vol_up {
label = "volume_up";
gpios = <0xbe 0x55 0x01>;
linux,input-type = <0x01>;
linux,code = <0x73>;
debounce-interval = <0x0f>;
};

usb_detect {
compatible = "qcom,gpio-usbdetect";
interrupt-parent = <0x11e>;
interrupts = <0x00 0xc6 0x00>;
interrupt-names = "vbus_det_irq";
key-gpio = <0xbe 0x55 0x01>;
usb-switch-gpio = <0xbe 0x1c 0x00>;
};

通过观察内核日志 (dmesg)，可以看到按下 S3 前后 usb_role_switch 的相关 log：

Copy
[ 1089.480345] [yunxi_bq25890]: vbus_detect_delay_work: key_level: 0
[ 1089.480374] [yunxi_bq25890]: usb_role_switch , is_host: 1

[ 1329.977896] --== bq25890_irq_handler
[ 1339.940379] [yunxi_bq25890]: vbus_detect_delay_work: key_level: 0
[ 1339.940410] [yunxi_bq25890]: usb_role_switch , is_host: 0

HDMI 采集

HDMI 采集是这个设备最核心且独特的功能了。我的目标是能够绕过限制重重的官方云犀 APP，直接利用这一硬件能力。

TC358843XBG 芯片通过 MIPI CSI-2 接口输出视频数据，这与一般 Sensor 接入 SoC 的方式一致。不同之处在于，TC358843XBG 直接输出 YUV 或 RGB 格式的数据，而非摄像头常见的 Bayer RAW 格式。

安装开源的 Open Camera 应用后，就可以直接识别并预览到两路 HDMI 输入的画面，两路 HDMI 输入即为两个摄像头。这样一来通过 Open Camera 便直接可以进行 HDMI 输入的实时监看和视频录制。再次让人感叹还得是安卓，良好的硬件抽象带来了强大的兼容性，让很多功能无需为每个特定平台重复开发，并且在庞大生态加持下，可以直接利用现成的甚至是开源的海量应用。

通过查看 Open Camera 的调试信息，并结合对云犀官方 APP 的反编译分析，还可以确认云犀 Box 使用的是较旧的 android.hardware.Camera API (Camera1 API)，并不支持功能更强大、更灵活的 Camera2 API。

其他

启动流程

由于在分区备份写入过程中出过一些差错，我在排查故障时对 MSM8953 平台的启动流程进行了大致的了解：

上电与 Boot ROM (PBL):

按下电源键后，处理器从内部 ROM (Boot ROM) 的预定义地址开始执行，运行主引导加载程序 (PBL - Primary Boot Loader)。

PBL 读取 eMMC 等启动介质上的分区表 (GPT)，找到 sbl1 (Secondary Boot Loader 1) 或备份 sbl1bak 分区，将其加载到片上内存 (IMEM) 并跳转执行。

SBL1:

SBL1 首先初始化 DDR 内存，然后加载并验证后续启动阶段所需的组件：

tz.mbn: TrustZone 相关组件 (QSEE, HYP, TEE 等)。

devcfg.mbn: 设备配置数据。

rpm.mbn: 资源功耗管理器 (RPM) 固件，加载到 RPM 核心的内部内存 (TCM)。

aboot.mbn: 应用引导加载程序 (APPSBL)，通常是 Little Kernel (LK)。

向 RPM 发送启动信号并解除其复位状态。

通过写 RMR_EL3 请求 Warm Reset，CPU 以 AArch64 EL3 特权级重新启动，进入 TrustZone 执行环境。

QSEE/ TrustZone:

在 EL3 环境下进行安全相关的初始化。

将 CPU 切换回 AArch32 EL1，跳转到 aboot.mbn (LK) 的入口点。

Aboot (LK):

LK (little kernel)，提供 Fastboot 刷机模式

初始化必要的硬件，如 MIPI-DSI 控制器和显示面板，从 splash 分区读取 Logo 文件并显示开机画面。

加载 boot.img (正常启动) 或 recovery.img (恢复模式)，并对其进行验证。

准备好内核运行环境，切换到 AArch64 EL1，跳转到内核的入口地址。

Kernel:

识别挂载 system、vendor、userdata、persist 等分区

加载并启动其他协处理器固件，如 Modem (基带)、WCNSS (Wi-Fi/Bluetooth) 。

进入 userspace，启动用户空间的第一个进程 init，安卓启动。

设备树提取

设备树描述了硬件信息，内核启动时会读取它来配置相应的驱动程序。在高通平台上，DTB (Device Tree Blob) 通常附加在内核镜像 (zImage) 的末尾。

使用 unpackbootimg 工具从 boot.img 中分离出 zImage，然后使用从moetayuko/split-appended-dtb获取的 split-appended-dtb 工具从 boot.img 中提取出所有附加的 DTB 文件，一共提取出 69 个 DTB 文件。

Copy
$ file boot.img
boot.img: Android bootimg, kernel, ramdisk, page size: 2048, cmdline (console=ttyHSL0,115200,n8 androidboot.console=ttyHSL0 androidboot.hardware=qcom msm_rtb.filter=0x237 ehci-hcd.park=3 lpm_levels)
$ ./unpackbootimg -i boot.img -o ./
BOARD_KERNEL_CMDLINE console=ttyHSL0,115200,n8 androidboot.console=ttyHSL0 androidboot.hardware=qcom msm_rtb.filter=0x237 ehci-hcd.park=3 lpm_levels.sleep_disabled=1 androidboot.bootdevice=7824900.sdhci earlycon=msm_hsl_uart,0x78af000 buildvariant=user
BOARD_KERNEL_BASE 80000000
BOARD_PAGE_SIZE 2048
$ ls
boot.img boot.img-base boot.img-cmdline boot.img-pagesize boot.img-ramdisk.gz boot.img-zImage
$ ./split-appended-dtb boot.img-zImage
Found 69 appended dtbs, please check the output.

随后通过 dtc (Device Tree Compiler) 工具将二进制的 DTB 文件反编译为人类可读的文本格式 DTS (Device Tree Source)。

Copy
#!/bin/bash
for i in $( ls *.dtb ); do
echo decompile $i
dtc -I dtb -O dts $i -o output/$i.dts
done

设备树数量众多但只会使用一个，LK 会根据 SBL 传入的硬件 ID (如 qcom,msm-id, qcom,board-id) 来选择最匹配的一个加载。通过查看 LK 在启动过程中通过串口输出的日志信息可以发现匹配到了 ID 为 <293 8 0 0x0> 的设备树

Copy
[2110] Found an appended flattened device tree (Qualcomm Technologies, Inc. MSM8953 + PMI8950 MTP - 293 8 0 0x0)
[2120] Add DTB entry 293/00000008/0x00000000/0/10016/0/0/0/a11c281f/3e740
[2120] Device tree exact match the board: <293 8 0 0x0> == <293 8 0 0x10001>

据此找到对应的设备树 DTS 文件即为：

Copy
/ {
model = "Qualcomm Technologies, Inc. MSM8953 + PMI8950 MTP";
compatible = "qcom,msm8953-mtp\0qcom,msm8953\0qcom,mtp";
qcom,msm-id = <0x125 0x00>;
interrupt-parent = <0x01>;
qcom,board-id = <0x08 0x00>;
qcom,pmic-id = <0x10016 0x00 0x00 0x00>;
...
};

从 DTS 文件中可以获取非常丰富的信息，包括特殊的 GPIO 分配、屏幕初始化序列等。对于进行系统移植非常有价值，比如有了下面的 MIPI DSI 屏幕初始化信息，点亮这块屏幕就变得容易了。

Copy
qcom,mdss_dsi_yunxi_1200p_video {
qcom,mdss-dsi-panel-name = "yunxi 1200p video mode dsi panel";
qcom,mdss-dsi-panel-controller = <0x1a4>;
qcom,mdss-dsi-panel-type = "dsi_video_mode";
qcom,mdss-dsi-panel-destination = "display_1";
qcom,mdss-dsi-panel-framerate = <0x3c>;
qcom,mdss-dsi-virtual-channel-id = <0x00>;
qcom,mdss-dsi-stream = <0x00>;
qcom,mdss-dsi-panel-width = <0x4b0>;
qcom,mdss-dsi-panel-height = <0x780>;
qcom,mdss-dsi-h-front-porch = <0x32>;
qcom,mdss-dsi-h-back-porch = <0x3e>;
qcom,mdss-dsi-h-pulse-width = <0x08>;
qcom,mdss-dsi-h-sync-skew = <0x00>;
qcom,mdss-dsi-v-back-porch = <0x08>;
qcom,mdss-dsi-v-front-porch = <0x0a>;
qcom,mdss-dsi-v-pulse-width = <0x06>;
qcom,mdss-dsi-h-left-border = <0x00>;
qcom,mdss-dsi-h-right-border = <0x00>;
qcom,mdss-dsi-v-top-border = <0x00>;
qcom,mdss-dsi-v-bottom-border = <0x00>;
qcom,mdss-dsi-bpp = <0x18>;
qcom,mdss-dsi-color-order = "rgb_swap_rgb";
qcom,mdss-dsi-underflow-color = <0xff>;
qcom,mdss-dsi-border-color = <0x00>;
qcom,mdss-dsi-on-command = [39 01 00 00 01 00 02 b0 00 39 01 00 00 01 00 06 b3 14 08 00 22 00 39 01 00 00 01 00 02 b4 0c 39 01 00 00 01 00 03 b6 3a d3 39 01 00 00 01 00 02 b7 00 39 01 00 00 01 00 07 b8 07 90 1e 00 1e 32 39 01 00 00 01 00 07 b9 07 82 3c 00 3c 87 39 01 00 00 01 00 07 ba 07 9e 20 00 20 8f 39 01 00 00 01 00 19 ce 7d 40 43 49 55 62 71 82 94 a8 b9 cb db e9 f5 fc ff 01 38 02 02 44 24 01 39 01 00 00 01 00 02 c0 ff 39 01 00 00 01 00 25 c1 04 61 00 20 8c a4 d6 ff ff ff ff 7f 73 ef b9 f6 ff ff ff ff bf 54 22 02 00 00 00 00 00 00 62 03 00 22 00 01 39 01 00 00 01 00 0a c2 31 f7 80 00 09 00 04 00 00 39 01 00 00 01 00 04 c3 00 00 00 39 01 00 00 01 00 0d c4 70 00 00 00 00 00 00 00 00 05 04 00 39 01 00 00 01 00 15 c6 6f 73 75 0a 14 0b 16 00 00 00 00 00 00 00 00 00 00 1c 17 09 39 01 00 00 01 00 1f c7 08 0e 19 22 31 3f 49 58 3c 44 4e 5b 64 6c 76 04 0c 13 1e 2d 3d 47 58 3c 44 50 5d 66 6e 76 39 01 00 00 01 00 14 c8 01 00 00 00 00 fc 00 00 00 00 00 fc 00 00 00 00 00 fc 00 39 01 00 00 01 00 0d cb 3f c0 0f f0 03 00 03 00 03 00 00 c0 39 01 00 00 01 00 02 cc 11 39 01 00 00 01 00 06 d0 44 81 bb 15 94 39 01 00 00 01 00 1a d3 0b 33 bf bb b3 33 33 37 00 01 00 a0 98 a0 00 39 39 33 3b 37 72 07 3d bf 99 39 01 00 00 01 00 03 44 07 7f 39 01 00 00 01 00 06 2a 00 00 04 af 00 39 01 00 00 01 00 06 2b 00 00 07 7f 00 39 01 00 00 01 00 02 d6 01 39 01 00 00 01 00 02 3a 77 39 01 00 00 01 00 02 2c 00 39 01 00 00 01 00 02 51 e6 39 01 00 00 01 00 02 53 24 39 01 00 00 01 00 02 55 01 05 01 00 00 32 00 02 29 00 05 01 00 00 78 00 02 11 00 39 01 00 00 32 00 02 35 00];
qcom,mdss-dsi-off-command = [05 01 00 00 32 00 02 28 00 05 01 00 00 78 00 02 10 00];
qcom,mdss-dsi-on-command-state = "dsi_lp_mode";
qcom,mdss-dsi-off-command-state = "dsi_hs_mode";
qcom,mdss-dsi-h-sync-pulse = <0x01>;
qcom,mdss-dsi-traffic-mode = "burst_mode";
qcom,mdss-dsi-lane-map = "lane_map_0123";
qcom,mdss-dsi-bllp-eof-power-mode;
qcom,mdss-dsi-bllp-power-mode;
qcom,mdss-dsi-lane-0-state;
qcom,mdss-dsi-lane-1-state;
qcom,mdss-dsi-lane-2-state;
qcom,mdss-dsi-lane-3-state;
qcom,mdss-dsi-panel-timings = <0xf23a2800 0x6c6e2c3e 0x2e030400>;
qcom,mdss-dsi-t-clk-post = <0x02>;
qcom,mdss-dsi-t-clk-pre = <0x2d>;
qcom,mdss-dsi-bl-min-level = <0x01>;
qcom,mdss-dsi-bl-max-level = <0xfff>;
qcom,mdss-dsi-dma-trigger = "trigger_sw";
qcom,mdss-dsi-mdp-trigger = "none";
qcom,mdss-dsi-bl-pmic-control-type = "bl_ctrl_pwm";
qcom,mdss-dsi-reset-sequence = <0x01 0x14 0x00 0x02 0x01 0x14>;
qcom,mdss-dsi-panel-timings-phy-v2 = <0x241f0809 0x50304a0 0x241f0809 0x50304a0 0x241f0809 0x50304a0 0x241f0809 0x50304a0 0x241c0809 0x50304a0>;
qcom,mdss-dsi-bl-pmic-pwm-frequency = <0x64>;
qcom,mdss-dsi-bl-pmic-bank-select = <0x00>;
qcom,mdss-dsi-pwm-gpio = <0x1a5 0x04 0x00>;
qcom,panel-supply-entries = <0x1a3>;
linux,phandle = <0x1a9>;
phandle = <0x1a9>;
};
};

参考

云犀 BOX - 视界，精彩一触即播，国内领先触屏直播硬件，让直播导播更简单 - 云犀直播

高通平台 dtb 文件的加载过程_高通 dump dts 文件 - CSDN 博客

我的 4g 网卡运行着 GNU/Linux -- 某 4g 无线网卡的逆向工程与主线 Linux 移植 (一)_fatal error: blob has incorrect magic number-CSDN 博客

利用 9008 刷写 / 备份单分区 - 哔哩哔哩

高通芯片启动流程 - asges 林 - 博客园

设备刷机需要的软件 | 宁宁's Blog

Android 设备开机日志分析_挂载安卓开机 log-CSDN 博客

转载 - Qualcomm MSM8953 启动流程：PBL-SBL1-(bootloader) LK-Android - liangliangge - 博客园

Android 启动加载器分析 —— Aboot - 知乎