User avatar
rin67630
Posts: 376
Joined: Fri Mar 04, 2016 10:15 am

Using a SPI LCD display in Non-Desktop mode?

Mon Jul 22, 2019 5:06 pm

Hi has someone ever discovered a way to use a SPI LCD display in Non-Desktop mode?
The means beside the desktop, to display stuff written from e.g. Python?

hippy
Posts: 5585
Joined: Fri Sep 09, 2011 10:34 pm
Location: UK

Re: Using a SPI LCD display in Non-Desktop mode?

Mon Jul 22, 2019 6:31 pm

TL:DR - Yes.

But it's not necessarily quick in pure Python when bit-banging a bitmap out. It may depend on which LCD you want to use as they don't all have the same interfacing, colour depths or command codes.

I got a WaveShare 480x320 3.5" LCD updated in a few seconds. That should be quicker using spidev. I couldn't get that to work but may just be 'user error'. I decided to create a C Extension for Python which could take the bitmap and churn it out at native C speeds but that's been put on the back burner.

There should also be some 'just the pixel data' modes, rather than having to send multiple addressing bytes per pixel which should also speed things up but never got that far.

User avatar
rin67630
Posts: 376
Joined: Fri Mar 04, 2016 10:15 am

Re: Using a SPI LCD display in Non-Desktop mode?

Mon Jul 22, 2019 6:43 pm

I currently have got a similar KeDei 480x320 3.5" LCD display with a SPI interface.
I would like to use it as a plotter, so nothing needs to be fast.
I suppose one can use a Library for the ILI controller, did you use it?

hippy
Posts: 5585
Joined: Fri Sep 09, 2011 10:34 pm
Location: UK

Re: Using a SPI LCD display in Non-Desktop mode?

Mon Jul 22, 2019 7:06 pm

rin67630 wrote:
Mon Jul 22, 2019 6:43 pm
I suppose one can use a Library for the ILI controller, did you use it?
I ended up looking at some of those and the datasheet and ended up creating my own routines -

Code: Select all

#/usr/bin/python2
# -*- coding: utf-8 -*-

# WaveShare 3.5" LCD connects to the first 26 pins of the GPIO connector. Note
# that it uses both 5V and 3V3 power pins. In the diagram below '-' are pins
# which are not used by the WaveShare LCD.
#
#                             .-----.
#                     3V3   1 | O O | 2   5V
#                           3 | - O | 4   5V
#                           5 | - O | 6   0V
#                           7 | - - | 8
#                      0V   9 | O - | 10
#       GPIO.17  IRQ       11 | O - | 12
#                          13 | - O | 14  0V
#                          15 | - - | 16
#                     3V3  17 | O O | 18      DC         GPIO.24
#       GPIO.10   DO       19 | O O | 20  0V
#       GPIO.9    DI       21 | O O | 22      RESET      GPIO.25
#       GPIO.11   CK       23 | O O | 24      CS_LCD     GPIO.8
#                      0V  25 | O O | 26      CS_TOUCH   GPIO.7
#                             `-----'

import RPi.GPIO as GPIO
import time

