使用杰理 AC7925B 实现 Windows电脑 USB 副屏

记录使用杰理 AC7925B 实现 Windows电脑 USB 副屏的开发过程

本文记录将一块运行 WiFi 音箱应用(wifi_soundbox)的杰理 AC7925 开发板改造为 Windows USB 副屏的完整过程,包括方案选型、分阶段实现、实测数据,以及开发中遇到的主要问题和解决方法。

本项目为个人学习用途,非商业用途。文中使用的 USB VID/PID 和 Windows 驱动来自第三方开源项目,来源详见第二节。

一、项目概述

1.1 目标

开发板通过一根 USB 线接入 Windows 电脑后,在系统中作为一块独立的扩展显示器使用。同时支持触摸操作,并作为 USB 声卡(扬声器与麦克风)工作。

1.2 软硬件平台

项目 规格
主控 杰理 AC7925B,双核 SMP,320 MHz
内存 8 MB DDR
USB USB 2.0 高速(480 Mbps)
显示屏 4.3 寸 800×480 RGB 屏(ST72611),24 位接口,板上接高 16 位,按 RGB565 送屏,刷新率约 56.7 Hz
触摸 GT911 电容触摸
SDK 杰理 AC792N SDK,wifi_soundbox 应用,LVGL v8
电脑端 Windows 10/11

1.3 最终效果

指标 实测结果
帧率 拖动窗口时电脑端发送 58--60 fps,屏幕刷新 55--56 fps(受屏幕 56.7 Hz 刷新率限制);播放 30 fps 视频时为 30 fps
解码耗时 硬件 JPEG 解码平均 11 ms/帧,最大 13 ms
画质 JPEG 质量等级 9(驱动支持的最高画质),静止桌面每帧约 67 KB
CPU 占用 60 fps 时 CPU0 约 5%、CPU1 约 15%
剩余内存 副屏工作时约 800 KB
功能 扩展显示、HID 触摸、UAC 扬声器与麦克风,同一个 USB 复合设备
电脑端开发量 无,使用现成的已签名 IDD 驱动

二、参考项目与驱动来源

2.1 参考项目

项目 地址 在本项目中的作用
乐鑫 esp-iot-solutionusb_extend_screen 例程 https://github.com/espressif/esp-iot-solution/tree/master/examples/usb/device/usb_extend_screen 主要参考 。USB 描述符结构、能力字符串格式、16 字节帧头、复合设备的接口顺序均与该例程保持一致,以便直接使用其配套的 Windows 驱动。帧头定义参考 main/app_vendor.c,描述符参考 main/tusb/usb_descriptors.c
乐鑫 ESP-IoT-Solution USB Device 方案文档 https://docs.espressif.com/projects/esp-iot-solution/en/latest/usb/usb_overview/usb_device_solutions.html 方案说明与性能数据:ESP32-S3 运行 800×480 约 13 fps,ESP32-S31 约 60 fps
chuanjinpang/win10_idd_xfz1986_usb_graphic_driver_display https://github.com/chuanjinpang/win10_idd_xfz1986_usb_graphic_driver_display Windows 端驱动来源(见 2.2 节)。其文档说明了能力字符串各字段的含义,支持 RGB565、RGB888、JPEG 三种编码
xkk6663/uext_screen https://github.com/xkk6663/uext_screen 性能参照:ESP32-S31 + 800×480 RGB 屏实测 59.8 fps,单帧 JPEG 50--150 KB。用于估算带宽与缓冲大小
notro/gud、roshkins/IddSampleDriver https://github.com/notro/gud 、https://github.com/roshkins/IddSampleDriver 仅作调研。前者为 Linux GUD 协议(不支持 JPEG),后者为微软 IDD 示例,自行开发驱动时可作起点。本项目未使用

以上项目的许可条款以各仓库为准。

2.2 Windows 驱动

本项目未开发电脑端软件,直接使用 xfz1986 USB Graphic 驱动的签名版本:

项目 内容
文件 xfz1986_usb_graphic_250224_rc_sign.exe
来源 chuanjinpang/win10_idd_xfz1986_usb_graphic_driver_display 仓库发布;乐鑫usb_extend_screen 例程文档中的驱动下载地址指向同一文件
版本 INFDriverVer = 02/24/2025, 1.2.25.2
签名 Authenticode 签名有效(签名者 Airspace Intelligent Technology (Changzhou),Sectigo 签发,带时间戳),无需开启 Windows 测试模式
SHA-256 268a9f8b9e7f7ef4223ee33386e6396cc1d470026188104aad93d8b96af52501
匹配的硬件 ID USB\VID_303A&PID_2986&MI_00(复合设备,显示接口须为接口 0)、USB\VID_303A&PID_2987(仅显示)

驱动属于 Windows 间接显示驱动(IDD):在系统中创建一块虚拟显示器,抓取桌面画面并编码为 JPEG,通过 USB bulk 端点发送给设备。

关于 VID/PID :驱动按乐鑫的 VID 0x303A 和 PID 0x2986/0x2987 匹配设备,因此本项目沿用了这组 ID。这只适用于个人学习验证;如用于产品,须申请自有 VID/PID,并自行开发和签名驱动。

2.3 方案选型依据

对比乐鑫各芯片的性能数据可以看出:ESP32-S3 只有 13 fps,ESP32-S31 能达到 60 fps,差别主要在于 USB 是否为高速 以及 JPEG 是否为硬件解码,与 CPU 主频关系不大。AC7925 同时具备 USB 2.0 高速接口和硬件 JPEG 解码器,因此判断方案可行。

