A little fun with displays and Micropython

Started by Jeff_T, Apr 11, 2024, 10:24 PM

Previous topic - Next topic

Jeff_T

A little while ago I bought a TFT display from Waveshare for use with a BBC microbit microcontroller. The display was based on the ST7735 driver. At the time I ported some typescript code provided by Waveshare to Micropython and managed to get it up and running in an okayish kind of way.

Since then I have made quite a bit of headway, using Micropython with the Nano ESP32 which gives me the memory to use the Micropython framebuffer module. I know there are display drivers out there written by professionals but you cant beat the fun of learning and having a go yourself.

I'm not looking for super fast animation but I do like flicker free, the framebuffer does a pretty decent job of this and includes the primitive lines and shapes and a small text object, on top of that I have created some custom fonts and also some methods for decoding and displaying full screen bitmaps.

I want to share some links and some code for anyone interested in playing around with this graphics demo and if requested I can also share some custom font routines and full screen image examples.

There is one small thing with the framebuffer that I don't understand and that is the colors dont display correctly so unless I find out why that is I have a method that adjusts the framebuffer colors to the correct values. For this purpose use the function rgb565().

Framebuffer documentation link
https://docs.micropython.org/en/latest/library/framebuf.html

Wiring diagram for a 1.8 inch 160 x 128 pixel spi tft display connected to a Nano ESP32
https://photos.app.goo.gl/aS86eKpmCsy1Gr5YA

ST7735 Datasheet
https://www.displayfuture.com/Display/datasheet/controller/ST7735.pdf

Copy these two code snippets to the ESP32 flash. The first snippet name "test.py" and the second snippet name "sc_ST7735.py", then run test.py to see the results.

test.py

from machine import SPI, Pin, PWM
import time
from sc_ST7735 import LCD
import gc

WHITE = const(0xFFFF)
BLACK = const(0x0000)
RED = const(0xF800)
BLUE = const(0x001F)
GREEN = const(0x07E0)
YELLOW = const(0xFFE0)
MAGENTA = const(0xF81F)
CYAN = const(0x07FF)
ORANGE = const(0xFB80)
LIGHTBLUE = const(0x033F)
PINK = const(0xF80A)
GRAY = const(0x9492)

bl = PWM(Pin(5))
cs = Pin(6,Pin.OUT)
dc = Pin(7,Pin.OUT)
rst = Pin(8,Pin.OUT)

spi=SPI(1, 32000000)
lcd = LCD(spi, cs, dc, bl, rst)

################# User Code ###################################

size=8
(xmax, ymax) = (160-size, 128-size)
(x, y) = (size, size)
(vx, vy) = (4, 4)


while True:   
    lcd.fill(lcd.rgb565(YELLOW))
    lcd.ellipse(x, y, size, size, lcd.rgb565(RED), True)
    x += vx
    if x == xmax or x == size:
        vx = -vx
    y += vy
    if y == ymax or y == size:
        vy = -vy
    lcd.write_data(lcd.buffer)
   

sc_ST7735.py

from time import sleep_ms
import framebuf
import gc

LCD_WIDTH = const(160)
LCD_HEIGHT = const(128)

