SPI LCD:理解全双工总线与显示刷新

2026-09-10 00:00    #ESP32-S3   #ESP-IDF   #外设   #阶段3  

本课对应《DNESP32S3 使用指南(IDF 版)》第 22 章。配套完整工程:examples/17-spi-lcd。

环境与验收边界: DNESP32S3 V1.2,ESP-IDF v5.5.5;本机 macOS 构建通过。Ubuntu 26、外设接线、屏幕效果和实际测量仍待实板验收。下文输出均为预期格式,不能当作已采集的硬件日志。

本课完成后能做什么

硬件连接与资源

信号或资源对应关系
模块ATK-MD0240,ST7789V;本例竖屏 240×320
MOSI / SCLK / CSGPIO11 / GPIO12 / GPIO21
D/CGPIO40,P5 IO_SEL 与 LCD_DC 跳线
复位 / 供电使能XL9555 IO1_2 / IO1_3
MISO本例只写屏,设 -1;板上总线信号为 GPIO13

以下为正点原子 DNESP32S3 V1.2 原理图的相关页,保留原厂标识;表格用于快速定位,遇到板卡版本差异以实际原理图为准。点击图片可查看引脚与网络名。

DNESP32S3 原理图第 1 页

DNESP32S3 原理图第 2 页

本课信号与数据处理流程

SPI 的四根线与本例的三线写屏

通常 SPI 有 SCLK、MOSI、MISO、CS。主机每发一个时钟,可以同时移出和移入一位,因此总线具备全双工能力。但显示屏绘图主要向显存写数据,本例不读取屏幕,MISO 配置为 -1。不能由此推断所有 SPI 外设都不需要 MISO。

D/C 是屏幕另外增加的信号:0 表示命令,1 表示命令参数或像素数据。CS 由 SPI 驱动控制,D/C 在事务开始前设置。P5 必须把 IO_SEL 接到 LCD_DC,否则 GPIO40 的变化到不了屏幕。

本工程明确针对 2.4 寸 ATK-MD0240 / ST7789V。模块常以 320×240 横屏宣传,本例 MADCTL=0 使用 240×320 竖屏。1.3 寸 240×240 模块可能需要显示偏移和初始化差异,不属于此默认配置。

一个像素为什么是两个字节

RGB565 分给红色 5 位、绿色 6 位、蓝色 5 位:

颜色16 位值发送字节
红0xF800F8 00
绿0x07E007 E0
蓝0x001F00 1F

ESP32 内存的小端布局不能直接当成屏幕要求的字节顺序。本例显式生成高字节、低字节,避免红蓝或明暗出现难以理解的异常。

先窗口,再像素

0x2A 设置列范围 0~239,0x2B 设置行范围 0~319,0x2C 开始写显存。程序只准备一行 480 字节,重复写 320 次,省去 153600 字节整屏缓冲。

spi_device_polling_transmit() 返回后才能复用行缓冲。本例用同步接口把缓冲区生命周期讲清楚;未来换异步 DMA 时必须等待完成事件,不能提交后立即改写数据。10 MHz 下仅像素净荷的理论下限约 122.9 ms,实际还包含事务和软件开销,不应把日志耗时当成精确 FPS。

驱动先打开屏幕电源、拉低复位,再释放并等待,随后退出睡眠、设置 RGB565 和显示方向。初始化次序与等待时间同样属于协议。

工程如何组织

驱动放在 components/BSP/,应用放在 main/main.c。本工程包含 BUS、EXIO、LCD 模块。项目根目录的 components/BSP 会被 ESP-IDF 自动发现,main 的 PRIV_REQUIRES BSP 声明组件依赖;INCLUDE_DIRS 导出头文件搜索路径,不是运行时查找文件。

固定引脚以 CONFIG_LESSON_* 常量写入本工程 Kconfig.projbuild,构建后可在 sdkconfig 核对;这些默认值对应本板连线,不表示任意改数字就能改变硬件。

