简介:面向中高级嵌入式开发者,这套示例工程演示了在ESP32 IDF环境下驱动ST7796屏幕并集成LVGL图形库的完整流程,同时加入FT6336触摸控制器,覆盖从SPI总线配置、屏幕参数初始化到触摸输入处理和图形界面刷新的各个环节。资源共24个文件,压缩包仅48KB,主要包含C语言源码(主程序与LVGL界面示例)、JSON格式的编辑器调试配置、TXT与Markdown说明文档、YAML/CMake构建辅助文件,以及运行时配置文件,布局完整且轻量。容器化构建配置还允许在隔离环境中复现工程,方便快速上手。整个项目以main目录为核心,搭配组件依赖和依赖锁文件,可视为一个规范且可移植的ESP-IDF工程样例,其中还保留了历史配置快照,便于对比参数调整。目前已有712人学习,适合希望参照现成代码理解LCD驱动与触摸接入,或基于此二次开发智能仪表、物联网设备交互界面的开发者。
1. 为什么IDF下驱动ST7796跑LVGL:画面卡死多半不在图形库
标题里的EPS32是ESP32的笔误,不过搜索这个词的人,基本都冲着同一件事来:一块ST7796驱动芯片的LCD屏,想在ESP32上用LVGL做出带控件、带动画的界面。ST7796是SPI接口的LCD控制芯片,常见于320x480的模组,价格便宜、资料却比ILI9341少一大截。IDF是乐鑫官方ESP-IDF框架,LVGL是嵌入式图形库。这三个词凑在一起,说明你要的不只是点屏,而是正经的图形界面。这个组合真正的难点不在LVGL本身,而在ST7796的驱动时序和IDF工程里组件化配置的衔接——我见过不少人在Arduino下跑通了,换到IDF就卡在spi_device_interface_config_t参数上。
2. 接线与工程初始化:先把ST7796点亮再谈LVGL
2.1 ST7796引脚与SPI接线:3.3V电平、背光和reset时序
ST7796模组的常见引脚是VCC、GND、CS、DC、RESET、SCLK、MOSI、BL(背光)。有的模块把DC标成RS,有的把BL单独引出,个别还带了MISO脚——但ST7796作为从设备,正常刷显存根本不需要读回,MISO可以空着。接线先确认电平:ESP32的IO是3.3V,ST7796的逻辑电平一般也是3.3V,但很多模组LCD供电是5V,板载稳压和电平转换已经做好,直接接3.3V没问题。如果用的是裸屏,就不要把5V怼到DC脚上,否则烧IO是分分钟的事。
背光引脚BL通常串一个电阻接3.3V或5V就能常亮。如果想做亮度调节,别用ESP32的GPIO直接驱动,LEDA引脚要的是几十毫安的电流,GPIO扛不住,加个三极管或者直接用模块上的背光控制。我一般图省事,开发阶段直接接高电平常亮,亮度问题放到LVGL这层用PWM做。另外,ST7796的复位时序是很多人翻车的第一个点:上电后电源稳定,RST拉低至少10us,再拉高,然后等120ms才能发初始化命令。如果上电就发初始化序列,或者ESP32复位后屏幕芯片没复位而你把初始化函数又跑了一遍,就会出现白屏或花屏。
| ST7796引脚 | ESP32 GPIO示例 | 说明 |
|---|---|---|
| CS | GPIO10 | SPI片选,低有效 |
| DC | GPIO12 | 命令/数据选择 |
| RESET | GPIO13 | 复位,低有效 |
| SCLK | GPIO11 | SPI时钟 |
| MOSI | GPIO9 | 主出从入 |
| BL | GPIO14 | 背光,可接PWM |
以上引脚是我常用的组合,实际以你的接线为准。注意ST7796有多个变体,常见的是320x480,也有240x320的型号,分辨率不同,后面的LVGL配置里hor_res和ver_res要跟着改。
2.2 用vs code esp idf插件新建工程:组件目录与IDF_TARGET配置
环境准备用vs code装Espressif IDF插件。插件首次启动会让你选择ESP-IDF下载路径,Windows下默认在%USERPROFILE%\.espressif,工具链、Python虚拟环境、目标芯片支持包都放在这里。如果之后手动开终端编译出现python环境不对,多半是没source插件生成的export.sh(Windows是export.ps1)。填路径有个经验:尽量只留一个ESP-IDF版本,插件支持多版本切换,但混着用容易让idf.py指向错误,编译报一些莫名其妙的头文件缺失。
工程目录我习惯这样组织,把ST7796驱动和LVGL都做成独立组件:
my_lvgl_st7796/ ├── main/ │ ├── CMakeLists.txt │ └── main.c ├── components/ │ ├── st7796/ │ │ ├── CMakeLists.txt │ │ ├── st7796.c │ │ └── st7796.h │ └── lvgl/ │ ├── CMakeLists.txt │ ├── lv_conf.h │ └── src/... └── CMakeLists.txtmain组件的CMakeLists.txt里这样写:idf_component_register(SRCS "main.c" INCLUDE_DIRS "." REQUIRES st7796 lvgl)。st7796组件负责SPI初始化和底层画点,lvgl组件不做任何改动,配置全在lv_conf.h里。这样组织的好处是后面要加触摸、要做OTA显示的进度界面,都是新增一个组件的事,不用回头重构main。
工程建好后,先不要碰LVGL,写一个最小SPI初始化,确认ST7796能收到波形再说。SPI总线初始化代码:
#include "driver/spi_master.h" #define PIN_NUM_MOSI 9 #define PIN_NUM_CLK 11 #define PIN_NUM_CS 10 #define PIN_NUM_DC 12 #define PIN_NUM_RST 13 spi_device_handle_t spi; void st7796_spi_init(void) { spi_bus_config_t buscfg = { .mosi_io_num = PIN_NUM_MOSI, .miso_io_num = -1, .sclk_io_num = PIN_NUM_CLK, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = 320 * 40 * 2 + 8, }; spi_device_interface_config_t devcfg = { .clock_speed_hz = 40 * 1000 * 1000, .mode = 0, .spics_io_num = PIN_NUM_CS, .queue_size = 4, .flags = SPI_DEVICE_HALFDUPLEX, }; spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO); spi_bus_add_device(SPI2_HOST, &devcfg, &spi); }说明几个参数:miso_io_num设为-1,因为ST7796不需要读。max_transfer_sz按一轮flush的最大字节数算,我用的是320列乘40行乘每像素2字节,再加8字节余量。clock_speed_hz先给40MHz,等驱动稳定后再试更高。mode=0对应SPI模式0,ST7796支持mode 0和mode 3,选0最常见。queue_size设为4,配合DMA可以缓存多笔事务。SPI_DMA_CH_AUTO让IDF自动分配DMA通道,这是IDF 5.x的推荐写法,如果是IDF 4.x,这里要写0或者手动指定DMA通道。SPI2_HOST在旧版本里叫VSPI_HOST,移植时注意名字。
3. 手写ST7796驱动:初始化序列、窗口函数与DMA flush
3.1 ST7796初始化序列与寄存器设置
网上ST7796的驱动不少,但很多是从ILI9341的例程改的,寄存器对不齐就会出现色偏、花屏或者纯白屏。ST7796和ILI9341的初始化序列确实接近,但MADCTL(0x36)、像素格式(0x3A)、电源控制(0xC0/0xC1)这些寄存器有差异。遇到花屏先别怀疑LVGL,先用最原始的方式确认:往DDRAM里写一行纯红色,如果整屏能显示纯色,再谈图形库。
我用的初始化序列结构是这样的,命令数组的格式是“命令、参数个数、参数”:
static const uint8_t st7796_init_cmd[] = { 0x11, 0, // Sleep out,之后要延时150ms 0x36, 1, 0x48, // MADCTL:RGB顺序、BGR 0x3A, 1, 0x55, // 16bit/pixel,RGB565 0xB0, 2, 0x00, 0x00,// Tearing Effect设置,部分屏需要 0xC0, 2, 0x00, 0x00,// 电源控制 0xC1, 2, 0x10, 0x10,// VCOM电压 0xC5, 2, 0x00, 0x00,// VGH/VGL设置 0xE8, 3, 0x00, 0x00, 0x00, // 显示偏置 0x29, 0, // Display ON };发送函数里先拉低DC发命令,再拉高DC发参数。特别注意0x11之后要延时150ms,0x29之后可以立即写显存。这些参数是通用占位值,不同批次、不同型号的屏幕可能微调——ST7796S和ST7796U的初始化序列就有细微差别。如果你的屏初始化后偏色,优先调电源和gamma相关寄存器,而不是去折腾LVGL的颜色格式。另有一种情况:有的模组出厂已经烧好了配置,你只需要0x11、0x36、0x3A、0x29四条就能点亮,其他命令反而帮倒忙。判断方法很笨但有效:逐条注释掉,看到底哪几条删了之后画面没变化。
3.2 画点、窗口与批量写像素:LVGL需要的底层接口
ST7796写像素有两种方式:一是设好列地址和行地址,然后连续批量写GRAM;二是每次写一个点都重新设地址。LVGL的flush_cb只调批量写接口,所以最小驱动只需三个函数:st7796_set_window(x0,y0,x1,y1)、st7796_write_pixels(buf,len)、以及底层的写命令/写数据函数。窗口函数的本质是往显存地址寄存器里写行列范围,然后在RAMWR指令下连续灌像素数据:
static void st7796_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { st7796_write_cmd(0x2A); // CASET 列地址 st7796_write_data(x0 >> 8); st7796_write_data(x0 & 0xFF); st7796_write_data(x1 >> 8); st7796_write_data(x1 & 0xFF); st7796_write_cmd(0x2B); // RASET 行地址 st7796_write_data(y0 >> 8); st7796_write_data(y0 & 0xFF); st7796_write_data(y1 >> 8); st7796_write_data(y1 & 0xFF); st7796_write_cmd(0x2C); // RAMWR 写显存 } static void st7796_write_pixels(uint16_t *buf, uint32_t len) { spi_transaction_t t = {0}; t.length = len * 16; // 单位是bit,不是byte t.tx_buffer = buf; spi_device_transmit(spi, &t); }这里有一个经典坑:spi_transaction_t的length单位是bit,不是byte。很多人从ILI9341的驱动移植过来,写成t.length = len,结果是每隔16个像素才发一个字节,画面变成竖直条纹。窗口坐标以像素为单位,LVGL传入的area坐标已经换算过了,所以这里直接按像素数乘以16算bit。
ST7796的GRAM写入方向由MADCTL的扫描方向位和窗口坐标共同决定。如果你的屏是320x480,但亮屏之后感觉是横的,问题基本不在初始化,而在MADCTL那一个字节。0x48不行就换0x88、0xE8、0xA8,逐个试到你想要的方向。
3.3 用SPI DMA异步传输flush数据
上面st7796_write_pixels用的是spi_device_transmit,会阻塞等待传输完成。LVGL一次flush大概传320x40x2=25KB数据,40MHz SPI下约5ms,如果每次都傻等,CPU浪费在等待上,帧率上不去。改进方式改成spi_device_queue_trans加传输完成回调:
static volatile bool spi_transfer_done = false; static void spi_transfer_cb(spi_transaction_t *t) { spi_transfer_done = true; } static void st7796_write_pixels_dma(uint16_t *buf, uint32_t len) { spi_transaction_t t = {0}; t.length = len * 16; t.tx_buffer = buf; t.user = (void *)&spi_transfer_done; spi_transfer_done = false; spi_device_queue_trans(spi, &t, portMAX_DELAY); while (!spi_transfer_done) { vTaskDelay(1); } }这个版本其实是DMA传输,但CPU仍然在等标志位,只是从等寄存器变成等DMA完成,好处是realsense的SPI状态机不再占用CPU轮询。真正异步的做法是把lv_disp_flush_is_done挪到传输完成回调里,flush_cb只负责入队就返回,让LVGL去渲染下一帧。这涉及LVGL的缓冲管理,下一章展开。
4. 移植LVGL到IDF:版本选择、disp_drv与FreeRTOS调度
4.1 LVGL选8.3还是9.6:配置方式与API差异
lvgl 8.3和9.6是目前用得最多的两个版本。9.x重构了配置系统和部分API,lv_conf.h里很多宏被移除,颜色管理也引入了新的lv_color_format_t。如果你搜到的教程是8.x的,直接套到9.6会编译不过。我的建议是:没有特殊需求直接上8.3。原因有三个:一是8.3的资料最多,遇到问题搜到的答案基本能直接用;二是LVGL官方在GitHub上的esp-idf示例工程目前仍推荐8.3;三是9.x的显示驱动接口对flush_cb的返回方式和缓冲区管理要求变化较大,新手阶段没必要趟这个复杂度。等你把8.3跑通,再看迁移文档,半天能转过去。
网上lvgl移植stm32的教程一抓一大把,但ESP32 IDF是另一回事——IDF的组件编译体系是CMake,lvgl源码必须作为组件加入。常见做法是把lvgl仓库放到components/lvgl,然后写一个CMakeLists.txt:
set(LV_CONF_KCONFIG_EXPAND 0) set(LV_CONF_PATH ${CMAKE_CURRENT_LIST_DIR}/lv_conf.h) idf_component_register(SRCS "src/lvgl.c" INCLUDE_DIRS "." "src")注意:LV_CONF_PATH必须指向自己的配置文件,否则lvgl会去找模板配置,默认配置的缓冲区小、时钟没开,你会看到屏幕只有残影。如果还不确定UI布局,先在vscode的lvgl模拟器里把界面调好再上板,比在真机上反复烧写省时间。
4.2 移植四件事:draw_buf、flush_cb、tick、task_handler
LVGL移植最少的四件套是:lv_disp_draw_buf_t(显示缓冲区)、lv_disp_drv_t(显示驱动结构体)、lv_tick_inc(心跳)、lv_task_handler(任务刷新)。先看核心代码:
static lv_disp_draw_buf_t disp_buf; static lv_disp_drv_t disp_drv; static lv_color_t buf[320 * 40]; static lv_color_t buf2[320 * 40]; static void disp_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { st7796_set_window(area->x1, area->y1, area->x2, area->y2); st7796_write_pixels_dma((uint16_t *)color_p, lv_area_get_width(area) * lv_area_get_height(area)); } void lvgl_port_init(void) { lv_init(); lv_disp_draw_buf_init(&disp_buf, buf, buf2, 320 * 40); lv_disp_drv_init(&disp_drv); disp_drv.hor_res = 320; disp_drv.ver_res = 480; disp_drv.flush_cb = disp_flush; disp_drv.draw_buf = &disp_buf; lv_disp_drv_register(&disp_drv); }正常情况下,lv_disp_flush_is_done不是在flush_cb里直接调用,而是在ST7796的DMA传输完成回调里调用。如果直接在flush_cb里调用,等于告诉LVGL“缓冲区已经空了”,LVGL会在下一轮刷新时直接覆盖color_p指向的缓冲区,而DMA可能还在读这个缓冲区的数据——lvgl画面撕裂就是这么来的,这是最常见的原因,后面避坑章还会重点说。
前面代码里st7796_write_pixels_dma内部还在等待DMA完成,所以这种情况下直接调lv_disp_flush_is_done不会撕裂,但性能差。更好的做法是给st7796组件增加注册回调的能力,把drv指针挂到transaction上:
static void spi_transfer_cb(spi_transaction_t *t) { lv_disp_flush_is_done((lv_disp_drv_t *)t->user); } static void disp_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { st7796_set_window(area->x1, area->y1, area->x2, area->y2); spi_transaction_t t = {0}; t.length = lv_area_get_width(area) * lv_area_get_height(area) * 16; t.tx_buffer = color_p; t.user = drv; spi_device_queue_trans(spi, &t, portMAX_DELAY); }flush_cb立刻返回,LVGL在DMA传buf1时去渲染buf2,CPU和DMA并行。但有个前提:
提示
LVGL必须启用双缓冲,flush完成通知才安全。单缓冲情况下,
lv_disp_flush_is_done没被调用,LVGL会死等下一轮。
tick实现很简单,单独建一个FreeRTOS任务喂心跳:
void lv_tick_task(void *arg) { while (1) { lv_tick_inc(1); vTaskDelay(pdMS_TO_TICKS(1)); } }task_handler调用也建议放任务里,10ms周期够了:
void lvgl_task(void *arg) { while (1) { lv_task_handler(); vTaskDelay(pdMS_TO_TICKS(5)); } }freertos移植lvgl的关键就是这两个任务:一个喂心跳,一个跑handler。优先级给2或3就行,LVGL渲染不是实时任务,偶尔延迟几毫秒看不出来。
4.3 lv_conf.h关键参数:内存、颜色深度与缓冲区
lv_conf.h里有几个参数决定整个方案能不能跑稳:
| 宏 | 推荐值 | 说明 |
|---|---|---|
| LV_COLOR_DEPTH | 16 | RGB565,省一半传输量 |
| LV_MEM_SIZE | 64 * 1024 | LVGL内部动态内存 |
| LV_DISP_DEF_REFR_PERIOD | 30 | 刷新周期,单位ms |
| LV_USE_LOG | 0 | 关掉LVGL日志,避免和IDF日志混淆 |
| LV_TICK_CUSTOM | 0 | 使用外部tick,即lv_tick_inc |
| LV_USE_DEMO | 1 | 开demo方便验证,跑通后关掉 |
LV_MEM_SIZE是被忽略得最多的一个。一个button对象几百字节,一个label加文字缓冲区也不少,设成8KB的话,控件一多就内存不足,表现是界面画到一半卡住或者控件莫名消失。64KB对ESP32来说不算多,ESP32的堆内存通常还剩300多KB。缓冲区buf和buf2各320x40x2=25KB,加上LV_MEM_SIZE,总共一百多KB,在ESP32上没压力。
LV_COLOR_DEPTH选16时,lv_color_t占2字节,buff数组大小按这个算。如果把颜色深度改成32,buff容量要翻倍,DMA传输量也翻倍,全屏一次刷新约600KB,SPI带宽就成瓶颈。所以不是做特殊颜色效果就坚持16位色。
5. ST7796+LVGL避坑记录:画面撕裂、日志函数重复定义与中文显示
5.1 画面撕裂:DMA异步和lv_disp_flush_is_done的调用时机
现象是:快速刷新界面时屏幕出现横向错位,顶部和底部画面不同步,滑动列表时能明显看到半帧切割。原因前面说过——flush_cb里用spi_device_transmit阻塞等待完成后才通知LVGL,性能差;改成异步后,在DMA没完成时就通知LVGL缓冲区释放了,于是LVGL把新内容写进同一个缓冲区,DMA还在读旧数据,画面就花了。
彻底解决要满足两个条件:双缓冲加DMA回调里通知。双缓冲保证LVGL渲染新一帧时写在buf2,而DMA传输的是buf1的数据,两者互不覆盖;DMA回调保证LVGL知道buf1已安全释放。代码就是4.2节的第二个版本:flush_cb只负责queue_trans,回调函数调用lv_disp_flush_is_done。如果坚持单缓冲,就必须保留等待DMA完成的逻辑,没有第三条路。
还有一个容易忽略的“撕裂”来源是字节序。ST7796期望RGB565高字节在前,而ESP32是little-endian,lv_color_t在内存里是低字节在前。如果画面颜色发紫或红蓝互换,这不是撕裂,但比撕裂更隐蔽。排查顺序:先查MADCTL的BGR位,再查flush_cb里是否做了字节交换,最后查DMA缓冲区的对齐。ESP32的SPI外设要求DMA传输缓冲区4字节对齐,lv_color_t静态数组默认没问题,但如果你用动态内存分配缓冲区,要手动对齐,否则IDF会报invalid dma buffer。
5.2 LVGL打印函数与IDF log的重复定义
现象:编译时出现printf或log相关函数重复定义,或者运行时LVGL日志和IDF日志同时刷屏,用ESP_LOG的等级过滤还拦不住LVGL的输出。
原因:lv_conf.h里LV_USE_LOG默认是1,LVGL日志默认通过printf输出。在ESP-IDF环境下,printf会被newlib重定向到串口,但不像ESP_LOG那样带等级标签。如果你在多个源文件里include了lvgl.h,并且LVGL日志等级又设成trace,串口会被大量日志淹没,拖慢刷新速度。
解决:最简单的直接#define LV_USE_LOG 0。如果确实需要看LVGL内部日志,自定义输出宏:
#define LV_USE_LOG 1 #define LV_LOG_PRINTF(level, format, ...) \ esp_log_write(ESP_LOG_WARN, "LVGL", format, ##__VA_ARGS__)发布前把LV_USE_LOG关掉是血泪经验。串口带宽在高速刷新时很宝贵,日志本身能拖慢3到5帧每秒,用户感觉就是界面“肉”。
5.3 中文显示与字体瘦身
LVGL显示中文不像是电脑上装个字体就完事。LVGL默认字体只覆盖ASCII,中文必须预先生成位图字体。工具用lv_font_conv即可,网页版或本地node脚本都行。核心思路:只用项目实际出现的汉字生成字体,不要转整个国标字库。一个16x16的汉字,按4bpp压完约20字节,300个字也就6KB,但3000常用字就直接300KB以上,Flash不是问题,运行时驻留内存才是问题。所以先把UI文案定下来,提取去重后再转字体。
代码里使用自定义字体:
LV_FONT_DECLARE(lv_font_cn_16); lv_obj_t *label = lv_label_create(lv_scr_act()); lv_obj_set_style_text_font(label, &lv_font_cn_16, 0); lv_label_set_text(label, "你好,ESP32");lv_font_cn_16是生成的C数组,编译进固件。字体文件生成后,注意源文件编码必须是UTF-8,转换工具生成的C文件里包含非ASCII字符串字面量,用其他IDE改过编码之后,编译出来字体就是乱码。字符串本身也要求UTF-8,从旧工程复制过来的GB2312字符串,显示出来全是乱码。所谓lvgl文字优化,在嵌入式场景主要做减法:缩小字符集、降低位深、必要时用字体缓存,而不是去调整抗锯齿。
5.4 老代码迁移:v7.11改背景色到8.x的API差异
如果你从网上找到了LVGL v7.11的例程,改背景色是这样写的:
lv_obj_set_style_local_bg_color(lv_scr_act(), LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);放到8.3编译直接报错。8.x重构了样式API,同样的效果要写:
lv_obj_set_style_bg_color(lv_scr_act(), lv_color_hex(0xFFFFFF), 0);这是LVGL从7到8最伤筋动骨的变化:local style被移除,所有样式统一走lv_obj_set_style_*系列接口。除了背景色,还会遇到控件名重命名,比如lv_ta变lv_textarea,lv_btnmatrix和lv_btn的职责也变了。迁移老工程别逐行改,先把官方Breaking Changes文档里的控件名对照表打出来,因为这种改动编译器帮不了你,只能肉眼扫。另外,7.x的lv_disp_buf_t在8.x变成lv_disp_draw_buf_t,参数数量一样,名字变了。你搜lvgl 8.3 9.6对比时能看到,这两个大版本之间还有类似的破坏性改动,所以工程一旦选定版本,就整体别换。
6. 性能调优与进阶验证:帧率统计、动画容器与OTA共存
6.1 用计数器测真实帧率
验证LVGL是否流畅,别用眼睛感受,用数字说话。在flush_cb里加一个计数器每秒打印一次,比看lv_task_handler的调用次数真实得多:
static uint32_t flush_count = 0; // 在disp_flush()末尾 flush_count++; if (xTaskGetTickCount() - last_sec >= 1000) { ESP_LOGI("FPS", "%lu", flush_count); flush_count = 0; last_sec = xTaskGetTickCount(); }注意flush_cb调用次数并不等于屏幕上完整刷新的次数,因为LVGL可能只刷新一个局部区域,比如按钮按下的反馈。但在全屏动画场景下,flush_cb频率基本等于帧率。320x480全屏RGB565刷新一次数据量300KB,40MHz SPI理论传输约7.5ms,算上命令开销,稳定在20到30fps是合理的。如果低于15fps,优先检查SPI时钟是不是被降了,或者LV_COLOR_DEPTH被改成32。
6.2 动画容器和OTA共存的小技巧
lvgl过渡动画用lv_anim实现,比如让控件从屏幕外滑入:lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_x),配合set_var和set_time就能跑。lvgl容器在8.3里配合flex布局很好用,lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW)一行代码搞定水平排列,省去手动计算坐标的麻烦。动画加容器,UI质感提升明显,代价只是LV_MEM_SIZE多给几KB。如果对界面布局没把握,先用vscode模拟器搭出原型再上板,比反复烧写效率高一截。
做esp32 ota idf升级时有个小坑:如果要显示升级进度条,注意lv_task_handler不能被网络任务的阻塞调用挡住。我建议把LVGL刷新任务优先级提到略高于OTA下载任务,同时OTA回调里不要做长时间阻塞操作。我在一个充电桩面板项目里遇到过OTA进度条假死,排查下来是HTTP下载霸占了CPU,把lvgl任务临时提优先级才解决。这些经验说不上多高级,但确实能让人少走几个月弯路。希望帮到你。
本文还有配套的精品资源,点击获取