{"code":1,"msg":"成功","time":"1793358199","data":{"title":"〖2026嵌入式大赛〗GD32H759IM 的 USB 副屏加 OV2640 摄像头二合一","content":"一、项目概述\r\n\r\n项目本体是一块 GD32H759IMK6 的板子加一块 4.3 寸 480×272 的 RGB 屏，整机通过 USB 高速口接到电脑上，当电脑的第二个显示器用。电脑端跑一个 Python 工具，板上固件负责收图、显示、推摄像头画面，两边靠 WinUSB 上的两个 bulk 端点加一个单字节模式包通信，协议是自定义的，没有走标准显示类。\r\n\r\n当初选这个方向，主要是想把 H7 上几块平时不太会凑到一起的外设串起来用一遍：TLI 并行 RGB 显示、EXMC 挂 SDRAM、DCI 接并口摄像头、USBHS0 高速设备。这四块单独拿出来都不算难，难的是它们要同时占内存带宽，还要和 USB 的收发节奏对上，所以整个项目其实一大半时间花在缓冲划分和线程同步上。\r\n\r\n软件这边，RTOS 用 RT-Thread 4.1.1，界面用 LVGL 9.5，主机端是 Python 3 加 pyusb。固件是 RT-Thread Studio 的 Eclipse CDT 工程配 GNU Arm 13.3 工具链，全开源工具链，编译完直接 pyOCD 烧进去。\r\n\r\n目前已经连起来的功能：板载菜单切换三种工作模式，电脑端一个进程常驻，按板子菜单选的动作自动切换信号源；USB 掉线（拔插、板子断电重启）工具会自己重新找设备接着用，不用重启工具。整机编译出来占 Flash 354KB 左右、RAM 84KB 左右，都是 -O0 构建的数据。\r\n\r\n\r\n二、功能说明\r\n\r\n1. PC 监控模式\r\n电脑端用 psutil 采样，5Hz 采一次，做了一块叫“SYSTEM PULSE”的仪表盘推给副屏，里面这些东西：\r\n左边一列是 CPU 总占用的大表盘（240 度的弧，颜色从绿过渡到琥珀再过渡到红）、每个逻辑核的占用条、内存占用条；\r\n中间是两条曲线，CPU 最近 30 秒的历史（150 个点是 5Hz 采 30 秒）和上下行网速曲线，网速曲线的纵轴按当前峰值自动缩放；\r\n右边是网络实时速率、磁盘读写速率、开机时长、进程数，有电池的话再显示电量和是否插着电源；\r\n顶栏有一个心跳波形，PQRST 的形状是算出来的，跳动频率跟着 CPU 负载走，CPU 越忙跳得越快，另外还有一个一闪一闪的链路指示灯。\r\n整帧在电脑上画完再转成 RGB565 发下来，默认 30fps。整块背景（面板、圆角、标签、退出角标）只在启动时画一次，每帧拷贝背景再画动态部分，省掉大部分绘制开销。\r\n退出方式有三种：点屏幕右上角的 X，按板上 PA0，或者主机数据流停一下——100ms 收不到数据就认为主机不发了，自动把屏幕还给菜单。\r\n\r\n2. 屏幕镜像模式\r\n电脑端用 mss 抓主屏，抓到的图先按整数倍缩一刀（比如 2560 宽的屏直接缩一半再算），再用 LANCZOS 缩到 480×272，过一遍 UnsharpMask 提锐度（半径 1.4、强度 80%、阈值 3），最后加 4×4 的 Bayer 抖动再打包成 RGB565——小屏上渐变很容易起横纹，抖动加上以后过渡干净很多，而且抖动幅度在颜色接近极值时会自动衰减，纯色块不会被噪点弄脏。镜像画面右上角会叠一个烧进去的退出角标，和仪表盘那个 X 位置一致。\r\n退出判定和监控模式一样，但要连续十次 100ms 收不到数据才放屏，因为镜像这边偶尔会因为抓屏线程卡一下掉几帧，用单次判定会误退。\r\n\r\n3. USB 摄像头模式\r\n板上 OV2640 出 320×240 的 RGB565，DCI 八位并行口收进来交给 DMA，双缓冲轮流往 SDRAM 里写。一块填满时中断里把写地址翻到另一块，同时放一个信号量；发送线程被唤醒后先对刚填满的那块做一次 D-Cache 失效（DMA 直接写内存，不经过 CPU 缓存），然后给这一帧加上 8 字节帧头（'C'、'A'、'M'、一个自增序号、四字节小端长度），按“帧头 + 数据”两段从 bulk IN 发到电脑，两段之间靠 USB 发送完成回调放的信号量同步。\r\n电脑端用一个无边框全屏窗口显示：RGB565 转 RGB888 走一张 65536 项的查表，再做一次灰世界自动白平衡（按三个通道的均值归到灰度均值，把廉价传感器的偏色拉回来），最后两级缩放铺满屏幕（先 LANCZOS 缩到 640×480，再 BILINEAR 缩到全屏，比一步缩到全屏快不少）。按 Esc 可以把窗口收起来，板子照旧推流，再进摄像头模式窗口能重新弹出来。\r\n这个模式不接管屏幕，板子还停在菜单上，退出就再点一下这个按钮或者按 PA0。整个过程不占用 bulk OUT，所以板子不用为它切显存。\r\n\r\n4. 板载菜单和触摸\r\nGT911 电容触摸，用软件模拟 I2C，SCL 是 PH7、SDA 是 PF9。上电时按 GT911 的时序复位，然后把一整张配置表连校验和一起写进 0x8047 起的配置区，写完才算初始化成功；读坐标时先读状态寄存器拿点数，再一次性把每个点的坐标和尺寸读回来，最后回写 0 清中断标志。菜单页三个按钮加底部一行提示，全部用显式坐标摆放，没有用布局器。\r\n\r\n5. 异常屏\r\n挂了 HardFault 之后不用连调试器就能看出大概原因：固件装了 RT-Thread 的异常钩子，直接在屏上清成蓝底，用自己写的 5×3 点阵字打出出错线程名、是不是在中断上下文里，以及 PC、LR、PSR、CFSR、HFSR、BFAR 六个寄存器值。异常上下文里不能调度也不能阻塞，所以这段代码只做最少的绘制，画完返回错误让系统停下来。\r\n\r\n6. 串口控制台\r\nMSH\/Finsh 挂在 uart1 上，可以看线程状态、内存占用这些。\r\n\r\n\r\n三、上机流程\r\n\r\n1. 板子插上 USB，上电，屏幕进菜单；\r\n2. 电脑上双击 tools\\start_subdisplay.bat（第一次要先跑一次 WinUSB 绑定脚本），工具会去找 VID 0x28E9、PID 0x028F 的设备并把 bulk 端点挑出来；\r\n3. 点菜单上的 PC Monitor，副屏立刻变成电脑状态仪表盘，想退出就点右上角 X、按板上 PA0，或者让工具停一下（Ctrl-C 也行）；\r\n4. 点 Screen Mirror 变成镜像，退出方式一样；\r\n5. 点 USB Camera，摄像头窗口弹出来开始预览，按 Esc 收起窗口，再点一次菜单按钮或者按 PA0 退出这个模式；\r\n6. 中途拔插线、给板子断电重启都不用管，工具会自己重连，仪表盘那 30 秒的历史数据也不会丢。\r\n\r\n\r\n四、硬件框架\r\n\r\n1. 主控和时钟\r\nGD32H759IMK6，Cortex-M7 内核，主频 480MHz，2MB Flash、1MB SRAM。板子上的外部晶振是 24MHz，主时钟和 USB 都从它倍频出来。给摄像头供时钟的那路是单独配的：把 24MHz 三分频成 8MHz，从 PA8 的 CKOUT0 输出给 OV2640 当 XCLK，这样摄像头的时钟不占定时器资源，频率也不用去调寄存器。\r\n\r\n2. 显示\r\n4.3 寸 480×272 的 RGB565 屏，走 TLI 并行 RGB 接口。TLI 那 24 根数据线和同步线分布在 GPIOA 到 GPIOH 上，统一用 AF14 复用；面板使能配成普通推挽输出并拉高。像素时钟由 PLL2 那条路分频来给（PLL2R 再八分频），不跟 CPU 抢主 PLL。\r\nTLI 的时序参数是这套：行同步 40、行后沿 42、行有效 522、行总数 524；场同步 9、场后沿 11、场有效 283、场总数 285；行场同步和 DE 都是低有效，像素时钟上升沿采样。图层 0 的窗口从 (43,12) 开始，正好铺 480×272，格式 RGB565，帧缓冲行长度 963、步进 960、总行数 272，另外打开了 TLI 自带的抖动，让 RGB565 的色带更平一点。\r\n\r\n3. 内存和地址分配\r\n内部 SRAM 0x24000000 这段 512KB 区域的最末尾一块拿来当显存：从 0x24040400 到 0x2407FFFF，正好 261120 字节，也就是一帧。选内部 SRAM 是因为 TLI 读显存是持续性的流量，放内部比放 SDRAM 稳，SDRAM 的带宽主要留给摄像头。\r\n外挂 32MB 的 SDRAM 挂在 EXMC 上，映射到 0xC0000000。摄像头的两块缓冲放在最前面两个位置：0xC0000000 和 0xC0030000，中间隔 192KB，比一帧 153600 字节多一点。SDRAM 里 0xC1800000 还留了一块备用显存，给调试时候切缓冲用。\r\n\r\n4. SDRAM 的配置\r\n16 位数据宽度，13 位行地址、9 位列地址、4 个 bank，CAS 延迟 3，SDCLK 取 EXMC 时钟的五分频，开了突发读切换和一级流水线延迟。时序上是负载模式寄存器延迟 2、退出自刷新 12、行地址选通 8、自动刷新 11、写恢复 2、行预充电 4、行到列 4，自动刷新计数填的 917。\r\n上电序列就是标准的四步：等控制器就绪以后先全预充电，再连发 8 次自动刷新把电容充起来，然后写模式寄存器（突发长度 1、顺序、CAS3、单次写突发），最后设刷新计数并等就绪标志落下去。这几步之间都要等 NREADY，不能只靠延时。\r\n\r\n5. 触摸\r\nGT911，SCL 挂在 PH7、SDA 挂在 PF9，复位接 PF6、中断接 PH6，全部是软件翻转 IO 模拟时序，没占硬件 I2C。复位时序是先拉低 RST 和 INT，等 1ms 再把 RST 拉高，等 6ms 之后把 INT 切成输入。配置区是从 0x8047 到 0x80FE 一共 184 字节，最后两个字节一个是校验和（把前面所有字节求和再取反加一）、一个是标志位，校验和写错整张配置表就不生效，这一点第一次调的时候卡了一下。\r\n\r\n6. 摄像头\r\nOV2640，走 DCI 八位并行接口，引脚用 AF13 复用，搬运交给 DMA1 通道 7。配置里 DMA 是外设到内存、外设地址不自增、内存地址自增、32 位宽度、循环模式、高优先级，一次搬 38400 个 32 位字，正好 153600 字节一帧。用 32 位宽度是为了把 4 个像素打包成一次传输，几个字节一次地搬会拉不满带宽。\r\n摄像头寄存器走的是硬件 I2C1，SDA 在 PB11、SCL 在 PH4，开漏加上拉，速率 100kHz，从机写地址 0x60。初始化的顺序是：先切到 DSP bank，写 0x12=0x80 软复位，等 10ms，然后灌 SVGA 默认表（177 条），等 100ms，灌 RGB565 输出格式表（14 条），再等 100ms，最后写输出尺寸寄存器设成 320×240。尺寸寄存器算的是 width\/4 和 height\/4，所以宽高都必须是 4 的倍数，不是 4 的倍数直接返回错误。\r\n\r\n7. USB\r\n用片内的 USBHS0 高速控制器和内置 PHY，480Mbps。设备枚举成一个 WinUSB 设备，VID 取的是 GigaDevice 的 0x28E9，PID 0x028F，固件里额外回了微软的 OS 描述符，Windows 第一次插上会自己认成 WinUSB 设备、自动装系统的 winusb.sys，正常不用手动装驱动。程序里通过 RT-Thread 的 winusb 设备跟主机收发，主机端则按 VID\/PID 找设备、自动挑出 bulk OUT 和 bulk IN 两个端点。\r\n\r\n8. 按键\r\n板上一个用户键接在 PA0，配成下拉输入加上升沿中断。中断里什么逻辑都不做，只往事件集里发一个标志，处理放在按键线程；线程收到以后等 20ms 再判定，用来消抖。只要当前有任何一个模式在被占用（包括摄像头模式），按它就退出。\r\n\r\n\r\n五、软件框架\r\n\r\n1. 系统与组件\r\nRT-Thread 4.1.1，1ms 一个 tick，内核这边开了信号量、互斥量、事件集、邮箱、消息队列、内存池和软件定时器；设备框架只留了串口和 PIN，串口用的是 V1 版本，MSH 控制台挂在 uart1 上。USB 用的是 RT-Thread 的 usbdevice 加 WinUSB 类。界面是 LVGL 9.5：16 位色深、只编译 RGB565 一种格式、绘制单元固定一个、刷新周期 33ms，内存和字符串函数都走 RT-Thread 自己的实现。整个工程没有文件系统，画面全部在内存里周转。\r\n\r\n2. 目录分层\r\napplications 里只有 main.c，负责把几个模块拉起来。BSP 是本项目自己写的板级代码，按功能分成六块：CAMERA（OV2640 驱动、DCI 和 DMA 配置、SDRAM 初始化、SCCB 读写）、KEY、LCD（TLI 显存操作和异常屏）、TOUCH、LVGL（显示端口、输入端口、板载菜单）、USB2SCR（USB 副屏核心）。drivers 和 libraries 是 GD32H7 的板级驱动和标准外设库，lvgl 和 rt-thread 是上游源码，没有改动。\r\n\r\n3. 启动顺序\r\nmain 里先把按键线程起起来（优先级 9，最先响应），然后依次初始化 USB 副屏（TLI 的 GPIO 和时序、五个 IPC 对象、三个工作线程）、异常屏（装 HardFault 钩子）、LVGL（显存清屏、lv_init、把 LVGL 的 tick 绑到 RT-Thread 的毫秒节拍上，省得再开一个定时器，然后建页面和线程）。主线程自己进死循环，什么都不干。\r\n\r\n4. 线程划分\r\nkey 优先级 9，栈 1024 字节；\r\nmain 优先级 10，栈 2048；\r\ncamera 优先级 12，栈 2048（采集线程）；\r\nlvgl 优先级 15，栈 4096，5ms 一轮调 lv_timer_handler；\r\nscr_disp 优先级 16，栈 2048（收显示流）；\r\nscr_cam 优先级 17，栈 2048（推摄像头帧）；\r\nscr_ctl 优先级 18，栈 2048（发模式包、轮询触摸）；\r\ntshell 优先级 20，栈 4096。\r\n优先级这么排的目的是：USB 收流和推流要压住界面刷新，而界面刷新又不能挡着按键和采集。\r\n\r\n5. USB 副屏这三个线程怎么配合\r\nscr_disp 管收帧。USB 每收完一块数据，接收回调里记下长度、置一块标志、放一次 scr_sem；scr_disp 拿着 100ms 超时去等这个信号量，等到了就把这块数据做一次 D-Cache 清写（保证 TLI 的 DMA 能看到最新像素），累加字节数，第一次收到数据时把链路和面板占用标志立起来，然后把写偏移往后推。凑满一帧（480×272×2 就是 261120 字节）就把偏移回绕、帧数加一。之后它马上发起下一次读，一次最多读 52224 字节——这个数不是随便定的，261120 正好是它的 5 倍，一帧刚好切五块。\r\n停顿判定用两个标志配合：发起读之前置“已武装”，收到数据时清掉。如果这一轮等信号量超时了，而“已武装”还是 1，说明主机这一轮没给数据，停顿计数加一。停止模式下一次停顿就放屏；监控和镜像要连续攒够 10 次。放屏的时候把面板占用标志清掉、偏移归零、链路标志清掉，如果是停止模式还会把显存整屏刷黑，免得菜单出来的时候底下压着上一帧。\r\nscr_ctl 管控制。它 20ms 走一轮，只有在模式变了、或者 USB 重新枚举过的时候才发东西，每次连发两遍防止丢包：发的是当前模式的一个字节，写 bulk IN 之前先抢 USB IN 端点的互斥锁，因为它要和摄像头线程共用这个端点。面板被占用的时候它还要负责触摸：读一帧 GT911，用一个“新按下”的判定（固件里按下是 2、松开递减到 0，只有大于 1 才算新按下）去比退出热区，热区是 x 从 410 到 480、y 从 0 到 56，比主机实际画的角标大一圈，手指好点。\r\nscr_cam 管推流。它平时 100ms 一轮看模式，切到摄像头模式就先确保采集线程起来了、把帧同步量复位、把采集流打开，然后等帧同步量（超时 3 秒）；拿到帧以后组帧头、抢 IN 端点锁、先发 8 字节头（超时 500ms）、再发 153600 字节数据（超时 3000ms），发成功了序号加一、发送计数加一，失败就打日志丢帧、歇 50ms 再来。发送这一步是先复位一个专用信号量，写完再等 USB 发送完成回调去放它，等于把异步的完成回调变成同步的发送函数。\r\n另外固件的 winusb 类里挂了一个使能回调，USB 一旦重新枚举就通知 scr_ctl 把当前模式补发两次，这样主机拔插之后不用手动再点一遍菜单。\r\n\r\n6. 屏幕归属怎么切\r\n用一个面板占用标志表示屏幕当前归不归主机。它立着的时候，LVGL 的 flush 回调直接返回、什么都不画，输入回调也只上报“没按下”，触摸改由 scr_ctl 独占轮询，这样两边不会同时去读 GT911 抢总线，也不会出现菜单和主机画面互相盖。主机退出以后 LVGL 线程会发现标志掉了，把页面切回菜单并强制重画一次。\r\n\r\n7. 显示通路\r\nLVGL 用部分渲染，draw buffer 开了 80 行，算下来 80×480×2 差不多 77KB 的静态数组。flush 回调里先算脏区大小，把 TLI 的行标记设到脏区底行往下两行（超过屏高就夹到 271），清一下行标记标志，然后忙等扫描线打到那个位置，到了才把这块数据拷进显存，拷完做一次显存整屏的 D-Cache 清写，最后告诉 LVGL 刷新完成。等扫描线这段必须忙等，一旦用了会触发调度的延时，线程被切走的时间不可控，画面立刻斜一条。\r\n\r\n8. 摄像头通路\r\n采集这边是 DMA 双缓冲：传输完成中断里先关通道，把刚填满的那块的索引记下来，把写地址翻到另一块，清标志、重新开通道，最后放帧同步量。采集线程拿到信号量以后，如果流是打开的并且上层注册了回调，就先对目标地址做一次 D-Cache 失效——DMA 是直接写 SDRAM 的，CPU 那边可能还留着这一段的旧缓存行，不做这一步读出来就是上一帧或者花屏——再把帧地址和长度回调给上层。\r\n\r\n9. 主机端工具\r\n一个文件 tools\/usb2screen.py 全包了。libusb 的 dll 在 Windows 上用 ctypes 自动找经常失败，所以脚本会先去 pip 装的 libusb 包里翻，找不到再退回系统路径。\r\n跑起来以后有一个专门的 ctl 线程读 bulk IN：平时用 200ms 超时读，收到第一个字节就是模式号；一旦模式是摄像头，就改成按帧头读，校验前三字节是不是 CAM、长度是不是 153600，不对就丢掉重新同步，读满一帧交给显示窗口；如果收到的是 1 到 4 字节的短包并且首字节是 0 到 3，就当成夹在帧中间的模式包处理。显示窗口只用主线程碰，ctl 线程只把最新的帧塞进一个带锁的槽位，避免多线程抢 Tk。\r\n主线程按当前模式决定干什么：仪表盘模式每帧实时画，镜像模式挂在后台抓屏线程产出的最新帧上，停止和摄像头模式就空转。发送按时间片控速（累加周期时间，比固定 sleep 准），镜像和仪表盘都是 30fps。\r\n还有两个和 Windows 相关的小处理：进程启动时声明 DPI 感知，免得系统缩放让抓屏拿到的是放大过的逻辑像素；开跑前先做一次垃圾回收再冻结，因为一帧几 MB 的缓冲如果正好撞上一次完整回收，发送循环会被卡住几百毫秒，固件那边就会判定主机停发、把屏幕还回菜单。\r\n另外留了几个固定信号源的调试模式（pattern 彩条、monitor 仪表盘、mirror 镜像、image 静态图），可以直接验证链路而不依赖板子菜单。\r\n\r\n10. 构建和烧录\r\n工程是 RT-Thread Studio（Eclipse CDT）建的，工具链 GNU Arm 13.3。链接的时候对象文件有一千四百多个，直接拼命令行会超过 Windows 32KB 的限制，报 Error 126，所以在 makefile.targets 里把对象列表按 200 个一批写进响应文件，再以 @文件 的方式传给 gcc。产物是 Debug 下的 rtthread.elf 和 rtthread.bin。\r\n烧录用 pyOCD 配合 GD-Link：under-reset 连接，也就是按住复位让芯片停下来再连，然后整片擦除再编程，避免旧固件一跑起来就把 SWD 抢走导致的 No ACK。\r\n\r\n\r\n六、数据协议\r\n\r\n1. 显示流（主机到板子，bulk OUT）\r\n主机把整帧切成 52224 字节的块连续写，固件收到一块就拷进显存对应偏移。一帧 261120 字节，正好 5 块。停止模式不写。\r\n\r\n2. 模式包（板子到主机，bulk IN）\r\n固定 1 字节，0 停止、1 监控、2 镜像、3 摄像头。模式变化或者 USB 重新枚举后连发两次。\r\n\r\n3. 摄像头帧（板子到主机，bulk IN）\r\n8 字节帧头加 153600 字节数据，分两段发：\r\n第 1 字节 'C'（0x43）\r\n第 2 字节 'A'（0x41）\r\n第 3 字节 'M'（0x4D）\r\n第 4 字节 帧序号，每个成功发送的帧加一，回绕\r\n第 5 到 8 字节 帧长度，小端 32 位，固定 153600\r\n后面就是 320×240 的 RGB565 像素，逐行小端。\r\n\r\n4. 几个超时\r\n固件读 USB 显示数据的块超时 100ms；摄像头等帧超时 3 秒；发帧头超时 500ms、发帧数据超时 3 秒；主机读模式包超时 200ms、读帧头 1 秒、读帧数据 3 秒、写一帧超时 2 秒。这些都只是兜底，正常跑起来碰不到。\r\n\r\n\r\n七、资源占用\r\n\r\nFlash 354.08KB、RAM 83.98KB（-O0 构建）。\r\n静态缓冲：显存 261120 字节、摄像头双缓冲两块各 153600 字节（间隔 192KB）、LVGL 绘制缓冲约 77KB。\r\n线程栈：key 1024 字节，main \/ camera \/ scr_disp \/ scr_cam \/ scr_ctl 各 2048 字节，lvgl 和 tshell 各 4096 字节。\r\n\r\n\r\n八、设计上的几个取舍\r\n\r\n不做 UVC。片上 USB 设备控制器走 UVC 要额外移植类驱动、还要处理描述符和带宽协商，而主机端我们本来就自己写工具，用 WinUSB 的 bulk 端点自定义协议更省事：一帧切成五块连续写，摄像头帧加个 8 字节头就能和模式包在同一对端点上区分开，主机端解析只有几十行。\r\n显存放内部 SRAM。TLI 是持续读显存的，内部 SRAM 的访问确定性比 SDRAM 好；把 SDRAM 的带宽留给 DCI 采集和摄像头双缓冲，两边互不打扰。\r\n一帧切五块。块太小中断和调度开销大，太大又会让 100ms 停顿判定的粒度变粗（一块 52224 字节按 30fps 大概 6ms 一块），52224 正好整除一帧也是选它的原因。\r\n镜像用十次停顿判定。镜像掉帧比仪表盘频繁，单次判定会误退。\r\n摄像头模式不接管屏幕。摄像头推流时 bulk OUT 是空的，屏幕没必要切给主机，留在菜单上反而能随时再点一次退出。\r\n关键位置都加了硬件层面的同步：USB IN 端点写有互斥锁，OV2640 的寄存器读写也有互斥锁，统计量有互斥锁，帧的传递用信号量而不是共享变量。\r\n\r\n\r\n九、调试记录\r\n\r\n画面撕裂。TLI 等扫描线那一段一开始用了 rt_thread_mdelay，只要一调度走开画面就斜，而且斜多少和系统负载有关，查了很久才定位到是等待方式的问题，改回忙等就干净了。\r\nUSB 枚举。第一次插上是黄叹号 Code 28，原因是设备节点被系统缓存过、固件回的微软 OS 描述符没被重新问一遍，删掉节点重新扫描设备就好了，tools 里留了个 PowerShell 脚本专门干这件事，脚本会先删节点重扫、不行再装一份引用系统 winusb.inf 的回退 INF。\r\n链接报 Error 126。对象文件上千个，命令行直接拼超了 Windows 的 32KB 限制，报的是 Bad file number 一类的怪错误，后来改成写响应文件传对象列表。\r\n烧录 No ACK。旧固件跑起来速度太快，GD-Link 还没连上就被抢了 SWD，改成 under-reset 连接加整片擦除才稳定，偶尔不行就拉高 BOOT0 重新上电再烧。\r\nLVGL 边缘被裁。页面容器只要带圆角、内边距或者边框，边上一圈就会被吃掉，最后所有容器都设成 radius、pad、border 全 0，控件全部用显式坐标摆，也没有用 flex 布局。\r\n缓存一致性。TLI 和 DCI 都是 DMA 直接读写内存，忘了做缓存维护就是花屏或者一直显示旧帧：读 DMA 写的区域之前要失效缓存，自己写完要让缓存落下去，两个方向不能搞反。\r\nGT911 不响应。它的上电时序不能让复位和中断引脚乱序，INT 必须在复位以后切成输入；另外配置表的校验和算错的话，整表写进去也不生效，表现出来就是能读 ID 但永远没有触摸。\r\n\r\n\r\n十、目录与文件\r\n\r\napplications\/main.c：启动入口，拉起各模块\r\nBSP\/CAMERA：OV2640 驱动、DCI 与 DMA 配置、SDRAM 初始化、SCCB 读写\r\nBSP\/KEY：PA0 按键，中断加事件集\r\nBSP\/LCD：TLI 显存操作、HardFault 异常屏\r\nBSP\/TOUCH：GT911 软件 I2C 驱动\r\nBSP\/LVGL：LVGL 移植层和板载菜单界面\r\nBSP\/USB2SCR：USB 副屏核心，三个线程加 WinUSB 通信\r\ndrivers、libraries：GD32H7 板级驱动和标准外设库\r\nlvgl、rt-thread：上游源码\r\nlinkscripts\/GD32H759IM\/link.ld：链接脚本\r\ntools\/usb2screen.py：主机端工具，三种模式的信号源和摄像头预览\r\ntools\/start_subdisplay.bat：双击启动主机工具\r\ntools\/install_winusb.ps1：WinUSB 驱动绑定脚本，需要管理员权限\r\ntools\/rtthread_winusb.inf：回退用的 INF\r\nflash.bat：一键烧录\r\nmakefile.targets：链接用响应文件的规则","updatetime":"2026-10-13 17:43:56","author":"miamaomao"}}