class WaveShare35():

  def __init__(self):
    self.RS = 25                # Reset
    self.CS =  8                # Chip Select LCD
    self.TS =  7                # Chip Select Touch Screen
    self.DC = 24                # Data/Command control
    self.DO = 10                # Data Out from Pi
    self.DI =  9                # Data In to Pi
    self.CK = 11                # Clock
    self.IQ = 17                # IRQ

    GPIO.setmode(GPIO.BCM)      # Use GPIO numbering
    GPIO.setwarnings(False)     # No warnings

    self.Dir(self.RS, 0, 1)
    self.Dir(self.DC, 0, 0)
    self.Dir(self.CS, 0, 1)
    self.Dir(self.TS, 0, 1)
    self.Dir(self.IQ, 1)        # Input

    self.Dir(self.CK, 0, 0)
    self.Dir(self.DO, 0, 0)
    self.Dir(self.DI, 1)        # Input

    # Reset
    self.Set(self.RS, 1)
    self.Set(self.RS, 0)
    self.Set(self.RS, 1)

    # Interface Mode Control
    #
    #        .---------------.---------------.
    # INTFAC | 1   0   1   1 | 0   0   0   0 | 0xB0
    #        |---.-----------+---.---.---.---|
    #        | 0 | -   -   - | 0 | 0 | 0 | 0 | 0x00
    #        `---^-----------^---^---^---^---'
    #         SDA             VSP HSP DPL EPL
    #
    #        SDA : 3/4-bit        - 0=DI+DO (4)        1=DO is not used (3)
    #        EPL : DE    polarity - 0=High for enable, 1=Low for enable
    #        DPL : PCLK  polarity - 0=Rising           1=Falling
    #        HSP : HSYNC polarity - 0=Low              1=High
    #        VSP : VSYNC polarity - 0=Low              1=High

    self.SpiOut(0xB0, 0x00)

    # Sleep OUT
    #
    #        .---------------.---------------.
    # COLMOD | 0   0   0   1 | 0   0   0   1 | 0x11
    #        `---------------^---------------'

    self.SpiOut(0x11)

    time.sleep(0.250)

    # Interface Pixel Format
    #
    #        .---------------.---------------.
    # COLMOD | 0   0   1   1 | 1   0   1   0 | 0x3A
    #        |---.-----------+---.-----------|
    #        | - | 1   0   1 | - | 1   0   1 | 0x55 = 16-bit per pixels
    #        `---^-----------^---^-----------'
    #                 RGB           CPU
    #
    #  RGB, CPU : 101 = 16 bits per pixel
    #           : 110 = 18 bits per pixel

    self.SpiOut(0x3A, 0x55)

    # Memory Access Control
    #
    #        .---------------.---------------.
    # MADCTL | 0   0   1   1 | 0   1   1   0 | 0x36
    #        |---.---.---.---+---.---.-------|
    #        | 0 | 1 | 1 | 0 | 1 | 0 | -   - | 0x38
    #        `---^---^---^---^---^---^-------'
    #         MY  MX  MV  ML  RGB  MH
    #
    #         MY  : Row Address Order
    #         MX  : Col Address Order
    #         MV  : Row / Column Exchange
    #         ML  : Vertical Refresh Order
    #         RGB : 0=RGB 1=BGR
    #         MH  : Horizontal Refresh order

    self.SpiOut(0x36, 0x38)

    # Power Control 3 (For Normal Mode)
    #
    #        .---------------.---------------.
    # PC3FNM | 1   1   0   0 | 0   0   1   0 | 0xC2
    #        |---.-----------+---.-----------|
    #        | - | 1   0   0 | - | 1   0   0 | 0x44
    #        `---^-----------^---^-----------'
    #                DCA1            DCA0
    #
    #        DCA1 : 000 = 1/2 H  DCA0 : 000 = 1/8 H
    #               001 = 1   H         001 = 1/4 H
    #               010 = 2   H         010 = 1/2 H
    #               011 = 4   H         011 = 1   H
    #               100 = 8   H         100 = 2   H

    self.SpiOut(0xC2, 0x44)

    # VCOM Control
    #
    #        .---------------.---------------.
    # VCOM   | 1   1   0   0 | 0   1   0   1 | 0xC2
    #        |---------------+-----------.---|
    #        | -   -   -   - | -   -   - | 0 | 0x00
    #        |---------------+-----------.---|
    #        | N   N   N   N | N   N   N   N | 0x00 = -2V
    #        |---.-----------+---------------|
    #        | 0 | -   -   - | -   -   -   - | 0x00 = Read from NVM
    #        |---^-----------+---------------|
    #        | N   N   N   N | N   N   N   N | 0x00
    #        `---------------^---------------'

    self.SpiOut(0xC5, 0x00, 0x00, 0x00, 0x00)

    # Positive Gamma Control
    self.SpiOut(0xE0, 0x0F, 0x1F, 0x1C, 0x0C, 0x0F, 0x08, 0x48, 0x98,
                      0x37, 0x0A, 0x13, 0x04, 0x11, 0x0D, 0x00)

    # Negative Gamma Correction
    self.SpiOut(0xE1, 0x0F, 0x32, 0x2E, 0x0B, 0x0D, 0x05, 0x47, 0x75,
                      0x37, 0x06, 0x10, 0x03, 0x24, 0x20, 0x00)

    # Digital Gamma Correction
    self.SpiOut(0xE2, 0x0F, 0x32, 0x2E, 0x0B, 0x0D, 0x05, 0x47, 0x75,
                      0x37, 0x06, 0x10, 0x03, 0x24, 0x20, 0x00)

    # Display ON
    #
    #        .---------------.---------------.
    # DISPON | 0   0   1   0 | 1   0   0   1 | 0x29
    #        `---------------^---------------'

    self.SpiOut(0x29)

  def __enter__(self):
    return self

  def __exit__(self, type, value, trace):
    GPIO.cleanup()

  def Dir(self, pin, dir, val=0):
    if dir == 0:
      if val == 0 : GPIO.setup(pin, GPIO.OUT, initial=GPIO.LOW)
      else        : GPIO.setup(pin, GPIO.OUT, initial=GPIO.HIGH)
    else          : GPIO.setup(pin, GPIO.IN)

  def Set(self, pin, val):
    if val == 0 : GPIO.output(pin, GPIO.LOW)
    else        : GPIO.output(pin, GPIO.HIGH)

  def Get(self, pin):
    return GPIO.input(pin)

  def SpiOut(self,b,*args):
    self.Set(self.DC, 0)
    self.Set(self.CS, 0)
    m = 0x8000
    while m != 0:
      self.Set(self.DO, b & m)
      self.Set(self.CK, 1)
      self.Set(self.CK, 0)
      m = m >> 1
    if len(args) > 0:
      self.Set(self.DC, 1)
      for b in args:
        m = 0x8000
        while m != 0:
          self.Set(self.DO, b & m)
          self.Set(self.CK, 1)
          self.Set(self.CK, 0)
          m = m >> 1
    self.Set(self.CS, 1)

  def SetPixelNative(self, row, col, rgb):
    self.SpiOut(0x2A, col >> 8, col & 0xFF, col >> 8, col & 0xFF)
    self.SpiOut(0x2B, row >> 8, row & 0xFF, row >> 8, row & 0xFF)
    self.SpiOut(0x2C, self.EncodeRGB565(rgb))

  def EncodeRGB565(self, rgb):
    r,g,b = rgb
    return ( ( r & 0xF8 ) << 8 ) | \
           ( ( g & 0xFC ) << 3 ) | \
           ( ( b & 0xF8 ) >> 3 )

