尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
STM32F103 SPI屏LVGL移植实战:从点不亮到流畅GUI
简介这份资源是面向嵌入式开发者的STM32F103 SPI屏幕LVGL移植工程适合已掌握C语言与基础外设驱动、希望快速搭建图形界面的中初级工程师。项目围绕SPI接口配置、屏幕驱动适配、LVGL库集成与帧缓冲区管理展开覆盖SSD1306、ST7789等常见SPI屏的初始化、画点与刷新流程并涉及触摸输入、系统时钟与调试输出等实际环节。压缩包共761个文件约3.77MB以189个c与186个h源码为核心配合117个d依赖文件、112个o编译产物、57个png界面素材及mk、bin、md等工程与说明文件目录结构完整可直接导入Keil或STM32CubeIDE继续开发。目前已有4227人学习下载读者可参考其SPI通信配置、屏幕驱动写法与LVGL移植思路快速将GUI方案应用到自己的STM32F103项目中。1. 从一块“点不亮”的 SPI 屏说起这套 LVGL 移植程序到底能干什么如果你手上有一块 STM32F103 最小系统板外加一块 SPI 接口的 TFT 屏大概率经历过这样的场景接线照着卖家给的图连好了代码烧进去屏幕要么全白要么花屏要么干脆一点反应都没有。更让人头大的是想上 LVGL 做点像样的 GUI结果底层驱动和 LVGL 的对接怎么都调不通lv_port_disp改来改去还是黑屏。这套 STM32F103 SPI 屏幕移植程序就是冲着这个痛点来的——它把 SPI 屏的底层驱动和 LVGL 的显示接口一次性打通拿到手改几个引脚定义就能跑。这套程序面向的是用 STM32F103 系列C8T6、RCT6 这类常见型号驱动 SPI 接口 TFT 屏的开发者核心价值在于“可用”两个字不是那种只跑个lv_demo_widgets就完事的演示工程而是把 SPI 初始化、屏幕初始化序列、刷屏函数、LVGL 的flush_cb回调、时基lv_tick_inc这些容易翻车的环节都串起来了。适合两类人一类是刚接触 LVGL、想找个能跑的参考工程照着改的新手另一类是手上有现成 SPI 屏、想快速验证 LVGL 效果、不想从零啃驱动的熟手。下面就从工程结构开始一层层拆开看它是怎么组织的。2. 工程结构与技术选型为什么是 STM32F103 软件 SPI LVGL2.1 目录结构一个能直接编译的 Keil 工程长什么样拿到这套源码第一件事是看目录。一个能直接编译下载的 STM32F103 工程通常不会把所有文件堆在一个文件夹里而是按“库文件 / 驱动 / 应用 / LVGL”分层。这套程序的目录大致是这样组织的Project/ ├── CORE/ // 内核启动文件、core_cm3.h ├── STM32F10x_FWLib/ // 标准外设库 ├── HARDWARE/ │ ├── SPI/ // SPI 底层驱动 │ ├── LCD/ // 屏幕初始化与刷屏 │ └── TIMER/ // 时基给 LVGL 提供 tick ├── LVGL/ │ ├── lvgl/ // LVGL 源码 │ └── lv_port/ // 移植层disp、indev、tick ├── USER/ │ ├── main.c │ └── stm32f10x_it.c └── Project.uvprojx // Keil 工程文件这个分层的好处是HARDWARE里放的是和硬件强相关的代码换屏、换引脚只动这里LVGL/lv_port是移植层负责把 LVGL 和你的硬件对接USER/main.c只写业务逻辑。很多人移植失败就是因为把这三层混在一起改改到最后自己都不知道哪层出了问题。提示如果你的工程里没有单独的lv_port目录LVGL 的显示接口通常写在lv_conf.h附近或者直接塞在main.c里这种结构后期维护会很痛苦建议按上面的方式拆开。2.2 技术选型标准库 软件 SPI 的取舍这套程序用的是 STM32 标准外设库StdPeriph Lib不是 HAL 库。原因很直接F103 这颗芯片用标准库的工程资料最多编译体积小跑 LVGL 这种吃 Flash 和 RAM 的东西能省一点是一点。LVGL 版本一般是 8.x因为 8.x 的 API 相对稳定lv_port_disp的写法也成熟。SPI 这块程序用的是软件 SPIGPIO 模拟还是硬件 SPI取决于你的屏和接线。软件 SPI 的好处是引脚随便选不受硬件 SPI 固定引脚的限制缺点是刷屏速度慢一些。对于 128x160 或 240x320 这种分辨率的屏软件 SPI 跑 LVGL 勉强够用但如果要做动画或者刷大图还是建议改成硬件 SPI DMA。这套程序默认给的是软件 SPI 的写法因为兼容性最好你拿到手不用改硬件就能先点亮。选型项方案理由MCUSTM32F103C8T6/RCT6资料多、便宜、够用库标准外设库体积小、编译快SPI软件模拟可改硬件引脚灵活、兼容性强GUILVGL 8.xAPI 稳定、移植资料多时基TIM2 定时器 1ms给 lv_tick_inc 提供心跳2.3 关键宏定义改这几个地方就能适配你的屏在LCD.h或SPI.h里通常会有一组宏定义控制引脚和屏幕参数。这是你拿到源码后第一个要改的地方// LCD.h 引脚定义根据你的实际接线修改 #define LCD_SCL_PIN GPIO_Pin_5 #define LCD_SCL_PORT GPIOA #define LCD_SDA_PIN GPIO_Pin_7 #define LCD_SDA_PORT GPIOA #define LCD_RES_PIN GPIO_Pin_4 #define LCD_RES_PORT GPIOA #define LCD_DC_PIN GPIO_Pin_3 #define LCD_DC_PORT GPIOA #define LCD_CS_PIN GPIO_Pin_2 #define LCD_CS_PORT GPIOA #define LCD_BLK_PIN GPIO_Pin_1 // 背光没有可不接 #define LCD_BLK_PORT GPIOA // 屏幕分辨率必须和你的屏一致 #define LCD_WIDTH 240 #define LCD_HEIGHT 320这几个宏是整份代码的“总开关”。SCL/SDA是 SPI 时钟和数据线RES是复位DC是数据/命令选择CS是片选。改完引脚还要确认LCD_WIDTH和LCD_HEIGHT和你的屏匹配——如果这里写错了LVGL 刷出来的画面会错位或者只显示一部分。我一般会先把这几个宏单独拎出来放在文件最上面改的时候一眼就能看到。3. 底层驱动拆解SPI 时序、屏幕初始化和刷屏函数3.1 软件 SPI 的读写时序一个字节怎么发出去软件 SPI 的核心就是手动翻转时钟线在时钟的上升沿或下降沿把数据位送出去。这段代码看着简单但时序错了屏幕就是不出图// 软件 SPI 写一个字节高位先出 void SPI_WriteByte(uint8_t data) { uint8_t i; for (i 0; i 8; i) { LCD_SCL 0; // 时钟拉低 if (data 0x80) // 取最高位 LCD_SDA 1; else LCD_SDA 0; data 1; // 左移准备下一位 LCD_SCL 1; // 时钟拉高从机采样 } }逻辑说明循环 8 次每次先拉低 SCL把当前最高位放到 SDA 上然后拉高 SCL。从机在 SCL 上升沿采样 SDA 的电平。data 1是为了让下一次循环取到次高位。参数data就是要发送的字节。这里没有做延时因为 F103 跑 72MHzGPIO 翻转速度足够慢SPI 屏能跟上如果你的屏比较挑时序可以在LCD_SCL 1后面加一个__NOP()。注意软件 SPI 的时序和硬件 SPI 的 CPOL/CPHA 是对应的。上面这段是“空闲低电平、上升沿采样”对应硬件 SPI 的 Mode 0。如果你的屏要求 Mode 3就得把时钟极性反过来。3.2 写命令和写数据DC 引脚的作用SPI 屏靠 DC 引脚区分这次发的是命令还是数据。DC 拉低是命令拉高是数据。这个逻辑封装成两个函数// 写命令 void LCD_WriteCmd(uint8_t cmd) { LCD_DC 0; // DC 低电平表示命令 LCD_CS 0; // 片选拉低 SPI_WriteByte(cmd); LCD_CS 1; // 片选拉高 } // 写数据 void LCD_WriteData(uint8_t data) { LCD_DC 1; // DC 高电平表示数据 LCD_CS 0; SPI_WriteByte(data); LCD_CS 1; }逻辑说明LCD_DC决定这次传输的性质LCD_CS是片选低电平有效。每次传输完拉高 CS是为了让屏幕知道一个字节传完了。有些屏不需要每次拉 CS但拉一下更保险。参数cmd和data都是 8 位如果是 16 位色数据需要分两次发先高字节后低字节。3.3 屏幕初始化序列那一长串寄存器配置屏幕初始化就是往一堆寄存器里写值告诉屏幕用多少分辨率、什么颜色格式、扫描方向怎么设。这段代码通常很长但结构很固定void LCD_Init(void) { LCD_RES 0; // 硬件复位 delay_ms(10); LCD_RES 1; delay_ms(120); LCD_WriteCmd(0x11); // Sleep out delay_ms(120); LCD_WriteCmd(0x36); // 扫描方向 LCD_WriteData(0x00); // 根据你的屏调整 LCD_WriteCmd(0x3A); // 像素格式 LCD_WriteData(0x05); // 16bit/pixel LCD_WriteCmd(0x29); // Display on // ... 其他初始化命令 }逻辑说明0x11是退出睡眠必须延时等屏幕稳定0x36控制扫描方向这个值决定了你显示的画面是正的还是反的、是横屏还是竖屏0x3A设置颜色格式0x05表示 16 位色RGB565。不同屏的初始化序列不一样这份代码里的序列是针对常见 ST7789/ILI9341 类控制器的如果你的屏型号不同需要替换成卖家给的初始化代码。3.4 刷屏函数LVGL 的 flush_cb 最终调用的就是它LVGL 渲染完一帧会调用你注册的flush_cb而flush_cb最终要调用刷屏函数把像素数据写到屏幕上。这套程序里的刷屏函数大致是这样// 设置刷屏区域 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_FillColor(uint16_t color, uint32_t len) { LCD_DC 1; LCD_CS 0; while (len--) { SPI_WriteByte(color 8); SPI_WriteByte(color 0xFF); } LCD_CS 1; }逻辑说明LCD_SetWindow告诉屏幕接下来要刷哪块区域0x2A和0x2B分别设置列和行的起止地址0x2C是写显存命令。之后连续发送的像素数据就会按顺序填进这块区域。LCD_FillColor是填充单色LVGL 刷图时会调用类似的函数把颜色缓冲区发出去。参数color是 RGB565 格式len是像素个数。4. LVGL 移植层disp、tick 和主循环怎么接4.1 lv_port_disp显示接口的注册LVGL 不知道你的屏幕长什么样它只认一个flush_cb回调。这个回调在lv_port_disp_init里注册void lv_port_disp_init(void) { static lv_disp_draw_buf_t draw_buf; static lv_color_t buf_1[LCD_WIDTH * 40]; // 缓冲区40 行 lv_disp_draw_buf_init(draw_buf, buf_1, NULL, LCD_WIDTH * 40); static lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.hor_res LCD_WIDTH; disp_drv.ver_res LCD_HEIGHT; disp_drv.flush_cb disp_flush; disp_drv.draw_buf draw_buf; lv_disp_drv_register(disp_drv); }逻辑说明buf_1是渲染缓冲区大小是LCD_WIDTH * 40也就是一次渲染 40 行。缓冲区越大刷屏次数越少但占 RAM 越多。F103 的 RAM 只有 20KB 左右40 行是个比较稳妥的值。disp_drv.hor_res和ver_res必须和屏幕分辨率一致。flush_cb指向下面要讲的disp_flush。4.2 disp_flush把 LVGL 的颜色数据写到屏幕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_DC 1; LCD_CS 0; uint32_t len (area-x2 - area-x1 1) * (area-y2 - area-y1 1); for (uint32_t i 0; i len; i) { SPI_WriteByte(color_p[i].full 8); SPI_WriteByte(color_p[i].full 0xFF); } LCD_CS 1; lv_disp_flush_ready(disp_drv); // 必须调用告诉 LVGL 刷完了 }逻辑说明area是 LVGL 告诉你要刷新的区域color_p是颜色数据指针。先调LCD_SetWindow设置区域然后把color_p里的每个像素按 RGB565 格式发出去。color_p[i].full是 LVGL 的颜色联合体取full就是 16 位色值。最后必须调lv_disp_flush_ready否则 LVGL 会一直等界面就卡死了。这是新手最容易漏的一步。4.3 tick 时基LVGL 的心跳不能停LVGL 需要知道时间流逝才能处理动画、超时这些逻辑。时基靠一个 1ms 的定时器中断来喂// TIM2 中断服务函数1ms 一次 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { lv_tick_inc(1); // 告诉 LVGL 过了 1ms TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } }逻辑说明lv_tick_inc(1)每次加 1msLVGL 内部维护一个 tick 计数器。这个中断必须稳定触发如果定时器配置错了LVGL 的动画会变慢或者卡顿。TIM2 的初始化在TIMER.c里配置成 1ms 周期使能更新中断。4.4 主循环lv_timer_handler 的调用节奏int main(void) { delay_init(); LCD_Init(); TIM2_Init(); // 时基 lv_init(); lv_port_disp_init(); // 显示接口 // 创建一个标签测试 lv_obj_t *label lv_label_create(lv_scr_act()); lv_label_set_text(label, Hello LVGL); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); while (1) { lv_timer_handler(); // LVGL 任务处理 delay_ms(5); // 不能太频繁也不能太久 } }逻辑说明lv_timer_handler是 LVGL 的主任务函数负责处理界面刷新、事件、动画。调用间隔一般 5ms 左右太频繁浪费 CPU太久界面响应迟钝。delay_ms(5)是简单的延时实际项目里可以用系统滴答或者空闲中断来替代。5. 避坑与排查移植过程中最容易翻车的几个点5.1 屏幕全白或全黑一点反应没有现象上电后屏幕背光亮但没有任何画面或者全白。原因通常是初始化序列不对或者复位时序没满足。解决先确认LCD_RES复位时序低电平至少保持 10ms拉高后延时 120ms 再发命令。然后检查初始化命令里0x11Sleep out后面有没有延时 120ms很多屏不等这个延时就直接发后续命令会不响应。最后确认0x29Display on有没有发。5.2 画面花屏、错位、颜色不对现象能显示但颜色是反的或者画面偏移。原因一般是0x36扫描方向寄存器的值不对或者0x3A颜色格式设错了。解决把0x36的值改成0x00、0x60、0xC0这几个常见值试一遍直到画面方向正确。颜色反了就把 RGB 和 BGR 对调通常改0x36的 bit3 就能解决。5.3 LVGL 界面卡死只显示一半现象界面显示了一部分就不动了。原因大概率是disp_flush里忘了调lv_disp_flush_ready。LVGL 在等这个信号等不到就不会发下一帧。解决检查disp_flush函数最后一行有没有lv_disp_flush_ready(disp_drv)。另外如果用了 DMA要在 DMA 传输完成中断里调这个函数不能在disp_flush里直接调。5.4 编译报错 undefined reference tolv_xxx现象编译时一堆 LVGL 函数找不到。原因通常是lv_conf.h没配置好或者 LVGL 源文件没全部加入工程。解决确认工程根目录有lv_conf.h并且#if 1打开了配置。然后在 Keil 里把LVGL/lvgl/src下所有.c文件加入工程别漏了lv_core、lv_draw、lv_widgets这些子目录。5.5 刷屏速度慢动画一顿一顿的现象静态画面正常一有动画就卡。原因是软件 SPI 刷屏太慢或者缓冲区太小导致刷屏次数太多。解决优先把缓冲区从 40 行加到 80 行甚至全屏如果 RAM 够。如果还不够把软件 SPI 改成硬件 SPISPI 时钟开到 18MHz 或 36MHz。再不行就上 DMA让 CPU 去干别的DMA 负责传像素。6. 二次开发换屏、加触摸、上硬件 SPI 的改造思路拿到这套程序二次开发无非三个方向换一块不同的屏、加触摸、把软件 SPI 换成硬件 SPI DMA。这三个改造我都踩过坑下面说下具体怎么动。换屏是最常见的。假设你从 240x320 的 ILI9341 换成 128x160 的 ST7735要改的地方就三处LCD_WIDTH和LCD_HEIGHT改成 128 和 160LCD_Init里的初始化序列换成 ST7735 的LCD_SetWindow里的列行地址命令确认一下ST7735 的0x2A和0x2B参数格式和 ILI9341 略有不同有的需要加偏移。改完先别急着上 LVGL写个LCD_FillColor(0xF800, 128*160)全屏填红确认底层通了再跑 LVGL。加触摸的话如果是电阻屏通常用 XPT2046 这类 SPI 触摸芯片。你需要再写一个lv_port_indev在里面注册read_cb读触摸坐标然后调lv_indev_set_cursor或者直接喂给 LVGL 的点击事件。注意触摸坐标和显示坐标要对应横屏竖屏切换时坐标要转置。上硬件 SPI DMA 是提升刷屏速度最有效的手段。改造步骤把SPI_WriteByte换成硬件 SPI 的SPI_I2S_SendData把LCD_FillColor里的循环换成 DMA 传输在 DMA 传输完成中断里调lv_disp_flush_ready。这里有个血泪经验DMA 传输期间不能改LCD_CS和LCD_DC否则数据会错位。我一般会在 DMA 启动前把 DC 拉高、CS 拉低传输完成中断里再拉高 CS。// 硬件 SPI DMA 刷屏的核心逻辑 void LCD_FillColor_DMA(uint16_t *color, uint32_t len) { LCD_DC 1; LCD_CS 0; DMA_Cmd(DMA1_Channel3, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel3, len * 2); // 16位色字节数x2 DMA_Cmd(DMA1_Channel3, ENABLE); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE); } // DMA 传输完成中断 void DMA1_Channel3_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC3) ! RESET) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) SET); LCD_CS 1; lv_disp_flush_ready(disp_drv); // 这里通知 LVGL DMA_ClearITPendingBit(DMA1_IT_TC3); } }逻辑说明DMA_SetCurrDataCounter设置要传的字节数16 位色一个像素占 2 字节所以是len * 2。SPI_I2S_DMACmd打开 SPI 的 DMA 发送请求。传输完成中断里先等 SPI 总线空闲再拉高 CS最后调lv_disp_flush_ready。这套流程跑通后240x320 的屏刷一帧能压到 10ms 以内动画就顺了。从那以后我每次移植 LVGL都强制先跑一个纯色填充测试确认底层刷屏没问题再上 LVGL 的 demo。这个习惯帮我省了至少一半的调试时间。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

