4-bit and ILI9341 support. Improve shared bus handling. Driver doc added.

pull/8/head
Peter Hinch 2020-12-15 11:41:23 +00:00
rodzic 227e614413
commit 9256c87436
3 zmienionych plików z 67 dodań i 56 usunięć

Wyświetl plik

@ -219,18 +219,17 @@ Adafruit make several displays using this chip, for example
`height` and `width` values. `height` and `width` values.
* `width=320` * `width=320`
* `usd=False` Upside down: set `True` to invert display. * `usd=False` Upside down: set `True` to invert display.
* `split=False` By default the entire display is refreshed by the `show`
method. A partial update may be specified for use with `uasyncio`. See below.
* `init_spi=False` This optional arg enables flexible options in configuring * `init_spi=False` This optional arg enables flexible options in configuring
the SPI bus. The default assumes exclusive access to the bus with the SPI bus. The default assumes exclusive access to the bus. In this normal
`color_setup.py` initialising it. Those settings will be left in place. If a case, `color_setup.py` initialises it and the settings will be left in place.
callback function is passed, it will be called prior to each SPI bus write: If the bus is shared with devices which require different settings, a callback
this is for shared bus applications. The callback will receive a single arg function should be passed. It will be called prior to each SPI bus write. The
being the SPI bus instance. In normal use it will be a one-liner or lambda callback will receive a single arg being the SPI bus instance. It will
initialising the bus. A minimal example is this function: typically be a one-liner or lambda initialising the bus. A minimal example is
this function:
```python ```python
def spi_init(spi): def spi_init(spi):
spi.init(baudrate=10_000_000) # Data sheet: max is 10MHz spi.init(baudrate=10_000_000)
``` ```
The ILI9341 class uses 4-bit color to conserve RAM. Even with this adaptation The ILI9341 class uses 4-bit color to conserve RAM. Even with this adaptation
@ -243,22 +242,18 @@ to use the `micropython.native` decorator.
#### Use with uasyncio #### Use with uasyncio
A full refresh blocks for ~200ms. This may be unacceptable for some `uasyncio` A full refresh blocks for ~200ms. If this is acceptable, no special precautions
applications. The `split` constructor arg limits the number of display lines are required. However this period may be unacceptable for some `uasyncio`
which are updated at one time, reducing the blocking time. To use this, an applications. The driver provides an asynchronous `do_refresh(split=4)` method.
integer value of 2, 4, or 8 should be passed. For example to reduce blocking by If this is run the display will regularly be refreshed, but will periodically
a factor of ~4 to 50ms the `split` constructor arg is set to 4. yield to the scheduler enabling other tasks to run. The arg determines the
number of times this will occur, so by default it will block for about 50ms.
A `ValueError` will result if `split` is not an integer divisor of the display
height.
For any value the following keeps the display updated: An application using this should call `refresh(ssd, True)` once at the start,
```python then launch the `do_refresh` method. After that, no calls to `refresh` should
import uasyncio as asyncio be made. See `gui/demos/scale_ili.py`.
from gui.core.nanogui import refresh
async def keep_refreshed(ssd):
while True:
refresh(ssd) # Blocks for a period defined by split
await asyncio.sleep_ms(0)
```
###### [Contents](./DRIVERS.md#contents) ###### [Contents](./DRIVERS.md#contents)

Wyświetl plik