def Main():
  with WaveShare35() as w:

    for row in range(50):
      for col in range(100):
         w.SetPixelNative(row, col, ((col & 0xFF)^0xFF,0,0) )

    for n in range(60):
      time.sleep(1)

if __name__ == '__main__':
  Main()
That should work 'as is' if your LCD has the same ILI9486 controller chip and same GPIO pinout.

No buffering, no optimisation, just direct pixel writes to the screen, one after the other. If I recall correctly it draws a smallish red rectangle in one corner.

User avatar
rin67630
Posts: 376
Joined: Fri Mar 04, 2016 10:15 am

Re: Using a SPI LCD display in Non-Desktop mode?

Mon Jul 22, 2019 8:37 pm

It is probably better to not reinvent the wheel.

You have got a library here https://github.com/adafruit/Adafruit_Python_ILI9341
that will give you functions to draw, write erase...

hippy
Posts: 5585
Joined: Fri Sep 09, 2011 10:34 pm
Location: UK

Re: Using a SPI LCD display in Non-Desktop mode?

Tue Jul 23, 2019 12:49 pm

rin67630 wrote:
Mon Jul 22, 2019 8:37 pm
It is probably better to not reinvent the wheel.
I think that depends.

Not only may a wheel, or anything it relies upon, come with licensing or usage conditions which can only be avoided by reinvention, but the reinvention will give insights into how what exists is, and can also provide opportunities for doing things in other and even better ways.

In some cases reinvention will reveal flaws or misunderstanding in existing implementations. And, if nothing else, it provides a basis for being able to assess the correctness and appropriateness of what there is.

I find reinvention is often the best path to gaining a full or deep understanding and find a bottom-up approach better than top-down, though inevitably it's a mix of both. Bottom-up reinvention is often useful for when it comes to implementing something similar where nothing so far exists. I also find that knowing my code gives me more confidence in it working, easier debugging when it doesn't, and for changing things or porting the code without running into expected issues.

For me the drive for reinventing the wheel here was to understand how to drive the actual display and its controller. The purpose five-fold; to figure out how that could be driven by a microcontroller which could not use existing libraries, to remove stuff which wasn't needed, to create a larger framework where any framebuffer or display could be used just by specifying what was being used, to do it without dependency on any other libraries or modules, and to deliver MIT licensed code. I suppose, in short, 'I wanted to do it better', by my measure anyway.

But none of that is relevant to anyone who just wants to use something. Just use what already exists.
rin67630 wrote:
Mon Jul 22, 2019 8:37 pm
You have got a library here https://github.com/adafruit/Adafruit_Python_ILI9341
that will give you functions to draw, write erase...
Yes, that's great if that's what you want or need and it works. Just use that.

I believe the ILI9341 and ILI9486 use different controller command sets and configurations so you probably need to check that's the right driver module for the display module you have.

Return to “Advanced users”