AWTK 开发实践(三):AMT630HV100 SDK 移植调用链分析

一、背景

项目的显示端选用 AMT630HV100------一颗带矢量 GPU 的显示驱动芯片。原厂 SDK 已经完成了 AWTK 的移植,程序在这颗芯片上可以直接跑起来。但"能跑"和"能改"是两回事:这套移植的矢量渲染核心实现在预编译静态库里,SDK 只开放 wrapper 层源码,调用链、配置宏、像素格式这些如果不读明白,后续任何调整都是盲改------改错一个宏,现象往往是花屏或帧率异常,排查成本很高。

还有一层更容易混淆的东西:AWTK 项目天然存在两套环境。PC 上用 AWTK Designer 设计界面、跑模拟器,用的是 IDE 自带 SDK 里的原版 AWTK 源码编译出来的程序;板端固件用的是芯片 SDK 里那份移植过的 AWTK 源码。两套环境各有一套颜色格式配置,入口、生效方式、命名习惯都不同,而 RGB(A)/BGR(A)、RGBA/BGRA/ARGB/ABGR 这些名词又长得极其相似。

本文做四件事:先把绕不开的专业术语用白话讲清楚;把板端移植拆开------启动、绘图刷新、送显、输入四条链全部给出函数级调用链和时序图;对着源码细读三个性能相关的核心机制(脏区算法、帧率时间控制、送显流水线与三本脏区账);最后梳理 PC 端与板端的颜色格式设置体系。所有结论标注文件与函数位置;运行时行为的推断均注明"未验证",留待实测补充。

二、术语速查:先把几个词说清楚

读移植代码之前,下面这些词必须先建立准确的概念,否则调用链里的每一步都会卡壳。全部用一句话白话解释,结合本移植的具体所指:

术语 白话解释 在本移植中的具体所指
离屏表面(offscreen surface) GPU 的一块不在屏幕上的画板。先在这块画板上把整帧画完,再一次性送去显示,观察者永远不会看到"画了一半"的画面 vgSurfaceCreate 创建的 1216×568 ARGB8888 显存区,AWTK 每帧在这里绘制
脏矩形(dirty rect) 屏幕上"内容变了、需要重画"的矩形区域。GUI 框架只重画脏区而不是整屏,是最重要的性能机制 控件调用 invalidate 后归并出的矩形列表,一路传到 VG 决定执行哪些绘制命令
包围盒(bounding box) 能把若干矩形全部罩住的最小外接矩形。矩形集合做"合并"时通常不追求精确形状,直接取包围盒------中间的空白会被多画一点,换来极简单的算法 dirty_rects_t 的 max 字段;rect_merge 函数的输出
场同步(vsync) 面板从上到下扫描完一帧的节拍信号,本例约 60Hz(周期约 17ms)。换显存地址必须在两帧之间的消隐期做,否则画面撕裂 LCD 控制器中断里的 0x01 状态位,换帧的唯一时机
帧缓冲(framebuffer) 存放一帧完整像素的内存块。"双缓冲"= 画一块、显示一块轮换;"三缓冲"再多一块排队,让渲染和显示彻底不打架 LCDC 直接扫描的显示帧,共三块,由队列管理
OSD 层 LCD 控制器的图文叠加层(区别于视频层)。仪表 UI 画在这层,视频倒车影像走另一层,硬件自动叠加 LCD_UI_LAYER,与 LCD_VIDEO_LAYER 并存
LCDC LCD 控制器。按面板时序逐像素扫描帧缓冲,送到显示接口 lcd.c 里那一族 ark_lcd_* 寄存器操作
PXP 一类经典的 2D 图形加速器硬件(Pixel Pipeline),擅长旋转、缩放、格式转换 pxp_scaler_rotate,每帧把横屏画面旋转 90 度
nanovg 轻量级矢量绘图库,API 风格接近 HTML5 Canvas 本移植里它只是一层"壳":接收 nvg 开头的绘图调用,翻译成 OpenVG
OpenVG Khronos 制定的 2D 矢量图形标准 API HV100 的 GPU 硬件实现的就是它,vg 开头的函数
vgcanvas AWTK 的矢量画布抽象接口(与 lcd 的位图接口并列)。所有 GPU 加速后端都挂在这条接口上 vgcanvas_nanovg_vg.c 实现的那张虚表
lcd_t 虚表 AWTK 把"显示器"抽象成一张函数表(开始帧/画图/结束帧......),移植一块屏就是填这张表 lcd_vgcanvas_init 挂接的那组函数

一个记忆锚点:离屏表面是"画板",显示帧是"展板",PXP 是"搬运工",场同步是"换展板的节拍"。后文所有流程都围绕这四个角色展开。

三、移植包的结构:两层架构与文件清单

AWTK 对"框架层"与"移植层"的切分相当干净。框架层的 widget、window_manager、canvas、image_manager 不出现任何硬件调用,绘制指令全部落在 lcd_t 与 vgcanvas_t 两个虚表接口上,事件循环落在 main_loop 接口上。所谓移植,就是把这几个接口在目标芯片上实现一遍:
#mermaid-svg-I62bdUCYiq13VE9h{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-I62bdUCYiq13VE9h .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-I62bdUCYiq13VE9h .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-I62bdUCYiq13VE9h .error-icon{fill:#552222;}#mermaid-svg-I62bdUCYiq13VE9h .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-I62bdUCYiq13VE9h .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-I62bdUCYiq13VE9h .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-I62bdUCYiq13VE9h .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-I62bdUCYiq13VE9h .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-I62bdUCYiq13VE9h .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-I62bdUCYiq13VE9h .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-I62bdUCYiq13VE9h .marker{fill:#333333;stroke:#333333;}#mermaid-svg-I62bdUCYiq13VE9h .marker.cross{stroke:#333333;}#mermaid-svg-I62bdUCYiq13VE9h svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-I62bdUCYiq13VE9h p{margin:0;}#mermaid-svg-I62bdUCYiq13VE9h .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-I62bdUCYiq13VE9h .cluster-label text{fill:#333;}#mermaid-svg-I62bdUCYiq13VE9h .cluster-label span{color:#333;}#mermaid-svg-I62bdUCYiq13VE9h .cluster-label span p{background-color:transparent;}#mermaid-svg-I62bdUCYiq13VE9h .label text,#mermaid-svg-I62bdUCYiq13VE9h span{fill:#333;color:#333;}#mermaid-svg-I62bdUCYiq13VE9h .node rect,#mermaid-svg-I62bdUCYiq13VE9h .node circle,#mermaid-svg-I62bdUCYiq13VE9h .node ellipse,#mermaid-svg-I62bdUCYiq13VE9h .node polygon,#mermaid-svg-I62bdUCYiq13VE9h .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-I62bdUCYiq13VE9h .rough-node .label text,#mermaid-svg-I62bdUCYiq13VE9h .node .label text,#mermaid-svg-I62bdUCYiq13VE9h .image-shape .label,#mermaid-svg-I62bdUCYiq13VE9h .icon-shape .label{text-anchor:middle;}#mermaid-svg-I62bdUCYiq13VE9h .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-I62bdUCYiq13VE9h .rough-node .label,#mermaid-svg-I62bdUCYiq13VE9h .node .label,#mermaid-svg-I62bdUCYiq13VE9h .image-shape .label,#mermaid-svg-I62bdUCYiq13VE9h .icon-shape .label{text-align:center;}#mermaid-svg-I62bdUCYiq13VE9h .node.clickable{cursor:pointer;}#mermaid-svg-I62bdUCYiq13VE9h .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-I62bdUCYiq13VE9h .arrowheadPath{fill:#333333;}#mermaid-svg-I62bdUCYiq13VE9h .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-I62bdUCYiq13VE9h .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-I62bdUCYiq13VE9h .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-I62bdUCYiq13VE9h .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-I62bdUCYiq13VE9h .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-I62bdUCYiq13VE9h .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-I62bdUCYiq13VE9h .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-I62bdUCYiq13VE9h .cluster text{fill:#333;}#mermaid-svg-I62bdUCYiq13VE9h .cluster span{color:#333;}#mermaid-svg-I62bdUCYiq13VE9h div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-I62bdUCYiq13VE9h .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-I62bdUCYiq13VE9h rect.text{fill:none;stroke-width:0;}#mermaid-svg-I62bdUCYiq13VE9h .icon-shape,#mermaid-svg-I62bdUCYiq13VE9h .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-I62bdUCYiq13VE9h .icon-shape p,#mermaid-svg-I62bdUCYiq13VE9h .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-I62bdUCYiq13VE9h .icon-shape .label rect,#mermaid-svg-I62bdUCYiq13VE9h .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-I62bdUCYiq13VE9h .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-I62bdUCYiq13VE9h .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-I62bdUCYiq13VE9h :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} lcd_t / vgcanvas_t / main_loop 三个接口
厂商移植层(芯片 SDK 提供,核心在预编译库)
nanovg → OpenVG → 矢量 GPU 硬件光栅化(渲染核心在预编译库)
vgReadPixels → PXP 旋转 → 三缓冲 → 场同步换帧送显
AWTK 框架层(不碰硬件,纯 C 实现)
widget / window_manager / canvas / image_manager 等一整套框架管理器(不含任何硬件调用)
全部绘制指令都落在 lcd_t 与 vgcanvas_t 两个虚表接口上
【图1:两层架构与每帧数据流向示意】

SDK 里移植相关的文件集中在四处:

文件 角色 关键内容
awtk/3rd/xm/src/main_loop_xm.c 事件循环移植 main_loop_init 把厂商事件系统接入 AWTK,触摸与按键分发
awtk/3rd/xm/src/native_window_xm.c 窗口与 LCD 工厂 创建 lcd 与 canvas,swap_buffer 送显
awtk/3rd/xm/src/vgcanvas_nanovg_vg.c vgcanvas 后端(约 1200 行,移植核心) nanovg API 到 OpenVG API 的翻译层,纹理与离屏表面管理
awtk/3rd/xm/src/xm_hmi_host.c 显示宿主 帧缓冲队列取帧、vgReadPixels 读回、PXP 旋转
awtk/src/platforms/ark/ OS 适配 时间、文件系统、线程(在 RTOS 上模拟 pthread)
vg_driver/vg_driver_wrapper.c 驱动桥(源码) xm_vg_loop 回调入口
vg_driver/VG_DRIVER_AWTK.a 矢量驱动(预编译) OpenVG 实现、xm_vg_init、canvasvg

一个值得先建立的认知:矢量渲染的核心实现在预编译静态库里,SDK 只开放 wrapper 层源码。判断"哪个符号在库里、哪个在自己工程里",最直接的办法是查链接器 map 文件:

text 复制代码
xm_vg_init   ...  Code  Gb  vg_main.o [2]           ← 预编译库内
xm_vg_loop   ...  Code  Gb  vg_driver_wrapper.o [1] ← 工程源码

这两行 map 输出直接引出下一节的问题。

四、启动调用链:调用了 xm_vg_init,为什么没看到 gui_app_start

SDK 板级示例的入口代码大致如下:

c 复制代码
#ifdef VG_DRIVER
    xm_vg_init((unsigned int)vgHeap, VG_HEAP_SIZE);  /* vgHeap 为静态数组,示例 14MB */
