Skip to content

Conversation

@jepler
Copy link

@jepler jepler commented Jul 31, 2020

This comes in several parts. Everything but the hypothetical adafruit_displayio_sharpmemory is in this PR:

Shim framebuffer protocol

Allows pure Python objects to implement the framebuffer protocol. However, because these displays are Python objects, they do not survive soft-reset.

A do-nothing implementation might read as follows:

class UFrame:
    def __init__(self, *, width=64, height=32, color_depth=8):
        if color_depth not in (1, 8, 16):
            raise ValueError("only supports 1, 8 or 16 bit depth")
        xwidth = (7 + width) // 8 if color_depth == 1 else width
        dtype = ulab.uint16 if color_depth == 16 else ulab.uint8
        self.buffer = ulab.zeros((height, xwidth), dtype=dtype)
        self.height = height
        self.width = width
        self.color_depth = color_depth
        print(self.width)

    def get_color_depth(self):
        return self.color_depth

    def get_buffer(self):
        return self.buffer

    def get_height(self):
        return self.height

    def get_width(self):
        return self.width

    def swapbuffers(self):
        pass

Supporting 1 (and probably 2 and 4) bpp displays

There was a COULDDO about this. It is handled by rounding the X coordinates to be computed to byte boundaries.

Bug fixing

I noticed or encountered bugs along the way.

framebufferio: Fix crash calling unimplemented get_brightness
framebufferio: Don't call swapbuffers unnecessarily
framebufferio: Set type to none when releasing

One I did NOT fix is that an 8x8 display causes an infinite loop. If 8x8 displays become interesting for displayio we can look at it again.

Shortcomings

I hard-coded the fact that the Sharp Memory Display needs pixels in the "reverse_pixels_in_byte" direction. This is subject to change, depending whether we merge the LSBfirst SPI feature branch. Ultimately, it might not be necessary for this PR, but all the "knobs" provided by displayio_display_core_construct should be turnable from C and Python implementations of framebuffers

adafruit_displayio_sharpmemory.py

This was a SUPER EASY conversion from the old framebuf version! I don't know what other kinds of displays haven't been converted yet, but this might make them all "pretty easy". Once we commit to this approcah, it needs to be librarified, added to bundle, etc.

--- adafruit_sharpmemorydisplay.py	2020-05-05 09:09:03.679323691 -0500
+++ displayio_sharpmemory.py	2020-07-31 15:34:56.000000000 -0500
@@ -67,7 +67,7 @@
     return result
 
 
-class SharpMemoryDisplay(adafruit_framebuf.FrameBuffer):
+class SharpMemoryDisplay:
     """A driver for sharp memory displays, you can use any size but the
     full display must be buffered in memory!"""
 
