Can no longer drive ST7789V SPI TFT w/ firmware 3.8.0 (works for <= 3.6.7)

I’ve been working with a custom TFT driver for the ST7789V IPS TFT (320x240 res.) and it’s been smooth-sailing until I upgraded to firmware 3.8.0. At the moment the TFT should initialize, the IDE’s framebuffer view freezes and no image is displayed on the TFT (not even a flicker, actually). I am well aware that there have been significant changes to how TFT’s are handled post 3.6.7, hence my delay in testing the new firmware. However, I am stumped on how that would affect a display directly driven via SPI. I am supplying a bare-minimum set of python code (below) that can drive the display for firmware <= 3.6.7. FYI my actual main application is significantly more complex, however the setup & initialization is identical to the below code. Really hoping I am looking over a mundane detail.

Side Note: I did try using the new LCD driver with the ST7789V, but the image displayed shows signs of incorrect setup and initialization, which I kind of suspected would happen since I am not aware of any official support for this display. Given the changes made to the LCD driver, I’d definitely like to migrate, but my suspicion is that I’d need to modify the firmware to support the ST7789V.

Appreciate any insight!

Note: Code under “ST7789V Driver” should be copied to file system as a file named "ST7789V.py"

Main Python Application:

#TFT screen demo to steam frame buffer to a external SPI screen

#Original Author: Shane Gingell
#Modified to support ST7789V by: Cameron R. MANN (12/2020)
#MIT License (MIT)

import sensor, image, time
from pyb import SPI
from ST7789V import TFT

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.skip_frames()

clock = time.clock()

spi = SPI(2, SPI.MASTER, baudrate=54000000) #Create SPI bus

#Create instance of the screen driver
#NOTE: CS='P3' & DC='P8' hardcoded in "ST7789V.py"
screen = TFT(spi)

#Set window on screen to write to (x_start, Y_start, width, height),should match frambuffer size
screen.display_setup()
screen.set_window(0,0,320,240)

while(True):
    clock.tick()

    img = sensor.snapshot()

    screen.write_to_screen(img) #send framebuffer to screen

ST7789V Driver:

'''
Copyright (c) 2018 Out of the BOTS
MIT License (MIT) 

Original Author: Shane Gingell

Modified to support ST7789V by: Cameron R. MANN (12/2020)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
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.
'''

from pyb import Pin
import time
from ustruct import pack
from micropython import const


class TFT():
  _SWRESET = const(0x01) # Software Reset
  _SLPOUT = const(0x11) # Sleep Out
  _COLMOD = const(0x3A) # Colour Mode
  _DISPON = const(0x29) # Display On
  _MADCTL = const(0x36) # Memory Data Access
  _CASET = const(0x2A) # Column Address Set
  _RASET = const(0x2B) # Row Address set
  _RAMWR = const(0x2C) #write to screen memory
  _INVON = const(0x21) #CRM: inversion on

  def send_spi(self,data, is_data):
    self.dc.value(is_data) #set data/command pin
    self.cs.value(0)
    self.hspi.write(data)
    self.cs.value(1)

  def __init__(self, spi, cs='P3', dc='P8'):
    self.hspi = spi
    self.cs = Pin(cs, Pin.OUT)
    self.dc = Pin(dc, Pin.OUT)

  def display_setup(self): #eliminated if here...assuming ST7789V
      self.send_spi(bytearray([_SWRESET]), False) #software reset
      time.sleep(200)
      self.send_spi(bytearray([_SLPOUT]), False)  #sleep out
      time.sleep(200)
      self.send_spi(bytearray([_COLMOD]), False)  #set 16 bit colour
      self.send_spi(bytearray([0x55]),True)
      self.send_spi(bytearray([_DISPON]), False)  #display on
      self.send_spi(bytearray([_MADCTL]), False)  #set mode for writing to screen
      self.send_spi(bytearray([0b10110000]),True) #this was the mode that I used for my screen
      self.send_spi(bytearray([_CASET]),False)    #set x writng window
      self.send_spi(pack(">HH", 0, 240), True)   #to 0 to 159
      self.send_spi(bytearray([_RASET]),False)    #set y writing window
      self.send_spi(pack(">HH", 0, 320), True)   #to 0 to 12
      self.send_spi(bytearray([_INVON]),False)    #inversion on


  def set_window(self, x, y, width, height):
    x_end=x+width-1
    y_end=y+height-1
    self.send_spi(bytearray([_CASET]),False)  # set Column addr command
    self.send_spi(pack(">HH", x, x_end), True)  # x_end
    self.send_spi(bytearray([_RASET]),False)  # set Row addr command
    self.send_spi(pack(">HH", y, y_end), True)  # y_end

  def write_to_screen(self, data):
    self.send_spi(bytearray([_RAMWR]),False)  # set to write to RAM
    self.send_spi(data, True)                 # send data

Hi, you don’t need any of that custom python code anymore. The lcd driver handles that larger display now seamlessly.

See the new lcd driver documentation. You can just pass the lcd screen size and it will magically work. Also, you can enable triple buffering too.

This does mean the old code will no longer work. However, the new stuff is 1000% better.