XAMPP本地开发环境搭建与配置避坑指南

XAMPP本地开发环境搭建与配置避坑指南

1. 为什么本地开发环境值得反复折腾刚入行那会儿,我最怕的不是写代码,而是配环境。一个项目跑不起来,排查半天发现是 PHP 版本对不上、MySQL 端口被占用、Apache 模块没开——这种坑踩一次记一辈子。后来带新人,发现大家卡的地方几…

📅 2026/10/19 16:24:52
板式家具下料算法:二维排样优化与Qt工程实现

板式家具下料算法:二维排样优化与Qt工程实现

简介:本资源是一套基于Qt框架实现的板式家具智能下料算法实践项目,面向家具制造行业工程师、工业优化算法学习者及C/GUI开发初学者,聚焦板材利用率提升这一核心生产痛点。压缩包共62个文件,含8个cpp与7个h源码文件构成核心算法逻辑…

📅 2026/10/19 16:24:52
CMake找不到Boost库?一文详解FindBoost报错排查与解决

CMake找不到Boost库?一文详解FindBoost报错排查与解决

1. 问题背景与核心症结拆解1.1 这个报错到底在说什么但凡在C项目里用过CMake搭配Boost库的人,大概率都见过这个让人血压升高的报错:CMake Error at /usr/share/cmake-3.22/Modules/FindBoost.cmake:1968 (message):Unable to find the requested Boost l…

