kopia lustrzana https://github.com/peterhinch/micropython-nano-gui
202 wiersze
7.8 KiB
Python
202 wiersze
7.8 KiB
Python
# ILI9486 nano-gui driver for ili9486 displays
|
|
|
|
# Copyright (c) Peter Hinch 2022
|
|
# Released under the MIT license see LICENSE
|
|
|
|
# Much help provided by @brave-ulysses in this thread
|
|
# https://github.com/micropython/micropython/discussions/10404 for the special handling
|
|
# required by the Waveshare Pi HAT.
|
|
|
|
# This driver configures the chip in portrait mode with rotation performed in the driver.
|
|
# This is done to enable default values to be used for the Column Address Set and Page
|
|
# Address Set registers. This avoids having to use commands with multi-byte data values,
|
|
# which would necessitate special code for the Waveshare Pi HAT (see DRIVERS.md).
|
|
|
|
from time import sleep_ms
|
|
import gc
|
|
import framebuf
|
|
import asyncio
|
|
from drivers.boolpalette import BoolPalette
|
|
|
|
# Portrait mode
|
|
@micropython.viper
|
|
def _lcopy(dest: ptr16, source: ptr8, lut: ptr16, length: int):
|
|
# rgb565 - 16bit/pixel
|
|
n = 0
|
|
for x in range(length):
|
|
c = source[x]
|
|
dest[n] = lut[c >> 4] # current pixel
|
|
n += 1
|
|
dest[n] = lut[c & 0x0F] # next pixel
|
|
n += 1
|
|
|
|
|
|
# FB is in landscape mode, hence issue a column at a time to portrait mode hardware.
|
|
@micropython.viper
|
|
def _lscopy(dest: ptr16, source: ptr8, lut: ptr16, ch: int):
|
|
col = ch & 0x1FF # Unpack (viper 4 parameter limit)
|
|
height = (ch >> 9) & 0x1FF
|
|
wbytes = ch >> 19 # Width in bytes is width // 2
|
|
# rgb565 - 16bit/pixel
|
|
n = 0
|
|
clsb = col & 1
|
|
idx = col >> 1 # 2 pixels per byte
|
|
for _ in range(height):
|
|
if clsb:
|
|
c = source[idx] & 0x0F
|
|
else:
|
|
c = source[idx] >> 4
|
|
dest[n] = lut[c] # 16 bit transfer of rightmost 4-bit pixel
|
|
n += 1 # 16 bit
|
|
idx += wbytes
|
|
|
|
|
|
class ILI9486(framebuf.FrameBuffer):
|
|
|
|
lut = bytearray(32)
|
|
COLOR_INVERT = 0
|
|
|
|
# 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
|
|
# ILI9486 expects RGB order. 8 bit register writes require padding
|
|
@classmethod
|
|
def rgb(cls, r, g, b):
|
|
return cls.COLOR_INVERT ^ (
|
|
(r & 0xF8) | (g & 0xE0) >> 5 | (g & 0x1C) << 11 | (b & 0xF8) << 5
|
|
)
|
|
|
|
# Transpose width & height for landscape mode
|
|
def __init__(
|
|
self, spi, cs, dc, rst, height=320, width=480, usd=False, mirror=False, init_spi=False
|
|
):
|
|
self._spi = spi
|
|
self._cs = cs
|
|
self._dc = dc
|
|
self._rst = rst
|
|
self.height = height # Logical dimensions for GUIs
|
|
self.width = width
|
|
self._long = max(height, width) # Physical dimensions of screen and aspect ratio
|
|
self._short = min(height, width)
|
|
self._spi_init = init_spi
|
|
mode = framebuf.GS4_HMSB
|
|
self.palette = BoolPalette(mode)
|
|
gc.collect()
|
|
buf = bytearray(height * width // 2)
|
|
self._mvb = memoryview(buf)
|
|
super().__init__(buf, width, height, mode) # Logical aspect ratio
|
|
self._linebuf = bytearray(self._short * 2)
|
|
|
|
# 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"\x55") # interface pixel format
|
|
# Normally use defaults. This allows it to work on the Waveshare board with a
|
|
# shift register. If size is not 320x480 assume no shift register.
|
|
# Default column address start == 0, end == 0x13F (319)
|
|
if self._short != 320: # Not the Waveshare board: no shift register
|
|
self._wcd(b"\x2a", int.to_bytes(self._short - 1, 4, "big"))
|
|
# Default page address start == 0 end == 0x1DF (479)
|
|
if self._long != 480:
|
|
self._wcd(b"\x2b", int.to_bytes(self._long - 1, 4, "big")) # SET_PAGE ht
|
|
# self._wcd(b"\x36", b"\x48" if usd else b"\x88") # MADCTL: RGB portrait mode
|
|
madctl = 0x48 if usd else 0x88
|
|
if mirror:
|
|
madctl ^= 0x80
|
|
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)
|
|
|
|
# @micropython.native # Made almost no difference to timing
|
|
def show(self): # Physical display is in portrait mode
|
|
clut = ILI9486.lut
|
|
lb = self._linebuf
|
|
buf = self._mvb
|
|
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)
|
|
if self.width < self.height: # Portrait 214ms on RP2 120MHz, 30MHz SPI clock
|
|
wd = self.width // 2
|
|
ht = self.height
|
|
for start in range(0, wd * ht, wd): # For each line
|
|
_lcopy(lb, buf[start:], clut, wd) # Copy and map colors
|
|
self._spi.write(lb)
|
|
else: # Landscpe 264ms on RP2 120MHz, 30MHz SPI clock
|
|
width = self.width
|
|
wd = width - 1
|
|
cargs = (self.height << 9) + (width << 18) # Viper 4-arg limit
|
|
for col in range(width): # For each column of landscape display
|
|
_lscopy(lb, buf, clut, wd - col + cargs) # Copy and map colors
|
|
self._spi.write(lb)
|
|
self._cs(1)
|
|
|
|
async def do_refresh(self, split=4):
|
|
async with self._lock:
|
|
lines, mod = divmod(self._long, split) # Lines per segment
|
|
if mod:
|
|
raise ValueError("Invalid do_refresh arg.")
|
|
clut = ILI9486.lut
|
|
lb = self._linebuf
|
|
buf = self._mvb
|
|
self._wcmd(b"\x2c") # WRITE_RAM
|
|
self._dc(1)
|
|
if self.width < self.height: # Portrait: write sets of rows
|
|
wd = self.width // 2
|
|
line = 0
|
|
for _ in range(split): # For each segment
|
|
if self._spi_init: # A callback was passed
|
|
self._spi_init(self._spi) # Bus may be shared
|
|
self._cs(0)
|
|
for start in range(wd * line, wd * (line + lines), wd): # For each line
|
|
_lcopy(lb, buf[start:], clut, wd) # Copy and map colors
|
|
self._spi.write(lb)
|
|
line += lines
|
|
self._cs(1) # Allow other tasks to use bus
|
|
await asyncio.sleep_ms(0)
|
|
else: # Landscape: write sets of cols. lines is no. of cols per segment.
|
|
cargs = (self.height << 9) + (self.width << 18) # Viper 4-arg limit
|
|
sc = self.width - 1 # Start and end columns
|
|
ec = sc - lines # End column
|
|
for _ in range(split): # For each segment
|
|
if self._spi_init: # A callback was passed
|
|
self._spi_init(self._spi) # Bus may be shared
|
|
self._cs(0)
|
|
for col in range(sc, ec, -1): # For each column of landscape display
|
|
_lscopy(lb, buf, clut, col + cargs) # Copy and map colors
|
|
self._spi.write(lb)
|
|
sc -= lines
|
|
ec -= lines
|
|
self._cs(1) # Allow other tasks to use bus
|
|
await asyncio.sleep_ms(0)
|