Skip to content

Handling rotation and mirroring with MADCTL #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 71 additions & 33 deletions library/ST7735/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,28 +98,46 @@
ST7735_YELLOW = 0xFFE0 # 0b 11111 111111 00000
ST7735_WHITE = 0xFFFF # 0b 11111 111111 11111


def color565(r, g, b):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you intend to delete this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sorry.

I thought that it was legacy code from a prior version of image_to_data since there was no public method that receives a RGB565 color and the conversion is being handled with bit manipulation in numpy.

Should I restore it?

(Or delete the references in the tests... I didn't see the tests 🤦 .)

"""Convert red, green, blue components to a 16-bit 565 RGB value. Components
should be values 0 to 255.
"""
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)


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')), 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()
# Rotation modes
ST7735_MADCTL_MY = 0x80
ST7735_MADCTL_MX = 0x40
ST7735_MADCTL_MV = 0x20
ST7735_MADCTL_ROTATIONS = {
0: 0,
90: ST7735_MADCTL_MX | ST7735_MADCTL_MV,
180: ST7735_MADCTL_MY | ST7735_MADCTL_MX,
270: ST7735_MADCTL_MY | ST7735_MADCTL_MV,
}


def image_to_data(image):
if not isinstance(image, np.ndarray):
if image.mode != 'RGB':
image = image.convert('RGB')
pb = np.array(image, dtype='uint16')
elif image.dtype != 'uint16':
pb = image.astype('uint16')
else:
pb = image

# Mask and shift the 888 RGB into 565 RGB
red = (pb[..., [0]] & 0xF8) << 8
green = (pb[..., [1]] & 0xFC) << 3
blue = (pb[..., [2]] & 0xF8) >> 3

# Stick 'em together
result = red | green | blue

# Output the raw bytes
return result.byteswap().tobytes()


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, rotation=90, offset_left=None, offset_top=None, invert=True, spi_speed_hz=4000000):
height=ST7735_TFTHEIGHT, rotation=90, mirror=False, 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.
Expand All @@ -133,6 +151,7 @@ def __init__(self, port, cs, dc, backlight=None, rst=None, width=ST7735_TFTWIDTH
: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 mirror: Mirror 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
Expand All @@ -153,6 +172,7 @@ def __init__(self, port, cs, dc, backlight=None, rst=None, width=ST7735_TFTWIDTH
self._width = width
self._height = height
self._rotation = rotation
self._mirror = mirror
self._invert = invert

# Default left offset to center display
Expand Down Expand Up @@ -285,7 +305,10 @@ def _init(self):
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
madctl = ST7735_MADCTL_ROTATIONS[self._rotation] | 0x08 # rgb
if self._mirror:
madctl ^= ST7735_MADCTL_MX
self.data(madctl)

self.command(ST7735_COLMOD) # set color mode
self.data(0x05) # 16-bit color
Expand Down Expand Up @@ -371,30 +394,45 @@ def set_window(self, x0=0, y0=0, x1=None, y1=None):
x0 += self._offset_left
x1 += self._offset_left

if self._rotation % 180:
r0, r1 = x0, x1
c0, c1 = y0, y1
else:
c0, c1 = x0, x1
r0, r1 = y0, y1

self.command(ST7735_CASET) # Column addr set
self.data(x0 >> 8)
self.data(x0) # XSTART
self.data(x1 >> 8)
self.data(x1) # XEND
self.data(c0 >> 8)
self.data(c0) # COLSTART
self.data(c1 >> 8)
self.data(c1) # COLEND
self.command(ST7735_RASET) # Row addr set
self.data(y0 >> 8)
self.data(y0) # YSTART
self.data(y1 >> 8)
self.data(y1) # YEND
self.data(r0 >> 8)
self.data(r0) # ROWSTART
self.data(r1 >> 8)
self.data(r1) # ROWEND
self.command(ST7735_RAMWR) # write to RAM

def display(self, image):
"""Write the provided image to the hardware.
"""Write the provided image to the hardware. image could be a PIL image,
a numpy array or raw data. If image is a PIL image it must be the same
dimensions as the display hardware. If image is a numpy array it must be
shape (width, height, 3). If the image is raw data it could be a bytes
string or a uint8 list both of width*height*2 length of RGB565 colors
encoded in big-endian format.

:param image: Should be RGB format and the same dimensions as the display hardware.
:param image: Should the same dimensions as the display hardware.

"""
# 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, self._rotation))

if not isinstance(image, (list, bytes)):
# 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.
image = image_to_data(image)

# Write data to hardware.
self.data(pixelbytes)
self.data(image)