@@ -86,12 +85,26 @@
         # even tho technically this display is LSB, we have to flip the bits
         # when writing out SPI so lets just do flipping once, in the buffer
         self.buffer = bytearray((width // 8) * height)
-        super().__init__(self.buffer, width, height, buf_format=adafruit_framebuf.MHMSB)
 
         # Set the vcom bit to a defined state
         self._vcom = True
 
-    def show(self):
+        self.width = width
+        self.height = height
+
+    def get_color_depth(self):
+        return 1
+
+    def get_buffer(self):
+        return self.buffer
+
+    def get_height(self):
+        return self.height
+
+    def get_width(self):
+        return self.width
+
+    def swapbuffers(self):
         """write out the frame buffer via SPI, we use MSB SPI only so some
         bit-swapping is rquired. The display also uses inverted CS for some
         reason so we con't use bus_device"""
# The MIT License (MIT)
#
# Copyright (c) 2018 ladyada for Adafruit Industries
#
# 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.

# pylint: disable=line-too-long
"""
`adafruit_sharpmemorydisplay`
====================================================

A display control library for Sharp 'memory' displays

* Author(s): ladyada

Implementation Notes
--------------------

**Hardware:**

* `Adafruit SHARP Memory Display Breakout - 1.3 inch 144x168 Monochrome <https://www.adafruit.com/product/3502>`_

* `Adafruit SHARP Memory Display Breakout - 1.3 inch 96x96 Monochrome <https://www.adafruit.com/product/1393>`_

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
  https://github.com/adafruit/circuitpython/releases

"""
# pylint: enable=line-too-long

from micropython import const

__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_SharpMemoryDisplay.git"

_SHARPMEM_BIT_WRITECMD = const(0x80)  # in lsb
_SHARPMEM_BIT_VCOM = const(0x40)  # in lsb
_SHARPMEM_BIT_CLEAR = const(0x20)  # in lsb


def reverse_bit(num):
    """Turn an LSB byte to an MSB byte, and vice versa. Used for SPI as
    it is LSB for the SHARP, but 99% of SPI implementations are MSB only!"""
    result = 0
    for _ in range(8):
        result <<= 1
        result += num & 1
        num >>= 1
    return result


class SharpMemoryDisplay:
    """A driver for sharp memory displays, you can use any size but the
    full display must be buffered in memory!"""

    # pylint: disable=too-many-instance-attributes,abstract-method

    def __init__(self, spi, scs_pin, width, height, *, baudrate=8000000):
        self._scs_pin = scs_pin
        scs_pin.switch_to_output(value=True)
        self._baudrate = baudrate
        # The SCS pin is active HIGH so we can't use bus_device. exciting!
        self._spi = spi
        # prealloc for when we write the display
        self._buf = bytearray(1)

        # even tho technically this display is LSB, we have to flip the bits
        # when writing out SPI so lets just do flipping once, in the buffer
        self.buffer = bytearray((width // 8) * height)

        # Set the vcom bit to a defined state
        self._vcom = True

        self.width = width
        self.height = height

    def get_color_depth(self):
        return 1

    def get_buffer(self):
        return self.buffer

    def get_height(self):
        return self.height

    def get_width(self):
        return self.width

    def swapbuffers(self):
        """write out the frame buffer via SPI, we use MSB SPI only so some
        bit-swapping is rquired. The display also uses inverted CS for some
        reason so we con't use bus_device"""

        # CS pin is inverted so we have to do this all by hand
        while not self._spi.try_lock():
            pass
        self._spi.configure(baudrate=self._baudrate)
        self._scs_pin.value = True

        # toggle the VCOM bit
        self._buf[0] = _SHARPMEM_BIT_WRITECMD
        if self._vcom:
            self._buf[0] |= _SHARPMEM_BIT_VCOM
        self._vcom = not self._vcom
        self._spi.write(self._buf)

        slice_from = 0
        line_len = self.width // 8
        for line in range(self.height):
            self._buf[0] = reverse_bit(line + 1)
            self._spi.write(self._buf)
            self._spi.write(memoryview(self.buffer[slice_from : slice_from + line_len]))
            slice_from += line_len
            self._buf[0] = 0
            self._spi.write(self._buf)
        self._spi.write(self._buf)  # we send one last 0 byte
        self._scs_pin.value = False
        self._spi.unlock()

go.py

Import this in your program or repl to get the display going.

import board
import digitalio
import displayio
import adafruit_displayio_sharpmemory
import framebufferio

displayio.release_displays()


buf = adafruit_displayio_sharpmemory.SharpMemoryDisplay(spi=board.SPI(), scs_pin=digitalio.DigitalInOut(board.D3), width=144, height=168)
display = framebufferio.FramebufferDisplay(buf)

@jepler
Copy link
Author

jepler commented Jul 31, 2020

Displays that may not have displayio equivalents yet:

  • pcd8544
  • is31fl3731
  • ht16k33
  • max7219
  • multiple adafruit_epd devices

I'm a bit interested in the is31fl3731 except that I have a 2x2 array of them so that won't exactly use standard code.

@jepler
Copy link
Author

jepler commented Jul 31, 2020

Alternately, adafruit_framebuf.FrameBuffer could be changed so that it provides those required methods for all the old drivers 🤔

jepler added 7 commits August 2, 2020 16:47
If set_brightness was implemented but get_brightness wasn't, this
would hard fault
This avoids the message "Too many displays in use" when they are released
directly, rather than via release_displays().
This allows the device interface part of a framebuffer to be implemented
in pure Python.  A minimal and non-functional example:

class UFrame:
    def __init__(self, width=64, height=32, bpp=8):
        if bpp not in (8, 16):
            raise ValueError("only supports 8 or 16 bit depth")
        dtype = ulab.uint16 if bpp == 16 else ulab.uint8
        self.buffer = ulab.zeros((width, height), dtype=dtype)
        self.height = height
        self.width = width

    def get_buffer(self):
        return self.buffer

    def get_height(self):
        return self.height

    def get_width(self):
        return self.width

    def swapbuffers(self):
        pass
A bit of cheating; we need to add a protocol getter for
reverse_bytes_in_word
@jepler
Copy link
Author

jepler commented Aug 3, 2020

@tannewt I'd appreciate your feedback on this approach before I go further. The main trade-off I perceive is that these new displays implemented in Python cannot survive soft-reset.

@tannewt
Copy link
Member

tannewt commented Aug 3, 2020

I'm not a huge fan of this approach for sharp memory. I was originally thinking it'd be added as a new native display type because if the different protocol for which line needs refresh. Here are a few concerns I have with this implementation.

Using a Python object as the transmitter leads to two weird things:

  1. You cannot have a "default" display for a board that has a sharp display in-built because it doesn't work outside the VM.
  2. Python code is run in-between other code because displays are refreshed in the background by default. (pixelbuf is ok because it's triggered by blocking calls.)

I think I'd be ok with python vm only displays if it was able to show the error output once before the heap disappears. It still doesn't help the default case though.

Using framebuffer also makes me worry about the memory overhead. How many bytes does the largest display take? The display retains the state so we don't need to keep it ourselves. (For larger RGB24 displays we'll have no choice.)

That's my initial read on this. Want to chat this week about it?

@jepler jepler requested a review from tannewt August 5, 2020 22:44
@jepler
Copy link
Author

jepler commented Aug 6, 2020

The largest display is 400x240 @ 1bpp. With some extra bookkeeping info to make updating fast, it's 12721 bytes. Limor recommended keeping the whole framebuffer in RAM.

@jepler
Copy link
Author

jepler commented Aug 6, 2020

We are going to:

  • keep this as a framebuffer display
  • but move it in to the core (module name: sharpdisplay)
  • transmitting only dirty rows is a "would be nice to have"

@jepler jepler changed the title framebufferdisplay: Improvements allowing driving of Sharp Memory Displays framebufferdisplay: Allow implementation of framebuffers in Python Aug 6, 2020
@jepler
Copy link
Author

jepler commented Aug 7, 2020

I'm going to close this up and leave this branch, because not much of it is applicable to the new direction for the memory displays. If we do decide someday to enable non-core displayio displays, we might want to revive this.

@jepler jepler closed this Aug 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants