Untested ili9341 driver. 4-bit drivers for other displays.

ili9341
Peter Hinch 2020-12-14 10:25:42 +00:00
rodzic 5dbcc65828
commit 227e614413
27 zmienionych plików z 1360 dodań i 499 usunięć

Wyświetl plik

@ -66,7 +66,9 @@ This is necessary because the display drivers use a high baudrate unsupported
by SD cards. Ensure applications do this before the first SD card access and by SD cards. Ensure applications do this before the first SD card access and
before subsequent ones if the display has been refreshed. before subsequent ones if the display has been refreshed.
# Hardware note: SPI clock rate # Notes on OLED displays
## Hardware note: SPI clock rate
For performance reasons the drivers for the Adafruit color displays run the SPI For performance reasons the drivers for the Adafruit color displays run the SPI
bus at a high rate (currently 10.5MHz). Leads should be short and direct. An bus at a high rate (currently 10.5MHz). Leads should be short and direct. An
@ -77,8 +79,6 @@ work. Note that the Pyboard hardware SPI supports only 10.5MHz and 21MHz.
In practice the 41ms update time is visually fast for most purposes except some In practice the 41ms update time is visually fast for most purposes except some
games. games.
# Notes on OLED displays
## Power consumption ## Power consumption
The power consumption of OLED displays is roughly proportional to the number The power consumption of OLED displays is roughly proportional to the number

470
DRIVERS.md 100644
Wyświetl plik

