From 5dbcc65828106cfb15c544bb86cc7380a9a83c47 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 29 Nov 2020 10:26:11 +0000 Subject: [PATCH] Documentation improvements. --- README.md | 112 +++++++++++++++++--------------- color_setup/st7735r144_setup.py | 5 +- color_setup/st7735r_setup.py | 5 +- drivers/st7735r/README.md | 15 +++++ drivers/st7735r/st7735r.py | 9 ++- drivers/st7735r/st7735r144.py | 97 ++++++--------------------- gui/demos/aclock.py | 2 +- 7 files changed, 103 insertions(+), 142 deletions(-) create mode 100644 drivers/st7735r/README.md diff --git a/README.md b/README.md index 98a7a86..fa3fdc2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ A lightweight and minimal MicroPython GUI library for display drivers based on -the `framebuf` class. Various display technologies are supported, primarily -small color OLED's. The GUI is cross-platform. +the `FrameBuffer` class. Various display technologies are supported, including +small color and monochrome OLED's and color TFT's. The GUI is cross-platform. -These images don't do justice to the OLED displays which are visually -impressive with bright colors and extreme contrast. For some reason they are -quite hard to photograph. +These images, most from OLED displays, are poor. OLEDs are visually impressive +displays with bright colors, wide viewing angle and extreme contrast. For some +reason I find them hard to photograph well. ![Image](images/clock.png) The aclock.py demo. ![Image](images/fonts.png) Label objects in two fonts. @@ -64,11 +64,15 @@ wiring details, pin names and hardware issues. # 1. Introduction This library provides a limited set of GUI objects (widgets) for displays whose -display driver is subclassed from the `framebuf` class. The GUI is display-only -and lacks provision for user input. This is because no `framebuf` based display -drivers exist for screens with a touch overlay. This is probably because touch -overlays require too many pixels and are best suited to displays with internal -frame buffers. +display driver is subclassed from the `FrameBuffer` class. Such drivers can be +tiny as the graphics primitives are supplied by the `FrameBuffer` class. + +The GUI is display-only and lacks provision for user input. Displays with touch +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). This doc explains how to configure for other platforms by adapting a single @@ -85,9 +89,10 @@ following displays. These have internal buffers: ## 1.1 Update -17 Nov 2020 -Add `Textbox` widget. `Scale` constructor arg `border` replaced by `bdcolor` as -per other widgets. +29 Nov 2020 Add ST7735R TFT drivers. + +17 Nov 2020 Add `Textbox` widget. `Scale` constructor arg `border` replaced by +`bdcolor` as per other widgets. 5 Nov 2020 This library has been refactored as a Python package. The aim is to reduce RAM @@ -115,15 +120,18 @@ Compatible and tested display drivers include: * A driver for Sharp ultra low power consumption monochrome displays such as [2.7 inch 400x240 pixels](https://www.adafruit.com/product/4694) is [here](./drivers/sharp/README.md). + * Drivers for Adafruit ST7735R based TFT's: + [1.8 inch](https://www.adafruit.com/product/358) and + [1.44 inch](https://www.adafruit.com/product/2088). 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 RAM usage. It also enables them to be effciently rendered at arbitrary scale on -devices with restricted processing power. The approach also enables widgets to +by hosts with restricted processing power. The approach also enables widgets to maximise information in ways that are difficult with icons, in particular using dynamic color changes in conjunction with moving elements. -Owing to RAM requirements and limitations on communication speed, `framebuf` +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. @@ -133,7 +141,7 @@ 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 update a 128x128x8 color ssd1351 display on a Pyboard 1.0 is 41ms. -Drivers based on `framebuf` must allocate contiguous RAM for the buffer. To +Drivers based on `FrameBuffer` must allocate contiguous RAM for the buffer. To avoid 'out of memory' errors it is best to instantiate the display before importing other modules. The demos illustrate this. @@ -178,14 +186,15 @@ for SSD1351 displays only the following are actually required: ### 2.1.1 Core files -The root directory contains setup files for monochrome and color displays. -These are templates for adaptation: only one file will normally need to be -copied to the target. Color files should be named `color_setup.py` on the -target, whereas the monochrome `ssd1306_setup.py` retains its own name. +The root directory contains two example setup files, for monochrome and color +displays respectively. Other examples may be found in the `color_setup` +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 +monochrome `ssd1306_setup.py` retains its own name. The chosen template will need to be edited to match the display in use, the MicroPython target and the electrical connections between display and target. -Electrical connections are detailed in the source. +Electrical connections are detailed in the driver source. * `color_setup.py` Setup for color displays. As written supports an SSD1351 display connected to a Pyboard. * `ssd1306_setup.py` Setup file for monochrome displays using the official @@ -213,8 +222,7 @@ The `gui/demos` directory contains test/demo scripts. monochrome 128*64 OLED display. * `color96.py` Tests/demos for the Adafruit 0.96 inch color OLED. -Demos for Adafruit 1.27 inch and 1.5 inch color OLEDs. These will run on either -display so long as `color_setup.py` has the correct `height` value. +Demos for larger displays. * `color15.py` Demonstrates a variety of widgets. Cross platform. * `aclock.py` Analog clock demo. Cross platform. * `alevel.py` Spirit level using Pyboard accelerometer. @@ -243,8 +251,8 @@ 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 structure must be maintained. Python fonts may be created using [font_to_py.py](https://github.com/peterhinch/micropython-font-to-py.git). The -`-x` option for horizontal mapping must be specified, along with -f for fixed -pitch rendering. Supplied examples are: +`-x` option for horizontal mapping must be specified. If fixed pitch rendering +is required `-f` is also required. Supplied examples are: * `arial10.py` Variable pitch Arial in various sizes. * `arial35.py` @@ -267,6 +275,8 @@ copied to the hardware root as `color_setup.py`. somewhat experimental. * `st7735r_setup.py` Assumes a Pyboard with an [Adafruit 1.8 inch TFT display](https://www.adafruit.com/product/358). + * `st7735r144_setup.py` For a Pyboard with an + [Adafruit 1.44 inch TFT display](https://www.adafruit.com/product/2088). ## 2.2 Dependencies @@ -280,7 +290,6 @@ Optional feature: * An STM32 implementation of [this optimisation](https://github.com/peterhinch/micropython-font-to-py/blob/master/writer/WRITER.md#224-a-performance-boost). - ### 2.2.1 Monochrome use A copy of the official driver for OLED displays using the SSD1306 chip is @@ -294,7 +303,6 @@ in this repo but may be found here: The Sharp display is supported in `drivers/sharp`. See [README](/drivers/sharp/README.md) and demos. - ### 2.2.2 Color use Drivers for Adafruit 0.96", 1.27" and 1.5" OLEDS are included in the source @@ -348,8 +356,8 @@ the border is at `[row-2, col-2]`. When a widget is drawn or updated (typically with its `value` method) it is not immediately displayed. To update the display `nanogui.refresh` is called: this -enables multiple updates to the `framebuf` contents before once copying the -buffer to the display. Postponement is for performance and provides a visually +enables multiple updates to the `FrameBuffer` contents before once copying the +buffer to the display. Postponement enhances performance providing a visually instant update. Text components of widgets are rendered using the `Writer` (monochrome) or @@ -393,10 +401,10 @@ then subsequently whenever a refresh is required. The method takes two args: ### 3.1.1 Setup file internals The file `color_setup.py` contains the hardware dependent code. It works as -described below, with the aim of allocating the `framebuf` before importing +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 import the driver: +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 @@ -461,27 +469,20 @@ If populating a label would cause it to extend beyond the screen boundary a warning is printed at the console. The label may appear at an unexpected place. The following is a complete "Hello world" script. ```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 -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, height) # Create a display instance +from color_setup import ssd # Create a display instance from gui.core.nanogui import refresh +from gui.core.writer import CWriter +from gui.core.colors import * + from gui.widgets.label import Label +import gui.fonts.freesans20 as freesans20 + refresh(ssd) # Initialise and clear display. -from gui.core.writer import CWriter # Import other modules -import gui.fonts.freesans20 as freesans20 # Font -GREEN = SSD.rgb(0, 255, 0) # Define colors -BLACK = 0 CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it wri = CWriter(ssd, freesans20, GREEN, BLACK, verbose=False) wri.set_clip(True, True, False) - # End of boilerplate code. This is our application: + +# End of boilerplate code. This is our application: Label(wri, 2, 2, 'Hello world!') refresh(ssd) ``` @@ -807,11 +808,14 @@ the oldest (topmost) being discarded as required. 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 for the transfer of an external frame buffer to the device and -little else. Such a transfer typically takes a few tens of milliseconds. Many -driver chips support graphics primitives in hardware. In performance orientated -applications such as games, drivers using these capabilities will be faster -than those provided here. +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 @@ -823,9 +827,11 @@ 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 `framebuf`. If the hardware does not support this, conversion to the +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. -To maximise update speed consider using native, viper or assembler. +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: diff --git a/color_setup/st7735r144_setup.py b/color_setup/st7735r144_setup.py index fd13ccf..59a2ce8 100644 --- a/color_setup/st7735r144_setup.py +++ b/color_setup/st7735r144_setup.py @@ -30,12 +30,9 @@ import gc from drivers.st7735r.st7735r144 import ST7735R as SSD -height = 128 -width = 128 - 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, baudrate=12_000_000) gc.collect() # Precaution before instantiating framebuf -ssd = SSD(spi, pcs, pdc, prst, height, width) # Create a display instance +ssd = SSD(spi, pcs, pdc, prst) # Create a display instance diff --git a/color_setup/st7735r_setup.py b/color_setup/st7735r_setup.py index adaa37e..13ea8d9 100644 --- a/color_setup/st7735r_setup.py +++ b/color_setup/st7735r_setup.py @@ -29,12 +29,9 @@ import gc from drivers.st7735r.st7735r import ST7735R as SSD -height = 128 -width = 160 - 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, baudrate=12_000_000) gc.collect() # Precaution before instantiating framebuf -ssd = SSD(spi, pcs, pdc, prst, height, width) # Create a display instance +ssd = SSD(spi, pcs, pdc, prst) # Create a display instance diff --git a/drivers/st7735r/README.md b/drivers/st7735r/README.md new file mode 100644 index 0000000..a135a52 --- /dev/null +++ b/drivers/st7735r/README.md @@ -0,0 +1,15 @@ +# 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. diff --git a/drivers/st7735r/st7735r.py b/drivers/st7735r/st7735r.py index 7602a5c..a3a2549 100644 --- a/drivers/st7735r/st7735r.py +++ b/drivers/st7735r/st7735r.py @@ -1,4 +1,4 @@ -# st7735r.py Driver for ST7735R LCD displays for nano-gui +# 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 @@ -12,6 +12,10 @@ # 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 @@ -19,7 +23,6 @@ import micropython # Datasheet para 8.4 scl write cycle 66ns == 15MHz - # _lcopy: copy a line in 8 bit format to one in 12 bit RGB444. para 9.8.20. # 2 bytes become 3 in destination. Source format: # < D7 D6 D5 D4 D3 D2 D1 D0> @@ -48,7 +51,7 @@ class ST7735R(framebuf.FrameBuffer): return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) # rst and cs are active low, SPI is mode 0 - def __init__(self, spi, cs, dc, rst, height, width): + def __init__(self, spi, cs, dc, rst, height=128, width=160): self._spi = spi self._rst = rst # Pins self._dc = dc diff --git a/drivers/st7735r/st7735r144.py b/drivers/st7735r/st7735r144.py index 5b65799..810248a 100644 --- a/drivers/st7735r/st7735r144.py +++ b/drivers/st7735r/st7735r144.py @@ -1,4 +1,4 @@ -# st7735r.py Driver for ST7735R LCD displays for nano-gui +# 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 @@ -23,8 +23,15 @@ import micropython # Datasheet para 8.4 scl write cycle 66ns == 15MHz +# _lcopy: copy a line in 8 bit format to one in 16 bit RGB565. +# 1 bytes becomes 2 in destination. Source format: +# < D7 D6 D5 D4 D3 D2 D1 D0> +# +# dest: +# <0 0 0 R02 R01 R00 0 0> + @micropython.viper -def _lcopy(dest:ptr8, source:ptr8, length:int): # TODO check this +def _lcopy(dest:ptr8, source:ptr8, length:int): n = 0 for x in range(length): c = source[x] @@ -34,29 +41,9 @@ def _lcopy(dest:ptr8, source:ptr8, length:int): # TODO check this n += 1 -# _lcopy: copy a line in 8 bit format to one in 12 bit RGB444. para 9.8.20. -# 2 bytes become 3 in destination. Source format: -# < D7 D6 D5 D4 D3 D2 D1 D0> -# -# dest: -# - -@micropython.viper -def _lcopy12(dest:ptr8, source:ptr8, length:int): - n = 0 - for x in range(0, length, 2): - c = source[x] - d = source[x + 1] - dest[n] = (c & 0xe0) | ((c & 0x1c) >> 1) # R0 G0 - n += 1 - dest[n] = ((c & 3) << 6) | ((d & 0xe0) >> 4) # B0 R1 - n += 1 - dest[n] = ((d & 0x1c) << 3) | ((d & 3) << 2) # G1 B1 - n += 1 - class ST7735R(framebuf.FrameBuffer): # Convert r, g, b in range 0-255 to an 8 bit colour value - # rrrgggbb. Converted to 12 bit on the fly. + # rrrgggbb. Converted to 16 bit on the fly. @staticmethod def rgb(r, g, b): return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) @@ -75,7 +62,6 @@ class ST7735R(framebuf.FrameBuffer): self.buffer = bytearray(self.height * self.width) self._mvb = memoryview(self.buffer) super().__init__(self.buffer, self.width, self.height, self.mode) - #self._linebuf = bytearray(int(self.width * 3 // 2)) # 12 bit color out self._linebuf = bytearray(self.width * 2) # 16 bit color out self._init() self.show() @@ -132,71 +118,28 @@ class ST7735R(framebuf.FrameBuffer): 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'\x03') # COLMOD 12 bit 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( 2 << 16 + self.width + 1, 4, 'big')) # CASET column address 0 start, 160/128 end - #wcd(b'\x2b', int.to_bytes(3 << 16 + self.height + 2, 4, 'big')) # RASET + 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 36ms on Pyboard D at stock frequency (160*128) - wcd = self._wcd + def show(self): # Blocks 38.6ms on Pyboard D at stock frequency wd = self.width ht = self.height lb = self._linebuf buf = self._mvb - start = 0 - row = self.height - 1 - while row >= 0: # For each line + self._dc(0) + self._cs(0) + 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 :], wd) # Copy and map colors (68us) - wcd(b'\x2a', int.to_bytes((3 << 16) + 160, 4, 'big')) # CASET column address 3 start, 160 end - wcd(b'\x2b', int.to_bytes(((row + 2) << 16) + row + 3, 4, 'big')) # RASET - wcd(b'\x2c', lb) # RAMWR - start += wd - row -= 1 - - #def show(self): # Blocks 36ms on Pyboard D at stock frequency (160*128) - #wd = self.width - #ht = self.height - #lb = self._linebuf - #buf = self._mvb - #self._dc(0) - #self._cs(0) - #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 :], wd) # Copy and map colors (68us) - #self._spi.write(lb) - #self._cs(1) - -#import machine -#import gc -#from time import sleep_ms -#from drivers.st7735r.st7735r import ST7735R as SSD -#height = 128 -#width = 128 # 160 - -#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, baudrate=12_000_000) -#gc.collect() # Precaution before instantiating framebuf -#ssd = SSD(spi, pcs, pdc, prst, height, width) # Create a display instance -#ssd.fill(0) -#ssd.show() -#sleep_ms(1000) -#ssd.line(0, 0, width - 1, 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() -#sleep_ms(2000) -#ssd.fill(0) -#ssd.show() -#ssd.line(0, 0, width - 1, height - 1, ssd.rgb(0, 255, 255)) # Green diagonal corner-to-corner -#ssd.rect(0, 0, 40, 40, ssd.rgb(0, 0, 255)) # Blue square at top left -#ssd.show() + self._spi.write(lb) + self._cs(1) diff --git a/gui/demos/aclock.py b/gui/demos/aclock.py index ce9c631..9c411c7 100644 --- a/gui/demos/aclock.py +++ b/gui/demos/aclock.py @@ -6,7 +6,7 @@ # Copyright (c) 2018-2020 Peter Hinch # Initialise hardware and framebuf before importing modules. -from color_setup import ssd, height # Create a display instance +from color_setup import ssd # Create a display instance from gui.core.nanogui import refresh from gui.widgets.label import Label from gui.widgets.dial import Dial, Pointer