SPI/QSPI LCD Driver Guide
Source code:
components/BSP/SPILCD/qspi.c/qspi.h/qspilcd.c/qspilcd.h
1. Mode Switching
Select the interface mode via the macros in qspilcd.h; both modes share most of the physical pins:
| Macro | Mode | Data Lines | Extra Pin |
|---|---|---|---|
#define LCD_QSPI | Quad SPI, 4-wire | D0/D1/D2/D3 (GPIO 4/5/6/7) | — |
#define LCD_SPI | Standard SPI, 1-wire | MOSI/MISO (GPIO 4/5) | DC (GPIO 19) |
Currently LCD_QSPI is the default.
Pin Definitions (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 bytes
// ── 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. Bus Initialization and Device Registration
2.1 SPI3 Bus (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 Device (called in 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, // half-duplex
};
spi_bus_add_device(SPI3_HOST, &devcfg, &qspilcd_handle);3. SPI/QSPI Driver Format
The TR2S LCD supports two communication modes; the low-level functions in qspi.h wrap both into unified spi_transaction_t transfers:
| Mode | Command/Data Distingushing | Transfer Method | Low-Level Flags |
|---|---|---|---|
| LCD_SPI | DC pin level (DC=0 command, DC=1 data) | 1-wire (MOSI) | no special flags |
| LCD_QSPI | frame header 0x02/0x03/0x12 | 1-wire command header + QIO data | SPI_TRANS_MODE_QIO |
Both modes use SPI Mode 0 (CPOL=0, CPHA=0), half-duplex (
SPI_DEVICE_HALFDUPLEX)
3.1 SPI Mode Format (LCD_SPI)
Standard SPI mode distinguishes commands from data via the DC pin; the format is simple:
Write register: DC=0 → [CMD] → DC=1 → [Data…]
Read register: DC=0 → [CMD] → DC=1 → read back [Data…]
Write GRAM: DC=0 → [0x2C] → DC=1 → [RGB565 Pixel Data…]// ── SPI mode: lcd_write_cmddata() branch ──
LCD_DC(0); // DC=0, command phase
spi3_write_cmd(qspilcd_handle, cmd, false); // 1-wire, send 1-byte command
LCD_DC(1); // DC=1, data phase
spi3_write_data(qspilcd_handle, param, len, false);// 1-wire, send parameters
// ── SPI low level: spi3_write_cmd() ──
spi_transaction_t t = {0};
t.length = 8; // 1 byte = 8 bits
t.tx_buffer = &cmd;
spi_device_polling_transmit(handle, &t);
// ── SPI low level: spi3_write_data() ──
spi_transaction_t t = {0};
if (CS) t.flags |= SPI_TRANS_CS_KEEP_ACTIVE; // keep CS low
t.length = len * 8;
t.tx_buffer = data;
spi_device_polling_transmit(handle, &t);3.2 QSPI Frame Protocol (LCD_QSPI)
QSPI mode does not need the DC pin; commands and data are distinguished by a fixed format of 4-byte frame header + data body:
┌──────────────┬──────────┬──────────┬──────────┬───────────────────┐
│ Byte 0 │ Byte 1 │ Byte 2 │ Byte 3 │ Byte 4 … Byte N │
├──────────────┼──────────┼──────────┼──────────┼───────────────────┤
│ Command type │ Reserved(00) │ Register number │ Reserved(00) │ Parameter data (variable length) │
└──────────────┴──────────┴──────────┴──────────┴───────────────────┘| Command Type | Byte 0 | Operation |
|---|---|---|
0x02 | Write register | [02][00][Reg][00][Data…] |
0x03 | Read register | [03][00][Reg][00] → read back |
0x12 | Write GRAM | [12][00][2C][00][RGB565…] |
The frame header is sent in 1-wire mode via
qspi3_write_cmd(); pixel data is sent in QIO 4-wire mode viaqspi3_write_data(), maximizing pixel throughput.
// ── QSPI mode: lcd_write_cmddata() branch ──
uint8_t data[256] = {0x02, 0x00, 0x00, 0x00}; // frame header: 02 00 [Reg] 00
uint8_t datalen = 4;
data[2] = cmd; // fill in the register number
if (cmd == 0x2C) { // write GRAM → change command type to 0x12
data[0] = 0x12;
qspi3_write_cmd(qspilcd_handle, data, datalen, true); // 1-wire send header, CS held
return; // pixel data sent later via qspi3_write_data
}
while (len--) data[datalen++] = *param++; // append parameters
qspi3_write_cmd(qspilcd_handle, data, datalen, false); // 1-wire send the complete frame
// ── QSPI low level: qspi3_write_cmd() (1-wire header) ──
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 low level: qspi3_write_data() (QIO 4-wire pixels) ──
spi_transaction_t t = {0};
t.flags = SPI_TRANS_MODE_QIO; // ★ 4-wire 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 Complete Write Flow (write register example)
lcd_write_cmddata(0x2A, {0x00,0x00,0x03,0x1F}, 4) // set CASET = 0~799
┌─ LCD_SPI mode ───────────────────────────────────┐
│ DC=0 → [0x2A] → DC=1 → [00][00][03][1F] │
│ ↑ cmd ↑ data (4 bytes, 1-wire) │
└──────────────────────────────────────────────────┘
┌─ LCD_QSPI mode ──────────────────────────────────┐
│ [02][00][2A][00][00][00][03][1F] │
│ ↑ ↑ │
│ header (4B, 1-wire) parameters (4B, 1-wire) │
└──────────────────────────────────────────────────┘4. LCD Initialization and Refresh
4.1 Device Structure
typedef struct {
uint16_t width; // current width
uint16_t height; // current height
uint8_t dir; // 0=portrait, 1=landscape
} _qspilcd_dev;
extern _qspilcd_dev qspilcd_dev;
extern spi_device_handle_t qspilcd_handle;4.2 lcd_write_data — Write Data
lcd_write_data() automatically switches the transfer method between the two modes:
static void lcd_write_data(const uint8_t *data, int len)
{
#ifdef LCD_SPI
spi3_write_data(qspilcd_handle, data, len, true); // 1-wire
#elif defined(LCD_QSPI)
qspi3_write_data(qspilcd_handle, data, len, true); // QIO 4-wire
#endif
}Flow control: when
len == 0it only ends the transfer (releases CS); the actual pixel data is sent by this function in a chunked loop insidelcd_lv_cb().
4.3 lcd_set_window — Set Window
static void lcd_set_window(uint16_t xstar, uint16_t ystar,
uint16_t xend, uint16_t yend)
{
uint8_t data[4]; // 16-bit coordinates split into 2 big-endian bytes
data[0] = xstar >> 8; data[1] = 0x00FF & xstar;
data[2] = xend >> 8; data[3] = 0x00FF & xend;
lcd_write_cmddata(0x2A, data, 4); // CASET → column address
data[0] = ystar >> 8; data[1] = 0x00FF & ystar;
data[2] = yend >> 8; data[3] = 0x00FF & yend;
lcd_write_cmddata(0x2B, data, 4); // RASET → page address
lcd_write_cmddata(0x2C, NULL, 0); // RAMWR → start writing GRAM
}4.4 Initialization
void lcd_init(void)
{
// 1. WAIT pin as input
gpio_config(&(gpio_config_t){
.mode = GPIO_MODE_INPUT, .pin_bit_mask = 1ull << LCD_NUM_WAIT
});
// 2. RST pin as output (pull-up)
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 pin as output (SPI mode only)
gpio_config(&(gpio_config_t){
.mode = GPIO_MODE_OUTPUT, .pin_bit_mask = 1ull << LCD_NUM_DC
});
#endif
// 3. SPI3 bus
spi3_init();
// 4. Add device (80MHz / Mode 0 / Half-Duplex)
spi_bus_add_device(SPI3_HOST, &devcfg, &qspilcd_handle);
// 5. Hardware reset (RST low 100ms → high 800ms)
lcd_hard_reset();
// 6. Initialization commands
lcd_write_cmddata(0x20, (const uint8_t[]){0x32}, 1); // turn on backlight
// 7. Landscape 800×480
lcd_display_dir(1);
vTaskDelay(120);
}4.5 Display Orientation and Clear Screen
static void lcd_display_dir(uint8_t dir)
{
qspilcd_dev.dir = dir;
if (dir == 0) { // portrait: 480×800
qspilcd_dev.width = LCD_DISPLAY_HEIGHT;
qspilcd_dev.height = LCD_DISPLAY_WIDTH;
} else { // landscape: 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) // fill the whole screen with a solid 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); // end transfer
spi_device_release_bus(qspilcd_handle);
}4.6 LVGL Refresh Callback
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); // set window
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); // send to 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 Sharing Interface
qspilcd.c exports two functions for the touchscreen (TR230S mode) to share the SPI bus. The code structure and session management are identical to lcd_write_cmddata(), with the addition of spi_device_acquire/release_bus() for bus sharing:
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);
}Read/write frame format: write operations use the QSPI header
[02][00][Reg][00], read operations use the header[03][00][Reg][00]— exactly the same as Section 3.2.spi_device_acquire/release_bus()ensures mutually exclusive access to the same SPI3_HOST with the LVGL flush thread.
6. API Quick Reference
| Function | Mode | Description |
|---|---|---|
spi3_write_cmd() | 1-wire | Send a 1-byte command |
spi3_write_data() | 1-wire | Send data |
spi3_read_data() | 1-wire | Read data |
qspi3_write_cmd() | 1-wire | Send a QSPI command packet |
qspi3_write_data() | 4-wire | High-speed 4-wire pixel transfer |
qspi3_read_data() | 1-wire | 1-wire receive |
lcd_write_cmddata() | — | LCD register-write wrapper |
lcd_write_data() | — | LCD data-write wrapper |
lcd_set_window() | — | Set CASET+RASET+RAMWR |
lcd_lv_cb() | — | LVGL refresh callback (unit=sizeof(lv_color_t)) |
lcd_ctp_write_cmd() | — | CTP write (TR230S) |
lcd_ctp_read_cmd() | — | CTP read (TR230S) |
7. Porting Checklist
- [ ]
qspi.h— modify theQSPI_D0~D3/CLKpin macros - [ ]
qspilcd.h— modify theCS/WAIT/RST/DCpin macros - [ ]
qspilcd.h— modifyLCD_DISPLAY_WIDTH/HEIGHT - [ ]
qspilcd.h— selectLCD_QSPIorLCD_SPI - [ ]
qspilcd.c→lcd_init()— adapt the initialization command sequence to the display datasheet - [ ]
qspilcd.c→devcfg.clock_speed_hz— adjust the SPI clock (40~80 MHz)
8. Performance
| Metric | Value |
|---|---|
| SPI clock | 80 MHz |
| QSPI theoretical bandwidth | 80×4 = 320 Mbps ≈ 40 MB/s |
| DMA single-transfer limit | 32,768 bytes |
| Full-screen data (RGB565) | 800×480×2 = 768 KB |