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.
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()

View File

@@ -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)

View File

@@ -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()