[EN] Improve st7735 blue-tab/red-tab 0.96 ” library

This article is an update of the st7735 library file for Micropython by Billy Cheung (accessed 2021-09-07) published on github. It is a library that has been updated by Guy Caver to support ST7735s. The required libraries include st7735.py and sysfont.py Guy Carver implements esp8266 and esp32 to provide better display speed through the principle of display buffering for pixel storage and additional instructions for sending data from buffer to TFT module via SPI bus.

(Figure. 1 dCore-miniML board with ST7735 0.96″ display module installed)

Original library

From the original library written by Guy Carver for use with the 1.8″ ST7735, someone has updated it to work with the 0.96″ ST7735 with a display resolution of 80×160 pixels as mentioned in both previous articles. For use with ESP32 and ESP8266, the source code is as follows.

#Added 1.44 inch 128x128 and 0.96 inch 80x160 SST7735 to the
# original 1.8 inch drivers developed by Guy Carver
# fixed the issue of different start Row and start col addresses of different OLED types
# fixed the RGB special equence in 80x160 ST7735B

import machine
import time
from math import sqrt

#TFTRotations and TFTRGB are bits to set
# on MADCTL to control display rotation/color layout
#Looking at display with pins on top.
#00 = upper left printing right
#10 = does nothing (MADCTL_ML)
#20 = upper left printing down (backwards) (Vertical flip)
#40 = upper right printing left (backwards) (X Flip)
#80 = lower left printing right (backwards) (Y Flip)
#04 = (MADCTL_MH)

#60 = 90 right rotation
#C0 = 180 right rotation
#A0 = 270 right rotation
TFTRotations = [0x00, 0x60, 0xC0, 0xA0]
TFTBGR = 0x08 # for 1.8 and 1.44 inch display
TFTRGB = 0x00

#@micropython.native
def clamp( aValue, aMin, aMax ) :
  return max(aMin, min(aMax, aValue))

#@micropython.native
def TFTColor( aR, aG, aB ) :
  '''Create a 16 bit rgb value from the given R,G,B from 0-255.
     This assumes rgb 565 layout and will be incorrect for bgr.'''
  return ((aR & 0xF8) << 8) | ((aG & 0xFC) << 3) | (aB >> 3)

#ScreenSize = (128, 160)

class TFT(object) :
  """ define different model of ST7735, circuit board color or types (tabcolor)."""
  GREENTAB        = 0x0 # 128x160 , start col 2, start row 1, rgb
  REDTAB          = 0x1 # 128x160 , start col 0, start row 0, rgb
  BLACKTAB        = 0x2 # 128x160 , start col 0, start row 0, bgr
  GREENTAB2       = 0x3 # 128x160 , start col 2, start row 1, bgr
  GREENTAB3       = 0x4 # 128x160 , start col 2, start row 3, rgb
  GREENTAB128x128 = 0x5 # 128x128 1.44 inches, bgr, start col 2
                              #         if rotation = 0, or 1, start row 1
                              #         if rotation = 2, or 3, start row 3
  GREENTAB80x160  = 0x6 # 80x160 0.96 inch, start col 26, start row 1, bgr, inverted
  REDTAB80x160    = 0x7 # 80x160 0.96 inch, start col 24, start row 0, rgb
  BLUETAB         = 0xB # 128x160 , start col 2, start row 1, rgb

  NOP = 0x0
  SWRESET = 0x01
  RDDID = 0x04
  RDDST = 0x09

  SLPIN  = 0x10
  SLPOUT  = 0x11
  PTLON  = 0x12
  NORON  = 0x13

  INVOFF = 0x20
  INVON = 0x21
  DISPOFF = 0x28
  DISPON = 0x29
  CASET = 0x2A
  RASET = 0x2B
  RAMWR = 0x2C
  RAMRD = 0x2E

  COLMOD = 0x3A
  MADCTL = 0x36

  FRMCTR1 = 0xB1
  FRMCTR2 = 0xB2
  FRMCTR3 = 0xB3
  INVCTR = 0xB4
  DISSET5 = 0xB6

  PWCTR1 = 0xC0
  PWCTR2 = 0xC1
  PWCTR3 = 0xC2
  PWCTR4 = 0xC3
  PWCTR5 = 0xC4
  VMCTR1 = 0xC5

  RDID1 = 0xDA
  RDID2 = 0xDB
  RDID3 = 0xDC
  RDID4 = 0xDD

  PWCTR6 = 0xFC

  GMCTRP1 = 0xE0
  GMCTRN1 = 0xE1

  BLACK = 0
  RED = TFTColor(0xFF, 0x00, 0x00)
  MAROON = TFTColor(0x80, 0x00, 0x00)
  GREEN = TFTColor(0x00, 0xFF, 0x00)
  FOREST = TFTColor(0x00, 0x80, 0x80)
  BLUE = TFTColor(0x00, 0x00, 0xFF)
  NAVY = TFTColor(0x00, 0x00, 0x80)
  CYAN = TFTColor(0x00, 0xFF, 0xFF)
  YELLOW = TFTColor(0xFF, 0xFF, 0x00)
  PURPLE = TFTColor(0xFF, 0x00, 0xFF)
  WHITE = TFTColor(0xFF, 0xFF, 0xFF)
  GRAY = TFTColor(0x80, 0x80, 0x80)

  @staticmethod
  def color( aR, aG, aB ) :
    '''Create a 565 rgb TFTColor value'''
    return TFTColor(aR, aG, aB)

  def __init__( self, spi, aDC, aReset=None, aCS=None) :
    """aLoc SPI pin location is either 1 for 'X' or 2 for 'Y'.
       aDC is the DC pin and aReset is the reset pin."""
    self.tabcolor = 0 # default
    self._size = (128,128)
    self.rotate = 2                    #Vertical with top toward pins.
    self._offset=(2,3)
    self._rgb = False                   #color order of rgb.
    self.dc  = machine.Pin(aDC, machine.Pin.OUT)

    if aReset == None :
        self.useReset = False
    else :
        self.useReset = True
        self.reset = machine.Pin(aReset, machine.Pin.OUT)
    if aCS == None :
        self.useCS = False
    else :
        self.useCS = True
        self.csPin = machine.Pin(aCS, machine.Pin.OUT)
    self.cs(1)
    self.spi = spi
    self.colorData = bytearray(2)
    self.windowLocData = bytearray(4)


  def cs (self, iologic) :
      if self.useCS :
          self.csPin(iologic)

  def size( self ) :
    return self._size

  def offset ( self ) :
    return self._offset

#   @micropython.native
  def on( self, aTF = True ) :
    '''Turn display on or off.'''
    self._writecommand(TFT.DISPON if aTF else TFT.DISPOFF)

#   @micropython.native
  def invertcolor( self, aBool ) :
    '''Invert the color data IE: Black = White.'''
    self._writecommand(TFT.INVON if aBool else TFT.INVOFF)

#   @micropython.native
  def rgb( self, aTF = True ) :
    '''True = rgb else bgr'''
    self._rgb = aTF
    self._setMADCTL()

#   @micropython.native
  def rotation( self, aRot ) :
    '''0 - 3. Starts vertical with top toward pins and rotates 90 deg
       clockwise each step.'''
    if (0 <= aRot < 4):
      rotchange = self.rotate ^ aRot
      self.rotate = aRot
      # If switching from vertical to horizontal (indicated by bit 0 changing).
      # swap screen size rows and columns and their start addresses offset

    if (rotchange & 1):
      self._size =(self._size[1], self._size[0])
      self._offset=(self._offset[1], self._offset[0])

    if self.tabcolor == self.GREENTAB128x128 :
      # special handling of 128x128 with different offsets during rotations
      if aRot == 0 :
        self._offset=(2,1)
      elif aRot == 1 :
        self._offset=(1,2)
      elif aRot == 2 :
        self._offset=(2,3)
      elif aRot == 3 :
        self._offset=(3,2)
    self._setMADCTL()

#  @micropython.native
  def pixel( self, aPos, aColor ) :
    '''Draw a pixel at the given position'''
    if 0 <= aPos[0] < self._size[0] and 0 <= aPos[1] < self._size[1]:
      self._setwindowpoint(aPos)
      self._pushcolor(aColor)

#   @micropython.native
  def text( self, aPos, aString, aColor, aFont, aSize = 1, nowrap = False ) :
    '''Draw a text at the given position.  If the string reaches the end of the
       display it is wrapped to aPos[0] on the next line.  aSize may be an integer
       which will size the font uniformly on w,h or a or any type that may be
       indexed with [0] or [1].'''

    if aFont == None:
      return

    #Make a size either from single value or 2 elements.
    if (type(aSize) == int) or (type(aSize) == float):
      wh = (aSize, aSize)
    else:
      wh = aSize

    px, py = aPos
    width = wh[0] * aFont["Width"] + 1
    for c in aString:
      self.char((px, py), c, aColor, aFont, wh)
      px += width
      #We check > rather than >= to let the right (blank) edge of the
      # character print off the right of the screen.
      if px + width > self._size[0]:
        if nowrap:
          break
        else:
          py += aFont["Height"] * wh[1] + 1
          px = aPos[0]