三、通信协议

协议与乐鑫例程保持一致。

能力字符串:设备通过 USB 字符串描述符向驱动声明自身能力,格式如下:

复制代码
ac792udisp0_R800x480_Ejpg9_Fps60_Bl195
            │        │     │     └─ 单帧上限(单位 KB)
            │        │     └─ 帧率上限
            │        └─ JPEG 质量等级(1--10)
            └─ 分辨率

帧格式:每帧由 16 字节帧头和一张完整的 JPEG 图像组成,小端序:

c 复制代码
typedef struct {
    uint16_t crc16;
    uint8_t  type;              // 3 = JPEG
    uint8_t  cmd;
    uint16_t x;
    uint16_t y;
    uint16_t width;
    uint16_t height;
    uint32_t frame_id      : 10;
    uint32_t payload_total : 22;
} __attribute__((packed)) udisp_frame_header_t;

四、系统架构

4.1 数据流

复制代码
Windows                                 USB 2.0 HS     AC7925
─────────────────────────────────────  ────────────   ───────────────────────────────────────────
IDD 驱动抓取桌面                                        [中断] EP1 OUT 收到数据,释放信号量
 → JPEG 编码(baseline,4:2:0)                         [收帧线程] 按包读取,解析帧头,拼完整帧
 → 添加 16 字节帧头                                          ↓ 3 块帧缓冲,解码不及时则丢弃最旧帧
 → bulk 写出  ───────────────────────────────────────►  [解码线程] 硬件流水线
                                                          virtual → jpeg_dec → rep → imc → disp(fb1)
                                                             ↓
                                                        [图层合成] DMA2D 将 LVGL 层 fb0 叠加在视频层 fb1 上
                                                          → RGB565 → RGB 屏(56.7 Hz)

4.2 USB 复合设备

接口 功能 端点 电脑端驱动
0 显示(vendor 类) EP1 OUT/IN,bulk xfz1986 IDD 驱动
1 触摸(HID 触摸屏) EP2 IN,中断 Windows 内置
2--4 扬声器 + 麦克风(UAC1) EP3 OUT/IN,同步 Windows 内置

设备类为 0xEF/0x02/0x01(IAD 复合设备),PID 为 0x2986。

4.3 代码组织

新增代码全部位于 sdk/user/usb_screen/,SDK 原有文件只增加少量由功能宏包裹的挂接点,便于 SDK 升级和整体回退:

目录 内容
class/ USB 显示类、HID 触摸类
app/ 收帧与解码线程、页面切换、触摸上报
bench/ 解码测速、显存导出等调试功能
tools/ 电脑端测试脚本(测试图生成、Python 发送端、显存还原)
pc_driver/ Windows 驱动安装包及说明
doc/ 可行性分析、实现计划、内存分析

SDK 中的挂接点:task_pc.c(PC 模式枚举时加入显示类)、usb_device.c(注册显示类和触摸类)、user_setup.c(控制请求处理前置副屏处理函数)、板级配置文件(功能开关)。

五、实现过程

项目按阶段推进,每个阶段先确定验收标准,达标后再进入下一阶段。

5.1 硬件 JPEG 解码性能测试

解码速度决定方案是否可行,因此作为第一步。方法是将若干张 800×480 测试图(简单桌面、密集文字、照片,不同质量与色度采样)转换为 C 数组编入固件,循环送入 SDK 的 JPEG 解码流水线,统计耗时。

测试图 大小 色度采样 平均耗时 折合帧率
简单桌面 q70 27 KB 4:2:0 11.4 ms 87.5 fps
密集文字 q85 79 KB 4:2:0 11.8 ms 84.3 fps
照片 q70 27 KB 4:2:0 11.3 ms 88.1 fps
密集文字 q70 59 KB 4:2:2 12.8 ms 78.0 fps
密集文字 q70 70 KB 4:4:4 22.6 ms 44.1 fps
progressive JPEG --- --- 不支持 ---

结论:

  • 解码耗时基本不随 JPEG 数据量变化,主要是固定开销,因此后续可以适当提高画质。
  • 电脑端须使用 baseline、4:2:0 编码;4:4:4 耗时翻倍,progressive 无法解码。
  • 送帧接口为同步调用,返回时该帧已解码完成,缓冲区可立即复用。

5.2 内存评估

启动日志显示代码在 Flash 上原地执行,不占用 DDR。8 MB DDR 中,数据段与 BSS 约 1.5 MB,其余为堆。

占用最大的是三组显示缓冲:

缓冲 大小 说明
fb0:LVGL 界面层,双缓冲 2 × 1125 KB 800×480×3 字节,ARGB8565
LCD 送屏缓冲,双缓冲 2 × 750 KB 800×480×2 字节,RGB565
fb1:副屏视频层,双缓冲 2 × 750 KB YUYV,副屏工作时分配

三者合计约 5.1 MB,占 DDR 的 64%。剩余空间用于收帧缓冲和解码流水线。

5.3 USB vendor 类与吞吐测试

参照 SDK 中打印机类(printer.c)的结构实现了一个 vendor 类,包含一个 0xFF 接口和一对 bulk 端点。设备描述符和能力字符串由 setup 钩子返回,其余请求交由 SDK 原有 USB 协议栈处理。具体实现见 6.3 节。

