资讯动态

STM32H743驱动ST77903 QSPI屏与LVGL移植排错

发布时间:2026/10/26 8:57:19 来源:尧图企业网站定制
简介面向嵌入式显示驱动开发者与智能穿戴产品工程师的ST77903驱动DEMO围绕矽创SITRONIX小体积LCD驱动IC展开运行于STM32H743平台集成RT-Thread实时系统、LVGL图形框架与QSPI接口可直接编译验证。由于ST77903内部不含显存QSPI必须保持连续传输工程专门开辟独立线程专职完成刷屏避免与UI主逻辑争抢总线是理解此类无RAM屏驱动架构与线程调度思路的典型参考。压缩包共2591个文件、约13.66MB以C源文件与头文件为主体1016个.c、1090个.h并配套链接脚本与分散加载文件、SConstruct/SConscript构建脚本、Kconfig配置项及少量PDF、Markdown说明与png素材层次覆盖底层驱动、板级移植到应用构建。DEMO内置三个手表表盘可直观观察LVGL界面在QSPI屏上的实际刷新表现。目前已有2592人学习下载适合需要快速搭建穿戴类GUI方案、研究刷屏线程与总线占用的开发者参考。1. ST77903 这块 QSPI 屏第一次点亮通常不是黑屏而是“花”ST77903 是 Sitronix 那颗走四线 QSPI 的 LCD 驱动 IC配 STM32H743 RT-Thread LVGL 这套组合第一现场往往不是完全不亮而是背光已经亮了、画面却像蒙了一层毛玻璃颜色发绿、边缘撕裂、刷两三秒直接进 HardFault。先别怀疑屏体坏了九成的问题出在 QSPI 时序、D-Cache 一致性或者 LVGL 的刷屏缓冲放错了内存区。这套组合在带屏的 H7 项目里出现频率极高——H743 主频够、片上 QUADSPI 外设现成、RT-Thread 有设备框架、LVGL 有成熟移植层凑在一起谁都想直接拿来用。它适合手上已经有一块 ST77903 屏、想把界面真正跑起来的人也适合之前只点过单色 SPI 屏、头一回碰四线 QSPI 的人。装不对不会给你任何报错只会让你在“到底是硬件还是软件”里耗掉一整周这段排查记下来能省你不少时间。2. 版本与依赖盘点H743 上这四个组件该锁哪个版本这套环境最容易出问题的环节其实是开工前那十分钟没把版本定下来。屏幕驱动对时序敏感RT-Thread 的设备框架在版本之间改过接口LVGL v8 和 v9 的移植层几乎是两套代码——任何一项版本随意变动都会让后面所有调试失去参照。2.1 四个组件各自的版本窗口我一般按下面这张表确定版本尤其两个项目并行时版本不一致会直接影响后面能不能复用调试经验。组件我通常锁的版本选择理由换版本的代价arm-none-eabi-gcc10.3对老 BSP 的启动汇编和链接脚本兼容性最好上 12/13 要处理一堆-Werror和 newlib 重定向RT-Thread4.1.x 稳妥新工程可上 5.xQSPI 设备框架已经成型rt_qspi_*接口够用Kconfig 选项名和drv_qspi.c里的 HAL 调用会变编译错误会集中在驱动层LVGLv8.3DEMO 移植层基本都按 v8 的lv_disp_drv_t写升 v9 要改lv_display_create、lv_draw_buf全套对接CubeMX / H7 HAL 包跟 BSP 自带.ioc同代引脚复用和时钟树能一键生成跨代升级 HALQSPI 的结构体字段名会跟着变LVGL 那条要特别说一句v9 把显示驱动抽象整个换掉了flush_cb的签名从(drv, area, color_p)变成带lv_display_t的一套缓冲区也从lv_disp_draw_buf_t换成lv_draw_buf_t。你手上这份 DEMO 如果是 v8 写的就老老实实锁 v8.3别一边调屏一边改 API那是给自己增加不必要的困难。2.2 工具链和构建依赖装到位RT-Thread 的 STM32 BSP 用 SCons 构建Linux 和 macOS 下先把这套装齐Windows 上直接用 MDK 也完全可以但menuconfig还是要有 Python 环境。# Debian/Ubuntu 上我常用的一套先确认基础工具在 sudo apt install -y git scons python3 python3-pip python3 -m pip install kconfiglib # 交叉工具链装完一定验证版本多个版本共存时 PATH 顺序会坑人 arm-none-eabi-gcc --version scons --versionkconfiglib是scons --menuconfig的依赖缺了它菜单打不开报错信息还挺隐晦。arm-none-eabi-gcc --version这一步不是形式主义——机器上同时装过两套工具链时which arm-none-eabi-gcc指到哪套决定了你编出来的是不是同一份固件。我一般还会顺手把arm-none-eabi-gcc -print-file-namelibc.a跑一遍确认链接时找得到 newlib。用 MDK 的话对应关系是AC6 编译器版本跟 ARM Compiler 包走RT-Thread 的scons --targetmdk5会重新生成工程文件此时你在 Keil 里手改的 include 路径会被覆盖。常见做法是把自定义路径写进SConscript别写在工程文件里。2.3 工程目录先分层别把 DEMO 直接解压进 BSP把 DEMO 解压到bsp/stm32/stm32h743-xxx/里是最省事也最贵的一步后面scons --dist打包、git status看改动都会一团糟。我习惯按职责分三层目录放什么谁维护bsp/stm32/stm32h743-xxx/板级初始化、时钟、rtconfig.h、CubeMX 生成的board/CubeMX_Config官方 BSP 自己改applications/lcd/ST77903 初始化序列、QSPI 屏驱动封装自己写applications/lvgl_port/flush_cb、lv_conf.h、LVGL 线程入口自己写LVGL 源码本体我不放进applications而是单独放一个packages/lvgl或者用 RT-Thread 的软件包管理拉下来版本写在pkgs --update的清单里。DEMO 里如果自带一份 LVGL 源码先看它的lv_version.h确认是不是 v8别跟自己项目里那份混用链接时会报重复符号而且报的位置跟真正的原因差得很远。3. QSPI 链路搭起来从 CubeMX 引脚到 rt_qspi_device屏能不能亮八成的功夫在这一章。思路是先让 RT-Thread 承认“总线上挂了个 QSPI 设备”再验证这个设备能往屏里写、能从屏里读最后才轮到 LVGL 往上画。顺序颠倒过来你会拿着一个花屏的界面去猜是驱动错了还是渲染错了。3.1 拿一个能跑的最小 BSP 打底先别急着接屏让板子能起 shell、能点灯。找一个引脚和时钟最接近的 H743 BSP 复制一份改比从零建工程快得多。# 以某个 H743 的 BSP 为底复制成自己的板级目录 cd rt-thread/bsp/stm32 cp -r stm32h743-xxx stm32h743-st77903 cd stm32h743-st77903 scons --menuconfig # 先只改晶振频率和串口别动别的 scons -j8 # 编出来烧进去确认 msh 能敲命令scons -j8里的-j是并行编译任务数按机器核数给H7 这种带一堆 HAL 源文件的工程单线程编要等很久。烧进去后串口能出msh /提示符说明时钟、串口、启动文件这三块是对的后面再出问题就能排除掉一大半。--menuconfig这一步只改两处RT_USING_SERIAL确认打开板子晶振BSP_USING_HSE_VALUE按实际值填。晶振值填错不会编译报错只会让串口波特率整体偏掉打印出来全是乱码——这是新手最容易误判成“串口线坏了”的地方。3.2 CubeMX 里把 QUADSPI 配成四线H743 的 QUADSPI 引脚是固定复用但具体落在哪组 GPIO、AF 号是几跟封装和板子走线强相关。以 CubeMX 里选中 QUADSPI 后自动带出的 AF 号为准别照抄别人板子的引脚表——我见过的翻车案例里有一半是把 CLK 和 NCS 的 AF 写反了结果是“能发数据但屏完全没反应”。关键参数就这几个在board/CubeMX_Config里重新生成后会被写进MX_QUADSPI_Init参数作用起步值调优方向ClockPrescalerQSPI 内核时钟分频先给 8 分频点亮稳定后逐级降到 2 分频FifoThreshold触发 DMA 请求的 FIFO 水位4刷大块像素时调到 16减少中断次数SampleShifting采样点偏移半个时钟高速时开HAL_QSPI_SAMPLE_SHIFTING_HALFCYCLE频率上到 50MHz 以上基本必开FlashSize24 位地址能寻到的空间2^N按屏 GRAM 大小对齐填小了高位地址会被截断ChipSelectHighTimeCS 拉高保持的时钟周期数2给不够会出现两条命令连在一起ClockMode空闲时时钟电平CPOL按屏手册配错表现为“偶尔能读到 ID偶尔读不到”前三个参数决定能不能通后三个决定稳不稳。我一般的顺序是先把ClockPrescaler拉到 8 分频、SampleShifting关掉确保能读到屏 ID再逐步往上提频率每提一级跑十分钟看有没有偶发花屏。这个“先降频再提频”的过程比较枯燥但比事后怀疑排线接触不良靠谱得多。3.3 让 RT-Thread 认出 qspi10 这个设备CubeMX 生成的只是 HAL 层的初始化RT-Thread 那边还要把 QUADSPI 注册成设备框架里的一个rt_qspi_device。BSP 的drv_qspi.c在打开BSP_USING_QSPI之后会把总线注册成qspi1设备名是qspi10。#include rtdevice.h #include drv_qspi.h static struct rt_qspi_device *qspi_dev RT_NULL; static int st77903_bus_init(void) { struct rt_qspi_configuration cfg {0}; qspi_dev (struct rt_qspi_device *)rt_qspi_device_find(qspi10); if (qspi_dev RT_NULL) { rt_kprintf(qspi10 not found, check BSP_USING_QSPI\n); return -RT_ERROR; } cfg.parent.mode RT_SPI_MODE_0 | RT_SPI_MSB; /* 屏手册多半是模式 0先照抄 */ cfg.parent.max_hz 50 * 1000 * 1000; /* 先给 50MHz稳了再往上抬 */ cfg.parent.data_width 8; cfg.qspi_dl_width 4; /* 数据线 4 位ST77903 的像素就是这么送的 */ cfg.medium.qspi_dl_width 4; return rt_qspi_configure(qspi_dev, cfg); }rt_qspi_device_find拿不到设备八成是BSP_USING_QSPI没开或者drv_qspi.c没被加进SConscript先在 msh 里敲list_device看总线上有没有对应条目。cfg.qspi_dl_width是数据线宽度配成 1 就退化成单线 SPI速度掉四倍但不报错特别容易在“怎么突然这么慢”的时候被忽略。真正发一条命令用rt_qspi_transfer_message把指令、地址、dummy 周期、数据长度填进struct rt_qspi_message里。指令码和地址位宽这几个字段直接照 DEMO 里的 QSPI 配置抄别自己按 flash 的习惯猜——QSPI 闪存的 0x02 是页编程屏的写命令虽然也用 0x02 附近的值但地址段含义完全不同猜错了就是“写进去了屏不动”。3.4 DMA 与 D-CacheH7 上最容易被忽略的一步H743 带 D-Cache这是好事也是坑的来源。CPU 写进缓冲区的像素数据先落在 Cache 里DMA 从内存搬的时候拿到的可能是旧数据表现就是画面局部错位、偶尔跳一块旧图。解决办法在发送前把对应区间清一次 Cache。/* 32 字节对齐由 Cache 行大小决定不对齐 clean 会连带邻居一起刷 */ static lv_color_t disp_buf[SCREEN_W * 40] __attribute__((aligned(32))); static void st77903_write_pixels(const void *data, uint32_t len) { /* 先把脏行推回 SRAMDMA 才看得到最新像素 */ SCB_CleanDCache_by_Addr((uint32_t *)data, len); st77903_qspi_send(data, len); }还有一条硬性限制DMA1/DMA2 访问不到 DTCM0x20000000 起始那块。如果你把disp_buf定义成普通全局变量链接器很可能就把它放在 DTCM 里DMA 搬过去全是 0屏上就是一整片纯色。常见做法是用链接脚本把它钉到 AXI SRAM0x24000000/* 链接脚本里加一段把刷屏缓冲单独放到 D1 域 */ .ram_d1 (NOLOAD) : { . ALIGN(32); *(.ram_d1) . ALIGN(32); } AXI_SRAM代码里配__attribute__((section(.ram_d1)))就能放进去。用 MDK 的话对应写法是__attribute__((at(0x24000000)))但要注意别跟其他变量地址冲突。这一步做完用SCB_InvalidateDCache_by_Addr在读回屏数据时也要配套调一次读写两侧都干净问题才不会反复。4. ST77903 屏参与 LVGL 对接flush 回调里要做对的三件事链路通了接下来才是“画得像不像”。这一层的症结几乎都集中在三处屏参没对齐、初始化序列组织乱了、flush_cb里少做了某一步。三处任意一处出错现象都长得差不多——花屏。4.1 屏参先对齐再谈显示屏的分辨率、色深、QSPI 线数以屏厂给的规格书和 DEMO 里的宏为准四个地方必须同步改漏一个就是错位参数改哪里不一致时的现象水平/垂直分辨率lv_conf.h的LV_HOR_RES_MAX、驱动里的SCREEN_W/H画面右半边或下半边内容错位色深LV_COLOR_DEPTH设 32 但屏只吃 RGB565颜色会整体偏字节序LV_COLOR_16_SWAP红蓝互换或者一片“马赛克”数据线宽cfg.qspi_dl_width能亮但刷新慢到肉眼可见LV_COLOR_16_SWAP这个宏特别容易懵它不改变 LVGL 内部的颜色结构只影响送出去时的字节顺序。屏接收 RGB565 时是高字节在前如果你发现红色显示成蓝色先别怀疑屏参把这个宏翻一下试试成本最低。4.2 初始化序列表怎么组织ST77903 的上电序列里有一堆厂商私有命令0xE0、0xF0 那一段这些值必须从 DEMO 或屏厂给的序列里逐条抄手写必错。我用一个结构体把“命令 参数 延时”打包成表主循环只负责遍历改参数的时候不用动逻辑。typedef struct { uint8_t cmd; uint8_t len; const uint8_t *data; uint16_t delay_ms; /* 这一条发完要等多久Sleep Out 之后必须等 */ } lcd_cmd_t; static const uint8_t para_1[] {0x00}; /* 占位实际值照 DEMO 的表填 */ static const lcd_cmd_t st77903_init_seq[] { {0x01, 0, RT_NULL, 120}, /* 软复位给足时间 */ {0x11, 0, RT_NULL, 120}, /* Sleep Out等 120ms 再继续 */ {0xF0, 1, para_1, 0}, /* 厂商私有命令段参数抄 DEMO */ {0x3A, 1, (const uint8_t[]){0x55}, 0},/* 像素格式 16bit按屏手册确认 */ {0x29, 0, RT_NULL, 20}, /* Display On */ }; void st77903_init(void) { for (uint32_t i 0; i sizeof(st77903_init_seq) / sizeof(lcd_cmd_t); i) { st77903_send_cmd(st77903_init_seq[i].cmd, st77903_init_seq[i].data, st77903_init_seq[i].len); if (st77903_init_seq[i].delay_ms) rt_thread_mdelay(st77903_init_seq[i].delay_ms); } }0x01、0x11、0x3A、0x29这些是 MIPI DCS 的标准命令各家屏都通用0xF0开头的厂商段才是各家不同。延时字段千万别为了“快一点”全填 0——Sleep Out 之后不等待就发 Display On屏会直接不理会后面的命令现象是“背光亮永远白屏”而且复位一次好一次非常让人困惑。初始化必须在 LVGL 起来之前跑完最好放在一个INIT_APP_EXPORT或者独立线程的最前面。放在main里屏还没上电就发命令也是偶发不亮的一个来源。4.3 flush_cb 的发送链路LVGL 每次渲染完一块区域就会回调flush_cb这里要做三件事开窗口、清 Cache、异步发数据。顺序不能乱。static void disp_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { uint32_t w lv_area_get_width(area); uint32_t h lv_area_get_height(area); uint32_t len w * h * sizeof(lv_color_t); /* 1. 先告诉屏这次要写哪块区域CASET / RASET / RAMWR 三连 */ st77903_set_window(area-x1, area-y1, area-x2, area-y2); /* 2. 脏行推回 SRAM否则 DMA 搬的是旧像素 */ SCB_CleanDCache_by_Addr((uint32_t *)color_p, len); /* 3. 异步发送DMA 完成回调里再调 lv_disp_flush_ready */ st77903_write_pixels_dma(color_p, len); }第三步是异步的所以flush_cb里不能立刻调lv_disp_flush_ready(drv)否则 LVGL 会以为这块画完了转头把同一片缓冲改成下一帧内容DMA 还在搬——这就是“画面偶尔撕裂”最典型的成因。正确位置在 DMA 传输完成的中断回调里调。LVGL v9 的话flush_cb签名换成lv_display_flush_cb_t参数是lv_display_t *和const lv_area_t *color_p要从lv_draw_buf里取逻辑一样接口不同。4.4 LVGL 的堆、刷新周期和线程LVGL 不是线程安全的所有lv_*调用必须在同一个线程里或者外面套互斥锁。RT-Thread 下我一般单独起一个线程栈给 8KB 起跑起来看list_thread里的栈水位再调。static void lvgl_thread_entry(void *param) { lv_init(); lv_port_disp_init(); /* 里面注册 disp_drv 和 flush_cb */ lv_port_indev_init(); /* 有触摸的话 */ while (1) { lv_timer_handler(); rt_thread_mdelay(5); /* 5ms 对应 200Hz 的定时粒度比这更快没意义 */ } }LV_MEM_SIZE在lv_conf.h里默认值往往是几十 KB一上图片和字体就LV_MEM分配失败现象是控件画到一半消失。带中文字体和大图的项目我一般直接给到 128KB 以上H743 的 AXI SRAM 有 512KB不缺这点。改完记得把lv_conf.h里的LV_MEM_ADR留空让它走内部数组自己指定地址反而容易和刷屏缓冲撞车。rt_thread_mdelay(5)这个值也不是随便写的给太小会让 LVGL 线程占满 CPU串口 shell 都卡给太大动画明显掉帧。5 到 10ms 之间调一调配合后面的帧率验证一起看。5. 排查记录ST77903 上电不亮、花屏、掉帧的五种翻车现场下面五条是我在这套组合上真实遇到过的按“现象 → 原因 → 解决”记遇到了直接对号入座比盲猜快得多。第一条背光亮屏全白复位一次好一次现象是上电后偶尔白屏按一下复位就正常冷启动概率性复现。原因基本是两个初始化序列里 Sleep Out 之后的延时被省掉了或者屏的 RESET 引脚在 RT-Thread 启动之前就已经被拉高等驱动跑起来时屏还处在复位后的不稳定期。解决方式是把复位脚显式控制一遍——拉低 10ms、拉高 120ms再开始发初始化命令同时把0x11后面的delay_ms补回 120。这条我踩过两次第二次是因为换了个线程优先级初始化提前跑了还是同一个坑。第二条能读到 ID但刷出来的画面整片偏移现象是通过 QSPI 回读寄存器一切正常一刷图就发现内容整体往右下偏了几十像素或者右边缘有一条重复的竖条。原因是CASET/RASET的窗口坐标写成了包含端点和不包含端点的混用——有的屏把结束坐标当“最后一个像素”有的当“后一个像素”。解决方式是先刷一个 10x10 的小方块到 (0,0)看实际画出来是 10 像素还是 11 像素再决定结束坐标要不要-1。窗口开错不会报错只会默默错位。第三条刷两三秒直接 HardFault现象是界面能跑几秒后进 HardFault看栈回溯落在一个和显示无关的地址上。两个高频原因disp_buf放在 DTCM 里DMA 搬不动还触发了总线错误或者 LVGL 缓冲区越界——lv_disp_draw_buf_init给的大小和实际数组长度对不上多刷几帧就踩到相邻变量。解决方式是先list_device和看 map 文件确认缓冲地址在 0x24000000 段再把sizeof(buf)用sizeof算而不是手写常量。HardFault 的栈回溯在 H7 上经常指错地方别死磕调用栈先查内存布局。第四条颜色对但边缘撕裂动一下界面就闪一条现象是静态画面正常一有动画就从中间撕开一条。原因是flush_cb里提前调了lv_disp_flush_ready或者只用了单缓冲。解决方式是把lv_disp_flush_ready挪进 DMA 完成回调同时改成双缓冲——两个disp_buf交替LVGL 在 DMA 搬一块的时候可以渲染另一块。双缓冲的代价是内存翻倍480 宽 40 行的缓冲两块也就几十 KBH743 扛得住。第五条帧率上不去滑动像放幻灯片现象是功能全对就是卡滑动列表明显掉帧。三个方向查QSPI 频率还在 8 分频没提上来FifoThreshold太小导致 DMA 中断过于频繁或者每帧都在全屏刷新。解决顺序是先提频率配合SampleShifting开半周期采样再把 FIFO 水位调到 16最后把刷新区域改成 partial 模式只刷脏区。我见过有人直接上 memory-mapped 模式从 0x90000000 写像素速度确实快但 MPU 要单独配成 Device 属性配错就是随机丢数据没跑稳之前别碰。6. 跑通验证从读 ID 到 8 小时长跑的三道关屏亮了不算跑通能稳定跑一晚上才算。我一般分三道关验每道关卡的判据都很硬过了就是过了。第一道关是链路验证不涉及任何图形。在 msh 里单独调一次读 ID把返回值打出来和规格书上的厂商码对一遍再读一次关键寄存器确认写入的值能回读一致。这一步过不了后面所有图形问题都不用查。/* 最小验证读 ID能对上说明指令段、地址位宽、dummy 周期都是对的 */ uint32_t id 0; st77903_read_reg(0x04, id, 3); /* 具体命令号按屏手册别照抄 */ rt_kprintf(st77903 id 0x%06x\n, id);第二道关是图形验证用不依赖 LVGL 的裸刷屏先画三样东西纯红、纯绿、纯蓝三个色块一条 0 到 255 的灰阶渐变条一个贴着屏幕四角的 1 像素边框。色块看颜色映射对不对渐变条看有没有色阶断层有断层多半是色深配错边框看窗口坐标和分辨率对不对。这三样都在flush_cb接进来之前跑能把屏参问题和 LVGL 问题彻底分开。第三道关是压力验证跑起来之后盯三个指标检查项通过标准怎么测帧率滑动列表时稳定在 30fps 以上在flush_cb里统计每秒回调次数内存list_thread里 LVGL 线程栈水位不持续上涨连续切页面 200 次后对比长稳8 小时不花屏、不进 HardFault循环播放动画串口挂日志长跑这一步别省。我做过的项目里有两个问题都是跑到四五个小时才出现的一个是 LVGL 里某个 label 反复重建没释放内存曲线缓慢爬升另一个是 QSPI 在温度上来之后时序余量不够偶发丢一帧。这两个问题在十分钟的测试里完全看不见一上产线就会出问题。从那以后我每次接新屏都强制自己把三道关走一遍尤其是第三关的长跑——哪怕工期再紧也留一晚上让板子自己跑着第二天看日志。前期多花的这一晚比后期在客户现场抓问题的成本低太多。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