ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32F103驱动ILI9341 SPI屏并移植LVGL V6.0全流程详解

STM32F103驱动ILI9341 SPI屏并移植LVGL V6.0全流程详解 简介本资源是一套基于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地GNDCS片选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但为了稳定起见尤其是在飞线连接时初期建议先设置一个较低的速度如9MHzPCLK2 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和1Mode 0或1和0Mode 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 移植详解LittleVGLLVGL是一个资源消耗小、功能强大的开源嵌入式图形库。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。双缓冲可以避免闪烁但需要更多内存。对于STM32F103RAM有限需要根据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 RGB565LV_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_SIZELVGL动态内存池但不要太小。优化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开发的道路。遇到问题多查资料多动手测试调试的过程本身就是最好的学习。本文还有配套的精品资源点击获取
返回列表