uDisplay support for SPI QSPI displays (#25156)

This commit is contained in:
s-hadinger authored and GitHub committed 2026-10-18 18:36:41 +02:00
1 parent 6776e2ea9a
commit fb629eddb5
10 files changed
+564 -23

No files matched your search

+7 -2
View File
@@ -6,7 +6,7 @@ Use this as the authoring prompt for a new file in `tasmota/displaydesc/`. It de
- Store the file as `/display.ini`; enable uDisplay with any GPIO set to `Option A3`. Terminate it with `#`.
- `:H` is required and must be first. Put `:F` before `:A`. Fields are comma-separated.
- Do **not** use spaces in descriptor lines or inline comments: a space ends parsing for that line. A whole line beginning with `;` is a comment. uTouch instruction lines are the exception: they require single spaces between opcode and operands.
- Do **not** use spaces in descriptor lines or comments: a space ends the line, and the text after it is parsed as a new data line of the current section. This applies to `;` comment lines too. For example, a spaced comment after `:B` overwrote the `:B` values and broke the color byte order. Spaced comments are harmless only before `:H`. uTouch instruction lines are the exception: they require single spaces between opcode and operands.
- Numbers are decimal except controller bytes, addresses, `:0`–`:3`, and `:b`, which are hexadecimal. Hex accepts `2A` or `0x2A`; write controller bytes as two uppercase hex digits.
- `*` substitutes the matching Tasmota GPIO assignment; `-1` means not connected. Missing values become `255`, so always provide flag fields.
- Source priority is `/display.ini`, then Scripter `>d`, disabled `Rule3` (all spaces become line breaks, so no uTouch scripts), then `DSP_ROM_DESC`. In-memory sources are limited to 999 bytes; keep `DSP_ROM_DESC` below 1000 bytes.
@@ -20,6 +20,7 @@ Use this as the authoring prompt for a new file in `tasmota/displaydesc/`. It de
| `I2C` / `I2C2` | `addr(H),scl,sda,rst` | `I2C2` selects bus 2. `*` maps SCL/SDA and `Display Rst`. |
| `SPI` | `bus,cs,clk,mosi,dc,bl,rst,miso,mhz` | Bus `1` HW SPI, `2` HW HSPI, `3` fast software SPI (GPIO < 32), `4` slow software SPI. `dc=-1` selects 3-wire 9-bit SPI; `miso` is the e-paper BUSY pin. |
| `PAR` | `8\|16,rst,cs,dc,wr,rd,bl,d0..d7[,d8..d15],mhz` | ESP32-S3 I80 only; use explicit pins. |
| `QSPI` | `bus,cs,clk,d0,d1,d2,d3,bl,rst,mhz[,lines]` | ESP32 only; explicit pins (`*` not substituted). Bus `1` SPI2, `2` SPI3. Optional `lines`: `4` (default) sends pixels on D0–D3 with `32 00 2C 00`; `1` sends them on D0 only with `02 00 2C 00`, about 4x slower, as a workaround when D1–D3 are unreliable. For QSPI AMOLED controllers (SH8601, CO5300...): `:I`, `:A`, `:R`, `:0`–`:3`, `:i`, `:o`/`:O`, `:D` use plain DCS bytes; the driver adds the `02 00 cmd 00` / `32 00 2C 00` framing. |
| `RGB` | `de,vsync,hsync,pclk,bl,B0..B4,G0..G5,R0..R4,mhz` | ESP32-S3; use explicit pins. Also write `:V`; use `:B,...,02` for this pin order. |
| `DSI` | `lanes,te,bl,rst,ldo_ch,ldo_mv,pclk_hz,lane_mbps,rgb_order,endian` | ESP32-P4 only. `pclk_hz` is Hz; `te`, `rgb_order`, and `endian` are presently unused. Also write `:V`. |
@@ -85,7 +86,7 @@ Write only options that apply to the chosen bus/controller. For a color SPI/PAR
| `:R` | `madctl[,startline]` (H) | Rotation register, normally `36`; optional start-line command. |
| `:0`–`:3` | `madctl,xoff,yoff,touch` (H) | Per rotation. Offsets are hex. On RGB, use only `:n,touch`. |
| `:P` | `bits` (D) | Pixel transfer size; default 16, use `18` for RGB666 panels such as SPI ILI9488. |
| `:D` | `opcode` (H) | Dimmer command plus brightness byte; SPI/I2C only. |
| `:D` | `opcode` (H) | Dimmer command plus brightness byte; SPI/QSPI/I2C only. |
| `:b` | `flags` (H) | Backlight: bit `01` active low; bit `02` digital on/off only. |
| `:B` | `lines,flags` (D) | LVGL buffer lines (default 40). Flags: `02` swap color bytes, `08` e-paper BUSY active low, `16` invert monochrome B/W, `32` use PSRAM buffers. Bit `01` DMA has no current effect. |
| `:V` RGB | `hpol,hfp,hpw,hbp,vpol,vfp,vpw,vbp,pclk_neg` (D) | `0` polarity means idle low. |
@@ -133,11 +134,15 @@ Use uTouch rather than legacy `:TI`, `:TS`, and `:TR`; legacy tags require optio
- SPI form: `:UTI,name,S1,cs,rst,irq`; bus numbering matches `:H`; `*` maps to `TS SPI CS`, `TS RST`, and `TS IRQ`.
- Resistive PAR form: `:UTI,name,R`.
- An IRQ means `:UTT` runs only after an interrupt. Each script has a 64-byte compiled limit; keep it short.
- Scripts are polled every 50 ms. A failed or short I2C read ends the script with result `0` and clears `A[]`, so `:UTT` reports "not touched" instead of stale data.
- Operands are separated by single spaces. A single `;` line inside a script is skipped, but a second consecutive `;` line and any text after the operands are scanned for opcode names. Prefer `;` comment lines before `:UTI`.
- There is no shift opcode: `SCL 0 16` multiplies `R` by 16 (shift left 4), `AND` masks it.
| Opcode | Parameters | Effect |
|---|---|---|
| `RD` / `RDM` | `reg(H)` / `reg(H),count(D)` | Read 1 / many bytes from an 8-bit I2C register into `A[]`. |
| `RDW` / `RDWM` | `reg(H)` / `reg(H),count(D)` | Same with a 16-bit register. |
| `RDL` / `RDLM` | `reg(H)` / `reg(H),count(D)` | Same with a 32-bit big-endian register address (ex: CHSC5816). |
| `WR` / `WRW` | `reg,value` (H) | Write to 8-/16-bit I2C register. |
| `CP`, `CPM`, `CPR` | hex values | Compare `A[0]`, any listed value (`CPM` count is hex), or `R`. |
| `MV` | `index(D),mode(D)` | Copy `A[index]` to `R`: mode 1 byte, 2 big-endian word, 3 little-endian word. |
+1
View File
@@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
## [16.0.0.1]
### Added
- Support OUI wildcard for command MI32Keys in case multiple devices use same bindkey like IoTorero: `mi32keys 0C3D5EFFFFFF=48656c6c6f204254486f6d65210d0a00`
- uDisplay support for SPI QSPI displays
### Breaking Changed
- BTHome un-reverse MAC address usage for command `MI32Keys` to make it consistent
+9 -2
View File
@@ -24,6 +24,8 @@
#if SOC_MIPI_DSI_SUPPORTED
#include "uDisplay_DSI_panel.h"
#endif
#define UDISPLAY_QSPI
#include "uDisplay_QSPI_panel.h"
#ifdef CONFIG_IDF_TARGET_ESP32S3
#define USE_ESP32_S3
@@ -61,7 +63,7 @@ union PanelConfigUnion {
};
enum {
UT_RD,UT_RDM,UT_CP,UT_RTF,UT_MV,UT_MVB,UT_RT,UT_RTT,UT_RDW,UT_RDWM,UT_WR,UT_WRW,UT_CPR,UT_AND,UT_SCALE,UT_LIM,UT_DBG,UT_GSRT,UT_XPT,UT_CPM,UT_END
UT_RD,UT_RDM,UT_CP,UT_RTF,UT_MV,UT_MVB,UT_RT,UT_RTT,UT_RDW,UT_RDWM,UT_WR,UT_WRW,UT_CPR,UT_AND,UT_SCALE,UT_LIM,UT_DBG,UT_GSRT,UT_XPT,UT_CPM,UT_RDL,UT_RDLM,UT_END
};
#define UDSP_WRITE_16 0xf0
@@ -78,6 +80,7 @@ enum {
#define _UDSP_PAR16 4
#define _UDSP_RGB 5
#define _UDSP_DSI 6
#define _UDSP_QSPI 7
#define UDISP1_WHITE 1
#define UDISP1_BLACK 0
@@ -148,7 +151,10 @@ private:
#endif
char dname[16];
SPIController *spiController;
SPIController *spiController = nullptr;
#ifdef UDISPLAY_QSPI
QSPIBusConfig qspi_bus = {};
#endif
TwoWire *wire;
UniversalPanel* universal_panel = nullptr;
@@ -273,6 +279,7 @@ private:
int16_t besttwoavg( int16_t x , int16_t y , int16_t z );
uint8_t ut_array[16];
bool ut_rd_ok = true; // false when the last I2C read failed or was short
uint8_t ut_i2caddr;
int8_t ut_spi_cs = -1;
int8_t ut_reset = -1;
@@ -0,0 +1,91 @@
// ======================================================
// uDisplay_QSPI_panel.h - QSPI (quad SPI) LCD/AMOLED Panel
// ======================================================
//
// For MIPI-DCS style controllers wired in QSPI mode (SH8601, CO5300, RM67162...)
// Every command is sent in its own CS frame as:
// 0x02 (1 line), 24-bit address 0x00 <cmd> 0x00 (1 line), parameters (1 line)
// Pixels are sent as:
// 0x32 (1 line), 24-bit address 0x00 2C|3C 0x00 (1 line), pixel data on 4 lines
// or with <lines> = 1: 0x02 (1 line), same address, pixel data on D0 only (~4x slower)
//
// Descriptor header:
// :H,<name>,<w>,<h>,16,QSPI,<bus>,<cs>,<clk>,<d0>,<d1>,<d2>,<d3>,<bl>,<rst>,<mhz>[,<lines>]
// bus: 1 = SPI2 (FSPI), 2 = SPI3 (if available on the SoC)
// lines: pixel data lines, 4 (default) or 1
#pragma once
#ifdef ESP32
#include "uDisplay_panel.h"
#include "uDisplay_SPI_panel.h" // SPIPanelConfig (commands, rotation, offsets)
#include "driver/spi_master.h"
struct QSPIBusConfig {
uint8_t bus_nr; // 1 = SPI2_HOST, 2 = SPI3_HOST
int8_t cs;
int8_t clk;
int8_t d0;
int8_t d1;
int8_t d2;
int8_t d3;
uint8_t speed_mhz;
uint8_t data_lines; // pixel data lines: 4 (default) or 1 (D0 only, 0x02 framing)
};
class QSPIPanel : public UniversalPanel {
public:
QSPIPanel(const SPIPanelConfig& config, const QSPIBusConfig& bus,
const uint8_t* init_cmds, uint16_t init_cmds_len);
~QSPIPanel();
bool isReady() const { return ready; }
// ===== UniversalPanel Interface =====
bool drawPixel(int16_t x, int16_t y, uint16_t color) override;
bool fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) override;
bool pushColors(uint16_t *data, uint32_t len, bool not_swapped = false) override;
bool setAddrWindow(int16_t x0, int16_t y0, int16_t x1, int16_t y1) override;
bool drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color) override;
bool drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color) override;
bool displayOnff(int8_t on) override;
bool invertDisplay(bool invert) override;
bool setRotation(uint8_t rotation) override;
bool updateFrame() override;
// Send a DCS command with optional parameters (1-line, own CS frame)
void writeCommand(uint8_t cmd, const uint8_t* params = nullptr, uint32_t len = 0);
private:
static constexpr uint32_t CHUNK_PIXELS = 2048; // pixels per DMA transaction
static constexpr uint8_t CMD_WRITE_1LINE = 0x02;
static constexpr uint8_t CMD_WRITE_4LINE = 0x32;
static constexpr uint8_t DCS_RAMWR_CONTINUE = 0x3C;
SPIPanelConfig cfg;
QSPIBusConfig bus_cfg;
spi_host_device_t host;
spi_device_handle_t dev = nullptr;
bool bus_initialized = false;
bool ready = false;
uint16_t* dma_buf[2] = { nullptr, nullptr };
spi_transaction_ext_t trans[2];
uint8_t rotation = 0;
uint16_t width;
uint16_t height;
bool ram_write_start = true; // next pixel push starts with RAMWR (2C) instead of continue (3C)
void csLow();
void csHigh();
void sendInitCommands(const uint8_t* cmds, uint16_t len);
void setWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1); // inclusive coordinates
void sendPixels(const uint16_t* data, uint32_t len, bool swap);
void sendRepeat(uint16_t color, uint32_t len);
void startPixelTrans(uint32_t idx, const uint16_t* buf, uint32_t n, bool first);
};
#endif // ESP32
+54 -7
View File
@@ -268,6 +268,23 @@ uDisplay::uDisplay(char *lp) : Renderer(800, 600) {
spi_cfg.speed = next_val(&lp1);
spiController = new SPIController(spi_cfg);
section = 0;
} else if (!strncmp(ibuff, "QSPI", 4)) {
#ifdef UDISPLAY_QSPI
// QSPI,bus,cs,clk,d0,d1,d2,d3,bl,rst,mhz[,lines]
interface = _UDSP_QSPI;
qspi_bus.bus_nr = next_val(&lp1);
qspi_bus.cs = next_val(&lp1);
qspi_bus.clk = next_val(&lp1);
qspi_bus.d0 = next_val(&lp1);
qspi_bus.d1 = next_val(&lp1);
qspi_bus.d2 = next_val(&lp1);
qspi_bus.d3 = next_val(&lp1);
bpanel = next_val(&lp1);
reset = next_val(&lp1);
qspi_bus.speed_mhz = next_val(&lp1);
qspi_bus.data_lines = (next_val(&lp1) == 1) ? 1 : 4; // optional, default 4
#endif // UDISPLAY_QSPI
section = 0;
} else if (!strncmp(ibuff, "PAR", 3)) {
#if defined(UDISPLAY_I80)
uint8_t bus = next_val(&lp1);
@@ -513,7 +530,7 @@ uDisplay::uDisplay(char *lp) : Renderer(800, 600) {
break;
case 'R':
// Parse directly into SPI config (used by SPI panels only)
if (interface == _UDSP_SPI) {
if (interface == _UDSP_SPI || interface == _UDSP_QSPI) {
panel_config->spi.cmd_memory_access = next_hex(&lp1);
panel_config->spi.cmd_startline = next_hex(&lp1);
}
@@ -530,7 +547,7 @@ uDisplay::uDisplay(char *lp) : Renderer(800, 600) {
break;
case '0':
if (interface != _UDSP_RGB) {
if (interface == _UDSP_SPI) {
if (interface == _UDSP_SPI || interface == _UDSP_QSPI) {
// Parse directly into SPI config
panel_config->spi.rot_cmd[0] = next_hex(&lp1);
panel_config->spi.x_addr_offset[0] = next_hex(&lp1);
@@ -554,7 +571,7 @@ uDisplay::uDisplay(char *lp) : Renderer(800, 600) {
break;
case '1':
if (interface != _UDSP_RGB) {
if (interface == _UDSP_SPI) {
if (interface == _UDSP_SPI || interface == _UDSP_QSPI) {
panel_config->spi.rot_cmd[1] = next_hex(&lp1);
panel_config->spi.x_addr_offset[1] = next_hex(&lp1);
panel_config->spi.y_addr_offset[1] = next_hex(&lp1);
@@ -577,7 +594,7 @@ uDisplay::uDisplay(char *lp) : Renderer(800, 600) {
break;
case '2':
if (interface != _UDSP_RGB) {
if (interface == _UDSP_SPI) {
if (interface == _UDSP_SPI || interface == _UDSP_QSPI) {
panel_config->spi.rot_cmd[2] = next_hex(&lp1);
panel_config->spi.x_addr_offset[2] = next_hex(&lp1);
panel_config->spi.y_addr_offset[2] = next_hex(&lp1);
@@ -600,7 +617,7 @@ uDisplay::uDisplay(char *lp) : Renderer(800, 600) {
break;
case '3':
if (interface != _UDSP_RGB) {
if (interface == _UDSP_SPI) {
if (interface == _UDSP_SPI || interface == _UDSP_QSPI) {
panel_config->spi.rot_cmd[3] = next_hex(&lp1);
panel_config->spi.x_addr_offset[3] = next_hex(&lp1);
panel_config->spi.y_addr_offset[3] = next_hex(&lp1);
@@ -656,7 +673,7 @@ uDisplay::uDisplay(char *lp) : Renderer(800, 600) {
i2c_col_start = panel_config->i2c.col_start;
i2c_col_end = panel_config->i2c.col_end;
saw_3 = panel_config->i2c.cmd_write_ram;
} else if (interface == _UDSP_SPI) {
} else if (interface == _UDSP_SPI || interface == _UDSP_QSPI) {
// Parse directly into SPI config
panel_config->spi.cmd_set_addr_x = next_hex(&lp1);
panel_config->spi.cmd_set_addr_y = next_hex(&lp1);
@@ -712,7 +729,7 @@ uDisplay::uDisplay(char *lp) : Renderer(800, 600) {
case 'i':
inv_off = next_hex(&lp1);
inv_on = next_hex(&lp1);
if (interface == _UDSP_SPI) {
if (interface == _UDSP_SPI || interface == _UDSP_QSPI) {
panel_config->spi.cmd_invert_off = inv_off;
panel_config->spi.cmd_invert_on = inv_on;
} else if (interface == _UDSP_I2C) {
@@ -1318,6 +1335,36 @@ if (interface == _UDSP_SPI) {
// so we don't need to call Init_EPD here anymore
}
#ifdef UDISPLAY_QSPI
if (interface == _UDSP_QSPI) {
HandeBP(-1);
if (reset >= 0) {
pinMode(reset, OUTPUT);
digitalWrite(reset, HIGH);
delay(10);
reset_pin(200, 200);
}
// Commands, rotation and offsets are parsed into the SPI config
panel_config->spi.width = gxs;
panel_config->spi.height = gys;
panel_config->spi.bpp = bpp;
panel_config->spi.cmd_display_on = dsp_on;
panel_config->spi.cmd_display_off = dsp_off;
panel_config->spi.reset_pin = reset;
panel_config->spi.bpanel = bpanel;
panel_config->spi.all_commands_mode = false;
QSPIPanel *qspi_panel = new QSPIPanel(panel_config->spi, qspi_bus, dsp_cmds, dsp_ncmds);
if (qspi_panel->isReady()) {
universal_panel = qspi_panel;
} else {
delete qspi_panel;
}
}
#endif // UDISPLAY_QSPI
#if SOC_LCD_RGB_SUPPORTED
if (interface == _UDSP_RGB) {
if (!UsePSRAM()) { // RGB is not supported on S3 without PSRAM
@@ -0,0 +1,324 @@
// ======================================================
// uDisplay_QSPI_panel.cpp - QSPI (quad SPI) LCD/AMOLED Panel
// ======================================================
#ifdef ESP32
#include "uDisplay_QSPI_panel.h"
#include <Arduino.h>
#include "esp_heap_caps.h"
#include "soc/soc_caps.h"
extern void AddLog(uint32_t loglevel, const char* formatP, ...);
QSPIPanel::QSPIPanel(const SPIPanelConfig& config, const QSPIBusConfig& bus,
const uint8_t* init_cmds, uint16_t init_cmds_len)
: cfg(config), bus_cfg(bus)
{
width = cfg.width;
height = cfg.height;
memset(trans, 0, sizeof(trans));
#if SOC_SPI_PERIPH_NUM > 2
host = (bus_cfg.bus_nr == 2) ? SPI3_HOST : SPI2_HOST;
#else
host = SPI2_HOST;
#endif
uint32_t speed_mhz = bus_cfg.speed_mhz;
if (speed_mhz == 0 || speed_mhz == 0xFF) { speed_mhz = 40; }
// CS is driven manually: it must stay low across several transactions
// (pixel stream split into DMA chunks)
if (bus_cfg.cs >= 0) {
pinMode(bus_cfg.cs, OUTPUT);
digitalWrite(bus_cfg.cs, HIGH);
}
spi_bus_config_t buscfg;
memset(&buscfg, 0, sizeof(buscfg));
buscfg.data0_io_num = bus_cfg.d0;
buscfg.data1_io_num = bus_cfg.d1;
buscfg.sclk_io_num = bus_cfg.clk;
buscfg.data2_io_num = bus_cfg.d2;
buscfg.data3_io_num = bus_cfg.d3;
buscfg.data4_io_num = -1;
buscfg.data5_io_num = -1;
buscfg.data6_io_num = -1;
buscfg.data7_io_num = -1;
buscfg.max_transfer_sz = CHUNK_PIXELS * 2;
buscfg.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_GPIO_PINS | SPICOMMON_BUSFLAG_QUAD;
esp_err_t ret = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
if (ret != ESP_OK) {
AddLog(1, "DSP: QSPI bus init failed %d", ret);
return;
}
bus_initialized = true;
spi_device_interface_config_t devcfg;
memset(&devcfg, 0, sizeof(devcfg));
devcfg.command_bits = 8;
devcfg.address_bits = 24;
devcfg.dummy_bits = 0;
devcfg.mode = 0;
devcfg.clock_speed_hz = (int)(speed_mhz * 1000000);
devcfg.spics_io_num = -1; // manual CS
devcfg.flags = SPI_DEVICE_HALFDUPLEX;
devcfg.queue_size = 1;
ret = spi_bus_add_device(host, &devcfg, &dev);
if (ret != ESP_OK) {
AddLog(1, "DSP: QSPI add device failed %d", ret);
dev = nullptr;
return;
}
for (uint32_t i = 0; i < 2; i++) {
dma_buf[i] = (uint16_t*)heap_caps_malloc(CHUNK_PIXELS * 2, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
if (!dma_buf[i]) {
AddLog(1, "DSP: QSPI no DMA memory");
return;
}
}
ready = true;
sendInitCommands(init_cmds, init_cmds_len);
AddLog(2, "DSP: QSPI panel host %d cs %d clk %d d0-3 %d,%d,%d,%d %d MHz, %d data line%s",
host, bus_cfg.cs, bus_cfg.clk, bus_cfg.d0, bus_cfg.d1, bus_cfg.d2, bus_cfg.d3, speed_mhz,
bus_cfg.data_lines, (bus_cfg.data_lines == 1) ? "" : "s");
}
QSPIPanel::~QSPIPanel() {
if (dev) { spi_bus_remove_device(dev); }
if (bus_initialized) { spi_bus_free(host); }
for (uint32_t i = 0; i < 2; i++) {
if (dma_buf[i]) { heap_caps_free(dma_buf[i]); }
}
}
// ===== Low level =====
void QSPIPanel::csLow() {
if (bus_cfg.cs >= 0) { digitalWrite(bus_cfg.cs, LOW); }
}
void QSPIPanel::csHigh() {
if (bus_cfg.cs >= 0) { digitalWrite(bus_cfg.cs, HIGH); }
}
void QSPIPanel::writeCommand(uint8_t cmd, const uint8_t* params, uint32_t len) {
if (!ready) { return; }
spi_transaction_t t;
memset(&t, 0, sizeof(t));
t.cmd = CMD_WRITE_1LINE;
t.addr = ((uint32_t)cmd) << 8;
if (params && len) {
if (len > CHUNK_PIXELS * 2) { len = CHUNK_PIXELS * 2; }
if (len <= 4) {
t.flags = SPI_TRANS_USE_TXDATA;
memcpy(t.tx_data, params, len);
} else {
memcpy(dma_buf[0], params, len);
t.tx_buffer = dma_buf[0];
}
t.length = len * 8;
}
csLow();
spi_device_polling_transmit(dev, &t);
csHigh();
}
// Init stream uses the uDisplay SPI format: CMD,CNT,ARG1..ARGn
// CNT & 0x1F = number of args, CNT & 0xE0 = delay code (80=150ms, A0=10ms, E0=500ms)
void QSPIPanel::sendInitCommands(const uint8_t* cmds, uint16_t len) {
if (!cmds) { return; }
uint16_t i = 0;
while (i + 1 < len) {
uint8_t cmd = cmds[i++];
uint8_t cnt = cmds[i++];
uint32_t nargs = cnt & 0x1F;
if (i + nargs > len) { nargs = len - i; }
writeCommand(cmd, &cmds[i], nargs);
i += nargs;
uint32_t delay_ms = 0;
switch (cnt & 0xE0) {
case 0x80: delay_ms = 150; break;
case 0xA0: delay_ms = 10; break;
case 0xE0: delay_ms = 500; break;
}
if (delay_ms) { delay(delay_ms); }
}
}
void QSPIPanel::setWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
x0 += cfg.x_addr_offset[rotation];
x1 += cfg.x_addr_offset[rotation];
y0 += cfg.y_addr_offset[rotation];
y1 += cfg.y_addr_offset[rotation];
uint8_t xa[4] = { (uint8_t)(x0 >> 8), (uint8_t)x0, (uint8_t)(x1 >> 8), (uint8_t)x1 };
uint8_t ya[4] = { (uint8_t)(y0 >> 8), (uint8_t)y0, (uint8_t)(y1 >> 8), (uint8_t)y1 };
writeCommand(cfg.cmd_set_addr_x, xa, 4);
writeCommand(cfg.cmd_set_addr_y, ya, 4);
ram_write_start = true;
}
// Start a pixel transaction. The first one of a CS frame carries the
// 0x32 (or 0x02 in 1-line mode) + RAMWR header, the following ones are raw
// data continuing the stream.
void QSPIPanel::startPixelTrans(uint32_t idx, const uint16_t* buf, uint32_t n, bool first) {
spi_transaction_ext_t* t = &trans[idx];
memset(t, 0, sizeof(spi_transaction_ext_t));
bool quad = (bus_cfg.data_lines != 1);
uint32_t mode_flags = quad ? SPI_TRANS_MODE_QIO : 0;
if (first) {
uint8_t ramcmd = (ram_write_start && cfg.cmd_write_ram != 0xFF) ? cfg.cmd_write_ram : DCS_RAMWR_CONTINUE;
t->base.flags = mode_flags;
t->base.cmd = quad ? CMD_WRITE_4LINE : CMD_WRITE_1LINE;
t->base.addr = ((uint32_t)ramcmd) << 8;
ram_write_start = false;
} else {
t->base.flags = mode_flags | SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY;
t->command_bits = 0;
t->address_bits = 0;
t->dummy_bits = 0;
}
t->base.tx_buffer = buf;
t->base.length = n * 16;
spi_device_polling_start(dev, &t->base, portMAX_DELAY);
}
// Send pixels, converting to big-endian (MSB first) if `swap`.
// Double buffered: next chunk is prepared while the previous one is transferred.
void QSPIPanel::sendPixels(const uint16_t* data, uint32_t len, bool swap) {
bool first = true;
bool pending = false;
uint32_t idx = 0;
csLow();
while (len) {
uint32_t n = (len > CHUNK_PIXELS) ? CHUNK_PIXELS : len;
uint16_t* buf = dma_buf[idx];
if (swap) {
for (uint32_t k = 0; k < n; k++) {
uint16_t c = data[k];
buf[k] = (c << 8) | (c >> 8);
}
} else {
memcpy(buf, data, n * 2);
}
data += n;
len -= n;
if (pending) { spi_device_polling_end(dev, portMAX_DELAY); }
startPixelTrans(idx, buf, n, first);
pending = true;
first = false;
idx ^= 1;
}
if (pending) { spi_device_polling_end(dev, portMAX_DELAY); }
csHigh();
}
void QSPIPanel::sendRepeat(uint16_t color, uint32_t len) {
if (!len) { return; }
uint16_t be_color = (color << 8) | (color >> 8);
uint32_t fill = (len > CHUNK_PIXELS) ? CHUNK_PIXELS : len;
uint16_t* buf = dma_buf[0];
for (uint32_t k = 0; k < fill; k++) { buf[k] = be_color; }
bool first = true;
bool pending = false;
uint32_t idx = 0;
csLow();
while (len) {
uint32_t n = (len > CHUNK_PIXELS) ? CHUNK_PIXELS : len;
len -= n;
if (pending) { spi_device_polling_end(dev, portMAX_DELAY); }
startPixelTrans(idx, buf, n, first); // same read-only buffer, separate transaction structs
pending = true;
first = false;
idx ^= 1;
}
if (pending) { spi_device_polling_end(dev, portMAX_DELAY); }
csHigh();
}
// ===== UniversalPanel Interface =====
bool QSPIPanel::drawPixel(int16_t x, int16_t y, uint16_t color) {
return fillRect(x, y, 1, 1, color);
}
bool QSPIPanel::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
if (!ready) { return true; }
if (x < 0) { w += x; x = 0; }
if (y < 0) { h += y; y = 0; }
if ((x >= width) || (y >= height) || (w <= 0) || (h <= 0)) { return true; }
if ((x + w) > width) { w = width - x; }
if ((y + h) > height) { h = height - y; }
setWindow(x, y, x + w - 1, y + h - 1);
sendRepeat(color, (uint32_t)w * h);
return true;
}
bool QSPIPanel::pushColors(uint16_t *data, uint32_t len, bool not_swapped) {
if (!ready || !data || !len) { return true; }
// not_swapped == true: native RGB565, needs MSB first on the wire
sendPixels(data, len, not_swapped);
return true;
}
// x1/y1 are exclusive (LVGL passes x1 + width); (0,0,0,0) ends a transfer
bool QSPIPanel::setAddrWindow(int16_t x0, int16_t y0, int16_t x1, int16_t y1) {
if (!ready) { return true; }
if (!x0 && !y0 && !x1 && !y1) { return true; }
if (x1 <= x0 || y1 <= y0) { return true; }
setWindow(x0, y0, x1 - 1, y1 - 1);
return true;
}
bool QSPIPanel::drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color) {
return fillRect(x, y, w, 1, color);
}
bool QSPIPanel::drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color) {
return fillRect(x, y, 1, h, color);
}
bool QSPIPanel::displayOnff(int8_t on) {
if (on && cfg.cmd_display_on != 0xFF) {
writeCommand(cfg.cmd_display_on);
} else if (!on && cfg.cmd_display_off != 0xFF) {
writeCommand(cfg.cmd_display_off);
}
return false; // let uDisplay handle the backlight pin, if any
}
bool QSPIPanel::invertDisplay(bool invert) {
if (invert && cfg.cmd_invert_on != 0xFF) {
writeCommand(cfg.cmd_invert_on);
} else if (!invert && cfg.cmd_invert_off != 0xFF) {
writeCommand(cfg.cmd_invert_off);
}
return true;
}
bool QSPIPanel::setRotation(uint8_t rot) {
rotation = rot & 3;
if (cfg.cmd_memory_access != 0xFF && cfg.rot_cmd[rotation] != 0xFF) {
writeCommand(cfg.cmd_memory_access, &cfg.rot_cmd[rotation], 1);
}
if (rotation & 1) {
width = cfg.height;
height = cfg.width;
} else {
width = cfg.width;
height = cfg.height;
}
return true;
}
bool QSPIPanel::updateFrame() {
return false;
}
#endif // ESP32
@@ -135,6 +135,12 @@ void uDisplay::dim10(uint8_t dim, uint16_t dim_gamma) {
spiController->csHigh();
spiController->endTransaction();
}
#ifdef UDISPLAY_QSPI
else if (interface == _UDSP_QSPI && universal_panel) {
QSPIPanel* qspi_pnl = static_cast<QSPIPanel*>(universal_panel);
qspi_pnl->writeCommand(dim_op, &dimmer8, 1);
}
#endif // UDISPLAY_QSPI
}
}
@@ -98,6 +98,12 @@ void uDisplay::ut_trans(char **sp, uint8_t **code) {
if (*cp == ':' || *cp == '#') {
break;
}
if (ut_code - tmp_code > (int)sizeof(tmp_code) - 12) {
// keep room for the largest instruction and UT_END
AddLog(LOG_LEVEL_INFO, PSTR("UTI: touch script too long, truncated"));
while (*cp && *cp != ':' && *cp != '#') cp++;
break;
}
if (*cp == ';') {
// skip comment line
while (*cp) {
@@ -109,7 +115,28 @@ void uDisplay::ut_trans(char **sp, uint8_t **code) {
}
}
if (!strncmp(cp, "RDWM", 4)) {
if (!strncmp(cp, "RDLM", 4)) {
// read many with 32-bit register address
*ut_code++ = UT_RDLM;
uint32_t lval = ut_par(&cp, 0);
*ut_code++ = lval >> 24;
*ut_code++ = lval >> 16;
*ut_code++ = lval >> 8;
*ut_code++ = lval;
wval = ut_par(&cp, 1);
if (wval > sizeof(ut_array)) {
wval = sizeof(ut_array);
}
*ut_code++ = wval;
} else if (!strncmp(cp, "RDL", 3)) {
// read one with 32-bit register address
*ut_code++ = UT_RDL;
uint32_t lval = ut_par(&cp, 0);
*ut_code++ = lval >> 24;
*ut_code++ = lval >> 16;
*ut_code++ = lval >> 8;
*ut_code++ = lval;
} else if (!strncmp(cp, "RDWM", 4)) {
// read word many
*ut_code++ = UT_RDWM;
wval = ut_par(&cp, 0);
@@ -147,9 +174,13 @@ void uDisplay::ut_trans(char **sp, uint8_t **code) {
// cmp multiple and set
*ut_code++ = UT_CPM;
uint8_t num = ut_par(&cp, 0);
*ut_code++ = num;
uint8_t kept = (num > 8) ? 8 : num; // keep within the compile buffer guard
*ut_code++ = kept;
for (uint32_t cnt = 0; cnt < num; cnt++) {
*ut_code++ = ut_par(&cp, 0);
uint8_t val = ut_par(&cp, 0);
if (cnt < kept) {
*ut_code++ = val;
}
}
} else if (!strncmp(cp, "CP", 2)) {
// cmp and set
@@ -249,8 +280,8 @@ uint32_t uDisplay::ut_par(char **lp, uint32_t mode) {
uint32_t result;
if (!mode) {
// hex
result = strtol(cp, &cp, 16);
// hex, unsigned so that 32-bit register addresses do not saturate
result = strtoul(cp, &cp, 16);
} else if (mode == 1) {
// word
result = strtol(cp, &cp, 10);
@@ -275,6 +306,7 @@ int16_t uDisplay::ut_execute(uint8_t *ut_code) {
uint8_t iob, len;
uint16_t wval;
ut_rd_ok = true;
while (*ut_code != UT_END) {
iob = *ut_code++;
switch (iob) {
@@ -298,6 +330,16 @@ int16_t uDisplay::ut_execute(uint8_t *ut_code) {
ut_code = ut_rd(ut_code, 2, 2);
break;
case UT_RDL:
// read 1 byte, 32-bit register address
ut_code = ut_rd(ut_code, 1, 4);
break;
case UT_RDLM:
// read multiple bytes, 32-bit register address
ut_code = ut_rd(ut_code, 2, 4);
break;
case UT_WR:
ut_code = ut_wr(ut_code, 1);
break;
@@ -452,6 +494,10 @@ int16_t uDisplay::ut_execute(uint8_t *ut_code) {
case UT_END:
break;
}
if (!ut_rd_ok) {
// failed or short I2C read: don't act on stale data
return 0;
}
}
return result;
@@ -460,10 +506,9 @@ int16_t uDisplay::ut_execute(uint8_t *ut_code) {
// ===== Low-Level Touch Communication =====
uint8_t *uDisplay::ut_rd(uint8_t *iop, uint32_t len, uint32_t amode) {
if (ut_wire) {
// i2c mode
// i2c mode, amode is the register address size in bytes (1, 2 or 4)
ut_wire->beginTransmission(ut_i2caddr);
ut_wire->write(*iop++);
if (amode == 2) {
for (uint32_t i = 0; i < amode; i++) {
ut_wire->write(*iop++);
}
ut_wire->endTransmission(false);
@@ -473,7 +518,15 @@ uint8_t *uDisplay::ut_rd(uint8_t *iop, uint32_t len, uint32_t amode) {
ut_wire->requestFrom(ut_i2caddr, (size_t)len);
uint8_t index = 0;
while (ut_wire->available()) {
ut_array[index++] = ut_wire->read();
uint8_t val = ut_wire->read();
if (index < sizeof(ut_array)) {
ut_array[index++] = val;
}
}
if (index < len) {
// NACK or short read: clear the buffer so stale values are never reused
memset(ut_array, 0, sizeof(ut_array));
ut_rd_ok = false;
}
} else {
// spi mode
@@ -490,6 +543,12 @@ uint8_t *uDisplay::ut_rd(uint8_t *iop, uint32_t len, uint32_t amode) {
ut_array[len + 1] = val;
digitalWrite(ut_spi_cs, HIGH);
}
} else {
// word/long register reads are I2C only: skip the operands
iop += amode;
if (len > 1) {
iop++;
}
}
}
@@ -598,7 +598,8 @@ bool Xdrv55(uint32_t function) {
switch (function) {
case FUNC_INIT:
break;
case FUNC_EVERY_100_MSECOND:
case FUNC_EVERY_50_MSECOND:
// 50 ms matches LVGL input reads and Berry touch drivers; 100 ms felt laggy
if (CST816S_found || FT5206_found || XPT2046_found || GT911_found || utouch_found || SRES_found) {
Touch_Check(TS_RotConvert);
}
@@ -206,12 +206,12 @@ Renderer *Init_uDisplay(const char *desc) {
}
}
cp = strstr(ddesc, "SPI,");
cp = strstr(ddesc, ",SPI,"); // leading comma so that ",QSPI," does not match
if (cp) {
// 9 params nr,cs,sclk,mosi,dc,bl,reset,miso,mhz
// SPI,1,*,*,*,*,*,*,*,80
// SPI,3,*,*,*,*,*,*,*,40
cp += 4;
cp += 5;
// 1,*,*,*,*,*,*,*,80
uint32_t spi_type = 2; // SPI,3 = Software SPI
if ((*cp == '1') || (*cp == '2')) {