资讯动态

STM32H743 QSPI屏ST77903驱动与LVGL刷新优化

发布时间:2026/10/26 17:07:16 来源:尧图企业网站定制
简介这份工程面向采用矽创ST77903小尺寸LCD的嵌入式开发者围绕STM32H743、RT-Thread、LVGL与QSPI给出可运行的参考方案重点解决该IC内部无显存、必须由主机以专用线程持续刷屏的问题适合穿戴类屏幕驱动移植与联调。压缩包共2591个文件约13.66MB以C/H源码、汇编启动文件、SConscript构建脚本、链接与分散加载配置、Kconfig菜单及说明文档为主另含少量图片、PDF和固件文件便于按目录定位驱动、板级配置与构建入口。目前已有2592人学习下载。DEMO内置三套手表表盘可用于理解LVGL显示驱动接入、RT-Thread线程调度与QSPI连续传输的配合并参考工程组织快速搭建验证环境。1. 从一块 QSPI 屏说起ST77903 在 STM32H743 上的最小可跑通路径手里有一块 ST77903 LCD 屏主控是 STM32H743系统跑 RT-ThreadGUI 用 LVGL屏走 QSPI——这套组合听起来像把几个热门模块拼在一起真正跑起来却常卡在“能读 ID 但不亮”“能亮但花屏”“能显示但 LVGL 刷新像幻灯片”。ST77903 这套 DEMO 的价值不是再贴一份初始化序列而是把 QSPI 命令通道、RT-Thread 设备模型和 LVGL 刷新回调串成一条可复现的链路。适合已经能把 H743 点灯、但第一次碰 QSPI 屏的工程师也适合手里有类似屏模组、想确认驱动分层是否合理的老手。先别急着开双缓冲最小闭环跑通再谈帧率。2. 硬件与驱动选型QSPI 四线、DMA 和 ST77903 命令通道怎么对齐2.1 为什么 STM32H743 的 QSPI 外设适合 ST77903QSPI 屏最容易让人误判的地方是把 QSPI 当成“SPI 多两根数据线”。STM32H743 的 QUADSPI 外设确实能一次把 4 bit 数据推出去但 ST77903 的命令阶段和像素阶段线数并不总是一样初始化命令常见单线发命令、单线或四线发参数到了 RAMWR 才切四线传像素。驱动里如果从头到尾都按四线发读 ID 可能正常一写像素就花屏。选 H743 的理由很直接QSPI 时钟分频灵活、带 FIFO 和 DMA 请求、AXI SRAM 能做 DMA 缓冲但前提是先把总线模式、时钟和线数拆开配置而不是拿一份 SPI 屏代码改个设备名。这套 DEMO 里我建议把 QSPI 分成三个参数面看总线模式决定 CLK 极性/相位和位序时钟分频决定实际像素吞吐线数决定每个阶段怎么发。H743 的 QSPI 还支持内存映射模式但 LCD 不是 NOR Flash不需要把屏挂到地址空间里medium_size这类参数别照抄 Flash 配置。常见做法是先按 50MHz、SDR、模式 0、MSB 先出调通再逐级往上加频率一上来就冲最高时钟花屏和偶发卡死会混在一起排查成本很高。配置项调通阶段建议作用与注意点时钟分频先降到 50MHz 以内排除信号完整性和屏厂时序余量问题采样边沿Half cycle 起步H743 QSPI 采样点偏了会偶发读错FIFO 阈值8 或 16 字节太小中断多太大短命令等待变长CS 高电平时间至少 2 个 QSPI 时钟命令之间留够片选释放时间DDR 模式先关闭ST77903 这套 DEMO 按 SDR 配置更稳内存映射关闭LCD 只走命令/数据不走地址映射/* 板级 QSPI 参数按屏厂时序和实际布线填不要直接抄频率 */ static int st77903_qspi_init(void) { struct rt_qspi_configuration cfg {0}; rt_qspi_device *dev rt_qspi_device_find(qspi1); if (dev RT_NULL) return -RT_ERROR; cfg.parent.mode RT_SPI_MODE_0 | RT_SPI_MSB; /* 常见组合模式 0MSB 先出 */ cfg.parent.max_hz 50 * 1000 * 1000; /* 先 50MHz 调通稳定后再阶梯上调 */ cfg.parent.data_width 8; /* 命令和参数按 8bit 组织 */ cfg.medium_size 0; /* LCD 不做内存映射置 0 */ cfg.ddr_mode 0; /* 这套 DEMO 用 SDR */ cfg.qspi_lines 4; /* 像素阶段四线命令阶段在传输时另设 */ if (rt_qspi_configure(dev, cfg) ! RT_EOK) return -RT_ERROR; return RT_EOK; }这段代码只做总线级配置不碰具体命令。max_hz是上限不是每个阶段都跑这个频率qspi_lines是默认数据线数真正发命令时还要在rt_qspi_message里分别设置instruction.qspi_lines和data.qspi_lines。medium_size0很关键照抄 Flash 的容量会让 QSPI 驱动进入不该走的路径。实际调的时候先把max_hz降到 30MHz 或 50MHz等纯色图、渐变图都正常再以 10MHz 一档往上试别一次改三个参数。2.2 引脚、时钟与 RT-Thread 设备注册硬件检查比软件配置更值得先花半小时。QSPI 的 CLK、CS、D0D3 不是“连上就行”CS 要有上拉CLK 串联电阻按板子留D0D3 尽量等长复位和背光必须独立 GPIO 控制。ST77903 这类屏模组常见的翻车点是背光先开、复位还没释放或者复位低电平只有几毫秒屏内部电源没稳定后面初始化序列写得再对也白搭。RT-Thread 下先把 QSPI 设备名确认清楚qspi1、qspi10这种命名跟 BSP 有关别在代码里硬编码错设备。信号检查点常见错误CS上拉、片选释放时间多个设备共用 CS命令粘连CLK串联电阻、走线长度高频下波形过冲读 ID 偶发失败D0D3等长、不要跨电源分割四线像素传输花屏命令却正常RST独立 GPIO、低电平时间复位不充分初始化序列被忽略BL最后再开、PWM 可选白屏误判为驱动没跑/* 复位和背光单独用 GPIO别指望 QSPI 外设帮你拉 */ #define LCD_RST_PIN GET_PIN(B, 1) #define LCD_BL_PIN GET_PIN(B, 0) static void st77903_hw_reset(void) { rt_pin_mode(LCD_RST_PIN, PIN_MODE_OUTPUT); rt_pin_mode(LCD_BL_PIN, PIN_MODE_OUTPUT); rt_pin_write(LCD_BL_PIN, PIN_LOW); /* 初始化期间先关背光避免白屏误判 */ rt_pin_write(LCD_RST_PIN, PIN_HIGH); rt_thread_mdelay(10); rt_pin_write(LCD_RST_PIN, PIN_LOW); rt_thread_mdelay(20); /* 低电平时间按屏厂手册常见不短于 10ms */ rt_pin_write(LCD_RST_PIN, PIN_HIGH); rt_thread_mdelay(120); /* 等内部电源稳定常见 120ms 量级 */ } static void st77903_backlight(int on) { rt_pin_write(LCD_BL_PIN, on ? PIN_HIGH : PIN_LOW); }复位的逻辑是“先关背光、再拉复位、等电源稳定”不是简单拉低再拉高。rt_thread_mdelay(20)和120是常见量级不同屏模组要求不同以手头屏厂初始化说明为准。背光最后开的好处是如果初始化序列没跑完你看到的是黑屏而不是白屏排查方向会清楚很多。RT-Thread 下这些延时用线程延时不要在中断里调用初始化线程栈别太小QSPI 命令表和 LVGL 初始化都可能吃掉不少栈空间。设备注册完以后先写一个只读 ID 的小测试发读 ID 命令打印返回值确认 QSPI 命令通道是通的。这个测试不要带 LVGL也不要开 DMA纯轮询发一条短命令即可。读 ID 正常但显示不正常问题基本落在初始化序列、窗口坐标、像素格式或字节序读 ID 都不正常先回头查引脚、时钟、片选和线数。把这条分界线划清楚后面能省很多来回。2.3 上电时序与初始化序列的落地方式ST77903 的初始化序列不适合写成一大串if/else表驱动更好维护。每条记录包含命令、数据长度、数据指针和延时跑表时统一发命令、统一延时。常见 DCS 命令包括 Sleep Out、Memory Access Control、Pixel Format、Display On中间还会夹屏厂自己的电源、VCOM、Gamma 参数。这些值不能凭经验猜必须按屏模组附带的初始化表填DEMO 里最有价值的部分往往就是这张表而不是 QSPI 初始化那几行。typedef struct { uint8_t cmd; uint8_t data_len; const uint8_t *data; uint16_t delay_ms; } st77903_init_cmd_t; static const uint8_t madctl 0x00; /* 行列方向、RGB/BGR 按屏模组填 */ static const uint8_t colmod 0x55; /* RGB565 常见值仍以屏厂序列为准 */ static const st77903_init_cmd_t init_seq[] { {0x11, 0, RT_NULL, 120}, /* Sleep Out */ {0x36, 1, madctl, 0}, /* Memory Access Control */ {0x3A, 1, colmod, 0}, /* Pixel Format */ /* 中间省略屏厂 gamma、电源、VCOM 序列按 DEMO 的 table 补齐 */ {0x29, 0, RT_NULL, 20}, /* Display On */ }; static rt_err_t st77903_run_init_seq(void) { for (rt_size_t i 0; i sizeof(init_seq) / sizeof(init_seq[0]); i) { if (st77903_write_cmd(init_seq[i].cmd, init_seq[i].data, init_seq[i].data_len) ! RT_EOK) return -RT_ERROR; if (init_seq[i].delay_ms) rt_thread_mdelay(init_seq[i].delay_ms); } return RT_EOK; }st77903_write_cmd在第 3 章实现这里先按“命令 参数”的语义调用。madctl控制行列方向和 RGB/BGR 顺序colmod控制像素格式0x55是 RGB565 的常见取值但不同屏厂可能要求别的组合。表驱动的好处是改参数不用动流程坏处是容易漏延时Sleep Out 后面通常要等 120ms 量级Display On 后也要留一点时间。每次改初始化表只改一条跑一次纯色图不要把 gamma 和方向参数一起改否则出错时根本不知道是哪一行造成的。3. RT-Thread 驱动层命令、显存与刷新队列的拆法3.1 用设备模型封装命令通道与像素通道ST77903 的 QSPI 传输可以粗分成两类短命令和长像素。短命令要求低延迟、不能被打断长像素要求高吞吐、最好走 DMA。把两者都塞进一个“写屏”函数后面 LVGL 一刷新就会互相抢总线。更合理的拆法是底层只保留一个 QSPI 消息发送函数上层分别封装write_cmd和write_pixels用 RT-Thread 互斥量保护总线。命令阶段线数按屏厂要求设像素阶段固定四线这样调试时一眼能看出是哪类传输出了问题。static rt_mutex_t lcd_lock; static rt_err_t st77903_qspi_xfer(uint8_t cmd, const void *tx, rt_size_t len, uint8_t cmd_lines, uint8_t data_lines) { struct rt_qspi_message msg {0}; rt_qspi_device *dev rt_qspi_device_find(qspi1); if (dev RT_NULL || lcd_lock RT_NULL) return -RT_ERROR; msg.instruction.content cmd; msg.instruction.qspi_lines cmd_lines; /* 命令阶段常见 1 线 */ msg.address.size 0; msg.dummy_cycles 0; msg.data.buffer (void *)tx; msg.data.length len; msg.data.qspi_lines data_lines; /* 像素阶段 4 线 */ rt_mutex_take(lcd_lock, RT_WAITING_FOREVER); rt_err_t err rt_qspi_transfer_message(dev, msg); rt_mutex_release(lcd_lock); return err; } rt_err_t st77903_write_cmd(uint8_t cmd, const uint8_t *data, rt_size_t len) { return st77903_qspi_xfer(cmd, data, len, 1, len ? 1 : 0); } rt_err_t st77903_write_pixels(const void *buf, rt_size_t len) { return st77903_qspi_xfer(0x2C, buf, len, 1, 4); /* RAMWR像素走四线 */ }这段代码的重点是“一个底层传输、两个语义封装”。instruction.qspi_lines和data.qspi_lines分开设置命令可以单线像素可以四线0x2C是常见 RAMWR 命令具体命令字仍以 ST77903 手册为准。互斥量保护的是整个 QSPI 消息不是单条命令否则命令和像素之间还是可能插队。data.buffer强转掉const是 RT-Thread QSPI 接口的类型限制调用方要保证缓冲区在传输完成前不被改写。调通阶段可以把rt_qspi_transfer_message换成阻塞轮询稳定后再接 DMA。3.2 显存放在哪里内部 SRAM、AXI SRAM 与 SDRAM 的取舍H743 的内存域比较多最容易踩的坑是把 LVGL 缓冲放进 DTCM然后 DMA 怎么都不动。DTCM 速度快但很多 DMA 控制器访问不到AXI SRAM 适合做 DMA 缓冲外部 SDRAM 容量大但带宽和 cache 维护更麻烦。ST77903 这套 DEMO 如果只做局部刷新40 行双缓冲已经够用320×40×2×2 大约 51KB放 AXI SRAM 压力不大。想上全屏双缓冲320×480×2×2 接近 600KB就得考虑 SDRAM 或单缓冲加局部刷新。缓冲位置速度DMA 可用性适合场景DTCM快通常不可用CPU 查表、栈、非 DMA 数据AXI SRAM较快可用LVGL 局部双缓冲、QSPI DMA外部 SDRAM一般可用需配置全屏缓冲、多图层内部 SRAM 小块快部分可用命令参数、小临时缓冲/* LVGL 绘制缓冲必须能 DMA按 cache line 对齐 */ #define LCD_HOR_RES 320 #define LCD_VER_RES 480 #define LCD_BUF_LINES 40 static uint8_t lv_buf1[LCD_HOR_RES * LCD_BUF_LINES * 2] __attribute__((aligned(32))); static uint8_t lv_buf2[LCD_HOR_RES * LCD_BUF_LINES * 2] __attribute__((aligned(32)));LCD_BUF_LINES是每个缓冲覆盖的行数40 行对应约 25.6KB双缓冲约 51.2KBaligned(32)是为了配合 Cortex-M7 的 cache line减少 DMA 前后的维护开销。如果编译后发现缓冲被放到 DTCM检查链接脚本和段属性别只看变量定义。放 SDRAM 时要确认 MPU 属性和 cache 策略否则 CPU 写进去的数据可能还在 cache 里DMA 读到的还是旧内容。调通阶段可以先用单缓冲确认窗口和颜色都对再换双缓冲。3.3 刷新队列与 DMA 完成回调LVGL 的flush_cb不是“把像素写完再返回”的地方。它跑在 LVGL 的任务上下文里如果里面直接等 QSPI DMA 完成整个 GUI 线程会被拖住动画和触摸都会卡。更稳的做法是flush_cb只把区域坐标和缓冲指针投递到消息队列由独立的 LCD 刷新线程做窗口设置和像素传输DMA 完成中断释放信号量刷新线程拿到信号后再调用lv_disp_flush_ready。这样 LVGL 的绘制和屏的传输能重叠起来。typedef struct { uint16_t x1, y1, x2, y2; const uint8_t *data; } lcd_area_t; static struct rt_messagequeue lcd_mq; static char lcd_mq_pool[8 * sizeof(lcd_area_t)]; static rt_sem_t lcd_dma_done; static void lcd_flush_task(void *arg) { lcd_area_t area; while (1) { if (rt_mq_recv(lcd_mq, area, sizeof(area), RT_WAITING_FOREVER) RT_EOK) { st77903_set_window(area.x1, area.y1, area.x2, area.y2); st77903_write_pixels(area.data, (area.x2 - area.x1 1) * (area.y2 - area.y1 1) * 2); /* 若用 DMA这里等完成信号量裸轮询只适合调通阶段 */ rt_sem_take(lcd_dma_done, RT_WAITING_FOREVER); lv_disp_flush_ready(disp_drv); } } }队列深度 8 是经验值刷新区域多、动画快的时候可以加到 16队列太深会增加延迟太浅会在flush_cb里阻塞。lcd_area_t只保存坐标和指针不拷贝像素所以必须保证 LVGL 缓冲在 DMA 完成前不被下一帧覆盖双缓冲就是为这个服务的。lcd_dma_done在 QSPI DMA 完成中断里释放中断里不要调用会阻塞的 API。调通阶段可以先把rt_sem_take去掉改成轮询等待 QSPI 忙标志确认链路没问题后再接 DMA 和信号量。4. LVGL 显示移植flush_cb、窗口坐标与 RGB565 字节序4.1 LVGL 显示驱动注册的最小闭环LVGL 移植最容易写成“能显示就行”的一团代码后面加触摸、改分辨率、换缓冲都很痛苦。最小闭环其实只有四件事初始化绘制缓冲、填显示参数、注册flush_cb、提供 tick。绘制缓冲的第三个参数是像素数不是字节数RGB565 下一个像素 2 字节别把sizeof混进去。full_refresh先关局部刷新能让 ST77903 的 QSPI 带宽压力小很多等局部刷新稳定再考虑全屏刷新或双缓冲。static lv_disp_draw_buf_t draw_buf; static lv_disp_drv_t disp_drv; static void lvgl_port_init(void) { lv_disp_draw_buf_init(draw_buf, lv_buf1, lv_buf2, LCD_HOR_RES * LCD_BUF_LINES); lv_disp_drv_init(disp_drv); disp_drv.hor_res LCD_HOR_RES; disp_drv.ver_res LCD_VER_RES; disp_drv.flush_cb st77903_flush_cb; disp_drv.draw_buf draw_buf; disp_drv.full_refresh 0; /* 先局部刷新别一上来全屏 */ lv_disp_drv_register(disp_drv); }lv_disp_draw_buf_init的第三个参数是LCD_HOR_RES * LCD_BUF_LINES个像素和缓冲区字节数不是一回事full_refresh0表示 LVGL 只把脏区域交给flush_cb。这段代码是 LVGL 8.x 风格如果工程用 LVGL 9.xAPI 会变成lv_display_create那一套但“缓冲、分辨率、flush 回调、tick”四件事不变。hor_res和ver_res必须和 ST77903 实际行列方向一致MADCTL 里改了方向这里也要跟着改否则触摸坐标和显示坐标会对不上。4.2 flush_cb 里把 area 转成 ST77903 窗口flush_cb收到的area是包含端点的坐标宽度是x2 - x1 1高度同理。ST77903 设置窗口通常要发 Column Address Set 和 Row Address Set再发 RAMWR 写像素。坐标端点如果少加 1会出现最后一列或最后一行不刷新坐标顺序如果和 MADCTL 不一致图像会整体偏移。RGB565 的字节序也要在这里确认STM32 是小端LVGL 默认像素排列和屏厂要求不一定一致颜色偏蓝或偏红时先查这个不要急着改 gamma。static void st77903_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { lcd_area_t req { .x1 area-x1, .y1 area-y1, .x2 area-x2, .y2 area-y2, .data (const uint8_t *)color_p, }; /* 只投递不在这里等 DMADMA 完成后再调用 flush_ready */ if (rt_mq_send(lcd_mq, req, sizeof(req)) ! RT_EOK) { /* 队列满时退化为同步避免丢帧造成花屏 */ st77903_set_window(req.x1, req.y1, req.x2, req.y2); st77903_write_pixels(req.data, (req.x2 - req.x1 1) * (req.y2 - req.y1 1) * 2); rt_sem_take(lcd_dma_done, RT_WAITING_FOREVER); lv_disp_flush_ready(drv); } }队列满时退化为同步是“后悔药”宁可这一帧慢一点也不要把缓冲指针直接丢掉。st77903_set_window内部要按 ST77903 的命令格式发0x2A、0x2B再发0x2C等像素这些命令的参数长度和字节序以手册为准。color_p指向 LVGL 的绘制缓冲RGB565 下每像素 2 字节传输长度就是宽 × 高 × 2。如果显示颜色明显不对先试 LVGL 的LV_COLOR_16_SWAP配置或者在发送前手动交换高低字节别一上来就改屏厂初始化表。4.3 刷新率与局部刷新什么时候该开双缓冲以 320×480、RGB565 为例一帧像素约 300KB。QSPI 四线在 50MHz 下的理论带宽是 25MB/s 量级实际受命令开销、DMA 启动、总线竞争影响能到 1218MB/s 就不错全屏一帧大约 1725ms。单缓冲局部刷新在 GUI 场景里通常够用因为 LVGL 只传脏区域但动画大面积刷新时单缓冲会出现“绘制下一帧时上一帧还没发完”的撕裂。双缓冲能缓解撕裂代价是内存翻倍而且 flush 完成回调的时序要更严谨。方案内存占用撕裂适合场景单缓冲 40 行约 25.6KB大区域动画可能撕裂首版调通、静态页面双缓冲 40 行约 51.2KB明显改善常规 GUI、列表滑动全屏单缓冲约 300KB刷新期间可能可见内存充足的 SDRAM 方案全屏双缓冲约 600KB较好但带宽压力大高帧率动画、视频帧static void lvgl_tick_thread(void *arg) { while (1) { lv_tick_inc(2); /* 2ms tick按 LVGL 配置的 tick 周期改 */ rt_thread_mdelay(2); } }tick 线程的周期要和 LVGL 内部配置一致lv_tick_inc(2)配 2ms 延时是常见组合不要把这个线程优先级设得比 LCD 刷新线程还高否则它会和 QSPI 传输抢 CPU。局部刷新阶段先把LCD_BUF_LINES设为 2040 行观察列表滑动和大面积重绘的表现再决定要不要加到 80 行或改双缓冲。QSPI 时钟也是同样思路50MHz 稳定后再试 65MHz、80MHz每次只改一个变量跑十分钟看有没有偶发花屏。5. 避坑与排查ST77903 QSPI 屏最容易翻车的五处5.1 现象白屏但读 ID 正常现象是上电本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