class LCD(framebuf.FrameBuffer):
   
    def __init__(self, spi, cs, dc, bl, rst):
        self.spi = spi
        self.cs = cs
        self.dc = dc
        self.bl = bl
        self.rst = rst
        self.height = LCD_HEIGHT
        self.width = LCD_WIDTH

        mode = framebuf.RGB565
        gc.collect()
        self.buffer = bytearray(self.height * self.width * 2)
        super().__init__(self.buffer, self.width, self.height, mode)

        self.init_lcd()

    def init_lcd(self):
        self.rst(1)
        sleep_ms(1)
        self.rst(0)
        sleep_ms(1)
        self.rst(1)

        # ST7735R Frame Rate
        self.write_reg(0xB1) #FRMCTR1
        self.write_cmd(0x01)
        self.write_cmd(0x2C)
        self.write_cmd(0x2D)

        self.write_reg(0xB2) #FRMCTR2
        self.write_cmd(0x01)
        self.write_cmd(0x2C)
        self.write_cmd(0x2D)

        self.write_reg(0xB3) #FRMCTR3
        self.write_cmd(0x01)
        self.write_cmd(0x2C)
        self.write_cmd(0x2D)
        self.write_cmd(0x01)
        self.write_cmd(0x2C)
        self.write_cmd(0x2D)

        self.write_reg(0xB4)  # INVCTR
        self.write_cmd(0x07)

        # ST7735 Power Sequence
        self.write_reg(0xC0) #PWCTR1
        self.write_cmd(0xA2)
        self.write_cmd(0x02)
        self.write_cmd(0x84)

        self.write_reg(0xC1) #PWCTR2
        self.write_cmd(0xC5)

        self.write_reg(0xC2) #PWCTR3
        self.write_cmd(0x0A)
        self.write_cmd(0x00)

        self.write_reg(0xC3) #PWCTR4
        self.write_cmd(0x8A)
        self.write_cmd(0x2A)

        self.write_reg(0xC4) #PWCTR5
        self.write_cmd(0x8A)
        self.write_cmd(0xEE)

        self.write_reg(0xC5)  #VMCTR1
        self.write_cmd(0x0E)

        # ST7735 Gamma Sequence
        self.write_reg(0xE0) #GMCTRP1
        self.write_cmd(0x0F)
        self.write_cmd(0x1A)
        self.write_cmd(0x0F)
        self.write_cmd(0x18)
        self.write_cmd(0x2F)
        self.write_cmd(0x28)
        self.write_cmd(0x20)
        self.write_cmd(0x22)
        self.write_cmd(0x1F)
        self.write_cmd(0x1B)
        self.write_cmd(0x23)
        self.write_cmd(0x37)
        self.write_cmd(0x00)
        self.write_cmd(0x07)
        self.write_cmd(0x02)
        self.write_cmd(0x10)

        self.write_reg(0xE1) #GMCTRN1
        self.write_cmd(0x0F)
        self.write_cmd(0x1B)
        self.write_cmd(0x0F)
        self.write_cmd(0x17)
        self.write_cmd(0x33)
        self.write_cmd(0x2C)
        self.write_cmd(0x29)
        self.write_cmd(0x2E)
        self.write_cmd(0x30)
        self.write_cmd(0x30)
        self.write_cmd(0x39)
        self.write_cmd(0x3F)
        self.write_cmd(0x00)
        self.write_cmd(0x07)
        self.write_cmd(0x03)
        self.write_cmd(0x10)

        self.write_reg(0xF0)  # Enable test command
        self.write_cmd(0x01)

        self.write_reg(0xF6)  # Disable ram power save mode
        self.write_cmd(0x00)

        self.write_reg(0x3A)  #COLMOD (RGB 565)
        self.write_cmd(0x05)

        self.write_reg(0x36)  #MADCTL
        self.write_cmd(0xF7 & 0xA0)  # RGB color filter panel (result = A0)
        self.write_reg(0x11) #SLPOUT
        sleep_ms(1)

        self.write_reg(0x29) #DISPON
        self.clear(0)