Edit, I didn’t read your post fully. Yes, I only do some display setup. Feel free to execute whatever custom SPI bus commands you need to execute before driving the display.

Both pyb and the lcd driver use the same SPI bus struct. So, both can be used at the same time.

E.g. you can just send SPI commands to finish the display setup after init and before sending images.

You should create you SPI bus object first, then init the screen using lcd.init, then you should do whatever custom setup you need.

Once you call display this will start an interrupt chain callback to refresh the display if triple buffering is enabled. If triple buffering is not enabled then you can use the SPI bus freely.

As for backlight support. Note that the new lcd backlight driver assumes you have a pull up resistor on the lcd backlight. The new driver doesn’t touch the backlight pin unless you specify a value other than 255.

Regarding your pure python driver. Not sure what could have broken that. SPI has not changed from us. MicroPython was updated however.

A big change though is that we finally fixed the RGB565 inversion. So, that code won’t even work anymore since the RGB565 values will be inversed.

You need to use the integrated lcd driver.

Really appreciate the feedback…I will move forward with integrating the new LCD driver, taking into account your guidance. You already answered a few questions I would have had at the start, so thanks for that.

At least I know the “pure python” code should-have-maybe-worked (ha), but I agree, better to spend the time migrating to the improved LCD driver (versus wrestling with the old way). The performance gains sound phenomenal and I am genuinely excited to get it running.

I will update this thread as I progress!

Update: I was able to get the ST7789 running with the new LCD driver. Most of the effort went into debugging the final stage of the initialization sequence. Turns out a chunk of the code I was using previously (pure python driver) was conflicting with the X & Y Window configuration. I’ve posted the working example below. The batch of SPI writes and DC/CS toggles is a bit ugly, but it works. Now I will work to integrate this into my “real” application, which should be rather simple. Again @OpenMV I really appreciated the LCD driver setup guidance.

BTW, these IPS TFT’s are great displays, definitely worth trying out!

#Demo for 2.0" 240x320 ST7789 IPS TFT & OpenMV LCD Driver
#Author: Cameron R. MANN (12/2020)
#Portions of TFT Command Sequence credited to Shane Gingell (Out of the BOTS)
#MIT License (MIT)

#Note: TFT is setup for "landscape" mode in this example (320x240)

import sensor, image, time, lcd
from pyb import SPI, Pin
from micropython import const
from ustruct import pack

_SWRESET = const(0x01) # Software Reset
_SLPOUT = const(0x11) # Sleep Out
_COLMOD = const(0x3A) # Colour Mode
_DISPON = const(0x29) # Display On
_MADCTL = const(0x36) # Memory Data Access
_CASET = const(0x2A) # Column Address Set
_RASET = const(0x2B) # Row Address set
_RAMWR = const(0x2C) # Write to screen memory
_INVON = const(0x21) # Inversion On

cs = Pin('P3', Pin.OUT)
dc = Pin('P8', Pin.OUT)

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.skip_frames()

clock = time.clock()

#create spi object first
spi = SPI(2, SPI.MASTER, baudrate=54000000) #Create SPI bus


#init screen using lcd.init
lcd.init(type=lcd.LCD_SHIELD, width=320, height=240,
framesize=lcd.QVGA,refresh=60, triple_buffer=False, bgr=False)


#custom LCD setup FOR ST7789
#Note DC is pin 8, CS is pin 3

dc.value(False) #set data/command pin
cs.value(0)
spi.send(bytearray([_SWRESET]))#software reset
cs.value(1)

time.sleep_ms(200)

dc.value(False) #set data/command pin
cs.value(0)
spi.send(bytearray([_SLPOUT]))#sleep out
cs.value(1)

time.sleep_ms(200)

dc.value(False) #set data/command pin
cs.value(0)
spi.send(bytearray([_COLMOD]))#set 16 bit color
cs.value(1)

dc.value(True) #set data/command pin
cs.value(0)
spi.send(bytearray([0x55]))#spi send
cs.value(1)

dc.value(False) #set data/command pin
cs.value(0)
spi.send(bytearray([_DISPON]))#display on
cs.value(1)

dc.value(False) #set data/command pin
cs.value(0)
spi.send(bytearray([_MADCTL]))#set mode for writing to screen
cs.value(1)

dc.value(True) #set data/command pin
cs.value(0)
spi.send(bytearray([0b10110000]))#this was the mode that I used for ST7789
cs.value(1)

dc.value(False) #set data/command pin
cs.value(0)
spi.send(bytearray([_CASET]))#set x writng window
cs.value(1)

dc.value(True) #set data/command pin
cs.value(0)
spi.send(pack(">HH", 0, 320))#spi send
cs.value(1)

dc.value(False) #set data/command pin
cs.value(0)
spi.send(bytearray([_RASET]))#set y writing window
cs.value(1)

dc.value(True) #set data/command pin
cs.value(0)
spi.send(pack(">HH", 0, 240))#spi send
cs.value(1)

dc.value(False) #set data/command pin
cs.value(0)
spi.send(bytearray([_INVON]))#inversion on
cs.value(1)

while(True):
    clock.tick()

    #show camera image on LCD
    lcd.display(sensor.snapshot())