#else
    extern int gui_app_start(int lcd_w, int lcd_h);
    gui_app_start(OSD_WIDTH, OSD_HEIGHT);
#endif

初读很容易困惑:定义了 VG_DRIVER 的分支只调用了 xm_vg_init,gui_app_start 是谁调用的?答案是回调。map 文件已经给出线索:xm_vg_init 在库里、xm_vg_loop 在工程源码里,两者是"驱动与回调"的关系。完整启动调用链如下:

text 复制代码
① main()                          板级入口
 └─ ② main_awtk()
     └─ ③ xTaskCreate(awtk_thread, "awtk", ...)     创建初始化任务
         └─ ④ awtk_thread()
             ├─ 存储初始化 / 触摸初始化 / 资源挂载
             └─ ⑤ xm_vg_init(vgHeap, 14MB)
                 │  【VG_DRIVER_AWTK.a 内部,vg_main.o】
                 │   登记 VG 显存堆
                 │   初始化 OpenVG 上下文与 canvasvg
                 │   创建 UI 任务,入口最终回调 ↓
                 └─ 返回(不等 UI)
             └─ ⑥ while(1) 转为监控空转

    ⑦ 任务"awtk_ui"(驱动创建)
     └─ ⑧ xm_vg_loop(NULL)                          vg_driver_wrapper.c
         ├─ 任务改名 "awtk_ui",优先级设为 16
         └─ ⑨ gui_app_start(VG_W, VG_H)
             └─ ⑩ tk_init(w, h, APP_MOBILE, ...)    awtk_global.c
                 ├─ assets/font/input_method/window_manager 管理器
                 └─ ⑪ main_loop_init(w, h)          ←【移植点1】main_loop_xm.c
                     └─ ⑫ native_window_xm_init()   ←【移植点2】native_window_xm.c
                         └─ ⑬ lcd_nanovg_init()
                             └─ ⑭ vgcanvas_create() ←【移植点3】vgcanvas_nanovg_vg.c
                                 ├─ vgContextCreate()      OpenVG 上下文
                                 ├─ vgSurfaceCreate()      离屏渲染面
                                 └─ nvgCreateOpenVG(...)   nanovg 后端实例
                             └─ ⑮ lcd_vgcanvas_init()     挂接 lcd 虚表
             └─ ⑯ assets_init() → application_init()
             └─ ⑰ tk_run()                           主循环,不再返回

任务视角的时序:
AWTK框架 awtk_ui任务 驱动创建 VG驱动库 预编译 板级初始化任务 awtk AWTK框架 awtk_ui任务 驱动创建 VG驱动库 预编译 板级初始化任务 awtk #mermaid-svg-diZwcSKF3YrXrE6n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-diZwcSKF3YrXrE6n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-diZwcSKF3YrXrE6n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-diZwcSKF3YrXrE6n .error-icon{fill:#552222;}#mermaid-svg-diZwcSKF3YrXrE6n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-diZwcSKF3YrXrE6n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-diZwcSKF3YrXrE6n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-diZwcSKF3YrXrE6n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-diZwcSKF3YrXrE6n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-diZwcSKF3YrXrE6n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-diZwcSKF3YrXrE6n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-diZwcSKF3YrXrE6n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-diZwcSKF3YrXrE6n .marker.cross{stroke:#333333;}#mermaid-svg-diZwcSKF3YrXrE6n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-diZwcSKF3YrXrE6n p{margin:0;}#mermaid-svg-diZwcSKF3YrXrE6n .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-diZwcSKF3YrXrE6n text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-diZwcSKF3YrXrE6n .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-diZwcSKF3YrXrE6n .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-diZwcSKF3YrXrE6n .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-diZwcSKF3YrXrE6n .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-diZwcSKF3YrXrE6n #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-diZwcSKF3YrXrE6n .sequenceNumber{fill:white;}#mermaid-svg-diZwcSKF3YrXrE6n #sequencenumber{fill:#333;}#mermaid-svg-diZwcSKF3YrXrE6n #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-diZwcSKF3YrXrE6n .messageText{fill:#333;stroke:none;}#mermaid-svg-diZwcSKF3YrXrE6n .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-diZwcSKF3YrXrE6n .labelText,#mermaid-svg-diZwcSKF3YrXrE6n .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-diZwcSKF3YrXrE6n .loopText,#mermaid-svg-diZwcSKF3YrXrE6n .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-diZwcSKF3YrXrE6n .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-diZwcSKF3YrXrE6n .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-diZwcSKF3YrXrE6n .noteText,#mermaid-svg-diZwcSKF3YrXrE6n .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-diZwcSKF3YrXrE6n .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-diZwcSKF3YrXrE6n .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-diZwcSKF3YrXrE6n .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-diZwcSKF3YrXrE6n .actorPopupMenu{position:absolute;}#mermaid-svg-diZwcSKF3YrXrE6n .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-diZwcSKF3YrXrE6n .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-diZwcSKF3YrXrE6n .actor-man circle,#mermaid-svg-diZwcSKF3YrXrE6n line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-diZwcSKF3YrXrE6n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} main_awtk 创建初始化任务存储/触摸/资源初始化① xm_vg_init vgHeap 14MB② 登记显存堆③ 初始化OpenVG上下文与canvasvg④ 创建UI任务 入口回调xm_vg_loop⑤ 返回 不等UI转入监控空转⑥ xm_vg_loop 改名awtk_ui 优先级16⑦ gui_app_start 1216x568⑧ tk_init 创建各管理器⑨ main_loop_init 建窗口 lcd vgcanvas⑩ assets_init application_init⑪ tk_run 主循环不再返回
【图2:启动阶段调用时序图(两个任务、一次回调)】

回调体的源码很短,值得原样读一遍:

c 复制代码
int xm_vg_loop(void *context)
{
#ifdef AWTK
    char* strName = pcTaskGetName(NULL);
    tk_snprintf(strName, configMAX_TASK_NAME_LEN-1, "awtk_ui"); /* 任务改名 */
    vTaskPrioritySet(NULL, 16);                                  /* 提升优先级 */
    gui_app_start(xm_vg_get_width(), xm_vg_get_height());       /* AWTK 在这里启动 */
    return 0;
#else
    /* 其余宿主形态分支省略 */
#endif
}

两个要点。其一,AWTK 主循环运行在驱动创建的 awtk_ui 任务里,板级初始化任务调用 xm_vg_init 后就返回了------调试时在 gui_app_start 里打断点,调用栈落在驱动任务内,不要在初始化任务里找。其二,UI 逻辑尺寸最终来自显示宿主层的窗口尺寸(即板级 OSD 配置),native_window_xm_get_info 中 ratio 固定为 1,与 Designer 工程 DPR 取 x1 正好对应。

五、绘图刷新的全景:一帧画面从"我变了"到上屏

这是全文最核心的一节。先不给代码,用"五幕剧"把一帧的生命周期讲清楚,再上调用链和流程图对照。

第 0 幕(静止) :界面没有任何变化时,系统什么都不做。没有定时器到期、没有触摸、没有动画时,主循环每一轮醒来,发现"脏矩形列表是空的",直接跳过绘制去睡觉。静止的界面不消耗渲染资源------这是后面一切性能讨论的前提。

第 1 幕(登记) :某个控件的状态变了------数值刷新、图片切换、动画走了一格。控件调用 invalidate 声明"我这块区域脏了",这个矩形被归并进原生窗口的脏矩形列表。注意这一步只是记账,不触发任何绘制。

第 2 幕(醒来) :主循环 main_loop_simple_run 的下一轮 step 醒来,先派发输入事件、执行到期的定时器和 idle 任务(数值刷新、动画推进通常就发生在定时器里,它们又产生新的 invalidate),然后进入绘制的总入口 window_manager_paint。

第 3 幕(裁决) :window_manager_paint_normal 先做两个裁决。裁决一:限帧 ------距上一帧的间隔加上一帧的绘制耗时不足 1000/max_fps,就不画了,把差额记下来让主循环多睡一会(精确算法见第七节);裁决二:脏区检查------脏矩形列表为空就不画(第 0 幕的情况)。两个裁决都通过,才真正开始画。

第 4 幕(绘制) :以脏矩形为裁剪范围,从窗口管理器向下递归调用每个控件的绘制函数。控件画的是矢量指令(路径、填充、文字、贴图),经 canvas 一路翻译到 nanovg 再到 OpenVG,由 VG 硬件在离屏表面上光栅化。三个关键细节:图片只在首次绘制时上传为 GPU 纹理并缓存,之后直接复用;文字由 stb_truetype 转成字形轮廓后同样走矢量路径填充;裁剪区被交给 VG 处理,所以只有脏区内的命令真正生效。

第 5 幕(收尾送显) :帧结束时提交全部 VG 命令,vgFinish 等硬件画完,然后把离屏表面整屏读回内存、PXP 旋转 90 度写入显示帧、入就绪队列;LCD 控制器在下一次场同步的消隐期换扫描地址。这一幕的细节在第八节展开。

对照的函数级调用链(每行都能在源码里找到):

text 复制代码
main_loop_simple_run                    主循环 while(main_loop_simple.c:229)
 └─ main_loop_simple_step               一帧(:212)
     ├─ main_loop_dispatch_input        ← 第2幕:触摸/按键事件注入
     ├─ idle / timer 派发               ← 第2幕:数值刷新/动画推进(再触发invalidate)
     └─ window_manager_paint
         └─ window_manager_default_paint
             └─ window_manager_paint_normal        ← 第3幕:裁决
                 ├─ 限帧检查:帧距不足 1000/max_fps → 跳过本帧(:644)
                 ├─ 脏矩形 nr==0 → 整帧跳过(native_window_begin_frame:114)
                 └─ 真正绘制:                       ← 第4幕
                     canvas_begin_frame             base/canvas.c:391
                      └─ lcd_vgcanvas_begin_frame   lcd_vgcanvas.inc:65(一行直通)
                          └─ vgcanvas_nanovg_begin_frame  vgcanvas_nanovg_vg.c:626
                              ├─ vgMakeCurrent(离屏表面)  把画板设为渲染目标
                              ├─ nvgBeginFrame(...)
                              └─ 记录脏矩形 → VG 只执行脏区内的命令
                     ── 控件树绘制(dirty_rects_paint 按脏区裁剪递归)──
                     │  矢量: nvgBeginPath/nvgFill → vg* → GPU 光栅化
                     │  文字: nvgText → fontstash/stb_truetype → VG 路径填充
                     │  图片: ensure_image 首次上传纹理并缓存(:1041)
                     └─ canvas_end_frame             ← 第5幕开始
                         └─ lcd_vgcanvas_end_frame
                             └─ vgcanvas_nanovg_end_frame(:699)
                                 ├─ nvgEndFrame   提交本帧 VG 命令
                                 ├─ vgFinish      等待硬件画完
                                 └─ native_window_swap_buffer → 送显(第八节)
     └─ main_loop_sleep                 按期望睡眠时间休眠(帧率控制见第七节)