测试时电脑端用 Zadig 安装 WinUSB,Python(pyusb)发送 64 MB 带序号的数据,设备端逐块校验:

  • 吞吐 14 MB/s,64 MB 数据无错误;
  • 800×480 JPEG 在 60 fps 下最多需要约 5 MB/s,带宽余量约 3 倍。

5.4 收帧、解码与显示

此阶段暂不接入 IDD 驱动,先用自行编写的 Python 发送端(发送测试图或抓取屏幕)联调设备端链路,以便区分设备端问题和驱动问题。

  • 线程:收帧线程与解码线程分离。
  • 缓冲策略:3 块帧缓冲,分别用于接收、解码和排队;解码不及时时丢弃最旧的帧、保留最新帧,避免显示延迟累积。
  • 输入校验 :帧头按外部输入严格校验(类型、尺寸、长度上限、JPEG 首尾标记 FFD8/FFD9),异常帧丢弃并重新同步。
  • 显示切换:在 UI 工程中新建一个全透明页面,收到第一帧时切换到该页面,使下层视频图层可见;断开时切回原页面。

调通后,30 fps 测试图下收帧、解码、显示均为 29 fps,无错误。收帧、缓冲池、解码和页面切换的具体实现见 6.4--6.7 节。

5.5 接入 Windows IDD 驱动

卸载 WinUSB 后安装签名驱动,Windows「显示设置」中出现一块 800×480 显示器。通过设备端日志确认了驱动的实际行为:

  • 驱动读取 iProduct(字符串索引 2) 获取能力字符串,与乐鑫例程中使用 iInterface 的做法不同,因此两处均返回同一字符串;
  • Bl 字段单位为 KB;乐鑫例程中填写的是字节数,按 KB 解释相当于不设上限;
  • 驱动只发送整帧,不发送局部更新;画面无变化时不发送,静止时约每秒补发一帧。

5.6 性能与画质优化

  • 帧率上限由 30 提高到 60 后,屏幕刷新达到 55--56 fps,已接近屏幕刷新率上限。
  • 曾怀疑 LVGL 层与视频层的 DMA2D 合成是瓶颈,因此实现了「副屏期间跳过透明 UI 层合成」的对照版本。A/B 测试显示两者帧率差异在误差范围内,故撤回该改动,避免为不明显的收益修改 SDK。
  • JPEG 质量等级最终定为 9,原因见 7.4 节。

5.7 触摸

将设备改为复合设备,接口 0 为显示,接口 1 为 HID 触摸屏:

  • 复用 SDK 中的 hid.c,通过 hid_set_report_desc() 替换为单点触摸屏报告描述符(Digitizer / Touch Screen);
  • 不修改触摸芯片驱动,由独立线程每 10 ms 读取一次 SDK 触摸驱动的最新坐标,仅在显示电脑画面期间且坐标或按下状态变化时上报;
  • Windows 默认将新增触摸屏映射到主显示器,需要在「控制面板 → 平板电脑设置 → 显示 → 设置 → 触控输入」中绑定到副屏,仅需操作一次。乐鑫例程文档也说明需要这一步。

5.8 并入 PC 模式,与 USB 声卡共存

wifi_soundbox 应用中,蓝牙模式和 PC 模式是互相切换的关系,进入 PC 模式时才枚举 USB 扬声器和麦克风。副屏最初实现为插入即单独枚举,与 PC 模式冲突。最终改为与声卡相同的方式:进入 PC 模式时,在原有的类组合上加入显示类和触摸类,一并枚举。

为满足端点数量,USB 类配置中去掉了大容量存储(MSC)和虚拟串口(CDC),仅保留扬声器和麦克风。端点分配为:EP1 显示,EP2 触摸,EP3 声卡。

六、实现细节

本章按模块说明设备端的具体做法,便于在其他杰理芯片或其他屏幕上复现。文中 UsbDisp_*、UsbScreen_*、UsbTouch_* 为本项目新增的函数,其余为杰理 SDK 原有接口。

6.1 SDK 改动一览

副屏代码全部在 sdk/user/usb_screen/ 下,对 SDK 原有文件的改动只有以下几处,均由 TCFG_USB_SCREEN_ENABLE 宏包裹:

文件 改动 目的
sdk_config.h USB_DEVICE_CLASS_CONFIG_2_0 改为 `SPEAKER_CLASS MIC_CLASS`
board_develop_AC7925B.h 新增TCFG_USB_SCREEN_* 功能开关;开启触摸时强制 TCFG_USB_SLAVE_HID_ENABLE 1;类配置中含 MSC/CDC/HID 时 #error 功能开关与端点冲突检查
task_pc.c 的 usb_start() 高速口按默认类配置启动(class == 0)时,类组合改为 `USB_DEVICE_CLASS_CONFIG_2_0 USB_DISPLAY_CLASS`
usb_device.c 的 usb_device_mode() 在登记其他类之前先登记显示类和 HID 类;释放分支中调用UsbDisp_Release() 保证显示为接口 0、触摸为接口 1
user_setup.c 的 user_setup_filter() 函数开头先调用UsbDisp_SetupFilter(),返回非 0 即视为已处理 返回自定义设备描述符、能力字符串和触摸 Feature 报告
Makefile INCLUDES 加入副屏头文件目录;c_SRC_FILES 加入副屏源文件和 GUI Builder 生成的 setup_scr_usb_screen.c 编译
lcd_rgb_st72611_800x480.c LCD_ST72611_DCLK_NEGATIVE_EN 改为 0 修正屏接口采样时序(见 7.5 节)
lv_conf.h wifi_soundbox 的LV_MEM_SIZE 改为 512 KB 为副屏腾出内存(见 7.12 节)
app_msg.h APP_MODE_NET 移到 APP_MODE_BT 之前 修复模式循环(见 7.11 节),与副屏无关

