首页 / 资讯中心 / 文章详情

STM32H743+QSPI驱动ST77903:RT-Thread与LVGL显示方案详解

STM32H743+QSPI驱动ST77903:RT-Thread与LVGL显示方案详解 ★ FEATURED ARTICLE
简介矽创ST77903 DEMO基于STM32H743平台运行RT-Thread实时系统并采用LVGL图形库面向穿戴设备与小型LCD驱动开发者。该芯片为SITRONIX小体积IC内部无RAMQSPI必须连续传输工程在HOST侧创建专门线程负责刷屏示例内置3个手表表盘演示无显存屏的驱动与UI整合。压缩包为7z共2591个文件、约13.66MB其中C源码和头文件合计超2100个另含汇编启动文件、链接脚本、SConscript构建脚本、IAR工程配置、Kconfig配置及文档图片目录结构清晰。该DEMO已有2585人学习下载。读者可从QSPI写入时序、刷屏线程调度、LVGL接口适配、多表盘界面组织中借鉴实现并移植到其他主控或屏驱方案是ST77903项目实用的参考。1. 一块屏点不亮问题往往不在屏本身做嵌入式显示的同学大概率有过这种经历新屏上板电源正常、背光亮了、像素时钟也有了但屏幕上要么是一片白要么是雪花噪点查了半天发现是初始化寄存器序列里某个延时不对或者 QSPI 的读写时序跟驱动 IC 的预期对不上。这个 ST77903 的 Demo 项目解决的就是这么一件事——用一块 STM32H743 通过 QSPI 接口驱动矽创 ST77903 这块 LCD 驱动 IC跑 RT-Thread 系统再叠一层 LVGL 图形库把界面画出来。整套环境是 H743 RT-Thread LVGL QSPI 的组合适合要在自己的板子上快速点亮同类 LCD 屏、又不想从头啃 datasheet 和驱动时序的开发者。它能帮你把「屏亮了」到「界面能交互」这条链路完整跑通省掉最折磨人的点亮阶段。2. QSPI 驱动方案的架构剖析为什么不用并口ST77903 是一颗分辨率支持到 480x854 级别的 LCD 驱动芯片常见的接法有两种一是传统的 MCU 并口8080 或 6800 时序数据线七八根起步还得配上 RD/WR/CS/RS 控制线二是 SPI/QSPI 串行接口引脚少、布线干净。这个 Demo 选的是 QSPI也就是四线 SPI一个时钟四根数据线DMA 搬运数据之后 CPU 基本可以撒手不管。2.1 外设资源分配与引脚规划整个系统跑在 STM32H743 上RT-Thread 作为实时操作系统内核LVGL 跑在它上面做图形渲染。看这个 Demo 的工程外设分配大概是这么个格局QSPI 挂在 QUADSPI 外设上使用 Bank1映射到 AHB 地址空间LCD 的 DC 脚数据/命令选择由普通 GPIO 控制因为 QSPI 协议本身不区分命令和数据得靠一个额外的引脚来告诉 ST77903 当前发的是命令还是数据背光 PWM 走一个定时器通道亮度可调触摸如果有的话挂在 I2C 或 SPI 上这个 Demo 主体是显示触摸不是重点。引脚规划的细节值得提一句QSPI 的四根数据线IO0-IO3、时钟线CLK、片选线CS必须保证布线等长高频下时序偏差才不会太离谱。H743 的 QUADSPI 引脚是固定的几个映射位置比如 PF8-PF10 是 QSPI 的一组映射不同封装引脚不一样选型时先查数据手册确认你用的封装有没有把 QSPI 引脚引出来。这个坑我在别的项目里踩过——芯片选了 100 脚的封装结果 QSPI 的四个 IO 被其他功能占掉了最后只能改方案走普通 SPI。2.2 目录结构从驱动到应用的层次划分这个 Demo 的工程目录沿用了 RT-Thread Studio 的标准结构但加了 LCD 相关的几个关键目录层大致如下project ├── applications │ ├── main.c // 入口创建 GUI 线程 │ └── lv_demo/ │ ├── lv_demo_main.c // Demo 页面逻辑 │ └── lv_demo_assets/ // 图片、字体资源 ├── drivers │ ├── qspi_if.c // H743 QUADSPI 底层读写封装 │ ├── st77903.c // ST77903 初始化、背光控制、显存配置 │ └── lcd_port.c // LVGL 的 flush 回调、画布底层对接 ├── rt-thread │ └── components │ └── drivers │ └── spi │ └── qspi.c // RT-Thread QSPI 设备驱动框架 └── packages └── LVGL // LVGL 源码包这个分层思路是RT-Thread 的设备框架负责把 QSPI 抽象成设备qspi_if.c 实现底层的读写函数st77903.c 调这些函数做初始化序列最上层 lcd_port.c 把 LVGL 的绘制请求转换成往 ST77903 显存里写像素。每一层只干一件事调试的时候能快速定位是底层读写问题还是上层渲染问题。2.3 QSPI 协议下 ST77903 的命令与数据封装ST77903 工作在 QSPI 模式时传输的格式分为命令阶段和数据阶段。QSPI 控制器发命令的时候四个数据线全部走命令内容发数据的时候四个线并行传输数据字节。用 QUADSPI 外设时典型操作是这么组织的先通过 QUADSPI 的通信配置寄存器设置传输模式命令阶段用单线 SPI 模式发指令码数据阶段用四线 QSPI 模式写参数。这里有个关键点ST77903 的 QSPI 模式下发命令和发数据靠 DC 引脚区分QSPI 外设本身不感知这个区别。所以驱动里每次操作都是同一个套路——先拉低 DC 脚写命令字节再拉高 DC 脚写参数数据。如果 DC 脚电平搞反了命令和数据互换了那结果就是屏幕花屏或者完全没反应而逻辑分析仪上看到的波形又好像是对的这个坑排查起来很玄学。3. ST77903 初始化时序拆解一帧图像的完整生命周期屏要亮起来初始化顺序不能乱。ST77903 从上电到正常显示中间要经过硬件复位、电源设置、显示模式配置、显存写入、背光开启这么几个阶段。这个 Demo 的 st77903.c 文件把初始化序列整理成了一个命令表结构每一行是一条命令加参数配合延时控制这种方式的好处是后期调整参数时不用改代码流程只改表就行。3.1 初始化命令表的组织方式命令表结构在 Demo 里大概是这样的typedef struct { uint8_t cmd; // 命令码 uint8_t data_len; // 参数长度 uint8_t *data; // 参数数组指针 uint16_t delay_ms; // 命令发送后的延时 } lcd_init_cmd_t; static const uint8_t st77903_power_seq[] {0x01, 0x02, 0x03}; static const uint8_t st77903_gamma_seq[] {0x10, 0x20, 0x30, 0x40}; static const lcd_init_cmd_t st77903_init_sequence[] { {0x11, 0, NULL, 120}, // Sleep Out {0xB0, 3, st77903_power_seq, 10}, // 电源控制 {0x36, 1, (uint8_t[]){0x00}, 0}, // 扫描方向 {0x3A, 1, (uint8_t[]){0x66}, 0}, // 像素格式 RGB666 {0xB6, 2, (uint8_t[]){0x0A, 0x82}, 0}, // 显示功能控制 {0x29, 0, NULL, 20}, // Display ON };每个命令发送函数会遍历这张表逐条调用 qspi_write_cmd() 发送。命令码 0x11 是 Sleep Out让芯片从睡眠模式唤醒0x36 设置 RGB 扫描方向和颜色顺序这个寄存器决定图像的旋转方向0x3A 设置像素格式0x66 表示 R、G、B 各 6 位也就是 RGB666正好符合 ST77903 支持的最大并行位数。延时的作用最早我在写驱动时没太在意后来发现少一个延时屏幕就是花的。ST77903 内部有 DC-DC 电源电路切换工作模式后需要时间等待内部稳压器稳定这个延时写短了后续命令在电源未稳定时被忽略表现就是寄存器写入了但从示波器上看到的 DC 脚波形又都对。所以这个命令表里必须保留延时字段上电初期几个关键节点的延时宁多勿少。3.2 QSPI 读写函数的核心实现底层读写函数是驱动的地基看这段代码uint8_t qspi_write_cmd(uint8_t cmd, uint8_t *data, uint32_t len) { QSPIDrv_Handle.Command cmd; QSPIDrv_Handle.Address 0; QSPIDrv_Handle.DataSize len; QSPIDrv_Handle.AddressSize 0; QSPIDrv_Handle.DummyCycles 0; QSPIDrv_Handle.InstructionMode HAL_QSPI_INSTRUCTION_1_LINE; QSPIDrv_Handle.AddressMode HAL_QSPI_ADDRESS_NONE; QSPIDrv_Handle.DataMode (len 1) ? HAL_QSPI_DATA_4_LINES : HAL_QSPI_DATA_1_LINE; QSPIDrv_Handle.NbData len; HAL_QSPI_Command(hqspi, QSPIDrv_Handle, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Transmit(hqspi, data, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); return 0; }这段代码的关键在 QSPIDrv_Handle 的配置上InstructionMode 设成单线模式发命令码DataMode 根据参数长度自动选择四线或单线。LCD 命令一般只有一两个字节走四线反而浪费所以这里做了一个分支。AddressMode 设为 NONE 是因为 ST77903 的显存访问走的是连续的窗口地址不需要单独发地址阶段。参数说明hqspi 是初始化好的 QUADSPI 句柄HAL_QSPI_TIMEOUT_DEFAULT_VALUE 是超时值一般用默认的 1000ms。注意 HAL_QSPI_Command 和 HAL_QSPI_Transmit 是两个独立调用Command 负责把命令码和传输参数配置好Transmit 才是真正启动传输。有同事把这个流程写成先 Transmit 后 Command结果数据全被当成命令发出去屏幕直接花掉。3.3 显存地址窗口的配置写像素前先划区域ST77903 内部有 GRAM访问它需要先设置访问窗口范围。每次刷屏或者 LVGL 局部刷新时驱动会把窗口设成需要更新的那块区域然后连续写像素数据。窗口设置命令是整个显示流程里调用频率最高的部分void st77903_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { uint8_t x0h (x0 8) 0xFF, x0l x0 0xFF; uint8_t x1h (x1 8) 0xFF, x1l x1 0xFF; uint8_t y0h (y0 8) 0xFF, y0l y0 0xFF; uint8_t y1h (y1 8) 0xFF, y1l y1 0xFF; uint8_t col_data[4] {x0h, x0l, x1h, x1l}; uint8_t row_data[4] {y0h, y0l, y1h, y1l}; qspi_write_cmd(0x2A, col_data, 4); // 列地址设置 qspi_write_cmd(0x2B, row_data, 4); // 行地址设置 qspi_write_cmd(0x2C, NULL, 0); // 准备写入 GRAM }0x2A 是列地址寄存器0x2B 是行地址寄存器0x2C 是显存写命令。窗口范围按照高字节在前、低字节在后的顺序传进去刚好覆盖到 480x854 的最大分辨率。设置完窗口后连续向 QSPI 写像素数据即可芯片会自动把数据填入窗口并滚动指针。要注意的是 0x2C 这条命令在 ST77903 上可以不带数据直接发芯片进入写显存状态后后续传输的全部字节都按像素数据处理。这个设计在 LVGL flush 时特别有用可以一口气刷一大堆数据而不用每条都重复设置窗口吞吐量能翻好几倍。4. LVGL 移植落地一张图像的渲染路径LVGL 在本项目里跑在 RT-Thread 上通过软件定时器驱动它的 tick 心跳显示部分对接 st77903 驱动输入设备暂时用不到。整体链路是LVGL 的渲染引擎计算完一帧画面后调用用户注册的 flush 回调把像素数据交给底层驱动写入显存QSPI DMA 完成实际搬运。4.1 flush 回调与画布缓冲的实现LVGL 的显示驱动结构体 lv_disp_drv_t 里需要注册 flush_cb核心代码长这样static void lcd_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { st77903_set_window(area-x1, area-y1, area-x2, area-y2); qspi_write_pixels((uint8_t *)color_p, area-x2 - area-x1 1, area-y2 - area-y1 1); lv_disp_flush_ready(drv); }area 结构体保存了 LVGL 需要更新的矩形区域坐标color_p 是指向像素缓冲区的指针。flush 回调里先设置显存窗口到目标区域然后把整个区域的像素一次性写入 QSPI。lv_disp_flush_ready 必须在数据写完后再调用它通知 LVGL 这块缓冲区可以回收复用了。如果提前调用LVGL 可能会在 DMA 还没搬完数据时复用这块 buffer画面上就会出现撕裂或闪烁。像素格式这块需要对齐LVGL 编译时需要把 LV_COLOR_DEPTH 设成 16 或 24取决于采用的 RGB565 还是 RGB888。Demo 里 ST77903 配的是 RGB666但 LVGL 的颜色深度不可能设 18常见做法是 LVGL 用 RGB565传输给 ST77903 之前做一次转换。如果直接写 RGB565 数据到 ST77903 的 RGB666 模式里颜色会偏色而且偏得很奇怪。这个的解决思路是 ST77903 支持 RGB565 输入格式把像素格式寄存器改成 0x55RGB565两边就对齐了性能还比 RGB666 高。4.2 双缓冲设计用空间换流畅度LVGL 可以配单缓冲也可以配双缓冲这个 Demo 用的是双缓冲模式static lv_color_t buf1[MY_DISP_HOR_RES * 40]; static lv_color_t buf2[MY_DISP_HOR_RES * 40]; lv_disp_draw_buf_init(draw_buf, buf1, buf2, MY_DISP_HOR_RES * 40);buf1 和 buf2 分成两个相同大小的 bufferLVGL 在绘制 buf1 里的内容时DMA 可以在搬 buf2 里的已绘制数据。两个 buffer 交替使用渲染和传输流水线化画面刷新率明显提升。行数取 40 的意思是每次渲染 40 行像素大约占整个屏幕高度的 1/12虽然不算大但对 QSPI 传输来说已经是足够大的一块连续数据。若内存允许把 buffer 高度放大到全屏的一半甚至整屏刷新率还能再上一个台阶。但 H743 的内部 RAM 就那么大还得给 RT-Thread 的任务栈和 LVGL 的堆预留空间所以双缓冲的高度选择是个折中。用 DMA 传输像素数据时buffer 地址要注意 32 位对齐否则 DMA 配置直接报错这是 H7 系列一个容易翻车的细节。4.3 RT-Thread 环境下的 LVGL 心跳与线程模型LVGL 需要一个周期性的心跳来计时动画和任务调度在裸机环境下通常用一个定时器中断。在 RT-Thread 里可以借助软件定时器的形式实现static rt_timer_t lvgl_tick_timer; static void lvgl_tick_handler(void *parameter) { lv_tick_inc(1); } void lvgl_port_init(void) { lv_init(); lvgl_tick_timer rt_timer_create(lvtick, lvgl_tick_handler, RT_NULL, 1, RT_TIMER_FLAG_PERIODIC | RT_TIMER_FLAG_SOFT_TIMER); rt_timer_start(lvgl_tick_timer); }每个系统 tick 调用一次 lv_tick_inc(1)LVGL 内部就会维护自己的时间基准动画和输入设备的轮询都倚赖这个时间源。这里用软定时器而不是硬定时器好处是不会阻塞中断上下文。tick 间隔取 1ms适配 LVGL 默认的 LV_DISP_DEF_REFR_PERIOD 配置。界面渲染线程的写法也很值得看一下void gui_thread_entry(void *parameter) { lvgl_port_init(); lv_demo_main(); while (1) { lv_timer_handler(); rt_thread_mdelay(5); } }lv_timer_handler() 是 LVGL 的核心循环函数它处理所有待执行的任务——包括动画、对象刷新、输入事件。每 5ms 调用一次正好匹配 LVGL 推荐的 20fps 左右的刷新节奏。mdelay 5ms 会让出 CPU给其他 RT-Thread 任务留出运行时间不至于因为 GUI 线程死循环把整个系统卡死。5. 实战避坑记录三个屏幕问题的排查全过程跑这种显示项目屏幕能亮跟屏幕稳定显示之间差了十万八千里。整理几个在这类项目里反复出现的坑每条都是真实调试路径的复盘希望能帮你省点时间。5.1 现象一上电后屏幕全白初始化命令像没发出去原因QSPI 的 IO 引脚没配置成复用功能或者复用功能编号选错了。H743 的每个 PF 引脚可以映射到多个外设如果 GPIO 配置里没把 AF 号选到 QUADSPI 对应的那个那 QUADSPI 发数据时引脚根本不响应。解决检查 GPIO 初始化代码确认 AF 号与芯片参考手册里的 QUADSPI 映射表一致。AF 号码跟引脚的物理位置无关同一个 QUADSPI_CLK 在不同引脚上的 AF 编号可能是不同的。一个验证办法是先把 QSPI 速度降到最低比如 2MHz 分频用逻辑分析仪抓一下 CLK 线有没有波形——如果有波形那基本是时序问题如果没波形就是引脚配置问题。5.2 现象二图像正常但偏色红色和蓝色互换原因像素格式寄存器 0x3A 里的格式位选错。ST77903 支持 RGB565、RGB666 等多种格式但控制位在不同版本芯片上的含义有差异。有时候寄存器写的是 0x66RGB666LVGL 那边给的是 RGB565 数据两边一错位颜色通道就乱了。解决要么把 LVGL 的 LV_COLOR_DEPTH 改成 16 并配套把 ST77903 设成 RGB565要么保持 RGB666 并把 LVGL 的 LV_COLOR_DEPTH 配成 24。最省事的做法是统一成 RGB565。改完记得同时检查 0x36 寄存器里的 RGB 顺序位BGR 还是 RGB有的屏是 BGR 排列数据一样但颜色定义不同表现同样是偏色。5.3 现象三LVGL 动画一开就闪烁画面隔几帧撕裂一次原因单缓冲模式下 LVGL 边渲染边显示DMA 传输和渲染引擎同时访问同一个 buffer互相干扰。另一个原因是 flush 回调里没有等 DMA 完成就调用了 lv_disp_flush_ready。解决至少启用双缓冲让渲染和传输并行。flush 回调里等待 DMA 传输完成可以用 HAL_QSPI_GetState 轮询状态位或者用 DMA 的中断标志。确认传输完成后才调用 lv_disp_flush_ready这是一个很隐蔽但后果很严重的问题——时序差那么一点点表现出来的就是满屏闪烁而这跟 QSPI 的时钟频率没有关系。6. 二次开发思路从 Demo 到可交付产品的三个改造点这个 Demo 跑通之后离一个真正能交付的产品还有一段路要走经验和踩坑的价值往往体现在这些改造上。第一个改造点是分辨率适配。ST77903 支持的分辨率和扫描方向取决于屏厂的具体面板切割不同的屏模组可能同样的 IC 配不同的初始化参数。拿到新屏之后不要直接复用命令表先找屏厂要一份出厂初始化代码把命令表里的差异参数替换掉尤其是伽马曲线相关的寄存器组跟屏厂的调色结果直接挂钩。命令表设计成数组的好处在这里就体现出来了——改参数不用改逻辑。第二个改造点是动画性能的验证方法。LVGL 的流畅度跟 QSPI 的时钟频率和 DMA 的传输效率强相关可以在工程里加一个帧率统计的 hook 函数在 lv_timer_handler() 里每隔一秒统计一次刷新帧数对比不同 QSPI 分频系数下的帧率。我一般会用这个方式去验证 QSPI 时钟频率的选型是否合理。H743 的 QUADSPI 最高可以跑到 100MHz 以上但 PCB 布线质量不够的话高频就跑不稳帧率测试能让你快速定位是布线瓶颈还是芯片瓶颈。第三个改造点是耗时操作的任务化。现在的 GUI 线程里如果直接处理大文件的读取或者复杂的计算LVGL 的动画就会卡顿。RT-Thread 的优势这时候就能发挥出来把耗时操作放到独立线程里执行GUI 线程保持轻载。这个 Demo 可以作为多线程 GUI 架构的起点模板把 LVGL 的任务优先级安排在低于关键实时任务的级别避免界面卡顿影响到底层控制逻辑。做完这几个改造再回头看这个 Demo其实套路清楚了也就没那么神秘QSPI 负责搬运ST77903 负责显示LVGL 负责画图RT-Thread 负责调度。从那以后我每接到一个带屏的新项目都会先按这个架构走一遍——先点亮屏幕再跑 LVGL Demo最后才做功能逻辑这套流程帮我少走了很多弯路希望也帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站