@ -802,6 +802,7 @@ would present no problem.
The ESP8266 is a minimal platform with typically 36.6KiB of free RAM. The The ESP8266 is a minimal platform with typically 36.6KiB of free RAM. The
framebuffer for a 128*128 OLED requires 16KiB of contiguous RAM (the display framebuffer for a 128*128 OLED requires 16KiB of contiguous RAM (the display
hardware uses 16 bit color but my driver uses an 8 bit buffer to conserve RAM). hardware uses 16 bit color but my driver uses an 8 bit buffer to conserve RAM).
The 4-bit driver halves this size.
A further issue is that, by default, ESP8266 firmware does not support complex A further issue is that, by default, ESP8266 firmware does not support complex
numbers. This rules out the plot module and the `Dial` widget. It is possible numbers. This rules out the plot module and the `Dial` widget. It is possible
@ -813,8 +814,8 @@ to create dynamic content, and the widgets themselves are relatively complex.
I froze a subset of the `drivers` and the `gui` directories. A subset minimises I froze a subset of the `drivers` and the `gui` directories. A subset minimises
the size of the firmware build and eliminates modules which won't compile due the size of the firmware build and eliminates modules which won't compile due
to the complex number issue. The directory structure in my frozen modules to the complex number issue. The directory structure in my frozen modules
directory matched that of the source. This is the structure of my frozen directory matched that of the source. This was the structure of my frozen
directory: directory before I added the 4 bit driver:
![Image](images/esp8266_tree.JPG) ![Image](images/esp8266_tree.JPG)
I erased flash, built and installed the new firmware. Finally I copied I erased flash, built and installed the new firmware. Finally I copied
@ -825,8 +826,8 @@ Both demos worked perfectly.
I modified the demos to regularly report free RAM. `scale.py` reported 10480 I modified the demos to regularly report free RAM. `scale.py` reported 10480
bytes, `tbox.py` reported 10512 bytes, sometimes more, as the demo progressed. bytes, `tbox.py` reported 10512 bytes, sometimes more, as the demo progressed.
In conclusion I think that applications of moderate complexity should be With the 4 bit driver `scale.py` reported 18112 bytes. In conclusion I think
feasible. that applications of moderate complexity should be feasible.
###### [Contents](./README.md#contents) ###### [Contents](./README.md#contents)

Wyświetl plik

