From 2805a719f2a74493d278d232ce9812ff84ad5aad Mon Sep 17 00:00:00 2001 From: Daniel Thompson Date: Tue, 21 Jan 2020 22:10:50 +0000 Subject: [PATCH] wasp: st7789: Add a simple proof-of-concept display driver --- Makefile | 4 +- README.md | 11 ++ micropython | 2 +- wasp/drivers/st7789.py | 310 +++++++++++++++++++++++++++++++++++++++++ wasp/pinetime.py | 19 +++ 5 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 wasp/drivers/st7789.py create mode 100644 wasp/pinetime.py diff --git a/Makefile b/Makefile index b61009c..4656b9c 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,9 @@ softdevice: micropython: $(MAKE) -C micropython/mpy-cross - $(MAKE) -C micropython/ports/nrf BOARD=$(BOARD) SD=s132 + $(MAKE) -C micropython/ports/nrf \ + BOARD=$(BOARD) SD=s132 \ + FROZEN_MPY_DIR=$(PWD)/wasp python3 -m nordicsemi dfu genpkg \ --dev-type 0x0052 \ --application micropython/ports/nrf/build-$(BOARD)-s132/firmware.hex \ diff --git a/README.md b/README.md index 8a626fe..4c0be7a 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,14 @@ Then use nRFConnect (for Android) to program micropython.zip. At the end of this process your watch may *look* dead but, if it works, you will be able to use the Nordic UART Service to access the MicroPython REPL. + +Drivers are, for the most part, an exercise for the reader but +there is a proof-of-concept display driver. To experiment try: + +~~~ +import pinetime, time +tft = pinetime.st7789() +tft.white() +time.sleep(2) +tft.black() +~~~ diff --git a/micropython b/micropython index 5457f42..ee18996 160000 --- a/micropython +++ b/micropython @@ -1 +1 @@ -Subproject commit 5457f42e7e128447e50a82720f7c321d4133dd44 +Subproject commit ee1899688b4d3d3f277f5cad61e06e5df496bc39 diff --git a/wasp/drivers/st7789.py b/wasp/drivers/st7789.py new file mode 100644 index 0000000..760af09 --- /dev/null +++ b/wasp/drivers/st7789.py @@ -0,0 +1,310 @@ +# Copyright (c) 2014 Adafruit Industries +# Author: Tony DiCola +# Extensive further hacking by Pimoroni and Daniel Thompson +# +# 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 time + +__version__ = '0.0.2' + +BG_SPI_CS_BACK = 0 +BG_SPI_CS_FRONT = 1 + +ST7789_NOP = 0x00 +ST7789_SWRESET = 0x01 +ST7789_RDDID = 0x04 +ST7789_RDDST = 0x09 + +ST7789_SLPIN = 0x10 +ST7789_SLPOUT = 0x11 +ST7789_PTLON = 0x12 +ST7789_NORON = 0x13 + +ST7789_INVOFF = 0x20 +ST7789_INVON = 0x21 +ST7789_DISPOFF = 0x28 +ST7789_DISPON = 0x29 + +ST7789_CASET = 0x2A +ST7789_RASET = 0x2B +ST7789_RAMWR = 0x2C +ST7789_RAMRD = 0x2E + +ST7789_PTLAR = 0x30 +ST7789_MADCTL = 0x36 +ST7789_COLMOD = 0x3A + +ST7789_FRMCTR1 = 0xB1 +ST7789_FRMCTR2 = 0xB2 +ST7789_FRMCTR3 = 0xB3 +ST7789_INVCTR = 0xB4 +# ILI9341_DFUNCTR = 0xB6 +ST7789_DISSET5 = 0xB6 + +ST7789_GCTRL = 0xB7 +ST7789_GTADJ = 0xB8 +ST7789_VCOMS = 0xBB + +ST7789_LCMCTRL = 0xC0 +ST7789_IDSET = 0xC1 +ST7789_VDVVRHEN = 0xC2 +ST7789_VRHS = 0xC3 +ST7789_VDVS = 0xC4 +ST7789_VMCTR1 = 0xC5 +ST7789_FRCTRL2 = 0xC6 +ST7789_CABCCTRL = 0xC7 + +ST7789_RDID1 = 0xDA +ST7789_RDID2 = 0xDB +ST7789_RDID3 = 0xDC +ST7789_RDID4 = 0xDD + +ST7789_GMCTRP1 = 0xE0 +ST7789_GMCTRN1 = 0xE1 + +ST7789_PWCTR6 = 0xFC + +class ST7789(object): + """Representation of an ST7789 TFT LCD.""" + + def __init__(self, spi, cs, dc, backlight=None, rst=None, width=240, + height=240, rotation=90, invert=True, spi_speed_hz=4000000): + """Create an instance of the display using SPI communication. + + Must provide the GPIO pin number for the D/C pin and the SPI driver. + + Can optionally provide the GPIO pin number for the reset pin as the rst parameter. + + :param spi: SPI instance to work with + :param cs: SPI chip-select Pin + :param backlight: Pin for controlling backlight + :param rst: reset Pin for ST7789 + :param width: Width of display connected to ST7789 + :param height: Height of display connected to ST7789 + :param rotation: Rotation of display connected to ST7789 + :param invert: Invert display + :param spi_speed_hz: SPI speed (in Hz) + + """ + + self._spi = spi + + self._cs = cs + self._dc = dc + self._rst = rst + self._width = width + self._height = height + self._rotation = rotation + self._invert = invert + + self._offset_left = 0 + self._offset_top = 0 + + # Initialize cs + self._cs.on() + + # Setup backlight as output (if provided). + self._backlight = backlight + if backlight is not None: + backlight.off() + time.sleep(0.1) + backlight.on() + + self.reset() + self._init() + + def send(self, data, is_data=True, chunk_size=4096): + """Write a byte or array of bytes to the display. Is_data parameter + controls if byte should be interpreted as display data (True) or command + data (False). Chunk_size is an optional size of bytes to write in a + single SPI transaction, with a default of 4096. + """ + # Set DC low for command, high for data. + self._dc.value(is_data) + # Convert scalar argument to list so either can be passed as parameter. + try: + data[0] + except: + data = [data & 0xFF] + data = bytearray(data) + # Write data a chunk at a time. + for start in range(0, len(data), chunk_size): + end = min(start + chunk_size, len(data)) + self._cs.off() + self._spi.write(data[start:end]) + self._cs.on() + + def set_backlight(self, value): + """Set the backlight on/off.""" + if self._backlight is not None: + self._backlight.value(value) + + @property + def width(self): + return self._width if self._rotation == 0 or self._rotation == 180 else self._height + + @property + def height(self): + return self._height if self._rotation == 0 or self._rotation == 180 else self._width + + def command(self, data): + """Write a byte or array of bytes to the display as command data.""" + self.send(data, False) + + def data(self, data): + """Write a byte or array of bytes to the display as display data.""" + self.send(data, True) + + def reset(self): + """Reset the display, if reset pin is connected.""" + if self._rst is not None: + self._rst.on() + time.sleep(0.500) + self._rst.off() + time.sleep(0.500) + self._rst.on() + time.sleep(0.500) + + def _init(self): + # Initialize the display. + + self.command(ST7789_SWRESET) # Software reset + time.sleep(0.150) # delay 150 ms + + self.command(ST7789_MADCTL) + self.data(0x70) + + self.command(ST7789_FRMCTR2) # Frame rate ctrl - idle mode + self.data(0x0C) + self.data(0x0C) + self.data(0x00) + self.data(0x33) + self.data(0x33) + + self.command(ST7789_COLMOD) + self.data(0x05) + + self.command(ST7789_GCTRL) + self.data(0x14) + + self.command(ST7789_VCOMS) + self.data(0x37) + + self.command(ST7789_LCMCTRL) # Power control + self.data(0x2C) + + self.command(ST7789_VDVVRHEN) # Power control + self.data(0x01) + + self.command(ST7789_VRHS) # Power control + self.data(0x12) + + self.command(ST7789_VDVS) # Power control + self.data(0x20) + + self.command(0xD0) + self.data(0xA4) + self.data(0xA1) + + self.command(ST7789_FRCTRL2) + self.data(0x0F) + + self.command(ST7789_GMCTRP1) # Set Gamma + self.data(0xD0) + self.data(0x04) + self.data(0x0D) + self.data(0x11) + self.data(0x13) + self.data(0x2B) + self.data(0x3F) + self.data(0x54) + self.data(0x4C) + self.data(0x18) + self.data(0x0D) + self.data(0x0B) + self.data(0x1F) + self.data(0x23) + + self.command(ST7789_GMCTRN1) # Set Gamma + self.data(0xD0) + self.data(0x04) + self.data(0x0C) + self.data(0x11) + self.data(0x13) + self.data(0x2C) + self.data(0x3F) + self.data(0x44) + self.data(0x51) + self.data(0x2F) + self.data(0x1F) + self.data(0x1F) + self.data(0x20) + self.data(0x23) + + if self._invert: + self.command(ST7789_INVON) # Invert display + else: + self.command(ST7789_INVOFF) # Don't invert display + + self.command(ST7789_SLPOUT) + + self.command(ST7789_DISPON) # Display on + time.sleep(0.100) # 100 ms + + def set_window(self, x0=0, y0=0, x1=None, y1=None): + """Set the pixel address window for proceeding drawing commands. x0 and + x1 should define the minimum and maximum x pixel bounds. y0 and y1 + should define the minimum and maximum y pixel bound. If no parameters + are specified the default will be to update the entire display from 0,0 + to width-1,height-1. + """ + if x1 is None: + x1 = self._width - 1 + + if y1 is None: + y1 = self._height - 1 + + y0 += self._offset_top + y1 += self._offset_top + + x0 += self._offset_left + x1 += self._offset_left + + self.command(ST7789_CASET) # Column addr set + self.data(x0 >> 8) + self.data(x0 & 0xFF) # XSTART + self.data(x1 >> 8) + self.data(x1 & 0xFF) # XEND + self.command(ST7789_RASET) # Row addr set + self.data(y0 >> 8) + self.data(y0 & 0xFF) # YSTART + self.data(y1 >> 8) + self.data(y1 & 0xFF) # YEND + self.command(ST7789_RAMWR) # write to RAM + + def white(self): + self.set_window() + for i in range(self._height): + self.data([255] * self._width * 2) + + def black(self): + self.set_window() + for i in range(self._height): + self.data([0] * self._width * 2) diff --git a/wasp/pinetime.py b/wasp/pinetime.py new file mode 100644 index 0000000..a0858f5 --- /dev/null +++ b/wasp/pinetime.py @@ -0,0 +1,19 @@ +from machine import Pin +from machine import SPI + +from drivers.st7789 import ST7789 + +def st7789(): + spi = SPI(0) + # Mode 3, maximum clock speed! + spi.init(polarity=1, phase=1, baudrate=8000000) + + # Extra pins required by the driver + cs = Pin("SPI_SS2", Pin.OUT) + dc = Pin("P18", Pin.OUT) + rst = Pin("P26", Pin.OUT) + bl = Pin("P22", Pin.OUT) + + tft = ST7789(spi, cs=cs, dc=dc, rst=rst) + bl.off() # active low + return tft