Add ST7789 8-bit driver.

pull/80/head
Peter Hinch 2024-09-17 12:04:31 +01:00
rodzic 514831a987
commit 0fad050301
5 zmienionych plików z 286 dodań i 9 usunięć

Wyświetl plik

@ -31,7 +31,7 @@ access via the `Writer` and `CWriter` classes is documented
# Contents
1. [Introduction](./DRIVERS.md#1-introduction)
1.1 [Color handling](./DRIVERS.md#11-color-handling)
1.1 [Color handling](./DRIVERS.md#11-color-handling) On 4, 8 and 16 bit drivers.
1.2 [Installation](./DRIVERS.md#12-installation)
2. [OLED displays](./DRIVERS.md#2-oled-displays)
2.1 [Drivers for SSD1351](./DRIVERS.md#21-drivers-for-ssd1351) Color OLEDs
@ -134,6 +134,13 @@ application which uses the predefined colors. Differences become apparent when
specifying custom colors. For detail see the main README
[User defined colors](./README.md#311-user-defined-colors).
For use in any of the supported GUIs, where the choice exists a 4-bit driver
should normally be preferred to conserve RAM: all demo scripts will work with
such a driver and results will be visually identical compared to the bigger
drivers. Where images are to be displayed a 4-bit driver can show a monochrome
image but color images require 8 or 16 bits. See
[IMAGE_DISPLAY.md](./IMAGE_DISPLAY.md).
## 1.2 Installation
Please ensure that device firmware is up to date. On networked hardware a
@ -474,6 +481,9 @@ colors. The resultant buffer size for the Adafruit displays is 28800 bytes. See
[Color handling](./DRIVERS.md#11-color-handling) for the implications of 4-bit
color.
An 8-bit driver is also provided. This may be used for rendering color images;
for use with the GUIs, demos are visually identical with the 4-bit driver.
[Tested display: Adafruit 1.3 inch](https://www.adafruit.com/product/4313). The
Adafruit [1.54 inch](https://www.adafruit.com/product/3787) has identical
resolution and uses the same CircuitPython driver so can be expected to work.
@ -511,6 +521,20 @@ below. An example file for the Pi Pico is in `setup_examples/st7789_pico.py`.
hardware. Current options (exported by the driver) are `GENERIC` for Adafruit
displays and `TDISPLAY` for the TTGO board.
#### Method (4-bit driver only)
* `greyscale(gs=None)` Setting `gs=True` enables the screen to be used to show
a full screen monochrome image. By default the frame buffer contents are
interpreted as color values. In greyscale mode the contents are treated as
greyscale values. This mode persists until the method is called with
`gs=False`. The method returns the current greyscale state. It is possible to
superimpose widgets on an image, but the mapping of colors onto the greyscale
may yield unexpected shades of grey. `WHITE` and `BLACK` work well. In
[micro-gui](https://github.com/peterhinch/micropython-micro-gui) and
[micropython-touch](https://github.com/peterhinch/micropython-touch) the
`after_open` method should be used to render the image to the framebuf and to
overlay any widgets.
#### Constants exported by the driver
The `color_setup.py` file should invoke the driver as follows:
@ -848,7 +872,7 @@ identical constructor args and method.
* `mirror=False` If `True` a mirror-image is displayed
* `init_spi=False` For shared SPI bus applications. See note below.
#### Method
#### Method (4-bit driver only)
* `greyscale(gs=None)` Setting `gs=True` enables the screen to be used to show
a full screen monochrome image. By default the frame buffer contents are
@ -857,10 +881,10 @@ greyscale values. This mode persists until the method is called with
`gs=False`. The method returns the current greyscale state. It is possible to
superimpose widgets on an image, but the mapping of colors onto the greyscale
may yield unexpected shades of grey. `WHITE` and `BLACK` work well. In
[micro-gui](https://github.com/peterhinch/micropython-micro-gui) and
[micropython-touch](https://github.com/peterhinch/micropython-touch) the
`after_open` method should be used to render the image to the framebuf and to
overlay any widgets.
[micro-gui](https://github.com/peterhinch/micropython-micro-gui) and
[micropython-touch](https://github.com/peterhinch/micropython-touch) the
`after_open` method should be used to render the image to the framebuf and to
overlay any widgets.
#### Shared SPI bus

Wyświetl plik

@ -2,6 +2,7 @@
"urls": [
["drivers/gc9a01/gc9a01.py", "github:peterhinch/micropython-nano-gui/drivers/gc9a01/gc9a01.py"],
["drivers/gc9a01/gc9a01_8_bit.py", "github:peterhinch/micropython-nano-gui/drivers/gc9a01/gc9a01_8_bit.py"],
["drivers/gc9a01/gc9a01_16_bit.py", "github:peterhinch/micropython-nano-gui/drivers/gc9a01/gc9a01_16_bit.py"],
["drivers/boolpalette.py", "github:peterhinch/micropython-nano-gui/drivers/boolpalette.py"]
],
"version": "0.1"

Wyświetl plik

@ -1,6 +1,7 @@
{
"urls": [
["drivers/st7789/st7789_4bit.py", "github:peterhinch/micropython-nano-gui/drivers/st7789/st7789_4bit.py"],
["drivers/st7789/st7789_8bit.py", "github:peterhinch/micropython-nano-gui/drivers/st7789/st7789_8bit.py"],
["drivers/boolpalette.py", "github:peterhinch/micropython-nano-gui/drivers/boolpalette.py"]
],
"version": "0.1"

Wyświetl plik

@ -1,4 +1,4 @@
# st7789.py Driver for ST7789 LCD displays for nano-gui
# st7789_4bit.py Driver for ST7789 LCD displays for nano-gui
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021-2024 Peter Hinch, Ihor Nehrutsa
@ -24,17 +24,19 @@ import asyncio
from drivers.boolpalette import BoolPalette
# User orientation constants
# Waveshare Pico res touch defaults to portrait. Requires PORTRAIT for landscape orientation.
LANDSCAPE = 0 # Default
REFLECT = 1
USD = 2
PORTRAIT = 4
# Display types
GENERIC = (0, 0, 0)
GENERIC = (0, 0, 0) # Default. Suits Waveshare Pico res touch.
TDISPLAY = (52, 40, 1)
PI_PICO_LCD_2 = (0, 0, 1) # Waveshare Pico LCD 2 determined by Mike Wilson.
DFR0995 = (34, 0, 0) # DFR0995 Contributed by @EdgarKluge
WAVESHARE_13 = (0, 0, 16) # Waveshare 1.3" 240x240 LCD contributed by Aaron Mittelmeier
ADAFRUIT_1_9 = (35, 0, PORTRAIT) # 320x170 TFT https://www.adafruit.com/product/5394
ADAFRUIT_1_9 = (35, 0, PORTRAIT) # 320x170 TFT https://www.adafruit.com/product/5394
@micropython.viper
def _lcopy(dest: ptr16, source: ptr8, lut: ptr16, length: int, gscale: bool):

Wyświetl plik

@ -0,0 +1,249 @@
# st7789_8bit.py Driver for ST7789 LCD displays for nano-gui
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021-2024 Peter Hinch, Ihor Nehrutsa
# Tested displays:
# Adafruit 1.3" 240x240 Wide Angle TFT LCD Display with MicroSD - ST7789
# https://www.adafruit.com/product/4313
# TTGO T-Display
# http://www.lilygo.cn/prod_view.aspx?TypeId=50044&Id=1126
# 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
from time import sleep_ms # , ticks_us, ticks_diff
import framebuf
import gc
import micropython
import asyncio
from drivers.boolpalette import BoolPalette
# User orientation constants
# Waveshare Pico res touch defaults to portrait. Requires PORTRAIT for landscape orientation.
LANDSCAPE = 0 # Default
REFLECT = 1
USD = 2
PORTRAIT = 4
# Display types
GENERIC = (0, 0, 0) # Default. Suits Waveshare Pico res touch.
TDISPLAY = (52, 40, 1)
PI_PICO_LCD_2 = (0, 0, 1) # Waveshare Pico LCD 2 determined by Mike Wilson.
DFR0995 = (34, 0, 0) # DFR0995 Contributed by @EdgarKluge
WAVESHARE_13 = (0, 0, 16) # Waveshare 1.3" 240x240 LCD contributed by Aaron Mittelmeier
ADAFRUIT_1_9 = (35, 0, PORTRAIT) # 320x170 TFT https://www.adafruit.com/product/5394
@micropython.viper
def _lcopy(dest: ptr16, source: ptr8, length: int):
# rgb565 - 16bit/pixel
n: int = 0
while length:
c = source[n]
# Source byte holds 8-bit rrrgggbb
# source rrrgggbb
# dest rrr00ggg000bb000
dest[n] = ((c & 0xE0) << 8) | ((c & 0x1C) << 6) | ((c & 0x03) << 3)
n += 1
length -= 1
class ST7789(framebuf.FrameBuffer):
# Convert r, g, b in range 0-255 to an 8 bit colour value
# rrrgggbb. Converted to 16 bit on the fly.
@staticmethod
def rgb(r, g, b):
return ((r & 0xE0) | ((g >> 3) & 0x1C) | (b >> 6)) ^ 0xFFFF
# rst and cs are active low, SPI is mode 0
def __init__(
self,
spi,
cs,
dc,
rst,
height=240,
width=240,
disp_mode=LANDSCAPE,
init_spi=False,
display=GENERIC,
):
if not 0 <= disp_mode <= 7:
raise ValueError("Invalid display mode:", disp_mode)
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
self._offset = display[:2] # display arg is (x, y, orientation)
orientation = display[2] # where x, y is the RAM offset
self._spi_init = init_spi # Possible user callback
self._lock = asyncio.Lock()
self._gscale = False # Interpret buffer as index into color LUT
self.mode = framebuf.GS8 # Use 8bit greyscale.
self.palette = BoolPalette(self.mode)
gc.collect()
buf = bytearray(height * width)
self.mvb = memoryview(buf)
super().__init__(buf, width, height, self.mode)
self._linebuf = bytearray(self.width * 2) # 16 bit color out
self._init(disp_mode, orientation)
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
# where I can find no requirement in the datasheet. I removed them with
# other redundant code.
def _init(self, user_mode, orientation):
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
sleep_ms(10) # Adafruit delay 500ms (datsheet 5ms)
wcd(b"\x3a", b"\x55") # _COLMOD 16 bit/pixel, 65Kbit color space
cmd(b"\x20") # INVOFF Adafruit turn inversion on. This driver fixes .rgb
cmd(b"\x13") # NORON Normal display mode
# Table maps user request onto hardware values. index values:
# 0 Normal
# 1 Reflect
# 2 USD
# 3 USD reflect
# Followed by same for LANDSCAPE
if not orientation:
user_mode ^= PORTRAIT
# Hardware mappings
# 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
# LANDSCAPE = 0
# PORTRAIT = 0x20
# REFLECT = 0x40
# USD = 0x80
mode = (0x60, 0xE0, 0xA0, 0x20, 0, 0x40, 0xC0, 0x80)[user_mode]
# Set display window depending on mode, .height and .width.
self.set_window(mode)
wcd(b"\x36", int.to_bytes(mode, 1, "little"))
cmd(b"\x29") # DISPON. Adafruit then delay 500ms.
# Define the mapping between RAM and the display.
# Datasheet section 8.12 p124.
def set_window(self, mode):
portrait, reflect, usd = 0x20, 0x40, 0x80
rht = 320
rwd = 240 # RAM ht and width
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.
self._wcd(b"\x2a", int.to_bytes((xs << 16) + xe, 4, "big"))
# Row address set
self._wcd(b"\x2b", int.to_bytes((ys << 16) + ye, 4, "big"))
def show(self): # Blocks for 83ms @60MHz SPI
# Blocks for 60ms @30MHz SPI on TTGO in PORTRAIT mode
# Blocks for 46ms @30MHz SPI on TTGO in LANDSCAPE mode
# ts = ticks_us()
wd = self.width
end = self.height * wd
lb = memoryview(self._linebuf)
buf = self.mvb
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"\x2c") # RAMWR
self._dc(1)
for start in range(0, end, wd):
_lcopy(lb, buf[start:], wd) # Copy and map colors
self._spi.write(lb)
self._cs(1)
# print(ticks_diff(ticks_us(), ts))
# Asynchronous refresh with support for reducing blocking time.
async def do_refresh(self, split=5):
async with self._lock:
lines, mod = divmod(self.height, split) # Lines per segment
if mod:
raise ValueError("Invalid do_refresh arg.")
wd = self.width
lb = memoryview(self._linebuf)
buf = self.mvb
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:], wd) # Copy and map colors
self._spi.write(lb)
line += lines
self._cs(1)
await asyncio.sleep(0)