应用代码:入口与处理流程

下面代码直接取自配套工程的 main/main.c,头文件和 BSP 实现在仓库中一并提供。初学时先看 app_main(),再追踪它调用的模块。

 1#include <stdio.h>
 2#include <stdint.h>
 3#include <stdbool.h>
 4#include <string.h>
 5#include <stdlib.h>
 6#include <assert.h>
 7#include "esp_err.h"
 8#include "esp_log.h"
 9#include "esp_timer.h"
10#include "freertos/FreeRTOS.h"
11#include "freertos/task.h"
12#include "lcd.h"
13#include "exio.h"
14void app_main(void)
15{
16    i2c_master_bus_handle_t bus;
17    ESP_ERROR_CHECK(bus_init(0,CONFIG_LESSON_I2C_SDA,CONFIG_LESSON_I2C_SCL,&bus));
18    ESP_ERROR_CHECK(exio_init(bus));
19    ESP_ERROR_CHECK(lcd_init());
20    const uint16_t colors[]={0xf800,0x07e0,0x001f,0xffff,0};
21    while(1) for(int i=0;i<5;i++) {
22        int64_t begin=esp_timer_get_time();
23        ESP_ERROR_CHECK(lcd_fill(colors[i]));
24        ESP_LOGI("lcd","RGB565=0x%04x fill=%lld us",colors[i],esp_timer_get_time()-begin);
25        vTaskDelay(pdMS_TO_TICKS(1000));
26    }
27}

BSP 关键实现:LCD

以下是本课对应驱动的完整源文件。对外声明见 lcd.h。公共 BUS 模块统一使用新 I²C master API,将单次事务等待限制在 50 ms;设备 API 使用 7 位地址。

 1#include "sdkconfig.h"
 2#include "lcd.h"
 3#include "exio.h"
 4#include "driver/spi_master.h"
 5#include "driver/gpio.h"
 6#include "freertos/FreeRTOS.h"
 7#include "freertos/task.h"
 8// ATK-MD0240 / ST7789V,竖屏 240x320;其他屏幕不能只改分辨率就假定兼容。
 9static spi_device_handle_t s_spi;
10static esp_err_t transfer(int dc,const void *data,size_t n)
11{
12    gpio_set_level(CONFIG_LESSON_LCD_DC,dc);
13    spi_transaction_t t={.length=n*8,.tx_buffer=data};
14    return spi_device_polling_transmit(s_spi,&t); // 返回后缓冲区才可复用。
15}
16static esp_err_t command(uint8_t cmd,const void *data,size_t n)
17{
18    ESP_RETURN_ON_ERROR(transfer(0,&cmd,1),"lcd","command");
19    return n ? transfer(1,data,n) : ESP_OK;
20}
21esp_err_t lcd_init(void)
22{
23    gpio_config_t g={.pin_bit_mask=1ULL<<CONFIG_LESSON_LCD_DC,.mode=GPIO_MODE_OUTPUT};
24    ESP_RETURN_ON_ERROR(gpio_config(&g),"lcd","DC");
25    spi_bus_config_t bus={.mosi_io_num=CONFIG_LESSON_SPI_MOSI,.miso_io_num=-1,.sclk_io_num=CONFIG_LESSON_SPI_SCLK,
26        .quadwp_io_num=-1,.quadhd_io_num=-1,.max_transfer_sz=480};
27    ESP_RETURN_ON_ERROR(spi_bus_initialize(SPI2_HOST,&bus,SPI_DMA_CH_AUTO),"lcd","bus");
28    spi_device_interface_config_t dev={.clock_speed_hz=10000000,.mode=0,
29        .spics_io_num=CONFIG_LESSON_LCD_CS,.queue_size=1};
30    ESP_RETURN_ON_ERROR(spi_bus_add_device(SPI2_HOST,&dev,&s_spi),"lcd","device");
31    ESP_RETURN_ON_ERROR(exio_output(0x0c00,0x0800),"lcd","power/reset");
32    vTaskDelay(pdMS_TO_TICKS(20));
33    ESP_RETURN_ON_ERROR(exio_output(0x0400,0x0400),"lcd","reset release");
34    vTaskDelay(pdMS_TO_TICKS(120));
35    ESP_RETURN_ON_ERROR(command(0x11,NULL,0),"lcd","sleep out");
36    vTaskDelay(pdMS_TO_TICKS(120));
37    uint8_t format=0x55,orientation=0;
38    ESP_RETURN_ON_ERROR(command(0x3a,&format,1),"lcd","RGB565");
39    ESP_RETURN_ON_ERROR(command(0x36,&orientation,1),"lcd","orientation");
40    ESP_RETURN_ON_ERROR(command(0x21,NULL,0),"lcd","inversion on");
41    return command(0x29,NULL,0);
42}
43esp_err_t lcd_fill(uint16_t rgb565)
44{
45    const uint8_t xs[]={0,0,0,239},ys[]={0,0,1,63}; // 319=0x013f。
46    ESP_RETURN_ON_ERROR(command(0x2a,xs,4),"lcd","x window");
47    ESP_RETURN_ON_ERROR(command(0x2b,ys,4),"lcd","y window");
48    ESP_RETURN_ON_ERROR(command(0x2c,NULL,0),"lcd","RAM write");
49    uint8_t row[480];
50    for(int x=0;x<240;x++) {
51        row[2*x]=rgb565>>8; row[2*x+1]=rgb565&255; // 屏幕要求高字节先发送。
52    }
53    for(int y=0;y<320;y++)
54        ESP_RETURN_ON_ERROR(transfer(1,row,sizeof(row)),"lcd","row");
55    return ESP_OK;
56}

