From bfba0b71a69255149b8a61e3b268c4a93998ca87 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 15 Jan 2021 16:54:29 +0000 Subject: [PATCH] Prior to merge. --- DRIVERS.md | 154 ++++++++++++++-- README.md | 2 +- color_setup/epd96_asyn.py | 41 +++++ color_setup/epd96_demo.py | 16 +- color_setup/waveshare_demo.py | 36 ++++ color_setup/waveshare_setup.py | 36 ++++ drivers/epaper/epaper2in7_fb.py | 302 ++++++++++++++++++++++++++++++++ drivers/epaper/epd29.py | 99 ++++++----- gui/demos/epd29_test.py | 14 +- gui/demos/waveshare_test.py | 125 +++++++++++++ images/epd_enable.png | Bin 0 -> 10384 bytes 11 files changed, 758 insertions(+), 67 deletions(-) create mode 100644 color_setup/epd96_asyn.py create mode 100644 color_setup/waveshare_demo.py create mode 100644 color_setup/waveshare_setup.py create mode 100644 drivers/epaper/epaper2in7_fb.py create mode 100644 gui/demos/waveshare_test.py create mode 100644 images/epd_enable.png diff --git a/DRIVERS.md b/DRIVERS.md index ee39bba..0e09ea4 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -1,7 +1,7 @@ # Display drivers for nano-gui -The nano-gui project currently supports three display technologies: OLED (color -and monochrome), color TFT, and monochrome Sharp displays. +The nano-gui project currently supports four display technologies: OLED (color +and monochrome), color TFT, monochrome Sharp displays and EPD (ePaper/eInk). # Contents @@ -23,10 +23,14 @@ and monochrome), color TFT, and monochrome Sharp displays.      6.4.1 [Micropower applications](./DRIVERS.md#641-micropower-applications) 6.5 [Resources](./DRIVERS.md#65-resources) 7. [ePaper displays](./DRIVERS.md#7-epaper-displays) - 7.1 [Waveshare eInk Display HAT](./DRIVERS.md#71-waveshare-eink-display-hat) + 7.1 [Adafruit flexible eInk Display](./DRIVERS.md#71-adafruit-flexible-eink-display)      7.1.1 [EPD constructor args](./DRIVERS.md#711-epd-constructor-args)      7.1.2 [EPD public methods](./DRIVERS.md#712-epd-public-methods) - 8. [Writing device drivers](./DRIVERS.md#8-writing-device-drivers) + 7.2 [Waveshare eInk Display HAT](./DRIVERS.md#71-waveshare-eink-display-hat) +      7.2.1 [EPD constructor args](./DRIVERS.md#711-epd-constructor-args) +      7.2.2 [EPD public methods](./DRIVERS.md#712-epd-public-methods) + 8. [EPD Asynchronous support](./DRIVERS.md#8-epd-asynchronous-support) + 9. [Writing device drivers](./DRIVERS.md#8-writing-device-drivers) ###### [Main README](./README.md) @@ -505,11 +509,91 @@ device retaining the image indefinitely. Some devices such as the Waveshare units perform the refresh internally. Earlier devices required the driver to perform this, tying up the CPU for the duration. -## 7.1 Waveshare eInk Display HAT +The drivers are compatible with `uasyncio`. One approach is to use synchronous +methods only and the standard demos (some of which use `uasyncio`) may be run. +However copying the framebuffer to the device blocks for some time - 250ms or +more - which may be problematic for applications which need to respond to +external events. A specific asynchronous mode provides support for reducing +blocking time. See [EPD Asynchronous support](./DRIVERS.md#8-epd-asynchronous-support). -This 2.7" 176*274 portrait mode display is designed for the Raspberry Pi. -Details [here](https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT). The driver -is cross-platform. +## 7.1 Adafruit flexible eInk Display + +The driver assumes an Adafruit 2.9 inch 296*128 pixel flexible +[display](https://www.adafruit.com/product/4262) interfaced via their +[interface breakout](https://www.adafruit.com/product/4224). + +This is currently my preferred ePaper setup, not least because the breakout +enables the display to be completely powered down. This facilitates micropower +applications: the host shuts down the display before going into deep sleep. + +The driver is cross platform and supports landscape or portrait mode. To keep +the buffer size down (to 4736 bytes) there is no greyscale support. + +##### Wiring + +The following assumes a Pyboard host. Pyboard pin numbers are based on hardware +SPI 2 and my arbitrary choice of GPIO. All may be changed and soft SPI may be +used. + +| Pyb | Breakout | +|:---:|:---------:| +| Vin | Vin (1) | +| Gnd | Gnd (3) | +| Y8 | MOSI (6) | +| Y6 | SCK (4) | +| Y4 | BUSY (11) | (Low = Busy) +| Y3 | RST (10) | +| Y2 | CS (7) | +| Y1 | DC (8) | + +In normal use the `ENA` pin (12) may be left unconnected. For micropower use, +see below. + +### 7.1.1 EPD 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. + * `busy` An initialised input pin. + * `landscape=True` By default the long axis is horizontal. + * `asyn=False` Setting this `True` invokes an asynchronous mode. See + [EPD Asynchronous support](./DRIVERS.md#8-epd-asynchronous-support). + +### 7.1.2 EPD public methods + +##### Synchronous methods + * `init` No args. Issues a hardware reset and initialises the hardware. This + is called by the constructor. It needs to explicitly be called to exit from a + deep sleep. + * `sleep` No args. Puts the display into deep sleep. If called while a refresh + is in progress it will block until the refresh is complete. `sleep` should be + called before a power down to avoid leaving the display in an abnormal state. + * `ready` No args. After issuing a `refresh` the device will become busy for + a period: `ready` status should be checked before issuing `refresh`. + * `wait_until_ready` No args. Pause until the device is ready. + +##### Asynchronous methods + * `updated` Asynchronous. No args. Pause until the framebuffer has been copied + to the display. + * `wait` Asynchronous. No args. Pause until the display refresh is complete. + + + +** POWER DOWN HARDWARE ** + +## 7.2 Waveshare eInk Display HAT + +This 2.7" 176*274 display is designed for the Raspberry Pi and is detailed +[here](https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT). + +I bought two of these units from different sources. Both have hardware issues +discussed [here](https://forum.micropython.org/viewtopic.php?f=2&t=9564). I +have failed to achieve consistent behaviour. Units behave perfectly one day and +fail the next. I published this driver on the assumption that I was sold +dubious Chinese clones and that genuine ones would be reliable. + +The driver is cross-platform. ##### Wiring @@ -520,7 +604,6 @@ connector is shown, with connections to a Pyboard to match `waveshare_setup.py`. Connections may be adapted for other MicroPython targets. The board may be powered from 5V or 3.3V: there is a regulator on board. -|:---:|:----:|:--:|:--:|:----:|:---:| | Pyb | | L | R | | Pyb | |:---:|:----:|:--:|:--:|:----:|:---:| | Vin | VIN | 2 | 1 | | | @@ -538,24 +621,69 @@ powered from 5V or 3.3V: there is a regulator on board. Pins 26-40 unused and omitted. -### 7.1.1 EPD constructor args +### 7.2.1 EPD constructor args * `spi` An initialised SPI bus instance. The device can support clock rates of upto 2MHz. * `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. * `busy` An initialised input pin. + * `landscape=False` By default the long axis is vertical. + * `asyn=False` -### 7.1.2 EPD public methods +### 7.2.2 EPD public methods + +##### Synchronous methods * `init` No args. Issues a hardware reset and initialises the hardware. This is called by the constructor. It needs to explicitly be called to exit from a deep sleep. - * `sleep` No args. Puts the display into deep sleep. + * `sleep` No args. Puts the display into deep sleep. If called while a refresh + is in progress it will block until the refresh is complete. `sleep` should be + called before a power down to avoid leaving the display in an abnormal state. * `ready` No args. After issuing a `refresh` the device will become busy for a period: `ready` status should be checked before issuing `refresh`. * `wait_until_ready` No args. Pause until the device is ready. -# 8. Writing device drivers +##### Asynchronous methods + * `updated` Asynchronous. No args. Pause until the framebuffer has been copied + to the display. + * `wait` Asynchronous. No args. Pause until the display refresh is complete. + +# 8. EPD Asynchronous support + +Normally when GUI code issues +```python +refresh(ssd) +``` +display data is copied to the device and a physical refresh is initiated. The +code blocks - typically for 250ms or more - before returning, with physical +refresh being performed by the display hardware and taking several seconds. +This blocking period, which may be longer on non-Pyboard hosts, is too long for +many `uasyncio` applications. + +If an `EPD` is instantiated with `asyn=True` the process of copying the data to +the device is performed by a task which periodically yields to the scheduler. +By default blocking is limited to around 30ms. + +An `updated` method allows user code to pause after issuing `refresh` before +modifying the content of the framebuf - at which time the old data has been +entirely copied to the hardware. The `wait` method will pause until any +physical update is complete. + +```python + while True: + # Normal procedure before refresh, but 10s sleep should mean it always returns immediately + await ssd.wait() + refresh(ssd) # Launches ._as_show() + await ssd.updated() + # Content has now been shifted out so coros can update + # framebuffer in background + evt.set() + evt.clear() + await asyncio.sleep(20) # Allow for slow refresh +``` + +# 9. 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 diff --git a/README.md b/README.md index 31c7339..6aabc17 100644 --- a/README.md +++ b/README.md @@ -516,7 +516,7 @@ Keyword only args: 11. `label=None` A text string will cause a `Label` to be drawn below the meter. An integer will create a `Label` of that width for later use. 12. `style=Meter.LINE` The pointer is a horizontal line. `Meter.BAR` causes a - vertical bar to be displayed. + vertical bar to be displayed. Much easier to read on monochrome displays. 13. `legends=None` If a tuple of strings is passed, `Label` instances will be displayed to the right hand side of the meter, starting at the bottom. E.G. `('0.0', '0.5', '1.0')` diff --git a/color_setup/epd96_asyn.py b/color_setup/epd96_asyn.py new file mode 100644 index 0000000..5dc19f7 --- /dev/null +++ b/color_setup/epd96_asyn.py @@ -0,0 +1,41 @@ +# epd96_asyn.py Config for asynchronous applications on 2.9" ePaper. +# Customise for your hardware config + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2020 Peter Hinch + +# Supports Adafruit 2.9" monochrome EPD with interface board connected to Pyboard. +# Interface breakout: https://www.adafruit.com/product/4224 +# Display: https://www.adafruit.com/product/4262 + +# 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. Adafruit schematic linked on the product web pagerefers to a different +# device. These are the pins on the physical board. +# Pyb Breakout +# Vin Vin (1) +# Gnd Gnd (3) +# Y8 MOSI (6) +# Y6 SCK (4) +# Y4 BUSY (11) (Low = Busy) +# Y3 RST (10) +# Y2 CS (7) +# Y1 DC (8) +import machine +import gc + +from drivers.epaper.epd29 import EPD 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) +pbusy = machine.Pin('Y4', machine.Pin.IN) + +# Baudrate. Adafruit use 1MHz at +# https://learn.adafruit.com/adafruit-eink-display-breakouts/circuitpython-code-2 +# Datasheet P35 indicates up to 10MHz. +spi = machine.SPI(2, baudrate=5_000_000) +gc.collect() # Precaution before instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, pbusy, asyn=True) # Create a display instance diff --git a/color_setup/epd96_demo.py b/color_setup/epd96_demo.py index 6975eaf..75e47c9 100644 --- a/color_setup/epd96_demo.py +++ b/color_setup/epd96_demo.py @@ -1,9 +1,12 @@ -# epd96_demo.py Allow standard demos to run on ePaper. Customise for your hardware config +# epd96_demo.py Allow standard demos to run on ePaper. +# Customise for your hardware config. +# Beware of running demos for long as they refresh the display more frequently +# than is advised by Adafruit. # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2020 Peter Hinch -# As written, supports Adafruit 2.9" monochrome EPD with interface board. +# Supports Adafruit 2.9" monochrome EPD with interface board connected to Pyboard. # Interface breakout: https://www.adafruit.com/product/4224 # Display: https://www.adafruit.com/product/4262 @@ -11,8 +14,8 @@ # when instantiating the frame buffer. The aim is to do this as early as # possible before importing other modules. -# WIRING. Adafruit schematic is incorrect in that it references a nonexistent -# SD card so that interface has an extra pin. +# WIRING. Adafruit schematic linked on the product web pagerefers to a different +# device. These are the pins on the physical board. # Pyb Breakout # Vin Vin (1) # Gnd Gnd (3) @@ -32,9 +35,10 @@ pcs = machine.Pin('Y2', machine.Pin.OUT_PP, value=1) prst = machine.Pin('Y3', machine.Pin.OUT_PP, value=1) pbusy = machine.Pin('Y4', machine.Pin.IN) -# Baudrate from https://learn.adafruit.com/adafruit-eink-display-breakouts/circuitpython-code-2 +# Baudrate. Adafruit use 1MHz at +# https://learn.adafruit.com/adafruit-eink-display-breakouts/circuitpython-code-2 # Datasheet P35 indicates up to 10MHz. -spi = machine.SPI(2, baudrate=1_000_000) +spi = machine.SPI(2, baudrate=5_000_000) gc.collect() # Precaution before instantiating framebuf ssd = SSD(spi, pcs, pdc, prst, pbusy) # Create a display instance ssd.demo_mode = True diff --git a/color_setup/waveshare_demo.py b/color_setup/waveshare_demo.py new file mode 100644 index 0000000..375fa1b --- /dev/null +++ b/color_setup/waveshare_demo.py @@ -0,0 +1,36 @@ +# waveshare_demo.py Allow standard demos to run on ePaper. Customise for your hardware config + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2020 Peter Hinch + +# As written, supports: +# Waveshare 2.7" 264h*176w monochrome ePaper display: +# https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT + +# 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 Pin numbers refer to RPI connector. +# Pyb ePaper +# Vin Vcc (2) +# Gnd Gnd (9) +# Y8 DIN MOSI (19) +# Y6 CLK SCK (23) +# Y4 BUSY (18) (Low = Busy) +# Y3 RST (11) +# Y2 CS (24) +# Y1 DC (22) +import machine +import gc + +from drivers.epaper.epaper2in7_fb import EPD 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) +pbusy = machine.Pin('Y4', machine.Pin.IN) +spi = machine.SPI(2, baudrate=4_000_000) # From https://github.com/mcauser/micropython-waveshare-epaper/blob/master/examples/2in9-hello-world/test.py +gc.collect() # Precaution before instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, pbusy, landscape=True) # Create a display instance +ssd.demo_mode = True diff --git a/color_setup/waveshare_setup.py b/color_setup/waveshare_setup.py new file mode 100644 index 0000000..b94a6ca --- /dev/null +++ b/color_setup/waveshare_setup.py @@ -0,0 +1,36 @@ +# waveshare_setup.py Customise for your hardware config + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2020 Peter Hinch + +# As written, supports: +# Waveshare 2.7" 264h*176w monochrome ePaper display: +# https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT + +# 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 Pin numbers refer to RPI connector. +# Pyb ePaper +# Vin Vcc (2) +# Gnd Gnd (9) +# Y8 DIN MOSI (19) +# Y6 CLK SCK (23) +# Y4 BUSY (18) (Low = Busy) +# Y3 RST (11) +# Y2 CS (24) +# Y1 DC (22) +import machine +import gc + +from drivers.epaper.epaper2in7_fb import EPD 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) +pbusy = machine.Pin('Y4', machine.Pin.IN) +spi = machine.SPI(2, baudrate=4_000_000) # From https://github.com/mcauser/micropython-waveshare-epaper/blob/master/examples/2in9-hello-world/test.py +gc.collect() # Precaution before instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, pbusy, landscape=False, asyn=True) # Create a display instance +#ssd.demo_mode = True diff --git a/drivers/epaper/epaper2in7_fb.py b/drivers/epaper/epaper2in7_fb.py new file mode 100644 index 0000000..572a3ea --- /dev/null +++ b/drivers/epaper/epaper2in7_fb.py @@ -0,0 +1,302 @@ +# epaper2in7_fb.py nanogui driver for ePpaper 2.7" display +# Tested with Pyboard linked to Raspberry Pi 2.7" E-Ink Display HAT +# EPD is subclassed from framebuf.FrameBuffer for use with Writer class and nanogui. +# Optimisations to reduce allocations and RAM use. + +# Copyright (c) Peter Hinch 2020 +# Released under the MIT license see LICENSE + +# Based on the following sources: +# https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT +# MicroPython Waveshare 2.7" Black/White GDEW027W3 e-paper display driver +# https://github.com/mcauser/micropython-waveshare-epaper referred to as "mcauser" +# https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd2in7.py ("official") + +import framebuf +import uasyncio as asyncio +from time import sleep_ms, ticks_ms, ticks_us, ticks_diff + +class EPD(framebuf.FrameBuffer): + # A monochrome approach should be used for coding this. The rgb method ensures + # nothing breaks if users specify colors. + @staticmethod + def rgb(r, g, b): + return int((r > 127) or (g > 127) or (b > 127)) + + def __init__(self, spi, cs, dc, rst, busy, landscape=False, asyn=False): + self._spi = spi + self._cs = cs # Pins + self._dc = dc + self._rst = rst + self._busy = busy + self._lsc = landscape + self._asyn = asyn + self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1). + self._updated = asyncio.Event() + # Dimensions in pixels. Waveshare code is portrait mode. + # Public bound variables required by nanogui. + self.width = 264 if landscape else 176 + self.height = 176 if landscape else 264 + self.demo_mode = False # Special mode enables demos to run + self._buffer = bytearray(self.height * self.width // 8) + self._mvb = memoryview(self._buffer) + mode = framebuf.MONO_VLSB if landscape else framebuf.MONO_HLSB + super().__init__(self._buffer, self.width, self.height, mode) + self.init() + + def _command(self, command, data=None): + self._dc(0) + self._cs(0) + self._spi.write(command) + self._cs(1) + if data is not None: + self._dc(1) + self._cs(0) + self._spi.write(data) + self._cs(1) + + def init(self): + # Hardware reset + self._rst(1) + sleep_ms(200) + self._rst(0) + sleep_ms(200) # 5ms in Waveshare code + self._rst(1) + sleep_ms(200) + # Initialisation + cmd = self._command + cmd(b'\x01', b'\x03\x00\x2B\x2B\x09') # POWER_SETTING: VDS_EN VDG_EN, VCOM_HV VGHL_LV[1] VGHL_LV[0], VDH, VDL, VDHR + cmd(b'\x06', b'\x07\x07\x17') # BOOSTER_SOFT_START + cmd(b'\xf8', b'\x60\xA5') # POWER_OPTIMIZATION + cmd(b'\xf8', b'\x89\xA5') + cmd(b'\xf8', b'\x90\x00') + cmd(b'\xf8', b'\x93\x2A') + cmd(b'\xf8', b'\xA0\xA5') + cmd(b'\xf8', b'\xA1\x00') + cmd(b'\xf8', b'\x73\x41') + cmd(b'\x16', b'\x00') # PARTIAL_DISPLAY_REFRESH + cmd(b'\x04') # POWER_ON + self.wait_until_ready() + cmd(b'\x00', b'\xAF') # PANEL_SETTING: KW-BF, KWR-AF, BWROTP 0f + cmd(b'\x30', b'\x3A') # PLL_CONTROL: 3A 100HZ, 29 150Hz, 39 200HZ 31 171HZ + cmd(b'\x50', b'\x57') # Vcom and data interval setting (PGH) + cmd(b'\x82', b'\x12') # VCM_DC_SETTING_REGISTER + sleep_ms(2) # No delay in official code + # Set LUT. Local bytes objects reduce RAM usage. + + # Values used by mcauser + #lut_vcom_dc =\ + #b'\x00\x00\x00\x0F\x0F\x00\x00\x05\x00\x32\x32\x00\x00\x02\x00'\ + #b'\x0F\x0F\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ + #b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + #lut_ww =\ + #b'\x50\x0F\x0F\x00\x00\x05\x60\x32\x32\x00\x00\x02\xA0\x0F\x0F'\ + #b'\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ + #b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' # R21H + #lut_bb =\ + #b'\xA0\x0F\x0F\x00\x00\x05\x60\x32\x32\x00\x00\x02\x50\x0F\x0F'\ + #b'\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ + #b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' # R24H b + + # Values from official code: + lut_vcom_dc =\ + b'\x00\x00\x00\x08\x00\x00\x00\x02\x60\x28\x28\x00\x00\x01\x00'\ + b'\x14\x00\x00\x00\x01\x00\x12\x12\x00\x00\x01\x00\x00\x00\x00'\ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + lut_ww =\ + b'\x40\x08\x00\x00\x00\x02\x90\x28\x28\x00\x00\x01\x40\x14\x00'\ + b'\x00\x00\x01\xA0\x12\x12\x00\x00\x01\x00\x00\x00\x00\x00\x00'\ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + lut_bb =\ + b'\x80\x08\x00\x00\x00\x02\x90\x28\x28\x00\x00\x01\x80\x14\x00'\ + b'\x00\x00\x01\x50\x12\x12\x00\x00\x01\x00\x00\x00\x00\x00\x00'\ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + # Both agree on this: + lut_bw = lut_ww # R22H r + lut_wb = lut_bb # R23H w + cmd(b'\x20', lut_vcom_dc) # LUT_FOR_VCOM vcom + cmd(b'\x21', lut_ww) # LUT_WHITE_TO_WHITE ww -- + cmd(b'\x22', lut_bw) # LUT_BLACK_TO_WHITE bw r + cmd(b'\x23', lut_bb) # LUT_WHITE_TO_BLACK wb w + cmd(b'\x24', lut_wb) # LUT_BLACK_TO_BLACK bb b + print('Init Done.') + + def wait_until_ready(self): + sleep_ms(50) + t = ticks_ms() + while not self.ready(): + sleep_ms(100) + dt = ticks_diff(ticks_ms(), t) + print('wait_until_ready {}ms {:5.1f}mins'.format(dt, dt/60_000)) + + async def wait(self): + while not self.ready(): + await asyncio.sleep_ms(100) + + # Pause until framebuf has been copied to device. + async def updated(self): + await self._updated.wait() + + # For polling in asynchronous code. Just checks pin state. + # 0 == busy. Comment in official code is wrong. Code is correct. + def ready(self): + return not(self._as_busy or (self._busy() == 0)) # 0 == busy + + async def _as_show(self, buf1=bytearray(1)): + mvb = self._mvb + send = self._spi.write + cmd = self._command + cmd(b'\x10') # DATA_START_TRANSMISSION_1 + self._dc(1) # For some reason don't need to deassert CS here + buf1[0] = 0xff + t = ticks_ms() + for i in range(len(mvb)): + self._cs(0) # but do when copying the framebuf + send(buf1) + if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20): + await asyncio.sleep_ms(0) + t = ticks_ms() + self._cs(1) + cmd(b'\x13') # DATA_START_TRANSMISSION_2 not in datasheet + + self._dc(1) + # Necessary to deassert CS after each byte otherwise display does not + # clear down correctly + t = ticks_ms() + if self._lsc: # Landscape mode + wid = self.width + tbc = self.height // 8 # Vertical bytes per column + iidx = wid * (tbc - 1) # Initial index + idx = iidx # Index into framebuf + vbc = 0 # Current vertical byte count + hpc = 0 # Horizontal pixel count + for i in range(len(mvb)): + self._cs(0) + buf1[0] = mvb[idx] # INVERSION HACK ~data + send(buf1) + self._cs(1) + idx -= self.width + vbc += 1 + vbc %= tbc + if not vbc: + hpc += 1 + idx = iidx + hpc + if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20): + await asyncio.sleep_ms(0) + t = ticks_ms() + else: + for i, b in enumerate(mvb): + self._cs(0) + buf1[0] = b # INVERSION HACK ~data + send(buf1) + self._cs(1) + if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20): + await asyncio.sleep_ms(0) + t = ticks_ms() + + self._updated.set() # framebuf has now been copied to the device + self._updated.clear() + cmd(b'\x12') # DISPLAY_REFRESH + await asyncio.sleep(1) + while self._busy() == 0: + await asyncio.sleep_ms(200) # Don't release lock until update is complete + self._as_busy = False + + # draw the current frame memory. Blocking time ~180ms + def show(self, buf1=bytearray(1)): + if self._asyn: + if self._as_busy: + raise RuntimeError('Cannot refresh: display is busy.') + self._as_busy = True + asyncio.create_task(self._as_show()) + return + t = ticks_us() + mvb = self._mvb + send = self._spi.write + cmd = self._command + cmd(b'\x10') # DATA_START_TRANSMISSION_1 + self._dc(1) # For some reason don't need to deassert CS here + buf1[0] = 0xff + for i in range(len(mvb)): + self._cs(0) # but do when copying the framebuf + send(buf1) + self._cs(1) + cmd(b'\x13') # DATA_START_TRANSMISSION_2 not in datasheet + + self._dc(1) + # Necessary to deassert CS after each byte otherwise display does not + # clear down correctly + if self._lsc: # Landscape mode + wid = self.width + tbc = self.height // 8 # Vertical bytes per column + iidx = wid * (tbc - 1) # Initial index + idx = iidx # Index into framebuf + vbc = 0 # Current vertical byte count + hpc = 0 # Horizontal pixel count + for _ in range(len(mvb)): + self._cs(0) + buf1[0] = mvb[idx] # INVERSION HACK ~data + send(buf1) + self._cs(1) + idx -= self.width + vbc += 1 + vbc %= tbc + if not vbc: + hpc += 1 + idx = iidx + hpc + else: + for b in mvb: + self._cs(0) + buf1[0] = b # INVERSION HACK ~data + send(buf1) + self._cs(1) + + cmd(b'\x12') # DISPLAY_REFRESH + te = ticks_us() + print('show time', ticks_diff(te, t)//1000, 'ms') + if not self.demo_mode: + # Immediate return to avoid blocking the whole application. + # User should wait for ready before calling refresh() + return + self.wait_until_ready() + sleep_ms(2000) # Give time for user to see result + + + # to wake call init() + def sleep(self): + self._as_busy = False + self.wait_until_ready() + cmd = self._command + cmd(b'\x50', b'\xf7') # From Waveshare code + cmd(b'\x02') # POWER_OFF + cmd(b'\x07', b'\xA5') # DEEP_SLEEP (Waveshare and mcauser) + self._rst(0) # According to schematic this turns off the power + +# Testing connections by toggling pins connected to 40-way connector and checking volts on small connector +# All OK except rst: a 1 level produced only about 1.6V as against 3.3V for all other I/O. +# Further the level on the 40-way connector read 2.9V as agains 3.3V for others. Suspect hardware problem, +# ordered a second unit from Amazon. +#import machine +#import gc + +#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) +#pbusy = machine.Pin('Y4', machine.Pin.IN) +## baudrate +## From https://github.com/mcauser/micropython-waveshare-epaper/blob/master/examples/2in9-hello-world/test.py 2MHz +## From https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd2in7.py 4MHz +#spi = machine.SPI(2, baudrate=2_000_000) +#gc.collect() # Precaution before instantiating framebuf +#epd = EPD(spi, pcs, pdc, prst, pbusy) # Create a display instance +#sleep_ms(100) +#epd.init() +#print('Initialised') +#epd.fill(1) # 1 seems to be white +#epd.show() +#sleep_ms(1000) +#epd.fill(0) +#epd.show() +#epd._rst(0) +#epd._dc(0) # Turn off power according to RPI code diff --git a/drivers/epaper/epd29.py b/drivers/epaper/epd29.py index 7e329c2..e5e033a 100644 --- a/drivers/epaper/epd29.py +++ b/drivers/epaper/epd29.py @@ -31,12 +31,13 @@ class EPD(framebuf.FrameBuffer): def rgb(r, g, b): return int((r > 127) or (g > 127) or (b > 127)) - def __init__(self, spi, cs, dc, rst, busy, asyn=False): + def __init__(self, spi, cs, dc, rst, busy, landscape=True, asyn=False): self._spi = spi self._cs = cs # Pins self._dc = dc self._rst = rst # Active low. self._busy = busy # Active low on IL0373 + self._lsc = landscape self._asyn = asyn # ._as_busy is set immediately on start of task. Cleared # when busy pin is logically false (physically 1). @@ -44,15 +45,15 @@ class EPD(framebuf.FrameBuffer): self._updated = asyncio.Event() # Public bound variables required by nanogui. # Dimensions in pixels as seen by nanogui (landscape mode). - self.width = 296 - self.height = 128 + self.width = 296 if landscape else 128 + self.height = 128 if landscape else 296 # Other public bound variable. # Special mode enables demos written for generic displays to run. self.demo_mode = False self._buffer = bytearray(self.height * self.width // 8) self._mvb = memoryview(self._buffer) - mode = framebuf.MONO_VLSB + mode = framebuf.MONO_VLSB if landscape else framebuf.MONO_HLSB super().__init__(self._buffer, self.width, self.height, mode) self.init() @@ -101,9 +102,9 @@ class EPD(framebuf.FrameBuffer): cmd(b'\x30', b'\x29') # Resolution 128w * 296h as required by IL0373 cmd(b'\x61', b'\x80\x01\x28') # Note hex(296) == 0x128 - # Set VCM_DC. 0 is datasheet default. I think Adafruit send 0x50 (-2.6V) rather than 0x12 (-1.0V) + # Set VCM_DC. Now clarified with Adafruit. # https://github.com/adafruit/Adafruit_CircuitPython_IL0373/issues/17 - cmd(b'\x82', b'\x12') # Set Vcom to -1.0V is my guess at Adafruit's intention. + cmd(b'\x82', b'\x12') # Set Vcom to -1.0V sleep_ms(50) print('Init Done.') @@ -136,24 +137,33 @@ class EPD(framebuf.FrameBuffer): dat = self._data cmd(b'\x13') t = ticks_ms() - wid = self.width - tbc = self.height // 8 # Vertical bytes per column - iidx = wid * (tbc - 1) # Initial index - idx = iidx # Index into framebuf - vbc = 0 # Current vertical byte count - hpc = 0 # Horizontal pixel count - for i in range(len(mvb)): - buf1[0] = mvb[idx] ^ 0xff - dat(buf1) - idx -= wid - vbc += 1 - vbc %= tbc - if not vbc: - hpc += 1 - idx = iidx + hpc - if not(i & 0x0f) and (ticks_diff(ticks_ms(), t) > _MAX_BLOCK): - await asyncio.sleep_ms(0) - t = ticks_ms() + if self._lsc: # Landscape mode + wid = self.width + tbc = self.height // 8 # Vertical bytes per column + iidx = wid * (tbc - 1) # Initial index + idx = iidx # Index into framebuf + vbc = 0 # Current vertical byte count + hpc = 0 # Horizontal pixel count + for i in range(len(mvb)): + buf1[0] = ~mvb[idx] + dat(buf1) + idx -= wid + vbc += 1 + vbc %= tbc + if not vbc: + hpc += 1 + idx = iidx + hpc + if not(i & 0x0f) and (ticks_diff(ticks_ms(), t) > _MAX_BLOCK): + await asyncio.sleep_ms(0) + t = ticks_ms() + else: + for i, b in enumerate(mvb): + buf1[0] = ~b + dat(buf1) + if not(i & 0x0f) and (ticks_diff(ticks_ms(), t) > _MAX_BLOCK): + await asyncio.sleep_ms(0) + t = ticks_ms() + cmd(b'\x11') # Data stop self._updated.set() self._updated.clear() @@ -174,7 +184,6 @@ class EPD(framebuf.FrameBuffer): asyncio.create_task(self._as_show()) return - # t = ticks_us() mvb = self._mvb cmd = self._command dat = self._data @@ -182,28 +191,32 @@ class EPD(framebuf.FrameBuffer): # busy pin low (True) and that it stays logically True until # refresh is complete. In my testing this doesn't happen. cmd(b'\x13') - wid = self.width - tbc = self.height // 8 # Vertical bytes per column - iidx = wid * (tbc - 1) # Initial index - idx = iidx # Index into framebuf - vbc = 0 # Current vertical byte count - hpc = 0 # Horizontal pixel count - for _ in range(len(mvb)): - buf1[0] = mvb[idx] ^ 0xff - dat(buf1) - idx -= wid - vbc += 1 - vbc %= tbc - if not vbc: - hpc += 1 - idx = iidx + hpc + if self._lsc: # Landscape mode + wid = self.width + tbc = self.height // 8 # Vertical bytes per column + iidx = wid * (tbc - 1) # Initial index + idx = iidx # Index into framebuf + vbc = 0 # Current vertical byte count + hpc = 0 # Horizontal pixel count + for _ in range(len(mvb)): + buf1[0] = ~mvb[idx] + dat(buf1) + idx -= wid + vbc += 1 + vbc %= tbc + if not vbc: + hpc += 1 + idx = iidx + hpc + else: + for b in mvb: + buf1[0] = ~b + dat(buf1) + cmd(b'\x11') # Data stop sleep_us(20) # Allow for data coming back: currently ignore this cmd(b'\x12') # DISPLAY_REFRESH # 258ms to get here on Pyboard D # Checking with scope, busy goes low now. For 4.9s. - # te = ticks_us() - # print('show time', ticks_diff(te, t)//1000, 'ms') if not self.demo_mode: # Immediate return to avoid blocking the whole application. # User should wait for ready before calling refresh() @@ -213,6 +226,8 @@ class EPD(framebuf.FrameBuffer): # to wake call init() def sleep(self): + self._as_busy = False + self.wait_until_ready() cmd = self._command # CDI: not sure about value or why we set this here. Copying Adafruit. cmd(b'\x50', b'\x17') diff --git a/gui/demos/epd29_test.py b/gui/demos/epd29_test.py index 24066a9..50adf84 100644 --- a/gui/demos/epd29_test.py +++ b/gui/demos/epd29_test.py @@ -3,7 +3,7 @@ # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2020 Peter Hinch -# color_setup must set landcsape False, asyn True and must not set demo_mode +# color_setup must set landcsape True, asyn True and must not set demo_mode import uasyncio as asyncio from color_setup import ssd from gui.core.writer import Writer @@ -70,9 +70,13 @@ async def meter(evt): wri = Writer(ssd, arial10, verbose=False) row = 10 col = 150 - m0 = Meter(wri, row, col, height = 80, width = 15, divisions = 4, legends=('0.0', '0.5', '1.0')) - m1 = Meter(wri, row, col + 50, height = 80, width = 15, divisions = 4, legends=('-1', '0', '+1')) - m2 = Meter(wri, row, col + 100, height = 80, width = 15, divisions = 4, legends=('-1', '0', '+1')) + args = {'height' : 80, + 'width' : 15, + 'divisions' : 4, + 'style' : Meter.BAR} + m0 = Meter(wri, row, col, legends=('0.0', '0.5', '1.0'), **args) + m1 = Meter(wri, row, col + 50, legends=('-1', '0', '+1'), **args) + m2 = Meter(wri, row, col + 100, legends=('-1', '0', '+1'), **args) random = xorshift64star(2**24 - 1) while True: steps = 10 @@ -114,6 +118,6 @@ try: except KeyboardInterrupt: # Defensive code: avoid leaving EPD hardware in an undefined state. print('Waiting for display to become idle') - ssd.wait_until_ready() # Synchronous code + ssd.sleep() # Synchronous code. May block for 5s if display is updating. finally: _ = asyncio.new_event_loop() diff --git a/gui/demos/waveshare_test.py b/gui/demos/waveshare_test.py new file mode 100644 index 0000000..287598a --- /dev/null +++ b/gui/demos/waveshare_test.py @@ -0,0 +1,125 @@ +# waveshare_test.py Demo program for nano_gui on an Waveshare ePaper screen +# https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2020 Peter Hinch + +# color_setup must set landcsape False, asyn True and must not set demo_mode +import uasyncio as asyncio +from color_setup import ssd +from gui.core.writer import Writer +from gui.core.nanogui import refresh +from gui.widgets.meter import Meter +from gui.widgets.label import Label + +# Fonts +import gui.fonts.arial10 as arial10 +import gui.fonts.courier20 as fixed +import gui.fonts.font6 as small + +# Some ports don't support uos.urandom. +# See https://github.com/peterhinch/micropython-samples/tree/master/random +def xorshift64star(modulo, seed = 0xf9ac6ba4): + x = seed + def func(): + nonlocal x + x ^= x >> 12 + x ^= ((x << 25) & 0xffffffffffffffff) # modulo 2**64 + x ^= x >> 27 + return (x * 0x2545F4914F6CDD1D) % modulo + return func + +async def fields(evt): + wri = Writer(ssd, fixed, verbose=False) + wri.set_clip(False, False, False) + textfield = Label(wri, 0, 2, wri.stringlen('longer')) + numfield = Label(wri, 25, 2, wri.stringlen('99.990'), bdcolor=None) + countfield = Label(wri, 0, 90, wri.stringlen('1')) + n = 1 + random = xorshift64star(65535) + while True: + for s in ('short', 'longer', '1', ''): + textfield.value(s) + numfield.value('{:5.2f}'.format(random() /1000)) + countfield.value('{:1d}'.format(n)) + n += 1 + await evt.wait() + +async def multi_fields(evt): + wri = Writer(ssd, small, verbose=False) + wri.set_clip(False, False, False) + + nfields = [] + dy = small.height() + 10 + y = 80 + col = 20 + width = wri.stringlen('99.990') + for txt in ('X:', 'Y:', 'Z:'): + Label(wri, y, 0, txt) + nfields.append(Label(wri, y, col, width, bdcolor=None)) # Draw border + y += dy + + random = xorshift64star(2**24 - 1) + while True: + for _ in range(10): + for field in nfields: + value = random() / 167772 + field.value('{:5.2f}'.format(value)) + await evt.wait() + +async def meter(evt): + wri = Writer(ssd, arial10, verbose=False) + args = {'height' : 80, + 'width' : 15, + 'divisions' : 4, + 'style' : Meter.BAR} + m0 = Meter(wri, 165, 2, legends=('0.0', '0.5', '1.0'), **args) + m1 = Meter(wri, 165, 62, legends=('-1', '0', '+1'), **args) + m2 = Meter(wri, 165, 122, legends=('-1', '0', '+1'), **args) + random = xorshift64star(2**24 - 1) + while True: + steps = 10 + for n in range(steps + 1): + m0.value(random() / 16777216) + m1.value(n/steps) + m2.value(1 - n/steps) + await evt.wait() + +async def main(): +# ssd.fill(1) +# ssd.show() +# await ssd.wait() + refresh(ssd, True) # Clear display + await ssd.wait() + print('Ready') + evt = asyncio.Event() + asyncio.create_task(meter(evt)) + asyncio.create_task(multi_fields(evt)) + asyncio.create_task(fields(evt)) + while True: + # Normal procedure before refresh, but 10s sleep should mean it always returns immediately + await ssd.wait() + refresh(ssd) # Launches ._as_show() + await ssd.updated() + # Content has now been shifted out so coros can update + # framebuffer in background + evt.set() + evt.clear() + await asyncio.sleep(9) # Allow for slow refresh + + +tstr = '''Runs the following tests, updates every 10s +fields() Label test with dynamic data. +multi_fields() More Labels. +meter() Demo of Meter object. +''' + +print(tstr) + +try: + asyncio.run(main()) +except KeyboardInterrupt: + print('Waiting for display to become idle') + ssd.wait_until_ready() # Synchronous code +finally: + _ = asyncio.new_event_loop() diff --git a/images/epd_enable.png b/images/epd_enable.png new file mode 100644 index 0000000000000000000000000000000000000000..b4c068d43e40e732c99c2af0afa4b3022ba0e24f GIT binary patch literal 10384 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 literal 0 HcmV?d00001