Skip to content

Commit 7e30426

Browse files
committed
Improve/simplify/modularize ST7789 SPI driver
Create helper functions for imshow() (could move these to a generic interface class for other display drivers to take advantage of) Move machine dependency from example to display driver Add extra checks for edge cases Remove unused functions and arguments Add clear() method Fix imshow() with float images to clip instead of wrap the range 0-1 Other minor optimizations
1 parent 361fbbe commit 7e30426

File tree

2 files changed

+157
-153
lines changed

2 files changed

+157
-153
lines changed

drivers/display/st7789_spi.py

Lines changed: 149 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import struct
55
from time import sleep_ms
6+
from machine import Pin, SPI
67
from ulab import numpy as np
78
import cv2
89

@@ -48,15 +49,6 @@
4849

4950
_ENCODE_POS = const(">HH")
5051

51-
_BIT7 = const(0x80)
52-
_BIT6 = const(0x40)
53-
_BIT5 = const(0x20)
54-
_BIT4 = const(0x10)
55-
_BIT3 = const(0x08)
56-
_BIT2 = const(0x04)
57-
_BIT1 = const(0x02)
58-
_BIT0 = const(0x01)
59-
6052
# Rotation tables
6153
# (madctl, width, height, xstart, ystart)[rotation % 4]
6254

@@ -121,22 +113,58 @@
121113
( b'\x29', b'\x00', 120) # Turn on the display
122114
)
123115

