SatCat5
spi_ili9341.cc
1 // Copyright 2024 The Aerospace Corporation.
3 // This file is a part of SatCat5, licensed under CERN-OHL-W v2 or later.
5 
6 #include <hal_devices/spi_ili9341.h>
7 #include <satcat5/cfgbus_spi.h>
8 #include <satcat5/utils.h>
9 
18 
19 // Workaround to ensure C++11 allocates static constants.
20 // See also: https://stackoverflow.com/questions/8452952/
21 constexpr satcat5::gui::LogColors Ili9341::DARK_THEME;
22 constexpr satcat5::gui::LogColors Ili9341::LIGHT_THEME;
23 
24 // Set the burst size for transfer of pixel data.
25 #ifndef SATCAT5_ILI9341_BURST
26 #define SATCAT5_ILI9341_BURST 32
27 #endif
28 
29 // Convert burst size from pixels to bytes, including overhead.
30 // Each burst has a fixed overhead for setup commands, so longer bursts
31 // are more efficient, up to overflow limit of the SPI transmit buffer.
32 // (Noting that each SPI byte requires two bytes in the working buffer.)
33 // Default burst of 32 pixels = 75 bytes yields an efficiency of 85%.
34 static constexpr u16 BURST_PIXELS = SATCAT5_ILI9341_BURST;
35 static constexpr u16 BURST_CADDR = 5;
36 static constexpr u16 BURST_PADDR = 5;
37 static constexpr u16 BURST_RAMWR = 1 + 2*BURST_PIXELS;
38 static constexpr u16 BURST_BYTES = BURST_CADDR + BURST_PADDR + BURST_RAMWR;
39 static_assert(2*BURST_BYTES <= SATCAT5_SPI_TXBUFF);
40 
41 // Native size is 240 cols x 320 rows before rotation.
42 // Rotation parameters may swap effective width and height.
43 static constexpr u16 TFT_WIDTH = 240;
44 static constexpr u16 TFT_HEIGHT = 320;
45 
46 inline constexpr u16 effective_height(u8 madctl)
47  { return (madctl & Ili9341::MADCTL_MV) ? TFT_WIDTH : TFT_HEIGHT; }
48 inline constexpr u16 effective_width(u8 madctl)
49  { return (madctl & Ili9341::MADCTL_MV) ? TFT_HEIGHT : TFT_WIDTH; }
50 
51 // ISI9341 command opcodes (Section 8.*):
52 static constexpr u8 // [Name given in datasheet]
53  CMD_NOOP = 0x00, // No-op (NOP)
54  CMD_SWRESET = 0x01, // Software Reset
55  CMD_WAKE = 0x11, // Sleep Out
56  CMD_INVOFF = 0x20, // Display Inversion OFF
57  CMD_INVON = 0x21, // Display inversion ON
58  CMD_GAMMA = 0x26, // Gamma Set
59  CMD_DISPON = 0x29, // Display ON
60  CMD_CADDR = 0x2A, // Column Address Set (CASET)
61  CMD_PADDR = 0x2B, // Page Address Set (PASET)
62  CMD_RAMWR = 0x2C, // Memory Write
63  CMD_VSCRDEF = 0x33, // Vertical Scrolling Definition
64  CMD_MADCTL = 0x36, // Memory Access Control
65  CMD_VSCRSET = 0x37, // Vertical Scrolling Address
66  CMD_PIXFMT = 0x3A, // COLMOD: Pixel Format Set
67  CMD_FRMCTR1 = 0xB1, // Frame Rate Control (In Normal Mode/Full Colors)
68  CMD_DFUNCTR = 0xB6, // Display Function Control
69  CMD_PWRCTR1 = 0xC0, // Power Control 1
70  CMD_PWRCTR2 = 0xC1, // Power Control 2
71  CMD_VCMCTR1 = 0xC5, // VCOM Control 1
72  CMD_VCMCTR2 = 0xC7, // VCOM Control 2
73  CMD_PWRCTRA = 0xCB, // Power Control A
74  CMD_PWRCTRB = 0xCF, // Power Control B
75  CMD_GMCTRP1 = 0xE0, // Positive Gamma Correction
76  CMD_GMCTRN1 = 0xE1, // Negative Gamma Correction
77  CMD_DRVTIMA = 0xE8, // Driver Timing Control A
78  CMD_DRVTIMB = 0xEA, // Driver Timing Control B
79  CMD_PWRSEQ = 0xED, // Power On Sequence Control
80  CMD_UNKNOWN = 0xEF, // (Undocumented command from Adafruit driver)
81  CMD_GAMMA3 = 0xF2, // Enable 3-gamma control
82  CMD_PUMPCTR = 0xF7; // Pump Ratio Control
83 
84 // Startup sequence, encoded as a series of length/data pairs.
85 // Note: Length = 0 indicates a wait command, next argument is delay in msec.
86 static constexpr u8 STARTUP[] = {
87  1, CMD_SWRESET, 0, 5, // Command + Wait
88  4, CMD_UNKNOWN, 0x03, 0x80, 0x02,
89  4, CMD_PWRCTRB, 0x00, 0xC1, 0x30,
90  5, CMD_PWRSEQ, 0x64, 0x03, 0x12, 0x81,
91  4, CMD_DRVTIMA, 0x85, 0x00, 0x78,
92  6, CMD_PWRCTRA, 0x39, 0x2C, 0x00, 0x34, 0x02,
93  2, CMD_PUMPCTR, 0x20,
94  3, CMD_DRVTIMB, 0x00, 0x00,
95  2, CMD_PWRCTR1, 0x23,
96  2, CMD_PWRCTR2, 0x10,
97  3, CMD_VCMCTR1, 0x3E, 0x28,
98  2, CMD_VCMCTR2, 0x86,
99  2, CMD_VSCRSET, 0x00,
100  2, CMD_PIXFMT, 0x55,
101  3, CMD_FRMCTR1, 0x00, 0x18,
102  4, CMD_DFUNCTR, 0x08, 0x82, 0x27,
103  2, CMD_GAMMA3, 0x00,
104  2, CMD_GAMMA, 0x01,
105  16, CMD_GMCTRP1, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00,
106  16, CMD_GMCTRN1, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F,
107  1, CMD_WAKE, 0, 150, // Command + Wait
108  1, CMD_DISPON, 0, 150}; // Command + Wait
109 
110 static constexpr unsigned INIT_DONE = sizeof(STARTUP) + 1;
111 
112 Ili9341::Ili9341(SpiGeneric* spi, u8 devidx, u8 madctl)
113  : Display(effective_height(madctl), effective_width(madctl))
114  , m_spi(spi) // Pointer to SPI interface
115  , m_cursor{} // Current draw position & colors
116  , m_draw_cmd() // Active draw command
117  , m_devidx(devidx) // SPI device-index (may be shared)
118  , m_madctl(madctl) // Display config & rotation
119  , m_init_step(0) // Initialization state
120  , m_scroll(0) // Current scroll position
121  , m_viewtop(0) // Scrolling viewport configuration
122  , m_viewsize(0)
123  , m_draw_step(0) // Burst index (one burst per tile)
124  , m_draw_done(0) // Number of bursts in this DrawCmd
125  , m_tile_col(0) // Current tile position
126  , m_tile_row(0)
127  , m_tile_width(0) // Nominal tile size
128  , m_tile_height(0)
129 {
130  // Wait for power-on-reset before initialization.
131  timer_once(150);
132 }
133 
134 bool Ili9341::busy() const {
135  return (m_init_step < INIT_DONE) || (m_draw_step < m_draw_done) || m_spi->busy();
136 }
137 
138 bool Ili9341::invert(bool inv) {
139  // Attempt to send the invert-on or invert-off command.
140  u8 cmd = inv ? CMD_INVON : CMD_INVOFF;
141  return spi_cmd(1, &cmd, 0); // No callback
142 }
143 
145  // Issue a software reset and reinitialize.
146  m_init_step = 0;
147  init_next();
148 }
149 
150 bool Ili9341::viewport(u16 top, u16 size) {
151  // Vertical scrolling isn't supported if X and Y are swapped.
152  // (Scrolling applies only to 320-pixel axis, ignoring the MV bit.)
153  if (m_madctl & MADCTL_MV) return false;
154  // Reset viewport parameters. If initialization is still running,
155  // send command at the end of that process. Otherwise, send it now.
156  m_scroll = 0;
157  m_viewtop = top;
158  m_viewsize = size;
159  return (m_init_step < INIT_DONE) || spi_vscrdef();
160 }
161 
162 bool Ili9341::draw(const Cursor& cursor, const DrawCmd& cmd) {
163  // Are we ready to start a new draw command?
164  if (busy()) return false; // False = Try again later
165 
166  // Skip planning if new size matches the previous command.
167  bool size_match = (cmd.height() == m_draw_cmd.height())
168  && (cmd.width() == m_draw_cmd.width());
169 
170  // Accept the new command parameters.
171  m_cursor = cursor;
172  m_draw_cmd = cmd;
173  m_draw_step = 0;
174  m_tile_col = 0;
175  m_tile_row = 0;
176 
177  // Planning phase: Split the draw area into equal-size rectangular tiles,
178  // where each tile is a single burst (i.e., area <= BURST_PIXELS). To
179  // minimize overhead, we want to minimize the required number of tiles.
180  if (!size_match) {
181  // Try every viable tile width to determine optimal size.
182  // (This sets m_draw_done, m_tile_width, and m_tile_height.)
183  m_draw_done = UINT16_MAX;
184  try_twidth(BURST_PIXELS); // Try 1 x N
185  for (u16 w = 1 ; w < BURST_PIXELS/2 ; ++w)
186  try_twidth(w); // Try 2 x N/2, 3 x N/3, ...
187  }
188 
189  // Start sending the first tile/burst.
190  draw_next();
191  return true;
192 }
193 
194 bool Ili9341::scroll(s16 rows) {
195  // Discard scroll commands if the viewport isn't configured.
196  if (!m_viewsize) return true;
197  // Update the scrolling offset, modulo viewport size.
198  u16 tmp = m_scroll + u16(rows);
199  if (rows < 0 && tmp >= m_viewsize) tmp += m_viewsize;
200  if (rows > 0 && tmp >= m_viewsize) tmp -= m_viewsize;
201  // Attempt to send "Vertical Scrolling Start Address" command.
202  u8 cmd[3] = {CMD_VSCRSET};
203  write_be_u16(cmd + 1, m_viewtop + tmp);
204  bool ok = spi_cmd(sizeof(cmd), cmd, 0); // No callback
205  // If successful, update scroll position.
206  if (ok) m_scroll = tmp;
207  return ok;
208 }
209 
210 void Ili9341::spi_done(unsigned nread, const u8* rbytes) {
211  if (m_init_step < INIT_DONE) init_next();
212  else if (m_draw_step < m_draw_done) draw_next();
213 }
214 
216  if (m_init_step < INIT_DONE) init_next();
217  else if (m_draw_step < m_draw_done) draw_next();
218 }
219 
220 bool Ili9341::in_viewport(u16 row) const {
221  return (m_viewtop <= row) && (row < m_viewtop + m_viewsize);
222 }
223 
224 // Each tile/burst transfers a contiguous burst of pixels:
225 // * CMD_CADDR = 5 bytes, set column(s) to be written
226 // * CMD_PADDR = 5 bytes, set row(s) to be written
227 // * CMD_RAMWR = 1 + 2N bytes, pixel data in raster order
228 void Ili9341::draw_next() {
229  u8 cmd[BURST_BYTES];
230  // Are we sending a partial tile?
231  u16 tile_width = min_u16(m_tile_width, m_draw_cmd.width() - m_tile_col);
232  u16 tile_height = min_u16(m_tile_height, m_draw_cmd.height() - m_tile_row);
233  // Construct the CADDR command (first 5 bytes).
234  cmd[0] = CMD_CADDR;
235  write_be_u16(cmd + 1, m_cursor.c + m_tile_col);
236  write_be_u16(cmd + 3, m_cursor.c + m_tile_col + tile_width - 1);
237  // Construct the PADDR command (next 5 bytes).
238  cmd[5] = CMD_PADDR;
239  write_be_u16(cmd + 6, m_cursor.r + m_tile_row);
240  write_be_u16(cmd + 8, m_cursor.r + m_tile_row + tile_height - 1);
241  // Construct and send the RAMWR command.
242  cmd[10] = CMD_RAMWR;
243  unsigned wrpos = 11;
244  for (u16 r = 0 ; r < tile_height ; ++r) {
245  for (u16 c = 0 ; c < tile_width ; ++c) {
246  u32 color = m_draw_cmd.rc(m_tile_row + r, m_tile_col + c)
247  ? m_cursor.fg : m_cursor.bg;
248  write_be_u16(cmd + wrpos, u16(color));
249  wrpos += 2;
250  }
251  }
252  // Attempt to send all three SPI commands.
253  // (Last includes callback to trigger next burst.)
254  bool ok = spi_cmd(5, cmd+0, 0) // CADDR
255  && spi_cmd(5, cmd+5, 0) // PADDR
256  && spi_cmd(wrpos-10, cmd+10, this); // RAMWR
257  // If commands were accepted, update tile position for next burst.
258  if (ok) {
259  // Next tile in raster order, left to right until end of row.
260  ++m_draw_step;
261  m_tile_col += tile_width;
262  if (m_tile_col >= m_draw_cmd.width()) {
263  // Row completed, start the next row of tiles.
264  bool vp_old = in_viewport(m_cursor.r + m_tile_row);
265  m_tile_col = 0;
266  m_tile_row += tile_height;
267  bool vp_new = in_viewport(m_cursor.r + m_tile_row);
268  // Wrap cursor position as needed to stay within viewport.
269  if (vp_old && !vp_new) m_cursor.r -= m_viewsize;
270  }
271  } else {
272  timer_once(1); // SPI busy, try again later.
273  }
274 }
275 
276 void Ili9341::init_next() {
277  if (m_init_step < sizeof(STARTUP)) {
278  // Read the next command...
279  u8 len = STARTUP[m_init_step++];
280  if (!len) {
281  // Null command = Wait for specified interval.
282  u8 wait = STARTUP[m_init_step++];
283  timer_once(wait);
284  } else if (spi_cmd(len, STARTUP + m_init_step, this)) {
285  // SPI command accepted, advance to next position.
286  m_init_step += len;
287  } else {
288  // SPI busy, try again later.
289  --m_init_step;
290  timer_once(1);
291  }
292  } else {
293  // Load dynamic parameters.
294  if (spi_madctl() && spi_vscrdef()) {
295  // Initialization completed.
296  m_init_step = INIT_DONE;
297  } else {
298  // SPI busy, try again later.
299  timer_once(1);
300  }
301  }
302 }
303 
304 bool Ili9341::spi_cmd(u8 len, const u8* cmd, Ili9341* callback) {
305  return m_spi->query(m_devidx, cmd, len, 0, callback);
306 }
307 
308 bool Ili9341::spi_madctl() {
309  // Memory access control (i.e., panel configuration and rotation).
310  u8 cmd[2] = {CMD_MADCTL, m_madctl};
311  return spi_cmd(sizeof(cmd), cmd, 0); // No callback
312 }
313 
314 bool Ili9341::spi_vscrdef() {
315  // Vertical scrolling definition.
316  u8 cmd[7] = {CMD_VSCRDEF};
317  write_be_u16(cmd + 1, m_viewtop);
318  write_be_u16(cmd + 3, m_viewsize);
319  write_be_u16(cmd + 5, TFT_HEIGHT - m_viewtop - m_viewsize);
320  return spi_cmd(sizeof(cmd), cmd, this);
321 }
322 
323 void Ili9341::try_twidth(u16 w) {
324  // Given tile width and max area, find maximum tile height.
325  u16 h = BURST_PIXELS / w;
326  // Calculate number of required tiles on each axis.
327  u16 r = div_ceil<u16>(m_draw_cmd.height(), h);
328  u16 c = div_ceil<u16>(m_draw_cmd.width(), w);
329  // If this beats the minimum, update stored parameters.
330  if (r * c < m_draw_done) {
331  m_draw_done = r * c;
332  m_tile_width = w;
333  m_tile_height = h;
334  }
335 }
Polymorphic API for a generic SPI interface.
Definition: cfg_spi.h:26
virtual bool busy()=0
Is the SPI controller currently busy?
virtual bool query(u8 devidx, const u8 *wrdata, u8 wrbytes, u8 rdbytes, satcat5::cfg::SpiEventListener *callback=0)=0
Queue a query transaction (write, read, or write-then-read).
Display device driver for the ILI9341.
Definition: spi_ili9341.h:46
bool draw(const satcat5::gui::Cursor &cursor, const satcat5::gui::DrawCmd &cmd) override
Draw pixels to the screen at the designated location.
Definition: spi_ili9341.cc:162
static constexpr u8 MADCTL_MV
Constants for the MADCTL register (Section 8.2.29).
Definition: spi_ili9341.h:54
bool viewport(u16 top, u16 size)
Configure the scrolling viewport:
Definition: spi_ili9341.cc:150
void timer_event() override
Child class MUST override this method.
Definition: spi_ili9341.cc:215
bool busy() const
Busy with initialization or a previous command?
Definition: spi_ili9341.cc:134
void reset()
Initiate software reset.
Definition: spi_ili9341.cc:144
bool scroll(s16 rows) override
Optional: Advance the predefined viewport by N pixels.
Definition: spi_ili9341.cc:194
bool invert(bool inv)
Invert entire display.
Definition: spi_ili9341.cc:138
Required API for display devices.
Definition: gui_display.h:105
Each "draw command" updates a rectangular region of pixels.
Definition: gui_display.h:61
u16 width() const
Size of the rectangular update region.
Definition: gui_display.cc:92
u16 height() const
Size of the rectangular update region.
Definition: gui_display.cc:77
bool rc(u16 r, u16 c) const
Get the new pixel value at the designated row and column.
Definition: gui_display.cc:52
void timer_once(unsigned msec)
Configure a one-time notification after X milliseconds.
Definition: polling.cc:316
Cursor object tracks position and foreground/background colors.
Definition: gui_display.h:33
u32 fg
Foreground color (format defined by display)
Definition: gui_display.h:36
u16 r
Row coordinate (0 = top)
Definition: gui_display.h:34
u32 bg
Background color (format defined by display)
Definition: gui_display.h:37
u16 c
Column coordinate (0 = left)
Definition: gui_display.h:35
Color parameters for the LogToDisplay class.
Definition: gui_display.h:237
Miscellaneous mathematical utility functions.
void write_be_u16(u8 *dst, u16 val)
Store fields into a big-endian byte array.
Definition: utils.cc:173
constexpr u16 min_u16(u16 a, u16 b)
Min and max functions.
Definition: utils.h:101
constexpr T div_ceil(T a, T b)
Integer division functions with various rounding options:
Definition: utils.h:265