USB_DISPLAY_CLASS 是新增的类位,取值 0x400(SDK 已用到 MIDI_CLASS 0x200)。

6.2 配置参数

板级开关(board_develop_AC7925B.h):

宏 取值 说明
TCFG_USB_SCREEN_ENABLE 1 副屏总开关
TCFG_USB_SCREEN_TOUCH_ENABLE 1 触摸,依赖 SDK 触摸驱动
TCFG_USB_SCREEN_DEC_BENCH_ENABLE 0 硬件解码测速
TCFG_USB_SCREEN_BULK_BENCH_ENABLE 0 bulk 吞吐测试,置 1 时替代正式的收帧解码
TCFG_USB_SCREEN_PIXDUMP_ENABLE 0 显存导出(画质排查)

显示参数(class/usb_disp_class.h):

宏 取值 说明
UDISP_SCREEN_W/H LCD_W/LCD_H 跟随lcd_board_cfg_template.h 选中的屏,换屏无需修改副屏代码;未定义时 #error
UDISP_JPEG_QUALITY 9 能力字符串中的Ejpg
UDISP_MAX_FPS 60 能力字符串中的Fps
UDISP_FRAME_LIMIT_BYTES 宽 × 高 × 0.52 单帧上限,也是每块帧缓冲的大小;800×480 时为 199680 字节,即Bl195
UDISP_STR_INDEX 9 能力字符串的 iInterface 索引,避开 SDK 已用的 5--8
UDISP_USB_PID 0x2986 / 0x2987 带触摸或声卡时为复合设备 0x2986,否则 0x2987

6.3 USB 显示类

描述符

  • 设备描述符:bcdUSB 2.00;复合设备时 class/subclass/protocol 为 0xEF/0x02/0x01,与 SDK PC 模式处理复合设备的方式一致,仅显示时为全 0;VID 0x303A;iProduct 指向字符串 2。
  • 接口描述符:bInterfaceClass 0xFF,subclass/protocol 为 0,两个端点,iInterface 为 9;接口号在登记时按顺序填写。
  • 端点描述符:EP1 IN 与 EP1 OUT,均为 bulk,包长按枚举速度填写(高速 512 字节,全速 64 字节)。

配置描述符无需整体替换。SDK 通过 usb_add_desc_config() 依次调用各类的描述符回调来拼接配置描述符,显示类只需提供输出接口和端点描述符的回调 UsbDisp_DescConfig(),并在其中用 usb_set_interface_hander()、usb_set_reset_hander() 安装接口请求和总线复位的处理函数。

能力字符串

类登记时按配置参数用 snprintf 生成 ASCII 字符串,再展开为 UTF-16LE 格式的字符串描述符(每个字符低字节为 ASCII 码、高字节为 0)。字符串描述符长度字段为 8 位,因此字符数上限为 62。

控制请求

UsbDisp_SetupFilter() 由 SDK 的 user_setup_filter() 最先调用,运行在 USB 中断上下文:

  1. 显示类未登记或不是高速口时直接返回 0,不影响其他模式。
  2. GET_DESCRIPTOR(DEVICE) 返回自定义设备描述符;GET_DESCRIPTOR(STRING) 索引为 2 或 9 时返回能力字符串。返回长度按主机请求的 wLength 截断。
  3. 其余请求交给触摸处理函数(见 6.8 节);仍未处理的返回 0,由 SDK 原有逻辑处理(主机类型识别、扬声器/麦克风字符串等)。

端点与中断

  • 登记时用 usb_alloc_ep_dmabuffer() 分配端点 DMA 缓冲:OUT 端点 2 × 512 字节,IN 端点 512 字节(该接口保证 DMA 要求的 64 字节对齐)。
  • 总线复位回调中用 usb_g_ep_config() 配置两个端点,用 usb_g_set_intr_hander() 安装 OUT 端点中断,再用 usb_enable_ep() 使能。每次总线复位后端点都会重新配置。
  • OUT 端点中断只释放一个信号量,不读数据,读取由收帧线程完成。

释放

退出 PC 模式时 SDK 调用 usb_device_mode(usb_id, 0),其中调用 UsbDisp_Release():先清除 is_active 标志,再释放一次信号量,唤醒可能正在等待数据的线程,最后释放 DMA 缓冲。信号量只创建一次、不删除,避免释放时与等待线程竞争。

6.4 收帧

根据 7.1 节确认的 usb_g_ep_read 行为,读取只能「整包读」或「按精确剩余长度读」。收帧线程的流程如下:

  1. 用 usb_read_rxcount() 查询当前已到达的包长。为 0 时等待端点信号量(超时 20 ms)后重新查询。信号量计数可能多于实际包数,因此每次醒来都以查询结果为准。
  2. 以恰好等于包长的长度非阻塞调用 usb_g_bulk_read(),完整读出这一包。主机每帧用一次 bulk 传输发出,帧头位于该传输第一个包的开头。
  3. 按字节解析 16 字节帧头(不依赖结构体打包与对齐):偏移 12 处的 32 位字中,低 10 位为 frame_id,高 22 位为 payload_total。
  4. 校验与分流:
    • 首包中帧头之后的数据量大于 payload_total:判定为失步(读到的是某帧中间的包),丢弃该包,等待下一个帧头;
    • 类型不是 JPEG:按长度读掉载荷,保持包边界同步;
    • 坐标或宽高超出屏幕:判定为坏帧头;
    • 长度超过帧缓冲、但载荷以 FFD8 开头:判定为超长帧,按长度整帧读掉,不逐包按坏帧头处理,以免长时间失步。
  5. 从帧缓冲池取一块缓冲,拷入首包中帧头之后的载荷,再阻塞读取精确的剩余长度。
  6. 检查载荷首尾是否为 FFD8 / FFD9。主机中途中断一帧时,按长度读取会把下一帧的开头拼进来,结尾不会是 FFD9,此时丢弃该帧。
  7. 校验通过后放入待解码队列,并释放「待解码」信号量。