📅 2026/10/19 16:24:52
MORE NEWS

更多资讯

📰

ESP32音乐播放器:用I2S接口软解WAV文件实战

简介:esp32_MusicPlayer是一套面向Arduino与ESP32开发者的音乐播放器示例项目,基于Arduino-ESP32框架,通过I2S接口读取SD卡中的WAV文件并实时播放,适合快速实现音频功能及学习I2S数字音频传输。项目代码简洁,覆盖I2S引…

📰

山东省17地市矢量图获取与处理:从坐标系转换到可视化落地

简介:这份面向地理信息系统(GIS)学习者与科研人员的山东省17地市行政边界矢量数据包,涵盖济南、青岛、淄博等全部17个地级市边界,可用于地图制作、空间查询、区域规划与GIS教学实验。压缩包共12个文件,包括…

📰

代码审查自动化实践:基于open-code-review提升团队效能

1. 代码审查这件事,为什么总是卡在“人”上做开发这些年,我经手过的代码审查流程少说也有七八套。从最早用邮件发 diff 给老员工看,到后来在 GitLab 上建 MR 走审批流,再到团队大了以后引入专门的审查平台——流程换了又换&#x…

📰

基于YOLOv8与PyQt5的实时人脸表情识别系统设计与实现

1. 项目整体设计与思路拆解1.1 为什么选YOLOv8 PyQt5这个组合?做这个项目的初衷很简单:我想在桌面端跑一个能实时识别人类表情的系统,把人脸框出来,并且判断这个人现在是开心、生气、惊讶还是难过,最后把结果随手就能…

📰

DeskcommCRM:把桌面通信与客户管理融为一体的落地实践

DeskcommCRM:一套把“桌面通信”和“客户管理”真正揉在一起的落地实践做客户管理这行的人,十有八九都经历过这种状态:电脑上开着 CRM 页面,旁边放着电话,左手拿听筒右手敲键盘,通话一结束就得赶紧把沟通要…

📰

从版本控制到代码审计:用Git精准追溯每一行代码的来龙去脉

代码被改出问题的时候,最怕听到一句话:“我记得这里之前不是这样的。”在没有版本控制的项目里,这句话基本意味着排查只能靠聊天记录和记忆;而在用 Git 管理的项目里,这句话其实可以转化为一次完整的代码审计。我这些年…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