流程图(五幕对应五个泳道阶段):
#mermaid-svg-rd5rK7wdfTnjmR2n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rd5rK7wdfTnjmR2n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rd5rK7wdfTnjmR2n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rd5rK7wdfTnjmR2n .error-icon{fill:#552222;}#mermaid-svg-rd5rK7wdfTnjmR2n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rd5rK7wdfTnjmR2n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rd5rK7wdfTnjmR2n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rd5rK7wdfTnjmR2n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rd5rK7wdfTnjmR2n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rd5rK7wdfTnjmR2n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rd5rK7wdfTnjmR2n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rd5rK7wdfTnjmR2n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rd5rK7wdfTnjmR2n .marker.cross{stroke:#333333;}#mermaid-svg-rd5rK7wdfTnjmR2n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rd5rK7wdfTnjmR2n p{margin:0;}#mermaid-svg-rd5rK7wdfTnjmR2n .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rd5rK7wdfTnjmR2n .cluster-label text{fill:#333;}#mermaid-svg-rd5rK7wdfTnjmR2n .cluster-label span{color:#333;}#mermaid-svg-rd5rK7wdfTnjmR2n .cluster-label span p{background-color:transparent;}#mermaid-svg-rd5rK7wdfTnjmR2n .label text,#mermaid-svg-rd5rK7wdfTnjmR2n span{fill:#333;color:#333;}#mermaid-svg-rd5rK7wdfTnjmR2n .node rect,#mermaid-svg-rd5rK7wdfTnjmR2n .node circle,#mermaid-svg-rd5rK7wdfTnjmR2n .node ellipse,#mermaid-svg-rd5rK7wdfTnjmR2n .node polygon,#mermaid-svg-rd5rK7wdfTnjmR2n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rd5rK7wdfTnjmR2n .rough-node .label text,#mermaid-svg-rd5rK7wdfTnjmR2n .node .label text,#mermaid-svg-rd5rK7wdfTnjmR2n .image-shape .label,#mermaid-svg-rd5rK7wdfTnjmR2n .icon-shape .label{text-anchor:middle;}#mermaid-svg-rd5rK7wdfTnjmR2n .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rd5rK7wdfTnjmR2n .rough-node .label,#mermaid-svg-rd5rK7wdfTnjmR2n .node .label,#mermaid-svg-rd5rK7wdfTnjmR2n .image-shape .label,#mermaid-svg-rd5rK7wdfTnjmR2n .icon-shape .label{text-align:center;}#mermaid-svg-rd5rK7wdfTnjmR2n .node.clickable{cursor:pointer;}#mermaid-svg-rd5rK7wdfTnjmR2n .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rd5rK7wdfTnjmR2n .arrowheadPath{fill:#333333;}#mermaid-svg-rd5rK7wdfTnjmR2n .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rd5rK7wdfTnjmR2n .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rd5rK7wdfTnjmR2n .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rd5rK7wdfTnjmR2n .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rd5rK7wdfTnjmR2n .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rd5rK7wdfTnjmR2n .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rd5rK7wdfTnjmR2n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rd5rK7wdfTnjmR2n .cluster text{fill:#333;}#mermaid-svg-rd5rK7wdfTnjmR2n .cluster span{color:#333;}#mermaid-svg-rd5rK7wdfTnjmR2n div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rd5rK7wdfTnjmR2n .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rd5rK7wdfTnjmR2n rect.text{fill:none;stroke-width:0;}#mermaid-svg-rd5rK7wdfTnjmR2n .icon-shape,#mermaid-svg-rd5rK7wdfTnjmR2n .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rd5rK7wdfTnjmR2n .icon-shape p,#mermaid-svg-rd5rK7wdfTnjmR2n .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rd5rK7wdfTnjmR2n .icon-shape .label rect,#mermaid-svg-rd5rK7wdfTnjmR2n .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rd5rK7wdfTnjmR2n .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rd5rK7wdfTnjmR2n .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rd5rK7wdfTnjmR2n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第5幕 收尾送显
第4幕 绘制
第3幕 裁决
第2幕 醒来
第1幕 登记
第0幕 静止
是
否
是
否
否
是
主循环醒来
脏矩形为空?
跳过绘制 去睡觉
控件状态变化
invalidate 矩形入脏区列表

只记账不绘制
派发输入/定时器/idle

动画推进产生新脏区
帧距足够?

max_fps 限帧
跳帧 差额记入期望睡眠
脏区非空?
begin_frame 三连下探

vgMakeCurrent 设离屏表面
按脏区裁剪递归绘制控件树
矢量→VG路径 GPU光栅化
文字→字形轮廓→VG
图片→首次上传纹理后缓存
nvgEndFrame 提交命令
vgFinish 等GPU画完
读回+旋转+换帧 见第八节
main_loop_sleep 休眠
【图3:绘图刷新五阶段流程图】

帧结束的代码很短,但每行都有分量:

c 复制代码
static ret_t vgcanvas_nanovg_end_frame(vgcanvas_t* vgcanvas) {
  nvgEndFrame(vg);                 /* 提交本帧全部 VG 命令 */
  vgFinish();                      /* 等待硬件画完 */
  native_window_swap_buffer(...);  /* 进入送显 */
}

六、脏区处理源码细读:账本结构、归并算法与裁剪的三重下传

第 1 幕里"只记账"的那本账,这一节彻底翻开。路线是:先看账本的数据结构,再逐行读记账算法,用数值算例走一遍,然后看绘制时怎么取账、裁剪怎么一路交到 VG 硬件。读完这一节,"脏区算法"四个字应该变成具体的几条 if。

6.1 账本长什么样:一个定长结构体,天生只为"少量大矩形"服务

AWTK 的脏区账本就是 dirty_rects.h:44 的这个结构体,没有任何动态内存:

c 复制代码
#define TK_MAX_DIRTY_RECT_NR 10               /* dirty_rects.h:30,可被工程覆盖 */

typedef struct _dirty_rects_t {
  uint32_t nr;                                /* 当前有效矩形个数 */
  rect_t max;                                 /* 全部脏区的总包围盒 */
  rect_t rects[TK_MAX_DIRTY_RECT_NR];         /* 矩形数组,定长 10 */
  bool_t debug;                               /* 画红框调试 */
  bool_t profile;                             /* 打印每块脏区耗时 */
  bool_t disable_multiple;                    /* 退化成单脏区模式(对比测试用) */
} dirty_rects_t;

设计取向写在头文件注释里:脏矩形通常个数不多而且互不重叠 ,所以用定长数组避免内存分配、用简单归并降低计算开销。注意 nr 和 max 是两套口径:rects[] 是"尽量互不相交的清单",绘制时逐块使用;max 是"一个罩住所有脏区的大包围盒",给只想要一个矩形的场合(后面 lcd_begin_frame 取的就是它)。整个结构体几十字节,所以后面的代码里到处直接 memcpy 整本账,值拷贝传播,零成本。

6.2 记账算法 dirty_rects_add:五条规则,一个取向

记一笔账的入口在 dirty_rects.h:173,全文不到 30 行:

c 复制代码
static inline ret_t dirty_rects_add(dirty_rects_t* dirty_rects, const rect_t* r) {
  if (r->w == 0 || r->h == 0) {
    return RET_OK;                                        /* 规则0:空矩形直接丢弃 */
  }
  if (dirty_rects->nr >= TK_MAX_DIRTY_RECT_NR) {          /* 规则1:账本满了(10个) */
    for (i = 1; i < dirty_rects->nr; i++) {
      rect_merge(dirty_rects->rects, dirty_rects->rects + i);  /* 全部并进第 0 个 */
    }
    dirty_rects->nr = 1;
  }
  rect_merge(&(dirty_rects->max), r);                     /* 规则2:max 永远更新 */
  for (i = 0; i < dirty_rects->nr; i++) {
    rect_t* iter = dirty_rects->rects + i;
    if (rect_has_intersect(iter, r)) {                    /* 规则3:与谁相交就并进谁 */
      rect_merge(iter, r);
      return dirty_rects_fix(dirty_rects);                /* 合并可能引发新的重叠,善后 */
    }
  }
  dirty_rects->rects[dirty_rects->nr++] = *r;             /* 规则4:谁都不相交,追加 */
  return RET_OK;
}

五个规则逐个说。规则 0 ,宽高为 0 的矩形(比如刚创建、还没布局的控件 invalidate)直接丢弃,账本不会出现废条目。规则 1 ,账本记满 10 个后再来新的,先把已有 10 个全部并成一个包围盒 、nr 归 1------注意这一步发生在"查相交"之前 ,意味着第 11 笔账必然落进这个唯一的大矩形;所以账本里的矩形数永远在 1~10 之间,最坏退化成一个大块。规则 2 ,max 无条件吸收每个新矩形------包围盒只会变大,一帧内不会缩小。规则 3 ,线性扫描找第一个与新矩形相交的旧矩形,rect_merge 并进去。规则 4,都不相交就追加。

两个容易误判的点。其一,rect_merge(tkc/rect.c:24)做的不是"精确并集",而是取两矩形的包围盒 :两个对角的小矩形合并后,中间的大片空白也会被划进脏区、跟着重画。这是典型的"宁可多画、不可漏画"取舍------精确并集要处理任意多边形,包围盒只要四次比较。其二,dirty_rects_fix(dirty_rects.h:145)的善后并不彻底:它只检查第 0 个矩形与其余矩形是否相交,合并后递归重启;非 0 号矩形之间的重叠可能保留。后果只是同一区域可能被两块脏区各画一次(多画无害),正确性不受影响------又是一个"简单优先"的取舍。

6.3 数值算例:仪表盘三笔账,账本如何演化

拿一个典型仪表场景走一遍。屏幕 1216×568,依次发生三次 invalidate:

事件 新矩形 (x,y,w,h) 与账本关系 记账后 rects\[\] nr / max
车速指针转了一格 (500, 200, 40, 40) 账本空,追加 (500,200,40,40) 1 / 同左
相邻转速数字刷新 (520, 210, 60, 30) 与指针区相交 → 并入 (500,200,80,40) 1 / 同左
右上角 FPS 角标 (1156, 0, 60, 30) 不相交,追加 (500,200,80,40), (1156,0,60,30) 2 / (500,0,716,240)

两个观察:转速数字与指针本是两个控件、两块区域,因为挨得近被并成一块(中间空隙多画);max 把左下角和右上角两块罩成一个 716×240 的大框------动画越是满屏跑,脏区越容易滚成大包围盒,这正是后面调试技巧要直观观察的东西。反过来,若一帧内产生第 11 笔不相交的账,规则 1 会把整个账本瞬间并成一块,脏区机制退化成"整帧重画",这就是 TK_MAX_DIRTY_RECT_NR 要留足够余量的原因。

6.4 绘制时怎么取账:两个早退分支与"整列表下传"

第 3 幕裁决通过后进入取账。先看两个早退分支,它们是"静止界面零开销"的保障。

第一个在 native_window_begin_frame(native_window.c:114):

c 复制代码
dr = &(win->dirty_rects);
if (dr->nr == 0) {
  return RET_FAIL;                 /* 账本是空的:整帧跳过 */
}
c = native_window_get_canvas(win);
canvas_begin_frame(c, dr, mode);   /* 把整本账(结构体指针)传给 canvas */

