diff --git a/.doc_for_ai/UDISPLAY_UTOUCH_GUIDE.md b/.doc_for_ai/UDISPLAY_UTOUCH_GUIDE.md index 703f446fc..918d202da 100644 --- a/.doc_for_ai/UDISPLAY_UTOUCH_GUIDE.md +++ b/.doc_for_ai/UDISPLAY_UTOUCH_GUIDE.md @@ -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. | diff --git a/CHANGELOG.md b/CHANGELOG.md index d10efcf31..33789167e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/lib_display/UDisplay/include/uDisplay.h b/lib/lib_display/UDisplay/include/uDisplay.h index 0d3ac97ee..13f174303 100644 --- a/lib/lib_display/UDisplay/include/uDisplay.h +++ b/lib/lib_display/UDisplay/include/uDisplay.h @@ -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; diff --git a/lib/lib_display/UDisplay/include/uDisplay_QSPI_panel.h b/lib/lib_display/UDisplay/include/uDisplay_QSPI_panel.h new file mode 100644 index 000000000..b8c104238 --- /dev/null +++ b/lib/lib_display/UDisplay/include/uDisplay_QSPI_panel.h @@ -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 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 = 1: 0x02 (1 line), same address, pixel data on D0 only (~4x slower) +// +// Descriptor header: +// :H,,,,16,QSPI,,,,,,,,,,[,] +// 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 diff --git a/lib/lib_display/UDisplay/src/uDisplay.cpp b/lib/lib_display/UDisplay/src/uDisplay.cpp index bf05b323d..363c5521e 100644 --- a/lib/lib_display/UDisplay/src/uDisplay.cpp +++ b/lib/lib_display/UDisplay/src/uDisplay.cpp @@ -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 diff --git a/lib/lib_display/UDisplay/src/uDisplay_QSPI_panel.cpp b/lib/lib_display/UDisplay/src/uDisplay_QSPI_panel.cpp new file mode 100644 index 000000000..53559e17b --- /dev/null +++ b/lib/lib_display/UDisplay/src/uDisplay_QSPI_panel.cpp @@ -0,0 +1,324 @@ +// ====================================================== +// uDisplay_QSPI_panel.cpp - QSPI (quad SPI) LCD/AMOLED Panel +// ====================================================== + +#ifdef ESP32 + +#include "uDisplay_QSPI_panel.h" +#include +#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 diff --git a/lib/lib_display/UDisplay/src/uDisplay_control.cpp b/lib/lib_display/UDisplay/src/uDisplay_control.cpp index 2f27162ae..608c51e1b 100644 --- a/lib/lib_display/UDisplay/src/uDisplay_control.cpp +++ b/lib/lib_display/UDisplay/src/uDisplay_control.cpp @@ -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(universal_panel); + qspi_pnl->writeCommand(dim_op, &dimmer8, 1); + } +#endif // UDISPLAY_QSPI } } diff --git a/lib/lib_display/UDisplay/src/uDisplay_touch.cpp b/lib/lib_display/UDisplay/src/uDisplay_touch.cpp index fbef02b25..bf8b41415 100644 --- a/lib/lib_display/UDisplay/src/uDisplay_touch.cpp +++ b/lib/lib_display/UDisplay/src/uDisplay_touch.cpp @@ -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++; + } } } diff --git a/tasmota/tasmota_xdrv_driver/xdrv_55_touch.ino b/tasmota/tasmota_xdrv_driver/xdrv_55_touch.ino index 8da4956bf..7f6934b70 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_55_touch.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_55_touch.ino @@ -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); } diff --git a/tasmota/tasmota_xdsp_display/xdsp_17_universal.ino b/tasmota/tasmota_xdsp_display/xdsp_17_universal.ino index 03ee71ff9..f083f5ac5 100644 --- a/tasmota/tasmota_xdsp_display/xdsp_17_universal.ino +++ b/tasmota/tasmota_xdsp_display/xdsp_17_universal.ino @@ -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')) {