Support for non 160x80 displays

This commit is contained in:
Phil Howard
2019-01-21 14:53:51 +00:00
parent 6f01a4dbb9
commit df2966b52d
3 changed files with 94 additions and 84 deletions

View File

@@ -20,7 +20,6 @@
# THE SOFTWARE. # THE SOFTWARE.
import time import time
import sys import sys
import signal
from PIL import Image from PIL import Image
import ST7735 as ST7735 import ST7735 as ST7735
@@ -31,18 +30,21 @@ if len(sys.argv) < 2:
image_file = sys.argv[1] image_file = sys.argv[1]
WIDTH = ST7735.ST7735_TFTWIDTH
HEIGHT = ST7735.ST7735_TFTHEIGHT
# Create ST7735 LCD display class. # Create ST7735 LCD display class.
disp = ST7735.ST7735( disp = ST7735.ST7735(
port=0, port=0,
cs=0, cs=0,
dc=24, dc=24,
backlight=18, backlight=18,
rotation=90,
spi_speed_hz=4000000 spi_speed_hz=4000000
) )
WIDTH = disp.width
HEIGHT = disp.height
print(WIDTH, HEIGHT)
# Initialize display. # Initialize display.
disp.begin() disp.begin()
@@ -50,14 +52,10 @@ disp.begin()
print('Loading image: {}...'.format(image_file)) print('Loading image: {}...'.format(image_file))
image = Image.open(image_file) image = Image.open(image_file)
# Resize the image and rotate it so matches the display. # Resize the image
# image = image.rotate(90).resize((WIDTH, HEIGHT))
image = image.resize((WIDTH, HEIGHT)) image = image.resize((WIDTH, HEIGHT))
# Draw the image on the display hardware. # Draw the image on the display hardware.
print('Drawing image') print('Drawing image')
disp.display(image) disp.display(image)
print("Press Ctl+C to exit!")
signal.pause()

View File

@@ -18,15 +18,12 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # 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 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
import signal
from PIL import Image from PIL import Image
from PIL import ImageDraw from PIL import ImageDraw
from PIL import ImageFont from PIL import ImageFont
import ST7735 import ST7735
WIDTH = ST7735.ST7735_TFTWIDTH
HEIGHT = ST7735.ST7735_TFTHEIGHT
# Create ST7735 LCD display class. # Create ST7735 LCD display class.
disp = ST7735.ST7735( disp = ST7735.ST7735(
@@ -34,21 +31,23 @@ disp = ST7735.ST7735(
cs=0, cs=0,
dc=24, dc=24,
backlight=18, backlight=18,
rotation=90,
spi_speed_hz=4000000 spi_speed_hz=4000000
) )
# Initialize display. # Initialize display.
disp.begin() disp.begin()
WIDTH = disp.width
HEIGHT = disp.height
# Clear the display to a red background. # Clear the display to a red background.
# Can pass any tuple of red, green, blue values (from 0 to 255 each). # 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. # 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 a purple rectangle with yellow outline.
draw.rectangle((10, 10, WIDTH-10, HEIGHT-10), outline=(255,255,0), fill=(255,0,255)) 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) image.paste(rotated, position, rotated)
# Write two lines of white text on the buffer, rotated 90 degrees counter clockwise. # 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(img, '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, '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 # Write buffer to display hardware, must be called to make things visible on the
# display! # display!
disp.display() disp.display(img)
print("Press Ctl+C to exit!")
signal.pause()

View File