paint_normal 里用 if (native_window_begin_frame(...) == RET_OK) 接住它(window_manager_default.c:703),返回失败就什么都不画------注意此时 fps 计数已经加过(见第七节),但控件树、VG 命令、送显全部不会发生。

第二个在 dirty_rects_paint(dirty_rects.inc:27):lcd_is_support_dirty_rect 为假时直接返回。这套移植里 lcd 必须声明支持脏矩形(lcd_vgcanvas_init 里 support_dirty_rect = TRUE),绘制完全建立在脏区机制上。

取账链路本身值得记一次:canvas_begin_frame → lcd_begin_frame(lcd.c:29)。后者做两件事:把账本的 max 存进 lcd->dirty_rect(给只想要一个矩形的后端用),把整本账的指针挂到 lcd->dirty_rects(lcd.c:42);随后 paint_normal 用 lcd_get_dirty_rects 取回,交给 dirty_rects_paint(window_manager_default.c:713-725)。

dirty_rects_paint 有两个编译变体,选哪个由 DIRTY_RECTS_CLIP_SUPPORT 决定------本工程的 IAR 工程文件在预处理器里定义了它(awtk.ewp),生效的是多脏区整列表变体:

c 复制代码
dirty_rects_clip = *dirty_rects;                            /* 账本值拷贝一份 */
widget_paint_with_clip_dirty_rects(widget, &dirty_rects_clip, c, on_paint);

它调用 widget_paint_with_clip_dirty_rects(widget_vtable.c:277):把控件矩形与整个脏区列表 求交(rect_intersect_dirty_rects,dirty_rects.h:259------逐块求交集再归并),交结果整体设为裁剪集合,然后控件树只走一遍。对照另一变体(dirty_rects.inc:111 的循环):每个脏区把整棵控件树重走一遍、每块单独裁剪------树小无所谓,树大时"走一遍、带一张裁剪清单"明显占优。两变体的语义相同:每个控件只在与其相交的脏区内被重画。

6.5 裁剪的三重下传:canvas 让权、vgcanvas 记账、nanovg 多重裁剪

脏区怎么变成真正的像素裁剪?软件渲染会在 canvas 上设裁剪矩形、逐像素判断;GPU 路线反其道而行,分三步把裁剪"让权"给硬件。

第一步,canvas 自认无限大。 canvas_set_clip_rect_dirty_rects(canvas.c:225)把脏区列表经 lcd_set_clip_dirty_rects 交给下层后,做了个反直觉的动作:

c 复制代码
#if defined( WITH_NANOVG_GPU ) || defined( WITH_VG_GPU )
    /* 把 canvas 的裁剪区设置为无限大,让 vg 来处理裁剪区的问题 */
    c->clip_left = 0;   c->clip_top = 0;
    c->clip_right = 0x7fffffff;   c->clip_bottom = 0x7fffffff;
#endif

canvas 自己的逐像素裁剪被关闭(本工程定义了 WITH_VG_GPU,此分支生效),CPU 不做无谓的边界判断。

第二步,vgcanvas 层先记账。 vgcanvas_nanovg_clip_dirty_rects(vgcanvas_nanovg.inc:420)把整本账 memcpy 进画布对象(base.dirty_rects),同时把 max 存进单矩形字段------到此为止还没碰硬件。

第三步,绘制调用发生时把"交集数组"交给 nanovg 的多重裁剪。 真正下 scissors 的是 vgcanvas_nanovg_clip_rect(vgcanvas_nanovg.inc:306)。它把控件要画的矩形 r1 与账本逐块求交,得到 0~10 个真正需要生效的交集矩形,装进数组后调用的是一个厂商扩展的原生 nanovg 接口:

c 复制代码
/* 单矩形版是 nvgScissor;这是多矩形版,vgcanvas_nanovg.inc:318 声明,实现预编译库 */
extern void nvgScissors(NVGcontext* ctx, int* dirty_rect, int dirty_size, int* max);
extern int  nvgGetCurrScissors(NVGcontext* ctx, float* scissor_rects, int cb);

rect_intersect_dirty_rects(&vgcanvas->dirty_rects, &r1, &cilp_rects);
for (i = 0; i < cilp_rects.nr; i++) { ... rects[nr++] = 交集矩形; }
nvgScissors(vg, (int*)rects, nr, (int*)&cilp_rects.max);

标准 nanovg 只有一个 scissor 矩形;OpenVG 硬件的 VG_SCISSOR_RECTS 天生支持矩形列表 裁剪。厂商在预编译库里给 nanovg 补了多矩形版本,把数组直接映射到硬件的多重裁剪(库内部实现,映射细节未开源,按 OpenVG 规范推断)。配套的 vgcanvas_nanovg_is_rectf_in_clip_rect(:379)还让框架在 CPU 侧做快速剔除:绘制目标与任何一块当前 scissors 都不相交时,命令根本不下发。所以"VG 只光栅化脏区"的准确含义是:裁剪以矩形列表形式驻留在硬件里,CPU 侧还有一道"不相交就不发命令"的预过滤。

6.6 第四站与三级节能总表

前三站省的是"画"的开销。第四站本可以省"送"的开销------只把脏区从离屏表面拷到显示帧,跳过整屏读回与 PXP 旋转。它的代码是现成的,但控制宏 ENABLE_FB_DIRTY_RECTS_COPY 只在 LCD 驱动源文件内部 define,vgcanvas 后端与显示宿主层编译时看不到,整条路径编译关闭。因为这套机制与三块显示帧的账本同步强相关,完整源码细读放到第八节 8.4。三级节能对照:

级别 机制 关键代码 本工程状态
1 画面无变化整帧跳过 native_window_begin_frame 返回失败 生效
2 只重画脏区内的控件,VG 只光栅化脏区 dirty_rects_paint + nvgScissors 多重裁剪 生效
3 读回/旋转只处理脏区 vg_surface_sync 三本账路径(第八节 8.4) 编译关闭(宏不可见)

两个实用调试开关 :dirty_rects_set_debug(dirty_rects.h:212)打开后,dirty_rects_paint 会用红框把每块脏区画在屏幕上(dirty_rects.inc:63);dirty_rects_set_profile(:227)会逐块打印"第几块矩形、耗时多少微秒"。调优时前者看面积(动画是否把脏区滚成大包围盒),后者看单块耗时分布,两个一起用基本能定位"帧率被谁吃掉"。dirty_rects_set_disable_multiple(:242)则把多脏区退化成单 max 模式,专门用来做 A/B 对比。收益数值未实测,留待实测篇。
#mermaid-svg-fi62sNhrI5EkppcS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fi62sNhrI5EkppcS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fi62sNhrI5EkppcS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fi62sNhrI5EkppcS .error-icon{fill:#552222;}#mermaid-svg-fi62sNhrI5EkppcS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fi62sNhrI5EkppcS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fi62sNhrI5EkppcS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fi62sNhrI5EkppcS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fi62sNhrI5EkppcS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fi62sNhrI5EkppcS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fi62sNhrI5EkppcS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fi62sNhrI5EkppcS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fi62sNhrI5EkppcS .marker.cross{stroke:#333333;}#mermaid-svg-fi62sNhrI5EkppcS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fi62sNhrI5EkppcS p{margin:0;}#mermaid-svg-fi62sNhrI5EkppcS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fi62sNhrI5EkppcS .cluster-label text{fill:#333;}#mermaid-svg-fi62sNhrI5EkppcS .cluster-label span{color:#333;}#mermaid-svg-fi62sNhrI5EkppcS .cluster-label span p{background-color:transparent;}#mermaid-svg-fi62sNhrI5EkppcS .label text,#mermaid-svg-fi62sNhrI5EkppcS span{fill:#333;color:#333;}#mermaid-svg-fi62sNhrI5EkppcS .node rect,#mermaid-svg-fi62sNhrI5EkppcS .node circle,#mermaid-svg-fi62sNhrI5EkppcS .node ellipse,#mermaid-svg-fi62sNhrI5EkppcS .node polygon,#mermaid-svg-fi62sNhrI5EkppcS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fi62sNhrI5EkppcS .rough-node .label text,#mermaid-svg-fi62sNhrI5EkppcS .node .label text,#mermaid-svg-fi62sNhrI5EkppcS .image-shape .label,#mermaid-svg-fi62sNhrI5EkppcS .icon-shape .label{text-anchor:middle;}#mermaid-svg-fi62sNhrI5EkppcS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fi62sNhrI5EkppcS .rough-node .label,#mermaid-svg-fi62sNhrI5EkppcS .node .label,#mermaid-svg-fi62sNhrI5EkppcS .image-shape .label,#mermaid-svg-fi62sNhrI5EkppcS .icon-shape .label{text-align:center;}#mermaid-svg-fi62sNhrI5EkppcS .node.clickable{cursor:pointer;}#mermaid-svg-fi62sNhrI5EkppcS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fi62sNhrI5EkppcS .arrowheadPath{fill:#333333;}#mermaid-svg-fi62sNhrI5EkppcS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fi62sNhrI5EkppcS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fi62sNhrI5EkppcS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fi62sNhrI5EkppcS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fi62sNhrI5EkppcS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fi62sNhrI5EkppcS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fi62sNhrI5EkppcS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fi62sNhrI5EkppcS .cluster text{fill:#333;}#mermaid-svg-fi62sNhrI5EkppcS .cluster span{color:#333;}#mermaid-svg-fi62sNhrI5EkppcS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fi62sNhrI5EkppcS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fi62sNhrI5EkppcS rect.text{fill:none;stroke-width:0;}#mermaid-svg-fi62sNhrI5EkppcS .icon-shape,#mermaid-svg-fi62sNhrI5EkppcS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fi62sNhrI5EkppcS .icon-shape p,#mermaid-svg-fi62sNhrI5EkppcS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fi62sNhrI5EkppcS .icon-shape .label rect,#mermaid-svg-fi62sNhrI5EkppcS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fi62sNhrI5EkppcS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fi62sNhrI5EkppcS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fi62sNhrI5EkppcS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
开(当前为关)
关(现状)
控件 invalidate 声明脏区
native_window_invalidate(native_window.c:85)

矩形裁到屏幕边界内
dirty_rects_add 五规则记账(6.2节)

相交并进/不相交追加/满10并1
下一轮 paint_normal 裁决
脏区列表为空?
整帧跳过 零渲染开销
dirty_rects_paint 整列表下传

控件树只走一遍 每控件与列表求交
canvas 裁剪设为无限大

账本交给 vgcanvas 层
clip_rect 求交集数组

nvgScissors 多重裁剪下发硬件
CPU 快速剔除:与所有裁剪块

都不相交的命令不下发
ENABLE_FB_DIRTY_RECTS_COPY 宏可见?
vg_surface_sync 三本账只拷脏区

见第八节8.4
整屏 vgReadPixels + PXP 旋转
【图4:脏区处理六步流程】

七、帧率控制源码细读:max_fps 的三级传导与时间账

帧率控制没有集中在一个函数里,而是三级传导、分散在三处。单独看任何一处都容易误判------比如把"帧率上不去"归因于 sleep 配错,方向就偏了。这一节除了逐级读源码,还把每级的时间账算清楚。

7.1 全景:三级各自的输入与输出