@ -12,6 +12,7 @@
from time import sleep_ms from time import sleep_ms
import gc import gc
import framebuf import framebuf
import uasyncio as asyncio
@micropython.viper @micropython.viper
def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int): def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int):
@ -20,7 +21,7 @@ def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int):
c = source[x] c = source[x]
d = (c & 0xf0) >> 3 # 2* pointers (lut is 16 bit color) d = (c & 0xf0) >> 3 # 2* pointers (lut is 16 bit color)
e = (c & 0x0f) << 1 e = (c & 0x0f) << 1
dest[n] = lut[d] dest[n] = lut[d] # LS byte 1st
n += 1 n += 1
dest[n] = lut[d + 1] dest[n] = lut[d + 1]
n += 1 n += 1
@ -34,24 +35,24 @@ class ILI9341(framebuf.FrameBuffer):
lut = bytearray(32) 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
# ILI9341 expects RGB order
@staticmethod @staticmethod
def rgb(r, g, b): def rgb(r, g, b):
return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3 return (r & 0xf8) | (g & 0xe0) >> 5 | (g & 0x1c) << 11 | (b & 0xf8) << 5
# Transpose width & height for landscape mode # Transpose width & height for landscape mode
def __init__(self, spi, cs, dc, rst, height=240, width=320, def __init__(self, spi, cs, dc, rst, height=240, width=320,
usd=False, split=False, init_spi=False): usd=False, init_spi=False):
self._spi = spi self._spi = spi
self._cs = cs self._cs = cs
self._dc = dc self._dc = dc
self._rst = rst self._rst = rst
self.height = height self.height = height
self.width = width self.width = width
if split and split not in (2, 4, 8):
raise ValueError('split must be 2, 4 or 8')
self._spi_init = init_spi self._spi_init = init_spi
self._lines = 0 if not split else height // split # For uasyncio use: show n lines only
self._line = 0 # Current line
mode = framebuf.GS4_HMSB mode = framebuf.GS4_HMSB
gc.collect() gc.collect()
buf = bytearray(self.height * self.width // 2) buf = bytearray(self.height * self.width // 2)
@ -84,7 +85,7 @@ class ILI9341(framebuf.FrameBuffer):
else: else:
self._wcd(b'\x36', b'\x28' if usd else b'\xe8') # MADCTL: RGB landscape mode self._wcd(b'\x36', b'\x28' if usd else b'\xe8') # MADCTL: RGB landscape mode
self._wcd(b'\x37', b'\x00') # VSCRSADD Vertical scrolling start address self._wcd(b'\x37', b'\x00') # VSCRSADD Vertical scrolling start address
self._wcd(b'\x3a', b'\x55') # PIXFMT COLMOD: Pixel format self._wcd(b'\x3a', b'\x55') # PIXFMT COLMOD: Pixel format 16 bits (MCU & interface)
self._wcd(b'\xb1', b'\x00\x18') # FRMCTR1 Frame rate ctrl self._wcd(b'\xb1', b'\x00\x18') # FRMCTR1 Frame rate ctrl
self._wcd(b'\xb6', b'\x08\x82\x27') # DFUNCTR self._wcd(b'\xb6', b'\x08\x82\x27') # DFUNCTR
self._wcd(b'\xf2', b'\x00') # ENABLE3G Enable 3 gamma ctrl self._wcd(b'\xf2', b'\x00') # ENABLE3G Enable 3 gamma ctrl
@ -93,7 +94,7 @@ class ILI9341(framebuf.FrameBuffer):
self._wcd(b'\xe1', b'\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F') # GMCTRN1 self._wcd(b'\xe1', b'\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F') # GMCTRN1
self._wcmd(b'\x11') # SLPOUT Exit sleep self._wcmd(b'\x11') # SLPOUT Exit sleep
sleep_ms(100) sleep_ms(100)
self._wcmd(b'\x29') # DISPLAY_ON Display on self._wcmd(b'\x29') # DISPLAY_ON
sleep_ms(100) sleep_ms(100)
# Write a command. # Write a command.
@ -114,11 +115,8 @@ class ILI9341(framebuf.FrameBuffer):
self._spi.write(data) self._spi.write(data)
self._cs(1) self._cs(1)
# No. of lines buffered vs time. Tested in portrait mode 240 pixels/line. # Time (ESP32 stock freq) 196ms portrait, 185ms landscape.
# ESP32 at stock freq. # mem free on ESP32 43472 bytes (vs 110192)
# 24 lines 171ms
# 2 lines 180ms
# 1 line 196ms
@micropython.native @micropython.native
def show(self): def show(self):
clut = ILI9341.lut clut = ILI9341.lut
@ -126,25 +124,42 @@ class ILI9341(framebuf.FrameBuffer):
ht = self.height ht = self.height
lb = self._linebuf lb = self._linebuf
buf = self._mvb buf = self._mvb
# Commands needed to start data write
#self._wcd(b'\x2a', *ustruct.pack(">HH", 0, self.width)) # SET_COLUMN
#self._wcd(b'\x2b', *ustruct.pack(">HH", 0, ht)) # SET_PAGE
if self._spi_init: # A callback was passed if self._spi_init: # A callback was passed
self._spi_init(self._spi) # Bus may be shared self._spi_init(self._spi) # Bus may be shared
# Commands needed to start data write
self._wcd(b'\x2a', int.to_bytes(self.width, 4, 'big')) # SET_COLUMN self._wcd(b'\x2a', int.to_bytes(self.width, 4, 'big')) # SET_COLUMN
self._wcd(b'\x2b', int.to_bytes(ht, 4, 'big')) # SET_PAGE self._wcd(b'\x2b', int.to_bytes(ht, 4, 'big')) # SET_PAGE
self._wcmd(b'\x2c') # WRITE_RAM self._wcmd(b'\x2c') # WRITE_RAM
self._dc(1) self._dc(1)
self._cs(0) self._cs(0)
if self._lines: for start in range(0, wd*ht, wd): # For each line
end = self._line + wd * self._lines _lcopy(lb, buf[start :], clut, wd) # Copy and map colors
for start in range(self._line, end, wd): # For each line self._spi.write(lb)
_lcopy(lb, buf[start :], clut, wd) # Copy and map colors
self._spi.write(lb)
nxt = start + wd
self._line = nxt % wd*ht
else:
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)
self._cs(1) self._cs(1)
async def do_refresh(self, split=4):
lines, mod = divmod(self.height, split) # Lines per segment
if mod:
raise ValueError('Invalid do_refresh arg.')
clut = ILI9341.lut
wd = self.width // 2
ht = self.height
lb = self._linebuf
buf = self._mvb
while True: # Perform a refresh
# Commands needed to start data write
self._wcd(b'\x2a', int.to_bytes(self.width, 4, 'big')) # SET_COLUMN
self._wcd(b'\x2b', int.to_bytes(ht, 4, 'big')) # SET_PAGE
self._wcmd(b'\x2c') # WRITE_RAM
self._dc(1)
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)