@ -0,0 +1,470 @@
# Display drivers for nano-gui
The nano-gui project currently supports three display technologies: OLED (color
and monochrome), color TFT, and monochrome Sharp displays.
# Contents
1. [Introduction](./DRIVERS.md#1-introduction)
1.1 [Color handling](./DRIVERS.md#11-color-handling)
2. [Drivers for SSD1351](./DRIVERS.md#2-drivers-for-ssd1351) Color OLEDs
3. [Drivers for SSD1331](./DRIVERS.md#3-drivers-for-ssd1331) Small color OLEDs
4. [Drivers for ST7735R](./DRIVERS.md#4-drivers-for-st7735r) Small color TFTs
5. [Drivers for ILI9341](./DRIVERS.md#5-drivers-for-ili9341) Large color TFTs
6. [Drivers for sharp displays](./DRIVERS.md#6-drivers-for-sharp-displays) Large low power monochrome displays
6.1 [Display characteristics](./DRIVERS.md#61-display-characteristics)
     6.1.1 [The VCOM bit](./DRIVERS.md#611-the-vcom-bit)
     6.1.2 [Refresh rate](./DRIVERS.md#612-refresh-rate)
6.2 [Test scripts](./DRIVERS.md#62-test-scripts)
6.3 [Device driver constructor](./DRIVERS.md#63-device-driver-constructor)
     6.3.1 [Device driver methods](./DRIVERS.md#631-device-driver-methods)
     6.3.2 [The vcom arg](./DRIVERS.md#632-the-vcom-arg)
6.4 [Application design](./DRIVERS.md#64-application-design)
     6.4.1 [Micropower applications](./DRIVERS.md#641-micropower-applications)
6.5 [Resources](./DRIVERS.md#65-resources)
7. [Writing device drivers](./DRIVERS.md#7-writing-device-drivers)
###### [Main README](./README.md)
# 1. Introduction
With the exception of the Sharp displays use of these drivers is very simple:
the main reason to consult this doc is to select the right driver for your
display, platform and application.
An application specifies a driver by means of `color_setup.py` or
`ssd1306_setup.py` located in the root directory of the target. This typically
contains code along these lines:
```python
import machine
import gc
from drivers.ssd1351.ssd1351 import SSD1351 as SSD # Choose device driver
pdc = machine.Pin('Y1', machine.Pin.OUT_PP, value=0)
pcs = machine.Pin('Y2', machine.Pin.OUT_PP, value=1)
prst = machine.Pin('Y3', machine.Pin.OUT_PP, value=1)
spi = machine.SPI(2)
gc.collect() # Precaution before instantiating framebuf
ssd = SSD(spi, pcs, pdc, prst, 96) # Create a display instance
```
In the interests of conserving RAM, supplied drivers support only the
functionality required by the GUI. More fully featured drivers may better suit
other applications.
## 1.1 Color handling
Most color displays support colors specified as 16-bit quantities. Storing two
bytes for every pixel results in large frame buffers. Most of the drivers
reduce this to 1 byte (the default) or 4 bits per pixel, with the data being
expanded at runtime when a line is displayed. This trades a large saving in RAM
for a small increase in refresh time. Minimising this increase while keeping
the driver cross-platform involves the use of the `viper` decorator.
Eight bit drivers store colors in `rrrgggbb`. This results in a loss of
precision in specifying a color. Four bit drivers store a color as the index
into a 16 bit lookup table. There is no loss of precision but only 16 distinct
colors can be supported.
The choice of 16, 8 or 4 bit drivers is largely transparent: all demo scripts
run in a visually identical manner under all drivers. This will apply to any
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).
# 2. Drivers for SSD1351
See [Adafruit 1.5" 128*128 OLED display](https://www.adafruit.com/product/1431)
and [Adafruit 1.27" 128*96 display](https://www.adafruit.com/product/1673).
There are four versions.
* `ssd1351.py` This is optimised for STM (e.g. Pyboard) platforms.
* `ssd1351_generic.py` Cross-platform version. Tested on ESP32 and ESP8266.
* `ssd1351_16bit.py` Cross-platform. Uses 16 bit RGB565 color.
* `ssd1351_4bit.py` Cross-platform. Uses 4 bit color.
All these drivers work with the provided demo scripts.
To conserve RAM the first two use 8 bit (rrrgggbb) color. This works well with
the GUI if saturated colors are used to render text and controls.
The `ssd1351_generic.py` and 4 bit versions use the `micropython.viper`
decorator. If your platform does not support this, comment it out and remove
the type annotations. You may be able to use the `micropython.native`
decorator.
If the platform supports the viper emitter performance should still be good: on
a Pyboard V1 the generic driver perorms a refresh of a 128*128 color display in
47ms. The STM version is faster but not by a large margin: a refresh takes
41ms. 32ms of these figures is consumed by the data transfer over the SPI
interface. The 4-bit version with Viper takes 44ms.
If the viper and native decorators are unsupported a screen redraw takes 272ms
(on Pyboard 1.0) which is visibly slow.
The `ssd1351_16bit` version on a 128x128 display requires 32KiB for the frame
buffer; this means it is only usable on platforms with plenty of RAM. Testing
was done on a Pyboard D SF2W. With the GUI this version offers no benefit, but
it delivers major advantages in applications such as rendering images.
For further information see the GUI README
[User defined colors](./README.md#311-user-defined-colors).
This driver was tested on Adafruit 1.5 and 1.27 inch displays.
#### SSD1351 Constructor args:
* `spi` An SPI bus instance.
* `pincs` An initialised output pin. Initial value should be 1.
* `pindc` An initialised output pin. Initial value should be 0.
* `pinrs` An initialised output pin. Initial value should be 1.
* `height=128` Display dimensions in pixels. Height must be 96 or 128.
* `width=128`
* `init_spi=spi_init` This optional arg enables flexible options in
configuring the SPI bus. The default preserves existing behaviour: the SPI bus
is initialised to 20MHz before each use. Other `spi.init` args are default.
This facilitates bus sharing. Passing `False` disables this: `color_setup.py`
must initialise the bus. Those settings will be left in place. If a callback
function is passed, it will be called prior to each SPI bus write: this is for
shared bus applications where a non-standard `init` is required. The callback
will receive a single arg being the SPI bus instance. In normal use it will be
a one-liner or lambda initialising the bus. The default is this function:
```python
def spi_init(spi):
spi.init(baudrate=20_000_000) # Data sheet: should support 20MHz
```
#### A "gotcha" in the datasheet
For anyone seeking to understand or modify the code, the datasheet para 8.3.2
is confusing. They use the colors red, green and blue to represent colors C, B
and A. With the setup used in these drivers, C is blue and A is red. The 16 bit
color streams sent to the display are:
s[x] 1st byte sent b7 b6 b5 b4 b3 g7 g6 g5
s[x + 1] 2nd byte sent g4 g3 g2 r7 r6 r5 r4 r3
###### [Contents](./DRIVERS.md#contents)
# 3. Drivers for SSD1331
See [Adafruit 0.96" OLED display](https://www.adafruit.com/product/684).
There are two versions. Both are cross-platform.
* `ssd1331.py` Uses 8 bit rrrgggbb color.
* `ssd1331_16bit.py` Uses 16 bit RGB565 color.
The `ssd1331_16bit` version requires 12KiB of RAM for the frame buffer, while
the standard version needs only 6KiB. For the GUI the standard version works
well because text and controls are normally drawn with saturated colors.
The 16 bit version provides greatly improved results when rendering images.
#### SSD1331 Constructor args:
* `spi` An SPI bus instance.
* `pincs` An initialised output pin. Initial value should be 1.
* `pindc` An initialised output pin. Initial value should be 0.
* `pinrs` An initialised output pin. Initial value should be 1.
* `height=64` Display dimensions in pixels.
* `width=96`
This driver initialises the SPI clock rate and polarity as required by the
device. The device can support clock rates of upto 6.66MHz.
# 4. Drivers for ST7735R
These are cross-platform but assume `micropython.viper` capability. They use
8-bit color to minimise the RAM used by the frame buffer.
* `st7735r.py` Supports [Adafruit 1.8" display](https://www.adafruit.com/product/358).
* `st7735r144.py` Supports [Adafruit 1.44" display](https://www.adafruit.com/product/2088).
Users of other ST7735R based displays should beware: there are many variants
with differing setup requirements.
[This driver](https://github.com/boochow/MicroPython-ST7735/blob/master/ST7735.py)
has four different initialisation routines for various display versions. Even
the supported Adafruit displays differ in their initialisation settings.
If your Chinese display doesn't work with my drivers you are on your own: I
can't support hardware I don't possess.
#### ST7735R Constructor args:
* `spi` An initialised SPI bus instance. The device can support clock rates of
upto 15MHz.
* `cs` An initialised output pin. Initial value should be 1.
* `dc` An initialised output pin. Initial value should be 0.
* `rst` An initialised output pin. Initial value should be 1.
* `height=128` Display dimensions in pixels. For portrait mode exchange
`height` and `width` values.
* `width=160`
* `usd=False` Upside down: set `True` to invert display.
* `init_spi=False` This optional arg enables flexible options in configuring
the SPI bus. The default assumes exclusive access to the bus with
`color_setup.py` initialising it. Those settings will be left in place. If a
callback function is passed, it will be called prior to each SPI bus write:
this is for shared bus applications. The callback will receive a single arg
being the SPI bus instance. In normal use it will be a one-liner or lambda
initialising the bus. A minimal example is this function:
```python
def spi_init(spi):
spi.init(baudrate=12_000_000) # Data sheet: max is 12MHz
```
# 5. Drivers for ILI9341
Adafruit make several displays using this chip, for example
[this 3.2 inch unit](https://www.adafruit.com/product/1743).
#### ILI9341 Constructor args:
* `spi` An initialised SPI bus instance. The device can support clock rates of
upto 10MHz.
* `cs` An initialised output pin. Initial value should be 1.
* `dc` An initialised output pin. Initial value should be 0.
* `rst` An initialised output pin. Initial value should be 1.
* `height=240` Display dimensions in pixels. For portrait mode exchange
`height` and `width` values.
* `width=320`
* `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
the SPI bus. The default assumes exclusive access to the bus with
`color_setup.py` initialising it. Those settings will be left in place. If a
callback function is passed, it will be called prior to each SPI bus write:
this is for shared bus applications. The callback will receive a single arg
being the SPI bus instance. In normal use it will be a one-liner or lambda
initialising the bus. A minimal example is this function:
```python
def spi_init(spi):
spi.init(baudrate=10_000_000) # Data sheet: max is 10MHz
```
The ILI9341 class uses 4-bit color to conserve RAM. Even with this adaptation
the buffer size is 37.5KiB. See [Color handling](./DRIVERS.md#11-color-handling)
for details of the implications of 4-bit color.
The driver uses the `micropython.viper` decorator. If your platform does not
support this, comment it out and remove the type annotations. You may be able
to use the `micropython.native` decorator.
#### Use with uasyncio
A full refresh blocks for ~200ms. This may be unacceptable for some `uasyncio`
applications. The `split` constructor arg limits the number of display lines
which are updated at one time, reducing the blocking time. To use this, an
integer value of 2, 4, or 8 should be passed. For example to reduce blocking by
a factor of ~4 to 50ms the `split` constructor arg is set to 4.
For any value the following keeps the display updated:
```python
import uasyncio as asyncio
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)
# 6. Drivers for sharp displays
These monochrome SPI displays exist in three variants from Adafruit.
1. [2.7 inch 400x240 pixels](https://www.adafruit.com/product/4694)
2. [1.3 inch 144x168](https://www.adafruit.com/product/3502)
3. [1.3 inch 96x96](https://www.adafruit.com/product/1393) - Discontinued.
I have tested on the first of these. However the
[Adfruit driver](https://github.com/adafruit/Adafruit_CircuitPython_SharpMemoryDisplay)
supports all of these and I would expect this one also to do so.
## 6.1. Display characteristics
These displays have extremely low current consumption: I measured ~90μA on the
2.7" board when in use. Refresh is fast, visually excellent and can run at up
to 20Hz. This contrasts with ePaper (eInk) displays where refresh is slow
(seconds) and visually intrusive; an alternative fast mode overcomes this, but
at the expense of ghosting.
On the other hand the power consumption of ePaper can be zero (you can switch
them off and the display is retained). If you power down a Sharp display the
image is retained, but only for a few seconds. In a Pyboard context 90μA is low
in comparison to stop mode and battery powered applications should be easily
realised.
The 2.7" display has excellent resolution and can display fine lines and small
fonts. In other respects the display quality is not as good as ePaper. For good
contrast best results are achieved if the viewing angle and the direction of
the light source are positioned to achieve reflection.
### 6.1.1 The VCOM bit
The significance of this is somewhat glossed-over in the Adafruit docs, and a
study of the datasheet is confusing in the absence of prior knowledge of LCD
technology.
The signals applied to an LCD display should have no DC component. This is
because DC can cause gradual electrolysis and deterioration of of the liquid
crystal material. Display driver hardware typically has an oscillator driving
exclusive-or gates such that antiphase signals are applied for ON pixels, and
in-phase for OFF pixels. The oscillator typically drives a D-type flip-flop to
ensure an accurate 1:1 mark space ratio and hence zero DC component.
These displays offer two ways of achieving this, in the device driver or using
an external 1:1 mark space logic signal. The bit controlling this is known as
`VCOM` and the external pins supporting it are `EXTMODE` and `EXTCOMIN`.
`EXTMODE` determines whether a hardware input is used (`Vcc`) or software
control is required (`Gnd`). It is pulled low.
The driver supports software control, in that `VCOM` is complemented each time
the display is refreshed. The Adafruit driver also does this.
Sofware control implies that, in long running applications, the display should
regularly be refreshed. The datasheet incicates that the maximum rate is 20Hz,
but a 1Hz rate is sufficient.
If hardware control is to be used, `EXTMODE` should be linked to `Vcc` and a
1:1 logic signal applied to `EXTCOMIN`. A frequency range of 0.5-10Hz is
specified, and the datasheet also specifies "`EXTCOMIN` frequency should be
made lower than frame frequency".
In my opinion the easiest way to deal with this is usually to use software
control, ensuring that the driver's `show` method is called at regular
intervals of at least 1Hz.
### 6.1.2 Refresh rate
The datasheet specifies a minimum refresh rate of 1Hz.
## 6.2. Test scripts
1. `sharptest.py` Basic functionality test.
2. `clocktest.py` Digital and analog clock display.
3. `clock_batt.py` As above but designed for low power operation. Pyboard
specific.
Tests assume that `nanogui` is installed as per the instructions. `sharptest`
should not be run for long periods as it does not regularly refresh the
display. It tests `writer.py` and some `framebuffer` graphics primitives.
`clocktest` demostrates use with `nanogui`.
The `clock_batt.py` demo needs `upower.py` from
[micropython-micropower](https://github.com/peterhinch/micropython-micropower).
Testing was done on a Pyboard D SF6W: frozen bytecode was not required. I
suspect a Pyboard 1.x would require it to prevent memory errors. Fonts in
particular benefit from freezing as their RAM usage is radically reduced.
## 6.3. Device driver constructor
Positional args:
1. `spi` An SPI bus instance. The constructor initialises this to the baudrate
and bit order required by the hardware.
2. `pincs` A `Pin` instance. The caller should initialise this as an output
with value 0 (unusually the hardware CS line is active high).
3. `height=240` Dimensions in pixels. Defaults are for 2.7" display.
4. `width=400`
5. `vcom=False` Accept the default unless using `pyb.standby`. See 3.2.
### 6.3.1 Device driver methods
1. `show` No args. Transfers the framebuffer contents to the device, updating
the display.
2. `update` Toggles the `VCOM` bit without transferring the framebuffer. This
is a power saving method for cases where the application calls `show` at a
rate of < 1Hz. In such cases `update` should be called at a 1Hz rate.
### 6.3.2 The vcom arg
It purpose is to support micropower applications which use `pyb.standby`.
Wakeup from standby is similar to a reboot in that program execution starts
from scratch. In the case where the board wakes up, writes to the display, and
returns to standby, the `VCOM` bit would never change. In this case the
application should store a `bool` in peristent storage, toggling it on each
restart, and pass that to the constructor.
Persistent storage exists in the RTC registers and backup RAM. See
[micopython-micropower](https://github.com/peterhinch/micropython-micropower)
for details of how to acces these resources.
## 6.4. Application design
In all cases the frame buffer is located on the target hardware. In the case of
the 2.7 inch display this is 400*240//8 = 12000 bytes in size. This should be
instantiated as soon as possible in the application to ensure that sufficient
contiguous RAM is available.
### 6.4.1 Micropower applications
These comments largely assume a Pyboard host. The application should import
`upower` from
[micropython-micropower](https://github.com/peterhinch/micropython-micropower).
This turns the USB interface off if not in use to conserve power. It also
provides an `lpdelay` function to implement a delay using `pyb.stop()` to
conserve power.
In tests the `clock_batt` demo consumed 700μA between updates. A full refresh
every 30s consumed about 48mA for 128ms. These figures correspond to a mean
current consumption of 904μA implying about 46 days operation per AH of
battery capacity. LiPo cells of 2AH capacity are widely available offering a
theoretical runtime of 92 days between charges.
Lower currents might be achieved using standby but I have major doubts. This is
because it is necessary to toggle the VCOM bit at a minimum of 1Hz. Waking from
standby uses significan amounts of power as the modules are compiled. Even if
frozen bytecode is used, there is still significant power usage importing
modules and instantiating classes; this usage is not incurred in the loop in
the demo.
## 6.5. Resources
[Schematic for 2.7" unit](https://learn.adafruit.com/assets/94077)
[Datasheet 2.7"](https://cdn-learn.adafruit.com/assets/assets/000/094/215/original/LS027B7DH01_Rev_Jun_2010.pdf?1597872422)
[Datasheet 1.3"](http://www.adafruit.com/datasheets/LS013B4DN04-3V_FPC-204284.pdf)
###### [Contents](./DRIVERS.md#contents)
# 7. Writing device drivers
Device drivers capable of supporting `nanogui` can be extremely simple: see
`drivers/sharp/sharp.py` for a minimal example. It should be noted that the
supplied device drivers are designed purely to support nanogui. To conserve RAM
they provide no functionality beyond the transfer of an external frame buffer
to the device. This transfer typically takes a few tens of milliseconds. While
visually instant, this period constitutes latency between an event occurring
and a consequent display update. This may be unacceptable in applications such
as games. In such cases the `FrameBuffer` approach is inappropriate. Many
driver chips support graphics primitives in hardware; drivers using these
capabilities will be faster than those provided here and may often be found
using a forum search.
For a driver to support `nanogui` it must be subclassed from
`framebuf.FrameBuffer` and provide `height` and `width` bound variables being
the display size in pixels. This, and a `show` method, are all that is required
for monochrome drivers.
Refresh must be handled by a `show` method taking no arguments; when called,
the contents of the buffer underlying the `FrameBuffer` must be copied to the
hardware.
For color drivers, to conserve RAM it is suggested that 8-bit color is used
for the `FrameBuffer`. If the hardware does not support this, conversion to the
supported color space needs to be done "on the fly" as per the SSD1351 driver.
This uses `framebuf.GS8` to stand in for 8 bit color in `rrrgggbb` format. To
maximise update speed consider using native, viper or assembler for the
conversion, typically to RGB565 format.
Color drivers should have a static method converting rgb(255, 255, 255) to a
form acceptable to the driver. For 8-bit rrrgggbb this can be:
```python
@staticmethod
def rgb(r, g, b):
return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6)
```
This should be amended if the hardware uses a different 8-bit format.
The `Writer` (monochrome) or `CWriter` (color) classes and the `nanogui` module
should then work automatically.
Drivers for displays using I2C may need to use
[I2C.writevto](http://docs.micropython.org/en/latest/library/machine.I2C.html?highlight=writevto#machine.I2C.writevto)
depending on the chip requirements.
###### [Contents](./DRIVERS.md#contents)

285
README.md
Wyświetl plik

@ -2,9 +2,10 @@ A lightweight and minimal MicroPython GUI library for display drivers based on
the `FrameBuffer` class. Various display technologies are supported, including the `FrameBuffer` class. Various display technologies are supported, including
small color and monochrome OLED's and color TFT's. The GUI is cross-platform. small color and monochrome OLED's and color TFT's. The GUI is cross-platform.
These images, most from OLED displays, are poor. OLEDs are visually impressive These images, most from OLED displays, fail to reproduce the quality of these
displays with bright colors, wide viewing angle and extreme contrast. For some displays. OLEDs are visually impressive displays with bright colors, wide
reason I find them hard to photograph well. viewing angle and extreme contrast. For some reason I find them hard to
photograph.
![Image](images/clock.png) The aclock.py demo. ![Image](images/clock.png) The aclock.py demo.
![Image](images/fonts.png) Label objects in two fonts. ![Image](images/fonts.png) Label objects in two fonts.
@ -22,9 +23,9 @@ Cartesian and polar plots, also realtime plotting including time series.
![Image](images/sine.png) A sample image from the plot module. ![Image](images/sine.png) A sample image from the plot module.
The following images are from a different display but illustrate the widgets. These images are from a TFT display. They illustrate the widgets.
![Image](images/scale.JPG) The Scale widget. Capable of precision display of ![Image](images/scale.JPG) The Scale widget. Capable of precision display of
floats. floats as the scale moves behind its small window.
![Image](images/textbox1.JPG) The Textbox widget for scrolling text. ![Image](images/textbox1.JPG) The Textbox widget for scrolling text.
@ -34,7 +35,7 @@ wiring details, pin names and hardware issues.
# Contents # Contents
1. [Introduction](./README.md#1-introduction) 1. [Introduction](./README.md#1-introduction)
1.1 [Update](./README.md#11-update) 1.1 [Change log](./README.md#11-change-log)
1.2 [Description](./README.md#12-description) 1.2 [Description](./README.md#12-description)
1.3 [Quick start](./README.md#13-quick-start) 1.3 [Quick start](./README.md#13-quick-start)
2. [Files and Dependencies](./README.md#2-files-and-dependencies) 2. [Files and Dependencies](./README.md#2-files-and-dependencies)
@ -48,7 +49,7 @@ wiring details, pin names and hardware issues.
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;2.2.2 [Color use](./README.md#222-color-use) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;2.2.2 [Color use](./README.md#222-color-use)
3. [The nanogui module](./README.md#3-the-nanogui-module) 3. [The nanogui module](./README.md#3-the-nanogui-module)
3.1 [Application Initialisation](./README.md#31-application-initialisation) Initial setup and refresh method. 3.1 [Application Initialisation](./README.md#31-application-initialisation) Initial setup and refresh method.
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3.1.1 [Setup file internals](./README.md#311-setup-file-internals) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3.1.1 [User defined colors](./README.md#311-user-defined-colors)
3.2 [Label class](./README.md#32-label-class) Dynamic text at any screen location. 3.2 [Label class](./README.md#32-label-class) Dynamic text at any screen location.
3.3 [Meter class](./README.md#33-meter-class) A vertical panel meter. 3.3 [Meter class](./README.md#33-meter-class) A vertical panel meter.
3.4 [LED class](./README.md#34-led-class) Virtual LED of any color. 3.4 [LED class](./README.md#34-led-class) Virtual LED of any color.
@ -56,55 +57,59 @@ wiring details, pin names and hardware issues.
or compass style display of one or more pointers. or compass style display of one or more pointers.
3.6 [Scale class](./README.md#36-scale-class) Linear display with wide dynamic range. 3.6 [Scale class](./README.md#36-scale-class) Linear display with wide dynamic range.
3.7 [Class Textbox](./README.md#37-class-textbox) Scrolling text display. 3.7 [Class Textbox](./README.md#37-class-textbox) Scrolling text display.
4. [Device drivers](./README.md#4-device-drivers) Device driver compatibility 4. [ESP8266](./README.md#4-esp8266) This can work. Contains information on
requirements (these are minimal).
5. [ESP8266](./README.md#5-esp8266) This can work. Contains information on
minimising the RAM and flash footprints of the GUI. minimising the RAM and flash footprints of the GUI.
5. [Hardware configuration](./README.md#5-hardware-configuration) How to write
color_setup.py
# 1. Introduction # 1. Introduction
This library provides a limited set of GUI objects (widgets) for displays whose This library provides a limited set of GUI objects (widgets) for displays whose
display driver is subclassed from the `FrameBuffer` class. Such drivers can be display driver is subclassed from the `FrameBuffer` class. Such drivers can be
tiny as the graphics primitives are supplied by the `FrameBuffer` class. tiny as the graphics primitives are supplied by the `FrameBuffer` class. A
range of device drivers is provided: [this doc](./DRIVERS.md) provides guidance
The GUI is display-only and lacks provision for user input. Displays with touch on selecting the right driver for your display, platform and application.
overlays are physically large, with correspondingly high pixel counts. Such
displays would require large frame buffers. These would consume RAM and be slow
to copy to the display. A `FrameBuffer` based driver is ill-suited to large
displays. Drivers should use graphics primitives hosted on the display
controller chip.
The GUI is cross-platform. By default it is configured for a Pyboard (1.x or D). The GUI is cross-platform. By default it is configured for a Pyboard (1.x or D).
This doc explains how to configure for other platforms by adapting a single This doc explains how to configure for other platforms by adapting a single
small file. The GUI supports multiple displays attached to a single target, but small file. The GUI supports multiple displays attached to a single target, but
bear in mind the RAM requirements for multiple frame buffers. It is tested on bear in mind the RAM requirements for multiple frame buffers. It is tested on
the ESP32 reference board without SPIRAM. Running on ESP8266 is possible but the ESP32 reference board without SPIRAM. Running on ESP8266 is possible but
frozen bytecode should be used owing to its restricted RAM. frozen bytecode must be used owing to its restricted RAM.
Authors of applications requiring touch should consider my touch GUI's for the It uses synchronous code but is compatible with `uasyncio`. Some demo programs
following displays. These have internal buffers: illustrate this. Code is standard MicroPython, but some device drivers use the
`native` and `viper` decorators.
The GUI is display-only and lacks provision for user input. Authors of
applications requiring touch should consider the touch GUI's for the following
displays:
* [Official lcd160cr](https://github.com/peterhinch/micropython-lcd160cr-gui) * [Official lcd160cr](https://github.com/peterhinch/micropython-lcd160cr-gui)
* [RA8875 large displays](https://github.com/peterhinch/micropython_ra8875) * [RA8875 large displays](https://github.com/peterhinch/micropython_ra8875)
* [SSD1963 large displays](https://github.com/peterhinch/micropython-tft-gui) * [SSD1963 large displays](https://github.com/peterhinch/micropython-tft-gui)
## 1.1 Update For historical reasons and to ensure consistency, code and documentation for
my GUI's employ the American spelling of `color`.
29 Nov 2020 Add ST7735R TFT drivers. ## 1.1 Change log
15 Dec 2020 Add ILI9341 driver, 4-bit drivers and SPI bus sharing improvements.
29 Nov 2020 Add ST7735R TFT drivers.
17 Nov 2020 Add `Textbox` widget. `Scale` constructor arg `border` replaced by 17 Nov 2020 Add `Textbox` widget. `Scale` constructor arg `border` replaced by
`bdcolor` as per other widgets. `bdcolor` as per other widgets.
5 Nov 2020 5 Nov 2020 - breaking change
This library has been refactored as a Python package. The aim is to reduce RAM This library has been refactored as a Python package. This reduces RAM usage:
usage: widgets are imported on demand rather than unconditionally. This enabled widgets are imported on demand rather than unconditionally. This has enabled
the addition of new widgets with zero impact on existsing applications. Another the addition of new widgets with zero impact on existsing applications. Another
aim was to simplify installation with dependencies such as `writer` included in aim was to simplify installation with dependencies such as `writer` included in
the tree. Finally hardware configuration is contained in a single file: details the tree. Finally hardware configuration is contained in a single script: only
only need to be edited in one place to run all demo scripts. this file needs to be customised to run all demo scripts or to port an
application to different hardware.
Existing users should re-install from scratch. In existing applications, import Users of versions prior to this refactor should re-install from scratch. In
statements will need to be adapted as per the demos. The GUI API is otherwise existing applications, import statements will need to be adapted as per the
unchanged. demos. The GUI API is otherwise unchanged.
## 1.2 Description ## 1.2 Description
@ -113,16 +118,18 @@ Compatible and tested display drivers include:
* The official [SSD1306 driver](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py). * The official [SSD1306 driver](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py).
* The [PCD8544/Nokia 5110](https://github.com/mcauser/micropython-pcd8544.git). * The [PCD8544/Nokia 5110](https://github.com/mcauser/micropython-pcd8544.git).
* The [Adafruit 0.96 inch color OLED](https://www.adafruit.com/product/684) * The [Adafruit 0.96 inch color OLED](https://www.adafruit.com/product/684)
with [this driver](https://github.com/peterhinch/micropython-nano-gui/tree/master/drivers/ssd1331). with [this driver](./DRIVERS.md#3-drivers-for-ssd1331).
* A driver for [Adafruit 1.5 inch OLED](https://www.adafruit.com/product/1431) * A driver for [Adafruit 1.5 inch OLED](https://www.adafruit.com/product/1431)
and [Adafruit 1.27 inch OLED](https://www.adafruit.com/product/1673) may be and [Adafruit 1.27 inch OLED](https://www.adafruit.com/product/1673) is
found [here](./drivers/ssd1351/README.md). documented [here](./DRIVERS.md#2-drivers-for-ssd1351).
* A driver for Sharp ultra low power consumption monochrome displays such as * A driver for Sharp ultra low power consumption monochrome displays such as
[2.7 inch 400x240 pixels](https://www.adafruit.com/product/4694) [2.7 inch 400x240 pixels](https://www.adafruit.com/product/4694)
is [here](./drivers/sharp/README.md). is documented [here](./DRIVERS.md#6-drivers-for-sharp-displays).
* Drivers for Adafruit ST7735R based TFT's: * Drivers for Adafruit ST7735R based TFT's:
[1.8 inch](https://www.adafruit.com/product/358) and [1.8 inch](https://www.adafruit.com/product/358) and
[1.44 inch](https://www.adafruit.com/product/2088). [1.44 inch](https://www.adafruit.com/product/2088) documented [here](./DRIVERS.md#4-drivers-for-st7735r).
* Drivers for ILI9341 such as [Adafruit 3.2 inch](https://www.adafruit.com/product/1743)
documented [here](./DRIVERS.md#5-drivers-for-ili9341).
Widgets are intended for the display of data from physical devices such as Widgets are intended for the display of data from physical devices such as
sensors. They are drawn using graphics primitives rather than icons to minimise sensors. They are drawn using graphics primitives rather than icons to minimise
@ -131,11 +138,6 @@ by hosts with restricted processing power. The approach also enables widgets to
maximise information in ways that are difficult with icons, in particular using maximise information in ways that are difficult with icons, in particular using
dynamic color changes in conjunction with moving elements. dynamic color changes in conjunction with moving elements.
Owing to RAM requirements and limitations on communication speed, `FrameBuffer`
based display drivers are intended for physically small displays with limited
numbers of pixels. The widgets are designed for displays as small as 0.96
inches: this involves some compromises.
Copying the contents of the frame buffer to the display is relatively slow. The Copying the contents of the frame buffer to the display is relatively slow. The
time depends on the size of the frame buffer and the interface speed, but the time depends on the size of the frame buffer and the interface speed, but the
latency may be too high for applications such as games. For example the time to latency may be too high for applications such as games. For example the time to
@ -165,22 +167,21 @@ OLED as per `color_setup.py`, move to the root directory of the repo and run
Note also that the `gui.demos.aclock.py` demo comprises 38 lines of actual Note also that the `gui.demos.aclock.py` demo comprises 38 lines of actual
code. This stuff is easier than you might think. code. This stuff is easier than you might think.
###### [Contents](./README.md#contents)
# 2. Files and Dependencies # 2. Files and Dependencies
Firmware should be V1.13 or later. Firmware should be V1.13 or later.
Installation comprises copying the `gui` and `drivers` directories, with their Installation comprises copying the `gui` and `drivers` directories, with their
contents, plus a hardware configuration file, to the target. The directory contents, plus a hardware configuration file, to the target. The directory
structure on the target must match that in the repo. structure on the target must match that in the repo. This consumes about 300KiB
of flash.
In the interests of conserving RAM, supplied drivers support only the
functionality required by the GUI. More fully featured drivers may better suit
other applications. See [section 4](./README.md#4-device-drivers).
Filesystem space may be conserved by copying only the required driver from Filesystem space may be conserved by copying only the required driver from
`drivers`, but the directory path to that file must be retained. For example, `drivers`, but the directory path to that file must be retained. For example,
for SSD1351 displays only the following are actually required: for SSD1351 displays only the following are actually required:
`drivers/ssd1351/ssd1351.py`, `drivers/ssd1351/__init__.py` `drivers/ssd1351/ssd1351.py`, `drivers/ssd1351/__init__.py`.
## 2.1 Files ## 2.1 Files
@ -189,7 +190,7 @@ for SSD1351 displays only the following are actually required:
The root directory contains two example setup files, for monochrome and color The root directory contains two example setup files, for monochrome and color
displays respectively. Other examples may be found in the `color_setup` displays respectively. Other examples may be found in the `color_setup`
directory. These are templates for adaptation: only one file is copied to the directory. These are templates for adaptation: only one file is copied to the
target. On the target a color files should be named `color_setup.py`. The target. On the target a color file should be named `color_setup.py`. The
monochrome `ssd1306_setup.py` retains its own name. monochrome `ssd1306_setup.py` retains its own name.
The chosen template will need to be edited to match the display in use, the The chosen template will need to be edited to match the display in use, the
@ -237,8 +238,8 @@ which occurs during transfer of the framebuffer to the display may affect more
demanding `uasyncio` applications. More generally the GUI works well with it. demanding `uasyncio` applications. More generally the GUI works well with it.
Demo scripts for Sharp displays are in `drivers/sharp`. Check source code for Demo scripts for Sharp displays are in `drivers/sharp`. Check source code for
wiring details. See [the README](./drivers/sharp/README.md). They may be run as wiring details. See [the docs](./DRIVERS.md#6-drivers-for-sharp-displays). They
follows: may be run as follows:
```python ```python
import drivers.sharp.sharptest import drivers.sharp.sharptest
# or # or
@ -249,7 +250,11 @@ import drivers.sharp.clocktest
Python font files are in the `gui/fonts` directory. The easiest way to conserve Python font files are in the `gui/fonts` directory. The easiest way to conserve
RAM is to freeze them which is highly recommended. In doing so the directory RAM is to freeze them which is highly recommended. In doing so the directory
structure must be maintained. Python fonts may be created using structure must be maintained: the [ESP8266](./README.md#4-esp8266) provides an
illustration.
To create alternatives, Python fonts may be generated from industry standard
font files with
[font_to_py.py](https://github.com/peterhinch/micropython-font-to-py.git). The [font_to_py.py](https://github.com/peterhinch/micropython-font-to-py.git). The
`-x` option for horizontal mapping must be specified. If fixed pitch rendering `-x` option for horizontal mapping must be specified. If fixed pitch rendering
is required `-f` is also required. Supplied examples are: is required `-f` is also required. Supplied examples are:
@ -271,20 +276,21 @@ copied to the hardware root as `color_setup.py`.
* `esp32_setup.py` As written supports an ESP32 connected to a 128x128 SSD1351 * `esp32_setup.py` As written supports an ESP32 connected to a 128x128 SSD1351
display. After editing to match the display and wiring, it should be copied to display. After editing to match the display and wiring, it should be copied to
the target as `/pyboard/color_setup.py`. the target as `/pyboard/color_setup.py`.
* `esp8266_setup.py` Similar for [ESP8266](./README.md#5-esp8266). Usage is * `esp8266_setup.py` Similar for [ESP8266](./README.md#4-esp8266). Usage is
somewhat experimental. somewhat experimental.
* `st7735r_setup.py` Assumes a Pyboard with an * `st7735r_setup.py` Assumes a Pyboard with an
[Adafruit 1.8 inch TFT display](https://www.adafruit.com/product/358). [Adafruit 1.8 inch TFT display](https://www.adafruit.com/product/358).
* `st7735r144_setup.py` For a Pyboard with an * `st7735r144_setup.py` For a Pyboard with an
[Adafruit 1.44 inch TFT display](https://www.adafruit.com/product/2088). [Adafruit 1.44 inch TFT display](https://www.adafruit.com/product/2088).
* `ili9341_setup.py` A 240*320 ILI9341 display on ESP32.
## 2.2 Dependencies ## 2.2 Dependencies
The source tree now includes all dependencies. These are listed to enable users The source tree now includes all dependencies. These are listed to enable users
to check for newer versions. to check for newer versions:
* [writer.py](https://github.com/peterhinch/micropython-font-to-py/blob/master/writer/writer.py) * [writer.py](https://github.com/peterhinch/micropython-font-to-py/blob/master/writer/writer.py)
Provides text rendering. Provides text rendering of Python font files.
Optional feature: Optional feature:
* An STM32 implementation of * An STM32 implementation of
@ -301,46 +307,21 @@ in this repo but may be found here:
* [PCD8544/Nokia 5110](https://github.com/mcauser/micropython-pcd8544.git) * [PCD8544/Nokia 5110](https://github.com/mcauser/micropython-pcd8544.git)
The Sharp display is supported in `drivers/sharp`. See The Sharp display is supported in `drivers/sharp`. See
[README](/drivers/sharp/README.md) and demos. [README](./DRIVERS.md#6-drivers-for-sharp-displays) and demos.
### 2.2.2 Color use ### 2.2.2 Color use
Drivers for Adafruit 0.96", 1.27" and 1.5" OLEDS are included in the source This script performs a basic check that the `color_setup.py` file matches the
tree. Each driver has its own small `README.md`. The default driver for the hardware, that all three primary colors can be displayed and that pixels up to
larger OLEDs is Pyboard specific, but there are slightly slower cross platform the edges of the display can be accessed.
alternatives in the directory - see the code below for usage on ESP32.
If using the Adafruit 1.5 or 1.27 inch color OLED displays it is suggested that
after installing the GUI the following script is pasted at the REPL. This will
verify the hardware. Please change `height` to 128 if using the 1.5 inch
display.
```python ```python
from machine import Pin, SPI from color_setup import ssd # Create a display instance
from drivers.ssd1351.ssd1351 import SSD1351 as SSD # Pyboard-specific driver from gui.core.nanogui import refresh
height = 96 # Ensure height is correct (96/128) refresh(ssd, True) # Initialise and clear display.
pdc = Pin('Y1', Pin.OUT_PP, value=0)
pcs = Pin('Y2', Pin.OUT_PP, value=1)
prst = Pin('Y3', Pin.OUT_PP, value=1)
spi = SPI(2)
ssd = SSD(spi, pcs, pdc, prst, height=height)
ssd.fill(0) ssd.fill(0)
ssd.line(0, 0, 127, height - 1, ssd.rgb(0, 255, 0)) # Green diagonal corner-to-corner ssd.line(0, 0, ssd.width - 1, ssd.height - 1, ssd.rgb(0, 255, 0)) # Green diagonal corner-to-corner
ssd.rect(0, 0, 15, 15, ssd.rgb(255, 0, 0)) # Red square at top left
ssd.show()
```
On ESP32 the following may be used:
```python
from machine import Pin, SPI
from drivers.ssd1351.ssd1351_generic import SSD1351 as SSD # Note generic driver
height = 128 # Ensure height is correct (96/128)
pdc = Pin(25, Pin.OUT, value=0)
pcs = Pin(26, Pin.OUT, value=1)
prst = Pin(27, Pin.OUT, value=1)
spi = SPI(1, 10_000_000, sck=Pin(14), mosi=Pin(13), miso=Pin(12))
ssd = SSD(spi, pcs, pdc, prst, height=height)
ssd.fill(0)
ssd.line(0, 0, 127, height - 1, ssd.rgb(0, 255, 0)) # Green diagonal corner-to-corner
ssd.rect(0, 0, 15, 15, ssd.rgb(255, 0, 0)) # Red square at top left ssd.rect(0, 0, 15, 15, ssd.rgb(255, 0, 0)) # Red square at top left
ssd.rect(ssd.width -15, ssd.height -15, 15, 15, ssd.rgb(0, 0, 255)) # Blue square at bottm right
ssd.show() ssd.show()
``` ```
@ -367,21 +348,19 @@ Text components of widgets are rendered using the `Writer` (monochrome) or
The GUI is initialised for color display by issuing: The GUI is initialised for color display by issuing:
```python ```python
from color_setup import ssd, height from color_setup import ssd
``` ```
This works as described [below](./README.md#311-setup-file-internals). This defines the hardware as described in [Hardware configuration](./README.md#5-hardware-configuration).
A typical application then imports `nanogui` modules and clears the display: A typical application then imports `nanogui` modules and clears the display:
```python ```python
from gui.core.nanogui import refresh from gui.core.nanogui import refresh
from gui.widgets.label import Label # Import any widgets you plan to use from gui.widgets.label import Label # Import any widgets you plan to use
from gui.widgets.dial import Dial, Pointer from gui.widgets.dial import Dial, Pointer
refresh(ssd, True) # Initialise and clear display.
refresh(ssd) # Initialise and clear display.
``` ```
This is followed by Python fonts. A `CWriter` instance is created for each Initialisation of text display follows. For each font a `CWriter` instance
font (for monochrome displays a `Writer` is used). Upside down rendering is not is created (for monochrome displays a `Writer` is used):
supported. Only the `Textbox` widget supports scrolling text.
```python ```python
from gui.core.writer import CWriter # Renders color text from gui.core.writer import CWriter # Renders color text
import gui.fonts.arial10 # A Python Font import gui.fonts.arial10 # A Python Font
@ -392,35 +371,39 @@ CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it
wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) # Colors are defaults wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) # Colors are defaults
wri.set_clip(True, True, False) wri.set_clip(True, True, False)
``` ```
The application calls `nanogui.refresh` on initialisation to clear the display, Calling `nanogui.refresh` on startup sets up and clears the display. The method
then subsequently whenever a refresh is required. The method takes two args: will subsequently be called whenever a refresh is required. It takes two args:
1. `device` The display instance (the GUI supports multiple displays). 1. `device` The display instance (the GUI supports multiple displays).
2. `clear=False` If set `True` the display will be blanked; it is also 2. `clear=False` If set `True` the display will be blanked; it is also
blanked when a device is refreshed for the first time. blanked when a device is refreshed for the first time.
### 3.1.1 Setup file internals ### 3.1.1 User defined colors
The file `color_setup.py` contains the hardware dependent code. It works as The file `gui/core/colors.py` defines standard color constants which may be
described below, with the aim of allocating the `FrameBuffer` before importing used with any display driver. This section describes how to change these or
other modules. This is intended to reduce the risk of memory failures. to create additional colors.
Firstly the file sets the display height and imports the driver: Most of the color display drivers define colors as 8-bit or larger values.
In such cases colors may be created and assigned to variables as follows:
```python ```python
height = 96 # 1.27 inch 96*128 (rows*cols) display. Set to 128 for 1.5 inch from color_setup import SSD
import machine PALE_YELLOW = SSD.rgb(150, 150, 0)
import gc
from drivers.ssd1351.ssd1351 import SSD1351 as SSD # Import the display driver
``` ```
It then sets up the bus (SPI or I2C) and instantiates the display. At this The GUI also provides drivers with 4-bit color to minimise RAM use. Colors are
point the framebuffer is created: assigned to a lookup table having 16 entries. The frame buffer stores 4-bit
color values, which are converted to the correct color depth for the hardware
when the display is refreshed.
Of the possible 16 colors 13 are assigned in `gui/core/colors.py`, leaving
color numbers 12, 13 and 14 free. Any color can be assigned as follows:
```python ```python
pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) from gui.core.colors import * # Imports the create_color function
pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) PALE_YELLOW = create_color(12, 150, 150, 0)
prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1)
spi = machine.SPI(1)
gc.collect() # Precaution before instantiating framebuf
ssd = SSD(spi, pcs, pdc, prst, height) # Create a display instance
``` ```
This creates a color `rgb(150, 150, 0)` assigns it to "spare" color number 12
then sets `PALE_YELLOW` to 12. Any color number in range `0 <= n <= 15` may be
used (implying that predefined colors may be reassigned). It is recommended
that `BLACK` (0) and `WHITE` (15) are not changed.
###### [Contents](./README.md#contents) ###### [Contents](./README.md#contents)
@ -436,7 +419,8 @@ Labels can be displayed with an optional single pixel border.
Colors are handled flexibly. By default the colors used are those of the Colors are handled flexibly. By default the colors used are those of the
`Writer` instance, however they can be changed dynamically; this might be used `Writer` instance, however they can be changed dynamically; this might be used
to warn of overrange or underrange values. to warn of overrange or underrange values. The `color15.py` demo illustrates
this.
Constructor args: Constructor args:
1. `writer` The `Writer` instance (font and screen) to use. 1. `writer` The `Writer` instance (font and screen) to use.
@ -803,55 +787,10 @@ the oldest (topmost) being discarded as required.
###### [Contents](./README.md#contents) ###### [Contents](./README.md#contents)
# 4. Device drivers
Device drivers capable of supporting `nanogui` can be extremely simple: see
`drivers/sharp/sharp.py` for a minimal example. It should be noted that the
supplied device drivers are designed purely to support nanogui. To conserve RAM
they provide no functionality beyond the transfer of an external frame buffer
to the device. This transfer typically takes a few tens of milliseconds. While
visually instant, this period constitutes latency between an event occurring
and a consequent display update. This may be unacceptable in applications such
as games. In such cases the `FrameBuffer` approach is inappropriate. Many
driver chips support graphics primitives in hardware; drivers using these
capabilities will be faster than those provided here and may often be found
using a forum search.
For a driver to support `nanogui` it must be subclassed from
`framebuf.FrameBuffer` and provide `height` and `width` bound variables being
the display size in pixels. This, and a `show` method, are all that is required
for monochrome drivers.
Refresh must be handled by a `show` method taking no arguments; when called,
the contents of the buffer underlying the `FrameBuffer` must be copied to the
hardware.
For color drivers, to conserve RAM it is suggested that 8-bit color is used
for the `FrameBuffer`. If the hardware does not support this, conversion to the
supported color space needs to be done "on the fly" as per the SSD1351 driver.
This uses `framebuf.GS8` to stand in for 8 bit color in `rrrgggbb` format. To
maximise update speed consider using native, viper or assembler for the
conversion, typically to RGB565 format.
Color drivers should have a static method converting rgb(255, 255, 255) to a
form acceptable to the driver. For 8-bit rrrgggbb this can be:
```python
@staticmethod
def rgb(r, g, b):
return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6)
```
This should be amended if the hardware uses a different 8-bit format.
The `Writer` (monochrome) or `CWriter` (color) classes and the `nanogui` module
should then work automatically.
Drivers for displays using I2C may need to use
[I2C.writevto](http://docs.micropython.org/en/latest/library/machine.I2C.html?highlight=writevto#machine.I2C.writevto)
depending on the chip requirements.
###### [Contents](./README.md#contents) ###### [Contents](./README.md#contents)
# 5. ESP8266 # 4. ESP8266
Some personal observations on successful use with an ESP8266. Some personal observations on successful use with an ESP8266.
@ -890,3 +829,27 @@ In conclusion I think that applications of moderate complexity should be
feasible. feasible.
###### [Contents](./README.md#contents) ###### [Contents](./README.md#contents)
# 5. Hardware configuration
The file `color_setup.py` contains the hardware dependent code. It works as
described below, with the aim of allocating the `FrameBuffer` before importing
other modules. This is intended to reduce the risk of memory failures.
Firstly the file sets the display height and imports the driver:
```python
height = 96 # 1.27 inch 96*128 (rows*cols) display. Set to 128 for 1.5 inch
import machine
import gc
from drivers.ssd1351.ssd1351 import SSD1351 as SSD # Import the display driver
```
It then sets up the bus (SPI or I2C) and instantiates the display. At this
point the framebuffer is created:
```python
pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0)
pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1)
prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1)
spi = machine.SPI(1)
gc.collect() # Precaution before instantiating framebuf
ssd = SSD(spi, pcs, pdc, prst, height) # Create a display instance
```

Wyświetl plik

@ -9,7 +9,7 @@
# Edit the driver import for other displays. # Edit the driver import for other displays.
# WIRING (Adafruit pin nos and names). # WIRING (Adafruit pin nos and names).
# Pyb SSD # ESP SSD
# 3v3 Vin (10) # 3v3 Vin (10)
# Gnd Gnd (11) # Gnd Gnd (11)
# IO25 DC (3 DC) # IO25 DC (3 DC)
@ -28,6 +28,6 @@ pdc = Pin(25, Pin.OUT, value=0) # Arbitrary pins
pcs = Pin(26, Pin.OUT, value=1) pcs = Pin(26, Pin.OUT, value=1)
prst = Pin(27, Pin.OUT, value=1) prst = Pin(27, Pin.OUT, value=1)
# Hardware SPI on native pins for performance # Hardware SPI on native pins for performance
spi = SPI(1, 10_000_000, sck=Pin(14), mosi=Pin(13), miso=Pin(12)) spi = SPI(1, 10_000_000, sck=Pin(14), mosi=Pin(13), miso=Pin(12), init_spi=False)
gc.collect() gc.collect()
ssd = SSD(spi, pcs, pdc, prst, height=height) ssd = SSD(spi, pcs, pdc, prst, height=height)

Wyświetl plik

@ -0,0 +1,38 @@
# color_setup.py Customise for your hardware config
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2020 Peter Hinch
# As written, supports:
# ili9341 240x320 displays on ESP32
# Edit the driver import for other displays.
# Demo of initialisation procedure designed to minimise risk of memory fail
# when instantiating the frame buffer. The aim is to do this as early as
# possible before importing other modules.
# WIRING
# ESP SSD
# 3v3 Vin
# Gnd Gnd
# IO25 DC
# IO26 CS
# IO27 Rst
# IO14 CLK Hardware SPI1
# IO13 DATA (AKA SI MOSI)
from machine import Pin, SPI
import gc
# *** Choose your color display driver here ***
# ili9341 specific driver
from drivers.ili93xx.ili9341 import ILI9341 as SSD
pdc = Pin(25, Pin.OUT, value=0) # Arbitrary pins
pcs = Pin(26, Pin.OUT, value=1)
prst = Pin(27, Pin.OUT, value=1)
# Kept as ssd to maintain compatability
gc.collect() # Precaution before instantiating framebuf
spi = SPI(1, 10_000_000, sck=Pin(14), mosi=Pin(13), miso=Pin(12))
ssd = SSD(spi, dc=pdc, cs=pcs, rst=prst)

Wyświetl plik

@ -0,0 +1,150 @@
# ILI9341 nano-gui driver for ili9341 displays
# As with all nano-gui displays, touch is not supported.
# Copyright (c) Peter Hinch 2020
# Released under the MIT license see LICENSE
# This work is based on the following sources.
# https://github.com/rdagger/micropython-ili9341
# Also this forum thread with ideas from @minyiky:
# https://forum.micropython.org/viewtopic.php?f=18&t=9368
from time import sleep_ms
import gc
import framebuf
@micropython.viper
def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int):
n = 0
for x in range(length):
c = source[x]
d = (c & 0xf0) >> 3 # 2* pointers (lut is 16 bit color)
e = (c & 0x0f) << 1
dest[n] = lut[d]
n += 1
dest[n] = lut[d + 1]
n += 1
dest[n] = lut[e]
n += 1
dest[n] = lut[e + 1]
n += 1
class ILI9341(framebuf.FrameBuffer):
lut = bytearray(32)
@staticmethod
def rgb(r, g, b):
return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3
# Transpose width & height for landscape mode
def __init__(self, spi, cs, dc, rst, height=240, width=320,
usd=False, split=False, init_spi=False):
self._spi = spi
self._cs = cs
self._dc = dc
self._rst = rst
self.height = height
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._lines = 0 if not split else height // split # For uasyncio use: show n lines only
self._line = 0 # Current line
mode = framebuf.GS4_HMSB
gc.collect()
buf = bytearray(self.height * self.width // 2)
self._mvb = memoryview(buf)
super().__init__(buf, self.width, self.height, mode)
self._linebuf = bytearray(self.width * 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
# Send initialization commands
self._wcmd(b'\x01') # SWRESET Software reset
sleep_ms(100)
self._wcd(b'\xcf', b'\x00\xC1\x30') # PWCTRB Pwr ctrl B
self._wcd(b'\xed', b'\x64\x03\x12\x81') # POSC Pwr on seq. ctrl
self._wcd(b'\xe8', b'\x85\x00\x78') # DTCA Driver timing ctrl A
self._wcd(b'\xcb', b'\x39\x2C\x00\x34\x02') # PWCTRA Pwr ctrl A
self._wcd(b'\xf7', b'\x20') # PUMPRC Pump ratio control
self._wcd(b'\xea', b'\x00\x00') # DTCB Driver timing ctrl B
self._wcd(b'\xc0', b'\x23') # PWCTR1 Pwr ctrl 1
self._wcd(b'\xc1', b'\x10') # PWCTR2 Pwr ctrl 2
self._wcd(b'\xc5', b'\x3E\x28') # VMCTR1 VCOM ctrl 1
self._wcd(b'\xc7', b'\x86') # VMCTR2 VCOM ctrl 2
# (b'\x88', b'\xe8', b'\x48', b'\x28')[rotation // 90]
if self.height > self.width:
self._wcd(b'\x36', b'\x48' if usd else b'\x88') # MADCTL: RGB portrait mode
else:
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'\x3a', b'\x55') # PIXFMT COLMOD: Pixel format
self._wcd(b'\xb1', b'\x00\x18') # FRMCTR1 Frame rate ctrl
self._wcd(b'\xb6', b'\x08\x82\x27') # DFUNCTR
self._wcd(b'\xf2', b'\x00') # ENABLE3G Enable 3 gamma ctrl
self._wcd(b'\x26', b'\x01') # GAMMASET Gamma curve selected
self._wcd(b'\xe0', b'\x0F\x31\x2B\x0C\x0E\x08\x4E\xF1\x37\x07\x10\x03\x0E\x09\x00') # GMCTRP1
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
sleep_ms(100)
self._wcmd(b'\x29') # DISPLAY_ON Display on
sleep_ms(100)
# Write a command.
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, 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)
# No. of lines buffered vs time. Tested in portrait mode 240 pixels/line.
# ESP32 at stock freq.
# 24 lines 171ms
# 2 lines 180ms
# 1 line 196ms
@micropython.native
def show(self):
clut = ILI9341.lut
wd = self.width // 2
ht = self.height
lb = self._linebuf
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
self._spi_init(self._spi) # Bus may be shared
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)
self._cs(0)
if self._lines:
end = self._line + wd * self._lines
for start in range(self._line, end, wd): # For each line
_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)

Wyświetl plik

@ -1,157 +0,0 @@
# A MICROPYTHON DRIVER FOR SHARP DISPLAYS
These monochrome SPI displays exist in three variants from Adafruit.
1. [2.7 inch 400x240 pixels](https://www.adafruit.com/product/4694)
2. [1.3 inch 144x168](https://www.adafruit.com/product/3502)
3. [1.3 inch 96x96](https://www.adafruit.com/product/1393) - Discontinued.
I have tested on the first of these. However the
[Adfruit driver](https://github.com/adafruit/Adafruit_CircuitPython_SharpMemoryDisplay)
supports all of these and I would expect this one also to do so.
# 1. Display characteristics
These displays have extremely low current consumption: I measured ~90μA on the
2.7" board when in use. Refresh is fast, visually excellent and can run at up
to 20Hz. This contrasts with ePaper (eInk) displays where refresh is slow
(seconds) and visually intrusive; an alternative fast mode overcomes this, but
at the expense of ghosting.
On the other hand the power consumption of ePaper can be zero (you can switch
them off and the display is retained). If you power down a Sharp display the
image is retained, but only for a few seconds. In a Pyboard context 90μA is low
in comparison to stop mode and battery powered applications should be easily
realised.
The 2.7" display has excellent resolution and can display fine lines and small
fonts. In other respects the display quality is not as good as ePaper. For good
contrast best results are achieved if the viewing angle and the direction of
the light source are positioned to achieve reflection.
## 1.1 The VCOM bit
The significance of this is somewhat glossed-over in the Adafruit docs, and a
study of the datasheet is confusing in the absence of prior knowledge of LCD
technology.
The signals applied to an LCD display should have no DC component. This is
because DC can cause gradual electrolysis and deterioration of of the liquid
crystal material. Display driver hardware typically has an oscillator driving
exclusive-or gates such that antiphase signals are applied for ON pixels, and
in-phase for OFF pixels. The oscillator typically drives a D-type flip-flop to
ensure an accurate 1:1 mark space ratio and hence zero DC component.
These displays offer two ways of achieving this, in the device driver or using
an external 1:1 mark space logic signal. The bit controlling this is known as
`VCOM` and the external pins supporting it are `EXTMODE` and `EXTCOMIN`.
`EXTMODE` determines whether a hardware input is used (`Vcc`) or software
control is required (`Gnd`). It is pulled low.
The driver supports software control, in that `VCOM` is complemented each time
the display is refreshed. The Adafruit driver also does this.
Sofware control implies that, in long running applications, the display should
regularly be refreshed. The datasheet incicates that the maximum rate is 20Hz,
but a 1Hz rate is sufficient.
If hardware control is to be used, `EXTMODE` should be linked to `Vcc` and a
1:1 logic signal applied to `EXTCOMIN`. A frequency range of 0.5-10Hz is
specified, and the datasheet also specifies "`EXTCOMIN` frequency should be
made lower than frame frequency".
In my opinion the easiest way to deal with this is usually to use software
control, ensuring that the driver's `show` method is called at regular
intervals of at least 1Hz.
## 1.2 Refresh rate
The datasheet specifies a minimum refresh rate of 1Hz.
# 2. Test scripts
1. `sharptest.py` Basic functionality test.
2. `clocktest.py` Digital and analog clock display.
3. `clock_batt.py` As above but designed for low power operation. Pyboard
specific.
Tests assume that `nanogui` is installed as per the instructions. `sharptest`
should not be run for long periods as it does not regularly refresh the
display. It tests `writer.py` and some `framebuffer` graphics primitives.
`clocktest` demostrates use with `nanogui`.
The `clock_batt.py` demo needs `upower.py` from
[micropython-micropower](https://github.com/peterhinch/micropython-micropower).
Testing was done on a Pyboard D SF6W: frozen bytecode was not required. I
suspect a Pyboard 1.x would require it to prevent memory errors. Fonts in
particular benefit from freezing as their RAM usage is radically reduced.
# 3. Device driver constructor
Positional args:
1. `spi` An SPI bus instance. The constructor initialises this to the baudrate
and bit order required by the hardware.
2. `pincs` A `Pin` instance. The caller should initialise this as an output
with value 0 (unusually the hardware CS line is active high).
3. `height=240` Dimensions in pixels. Defaults are for 2.7" display.
4. `width=400`
5. `vcom=False` Accept the default unless using `pyb.standby`. See 3.2.
# 3.1 Device driver methods
1. `show` No args. Transfers the framebuffer contents to the device, updating
the display.
2. `update` Toggles the `VCOM` bit without transferring the framebuffer. This
is a power saving method for cases where the application calls `show` at a
rate of < 1Hz. In such cases `update` should be called at a 1Hz rate.
# 3.2 The vcom arg
It purpose is to support micropower applications which use `pyb.standby`.
Wakeup from standby is similar to a reboot in that program execution starts
from scratch. In the case where the board wakes up, writes to the display, and
returns to standby, the `VCOM` bit would never change. In this case the
application should store a `bool` in peristent storage, toggling it on each
restart, and pass that to the constructor.
Persistent storage exists in the RTC registers and backup RAM. See
[micopython-micropower](https://github.com/peterhinch/micropython-micropower)
for details of how to acces these resources.
# 4. Application design
In all cases the frame buffer is located on the target hardware. In the case of
the 2.7 inch display this is 400*240//8 = 12000 bytes in size. This should be
instantiated as soon as possible in the application to ensure that sufficient
contiguous RAM is available.
## 4.1 Micropower applications
These comments largely assume a Pyboard host. The application should import
`upower` from
[micropython-micropower](https://github.com/peterhinch/micropython-micropower).
This turns the USB interface off if not in use to conserve power. It also
provides an `lpdelay` function to implement a delay using `pyb.stop()` to
conserve power.
In tests the `clock_batt` demo consumed 700μA between updates. A full refresh
every 30s consumed about 48mA for 128ms. These figures correspond to a mean
current consumption of 904μA implying about 46 days operation per AH of
battery capacity. LiPo cells of 2AH capacity are widely available offering a
theoretical runtime of 92 days between charges.
Lower currents might be achieved using standby but I have major doubts. This is
because it is necessary to toggle the VCOM bit at a minimum of 1Hz. Waking from
standby uses significan amounts of power as the modules are compiled. Even if
frozen bytecode is used, there is still significant power usage importing
modules and instantiating classes; this usage is not incurred in the loop in
the demo.
# 5. Resources
[Schematic for 2.7" unit](https://learn.adafruit.com/assets/94077)
[Datasheet 2.7"](https://cdn-learn.adafruit.com/assets/assets/000/094/215/original/LS027B7DH01_Rev_Jun_2010.pdf?1597872422)
[Datasheet 1.3"](http://www.adafruit.com/datasheets/LS013B4DN04-3V_FPC-204284.pdf)

Wyświetl plik

@ -1,11 +0,0 @@
# Drivers for SSD1331 (Adafruit 0.96" OLED display)
There are two versions. Both are designed to be cross-platform.
* `ssd1331.py` Uses 8 bit rrrgggbb color.
* `ssd1331_16bit.py` Uses 16 bit RGB565 color.
The `ssd1331_16bit` version requires 12KiB of RAM for the frame buffer, while
the standard version needs only 6KiB. For the GUI the standard version works
well because text and controls are normally drawn with saturated colors.
The 16 bit version provides greatly improved results when rendering images.

Wyświetl plik

@ -1,29 +0,0 @@
# Drivers for SSD1351
There are three versions.
* `ssd1351.py` This is optimised for STM (e.g. Pyboard) platforms.
* `ssd1351_generic.py` Cross-platform version. Tested on ESP32 and ESP8266.
* `ssd1351_16bit.py` Cross-platform. Uses 16 bit RGB565 color.
To conserve RAM the first two use 8 bit (rrrgggbb) color. This works well with
the GUI if saturated colors are used to render text and controls.
The `ssd1351_generic.py` version includes the `micropython.viper` decorator. If
your platform does not support this, comment it out and remove the type
annotations. You may be able to use the `micropython.native` decorator.
If the platform supports the viper emitter performance should still be good: on
a Pyboard V1 this driver perorms a refresh of a 128*128 color display in 47ms.
The STM version is faster but not by a large margin: a refresh takes 41ms. 32ms
of these figures is consumed by the data transfer over the SPI interface.
If the viper and native decorators are unsupported a screen redraw takes 272ms
(on Pyboard 1.0) which is visibly slow.
The `ssd1351_16bit` version on a 128x128 display requires 32KiB for the frame
buffer; this means it is only usable on platforms with plenty of RAM. Testing
was done on a Pyboard D SF2W. With the GUI this version offers little benefit,
but it delivers major advantages in applications such as rendering images.
This driver was tested on official Adafruit 1.5 and 1.27 inch displays, also a
Chinese 1.5 inch unit.

Wyświetl plik

@ -8,27 +8,8 @@
# This driver is based on the Adafruit C++ library for Arduino # This driver is based on the Adafruit C++ library for Arduino
# https://github.com/adafruit/Adafruit-SSD1351-library.git # https://github.com/adafruit/Adafruit-SSD1351-library.git
# The MIT License (MIT) # Copyright (c) Peter Hinch 2018-2020
# Released under the MIT license see LICENSE
# Copyright (c) 2018 Peter Hinch
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import framebuf import framebuf
import utime import utime
@ -36,6 +17,11 @@ import gc
import micropython import micropython
from uctypes import addressof from uctypes import addressof
# ESP32 produces 20MHz, Pyboard D SF2W: 15MHz, SF6W: 18MHz, Pyboard 1.1: 10.5MHz
# OLED datasheet: should support 20MHz
def spi_init(spi):
spi.init(baudrate=20_000_000)
# Timings with standard emitter # Timings with standard emitter
# 1.86ms * 128 lines = 240ms. copy dominates: show() took 272ms # 1.86ms * 128 lines = 240ms. copy dominates: show() took 272ms
# Buffer transfer time = 272-240 = 32ms which accords with expected: # Buffer transfer time = 272-240 = 32ms which accords with expected:
@ -99,25 +85,26 @@ class SSD1351(framebuf.FrameBuffer):
def rgb(r, g, b): def rgb(r, g, b):
return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6)
def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128): def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128, init_spi=spi_init):
if height not in (96, 128): if height not in (96, 128):
raise ValueError('Unsupported height {}'.format(height)) raise ValueError('Unsupported height {}'.format(height))
self.spi = spi self.spi = spi
self.rate = 11000000 # See baudrate note above. self.spi_init = init_spi
self.pincs = pincs self.pincs = pincs
self.pindc = pindc # 1 = data 0 = cmd self.pindc = pindc # 1 = data 0 = cmd
self.height = height # Required by Writer class self.height = height # Required by Writer class
self.width = width self.width = width
# Save color mode for use by writer_gui (blit) mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color.
self.mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color.
gc.collect() gc.collect()
self.buffer = bytearray(self.height * self.width) self.buffer = bytearray(self.height * self.width)
super().__init__(self.buffer, self.width, self.height, self.mode) super().__init__(self.buffer, self.width, self.height, mode)
self.linebuf = bytearray(self.width * 2) self.linebuf = bytearray(self.width * 2)
pinrs(0) # Pulse the reset line pinrs(0) # Pulse the reset line
utime.sleep_ms(1) utime.sleep_ms(1)
pinrs(1) pinrs(1)
utime.sleep_ms(1) utime.sleep_ms(1)
if self.spi_init: # A callback was passed
self.spi_init(spi) # Bus may be shared
# See above comment to explain this allocation-saving gibberish. # See above comment to explain this allocation-saving gibberish.
self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\ self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\
b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\ b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\
@ -127,7 +114,6 @@ class SSD1351(framebuf.FrameBuffer):
gc.collect() gc.collect()
def _write(self, buf, dc): def _write(self, buf, dc):
self.spi.init(baudrate=self.rate, polarity=1, phase=1)
self.pincs(1) self.pincs(1)
self.pindc(dc) self.pindc(dc)
self.pincs(0) self.pincs(0)
@ -139,6 +125,8 @@ class SSD1351(framebuf.FrameBuffer):
def show(self): def show(self):
lb = self.linebuf lb = self.linebuf
buf = self.buffer buf = self.buffer
if self.spi_init: # A callback was passed
self.spi_init(self.spi) # Bus may be shared
self._write(b'\x5c', 0) # Enable data write self._write(b'\x5c', 0) # Enable data write
if self.height == 128: if self.height == 128:
for l in range(128): for l in range(128):

Wyświetl plik

@ -7,38 +7,23 @@
# This driver is based on the Adafruit C++ library for Arduino # This driver is based on the Adafruit C++ library for Arduino
# https://github.com/adafruit/Adafruit-SSD1351-library.git # https://github.com/adafruit/Adafruit-SSD1351-library.git
# The MIT License (MIT) # Copyright (c) Peter Hinch 2019-2020
# Released under the MIT license see LICENSE
# Copyright (c) 2019 Peter Hinch
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import framebuf import framebuf
import utime import utime
import gc import gc
import micropython import micropython
from uctypes import addressof from uctypes import addressof
import sys
# https://github.com/peterhinch/micropython-nano-gui/issues/2 # https://github.com/peterhinch/micropython-nano-gui/issues/2
# The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct. # The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct.
# Keep 0,0 on STM as testing was done in that mode. # Now using 0,0 on STM and ESP32
_bs = 0 if sys.platform == 'esp32' else 1 # SPI bus state
# ESP32 produces 20MHz, Pyboard D SF2W: 15MHz, SF6W: 18MHz, Pyboard 1.1: 10.5MHz
# OLED datasheet: should support 20MHz
def spi_init(spi):
spi.init(baudrate=20_000_000) # Data sheet: should support 20MHz
# Initialisation commands in cmd_init: # Initialisation commands in cmd_init:
# 0xfd, 0x12, 0xfd, 0xb1, # Unlock command mode # 0xfd, 0x12, 0xfd, 0xb1, # Unlock command mode
@ -61,8 +46,7 @@ _bs = 0 if sys.platform == 'esp32' else 1 # SPI bus state
# 0xb6, 1, # Precharge 2 # 0xb6, 1, # Precharge 2
# 0xaf, # Display on # 0xaf, # Display on
# SPI baudrate: Pyboard can produce 10.5MHz or 21MHz. Datasheet gives max of 20MHz.
# Attempt to use 21MHz failed but might work on a PCB or with very short leads.
class SSD1351(framebuf.FrameBuffer): class SSD1351(framebuf.FrameBuffer):
# Convert r, g, b in range 0-255 to a 16 bit colour value RGB565 # Convert r, g, b in range 0-255 to a 16 bit colour value RGB565
# acceptable to hardware: rrrrrggggggbbbbb # acceptable to hardware: rrrrrggggggbbbbb
@ -70,25 +54,26 @@ class SSD1351(framebuf.FrameBuffer):
def rgb(r, g, b): def rgb(r, g, b):
return ((r & 0xf8) << 5) | ((g & 0x1c) << 11) | (b & 0xf8) | ((g & 0xe0) >> 5) return ((r & 0xf8) << 5) | ((g & 0x1c) << 11) | (b & 0xf8) | ((g & 0xe0) >> 5)
def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128): def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128, init_spi=spi_init):
if height not in (96, 128): if height not in (96, 128):
raise ValueError('Unsupported height {}'.format(height)) raise ValueError('Unsupported height {}'.format(height))
self.spi = spi self.spi = spi
self.rate = 11000000 # See baudrate note above. self.spi_init = init_spi
self.pincs = pincs self.pincs = pincs
self.pindc = pindc # 1 = data 0 = cmd self.pindc = pindc # 1 = data 0 = cmd
self.height = height # Required by Writer class self.height = height # Required by Writer class
self.width = width self.width = width
# Save color mode for use by writer_gui (blit) mode = framebuf.RGB565
self.mode = framebuf.RGB565
gc.collect() gc.collect()
self.buffer = bytearray(self.height * self.width * 2) self.buffer = bytearray(self.height * self.width * 2)
super().__init__(self.buffer, self.width, self.height, self.mode) super().__init__(self.buffer, self.width, self.height, mode)
self.mvb = memoryview(self.buffer) self.mvb = memoryview(self.buffer)
pinrs(0) # Pulse the reset line pinrs(0) # Pulse the reset line
utime.sleep_ms(1) utime.sleep_ms(1)
pinrs(1) pinrs(1)
utime.sleep_ms(1) utime.sleep_ms(1)
if self.spi_init: # A callback was passed
self.spi_init(spi) # Bus may be shared
# See above comment to explain this allocation-saving gibberish. # See above comment to explain this allocation-saving gibberish.
self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\ self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\
b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\ b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\
@ -98,7 +83,6 @@ class SSD1351(framebuf.FrameBuffer):
gc.collect() gc.collect()
def _write(self, mv, dc): def _write(self, mv, dc):
self.spi.init(baudrate=self.rate, polarity=_bs, phase=_bs)
self.pincs(1) self.pincs(1)
self.pindc(dc) self.pindc(dc)
self.pincs(0) self.pincs(0)
@ -110,6 +94,8 @@ class SSD1351(framebuf.FrameBuffer):
def show(self): def show(self):
mvb = self.mvb mvb = self.mvb
bw = self.width * 2 # Width in bytes bw = self.width * 2 # Width in bytes
if self.spi_init: # A callback was passed
self.spi_init(self.spi) # Bus may be shared
self._write(b'\x5c', 0) # Enable data write self._write(b'\x5c', 0) # Enable data write
if self.height == 128: if self.height == 128:
for l in range(128): for l in range(128):

Wyświetl plik

@ -0,0 +1,142 @@
# SSD1351_4bit.py MicroPython driver for Adafruit color OLED displays.
# This is cross-platform and uses 4 bit color for minimum RAM usage.
# Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431
# Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673
# For wiring details see drivers/ADAFRUIT.md in this repo.
# This driver is based on the Adafruit C++ library for Arduino
# https://github.com/adafruit/Adafruit-SSD1351-library.git
# Copyright (c) Peter Hinch 2020
# Released under the MIT license see LICENSE
import framebuf
import utime
import gc
import micropython
from uctypes import addressof
# https://github.com/peterhinch/micropython-nano-gui/issues/2
# The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct.
# Now using 0,0 on STM and ESP32
# ESP32 produces 20MHz, Pyboard D SF2W: 15MHz, SF6W: 18MHz, Pyboard 1.1: 10.5MHz
# OLED datasheet: should support 20MHz
def spi_init(spi):
spi.init(baudrate=20_000_000) # Data sheet: should support 20MHz
@micropython.viper
def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int):
n = 0
for x in range(length):
c = source[x]
d = (c & 0xf0) >> 3 # 2* LUT indices (LUT is 16 bit color)
e = (c & 0x0f) << 1
dest[n] = lut[d]
n += 1
dest[n] = lut[d + 1]
n += 1
dest[n] = lut[e]
n += 1
dest[n] = lut[e + 1]
n += 1
# Initialisation commands in cmd_init:
# 0xfd, 0x12, 0xfd, 0xb1, # Unlock command mode
# 0xae, # display off (sleep mode)
# 0xb3, 0xf1, # clock div
# 0xca, 0x7f, # mux ratio
# 0xa0, 0x74, # setremap 0x74
# 0x15, 0, 0x7f, # setcolumn
# 0x75, 0, 0x7f, # setrow
# 0xa1, 0, # set display start line
# 0xa2, 0, # displayoffset
# 0xb5, 0, # setgpio
# 0xab, 1, # functionselect: serial interface, internal Vdd regulator
# 0xb1, 0x32, # Precharge
# 0xbe, 0x05, # vcommh
# 0xa6, # normaldisplay
# 0xc1, 0xc8, 0x80, 0xc8, # contrast abc
# 0xc7, 0x0f, # Master contrast
# 0xb4, 0xa0, 0xb5, 0x55, # set vsl (see datasheet re ext circuit)
# 0xb6, 1, # Precharge 2
# 0xaf, # Display on
class SSD1351(framebuf.FrameBuffer):
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
# Note pretty colors in datasheet don't match actual colors. See doc.
@staticmethod
def rgb(r, g, b):
return (r & 0xf8) << 5 | (g & 0x1c) << 11 | (g & 0xe0) >> 5 | (b & 0xf8)
def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128, init_spi=spi_init):
if height not in (96, 128):
raise ValueError('Unsupported height {}'.format(height))
self.spi = spi
self.pincs = pincs
self.pindc = pindc # 1 = data 0 = cmd
self.height = height # Required by Writer class
self.width = width
self.spi_init = init_spi
mode = framebuf.GS4_HMSB # Use 4bit greyscale.
gc.collect()
self.buffer = bytearray(self.height * self.width // 2)
super().__init__(self.buffer, self.width, self.height, mode)
self.linebuf = bytearray(self.width * 2)
pinrs(0) # Pulse the reset line
utime.sleep_ms(1)
pinrs(1)
utime.sleep_ms(1)
if self.spi_init: # A callback was passed
self.spi_init(spi) # Bus may be shared
# See above comment to explain this allocation-saving gibberish.
self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\
b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\
b'\xb1\x32\xbe\x05\xa6\xc1\xc8\x80\xc8\xc7\x0f'\
b'\xb4\xa0\xb5\x55\xb6\x01\xaf', 0)
gc.collect()
self.show()
def _write(self, buf, dc):
self.pincs(1)
self.pindc(dc)
self.pincs(0)
self.spi.write(buf)
self.pincs(1)
# Write lines from the framebuf out of order to match the mapping of the
# SSD1351 RAM to the OLED device.
def show(self): # 44ms on Pyboard 1.x
clut = SSD1351.lut
lb = self.linebuf
wd = self.width // 2
buf = memoryview(self.buffer)
if self.spi_init: # A callback was passed
self.spi_init(self.spi) # Bus may be shared
self._write(b'\x5c', 0) # Enable data write
if self.height == 128:
for l in range(128):
l0 = (95 - l) % 128 # 95 94 .. 1 0 127 126...
start = l0 * wd
_lcopy(lb, buf[start : start + wd], clut, wd)
self._write(lb, 1) # Send a line
else:
for l in range(128):
if l < 64:
start = (63 -l) * wd
_lcopy(lb, buf[start : start + wd], clut, wd)
elif l < 96: # This is daft but I can't get setrow to work
pass # Let RAM counter increase
else:
start = (191 - l) * wd
_lcopy(lb, buf[start : start + wd], clut, wd)
self._write(lb, 1) # Send a line

Wyświetl plik

@ -8,27 +8,9 @@
# This driver is based on the Adafruit C++ library for Arduino # This driver is based on the Adafruit C++ library for Arduino
# https://github.com/adafruit/Adafruit-SSD1351-library.git # https://github.com/adafruit/Adafruit-SSD1351-library.git
# The MIT License (MIT)
# Copyright (c) 2018 Peter Hinch # Copyright (c) Peter Hinch 2018-2020
# Released under the MIT license see LICENSE
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import framebuf import framebuf
import utime import utime
@ -39,8 +21,12 @@ from uctypes import addressof
import sys import sys
# https://github.com/peterhinch/micropython-nano-gui/issues/2 # https://github.com/peterhinch/micropython-nano-gui/issues/2
# The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct. # The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct.
# Keep 0,0 on STM as testing was done in that mode. # Now using 0,0 on STM and ESP32
_bs = 0 if sys.platform == 'esp32' else 1 # SPI bus state
# ESP32 produces 20MHz, Pyboard D SF2W: 15MHz, SF6W: 18MHz, Pyboard 1.1: 10.5MHz
# OLED datasheet: should support 20MHz
def spi_init(spi):
spi.init(baudrate=20_000_000) # Data sheet: should support 20MHz
# Timings with standard emitter # Timings with standard emitter
# 1.86ms * 128 lines = 240ms. copy dominates: show() took 272ms # 1.86ms * 128 lines = 240ms. copy dominates: show() took 272ms
@ -86,25 +72,26 @@ class SSD1351(framebuf.FrameBuffer):
def rgb(r, g, b): def rgb(r, g, b):
return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6)
def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128): def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128, init_spi=spi_init):
if height not in (96, 128): if height not in (96, 128):
raise ValueError('Unsupported height {}'.format(height)) raise ValueError('Unsupported height {}'.format(height))
self.spi = spi self.spi = spi
self.rate = 20000000 # Data sheet: should support 20MHz self.spi_init = init_spi
self.pincs = pincs self.pincs = pincs
self.pindc = pindc # 1 = data 0 = cmd self.pindc = pindc # 1 = data 0 = cmd
self.height = height # Required by Writer class self.height = height # Required by Writer class
self.width = width self.width = width
# Save color mode for use by writer_gui (blit) mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color.
self.mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color.
gc.collect() gc.collect()
self.buffer = bytearray(self.height * self.width) self.buffer = bytearray(self.height * self.width)
super().__init__(self.buffer, self.width, self.height, self.mode) super().__init__(self.buffer, self.width, self.height, mode)
self.linebuf = bytearray(self.width * 2) self.linebuf = bytearray(self.width * 2)
pinrs(0) # Pulse the reset line pinrs(0) # Pulse the reset line
utime.sleep_ms(1) utime.sleep_ms(1)
pinrs(1) pinrs(1)
utime.sleep_ms(1) utime.sleep_ms(1)
if self.spi_init: # A callback was passed
self.spi_init(spi) # Bus may be shared
# See above comment to explain this allocation-saving gibberish. # See above comment to explain this allocation-saving gibberish.
self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\ self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\
b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\ b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\
@ -114,7 +101,6 @@ class SSD1351(framebuf.FrameBuffer):
self.show() self.show()
def _write(self, buf, dc): def _write(self, buf, dc):
self.spi.init(baudrate=self.rate, polarity=_bs, phase=_bs)
self.pincs(1) self.pincs(1)
self.pindc(dc) self.pindc(dc)
self.pincs(0) self.pincs(0)
@ -126,6 +112,8 @@ class SSD1351(framebuf.FrameBuffer):
def show(self): def show(self):
lb = self.linebuf lb = self.linebuf
buf = memoryview(self.buffer) buf = memoryview(self.buffer)
if self.spi_init: # A callback was passed
self.spi_init(self.spi) # Bus may be shared
self._write(b'\x5c', 0) # Enable data write self._write(b'\x5c', 0) # Enable data write
if self.height == 128: if self.height == 128:
for l in range(128): for l in range(128):

Wyświetl plik

@ -1,15 +0,0 @@
# Drivers for ST7735R
These are cross-platform but assume `micropython.viper` capability. They use
8-bit color to minimise the RAM used by the frame buffer.
* `st7735r.py` Supports [Adafruit 1.8" display](https://www.adafruit.com/product/358).
* `st7735r144.py` Supports [Adafruit 1.44" display](https://www.adafruit.com/product/2088).
Users of other ST7735R based displays should beware: there are many variants
with differing setup requirements.
[This driver](https://github.com/boochow/MicroPython-ST7735/blob/master/ST7735.py)
has four different initialisation routines for various display versions. Even
the supported Adafruit displays differ in their initialisation settings.
If your Chinese display doesn't work with my drivers you are on your own: I
can't support hardware I don't possess.

Wyświetl plik

@ -51,21 +51,22 @@ class ST7735R(framebuf.FrameBuffer):
return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6)
# rst and cs are active low, SPI is mode 0 # rst and cs are active low, SPI is mode 0
def __init__(self, spi, cs, dc, rst, height=128, width=160): def __init__(self, spi, cs, dc, rst, height=128, width=160, usd=False, init_spi=False):
self._spi = spi self._spi = spi
self._rst = rst # Pins self._rst = rst # Pins
self._dc = dc self._dc = dc
self._cs = cs self._cs = cs
self.height = height # Required by Writer class self.height = height # Required by Writer class
self.width = width self.width = width
self._spi_init = init_spi
# Save color mode for use by writer_gui (blit) # Save color mode for use by writer_gui (blit)
self.mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color. mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color.
gc.collect() gc.collect()
self.buffer = bytearray(height * width) buf = bytearray(height * width)
self._mvb = memoryview(self.buffer) self._mvb = memoryview(buf)
super().__init__(self.buffer, width, height, self.mode) super().__init__(buf, width, height, mode)
self._linebuf = bytearray(int(width * 3 // 2)) # 12 bit color out self._linebuf = bytearray(int(width * 3 // 2)) # 12 bit color out
self._init() self._init(usd)
self.show() self.show()
# Hardware reset # Hardware reset
@ -97,8 +98,10 @@ class ST7735R(framebuf.FrameBuffer):
self._cs(1) self._cs(1)
# Initialise the hardware. Blocks 500ms. # Initialise the hardware. Blocks 500ms.
def _init(self): def _init(self, usd):
self._hwreset() # Hardware reset. Blocks 3ms 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 cmd = self._wcmd
wcd = self._wcd wcd = self._wcd
cmd(b'\x01') # SW reset datasheet specifies > 120ms cmd(b'\x01') # SW reset datasheet specifies > 120ms
@ -119,7 +122,10 @@ class ST7735R(framebuf.FrameBuffer):
cmd(b'\x20') # INVOFF cmd(b'\x20') # INVOFF
# d7..d5 of MADCTL determine rotation/orientation # d7..d5 of MADCTL determine rotation/orientation
wcd(b'\x36', b'\x20') # MADCTL: RGB landscape mode if self.height > self.width:
wcd(b'\x36', b'\x80' if usd else b'\x40') # MADCTL: RGB portrait mode
else:
wcd(b'\x36', b'\xe0' if usd else b'\x20') # MADCTL: RGB landscape mode
wcd(b'\x3a', b'\x03') # COLMOD 12 bit wcd(b'\x3a', b'\x03') # COLMOD 12 bit
wcd(b'\xe0', b'\x02\x1c\x07\x12\x37\x32\x29\x2d\x29\x25\x2B\x39\x00\x01\x03\x10') # GMCTRP1 Gamma wcd(b'\xe0', b'\x02\x1c\x07\x12\x37\x32\x29\x2d\x29\x25\x2B\x39\x00\x01\x03\x10') # GMCTRP1 Gamma
wcd(b'\xe1', b'\x03\x1d\x07\x06\x2E\x2C\x29\x2D\x2E\x2E\x37\x3F\x00\x00\x02\x10') # GMCTRN1 wcd(b'\xe1', b'\x03\x1d\x07\x06\x2E\x2C\x29\x2D\x2E\x2E\x37\x3F\x00\x00\x02\x10') # GMCTRN1
@ -139,6 +145,8 @@ class ST7735R(framebuf.FrameBuffer):
buf = self._mvb buf = self._mvb
self._dc(0) self._dc(0)
self._cs(0) self._cs(0)
if self._spi_init: # A callback was passed
self._spi_init(self._spi) # Bus may be shared
self._spi.write(b'\x2c') # RAMWR self._spi.write(b'\x2c') # RAMWR
self._dc(1) self._dc(1)
for start in range(wd * (ht - 1), -1, - wd): # For each line for start in range(wd * (ht - 1), -1, - wd): # For each line

Wyświetl plik

@ -49,19 +49,20 @@ class ST7735R(framebuf.FrameBuffer):
return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6)
# rst and cs are active low, SPI is mode 0 # rst and cs are active low, SPI is mode 0
def __init__(self, spi, cs, dc, rst, height=128, width=128): def __init__(self, spi, cs, dc, rst, height=128, width=128, init_spi=False):
self._spi = spi self._spi = spi
self._rst = rst # Pins self._rst = rst # Pins
self._dc = dc self._dc = dc
self._cs = cs self._cs = cs
self.height = height # Required by Writer class self.height = height # Required by Writer class
self.width = width self.width = width
self._spi_init = init_spi
# Save color mode for use by writer_gui (blit) # Save color mode for use by writer_gui (blit)
self.mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color. mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color.
gc.collect() gc.collect()
self.buffer = bytearray(self.height * self.width) buf = bytearray(self.height * self.width)
self._mvb = memoryview(self.buffer) self._mvb = memoryview(buf)
super().__init__(self.buffer, self.width, self.height, self.mode) super().__init__(buf, self.width, self.height, mode)
self._linebuf = bytearray(self.width * 2) # 16 bit color out self._linebuf = bytearray(self.width * 2) # 16 bit color out
self._init() self._init()
self.show() self.show()
@ -97,6 +98,8 @@ class ST7735R(framebuf.FrameBuffer):
# Initialise the hardware. Blocks 500ms. # Initialise the hardware. Blocks 500ms.
def _init(self): def _init(self):
self._hwreset() # Hardware reset. Blocks 3ms 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 cmd = self._wcmd
wcd = self._wcd wcd = self._wcd
cmd(b'\x01') # SW reset datasheet specifies > 120ms cmd(b'\x01') # SW reset datasheet specifies > 120ms
@ -137,6 +140,8 @@ class ST7735R(framebuf.FrameBuffer):
buf = self._mvb buf = self._mvb
self._dc(0) self._dc(0)
self._cs(0) self._cs(0)
if self._spi_init: # A callback was passed
self._spi_init(self._spi) # Bus may be shared
self._spi.write(b'\x2c') # RAMWR self._spi.write(b'\x2c') # RAMWR
self._dc(1) self._dc(1)
for start in range(wd * (ht - 1), -1, - wd): # For each line for start in range(wd * (ht - 1), -1, - wd): # For each line

Wyświetl plik

@ -0,0 +1,154 @@
# st7735r144.py Driver for ST7735R 1.44" LCD display for nano-gui
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2018-2020 Peter Hinch
# Supported display
# Adafruit 1.44' Color TFT LCD Display with MicroSD Card breakout:
# https://www.adafruit.com/product/2088
# Based on
# https://github.com/adafruit/Adafruit_CircuitPython_ST7735R/blob/master/adafruit_st7735r.py
# https://github.com/GuyCarver/MicroPython/blob/master/lib/ST7735.py
# https://github.com/boochow/MicroPython-ST7735
# https://learn.adafruit.com/adafruit-1-44-color-tft-with-micro-sd-socket/python-usage
# disp = st7735.ST7735R(spi, rotation=90, # 1.8" ST7735R
# disp = st7735.ST7735R(spi, rotation=270, height=128, x_offset=2, y_offset=3, # 1.44" ST7735R
from time import sleep_ms
import framebuf
import gc
import micropython
# Datasheet para 8.4 scl write cycle 66ns == 15MHz
@micropython.viper
def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int):
n = 0
for x in range(length):
c = source[x]
d = (c & 0xf0) >> 3 # 2* LUT indices (LUT is 16 bit color)
e = (c & 0x0f) << 1
dest[n] = lut[d]
n += 1
dest[n] = lut[d + 1]
n += 1
dest[n] = lut[e]
n += 1
dest[n] = lut[e + 1]
n += 1
class ST7735R(framebuf.FrameBuffer):
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
@staticmethod
def rgb(r, g, b):
return (b & 0xf8) << 5 | (g & 0x1c) << 11 | (g & 0xe0) >> 5 | (r & 0xf8)
# rst and cs are active low, SPI is mode 0
def __init__(self, spi, cs, dc, rst, height=128, width=128, init_spi=False):
self._spi = spi
self._rst = rst # Pins
self._dc = dc
self._cs = cs
self.height = height # Required by Writer class
self.width = width
self._spi_init = init_spi
# Save color mode for use by writer_gui (blit)
mode = framebuf.GS4_HMSB # Use 4bit greyscale.
gc.collect()
buf = bytearray(self.height * self.width // 2)
self._mvb = memoryview(buf)
super().__init__(buf, self.width, self.height, mode)
self._linebuf = bytearray(self.width * 2) # 16 bit color out
self._init()
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 500ms.
def _init(self):
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
sleep_ms(150)
cmd(b'\x11') # SLPOUT
sleep_ms(256) # Adafruit delay (datsheet 120ms)
wcd(b'\xb1', b'\x01\x2C\x2D') # FRMCTRL1
wcd(b'\xb2', b'\x01\x2C\x2D') # FRMCTRL2
wcd(b'\xb3', b'\x01\x2C\x2D\x01\x2C\x2D') # FRMCTRL3
wcd(b'\xb4', b'\x07') # INVCTR line inversion
wcd(b'\xc0', b'\xa2\x02\x84') # PWCTR1 GVDD = 4.7V, 1.0uA
wcd(b'\xc1', b'\xc5') # PWCTR2 VGH=14.7V, VGL=-7.35V
wcd(b'\xc2', b'\x0a\x00') # PWCTR3 Opamp current small, Boost frequency
wcd(b'\xc3', b'\x8a\x2a') # PWCTR4
wcd(b'\xc4', b'\x8a\xee') # PWCTR5
wcd(b'\xc5', b'\x0e') # VMCTR1 VCOMH = 4V, VOML = -1.1V NOTE I make VCOM == -0.775V
cmd(b'\x20') # INVOFF
# d7..d5 of MADCTL determine rotation/orientation
wcd(b'\x36', b'\xe0') # MADCTL: RGB landscape mode for 1.4" display
wcd(b'\x3a', b'\x05') # COLMOD 16 bit
wcd(b'\xe0', b'\x02\x1c\x07\x12\x37\x32\x29\x2d\x29\x25\x2B\x39\x00\x01\x03\x10') # GMCTRP1 Gamma
wcd(b'\xe1', b'\x03\x1d\x07\x06\x2E\x2C\x29\x2D\x2E\x2E\x37\x3F\x00\x00\x02\x10') # GMCTRN1
wcd(b'\x2a', int.to_bytes((3 << 16) + self.width + 2, 4, 'big')) # CASET
wcd(b'\x2b', int.to_bytes((2 << 16) + self.height + 2, 4, 'big')) # RASET
cmd(b'\x13') # NORON
sleep_ms(10)
cmd(b'\x29') # DISPON
sleep_ms(100)
def show(self): # Blocks 38.6ms on Pyboard D at stock frequency
clut = ST7735R.lut
wd = self.width // 2
ht = self.height
lb = self._linebuf
buf = self._mvb
self._dc(0)
self._cs(0)
if self._spi_init: # A callback was passed
self._spi_init(self._spi) # Bus may be shared
self._spi.write(b'\x2c') # RAMWR
self._dc(1)
for start in range(wd * (ht - 1), -1, - wd): # For each line
_lcopy(lb, buf[start :], clut, wd) # Copy and map colors (68us)
self._spi.write(lb)
self._cs(1)

Wyświetl plik

@ -0,0 +1,157 @@
# st7735r.py Driver for 1.8" 128*160 ST7735R LCD displays for nano-gui
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2018-2020 Peter Hinch
# Supported display
# Adfruit 1.8' Color TFT LCD display with MicroSD Card Breakout:
# https://www.adafruit.com/product/358
# Based on
# https://github.com/adafruit/Adafruit_CircuitPython_ST7735R/blob/master/adafruit_st7735r.py
# https://github.com/GuyCarver/MicroPython/blob/master/lib/ST7735.py
# https://github.com/boochow/MicroPython-ST7735
# https://learn.adafruit.com/adafruit-1-44-color-tft-with-micro-sd-socket/python-usage
# disp = st7735.ST7735R(spi, rotation=90, # 1.8" ST7735R
# disp = st7735.ST7735R(spi, rotation=270, height=128, x_offset=2, y_offset=3, # 1.44" ST7735R
from time import sleep_ms
import framebuf
import gc
import micropython
# Datasheet para 8.4 scl write cycle 66ns == 15MHz
@micropython.viper
def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int):
n = 0
for x in range(length):
c = source[x]
d = (c & 0xf0) >> 3 # 2* LUT indices (LUT is 16 bit color)
e = (c & 0x0f) << 1
dest[n] = lut[d]
n += 1
dest[n] = lut[d + 1]
n += 1
dest[n] = lut[e]
n += 1
dest[n] = lut[e + 1]
n += 1
class ST7735R(framebuf.FrameBuffer):
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
@staticmethod
def rgb(r, g, b):
return (b & 0xf8) << 5 | (g & 0x1c) << 11 | (g & 0xe0) >> 5 | (r & 0xf8)
# rst and cs are active low, SPI is mode 0
def __init__(self, spi, cs, dc, rst, height=128, width=160, usd=False, init_spi=False):
self._spi = spi
self._rst = rst # Pins
self._dc = dc
self._cs = cs
self.height = height # Required by Writer class
self.width = width
self._spi_init = init_spi
# Save color mode for use by writer_gui (blit)
mode = framebuf.GS4_HMSB # Use 4bit greyscale.
gc.collect()
buf = bytearray(height * width // 2)
self._mvb = memoryview(buf)
super().__init__(buf, width, height, mode)
self._linebuf = bytearray(self.width * 2) # 16 bit color out
self._init(usd)
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 500ms.
def _init(self, usd):
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
sleep_ms(150)
cmd(b'\x11') # SLPOUT
sleep_ms(256) # Adafruit delay (datsheet 120ms)
wcd(b'\xb1', b'\x01\x2C\x2D') # FRMCTRL1
wcd(b'\xb2', b'\x01\x2C\x2D') # FRMCTRL2
wcd(b'\xb3', b'\x01\x2C\x2D\x01\x2C\x2D') # FRMCTRL3
wcd(b'\xb4', b'\x07') # INVCTR line inversion
wcd(b'\xc0', b'\xa2\x02\x84') # PWCTR1 GVDD = 4.7V, 1.0uA
wcd(b'\xc1', b'\xc5') # PWCTR2 VGH=14.7V, VGL=-7.35V
wcd(b'\xc2', b'\x0a\x00') # PWCTR3 Opamp current small, Boost frequency
wcd(b'\xc3', b'\x8a\x2a') # PWCTR4
wcd(b'\xc4', b'\x8a\xee') # PWCTR5
wcd(b'\xc5', b'\x0e') # VMCTR1 VCOMH = 4V, VOML = -1.1V NOTE I make VCOM == -0.775V
cmd(b'\x20') # INVOFF
# d7..d5 of MADCTL determine rotation/orientation
if self.height > self.width:
wcd(b'\x36', b'\x80' if usd else b'\x40') # MADCTL: RGB portrait mode
else:
wcd(b'\x36', b'\xe0' if usd else b'\x20') # MADCTL: RGB landscape mode
wcd(b'\x3a', b'\x05') # COLMOD 16 bit
wcd(b'\xe0', b'\x02\x1c\x07\x12\x37\x32\x29\x2d\x29\x25\x2B\x39\x00\x01\x03\x10') # GMCTRP1 Gamma
wcd(b'\xe1', b'\x03\x1d\x07\x06\x2E\x2C\x29\x2D\x2E\x2E\x37\x3F\x00\x00\x02\x10') # GMCTRN1
wcd(b'\x2a', int.to_bytes(self.width, 4, 'big')) # CASET column address 0 start, 160 end
wcd(b'\x2b', int.to_bytes(self.height, 4, 'big')) # RASET
cmd(b'\x13') # NORON
sleep_ms(10)
cmd(b'\x29') # DISPON
sleep_ms(100)
def show(self): # Blocks 36ms on Pyboard D at stock frequency (160*128)
clut = ST7735R.lut
wd = self.width // 2
ht = self.height
lb = self._linebuf
buf = self._mvb
self._dc(0)
self._cs(0)
if self._spi_init: # A callback was passed
self._spi_init(self._spi) # Bus may be shared
self._spi.write(b'\x2c') # RAMWR
self._dc(1)
for start in range(wd * (ht - 1), -1, - wd): # For each line
_lcopy(lb, buf[start :], clut, wd) # Copy and map colors
self._spi.write(lb)
self._cs(1)

Wyświetl plik

@ -4,17 +4,41 @@
# Copyright (c) 2020 Peter Hinch # Copyright (c) 2020 Peter Hinch
from color_setup import SSD from color_setup import SSD
if hasattr(SSD, 'lut'): # Colors defined by LUT
def create_color(idx, r, g, b):
if not 0 <= idx <= 15:
raise ValueError('Color nos must be 0..15')
x = idx << 1
c = SSD.rgb(r, g, b)
SSD.lut[x] = c & 0xff
SSD.lut[x + 1] = c >> 8
return idx
GREEN = SSD.rgb(0, 255, 0) BLACK = create_color(0, 0, 0, 0)
RED = SSD.rgb(255, 0, 0) GREEN = create_color(1, 0, 255, 0)
LIGHTRED = SSD.rgb(140, 0, 0) RED = create_color(2, 255, 0, 0)
BLUE = SSD.rgb(0, 0, 255) LIGHTRED = create_color(3, 140, 0, 0)
YELLOW = SSD.rgb(255, 255, 0) BLUE = create_color(4, 0, 0, 255)
BLACK = 0 YELLOW = create_color(5, 255, 255, 0)
WHITE = SSD.rgb(255, 255, 255) GREY = create_color(6, 100, 100, 100)
GREY = SSD.rgb(100, 100, 100) MAGENTA = create_color(7, 255, 0, 255)
MAGENTA = SSD.rgb(255, 0, 255) CYAN = create_color(8, 0, 255, 255)
CYAN = SSD.rgb(0, 255, 255) LIGHTGREEN = create_color(9, 0, 100, 0)
LIGHTGREEN = SSD.rgb(0, 100, 0) DARKGREEN = create_color(10, 0, 80, 0)
DARKGREEN = SSD.rgb(0, 80, 0) DARKBLUE = create_color(11, 0, 0, 90)
DARKBLUE = SSD.rgb(0, 0, 90) # 12, 13, 14 free for user definition
WHITE = create_color(15, 255, 255, 255)
else:
BLACK = SSD.rgb(0, 0, 0)
GREEN = SSD.rgb(0, 255, 0)
RED = SSD.rgb(255, 0, 0)
LIGHTRED = SSD.rgb(140, 0, 0)
BLUE = SSD.rgb(0, 0, 255)
YELLOW = SSD.rgb(255, 255, 0)
GREY = SSD.rgb(100, 100, 100)
MAGENTA = SSD.rgb(255, 0, 255)
CYAN = SSD.rgb(0, 255, 255)
LIGHTGREEN = SSD.rgb(0, 100, 0)
DARKGREEN = SSD.rgb(0, 80, 0)
DARKBLUE = SSD.rgb(0, 0, 90)
WHITE = SSD.rgb(255, 255, 255)

Wyświetl plik

@ -10,7 +10,7 @@ from color_setup import ssd # Create a display instance
from gui.core.nanogui import refresh from gui.core.nanogui import refresh
from gui.widgets.label import Label from gui.widgets.label import Label
from gui.widgets.dial import Dial, Pointer from gui.widgets.dial import Dial, Pointer
refresh(ssd) # Initialise and clear display. refresh(ssd, True) # Initialise and clear display.
# Now import other modules # Now import other modules
import cmath import cmath

Wyświetl plik

@ -10,7 +10,7 @@ from color_setup import ssd # Create a display instance
from gui.core.nanogui import refresh from gui.core.nanogui import refresh
from gui.widgets.dial import Dial, Pointer from gui.widgets.dial import Dial, Pointer
refresh(ssd) # Initialise and clear display. refresh(ssd, True) # Initialise and clear display.
# Now import other modules # Now import other modules

Wyświetl plik

@ -16,7 +16,7 @@ from gui.core.nanogui import refresh
from gui.widgets.led import LED from gui.widgets.led import LED
from gui.widgets.meter import Meter from gui.widgets.meter import Meter
refresh(ssd) refresh(ssd, True)
# Fonts # Fonts
import gui.fonts.arial10 as arial10 import gui.fonts.arial10 as arial10

Wyświetl plik

@ -155,7 +155,7 @@ def compass(x):
refresh(ssd) refresh(ssd)
print('Color display test is running.') print('Color display test is running.')
print('Test runs to completion.') print('Test runs to completion: ~65 secs.')
clock(70) clock(70)
compass(70) compass(70)
meter() meter()

Wyświetl plik

@ -15,7 +15,7 @@ from gui.widgets.led import LED
from gui.widgets.meter import Meter from gui.widgets.meter import Meter
from gui.widgets.label import Label from gui.widgets.label import Label
refresh(ssd) refresh(ssd, True)
# Fonts # Fonts
import gui.fonts.arial10 as arial10 import gui.fonts.arial10 as arial10

Wyświetl plik

@ -19,7 +19,7 @@ from gui.core.fplot import PolarGraph, PolarCurve, CartesianGraph, Curve, TSeque
from gui.core.nanogui import refresh from gui.core.nanogui import refresh
from gui.widgets.label import Label from gui.widgets.label import Label
refresh(ssd) refresh(ssd, True)
# Fonts # Fonts
import gui.fonts.arial10 as arial10 import gui.fonts.arial10 as arial10

Wyświetl plik

@ -58,7 +58,7 @@ def test():
return c return c
def legendcb(f): def legendcb(f):
return '{:2.0f}'.format(88 + ((f + 1) / 2) * (108 - 88)) return '{:2.0f}'.format(88 + ((f + 1) / 2) * (108 - 88))
refresh(ssd) # Initialise and clear display. refresh(ssd, True) # Initialise and clear display.
CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it
wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False)
wri.set_clip(True, True, False) wri.set_clip(True, True, False)

Wyświetl plik

@ -58,7 +58,7 @@ async def main(wri):
await clip(wri) await clip(wri)
def test(): def test():
refresh(ssd) # Initialise and clear display. refresh(ssd, True) # Initialise and clear display.
CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it
wri = CWriter(ssd, arial10, verbose=False) wri = CWriter(ssd, arial10, verbose=False)
wri.set_clip(True, True, False) wri.set_clip(True, True, False)