
在 ESP32-S3 的嵌入式 AI 或物联网项目里屏幕往往是最直接的人机交互出口。不管是做离线语音助手、桌面气象站还是边缘 AI 识别结果展示一块小尺寸 TFT LCD 都能让调试和产品体验提升一个档次。最近在基于 ESP-IDF 和 FreeRTOS 的实战项目中我使用了一块 1.86 寸的 ST7789P3 屏幕通过 SPI 接口驱动过程中把初始化命令、刷屏流程、任务栈管理都完整走了一遍。这篇教程就来系统梳理 SPI 下 ST7789P3 的常用命令、ESP32-S3 SPI 驱动编写以及如何结合 FreeRTOS 实现稳定的刷屏任务。如果你刚好在用 ESP32-S3 驱动 ST7789P3 或者其他 ST7789 系列屏幕这篇内容可以直接照着用。新手可以理解 LCD 和 SPI 的基本工作方式有基础的开发者可以跳过概念直接看命令表和代码。1. 背景为什么 ST7789P3 适合嵌入式 AI 物联网项目ST7789P3 是 ST7789 系列中非常常见的一款 LCD 驱动芯片型号广泛出现在 1.28 寸、1.54 寸、1.69 寸、1.86 寸等小尺寸圆屏或方屏上。它在嵌入式领域流行主要因为几个原因分辨率支持范围合适典型是 240x320、240x280、240x240 等适合显示中文、图表和简单 UI。SPI 接口占用引脚少只需要 MISO 可以省略实际最常用的 6 针接法是 SCLK、MOSI、CS、DC、RES、BLK。功耗相对较低适合电池设备。价格便宜模块资料成熟社区生态好。在 ESP32-S3 嵌入式 AI 物联网项目中屏幕主要用于展示传感器数据、识别结果、日志信息或摄像头画面。此时 SPI 屏幕虽然比不上 RGB 并口屏幕的刷新速度但胜在接线简单能够满足大部分 UI 刷新需求。配合 ESP32-S3 的 SPI 外设和 DMA刷新率也能做到基本流畅。需要区分的是ST7789P3 和 ST7789V 的寄存器命令集基本相同命令名和参数格式可以互相参考。很多模组厂商把 ST7789 系列的初始化序列做成 Lua 脚本、C 数组或者 C 头文件但只要理解了核心命令面对不同初始化序列就不会觉得神秘。2. 硬件接线与 SPI 通信基础2.1 6 针 SPI 到底接哪些线常见的 ST7789P3 屏幕模块引出 6 个引脚引脚作用VCC电源一般 3.3VGND地SCL / SCLKSPI 时钟SDA / MOSISPI 数据线RES / RST复位低电平有效DC数据/命令选择CS片选BLK / BL背光控制注意有些模块把 DC 写作 AO 或 RS把 BLK 写作 LEDA但功能是一样的。所谓 6 针 SPI通常指的是 SCLK、MOSI、CS、DC、RES、BLK 这 6 个功能引脚电源和地单独接。2.2 SPI 模式与速度选择ST7789 系列 SPI 接口通常支持 SPI Mode 0 和 Mode 3绝大多数模块默认使用 Mode 0即 CPOL0、CPHA0。在 ESP-IDF 的 SPI 驱动中spi_device_interface_config_t的mode字段填 0 即可。SPI 时钟频率方面模块标称支持几十 MHz但实际使用中建议先保守一点。ESP32-S3 的 SPI 外设本身可以跑很高但屏幕模块的走线、杜邦线长度、电源质量都会影响稳定性。一般调试阶段先从 10MHz 到 20MHz 开始确认花屏和抖动问题后再尝试 40MHz 或更高。2.3 关于三线 SPI 和九位模式有些低引脚 MCU 上会使用三线 SPI 驱动 ST7789也就是没有 DC 引脚此时数据线上需要按照 9 位一帧的方式传输第一个 bit 表示命令/数据标识后面 8 位才是实际内容。这种方法在 STM32 上比较常见官方术语叫 9-bit SPI 模式。在 ESP32-S3 上也可以实现九位模式但配置相对复杂需要自己控制发送数据的位长。如果屏幕模块已经有 DC 引脚直接用标准 4 线 SPI 加上 DC 区分命令和数据即可不需要处理九位协议。本文示例也基于标准 4 线 SPI 展开。3. 环境准备ESP-IDF 与项目结构3.1 ESP-IDF 版本说明本文代码基于 ESP-IDF 5.x 版本编写。ESP-IDF 是乐鑫官方物联网开发框架内部集成了 FreeRTOS、SPI 驱动、GPIO 驱动等组件。5.x 版本在主线上已经非常稳定SPI master 驱动和 GPIO 驱动接口变化不大后续版本也基本兼容。如果你还在使用 ESP-IDF 4.4API 命名有些差异例如 SPI DMA channel 的配置方式不同但命令发送逻辑是一样的。建议新项目尽量使用 5.x 或更新版本官方工具链会持续维护。3.2 安装 ESP-IDF安装步骤不复杂到乐鑫官方 Espressif 开发环境下载页获取安装工具。安装过程中选择目标芯片 ESP32-S3。安装完成后打开 IDF Terminal或者通过 IDE 选择 ESP-IDF 工具链。安装完成后可以先创建一个空白工程idf.py create-project lcd_st7789_demo cd lcd_st7789_demo如果你的开发板本身带有 ST7789 屏幕比如一些 ESP32-S3 开发板屏幕引脚已经固定。如果是自行接线需要根据实际连接修改代码里的引脚定义。3.3 推荐项目结构在main目录下拆分成驱动文件和主程序lcd_st7789_demo/ ├── CMakeLists.txt └── main/ ├── CMakeLists.txt ├── main.c ├── lcd_st7789.h └── lcd_st7789.c这样做的目的是把屏幕驱动独立封装后续在 FreeRTOS 任务里只需要调用lcd_st7789.h暴露出来的 API 即可。4. ST7789P3 常用命令解析ST7789P3 内部有一套寄存器通过 SPI 发送命令和参数来配置显示状态。命令的发送方式是DC 拉低时发送命令字节DC 拉高时发送参数或数据。4.1 控制类命令SWRESET软件复位命令码 0x01复位屏幕内部状态通常在初始化最开始执行。执行该命令后一般需要等待一段时间常见等待时间是 120ms 到 150ms。有些初始化序列会省略这一步因为硬件 RES 引脚已经做过一次复位但为了保险软件复位依然建议执行。SLPOUT退出睡眠模式命令码 0x11屏幕上电后默认可能处于睡眠状态SLPOUT 会让屏幕进入正常工作模式。执行后需要等待约 120ms等待内部振荡器稳定。DISPON打开显示命令码 0x29初始化配置完成后用 DISPON 让屏幕真正显示内容。如果没有这条命令即使写了显存屏幕也可能不显示。对应的还有 DISPOFF命令码 0x28用于关闭显示进入低功耗状态。4.2 显示格式类命令MADCTL内存数据访问控制命令码 0x36这个命令控制扫描方向、颜色顺序和行列交换。通过修改这个寄存器的值可以完成屏幕 0 度、90 度、180 度、270 度旋转。常见位定义包括MX行地址镜像。MY列地址镜像。MV行列交换。RGBRGB 颜色顺序是否交换为 BGR。实际横屏还是竖屏以及是否需要镜像取决于模组的接线和屏幕安装方向。调试时最直接的方法就是修改这一字节后观察效果常见组合有 0x00、0xA0、0xC0 等。COLMOD接口像素格式设置命令码 0x3A设置显示颜色格式。ST7789P3 支持 12 位、16 位、18 位等格式最常用的是 65K 色 16 位格式也就是 RGB565参数填 0x55。RGB565 表示每个像素用两个字节表示R 占 5 bitG 占 6 bitB 占 5 bit。选择 16 位色可以在显示质量和传输量之间取得平衡。CASET列地址设置命令码 0x2A设置写入显存时的列范围。参数是四个字节分别是起始列高 8 位、起始列低 8 位、结束列高 8 位、结束列低 8 位。例如设置整个屏幕列范围0x2A x0 8, x0 0xFF, x1 8, x1 0xFFRASET行地址设置命令码 0x2B和 CASET 类似设置行范围。配合 CASET 可以确定一个矩形窗口。RAMWR写显存命令命令码 0x2C这是刷屏最核心的命令。发送 0x2C 之后接下来连续发送的数据会按照已设置的 CASET 和 RASET 窗口写入显存。数据量等于窗口内像素数乘以每像素字节数。例如全屏清屏如果分辨率是 240x320RGB565 格式那么需要发送240 * 320 * 2 153600字节数据。4.3 其他常用命令除了上面这些命令初始化过程中还可能用到0xB1、0xB2、0xB3帧率控制。0xB6显示功能控制。0xB7进入空闲模式。0xE0、0xE1Gamma 校正。0x35开启撕裂效果线Tearing Effect Line。0x34关闭撕裂效果线。这些命令的参数通常由屏幕模组厂商提供不同批次、不同厂商的屏幕会有差别。工程中最安全的做法是优先使用模组资料包里的初始化数组不要凭感觉改参数。5. ESP-IDF SPI 初始化与命令发送实现5.1 引脚定义在lcd_st7789.h中定义引脚// 文件路径main/lcd_st7789.h #ifndef LCD_ST7789_H #define LCD_ST7789_H #include stdint.h #include stddef.h #define LCD_PIN_SCLK 12 #define LCD_PIN_MOSI 11 #define LCD_PIN_CS 10 #define LCD_PIN_DC 9 #define LCD_PIN_RST 8 #define LCD_PIN_BLK 7 #define LCD_H_RES 240 #define LCD_V_RES 320 void lcd_st7789_init(void); void lcd_st7789_fill_screen(uint16_t color); void lcd_st7789_fill_rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color); #endif不同开发板的引脚编号不同请按实际接线修改。5.2 SPI 总线初始化在 ESP-IDF 中使用 SPI master 驱动需要两步先初始化 SPI 总线再挂载 SPI 设备。// 文件路径main/lcd_st7789.c #include string.h #include freertos/FreeRTOS.h #include freertos/task.h #include driver/spi_master.h #include driver/gpio.h #include esp_log.h #include lcd_st7789.h static const char *TAG lcd_st7789; static spi_device_handle_t lcd_spi; static void lcd_st7789_reset(void) { gpio_set_level(LCD_PIN_RST, 0); vTaskDelay(pdMS_TO_TICKS(50)); gpio_set_level(LCD_PIN_RST, 1); vTaskDelay(pdMS_TO_TICKS(120)); } void lcd_st7789_init(void) { gpio_config_t io_conf { .pin_bit_mask (1ULL LCD_PIN_DC) | (1ULL LCD_PIN_RST) | (1ULL LCD_PIN_BLK), .mode GPIO_MODE_OUTPUT, .pull_up_en GPIO_PULLUP_DISABLE, .pull_down_en GPIO_PULLDOWN_DISABLE, .intr_type GPIO_INTR_DISABLE, }; gpio_config(io_conf); gpio_set_level(LCD_PIN_DC, 1); gpio_set_level(LCD_PIN_RST, 1); gpio_set_level(LCD_PIN_BLK, 1); spi_bus_config_t buscfg { .mosi_io_num LCD_PIN_MOSI, .miso_io_num -1, .sclk_io_num LCD_PIN_SCLK, .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz 32 * 1024, }; spi_device_interface_config_t devcfg { .clock_speed_hz 20 * 1000 * 1000, .mode 0, .spics_io_num LCD_PIN_CS, .queue_size 8, }; ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, buscfg, SPI_DMA_CH_AUTO)); ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, devcfg, lcd_spi)); lcd_st7789_reset(); }说明几个配置项mosi_io_num设置为屏幕数据引脚miso_io_num设置为 -1表示不使用 MISO因为 ST7789 屏幕模块一般不回传数据。max_transfer_sz决定单次 SPI 传输的最大字节数。这里设置 32KB刷屏时按块拆分。queue_size是事务队列大小并不是总线时钟频率DMA 场景下适当增大可以提升吞吐率。5.3 命令与数据发送函数从 SPI master 驱动的角度看命令和数据本质上都是通过 SPI 往同一个 MOSI 线上发送字节。区别只在于 DC 引脚的电平static void lcd_write_cmd(uint8_t cmd) { gpio_set_level(LCD_PIN_DC, 0); spi_transaction_t t { .length 8, .tx_buffer cmd, }; spi_device_polling_transmit(lcd_spi, t, portMAX_DELAY); } static void lcd_write_data(const uint8_t *data, size_t len) { gpio_set_level(LCD_PIN_DC, 1); spi_transaction_t t { .length len * 8, .tx_buffer data, }; spi_device_polling_transmit(lcd_spi, t, portMAX_DELAY); }这里使用的是spi_device_polling_transmit它会阻塞等待传输完成优点是代码简单适合发送命令和参数。对于大批量像素数据可以改用spi_device_transmit加 DMA避免反复占用 CPU 等待。注意tx_buffer所指向的缓冲区在 polling 模式下可以是指向栈上变量因为函数返回后缓冲区不再被使用。如果改用 DMA 异步模式缓冲区必须在函数返回后仍然有效。5.4 初始化序列一个最小可用的 ST7789P3 初始化序列如下static void lcd_st7789_init_seq(void) { uint8_t data[4]; lcd_write_cmd(0x01); vTaskDelay(pdMS_TO_TICKS(150)); lcd_write_cmd(0x11); vTaskDelay(pdMS_TO_TICKS(120)); lcd_write_cmd(0x36); data[0] 0x00; lcd_write_data(data, 1); lcd_write_cmd(0x3A); data[0] 0x55; lcd_write_data(data, 1); lcd_write_cmd(0x2A); data[0] 0x00; data[1] 0x00; data[2] (LCD_H_RES - 1) 8; data[3] (LCD_H_RES - 1) 0xFF; lcd_write_data(data, 4); lcd_write_cmd(0x2B); data[0] 0x00; data[1] 0x00; data[2] (LCD_V_RES - 1) 8; data[3] (LCD_V_RES - 1) 0xFF; lcd_write_data(data, 4); lcd_write_cmd(0x29); vTaskDelay(pdMS_TO_TICKS(20)); }这段序列只包含最基本的命令很多模组还需要额外的电源控制、Gamma 校正参数。项目中使用时建议把模组厂商提供的初始化数组放在函数里执行再对照终端现象调整。5.5 设置显示窗口与填充刷屏的核心思路是先设置窗口再启动 RAMWR最后连续发送像素数据。static void lcd_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { uint8_t data[4]; lcd_write_cmd(0x2A); data[0] x0 8; data[1] x0 0xFF; data[2] x1 8; data[3] x1 0xFF; lcd_write_data(data, 4); lcd_write_cmd(0x2B); data[0] y0 8; data[1] y0 0xFF; data[2] y1 8; data[3] y1 0xFF; lcd_write_data(data, 4); } static void lcd_write_pixels(uint16_t color, size_t pixel_count) { lcd_write_cmd(0x2C); // 分块发送每块不超过 16KB uint8_t buf[512]; size_t byte_count pixel_count * 2; for (size_t i 0; i byte_count; i 2) { buf[i % sizeof(buf)] color 8; buf[(i % sizeof(buf)) 1] color 0xFF; } lcd_write_data(buf, sizeof(buf)); }如果你只是做简单调试可以直接在lcd_write_pixels里用memset方式填充。更高效的做法是预定义一块缓冲区用 SPI DMA 一次性发送大块数据。5.6 填充整个屏幕void lcd_st7789_fill_screen(uint16_t color) { lcd_st7789_fill_rect(0, 0, LCD_H_RES - 1, LCD_V_RES - 1, color); } void lcd_st7789_fill_rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t pixel_count (uint32_t)(x1 - x0 1) * (y1 - y0 1); lcd_set_window(x0, y0, x1, y1); lcd_write_cmd(0x2C); // 分批发送避免单次 SPI 事务过大 uint8_t buf[1024]; for (uint32_t i 0; i pixel_count; i sizeof(buf) / 2) { uint32_t cnt (pixel_count - i) (sizeof(buf) / 2) ? (sizeof(buf) / 2) : (pixel_count - i); for (uint32_t j 0; j cnt; j) { buf[j * 2] color 8; buf[j * 2 1] color 0xFF; } lcd_write_data(buf, cnt * 2); } }注意RGB565 颜色值在 ST7789 显存里通常高字节在前。如果你的屏幕颜色出现红蓝互换可以把color 8和color 0xFF对调或者调整 MADCTL 的 RGB 位。6. 基于 FreeRTOS 的刷屏任务实战6.1 为什么需要任务化在嵌入式 AI 物联网项目中主循环往往要处理传感器采集、算法推理、网络通信等任务。如果把刷屏操作放进主循环当 SPI 发送大量像素数据时主循环会被阻塞导致语音识别或网络任务卡顿。正确做法是把屏幕刷新封装成独立任务用 FreeRTOS 的任务调度来管理。6.2 创建 LCD 刷新任务在main.c中创建任务// 文件路径main/main.c #include stdio.h #include freertos/FreeRTOS.h #include freertos/task.h #include esp_log.h #include lcd_st7789.h static void lcd_task(void *arg) { uint16_t colors[] { 0xF800, // 红色 0x07E0, // 绿色 0x001F, // 蓝色 0xFFFF, // 白色 0x0000, // 黑色 }; int idx 0; while (1) { lcd_st7789_fill_screen(colors[idx % 5]); idx; vTaskDelay(pdMS_TO_TICKS(1000)); } } void app_main(void) { lcd_st7789_init(); xTaskCreate(lcd_task, lcd_task, 4096, NULL, 5, NULL); }任务栈大小 4096 字节对于lcd_st7789_fill_screen来说基本够用但如果你在刷屏函数内部定义了较大的临时缓冲区比如 4KB 的数组栈大小就要相应调大。否则 FreeRTOS 可能触发栈溢出。6.3 栈溢出检测与水位监控FreeRTOS 提供了两种手段来避免栈溢出问题第一种是在menuconfig中开启栈溢出检测。找到Component config - FreeRTOS - Enable stack overflow check一般选择Check stack pointer或Check stack pointer and canary value。开启后如果任务栈溢出系统会进入断言或调用钩子函数。第二种是在任务中主动打印栈的高水位提前判断任务栈剩余空间static void lcd_task(void *arg) { while (1) { UBaseType_t high_water_mark uxTaskGetStackHighWaterMark(NULL); ESP_LOGI(lcd_task, remaining stack: %u, high_water_mark); lcd_st7789_fill_screen(0xF800); vTaskDelay(pdMS_TO_TICKS(1000)); } }这个值代表任务运行以来栈剩余的最小值。如果这个值接近 0说明任务栈不够需要调大任务栈。6.4 刷屏任务中的注意事项不要在 FreeRTOS 任务中频繁使用过大的局部数组建议把缓冲区声明为静态变量或全局变量。原因有三点局部数组占用任务栈空间任务栈默认往往只有几 KB。静态缓冲区可以复用避免反复初始化的开销。后续如果要切换到 DMA 异步传输静态缓冲区可以在传输期间保持有效。如果你的项目需要频繁刷新 UI建议把刷屏数据写入 PSRAM 缓冲区再通过 SPI DMA 发送。ESP32-S3 支持外部 PSRAM可以分配较大帧缓冲避免占用内部 SRAM。7. 常见问题与排查思路在实际调试 ST7789P3 的过程中最容易遇到下面几类问题。问题现象常见原因解决思路白屏但背光亮初始化未完成没有执行 SLPOUT 或 DISPON检查初始化序列确保 SLPOUT 后等待足够时间花屏SPI 时钟太快或初始化参数不全降低 SPI 时钟到 10MHz 试一下对照模组资料补充初始化序列颜色反了RGB/BGR 顺序不对调整 MADCTL 的 RGB 位或交换 RGB565 高低字节显示方向不对MADCTL 参数错误修改 MADCTL 的 MX、MY、MV 位组合屏幕闪烁撕裂刷新窗口与屏幕扫描不同步使用 TE 引脚或者降低刷新频率使用 DMA 整块发送任务卡死SPI transmission 超时或任务栈溢出检查 SPI 设备句柄是否初始化增大任务栈减少单次事务长度只有上半屏显示CASET/RASET 设置错误检查窗口起始和结束地址是否匹配分辨率排查时建议按以下顺序确认硬件接线特别是 DC 和 CS 引脚是否接对。用逻辑分析仪或示波器查看 SPI 波形确认是否有数据发出。下载一个已知能正常显示的官方 demo比如 ESP-IDF 的spi_lcd_touch示例验证屏幕硬件本身是否正常。再切换到自己的驱动代码逐条对比初始化序列。如果你用的是 ESP-IDF 官方esp_lcd组件那么驱动框架会更稳定但排查方式类似。遇到问题时优先怀疑初始化序列而不是 SPI 总线配置。8. 最佳实践与工程经验8.1 优先使用 ESP-IDF 自带的 esp_lcd 组件虽然本文为了讲清楚命令手写了 SPI 驱动底层逻辑但在实际产品开发中更推荐直接使用 ESP-IDF 提供的esp_lcd组件。ESP-IDF 自带 ST7789 的驱动实现封装了esp_lcd_panel_io、esp_lcd_panel、esp_lcd_panel_ops等接口代码更可靠后续升级维护也更方便。使用esp_lcd时初始化代码会变成这样esp_lcd_panel_io_handle_t io_handle NULL; esp_lcd_panel_io_spi_config_t io_config { .dc_gpio_num LCD_PIN_DC, .cs_gpio_num LCD_PIN_CS, .pclk_hz 20 * 1000 * 1000, .trans_queue_depth 8, }; esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)spi_bus, io_config, io_handle);但esp_lcd组件的底层仍然是 SPI 命令理解 ST7789 常用命令依然有助于排查问题。8.2 使用 DMA 提升刷新效率大批量像素发送时如果使用阻塞式 pollingCPU 会一直等待 SPI 发送完成浪费大量时间。更高效的方式是使用 DMA将缓冲区地址交给 SPI 外设让 DMA 自动搬运数据CPU 可以继续执行其他任务。使用 DMA 时spi_transaction_t中要加入SPI_TRANS_USE_DMA标志并且确保缓冲区在 DMA 传输期间不被修改。在多任务场景下可以使用 FreeRTOS 队列或信号量来通知传输完成。8.3 缓冲区与内存管理ESP32-S3 内部 SRAM 有限如果使用全屏 RGB565 帧缓冲一块 240x320 的屏幕就需要240 * 320 * 2 153600字节约 150KB这对内部 RAM 来说很紧张。建议部分刷新尽量只更新变化区域。使用外部 PSRAM 存放帧缓冲。如果没有 PSRAM考虑用局部刷新替代全屏刷新。8.4 SPI 片选的两种方式ESP-IDF 的 SPI master 驱动支持硬件片选和软件片选。默认情况下配置spics_io_num后CS 引脚由 SPI 硬件自动控制每次传输开始和结束时自动拉低拉高这种是硬件片选代码里不需要手动操作。如果多个 SPI 设备共用一根总线或者你需要长时间保持 CS 拉低可以改用软件片选手动通过gpio_set_level控制 CS。但要注意软件片选需要额外处理 CS 释放时机避免下个 SPI 设备误触发。在单屏场景下使用硬件片选最简单。8.5 电源稳定性ST7789P3 屏幕模块在刷新大面积颜色时瞬时电流会增大。如果电源纹波过大可能出现闪烁、花屏甚至复位。建议屏幕供电和 ESP32-S3 使用同一路 3.3V但如果电流不足优先单独供电。在屏幕电源引脚附近加 10uF 和 100nF 电容。杜邦线长度尽量短特别是 SCLK 和 MOSI。8.6 FreeRTOS 任务分配建议在嵌入式 AI 物联网项目中建议任务拆分如下lcd_task负责界面刷新优先级中等偏低。ai_task负责 AI 推理优先级较高。network_task负责网络上报可阻塞等待。sensor_task负责采集传感器数据。不要让高优先级的任务长时间占用 CPU否则 LCD 刷新任务得不到执行界面会卡顿。如果 AI 推理耗时较长可以在推理任务中加入vTaskDelay或使用调度锁来保证其他任务有机会运行。9. 总结与下一步学习建议这篇教程围绕 SPI 下的 ST7789P3 展开把常用命令、ESP32-S3 SPI 驱动初始化、FreeRTOS 刷屏任务完整串了一遍。核心要点可以归纳为ST7789P3 的初始化核心是复位、SLPOUT、COLMOD、CASET、RASET、DISPON 这几个命令。SPI 下区分命令和数据靠 DC 引脚命令发送和参数发送本身没有协议层区别。刷屏本质就是先设置窗口再发送 RAMWR 命令然后连续写像素数据。ESP-IDF 的 SPI master 驱动提供了 polling 和 DMA 两种发送方式大批量像素数据优先使用 DMA。FreeRTOS 中刷屏任务要注意栈大小和 SPI 传输阻塞问题可以开启栈溢出检测并检查栈水位。下一步你可以继续学习ESP-IDF 自带的esp_lcd组件用官方封装替代手写驱动。在 LCD 上移植 LVGL 图形库完成更复杂的 UI 交互。结合 ST7789P3 的 TE 引脚实现撕裂效果控制优化动态画面。尝试使用 RGB 并口屏幕做更高帧率的显示适合对刷新速度要求更高的 AI 可视化场景。如果你也是在 ESP32-S3 驱动 ST7789P3 的过程中踩了坑建议把初始化数组和引脚配置先跑通再去优化刷新速度和显示效果。屏幕这类外设只要 SPI 波形正确、电源稳定、初始化完整剩下的事情就变得很顺利。希望这篇内容能帮你少走一些弯路。