线程优先级:收帧 20,解码 19,触摸 18。

6.5 帧缓冲池

  • 共 3 块,每块 UDISP_FRAME_LIMIT_BYTES。首次连接电脑时分配,之后不释放,避免反复分配大块内存造成碎片。
  • 数据结构为「空闲栈 + 待解码 FIFO」,由互斥锁保护;待解码帧数通过信号量通知解码线程。
  • 收帧线程取缓冲时优先取空闲块;没有空闲块时,取走待解码队列中最旧的一帧(计为丢帧)。
  • 解码线程只取最新的一帧,更旧的直接回收。
  • 收帧最多占用 1 块、解码最多占用 1 块,因此 3 块时收帧线程总能取到缓冲。

6.6 解码与显示

流水线搭建

使用 SDK 的 pipeline 框架,插件链与 apps/common/video/net_avi_player/pipeline_jpeg_dec.c 相同:

复制代码
virtual(软件送帧源)→ jpeg_dec(硬件 JPEG 解码)→ rep → imc(硬件缩放与格式转换)→ disp(写入视频图层 fb1)
  • 用 plugin_factory_find() 查找插件,pipeline_filter_add() 加入,pipeline_filter_link() 依次连接,再 pipeline_prepare()、pipeline_start()。
  • PIPELINE_SET_FORMAT 参数:type 为 VIDEO_BUF_TYPE_VIDEO_PLAY;pixelformat 为 VIDEO_PIX_FMT_JPEG 加上该 JPEG 的色度采样格式;源宽高取自 JPEG;显示窗口为全屏 (0, 0, LCD_W, LCD_H);combine 必须为 1(见 7.3 节)。
  • PIPELINE_SET_BUFFER_LINE 设为 16,即解码器每 16 行输出一次。

按需重建

每帧先用 jpeg_get_sof_marker() 确认是 baseline JPEG,再用 jpeg_decode_image_info() 读取宽高和色度采样。如与当前流水线的参数不同,或流水线尚未建立,则关闭后按新参数重建。驱动发送的帧参数是固定的,因此正常情况下流水线只在首帧建立一次。局部更新帧(坐标不为 0 或尺寸不等于全屏)只计数、不解码;所用驱动不发送局部帧。

送帧

调用 pipeline_param_set(pipe, NULL, VIDIOC_RDBUF, &buffer),buffer 指向帧缓冲中的 JPEG 数据。该调用为同步调用,返回时这一帧已写入 fb1,帧缓冲可以立即回收。

图层合成

fb1 由 disp 插件在流水线启动时分配。SDK 的 fb_combine 模块用 DMA2D 把 LVGL 图层 fb0 叠加在 fb1 之上,输出 RGB565 送屏。因此只要 LVGL 当前页面完全透明,屏幕上显示的就是 fb1 的内容。

会话结束

解码线程每 200 ms 超时醒来一次,检查显示类是否已释放(退出 PC 模式或拔线)。检测到释放后:关闭流水线(fb1 随之释放)、切回原页面、回收所有待解码帧。

6.7 UI 页面切换

  • 在 GUI Builder 中新建页面 usb_screen:只有一个 Screen,背景不透明度 bg_opa 为 0,不放任何控件。新建页面的注意事项见 7.2 节。
  • LVGL 接口只能在 LVGL 线程中调用。解码线程用 SDK 的 lvgl_rpc_post_func() 把页面切换函数投递到 LVGL 线程执行。
  • 进入:收到第一帧并建立流水线后,调用 GUI Builder 生成的 ui_load_scr_anim() 加载 usb_screen 页面,参数与生成代码中的页面跳转一致:无动画、删除原页面对象、原页面压入页面栈。
  • 退出:只在当前页面仍是 usb_screen 时处理(已被其他模块切走则不干预)。页面栈非空时弹栈回到原页面,否则回到音乐播放页。
  • 「正在显示电脑画面」标志为 volatile 布尔量,由解码线程写、触摸线程读。

6.8 触摸

报告描述符

采用 Windows 触摸屏所需的最小集合,单点:

  • Usage Page Digitizer,Usage Touch Screen,应用集合;
  • 输入报告(Report ID 1):一个 Finger 逻辑集合,包含 Tip Switch 和 In Range(各 1 位,另补 6 位常量)、Contact Identifier(8 位)、X 和 Y(各 16 位);集合外为 Contact Count(8 位);
  • X/Y 的逻辑最大值为屏幕宽、高减 1;物理尺寸按屏幕像素间距 0.11 mm 计算,单位厘米、指数 −2。物理尺寸只影响 Windows 估算 DPI,不影响坐标映射;
  • Feature 报告(Report ID 2):Contact Count Maximum,值为 1。

输入报告共 8 字节:[报告 ID][按下|在范围][触点 ID][X 低][X 高][Y 低][Y 高][触点数]。抬起时 Tip Switch 与 In Range 为 0,触点数仍为 1,表示该触点结束。