@@ -21,22 +21,19 @@
import numbers import numbers
import time import time
import numpy as np import numpy as np
import atexit
import spidev import spidev
import RPi.GPIO as GPIO 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 SPI_CLOCK_HZ = 16000000 # 4 MHz
# Constants for interacting with display registers. # Constants for interacting with display registers.
ST7735_TFTWIDTH = 160 ST7735_TFTWIDTH = 80
ST7735_TFTHEIGHT = 80 ST7735_TFTHEIGHT = 160
ST7735_COLS = 132
ST7735_ROWS = 162
ST7735_NOP = 0x00 ST7735_NOP = 0x00
ST7735_SWRESET = 0x01 ST7735_SWRESET = 0x01
@@ -113,11 +110,11 @@ def color565(r, g, b):
""" """
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) 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.""" """Generator function to convert a PIL image to 16-bit 565 RGB bytes."""
# NumPy is much faster at doing this. NumPy code provided by: # NumPy is much faster at doing this. NumPy code provided by:
# Keith (https://www.blogger.com/profile/02555547344016007163) # 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) color = ((pb[:,:,0] & 0xF8) << 8) | ((pb[:,:,1] & 0xFC) << 3) | (pb[:,:,2] >> 3)
return np.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist() return np.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
@@ -125,15 +122,29 @@ class ST7735(object):
"""Representation of an ST7735 TFT LCD.""" """Representation of an ST7735 TFT LCD."""
def __init__(self, port, cs, dc, backlight=None, rst=None, width=ST7735_TFTWIDTH, 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 """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 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 optionally provide the GPIO pin number for the reset pin as the rst
parameter. parameter.
:param port: SPI port number :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 = spidev.SpiDev(port, cs)
self._spi.mode = 0 self._spi.mode = 0
self._spi.lsbfirst = False self._spi.lsbfirst = False
@@ -141,8 +152,22 @@ class ST7735(object):
self._dc = dc self._dc = dc
self._rst = rst self._rst = rst
self.width = width self._width = width
self.height = height 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. # Set DC as output.
GPIO.setup(dc, GPIO.OUT) GPIO.setup(dc, GPIO.OUT)
@@ -155,8 +180,6 @@ class ST7735(object):
if rst is not None: if rst is not None:
GPIO.setup(rst, GPIO.OUT) 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): def send(self, data, is_data=True, chunk_size=4096):
"""Write a byte or array of bytes to the display. Is_data parameter """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)) end = min(start+chunk_size, len(data))
self._spi.xfer(data[start:end]) 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): def command(self, data):
"""Write a byte or array of bytes to the display as command data.""" """Write a byte or array of bytes to the display as command data."""
self.send(data, False) self.send(data, False)
@@ -243,7 +274,10 @@ class ST7735(object):
self.command(ST7735_VMCTR1) # Power control self.command(ST7735_VMCTR1) # Power control
self.data(0x0E) 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.command(ST7735_MADCTL) # Memory access control (directions)
self.data(0xC8) # row addr/col addr, bottom to top refresh 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.command(ST7735_CASET) # Column addr set
self.data(0x00) # XSTART = 0 self.data(0x00) # XSTART = 0
self.data(132 - ST7735_TFTHEIGHT) self.data(self._offset_left)
self.data(0x00) # XEND = 127 self.data(0x00) # XEND = ROWS - height
self.data(ST7735_TFTHEIGHT - 1) self.data(self._width + self._offset_left - 1)
self.command(ST7735_RASET) # Row addr set self.command(ST7735_RASET) # Row addr set
self.data(0x00) # XSTART = 0 self.data(0x00) # XSTART = 0
self.data(161 - ST7735_TFTWIDTH) self.data(self._offset_top)
self.data(0x00) # XEND = 159 self.data(0x00) # XEND = COLS - width
self.data(ST7735_TFTWIDTH - 1) self.data(self._height + self._offset_top - 1)
self.command(ST7735_GMCTRP1) # Set Gamma self.command(ST7735_GMCTRP1) # Set Gamma
@@ -313,7 +347,6 @@ class ST7735(object):
""" """
self.reset() self.reset()
self._init() self._init()
atexit.register(self.clear_on_exit)
def set_window(self, x0=0, y0=0, x1=None, y1=None): def set_window(self, x0=0, y0=0, x1=None, y1=None):
"""Set the pixel address window for proceeding drawing commands. x0 and """Set the pixel address window for proceeding drawing commands. x0 and
@@ -323,59 +356,42 @@ class ST7735(object):
to width-1,height-1. to width-1,height-1.
""" """
if x1 is None: if x1 is None:
x1 = self.width-1 x1 = self._width-1
if y1 is None: if y1 is None:
y1 = self.height-1 y1 = self._height-1
y0 += 26 y0 += self._offset_top
y1 += 26 y1 += self._offset_top
x0 += 1 x0 += self._offset_left
x1 += 1 x1 += self._offset_left
self.command(ST7735_CASET) # Column addr set 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 >> 8)
self.data(x0) # YSTART self.data(x0) # XSTART
self.data(x1 >> 8) 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 self.command(ST7735_RAMWR) # write to RAM
def display(self, image=None): def display(self, image):
"""Write the display buffer or provided image to the hardware. If no """Write the provided image to the hardware.
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 :param image: Should be RGB format and the same dimensions as the display hardware.
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. # Set address bounds to entire display.
self.set_window() self.set_window()
# Convert image to array of 16bit 565 RGB data bytes. # Convert image to array of 16bit 565 RGB data bytes.
# Unfortunate that this copy has to occur, but the SPI byte writing # 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 # function needs to take an array of bytes and PIL doesn't natively
# store images in 16-bit 565 RGB format. # 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. # Write data to hardware.
self.data(pixelbytes) 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()