mirror of
https://github.com/arendst/Tasmota.git
synced 2026-10-26 22:11:22 +00:00
uDisplay support for SPI QSPI displays (#25156)
This commit is contained in:
1 parent
6776e2ea9a
commit
fb629eddb5
10 files changed
+564
-23
No files matched your search
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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')) {
|
||||
|
||||
Reference in new issue
Block a user