登记

在显示类之后用 SDK 的 hid_desc_config 和 hid_register() 登记 HID 接口(接口 1),再用 hid_set_report_desc() 替换为上述报告描述符。HID 类不放入 USB_DEVICE_CLASS_CONFIG_2_0,只在板级强制打开 TCFG_USB_SLAVE_HID_ENABLE 以编译 hid.c,PC 模式原有的类组合保持不变。

GET_REPORT 应答

在副屏的 setup 处理中应答接口 1 的 GET_REPORT 请求:Feature 报告 2 返回 [2, 1];输入报告 1 返回最近一次发送的报告;其他请求返回 STALL。

坐标采集

不修改 GT911 驱动。独立线程在显示电脑画面期间每 10 ms 调用一次 SDK 的 get_touch_x_y_status(),读取最新坐标和按下状态。该坐标已按屏幕像素换算,与副屏分辨率一一对应,无需再映射。

  • 只在按下、移动、抬起时发送报告;按住不动不重复发送,Windows 按时间判断长按。
  • 未显示电脑画面时,线程每 200 ms 检查一次。若会话结束时仍处于按下状态,补发一包抬起,避免电脑端一直保持按下。
  • usb_screen 页面没有控件,LVGL 同时收到触摸也不会误触发。

发送

设备未进入 Configured 状态时不发送,否则 hid.c 会积压报告,在配置完成后集中发出。发送使用同步接口 hid_tx_data(),原因见 7.6 节。

6.9 并入 PC 模式

  • 进入 PC 模式时 SDK 调用 usb_start(1, 0),class 为 0 表示按板级类配置启动。挂接点在此把类组合改为 USB_DEVICE_CLASS_CONFIG_2_0 | USB_DISPLAY_CLASS。
  • usb_device_mode() 按以下顺序登记:显示(接口 0)→ 触摸(接口 1)→ 扬声器/麦克风(接口 2--4,带 IAD)。显示接口必须是接口 0,因为驱动 INF 按 MI_00 匹配。
  • 端点分配:EP1 显示,EP2 触摸,EP3 声卡(SDK 原有分配)。SDK 中 MSC 默认使用 EP1、CDC 使用 EP2,因此类配置中不能保留这两者,板级配置中用 #error 检查。
  • 退出 PC 模式时,SDK 调用 usb_device_mode(usb_id, 0) 释放各类,显示类随之释放,解码线程检测到后结束会话。

6.10 电脑端使用步骤

  1. 开发板切换到 PC 模式,用 USB 线连接电脑的 USB 2.0 及以上接口。
  2. 如此前用 Zadig 为该设备安装过 WinUSB,需先删除,否则 IDD 驱动无法绑定:用 pnputil /enum-drivers 找到提供者为 libwdi、硬件 ID 含 303A 的 oemNN.inf,再执行 pnputil /delete-driver oemNN.inf /uninstall /force。
  3. 运行驱动安装包。「设置 → 系统 → 屏幕」中出现一块 800×480 显示器,选择「扩展这些显示器」。
  4. 在「控制面板 → 平板电脑设置 → 显示 → 设置 → 触控输入」中,将触摸屏绑定到副屏。
  5. 如需从开发板放音,在 Windows 声音设置中选择该设备的扬声器。

6.11 调试与测试工具

工具 位置 用途
测试图生成 tools/gen_test_jpg.py 按屏幕分辨率生成不同场景、质量、色度采样的测试 JPEG,并导出为 C 数组
解码测速 bench/usb_screen_bench.c 循环解码上述测试图,统计每帧耗时
bulk 吞吐测试 tools/bulk_bench.py、bench/usb_bulk_bench.c 电脑端发送带序号的数据,设备端校验并统计吞吐
Python 发送端 tools/send_frames.py 不依赖 IDD 驱动,发送测试图或抓屏画面;分辨率与单帧上限从设备能力字符串读取;需配合 WinUSB
显存导出 bench/usb_screen_pixdump.c、tools/pixdump_parse.py 用 SDK 的fb_combine_set_out_cb() 挂合成输出回调,截取 fb1 和送屏缓冲的同一区域,经串口导出;电脑端还原为 PNG,并与截图比对误差
setup 请求记录 class/usb_disp_class.c 中断中把每个 setup 请求和总线复位写入环形缓冲,在线程中打印,用于排查枚举问题
运行统计 app/usb_screen_app.c 每秒打印收帧与解码帧率、平均帧大小、解码耗时,以及丢帧、坏帧头、接收错误、超长帧、解码错误计数

七、问题与解决

7.1 USB 读取函数丢弃未读完的数据包

现象:按「先读取 16 字节帧头,再读取剩余数据」的方式实现时,数据持续错位。

分析 :SDK 底层 USB 协议栈闭源,以 LLVM bitcode 形式提供(cpu.a),可用 clang -S -emit-llvm -x ir 转换为可读的 IR 进行分析。确认 usb_g_ep_read 的行为如下:

  • 每次读取一个数据包,只拷贝 min(包长, 剩余长度) 字节,随后释放整个包,未拷贝的部分丢失;
  • 收到短包不会结束读取,而是继续等待下一个包;
  • 阻塞读取超时(约 2 s 加每包 20 ms)返回 0,已读取的部分作废。

解决 :先通过 usb_read_rxcount() 获取当前包长度,完整读出首包并解析帧头,再按帧头中的长度精确读取剩余数据。

