十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ESP32S3+LVGL9驱动SPI屏全攻略:从ST7789到ILI9488

ESP32S3+LVGL9驱动SPI屏全攻略:从ST7789到ILI9488 简介本资源是一套专为ESP32-S3平台适配LVGL v9图形库的SPI LCD驱动代码集合面向嵌入式开发工程师、物联网项目开发者及LVGL初学者解决多型号屏幕在ESP-IDF环境下快速移植与稳定显示的核心痛点。压缩包共13个文件29KB涵盖核心驱动源码lcd_lvgl.c、硬件抽象层头文件config.h、构建配置CMakeLists.txt、sdkconfig、idf_component.yml、开发环境支持.vscode配置、项目说明README.md及分区表partitions.csv等关键组件结构清晰、开箱即用。已有950人学习下载代码已实测兼容ST7789、ST7735、ST7796、ILI9341和ILI9488五类主流SPI屏覆盖135×240至480×320等多种分辨率显著降低LVGL在ESP32-S3上的显示适配门槛助力用户聚焦UI设计与业务逻辑开发。 ESP32S3 配 LVGL9再把 SPI 接口的 ST7789、ST7735、ST7796、ILI9341、ILI9488 这些常见屏全跑起来这事我前后折腾了挺长时间踩过的坑最后都变成了经验。如果你正在做小尺寸 HMI、桌面摆件、温湿度计、甚至开源手表之类的东西这篇文章基本能把你从“不知道从哪下手”带到“一次点亮并流畅跑动画”的阶段。先说结论ESP32S3 的 SPIn 外设足够强LVGL9 的接口比 LVGL8 清晰不少真正会卡人的地方反而不是 LVGL 本身而是屏幕 IC 的初始化序列、偏移量设置、以及 DMA 传输的同步方式。这篇文章不会只贴一份 ST7789 的代码就完事我会把常见屏的差异、LVGL9 的 display 驱动结构、SPI 参数选择、以及花屏和偏移的排查方法全部拆开讲。1. 整体设计思路为什么这个组合值得写1.1 ESP32S3 在 GUI 项目里的定位ESP32S3 跟早期的 ESP32 比最大的变化是增加了向量指令和 SIMD 支持虽然主频还是 240MHz但在跑 LVGL 这种 2D 图形库时软件渲染的功耗和流畅度都会好一些。关键是这颗芯片的 SPIn 控制器支持 DMA最大传输数据量可以做到按描述符走完一整块显存数据不需要 CPU 一点点往外挤。这个是决定你能不能跑“高分屏动画”的核心。很多人会问那为什么不用 ESP32-S3 直接接 RGB 并口屏因为 RGB 屏需要的引脚太多而且需要 PSRAM 配合帧缓冲成本也比 SPI 屏高一截。SPI 屏虽然带宽有限但胜在引脚少6 根线搞定、驱动 IC 成熟、资料多而且 LVGL9 本身对低带宽的优化做得不错缓存策略合理的话日常刷新 320x240 的屏没有任何压力。1.2 为什么选择 LVGL9 而不是继续用 LVGL8LVGL9 改了不少底层接口最大的变化是把显示驱动从lv_disp_drv_t重构为lv_display_t注册方式、buffer 处理、flush 回调的触发逻辑都有调整。网上大量教程还停留在 LVGL8 甚至 LVGL7直接抄代码在 LVGL9 下面没法编译。这篇文章所有的代码都基于 LVGL9.x你拿最新的 release 过来可以直接用。还有一点就是 LVGL9 对 partial refresh局部刷新的处理更聪明了它不再要求每次全屏 flush而是会计算 dirty area配合 ESP32S3 的 DMA实际跑起来帧率比 LVGL8 有明显提升。所以如果你是新项目别犹豫直接用 LVGL9。2. LVGL9 的显示驱动接口变化2.1 LVGL8 到 LVGL9 哪些旧代码不能抄LVGL8 时代典型的初始化代码长这样static lv_disp_draw_buf_t draw_buf; static lv_color_t buf[240 * 10]; lv_disp_draw_buf_init(draw_buf, buf, NULL, 240 * 10); static lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.flush_cb my_flush_cb; disp_drv.draw_buf draw_buf; lv_disp_drv_register(disp_drv);到了 LVGL9这段代码里的lv_disp_drv_t已经被拆掉统一走lv_display_tstatic lv_display_t *disp; disp lv_display_create(240, 320); lv_display_set_flush_cb(disp, my_flush_cb); lv_display_set_buffers(disp, buf1, buf2, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL);注意 LVGL9 里 buffer 是用字节数传入的不是用像素数。而且有两个 buffer 的话就是双缓冲LVGL9 会在后台渲染一个 buffer 的同时把另一个 buffer 通过 flush_cb 送出去。这个机制是流畅度的关键。2.2 flush_cb 到底该怎么写LVGL9 的 flush_cb 签名是void my_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map);它比 LVGL8 更直接第三个参数直接给你渲染好的像素数据指针px_map就是你要通过 SPI 发给屏幕的内容。你需要做的是把px_map里的数据切分成 16 位的 RGB565 数据通过 SPI 发出去发送完成后调用lv_display_flush_ready(disp)告诉 LVGL 可以继续渲染下一帧。这里最容易犯错的是LVGL9 默认的像素格式是 RGB565但很多屏幕尤其是国产屏内部实际是按 BGR 方式排列的你需要额外配置LV_COLOR_16_SWAP或者自己转换字节顺序否则颜色会明显偏色。px_map是像素数据不是完整的“要发送的 frame buffer”它只是 dirty area 的那部分。你拿到area结构体后要自己计算出显存地址偏移然后把 area 的坐标换算成屏幕的起始 column 和 row通过 SPI 写命令设置 window再发送像素数据。这一步是整个驱动里最容易写错的地方。3. 常见 LCD 驱动 IC 适配记录3.1 ST7789现在用得最多的“街屏”ST7789 是当前 1.3 寸到 2.0 寸小屏里最常见的驱动 IC分辨率全覆盖240x240、240x320、320x480 甚至部分圆屏。它支持 RGB565 和 RGB666 两种格式通常我们走 RGB565。ST7789 的一个特点是它没有单独的VCOM引脚初始化序列较短而且很多模组已经把 madctl内存访问控制的翻转电路做在 PCB 上了所以你拿到屏之后可能需要在代码里根据实际显示方向调整MADCTL寄存器的值。初始化序列里有一个坑ST7789 在 240x320 和 240x240 屏上的 row/column 偏移不一样。大部分 240x240 的模组其实都是 240x320 的玻璃只是为了做方形显示而已驱动 IC 内部仍然有 320 行的寻址空间所以你需要把 row offset 设置成 80。也就是说当你要刷新第 0 行的时候实际上要写命令到 row 80 开始的位置。这个偏移量不确定时可以查模组资料或者通过反复改 x/y offset 看屏幕显示情况来确定。3.2 ST7735小尺寸花屏重灾区ST7735 常见于 1.8 寸 128x160 和 1.44 寸 128x128是 ST7789 的“老前辈”。ST7735 的初始化序列更麻烦因为它有多个版本A/B/C不同版本的 gamma 曲线和偏压配置不完全一样。如果初始化不对最常见的现象是白屏或者显示重影。ST7735 的另一个坑是它的 MADCTL 默认扫描方向和很多 128x160 模组的实际接线不匹配你经常需要在代码里反转 RGB 顺序MADCTL中的 BGR 位才能得到正确的颜色如果颜色反了把MADCTL的 BGR 位从 0 改成 1 或者反过来即可。ST7735 的偏移量128x160 屏通常没有偏移问题但 128x128 方屏需要把 row offset 设成 2也就是数据区域从第 2 行开始。如果没有设置这个偏移图形会整体向下偏两行像一条细线出现在顶部。3.3 ST7796 和 ILI9341大屏兼容性ST7796 和 ILI9341 都常见于 2.4 寸和 2.8 寸的 320x240 屏这两颗 IC 的初始化序列有很多相似之处网上也经常有人用同一套初始化序列去兼容两种情况但严格来说还是不一样。ILI9341 是最经典的 SPI LCD 驱动 IC几乎所有单片机教程都会提到它。它的初始化序列很长但兼容性很好如果你不确定自己手里的屏是哪颗 IC先用 ILI9341 的序列试着点亮成功的概率很高。ST7796 跟 ILI9341 默认的 gamma 不同ST7796 的色彩更鲜艳一些但如果你把 ILI9341 的 gamma 设置到了 ST7796 上屏幕可能会发白或者发灰。我实测过如果你的屏是 ST7796 而用了 ILI9341 的初始化序列部分屏能显示但对比度明显差一截建议还是找对应 IC 的初始化序列来跑。3.4 ILI9488RGB666 的坑ILI9488 常见于 3.5 寸 480x320 屏有一个大坑它默认是 RGB666 格式不是 RGB565。如果你直接把 LVGL 的 RGB565 buffer 发给它显示颜色会明显不对几乎是扭曲的。解决办法有两种初始化序列里切换到 RGB565 模式如果 IC 支持具体操作是在COLMOD3Ah 命令里设置 16 位色深。如果你的屏模块不支持 RGB565 模式就得在 flush_cb 里把 RGB565 转换成 RGB666。但这样不仅费 CPU还会降低帧率。所以我一般不建议在 ESP32S3 上用 ILI9488除非你确实需要大屏且没有替代方案。下面是这几颗 IC 的直观对比表方便你快速定位自己手上的屏。IC 型号常见分辨率色深支持常见偏移问题初始化复杂度实测推荐度ST7789240x240 / 240x320RGB565 / RGB666240x240 需 row offset 80低高ST7735128x160 / 128x128RGB565128x128 需 row offset 2中中ST7796320x240RGB565基本无低高ILI9341320x240RGB565基本无中高ILI9488480x320RGB666 默认色深转换麻烦高低4. 实操完整驱动配置与初始化4.1 引脚分配与接线ESP32S3 的 SPIn 外设很灵活GPIO 矩阵允许你几乎把 SPI 信号映射到任意引脚。但为了 DMA 性能和信号稳定我个人建议把 SCLK、MOSI 固定到一组相邻引脚上MISO 不用接LCD 输出我们一般不读。我常用的引脚分配是这样信号GPIO说明SCLKGPIO12SPI 时钟MOSIGPIO11SPI 数据输出CSGPIO10片选DCGPIO9数据/命令选择RSTGPIO8复位BLGPIO45背光控制每个人手里的板子不一样引脚完全可以根据自己的布局换。注意 DC 引脚很关键LCD 通过 DC 来区分命令和数据如果你的 DC 接错屏幕能亮但显示内容会是一堆乱码。RST 可以用 GPIO 也可以用 ESP32S3 的上电自动复位电路但手动控制更可靠。SPI LCD 常用 7 根线包含 GND 和 VCC有些模组是 6 针省掉了 RST 或 BL这就意味着 RST 要靠寄存器软复位、BL 要一直拉高在代码里要注意别去操作不存在的引脚。4.2 SPI 初始化配置使用 ESP-IDF 的spi_bus_initialize来初始化 SPI 主机ESP32S3 有 SPI2 和 SPI3 两个可用主机LCD 接到 SPI2 比较常见spi_bus_config_t bus_cfg { .sclk_io_num 12, .mosi_io_num 11, .miso_io_num -1, .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz 240 * 320 * 2 8, }; spi_bus_initialize(SPI2_HOST, bus_cfg, SPI_DMA_CH_AUTO);重点在max_transfer_sz这个值决定了 DMA 能一次传输的最大字节数。如果你要直接传输整个 320x240 的 RGB565 frame153600 字节这里必须留够空间。我一般会设成屏幕宽 * 屏幕高 * 2 8留一点余量。SPI_DMA_CH_AUTO让驱动自动分配 DMA 通道ESP32S3 上可用 DMA 通道比老 ESP32 多这个建议别手动指定让系统自动管理。4.3 LCD 初始化序列与偏移量设置先把 GPIO 和 SPI 总线准备好然后对 LCD 进行复位和初始化。下面是一份标准 ST7789 初始化序列的核心部分适用于 240x240 或 240x320 屏// 复位 gpio_set_level(RST_PIN, 0); vTaskDelay(pdMS_TO_TICKS(20)); gpio_set_level(RST_PIN, 1); vTaskDelay(pdMS_TO_TICKS(120)); // 初始化命令 lcd_cmd(0x01); // SWRESET vTaskDelay(pdMS_TO_TICKS(20)); lcd_cmd(0x11); // SLPOUT vTaskDelay(pdMS_TO_TICKS(120)); lcd_cmd(0x3A); // COLMOD lcd_data(0x05); // 16-bit color // 设置偏移 uint16_t x_offset 0; uint16_t y_offset 80; // 240x240 屏常见偏移 lcd_cmd(0x2A); // CASET lcd_data(x_offset 8); lcd_data(x_offset 0xFF); lcd_data((x_offset width - 1) 8); lcd_data((x_offset width - 1) 0xFF); lcd_cmd(0x2B); // RASET lcd_data(y_offset 8); lcd_data(y_offset 0xFF); lcd_data((y_offset height - 1) 8); lcd_data((y_offset height - 1) 0xFF); lcd_cmd(0x21); // INVON lcd_cmd(0x11); vTaskDelay(pdMS_TO_TICKS(20)); lcd_cmd(0x29); // DISPON这里最重要的就是CASET和RASET也就是 column address set 和 row address set。LVGL 告诉你需要刷新窗口的左上角和右下角坐标你要把这两个坐标加上偏移量后换算成屏幕内部显存地址。如果偏移量设置有误显示的内容会整体偏移轻则边缘出现黑边重则内容错位或被截断。4.4 LVGL9 display 注册代码初始化完 LCD 之后把 display 注册到 LVGL9 里static lv_color_t buf1[240 * 10]; static lv_color_t buf2[240 * 10]; lv_display_t *disp lv_display_create(240, 320); lv_display_set_flush_cb(disp, lcd_flush_cb); lv_display_set_buffers(disp, buf1, buf2, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL);注意两个 buffer 各是240*10像素换算成 RGB565 就是 4800 字节。这个 buffer 大小是我实测下来性能和内存占用的平衡点太小会导致 LVGL 频繁刷新太大又浪费内存。如果你开了 PSRAM 且不在意内存可以把 buffer 扩大到一行像素的 2~4 倍帧率会更好。flush 回调里需要根据 LVGL 传入的 area 来控制 LCD 的 window。假设我们的屏是 240x320LVGL 的坐标原点在左上角物理屏的坐标原点也在左上角void lcd_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { uint16_t x1 area-x1; uint16_t x2 area-x2; uint16_t y1 area-y1; uint16_t y2 area-y2; uint16_t w x2 - x1 1; uint16_t h y2 - y1 1; // 设置 window lcd_cmd(0x2A); // CASET lcd_data((x1 x_offset) 8); lcd_data((x1 x_offset) 0xFF); lcd_data((x2 x_offset) 8); lcd_data((x2 x_offset) 0xFF); lcd_cmd(0x2B); // RASET lcd_data((y1 y_offset) 8); lcd_data((y1 y_offset) 0xFF); lcd_data((y2 y_offset) 8); lcd_data((y2 y_offset) 0xFF); lcd_cmd(0x2C); // RAMWR // 发送像素数据 spi_transaction_t t { .length w * h * 2 * 8, .tx_buffer px_map, }; spi_device_polling_transmit(spi_dev, t); lv_display_flush_ready(disp); }spi_device_polling_transmit是阻塞式发送简单但会浪费 CPU。更好的方式是使用spi_device_queue_trans配合传输完成中断在中断回调里调用lv_display_flush_ready。不过这套异步逻辑对初学者不太友好我建议先把阻塞式跑通再改成 DMA 中断。5. 性能优化DMA、双缓冲、帧率调整5.1 SPI DMA 为什么是刚需如果不使用 DMA每次发送像素数据都要 CPU 把数据搬到 SPI 外设的 FIFO320x240 一帧 153600 字节按每字节 8 个 SPI 时钟算纯 SPI 传输就需要约 240 万次外设写入操作。即使 SPI 跑到 80MHzCPU 也会被 SPI 中断占满LVGL 的渲染时间被严重挤压。ESP32S3 的 SPI 主机支持 DMA 方式发送你需要用spi_device_queue_trans接口或者在初始化 SPI 设备时把spi_device_interface_config_t里的.flags设置上SPI_DEVICE_HALFDUPLEX和SPI_DEVICE_NO_DUMMY。发送大块 buffer 时驱动会自动拆分成 DMA 描述符传输过程中 CPU 可以继续做下一帧渲染。我用 80MHz SPI 时钟 DMA 的方式跑 240x320 的 ST7789LVGL9 的动画帧率能到 50fps 左右简单场景日常 UI 操作也不卡。如果 SPI 时钟降为 40MHz帧率会掉到一半感官上就明显“肉”了。5.2 双缓冲与 LVGL9 的渲染模式LVGL9 的双缓冲机制比 LVGL8 要顺畅原因在于它的渲染任务和 flush 任务可以并行。你在lv_display_set_buffers里传入两个 buffer 后LVGL 会在 buffer 1 上渲染渲染完成触发 flush_cb然后立刻开始在 buffer 2 上渲染下一帧DMA 传输 buffer 1 的同时CPU 在渲染 buffer 2等于把传输时间和渲染时间重叠起来了。如果你的内存紧张可以退化成单缓冲但代价是每次渲染完必须等 SPI 发送完GPU 和 IO 完全串行流畅度会明显下降。所以我建议哪怕是牺牲一点 buffer 大小也要保留双缓冲。5.3 SPI 时钟频率与传输优化ESP32S3 的 SPI 时钟在驱动 LCD 时通常可以跑到 60~80MHz但不是所有屏都能稳定跑 80MHz。如果你的屏模组走线很长或者使用了杜邦线连接跑 80MHz 大概率会出现随机花屏或条纹。这时候不要怀疑代码先把 SPI 时钟降到 40MHz 试试90% 的“花屏”都能好。另外SPI 设备初始化时有一个clock_source参数。ESP32S3 上可以选择SPI_CLK_SRC_DEFAULT或SPI_CLK_SRC_XTAL。我实测发现用外部晶振作为 SPI 时钟源在高温环境下更稳定。不过这个差异很小一般不需要专门改。6. 常见问题与排查技巧实录6.1 花屏 / 显示错位花屏是一大类问题我把它拆成几个具体现象现象大概率原因排查方法全屏噪点 / 雪花SPI 时序不稳或频率太高降低 SPI 时钟到 20MHz 测试显示内容偏移 80 行ST7789 240x240 屏 offset 未设置加 row offset 80局部区域内容错位window 设置不正确检查 CASET/RASET 是否使用了正确的 x_offset / y_offset顶部有一条彩色线偏移量少了 1~2 像素调整 row/column offset 的细微值刷新时撕裂单缓冲 高速刷新改用双缓冲花屏排查有一个通用技巧先用纯色填充命令来测试 LCD 本身是否正常。如果你调用lcd_cmd(0x2C)后直接发送 2 字节的红色像素数据屏幕能正常显示红色说明 LCD 和 SPI 底层没问题问题出在 LVGL 的坐标或 buffer 管理上。6.2 颜色不对颜色不对的原因主要有两个RGB 顺序不对如果你的红色显示成蓝色、绿色显示成紫色多半是 RGB 与 BGR 顺序反了。在初始化序列里尝试把 MADCTL36h中的 BGR 位翻转或者设置LV_COLOR_16_SWAP 1取决于你的屏和 ESP32S3 的字节序。RGB565 与 RGB666 混用ST7789 默认 RGB565 没问题但 ILI9488 初始状态往往是 RGB666如果不显式切换颜色会整体发灰需要手动设置 COLMOD 寄存器。另外ESP32S3 是小端 CPU当你把 RGB565 颜色存入内存时字节序是反的。LVGL9 默认会处理这个但如果你直接输出了裸 buffer可能就要手动 swap 一下高低字节。这个看具体编译选项不需要死记先跑起来再根据现象调。6.3 亮度、闪烁背光控制建议用 PWM 引脚不要直接 GPIO 拉高拉低。一级线性调光时ESP32S3 的 LEDC 外设就能胜任。接线时把 BL 接到 LEDC 输出引脚即可。闪烁问题则大概率不是背光的问题而是 LVGL 的刷新频率过低或 buffer 太小。比如 240x320 的屏只给了 240x4 的 bufferLVGL 就得把屏幕切成几十个局部区域刷新人眼看起来就像一直在闪。这时候加大 buffer、关闭 LVGL 的LV_DISPLAY_RENDER_MODE_DRAW_UNIT或者调整刷新周期都会有效果。6.4 初始化不成功白屏白屏通常说明 LCD 已经上电、背光也亮了但没有收到有效的初始化序列或命令参数不对。先检查 RST 和 DC 引脚是否连接正确然后用逻辑分析仪抓 SPI 信号确认 MOSI 上有没有数据、SCLK 有没有时钟。如果手头没有逻辑分析仪就写一个最简单的循环反复发送0x2C命令和全红像素数据。如果屏幕有反应但颜色不对说明 SPI 通信正常只是初始化序列或者 RGB 配置有问题。这个循环排查法能快速把问题从“通信”和“时序配置”里分离出来。7. 我的实际配置参考与最后提醒最后给一套我用着比较顺手的模板配置你可以直接照着抄一部分。屏幕是 1.86 寸 240x320 ST7789SPI 时钟 80MHzLVGL9.1ESP-IDF 5.2static spi_device_interface_config_t dev_cfg { .clock_speed_hz SPI_MAIN_FREQ, // 80MHz .mode 0, .spics_io_num PIN_CS, .queue_size 7, .flags SPI_DEVICE_HALFDUPLEX, .pre_cb NULL, };static lv_display_t *disp lv_display_create(240, 320); lv_display_set_flush_cb(disp, lcd_flush_cb); lv_display_set_buffers(disp, buf1, buf2, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL); lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_0);其他屏的适配其实就是替换初始化序列和调整 x_offset / y_offsetflush_cb 里的逻辑完全通用。你只需要知道手里屏的 IC 型号和玻璃分辨率就能把整套驱动迁移过去。我个人在实际操作中的体会是SPI LCD 驱动的难点从来不在 LVGL 本身而在于你对“屏幕内部显存寻址”的理解。只要把 CASET / RASET 偏移量、RGB 顺序、以及 SPI 传输的同步方式弄明白SPI LCD 就是一个数据搬运工的角色。说白了你是在用一根 SPI 线往屏幕显存里不断的写LVGL9 负责告诉你“该往哪里写、写什么颜色”你负责用最省 CPU 的方式把数据送出去。剩下的就是调参和磨代码的过程了。本文还有配套的精品资源点击获取
返回列表