简介:本资源是一套基于STM32F103RC微控制器、ILI9341 SPI接口TFT显示屏与LittlevGL V6.0图形库的嵌入式GUI开发实测工程,面向嵌入式初学者及中级开发者,解决MCU驱动彩色屏并构建交互式图形界面的核心技术难点。压缩包共39个文件,含18个头文件(h)用于外设配置与LVGL适配、12个源文件(c)实现SPI驱动、DMA传输、编码器输入及LVGL移植层逻辑,另有Keil工程文件(uvprojx/uvoptx)、README说明文档、硬件连接参考图(png/jpg)及Git配置文件,整体大小2.39MB,结构清晰,便于快速导入与二次开发。目前已有32人学习下载,资源提供完整可运行的LVGL V6.0移植方案,涵盖ILI9341初始化、显存刷新优化、触摸/编码器事件接入及基础UI组件演示,附带详细配置注释与模块化驱动分层设计,显著降低图形界面开发门槛。
1. 项目缘起与核心目标
最近在整理一个老项目,翻出来一块吃灰已久的2.4寸TFT屏,型号是ILI9341,接口是SPI。手头正好有一块STM32F103RC的核心板,想着能不能给它驱动起来,顺便把一直想玩的LittleVGL图形库(现在叫LVGL)也集成进去,做个综合性的测试。这个想法听起来简单,但实际操作起来,从底层SPI驱动到上层GUI框架的适配,每一步都可能藏着“惊喜”。网上资料虽然多,但要么是针对特定开发板的,要么是版本过时的,直接照搬大概率会卡在某个环节。所以,我决定把这次从零开始,基于STM32标准外设库和LittleVGL V6.0,点亮ILI9341 SPI屏的完整过程记录下来,重点分享那些容易踩坑的细节和调试思路,希望能给有同样需求的朋友提供一个清晰的参考路径。
这个项目的核心目标很明确:第一,打通STM32F103的SPI硬件与ILI9341显示屏的通信链路;第二,成功移植LittleVGL V6.0图形库,并使其能在屏幕上正确绘制基础图形和控件;第三,整个过程不依赖特定HAL库或CubeMX,使用标准外设库实现,以便更清晰地理解底层机制。最终,我们会得到一个可以流畅显示UI界面、响应触摸(如果屏带触摸)的测试平台。无论你是刚接触嵌入式GUI,还是想从8位或51单片机升级到STM32进行更复杂的显示应用,这个实践过程都能提供不少有价值的经验。
2. 硬件选型与电路连接要点
工欲善其事,必先利其器。硬件连接是第一步,也是最容易出错的一步。我使用的核心是STM32F103RCT6,拥有256KB Flash和48KB RAM,性能对于驱动LittleVGL和ILI9341绰绰有余。显示屏是常见的2.4寸240x320分辨率TFT,驱动芯片为ILI9341,采用4线SPI接口(注意,还有8位并口等其它模式,我们用的是SPI)。
2.1 引脚连接方案
ILI9341的SPI接口通常需要6根线(如果算上背光控制就是7根)。具体连接和STM32的引脚分配如下:
| ILI9341引脚 | 功能 | STM32F103RC引脚 (示例) | 备注 |
|---|---|---|---|
| VCC | 电源 (3.3V) | 3.3V | 务必确认屏的电压范围,有些屏是5V的。 |
| GND | 地 | GND | |
| CS | 片选 | PA4 (SPI1_NSS) | 硬件片选,也可用任意GPIO做软件片选。 |
| RESET | 复位 | PA0 | 低电平有效,用于硬复位屏幕。 |
| DC (或 RS) | 数据/命令选择 | PA1 | 高电平为数据,低电平为命令,这是关键信号。 |
| SCL (SCK) | 时钟线 | PA5 (SPI1_SCK) | SPI时钟。 |
| SDA (MOSI) | 数据线 (主机输出) | PA7 (SPI1_MOSI) | STM32发送数据给屏幕。 |
| LED (BL) | 背光控制 | PA2 | 接限流电阻,高电平点亮。 |
注意:这里MISO线(PA6)没有连接,因为ILI9341在SPI写模式下是单向通信,STM32只发不收。如果你的屏支持读寄存器或带触摸功能需要读数据,则需要连接MISO。
2.2 几个关键硬件细节
- 电源与背光:确保STM32的3.3V输出能力足够(通常需100mA以上)。背光LED一般需要串联一个合适的限流电阻(如100Ω-1KΩ),直接接3.3V可能会过流。
- 复位电路:RESET引脚必须接。上电后,需要先拉低一段时间(典型值>10ms)再拉高,完成硬件复位。很多初始化失败的问题源于复位时序不对。
- DC引脚的重要性:这是SPI驱动TFT屏与驱动普通SPI Flash最大的不同。发送命令(如初始化指令集)前,DC要置低;发送数据(如图像数据、初始化参数)前,DC要置高。这个时序控制必须严格。
- SPI时钟速度:ILI9341的SPI接口最高时钟频率在写模式下可以达到几十MHz,但为了稳定起见,尤其是在飞线连接时,初期建议先设置一个较低的速度,如9MHz(PCLK2 72MHz / 8分频)。待通信稳定后,再逐步提高。
3. 底层SPI驱动与ILI9341初始化
有了硬件连接,接下来就是让STM32的SPI“开口说话”。我们使用SPI1,工作在主机模式。
3.1 SPI外设初始化配置
我们不使用DMA,先采用查询方式,这样更容易调试。关键配置如下:
void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; // 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // 配置SPI引脚: SCK, MOSI GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置CS, DC, RESET为普通推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_1 | GPIO_Pin_0 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始状态 LCD_CS_HIGH(); // 片选高,不选中 LCD_DC_HIGH(); // 默认数据模式 LCD_RST_HIGH(); // 复位高 LCD_BL_ON(); // 打开背光 // SPI参数配置 SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx; // 单线发送 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主机 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位数据 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟极性:低电平空闲 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 时钟相位:第一个边沿采样 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件管理NSS(即CS) SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; // 9MHz SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先行 SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }这里有几个值得注意的点:第一,SPI_Direction_1Line_Tx表示我们只使用发送模式,不接收,节省了MISO线的初始化。第二,时钟极性和相位(CPOL/CPHA)通常设置为0和1(Mode 0)或1和0(Mode 3),具体需要查ILI9341的数据手册。我使用的屏是Mode 0,即空闲时SCK为低,在SCK的第一个边沿(上升沿)采样数据。如果初始化后屏幕白屏或花屏,首先应该检查这两个参数。第三,SPI_NSS_Soft意味着片选(CS)由我们手动控制GPIO电平,而不是硬件自动控制,这样更灵活。
3.2 ILI9341的初始化序列
初始化屏幕就是通过SPI发送一系列预定义的命令和数据。网上有很多ILI9341的初始化代码,但直接复制可能不工作,因为不同厂家、不同批次的屏幕,其初始化参数可能有细微差别。最好的方法是找到屏幕卖家提供的资料或示例代码。
一个典型的初始化流程包括:硬件复位 -> 发送初始化命令序列 -> 设置显示方向、颜色模式等。这里提供一个核心的初始化函数框架:
void LCD_Init(void) { // 1. 硬件复位 LCD_RST_LOW(); delay_ms(100); // 保持低电平至少10ms LCD_RST_HIGH(); delay_ms(120); // 等待复位稳定 // 2. 发送初始化命令序列 LCD_Write_Cmd(0xCF); // 命令1 LCD_Write_Data(0x00); LCD_Write_Data(0xC1); LCD_Write_Data(0X30); // ... 发送更多命令和数据,通常有十几条 LCD_Write_Cmd(0x36); // 内存访问控制命令 // 设置屏幕方向,例如0x48为横屏,0x28为竖屏等 LCD_Write_Data(0x48); LCD_Write_Cmd(0x3A); // 像素格式设置 LCD_Write_Data(0x55); // 16位RGB565格式 LCD_Write_Cmd(0x11); // 退出睡眠模式 delay_ms(120); LCD_Write_Cmd(0x29); // 开启显示 // 3. 清屏为白色 LCD_Clear(WHITE); }其中,LCD_Write_Cmd和LCD_Write_Data函数的核心区别就是控制DC引脚的电平,然后调用同一个SPI发送函数。
void LCD_Write_Cmd(uint8_t cmd) { LCD_DC_LOW(); // 命令模式 LCD_CS_LOW(); // 选中设备 SPI1_SendByte(cmd); LCD_CS_HIGH(); } void LCD_Write_Data(uint8_t data) { LCD_DC_HIGH(); // 数据模式 LCD_CS_LOW(); SPI1_SendByte(data); LCD_CS_HIGH(); }SPI1_SendByte函数就是通过查询状态寄存器,等待发送缓冲区空,然后写入数据寄存器进行发送。
3.3 画点与填充函数实现
GUI库的底层需要屏幕的“画点”函数。对于ILI9341,画一个点的流程是:设置光标位置(即行列地址) -> 发送写内存命令 -> 发送像素颜色数据(16位RGB565)。
void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { // 设置X坐标 LCD_Write_Cmd(0x2A); LCD_Write_Data(x >> 8); LCD_Write_Data(x & 0xFF); LCD_Write_Data(x >> 8); LCD_Write_Data(x & 0xFF); // 设置Y坐标 LCD_Write_Cmd(0x2B); LCD_Write_Data(y >> 8); LCD_Write_Data(y & 0xFF); LCD_Write_Data(y >> 8); LCD_Write_Data(y & 0xFF); // 发送写内存命令 LCD_Write_Cmd(0x2C); // 发送颜色数据(16位,分两次8位发送) LCD_Write_Data(color >> 8); LCD_Write_Data(color & 0xFF); }基于画点函数,可以很容易实现矩形填充、画线、画圆等基础函数,这些将是LittleVGL的底层驱动接口。这里有一个效率陷阱:每次画点都重复发送设置地址的命令是非常低效的。优化方法是实现一个“设置窗口”函数,一次性设定好一块矩形区域,然后连续发送颜色数据,这在填充大面积颜色或显示图片时至关重要。
4. LittleVGL V6.0 移植详解
LittleVGL(LVGL)是一个资源消耗小、功能强大的开源嵌入式图形库。V6.0版本虽然已经不是最新,但非常稳定,资料丰富,适合学习。移植的核心是为LVGL提供必要的底层接口。
4.1 获取源码与文件结构
从官网或GitHub下载LVGL V6.0源码。我们需要关注以下几个目录/文件:
lvgl/:核心库源码。lvgl/examples/:示例。lvgl/porting/:这里存放移植模板。我们需要重点关注lv_port_disp_template.c和lv_port_indev_template.c(如果用到输入设备)。- 我们自己工程中,通常会在
User文件夹下创建lvgl目录,将上述源码拷贝进去,并添加头文件路径。
4.2 显示接口移植 (lv_port_disp)
这是最关键的一步。我们复制lv_port_disp_template.c和.h到我们的工程,重命名为lv_port_disp.c/h,并进行修改。
- 初始化函数
disp_init:在这里调用我们前面写好的LCD_Init()。 - 刷新函数
disp_flush:这是LVGL驱动显示的核心。当LVGL需要更新屏幕某一块区域时,会调用此函数,并传入一个lv_disp_drv_t指针、一个lv_area_t区域(定义了需要更新的矩形范围)和一个lv_color_t数组(包含了该区域所有像素的颜色数据)。 我们的任务就是把这个颜色数组的数据,搬运到屏幕的对应区域。
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { // 1. 根据area设置ILI9341的显示窗口 LCD_SetWindow(area->x1, area->y1, area->x2, area->y2); // 2. 将color_p数组中的颜色数据,通过SPI连续发送到屏幕 uint32_t size = (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1); LCD_Write_Cmd(0x2C); // 写内存命令 LCD_DC_HIGH(); LCD_CS_LOW(); for(uint32_t i = 0; i < size; i++) { uint16_t color = color_p[i].full; // 获取RGB565颜色值 SPI1_SendByte(color >> 8); SPI1_SendByte(color & 0xFF); } LCD_CS_HIGH(); // 3. 重要!通知LVGL刷新完成 lv_disp_flush_ready(disp_drv); }这里的关键优化点在于LCD_SetWindow函数和SPI的连续发送。LCD_SetWindow内部就是通过0x2A和0x2B命令设定行列地址范围。而SPI连续发送时,务必确保DC在发送数据期间保持高电平,且CS只在一次传输的开始和结束拉低/拉高,而不是每个字节都操作,这能极大提升速度。
- 注册显示驱动:在初始化LVGL后,需要调用一个注册函数。
void lv_port_disp_init(void) { static lv_disp_buf_t disp_buf; static lv_color_t buf[LV_HOR_RES_MAX * 10]; // 定义一个显示缓冲区 lv_disp_buf_init(&disp_buf, buf, NULL, LV_HOR_RES_MAX * 10); lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.flush_cb = disp_flush; disp_drv.buffer = &disp_buf; lv_disp_drv_register(&disp_drv); }这里创建了一个显示缓冲区buf。LVGL支持单缓冲、双缓冲甚至全屏缓冲。LV_HOR_RES_MAX * 10意味着缓冲区是10行像素高。当LVGL绘制的内容超过缓冲区大小时,它会分块调用disp_flush。双缓冲可以避免闪烁,但需要更多内存。对于STM32F103,RAM有限,需要根据UI复杂度和刷新率权衡缓冲区大小。
4.3 心跳与任务调度
LVGL需要周期性的心跳(lv_tick_inc())来处理动画、定时器等任务,以及执行任务处理器(lv_task_handler())。通常我们在SysTick中断中提供心跳,在主循环中调用任务处理器。
// 在SysTick中断服务函数中(1ms中断一次) void SysTick_Handler(void) { lv_tick_inc(1); } // 在主循环中 while(1) { lv_task_handler(); delay_ms(5); // 适当延时,避免CPU跑满 }4.4 基础测试与优化
完成移植后,首先创建一个简单的标签(Label)来测试。
lv_obj_t * label = lv_label_create(lv_scr_act(), NULL); lv_label_set_text(label, "Hello, LVGL!"); lv_obj_align(label, NULL, LV_ALIGN_CENTER, 0, 0);如果屏幕上能正确显示文字,恭喜你,移植成功了90%。接下来可能会遇到两个典型问题:第一,刷新速度慢,有肉眼可见的刷屏痕迹。这通常是因为SPI时钟设置过低,或者disp_flush函数中的数据传输没有优化(比如每个像素都单独操作CS和DC)。第二,显示错位或颜色不对。检查lv_conf.h中的屏幕分辨率(LV_HOR_RES_MAX,LV_VER_RES_MAX)和颜色深度(LV_COLOR_DEPTH)设置是否与你的屏幕匹配。对于ILI9341 RGB565,LV_COLOR_DEPTH应设置为16。
5. 性能瓶颈分析与SPI DMA优化
当UI元素变多,特别是需要滑动、动画时,使用查询方式的SPI传输可能会成为性能瓶颈,导致界面卡顿。CPU大量时间被阻塞在等待SPI发送完成上。此时,引入DMA(直接存储器访问)来搬运显示数据,让CPU腾出手来处理LVGL的逻辑,是质的提升。
5.1 SPI DMA发送配置
STM32的SPI TX可以关联到DMA通道。我们以DMA1的Channel3(对应SPI1_TX)为例。
void SPI1_DMA_Init(uint32_t buf_addr, uint16_t length) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(SPI1->DR); // 外设地址是SPI数据寄存器 DMA_InitStructure.DMA_MemoryBaseAddr = buf_addr; // 内存地址(颜色数据数组) DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 内存到外设 DMA_InitStructure.DMA_BufferSize = length; // 传输数据量 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址固定 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外设数据宽度8位 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 内存数据宽度16位(RGB565) DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 正常模式,传输一次 DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 不是内存到内存 DMA_Init(DMA1_Channel3, &DMA_InitStructure); // 使能SPI1的TX DMA请求 SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE); }这里有一个关键点:DMA_MemoryDataSize设置为半字(16位),而DMA_PeripheralDataSize设置为字节(8位)。因为我们的颜色数据在内存中是16位的(RGB565),但SPI数据寄存器是8位的,DMA会自动将每个16位数据拆分成两个8位传输。这要求我们的源数据数组是uint16_t类型。
5.2 改造disp_flush函数
使用DMA后,disp_flush函数不再需要for循环发送数据,而是启动DMA传输,并在DMA传输完成中断中通知LVGL刷新完成。
static volatile bool dma_transfer_done = false; void DMA1_Channel3_IRQHandler(void) { if(DMA_GetITStatus(DMA1_IT_TC3)) { DMA_ClearITPendingBit(DMA1_IT_TC3); dma_transfer_done = true; LCD_CS_HIGH(); // 传输完成,拉高CS } } static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { uint32_t size = (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1); LCD_SetWindow(area->x1, area->y1, area->x2, area->y2); LCD_Write_Cmd(0x2C); LCD_DC_HIGH(); LCD_CS_LOW(); dma_transfer_done = false; SPI1_DMA_Init((uint32_t)color_p, size * 2); // size*2是因为每个像素16位,DMA按字节计数 DMA_Cmd(DMA1_Channel3, ENABLE); // 等待DMA传输完成(也可以不等待,利用LVGL的双缓冲和回调) while(!dma_transfer_done); lv_disp_flush_ready(disp_drv); }注意:在DMA传输完成中断中才拉高CS,确保了整个数据块发送完毕。同时,在
disp_flush函数中等待dma_transfer_done标志,这种方式是阻塞的,但简单可靠。更高级的做法是利用LVGL的双缓冲机制和DMA传输完成回调,实现非阻塞刷新,从而获得更流畅的体验。
5.3 优化效果与权衡
启用DMA后,CPU占用率会显著下降,UI流畅度提升明显。但这也带来了复杂性:需要管理DMA中断、处理传输过程中的潜在错误(如下一帧数据覆盖当前正在传输的数据)。对于STM32F103,内存有限,如果使用双缓冲,意味着需要两倍于单缓冲的内存。你需要根据项目需求(UI复杂度、动画要求)和资源(RAM大小)来做出权衡。一个折中的方案是使用一个稍大的单缓冲区(例如屏幕高度的1/4),并配合DMA,这样既能减少刷新次数,又不会占用太多内存。
6. 调试技巧与常见问题排查
在整个开发过程中,调试是家常便饭。以下是我总结的一些实用技巧和常见问题的排查思路。
6.1 硬件层排查
屏幕无任何反应(背光可能亮):
- 电源:用万用表测量屏幕VCC和GND引脚电压是否为稳定的3.3V。
- 复位:用逻辑分析仪或示波器抓取RESET引脚波形,确保有低电平脉冲且持续时间足够。
- SPI信号:抓取SCK、MOSI、CS、DC的波形。首先看SCK是否有时钟输出,频率是否符合预期。然后看发送初始化命令时,DC电平是否在命令和数据间正确切换。最后对照ILI9341数据手册的SPI时序图,检查CPOL和CPHA设置是否正确。
屏幕全白、全黑或显示杂乱色块:
- 这通常是初始化序列不正确导致的。确保你使用的初始化代码与你的屏幕型号匹配。尝试注释掉部分初始化命令,看屏幕是否有变化,以定位问题命令。
- 检查
0x36(内存访问控制)命令的参数,它控制了显示方向、颜色顺序等。错误的设置会导致颜色错乱。 - 检查
0x3A(像素格式)命令,对于16位色,应该是0x55或0x66。
6.2 软件与驱动层排查
LVGL显示错位、只有部分区域更新或颜色异常:
lv_conf.h配置:反复检查LV_HOR_RES_MAX和LV_VER_RES_MAX是否严格等于你的屏幕分辨率(240和320)。LV_COLOR_DEPTH是否为16。disp_flush区域设置:在disp_flush函数开头,打印或通过LED指示area->x1, y1, x2, y2的值,看LVGL要求刷新的区域是否合理。确保你的LCD_SetWindow函数能正确设置该区域。- 颜色格式:LVGL默认使用RGB565,但字节序可能是小端或大端。在
lv_conf.h中可以通过LV_COLOR_16_SWAP宏来调整。如果颜色红蓝反了,可以尝试修改这个宏。
界面卡顿、刷新慢:
- SPI时钟:逐步提高SPI的预分频系数,测试最高稳定速度。
disp_flush效率:使用DMA。如果还用查询方式,确保发送大量数据时CS和DC只操作一次,而不是每个像素操作一次。- LVGL缓冲区:增大
lv_disp_buf_t的缓冲区大小。缓冲区越大,LVGL一次刷新能处理的区域就越大,调用disp_flush的次数就越少。但受限于RAM。 - 任务处理器调用频率:确保
lv_task_handler()在主循环中被频繁调用,延时不能太长(如delay_ms(100))。
内存不足导致崩溃: STM32F103RC只有48KB RAM。LVGL本身、显示缓冲区、你的应用程序变量都共享这块内存。
- 在
lv_conf.h中,减小LV_MEM_SIZE(LVGL动态内存池),但不要太小。 - 优化UI,减少同时存在的对象数量,及时删除不再使用的对象(
lv_obj_del())。 - 使用
lv_obj_set_style_local...替代lv_style_create来创建样式,后者会动态分配内存。 - 考虑使用内存较小的字体,或者将部分UI资源放到外部Flash,需要时再加载。
- 在
6.3 利用工具辅助调试
- 逻辑分析仪:是调试SPI、GPIO时序的利器,可以直观地看到命令、数据的每一位,是排查通信问题的终极手段。
- 串口打印:在关键函数入口、错误分支添加
printf,输出变量值、状态标志,是最简单有效的软件调试方法。注意,打印本身会占用时间,可能影响实时性。 - IO口模拟:在调试初期,可以先用GPIO模拟SPI时序来驱动屏幕,虽然慢,但可以完全控制时序,有助于理解通信过程,排除硬件SPI配置问题。
7. 项目总结与进阶思考
经过从硬件连接到底层驱动,再到GUI库移植和性能优化这一整套流程,一个基于STM32F103和LittleVGL的显示测试平台就搭建完成了。这个过程不仅仅是让一块屏幕亮起来,更是对嵌入式系统软硬件协同工作的一次深度实践。
回过头看,有几个体会特别深:第一,数据手册和时序图永远是最可靠的参考资料,尤其是在初始化阶段,网上的代码只能作为参考。第二,分层设计的思想非常重要,将硬件驱动(SPI、LCD)、中间件(LVGL移植层)、应用逻辑分开,使得代码结构清晰,调试和后续维护也更容易。第三,性能优化是一个持续的过程,需要根据实际应用场景在资源(内存、CPU)、功耗和效果(流畅度)之间找到平衡点。
在这个基础平台之上,还有很多可以深入探索的方向:例如,如果屏幕带触摸功能(通常是电阻屏或电容屏),下一步就是移植LVGL的输入设备接口(lv_port_indev),实现触控交互。再比如,可以尝试使用STM32的FSMC接口驱动并口屏,以获得更高的刷新率。或者,将LVGL升级到更新的版本(如V8.x),体验更丰富的控件和特效,但也要注意新版本对资源的需求可能更高。
最后,这个项目的所有代码和工程文件,我都整理在了开头的那个“.zip”压缩包里。里面包含了完整的Keil MDK工程、LVGL V6.0源码、以及我调试过程中用到的各种测试程序。希望这份结合了具体实践和踩坑经验的总结,能帮你更顺利地踏上嵌入式GUI开发的道路。遇到问题,多查资料,多动手测试,调试的过程本身就是最好的学习。
本文还有配套的精品资源,点击获取