kopia lustrzana https://github.com/peterhinch/micropython-nano-gui
263 wiersze
8.2 KiB
Python
263 wiersze
8.2 KiB
Python
# ILI9488 nano-gui driver for ili9488 displays
|
|
|
|
### Based on ili9486.py by Peter Hinch.
|
|
### Retaining his copyright
|
|
|
|
# Copyright (c) Peter Hinch 2022-2025
|
|
# Released under the MIT license see LICENSE
|
|
|
|
# This driver, adapted from ILI9486, was contributed by Carl Pottle (cpottle9).
|
|
|
|
# Note: If your hardware uses the ILI9488 parallel interface
|
|
# you will likely be better off using the ili9486 driver.
|
|
# It will send 2 bytes per pixel which will run faster.
|
|
#
|
|
# You must use this driver only when using the ILI9488 SPI
|
|
# interface. It will send 3 bytes per pixel.
|
|
|
|
# ILI9488 max SPI baudrate 20MHz (datasheet 17.4.3) but 24MHz is a reasonable overclock.
|
|
|
|
from time import sleep_ms
|
|
import gc
|
|
import framebuf
|
|
import asyncio
|
|
from drivers.boolpalette import BoolPalette
|
|
|
|
# Do processing from end to beginning for
|
|
# small performance improvement.
|
|
# greyscale
|
|
@micropython.viper
|
|
def _lcopy_gs(dest: ptr8, source: ptr8, length: int):
|
|
# rgb666 - 18bit/pixel
|
|
n: int = length * 6 - 1
|
|
while length:
|
|
length -= 1
|
|
c: uint = source[length]
|
|
# Store the index in the 4 high order bits
|
|
p: uint = c & 0xF0 # current pixel
|
|
q: uint = c << 4 # next pixel
|
|
|
|
dest[n] = q
|
|
n -= 1
|
|
dest[n] = q
|
|
n -= 1
|
|
dest[n] = q
|
|
n -= 1
|
|
|
|
dest[n] = p
|
|
n -= 1
|
|
dest[n] = p
|
|
n -= 1
|
|
dest[n] = p
|
|
n -= 1
|
|
|
|
|
|
# Do processing from end to beginning for
|
|
# small performance improvement.
|
|
# color
|
|
@micropython.viper
|
|
def _lcopy(dest: ptr8, source: ptr8, lut: ptr16, length: int):
|
|
# Convert lut rgb 565 to rgb666
|
|
n: int = length * 6 - 1
|
|
while length:
|
|
length -= 1
|
|
c: uint = source[length]
|
|
|
|
v = lut[c & 0x0F] # next pixel
|
|
dest[n] = (v & 0x001F) << 3 # B
|
|
n -= 1
|
|
dest[n] = (v & 0x07E0) >> 3 # G
|
|
n -= 1
|
|
dest[n] = (v & 0xF800) >> 8 # R
|
|
n -= 1
|
|
|
|
v: uint = lut[c >> 4] # current pixel
|
|
dest[n] = (v & 0x001F) << 3 # B
|
|
n -= 1
|
|
dest[n] = (v & 0x07E0) >> 3 # G
|
|
n -= 1
|
|
dest[n] = (v & 0xF800) >> 8 # R
|
|
n -= 1
|
|
|
|
|
|
class ILI9488(framebuf.FrameBuffer):
|
|
|
|
lut = bytearray(32)
|
|
COLOR_INVERT = 0
|
|
|
|
# Convert r, g, b in range 0-255 to a 16 bit colour value
|
|
# 5-6-5 format
|
|
# byte order not swapped (compared to ili9486 driver).
|
|
@classmethod
|
|
def rgb(cls, r, g, b):
|
|
return cls.COLOR_INVERT ^ ((r & 0xF8) << 8 | (g & 0xFC) << 3 | (b >> 3))
|
|
|
|
# Transpose width & height for landscape mode
|
|
def __init__(
|
|
self,
|
|
spi,
|
|
cs,
|
|
dc,
|
|
rst,
|
|
height=320,
|
|
width=480,
|
|
usd=False,
|
|
mirror=False,
|
|
init_spi=False,
|
|
lines_per_write=4,
|
|
):
|
|
self._spi = spi
|
|
self._cs = cs
|
|
self._dc = dc
|
|
self._rst = rst
|
|
self.lock_mode = False # If set, user lock is passed to .do_refresh
|
|
self.height = height # Logical dimensions for GUIs
|
|
self.width = width
|
|
self._spi_init = init_spi
|
|
self._gscale = False # Interpret buffer as index into color LUT
|
|
self.mode = framebuf.GS4_HMSB
|
|
self.palette = BoolPalette(self.mode)
|
|
#
|
|
# lines_per_write must divide evenly into height
|
|
#
|
|
if (self.height % lines_per_write) != 0:
|
|
raise ValueError("lines_per_write invalid")
|
|
self._lines_per_write = lines_per_write
|
|
gc.collect()
|
|
buf = bytearray(height * width // 2)
|
|
self.mvb = memoryview(buf)
|
|
super().__init__(buf, width, height, self.mode) # Logical aspect ratio
|
|
self._linebuf = bytearray(self._lines_per_write * self.width * 3)
|
|
|
|
# Hardware reset
|
|
self._rst(0)
|
|
sleep_ms(50)
|
|
self._rst(1)
|
|
sleep_ms(50)
|
|
if self._spi_init: # A callback was passed
|
|
self._spi_init(spi) # Bus may be shared
|
|
self._lock = asyncio.Lock()
|
|
# Send initialization commands
|
|
|
|
self._wcmd(b"\x01") # SWRESET Software reset
|
|
sleep_ms(100)
|
|
self._wcmd(b"\x11") # sleep out
|
|
sleep_ms(20)
|
|
self._wcd(b"\x3a", b"\x66") # interface pixel format 18 bits per pixel
|
|
|
|
self._wcd(b"\x2a", int.to_bytes(self.width - 1, 4, "big"))
|
|
self._wcd(b"\x2b", int.to_bytes(self.height - 1, 4, "big")) # SET_PAGE ht
|
|
|
|
if self.width > self.height:
|
|
# landscape
|
|
madctl = 0xE8 if usd else 0x28
|
|
else:
|
|
# portrait
|
|
madctl = 0x48 if usd else 0x88
|
|
if mirror:
|
|
madctl ^= 0x80 # toggle MY
|
|
self._wcd(b"\x36", madctl.to_bytes(1, "big")) # MADCTL: RGB portrait mode
|
|
self._wcmd(b"\x11") # sleep out
|
|
self._wcmd(b"\x29") # display on
|
|
|
|
# Write a command.
|
|
def _wcmd(self, command):
|
|
self._dc(0)
|
|
self._cs(0)
|
|
self._spi.write(command)
|
|
self._cs(1)
|
|
|
|
# Write a command followed by a data arg.
|
|
def _wcd(self, command, data):
|
|
self._dc(0)
|
|
self._cs(0)
|
|
self._spi.write(command)
|
|
self._cs(1)
|
|
self._dc(1)
|
|
self._cs(0)
|
|
self._spi.write(data)
|
|
self._cs(1)
|
|
|
|
def greyscale(self, gs=None):
|
|
if gs is not None:
|
|
self._gscale = gs
|
|
return self._gscale
|
|
|
|
# @micropython.native # Made almost no difference to timing
|
|
def show(self): # Physical display is in portrait mode
|
|
lb = self._linebuf
|
|
buf = self.mvb
|
|
cm = self._gscale # color False, greyscale True
|
|
if self._spi_init: # A callback was passed
|
|
self._spi_init(self._spi) # Bus may be shared
|
|
self._wcmd(b"\x2c") # WRITE_RAM
|
|
self._dc(1)
|
|
self._cs(0)
|
|
wd = self.width >> 1
|
|
ht = self.height
|
|
spi_write = self._spi.write
|
|
length = self._lines_per_write * wd
|
|
r = range(0, wd * ht, length)
|
|
if cm:
|
|
lcopy = _lcopy_gs # Copy greyscale
|
|
for start in r: # For each line
|
|
lcopy(lb, buf[start:], length)
|
|
spi_write(lb)
|
|
else:
|
|
clut = ILI9488.lut
|
|
lcopy = _lcopy # Copy and map colors
|
|
for start in r: # For each line
|
|
lcopy(lb, buf[start:], clut, length)
|
|
spi_write(lb)
|
|
self._cs(1)
|
|
|
|
def short_lock(self, v=None):
|
|
if v is not None:
|
|
self.lock_mode = v # If set, user lock is passed to .do_refresh
|
|
return self.lock_mode
|
|
|
|
# nanogui apps typically call with no args. ugui and tgui pass split and
|
|
# may pass a Lock depending on lock_mode
|
|
async def do_refresh(self, split=4, elock=None):
|
|
if elock is None:
|
|
elock = asyncio.Lock()
|
|
async with self._lock:
|
|
lines, mod = divmod(self.height, split) # Lines per segment
|
|
if mod:
|
|
raise ValueError("Invalid do_refresh arg 'split'")
|
|
if lines % self._lines_per_write != 0:
|
|
raise ValueError(
|
|
"Invalid do_refresh arg 'split' for lines_per_write of %d"
|
|
% (self._lines_per_write)
|
|
)
|
|
clut = ILI9488.lut
|
|
lb = self._linebuf
|
|
buf = self.mvb
|
|
cm = self._gscale # color False, greyscale True
|
|
self._wcmd(b"\x2c") # WRITE_RAM
|
|
self._dc(1)
|
|
wd = self.width // 2
|
|
line = 0
|
|
spi_write = self._spi.write
|
|
length = self._lines_per_write * wd
|
|
for _ in range(split): # For each segment
|
|
async with elock:
|
|
if self._spi_init: # A callback was passed
|
|
self._spi_init(self._spi) # Bus may be shared
|
|
self._cs(0)
|
|
r = range(wd * line, wd * (line + lines), length)
|
|
if cm:
|
|
lcopy = _lcopy_gs # Copy and greyscale
|
|
for start in r:
|
|
lcopy(lb, buf[start:], length)
|
|
spi_write(lb)
|
|
else:
|
|
lcopy = _lcopy # Copy and map colors
|
|
for start in r:
|
|
lcopy(lb, buf[start:], clut, length)
|
|
spi_write(lb)
|
|
|
|
line += lines
|
|
self._cs(1) # Allow other tasks to use bus
|
|
await asyncio.sleep_ms(0)
|