CMake 与中文注释

1# 私有依赖只供本组件源码使用。
2idf_component_register(SRCS "main.c" INCLUDE_DIRS "."
3    PRIV_REQUIRES esp_timer BSP )

每个工程有自己的 project() 名称和 sdkconfig.defaults。VS Code 设置已关闭普通 CMake 自动配置,C/C++ 语言服务读取 build/compile_commands.json;首次构建前文件尚不存在,应先激活 IDF 并构建,而不是手工拼头文件路径。

编译、烧录与观察

 1# 激活 EIM 安装的 IDF;每个新终端都需要激活。
 2source ~/.espressif/tools/activate_idf_v5.5.5.sh
 3# 进入单个示例根目录,不是在 examples 总目录运行。
 4cd ~/mycode/esp32-blog/examples/17-spi-lcd
 5# 首次配置目标;已有正确配置时不用每次重复 set-target。
 6idf.py set-target esp32s3
 7# 编译会生成 build/compile_commands.json,供 VS Code 使用。
 8idf.py build
 9# 把这个值换成实际连接开发板的端口;macOS 通常为 /dev/cu.*。
10PORT=/dev/ttyUSB0
11idf.py -p "$PORT" flash monitor
12# 串口监视退出:Ctrl+]。

预期输出或屏幕内容(示意;数值、日志前缀和启动时间依现场而变):

1lcd: RGB565=0xf800 fill=<实测微秒> us
2lcd: RGB565=0x07e0 fill=<实测微秒> us

故障定位与逐项验收

先检查目标是否是 esp32s3、实际端口与供电,再按本文引脚表核对跳线。编译成功只能说明源码与工具链兼容,无法证明模块型号、物理线路或时序已经正确。

遇到通信错误先看错误码和对应模块,禁止简单用“错误时返回 0”掩盖异常。预期日志是阅读程序的参考,验收时应保存自己的现象、日志与接线照片。

与教材和官方实现对照

对照 10_spilcd:保留 SPI2、GPIO11/12/21/40 和 XL9555 屏幕控制;缩小为 ST7789V 240×320 色块教学,不引入字体与多型号自动判断。

本地验证记录与参考

返回课程目录