级 发生位置 输入 输出 一句话
一:设定上限 window_manager_set_max_fps 期望帧率 wm->max_fps 一个数字 只是目标,不产生任何动作
二:跳帧裁决 window_manager_paint_normal 开头 帧距 elapsed、上限 duration 画/不画;不画时输出期望睡眠 真正的节流阀
三:执行休眠 main_loop_sleep_default 三个数取最小 实际 sleep 毫秒数 配角,保证响应上限

主循环每轮的完整顺序(main_loop_simple.c:212 的 step)把三级串起来:派发输入 → 派发定时器/idle → 布局检查 → window_manager_paint(内含一、二级)→ 取回 paint 写下的期望睡眠时间交给主循环 → 本轮结束进入 main_loop_sleep(第三级)。

7.2 第二级跳帧裁决:elapsed 的本质是"帧起点到帧起点"

节流发生在 window_manager_paint_normal 的开头(window_manager_default.c:644):

c 复制代码
uint64_t start_time = time_now_ms();                       /* :641 进函数就掐表 */
if (WINDOW_MANAGER(wm)->max_fps) {
  uint32_t duration = 1000 / WINDOW_MANAGER(wm)->max_fps;              /* 30fps → 33ms */
  uint32_t elapsed_time = start_time - wm->last_paint_time + wm->last_paint_cost;
  if (elapsed_time < duration) {
    window_manager_set_curr_expected_sleep_time(widget, duration - elapsed_time);
    return RET_OK;                                                      /* 本帧不画 */
  }
}
/* ......真正绘制...... */
wm->last_paint_time = time_now_ms();                       /* :731 画完的时刻 */
wm->last_paint_cost = wm->last_paint_time - start_time;    /* :732 本帧总耗时 */

关键在 elapsed_time 的算法。last_paint_time 是上一帧画完 的时刻,last_paint_cost 是上一帧从进 paint_normal 到画完 的耗时------注意这个"画完"包含的不只是控件绘制:native_window_end_frame 里的 nvgEndFrame、vgFinish(等 GPU)、整屏 vgReadPixels、PXP 旋转、帧入队(第八节的三段接力)全部发生在 :731 赋值之前。也就是说 last_paint_cost 是"绘制+送显"的全口径耗时。

于是:

复制代码
elapsed = 现在 − 上帧画完的时刻 + 上帧画了多久 ≈ 现在 − 上帧开始的时刻

它的本质是帧起点到帧起点 的间隔。为什么必须这么算?如果只按"现在 − 上帧画完"判断,绘制越慢的机器越容易满足"隔得够久",实际帧率会被绘制耗时拖到 max_fps 以下;把绘制耗时加回去之后,无论每帧画多快,稳态帧周期都精确等于 1000/max_fps(前提:绘制耗时小于帧周期)。跳帧时把差额 duration − elapsed 写进"期望睡眠时间",这是传给第三级的接力棒。

用 30fps(duration≈33ms)列三笔账:

情形 上帧耗时 距上帧画完 elapsed 判定 效果
A 画得快 10ms 25ms 35ms ≥33 → 画 正常按 33ms 节奏出帧
B 刚画完不久 10ms 15ms 25ms <33 → 跳 期望睡眠 8ms,下轮再看
C 画得慢(本板现状量级) 45ms 5ms 50ms ≥33 → 画 永不跳帧,帧率=1000÷50=20fps

情形 C 就是理解实测帧率的钥匙:一旦"绘制+送显"耗时超过帧周期,二级节流就不再起作用,帧率被耗时决定------max_fps 从"上限"变成了够不着的"目标"。这与第五节实测 20~22fps、以及第八节 8MB/帧的搬运账单能对上(每帧约 45~50ms,推断值,留待实测篇用毫秒级打点证实)。

7.3 第三级休眠:三个数取最小,各自守护一件事

主循环每轮 step 结束调用 main_loop_sleep(main_loop.c:90):

c 复制代码
ret_t main_loop_sleep_default(main_loop_t* l) {
  uint32_t now = time_now_ms();
  uint32_t gap = now - l->last_loop_time;                  /* 距上轮循环结束 */
  uint32_t sleep_time = TK_MAX_SLEEP_TIME;                 /* 1000/120 ≈ 8ms */
  int32_t least_sleep_time = gap > TK_MAX_SLEEP_TIME ? 0 : (TK_MAX_SLEEP_TIME - gap);
  sleep_time = tk_min(least_sleep_time, sleep_time);       /* 数1:循环节拍余量 */
  sleep_time = tk_min(sleep_time, l->curr_expected_sleep_time);  /* 数2:第二级接力棒 */
  if (sleep_time > 0) { sleep_ms(sleep_time); }
  l->last_loop_time = time_now_ms();
  return RET_OK;
}

三个数各自守护一件事。8ms 这个数 来自 types_def.h:266 的 TK_MAX_LOOP_FPS 120,即 TK_MAX_SLEEP_TIME = 1000/120 ≈ 8------它保证主循环醒来的频率不低于 120Hz ,任何输入事件最迟约 8ms 就会被看到(触摸响应的大头不在链路,在这里的睡眠上限,见第九节)。数 1(节拍余量) :本轮醒着的时间已经吃掉多少,就把 8ms 的额度减掉多少------它管的是"主循环节拍恒定",与帧率无关。数 2(接力棒):跳帧时第二级写下的差额,管的是"别太早醒来白转一圈"。

接力棒还有一个特殊值:0xFFFFFFFF。window_manager_default_paint 入口先把它重置为 0xFFFFFFFF(window_manager_default.c:927),paint_normal 里只有"跳帧"才会写小值;main_loop_simple_step 在 paint 结束后取回这个值(main_loop_simple.c:220)。0xFFFFFFFF 参与取最小等于不生效------也就是说正常画帧的轮次,睡眠由 8ms 上限独管 ,即使 max_fps 设 0(不限帧),循环也至多睡 8ms。三数取最小之后,sleep_ms 一觉醒来,last_loop_time 更新,进入下一轮。

7.4 FPS 角标:一次自驱动的 invalidate 往返

这套机制演示了脏区与帧率如何咬合,值得单独拆开。计数在 fps_inc(tkc/fps.h:64)------它在 fps 闸门之后 、脏区检查之前累加(:663),语义是"通过节流闸的轮次";fps_get(fps.h:79)用 200ms 以上的滑动窗口换算成每秒帧数。只要 show_fps 打开,每个通过闸门的轮次都把右上角 60×30 的矩形 invalidate(:664-667)------这是一个自驱动的脏区,保证账本永不为空、每帧都真的绘制;真正画数字的是 canvas_draw_fps(canvas.c:1887),深灰底、白字 16 号、内容 "NNfps"。换句话说,开着 FPS 显示时看到的数字,走的正是第五节第 1、4 幕的完整流程,是这套机制最小、最直观的活样本。
#mermaid-svg-stvo38Zvk4emVOGt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-stvo38Zvk4emVOGt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-stvo38Zvk4emVOGt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-stvo38Zvk4emVOGt .error-icon{fill:#552222;}#mermaid-svg-stvo38Zvk4emVOGt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-stvo38Zvk4emVOGt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-stvo38Zvk4emVOGt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-stvo38Zvk4emVOGt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-stvo38Zvk4emVOGt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-stvo38Zvk4emVOGt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-stvo38Zvk4emVOGt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-stvo38Zvk4emVOGt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-stvo38Zvk4emVOGt .marker.cross{stroke:#333333;}#mermaid-svg-stvo38Zvk4emVOGt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-stvo38Zvk4emVOGt p{margin:0;}#mermaid-svg-stvo38Zvk4emVOGt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-stvo38Zvk4emVOGt .cluster-label text{fill:#333;}#mermaid-svg-stvo38Zvk4emVOGt .cluster-label span{color:#333;}#mermaid-svg-stvo38Zvk4emVOGt .cluster-label span p{background-color:transparent;}#mermaid-svg-stvo38Zvk4emVOGt .label text,#mermaid-svg-stvo38Zvk4emVOGt span{fill:#333;color:#333;}#mermaid-svg-stvo38Zvk4emVOGt .node rect,#mermaid-svg-stvo38Zvk4emVOGt .node circle,#mermaid-svg-stvo38Zvk4emVOGt .node ellipse,#mermaid-svg-stvo38Zvk4emVOGt .node polygon,#mermaid-svg-stvo38Zvk4emVOGt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-stvo38Zvk4emVOGt .rough-node .label text,#mermaid-svg-stvo38Zvk4emVOGt .node .label text,#mermaid-svg-stvo38Zvk4emVOGt .image-shape .label,#mermaid-svg-stvo38Zvk4emVOGt .icon-shape .label{text-anchor:middle;}#mermaid-svg-stvo38Zvk4emVOGt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-stvo38Zvk4emVOGt .rough-node .label,#mermaid-svg-stvo38Zvk4emVOGt .node .label,#mermaid-svg-stvo38Zvk4emVOGt .image-shape .label,#mermaid-svg-stvo38Zvk4emVOGt .icon-shape .label{text-align:center;}#mermaid-svg-stvo38Zvk4emVOGt .node.clickable{cursor:pointer;}#mermaid-svg-stvo38Zvk4emVOGt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-stvo38Zvk4emVOGt .arrowheadPath{fill:#333333;}#mermaid-svg-stvo38Zvk4emVOGt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-stvo38Zvk4emVOGt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-stvo38Zvk4emVOGt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-stvo38Zvk4emVOGt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-stvo38Zvk4emVOGt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-stvo38Zvk4emVOGt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-stvo38Zvk4emVOGt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-stvo38Zvk4emVOGt .cluster text{fill:#333;}#mermaid-svg-stvo38Zvk4emVOGt .cluster span{color:#333;}#mermaid-svg-stvo38Zvk4emVOGt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-stvo38Zvk4emVOGt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-stvo38Zvk4emVOGt rect.text{fill:none;stroke-width:0;}#mermaid-svg-stvo38Zvk4emVOGt .icon-shape,#mermaid-svg-stvo38Zvk4emVOGt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-stvo38Zvk4emVOGt .icon-shape p,#mermaid-svg-stvo38Zvk4emVOGt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-stvo38Zvk4emVOGt .icon-shape .label rect,#mermaid-svg-stvo38Zvk4emVOGt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-stvo38Zvk4emVOGt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-stvo38Zvk4emVOGt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-stvo38Zvk4emVOGt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} elapsed < 33ms
elapsed ≥ 33ms
application 层调用 window_manager_set_max_fps(30)

wm->max_fps = 30(仅数字,无动作)
paint_normal 帧距裁决(:644)

elapsed = 距上帧画完 + 上帧耗时
跳过本帧

期望睡眠 = 33 - elapsed
正常绘制本帧

画完记录 last_paint_time / cost
main_loop_sleep(:90)三数取最小

8ms上限 / 节拍余量 / 期望睡眠
睡醒进入下一轮 step
【图5:帧率控制三级传导】

八、送显调用链:帧队列、三段接力、场同步换帧与三本脏区账

第 5 幕的展开。渲染完成后,像素要经过一条"帧队列"和三段接力才到屏幕;而如果第 3 级节能开着,这条流水线还有一套"三本脏区账"的替代路径。按数据结构 → 主路径 → 中断 → 脏区账的顺序讲。

