diff --git a/drivers/sharp/README.md b/drivers/sharp/README.md index c183393..d170f29 100644 --- a/drivers/sharp/README.md +++ b/drivers/sharp/README.md @@ -7,7 +7,7 @@ These monochrome SPI displays exist in three variants from Adafruit. 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 to do so. +supports all of these and I would expect this one also to do so. # 1. Display characteristics @@ -24,9 +24,9 @@ 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. However 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. +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 @@ -63,21 +63,68 @@ 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. `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` tests `nanogui.py`. To run the tests the fonts in the directory, `writer.py` and `nanogui.py` must -be copied to the device or frozen as bytecode. 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. +be copied to the device or frozen as bytecode. The `clack_batt.py` demo needs +`upower.py` from +[micropython-micropower](https://github.com/peterhinch/micropython-micropower). -# 3. Resources +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. + +# 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. + +# 5. Resources [Schematic for 2.7" unit](https://learn.adafruit.com/assets/94077) diff --git a/drivers/sharp/clock_batt.py b/drivers/sharp/clock_batt.py new file mode 100644 index 0000000..32ae7a9 --- /dev/null +++ b/drivers/sharp/clock_batt.py @@ -0,0 +1,87 @@ +# clock_batt.py Battery powered clock demo for Pyboard/Adafruit sharp 2.7" display + +# Copyright (c) 2020 Peter Hinch +# Released under the MIT license. See LICENSE + +# HARDWARE +# This assumes a Pybaord D in WBUS-DIP28 and powered by a LiPo cell +# WIRING +# Pyb SSD +# Vin Vin Pyboard D: Vin on DIP28 is an output when powered by LiPo +# Gnd Gnd +# Y8 DI +# Y6 CLK +# Y5 CS + + +# 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. + +import machine +import gc +from sharp import SHARP as SSD + +# Initialise hardware +pcs = machine.Pin('Y5', machine.Pin.OUT_PP, value=0) # Active high +spi = machine.SPI(2) +gc.collect() # Precaution before instantiating framebuf +ssd = SSD(spi, pcs) + +# Now import other modules +import upower +from nanogui import Dial, Pointer, refresh, Label +import pyb +import cmath +from writer import Writer + +# Fonts for Writer +import freesans20 as font_small +import arial35 as font_large + +refresh(ssd) # Initialise display. + +def aclock(): + rtc = pyb.RTC() + uv = lambda phi : cmath.rect(1, phi) # Return a unit vector of phase phi + pi = cmath.pi + days = ('Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat', 'Sun') + months = ('Jan', 'Feb', 'March', 'April', 'May', 'June', 'July', + 'Aug', 'Sept', 'Oct', 'Nov', 'Dec') + # Instantiate Writer + Writer.set_textpos(ssd, 0, 0) # In case previous tests have altered it + wri = Writer(ssd, font_small, verbose=False) + wri.set_clip(True, True, False) + wri_tim = Writer(ssd, font_large, verbose=False) + wri_tim.set_clip(True, True, False) + + # Instantiate displayable objects + dial = Dial(wri, 2, 2, height = 215, ticks = 12, bdcolor=None, pip=True) + lbltim = Label(wri_tim, 50, 230, '00.00.00') + lbldat = Label(wri, 100, 230, 100) + hrs = Pointer(dial) + mins = Pointer(dial) + secs = Pointer(dial) + + hstart = 0 + 0.7j # Pointer lengths and position at top + mstart = 0 + 0.92j + sstart = 0 + 0.92j + while True: + t = rtc.datetime() # (year, month, day, weekday, hours, minutes, seconds, subseconds) + hang = -t[4]*pi/6 - t[5]*pi/360 # Angles of hour and minute hands + mang = -t[5] * pi/30 + sang = -t[6] * pi/30 + if abs(hang - mang) < pi/360: # Avoid overlap of hands + hang += pi/18 + hrs.value(hstart * uv(hang)) + mins.value(mstart * uv(mang)) + secs.value(sstart * uv(sang)) + lbltim.value('{:02d}.{:02d}.{:02d}'.format(t[4], t[5], t[6])) + lbldat.value('{} {} {} {}'.format(days[t[3] - 1], t[2], months[t[1] - 1], t[0])) + refresh(ssd) + # Power saving: only refresh every 10s + for _ in range(10): + upower.lpdelay(1000) + ssd.update() # Toggle VCOM + +aclock() diff --git a/drivers/sharp/clocktest.py b/drivers/sharp/clocktest.py index f04e0de..7ca8382 100644 --- a/drivers/sharp/clocktest.py +++ b/drivers/sharp/clocktest.py @@ -1,36 +1,15 @@ # clocktest.py Test/demo program for Adafruit sharp 2.7" display # Copyright (c) 2020 Peter Hinch +# Released under the MIT license. See LICENSE -# The MIT License (MIT) - -# 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. - -# WIRING TODO +# WIRING # Pyb SSD -# 3v3 Vin +# Vin Vin Pyboard: Vin is an output when powered by USB # Gnd Gnd -# X1 DC -# X2 CS -# X3 Rst -# X6 CLK -# X8 DATA +# Y8 DI +# Y6 CLK +# Y5 CS # Demo of initialisation procedure designed to minimise risk of memory fail @@ -46,17 +25,19 @@ pcs = machine.Pin('Y5', machine.Pin.OUT_PP, value=0) # Active high spi = machine.SPI(2) gc.collect() # Precaution before instantiating framebuf ssd = SSD(spi, pcs) -from nanogui import Dial, Pointer, refresh, Label -refresh(ssd) # Initialise and clear display. # Now import other modules +from nanogui import Dial, Pointer, refresh, Label import cmath import utime from writer import Writer -# Font for Writer +# Fonts for Writer import freesans20 as font_small import arial35 as font_large + +refresh(ssd) # Initialise display. + def aclock(): uv = lambda phi : cmath.rect(1, phi) # Return a unit vector of phase phi pi = cmath.pi @@ -83,7 +64,9 @@ def aclock(): sstart = 0 + 0.92j while True: t = utime.localtime() - hrs.value(hstart * uv(-t[3]*pi/6 - t[4]*pi/360)) + # Add 0.5min offset to hour hand. This avoids a confusing display by + # ensuring hour and minute hand never exactly overlap + hrs.value(hstart * uv(-t[3]*pi/6 - t[4]*pi/360) - pi/720) mins.value(mstart * uv(-t[4] * pi/30)) secs.value(sstart * uv(-t[5] * pi/30)) lbltim.value('{:02d}.{:02d}.{:02d}'.format(t[3], t[4], t[5])) diff --git a/drivers/sharp/sharp.py b/drivers/sharp/sharp.py index 0417830..e80d039 100644 --- a/drivers/sharp/sharp.py +++ b/drivers/sharp/sharp.py @@ -24,22 +24,22 @@ _VCOM = const(2) class SHARP(framebuf.FrameBuffer): - def __init__(self, spi, pincs): + def __init__(self, spi, pincs, height=240, width=400, vcom=False): spi.init(baudrate=2_000_000, firstbit=machine.SPI.LSB) # Data sheet: should support 2MHz self._spi = spi self._pincs = pincs - self.height = 240 # Required by Writer class and nanogui - self.width = 400 + self.height = height # Required by Writer class and nanogui + self.width = width self._buffer = bytearray(self.height * self.width // 8) self._mvb = memoryview(self._buffer) super().__init__(self._buffer, self.width, self.height, framebuf.MONO_HMSB) self._cmd = bytearray(1) # Buffer for command. Holds current VCOM bit - self._cmd[0] = _WRITECMD + self._cmd[0] = _WRITECMD | _VCOM if vcom else _WRITECMD self._lno = bytearray(1) # Line no. self._dummy = bytearray(1) # Dummy (0) # .show should be called periodically to avoid frame inversion flag - # retaining the same value for long periods + # (VCOM) retaining the same value for long periods def show(self): spi = self._spi bpl = self.width // 8 # Bytes per line @@ -57,3 +57,11 @@ class SHARP(framebuf.FrameBuffer): spi.write(self._dummy) self._pincs(0) self._cmd[0] ^= _VCOM # Toggle frame inversion flag + + # Toggle the VCOM bit without changing the display. Power saving method. + def update(self): + self._pincs(1) + self._lno[0] = self._cmd[0] & _VCOM + self._spi.write(self._lno) + self._cmd[0] ^= _VCOM # Toggle frame inversion flag + self._pincs(0) diff --git a/drivers/sharp/sharptest.py b/drivers/sharp/sharptest.py index a3adcd7..be9621b 100644 --- a/drivers/sharp/sharptest.py +++ b/drivers/sharp/sharptest.py @@ -5,6 +5,13 @@ # Copyright (c) Peter Hinch 2020 # Released under the MIT license see LICENSE +# WIRING +# Pyb SSD +# Vin Vin Pyboard: Vin is an output when powered by USB +# Gnd Gnd +# Y8 DI +# Y6 CLK +# Y5 CS import machine from sharp import SHARP