背景#
手里有台闲置多年的云犀 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) 的切换逻辑。
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:
[ 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 执行环境。
- SBL1 首先初始化 DDR 内存,然后加载并验证后续启动阶段所需的组件:
- 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 文件。
$ 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)。
#!/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>
的设备树
[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 文件即为:
/ {
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 屏幕初始化信息,点亮这块屏幕就变得容易了。
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 博客
Android 设备开机日志分析_挂载安卓开机 log-CSDN 博客
转载 - Qualcomm MSM8953 启动流程:PBL-SBL1-(bootloader) LK-Android - liangliangge - 博客园