124-
class ST7789():
116+
class ST7789_SPI():
125117
"""
126-
ST7789 driver class base
118+
OpenCV SPI driver for ST7789 displays
119+
120+
Args:
121+
width (int): display width **Required**
122+
height (int): display height **Required**
123+
spi_id (int): SPI bus ID
124+
spi_baudrate (int): SPI baudrate, default 24MHz
125+
pin_sck (pin): SCK pin number
126+
pin_mosi (pin): MOSI pin number
127+
pin_miso (pin): MISO pin number
128+
pin_cs (pin): Chip Select pin number
129+
pin_dc (pin): Data/Command pin number
130+
rotation (int): Orientation of display
131+
- 0-Portrait, default
132+
- 1-Landscape
133+
- 2-Inverted Portrait
134+
- 3-Inverted Landscape
135+
color_order (int):
136+
- RGB: Red, Green Blue, default
137+
- BGR: Blue, Green, Red
138+
reverse_bytes_in_word (bool):
139+
- Enable if the display uses LSB byte order for color words
127140
"""
128-
def __init__(self, width, height, backlight, bright, rotation, color_order, reverse_bytes_in_word):
129-
"""
130-
Initialize display and backlight.
131-
"""
141+
def __init__(
142+
self,
143+
width,
144+
height,
145+
spi_id,
146+
spi_baudrate=24000000,
147+
pin_sck=None,
148+
pin_mosi=None,
149+
pin_miso=None,
150+
pin_cs=None,
151+
pin_dc=None,
152+
rotation=0,
153+
color_order=BGR,
154+
reverse_bytes_in_word=True,
155+
):
156+
# Store SPI arguments
157+
self.spi = SPI(spi_id, baudrate=spi_baudrate,
158+
sck=Pin(pin_sck, Pin.OUT) if pin_sck else None,
159+
mosi=Pin(pin_mosi, Pin.OUT) if pin_mosi else None,
160+
miso=Pin(pin_miso, Pin.IN) if pin_miso else None)
161+
self.cs = Pin(pin_cs, Pin.OUT, value=1) if pin_cs else None
162+
self.dc = Pin(pin_dc, Pin.OUT, value=1) if pin_dc else None
132163
# Initial dimensions and offsets; will be overridden when rotation applied
133164
self.width = width
134165
self.height = height
135166
self.xstart = 0
136167
self.ystart = 0
137-
# backlight pin
138-
self.backlight = backlight
139-
self._pwm_bl = True
140168
# Check display is known and get rotation table
141169
self.rotations = self._find_rotations(width, height)
142170
if not self.rotations:
@@ -147,18 +175,17 @@ def __init__(self, width, height, backlight, bright, rotation, color_order, reve
147175
# Colors
148176
self.color_order = color_order
149177
self.needs_swap = reverse_bytes_in_word
150-
# init the st7789
151-
self.init_cmds = _ST7789_INIT_CMDS
178+
# Reset the display
152179
self.soft_reset()
153180
# Yes, send init twice, once is not always enough
154-
self.send_init(self.init_cmds)
155-
self.send_init(self.init_cmds)
181+
self.send_init(_ST7789_INIT_CMDS)
182+
self.send_init(_ST7789_INIT_CMDS)
156183
# Initial rotation
157184
self._rotation = rotation % 4
158185
# Apply rotation
159186
self.rotation(self._rotation)
160187
# Create the framebuffer for the correct rotation
161-
self.buffer = np.zeros((self.rotations[self._rotation][2], self.rotations[self._rotation][1], 2), dtype=np.uint8)
188+
self.buffer = np.zeros((self.height, self.width, 2), dtype=np.uint8)
162189

163190
def send_init(self, commands):
164191
"""
@@ -220,112 +247,117 @@ def rotation(self, rotation):
220247
# TODO: Can we swap (modify) framebuffer width/height in the super() class?
221248
self._rotation = rotation
222249

223-
def imshow(self, image):
250+
def _get_common_roi_with_buffer(self, image):
224251
"""
225-
Display an image on the screen.
252+
Get the common region of interest (ROI) between the image and the
253+
display's internal buffer.
226254
227255
Args:
228-
image (Image): Image to display
256+
image (ndarray): Image to display
257+
258+
Returns:
259+
tuple: (image_roi, buffer_roi)
229260
"""
230-
# Check if image is a NumPy ndarray
261+
# Ensure image is a NumPy ndarray
231262
if type(image) is not np.ndarray:
232263
raise TypeError("Image must be a NumPy ndarray")
264+
265+
# Determing number of rows and columns in the image
266+
image_rows = image.shape[0]
267+
if len(image.shape) < 2:
268+
image_cols = 1
269+
else:
270+
image_cols = image.shape[1]
271+
272+
# Get the common ROI between the image and the buffer
273+
row_max = min(image_rows, self.height)
274+
col_max = min(image_cols, self.width)
275+
img_roi = image[:row_max, :col_max]
276+
buffer_roi = self.buffer[:row_max, :col_max]
277+
return img_roi, buffer_roi
278+
279+
def _convert_image_to_uint8(self, image):
280+
"""
281+
Convert the image to uint8 format if necessary.
282+
283+
Args:
284+
image (ndarray): Image to convert
285+
286+
Returns:
287+
Image: Converted image
288+
"""
289+
# Check if the image is already in uint8 format
290+
if image.dtype is np.uint8:
291+
return image
292+
293+
# Convert to uint8 format. This unfortunately requires creating a new
294+
# buffer for the converted image, which takes more memory
295+
if image.dtype == np.int8:
296+
return cv2.convertScaleAbs(image, alpha=1, beta=127)
297+
elif image.dtype == np.int16:
298+
return cv2.convertScaleAbs(image, alpha=1/255, beta=127)
299+
elif image.dtype == np.uint16:
300+
return cv2.convertScaleAbs(image, alpha=1/255)
301+
elif image.dtype == np.float:
302+
# This implementation creates an additional buffer from np.clip()
303+
# TODO: Find another solution that avoids an additional buffer
304+
return cv2.convertScaleAbs(np.clip(image, 0, 1), alpha=255)
305+
else:
306+
raise ValueError(f"Unsupported image dtype: {image.dtype}")
307+
308+
def _write_image_to_buffer_bgr565(self, image_roi, buffer_roi):
309+
"""
310+
Convert the image ROI to BGR565 format and write it to the buffer ROI.
311+
312+
Args:
313+
image_roi (ndarray): Image region of interest
314+
buffer_roi (ndarray): Buffer region of interest
315+
"""
316+
# Determine the number of channels in the image
317+
if len(image_roi.shape) < 3:
318+
ch = 1
319+
else:
320+
ch = image_roi.shape[2]
321+
322+
if ch == 1: # Grayscale
323+
buffer_roi = cv2.cvtColor(image_roi, cv2.COLOR_GRAY2BGR565, buffer_roi)
324+
elif ch == 2: # Already in BGR565 format
325+
buffer_roi[:] = image_roi
326+
elif ch == 3: # BGR
327+
buffer_roi = cv2.cvtColor(image_roi, cv2.COLOR_BGR2BGR565, buffer_roi)
328+
else:
329+
raise ValueError("Image must be 1, 2 or 3 channels (grayscale, BGR565, or BGR)")
233330

234-
# Ensure image is 3D (row, col, ch) by reshaping if necessary
235-
ndim = len(image.shape)
236-
if ndim == 1:
237-
image = image.reshape((image.shape[0], 1, 1))
238-
elif ndim == 2:
239-
image = image.reshape((image.shape[0], image.shape[1], 1))
240-
241-
# Determine number of rows, columns, and channels
242-
row, col, ch = image.shape
243-
244-
# Crop input image to match display size
245-
row_max = min(row, self.height)
246-
col_max = min(col, self.width)
247-
img_cropped = image[:row_max, :col_max]
248-
249-
# Crop the buffer if image is smaller than the display
250-
row_max = min(row_max, self.buffer.shape[0])
251-
col_max = min(col_max, self.buffer.shape[1])
252-
buffer_cropped = self.buffer[:row_max, :col_max]
253-
254-
# Check dtype and convert to uint8 if necessary
255-
if img_cropped.dtype is not np.uint8:
256-
# Have to create a new buffer for non-uint8 images
257-
if img_cropped.dtype == np.int8:
258-
temp = cv2.convertScaleAbs(img_cropped, alpha=1, beta=127)
259-
elif img_cropped.dtype == np.int16:
260-
temp = cv2.convertScaleAbs(img_cropped, alpha=1/255, beta=127)
261-
elif img_cropped.dtype == np.uint16:
262-
temp = cv2.convertScaleAbs(img_cropped, alpha=1/255)
263-
elif img_cropped.dtype == np.float:
264-
# Standard OpenCV will clamp values to 0-1 using convertTo(),
265-
# but this implementation wraps instead
266-
temp = np.asarray(img_cropped * 255, dtype=np.uint8)
267-
img_cropped = temp
268-
269-
# Convert image to BGR565 format
270-
if ch == 3: # BGR
271-
buffer_cropped = cv2.cvtColor(img_cropped, cv2.COLOR_BGR2BGR565, buffer_cropped)
272-
elif ch == 1: # Grayscale
273-
buffer_cropped = cv2.cvtColor(img_cropped, cv2.COLOR_GRAY2BGR565, buffer_cropped)
274-
else: # Already in BGR565 format
275-
buffer_cropped[:] = img_cropped
276-
277-
# Write to display. Swap bytes if needed
331+
def imshow(self, image):
332+
"""
333+
Display a NumPy image on the screen.
334+
335+
Args:
336+
image (ndarray): Image to display
337+
"""
338+
# Get the common ROI between the image and internal display buffer
339+
image_roi, buffer_roi = self._get_common_roi_with_buffer(image)
340+
341+
# Ensure the image is in uint8 format
342+
image_roi = self._convert_image_to_uint8(image_roi)
343+
344+
# Convert the image to BGR565 format and write it to the buffer
345+
self._write_image_to_buffer_bgr565(image_roi, buffer_roi)
346+
347+
# Write buffer to display. Swap bytes if needed
278348
if self.needs_swap:
279349
self._write(None, self.buffer[:, :, ::-1])
280350
else:
281351
self._write(None, self.buffer)
282352

283-
class ST7789_SPI(ST7789):
284-
"""
285-
ST7789 driver class for SPI bus devices
286-
287-
Args:
288-
spi (bus): bus object **Required**
289-
width (int): display width **Required**
290-
height (int): display height **Required**
291-
reset (pin): reset pin
292-
cs (pin): cs pin
293-
dc (pin): dc pin
294-
backlight (pin) or (pwm): backlight pin
295-
- can be type Pin (digital), PWM or None
296-
bright (value): Initial brightness level; default 'on'
297-
- a (float) between 0 and 1 if backlight is pwm
298-
- otherwise (bool) or (int) for pin value()
299-
rotation (int): Orientation of display
300-
- 0-Portrait, default
301-
- 1-Landscape
302-
- 2-Inverted Portrait
303-
- 3-Inverted Landscape
304-
color_order (int):
305-
- RGB: Red, Green Blue, default
306-
- BGR: Blue, Green, Red
307-
reverse_bytes_in_word (bool):
308-
- Enable if the display uses LSB byte order for color words
309-
"""
310-
def __init__(
311-
self,
312-
spi,
313-
width,
314-
height,
315-
reset=None,
316-
cs=None,
317-
dc=None,
318-
backlight=None,
319-
bright=1,
320-
rotation=0,
321-
color_order=BGR,
322-
reverse_bytes_in_word=True,
323-
):
324-
self.spi = spi
325-
self.reset = reset
326-
self.cs = cs
327-
self.dc = dc
328-
super().__init__(width, height, backlight, bright, rotation, color_order, reverse_bytes_in_word)
353+
def clear(self):
354+
"""
355+
Clear the display by filling it with black color.
356+
"""
357+
# Clear the buffer by filling it with zeros (black)
358+
self.buffer[:] = 0
359+
# Write the buffer to the display
360+
self._write(None, self.buffer)
329361

330362
def _write(self, command=None, data=None):
331363
"""SPI write to the device: commands and data."""
@@ -339,21 +371,3 @@ def _write(self, command=None, data=None):
339371
self.spi.write(data)
340372
if self.cs:
341373
self.cs.on()
342-
343-
def hard_reset(self):
344-
"""
345-
Hard reset display.
346-
"""
347-
if self.cs:
348-
self.cs.off()
349-
if self.reset:
350-
self.reset.on()
351-
sleep_ms(10)
352-
if self.reset:
353-
self.reset.off()
354-
sleep_ms(10)
355-
if self.reset:
356-
self.reset.on()
357-
sleep_ms(120)
358-
if self.cs:
359-
self.cs.on()

examples/hello_opencv.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,16 @@
99
# implements an `imshow()` function that takes an NumPy array as input
1010
import st7789_spi as st7789
1111

12-
# The display driver requires some hardware-specific imports
13-
from machine import Pin, SPI
14-
15-
# Create SPI object
16-
spi = SPI(0, baudrate=24000000)
17-
1812
# Create display object
19-
display = st7789.ST7789_SPI(spi,
20-
240, 320,
21-
reset=None,
22-
cs=machine.Pin(17, Pin.OUT, value=1),
23-
dc=machine.Pin(16, Pin.OUT, value=1),
24-
backlight=None,
25-
bright=1,
26-
rotation=1,
27-
color_order=st7789.BGR,
28-
reverse_bytes_in_word=True)
13+
display = st7789.ST7789_SPI(width=240,
14+
height=320,
15+
spi_id=0,
16+
pin_cs=17,
17+
pin_dc=16,
18+
rotation=1,)
2919

3020
# Initialize an image (NumPy array) to be displayed
31-
img = np.zeros((240,320, 3), dtype=np.uint8)
21+
img = np.zeros((240, 320, 3), dtype=np.uint8)
3222

3323
# Images can be modified directly if desired. Here we set the top 50 rows of the
3424
# image to blue (255, 0, 0) in BGR format
@@ -41,7 +31,7 @@
4131
# same variable `img`, which has almost no overhead
4232
img = cv2.ellipse(img, (160, 120), (100, 50), 0, 0, 360, (0, 255, 0), -1)
4333

44-
# And the obligatory text, this time in red
34+
# And the obligatory "Hello OpenCV" text, this time in red
4535
img = cv2.putText(img, "Hello OpenCV!", (50, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
4636

4737
# Once we have an image ready to show, just call `imshow()` as you would in

0 commit comments

Comments
 (0)