From df2966b52d3290dc1b0a3385602673cb0da7499e Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 21 Jan 2019 14:53:51 +0000 Subject: [PATCH] Support for non 160x80 displays --- examples/image.py | 16 ++--- examples/shapes.py | 26 ++++---- library/ST7735/ST7735.py | 136 ++++++++++++++++++++++----------------- 3 files changed, 94 insertions(+), 84 deletions(-) diff --git a/examples/image.py b/examples/image.py index cd71d88..a2248cb 100644 --- a/examples/image.py +++ b/examples/image.py @@ -20,7 +20,6 @@ # THE SOFTWARE. import time import sys -import signal from PIL import Image import ST7735 as ST7735 @@ -31,18 +30,21 @@ if len(sys.argv) < 2: image_file = sys.argv[1] -WIDTH = ST7735.ST7735_TFTWIDTH -HEIGHT = ST7735.ST7735_TFTHEIGHT - # Create ST7735 LCD display class. disp = ST7735.ST7735( port=0, cs=0, dc=24, backlight=18, + rotation=90, spi_speed_hz=4000000 ) +WIDTH = disp.width +HEIGHT = disp.height + +print(WIDTH, HEIGHT) + # Initialize display. disp.begin() @@ -50,14 +52,10 @@ disp.begin() print('Loading image: {}...'.format(image_file)) image = Image.open(image_file) -# Resize the image and rotate it so matches the display. -# image = image.rotate(90).resize((WIDTH, HEIGHT)) +# Resize the image image = image.resize((WIDTH, HEIGHT)) # Draw the image on the display hardware. print('Drawing image') disp.display(image) - -print("Press Ctl+C to exit!") -signal.pause() diff --git a/examples/shapes.py b/examples/shapes.py index c7407f9..36de8b4 100644 --- a/examples/shapes.py +++ b/examples/shapes.py @@ -18,15 +18,12 @@ # 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 signal from PIL import Image from PIL import ImageDraw from PIL import ImageFont import ST7735 -WIDTH = ST7735.ST7735_TFTWIDTH -HEIGHT = ST7735.ST7735_TFTHEIGHT # Create ST7735 LCD display class. disp = ST7735.ST7735( @@ -34,21 +31,23 @@ disp = ST7735.ST7735( cs=0, dc=24, backlight=18, + rotation=90, spi_speed_hz=4000000 ) # Initialize display. disp.begin() +WIDTH = disp.width +HEIGHT = disp.height + + # Clear the display to a red background. # Can pass any tuple of red, green, blue values (from 0 to 255 each). -disp.clear((255, 0, 0)) - -# Alternatively can clear to a black screen by calling: -# disp.clear() - # Get a PIL Draw object to start drawing on the display buffer. -draw = disp.draw() +img = Image.new('RGB', (WIDTH, HEIGHT), color=(255, 0, 0)) + +draw = ImageDraw.Draw(img) # Draw a purple rectangle with yellow outline. draw.rectangle((10, 10, WIDTH-10, HEIGHT-10), outline=(255,255,0), fill=(255,0,255)) @@ -89,12 +88,9 @@ def draw_rotated_text(image, text, position, angle, font, fill=(255,255,255)): image.paste(rotated, position, rotated) # Write two lines of white text on the buffer, rotated 90 degrees counter clockwise. -draw_rotated_text(disp.buffer, 'Hello World!', (0, 0), 90, font, fill=(255,255,255)) -draw_rotated_text(disp.buffer, 'This is a line of text.', (10, HEIGHT-10), 0, font, fill=(255,255,255)) +draw_rotated_text(img, 'Hello World!', (0, 0), 90, font, fill=(255,255,255)) +draw_rotated_text(img, 'This is a line of text.', (10, HEIGHT-10), 0, font, fill=(255,255,255)) # Write buffer to display hardware, must be called to make things visible on the # display! -disp.display() - -print("Press Ctl+C to exit!") -signal.pause() +disp.display(img) diff --git a/library/ST7735/ST7735.py b/library/ST7735/ST7735.py index 95480ef..a132df3 100644 --- a/library/ST7735/ST7735.py +++ b/library/ST7735/ST7735.py @@ -21,22 +21,19 @@ import numbers import time import numpy as np -import atexit import spidev import RPi.GPIO as GPIO -from PIL import Image -from PIL import ImageDraw - -GPIO.setwarnings(False) -GPIO.setmode(GPIO.BCM) SPI_CLOCK_HZ = 16000000 # 4 MHz # Constants for interacting with display registers. -ST7735_TFTWIDTH = 160 -ST7735_TFTHEIGHT = 80 +ST7735_TFTWIDTH = 80 +ST7735_TFTHEIGHT = 160 + +ST7735_COLS = 132 +ST7735_ROWS = 162 ST7735_NOP = 0x00 ST7735_SWRESET = 0x01 @@ -113,11 +110,11 @@ def color565(r, g, b): """ return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) -def image_to_data(image): +def image_to_data(image, rotation=0): """Generator function to convert a PIL image to 16-bit 565 RGB bytes.""" # NumPy is much faster at doing this. NumPy code provided by: # Keith (https://www.blogger.com/profile/02555547344016007163) - pb = np.rot90(np.array(image.convert('RGB')), 1).astype('uint16') + pb = np.rot90(np.array(image.convert('RGB')), rotation // 90).astype('uint16') color = ((pb[:,:,0] & 0xF8) << 8) | ((pb[:,:,1] & 0xFC) << 3) | (pb[:,:,2] >> 3) return np.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist() @@ -125,15 +122,29 @@ class ST7735(object): """Representation of an ST7735 TFT LCD.""" def __init__(self, port, cs, dc, backlight=None, rst=None, width=ST7735_TFTWIDTH, - height=ST7735_TFTHEIGHT, spi_speed_hz=4000000): + height=ST7735_TFTHEIGHT, rotation=90, offset_left=None, offset_top=None, 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 port: SPI port number - :param cs: SPI CS number (0 or 1 for BCM + :param cs: SPI chip-select number (0 or 1 for BCM + :param backlight: Pin for controlling backlight + :param rst: Reset pin for ST7735 + :param width: Width of display connected to ST7735 + :param height: Height of display connected to ST7735 + :param rotation: Rotation of display connected to ST7735 + :param offset_left: COL offset in ST7735 memory + :param offset_top: ROW offset in ST7735 memory + :param invert: Invert display + :param spi_speed_hz: SPI speed (in Hz) + """ + + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) + self._spi = spidev.SpiDev(port, cs) self._spi.mode = 0 self._spi.lsbfirst = False @@ -141,8 +152,22 @@ class ST7735(object): self._dc = dc self._rst = rst - self.width = width - self.height = height + self._width = width + self._height = height + self._rotation = rotation + self._invert = invert + + # Default left offset to center display + if offset_left is None: + offset_left = (ST7735_COLS - width) // 2 + + self._offset_left = offset_left + + # Default top offset to center display + if offset_top is None: + offset_top = (ST7735_ROWS - height) // 2 + + self._offset_top = offset_top # Set DC as output. GPIO.setup(dc, GPIO.OUT) @@ -155,8 +180,6 @@ class ST7735(object): if rst is not None: GPIO.setup(rst, GPIO.OUT) - # Create an image buffer. - self.buffer = Image.new('RGB', (width, height)) def send(self, data, is_data=True, chunk_size=4096): """Write a byte or array of bytes to the display. Is_data parameter @@ -174,6 +197,14 @@ class ST7735(object): end = min(start+chunk_size, len(data)) self._spi.xfer(data[start:end]) + @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) @@ -243,7 +274,10 @@ class ST7735(object): self.command(ST7735_VMCTR1) # Power control self.data(0x0E) - self.command(ST7735_INVON) # Don't invert display + if self._invert: + self.command(ST7735_INVON) # Invert display + else: + self.command(ST7735_INVOFF) # Don't invert display self.command(ST7735_MADCTL) # Memory access control (directions) self.data(0xC8) # row addr/col addr, bottom to top refresh @@ -254,15 +288,15 @@ class ST7735(object): self.command(ST7735_CASET) # Column addr set self.data(0x00) # XSTART = 0 - self.data(132 - ST7735_TFTHEIGHT) - self.data(0x00) # XEND = 127 - self.data(ST7735_TFTHEIGHT - 1) + self.data(self._offset_left) + self.data(0x00) # XEND = ROWS - height + self.data(self._width + self._offset_left - 1) self.command(ST7735_RASET) # Row addr set self.data(0x00) # XSTART = 0 - self.data(161 - ST7735_TFTWIDTH) - self.data(0x00) # XEND = 159 - self.data(ST7735_TFTWIDTH - 1) + self.data(self._offset_top) + self.data(0x00) # XEND = COLS - width + self.data(self._height + self._offset_top - 1) self.command(ST7735_GMCTRP1) # Set Gamma @@ -313,7 +347,6 @@ class ST7735(object): """ self.reset() self._init() - atexit.register(self.clear_on_exit) def set_window(self, x0=0, y0=0, x1=None, y1=None): """Set the pixel address window for proceeding drawing commands. x0 and @@ -323,59 +356,42 @@ class ST7735(object): to width-1,height-1. """ if x1 is None: - x1 = self.width-1 + x1 = self._width-1 if y1 is None: - y1 = self.height-1 + y1 = self._height-1 - y0 += 26 - y1 += 26 + y0 += self._offset_top + y1 += self._offset_top - x0 += 1 - x1 += 1 + x0 += self._offset_left + x1 += self._offset_left self.command(ST7735_CASET) # Column addr set - self.data(y0 >> 8) - self.data(y0) # XSTART - self.data(y1 >> 8) - self.data(y1) # XEND - self.command(ST7735_RASET) # Row addr set self.data(x0 >> 8) - self.data(x0) # YSTART + self.data(x0) # XSTART self.data(x1 >> 8) - self.data(x1) # YEND + self.data(x1) # XEND + self.command(ST7735_RASET) # Row addr set + self.data(y0 >> 8) + self.data(y0) # YSTART + self.data(y1 >> 8) + self.data(y1) # YEND self.command(ST7735_RAMWR) # write to RAM - def display(self, image=None): - """Write the display buffer or provided image to the hardware. If no - image parameter is provided the display buffer will be written to the - hardware. If an image is provided, it should be RGB format and the - same dimensions as the display hardware. + def display(self, image): + """Write the provided image to the hardware. + + :param image: Should be RGB format and the same dimensions as the display hardware. + """ - # By default write the internal buffer to the display. - if image is None: - image = self.buffer # Set address bounds to entire display. self.set_window() # Convert image to array of 16bit 565 RGB data bytes. # Unfortunate that this copy has to occur, but the SPI byte writing # function needs to take an array of bytes and PIL doesn't natively # store images in 16-bit 565 RGB format. - pixelbytes = list(image_to_data(image)) + pixelbytes = list(image_to_data(image, self._rotation)) # Write data to hardware. self.data(pixelbytes) - def clear(self, color=(0,0,0)): - """Clear the image buffer to the specified RGB color (default black).""" - width, height = self.buffer.size - self.buffer.putdata([color]*(width*height)) - - def draw(self): - """Return a PIL ImageDraw instance for 2D drawing on the image buffer.""" - return ImageDraw.Draw(self.buffer) - - def clear_on_exit(self): - self.clear() - self.display() - -