33
44import struct
55from time import sleep_ms
6+ from machine import Pin , SPI
67from ulab import numpy as np
78import cv2
89
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
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 ()
0 commit comments