7.2 新建 UI 页面在 GUI Builder 中不显示

现象:复制现有页面的 json 文件后,JLGuiBuilder 中看不到新页面。

原因 :新页面需要在四处登记:project.jlui 的 screenList、ui.json、ui/<页面>.json、scene.json,其中决定工具显示哪些页面的是 screenList。打包后还需要手动将生成的 setup_scr_<页面>.c 加入 SDK 的 Makefile。另外,复制页面时原页面的手势事件也被一并复制,需要清除。

7.3 视频层不参与合成时无法显示

设想 :将 disp 插件设为 combine=0,视频层直接送屏,省去 DMA2D 合成开销。

结果 :分析闭源的 disp 插件(video.a)后确认,combine=0 时插件不会打开输出设备,帧数据只保留在缓冲区中,不会送屏;该模式用于编码等自行读取缓冲区的场景。后续 A/B 测试也证明合成并非瓶颈(见 5.6 节),因此保持 combine=1。

7.4 JPEG 质量等级 10 画质最差

现象:将质量等级从 8 提高到 10 后,画面方块感更加明显。

分析:驱动在画面静止时约每秒补发一帧,因此可以用同一张静止壁纸的单帧大小比较实际压缩程度:

质量等级 单帧大小 现象
8 45 KB 天空、水面有 16×16 宏块状黄绿色斑
9 67 KB 色斑消失
10 12 KB 整屏方块

驱动对等级 10 的处理与预期不符,实际压缩最重。最终取值为 9。

7.5 渐变区域出现彩色等高线,静止画面像素闪烁

这是本项目中排查时间最长的问题。

现象:天空、水面等渐变区域出现黄绿色等高线,静止画面上的像素也在闪烁。随后发现 LVGL 自身绘制的专辑封面也有同样现象。

排查过程:

  1. 怀疑 JPEG 压缩:提高画质后宏块色斑消失,但细等高线仍在。
  2. 怀疑 RGB565 色深不足:用电脑截图离线模拟 RGB565 截断和抖动,模拟结果平滑,与实拍差异很大,排除。
  3. 导出显存比对:新增调试功能,在合成模块的输出回调中截取一块解码后的视频层(YUYV)和最终送屏缓冲(RGB565),经串口以十六进制导出,电脑端用 Python 还原为图片,与电脑截图逐像素比对。结果显示:解码误差平均仅 1--3 个色阶,送屏缓冲平滑、无等高线。显存数据正确,问题发生在显存到屏幕的传输环节。
  4. 静止画面显存不变而屏幕像素变化,进一步指向 RGB 并口的时钟采样时序。

原因 :屏幕规格书中 DCLK polarity: negative 表示屏幕在 DCLK 下降沿采样 ;屏驱动据此配置了 CLK_NEGATIVE,但实测杰理该配置表示芯片在下降沿输出数据 。输出与采样在同一边沿,建立/保持时间余量为零,屏幕可能锁存到新旧混合的数据。渐变跨越色阶时多根数据线同时翻转(例如 01111 → 10000),采样错误即表现为沿等高线分布的彩色线;每帧出错位置不同,因此产生闪烁。

解决:将 DCLK 极性改为另一边沿(芯片上升沿输出、屏幕下降沿采样,余量为半个时钟周期,约 20 ns)。修改一项配置后,等高线和闪烁全部消失。

经验:

  • 屏幕厂商描述的时钟极性通常指采样沿,芯片厂商的配置项可能指输出沿,二者含义需实测确认。
  • 显存导出比对能有效区分「数据错误」与「接口时序错误」,适用于各类显示异常的排查。

7.6 HID 异步发送竞态导致触摸失灵

现象 :触摸初期正常,一段时间后完全失灵,串口持续输出 .。

原因 :SDK hid.c 中的异步发送函数 hid_send_buf_async() 存在竞态:

c 复制代码
usb_g_intr_write(usb_id, HID_EP_IN, NULL, len);   // ① 启动发送
hid_info[usb_id]->trig = 1;                        // ② 标记发送中

主机以 1 ms 间隔轮询。若数据包恰好在 ① 和 ② 之间被取走,发送完成中断检测到 trig == 0 后直接返回;随后 trig 被置 1,且不会再有中断将其清零。此后所有报告滞留在缓冲区中无法发出,缓冲区满后持续打印 .。双核环境下更容易触发。SDK 原有场景仅偶尔发送音量键,因此不易暴露。

解决 :改用同步发送接口 hid_tx_data()。

7.7 Windows 枚举触摸屏时请求 Feature 报告

Windows 识别触摸屏时会通过 GET_REPORT 读取「最大触点数」Feature 报告(实测请求为 bmRequestType 0xA1, bRequest 0x01, wValue 0x0302),而 SDK hid.c 未处理该请求。在副屏的 setup 处理函数中补充应答后,枚举正常。

7.8 字符串描述符索引冲突

现象:并入 PC 模式后,Windows 中扬声器设备的名称变为能力字符串。

原因:能力字符串最初使用索引 5,而 SDK 中索引 5 为扬声器接口名称。副屏的 setup 处理函数先于 SDK 执行,导致扬声器名称被覆盖。

解决:能力字符串改用未被占用的索引 9(索引 5--8 分别为扬声器、麦克风、大容量存储、UVC 使用)。

7.9 setup 处理函数应插入而非替换

早期为返回自定义设备描述符,直接替换了 SDK 的 user_setup_filter。单独枚举副屏时没有问题,但并入 PC 模式后,PC 模式依赖该函数识别主机类型(Windows / iOS / Android)并应答扬声器、麦克风的字符串请求。

