先把卡顿拆成可观察的问题
本实验让一个计数标签按名义100ms更新,保持同一套UI,比较只改标签与每次invalidate整个screen,再比较一个和两个40行缓冲。它不加载网络、图片解码或文件保存,先把失效区域与缓冲这两条轴分开。
本站没有这块板的运行曲线,所以不预填FPS、不声称双缓冲必胜。最终可交付的是两个已构建工程配置、同源统计模型、日志汇总工具和四格空记录表。原D08题目的性能对照范围保留为可执行实验方法。
参考:[2] BSP3.2.1固定提交:显示初始化、条带大小、内部DMA与双缓冲[3] LVGL9.4.0固定提交:render/flush/wait事件的实际调用边界
先固定屏幕板与软件栈
仅用官方ESP32-S3-BOX-3主机,实物确认TT21100触摸与ST7789屏;BOX、BOX-Lite和其他S3触摸屏不能直接套用。主机Type-C承担供电、刷写与原生USB Serial/JTAG日志,115200参数随IDF monitor;不外接GPIO或外部负载。
启动先初始化BSP I²C,要求0x24应答且GT911的0x5D/0x14不应答,再启动显示/触摸。地址ACK不能证明实际料号,控制器不明时不要跳过检查。屏幕方向和触摸映射沿用BSP,先按D06检查四角与中心。
dependencies.lock固定14个传递组件及包hash;BSP可能携带本例未初始化的音频/传感器依赖,不能把下载到的依赖都称为正在运行的外设。两种真实编译均成功,应用文件各584656B,大小相同不代表二进制或内存行为相同。
- 做到这里,你应该看到
- 板号、控制器、锁文件、固件SHA和显示方向都能对应到同一次实验。
参考:[1] BOX-3硬件概览:固定官方提交与16MB/16MB物料[2] BSP3.2.1固定提交:显示初始化、条带大小、内部DMA与双缓冲[5] port2.9.0:SPI异步颜色传输回调与flush_ready所有权
先定义四种时间,再谈帧率
RENDER_START到RENDER_READY涵盖失效区域处理、条带绘制、可能的缓冲等待和传输提交,不是纯CPU绘制时间。FLUSH_START到FLUSH_FINISH围住同步提交路径到flush_cb返回;固定port可能异步提交SPI,不能当最终DMA完成。RENDER_READY同样不保证最后一笔DMA结束。
本例只订阅事件,不替换port的flush回调、不手工flush_ready。SPI颜色传输完成仍由port的on_color_trans_done回调负责;擅自提前ready会让仍在发送的缓冲被重用。
每次触摸PRESSED回调入口保存完整64位启动微秒,更新右上x240–307/y8–35的反馈块;第一次相交FLUSH_START记软件反馈延迟。它不包含手指到触摸驱动回调前的延迟,也不证明像素已亮。最多保留一个等待项,重复触摸记覆盖,超过2秒记超时,不把丢失事件算成0ms成功。
- 做到这里,你应该看到
- 能说明每个统计值从哪一行事件开始、到哪一行结束。
参考:[3] LVGL9.4.0固定提交:render/flush/wait事件的实际调用边界[4] LVGL9.4.0显示事件和接口[5] port2.9.0:SPI异步颜色传输回调与flush_ready所有权
独立配置构建两个缓冲变体
在解压工程根目录进入IDF5.5.5环境,再执行两条独立SDKCONFIG/build命令。不要只改defaults后继续复用旧sdkconfig:已有配置优先,可能让你以为换了模式。刷写时使用与选定构建目录一致的端口和monitor命令,先保存自己原固件的恢复办法。
BSP固定使用内部可DMA内存,320×40×2=25600B是一个条带的名义像素容量,双条带是51200B;320×240×2=153600B才是一份完整帧。本例没有两个完整帧缓存,也没有把像素缓冲挪进PSRAM。具体堆损耗还含分配器、对象和传递依赖,应按实测剩余量记录。
idf.py --version
# 必须ESP-IDF v5.5.5,在本工程根目录执行
idf.py -B build.single -D SDKCONFIG=sdkconfig.single -D SDKCONFIG_DEFAULTS=sdkconfig.defaults build
idf.py -B build.double -D SDKCONFIG=sdkconfig.double -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.double.defaults' build
idf.py -B build.single -p REPLACE_WITH_BOX3_PORT flash monitor
# 换双缓冲时改用 build.double;不要混用固件与日志标签参考:[1] BOX-3硬件概览:固定官方提交与16MB/16MB物料[2] BSP3.2.1固定提交:显示初始化、条带大小、内部DMA与双缓冲
统计与UI只由LVGL任务拥有
app_main获取BSP显示锁创建对象和事件订阅,随后解锁;所有UI事件、100ms timer和统计模型都在LVGL任务中执行,不另开线程共享lv_obj。Full/Local按钮切换策略,Reset window只重新开始观测。不要在其他任务无锁修改这些变量。
每次切换先丢弃2秒预热,再累计统计,每2秒输出一行,不在每个flush中打印串口。数据源100ms是请求周期,调度、日志与绘制仍可能延后;日志开销是实验条件的一部分,两种缓冲保持相同设置。
提交字节按裁到320×240内的矩形面积×2累计,包含重复像素,不含SPI命令,不代表独特变化像素数、总线物理吞吐或光学FPS。wait_elapsed_us还包含未阻塞的等待函数开销,不能当成SPI忙占空比。内部free、largest和min-since-boot三个数分别表示当前可用、最大连续块和启动以来最低水位;最低水位不是当前两秒窗口的最小值。
// ---- main/refresh_metrics.h ----
/* SPDX-License-Identifier: MIT */
#pragma once
#include <stdint.h>
#include <stdbool.h>
typedef struct {
uint64_t render_started,flush_started,wait_started,touch_started;
uint64_t render_count,render_us,render_max_us,flush_count,payload_bytes,submit_us,wait_us;
uint64_t feedback_count,feedback_us,feedback_max_us,feedback_overwritten,feedback_timeout;
bool in_render,in_flush,in_wait,touch_pending;
} refresh_metrics_t;
static inline void metrics_reset(refresh_metrics_t *m){*m=(refresh_metrics_t){0};}
static inline bool metrics_intersects(int32_t x1,int32_t y1,int32_t x2,int32_t y2){
return x1<=x2&&y1<=y2&&x1<=307&&x2>=240&&y1<=35&&y2>=8;
}
static inline void metrics_touch(refresh_metrics_t *m,uint64_t now){
if(m->touch_pending)++m->feedback_overwritten;
m->touch_pending=true;m->touch_started=now;
}
static inline void metrics_expire_touch(refresh_metrics_t *m,uint64_t now){
if(m->touch_pending&&now>=m->touch_started&&now-m->touch_started>2000000){m->touch_pending=false;++m->feedback_timeout;}
}
static inline void metrics_render_start(refresh_metrics_t *m,uint64_t now){m->render_started=now;m->in_render=true;}
static inline void metrics_render_end(refresh_metrics_t *m,uint64_t now){
if(!m->in_render||now<m->render_started)return;
uint64_t elapsed=now-m->render_started;++m->render_count;m->render_us+=elapsed;
if(elapsed>m->render_max_us)m->render_max_us=elapsed;
m->in_render=false;
}
static inline void metrics_flush_start(refresh_metrics_t *m,uint64_t now,int32_t x1,int32_t y1,int32_t x2,int32_t y2){
metrics_expire_touch(m,now);
m->flush_started=now;m->in_flush=true;++m->flush_count;
/* Sum submitted clipped RGB565 areas, including repeated pixels. */
if(x1<0)x1=0;
if(y1<0)y1=0;
if(x2>319)x2=319;
if(y2>239)y2=239;
if(x1<=x2&&y1<=y2)m->payload_bytes+=(uint64_t)(x2-x1+1)*(uint64_t)(y2-y1+1)*2;
if(m->touch_pending&&now>=m->touch_started&&metrics_intersects(x1,y1,x2,y2)){
uint64_t elapsed=now-m->touch_started;++m->feedback_count;m->feedback_us+=elapsed;
if(elapsed>m->feedback_max_us)m->feedback_max_us=elapsed;
m->touch_pending=false;
}
}
static inline void metrics_flush_end(refresh_metrics_t *m,uint64_t now){
if(m->in_flush&&now>=m->flush_started)m->submit_us+=now-m->flush_started;
m->in_flush=false;
}
static inline void metrics_wait_start(refresh_metrics_t *m,uint64_t now){m->wait_started=now;m->in_wait=true;}
static inline void metrics_wait_end(refresh_metrics_t *m,uint64_t now){
if(m->in_wait&&now>=m->wait_started)m->wait_us+=now-m->wait_started;
m->in_wait=false;
}
参考:[2] BSP3.2.1固定提交:显示初始化、条带大小、内部DMA与双缓冲[3] LVGL9.4.0固定提交:render/flush/wait事件的实际调用边界[5] port2.9.0:SPI异步颜色传输回调与flush_ready所有权[6] IDF5.5.5:堆容量、最大块与启动以来最低水位
完整入口保留真实异步显示所有权
main/main.c将模式按钮、触摸反馈、周期统计与控制器检查连在一起。共享header进入真实固件,也由host/host_check.c编译;tools/summarize.py只从D08开头的实际行计算最后一次累计窗口。
完整CMake、defaults、双缓冲覆盖配置、组件清单/锁与许可均在下载包。不要把相同byte长度当相同固件;实际源码/配置/日志/镜像SHA分别留证。
// ---- main/main.c ----
/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include "driver/i2c_master.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_heap_caps.h"
#include "bsp/esp-bsp.h"
#include "bsp/display.h"
#include "lvgl.h"
#include "refresh_metrics.h"
static const char *TAG="refresh_lab";
/* Everything below belongs to LVGL's task; app_main holds its BSP lock to create. */
static refresh_metrics_t metrics;
static lv_obj_t *screen,*counter_label,*feedback_label,*mode_label;
static bool full_invalidate,measuring;
static uint64_t warmup_until,measured_since,last_report;
static uint32_t counter,touches;
static uint64_t now_us(void){return (uint64_t)esp_timer_get_time();}
static void display_event(lv_event_t *e){
if(!measuring)return;
uint64_t now=now_us();
switch(lv_event_get_code(e)){
case LV_EVENT_RENDER_START:metrics_render_start(&metrics,now);break;
case LV_EVENT_RENDER_READY:metrics_render_end(&metrics,now);break;
case LV_EVENT_FLUSH_START:{
const lv_area_t *a=lv_event_get_param(e);
if(a)metrics_flush_start(&metrics,now,a->x1,a->y1,a->x2,a->y2);
break;
}
case LV_EVENT_FLUSH_FINISH:metrics_flush_end(&metrics,now);break;
case LV_EVENT_FLUSH_WAIT_START:metrics_wait_start(&metrics,now);break;
case LV_EVENT_FLUSH_WAIT_FINISH:metrics_wait_end(&metrics,now);break;
default:break;
}
/* Never replace the port flush callback or call flush_ready here. */
}
static void restart_window(void){
measuring=false;metrics_reset(&metrics);warmup_until=now_us()+2000000;
lv_label_set_text(mode_label,full_invalidate?"Mode: full invalidation":"Mode: local changes");
}
static void mode_clicked(lv_event_t *e){
(void)e;full_invalidate=!full_invalidate;restart_window();
}
static void reset_clicked(lv_event_t *e){(void)e;restart_window();}
static void touch_pressed(lv_event_t *e){
(void)e;uint64_t pressed_at=now_us();++touches;
lv_label_set_text_fmt(feedback_label,"T%lu",(unsigned long)(touches%10));
lv_obj_set_style_bg_color(feedback_label,lv_color_hex(touches%2?0x206048:0x74284C),0);
lv_obj_invalidate(feedback_label);
if(measuring)metrics_touch(&metrics,pressed_at);
}
static void sample_tick(lv_timer_t *timer){
(void)timer;uint64_t now=now_us();
if(!measuring&&now>=warmup_until){metrics_reset(&metrics);measuring=true;measured_since=last_report=now;}
lv_label_set_text_fmt(counter_label,"Count %06lu",(unsigned long)(++counter%1000000));
if(full_invalidate)lv_obj_invalidate(screen);
if(!measuring)return;
metrics_expire_touch(&metrics,now);
if(now-last_report<2000000)return;
last_report=now;
/* Cumulative after warmup. No reset halfway through an in-flight render. */
printf("D08,%s,%u,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%u,%u,%u\n",
full_invalidate?"full":"local",
#if CONFIG_BSP_LCD_DRAW_BUF_DOUBLE
2U,
#else
1U,
#endif
(unsigned long long)(now-measured_since),
(unsigned long long)metrics.render_count,(unsigned long long)metrics.render_us,(unsigned long long)metrics.render_max_us,
(unsigned long long)metrics.flush_count,(unsigned long long)metrics.payload_bytes,(unsigned long long)metrics.submit_us,
(unsigned long long)metrics.wait_us,(unsigned long long)metrics.feedback_count,(unsigned long long)metrics.feedback_us,
(unsigned long long)metrics.feedback_max_us,(unsigned long long)metrics.feedback_overwritten,(unsigned long long)metrics.feedback_timeout,
(unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT),
(unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT),
(unsigned)heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT));
}
static lv_obj_t *label_at(int x,int y,const char *text){
lv_obj_t *o=lv_label_create(screen);lv_obj_set_pos(o,x,y);lv_label_set_text(o,text);return o;
}
static void button_at(int x,const char *text,lv_event_cb_t callback){
lv_obj_t *o=lv_button_create(screen);lv_obj_set_pos(o,x,174);lv_obj_set_size(o,140,44);
lv_obj_add_event_cb(o,callback,LV_EVENT_CLICKED,NULL);
lv_obj_t *t=lv_label_create(o);lv_label_set_text(t,text);lv_obj_center(t);
}
static void create_ui(void){
screen=lv_screen_active();lv_obj_remove_flag(screen,LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_bg_color(screen,lv_color_hex(0x101C2B),0);lv_obj_set_style_text_color(screen,lv_color_white(),0);
label_at(12,12,"Refresh experiment");
feedback_label=label_at(240,8,"T0");lv_obj_set_size(feedback_label,68,28);
lv_obj_set_style_bg_opa(feedback_label,LV_OPA_COVER,0);lv_obj_set_style_text_align(feedback_label,LV_TEXT_ALIGN_CENTER,0);
mode_label=label_at(12,58,"");counter_label=label_at(12,96,"");lv_obj_set_size(counter_label,230,28);
label_at(12,138,"100ms source; software counters");
button_at(12,"Full / Local",mode_clicked);button_at(168,"Reset window",reset_clicked);
restart_window();lv_timer_create(sample_tick,100,NULL);
}
void app_main(void){
ESP_ERROR_CHECK(bsp_i2c_init());i2c_master_bus_handle_t bus=bsp_i2c_get_handle();
if(i2c_master_probe(bus,0x24,100)!=ESP_OK||i2c_master_probe(bus,0x5D,100)==ESP_OK||i2c_master_probe(bus,0x14,100)==ESP_OK){
ESP_LOGE(TAG,"Require confirmed BOX3 TT21100/ST7789 combination; touch probe failed/other controller branch");return;
}
lv_display_t *disp=bsp_display_start();lv_indev_t *input=bsp_display_get_input_dev();
if(!disp||!input){ESP_LOGE(TAG,"Display/touch allocation failed");return;}
ESP_ERROR_CHECK(bsp_display_brightness_set(60));
if(!bsp_display_lock(1000)){ESP_LOGE(TAG,"LVGL lock timeout");return;}
create_ui();lv_display_add_event_cb(disp,display_event,LV_EVENT_ALL,NULL);
lv_indev_add_event_cb(input,touch_pressed,LV_EVENT_PRESSED,NULL);bsp_display_unlock();
ESP_LOGI(TAG,"320x240 RGB565, internal DMA strips 320x40; software submit != DMA completion != optical pass");
puts("kind,mode,buffers,elapsed_us,render_cycles,render_elapsed_us,render_max_us,flush_submits,rgb565_submitted_bytes,submit_elapsed_us,wait_elapsed_us,feedback_submits,feedback_elapsed_us,feedback_max_us,feedback_overwritten,feedback_timeout,internal_free_bytes,internal_largest_bytes,internal_min_since_boot_bytes");
}
参考:[2] BSP3.2.1固定提交:显示初始化、条带大小、内部DMA与双缓冲[3] LVGL9.4.0固定提交:render/flush/wait事件的实际调用边界[4] LVGL9.4.0显示事件和接口[5] port2.9.0:SPI异步颜色传输回调与flush_ready所有权
按四格对照收集自己的数据
固定同一主机、屏幕方向、240MHz CPU、背光PWM60%、计数标签/字体、USB连接和日志设置。单缓冲局部、单缓冲全屏失效、双缓冲局部、双缓冲全屏失效分别记录,切换等待2秒预热后至少20秒,每格至少三次,冷启动顺序交替,保存原始日志和固件SHA。
每格先不触摸采一组,再以同一位置、相近节奏点右上附近或按按钮做反馈组,记录覆盖/超时。不要把模式切换时被丢弃的预热触摸当故障;触摸密集覆盖也不能无说明删除。
把捕获的原始串口保存为run.log,再执行工具。render_cycles_per_s是该窗口有失效区域的render周期率,源更新只有名义10Hz,绝不能凭这个计数宣称显示器峰值FPS。每个窗口用累计计数除真实elapsed;每次Reset另存日志文件,不把不同窗口拼接后相加。工具遇到同窗口累计数倒退会拒绝,不能依靠它猜测全部重置。
| 对照格 | 至少记录 | 当前本站结果 |
|---|---|---|
| single + local | 窗口/周期/提交/等待/堆/软件反馈 | 空;待测 |
| single + full | 相同数据源与相同窗口 | 空;待测 |
| double + local | 分配成功与内部最大连续块 | 空;待测 |
| double + full | 重复次数、离散程度与视觉现象 | 空;待测 |
python3 tools/summarize.py run.log
cc -std=c11 -Wall -Wextra -Werror -I main host/host_check.c -o /tmp/d08-host
/tmp/d08-host
python3 host/summary_test.py参考:[2] BSP3.2.1固定提交:显示初始化、条带大小、内部DMA与双缓冲[3] LVGL9.4.0固定提交:render/flush/wait事件的实际调用边界[6] IDF5.5.5:堆容量、最大块与启动以来最低水位
用独立录像补齐真实触摸与闪烁
高速录像固定相机、曝光、帧率、屏幕亮度和可见触发动作,至少拍触摸动作与右上反馈块。逐帧定义“第一次接触”和“第一个可辨像素变化”,报告设备实际帧率及一帧量化误差;无法从视频确认触点时,只报告可见触发物到画面的延迟,不能冒称物理触摸总延迟。
同时记录局部撕裂、短暂旧字、背景闪烁和全屏变化。屏幕扫描与相机滚动快门可能产生条带,换快门/机位对照后再判断;软件统计变快而画面仍闪,不说明实验已解决问题。本文没有这组录像,不给毫秒结论。
- 做到这里,你应该看到
- 软件提交延迟、DMA边界与光学变化分栏,未知项保持空。
参考:[3] LVGL9.4.0固定提交:render/flush/wait事件的实际调用边界[5] port2.9.0:SPI异步颜色传输回调与flush_ready所有权
按证据选择方案并保留恢复路径
显示探测失败先回D06核对实际控制器、版本和坐标;分配失败先查内部可DMA最大连续块、缓冲尺寸和其他初始化对象。双缓冲会多用名义25600B像素空间,不因Flash/PSRAM总容量大就保证内部DMA分配足够。
若全屏失效使提交字节和等待显著增加,先减少不必要的invalidate和改变区域,再看双缓冲是否在相同画面任务下有可重复改善。若render elapsed高而flush/wait低,再调查字体/混合/布局;这些是诊断方向,不能仅凭本例软件计数断言瓶颈已确定。
保留原始日志、工具版本、RGB565设置、BSP配置和视频。恢复用同一BOX-3官方刷写流程换回已保存固件;不修改eFuse或靠关闭检查隐藏问题。11组同源模型与7组日志工具合成检查已通过,全部真实屏幕、触摸、FPS和内存运行数值仍pending。
参考:[1] BOX-3硬件概览:固定官方提交与16MB/16MB物料[2] BSP3.2.1固定提交:显示初始化、条带大小、内部DMA与双缓冲[3] LVGL9.4.0固定提交:render/flush/wait事件的实际调用边界[5] port2.9.0:SPI异步颜色传输回调与flush_ready所有权[6] IDF5.5.5:堆容量、最大块与启动以来最低水位
资料来源与版本说明
保留原D08 ID/slug;原题性能目标制作成完整实验方法,因为本站没有实板测量。两个真实构建不表示双缓冲更快;所有结果栏留空。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。