8.1 帧队列:两条队列管三块显存

LCD 驱动内部(lcd.c:151-255)为三块显示帧(FB_COUNT=3)维护两条链表队列:

c 复制代码
static queue_s lcd_fb_ready;          /* 已画好、等着上屏的帧 */
static queue_s lcd_fb_free;           /* 空闲、可以拿来写的帧 */
static fb_queue_s fb_queue_unit[FB_COUNT];

所有队列操作用 XM_lock 临界区保护;带 _isr 后缀的版本(fb_queue_get_ready_unit_isr / fb_queue_set_free_isr)不加锁,专供中断上下文使用------中断不会嵌套自己,ISR 版本天然安全。生产者是 UI 任务(取 free、还 ready),消费者是场同步中断(取 ready、还 free),两条队列就是标准的双端生产消费模型。三块帧的含义:LCD 正在扫描一块、一块画好排队、一块正在被写------渲染和显示永不同时碰同一块。

8.2 三段接力主路径:每帧的源码与数据量

送显入口 XM_HmiHost_WindowBuffersSwap(xm_hmi_host.c:219)在当前编译配置下走这条路:

text 复制代码
XM_HmiHost_WindowBuffersSwap(xm_hmi_host.c:219)
 ├─ ① xm_vg_require_gpu_fb        从 free 队列取空闲显示帧
 │       队列空则阻塞轮询 → 三块全忙时 UI 自然背压(库内实现,未验证)
 ├─ ② vgReadPixels(...)           GPU 离屏表面整屏读回到临时缓冲
 │       1216×568×4 ≈ 2.7MB,32bit 时格式 VG_sARGB_8888(xm_hmi_host.c:257)
 ├─ ③ pxp_scaler_rotate(...)      PXP 硬件把临时缓冲旋转 90° 写入显示帧(:266)
 │       读 2.7MB + 写 2.7MB;仅首帧额外配置 OSD 尺寸与格式(:273-278)
 └─ ④ xm_vg_release_gpu_fb        帧入 ready 队列,等场同步取走

用"画板/展板"的语言:①从展板架拿一块空展板;②把画板整幅誊到临时桌面上;③搬运工把横着的画转成竖的贴上展板;④展板挂到"待上墙"队列。两个源码分支值得记:若面板不需要旋转(LCD_ROTATE_ANGLE 为 0 的配置),②直接读进显示帧,③和临时缓冲全省(:279-288);16bit 配置整条链换成 RGB565(:259-265)。另外读回格式 VG_sARGB_8888 与第十节"命名反转"直接相关------VG 标签描述的是 32 位字的通道顺序,实际内存布局是 B,G,R,A 字节。

三段接力的时序(三缓冲流水与中断换帧):
LCD控制器与面板 LCD场同步中断 约60Hz PXP旋转引擎 帧缓冲队列 3块 awtk_ui任务 LCD控制器与面板 LCD场同步中断 约60Hz PXP旋转引擎 帧缓冲队列 3块 awtk_ui任务 #mermaid-svg-New3BYVdLYQrf1fc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-New3BYVdLYQrf1fc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-New3BYVdLYQrf1fc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-New3BYVdLYQrf1fc .error-icon{fill:#552222;}#mermaid-svg-New3BYVdLYQrf1fc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-New3BYVdLYQrf1fc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-New3BYVdLYQrf1fc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-New3BYVdLYQrf1fc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-New3BYVdLYQrf1fc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-New3BYVdLYQrf1fc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-New3BYVdLYQrf1fc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-New3BYVdLYQrf1fc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-New3BYVdLYQrf1fc .marker.cross{stroke:#333333;}#mermaid-svg-New3BYVdLYQrf1fc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-New3BYVdLYQrf1fc p{margin:0;}#mermaid-svg-New3BYVdLYQrf1fc .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-New3BYVdLYQrf1fc text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-New3BYVdLYQrf1fc .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-New3BYVdLYQrf1fc .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-New3BYVdLYQrf1fc .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-New3BYVdLYQrf1fc .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-New3BYVdLYQrf1fc #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-New3BYVdLYQrf1fc .sequenceNumber{fill:white;}#mermaid-svg-New3BYVdLYQrf1fc #sequencenumber{fill:#333;}#mermaid-svg-New3BYVdLYQrf1fc #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-New3BYVdLYQrf1fc .messageText{fill:#333;stroke:none;}#mermaid-svg-New3BYVdLYQrf1fc .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-New3BYVdLYQrf1fc .labelText,#mermaid-svg-New3BYVdLYQrf1fc .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-New3BYVdLYQrf1fc .loopText,#mermaid-svg-New3BYVdLYQrf1fc .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-New3BYVdLYQrf1fc .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-New3BYVdLYQrf1fc .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-New3BYVdLYQrf1fc .noteText,#mermaid-svg-New3BYVdLYQrf1fc .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-New3BYVdLYQrf1fc .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-New3BYVdLYQrf1fc .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-New3BYVdLYQrf1fc .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-New3BYVdLYQrf1fc .actorPopupMenu{position:absolute;}#mermaid-svg-New3BYVdLYQrf1fc .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-New3BYVdLYQrf1fc .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-New3BYVdLYQrf1fc .actor-man circle,#mermaid-svg-New3BYVdLYQrf1fc line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-New3BYVdLYQrf1fc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 同时开始渲染下一帧 离屏表面只有一块不冲突 ① 取空闲显示帧 无则阻塞轮询空闲显示帧② vgReadPixels 整屏读回约2.7MB③ 旋转90度写入显示帧 读写共5.4MB完成④ 入就绪队列场消隐取就绪帧换扫描地址并回收旧帧从新地址扫描输出
【图6:送显三段接力时序图】

8.3 场同步中断:换帧的唯一出口

LCD 控制器每扫完一帧发一次中断(lcd.c:1064),核心几行:

c 复制代码
if (status & (0x01 << 0)) {                       /* 场消隐状态位 */
    fb_queue_s *next = fb_queue_get_ready_unit_isr();   /* 不加锁取就绪帧 */
    if (next) {
        ark_lcd_set_osd_yaddr(LCD_UI_LAYER, next->fb_base);  /* 换扫描地址 */
        ark_lcd_set_osd_sync(LCD_UI_LAYER);
        if (last_fb) { fb_queue_set_free_isr(last_fb); }     /* 旧帧回收进 free */
        last_fb = next;
    }
}

为什么必须等消隐期:LCD 控制器正逐行扫描显存,中途改地址会得到"上半帧旧图、下半帧新图"的撕裂画面;场消隐是两帧之间的空档,唯一安全的换帧窗口。ready 队列空时(UI 没画完)中断什么也不做,旧帧继续扫描------显示节奏完全由面板时序决定,UI 只是往队列里供货。同一段还有个 vsync_waitq 事件组(:1101-1102),给需要"等这一帧真正上屏"的代码提供同步点。

8.4 VG 侧的自动脏区账本:三块显存各记一笔,缺什么补什么

这是整套移植里最精巧、也最容易被问到的机制------"VG 的自动脏区合并"指的就是它 :vgcanvas_nanovg_vg.c:98 注释原话"脏矩形自动管理与刷新"。它由宏 ENABLE_FB_DIRTY_RECTS_COPY 门控,回答一个三缓冲特有的问题。

问题是什么? 渲染永远发生在一块 VG 离屏表面上,而显示端有三块帧轮换。若每帧只把"本帧脏区"拷进"本帧分到的那块显示帧",那么第 1 帧更新的像素只进了帧 A、第 2 帧只进了帧 B------轮换一圈回到帧 A 上屏时,它缺第 2 帧的更新,屏幕上就是旧内容闪一下。要么整屏拷(现状主路径,8.2 的 2.7MB×3 段),要么记账。

三本账怎么记? vgcanvas 层自己维护一个与三块显示帧一一对应的账本数组(vgcanvas_nanovg_vg.c:103):

c 复制代码
static dirty_rects_t surface_dirty_rects[SURFACE_COUNT];   /* 三本账,每块帧一本 */
static int surface_fifo_index = -1;                        /* 当前轮换槽位 */

static void vg_set_dirty_rects(const dirty_rects_t *dirty_rects) {  /* :112 */
    surface_fifo_index++;                                   /* 槽位轮转 +1(mod 3) */
    if (surface_fifo_index >= surface_fifo_count) surface_fifo_index = 0;
    memcpy(&surface_dirty_rects[surface_fifo_index], dirty_rects, sizeof(dirty_rects_t));
}

它在每帧 begin_frame 末尾被调用(:615-618):帧开始记账时槽位先轮转,本帧脏区抄进"即将分到的那块帧"的账上。帧地址则由送显时登记------vg_surface_sync 把 xm_vg_require_gpu_fb 取到的显示帧地址写进当前槽位(:407),账与显存从此对应。

送显时怎么补? vg_surface_sync(:400)做两步。第一步 fb 间差量补拷 vg_fb_dirty_rects_copy(:329):遍历"上一帧的帧"和"上上帧的帧"(最多回看两块,正好等于三缓冲一轮),把本帧缺的更新 直接从那两块显存拷过来------注意源不是 VG 表面而是兄弟显示帧,它们身上带着最新内容。要拷哪些区域由集合运算决定,源码注释把语义讲得很直白:

c 复制代码
/* 从当前FB之前的第二帧拷贝脏数据到当前FB:
   排除"上一帧已覆盖的"和"本帧马上要画的",剩下的才是本帧真缺的 */
dirty_rects_exclude(surface_dirty_rects + index,   /* 上上帧的账 */
                    surface_dirty_rects + index1,  /* 减去上帧的账 */
                    &dr2);
dirty_rects_exclude(&dr2, surface_dirty_rects + surface_fifo_index, &dr); /* 再减本帧的账 */

