简介:面向穿戴设备小屏显示开发的 ST77903 QSPI 屏驱动示例工程,基于 STM32H743 主控、RT-Thread 实时系统与 LVGL 图形框架搭建,重点处理该小体积 LCD 内部无显存、必须依靠 QSPI 连续传输刷新画面的限制,并在主机侧采用专门线程持续完成刷屏任务,适合从事低功耗小屏 UI、LCD 驱动移植和系统联调的嵌入式开发者参考。压缩包共 2591 个文件,约 13.66MB,以 .h 与 .c 源码、.s 启动汇编、.sconscript 与 .kconfig 构建配置、.icf 与 .ld 链接脚本以及 .txt、.md 说明文档为主,并包含 PNG 图片素材和完整工程配置。已有 2592 人学习下载。包内附带 3 个手表表盘示例,涉及屏幕初始化、QSPI 连续刷新线程、LVGL 显示接口与表盘资源组织,可帮助理解界面渲染、线程调度和刷屏链路的配合,也能参考链接脚本、构建脚本及目录结构完成驱动移植、刷新稳定性排查与工程裁剪。
1. 从一块 480×480 的方屏说起
把一块 480×480 的方屏焊到 STM32H743 的板子上,先用普通 SPI 试了一版,刷一帧七十多毫秒,翻页像放幻灯片;换成 QSPI 四线以后,理论带宽一下子翻了四倍。可真正把 ST77903 这颗驱动 IC 接上去,花屏、画面整体偏移、第一帧白屏这些说不清原因的问题却一个都没落下。这套 DEMO 要解决的正是这类问题:H743 的 QUADSPI 外设直连面板的 QSPI 接口,RT-Thread 负责线程与系统节拍,LVGL 只管把界面渲染到 draw buffer,像素搬运交给 DMA,CPU 全程不参与搬运。它适合正在用 H7 系列带小尺寸屏、又想在 RT-Thread 上跑 LVGL 的开发者,尤其是屏已经焊死在板子上、驱动得自己从零写、规格书翻了三遍还是调不出图的场景。下面按工程目录一路拆到 flush 回调,能直接抄的部分我都贴了代码。
2. 技术选型:H743 的 QUADSPI 怎么把 ST77903 喂饱
2.1 为什么这块屏必须走 QSPI,而不是普通 SPI
先算一笔账,480×480 分辨率、RGB565 每像素 2 字节,一帧就是 460800 字节,换算过来约 450KB。普通 SPI 单线在 50MHz 下理论带宽 6.25MB/s,一帧要 74ms,也就是 13fps 封顶,而且这 74ms 里 CPU 要么死等要么被中断打断,界面滑动基本没戏。
| 接口形态 | 数据线 | 典型时钟 | 理论带宽 | 一帧耗时(480×480×16bit) |
|---|---|---|---|---|
| 普通 SPI | 1 | 50 MHz | 6.25 MB/s | 约 74 ms |
| Dual SPI | 2 | 50 MHz | 12.5 MB/s | 约 37 ms |
| QSPI | 4 | 50 MHz | 25 MB/s | 约 18 ms |
| QSPI | 4 | 100 MHz | 50 MB/s | 约 9 ms |
H743 的 QUADSPI 内核时钟可以挂到 200MHz,前置分频器取值 1 到 256,所以 100MHz 的 SCLK 只需要把ClockPrescaler设成 2,这是一般做法。剩下的接口选项里,8080 并口虽然更快,但 16 根数据线加控制线太吃引脚,H743 的封装未必排得开;QSPI 只用 6 根线(CLK、CS、IO0~IO3)就能拿到 50MB/s,性价比最高,这也是这块板子最终选它的原因。
有个细节我踩过一次:H743 的 QUADSPI 支持内存映射模式,把外部设备当成一段地址空间直接读。但内存映射在 H7 上只能读不能写,而刷屏本质是往面板 GRAM 里写像素,所以内存映射这条路对显示没意义,只能老老实实走“命令 + 数据”的间接模式。想省事直接memcpy到面板地址的同学,早点放弃这个念头。
2.2 RT-Thread 与 LVGL 的嫁接点只有三个
很多人以为 RT-Thread 上跑 LVGL 要写一大堆适配层,其实真正的嫁接点就三个:一个毫秒级 tick 源、一个周期性调用lv_timer_handler()的线程、一个把渲染结果推给硬件的 flush 回调。剩下的都是 LVGL 自己的事。
tick 源最省事的做法是直接借 RT-Thread 的系统节拍,前提是RT_TICK_PER_SECOND配成 1000,这样rt_tick_get()返回的就是毫秒:
/* LVGL v9 的写法:注册一个 tick 回调,LVGL 自己按需取 */ static uint32_t lv_tick_from_rt(void) { return (uint32_t)rt_tick_get(); /* RT_TICK_PER_SECOND = 1000 时正好是毫秒 */ } void lv_port_tick_init(void) { lv_tick_set_cb(lv_tick_from_rt); }LVGL v8 没有lv_tick_set_cb,得反过来做:起一个 1ms 周期的软件定时器,在回调里调lv_tick_inc(1)。两种方式的差别在于 v8 是“推”,v9 是“拉”,效果一样,但 v8 那个定时器回调跑在中断上下文里,别在里面做任何耗时动作,否则整个系统的节拍都会被拖慢。
线程部分给个普通优先级的静态线程就够了,栈给 8KB 起步,开了抗锯齿和大字体再往上加:
static void lvgl_thread_entry(void *parameter) { lv_init(); lv_port_tick_init(); lv_port_disp_init(); /* 里面会注册 flush 回调 */ while (1) { lv_timer_handler(); /* 渲染、动画、输入扫描都在这 */ rt_thread_mdelay(5); /* 5ms 一轮,兼顾响应和 CPU 占用 */ } } static int lvgl_thread_init(void) { rt_thread_t tid = rt_thread_create("lvgl", lvgl_thread_entry, RT_NULL, 8192, 15, 10); if (tid != RT_NULL) rt_thread_startup(tid); return RT_EOK; } INIT_APP_EXPORT(lvgl_thread_init);这里lv_timer_handler()的返回值和rt_thread_mdelay(5)是两个可以调的参数。lv_timer_handler()会返回距离下一个定时器到期还有多少毫秒,理论上可以用它动态决定睡多久,但实际项目里我一般固定 5ms:小于 5ms 时收益不明显,还容易和刷屏线程抢 CPU;大于 10ms 又会明显感到滑动不跟手。
2.3 ST77903 在 QSPI 下的三种帧型:指令、地址、数据
理解这颗 IC 的 QSPI 协议,关键是把一次总线操作拆成三段看:指令段(Instruction)、地址段(Address)、数据段(Data)。指令段通常是单线发的 8 位命令码,地址段放寄存器地址或参数,数据段才是真正搬运的像素流。数据段走几根线,就决定了带宽上限。
STM32 的 HAL 把这套东西抽象成了QSPI_CommandTypeDef结构体,字段和 ST77903 时序段的对应关系如下:
| HAL 字段 | 典型取值 | 对应时序段 |
|---|---|---|
Instruction | 0x2A / 0x2B / 0x2C | 指令段,面板命令码 |
InstructionMode | QSPI_INSTRUCTION_1_LINE | 指令段单线发送 |
Address | 寄存器地址,无地址时填 0 | 地址段内容 |
AddressMode | QSPI_ADDRESS_1_LINE或QSPI_ADDRESS_NONE | 地址段线数 |
AddressSize | QSPI_ADDRESS_24_BITS | 地址段位宽 |
DataMode | QSPI_DATA_4_LINES | 数据段线数 |
NbData | 参数字节数或像素字节数 | 数据段长度 |
SIOOMode | QSPI_SIOO_INST_EVERY_CMD | 每条命令都重发指令段 |
规格书里常见的写法是“1-1-4”或者“1-4-4”,说的就是指令段、地址段、数据段各用几根线。如果换了一颗驱动 IC 或者换了块屏,最省事的调试方式不是重写驱动,而是把InstructionMode、AddressMode、DataMode这三个字段按规格书改一遍,能出图就说明协议对上了。这套 DEMO 用的是单线指令 + 四线数据,也就是参数和像素都走四线,因为 0x2A、0x2B 这种坐标命令的参数只有几个字节,线数对耗时影响可以忽略,统一成四线能少一套分支代码。
3. 目录结构与上电链路:从上电到第一帧
3.1 工程目录怎么分层
这套 DEMO 的目录结构是典型的 RT-Thread 工程加一层外设驱动,看得懂这棵树基本就摸清了它的组织方式:
project/ ├── applications/ # 业务线程、LVGL 界面、MSH 调试命令 │ ├── main.c │ ├── lvgl_thread.c │ └── msh_cmd.c ├── board/ # 板级初始化:时钟树、SDRAM、引脚复用 │ └── board.c ├── drivers/ # 外设驱动,和 RT-Thread 设备框架对接 │ ├── drv_qspi.c # QUADSPI 底层 + DMA 通道 │ ├── st77903.c # 面板初始化序列、开窗、写像素 │ ├── st77903.h │ └── lv_port_disp.c # LVGL 显示适配层 ├── packages/ │ └── lvgl/ # LVGL 源码与配置 lv_conf.h ├── rt-thread/ # 内核与组件 └── libraries/ # H7 HAL 与 CMSIS分层意图很清楚:drv_qspi.c只管总线和 DMA,不认面板;st77903.c只管面板时序和命令,不认 LVGL;lv_port_disp.c是唯一同时认识两边的文件。这样换屏的时候只动st77903.c和lv_port_disp.c里的分辨率宏,换 GUI 框架的时候只动lv_port_disp.c,二者互不影响。我见过不少项目把三层糅在一个文件里,改个分辨率要翻八百行,那种结构后期基本没法维护。
3.2 启动顺序:顺序错了就是白屏
上电到出第一帧,顺序是固定的,中间任何一步提前或推后都会出问题:
- 板级时钟初始化,把 QUADSPI 的内核时钟源配到 200MHz(一般走 PLL2 或者 per_ck);
- 六个 QSPI 引脚配成复用功能 9、推挽输出、
GPIO_SPEED_FREQ_VERY_HIGH,引脚号照原理图来; - 调用
HAL_QSPI_Init()配分频、片选高电平时间、采样相位; - 硬件复位面板:RST 拉低至少 10ms,拉高后等 120ms;
- 逐条下发初始化序列,最后发 0x29(Display On);
- 打开背光;
- 调
lv_port_disp_init()注册显示设备,LVGL 开始刷第一帧。
第 4 步的延时是新手最常漏掉的。ST77903 内部有电荷泵和振荡器,复位后需要时间稳定,手册上给的 120ms 是最小值,我一般给 150ms,宁可慢一点也不要赌。第 5 步和第 7 步之间如果不留一点空隙,LVGL 会立刻全屏刷一次,此时电荷泵还没完全起来,表现就是“第一帧全白、第二帧开始正常”,看着像玄学,其实是时序。
3.3 面板初始化序列怎么写、怎么调
初始化序列不要写成一大串write_cmd(); write_data();的平铺代码,那样加一条命令要改三处。用查表法,把命令、参数、延时打包成结构体数组,驱动只负责遍历:
typedef struct { uint8_t cmd; /* 面板命令码 */ uint8_t len; /* 参数字节数,0 表示纯命令 */ uint8_t data[12]; /* 参数,多余位置补 0 */ uint16_t delay_ms; /* 发完这条命令后等待的毫秒数 */ } lcd_init_cmd_t; static const lcd_init_cmd_t st77903_init_seq[] = { {0x01, 0, {0}, 120}, /* 软复位,等电荷泵稳定 */ {0x11, 0, {0}, 150}, /* Sleep Out */ {0x3A, 1, {0x55}, 0}, /* 像素格式 RGB565 */ {0x36, 1, {0x00}, 0}, /* MADCTL:方向与 RGB 顺序 */ {0xB2, 12, {0x0C,0x0C,0x00,0x33,0x33,0x33,0x00,0x00,0x00,0x00,0x00,0x00}, 0}, {0xE0, 12, {0x00,0x07,0x0F,0x0E,0x0B,0x0A,0x0A,0x0A,0x0A,0x0A,0x0A,0x0A}, 0}, {0x21, 0, {0}, 0}, /* Display Inversion,IPS 屏一般要开 */ {0x29, 0, {0}, 20}, /* Display On */ };驱动侧只是遍历:
void st77903_init(void) { lcd_hw_reset(); /* RST 低 10ms,高后延时 120ms */ for (uint32_t i = 0; i < sizeof(st77903_init_seq) / sizeof(st77903_init_seq[0]); i++) { const lcd_init_cmd_t *c = &st77903_init_seq[i]; lcd_qspi_cmd(c->cmd, c->len ? c->data : RT_NULL, c->len); if (c->delay_ms) { rt_thread_mdelay(c->delay_ms); } } }参数说明:0x3A的0x55表示 16bit RGB565,改成0x66就是 18bit,带宽立刻涨 12.5%,一般不值当;0x36的 MADCTL 决定画面方向,低三位控制行列交换和镜像,这块屏的默认值 0x00 是竖屏正向,要横屏就改成 0x60;0xB2是 porch 时序,必须照屏厂给的参数填,抄别的屏大概率出偏移;0xE0是伽马,先用屏厂默认值,色温不满意再来调这一组,别一上来就动它。
调试这套序列最有效的手段是“二分法”:注释掉一半命令,看屏幕是白屏、花屏还是纯黑,能把问题锁定在一小段区间内。另外,如果板子引出了 QSPI 的 IO 引脚,拿逻辑分析仪抓一帧,直接看指令段和数据段的波形,比盲猜快十倍——这就是我说的“黑匣子”思路,总线上的东西骗不了人。
4. 刷屏核心:flush_cb、开窗与 DMA 收尾
4.1 flush_cb 的两段式:先开窗,再灌像素
LVGL 渲染完一块区域后,会把这块矩形交给flush_cb,剩下的全靠我们。面板的模型是“先声明写入窗口,再连续往窗口里灌像素”,所以一次刷新天生分成两段:
/* 设置写入窗口:CASET 定列范围,RASET 定行范围,RAMWR 准备接收像素 */ static void st77903_set_window(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { uint8_t buf[4]; buf[0] = x1 >> 8; buf[1] = x1 & 0xFF; buf[2] = x2 >> 8; buf[3] = x2 & 0xFF; lcd_qspi_cmd(0x2A, buf, 4); /* CASET:列起始 / 列结束 */ buf[0] = y1 >> 8; buf[1] = y1 & 0xFF; buf[2] = y2 >> 8; buf[3] = y2 & 0xFF; lcd_qspi_cmd(0x2B, buf, 4); /* RASET:行起始 / 行结束 */ lcd_qspi_cmd(0x2C, RT_NULL, 0); /* RAMWR:后面直接跟像素流 */ }坐标用 16 位,是因为 480 已经超过 8 位能表示的 255,高位字节在前还是在后取决于面板,ST77903 这类 IC 是大端,先发高字节。如果画面整体左右偏移几个像素,八成是这里的高低位反了,或者屏本身有offset_x/offset_y,得在调用前加上去。
真正把像素推出去的函数有两个版本,同步版用来调试,DMA 版用来跑正式效果:
/* 同步版:CPU 死等,调试时用来判断是总线问题还是 DMA 问题 */ void st77903_write_pixels_sync(const uint8_t *buf, uint32_t bytes) { QSPI_CommandTypeDef s = {0}; s.Instruction = 0x2C; /* 直接续上 RAMWR 的像素流 */ s.InstructionMode = QSPI_INSTRUCTION_1_LINE; s.AddressMode = QSPI_ADDRESS_NONE; s.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; s.DataMode = QSPI_DATA_4_LINES; /* 像素走四线 */ s.NbData = bytes; s.DummyCycles = 0; s.DdrMode = QSPI_DDR_MODE_DISABLE; s.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_Command(&hqspi, &s, 100); HAL_QSPI_Transmit(&hqspi, (uint8_t *)buf, 100); }NbData是字节数不是像素数,RGB565 下一个像素 2 字节,别传成像素个数,不然只会刷出半屏。SIOOMode选INST_EVERY_CMD表示每条命令都重新发一次指令段,配合面板的 QSPI 从机逻辑;如果选INST_ONCE,连续写的时候第二段会没有指令头,面板直接不认。
4.2 DMA 版与双缓冲:把 CPU 从总线里摘出来
同步版里 CPU 全程等着总线发完,480×480 一帧按 100MHz QSPI 算大概 9ms,这 9ms 里 LVGL 什么都干不了。改成 DMA 之后,CPU 发完命令就去渲染下一块,等传输完成中断再回来告诉 LVGL“这块 buffer 可以复用了”。
/* 异步版:启动 DMA 后立刻返回,传输完成在回调里通知 LVGL */ void st77903_write_pixels_dma(const uint8_t *buf, uint32_t bytes) { QSPI_CommandTypeDef s = {0}; s.Instruction = 0x2C; s.InstructionMode = QSPI_INSTRUCTION_1_LINE; s.AddressMode = QSPI_ADDRESS_NONE; s.DataMode = QSPI_DATA_4_LINES; s.NbData = bytes; s.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; /* H7 的 D-Cache 默认开着,DMA 读的是内存里的旧数据,必须先 clean */ SCB_CleanDCache_by_Addr((uint32_t *)buf, bytes); HAL_QSPI_Command(&hqspi, &s, 100); HAL_QSPI_Transmit_DMA(&hqspi, (uint8_t *)buf); } /* 传输完成回调:告诉 LVGL 这块 draw buffer 空了,可以拿去画下一帧 */ void HAL_QSPI_TxCpltCallback(QSPI_HandleTypeDef *hqspi) { if (hqspi->Instance == QUADSPI) { lv_disp_flush_ready(&disp_drv); } }参数上要注意两点。第一,SCB_CleanDCache_by_Addr要求起始地址 32 字节对齐、长度按 32 字节向上取整,所以 draw buffer 最好直接用RT_ALIGN(480 * 40 * 2, 32)声明成静态数组,别用malloc随手要一块,对齐不上的时候 cache 维护函数会静默失效,表现出来就是“偶尔出现几条花带”。第二,H7 的 QSPI 传输 DMA 走的是 MDMA 而不是普通 DMA,需要在HAL_QSPI_MspInit里用__HAL_LINKDMA把句柄挂上,通道搞错了会出现“命令发出去了但数据一个字节没动”的现象。
双缓冲的意义在于:LVGL 往 buffer A 里画的时候,buffer B 正在总线上传,两边并行。注册方式是在lv_disp_drv_t里同时给buffer和buffer2,并把full_refresh置 0,让 LVGL 自己做局部刷新。buffer 大小不用给满一屏,给 1/10 屏(480×48×2 ≈ 46KB)就能拿到不错的帧率,剩下的内存留给界面。
4.3 局部刷新:脏矩形怎么落到 CASET/RASET
LVGL 默认只重绘“脏”的区域,flush_cb收到的area可能是一行文字大小的矩形,而不是整屏。这带来一个取舍:矩形越碎,命令开销占比越高。0x2A/0x2B/0x2C 三条命令加起来十几个字节,按四线算不到 1 微秒,但如果一次刷新拆成两百个小矩形,光命令开销就够呛。
我一般做两级处理。第一级是给 LVGL 的lv_disp_drv_t设一个最小合并粒度,把相邻的小矩形在应用层合并;第二级是在flush_cb里判断面积,如果这次刷新的像素数小于阈值(比如 2000 像素),就攒着不刷,等下一次一起发。代价是可能多等一帧,收益是命令开销骤降。
static void disp_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *px_map) { uint32_t w = area->x2 - area->x1 + 1; uint32_t h = area->y2 - area->y1 + 1; st77903_set_window(area->x1 + PANEL_OFFSET_X, area->y1 + PANEL_OFFSET_Y, area->x2 + PANEL_OFFSET_X, area->y2 + PANEL_OFFSET_Y); st77903_write_pixels_dma((const uint8_t *)px_map, w * h * 2); /* 注意:这里不能调 lv_disp_flush_ready,要等 TxCplt 回调里再调 */ }提示:
lv_disp_flush_ready()的位置是这套代码里最容易写错的一行。写在flush_cb末尾代表同步模式,写在 DMA 完成回调里代表异步模式。写错位置不会报错,只会表现为画面撕裂或者数据错乱,排查起来相当费时间。
5. 避坑与排查:QSPI 刷屏的五条踩坑记录
5.1 花屏、斜纹、颜色错乱
现象:屏幕能亮,但画面像被撕成斜条,颜色也发紫发绿。
原因:九成是 QSPI 的采样相位不对或者时钟太快。H7 的 QUADSPI 在SampleShifting为NONE时,是在 SCLK 上升沿采样,如果屏端输出延迟偏大,采到的就是上一个 bit。
解决:先把ClockPrescaler从 2 改成 6(SCLK 降到 33MHz),如果花屏消失,说明是时序余量问题,再往上试;同时把SampleShifting改成QSPI_SAMPLE_SHIFTING_HALFCYCLE,相当于多给半个周期的建立时间。还有一个容易被忽略的点是ChipSelectHighTime,片选拉高时间太短,面板来不及结束上一帧,也会出现整行错位,一般设成 2 个周期以上。
5.2 第一帧白屏,第二帧正常
现象:上电后先白一下,之后画面完全正常,复位一次又复现。
原因:初始化序列里“Sleep Out”之后没等够时间。ST77903 内部电荷泵起振需要时间,手册写 120ms 是最小值,实际板子上电源爬升慢的话要更久。
解决:把0x11后面的延时从 120ms 提到 150~200ms,0x29(Display On)后面也补 20ms。另外确认lcd_hw_reset()里 RST 拉低的时间够 10ms,只拉低 1ms 的话有些批次的面板根本不认。
5.3 画面整体偏移几个像素或上下颠倒
现象:图形能画出来,但整体往右下角挪了若干像素,边缘被裁掉一条。
原因:一是CASET/RASET传坐标时没加面板偏移量,这类方形屏的可视区在 GRAM 里不是从 (0,0) 开始的;二是MADCTL(0x36)配错,行列交换或者镜像方向反了。
解决:先只画一个从 (0,0) 开始的 100×100 红块,看它出现在屏幕的哪个角,根据偏移量反推PANEL_OFFSET_X/Y;再把 0x36 的参数在 0x00 / 0x60 / 0xC0 / 0xA0 四个值之间试一遍,一般能对上。这一步别猜,用红块实测最快。
5.4 偶发花带、几行像素乱掉
现象:大部分时间正常,偶尔出现几条横向花带,跑久了概率变高。
原因:D-Cache 一致性。H7 默认开 D-Cache,CPU 刚写完 draw buffer,数据还在 cache 里没落到 SRAM,DMA 从 SRAM 读到的就是旧内容。
解决:每次启动 DMA 前调用SCB_CleanDCache_by_Addr(),且 buffer 地址必须 32 字节对齐、长度按 32 字节向上取整。如果 buffer 是malloc出来的,用rt_malloc_align(size, 32)分配。这条坑我调了整整一个下午,最后发现是缓冲区地址没对齐,函数悄悄什么都没做。
5.5 帧率上不去,滑动明显卡顿
现象:界面能跑,但滑动列表像卡在 20fps,CPU 占用也不高。
原因:flush_cb里用的是同步HAL_QSPI_Transmit,CPU 全程等总线;或者 draw buffer 太小,一帧被拆成几十次刷新,每次都要重发三条开窗命令。
解决:换成 DMA 版本,并把lv_disp_flush_ready()挪到TxCpltCallback里;draw buffer 从 1/20 屏提到 1/10 屏甚至 1/5 屏。这两步做完,同样是 100MHz QSPI,帧率通常能翻一倍。
排查顺序我总结成一句话:先降频看是不是时序,再看延时是不是不够,最后查 cache 和 DMA。这三步能覆盖九成以上的问题,剩下的基本是硬件焊接或者排线阻抗。
6. 二次开发:换分辨率、挂触摸、跑分验证
6.1 换屏或改分辨率要动的六个地方
假设你手上是另一块 320×480 的屏,用的还是这颗 IC,改动清单如下:
| 改动位置 | 具体内容 |
|---|---|
st77903.h | LCD_HOR_RES/LCD_VER_RES改成 320 / 480 |
| 初始化表 0xB2 | porch 参数换成新屏厂的,不能沿用 |
| 初始化表 0x36 | 按新屏的排线方向重设 MADCTL |
PANEL_OFFSET_X/Y | 按红块实测结果填 |
lv_port_disp.c | draw buffer 尺寸按新宽度重新算,注意 32 字节对齐 |
lv_conf.h | LV_COLOR_DEPTH保持 16,横竖屏切换时同步改LV_DPI_DEF |
改完先别急着跑界面,用st77903_fill(0xF800)全屏填红,确认颜色和方向都对,再往上叠 LVGL。顺序反了的话,出问题你分不清是驱动错还是界面错。
6.2 挂一颗 I2C 触摸并注册成输入设备
触摸部分和显示是两条独立链路,I2C 读坐标,转成 LVGL 的输入事件即可:
static void touch_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { uint16_t x, y; if (tp_read_xy(&x, &y) == 0) { /* 读到了按下 */ >static int lcd_bench(int argc, char **argv) { uint32_t t0 = rt_tick_get(); for (int i = 0; i < 100; i++) { st77903_fill(i & 1 ? 0xFFFF : 0x0000); /* 全屏黑白交替 */ } uint32_t cost = rt_tick_get() - t0; rt_kprintf("100 frames in %d ms, %d fps\n", cost, 100000 / cost); return 0; } MSH_CMD_EXPORT(lcd_bench, qspi lcd fill benchmark);这个命令测的是纯总线吞吐,不含 LVGL 渲染,是硬件的上限。再对比 LVGL 实际帧率,两者差距大说明瓶颈在渲染而不是总线,那就该去调 buffer 大小和界面复杂度了,而不是继续折腾 QSPI 分频。
最后说个教训。早年调这类屏,我总喜欢先写界面再调驱动,觉得能看到 UI 才有成就感,结果每次花屏都在“到底是驱动错还是布局错”之间反复横跳。后来我强制自己按三步走:先用纯色填充验证总线和开窗,再用色块验证方向和偏移,最后才上 LVGL 界面。从那以后每次换屏我都强制走一遍这套流程,哪怕看起来浪费时间,也比在两层问题里互相甩锅强。这套 DEMO 的代码结构其实就是按这个顺序搭的,照着它的分层去改,比从零拼一个要省不少事。希望帮到你。
本文还有配套的精品资源,点击获取