解决:改为在 SDK 原函数开头先调用副屏处理函数,副屏不处理的请求继续走原有逻辑。

7.10 偶发总线复位

现象 :测试中出现过两次:短时间内连续多次 usb suspend(一次为 0.1 s 内 9 次),随后总线复位,Windows 重新枚举设备,电脑端 Python 发送脚本报 USBError [Errno 5]。设备端错误统计均为 0,并可自动恢复。

分析:怀疑与 USB 线缆、Hub 或 Windows 的「USB 选择性暂停」节能策略有关,根因尚未确认。

处理:电脑端发送脚本增加自动重连,IDD 驱动本身可自动恢复连接。如需排除节能策略的影响,可在电源选项中关闭「USB 选择性暂停」。

7.11 模式切换无法回到蓝牙模式

现象:按模式键可从蓝牙依次切换到 PC 模式,但之后只在「音乐 → PC」之间循环,无法回到蓝牙模式。

原因 :模式循环函数只在 [APP_MODE_NET, APP_MODE_SINK) 区间内轮转,而该应用的模式枚举中 APP_MODE_BT 位于 APP_MODE_NET 之前(SDK 中其他应用的顺序相反),导致蓝牙模式不在循环范围内。

解决:调整枚举顺序,使蓝牙模式位于循环区间内。该问题属于 SDK 本身,与副屏功能无关。

7.12 LVGL 内存池优化

LVGL 内存池配置为 1 MB,运行中最大使用量仅 125 KB(播放音乐时 199 KB)。主要风险来自专辑封面:原实现按图片原始分辨率解码到内存池,一张 500×500 的封面需要约 500 KB。

解决:封面最长边超过 200 像素时,在 YUV 转 RGB565 的过程中按区域平均缩小(而非隔点抽样,避免锯齿),再将内存池减为 512 KB。副屏工作期间剩余内存由约 280 KB 增加到约 800 KB。

7.13 增量编译未更新固件

编译过程被中断后残留了一个 0 字节的 sdk.elf,make 认为链接产物已是最新,跳过了链接步骤,导致烧录的仍是旧固件。删除该文件后重新编译即可。遇到「修改未生效」时,应首先检查各级编译产物的时间戳。

八、实测数据汇总

场景 数据
拖动窗口 收帧与解码 58--60 fps,屏幕刷新 55--56 fps
播放 30 fps 视频 收帧、解码、显示均为 30 fps
解码耗时 平均 11 ms,最大 13 ms
单帧大小(质量等级 9) 静止壁纸约 67 KB
错误计数 帧头错误、接收错误、解码错误、超长帧均为 0
CPU(60 fps) CPU0 5%;CPU1 15%,其中收帧线程 11.3%、DMA2D 合成 2.3%、解码线程 0.85%
剩余内存 副屏工作 806 KB;副屏 + USB 放音 749 KB;蓝牙播放 3.1 MB

九、待完善事项

  • 长时间稳定性测试(连续运行 2 小时、反复插拔 50 次)尚未系统进行。
  • 偶发总线复位的根因尚未确认。
  • 收帧缓冲按单帧上限 195 KB 分配,统计实际最大帧后预计可再节省约 150 KB。

十、总结

  1. 优先验证风险最高的环节。 第一阶段即测试硬件解码速度,确认单帧约 11.5 ms 后,再投入后续开发。
  2. 尽量复用现有方案。 协议与乐鑫例程保持一致,可直接使用已签名的驱动,电脑端无需开发;HID 和 UAC 复用 SDK 已有的类驱动,只补充缺失的部分。
  3. 闭源库的行为应通过分析确认,而非推测。 杰理 SDK 的闭源库以 LLVM bitcode 形式提供,转换为 IR 后可以阅读,USB 读取函数和 disp 插件的行为均由此确认。
  4. 显示异常先导出显存。 显存数据与源图像的逐像素比对,能够快速区分数据问题与接口时序问题。
  5. 控制对 SDK 的改动范围。 新增代码集中存放,SDK 中只保留由功能宏包裹的挂接点;收益不明显的改动及时撤回。

参考资料

相关推荐
我是海飞3 小时前
杰理 JL703N SDK 实现 AI 自动化调试:自动进入下载模式、下载、重启与日志抓取
人工智能·单片机·自动化·音视频·杰理
Zwawa3 小时前
PID 的三个参数之外:位置式、增量式和反馈控制
c语言·单片机·嵌入式
创界工坊工作室3 小时前
29-基于单片机的万年历设计
stm32·单片机·嵌入式硬件·proteus
木由里予3 小时前
NAND 这门生意
运维·服务器·网络·单片机
律宏阔4 小时前
Windows 双网卡配置:通过跃点数实现网线访问局域网、Wi-Fi 访问公网
windows
沐欣工作室_lvyiyi4 小时前
基于STM32的社区养老控制系统设计(论文+源码)
stm32·单片机·嵌入式硬件·社区养老控制系统·保定理工学院·华德学院·淮南师范学院
一禅(OneZen)4 小时前
小米平板 5 Pro 5G(enuma)完整 LineageOS 系统刷机流程
5g·智能手机·电脑·echo·ceho 命令·ceho 语法
Source.Liu4 小时前
【A11】拥有者模式平台架构笔记
windows·笔记·架构·rust
snow@li4 小时前
单片机:全景梳理核心原理与体系深度分析
单片机·嵌入式硬件