micropython-nano-gui/DRIVERS.md

22 KiB

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
    1.1 Color handling
  2. Drivers for SSD1351 Color OLEDs
  3. Drivers for SSD1331 Small color OLEDs
  4. Drivers for ST7735R Small color TFTs
  5. Drivers for ILI9341 Large color TFTs
  6. Drivers for sharp displays Large low power monochrome displays
    6.1 Display characteristics
         6.1.1 The VCOM bit
         6.1.2 Refresh rate
    6.2 Test scripts
    6.3 Device driver constructor
         6.3.1 Device driver methods
         6.3.2 The vcom arg
    6.4 Application design
         6.4.1 Micropower applications
    6.5 Resources
  7. Writing device drivers
Main README

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:

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.

2. Drivers for SSD1351

See Adafruit 1.5" 128*128 OLED display and Adafruit 1.27" 128*96 display.

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.

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:
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

3. Drivers for SSD1331

See Adafruit 0.96" OLED display.

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.

Users of other ST7735R based displays should beware: there are many variants with differing setup requirements. This driver 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:
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.

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:
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 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:

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

6. Drivers for sharp displays

These monochrome SPI displays exist in three variants from Adafruit.

  1. 2.7 inch 400x240 pixels
  2. 1.3 inch 144x168
  3. 1.3 inch 96x96 - Discontinued.

I have tested on the first of these. However the Adfruit driver 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.

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 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. 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

Datasheet 2.7"

Datasheet 1.3"

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:

    @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 depending on the chip requirements.

Contents