简介:面向STM32H750的嵌入式开发者,该资源包围绕三线SPI通信、RGB屏幕显示与LVGL图形库的集成,提供了从底层驱动到上层界面的完整裸机工程,重点解决三线SPI单向写屏与RGB屏刷新之间的协同问题。内容包含LTDC显示控制器驱动、480×480 RGB屏初始化配置、LVGL V8.2移植源码、窗口/按钮/文本等UI元素示例,以及编译烧录脚本与Keil工程模板;同时附带了音乐播放器Demo的图片素材,便于直观理解LVGL在RGB屏上的页面切换与动画效果。压缩包共1924个文件,以C/H源码、编译中间文件、Python辅助脚本、Markdown技术文档、PNG/JPG图片素材为主,整体约83.17MB,目录分层明确,可按驱动、中间层、应用示例顺序阅读。目前已有100人学习下载,特别适合中高性能MCU彩色界面开发入门者,以及需要验证STM32H750与LVGL兼容性的项目工程师,可将其作为工程起点直接复用驱动层与LVGL配置。
1. STM32H750 上跑三线 SPI + RGB 屏 + LVGL,先别急着吐槽“有 LTDC 不用”
把 STM32H750 的 LTDC 放着不用,偏偏用三线 SPI 去推一块 RGB 屏,第一眼确实像降级。但如果你手上的项目是 2.4~4.3 寸的仪器面板、充电桩 HMI、桌面小摆件,对帧率要求没那么苛刻,三线 SPI + RGB 屏的方案就很实际:屏体便宜、货期好找,连接上只需要 SCK、SDI、CS 再加背光和复位,总共四五根线,PCB 布线压力和 MCU 引脚占用都比并口 RGB 小一大截。配合 LVGL 做界面,渲染交给 Cortex-M7,像素搬运交给 SPI DMA,H750 跑 320×240 的界面 20~30 帧是够用的。这篇文章就是把这条链路从硬件选型、CubeMX 初始化、LVGL 移植到踩坑全部拆开讲,照着一遍能跑通,也能让你在画板子之前先算清楚性能账。
2. 三线 SPI RGB 屏的带宽账:哪些分辨率能上,哪些一算就死
2.1 三线 SPI 是怎么把 RGB 像素送进屏幕的
先把这个概念钉死,因为很多人把“三线 SPI + RGB 屏”和“SPI 接口的 MCU 屏”混为一谈。常规的 ST7789/ILI9341 这类屏,内部有一个 TFT 控制器,SPI 负责写寄存器、写显存,控制器再把显存内容扫到玻璃上,你可以随时改一个小窗口。而标题里的 RGB 屏不一样,它没有完整控制器,驱动器接收的是你已经排好的像素流,内部只做行缓存和点阵扫描,后级的 DE/HSYNC/VSYNC 时序由驱动器自己生成,MCU 这边只需要按约定速率喂像素数据。三线 SPI 这种 RGB 屏,把“喂像素”这件事压缩成一根 SDI 数据线加一根 SCK 时钟,命令和像素全部从同一根线进去。
这根数据线的速度就是整个系统的天花板。H750 的 SPI 外设本身能跑到很高,但屏体驱动器、PCB 走线、连接器接触都会限制实际可用时钟。我一般先按 30~40MHz 做预算,而不是直接拉满 60MHz。这个方案的本质是用串行带宽换引脚数和成本,屏幕尺寸一大、分辨率一高,第一道坎就在带宽,不是 LVGL 也不是 DMA。
2.2 带宽公式与分辨率选型:320×240 是个坎
像素流量的计算很直接:SPI 有效数据速率除以每像素位数,得到每秒能送进去的像素数,再除以屏幕分辨率就是帧率上限。公式写作:
- 帧率上限 = SPI 时钟 × 传输效率 /(宽 × 高 × 单像素位数)
- 三线 SPI 没有额外握手,传输效率基本可以按 1 算,但别忘了 DMA 启动和帧与帧之间的间隔不是零。
以 320×240 RGB565 为例,单像素 16 位。40Mbps 下每秒 2.5M 像素,屏幕总共 7.68 万像素,帧率上限约 32 帧。看起来够用,但 LVGL 的脏矩形刷新、动画、DMA 间隙都会吃掉一部分带宽,实际体感会在 25 帧上下。480×272 同配置下上限不到 19 帧,已经能看出明显掉帧。800×480 这种分辨率在三线 SPI 下基本可以不考虑,除非你只刷局部区域,否则数据量摆在那里。
我把常见分辨率的预算列成一个表,方便画板子前对照:
| 分辨率 | 颜色格式 | SPI 时钟 | 理论帧率上限 | 落地建议 |
|---|---|---|---|---|
| 240×240 | RGB565 | 40MHz | 43 fps | 很宽裕,可以开动画 |
| 320×240 | RGB565 | 40MHz | 32 fps | 够用,动画要克制 |
| 480×272 | RGB565 | 40MHz | 19 fps | 勉强,只能做静态界面 |
| 480×272 | RGB666 | 40MHz | 15 fps | 不推荐 |
| 800×480 | RGB565 | 40MHz | 6.5 fps | 直接否决 |
如果你拿到的屏支持 RGB666,三个颜色通道分开送,带宽就更紧张;一般这种三线 SPI 屏跑 LVGL 都是固定 RGB565,LVGL 那边LV_COLOR_DEPTH配 16 位,两边正好对上。分辨率选型的核心结论是:这个方案适合 320×240 及以下,再大就要么降刷新意图,要么换 RGB 并口/LTDC。
2.3 引脚与电路:SPI 信号线、背光、复位怎么接
三线 SPI 屏的典型引脚是 SCK、SDI、CS、RST、BLK,再加上电源和 LED 背光。注意它不是标准四线 SPI 的 MISO 概念,很多屏只有一根 SDI 做双向,回读数据时这根线要临时切方向,这个细节放到第 5 章避坑里展开。电路上我一般这样做:
- SCK 和 SDI 串 22Ω 电阻,放在 MCU 一侧,配合驱动器输入引脚做阻抗匹配,能明显减少反射造成的毛刺;
- CS 用普通 GPIO 控制,不用 SPI 硬件的 NSS。原因是三线屏的 CS 拉低时机要和 SCK 第一个沿严格对齐,软件 CS 想什么时候拉就什么时候拉,排查时序问题也更直接;
- 背光 BLK 用一个 PWM 通道,既能调亮度,也能在初始化没完成之前保持熄灭,避免上电瞬间闪白屏;
- RST 不要直接并到 MCU 复位,单独 GPIO 控制,让它比主控晚一点释放。
这里有一个硬件片选和软件片选的选择问题,很多人习惯用SPI_NSS_HARD。三线 SPI + RGB 屏这类外设,我从来都用软件 CS,因为硬件 NSS 在 SPI 总线空闲时电平行为受NSSP位影响,不同厂家的屏驱动器对 CS 电平要求不一致,软件 CS 可以彻底绕过这个坑。PCB 走线方面,SCK 和 SDI 尽量短、等长,远离电感类器件,走线不要跨分割;连接器用排针时尤其小心,线一长速度一高,花屏概率直线上升。
3. CubeMX 初始化 H750 的 SPI DMA:最小工程与回读验证
3.1 SPI 外设与 GPIO 的配置
在 STM32CubeIDE 里建工程,先配 RCC,确保 H750 的主频跑到 480MHz,再把 SPI1 放在 APB2 上,这样 SPI 时钟来源是 120MHz,分频后才有足够的余量。用 CubeMX 生成工程时,SPI 参数我通常这样设置,用一个基础配置示例:
// SPI1 初始化参数(CubeMX 生成的 HAL 配置核心部分) hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; // 主机模式 hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 全双工,三线屏只用 MOSI 和 SCK hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // 空闲时 SCK 为低 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // 第一个边沿采样 hspi1.Init.NSS = SPI_NSS_SOFT; // 软件片选 hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 120MHz/4 = 30MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1);这段配置里有两个点要说明。一是SPI_DIRECTION_2LINES即使屏没有 MISO,也建议保留下行通道,因为回读 ID 时 RX 引脚和 TX 引脚是同一根物理线,HAL 的收发都需要这个模式。二是时钟极性和相位,绝大多数三线 RGB 屏驱动器用模式 0(空闲低、第一个沿采样),但不排除个别屏要求模式 3,这个一定以你拿到的数据手册时序图为准,不同批次屏的驱动器可能不一样,这也是后面回读验证要做的事。
GPIO 部分,CS、RST、BLK 全部设为GPIO_MODE_OUTPUT_PP,初始电平分别为高、低、低。SCK 和 SDI 在 CubeMX 里分配到 SPI1 的复用功能后不需要手动配置。下一步别忘了在MX_DMA_Init里把 SPI1_TX 的 DMA 请求打开,通道选择默认的DMA1_Stream0即可,方向设成内存到外设,数据宽度字节,优先级中。
3.2 屏幕复位时序与初始化序列发送
复位时序是“屏幕点不亮”的第一大原因。正常的做法是上电后先等电源稳定,然后 RST 拉低至少 10ms,再拉高,接着等 120ms 让驱动器内部晶振起来。H750 主频高,用 HAL 的HAL_Delay需要先把 SysTick 配成 1ms 中断,否则延时全部不准。初始化序列每家屏厂给的不一样,一般是一个“寄存器地址 + 数据”的列表,我习惯把它做成const uint8_t数组,按长度分段发送:
#define RST_PIN_Pin GPIO_PIN_4 #define RST_PIN_GPIO_Port GPIOB #define CS_PIN_Pin GPIO_PIN_5 #define CS_PIN_GPIO_Port GPIOB #define BLK_PIN_Pin GPIO_PIN_6 #define BLK_PIN_GPIO_Port GPIOB static void lcd_reset(void) { HAL_GPIO_WritePin(RST_PIN_GPIO_Port, RST_PIN_Pin, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(RST_PIN_GPIO_Port, RST_PIN_Pin, GPIO_PIN_SET); HAL_Delay(150); // 给驱动器留足启动时间 } static void lcd_init_sequence(const uint8_t *cmd_seq, uint32_t len) { HAL_GPIO_WritePin(CS_PIN_GPIO_Port, CS_PIN_Pin, GPIO_PIN_RESET); // 拉低片选 HAL_SPI_Transmit(&hspi1, (uint8_t *)cmd_seq, len, 1000); while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY); HAL_GPIO_WritePin(CS_PIN_GPIO_Port, CS_PIN_Pin, GPIO_PIN_SET); // 拉高片选 }s这里用HAL_SPI_Transmit阻塞发送初始化序列没问题,序列总共也就几十个字节。真正重要的是HAL_SPI_GetState轮询等传输结束,否则下一次片选拉高时 SCK 可能还在补最后的尾巴,驱动器把残留字节当成命令吃掉。初始化序列做完后先不急着开背光,等回读验证通过再点灯,不然白屏时很难判断是背光问题还是时序问题。
3.3 用回读 ID 验证三线链路
初始化之后第一件事不是跑 LVGL,而是回读屏的型号 ID。绝大多数带 SPI 接口的 RGB 屏驱动器支持0x04之类的读命令,数据手册里会给出 ID 寄存器的值和回读时序。这里要处理的就是三线屏的痛处:SDI 在发送完成后要临时切到输入模式,否则读出来的全是 0xFF。
static uint8_t lcd_read_id(void) { uint8_t cmd = 0x04; // 读 ID 命令,按屏驱动手册填写 uint8_t id = 0; HAL_GPIO_WritePin(CS_PIN_GPIO_Port, CS_PIN_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, &cmd, 1, 100); while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY); // 三线屏回读:把 SDI 引脚临时切到输入模式 GPIO_InitTypeDef gpio_cfg = {0}; gpio_cfg.Pin = SPI1_MOSI_Pin; gpio_cfg.Mode = GPIO_MODE_INPUT; gpio_cfg.Pull = GPIO_PULLUP; HAL_GPIO_Init(SPI1_MOSI_GPIO_Port, &gpio_cfg); HAL_SPI_Receive(&hspi1, &id, 1, 100); while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY); // 恢复成 SPI 复用功能 gpio_cfg.Mode = GPIO_MODE_AF_PP; gpio_cfg.Alternate = GPIO_AF5_SPI1; HAL_GPIO_Init(SPI1_MOSI_GPIO_Port, &gpio_cfg); HAL_GPIO_WritePin(CS_PIN_GPIO_Port, CS_PIN_Pin, GPIO_PIN_SET); return id; }这里GPIO_AF5_SPI1是 H750 上 SPI1 的复用编号,具体要看数据手册的 AF 映射表,不同引脚可能不同。回读的时序有个坑:屏驱动器在读命令后第一个时钟沿是采样还是输出,必须对着数据手册的时序图数清楚,有些屏需要先发一个 dummy 字节再真正读数。如果读回来的 ID 和手册不一致,不要急着改代码,先用逻辑分析仪抓一下 CS、SCK、SDI 三根线的实际波形,确认数据位顺序、时钟极性和命令字节是否对齐,这步能排除掉八成的前期接线问题。
4. LVGL 移植到 H750:从 flush 回调到 DMA 双缓冲
4.1 先解决最关键的问题:SPI RGB 屏没有窗口概念
LVGL 默认的 flush 设计是给带窗口控制器的屏准备的,比如 ST7789 可以设置坐标窗口,LVGL 只把脏矩形区域发给它。但三线 SPI RGB 屏不行,它没有 GRAM,也没有窗口寄存器,只能按像素顺序一帧一帧吃数据。如果直接把 LVGL 默认的 flush 回调套上来,屏幕会花得没法看,因为 LVGL 每次只给一小块区域,驱动器却把这一小块当成一整帧的头部来扫描。
解决方案简单粗暴:把 LVGL 的显示缓冲设置为全屏大小,flush 回调里无视area参数,每次都把整个缓冲区发出去。这样 LVGL 在缓冲区里画完整个画面,flush 时一次送全帧,驱动器和屏幕的时序天然对齐。代价是每一帧哪怕只改了一个像素,也要整帧重传,刷新率完全受限于 2.2 节算出来的带宽。这个取舍是值得的,因为三线 SPI RGB 屏的本身定位就是数据量可控的小分辨率屏。
实现上,lv_conf.h里把分辨率定义成实际屏幕尺寸,例如 320×240,颜色深度 16 位,然后在主程序里分配一个全屏大小的 draw buffer。H750 内部 RAM 有 512KB,320×240×2 = 150KB,一个整帧缓冲放内部 RAM 没问题;如果要跑双缓冲就吃紧,所以先把单缓冲跑通再谈优化。
4.2 flush 回调写法和 DMA 完成处理
LVGL 8.3 的 flush 回调签名建立在lv_disp_drv_t上,DMA 传输的逻辑用一个忙碌标志和一个完成回调来管理:
static volatile int dma_busy = 0; static void disp_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { // SPI RGB 屏没有窗口,整帧刷新,不解析 area uint32_t len = DISP_HOR_RES * DISP_VER_RES; while (dma_busy) { __NOP(); // 等待上一次 DMA 彻底结束,再交 buffer 所有权 } dma_busy = 1; HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)color_p, len * 2); } void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { dma_busy = 0; lv_disp_flush_ready(&disp_drv); // 告诉 LVGL 缓冲可以复用了 } }color_p指向的就是 LVGL 的整个 draw buffer,len * 2是因为 RGB565 每像素两个字节。lv_disp_flush_ready要不要放在 DMA 中断回调里?我建议放,而不是放在主循环轮询里,因为 HAL 的 SPI DMA 完成中断里调用它是安全的,LVGL 会在下一次lv_timer_handler里重新绘制脏区域。dma_busy的等待放在 flush 开头能防止一种情况:上一次 DMA 还没发完,LVGL 已经把新内容画进同一块 buffer,导致屏幕出现上半帧旧图、下半帧新图的撕裂。
如果用的是双缓冲,这个等待可以省略一半,LVGL 会在两个 buffer 间交替,但你要保证 DMA 读的是上一块、LVGL 画的是当前块,这需要lv_disp_drv_t里的buf_act判断,逻辑比单缓冲绕。我的经验是先把单缓冲跑起来,确认 SPI 链路没问题,再考虑加第二个缓冲和 SDRAM,不然撕裂和花屏混在一起,排查难度翻倍。
4.3 LVGL 8.3 与 9.6 的移植差异和 UI 细节
看到这,你可能会直接参考网上 8.3 的教程,但不少新拿到的官方面板例程已经切到 LVGL 9.6。两版改动不小,简单列一下移植时最容易撞上的差异:
| 关注点 | LVGL 8.3 | LVGL 9.6 |
|---|---|---|
| 显示器对象 | lv_disp_drv_t disp_drv | lv_display_t *disp |
| 注册方式 | lv_disp_drv_register(&disp_drv) | lv_display_create(w, h) |
| flush 回调 | disp_drv.flush_cb = disp_flush_cb | lv_display_set_flush_cb(disp, disp_flush_cb) |
| 缓冲注册 | lv_disp_draw_buf_init(&draw_buf, buf, NULL, px_cnt) | lv_display_set_buffers(disp, buf, NULL, px_cnt, LV_DISPLAY_RENDER_MODE_DIRECT) |
| flush 完成通知 | lv_disp_flush_ready(drv) | lv_display_flush_ready(disp) |
如果是新工程,直接上 9.6 更省事,8.3 的一些旧 API 在 CubeMX 集成包里已经逐渐被替换。9.6 的LV_DISPLAY_RENDER_MODE_DIRECT模式非常适合三线 SPI 屏,它强制 LVGL 直接渲染到全屏 buffer,不会走中间层,正好匹配“没有窗口”的屏幕特性。
UI 层面,容器和布局用lv_obj_set_layout(obj, LV_LAYOUT_FLEX)或LV_LAYOUT_GRID,比手动lv_obj_set_pos靠谱得多,分辨率一旦调整布局不会乱。中文显示是避免不了的,LVGL 默认字体不带中文,常见做法是把 ttf 字体通过字体转换工具生成 C 数组,只勾选你用到的几百个字,全量字库会让 flash 和 RAM 都吃不消。动画方面,lv_anim和过渡动画在 320×240 上可以开,但每个动画都会增加脏矩形面积,SPI 带宽有限,动画多了帧率会掉到肉眼可见卡顿,我一般只在当前页面保留一个控件动画。另外,如果在 FreeRTOS 里跑 LVGL,建议把lv_timer_handler放到独立任务,优先级不要高过 SPI 中断,DMA 完成回调里不要直接调用可能阻塞的 HAL 函数,有冲突就用信号量通知任务去调lv_display_flush_ready。
5. 避坑:三线 SPI RGB 屏最常翻车的 5 个现场
5.1 花屏和横条纹:SCK 太快还是线太长
现象是屏幕能亮,但显示内容出现细密横纹或者斜纹,静态画面也花,动起来更严重。第一次遇到时我第一反应是初始化序列错了,反复检查寄存器列表无果,最后用逻辑分析仪抓 SCK,发现波形上升沿有明显振铃,20cm 长的杜邦线在 40MHz 下根本扛不住。
原因是信号完整性,不是代码逻辑。SCK 频率高,走线长,阻抗不连续,反射信号在驱动器采样点附近造成误判。解决方法是把 SPI 预分频从 2 降到 4,时钟从 60MHz 降到 30MHz,同时 SCK 和 SDI 串上 22Ω 电阻;如果是杜邦线连接临时调试,尽量缩短线长,或者改用排线并加磁环。降频后横纹消失,再考虑逐步提频,直到找到可靠临界点。这块板子布板和排线质量,决定了你能跑多快,不能只看 MCU 数据手册上的 SPI 上限。
5.2 白屏无响应:复位时序与背光互斥
现象是上电后屏幕纯白,背光亮,但怎么发数据都没反应。最常见的原因是复位时序不对。三线 RGB 屏对 RST 脉冲宽度和释放后等待时间有硬性要求,很多屏要求 RST 拉低至少 10ms,有的要 20ms,拉高后还要等 120ms 以上。如果主控跑得快,HAL_Delay(1)配成 1ms 确实没问题,但你用了HAL_Delay(10)实际只延时了几毫秒,驱动器没完成启动,命令全丢。
另一个被忽略的点是背光。我遇到过一款屏,BLK 引脚在高阻态和低电平时背光都亮,只有在正常驱动时才受控,结果初始化序列没跑完就点亮,白屏暴露无遗。解决方法是把背光放到初始化序列全部走完、回读 ID 验证通过之后才拉高,并且确认 BLK 引脚没有浮空。排查顺序建议是:先量 RST 波形,再量 CS 片选是否正常拉低,最后用逻辑分析仪看 SCK 上有没有数据,一层层排除。
5.3 三线模式回读失败:SDI 方向没切换
现象是发完读命令后收回来的一直是 0x00 或 0xFF,和手册标称 ID 对不上。这不是 SPI 外设坏了,是三线屏的物理层决定了 SDI 是双向的,你在发送命令时它被配置成了复用推挽输出,读完命令必须切到输入模式,否则驱动器输出的数据字节被你自己的输出引脚拉死。
解决方法是参照第 3.3 节的代码,在HAL_SPI_Transmit结束后把 SDI 的 GPIO 模式切成GPIO_MODE_INPUT,接收完再切回复用功能。这里有个容易忽视的时序点:方向切换和时钟沿是对不齐的,驱动器在命令字节最后一个位之后会继续驱动 SDI,切输入这个动作要发生在 CS 保持低电平期间,所以切换代码不能放在片选拉高之后。建议对着数据手册的读时序图数清楚,哪个边沿是命令结束、哪个边沿开始返回数据,比盲目改代码高效得多。
5.4 LVGL 动画撕裂:缓冲区的所有权交接
现象是静态界面正常,一开动画,屏幕中间出现一条水平分界线,上面是上一帧、下面是这一帧,上下错位。前面 4.2 节里dma_busy等待就是为了防这个问题,但有些时候等待放在 flush 开头还不够,因为 LVGL 9.6 的LV_DISPLAY_RENDER_MODE_DIRECT模式下,lv_display_flush_ready返回后 LVGL 可能立刻开始渲染下一帧,而 DMA 还在读 buffer 的尾部。
解决思路是把“DMA 真正结束”和“LVGL 拿到 buffer 所有权”同步起来,回调用lv_display_flush_ready本身没错,但前提是dma_busy在 DMA 完全停止后才清零,不要在 DMA 传输一半去清。HAL 的SPI_TxCpltCallback是传输完成中断,到这里 DMA 已经搬完,所以回调里清标志是可靠的。另外,如果用了双缓冲,LVGL 会在两个 buffer 间切换,撕裂风险反而小,但内存占用翻倍;单缓冲加一个标志是代价最小的抗撕裂方案。
5.5 卡顿但不是 SPI 的锅:H750 的时钟树和缓存没开
现象是 SPI 明明已经降到 30MHz,带宽算出来应该能跑 25 帧以上,结果 LVGL 只有不到 10 帧,操作界面明显拖沓。这时候别急着怀疑屏,先查 H750 的时钟树和 Cache。H750 的SystemClock_Config如果没配置对,CPU 可能跑在 16MHz 内部时钟而不是 480MHz,整个 LVGL 渲染就成了瓶颈。
再一个就是 TCM 和 Cache 的配置。LVGL 的 draw buffer 如果放在普通 SRAM,而SCB_EnableICache/SCB_EnableDCache没开,M7 的 CPU 每次访问内存都要等主频,渲染速度大打折扣。开启 D-Cache 后要注意 DMA 和 CPU 的一致性,发送前可以调用SCB_CleanDCache把 buffer 内容刷到内存,避免 DMA 读到过期的缓存数据。顺序是先确认SystemCoreClock,再检查 Cache,最后才怀疑 SPI 分频。这些不是玄学,是 H750 这种高主频芯片的必经步骤。
6. 性能验证:用逻辑分析仪测刷新率,再决定要不要上 SDRAM
6.1 一帧刷新时间怎么测
代码跑通之后,第一步不是看画面好不好看,而是量化帧率。最简单有效的方法是在 flush 回调里翻转一个空闲 GPIO,用逻辑分析仪量它的周期。GPIO 翻转的位置放在HAL_SPI_Transmit_DMA之前拉高,在 DMA 完成回调里拉低,这样 GPIO 高电平的宽度就是实际 SPI 搬运一帧像素数据的时间。
// 性能测试:在 D2 上输出帧同步信号 static void disp_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // 帧起始,拉高 // ... 发送逻辑 ... } void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // 帧结束,拉低 // ... } }测出来的高电平时间乘以帧率,就是当前 SPI 时钟下的真实刷新上限。比如 320×240 RGB565,高电平实测 42ms,意味着 SPI 有效带宽只有 17.4Mbps,远低于 30MHz 的理论值。这时候检查是不是HAL_SPI_Transmit_DMA之间有等待间隙,或者dma_busy阻塞了 LVGL 的渲染调度,别急着加 SDRAM,先把已有带宽榨干。用这个 GPIO 测法还能验证不同预分频下的实际效果,比盯着数据手册算理论值靠谱得多。
6.2 后续优化的两个方向
如果测量结果卡在 20 帧附近,想再往上提,常见的做法有两个方向。一是把 draw buffer 换成双缓冲并放到外部 SDRAM,H750 的 FMC 接口接 SDRAM 成本不高,但要注意 D-Cache 和 SDRAM 的交互,flush 前做 Cache Clean;二是换 RGB666 转 RGB565 的转换方式,有些屏驱动支持压缩格式或者低色深模式,能和 LVGL 的颜色深度配置对齐,省掉中间转换过程。
最后提一个我自己的教训:不要在项目一开始就追求“满配”,H750 的 LTDC 和三线 SPI RGB 屏是两条路线,前者适合高分辨率高帧率,后者牺牲性能换引脚和成本。选三线 SPI 之前先把第 2 章的带宽表过一遍,确认你要跑的分辨率和帧率在表里能站住脚,再画板子写代码。这套链路调通一次之后,后续换屏型号基本就是改初始化序列和回读 ID 的事,真正的硬骨头永远是带宽、信号完整性和缓冲区交接。希望帮到你。
本文还有配套的精品资源,点击获取