############### Start of user methods ###############

    def clear(self, color=0xFFFF):
        self.set_window(0, 0, LCD_WIDTH, LCD_HEIGHT)
        _col = bytearray(3)
        _col = color.to_bytes(2, "big")
        pixel_data=(_col[:2] * 5120)
        self.dc(1)  # EN LCD data
        self.cs(0)  # EN LCD CS
        for i in range(4):
            self.spi.write(pixel_data)
        self.cs(1)  # remove LCD CS EN
        pixel_data = bytearray(0)

    def write_reg(self, register):
        self.dc(0)  # EN LCD command
        self.cs(0)  # EN LCD CS
        self.spi.write(bytearray([register]))
        self.cs(1)  # remove LCD CS EN

    def write_cmd(self, data):
        self.dc(1)  # EN LCD data
        self.cs(0)  # EN LCD CS
        self.spi.write(bytearray([data]))
        self.cs(1)  # remove LCD CS
       
    def write_data(self, data):
        self.dc(1)  # EN LCD data
        self.cs(0)  # EN LCD CS
        self.spi.write(data)
        self.cs(1)  # remove LCD CS
       
    def set_backlight(self, level=500):
        self.bl.freq(5000)
        self.bl.duty(level)

    def set_window(self, Xstart, Ystart, Xend, Yend):
        # set the X coordinates
        self.write_reg(0x2A)
        self.write_cmd(0x00)
        self.write_cmd((Xstart & 0xFF) + 1)
        self.write_cmd(0x00)
        self.write_cmd(((Xend - 1) & 0xFF) + 1)

        # set the Y coordinates
        self.write_reg(0x2B)
        self.write_cmd(0x00)
        self.write_cmd((Ystart & 0xFF) + 2)
        self.write_cmd(0x00)
        self.write_cmd(((Yend - 1) & 0xFF) + 2)

        self.write_reg(0x2C)
       
    def draw_image(self,filename):
        self.set_window(0, 0, LCD_WIDTH, LCD_HEIGHT)
        with open(filename, 'rb') as f:
            self.dc(1)  # EN LCD data
            self.cs(0)  # EN LCD CS
            pixel_data = f.read()
            self.spi.write(pixel_data)
            self.cs(1)  # remove LCD CS EN
        pixel_data = bytearray(0)
       
    def rgb565(self,c):
        b = c & 0x1f
        g = c >> 5 & 0x3f
        r = c >> 11 & 0x1f
        new_color = b << 11 | r << 5 | g
        return new_color
   
   



Chris Savage

@Jeff_T - I hope you don't mind me fixing a broken link in the message.  ;)

I'll have to see which displays I have. I have several, but don't think any use that chipset.

        I'm only responsible for what I say, not what you understand.

Jeff_T

Hi Chris, you might be able to find a driver online. The ST7735 (160 x 128) and also the ILI9341 (320 x 240) are just two that I have the initialization code for and I enjoy creating my own driver functions. I prefer to use a display as a status indicator but I have had a ball bouncing around at 72 frames a second on the smaller display, of course that rate gets harder to achieve with a bigger display using the same methods.


Jeff_T

#4
This one would be ideal Chris, https://a.co/d/djlXPKu

This repository has a driver and the author is a superstar on the micropython forums
https://github.com/peterhinch/micropython-nano-gui/blob/master/drivers/ssd1331/ssd1331.py

The connections are practically identical to my original post and should only take a few minutes to get up and running.

If you can install micropython on one of your controllers I can help with the setup.

I would like to recommend and point you at a simple micropython tutorial I wrote a while ago on the Arduino forum, it explains a few basic principals. I'm not a technical writer but I think I get the point across and you might find it helpful.

EDIT I forgot a link
https://forum.arduino.cc/t/micropython-simple-gpio-control/1158187

Jeff_T

This would be the connections between the Nano ESP32 and the display, 5 GPIO plus a ground and positive.

Nano                   Display

3.3v  ------------------ VCC
GND   ------------------ GND
NC    ------------------ NC
SPI mosi GPIO38  ------- DIN
SPI clock GPIO48 ------- CLK
GPIO6   ---------------- CS
GPIO7  ----------------- D/C
GPIO8  ----------------- RES

The driver in the link has has two drivers for the ssd1331 display, the difference is bit depth one is 8 bit rgb and the other is 16 bit rgb and they are both orientated in the landscape position. 16 bit should be fine for us.

I don't have one of these displays so I have not tested anything but I feel sure this driver will be good.

Chris Savage

I worked overtime today, so I didn't get home in time to do anything. I checked and it looks like I purchased that display on December 2, 2022. It appears I bought the display by itself, so now I have to find it. I never used it, but not sure where it ended up. Here's a good laugh, while I try to locate this display...

Because the seller listed it as a motherboard:
You cannot view this attachment.

It recommends buying this display WITH an AMD Ryzen 7 CPU and Corsair DDR5 memory:
You cannot view this attachment.

        I'm only responsible for what I say, not what you understand.

Chris Savage