#   @micropython.native
  def char( self, aPos, aChar, aColor, aFont, aSizes ) :
    '''Draw a character at the given position using the given font and color.
       aSizes is a tuple with x, y as integer scales indicating the
       # of pixels to draw for each pixel in the character.'''

    if aFont == None:
      return

    startchar = aFont['Start']
    endchar = aFont['End']

    ci = ord(aChar)
    if (startchar <= ci <= endchar):
      fontw = aFont['Width']
      fonth = aFont['Height']
      ci = (ci - startchar) * fontw

      charA = aFont["Data"][ci:ci + fontw]
      px = aPos[0]
      if aSizes[0] <= 1 and aSizes[1] <= 1 :
        for c in charA :
          py = aPos[1]
          for r in range(fonth) :
            if c & 0x01 :
              self.pixel((px, py), aColor)
            py += 1
            c >>= 1
          px += 1
      else:
        for c in charA :
          py = aPos[1]
          for r in range(fonth) :
            if c & 0x01 :
              self.fillrect((px, py), aSizes, aColor)
            py += aSizes[1]
            c >>= 1
          px += aSizes[0]

#   @micropython.native
  def line( self, aStart, aEnd, aColor ) :
    '''Draws a line from aStart to aEnd in the given color.  Vertical or horizontal
       lines are forwarded to vline and hline.'''
    if aStart[0] == aEnd[0]:
      #Make sure we use the smallest y.
      pnt = aEnd if (aEnd[1] < aStart[1]) else aStart
      self.vline(pnt, abs(aEnd[1] - aStart[1]) + 1, aColor)
    elif aStart[1] == aEnd[1]:
      #Make sure we use the smallest x.
      pnt = aEnd if aEnd[0] < aStart[0] else aStart
      self.hline(pnt, abs(aEnd[0] - aStart[0]) + 1, aColor)
    else:
      px, py = aStart
      ex, ey = aEnd
      dx = ex - px
      dy = ey - py
      inx = 1 if dx > 0 else -1
      iny = 1 if dy > 0 else -1

      dx = abs(dx)
      dy = abs(dy)
      if (dx >= dy):
        dy <<= 1
        e = dy - dx
        dx <<= 1
        while (px != ex):
          self.pixel((px, py), aColor)
          if (e >= 0):
            py += iny
            e -= dx
          e += dy
          px += inx
      else:
        dx <<= 1
        e = dx - dy
        dy <<= 1
        while (py != ey):
          self.pixel((px, py), aColor)
          if (e >= 0):
            px += inx
            e -= dy
          e += dx
          py += iny

#   @micropython.native
  def vline( self, aStart, aLen, aColor ) :
    '''Draw a vertical line from aStart for aLen. aLen may be negative.'''
    start = (clamp(aStart[0], 0, self._size[0]), clamp(aStart[1], 0, self._size[1]))
    stop = (start[0], clamp(start[1] + aLen, 0, self._size[1]))
    #Make sure smallest y 1st.
    if (stop[1] < start[1]):
      start, stop = stop, start
    self._setwindowloc(start, stop)
    self._setColor(aColor)
    self._draw(aLen)

#   @micropython.native
  def hline( self, aStart, aLen, aColor ) :
    '''Draw a horizontal line from aStart for aLen. aLen may be negative.'''
    start = (clamp(aStart[0], 0, self._size[0]), clamp(aStart[1], 0, self._size[1]))
    stop = (clamp(start[0] + aLen, 0, self._size[0]), start[1])
    #Make sure smallest x 1st.
    if (stop[0] < start[0]):
      start, stop = stop, start
    self._setwindowloc(start, stop)
    self._setColor(aColor)
    self._draw(aLen)

#   @micropython.native
  def rect( self, aStart, aSize, aColor ) :
    '''Draw a hollow rectangle.  aStart is the smallest coordinate corner
       and aSize is a tuple indicating width, height.'''
    self.hline(aStart, aSize[0], aColor)
    self.hline((aStart[0], aStart[1] + aSize[1] - 1), aSize[0], aColor)
    self.vline(aStart, aSize[1], aColor)
    self.vline((aStart[0] + aSize[0] - 1, aStart[1]), aSize[1], aColor)

#   @micropython.native
  def fillrect( self, aStart, aSize, aColor ) :
    '''Draw a filled rectangle.  aStart is the smallest coordinate corner
       and aSize is a tuple indicating width, height.'''
    start = (clamp(aStart[0], 0, self._size[0]), clamp(aStart[1], 0, self._size[1]))
    end = (clamp(start[0] + aSize[0] - 1, 0, self._size[0]), clamp(start[1] + aSize[1] - 1, 0, self._size[1]))

    if (end[0] < start[0]):
      tmp = end[0]
      end = (start[0], end[1])
      start = (tmp, start[1])
    if (end[1] < start[1]):
      tmp = end[1]
      end = (end[0], start[1])
      start = (start[0], tmp)

    self._setwindowloc(start, end)
    numPixels = (end[0] - start[0] + 1) * (end[1] - start[1] + 1)
    self._setColor(aColor)
    self._draw(numPixels)

#   @micropython.native
  def circle( self, aPos, aRadius, aColor ) :
    '''Draw a hollow circle with the given radius and color with aPos as center.'''
    self.colorData[0] = aColor >> 8
    self.colorData[1] = aColor
    xend = int(0.7071 * aRadius) + 1
    rsq = aRadius * aRadius
    for x in range(xend) :
      y = int(sqrt(rsq - x * x))
      xp = aPos[0] + x
      yp = aPos[1] + y
      xn = aPos[0] - x
      yn = aPos[1] - y
      xyp = aPos[0] + y
      yxp = aPos[1] + x
      xyn = aPos[0] - y
      yxn = aPos[1] - x

      self._setwindowpoint((xp, yp))
      self._writedata(self.colorData)
      self._setwindowpoint((xp, yn))
      self._writedata(self.colorData)
      self._setwindowpoint((xn, yp))
      self._writedata(self.colorData)
      self._setwindowpoint((xn, yn))
      self._writedata(self.colorData)
      self._setwindowpoint((xyp, yxp))
      self._writedata(self.colorData)
      self._setwindowpoint((xyp, yxn))
      self._writedata(self.colorData)
      self._setwindowpoint((xyn, yxp))
      self._writedata(self.colorData)
      self._setwindowpoint((xyn, yxn))
      self._writedata(self.colorData)

#   @micropython.native
  def fillcircle( self, aPos, aRadius, aColor ) :
    '''Draw a filled circle with given radius and color with aPos as center'''
    rsq = aRadius * aRadius
    for x in range(aRadius) :
      y = int(sqrt(rsq - x * x))
      y0 = aPos[1] - y
      ey = y0 + y * 2
      y0 = clamp(y0, 0, self._size[1])
      ln = abs(ey - y0) + 1;

      self.vline((aPos[0] + x, y0), ln, aColor)
      self.vline((aPos[0] - x, y0), ln, aColor)

  def fill( self, aColor = BLACK ) :
    '''Fill screen with the given color.'''
    self.fillrect((0, 0), self._size, aColor)

  def image( self, x0, y0, x1, y1, data ) :
    self._setwindowloc((x0, y0), (x1, y1))
    self._writedata(data)

#   @micropython.native
  def _setColor( self, aColor ) :
    self.colorData[0] = aColor >> 8
    self.colorData[1] = aColor
    self.buf = bytes(self.colorData) * 32