辅助函数 dirty_rects_exclude(:295)的取舍与 6.2 节一脉相承:只排除"完全被包含"的矩形,相交不排除 (源码注释明说)------宁可多拷一点,绝不漏拷,像素拷贝是幂等的,多拷无害。第二步(:412-426)把本帧脏区从 VG 离屏表面拷进显示帧,基址由厂商扩展函数 vgGetCurrentSurfacePixels 直取。
#mermaid-svg-7dyhwrAaTd13UZ4Q{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7dyhwrAaTd13UZ4Q .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7dyhwrAaTd13UZ4Q .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7dyhwrAaTd13UZ4Q .error-icon{fill:#552222;}#mermaid-svg-7dyhwrAaTd13UZ4Q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7dyhwrAaTd13UZ4Q .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7dyhwrAaTd13UZ4Q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7dyhwrAaTd13UZ4Q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7dyhwrAaTd13UZ4Q .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7dyhwrAaTd13UZ4Q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7dyhwrAaTd13UZ4Q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7dyhwrAaTd13UZ4Q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7dyhwrAaTd13UZ4Q .marker.cross{stroke:#333333;}#mermaid-svg-7dyhwrAaTd13UZ4Q svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7dyhwrAaTd13UZ4Q p{margin:0;}#mermaid-svg-7dyhwrAaTd13UZ4Q .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7dyhwrAaTd13UZ4Q .cluster-label text{fill:#333;}#mermaid-svg-7dyhwrAaTd13UZ4Q .cluster-label span{color:#333;}#mermaid-svg-7dyhwrAaTd13UZ4Q .cluster-label span p{background-color:transparent;}#mermaid-svg-7dyhwrAaTd13UZ4Q .label text,#mermaid-svg-7dyhwrAaTd13UZ4Q span{fill:#333;color:#333;}#mermaid-svg-7dyhwrAaTd13UZ4Q .node rect,#mermaid-svg-7dyhwrAaTd13UZ4Q .node circle,#mermaid-svg-7dyhwrAaTd13UZ4Q .node ellipse,#mermaid-svg-7dyhwrAaTd13UZ4Q .node polygon,#mermaid-svg-7dyhwrAaTd13UZ4Q .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7dyhwrAaTd13UZ4Q .rough-node .label text,#mermaid-svg-7dyhwrAaTd13UZ4Q .node .label text,#mermaid-svg-7dyhwrAaTd13UZ4Q .image-shape .label,#mermaid-svg-7dyhwrAaTd13UZ4Q .icon-shape .label{text-anchor:middle;}#mermaid-svg-7dyhwrAaTd13UZ4Q .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7dyhwrAaTd13UZ4Q .rough-node .label,#mermaid-svg-7dyhwrAaTd13UZ4Q .node .label,#mermaid-svg-7dyhwrAaTd13UZ4Q .image-shape .label,#mermaid-svg-7dyhwrAaTd13UZ4Q .icon-shape .label{text-align:center;}#mermaid-svg-7dyhwrAaTd13UZ4Q .node.clickable{cursor:pointer;}#mermaid-svg-7dyhwrAaTd13UZ4Q .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7dyhwrAaTd13UZ4Q .arrowheadPath{fill:#333333;}#mermaid-svg-7dyhwrAaTd13UZ4Q .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7dyhwrAaTd13UZ4Q .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7dyhwrAaTd13UZ4Q .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7dyhwrAaTd13UZ4Q .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7dyhwrAaTd13UZ4Q .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7dyhwrAaTd13UZ4Q .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7dyhwrAaTd13UZ4Q .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7dyhwrAaTd13UZ4Q .cluster text{fill:#333;}#mermaid-svg-7dyhwrAaTd13UZ4Q .cluster span{color:#333;}#mermaid-svg-7dyhwrAaTd13UZ4Q div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7dyhwrAaTd13UZ4Q .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7dyhwrAaTd13UZ4Q rect.text{fill:none;stroke-width:0;}#mermaid-svg-7dyhwrAaTd13UZ4Q .icon-shape,#mermaid-svg-7dyhwrAaTd13UZ4Q .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7dyhwrAaTd13UZ4Q .icon-shape p,#mermaid-svg-7dyhwrAaTd13UZ4Q .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7dyhwrAaTd13UZ4Q .icon-shape .label rect,#mermaid-svg-7dyhwrAaTd13UZ4Q .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7dyhwrAaTd13UZ4Q .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7dyhwrAaTd13UZ4Q .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7dyhwrAaTd13UZ4Q :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} begin_frame:vg_set_dirty_rects

槽位轮转 本帧脏区记到对应账本(:615)
送显:vg_surface_sync 登记帧地址(:407)
第一步 fb间差量补拷(:329)
上上帧的账 减去上帧与本帧已覆盖

= 本帧真缺的区域
上一帧的账 减去本帧已覆盖

= 缺的上一帧更新
逐矩形 帧到帧 CPU拷贝

32字节对齐 + DMA cache刷新(:170)
第二步 本帧脏区

从VG离屏表面拷入显示帧(:412)
返回1:整屏 vgReadPixels 与 PXP 旋转全部跳过
【图7:三本脏区账的差量同步】

拷贝引擎本身也有讲究 :_surface_copy_block32(:170)是一段手写 ARM 汇编,按 32 字节 cache line 整块 LDM/STM,配合 dma_flush_range 保证写显存后 cache 一致性------拷贝带宽直接打到 DDR 访问效率的上限。还有一处兜底(:653-656):若某帧账本是空的(整屏重绘场景 dirty_rects 为 NULL),强制把该帧账本设为全屏------因为全屏重绘意味着三块帧都要追平,账本必须记满才能在后续差量同步里收敛。

现状:机制完好,编译关闭。 这么精巧的机制,当前工程里并不生效,原因是宏的作用域:ENABLE_FB_DIRTY_RECTS_COPY 只在 LCD 驱动 lcd.c 内部 define(lcd.c:157),vgcanvas_nanovg_vg.c 与 xm_hmi_host.c 编译时看不到。于是出现了"半接通"的有趣状态------lcd.c 侧的 fb_queue_init 确实在运行时调用了 vg_set_surface 把三块帧地址注册进去(lcd.c:184),但对端编译的是空壳实现(vgcanvas_nanovg_vg.c:432,收到参数直接 return 0);vg_surface_sync 的调用点(xm_hmi_host.c:228-236)也被 ifdef 整块裁掉。要激活整套机制,把宏加进工程级编译选项(IAR 工程 awtk 目标的预处理器定义),让三个文件都看见即可;代价是三块帧必须常驻、且 fb 间差量拷贝吃 CPU 带宽,收益与代价的实测对比留待实测篇。

8.5 每帧账单与帧率的闭环

主路径下每帧固定搬运:读回 2.7MB + PXP 读 2.7MB + PXP 写 2.7MB ≈ 8.1MB 总线流量,外加 VG 光栅化本身的显存读写。三段串行发生在第七节的 last_paint_cost 口径内,与 20~22fps 实测互为印证:每帧约 45~50ms 中,三段接力占多大比例,是后续打点实测的第一课题;第 3 级节能(8.4)若打开,画面小改动时三段全部消失,只剩差量拷贝------这是最值得做的 A/B 实验。

运行期各角色汇总:

资源 来源 说明
awtk_ui 任务 驱动库创建,wrapper 调整优先级 AWTK 主循环宿主
VG 显存堆 板级入口的静态数组(示例 14MB) 离屏表面、纹理、VG 上下文
显示帧缓冲 ×3 LCD 驱动内动态分配 free/ready 两条队列管理,控制器直接扫描
三本脏区账 vgcanvas 层静态数组 随 ENABLE_FB_DIRTY_RECTS_COPY 编译开关
场同步中断 LCD 驱动 换帧与旧帧回收的唯一出口

九、输入调用链

输入是跨任务的生产者消费者模型。触摸 IC 驱动解析坐标后调用显示宿主层的 XM_TpEventProc,事件写入环形缓冲并触发信号量;AWTK 主循环的派发阶段(第 2 幕)经 XM_WaitEvent 取走事件,包装成 pointer_event 后交给控件树:
#mermaid-svg-Z95sPPdz4AFL4GBE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Z95sPPdz4AFL4GBE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Z95sPPdz4AFL4GBE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Z95sPPdz4AFL4GBE .error-icon{fill:#552222;}#mermaid-svg-Z95sPPdz4AFL4GBE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Z95sPPdz4AFL4GBE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Z95sPPdz4AFL4GBE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Z95sPPdz4AFL4GBE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Z95sPPdz4AFL4GBE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Z95sPPdz4AFL4GBE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Z95sPPdz4AFL4GBE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Z95sPPdz4AFL4GBE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Z95sPPdz4AFL4GBE .marker.cross{stroke:#333333;}#mermaid-svg-Z95sPPdz4AFL4GBE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Z95sPPdz4AFL4GBE p{margin:0;}#mermaid-svg-Z95sPPdz4AFL4GBE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Z95sPPdz4AFL4GBE .cluster-label text{fill:#333;}#mermaid-svg-Z95sPPdz4AFL4GBE .cluster-label span{color:#333;}#mermaid-svg-Z95sPPdz4AFL4GBE .cluster-label span p{background-color:transparent;}#mermaid-svg-Z95sPPdz4AFL4GBE .label text,#mermaid-svg-Z95sPPdz4AFL4GBE span{fill:#333;color:#333;}#mermaid-svg-Z95sPPdz4AFL4GBE .node rect,#mermaid-svg-Z95sPPdz4AFL4GBE .node circle,#mermaid-svg-Z95sPPdz4AFL4GBE .node ellipse,#mermaid-svg-Z95sPPdz4AFL4GBE .node polygon,#mermaid-svg-Z95sPPdz4AFL4GBE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Z95sPPdz4AFL4GBE .rough-node .label text,#mermaid-svg-Z95sPPdz4AFL4GBE .node .label text,#mermaid-svg-Z95sPPdz4AFL4GBE .image-shape .label,#mermaid-svg-Z95sPPdz4AFL4GBE .icon-shape .label{text-anchor:middle;}#mermaid-svg-Z95sPPdz4AFL4GBE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Z95sPPdz4AFL4GBE .rough-node .label,#mermaid-svg-Z95sPPdz4AFL4GBE .node .label,#mermaid-svg-Z95sPPdz4AFL4GBE .image-shape .label,#mermaid-svg-Z95sPPdz4AFL4GBE .icon-shape .label{text-align:center;}#mermaid-svg-Z95sPPdz4AFL4GBE .node.clickable{cursor:pointer;}#mermaid-svg-Z95sPPdz4AFL4GBE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Z95sPPdz4AFL4GBE .arrowheadPath{fill:#333333;}#mermaid-svg-Z95sPPdz4AFL4GBE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Z95sPPdz4AFL4GBE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Z95sPPdz4AFL4GBE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z95sPPdz4AFL4GBE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Z95sPPdz4AFL4GBE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z95sPPdz4AFL4GBE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Z95sPPdz4AFL4GBE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Z95sPPdz4AFL4GBE .cluster text{fill:#333;}#mermaid-svg-Z95sPPdz4AFL4GBE .cluster span{color:#333;}#mermaid-svg-Z95sPPdz4AFL4GBE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Z95sPPdz4AFL4GBE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Z95sPPdz4AFL4GBE rect.text{fill:none;stroke-width:0;}#mermaid-svg-Z95sPPdz4AFL4GBE .icon-shape,#mermaid-svg-Z95sPPdz4AFL4GBE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z95sPPdz4AFL4GBE .icon-shape p,#mermaid-svg-Z95sPPdz4AFL4GBE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Z95sPPdz4AFL4GBE .icon-shape .label rect,#mermaid-svg-Z95sPPdz4AFL4GBE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z95sPPdz4AFL4GBE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Z95sPPdz4AFL4GBE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Z95sPPdz4AFL4GBE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 触摸 IC 驱动:I2C 中断/轮询读取并解析触点坐标
XM_TpEventProc:事件写入环形缓冲并触发事件信号量
环形缓冲(读写双指针 + 临界区保护)+ 事件信号量两级缓冲
XM_WaitEvent:awtk_ui 任务内阻塞等待信号量被触发
main_loop_xm_dispatch_tp_event:包装成 AWTK 的 pointer_event 指针事件
window_manager_dispatch_input_event:按焦点控件把输入事件分发进对应的控件树节点
控件树:EVT_POINTER_DOWN / MOVE / UP 事件由目标控件回调处理
【图8:触摸输入链路】