Quote from: Jeff_T on Apr 16, 2024, 04:08 PMThis would be the connections between the Nano ESP32 and the display, 5 GPIO plus a ground and positive.

I don't have one of these displays so I have not tested anything but I feel sure this driver will be good.

Okay, I will locate this display ASAP and post my results.

        I'm only responsible for what I say, not what you understand.

Jeff_T

#8
Hi Chris, I've seen them labelled motherboards before it's weird.

If you can't find the display and you decide to buy a display the following is cheaper and better IMHO. It's a 1.8 inch 160 x 128 pixels with ST7735 driver chip.

https://www.amazon.com/Display-Module-4-wires-interface-128x160/dp/B09HCSV72V/ref=sr_1_6?crid=I08JO8G9I9OY&dib=eyJ2IjoiMSJ9.2Nf0s4_Dx-QOeM-XNnsHZ1YtbkiFFI37BOQ47OKGpslZwgGdkbbBREEnduy-y_knMy6n8xduMXWOPeRA0i8J6gdF_50PGtaggPklVyD33Ovs4U3L_jRimR-AnMPaIu-QhOPLSwjAHhm78JE-OPfJ9SMU5-XFwQC7DaY6UfZmqXu1LPe4JVZyuqzyitGl3WClR_6ZCcCY2_uzo2Rr2HWwpKdCOmmZxnlH-XLiwBx4PpPPcmETDd50JEkkvUAFYdYI0-XpeUJ7B15IFa3z83zHUrxdg-KuqUPsnMtuCD6S_gk.czpyFoz4a9oOVrfA1lMouUc-Dwr-CPb-Dj8hiL0U0sc&dib_tag=se&keywords=st7735&qid=1713362438&s=electronics&sprefix=st7735%2Celectronics%2C89&sr=1-6

It also has the on board SD card which is something else to consider, using the SD alone is a useful exercise. I also have a driver for this display that I know works.

Chris Savage

Okay, I ordered that display. It said 1 left, so let's hope I got it in time! I should have it by the 19th and then we'll see what's up. Until then, I will still look for the other display. I'm also creating a folder structure for these experiments on my NAS Drive Engineering directory. More to come...

        I'm only responsible for what I say, not what you understand.

Jeff_T

The 19th that's great, I saw several on Amazon below $10 so if they let you down then at least there are alternatives.

The 1.8 inch display is is a pretty good size and gives good performance from a SPI driver.

I think the folder for keeping a record is an awesome idea, there are many resources and references on the internet but it is invaluable to have written notes of your own success and failures to look back on.

Talking of references and resources the official Micropython docs. are usually my first go to. Here is a link to one of those docs. and it has a reference to many of the hardware interfaces you might use at some time, UART,SPI,SD, Pins, Network, Neopixel and many more.

https://docs.micropython.org/en/latest/esp32/quickref.html

This is the best discussion forum with some really smart contributors

https://github.com/orgs/micropython/discussions

Chris Savage

#11
Quote from: Jeff_T on Apr 15, 2024, 03:05 PMThis one would be ideal Chris, https://a.co/d/djlXPKu

Guess what I found tonight?!?  :o  It was in a box of Arduino Accessories I got from Amazon, but haven't yet opened or used, including some multi-channel D/A and A/D boards.

This means tomorrow I can test some code, prior to getting the other displays.

You cannot view this attachment.

You cannot view this attachment.

        I'm only responsible for what I say, not what you understand.

Jeff_T

#12
That's great Chris, the connections are as in post #5.

I adjusted the driver to work with the Nano ESP32. The driver came with a simple "test file".

If we can get this to work then we can expand on the graphic capabilities, I feel more confident about the new display you just ordered but lets have a go with this one.

So there are two files, I'm going to post the code for both. The first is the driver and must be saved to the flash of the microcontroller and must have the name sc_1331.py.

sc_1331.py

import framebuf
import time
import gc


