kopia lustrzana https://github.com/peterhinch/micropython-nano-gui
ILI9486 update.
rodzic
51b590faad
commit
7ed8f7d627
51
DRIVERS.md
51
DRIVERS.md
|
@ -638,18 +638,41 @@ If your display shows garbage, check the following (I have seen both):
|
|||
|
||||
## 3.4 Driver for ILI9486
|
||||
|
||||
This was tested with
|
||||
The ILI9486 supports displays of up to 480x320 pixels which is large by
|
||||
microcontroller standards. Even with 4-bit color the frame buffer requires
|
||||
76,800 bytes. On a Pico `nanogui` works fine, but `micro-gui` fails to
|
||||
compile unless frozen bytecode is used, in which case it runs with about 75K of
|
||||
free RAM.
|
||||
|
||||
The driver aims to work with any ILI9486, however
|
||||
[this display](https://www.waveshare.com/product/3.5inch-RPi-LCD-A.htm), a
|
||||
480x320 color LCD designed for the Raspberry Pi. Note that even with 4-bit
|
||||
color the display buffer is 76,800 bytes. On a Pico `nanogui` works fine, but
|
||||
`micro-gui` fails to compile unless frozen bytecode is used, in which case it
|
||||
runs with about 75K free RAM.
|
||||
480x320 color LCD designed for the Raspberry Pi, has special hardware. Rather
|
||||
than driving the ILI9486 via SPI, it uses SPI to fill a shift register, copying
|
||||
the data to the chip using a parallel interface. The driver is designed to work
|
||||
with both types of hardware.
|
||||
|
||||
See [nanogui setup](https://github.com/peterhinch/micropython-nano-gui/blob/master/setup_examples/ili9486_pico.py)
|
||||
and [microgui setup](https://github.com/peterhinch/micropython-micro-gui/blob/main/setup_examples/ili9486_pico.py)
|
||||
for examples of initialisation files.
|
||||
##### Generic display wiring
|
||||
|
||||
##### Wiring
|
||||
Testing was done with a Pico, using the following setup files:
|
||||
[nanogui setup](https://github.com/peterhinch/micropython-nano-gui/blob/master/setup_examples/ili9486_pico.py)
|
||||
and [microgui setup](https://github.com/peterhinch/micropython-micro-gui/blob/main/setup_examples/ili9486_pico.py).
|
||||
These use the following pinout:
|
||||
|
||||
| Pico pin | GPIO | Display | Signal |
|
||||
|:---------|:-----|:--------|:--------|
|
||||
| 40 | n/a | | Vbus 5V |
|
||||
| 36 | n/a | | 3.3V |
|
||||
| 3,8,36.. | n/a | Gnd | Gnd |
|
||||
| 9 | 6 | SCLK | |
|
||||
| 10 | 7 | MOSI | |
|
||||
| 11 | 8 | DC | |
|
||||
| 12 | 9 | RST | |
|
||||
| 14 | 10 | CS | |
|
||||
|
||||
Please check the power requirements of the display board, which may require a
|
||||
5V or a 3.3V supply.
|
||||
|
||||
##### Waveshare PI HAT wiring
|
||||
|
||||
This shows the Raspberry Pi connector looking at the underside of the board
|
||||
with the bulk of the board to the right. This was tested with a Pi Pico.
|
||||
|
@ -697,11 +720,11 @@ def spi_init(spi):
|
|||
spi.init(baudrate=10_000_000)
|
||||
```
|
||||
|
||||
The ILI9486 class uses 4-bit color to conserve RAM. Even with this adaptation
|
||||
the buffer size is 76.85KiB. See [Color handling](./DRIVERS.md#11-color-handling)
|
||||
for details of the implications of 4-bit color. On the Pico with the display
|
||||
driver loaded there was 85KiB free RAM running `nano-gui` but `micro-gui` ran
|
||||
out of RAM..
|
||||
The ILI9486 class uses 4-bit color to conserve RAM. See
|
||||
[Color handling](./DRIVERS.md#11-color-handling) for the implications of 4-bit
|
||||
color. On the Pico with the display driver loaded there was 85KiB free RAM
|
||||
running `nano-gui`. To run `micro-gui` it was necessary to run the GUI as
|
||||
frozen bytecode, when it ran with 75K of free RAM.
|
||||
|
||||
The driver uses the `micropython.viper` decorator. If your platform does not
|
||||
support this, the Viper code will need to be rewritten with a substantial hit
|
||||
|
|
|
@ -4,11 +4,14 @@
|
|||
# Copyright (c) Peter Hinch 2022
|
||||
# Released under the MIT license see LICENSE
|
||||
|
||||
# Inspired by @brave-ulysses https://github.com/micropython/micropython/discussions/10404
|
||||
# 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.
|
||||
|
||||
# Design note. I could not find a way to do landscape display at the chip level
|
||||
# without a nasty hack. Consequently this driver uses portrait mode at chip level,
|
||||
# with rotation performed in the driver.
|
||||
# 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
|
||||
|
@ -18,21 +21,22 @@ from drivers.boolpalette import BoolPalette
|
|||
|
||||
# Portrait mode
|
||||
@micropython.viper
|
||||
def _lcopy(dest:ptr16, source:ptr8, lut:ptr16, length:int):
|
||||
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
|
||||
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
|
||||
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
|
||||
|
@ -40,7 +44,7 @@ def _lscopy(dest:ptr16, source:ptr8, lut:ptr16, ch:int):
|
|||
idx = col >> 1 # 2 pixels per byte
|
||||
for _ in range(height):
|
||||
if clsb:
|
||||
c = source[idx] & 0x0f
|
||||
c = source[idx] & 0x0F
|
||||
else:
|
||||
c = source[idx] >> 4
|
||||
dest[n] = lut[c] # 16 bit transfer of rightmost 4-bit pixel
|
||||
|
@ -58,7 +62,7 @@ class ILI9486(framebuf.FrameBuffer):
|
|||
# ILI9486 expects RGB order. 8 bit register writes require padding
|
||||
@staticmethod
|
||||
def rgb(r, g, b):
|
||||
return (r & 0xf8) | (g & 0xe0) >> 5 | (g & 0x1c) << 11 | (b & 0xf8) << 5
|
||||
return (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, init_spi=False):
|
||||
|
@ -71,12 +75,12 @@ class ILI9486(framebuf.FrameBuffer):
|
|||
self._long = max(height, width) # Physical dimensions of screen and aspect ratio
|
||||
self._short = min(height, width)
|
||||
self._spi_init = init_spi
|
||||
pmode = framebuf.GS4_HMSB
|
||||
self.palette = BoolPalette(pmode)
|
||||
mode = framebuf.GS4_HMSB
|
||||
self.palette = BoolPalette(mode)
|
||||
gc.collect()
|
||||
buf = bytearray(height * width // 2)
|
||||
self._mvb = memoryview(buf)
|
||||
super().__init__(buf, width, height, pmode) # Logical aspect ratio
|
||||
super().__init__(buf, width, height, mode) # Logical aspect ratio
|
||||
self._linebuf = bytearray(self._short * 2)
|
||||
|
||||
# Hardware reset
|
||||
|
@ -89,64 +93,63 @@ class ILI9486(framebuf.FrameBuffer):
|
|||
self._lock = asyncio.Lock()
|
||||
# Send initialization commands
|
||||
|
||||
self._wcmd(b'\x01') # SWRESET Software reset
|
||||
self._wcmd(b"\x01") # SWRESET Software reset
|
||||
sleep_ms(100)
|
||||
self._wcmd(b'\x11') # sleep out
|
||||
self._wcmd(b"\x11") # sleep out
|
||||
sleep_ms(20)
|
||||
self._wcd(b'\x3a', b'\x55') # interface pixel format
|
||||
self._wcd(b'\x36', b'\x48' if usd else b'\x88') # MADCTL: RGB portrait mode
|
||||
self._wcmd(b'\x11') # sleep out
|
||||
self._wcmd(b'\x29') # display on
|
||||
|
||||
# Write data.
|
||||
def _wdata(self, data):
|
||||
self._dc(1)
|
||||
self._cs(0)
|
||||
self._spi.write( data )
|
||||
self._cs(1)
|
||||
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
|
||||
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._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._spi.write(command)
|
||||
self._cs(1)
|
||||
self._dc(1)
|
||||
self._cs(0)
|
||||
self._spi.write( data )
|
||||
self._spi.write(data)
|
||||
self._cs(1)
|
||||
|
||||
@micropython.native
|
||||
# @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
|
||||
# Commands needed to start data write
|
||||
self._wcd(b'\x2a', int.to_bytes(self._short -1, 4, 'big')) # SET_COLUMN works 0 .. width
|
||||
self._wcd(b'\x2b', int.to_bytes(self._long -1, 4, 'big')) # SET_PAGE ht
|
||||
self._wcmd(b'\x2c') # WRITE_RAM
|
||||
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
|
||||
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
|
||||
cargs = (self.height << 9) + (self.width << 18) # Viper 4-arg limit
|
||||
for col in range(self.width -1, -1, -1): # For each column of landscape display
|
||||
_lscopy(lb, buf, clut, col + cargs) # Copy and map colors
|
||||
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)
|
||||
|
||||
|
@ -154,31 +157,28 @@ class ILI9486(framebuf.FrameBuffer):
|
|||
async with self._lock:
|
||||
lines, mod = divmod(self._long, split) # Lines per segment
|
||||
if mod:
|
||||
raise ValueError('Invalid do_refresh arg.')
|
||||
raise ValueError("Invalid do_refresh arg.")
|
||||
clut = ILI9486.lut
|
||||
lb = self._linebuf
|
||||
buf = self._mvb
|
||||
self._wcd(b'\x2a', int.to_bytes(self._short -1, 4, 'big')) # SET_COLUMN works 0 .. width
|
||||
self._wcd(b'\x2b', int.to_bytes(self._long -1, 4, 'big')) # SET_PAGE ht
|
||||
self._wcmd(b'\x2c') # WRITE_RAM
|
||||
self._wcmd(b"\x2c") # WRITE_RAM
|
||||
self._dc(1)
|
||||
if self.width < self.height: # Portrait: write sets of rows
|
||||
wd = self.width // 2
|
||||
ht = self.height
|
||||
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
|
||||
_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
|
||||
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
|
||||
|
@ -191,4 +191,3 @@ class ILI9486(framebuf.FrameBuffer):
|
|||
ec -= lines
|
||||
self._cs(1) # Allow other tasks to use bus
|
||||
await asyncio.sleep_ms(0)
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ import gc
|
|||
from drivers.ili94xx.ili9486 import ILI9486 as SSD
|
||||
|
||||
pdc = Pin(8, Pin.OUT, value=0)
|
||||
pcs = Pin(9, Pin.OUT, value=1)
|
||||
prst = Pin(10, Pin.OUT, value=1)
|
||||
prst = Pin(9, Pin.OUT, value=1)
|
||||
pcs = Pin(10, Pin.OUT, value=1)
|
||||
spi = SPI(0, sck=Pin(6), mosi=Pin(7), miso=Pin(4), baudrate=30_000_000)
|
||||
gc.collect() # Precaution before instantiating framebuf
|
||||
ssd = SSD(spi, pcs, pdc, prst)
|
||||
|
|
Ładowanie…
Reference in New Issue