2021-03-23 11:14:15 +00:00
|
|
|
# st7789.py Driver for ST7789 LCD displays for nano-gui
|
|
|
|
|
|
|
|
# Released under the MIT License (MIT). See LICENSE.
|
|
|
|
# Copyright (c) 2021 Peter Hinch
|
|
|
|
|
2021-04-25 09:35:28 +00:00
|
|
|
# Tested displays:
|
2021-03-23 11:14:15 +00:00
|
|
|
# Adafruit 1.3" 240x240 Wide Angle TFT LCD Display with MicroSD - ST7789
|
|
|
|
# https://www.adafruit.com/product/4313
|
2021-04-25 09:35:28 +00:00
|
|
|
# TTGO T-Display
|
2021-03-23 11:14:15 +00:00
|
|
|
# Based on
|
|
|
|
# Adfruit https://github.com/adafruit/Adafruit_CircuitPython_ST7789/blob/master/adafruit_st7789.py
|
|
|
|
# Also see st7735r_4bit.py for other source acknowledgements
|
|
|
|
|
|
|
|
# SPI bus: default mode. Driver performs no read cycles.
|
|
|
|
# Datasheet table 6 p44 scl write cycle 16ns == 62.5MHz
|
|
|
|
|
2021-03-25 10:18:45 +00:00
|
|
|
from time import sleep_ms #, ticks_us, ticks_diff
|
2021-03-23 11:14:15 +00:00
|
|
|
import framebuf
|
|
|
|
import gc
|
|
|
|
import micropython
|
2021-03-25 10:18:45 +00:00
|
|
|
import uasyncio as asyncio
|
2021-03-23 11:14:15 +00:00
|
|
|
|
2021-04-25 15:56:52 +00:00
|
|
|
# d7..d5 of MADCTL determine rotation/orientation datasheet P124, P231
|
|
|
|
# d5 = MV row/col exchange
|
|
|
|
# d6 = MX col addr order
|
|
|
|
# d7 = MY page addr order
|
|
|
|
# These constants are also used as user specifiers for orientation. However
|
|
|
|
# mapping is required between user value and that presented to hardware.
|
2021-04-23 12:49:02 +00:00
|
|
|
LANDSCAPE = 0 # Default
|
2021-03-23 11:14:15 +00:00
|
|
|
PORTRAIT = 0x20
|
|
|
|
REFLECT = 0x40
|
|
|
|
USD = 0x80
|
|
|
|
|
|
|
|
@micropython.viper
|
|
|
|
def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int):
|
|
|
|
n = 0
|
|
|
|
for x in range(length):
|
|
|
|
c = source[x]
|
|
|
|
d = (c & 0xf0) >> 3 # 2* LUT indices (LUT is 16 bit color)
|
|
|
|
e = (c & 0x0f) << 1
|
|
|
|
dest[n] = lut[d]
|
|
|
|
n += 1
|
|
|
|
dest[n] = lut[d + 1]
|
|
|
|
n += 1
|
|
|
|
dest[n] = lut[e]
|
|
|
|
n += 1
|
|
|
|
dest[n] = lut[e + 1]
|
|
|
|
n += 1
|
|
|
|
|
|
|
|
class ST7789(framebuf.FrameBuffer):
|
|
|
|
|
|
|
|
lut = bytearray(32)
|
|
|
|
|
|
|
|
# Convert r, g, b in range 0-255 to a 16 bit colour value
|
|
|
|
# LS byte goes into LUT offset 0, MS byte into offset 1
|
|
|
|
# Same mapping in linebuf so LS byte is shifted out 1st
|
2021-03-26 10:57:37 +00:00
|
|
|
# For some reason color must be inverted on this controller.
|
2021-03-23 11:14:15 +00:00
|
|
|
@staticmethod
|
|
|
|
def rgb(r, g, b):
|
|
|
|
return ((b & 0xf8) << 5 | (g & 0x1c) << 11 | (g & 0xe0) >> 5 | (r & 0xf8)) ^ 0xffff
|
|
|
|
|
|
|
|
# rst and cs are active low, SPI is mode 0
|
2021-04-21 16:05:34 +00:00
|
|
|
def __init__(self, spi, cs, dc, rst, height=240, width=240,
|
2021-04-25 09:35:28 +00:00
|
|
|
disp_mode=0, init_spi=False, offset=(0, 0, 0)):
|
2021-03-23 11:14:15 +00:00
|
|
|
self._spi = spi # Clock cycle time for write 16ns 62.5MHz max (read is 150ns)
|
|
|
|
self._rst = rst # Pins
|
|
|
|
self._dc = dc
|
|
|
|
self._cs = cs
|
|
|
|
self.height = height # Required by Writer class
|
|
|
|
self.width = width
|
2021-04-21 16:05:34 +00:00
|
|
|
self._offset = offset
|
2021-03-26 10:57:37 +00:00
|
|
|
self._spi_init = init_spi # Possible user callback
|
|
|
|
self._lock = asyncio.Lock()
|
2021-03-23 11:14:15 +00:00
|
|
|
mode = framebuf.GS4_HMSB # Use 4bit greyscale.
|
|
|
|
gc.collect()
|
2021-04-24 12:58:26 +00:00
|
|
|
#buf = bytearray(height * width // 2)
|
|
|
|
buf = bytearray(height * -(-width // 2)) # Ceiling division for odd widths
|
2021-03-23 11:14:15 +00:00
|
|
|
self._mvb = memoryview(buf)
|
|
|
|
super().__init__(buf, width, height, mode)
|
|
|
|
self._linebuf = bytearray(self.width * 2) # 16 bit color out
|
2021-04-25 15:56:52 +00:00
|
|
|
self._init(disp_mode, offset[2])
|
2021-03-23 11:14:15 +00:00
|
|
|
self.show()
|
|
|
|
|
|
|
|
# Hardware reset
|
|
|
|
def _hwreset(self):
|
|
|
|
self._dc(0)
|
|
|
|
self._rst(1)
|
|
|
|
sleep_ms(1)
|
|
|
|
self._rst(0)
|
|
|
|
sleep_ms(1)
|
|
|
|
self._rst(1)
|
|
|
|
sleep_ms(1)
|
|
|
|
|
|
|
|
# Write a command, a bytes instance (in practice 1 byte).
|
|
|
|
def _wcmd(self, buf):
|
|
|
|
self._dc(0)
|
|
|
|
self._cs(0)
|
|
|
|
self._spi.write(buf)
|
|
|
|
self._cs(1)
|
|
|
|
|
|
|
|
# Write a command followed by a data arg.
|
|
|
|
def _wcd(self, c, d):
|
|
|
|
self._dc(0)
|
|
|
|
self._cs(0)
|
|
|
|
self._spi.write(c)
|
|
|
|
self._cs(1)
|
|
|
|
self._dc(1)
|
|
|
|
self._cs(0)
|
|
|
|
self._spi.write(d)
|
|
|
|
self._cs(1)
|
|
|
|
|
|
|
|
# Initialise the hardware. Blocks 163ms. Adafruit have various sleep delays
|
2021-03-26 10:57:37 +00:00
|
|
|
# where I can find no requirement in the datasheet. I removed them with
|
|
|
|
# other redundant code.
|
2021-04-25 15:56:52 +00:00
|
|
|
def _init(self, disp_mode, orientation):
|
2021-03-23 11:14:15 +00:00
|
|
|
self._hwreset() # Hardware reset. Blocks 3ms
|
|
|
|
if self._spi_init: # A callback was passed
|
|
|
|
self._spi_init(self._spi) # Bus may be shared
|
|
|
|
cmd = self._wcmd
|
|
|
|
wcd = self._wcd
|
|
|
|
cmd(b'\x01') # SW reset datasheet specifies 120ms before SLPOUT
|
|
|
|
sleep_ms(150)
|
|
|
|
cmd(b'\x11') # SLPOUT: exit sleep mode
|
2021-04-25 15:56:52 +00:00
|
|
|
sleep_ms(10) # Adafruit delay 500ms (datsheet 5ms)
|
2021-03-23 11:14:15 +00:00
|
|
|
wcd(b'\x3a', b'\x55') # _COLMOD 16 bit/pixel, 64Kib color space
|
|
|
|
cmd(b'\x20') # INVOFF Adafruit turn inversion on. This driver fixes .rgb
|
|
|
|
cmd(b'\x13') # NORON Normal display mode
|
|
|
|
|
2021-04-25 15:56:52 +00:00
|
|
|
# Display modes correspond to values expected by hardware. Sometimes
|
|
|
|
# these have to be applied in combination to achieve what the user wants.
|
|
|
|
# Table entries map user request onto what is required. idx values:
|
|
|
|
# 0 Normal
|
|
|
|
# 1 Reflect
|
|
|
|
# 2 USD
|
|
|
|
# 3 USD reflect
|
|
|
|
# Followed by same for LANDSCAPE
|
|
|
|
if orientation: # Display hardware is portrait mode
|
|
|
|
disp_mode ^= PORTRAIT
|
|
|
|
idx = disp_mode >> 6 if disp_mode & PORTRAIT else (disp_mode >> 6) + 4
|
|
|
|
disp_mode = (0x60, 0xe0, 0xa0, 0x20, 0, 0x40, 0xc0, 0x80)[idx]
|
2021-03-26 10:57:37 +00:00
|
|
|
# Set display window depending on mode, .height and .width.
|
2021-03-24 12:11:23 +00:00
|
|
|
self.set_window(disp_mode)
|
2021-03-23 11:14:15 +00:00
|
|
|
wcd(b'\x36', int.to_bytes(disp_mode, 1, 'little'))
|
2021-03-26 10:57:37 +00:00
|
|
|
cmd(b'\x29') # DISPON. Adafruit then delay 500ms.
|
2021-03-24 12:11:23 +00:00
|
|
|
|
2021-04-24 12:58:26 +00:00
|
|
|
# Define the mapping between RAM and the display.
|
|
|
|
# Datasheet section 8.12 p124.
|
2021-03-24 12:11:23 +00:00
|
|
|
def set_window(self, mode):
|
|
|
|
rht = 320
|
|
|
|
rwd = 240 # RAM ht and width
|
2021-04-24 12:58:26 +00:00
|
|
|
wht = self.height # Window (framebuf) dimensions.
|
|
|
|
wwd = self.width # In portrait mode wht > wwd
|
|
|
|
if mode & PORTRAIT:
|
|
|
|
xoff = self._offset[1] # x and y transposed
|
|
|
|
yoff = self._offset[0]
|
|
|
|
xs = xoff
|
|
|
|
xe = wwd + xoff - 1
|
|
|
|
ys = yoff # y start
|
|
|
|
ye = wht + yoff - 1 # y end
|
|
|
|
if mode & REFLECT:
|
|
|
|
ys = rwd - wht - yoff
|
|
|
|
ye = rwd - yoff - 1
|
|
|
|
if mode & USD:
|
|
|
|
xs = rht - wwd - xoff
|
|
|
|
xe = rht - xoff - 1
|
|
|
|
else: # LANDSCAPE
|
|
|
|
xoff = self._offset[0]
|
|
|
|
yoff = self._offset[1]
|
|
|
|
xs = xoff
|
|
|
|
xe = wwd + xoff - 1
|
|
|
|
ys = yoff # y start
|
|
|
|
ye = wht + yoff - 1 # y end
|
|
|
|
if mode & USD:
|
|
|
|
ys = rht - wht - yoff
|
|
|
|
ye = rht - yoff - 1
|
|
|
|
if mode & REFLECT:
|
|
|
|
xs = rwd - wwd - xoff
|
|
|
|
xe = rwd - xoff - 1
|
|
|
|
|
|
|
|
# Col address set.
|
2021-03-24 12:11:23 +00:00
|
|
|
self._wcd(b'\x2a', int.to_bytes(xs, 2, 'big') + int.to_bytes(xe, 2, 'big'))
|
|
|
|
# Row address set
|
|
|
|
self._wcd(b'\x2b', int.to_bytes(ys, 2, 'big') + int.to_bytes(ye, 2, 'big'))
|
2021-03-23 11:14:15 +00:00
|
|
|
|
2021-03-25 10:18:45 +00:00
|
|
|
#@micropython.native # Made virtually no difference to timing.
|
2021-03-23 11:14:15 +00:00
|
|
|
def show(self): # Blocks for 83ms @60MHz SPI
|
2021-03-25 10:18:45 +00:00
|
|
|
#ts = ticks_us()
|
2021-03-23 11:14:15 +00:00
|
|
|
clut = ST7789.lut
|
2021-04-24 12:58:26 +00:00
|
|
|
wd = -(-self.width // 2) # Ceiling division for odd number widths
|
2021-03-23 11:14:15 +00:00
|
|
|
end = self.height * wd
|
|
|
|
lb = self._linebuf
|
|
|
|
buf = self._mvb
|
|
|
|
if self._spi_init: # A callback was passed
|
|
|
|
self._spi_init(self._spi) # Bus may be shared
|
2021-03-25 10:18:45 +00:00
|
|
|
self._dc(0)
|
|
|
|
self._cs(0)
|
2021-03-23 11:14:15 +00:00
|
|
|
self._spi.write(b'\x2c') # RAMWR
|
|
|
|
self._dc(1)
|
|
|
|
for start in range(0, end, wd):
|
|
|
|
_lcopy(lb, buf[start :], clut, wd) # Copy and map colors
|
|
|
|
self._spi.write(lb)
|
|
|
|
self._cs(1)
|
2021-03-25 10:18:45 +00:00
|
|
|
#print(ticks_diff(ticks_us(), ts))
|
|
|
|
|
|
|
|
# Asynchronous refresh with support for reducing blocking time.
|
|
|
|
async def do_refresh(self, split=4):
|
2021-03-26 10:57:37 +00:00
|
|
|
async with self._lock:
|
|
|
|
lines, mod = divmod(self.height, split) # Lines per segment
|
|
|
|
if mod:
|
|
|
|
raise ValueError('Invalid do_refresh arg.')
|
|
|
|
clut = ST7789.lut
|
2021-04-24 12:58:26 +00:00
|
|
|
wd = -(-self.width // 2)
|
2021-03-26 10:57:37 +00:00
|
|
|
lb = self._linebuf
|
|
|
|
buf = self._mvb
|
2021-03-25 10:18:45 +00:00
|
|
|
line = 0
|
|
|
|
for n in range(split):
|
|
|
|
if self._spi_init: # A callback was passed
|
|
|
|
self._spi_init(self._spi) # Bus may be shared
|
|
|
|
self._dc(0)
|
|
|
|
self._cs(0)
|
|
|
|
self._spi.write(b'\x3c' if n else b'\x2c') # RAMWR/Write memory continue
|
|
|
|
self._dc(1)
|
|
|
|
for start in range(wd * line, wd * (line + lines), wd):
|
|
|
|
_lcopy(lb, buf[start :], clut, wd) # Copy and map colors
|
|
|
|
self._spi.write(lb)
|
|
|
|
line += lines
|
|
|
|
self._cs(1)
|
|
|
|
await asyncio.sleep(0)
|