注意事件从触摸 IC 任务进入 awtk_ui 任务,中间隔着环形缓冲和信号量两级缓冲,两级各有保护:环形缓冲用临界区保护读写指针,信号量负责唤醒消费任务。触摸响应延迟的最大头不是这条链,而是第七节的主循环休眠------所以休眠上限被 TK_MAX_SLEEP_TIME 压在约 8ms。

十、PC 端与板端的颜色格式设置:两套体系怎么对齐

这是整套 SDK 里最容易混淆的部分。先立住一个事实:PC 模拟器和板端固件是两套独立编译的 AWTK。模拟器由 IDE SDK 的原版源码编译,在电脑窗口里跑;固件由芯片 SDK 的移植版源码编译,跑在 AMT630HV100 上。两者的唯一交集是 UI 业务代码和资源文件------而资源文件(PNG)本身是压缩的原始字节,没有像素格式,所以"格式"是在各自环境的编译期和运行期分别决定的:
#mermaid-svg-L5dRzplXAlFgNRhu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-L5dRzplXAlFgNRhu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-L5dRzplXAlFgNRhu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-L5dRzplXAlFgNRhu .error-icon{fill:#552222;}#mermaid-svg-L5dRzplXAlFgNRhu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-L5dRzplXAlFgNRhu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-L5dRzplXAlFgNRhu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-L5dRzplXAlFgNRhu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-L5dRzplXAlFgNRhu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-L5dRzplXAlFgNRhu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-L5dRzplXAlFgNRhu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-L5dRzplXAlFgNRhu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-L5dRzplXAlFgNRhu .marker.cross{stroke:#333333;}#mermaid-svg-L5dRzplXAlFgNRhu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-L5dRzplXAlFgNRhu p{margin:0;}#mermaid-svg-L5dRzplXAlFgNRhu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-L5dRzplXAlFgNRhu .cluster-label text{fill:#333;}#mermaid-svg-L5dRzplXAlFgNRhu .cluster-label span{color:#333;}#mermaid-svg-L5dRzplXAlFgNRhu .cluster-label span p{background-color:transparent;}#mermaid-svg-L5dRzplXAlFgNRhu .label text,#mermaid-svg-L5dRzplXAlFgNRhu span{fill:#333;color:#333;}#mermaid-svg-L5dRzplXAlFgNRhu .node rect,#mermaid-svg-L5dRzplXAlFgNRhu .node circle,#mermaid-svg-L5dRzplXAlFgNRhu .node ellipse,#mermaid-svg-L5dRzplXAlFgNRhu .node polygon,#mermaid-svg-L5dRzplXAlFgNRhu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-L5dRzplXAlFgNRhu .rough-node .label text,#mermaid-svg-L5dRzplXAlFgNRhu .node .label text,#mermaid-svg-L5dRzplXAlFgNRhu .image-shape .label,#mermaid-svg-L5dRzplXAlFgNRhu .icon-shape .label{text-anchor:middle;}#mermaid-svg-L5dRzplXAlFgNRhu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-L5dRzplXAlFgNRhu .rough-node .label,#mermaid-svg-L5dRzplXAlFgNRhu .node .label,#mermaid-svg-L5dRzplXAlFgNRhu .image-shape .label,#mermaid-svg-L5dRzplXAlFgNRhu .icon-shape .label{text-align:center;}#mermaid-svg-L5dRzplXAlFgNRhu .node.clickable{cursor:pointer;}#mermaid-svg-L5dRzplXAlFgNRhu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-L5dRzplXAlFgNRhu .arrowheadPath{fill:#333333;}#mermaid-svg-L5dRzplXAlFgNRhu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-L5dRzplXAlFgNRhu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-L5dRzplXAlFgNRhu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-L5dRzplXAlFgNRhu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-L5dRzplXAlFgNRhu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-L5dRzplXAlFgNRhu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-L5dRzplXAlFgNRhu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-L5dRzplXAlFgNRhu .cluster text{fill:#333;}#mermaid-svg-L5dRzplXAlFgNRhu .cluster span{color:#333;}#mermaid-svg-L5dRzplXAlFgNRhu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-L5dRzplXAlFgNRhu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-L5dRzplXAlFgNRhu rect.text{fill:none;stroke-width:0;}#mermaid-svg-L5dRzplXAlFgNRhu .icon-shape,#mermaid-svg-L5dRzplXAlFgNRhu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-L5dRzplXAlFgNRhu .icon-shape p,#mermaid-svg-L5dRzplXAlFgNRhu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-L5dRzplXAlFgNRhu .icon-shape .label rect,#mermaid-svg-L5dRzplXAlFgNRhu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-L5dRzplXAlFgNRhu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-L5dRzplXAlFgNRhu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-L5dRzplXAlFgNRhu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Designer 工程

project.json 颜色格式设置
PC 线:模拟器
板端线:固件
SDK 脚本映射 imagegen 参数

并决定模拟器编译宏
模拟器帧缓冲与解码格式
资源以 PNG 原始字节打包

格式与设计器设置无关
板端编译宏决定解码格式

board.h 决定输出格式
【图9:同一次设计,两条独立的格式决策线】

PC 端的设置入口是工程根目录 project.json 里 assets.themes.default.lcd 节点的 colorFormat 与 colorDepth(也就是 Designer"项目设置"界面上的颜色深度、颜色格式两项)。生成资源时,SDK 脚本按这个设置选择图片生成参数,映射关系在 SDK 的 update_res_app.py 里写死:

python 复制代码
if color_format == 'MONO':
    imagegen_options = 'mono'
elif color_format == 'BGR(A)':
    imagegen_options = 'bgr565' if color_depth == '16bit' else 'bgra'
else:
    imagegen_options = 'rgb565' if color_depth == '16bit' else 'rgba'

注意两点。其一,工程若按"资源原始数据"方式打包(project.json 的 const 为 resource_data),图片仍以 PNG 原始字节进包,imagegen 参数此刻主要影响模拟器预览,不改变固件里图片的解码格式------板端解码成什么,由板端编译宏决定。其二,模拟器自身的帧缓冲格式由 SDK 根目录 awtk_config.py 决定,默认软件渲染(AGGE)分支下有一处容易踩的默认值:

python 复制代码
if NANOVG_BACKEND == 'AGGE' or NANOVG_BACKEND == 'AGG':
    LCD = 'SDL_FB'
    FRAME_BUFFER_FORMAT = 'bgr565'        # 默认 16 位
    # FRAME_BUFFER_FORMAT='bgra8888'      # 32 位需手动启用

也就是说,命令行用 scons 构建的模拟器默认跑在 16 位 BGR565 帧缓冲上(编译宏 WITH_FB_BGR565),而 32 位面板的固件是 ARGB8888------渐变、精细选色在两边会有色带差异。要对齐,把注释里的 bgra8888 一行启用即可,模拟器随即按 WITH_FB_BGRA8888 加 WITH_BITMAP_BGRA 编译,与板端 VG 表面同规格。

板端的格式由两处决定:board.h 的 LCD_BPP 决定 VG 表面与最终输出(32 为 ARGB8888,16 为 RGB565);工程编译宏 WITH_BITMAP_BGRA、WITH_BITMAP_BGR565、WITH_BITMAP_RGB565 决定 PNG 解码成什么位图,全部不定义则解码 RGBA8888。解码格式与输出格式的衔接靠移植层的映射表:

c 复制代码
switch (img->format) {
case BITMAP_FMT_RGBA8888: f = VG_sABGR_8888; break;  /* 内存字节 R,G,B,A */
case BITMAP_FMT_BGRA8888: f = VG_sARGB_8888; break;  /* 内存字节 B,G,R,A */
case BITMAP_FMT_RGB565:                              /* 565 两种共用一个 VG 常量 */
case BITMAP_FMT_BGR565: f = VG_sRGB_565;  break;
}
image = vgCreateImage(f, img->w, img->h, ...);
vgImageSubData(image, img_data, img->line_length, f, ...);

这张表背后是一个命名陷阱:AWTK 的 BITMAP_FMT 系列按内存字节序命名,而 VG 驱动按 32 位字的通道顺序命名,小端 CPU 上同一个缓冲区在两边的名字互为倒拼。一个常见误记是把 Designer 的 BGR(A) 对应到 BITMAP_FMT_ABGR8888------按字节序严格对应的是 BGRA8888。完整对应关系:

Designer 颜色格式 AWTK 位图格式 内存字节序 VG 常量
BGR(A) 32bit BITMAP_FMT_BGRA8888 B,G,R,A VG_sARGB_8888
RGB(A) 32bit BITMAP_FMT_RGBA8888 R,G,B,A VG_sABGR_8888
BGR(A) 16bit BITMAP_FMT_BGR565 565 位打包 VG_sRGB_565
RGB(A) 16bit BITMAP_FMT_RGB565 565 位打包 VG_sRGB_565

上传表保证"VG 标签如实描述内存布局",因此解码格式选哪种颜色都正确------配置宏影响的是路径,不是正确性。两套环境要对齐时按这张清单核对:

核对项 PC 端位置 板端位置
分辨率 / 颜色深度 / 颜色格式 project.json 的 lcd 节点 板级 OSD 配置与 board.h 的 LCD_BPP
模拟器帧缓冲格式 SDK awtk_config.py 的 FRAME_BUFFER_FORMAT 无对应项,由 LCD_BPP 决定
图片解码格式 SDK 默认 WITH_BITMAP_BGRA 工程编译宏 WITH_BITMAP_*
像素比 DPR project.json 的 screenDPR 移植层 ratio 固定为 1
资源内容 res 目录(生成产物) 同一份产物打包进固件

十一、参考资料

相关推荐
新思维软件2 小时前
基于单片机的激光防盗报警器语音报警系统:STM32 激光围栏 + ESP32-CAM 图像留痕全链路实现
stm32·单片机·物联网·物联网开发
恶魔泡泡糖2 小时前
stm32F103C8T6标准库串口接收之控制LED灯3
stm32·单片机·嵌入式硬件
新思维软件2 小时前
基于无线传输的地衡系统:三节点 LoRa 组网与 MQTT 上云的分布式称重方案
分布式·stm32·单片机·嵌入式硬件·物联网·物联网开发
芯祥联2 小时前
STM32 上跑 MQTT 怎么选?嵌入式 MQTT 客户端 C 语言实现对比
c语言·stm32·单片机·嵌入式硬件·网络协议·开源·broker
Brilliantwxx11 小时前
【STM32】 NVIC 与 EXTI 中断 · 超详细源码级解析
stm32·单片机·嵌入式硬件
水水不水啊12 小时前
告别杂乱的调试窗口:我用 Python + WebView 写了一个现代化串口助手
python·测试工具·嵌入式·嵌入式开发·串口调试·串口助手
niuTaylor12 小时前
飞腾 FT-2000/4 Ubuntu 内存卡 / NVMe SSD 系统制作完整指南
linux·服务器·人工智能·驱动开发·单片机
思茂信息13 小时前
CST软件BCI仿真模型及仿真案例
开发语言·单片机·嵌入式硬件·算法·emc
李永奉14 小时前
中科蓝讯SDK开发-BT897X、BT891X、AB573X、AB572X 系列 触摸按键提示音功能实现
网络·单片机·嵌入式硬件·mcu·物联网