TinyGPU
Loading...
Searching...
No Matches
LCDBoardsSTM32.h
Go to the documentation of this file.
1#pragma once
2/**
3 * @file LCDBoardsSTM32.h
4 * @brief One-call setup for STM32duino boards that bundle an LCD, following
5 * the same LCDBoard interface as LCDBoardsESP32.h's ESP32 boards.
6 *
7 * See LCDBoards.h for the platform-independent LCDBoard interface these
8 * classes implement.
9 */
10#include "TinyGPU/Emulation.h"
11
12#if defined(ARDUINO_ARCH_STM32)
13
14// Unlike ESP32 (SPI.h/Wire.h are part of the core itself), STM32duino
15// ships both as separate libraries - arduino-cli only adds a library to
16// the include path when some file textually #includes it, which
17// Emulation.h's __has_include check alone doesn't trigger (that's a
18// compile-time check, after arduino-cli's library dependency resolution
19// has already run). TouchDriverArduino.h (pulled in transitively by
20// TinyGPU.h, unconditionally on every platform) needs SPIClass/TwoWire
21// declared even though this board has no touch controller of its own.
22#include <SPI.h>
23#include <Wire.h>
24#include "TinyGPU/Boards/LCDBoardsCommon.h"
25#include "TinyGPU/Drivers/DisplayDriver.h"
26
27namespace tinygpu {
28
29/**
30 * @brief Hardware-SPI4 ST7735 driver for the WeAct MiniSTM32H7xx's bundled
31 * 0.96" 160x80 TFT.
32 *
33 * Getting this panel working took a few rounds of hardware debugging:
34 * - Passing NC as SPIClass's MISO pin hangs the STM32 core's
35 * spi_transfer() forever (TXP/RXP flag never sets) for SPI4 on this
36 * board's custom PeripheralPins variant - worked around by giving it a
37 * real but physically-unwired MISO pin instead (this panel has no
38 * MISO/SDO line).
39 * - The backlight (see LCDBoardWeActMiniSTM32H750::begin()) needs
40 * analogWrite() PWM, not a plain digital HIGH.
41 * - This panel needs CS held low continuously across a command byte and
42 * its data bytes (see writeCommand()'s comment) - closing and
43 * reopening CS between them makes it misparse the data as a new
44 * command instead of a parameter.
45 * - It needs Adafruit_ST7735's INITR_MINI160x80_PLUGIN init parameters
46 * (colstart/rowstart/MADCTL/INVON - see begin() and
47 * LCDBoardWeActMiniSTM32H750's doc comment), not the plain
48 * INITR_MINI160x80 tab.
49 * With all of those fixed, hardware SPI4 works reliably and is much
50 * faster than a bit-banged fallback would be.
51 */
52class ST7735DriverHardwareSPI : public DisplayDriver<RGB565> {
53 public:
54 /// @param frequencyHz SPI clock - 15MHz default matches WeAct's own
55 /// factory firmware for this exact panel (SPI4 at APB1/8, ~120MHz/8),
56 /// itself in line with the ST7735's typical 15-20MHz datasheet ceiling.
57 ST7735DriverHardwareSPI(SPIClass& spi, int8_t cs, int8_t dc, int8_t rst,
58 size_t xOffset, size_t yOffset, size_t width,
59 size_t height, uint8_t madctl,
60 uint32_t frequencyHz = 15000000)
61 : spi_(spi),
62 cs_(cs),
63 dc_(dc),
64 rst_(rst),
65 xOffset_(xOffset),
66 yOffset_(yOffset),
67 width_(width),
68 height_(height),
69 madctl_(madctl),
70 frequencyHz_(frequencyHz) {}
71
72 size_t width() const override { return width_; }
73 size_t height() const override { return height_; }
74
75 bool begin() override {
76 pinMode(cs_, OUTPUT);
77 pinMode(dc_, OUTPUT);
78 digitalWrite(cs_, HIGH);
79 spi_.begin();
80
81 if (rst_ >= 0) {
82 pinMode(rst_, OUTPUT);
83 digitalWrite(rst_, HIGH);
84 delay(5);
85 digitalWrite(rst_, LOW);
86 delay(20);
87 digitalWrite(rst_, HIGH);
88 delay(150);
89 }
90
91 beginTransaction();
92 writeCommand(0x01); // SWRESET, no data
93 endTransaction();
94 delay(150);
95 beginTransaction();
96 writeCommand(0x11); // SLPOUT, no data
97 endTransaction();
98 delay(120);
99 beginTransaction();
100 writeCommand(0x21); // INVON, no data - this panel is ribbon/FPC-
101 // mounted (fold-and-tape onto the core board),
102 // matching Adafruit_ST7735's
103 // INITR_MINI160x80_PLUGIN variant, which
104 // explicitly inverts - not the plain
105 // INITR_MINI160x80 tab (INVOFF).
106 uint8_t colmod = 0x05;
107 writeCommand(0x3A, &colmod, 1); // COLMOD: 16bpp
108 uint8_t madctlData = madctl_;
109 writeCommand(0x36, &madctlData, 1); // MADCTL
110 writeCommand(0x29); // DISPON, no data
111 endTransaction();
112 return true;
113 }
114
115 bool writeData(ISurface<RGB565>& surface) override {
116 return writeData(surface, 0, 0);
117 }
118
119 bool writeData(ISurface<RGB565>& surface, size_t x, size_t y) override {
120 beginTransaction();
121 setAddressWindow(x, y, surface.width(), surface.height());
122 // setAddressWindow() has already sent RAMWR (0x2C) and left CS low/DC
123 // high, deliberately not closing that transaction - the pixel bytes
124 // below are RAMWR's data and must share its CS assertion (see
125 // writeCommand()'s comment for why).
126 const uint8_t* src = surface.data();
127 const size_t n = surface.size();
128 // Sent in chunks through a small scratch buffer, like
129 // DisplayDriverSPI::writeData() - SPIClass::transfer(buf, count) is
130 // full-duplex and overwrites buf with received data, which would
131 // corrupt surface's own buffer if passed directly.
132 constexpr size_t kChunkBytes = 256;
133 uint8_t chunk[kChunkBytes];
134 size_t remaining = n;
135 const uint8_t* p = src;
136 while (remaining > 0) {
137 const size_t c = remaining < kChunkBytes ? remaining : kChunkBytes;
138 memcpy(chunk, p, c);
139 spi_.transfer(chunk, c);
140 p += c;
141 remaining -= c;
142 }
143 digitalWrite(cs_, HIGH); // closes the RAMWR + pixel-data transaction
144 endTransaction();
145 return true;
146 }
147
148 protected:
149 bool setAddressWindow(size_t x, size_t y, size_t w, size_t h) override {
150 uint8_t caset[4] = {
151 static_cast<uint8_t>((x + xOffset_) >> 8),
152 static_cast<uint8_t>((x + xOffset_) & 0xFF),
153 static_cast<uint8_t>((x + xOffset_ + w - 1) >> 8),
154 static_cast<uint8_t>((x + xOffset_ + w - 1) & 0xFF)};
155 writeCommand(0x2A, caset, 4); // CASET
156 uint8_t raset[4] = {
157 static_cast<uint8_t>((y + yOffset_) >> 8),
158 static_cast<uint8_t>((y + yOffset_) & 0xFF),
159 static_cast<uint8_t>((y + yOffset_ + h - 1) >> 8),
160 static_cast<uint8_t>((y + yOffset_ + h - 1) & 0xFF)};
161 writeCommand(0x2B, raset, 4); // RASET
162
163 // RAMWR: deliberately does NOT close CS - see writeData()'s comment.
164 digitalWrite(cs_, LOW);
165 digitalWrite(dc_, LOW);
166 spi_.transfer(static_cast<uint8_t>(0x2C));
167 digitalWrite(dc_, HIGH);
168 return true;
169 }
170
171 private:
172 SPIClass& spi_;
173 int8_t cs_, dc_, rst_;
174 size_t xOffset_, yOffset_, width_, height_;
175 uint8_t madctl_;
176 uint32_t frequencyHz_;
177
178 void beginTransaction() {
179 spi_.beginTransaction(SPISettings(frequencyHz_, MSBFIRST, SPI_MODE0));
180 }
181 void endTransaction() { spi_.endTransaction(); }
182
183 /// Sends a command byte and (optionally) its data bytes under one
184 /// continuous CS assertion - matching Adafruit_SPITFT::sendCommand()
185 /// (see Adafruit_SPITFT.cpp), which this panel needs: closing CS
186 /// between the command and its data, then reopening it for the data,
187 /// makes this panel misparse the data bytes as new command opcodes
188 /// instead of parameters - confirmed on real hardware (deterministic,
189 /// wrong-but-consistent colors, unaffected by widening this driver's
190 /// SPI clock's timing margins, which ruled out a signal-integrity
191 /// explanation before this framing bug was found). Assumes a
192 /// beginTransaction() is already active (begin()/writeData() bracket
193 /// their own calls to this).
194 void writeCommand(uint8_t cmd, const uint8_t* data = nullptr,
195 size_t len = 0) {
196 digitalWrite(cs_, LOW);
197 digitalWrite(dc_, LOW);
198 spi_.transfer(cmd);
199 if (len > 0) {
200 digitalWrite(dc_, HIGH);
201 for (size_t i = 0; i < len; ++i) {
202 spi_.transfer(data[i]);
203 }
204 }
205 digitalWrite(cs_, HIGH);
206 }
207};
208
209/**
210 * @brief WeAct "MiniSTM32H7xx" core board (STM32H750VBT6/STM32H743VIT6)
211 * with its bundled, ribbon/FPC-mounted 0.96" 160x80 ST7735 TFT, mounted
212 * landscape:
213 * https://github.com/WeActStudio/MiniSTM32H7xx
214 *
215 * Display: ST7735, 160x80, hardware SPI4 (see ST7735DriverHardwareSPI's
216 * doc comment for the earlier bit-banged detour and why hardware
217 * SPI4 turned out fine after all) - CS=PE11 DC(RS)=PE13 SCK=PE12
218 * MOSI=PE14 RST=PE15; BL=PE10 (TIM1_CH2N, PWM-capable - driven
219 * via analogWrite() by begin(); a plain digital HIGH left the
220 * backlight dark on real hardware, see backlightPin() for
221 * further brightness control).
222 * Touch/I2S/LED: none on this board.
223 *
224 * Panel offsets (xOffset=1, yOffset=26) and MADCTL (0xA8 = MY|MV|BGR)
225 * match Adafruit_ST7735's INITR_MINI160x80_PLUGIN tab + setRotation(1) -
226 * the variant meant for ribbon/FPC-mounted mini panels like this one, as
227 * opposed to a directly-soldered module - confirmed working on real
228 * hardware, including its need for INVON (see
229 * ST7735DriverHardwareSPI::begin()) where the plain (non-plugin)
230 * INITR_MINI160x80 tab uses INVOFF.
231 */
232class LCDBoardWeActMiniSTM32H750 : public LCDBoard {
233 public:
234 /// Sets up the backlight and display controller. Returns false if the
235 /// display begin() fails.
236 bool begin() override {
237 pinMode(kPinBacklight, OUTPUT);
238 analogWrite(kPinBacklight, 180);
239
240 return display_.begin();
241 }
242
243 /// Panel width in pixels (landscape).
244 size_t width() const override { return 160; }
245 /// Panel height in pixels (landscape).
246 size_t height() const override { return 80; }
247
248 /// The board's display driver.
249 ST7735DriverHardwareSPI& display() override { return display_; }
250 /// This board has no touch controller.
251 TouchDriver* touch() override { return nullptr; }
252 /// This board has no I2S bus - every I2SPins field is -1.
253 const I2SPins& i2s() const override { return i2s_; }
254 /// This board has no RGB LED - every LEDPins field is -1.
255 const LEDPins& led() const override { return led_; }
256 /// The board's backlight GPIO pin.
257 int8_t backlightPin() const override { return kPinBacklight; }
258
259 private:
260 static constexpr int8_t kPinCs = PE11;
261 static constexpr int8_t kPinDc = PE13;
262 static constexpr int8_t kPinRst = PE15;
263 static constexpr int8_t kPinSck = PE12;
264 static constexpr int8_t kPinMosi = PE14;
265 static constexpr int8_t kPinBacklight = PE10;
266 // Not physically wired to the display (this panel has no MISO/SDO
267 // line) - passed to SPIClass purely as a workaround. Passing NC for
268 // miso instead hangs forever inside the STM32 core's spi_transfer()
269 // (TXP/RXP flag never sets) for SPI4 on this board's custom
270 // PeripheralPins variant. PE5 is SPI4-MISO-capable per this variant's
271 // own PeripheralPins_WeActMiniH7xx.c, and otherwise unused on this
272 // board.
273 static constexpr int8_t kPinMisoWorkaround = PE5;
274
275 SPIClass spi_{kPinMosi, kPinMisoWorkaround, kPinSck};
276
277 // xOffset/yOffset/madctl match Adafruit_ST7735's
278 // initR(INITR_MINI160x80_PLUGIN) + setRotation(1) - colstart=26/
279 // rowstart=1, MADCTL=MY|MV|BGR (0xA8) - the "_PLUGIN" tab, for
280 // ribbon/FPC-mounted mini panels like this board's fold-and-tape 0.96"
281 // display, not the plain INITR_MINI160x80 tab (colstart=24/rowstart=0,
282 // RGB not BGR) tried earlier, which rendered close but not quite right
283 // (clean-looking but wrong hues). The _PLUGIN tab also needs INVON
284 // instead of INVOFF - see begin()'s comment on that command.
285 ST7735DriverHardwareSPI display_{spi_,
286 kPinCs,
287 kPinDc,
288 kPinRst,
289 /*xOffset=*/1,
290 /*yOffset=*/26,
291 /*width=*/160,
292 /*height=*/80,
293 /*madctl=*/0xA8};
294 I2SPins i2s_{};
295 LEDPins led_{};
296};
297
298using MiniSTM32H750 = LCDBoardWeActMiniSTM32H750;
299using MiniSTM32H7xx = LCDBoardWeActMiniSTM32H750;
300
301} // namespace tinygpu
302
303#endif // ARDUINO_ARCH_STM32