简介:ST77903是矽创(SITRONIX)推出的无内置RAM小尺寸LCD驱动IC,常见于智能手表等穿戴设备。由于内部不带显存,QSPI接口必须保持连续传输,主机侧需专门线程负责刷屏;这套DEMO基于STM32H743 + RT-Thread + LVGL + QSPI搭建了一套完整可运行的工程。压缩包共2591个文件,约13.66MB,其中以C/H源码为主,另有sconscript构建脚本、icf/ld链接脚本及少量png/gif图像资源,目录结构清晰,方便按模块裁剪和移植。DEMO内包含3个手表表盘界面,覆盖LVGL界面框架接入、RT-Thread多线程调度、QSPI连续刷屏机制、底层驱动初始化等关键实现,并可直接在H743上运行验证。已有2585人学习/下载本资源,适合嵌入式开发者和穿戴设备软件工程师用于入门ST77903驱动开发,或作为QSPI小尺寸LCD刷屏方案的工程参考。
1. 为什么是 ST77903:QSPI 接口把 LVGL 刷屏带出了新带宽
在 H743 上跑 LVGL,最让人头疼的画面往往不是控件布局,而是一块普通 SPI 屏把动画拖成了 PPT。ST77903 这颗矽创 LCD 驱动 IC 支持的 QSPI 四线接口,正好补上了"接口带宽不够"这块短板:四根数据线同时搬运数据,配合 H743 的 QUADSPI 外设和内存映射模式,用五六个引脚就能拿到接近并行 RGB 接口的刷屏体验。这套 DEMO 把 RT-Thread 的线程调度、LVGL 的图形栈、QSPI 的底层时序完整串了起来,拿到手可以直接编译下载看效果,也能拆开改成自己的产品和面板。适合正在做 H743 + LVGL 显示方案、又不想在屏接口上反复翻车的嵌入式工程师。
2. 选型与硬件连接:四根数据线换来的带宽账
2.1 为什么选 ST77903:先算带宽
选屏驱动 IC 的核心指标不是分辨率,而是每帧裸数据传输耗时。ST77903 支持四线 QSPI 传输,和传统 SPI LCD 最大的差别在数据通道宽度:SPI 一次只能挪 1 个 bit,QSPI 能同时挪 4 个 bit。这个差距在 320×480 这类分辨率下是决定性的。
一块 320×480、RGB565 的屏,一帧裸数据大约 300KB。20MHz 常规 SPI 全速传输要 120ms 以上,这还没算写命令、切行等开销,跑 LVGL 动画必然卡成幻灯片;同样 20MHz 下,QSPI 四线能把传输时间压到 30ms 左右,算上命令开销也能跑到 25fps 上下。要是把 QSPI 时钟提到 40MHz,一帧写入能压进 15ms,UI 的跟手程度完全不一样。
| 接口 | 有效数据宽度 | 20MHz 理论带宽 | 320×480×16bit 一帧耗时 |
|---|---|---|---|
| SPI 单线 | 1 bit | 2.5 MB/s | 约 120 ms |
| QSPI 四线 | 4 bit | 10 MB/s | 约 30 ms |
| QSPI 四线 @40MHz | 4 bit | 20 MB/s | 约 15 ms |
有人会问:为什么不直接上 RGB 并行接口?RGB888 需要 24 根数据线加时钟、同步信号,占掉的引脚资源够做一路完整的 SDRAM 了,在引脚紧张的 H743 板子上很不划算。QSPI 屏用更少的线换走并行接口的带宽优势,而且这套 DEMO 里没有外部 QSPI Flash 参与,QSPI 外设直接挂载 LCD 本体,硬件链路更短,问题也更好定位。
2.2 硬件接线:H743 QUADSPI 引脚映射
ST77903 屏模块一般通过 FPC 软排线引出接口,引脚命名通常直接标 QSPI 相关信号。H743 的 QUADSPI 外设按 Bank1 的典型接法如下,具体以开发板原理图为准:
| 信号 | MCU 引脚 | 方向 | 说明 |
|---|---|---|---|
| QUADSPI_BK1_IO0 | PF6 | 双向 | QSPI 数据线 0 |
| QUADSPI_BK1_IO1 | PF7 | 双向 | QSPI 数据线 1 |
| QUADSPI_BK1_IO2 | PF8 | 双向 | QSPI 数据线 2 |
| QUADSPI_BK1_IO3 | PF9 | 双向 | QSPI 数据线 3 |
| QUADSPI_BK1_CLK | PF10 | 输出 | QSPI 时钟 |
| QUADSPI_BK1_NCS | PF11 | 输出 | 片选,低有效 |
除了 QSPI 六根线,最少还需要一根硬件复位引脚(屏模块上一般标 RESX 或 RST)和一路背光控制。ST77903 的 TE 引脚(撕裂同步信号)建议也接出来,后面刷新优化会用到。电源部分必须按模块手册配置,IOVCC 和 VCI 电压不能乱加压,背光也不要直接从 GPIO 硬拉大电流,该加三极管或 MOS 开关就加。
2.3 软件栈配合:RT-Thread 管调度,LVGL 管界面
这套 DEMO 的软件结构比较清晰:LVGL 负责 UI 树、控件绘制、事件分发;RT-Thread 负责提供任务调度和环境基础。LVGL 的lv_timer_handler()不能裸奔,它需要稳定的时间心跳。在 RT-Thread 里,可以建一个 UI 线程,循环里跑lv_timer_handler()再用rt_thread_mdelay(5)让出 CPU,tick 则直接绑到系统 tick 上,两边时间基准保持一致。
内存开销也在这个软件栈里算一算。LVGL 双缓冲 320×480 分辨率下,每 16 位色一行 320 像素约 640 字节,如果开 40 行双缓冲,要 50KB 左右;再加上 LVGL 对象池、demo 控件,总体建议给到 120KB 以上堆空间。H743 内部 RAM 足够,关键是 RT-Thread 工程里别把堆配小,否则 UI 初始化到一半就分配失败,看起来就是屏幕半边不刷新。
3. 核心模块拆解:一块像素从 LVGL 到屏上 GRAM 的完整链路
3.1 工程目录结构:驱动、UI 与业务分开
RT-Thread Studio 生成的工程大致是这个结构,ST77903 相关文件都放在 applications 下,和系统驱动、LVGL 软件包保持独立:
project ├── applications │ ├── main.c # 入口,初始化顺序在这里 │ ├── st77903_qspi.c # ST77903 底层驱动 │ ├── st77903_qspi.h │ ├── lv_port_disp.c # LVGL 显示适配层 │ └── lv_port_disp.h ├── drivers │ └── qspi ├── board ├── packages │ └── LVGL └── rt-thread调用关系从 main 出发:先st77903_qspi_init()初始化屏,再lv_init()启动 LVGL,最后lv_port_disp_init()注册刷新回调。业务界面代码继续往上层放,驱动文件不掺 UI 逻辑,后面要换屏或换分辨率,只动底层驱动和移植层的几个宏。
3.2 QSPI 初始化:先用间接模式把命令写进去
H743 的 QUADSPI 有两种工作模式。初始化阶段用间接模式,CPU 通过寄存器逐个发命令;显示运行阶段切到内存映射模式,把 ST77903 的 GRAM 直接映射到 MCU 地址空间。QSPI 的 HAL 初始化核心参数如下:
static void MX_QUADSPI_Init(void) { hqspi.Instance = QUADSPI; hqspi.Init.ClockPrescaler = 4; /* AHB 200MHz / (4+1) = 40MHz */ hqspi.Init.FifoThreshold = 4; /* FIFO 阈值 4 字节 */ hqspi.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_NONE; hqspi.Init.FlashSize = 19; /* 2^19 = 512KB,覆盖屏的 GRAM */ hqspi.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_5_CYCLE; hqspi.Init.ClockMode = QSPI_CLOCK_MODE_0; hqspi.Init.FlashID = QSPI_FLASH_ID_1; hqspi.Init.DualFlash = QSPI_DUALFLASH_DISABLE; HAL_QSPI_Init(&hqspi); }参数说明:ClockPrescaler决定 QSPI 时钟频率,分频值越大越慢越稳定,白屏排查时优先把它调大;FlashSize是"容量地址位宽",按 2 的幂计算,这个 DEMO 里没有外部 Flash,填的是 ST77903 内部 GRAM 的大小;ChipSelectHighTime控制片选释放后的高电平持续时间,太短会导致连续命令间隔不够,表现为偶发写不进命令。
3.3 ST77903 初始化序列:复位、SLPOUT、COLMOD
ST77903 是 MIPI DBI 风格的驱动 IC,命令集和常见屏驱同源。底层命令发送用间接模式的 QSPI 命令结构体拼出来:
static void st77903_cmd(uint8_t cmd, uint8_t *data, uint32_t len) { QSPI_CommandTypeDef qcmd = {0}; qcmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 命令段走单线 */ qcmd.Instruction = cmd; qcmd.AddressMode = QSPI_ADDRESS_NONE; qcmd.DataMode = (len > 0) ? QSPI_DATA_4_LINES : QSPI_DATA_NONE; qcmd.NbData = len; HAL_QSPI_Command(&hqspi, &qcmd, 1000); if (len > 0) { HAL_QSPI_Transmit(&hqspi, data, len, 1000); } }注意InstructionMode是单线、DataMode是四线,两种线宽在同一帧命令里共存,这是 QSPI LCD 和 QSPI Flash 驱动的明显区别。初始化序列一般按下面这个顺序走:
static void st77903_init_sequence(void) { st77903_hw_reset(); /* RESX 拉低 10ms 再拉高 */ st77903_cmd(0x01, NULL, 0); /* SWRESET 软件复位 */ rt_thread_mdelay(120); st77903_cmd(0x11, NULL, 0); /* SLPOUT 退出休眠 */ rt_thread_mdelay(120); st77903_cmd(0x3A, (uint8_t[]){0x55}, 1); /* COLMOD RGB565 16bit */ st77903_cmd(0x36, (uint8_t[]){0x00}, 1); /* MADCTL 默认扫描方向 */ st77903_cmd(0x29, NULL, 0); /* DISPON 打开显示 */ }参数说明:0x01 软件复位后必须等待,序列要求给足延时再做后续命令;0x3A 配 0x55 表示 RGB565,这个值和 LVGL 的LV_COLOR_DEPTH 16必须对上,否则颜色直接错乱;0x36 是方向控制寄存器,屏反过来显示或者需要旋转 90° 时,改它的值比重写坐标计算简单得多。屏厂一般会给一份完整的寄存器初始化表,比这个公共序列长得多,实际项目里把那张表按同样的命令发送方式灌进去即可。
3.4 显存映射:把 GRAM 搬到 0x90000000
初始化完成、显示开启后,切到内存映射模式,这是让刷屏性能起飞的关键一步:
static void st77903_enter_memory_mapped(void) { QSPI_MemoryMappedTypeDef mem = {0}; mem.TimeOutActivation = QSPI_TIMEOUT_COUNTER_DISABLE; mem.TimeOutPeriod = 0; HAL_QSPI_MemoryMapped(&hqspi, &mem, 1000); /* 成功后,ST77903 的 GRAM 映射到 0x90000000 起始地址 */ }这个模式下,直接向 0x90000000 地址写数据,等效于向屏发 RAMWR 命令写显存。H743 的 QSPI 映射区按 MCU 地址总线访问,memcpy就能刷一整块颜色数据,不再需要逐包拼命令,CPU 负担大幅下降。
切模式有个顺序问题要先记住:执行HAL_QSPI_MemoryMapped()之后,系统再调用HAL_QSPI_Command()就会出错,因为外设已经切走了。需要回间接模式时,得先HAL_QSPI_Abort()再重新初始化。很多二次开发的人在这里翻车,后面避坑部分会细说。
3.5 LVGL 移植:flush_cb 里的逐行拷贝
LVGL 的显示适配集中在lv_port_disp_init()和刷新回调里。双缓冲配置如下:
#define LCD_HOR_RES 320 #define LCD_VER_RES 480 #define LCD_BUF_SIZE (LCD_HOR_RES * 40) /* 40 行像素 */ static lv_disp_draw_buf_t draw_buf; static lv_color_t buf1[LCD_BUF_SIZE]; static lv_color_t buf2[LCD_BUF_SIZE]; void lv_port_disp_init(void) { lv_disp_draw_buf_init(&draw_buf, buf1, buf2, LCD_BUF_SIZE); static lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.hor_res = LCD_HOR_RES; disp_drv.ver_res = LCD_VER_RES; disp_drv.flush_cb = disp_flush_cb; disp_drv.draw_buf = &draw_buf; lv_disp_drv_register(&disp_drv); }刷新回调负责把 LVGL 绘制好的一块区域搬到液晶屏。因为已经进入映射模式,直接按行偏移算地址拷贝:
static void disp_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { uint32_t y; uint32_t line_bytes = (area->x2 - area->x1 + 1) * sizeof(lv_color_t); for (y = area->y1; y <= area->y2; y++) { uint32_t offset = (y * LCD_HOR_RES + area->x1) * sizeof(lv_color_t); memcpy((void *)(0x90000000 + offset), (uint8_t *)color_p + (y - area->y1) * line_bytes, line_bytes); } lv_disp_flush_ready(drv); }逻辑说明:LVGL 回传的area是要刷新的矩形区域,color_p是该区域左上角的像素首地址。回调按行把区域内的像素拷贝到 GRAM 的对应偏移,一次刷一个矩形,比整屏重绘快得多。line_bytes用的是sizeof(lv_color_t),这是 RGB565 下的 2 字节;如果换成 RGB888,这里也要跟着变。映射模式绕开了 CASET/RASET 窗口命令,直接按地址写,所以整个链路里不需要再维护窗口坐标。
4. 编译烧录跑通:从 RT-Thread Studio 建工程到整屏渐变
4.1 新建工程并配置 QSPI 引脚
打开 RT-Thread Studio,新建 STM32H743 系列工程,选一个基于 H743 的 BSP 作为起点。工程生成后,第一步是确认 QSPI 的 GPIO 复用是否配置好。PF6 到 PF11 这六根引脚要设置为 AF10(QUADSPI)复用功能,推挽输出、高速、带上拉。
最省事的方式是先用 CubeMX 配一遍引脚和外设,生成初始化代码后再把MX_QUADSPI_Init()搬回 RT-Thread 工程。注意 CubeMX 生成的代码默认假设了 HSE 时钟,H743 板子的外部晶振一般是 25MHz,如果 board 文件里配的是 8MHz,整个系统时钟都会偏,QSPI 的 40MHz 也全乱套。所以先把串口 printf 调通,确认 RT-Thread 启动日志里的节拍正常,再继续做屏。
4.2 引入 LVGL 软件包并调整配置
在工程配置里打开 RT-Thread Settings,在软件包中心找到 LVGL,勾选后同步到工程。版本建议先用 v8 系列,资料多,demo 齐全;v9 接口有调整,移植层要跟着改。LVGL 包几个关键配置:
- 颜色深度设置为 16 bit,与 ST77903 的 RGB565 对应;
- 打开 demo 示例,默认会带
lv_demo_widgets()等演示界面; - 内存管理使用 LVGL 自带的内存池,池大小建议配置到 64KB 以上;
- 勾选"软件定时器""GPU 加速"等选项按需开,开了不用反而占资源。
把st77903_qspi.c、lv_port_disp.c放进 applications 目录后,在 main 里按顺序初始化。RT-Thread 的 main 线程栈默认可能只有 4096 字节,LVGL demo 跑起来后栈压力大,建议把主线程栈调到 8192 或 16384,堆配置也一并核对,别让 LVGL 对象分配时直接返回 NULL。
4.3 编译烧录与验证
工程编译通过后,通过 SWD 调试器下载到板子。上电后串口应该先看到 RT-Thread 的启动 logo,然后跟着 LVGL 的版本打印和 demo 初始化日志。如果一切正常,屏幕会显示 LVGL 官方的 widget 示例界面,滑动、进度条、图表都能动起来。
验证链路是否畅通,一个小技巧是手动刷全屏渐变。初始化完成后直接向映射地址写一段颜色渐变的 buffer,如果渐变从上到下连续、无跳变,说明 QSPI 时钟和行偏移都对;如果出现斜向撕裂或者颜色断层,优先检查ClockPrescaler和MADCTL。确认屏的底子没问题,再跑 LVGL demo,这时候出现任何花色,都往 LVGL 层去查。
5. 避坑记录:白屏、花屏、撕裂和 HardFault 的排查顺序
5.1 白屏加串口无报错:先把 QSPI 时钟分频调大
现象:初始化日志正常打印,LVGL 也报准备好了,但屏幕一直白屏或者灰屏,用手电照能看到隐约有图像残影。
原因:白屏且日志正常,最常见的是 QSPI 时钟频率超过了 ST77903 的承受范围。初始化序列本身可能已经错了,但屏驱芯片对关键命令的时序有容忍度,只有写显存这种持续高速传输时才暴露。另一种常见原因是板子电平不匹配,IOVCC 接错电压会让屏逻辑接口直接不工作。
解决:先把ClockPrescaler从 4 改到 8,让 QSPI 时钟降到 22MHz 左右,重新烧录。如果白屏变正常,说明就是时钟过快。确认能用后,再把分频往小调一档一档提,直到出现花屏再回退一档,这就是这套板子能稳定跑的最高时钟。我一般会顺手在初始化末尾加一个读 ID 函数,读回来全 0xFF 就别再往上调时钟了。
5.2 颜色错乱:红色显示成蓝色,先查 RGB/BGR 交换
现象:LVGL 控件能正常显示,但所有颜色都偏色,红色和蓝色完全交换,绿色正常。
原因:ST77903 的MADCTL寄存器里有一个 BGR 位,决定像素接口按 RGB 还是 BGR 解释。屏厂模块出厂时可能已经把 BGR 位设过了,LVGL 那边又按默认的 RGB565 解释,两边一叠加就出了红蓝互换。
解决:两个地方都能改。底层的改法是初始化序列里给 0x36 寄存器加一字节0x08或0xC8(把 BGR 位置 1),具体按扫描方向组合;上层改法是打开 LVGL 的LV_COLOR_16_SWAP宏,让 LVGL 在输出前交换高低字节。我习惯先改屏侧寄存器,这样 LVGL 层保持标准配置,后面换 UI 代码不用跟着动。改完烧录看红色控件是否恢复正常,一次只改一边,别两边同时动,否则又变成另一种错乱。
5.3 刷新撕裂:动画时出现水平横带
现象:控件拖动时屏幕中间有一条不断移动的撕裂带,越靠近屏幕下部越明显,静止时完全正常。
原因:刷新回调在 ST77903 的 GRAM 写入和屏内部的行扫描不同步。LVGL 把新一帧画进 buffer 的时候,屏还在上一帧的扫描中,两帧画面同时显示就撕裂了。单缓冲时最容易出现,双缓冲只能缓解,不能根除。
解决:把 ST77903 的 TE 引脚接到 H743 的输入捕获或外部中断引脚,初始化序列里开启 TEON(0x35),等待 TE 信号进入 V-blanking 窗口后再调lv_disp_flush_ready()。这样 LVGL 只在屏不扫描的时候换帧。如果板子没接 TE,退而求其次是让刷新回调里整帧一次性写完,避免分多次小区域刷新,也能明显减少撕裂。
5.4 HardFault:进入映射模式后乱调 HAL_QSPI_Command
现象:程序在 LVGL 跑起来后随机进入 HardFault_Handler,有时刚进 demo 就崩,有时跑几个小时后才崩,错误调用栈指到 memcpy。
原因:映射模式激活期间,HAL_QSPI_Command 会去改 QUADSPI 的配置寄存器,这会把正在工作的外设打断,触发总线错误。另外如果向映射区写的地址越过了FlashSize配置的地址范围,也会直接 HardFault。320×480 的屏,偏移算到末尾时刚好卡在 512KB 边界上,一不小心就越界。
解决:强制约定一个规则——映射模式和间接模式严格互斥,驱动层封装一个st77903_switch_mode()函数,所有要发命令的地方先退出映射模式,发完再切回来。HAL_QSPI_Command()调用前统一走这个开关。另外把FlashSize按 2^19 算好,不要给 480×800 这种大分辨率屏留不够。从那以后,我每次调显示驱动都先检查一遍:映射模式下有没有人偷偷调命令函数。
5.5 整屏渐变显示但 LVGL 控件不刷新:检查 flush_ready 是否漏调用
现象:手动写渐变能正常显示,但 LVGL 创建控件后屏幕永远停在第一帧,再不动画。
原因:disp_flush_cb里拷贝完数据后没有调用lv_disp_flush_ready(drv),LVGL 一直认为上一帧还没发完,后续帧全部排队等待,表现为界面卡死在第一帧。这个坑在把裸机 LVGL 例程改成 RT-Thread 线程版时特别容易踩,因为拷贝动作移到了线程里,flush_ready 也跟着丢在了原函数外面。
解决:确保刷新回调的所有返回路径都执行lv_disp_flush_ready(drv)。如果有 QSPI 传输失败提前 return 的分支,也要在那里先 flush_ready。我把这个调用放在回调最末尾统一执行,任何中间跳转都要经过它。检查方法简单粗暴:在回调里加一个计数器,跑 demo 看串口计数是否持续增长。
6. 二次开发:改分辨率、加触摸和再把帧率往上顶一截
6.1 改分辨率和加触摸的联动点
把 320×480 换成 480×800 的型号时,要同步改四处:LCD_HOR_RES、LCD_VER_RES、FlashSize按新分辨率重算、以及初始化序列里的窗口相关寄存器。触摸板一般是 SPI 或 I2C 接口的 GT911 或 XPT2046 方案,接好中断引脚后在 LVGL 里注册一个indev驱动,read_cb里把触摸坐标填进lv_indev_data_t,顺手调lv_indev_drv_update()就行。这里最容易忽略的是触摸坐标的旋转方向要和屏的MADCTL方向一致,否则 UI 朝上,触摸却是横着的。
6.2 性能优化三板斧
第一,QSPI 时钟分频逐步调低,从 40MHz 往 50MHz 试,每档都要整帧渐变换色验证稳定性;第二,把 LVGL buffer 从 40 行加到 80 行,减少刷新回调次数;第三,数据搬运交给 DMA2D,disp_flush_cb里用 DMA2D 的 memory-to-memory 模式把color_p拷到 0x90000000,CPU 只在启动 DMA 和等中断时占用,传输过程可以干别的线程。DMA2D 地址要求 4 字节对齐,映射区地址没问题,LVGL 的 buffer 对齐需要留意,不行就在 lv_conf.h 里开LV_ATTRIBUTE_MEM_ALIGN。
6.3 一个收尾习惯
这套工程我后来在别的项目上也复用了几次,最大的习惯是:任何一次新屏上板,都强制走一遍"最低时钟 → 读 ID → 整屏渐变 → LVGL 刷图 → 加触摸"的顺序,每一步都在串口打日志,确认一步再走下一步。这样做能在一分钟之内定位到底是时钟、命令、方向还是适配层的问题,省掉了很多玄学排查时间。希望帮到你。
本文还有配套的精品资源,点击获取