Savage///Chats

Software Development => Microcontrollers & Processors => Topic started by: Jeff_T on Apr 11, 2024, 10:24 PM

Title: A little fun with displays and Micropython
Post by: Jeff_T on Apr 11, 2024, 10:24 PM
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
   
   


Title: Re: A little fun with displays and Micropython
Post by: Chris Savage on Apr 12, 2024, 07:22 PM
@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.
Title: Re: A little fun with displays and Micropython
Post by: Jeff_T on Apr 12, 2024, 08:21 PM
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.
Title: Re: A little fun with displays and Micropython
Post by: Chris Savage on Apr 15, 2024, 12:35 PM
Okay, these are the displays I have purchased over the last few years:

ROADOM Raspberry Pi Screen, 10.1'' Touchscreen Monitor (https://a.co/d/7ug0wSM)

ELEGOO UNO R3 2.8 Inches TFT Touch Screen with SD Card Socket (https://a.co/d/6ouNqZM)

UCTRONICS 0.96" OLED Display Module for Arduino, 128x64 Pixel SSD1306 I2C Serial Mini Screen (https://a.co/d/7TqEgHR)

MakerFocus I2C OLED Display Module 0.91 Inch I2C SSD1306 OLED Display Module (https://a.co/d/0n49z8r)

ELECROW 5 Inch Touchscreen for Raspberry Pi Screen 800x480 TFT LCD Screen Monitor (https://a.co/d/5x9qeyZ)

0.95 inch RGB OLED (A) 96×64 SPI Interface Horizontal Pinheader Display Panel Module (https://a.co/d/djlXPKu)

I also have two Waveshare 400x300, 4.2inch E-Ink display modules (https://www.waveshare.com/4.2inch-e-paper-module.htm)
Title: Re: A little fun with displays and Micropython
Post by: Jeff_T on Apr 15, 2024, 03:05 PM
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
Title: Re: A little fun with displays and Micropython
Post by: Jeff_T on Apr 16, 2024, 04:08 PM
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.
Title: Re: A little fun with displays and Micropython
Post by: Chris Savage on Apr 16, 2024, 11:08 PM
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:
XYGStudy_01.jpg

It recommends buying this display WITH an AMD Ryzen 7 CPU and Corsair DDR5 memory:
XYGStudy_02.jpg
Title: Re: A little fun with displays and Micropython
Post by: Chris Savage on Apr 16, 2024, 11:09 PM
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.
Title: Re: A little fun with displays and Micropython
Post by: Jeff_T on Apr 17, 2024, 10:06 AM
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.
Title: Re: A little fun with displays and Micropython
Post by: Chris Savage on Apr 17, 2024, 01:41 PM
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...
Title: Re: A little fun with displays and Micropython
Post by: Jeff_T on Apr 17, 2024, 02:40 PM
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
Title: Re: A little fun with displays and Micropython
Post by: Chris Savage on Apr 17, 2024, 10:26 PM
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.

Waveshare_095_01.jpg

Waveshare_095_02.jpg
Title: Re: A little fun with displays and Micropython
Post by: Jeff_T on Apr 17, 2024, 10:58 PM
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.
Title: Re: A little fun with displays and Micropython
Post by: Chris Savage on Apr 17, 2024, 11:42 PM
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.

NAS-Path_01.jpg
Title: Re: A little fun with displays and Micropython
Post by: Jeff_T on Apr 18, 2024, 09:40 AM
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
Title: Re: A little fun with displays and Micropython
Post by: Chris Savage on Apr 18, 2024, 08:07 PM
Quote from: Jeff_T on Apr 18, 2024, 09:40 AMSo your moving to Tennessee? that would be cool. Nashville is booming its one of the fastest growing regions in the country.  ;D

Tennessee? My sister use to live in Nashville, but she didn't stick around. I'm not sure why. She said the same thing to me before she moved there, convincing me to move there. This is when I was travelling in my RV. But she didn't stay, so I ended up not going there to visit and check stuff out.

I'm in NY. I'm moving 30 minutes from here onto some farm land from my wife's farm. We're subdividing a one acre lot for us to live on. My new office will be two of the three extra bedrooms. She gets the other.

As for the code, as I save the examples you posted, I will save those as V1.0 so that as I make changes, I can track them.
Title: Re: A little fun with displays and Micropython
Post by: Jeff_T on Apr 20, 2024, 11:17 PM
Chris I have a video that gives a demo of converting a bmp to a binary file for use with microcontrollers and tft displays, the demo uses python but the resulting file could be used with any programming language that you wanted.

If you think it's useful I can post the bmp to bin sketch.

At the end of the video the image is displayed on a 1.8 inch display with a ST7735 driver chip, I will modify the code in my original post of this thread to fall in line with this demonstration video.