#   @micropython.native
  def _draw( self, aPixels ) :
    '''Send given color to the device aPixels times.'''

    self.dc(1)
    self.cs(0)
    for i in range(aPixels//32):
      self.spi.write(self.buf)
    rest = (int(aPixels) % 32)
    if rest > 0:
        buf2 = bytes(self.colorData) * rest
        self.spi.write(buf2)
    self.cs(1)

#   @micropython.native
  def _setwindowpoint( self, aPos ) :
    '''Set a single point for drawing a color to.'''
    x = self._offset[0] + int(aPos[0])
    y = self._offset[1] + int(aPos[1])
    self._writecommand(TFT.CASET)            #Column address set.
    self.windowLocData[0] = self._offset[0]
    self.windowLocData[1] = x
    self.windowLocData[2] = self._offset[0]
    self.windowLocData[3] = x
    self._writedata(self.windowLocData)

    self._writecommand(TFT.RASET)            #Row address set.
    self.windowLocData[0] = self._offset[1]
    self.windowLocData[1] = y
    self.windowLocData[2] = self._offset[1]
    self.windowLocData[3] = y
    self._writedata(self.windowLocData)
    self._writecommand(TFT.RAMWR)            #Write to RAM.

#   @micropython.native
  def _setwindowloc( self, aPos0, aPos1 ) :
    '''Set a rectangular area for drawing a color to.'''
    self._writecommand(TFT.CASET)            #Column address set.
    self.windowLocData[0] = self._offset[0]
    self.windowLocData[1] = self._offset[0] + int(aPos0[0])
    self.windowLocData[2] = self._offset[0]
    self.windowLocData[3] = self._offset[0] + int(aPos1[0])
    self._writedata(self.windowLocData)

    self._writecommand(TFT.RASET)            #Row address set.
    self.windowLocData[0] = self._offset[1]
    self.windowLocData[1] = self._offset[1] + int(aPos0[1])
    self.windowLocData[2] = self._offset[1]
    self.windowLocData[3] = self._offset[1] + int(aPos1[1])
    self._writedata(self.windowLocData)

    self._writecommand(TFT.RAMWR)            #Write to RAM.

  #@micropython.native
  def _writecommand( self, aCommand ) :
    '''Write given command to the device.'''
    self.dc(0)
    self.cs(0)
    self.spi.write(bytearray([aCommand]))
    self.cs(1)

  #@micropython.native
  def _writedata( self, aData ) :
    '''Write given data to the device.  This may be
       either a single int or a bytearray of values.'''
    self.dc(1)
    self.cs(0)
    self.spi.write(aData)
    self.cs(1)

  #@micropython.native
  def _pushcolor( self, aColor ) :
    '''Push given color to the device.'''
    self.colorData[0] = aColor >> 8
    self.colorData[1] = aColor
    self._writedata(self.colorData)

  #@micropython.native
  def _setMADCTL( self ) :
    '''Set screen rotation and RGB/BGR format.'''
    self._writecommand(TFT.MADCTL)
    rgb = TFTRGB if self._rgb else TFTBGR
    self._writedata(bytearray([TFTRotations[self.rotate] | rgb]))

  #@micropython.native
  def _reset( self ) :
    '''Reset the device.'''
    self.dc(0)
    if self.useReset :
        self.reset(1)
        time.sleep_us(500)
        self.reset(0)
        time.sleep_us(500)
        self.reset(1)

    time.sleep_us(500)

  def init_7735 ( self, Tabcolor) :
    self.tabcolor = Tabcolor
    if self.tabcolor == self.BLUETAB :
        # Initialize blue tab version.
        self._size = (128, 160)
        self._offset = (2,1)
        self._rgb = True

        self._reset()
        self._writecommand(TFT.SWRESET)              #Software reset.
        time.sleep_us(50)
        self._writecommand(TFT.SLPOUT)               #out of sleep mode.
        time.sleep_us(500)

        data1 = bytearray(1)
        self._writecommand(TFT.COLMOD)               #Set color mode.
        data1[0] = 0x05                             #16 bit color.
        self._writedata(data1)
        time.sleep_us(10)

        data3 = bytearray([0x00, 0x06, 0x03])       #fastest refresh, 6 lines front, 3 lines back.
        self._writecommand(TFT.FRMCTR1)              #Frame rate control.
        self._writedata(data3)
        time.sleep_us(10)

        self._writecommand(TFT.MADCTL)
        data1[0] = 0x08                             #row address/col address, bottom to top refresh
        self._writedata(data1)

        data2 = bytearray(2)
        self._writecommand(TFT.DISSET5)              #Display settings
        data2[0] = 0x15                             #1 clock cycle nonoverlap, 2 cycle gate rise, 3 cycle oscil, equalize
        data2[1] = 0x02                             #fix on VTL
        self._writedata(data2)

        self._writecommand(TFT.INVCTR)               #Display inversion control
        data1[0] = 0x00                             #Line inversion.
        self._writedata(data1)

        self._writecommand(TFT.PWCTR1)               #Power control
        data2[0] = 0x02   #GVDD = 4.7V
        data2[1] = 0x70   #1.0uA
        self._writedata(data2)
        time.sleep_us(10)

        self._writecommand(TFT.PWCTR2)               #Power control
        data1[0] = 0x05                             #VGH = 14.7V, VGL = -7.35V
        self._writedata(data1)

        self._writecommand(TFT.PWCTR3)           #Power control
        data2[0] = 0x01   #Opamp current small
        data2[1] = 0x02   #Boost frequency
        self._writedata(data2)

        self._writecommand(TFT.VMCTR1)               #Power control
        data2[0] = 0x3C   #VCOMH = 4V
        data2[1] = 0x38   #VCOML = -1.1V
        self._writedata(data2)
        time.sleep_us(10)

        self._writecommand(TFT.PWCTR6)               #Power control
        data2[0] = 0x11
        data2[1] = 0x15
        self._writedata(data2)

        #These different values don't seem to make a difference.
    #     dataGMCTRP = bytearray([0x0f, 0x1a, 0x0f, 0x18, 0x2f, 0x28, 0x20, 0x22, 0x1f,
    #                             0x1b, 0x23, 0x37, 0x00, 0x07, 0x02, 0x10])
        dataGMCTRP = bytearray([0x02, 0x1c, 0x07, 0x12, 0x37, 0x32, 0x29, 0x2d, 0x29,
                                0x25, 0x2b, 0x39, 0x00, 0x01, 0x03, 0x10])
        self._writecommand(TFT.GMCTRP1)
        self._writedata(dataGMCTRP)

    #     dataGMCTRN = bytearray([0x0f, 0x1b, 0x0f, 0x17, 0x33, 0x2c, 0x29, 0x2e, 0x30,
    #                             0x30, 0x39, 0x3f, 0x00, 0x07, 0x03, 0x10])
        dataGMCTRN = bytearray([0x03, 0x1d, 0x07, 0x06, 0x2e, 0x2c, 0x29, 0x2d, 0x2e,
                                0x2e, 0x37, 0x3f, 0x00, 0x00, 0x02, 0x10])
        self._writecommand(TFT.GMCTRN1)
        self._writedata(dataGMCTRN)
        time.sleep_us(10)

        self._writecommand(TFT.CASET)                #Column address set.
        self.windowLocData[0] = 0x00
        self.windowLocData[1] = self._offset[0]                   #Start at column 2
        self.windowLocData[2] = 0x00
        self.windowLocData[3] = self._size[0] + self._offset[0]
        self._writedata(self.windowLocData)

        self._writecommand(TFT.RASET)                #Row address set.
        self.windowLocData[1] = self._offset[1]                   #Start at row 1.
        self.windowLocData[3] = self._size[1] + self._offset[1]
        self._writedata(self.windowLocData)

        self._writecommand(TFT.NORON)                #Normal display on.
        time.sleep_us(10)

        self._writecommand(TFT.RAMWR)
        time.sleep_us(500)

        self._writecommand(TFT.DISPON)
        self.cs(1)
        time.sleep_us(500)
    else :
        # Initialize a green tab version.
        self._reset()
        self._writecommand(TFT.SWRESET)              #Software reset.
        time.sleep_us(150)
        self._writecommand(TFT.SLPOUT)               #out of sleep mode.
        time.sleep_us(255)

        data3 = bytearray([0x01, 0x2C, 0x2D])       #fastest refresh, 6 lines front, 3 lines back.
        self._writecommand(TFT.FRMCTR1)              #Frame rate control.
        self._writedata(data3)

        self._writecommand(TFT.FRMCTR2)              #Frame rate control.
        self._writedata(data3)

        data6 = bytearray([0x01, 0x2c, 0x2d, 0x01, 0x2c, 0x2d])
        self._writecommand(TFT.FRMCTR3)              #Frame rate control.
        self._writedata(data6)
        time.sleep_us(10)

        self._writecommand(TFT.INVCTR)               #Display inversion control
        self._writedata(bytearray([0x07]))
        self._writecommand(TFT.PWCTR1)               #Power control
        data3[0] = 0xA2
        data3[1] = 0x02
        data3[2] = 0x84
        self._writedata(data3)

        self._writecommand(TFT.PWCTR2)               #Power control
        self._writedata(bytearray([0xC5]))

        data2 = bytearray(2)
        self._writecommand(TFT.PWCTR3)               #Power control
        data2[0] = 0x0A   #Opamp current small
        data2[1] = 0x00   #Boost frequency
        self._writedata(data2)

        self._writecommand(TFT.PWCTR4)               #Power control
        data2[0] = 0x8A   #Opamp current small
        data2[1] = 0x2A   #Boost frequency
        self._writedata(data2)

        self._writecommand(TFT.PWCTR5)               #Power control
        data2[0] = 0x8A   #Opamp current small
        data2[1] = 0xEE   #Boost frequency
        self._writedata(data2)

        self._writecommand(TFT.VMCTR1)               #Power control
        self._writedata(bytearray([0x0E]))

        self._writecommand(TFT.INVOFF)

        if self.tabcolor == self.GREENTAB :
            self._offset = (2,1)
        if self.tabcolor == self.REDTAB :
            self._offset = (0,0)
        if self.tabcolor == self.BLACKTAB :
            self._offset = (0,0)
            self._rgb = False
        elif self.tabcolor == self.GREENTAB2 :
            self._offset = (2,1)
            self._rgb = False
        elif self.tabcolor == self.GREENTAB3 :
            self._offset = (2,3)
        elif self.tabcolor == self.GREENTAB128x128 :
            self._size = (128,128)
            self._offset = (2,1)
            self._rgb = False
        elif self.tabcolor == self.GREENTAB80x160 :
            self._size = (80,160)
            self._offset = (26,1)
            self._rgb = False
            self._writecommand(TFT.INVON)
        elif self.tabcolor == self.REDTAB80x160 :
            self._size = (80,160)
            self._offset = (24,0)

        # rotate to the same orientation with the Pins on the boards at the top
        if self.tabcolor == self.GREENTAB80x160 or self.tabcolor == self.REDTAB80x160 :
            self.rotation(1)
        else :
            self.rotation(2)

        # set the color mapping of  RGB or GBR
        self._setMADCTL()

        self._writecommand(TFT.COLMOD)
        self._writedata(bytearray([0x05]))

        self._writecommand(TFT.CASET)                #Column address set.

        self.windowLocData[0] = 0x00
        self.windowLocData[1] = self._offset[0]
        self.windowLocData[2] = 0x00
        self.windowLocData[3] = self._size[0]+self._offset[0]
        self._writedata(self.windowLocData)

        self._writecommand(TFT.RASET)                #Row address set.
        self.windowLocData[1] = self._offset[1]
        self.windowLocData[3] = self._size[1]+self._offset[1]
        self._writedata(self.windowLocData)

        dataGMCTRP = bytearray([0x02, 0x1c, 0x07, 0x12, 0x37, 0x32, 0x29, 0x2d, 0x29,
                                0x25, 0x2b, 0x39, 0x00, 0x01, 0x03, 0x10])
        self._writecommand(TFT.GMCTRP1)
        self._writedata(dataGMCTRP)

        dataGMCTRN = bytearray([0x03, 0x1d, 0x07, 0x06, 0x2e, 0x2c, 0x29, 0x2d, 0x2e,
                                0x2e, 0x37, 0x3f, 0x00, 0x00, 0x02, 0x10])
        self._writecommand(TFT.GMCTRN1)
        self._writedata(dataGMCTRN)

        self._writecommand(TFT.NORON)                #Normal display on.
        time.sleep_us(10)

        self._writecommand(TFT.DISPON)
        time.sleep_us(100)

        self.cs(1)

From the source code, it can be seen that the drawings are executed one by one. When the command is received, the data will be sent to the display module immediately. This action results in the frequent display, frequent I/O connection, and followed by operational bottlenecks as the SPI buses are significantly slower than microcontrollers: around 20MHz, while esp8266 is running at 160MHz and esp32 is running at 240MHz.

If create a buffer for storing pixel data and take it out once, we have already tested to draw Thai characters in the article on i2c bus OLED, which has the same performance. Drawing in memory and exporting once Reduces the time wasted to turn on work mode / transmit data / turn off work mode. And when looking at the pixel() command’s code, it is found that the data is sent to the SPI bus every time it is called.

  def pixel( self, aPos, aColor ) :
    '''Draw a pixel at the given position'''
    if 0 <= aPos[0] < self._size[0] and 0 <= aPos[1] < self._size[1]:
      self._setwindowpoint(aPos)
      self._pushcolor(aColor)

In the char() command for drawing characters, pixel() is called every time a point of each character is drawn. This means that data is always being sent to the SPI bus.

#   @micropython.native
  def char( self, aPos, aChar, aColor, aFont, aSizes ) :
    '''Draw a character at the given position using the given font and color.
       aSizes is a tuple with x, y as integer scales indicating the
       # of pixels to draw for each pixel in the character.'''

    if aFont == None:
      return

    startchar = aFont['Start']
    endchar = aFont['End']

    ci = ord(aChar)
    if (startchar <= ci <= endchar):
      fontw = aFont['Width']
      fonth = aFont['Height']
      ci = (ci - startchar) * fontw

      charA = aFont["Data"][ci:ci + fontw]
      px = aPos[0]
      if aSizes[0] <= 1 and aSizes[1] <= 1 :
        for c in charA :
          py = aPos[1]
          for r in range(fonth) :
            if c & 0x01 :
              self.pixel((px, py), aColor)
            py += 1
            c >>= 1
          px += 1
      else:
        for c in charA :
          py = aPos[1]
          for r in range(fonth) :
            if c & 0x01 :
              self.fillrect((px, py), aSizes, aColor)
            py += aSizes[1]
            c >>= 1
          px += aSizes[0]

And when considering the image() command that sends the bitmap data of the whole image at once as follows:

  def image( self, x0, y0, x1, y1, data ) :
    self._setwindowloc((x0, y0), (x1, y1))
    self._writedata(data)

We will find that the command _serwindowloc() is the same but different from _pushcolor() and _writedata(), the working code is as follows.

  #@micropython.native
  def _pushcolor( self, aColor ) :
    '''Push given color to the device.'''
    self.colorData[0] = aColor >> 8
    self.colorData[1] = aColor
    self._writedata(self.colorData)

  def _writedata( self, aData ) :
    '''Write given data to the device.  This may be
       either a single int or a bytearray of values.'''
    self.dc(1)
    self.cs(0)
    self.spi.write(aData)
    self.cs(1)

As you’ll notice, _pushcolor() calls _writedata() as well, so if you want to draw a character of 8×8 pixels, char() has to send pixels to the display module as follows:

  • Number of times to send data = 8×8 = 64 times
  • Each time using instructions  = 2 commands
  • 64 times, it takes operations = 2*64 = 128 times.

But if we store the characters in a buffer or array of 8×8 members or 64 characters, it will look like this:

  • Number of times to send data = 1 time
  • Each time using instructions = 2 commands
  • 1 time for operation = 1+ number of data = 1+64 = 65 times

From a rough comparison above excluding that the command pixel() calls _writedata(), which increases the number of operations, it is found that the command to draw a letter size of 8×8 pixels, the normal drawing method uses 128 operations (namely, positioning and sending color values) with another method that takes data from memory to the display module, 65 operations indicating where to send and then another 64 sets will find that the use of instructions was significantly reduced.

Therefore, when applied to a large number of draws on each display, the difference in render speed will be even greater. However Double buffering requires a large amount of memory to hold the display buffer. When used with Blue tab size 160×80 pixels, there must be approximately the amount of memory as follows.

  • Amount of memory = Width x Height x 2 bytes = 160 x 80 x 2 = 25,600 bytes.

The esp8266 and esp32 microcontrollers have enough memory of 25,600 bytes for display storage.

Note
We compare the number of times, not the time to show the number of times. It doesn’t measure the speed of each command.

Buffer

One of Micropython’s major libraries is framebuf, which serves as a drawing board. By drawing into memory according to the format of the pixels that can be defined as follows

  • RGB565
  • MONO_VLSB
  • GS2_HMSB
  • GS4_HMSB
  • GS8
  • MONO_HLSB
  • MONO_HMSB

Additionally, when we choose to use buffers from the framebuf class, we can use the following command to draw pixels into memory. The in-memory draw is faster than TFT modules as SPIRAM supports 80MHz.

  • fill()
  • fill_rect()
  • pixel()
  • hline()
  • vline()
  • rect()
  • line()
  • blit()
  • scroll()
  • text()

From the instructions provided, it will be found that it is sufficient to use and can be written more without difficulty and we have improved the code.

    def __init__( self, spi, aDC, aReset, aCS):
        self.dcPin  = machine.Pin(aDC, machine.Pin.OUT)
        if aReset == None :
            self.useReset = False
        else:
            self.useReset = True
            self.resetPin = machine.Pin(aReset, machine.Pin.OUT)
        if aCS == None :
            self.useCS = False
        else :
            self.useCS = True
            self.csPin = machine.Pin(aCS, machine.Pin.OUT)
        self.csPin(1)
        self.spi = spi
        self.colorData = bytearray(2)
        self.windowLocData = bytearray(4)
        self.buffer = framebuf.FrameBuffer(bytearray(80*160*2),160,80,framebuf.RGB565)
        
        self._reset()
        self._writecommand(TFT.SWRESET)              #Software reset.
        ...

        self.csPin(1)

In addition, a set of instructions for reading color values and entering color values in memory have been added as follows:

    def setPixel( self, aX, aY, aColor ) :
        self.buffer.pixel(aX, aY, aColor)
    
    def readPixel( self, aX, aY ):
       return self.buffer.pixel(aX, aY)

The part that must be done when drawing things into memory is delivering this buffer to the display module via the SPI bus with the following code.


    def swap( self ):
        self._setwindowloc((0,0), (self._size[0]-1, self._size[1]-1))
        self.dcPin(1)
        self.csPin(0)
        self.spi.write(self.buffer)
        self.csPin(1)

Improved library

The idea of making a double buffer and using it with ST7735 BLUE TAB 0.96 inch size 80×160 has a library of work as follows.

#Added 1.44 inch 128x128 and 0.96 inch 80x160 SST7735 to the
# original 1.8 inch drivers developed by Guy Carver
# fixed the issue of different start Row and start col addresses of different OLED types
# fixed the RGB special equence in 80x160 ST7735B
#
# ปรับปรุงเพื่อใช้กับ ST7735S GREENTAB80x160 (เท่านั้น)
# by JarutEx (www.jarutex.com)
# Update 2021-09-07
from machine import Pin
import time
import machine
import framebuf

TFTRotations = [0x00, 0x60, 0xC0, 0xA0]
TFTBGR = 0x08 # for 1.8 and 1.44 inch display

#@micropython.native
def clamp( aValue, aMin, aMax ):
    return max(aMin, min(aMax, aValue))

#@micropython.native
def TFTColor( aR, aG, aB ):
    # Convert to RGB5655
    aColor = ((aR & 0xF8) << 8) | ((aG & 0xFC) << 3) | (aB >> 3)
    # Swap
    lColor = aColor >> 8
    hColor = aColor << 8
    return (lColor | hColor)

class TFT(object):
    NOP = 0x0
    SWRESET = 0x01
    RDDID = 0x04
    RDDST = 0x09
    
    SLPIN  = 0x10
    SLPOUT = 0x11
    PTLON  = 0x12
    NORON  = 0x13

    INVOFF = 0x20
    INVON = 0x21
    DISPOFF = 0x28
    DISPON = 0x29
    CASET = 0x2A
    RASET = 0x2B
    RAMWR = 0x2C
    RAMRD = 0x2E

    COLMOD = 0x3A
    MADCTL = 0x36

    FRMCTR1 = 0xB1
    FRMCTR2 = 0xB2
    FRMCTR3 = 0xB3
    INVCTR = 0xB4
    DISSET5 = 0xB6

    PWCTR1 = 0xC0
    PWCTR2 = 0xC1
    PWCTR3 = 0xC2
    PWCTR4 = 0xC3
    PWCTR5 = 0xC4
    VMCTR1 = 0xC5

    RDID1 = 0xDA
    RDID2 = 0xDB
    RDID3 = 0xDC
    RDID4 = 0xDD

    PWCTR6 = 0xFC

    GMCTRP1 = 0xE0
    GMCTRN1 = 0xE1

    BLACK = 0
    RED = TFTColor(0xFF, 0x00, 0x00)
    MAROON = TFTColor(0x80, 0x00, 0x00)
    GREEN = TFTColor(0x00, 0xFF, 0x00)
    FOREST = TFTColor(0x00, 0x80, 0x80)
    BLUE = TFTColor(0x00, 0x00, 0xFF)
    NAVY = TFTColor(0x00, 0x00, 0x80)
    CYAN = TFTColor(0x00, 0xFF, 0xFF)
    YELLOW = TFTColor(0xFF, 0xFF, 0x00)
    PURPLE = TFTColor(0xFF, 0x00, 0xFF)
    WHITE = TFTColor(0xFF, 0xFF, 0xFF)
    GRAY = TFTColor(0x80, 0x80, 0x80)

    @staticmethod
    def color( aR, aG, aB ):
        return TFTColor(aR, aG, aB)

    def __init__( self, spi, aDC, aReset, aCS):
        self.dcPin  = machine.Pin(aDC, machine.Pin.OUT)
        if aReset == None :
            self.useReset = False
        else:
            self.useReset = True
            self.resetPin = machine.Pin(aReset, machine.Pin.OUT)
        if aCS == None :
            self.useCS = False
        else :
            self.useCS = True
            self.csPin = machine.Pin(aCS, machine.Pin.OUT)
        self.csPin(1)
        self.spi = spi
        self.colorData = bytearray(2)
        self.windowLocData = bytearray(4)
        self.buffer = framebuf.FrameBuffer(bytearray(80*160*2),160,80,framebuf.RGB565)
        
        self._reset()
        self._writecommand(TFT.SWRESET)              #Software reset.
        time.sleep_us(150)
        self._writecommand(TFT.SLPOUT)               #out of sleep mode.
        time.sleep_us(255)

        data3 = bytearray([0x01, 0x2C, 0x2D])       #fastest refresh, 6 lines front, 3 lines back.
        self._writecommand(TFT.FRMCTR1)              #Frame rate control.
        self._writedata(data3)

        self._writecommand(TFT.FRMCTR2)              #Frame rate control.
        self._writedata(data3)

        data6 = bytearray([0x01, 0x2c, 0x2d, 0x01, 0x2c, 0x2d])
        self._writecommand(TFT.FRMCTR3)              #Frame rate control.
        self._writedata(data6)
        time.sleep_us(10)

        self._writecommand(TFT.INVCTR)               #Display inversion control
        self._writedata(bytearray([0x07]))
        self._writecommand(TFT.PWCTR1)               #Power control
        data3[0] = 0xA2
        data3[1] = 0x02
        data3[2] = 0x84
        self._writedata(data3)

        self._writecommand(TFT.PWCTR2)               #Power control
        self._writedata(bytearray([0xC5]))

        data2 = bytearray(2)
        self._writecommand(TFT.PWCTR3)               #Power control
        data2[0] = 0x0A   #Opamp current small
        data2[1] = 0x00   #Boost frequency
        self._writedata(data2)

        self._writecommand(TFT.PWCTR4)               #Power control
        data2[0] = 0x8A   #Opamp current small
        data2[1] = 0x2A   #Boost frequency
        self._writedata(data2)

        self._writecommand(TFT.PWCTR5)               #Power control
        data2[0] = 0x8A   #Opamp current small
        data2[1] = 0xEE   #Boost frequency
        self._writedata(data2)

        self._writecommand(TFT.VMCTR1)               #Power control
        self._writedata(bytearray([0x0E]))

        self._writecommand(TFT.INVOFF)

        self._size = (80,160)
        self._offset = (26,1)
        self._rgb = False
        self._writecommand(TFT.INVON)

        self.rotate = 2
        self.rotation(1)
        # set the color mapping of  RGB or GBR
        self._setMADCTL()

        self._writecommand(TFT.COLMOD)
        self._writedata(bytearray([0x05]))

        self._writecommand(TFT.CASET)                #Column address set.

        self.windowLocData[0] = 0x00
        self.windowLocData[1] = self._offset[0]
        self.windowLocData[2] = 0x00
        self.windowLocData[3] = self._size[0]+self._offset[0]
        self._writedata(self.windowLocData)

        self._writecommand(TFT.RASET)                #Row address set.
        self.windowLocData[1] = self._offset[1]
        self.windowLocData[3] = self._size[1]+self._offset[1]
        self._writedata(self.windowLocData)

        dataGMCTRP = bytearray([0x02, 0x1c, 0x07, 0x12, 0x37, 0x32, 0x29, 0x2d,
                                0x29, 0x25, 0x2b, 0x39, 0x00, 0x01, 0x03, 0x10])
        self._writecommand(TFT.GMCTRP1)
        self._writedata(dataGMCTRP)

        dataGMCTRN = bytearray([0x03, 0x1d, 0x07, 0x06, 0x2e, 0x2c, 0x29, 0x2d,
                                0x2e, 0x2e, 0x37, 0x3f, 0x00, 0x00, 0x02, 0x10])
        self._writecommand(TFT.GMCTRN1)
        self._writedata(dataGMCTRN)

        self._writecommand(TFT.NORON)                #Normal display on.
        time.sleep_us(10)

        self._writecommand(TFT.DISPON)
        time.sleep_us(100)

        self.csPin(1)

    #@micropython.native
    def setPixel( self, aX, aY, aColor ) :
        self.buffer.pixel(aX, aY, aColor)
    
    #@micropython.native
    def clear(self):
        self.buffer.fill(0)

    #@micropython.native
    def readPixel( self, aX, aY ):
       return self.buffer.pixel(aX, aY)
    
    #@micropython.native
    def swap( self ):
        self._setwindowloc((0,0), (self._size[0]-1, self._size[1]-1))
        self.dcPin(1)
        self.csPin(0)
        self.spi.write(self.buffer)
        self.csPin(1)
        
    #@micropython.native
    def on( self, aTF = True ) :
        self._writecommand(TFT.DISPON if aTF else TFT.DISPOFF)
        
    #@micropython.native
    def rgb( self, aTF = True ) :
        self._rgb = aTF
        self._setMADCTL()
        
    #@micropython.native
    def size( self ) :
        return self._size

    #@micropython.native
    def rotation( self, aRot ) :
        if (0 <= aRot < 4):
            rotchange = self.rotate ^ aRot
            self.rotate = aRot
        if (rotchange & 1):
            self._size =(self._size[1], self._size[0])
            self._offset=(self._offset[1], self._offset[0])
        self._setMADCTL()

    #@micropython.native
    def fillrect( self, aStart, aSize, aColor ) :
        self.buffer.fill_rect(aStart[0], aStart[1], aSize[0], aSize[1], aColor)

    def fill( self, aColor = BLACK ) :
        self.buffer.fill(aColor)

    def image( self, x0, y0, x1, y1, data ) :
        self._setwindowloc((x0, y0), (x1, y1))
        self._writedata(data)

    #@micropython.native
    def line( self, aStart, aEnd, aColor ) :
        self.buffer.line(aStart[0],aStart[1],aEnd[0],aEnd[1],aColor)

    #@micropython.native
    def hline( self, aStart, aW, aColor ) :
        self.buffer.hline(aStart[0],aStart[1],aW,aColor)

    #@micropython.native
    def vline( self, aStart, aH, aColor ) :
        self.buffer.vline(aStart[0],aStart[1],aH,aColor)

    #@micropython.native
    def text( self, aText, aStart, aColor ) :
        self.buffer.text(aText,aStart[0],aStart[1],aColor)

    #@micropython.native
    def scroll( self, xStep, yStep ) :
        self.buffer.scroll(xStep, yStep)

    #@micropython.native
    def rect( self, aStart, aSize, aColor ) :
        self.buffer.rect(aStart[0],aStart[1],aSize[0],aSize[1],aColor)

    #@micropython.native
    def _setColor( self, aColor ) :
        self.colorData[0] = aColor >> 8
        self.colorData[1] = aColor
        self.buf = bytes(self.colorData) * 32

    #@micropython.native
    def _draw( self, aPixels ) :
        self.dcPin(1)
        self.csPin(0)
        for i in range(aPixels//32):
            self.spi.write(self.buf)
        rest = (int(aPixels) % 32)
        if rest > 0:
            buf2 = bytes(self.colorData) * rest
            self.spi.write(buf2)
        self.csPin(1)

    #@micropython.native
    def _setwindowpoint( self, aPos ) :
        x = self._offset[0] + int(aPos[0])
        y = self._offset[1] + int(aPos[1])
        self._writecommand(TFT.CASET)            #Column address set.
        self.windowLocData[0] = self._offset[0]
        self.windowLocData[1] = x
        self.windowLocData[2] = self._offset[0]
        self.windowLocData[3] = x
        self._writedata(self.windowLocData)

        self._writecommand(TFT.RASET)            #Row address set.
        self.windowLocData[0] = self._offset[1]
        self.windowLocData[1] = y
        self.windowLocData[2] = self._offset[1]
        self.windowLocData[3] = y
        self._writedata(self.windowLocData)
        self._writecommand(TFT.RAMWR)            #Write to RAM.

    #@micropython.native
    def _setwindowloc( self, aPos0, aPos1 ) :
        self._writecommand(TFT.CASET)            #Column address set.
        self.windowLocData[0] = self._offset[0]
        self.windowLocData[1] = self._offset[0] + int(aPos0[0])
        self.windowLocData[2] = self._offset[0]
        self.windowLocData[3] = self._offset[0] + int(aPos1[0])
        self._writedata(self.windowLocData)

        self._writecommand(TFT.RASET)            #Row address set.
        self.windowLocData[0] = self._offset[1]
        self.windowLocData[1] = self._offset[1] + int(aPos0[1])
        self.windowLocData[2] = self._offset[1]
        self.windowLocData[3] = self._offset[1] + int(aPos1[1])
        self._writedata(self.windowLocData)

        self._writecommand(TFT.RAMWR)            #Write to RAM.

    #@micropython.native
    def _writecommand( self, aCommand ) :
        self.dcPin(0)
        self.csPin(0)
        self.spi.write(bytearray([aCommand]))
        self.csPin(1)

    #@micropython.native
    def _writedata( self, aData ) :
        self.dcPin(1)
        self.csPin(0)
        self.spi.write(aData)
        self.csPin(1)

    #@micropython.native
    def _pushcolor( self, aColor ) :
        self.colorData[0] = aColor >> 8
        self.colorData[1] = aColor
        self._writedata(self.colorData)

    #@micropython.native
    def _setMADCTL( self ) :
        self._writecommand(TFT.MADCTL)
        rgb = TFTRGB if self._rgb else TFTBGR
        self._writedata(bytearray([TFTRotations[self.rotate] | rgb]))

    #@micropython.native
    def _reset( self ) :
        self.dcPin(0)
        if self.useReset :
            self.resetPin(1)
            time.sleep_us(500)
            self.resetPin(0)
            time.sleep_us(500)
            self.resetPin(1)
        time.sleep_us(500)

The implementation relies on the swap() command as the final command to bring the data from the buffer to display in the TFT module. Therefore, the programmer must design the code to suit the double-buffer operation as well.

Code

from st7735x import TFT
from sysfont import sysfont
from machine import SPI,Pin
import machine as mc
import time
import math
mc.freq(240000000)
spi = SPI(2, baudrate=33000000,
          sck=Pin(14), mosi=Pin(12),
          polarity=0, phase=0)
# dc, rst, cs
tft=TFT(spi,15,13,2)

tft.fill(tft.BLACK)
tft.text("(C)2020-21",(10,36),tft.YELLOW)
tft.text("JarutEx",(92,36),tft.WHITE)
tft.text("JarutEx",(93,36),tft.WHITE)
tft.swap()
time.sleep_ms(2000)
tft.fill(tft.BLACK)

t0 = time.ticks_ms()
for row in range(tft._size[1]):
    color = tft.color(int(row*(256.0/tft._size[1])),32,32)
    for col in range(tft._size[0]):
        tft.setPixel(col, row, color)
tft.swap()
print("done setPixel() in {} msec".format(time.ticks_ms()-t0))

color = tft.color(192,32,64)

t0 = time.ticks_ms()
tft.fill(tft.BLACK)
for x in range(0, tft.size()[0], 6):
    tft.line((0,0),(x, tft.size()[1] - 1), color)
    tft.swap()
for y in range(0, tft.size()[1], 6):
    tft.line((0,0),(tft.size()[0] - 1, y), color)
    tft.swap()
print("done test line() #1 in {} msec".format(time.ticks_ms()-t0))

t0 = time.ticks_ms()
tft.fill(tft.BLACK)
for x in range(0, tft.size()[0], 6):
    tft.line((tft.size()[0] - 1, 0), (x, tft.size()[1] - 1), color)
    tft.swap()
for y in range(0, tft.size()[1], 6):
    tft.line((tft.size()[0] - 1, 0), (0, y), color)
    tft.swap()
print("done test line() #2 in {} msec".format(time.ticks_ms()-t0))

t0 = time.ticks_ms()
tft.fill(tft.BLACK)
for x in range(0, tft.size()[0], 6):
    tft.line((0, tft.size()[1] - 1), (x, 0), color)
    tft.swap()
for y in range(0, tft.size()[1], 6):
    tft.line((0, tft.size()[1] - 1), (tft.size()[0] - 1,y), color)
    tft.swap()
print("done test line() #3 in {} msec".format(time.ticks_ms()-t0))

t0 = time.ticks_ms()
tft.fill(tft.BLACK)
for x in range(0, tft.size()[0], 6):
    tft.line((tft.size()[0] - 1, tft.size()[1] - 1), (x, 0), color)
    tft.swap()
for y in range(0, tft.size()[1], 6):
    tft.line((tft.size()[0] - 1, tft.size()[1] - 1), (0, y), color)
    tft.swap()
print("done test line() #4 in {} msec".format(time.ticks_ms()-t0))

tft.fill(tft.BLACK)
tft.text("(C)2020-21",(10,36),tft.YELLOW)
tft.text("JarutEx",(92,36),tft.WHITE)
tft.text("JarutEx",(93,36),tft.WHITE)
tft.swap()
time.sleep_ms(10000)
tft.fill(0)
tft.on(False)
spi.deinit()

The result is shown in Figure 2.

(Figure. 2 Rendering of the line drawing operation.)

From the modified code, the result is shown in Figure 3.

(Figure. 3 An example of the resulting code from the improved library)

For testing with old libraries write the following code: and the result is as shown in Figure 4.

from st7735 import TFT
from sysfont import sysfont
from machine import SPI,Pin
import machine as mc
import time
import math
mc.freq(240000000)
spi = SPI(2, baudrate=27000000,
          sck=Pin(14), mosi=Pin(12),
          polarity=0, phase=0)
# dc, rst, cs
tft=TFT(spi,15,13,2)
tft.init_7735(tft.GREENTAB80x160)

tft.fill(tft.BLACK)
tft.text((10,36),"(C)2020-21",tft.YELLOW, sysfont)
tft.text((92,36),"JarutEx",tft.WHITE, sysfont)
tft.text((93,36),"JarutEx",tft.WHITE, sysfont)
time.sleep_ms(2000)
tft.fill(tft.BLACK)

t0 = time.ticks_ms()
for row in range(tft._size[1]):
    color = tft.color(int(row*(256.0/tft._size[1])),32,32)
    for col in range(tft._size[0]):
        tft.pixel((col,row), color)
print("done setPixel() in {} msec".format(time.ticks_ms()-t0))

color = tft.color(192,32,64)

t0 = time.ticks_ms()
tft.fill(tft.BLACK)
for x in range(0, tft.size()[0], 6):
    tft.line((0,0),(x, tft.size()[1] - 1), color)
for y in range(0, tft.size()[1], 6):
    tft.line((0,0),(tft.size()[0] - 1, y), color)
print("done test line() #1 in {} msec".format(time.ticks_ms()-t0))

t0 = time.ticks_ms()
tft.fill(tft.BLACK)
for x in range(0, tft.size()[0], 6):
    tft.line((tft.size()[0] - 1, 0), (x, tft.size()[1] - 1), color)
for y in range(0, tft.size()[1], 6):
    tft.line((tft.size()[0] - 1, 0), (0, y), color)
print("done test line() #2 in {} msec".format(time.ticks_ms()-t0))

t0 = time.ticks_ms()
tft.fill(tft.BLACK)
for x in range(0, tft.size()[0], 6):
    tft.line((0, tft.size()[1] - 1), (x, 0), color)
for y in range(0, tft.size()[1], 6):
    tft.line((0, tft.size()[1] - 1), (tft.size()[0] - 1,y), color)
print("done test line() #3 in {} msec".format(time.ticks_ms()-t0))

t0 = time.ticks_ms()
tft.fill(tft.BLACK)
for x in range(0, tft.size()[0], 6):
    tft.line((tft.size()[0] - 1, tft.size()[1] - 1), (x, 0), color)
for y in range(0, tft.size()[1], 6):
    tft.line((tft.size()[0] - 1, tft.size()[1] - 1), (0, y), color)
print("done test line() #4 in {} msec".format(time.ticks_ms()-t0))

tft.fill(tft.BLACK)
tft.text((10,36),"(C)2020-21",tft.YELLOW, sysfont)
tft.text((92,36),"JarutEx",tft.WHITE, sysfont)
tft.text((93,36),"JarutEx",tft.WHITE, sysfont)

time.sleep_ms(10000)
tft.fill(0)
tft.on(False)
spi.deinit()


(Figure. 4 Result from the library before updating)

Modifications for use with REDTAB80x160

Code for REDTAB80x160

#Added 1.44 inch 128x128 and 0.96 inch 80x160 SST7735 to the
# original 1.8 inch drivers developed by Guy Carver
# fixed the issue of different start Row and start col addresses of different OLED types
# fixed the RGB special equence in 80x160 ST7735B
#
# ปรับปรุงเพื่อใช้กับ ST7735S GREENTAB80x160 (เท่านั้น)
# by JarutEx (www.jarutex.com)
# Update 2021-09-07
# REDTAB80x160 - 2021-09-27
from machine import Pin
import time
import machine
import framebuf

#TFTRotations and TFTRGB are bits to set
# on MADCTL to control display rotation/color layout
#Looking at display with pins on top.
#00 = upper left printing right
#10 = does nothing (MADCTL_ML)
#20 = upper left printing down (backwards) (Vertical flip)
#40 = upper right printing left (backwards) (X Flip)
#80 = lower left printing right (backwards) (Y Flip)
#04 = (MADCTL_MH)

#60 = 90 right rotation
#C0 = 180 right rotation
#A0 = 270 right rotation
TFTRotations = [0x00, 0x60, 0xC0, 0xA0]
TFTBGR = 0x08 # for 1.8 and 1.44 inch display
TFTRGB = 0x00

#@micropython.native
def clamp( aValue, aMin, aMax ):
    return max(aMin, min(aMax, aValue))

#@micropython.native
def TFTColor( aR, aG, aB ):
    # Convert to RGB5655
    aColor = ((aR & 0xF8) << 8) | ((aG & 0xFC) << 3) | (aB >> 3)
    # Swap
    lColor = aColor >> 8
    hColor = aColor << 8
    return (lColor | hColor)

class TFT(object):
    NOP = 0x0
    SWRESET = 0x01
    RDDID = 0x04
    RDDST = 0x09
    
    SLPIN  = 0x10
    SLPOUT = 0x11
    PTLON  = 0x12
    NORON  = 0x13

    INVOFF = 0x20
    INVON = 0x21
    DISPOFF = 0x28
    DISPON = 0x29
    CASET = 0x2A
    RASET = 0x2B
    RAMWR = 0x2C
    RAMRD = 0x2E

    COLMOD = 0x3A
    MADCTL = 0x36

    FRMCTR1 = 0xB1
    FRMCTR2 = 0xB2
    FRMCTR3 = 0xB3
    INVCTR = 0xB4
    DISSET5 = 0xB6

    PWCTR1 = 0xC0
    PWCTR2 = 0xC1
    PWCTR3 = 0xC2
    PWCTR4 = 0xC3
    PWCTR5 = 0xC4
    VMCTR1 = 0xC5

    RDID1 = 0xDA
    RDID2 = 0xDB
    RDID3 = 0xDC
    RDID4 = 0xDD

    PWCTR6 = 0xFC

    GMCTRP1 = 0xE0
    GMCTRN1 = 0xE1

    BLACK = 0
    RED = TFTColor(0xFF, 0x00, 0x00)
    MAROON = TFTColor(0x80, 0x00, 0x00)
    GREEN = TFTColor(0x00, 0xFF, 0x00)
    FOREST = TFTColor(0x00, 0x80, 0x80)
    BLUE = TFTColor(0x00, 0x00, 0xFF)
    NAVY = TFTColor(0x00, 0x00, 0x80)
    CYAN = TFTColor(0x00, 0xFF, 0xFF)
    YELLOW = TFTColor(0xFF, 0xFF, 0x00)
    PURPLE = TFTColor(0xFF, 0x00, 0xFF)
    WHITE = TFTColor(0xFF, 0xFF, 0xFF)
    GRAY = TFTColor(0x80, 0x80, 0x80)

    @staticmethod
    def color( aR, aG, aB ):
        return TFTColor(aR, aG, aB)

    def __init__( self, spi, aDC, aReset, aCS):
        self.dcPin  = machine.Pin(aDC, machine.Pin.OUT)
        if aReset == None :
            self.useReset = False
        else:
            self.useReset = True
            self.resetPin = machine.Pin(aReset, machine.Pin.OUT)
        if aCS == None :
            self.useCS = False
        else :
            self.useCS = True
            self.csPin = machine.Pin(aCS, machine.Pin.OUT)
        self.csPin(1)
        self.spi = spi
        self.colorData = bytearray(2)
        self.windowLocData = bytearray(4)
        self.buffer = framebuf.FrameBuffer(bytearray(80*160*2),160,80,framebuf.RGB565)
        
        self._reset()
        self._writecommand(TFT.SWRESET)              #Software reset.
        time.sleep_us(150)
        self._writecommand(TFT.SLPOUT)               #out of sleep mode.
        time.sleep_us(255)

        data3 = bytearray([0x01, 0x2C, 0x2D])       #fastest refresh, 6 lines front, 3 lines back.
        self._writecommand(TFT.FRMCTR1)              #Frame rate control.
        self._writedata(data3)

        self._writecommand(TFT.FRMCTR2)              #Frame rate control.
        self._writedata(data3)

        data6 = bytearray([0x01, 0x2c, 0x2d, 0x01, 0x2c, 0x2d])
        self._writecommand(TFT.FRMCTR3)              #Frame rate control.
        self._writedata(data6)
        time.sleep_us(10)

        self._writecommand(TFT.INVCTR)               #Display inversion control
        self._writedata(bytearray([0x07]))
        self._writecommand(TFT.PWCTR1)               #Power control
        data3[0] = 0xA2
        data3[1] = 0x02
        data3[2] = 0x84
        self._writedata(data3)

        self._writecommand(TFT.PWCTR2)               #Power control
        self._writedata(bytearray([0xC5]))

        data2 = bytearray(2)
        self._writecommand(TFT.PWCTR3)               #Power control
        data2[0] = 0x0A   #Opamp current small
        data2[1] = 0x00   #Boost frequency
        self._writedata(data2)

        self._writecommand(TFT.PWCTR4)               #Power control
        data2[0] = 0x8A   #Opamp current small
        data2[1] = 0x2A   #Boost frequency
        self._writedata(data2)

        self._writecommand(TFT.PWCTR5)               #Power control
        data2[0] = 0x8A   #Opamp current small
        data2[1] = 0xEE   #Boost frequency
        self._writedata(data2)

        self._writecommand(TFT.VMCTR1)               #Power control
        self._writedata(bytearray([0x0E]))

        self._writecommand(TFT.INVOFF)

        self._size = (80,160)
        self._offset = (24,0)
        #self._rgb = False
        self._rgb = True
        self._writecommand(TFT.INVON)

        self.rotate = 2
        self.rotation(1)
        # set the color mapping of  RGB or GBR
        self._setMADCTL()

        self._writecommand(TFT.COLMOD)
        self._writedata(bytearray([0x05]))

        self._writecommand(TFT.CASET)                #Column address set.

        self.windowLocData[0] = 0x00
        self.windowLocData[1] = self._offset[0]
        self.windowLocData[2] = 0x00
        self.windowLocData[3] = self._size[0]+self._offset[0]
        self._writedata(self.windowLocData)

        self._writecommand(TFT.RASET)                #Row address set.
        self.windowLocData[1] = self._offset[1]
        self.windowLocData[3] = self._size[1]+self._offset[1]
        self._writedata(self.windowLocData)

        dataGMCTRP = bytearray([0x02, 0x1c, 0x07, 0x12, 0x37, 0x32, 0x29, 0x2d,
                                0x29, 0x25, 0x2b, 0x39, 0x00, 0x01, 0x03, 0x10])
        self._writecommand(TFT.GMCTRP1)
        self._writedata(dataGMCTRP)

        dataGMCTRN = bytearray([0x03, 0x1d, 0x07, 0x06, 0x2e, 0x2c, 0x29, 0x2d,
                                0x2e, 0x2e, 0x37, 0x3f, 0x00, 0x00, 0x02, 0x10])
        self._writecommand(TFT.GMCTRN1)
        self._writedata(dataGMCTRN)

        self._writecommand(TFT.NORON)                #Normal display on.
        time.sleep_us(10)

        self._writecommand(TFT.DISPON)
        time.sleep_us(100)

        self.csPin(1)

    #@micropython.native
    def setPixel( self, aX, aY, aColor ) :
        self.buffer.pixel(aX, aY, aColor)
    
    #@micropython.native
    def clear(self):
        self.buffer.fill(0)

    #@micropython.native
    def readPixel( self, aX, aY ):
       return self.buffer.pixel(aX, aY)
    
    #@micropython.native
    def swap( self ):
        self._setwindowloc((0,0), (self._size[0]-1, self._size[1]-1))
        self.dcPin(1)
        self.csPin(0)
        self.spi.write(self.buffer)
        self.csPin(1)
        
    #@micropython.native
    def on( self, aTF = True ) :
        self._writecommand(TFT.DISPON if aTF else TFT.DISPOFF)
        
    #@micropython.native
    def rgb( self, aTF = True ) :
        self._rgb = aTF
        self._setMADCTL()
        
    #@micropython.native
    def size( self ) :
        return self._size

    #@micropython.native
    def rotation( self, aRot ) :
        if (0 <= aRot < 4):
            rotchange = self.rotate ^ aRot
            self.rotate = aRot
        if (rotchange & 1):
            self._size =(self._size[1], self._size[0])
            self._offset=(self._offset[1], self._offset[0])
        self._setMADCTL()

    #@micropython.native
    def fillrect( self, aStart, aSize, aColor ) :
        self.buffer.fill_rect(aStart[0], aStart[1], aSize[0], aSize[1], aColor)

    def fill( self, aColor = BLACK ) :
        self.buffer.fill(aColor)

    def image( self, x0, y0, x1, y1, data ) :
        self._setwindowloc((x0, y0), (x1, y1))
        self._writedata(data)

    #@micropython.native
    def line( self, aStart, aEnd, aColor ) :
        self.buffer.line(aStart[0],aStart[1],aEnd[0],aEnd[1],aColor)

    #@micropython.native
    def hline( self, aStart, aW, aColor ) :
        self.buffer.hline(aStart[0],aStart[1],aW,aColor)

    #@micropython.native
    def vline( self, aStart, aH, aColor ) :
        self.buffer.vline(aStart[0],aStart[1],aH,aColor)

    #@micropython.native
    def text( self, aText, aStart, aColor ) :
        self.buffer.text(aText,aStart[0],aStart[1],aColor)

    #@micropython.native
    def scroll( self, xStep, yStep ) :
        self.buffer.scroll(xStep, yStep)



    #@micropython.native
    def rect( self, aStart, aSize, aColor ) :
        self.buffer.rect(aStart[0],aStart[1],aSize[0],aSize[1],aColor)

    #@micropython.native
    def _setColor( self, aColor ) :
        self.colorData[0] = aColor >> 8
        self.colorData[1] = aColor
        self.buf = bytes(self.colorData) * 32

    #@micropython.native
    def _draw( self, aPixels ) :
        self.dcPin(1)
        self.csPin(0)
        for i in range(aPixels//32):
            self.spi.write(self.buf)
        rest = (int(aPixels) % 32)
        if rest > 0:
            buf2 = bytes(self.colorData) * rest
            self.spi.write(buf2)
        self.csPin(1)

    #@micropython.native
    def _setwindowpoint( self, aPos ) :
        x = self._offset[0] + int(aPos[0])
        y = self._offset[1] + int(aPos[1])
        self._writecommand(TFT.CASET)            #Column address set.
        self.windowLocData[0] = self._offset[0]
        self.windowLocData[1] = x
        self.windowLocData[2] = self._offset[0]
        self.windowLocData[3] = x
        self._writedata(self.windowLocData)

        self._writecommand(TFT.RASET)            #Row address set.
        self.windowLocData[0] = self._offset[1]
        self.windowLocData[1] = y
        self.windowLocData[2] = self._offset[1]
        self.windowLocData[3] = y
        self._writedata(self.windowLocData)
        self._writecommand(TFT.RAMWR)            #Write to RAM.

    #@micropython.native
    def _setwindowloc( self, aPos0, aPos1 ) :
        self._writecommand(TFT.CASET)            #Column address set.
        self.windowLocData[0] = self._offset[0]
        self.windowLocData[1] = self._offset[0] + int(aPos0[0])
        self.windowLocData[2] = self._offset[0]
        self.windowLocData[3] = self._offset[0] + int(aPos1[0])
        self._writedata(self.windowLocData)

        self._writecommand(TFT.RASET)            #Row address set.
        self.windowLocData[0] = self._offset[1]
        self.windowLocData[1] = self._offset[1] + int(aPos0[1])
        self.windowLocData[2] = self._offset[1]
        self.windowLocData[3] = self._offset[1] + int(aPos1[1])
        self._writedata(self.windowLocData)

        self._writecommand(TFT.RAMWR)            #Write to RAM.

    #@micropython.native
    def _writecommand( self, aCommand ) :
        self.dcPin(0)
        self.csPin(0)
        self.spi.write(bytearray([aCommand]))
        self.csPin(1)

    #@micropython.native
    def _writedata( self, aData ) :
        self.dcPin(1)
        self.csPin(0)
        self.spi.write(aData)
        self.csPin(1)

    #@micropython.native
    def _pushcolor( self, aColor ) :
        self.colorData[0] = aColor >> 8
        self.colorData[1] = aColor
        self._writedata(self.colorData)

    #@micropython.native
    def _setMADCTL( self ) :
        self._writecommand(TFT.MADCTL)
        rgb = TFTRGB if self._rgb else TFTBGR
        self._writedata(bytearray([TFTRotations[self.rotate] | rgb]))

    #@micropython.native
    def _reset( self ) :
        self.dcPin(0)
        if self.useReset :
            self.resetPin(1)
            time.sleep_us(500)
            self.resetPin(0)
            time.sleep_us(500)
            self.resetPin(1)
        time.sleep_us(500)

Conclusion

From this article, we hope that you will be stuck in the matter of improving the efficiency of the code studied from the Internet. And it gets even better when we roll out our improvements so others can spot bugs and improve the code of our predecessors, including us who pushed it for better service life and performance.

Finally, have fun with programming.

If you want to talk with us, feel free to leave comments below!!

(C) 2020-2021, By Jarut Busarathid and Danai Jedsadathitikul
Updated 2021-12-15