TinyGPU
Loading...
Searching...
No Matches
DisplayDriverParallel8ESP32.h
Go to the documentation of this file.
1#pragma once
2#include <initializer_list>
3#include <stdint.h>
4#include <string.h>
5
6#include "TinyGPU/Drivers/DisplayDriver.h"
7#include "TinyGPU/Emulation.h"
8
9#if defined(ESP32)
10#include "esp_lcd_panel_io.h"
11#include "soc/soc_caps.h"
12// Both headers are safe to include unconditionally - the declarations
13// inside each are themselves guarded by the matching SOC_..._SUPPORTED
14// macro, so only the one the target chip actually has compiles to
15// anything.
16#include "esp_lcd_io_i80.h"
17#include "esp_lcd_io_parl.h"
18#define TINYGPU_HAS_ESP_LCD_PARALLEL8 1
19#else
20#error
21 "DisplayDriverParallel8ESP32.h needs the ESP-IDF esp_lcd component (LCD_CAM/I80 or PARLIO peripheral) - ESP32-only. For a portable, any-Arduino-core bit-banged 8-bit parallel driver see DisplayDriverParallel8.h instead."
22#endif
23
24namespace tinygpu {
25
26#ifdef TINYGPU_HAS_ESP_LCD_PARALLEL8
27
28#if SOC_LCD_I80_SUPPORTED
29#define TINYGPU_PARALLEL8_BACKEND_I80 1
30#elif SOC_PARLIO_SUPPORTED
31#define TINYGPU_PARALLEL8_BACKEND_PARLIO 1
32#else
33#error
34 "This ESP32 target has neither the LCD_CAM/I80 peripheral (SOC_LCD_I80_SUPPORTED) nor the PARLIO peripheral (SOC_PARLIO_SUPPORTED) - hardware-accelerated 8-bit parallel output isn't available on it. Use the portable bit-banged DisplayDriverParallel8.h instead."
35#endif
36
37/**
38 * @brief Hardware-accelerated 8-bit parallel ("Intel 8080"-style) TFT
39 * display driver for ESP32-family targets, built on ESP-IDF's esp_lcd
40 * panel-IO layer - the same layer DisplayDriverQSPI.h uses for QSPI, just
41 * pointed at a different underlying peripheral. Where DisplayDriverSPI.h's
42 * hardware-parallel counterpart (DisplayDriverParallel8.h) bit-bangs D0-D7
43 * one digitalWrite() per bit, this class drives the bus via DMA through
44 * whichever peripheral the target actually has:
45 *
46 * - SOC_LCD_I80_SUPPORTED targets (ESP32, ESP32-S2, ESP32-S3): the
47 * LCD_CAM peripheral's I80 mode, via esp_lcd_new_i80_bus() +
48 * esp_lcd_new_panel_io_i80().
49 * - SOC_PARLIO_SUPPORTED targets (ESP32-C6, ESP32-H2, ESP32-P4, ...,
50 * which have no LCD_CAM): the PARLIO peripheral's 8-bit parallel LCD
51 * mode, via esp_lcd_new_panel_io_parl().
52 *
53 * Which backend compiles is decided automatically from the target's
54 * soc_caps.h (TINYGPU_PARALLEL8_BACKEND_I80 / _PARLIO, set above) - the
55 * public API (constructor, writeCommand()/writeData()/setAddressWindow())
56 * is identical either way, so concrete panel drivers below don't need to
57 * know or care which peripheral is underneath.
58 *
59 * Build-verified (compiles and links) for the I80 backend via arduino-cli
60 * against esp32:esp32 3.3.10 on an ESP32-S3 target
61 * (esp32:esp32:adafruit_metro_esp32s3). The PARLIO backend's source
62 * compiles cleanly against the same core/version on an ESP32-C6 target
63 * (esp32:esp32:esp32c6), but fails to *link* there:
64 * esp_lcd_new_panel_io_parl is declared in that core's esp_lcd headers but
65 * missing from its prebuilt esp32-arduino-libs static libraries for C6 -
66 * an SDK-packaging gap in that specific core version, not a bug in this
67 * file. Verify linking against whatever core version you actually build
68 * with before relying on the PARLIO path; a plain ESP-IDF build (rather
69 * than arduino-esp32) may not have this gap.
70 *
71 * Subclasses call the inherited beginBus() first thing in their begin()
72 * (setting up the bus/panel-IO), then send their chip's own init register
73 * sequence via writeCommand() - the same division of labor
74 * DisplayDriverQSPI.h's beginBus()/writeCommand() split uses.
75 */
76template <typename RGB_T = RGB565>
78 public:
79 /// @param d0..d7 the 8 data lines, D0 = least significant bit.
80 /// @param wr write-strobe pin (WR on I80, wired to PARLIO's clk_gpio_num
81 /// on PARLIO targets - PARLIO calls the same physical strobe line "clk").
82 /// @param dc data/command ("RS") select pin.
83 /// @param cs chip-select pin, or -1 to declare exclusive bus ownership
84 /// (I80 backend only - the PARLIO backend always wants a real CS pin).
85 /// @param width/height the panel's addressable resolution - subclasses
86 /// with a rotation concept (e.g. ILI9341Driver8080ESP32) override
87 /// width()/height() themselves instead and can ignore these.
91 size_t width = 240, size_t height = 320,
92 uint32_t pclkHz = 20000000)
93 : d_{d0, d1, d2, d3, d4, d5, d6, d7},
94 wr_(wr),
95 dc_(dc),
96 cs_(cs),
99 pclkHz_(pclkHz) {}
100
101 void end() override {
102 if (io_ != nullptr) {
104 io_ = nullptr;
105 }
106#ifdef TINYGPU_PARALLEL8_BACKEND_I80
107 if (bus_ != nullptr) {
109 bus_ = nullptr;
110 }
111#endif
112 }
113
115 end();
116 delete[] scratch_;
117 }
118
119 size_t width() const override { return width_; }
120 size_t height() const override { return height_; }
121
123 return writeData(surface, 0, 0);
124 }
125
127 static_assert(sizeof(RGB_T) == 2,
128 "writeData assumes a 16bpp RGB_T (RGB565) stored in "
129 "wire byte order");
130 if (io_ == nullptr) return false;
132 return false;
133 }
134
135 // Same reasoning as DisplayDriverQSPI::writeData(): tx_color() is
136 // asynchronous, so this needs a persistent scratch buffer (not one
137 // freed/reused based on when a previous transfer happens to finish)
138 // and blocks on the completion callback rather than chunking the
139 // transfer at this level - chunking would re-send the 0x2C RAM-write
140 // command per chunk, resetting the panel's write cursor each time.
141 const size_t n = surface.size();
142 if (scratchCapacity_ < n) {
143 delete[] scratch_;
144 scratch_ = new uint8_t[n];
146 }
148
149 colorTransPending_ = true;
151 ESP_OK) {
152 colorTransPending_ = false;
153 return false;
154 }
155 while (colorTransPending_) {
156 // busy-wait for the DMA transfer to finish
157 }
158 return true;
159 }
160
161 protected:
162 static constexpr uint8_t kCmdRamWrite = 0x2C;
163
169#ifdef TINYGPU_PARALLEL8_BACKEND_I80
171#endif
172 uint8_t* scratch_ = nullptr;
174 volatile bool colorTransPending_ = false;
175
176 /// Sets up the bus (I80 or PARLIO, picked at compile time above) and the
177 /// panel-IO layer. Subclasses call this first thing in their begin(),
178 /// then send their chip's own init register sequence via writeCommand()
179 /// before returning.
180 bool beginBus() {
181#if defined(TINYGPU_PARALLEL8_BACKEND_I80)
183 busCfg.dc_gpio_num = static_cast<gpio_num_t>(dc_);
184 busCfg.wr_gpio_num = static_cast<gpio_num_t>(wr_);
186 for (int i = 0; i < 8; ++i) {
187 busCfg.data_gpio_nums[i] = static_cast<gpio_num_t>(d_[i]);
188 }
189 busCfg.bus_width = 8;
191 static_cast<size_t>(width_ * height_ * sizeof(RGB_T));
193
195 return false;
196 }
197
199 ioCfg.cs_gpio_num = static_cast<gpio_num_t>(cs_);
204 ioCfg.user_ctx = this;
211
213#elif defined(TINYGPU_PARALLEL8_BACKEND_PARLIO)
215 ioCfg.dc_gpio_num = static_cast<gpio_num_t>(dc_);
216 ioCfg.clk_gpio_num = static_cast<gpio_num_t>(wr_);
217 ioCfg.cs_gpio_num = static_cast<gpio_num_t>(cs_);
218 for (int i = 0; i < 8; ++i) {
219 ioCfg.data_gpio_nums[i] = static_cast<gpio_num_t>(d_[i]);
220 }
221 ioCfg.data_width = 8;
225 static_cast<size_t>(width_ * height_ * sizeof(RGB_T));
232
234#endif
235 }
236
237 bool writeCommand(uint8_t cmd, const uint8_t* param = nullptr,
238 size_t len = 0) {
240 }
241
242 /// Convenience for an init sequence's fixed, short parameter lists -
243 /// mirrors DisplayDriverSPI::writeDataN()/DisplayDriverQSPI's inline
244 /// {data} calls, just spelled as one call instead of N.
246 uint8_t buf[16];
247 size_t i = 0;
248 for (uint8_t b : data) {
249 if (i >= sizeof(buf)) break; // no init sequence below needs more
250 buf[i++] = b;
251 }
252 return writeCommand(cmd, buf, i);
253 }
254
256 const uint8_t caset[4] = {
257 static_cast<uint8_t>(x >> 8), static_cast<uint8_t>(x & 0xFF),
258 static_cast<uint8_t>((x + w - 1) >> 8),
259 static_cast<uint8_t>((x + w - 1) & 0xFF)};
260 const uint8_t raset[4] = {
261 static_cast<uint8_t>(y >> 8), static_cast<uint8_t>(y & 0xFF),
262 static_cast<uint8_t>((y + h - 1) >> 8),
263 static_cast<uint8_t>((y + h - 1) & 0xFF)};
264 return writeCommand(0x2A, caset, 4) && writeCommand(0x2B, raset, 4);
265 }
266
267 private:
268 // Runs in ISR context (per esp_lcd_panel_io_color_trans_done_cb_t's
269 // contract) - keep this fast. See writeData() for why this exists.
270 static bool onColorTransDone(esp_lcd_panel_io_handle_t /*panel_io*/,
272 void* userCtx) {
274 ->colorTransPending_ = false;
275 return false; // no higher-priority task woken
276 }
277};
278
279/**
280 * @brief Driver for ILI9341 8-bit parallel display controller, driven via
281 * ESP32's LCD_CAM/I80 or PARLIO peripheral (see
282 * DisplayDriverParallel8ESP32).
283 *
284 * Same power/gamma/MADCTL init sequence as DisplayDriverSPI.h's
285 * ILI9341Driver (see that class's doc comment for provenance) - only the
286 * bus underneath differs.
287 */
288template <typename RGB_T = RGB565>
290 public:
293
295
296 /// @param nativeWidth/nativeHeight the panel's physical resolution in
297 /// its native (portrait, MV bit clear) orientation - 240x320 is the
298 /// common ILI9341 module size and the default; pass your panel's real
299 /// values if it differs.
304 uint32_t pclkHz = 20000000, size_t nativeWidth = 240,
305 size_t nativeHeight = 320)
308 pclkHz),
312
315 }
318 }
319
321 if (!beginBus()) return false;
322
323 writeCommand(0x01); // SWRESET
324 delay(150);
325
326 writeCommand(0xEF, {0x03, 0x80, 0x02});
327 writeCommand(0xCF, {0x00, 0xC1, 0x30});
328 writeCommand(0xED, {0x64, 0x03, 0x12, 0x81});
329 writeCommand(0xE8, {0x85, 0x00, 0x78});
330 writeCommand(0xCB, {0x39, 0x2C, 0x00, 0x34, 0x02});
331 writeCommand(0xF7, {0x20});
332 writeCommand(0xEA, {0x00, 0x00});
333 writeCommand(0xC0, {0x23});
334 writeCommand(0xC1, {0x10});
335 writeCommand(0xC5, {0x3E, 0x28});
336 writeCommand(0xC7, {0x86});
337 writeCommand(0x36, {madctlForRotation(rotation_)}); // MADCTL
338 writeCommand(0x3A, {0x55}); // Pixel format: 16bpp
339 writeCommand(0xB1, {0x00, 0x13});
340 writeCommand(0xB6, {0x08, 0x82, 0x27});
341 writeCommand(0xF2, {0x00});
342 writeCommand(0x26, {0x01});
343 writeCommand(0xE0, {0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E, 0xF1, 0x37,
344 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00});
345 writeCommand(0xE1, {0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31, 0xC1, 0x48,
346 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F});
347
348 writeCommand(0x11); // Sleep out
349 delay(120);
350 writeCommand(0x29); // Display on
351
352 writeCommand(invertColor_ ? 0x21 : 0x20);
353
354 return true;
355 }
356
358 if (rotation == Rotation::kNone) return;
361 }
362
363 Rotation rotation() const { return rotation_; }
364
365 /// See DisplayDriverSPI.h's ILI9341Driver::setInvertColor() - same
366 /// panel quirk, same fix, just re-sent over this bus's begin() instead.
369 writeCommand(invert ? 0x21 : 0x20);
370 }
371
372 protected:
375 bool invertColor_ = false;
376
378 switch (rotation) {
379 case Rotation::kLandscape:
380 return 0x28;
382 return 0x88;
384 return 0xE8;
385 case Rotation::kNone:
386 case Rotation::kPortrait:
387 default:
388 return 0x48;
389 }
390 }
391};
392
393/**
394 * @brief Driver for ST7789 8-bit parallel display controller, driven via
395 * ESP32's LCD_CAM/I80 or PARLIO peripheral (see
396 * DisplayDriverParallel8ESP32).
397 */
398template <typename RGB_T = RGB565>
400 public:
403
407 size_t height = 320, uint32_t pclkHz = 20000000)
409 dc, cs, width, height, pclkHz) {}
410
412 if (!beginBus()) return false;
413 writeCommand(0x01); // SWRESET
414 delay(150);
415 writeCommand(0x11); // Sleep out
416 delay(120);
417 writeCommand(0x3A, {0x55}); // Pixel format: 16bpp
418 writeCommand(0x29); // Display on
419 return true;
420 }
421};
422
423#endif // TINYGPU_HAS_ESP_LCD_PARALLEL8
424
425} // namespace tinygpu
Definition: DSIBusESP32.h:19