class SSD1331(framebuf.FrameBuffer):

    def rgb(self,r, g, b):
        return ((b & 0xf8) << 5) | ((g & 0x1c) << 11) | (r & 0xf8) | ((g & 0xe0) >> 5)
   

    def __init__(self, spi, cs, dc, res, height=64, width=96, init_spi=False):
        self.spi = spi
        self.cs = cs
        self.dc = dc  # 1 = data 0 = cmd
        self.height = height  # Required by Writer class
        self.width = width
        self.spi_init = init_spi
        mode = framebuf.RGB565
        gc.collect()
        self.buffer = bytearray(self.height * self.width * 2)
        super().__init__(self.buffer, self.width, self.height, mode)
        res(0)  # Pulse the reset line
        time.sleep_ms(1)
        res(1)
        time.sleep_ms(1)
        if self.spi_init:  # A callback was passed
            self.spi_init(spi)  # Bus may be shared
        self.write(b'\xae\xa0\x72\xa1\x00\xa2\x00\xa4\xa8\x3f\xad\x8e\xb0'\
        b'\x0b\xb1\x31\xb3\xf0\x8a\x64\x8b\x78\x8c\x64\xbb\x3a\xbe\x3e\x87'\
        b'\x06\x81\x91\x82\x50\x83\x7d\xaf', 0)
        gc.collect()
        self.show()

    def write(self, buf, dc):
        self.cs(1)
        self.dc(dc)
        self.cs(0)
        self.spi.write(buf)
        self.cs(1)

    def show(self, cmd=b'\x15\x00\x5f\x75\x00\x3f'):  # Pre-allocate
        if self.spi_init:  # A callback was passed
            self.spi_init(spi)  # Bus may be shared
        self.write(cmd, 0)
        self.write(self.buffer, 1)

The second file is the test file and can be named anything but must have the .py extension. The author called it test_colors.py. Copy the code into the IDE and press F5 to run, if all is well the display will fill with a red, green and blue bands with a gradient. If that does not happen then we will have to do some troubleshooting.

test_colors.py

import machine
from machine import Pin,SPI,PWM
from sc_1331 import SSD1331 as SSD


def setup():
    cs = Pin(6,Pin.OUT,value=1)
    dc = Pin(7,Pin.OUT)
    rst = Pin(8,Pin.OUT)
    spi = machine.SPI(1,10000000)
    ssd = SSD(spi, cs, dc, rst) 
    return ssd

ssd = setup()
ssd.fill(0)

x = 0
for y in range(96):
    ssd.line(y, x, y, x+20, ssd.rgb(round(255*y/96), 0, 0))
x += 20
for y in range(96):
    ssd.line(y, x, y, x+20, ssd.rgb(0, round(255*y/96), 0))
x += 20
for y in range(96):
    ssd.line(y, x, y, x+20, ssd.rgb(0, 0, round(255*y/96)))
   
ssd.show()

If you think it needs a little more explanation I can always put it down on a vid real quick.

P.S. I'll give a little insight to what these files do when we are up and running.

Chris Savage

#13
Quote from: Jeff_T on Apr 17, 2024, 02:40 PMI think the folder for keeping a record is an awesome idea, there are many resources and references on the internet but it is invaluable to have written notes of your own success and failures to look back on.

On my NAS drive is a mapped drive (S:) which is for Savage (Savage///Circuits). There is a directory called, "Projects" and within that directory are each project, sorted by date. Within each project directory are common folders, such as, "Images", "PCB Files", "Schematics" and "Source Code". Not every project uses the same directories, but you get the idea.

Source code is stored by version. On microcontrollers I use a major and minor version to track previous versions, such as V1.2. I save all the old versions.

On PC code, I use major, minor and build, such as V1.2.12. My parts cabinets are sorted quite orderly with labels containing the name of the part, form factor and number of pins.

Just as I posted my Office Evolution, I plan on posting some photos of my storage system for parts, though I am going to be moving soon, so things will change just a bit, eventually.

You cannot view this attachment.

        I'm only responsible for what I say, not what you understand.

Jeff_T

The 'S' drive makes sense, I am modifying files and saving them with a prefix of 'sc' to distinguish them from the original. Most of it right now is just small test files so I'm not attaching version numbers.

So your moving to Tennessee? that would be cool. Nashville is booming its one of the fastest growing regions in the country.  ;D