SPI/QSPI LCD 驱动说明
源码:
components/BSP/SPILCD/qspi.c/qspi.h/qspilcd.c/qspilcd.h
1. 模式切换
通过 qspilcd.h 中的宏选择接口模式,两种模式共享大部分物理引脚:
| 宏 | 模式 | 数据线 | 额外引脚 |
|---|---|---|---|
#define LCD_QSPI | Quad SPI 四线 | D0/D1/D2/D3 (GPIO 4/5/6/7) | — |
#define LCD_SPI | 标准 SPI 单线 | MOSI/MISO (GPIO 4/5) | DC (GPIO 19) |
当前默认 LCD_QSPI。
引脚定义 (qspi.h + qspilcd.h)
// ── qspi.h ──
#ifdef LCD_SPI
#define SPI_MOSI_GPIO_PIN GPIO_NUM_4
#define SPI_MISO_GPIO_PIN GPIO_NUM_5
#define SPI_CLK_GPIO_PIN GPIO_NUM_16
#elif defined(LCD_QSPI)
#define QSPI_D0_GPIO_PIN GPIO_NUM_4
#define QSPI_D1_GPIO_PIN GPIO_NUM_5
#define QSPI_D2_GPIO_PIN GPIO_NUM_6
#define QSPI_D3_GPIO_PIN GPIO_NUM_7
#define QSPI_CLK_GPIO_PIN GPIO_NUM_16
#endif
#define DMA_SPI3_MAX_BITS (1 << 18) / 8 // 32768 字节
// ── qspilcd.h ──
#define LCD_NUM_CS GPIO_NUM_15
#define LCD_NUM_WAIT GPIO_NUM_46
#define LCD_NUM_RST GPIO_NUM_18
#ifdef LCD_SPI
#define LCD_NUM_DC GPIO_NUM_19
#define LCD_DC(x) gpio_set_level(LCD_NUM_DC, (x))
#endif
#define LCD_CS(x) gpio_set_level(LCD_NUM_CS, (x))
#define LCD_RST(x) gpio_set_level(LCD_NUM_RST, (x))
#define LCD_WAIT while (gpio_get_level(LCD_NUM_WAIT) == 0) { vTaskDelay(1); }
#define LCD_DISPLAY_WIDTH 800
#define LCD_DISPLAY_HEIGHT 4802. 总线初始化与设备注册
2.1 SPI3 总线 (spi3_init())
void spi3_init(void)
{
spi_bus_config_t spi_bus_conf = {0};
#ifdef LCD_SPI
spi_bus_conf.mosi_io_num = SPI_MOSI_GPIO_PIN;
spi_bus_conf.miso_io_num = SPI_MISO_GPIO_PIN;
spi_bus_conf.sclk_io_num = SPI_CLK_GPIO_PIN;
spi_bus_conf.quadwp_io_num = -1;
spi_bus_conf.quadhd_io_num = -1;
#elif defined(LCD_QSPI)
spi_bus_conf.data0_io_num = QSPI_D0_GPIO_PIN;
spi_bus_conf.data1_io_num = QSPI_D1_GPIO_PIN;
spi_bus_conf.data2_io_num = QSPI_D2_GPIO_PIN;
spi_bus_conf.data3_io_num = QSPI_D3_GPIO_PIN;
spi_bus_conf.sclk_io_num = QSPI_CLK_GPIO_PIN;
spi_bus_conf.quadwp_io_num = QSPI_D2_GPIO_PIN;
spi_bus_conf.quadhd_io_num = QSPI_D3_GPIO_PIN;
spi_bus_conf.flags = SPICOMMON_BUSFLAG_QUAD;
#endif
spi_bus_conf.max_transfer_sz = DMA_SPI3_MAX_BITS;
spi_bus_initialize(SPI3_HOST, &spi_bus_conf, SPI_DMA_CH_AUTO);
}2.2 SPI 设备 (lcd_init() 中调用)
spi_device_interface_config_t devcfg = {
.clock_speed_hz = 80 * 1000 * 1000, // 80 MHz
.mode = 0, // CPOL=0, CPHA=0
.spics_io_num = LCD_NUM_CS, // GPIO 15
.queue_size = 30,
.flags = SPI_DEVICE_HALFDUPLEX, // 半双工
};
spi_bus_add_device(SPI3_HOST, &devcfg, &qspilcd_handle);3. SPI/QSPI 驱动格式
TR2S LCD 支持两种通信模式,通过 qspi.h 底层函数封装统一的 spi_transaction_t 传输:
| 模式 | 命令/数据区分 | 传输方式 | 底层 flags |
|---|---|---|---|
| LCD_SPI | DC 引脚电平 (DC=0 命令, DC=1 数据) | 单线 (MOSI) | 无特殊标志 |
| LCD_QSPI | 帧头 0x02/0x03/0x12 | 单线发命令头 + QIO 发数据 | SPI_TRANS_MODE_QIO |
两种模式均使用 SPI Mode 0 (CPOL=0, CPHA=0),半双工 (
SPI_DEVICE_HALFDUPLEX)
3.1 SPI 模式格式(LCD_SPI)
SPI 标准模式通过 DC 引脚 区分命令与数据,格式简单:
写寄存器: DC=0 → [CMD] → DC=1 → [Data…]
读寄存器: DC=0 → [CMD] → DC=1 → 回读 [Data…]
写 GRAM: DC=0 → [0x2C] → DC=1 → [RGB565 Pixel Data…]// ── SPI 模式: lcd_write_cmddata() 分支 ──
LCD_DC(0); // DC=0, 命令阶段
spi3_write_cmd(qspilcd_handle, cmd, false); // 单线发 1 字节命令
LCD_DC(1); // DC=1, 数据阶段
spi3_write_data(qspilcd_handle, param, len, false);// 单线发参数
// ── SPI 底层: spi3_write_cmd() ──
spi_transaction_t t = {0};
t.length = 8; // 1 字节 = 8 位
t.tx_buffer = &cmd;
spi_device_polling_transmit(handle, &t);
// ── SPI 底层: spi3_write_data() ──
spi_transaction_t t = {0};
if (CS) t.flags |= SPI_TRANS_CS_KEEP_ACTIVE; // 保持 CS 低
t.length = len * 8;
t.tx_buffer = data;
spi_device_polling_transmit(handle, &t);3.2 QSPI 帧协议(LCD_QSPI)
QSPI 模式不需要 DC 引脚,命令和数据统一通过 4 字节帧头 + 数据体 的固定格式区分:
┌──────────────┬──────────┬──────────┬──────────┬───────────────────┐
│ Byte 0 │ Byte 1 │ Byte 2 │ Byte 3 │ Byte 4 … Byte N │
├──────────────┼──────────┼──────────┼──────────┼───────────────────┤
│ 命令类型 │ 保留(00) │ 寄存器号 │ 保留(00) │ 参数数据 (变长) │
└──────────────┴──────────┴──────────┴──────────┴───────────────────┘| 命令类型 | Byte 0 | 操作含义 |
|---|---|---|
0x02 | 写寄存器 | [02][00][Reg][00][Data…] |
0x03 | 读寄存器 | [03][00][Reg][00] → 回读 |
0x12 | 写 GRAM | [12][00][2C][00][RGB565…] |
帧头通过
qspi3_write_cmd()以单线模式发送,像素数据通过qspi3_write_data()以 QIO 四线模式发送,最大化像素吞吐。
// ── QSPI 模式: lcd_write_cmddata() 分支 ──
uint8_t data[256] = {0x02, 0x00, 0x00, 0x00}; // 帧头: 02 00 [Reg] 00
uint8_t datalen = 4;
data[2] = cmd; // 填入寄存器号
if (cmd == 0x2C) { // 写 GRAM → 命令类型改为 0x12
data[0] = 0x12;
qspi3_write_cmd(qspilcd_handle, data, datalen, true); // 单线发帧头, CS 保持
return; // 像素数据后续由 qspi3_write_data 发
}
while (len--) data[datalen++] = *param++; // 追加参数
qspi3_write_cmd(qspilcd_handle, data, datalen, false); // 单线发完整帧
// ── QSPI 底层: qspi3_write_cmd() (单线发帧头) ──
spi_transaction_t t = {0};
if (CS) t.flags |= SPI_TRANS_CS_KEEP_ACTIVE;
t.length = len * 8;
t.tx_buffer = data;
spi_device_polling_transmit(handle, &t);
// ── QSPI 底层: qspi3_write_data() (QIO 四线发像素) ──
spi_transaction_t t = {0};
t.flags = SPI_TRANS_MODE_QIO; // ★ 四线 I/O
if (CS) t.flags |= SPI_TRANS_CS_KEEP_ACTIVE;
t.length = len * 8;
t.tx_buffer = data;
spi_device_polling_transmit(handle, &t);3.3 完整写入流程 (以写寄存器为例)
lcd_write_cmddata(0x2A, {0x00,0x00,0x03,0x1F}, 4) // 设 CASET = 0~799
┌─ LCD_SPI 模式 ───────────────────────────────────┐
│ DC=0 → [0x2A] → DC=1 → [00][00][03][1F] │
│ ↑ cmd ↑ data (4 bytes, 单线) │
└──────────────────────────────────────────────────┘
┌─ LCD_QSPI 模式 ──────────────────────────────────┐
│ [02][00][2A][00][00][00][03][1F] │
│ ↑ ↑ │
│ 帧头(4B, 单线) 参数(4B, 单线) │
└──────────────────────────────────────────────────┘4. LCD 初始化与刷新
4.1 设备结构体
typedef struct {
uint16_t width; // 当前宽度
uint16_t height; // 当前高度
uint8_t dir; // 0=竖屏, 1=横屏
} _qspilcd_dev;
extern _qspilcd_dev qspilcd_dev;
extern spi_device_handle_t qspilcd_handle;4.2 lcd_write_data — 写数据
lcd_write_data() 在两种模式下自动切换传输方式:
static void lcd_write_data(const uint8_t *data, int len)
{
#ifdef LCD_SPI
spi3_write_data(qspilcd_handle, data, len, true); // 单线
#elif defined(LCD_QSPI)
qspi3_write_data(qspilcd_handle, data, len, true); // QIO 四线
#endif
}流程控制:
len == 0时仅结束传输(CS 释放),实际像素数据通过lcd_lv_cb()中的循环分块调用此函数发送。
4.3 lcd_set_window — 设置窗口
static void lcd_set_window(uint16_t xstar, uint16_t ystar,
uint16_t xend, uint16_t yend)
{
uint8_t data[4]; // 16 位坐标拆为 2 字节大端
data[0] = xstar >> 8; data[1] = 0x00FF & xstar;
data[2] = xend >> 8; data[3] = 0x00FF & xend;
lcd_write_cmddata(0x2A, data, 4); // CASET → 列地址
data[0] = ystar >> 8; data[1] = 0x00FF & ystar;
data[2] = yend >> 8; data[3] = 0x00FF & yend;
lcd_write_cmddata(0x2B, data, 4); // RASET → 页地址
lcd_write_cmddata(0x2C, NULL, 0); // RAMWR → 开始写 GRAM
}4.4 初始化
void lcd_init(void)
{
// 1. WAIT 引脚输入
gpio_config(&(gpio_config_t){
.mode = GPIO_MODE_INPUT, .pin_bit_mask = 1ull << LCD_NUM_WAIT
});
// 2. RST 引脚输出 (上拉)
gpio_config(&(gpio_config_t){
.mode = GPIO_MODE_OUTPUT, .pin_bit_mask = 1ull << LCD_NUM_RST,
.pull_up_en = GPIO_PULLUP_ENABLE
});
#ifdef LCD_SPI
// 3a. DC 引脚输出 (仅 SPI 模式)
gpio_config(&(gpio_config_t){
.mode = GPIO_MODE_OUTPUT, .pin_bit_mask = 1ull << LCD_NUM_DC
});
#endif
// 3. SPI3 总线
spi3_init();
// 4. 添加设备 (80MHz / Mode 0 / Half-Duplex)
spi_bus_add_device(SPI3_HOST, &devcfg, &qspilcd_handle);
// 5. 硬件复位 (RST 低 100ms → 高 800ms)
lcd_hard_reset();
// 6. 初始化命令
lcd_write_cmddata(0x20, (const uint8_t[]){0x32}, 1); // 打开背光
// 7. 横屏 800×480
lcd_display_dir(1);
vTaskDelay(120);
}4.5 显示方向与清屏
static void lcd_display_dir(uint8_t dir)
{
qspilcd_dev.dir = dir;
if (dir == 0) { // 竖屏: 480×800
qspilcd_dev.width = LCD_DISPLAY_HEIGHT;
qspilcd_dev.height = LCD_DISPLAY_WIDTH;
} else { // 横屏: 800×480
qspilcd_dev.width = LCD_DISPLAY_WIDTH;
qspilcd_dev.height = LCD_DISPLAY_HEIGHT;
}
if (lcd_buf == NULL)
lcd_buf = heap_caps_malloc(qspilcd_dev.width * 2, MALLOC_CAP_SPIRAM);
}
static void lcd_clear(uint16_t color) // 纯色填充整屏
{
for (int j = 0; j < qspilcd_dev.width; j++) {
lcd_buf[j*2] = color >> 8;
lcd_buf[j*2+1] = color;
}
spi_device_acquire_bus(qspilcd_handle, portMAX_DELAY);
lcd_set_window(0, 0, qspilcd_dev.width-1, qspilcd_dev.height-1);
for (int i = 0; i < qspilcd_dev.height; i++)
lcd_write_data(lcd_buf, qspilcd_dev.width * 2);
lcd_write_data(NULL, 0); // 结束传输
spi_device_release_bus(qspilcd_handle);
}4.6 LVGL 刷新回调
void lcd_lv_cb(uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey, void *data, uint8_t unit)
{
spi_device_acquire_bus(qspilcd_handle, portMAX_DELAY);
lcd_set_window(sx, sy, ex, ey); // 设窗口
uint32_t totaldatas = (ex - sx + 1) * (ey - sy + 1) * unit;
uint32_t blocksize = (totaldatas > DMA_SPI3_MAX_BITS)
? DMA_SPI3_MAX_BITS : totaldatas;
uint8_t *p = data;
while (1) {
lcd_write_data(p, blocksize); // LCD 发送
p += blocksize; totaldatas -= blocksize;
if (totaldatas == 0) break;
if (blocksize > totaldatas) blocksize = totaldatas;
}
lcd_write_data(NULL, 0);
spi_device_release_bus(qspilcd_handle);
}5. CTP 复用接口
qspilcd.c 导出两个函数供触摸屏(TR230S 模式)复用 SPI 总线。 代码结构与会话管理模式与 lcd_write_cmddata() 完全相同,额外增加了 spi_device_acquire/release_bus() 实现总线共享:
void lcd_ctp_write_cmd(const uint8_t cmd, const uint8_t *param, uint8_t len)
{
LCD_WAIT
spi_device_acquire_bus(qspilcd_handle, portMAX_DELAY);
#ifdef LCD_SPI
LCD_DC(0);
spi3_write_cmd(qspilcd_handle, cmd, false);
LCD_DC(1);
spi3_write_data(qspilcd_handle, param, len, false);
#elif defined(LCD_QSPI)
uint8_t data[256] = {0x02, 0x00, 0x00, 0x00};
uint8_t datalen = 4;
data[2] = cmd;
while (len--) data[datalen++] = *param++;
qspi3_write_cmd(qspilcd_handle, data, datalen, false);
#endif
spi_device_release_bus(qspilcd_handle);
}
void lcd_ctp_read_cmd(const uint8_t cmd, uint8_t *param, uint8_t len)
{
LCD_WAIT
spi_device_acquire_bus(qspilcd_handle, portMAX_DELAY);
#ifdef LCD_SPI
LCD_DC(0);
spi3_write_cmd(qspilcd_handle, cmd, true);
LCD_DC(1);
spi3_read_data(qspilcd_handle, param, len, false);
#elif defined(LCD_QSPI)
uint8_t data[4] = {0x03, 0x00, 0x00, 0x00};
data[2] = cmd;
qspi3_write_cmd(qspilcd_handle, data, 4, true);
qspi3_read_data(qspilcd_handle, param, len, false);
#endif
spi_device_release_bus(qspilcd_handle);
}读写帧格式: 写操作 QSPI 帧头
[02][00][Reg][00],读操作帧头[03][00][Reg][00]— 与第 3.2 节完全一致。spi_device_acquire/release_bus()确保与 LVGL 刷新线程互斥访问同一 SPI3_HOST。
6. API 速查
| 函数 | 模式 | 说明 |
|---|---|---|
spi3_write_cmd() | 单线 | 发 1 字节命令 |
spi3_write_data() | 单线 | 发数据 |
spi3_read_data() | 单线 | 读数据 |
qspi3_write_cmd() | 单线 | 发 QSPI 命令包 |
qspi3_write_data() | 四线 | 四线高速发像素 |
qspi3_read_data() | 单线 | 单线接收 |
lcd_write_cmddata() | — | LCD 写寄存器封装 |
lcd_write_data() | — | LCD 写数据封装 |
lcd_set_window() | — | 设 CASET+RASET+RAMWR |
lcd_lv_cb() | — | LVGL 刷新回调 (unit=sizeof(lv_color_t)) |
lcd_ctp_write_cmd() | — | CTP 写 (TR230S) |
lcd_ctp_read_cmd() | — | CTP 读 (TR230S) |
7. 移植检查清单
- [ ]
qspi.h— 修改QSPI_D0~D3/CLK引脚宏 - [ ]
qspilcd.h— 修改CS/WAIT/RST/DC引脚宏 - [ ]
qspilcd.h— 修改LCD_DISPLAY_WIDTH/HEIGHT - [ ]
qspilcd.h— 选择LCD_QSPI或LCD_SPI - [ ]
qspilcd.c→lcd_init()— 按屏幕手册修改初始化命令序列 - [ ]
qspilcd.c→devcfg.clock_speed_hz— 调整 SPI 时钟 (40~80 MHz)
8. 性能
| 指标 | 数值 |
|---|---|
| SPI 时钟 | 80 MHz |
| QSPI 理论带宽 | 80×4 = 320 Mbps ≈ 40 MB/s |
| DMA 单次上限 | 32,768 字节 |
| 全屏数据量 (RGB565) | 800×480×2 = 768 KB |