简介这份资源是面向嵌入式开发者的STM32F103 SPI屏幕LVGL移植工程适合已掌握C语言与基本外设驱动、希望快速搭建图形界面的中初级工程师。它解决的是从零配置SPI通信、适配屏幕驱动到集成LVGL库的完整链路问题可直接用于小尺寸TFT屏的GUI项目开发。压缩包共761个文件约3.77MB以189个c源文件与186个h头文件为核心配合57个mk构建脚本、112个o编译产物及若干bin固件、png图片与md说明覆盖LVGL源码、屏幕驱动、帧缓冲管理与工程配置。资源中已包含可用的移植代码与示例界面读者可参考SPI时钟分频、极性相位设置、命令与数据发送函数、颜色深度与分辨率配置等关键实现并借助串口调试与断点排查思路快速定位问题。目前已有4227人学习下载适合作为STM32F103图形界面开发的实践参考。1. 从一块“点不亮”的 SPI 屏说起这套 LVGL 移植程序到底能省你多少事如果你手上有一块 STM32F103 最小系统板外加一块 SPI 接口的 TFT 彩屏大概率经历过这样的场景照着某份教程把引脚接好烧进去一个“点亮测试”屏幕要么全白、要么花屏、要么干脆没反应。更别提后面还要跑 LVGL——那个带按钮、滑块、动画的图形库。很多人卡在“底层驱动通了但 LVGL 刷不出画面”这一步反复怀疑是 SPI 时序问题、显存不够、还是 DMA 配置错了。这套 STM32F103 SPI 屏幕移植程序就是冲着这个痛点来的。它把 SPI 屏的底层初始化、刷屏接口、以及 LVGL 的显示与输入对接层都写好了自写、可用不是那种只贴关键片段的半成品。你拿到手改几个引脚定义和屏幕分辨率就能在 F103 上跑起 LVGL 的 demo。适合谁适合已经会点 STM32 标准库或 HAL 库、想快速验证 LVGL 在低端 M3 上到底能不能跑、以及想省掉“从零写 SPI 刷屏驱动”那两三天折腾的人。LVGL 在 STM32F103 上跑核心矛盾从来不是“能不能跑”而是“刷屏够不够快、内存够不够用”这套程序把这两个问题的答案都摆在了代码里。2. 先看透 F103 跑 LVGL 的硬件账SPI 屏、显存与刷新率怎么算2.1 SPI 屏的两种常见接法4 线 SPI 与 8080 并口转 SPI市面上能买到的 SPI 屏多数是 4 线 SPI 接口SCK、MOSI、CS、DC有的还带 RESET 和背光控制。F103 的硬件 SPI 最高跑到 18 MHzAPB2 72 MHz 六分频但实际能稳定跑多少取决于屏幕控制器型号和走线长度。常见控制器有 ILI9341、ST7789、ILI9488 等它们对 SPI 时钟的容忍度不同。ILI9341 手册标称写周期最小 100 ns对应 10 MHz 左右比较稳ST7789 可以跑到 15 MHz 以上但前提是杜邦线别太长。这套移植程序默认按 4 线 SPI 写DC 线用来区分“命令”和“数据”。如果你用的是并口屏转 SPI 模块注意模块上那颗转换芯片可能引入额外延迟SPI 时钟要往下调。我一般会先把 SPI 分频设到 8 分频9 MHz点亮后再往上试别一上来就 2 分频花屏了都不知道是时序还是接线问题。2.2 显存怎么算LVGL 的 draw buffer 与 F103 的 SRAM 边界F103C8T6 只有 20 KB SRAMF103ZE 有 64 KB。LVGL 需要一个 draw buffer 来存放渲染好的像素块大小至少是屏幕宽度的若干倍。常见做法是开一个 1/10 屏幕大小的缓冲区比如 240x320 的屏一行 240 像素开 240x32 的 buffer就是 240322 15360 字节约 15 KB。这在 C8T6 上已经吃掉大半 SRAM再跑 LVGL 的其他对象和用户变量很容易栈溢出。所以这套程序里draw buffer 的大小是可配置的。如果你用 C8T6建议先开 240x10 或 240x16跑通再往上加。LVGL 支持部分刷新缓冲区小只是刷新次数多不会导致功能异常但刷新率会掉。下面这段是 LVGL 显示缓冲区的典型初始化代码我按这套程序的风格写出来/* lv_port_disp.c 中的缓冲区定义按屏幕宽度和行数配置 */ #define MY_DISP_HOR_RES 240 #define MY_DISP_VER_RES 320 #define DRAW_BUF_LINES 16 /* 先给 16 行C8T6 上约 7.5KB */ static lv_color_t buf_1[MY_DISP_HOR_RES * DRAW_BUF_LINES]; void lv_port_disp_init(void) { lv_disp_draw_buf_init(draw_buf, buf_1, NULL, MY_DISP_HOR_RES * DRAW_BUF_LINES); static lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.hor_res MY_DISP_HOR_RES; disp_drv.ver_res MY_DISP_VER_RES; disp_drv.flush_cb disp_flush; /* 底层刷屏函数由移植层实现 */ disp_drv.draw_buf draw_buf; lv_disp_drv_register(disp_drv); }逻辑说明lv_disp_draw_buf_init的第三个参数是缓冲区能容纳的像素点数不是字节数。LVGL 内部按lv_color_t大小算默认 16 位色就是 2 字节。flush_cb是 LVGL 渲染完一块区域后回调你的函数你把这块像素通过 SPI 打到屏幕对应窗口。参数DRAW_BUF_LINES直接决定 SRAM 占用和刷新率16 行在 9 MHz SPI 下全屏刷新大约 8 到 10 帧够跑简单 UI。2.3 刷新率估算SPI 时钟、像素格式与 DMA 的取舍全屏 240x32016 位色总共 153600 字节。SPI 在 9 MHz 下理论字节率 1.125 MB/s全屏一次要 136 ms约 7.3 帧。如果开 DMACPU 可以在这期间干别的但 SPI 时钟不变帧率上限不变。想再快只能提 SPI 时钟或换并口屏。这套程序里刷屏函数用的是阻塞式 SPI 发送没上 DMA因为 F103 的 SPI DMA 通道和 LVGL 的刷新回调配合起来容易出“撕裂”问题新手调起来更痛苦。先把阻塞版跑通确认画面正常再考虑 DMA 优化。提示如果你用的是 ST7789 且走线很短SPI 分频可以设到 4 分频18 MHz全屏能到 14 帧左右。但先用手摸一下屏幕排线发烫就降频。3. 把程序跑起来引脚配置、SPI 初始化与 LVGL 对接的完整步骤3.1 硬件连线与引脚定义别让 CS 和 DC 打架拿到程序后第一件事是打开spi_port.h或类似的引脚定义头文件把下面这些宏改成你实际接的引脚。这套程序默认用的是 SPI1引脚是 PA5SCK、PA7MOSICS 用 PA4DC 用 PA3RESET 用 PA2背光用 PA1。如果你接的是 SPI2就要改到 PB13、PB15同时注意 SPI2 挂在 APB1 上最高时钟 36 MHz分频系数要重新算。/* spi_port.h 中需要你改的引脚定义 */ #define LCD_SPI_PORT SPI1 #define LCD_SPI_CLK RCC_APB2Periph_SPI1 #define LCD_GPIO_PORT GPIOA #define LCD_GPIO_CLK RCC_APB2Periph_GPIOA #define LCD_PIN_CS GPIO_Pin_4 #define LCD_PIN_DC GPIO_Pin_3 #define LCD_PIN_RESET GPIO_Pin_2 #define LCD_PIN_BL GPIO_Pin_1 #define LCD_SPI_PRESCALER SPI_BaudRatePrescaler_8 /* 9 MHz先稳后快 */逻辑说明LCD_SPI_PRESCALER是分频系数SPI1 在 APB2 上 72 MHz8 分频就是 9 MHz。如果你改成 4 分频就是 18 MHz。改完引脚后检查 CS 和 DC 有没有接反——CS 是片选低电平有效DC 是数据/命令选择高电平数据、低电平命令。接反的典型现象是屏幕全白或全黑但背光亮。3.2 SPI 初始化与刷屏函数从 set_window 到 push_pixels底层刷屏的核心是两步先告诉屏幕“我要往哪个矩形区域写”再连续发像素数据。这套程序里LCD_SetWindow负责发命令 0x2A列地址和 0x2B行地址然后LCD_PushPixels负责发 0x2C写显存并连续发送颜色值。下面是关键代码段/* lcd_driver.c 中的窗口设置与像素推送 */ void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { LCD_WriteCmd(0x2A); /* 列地址设置 */ LCD_WriteData(x1 8); LCD_WriteData(x1 0xFF); LCD_WriteData(x2 8); LCD_WriteData(x2 0xFF); LCD_WriteCmd(0x2B); /* 行地址设置 */ LCD_WriteData(y1 8); LCD_WriteData(y1 0xFF); LCD_WriteData(y2 8); LCD_WriteData(y2 0xFF); LCD_WriteCmd(0x2C); /* 开始写显存 */ } void LCD_PushPixels(uint16_t *colors, uint32_t len) { LCD_DC_Set(); /* DC 拉高进入数据模式 */ LCD_CS_Clr(); /* CS 拉低选中屏幕 */ while (len--) { SPI_WriteByte(*colors 8); SPI_WriteByte(*colors 0xFF); colors; } LCD_CS_Set(); /* 发完拉高 CS */ }逻辑说明LCD_WriteCmd内部会先把 DC 拉低发一个字节再把 DC 拉高。LCD_PushPixels里先拉高 DC然后连续发两个字节表示一个像素高字节在前RGB565 格式。注意 CS 在整段像素发送期间要保持低电平不要每发一个字节就翻 CS否则屏幕会丢数据。参数len是像素个数不是字节数。3.3 LVGL 的 flush_cb 怎么写把渲染块打到屏幕的正确姿势LVGL 渲染完一块区域后会调用你注册的flush_cb传入区域坐标和颜色数组。你要做的就是把这块区域通过LCD_SetWindow和LCD_PushPixels打到屏幕然后调用lv_disp_flush_ready告诉 LVGL 可以继续渲染下一块。下面是这套程序里的对接代码/* lv_port_disp.c 中的刷屏回调 */ static void disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { LCD_SetWindow(area-x1, area-y1, area-x2, area-y2); LCD_PushPixels((uint16_t *)color_p, (area-x2 - area-x1 1) * (area-y2 - area-y1 1)); lv_disp_flush_ready(disp_drv); /* 必须调用否则 LVGL 卡死 */ }逻辑说明area是 LVGL 算好的矩形区域color_p是渲染好的像素数组。LCD_PushPixels的第二个参数是像素个数用(x2-x11)*(y2-y11)算出来。lv_disp_flush_ready必须调用而且要在 SPI 发送完成之后调用。如果你用了 DMA就在 DMA 传输完成中断里调用它否则 LVGL 会一直等界面直接卡住不动。3.4 主循环与心跳lv_tick_inc 和 lv_timer_handler 的配合LVGL 需要一个毫秒级心跳来驱动动画和定时器。这套程序里用 SysTick 中断每 1 ms 调用一次lv_tick_inc(1)主循环里不断调用lv_timer_handler()。注意lv_timer_handler的调用间隔不要太长否则界面响应迟钝也不要太短否则 CPU 全耗在 LVGL 内部。一般 5 ms 调一次比较合适。/* main.c 中的主循环 */ while (1) { lv_timer_handler(); /* 处理 LVGL 定时器、动画、输入 */ HAL_Delay(5); /* 留出时间给其他任务也避免 LVGL 跑太猛 */ }逻辑说明HAL_Delay(5)不是必须的但如果你还有其他任务要跑可以用它让出 CPU。如果只跑 LVGL可以去掉但要注意lv_timer_handler本身会阻塞到下一个定时器到期不会空转。SysTick 中断里调用lv_tick_inc(1)的代码在stm32f1xx_it.c里别忘记加。4. 避坑与排查SPI 屏跑 LVGL 最常见的五个翻车现场4.1 现象屏幕全白背光亮但没有任何图像原因SPI 初始化没成功或者 CS/DC 引脚接错。F103 的 SPI1 默认引脚是 PA5/PA7如果你在代码里改了引脚但没开 GPIO 复用时钟SPI 根本发不出波形。另一个常见原因是屏幕的 RESET 引脚没有正确拉低再拉高控制器没复位。解决先用逻辑分析仪或示波器看 SCK 有没有波形。没有波形就查 SPI 使能位和 GPIO 配置。有波形但屏幕不亮查 RESET 时序一般要求低电平至少 10 ms再拉高等待 120 ms。这套程序里LCD_Init开头有复位序列确认你没注释掉。4.2 现象花屏颜色错位或者图像斜着跑原因SPI 时钟太快或者像素格式不对。ILI9341 默认是 RGB565但有些屏幕出厂是 RGB666 或 BGR 顺序。如果你发的是 RGB565 但屏幕按 BGR 解析红色和蓝色会互换。图像斜着跑通常是窗口设置不对列地址和行地址的范围超了。解决先把 SPI 分频降到 16 或 32看花屏是否消失。如果颜色不对改LCD_WriteCmd(0x36)后面的参数试一下 0x48 和 0x88。窗口设置要确保 x1x2、y1y2且不超过屏幕分辨率。4.3 现象LVGL 界面卡死或者只显示一半就不动了原因lv_disp_flush_ready没调用或者调用早了。如果你在 SPI 还没发完就调用它LVGL 会覆盖正在发送的缓冲区导致画面撕裂或卡死。另一个原因是 draw buffer 太小LVGL 渲染一块后等 flush但 flush 里又调用了阻塞延时导致死锁。解决确认lv_disp_flush_ready在LCD_PushPixels之后调用。如果用了 DMA在 DMA 完成中断里调用。draw buffer 至少开 240x10别小于这个值。4.4 现象编译通过但运行到 LVGL 初始化就进 HardFault原因SRAM 不够栈溢出。F103C8T6 只有 20 KB SRAMLVGL 的 draw buffer 加上对象结构体很容易超。HardFault 常见于lv_disp_draw_buf_init或lv_disp_drv_register内部。解决把 draw buffer 改小比如 240x8。在启动文件里把栈大小从 0x400 改成 0x800。如果还不行检查有没有大数组定义在函数内部挪到全局或加static。4.5 现象触摸没反应或者坐标完全对不上原因这套程序主要针对显示移植触摸部分需要你根据触摸芯片自己接。如果你用的是 XPT2046 或 FT6236I2C 或 SPI 的初始化要单独写。坐标对不上通常是触摸屏的 XY 和显示 XY 没有交换或镜像。解决先读触摸原始坐标在屏幕上画点看位置。如果 X 和 Y 反了在touch_read回调里交换如果镜像了用hor_res - x或ver_res - y翻转。LVGL 的输入设备注册里要设置disp_drv关联否则坐标映射会错。5. 进阶把刷新率再压榨一点以及一个验证移植是否成功的笨办法如果你已经跑通了基本 demo想再提刷新率可以试三件事。第一把 SPI 分频从 8 改成 4前提是屏幕和走线扛得住。第二把 draw buffer 从 16 行加到 32 行减少 flush 次数但注意 SRAM 余量。第三把LCD_PushPixels里的逐字节发送改成 16 位发送F103 的 SPI 支持 16 位数据帧配置SPI_DataSize_16b后一次发一个像素能省一半的循环开销。下面是对应的 SPI 配置改动/* spi_port.c 中改为 16 位数据帧 */ SPI_InitStructure.SPI_DataSize SPI_DataSize_16b; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; /* 注意发命令时仍要按 8 位发所以发命令前切回 8 位 发像素前切回 16 位或者用两个 SPI 配置函数 */逻辑说明16 位数据帧下SPI_I2S_SendData一次写 16 位SPI 硬件自动分两个时钟发出去。但命令字节是 8 位的所以你要在LCD_WriteCmd里临时切回 8 位或者干脆用软件模拟命令时序。这套程序默认没开 16 位因为切换麻烦但如果你追求极致可以试。验证移植是否成功我有个笨办法但很管用写一个测试函数在屏幕上画三个纯色矩形——红、绿、蓝每个矩形 100x100位置分别在左上、中间、右下。然后调用lv_timer_handler跑起来。如果三个矩形颜色正、位置对、没有花边说明窗口设置、像素格式、刷新回调都对了。之后再跑 LVGL 的lv_demo_widgets看按钮能不能点、滑块能不能拖。这个笨办法帮我省了很多次“以为是 LVGL 问题其实是底层刷屏没对”的来回折腾。从那以后我每次移植新的 SPI 屏都强制先跑这个三色矩形测试再上 LVGL。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?