Friday, April 16, 2021

Raspberry Pi Pico/CircuitPython + ILI9341 SPI Display with Touch

In this exercise, Raspberry Pi Pico is flashed with Adafruit CircuitPython 6.2.0. Display on ILI9341 SPI Display with Touch using adafruit_ili9341 library. For the touch detection, xpt2046.py of rdagger/micropython-ili9341 was modified to work for CircuitPython. It's CircuitPython version of my former exercise RPi Pico/MicroPython + ILI9341 SPI Display with Touch.

The display used in this exercise is a 2.4-inch 65K color using ili9341 driver with touch, 2.4inch_SPI_Module_ILI9341_SKU:MSP2402.

Connection:

Connection:
ILI9341 TFT SPI	    RPi Pico
------------------------------
VCC		    3V3
GND		    GND
CS		    GP13
RESET		    GP14
DC		    GP15
SDI(MOSI)	    GP7
SCK		    GP6
LED		    3V3
SDO(MISO)
T- T_CLK	    GP10
O  T_CS		    GP12
U  T_DIN	    GP11
C  T_DO		    GP8
H- T_IRQ

Install display library for ili9341:

Visit https://circuitpython.org/libraries, download the appropriate bundle for your version of CircuitPython. Unzip the file, and copy adafruit_ili9341.mpy and  adafruit_display_text directory to /lib directory of your Raspberry Pi Pico CIRCUITPY driver.

Touch driver for xpt2046:

(If you do not use touch part, you can skip this part.)

xpt2046.py of rdagger/micropython-ili9341 is a xpt2046 driver for MicroPython. I modify to make it work for CircuitPython. The main modification are removing interrupt and re-config pin for CircuitPython using digitalio.

Save it to Raspberry Pi Pico CIRCUITPY driver, name cpy_xpt2046.py.

"""
XPT2046 Touch module for CircuitPython
modified from xpt2046.py of rdagger/micropython-ili9341
https://github.com/rdagger/micropython-ili9341/blob/master/xpt2046.py

remove interrupt and re-config pin for CircuitPython
"""
from time import sleep
import digitalio

class Touch(object):
    """Serial interface for XPT2046 Touch Screen Controller."""

    # Command constants from ILI9341 datasheet
    GET_X = const(0b11010000)  # X position
    GET_Y = const(0b10010000)  # Y position
    GET_Z1 = const(0b10110000)  # Z1 position
    GET_Z2 = const(0b11000000)  # Z2 position
    GET_TEMP0 = const(0b10000000)  # Temperature 0
    GET_TEMP1 = const(0b11110000)  # Temperature 1
    GET_BATTERY = const(0b10100000)  # Battery monitor
    GET_AUX = const(0b11100000)  # Auxiliary input to ADC
    
    """ remove support of interrupt
    def __init__(self, spi, cs, int_pin=None, int_handler=None,
                 width=240, height=320,
                 x_min=100, x_max=1962, y_min=100, y_max=1900):
    """
    def __init__(self, spi, cs, width=240, height=320,
                 x_min=100, x_max=1962, y_min=100, y_max=1900):
        """Initialize touch screen controller.

        Args:
            spi (Class Spi):  SPI interface for OLED
            cs (Class Pin):  Chip select pin
            int_pin (Class Pin):  Touch controller interrupt pin
            int_handler (function): Handler for screen interrupt
            width (int): Width of LCD screen
            height (int): Height of LCD screen
            x_min (int): Minimum x coordinate
            x_max (int): Maximum x coordinate
            y_min (int): Minimum Y coordinate
            y_max (int): Maximum Y coordinate
        """
        self.spi = spi
        self.cs = cs
        #self.cs.init(self.cs.OUT, value=1)
        self.cs_io = digitalio.DigitalInOut(cs)
        self.cs_io.direction = digitalio.Direction.OUTPUT
        self.cs_io.value=1
        
        self.rx_buf = bytearray(3)  # Receive buffer
        self.tx_buf = bytearray(3)  # Transmit buffer
        self.width = width
        self.height = height
        # Set calibration
        self.x_min = x_min
        self.x_max = x_max
        self.y_min = y_min
        self.y_max = y_max
        self.x_multiplier = width / (x_max - x_min)
        self.x_add = x_min * -self.x_multiplier
        self.y_multiplier = height / (y_max - y_min)
        self.y_add = y_min * -self.y_multiplier

        """ ignore int_pin
        if int_pin is not None:
            self.int_pin = int_pin
            self.int_pin.init(int_pin.IN)
            
            self.int_handler = int_handler
            self.int_locked = False
            int_pin.irq(trigger=int_pin.IRQ_FALLING | int_pin.IRQ_RISING,
                        handler=self.int_press)
        """
        
    def get_touch(self):
        """Take multiple samples to get accurate touch reading."""
        timeout = 2  # set timeout to 2 seconds
        confidence = 5
        buff = [[0, 0] for x in range(confidence)]
        buf_length = confidence  # Require a confidence of 5 good samples
        buffptr = 0  # Track current buffer position
        nsamples = 0  # Count samples
        while timeout > 0:
            if nsamples == buf_length:
                meanx = sum([c[0] for c in buff]) // buf_length
                meany = sum([c[1] for c in buff]) // buf_length
                dev = sum([(c[0] - meanx)**2 +
                          (c[1] - meany)**2 for c in buff]) / buf_length
                if dev <= 50:  # Deviation should be under margin of 50
                    return self.normalize(meanx, meany)
            # get a new value
            sample = self.raw_touch()  # get a touch
            if sample is None:
                nsamples = 0    # Invalidate buff
            else:
                buff[buffptr] = sample  # put in buff
                buffptr = (buffptr + 1) % buf_length  # Incr, until rollover
                nsamples = min(nsamples + 1, buf_length)  # Incr. until max

            sleep(.05)
            timeout -= .05
        return None

    """
    def int_press(self, pin):
        
        if not pin.value() and not self.int_locked:
            self.int_locked = True  # Lock Interrupt
            buff = self.raw_touch()

            if buff is not None:
                x, y = self.normalize(*buff)
                self.int_handler(x, y)
            sleep(.1)  # Debounce falling edge
        elif pin.value() and self.int_locked:
            sleep(.1)  # Debounce rising edge
            self.int_locked = False  # Unlock interrupt
    """
    
    def normalize(self, x, y):
        """Normalize mean X,Y values to match LCD screen."""
        x = int(self.x_multiplier * x + self.x_add)
        y = int(self.y_multiplier * y + self.y_add)
        return x, y

    def raw_touch(self):
        """Read raw X,Y touch values.

        Returns:
            tuple(int, int): X, Y
        """
        x = self.send_command(self.GET_X)
        y = self.send_command(self.GET_Y)
        if self.x_min <= x <= self.x_max and self.y_min <= y <= self.y_max:
            return (x, y)
        else:
            return None

    def send_command(self, command):
        """Write command to XT2046 (MicroPython).

        Args:
            command (byte): XT2046 command code.
        Returns:
            int: 12 bit response
        """
        self.tx_buf[0] = command
        
        #self.cs(0)
        self.cs_io.value=0
        
        self.spi.try_lock()
        self.spi.write_readinto(self.tx_buf, self.rx_buf)
        self.spi.unlock()
        
        #self.cs(1)
        self.cs_io.value=1

        return (self.rx_buf[1] << 4) | (self.rx_buf[2] >> 4)

Example code:

cpyPico_spi_ILI9341_20210416.py
from sys import implementation
from os import uname
import board
import time
import displayio
import terminalio
import busio
import adafruit_ili9341
from adafruit_display_text import label

print('=======================')    
print(implementation[0], uname()[3])

displayio.release_displays()

TFT_WIDTH = 320
TFT_HEIGHT = 240

tft_cs = board.GP13
tft_dc = board.GP15
tft_res = board.GP14
spi_mosi = board.GP7
#spi_miso = board.GP4
spi_clk = board.GP6

spi = busio.SPI(spi_clk, MOSI=spi_mosi)

display_bus = displayio.FourWire(
    spi, command=tft_dc, chip_select=tft_cs, reset=tft_res)

display = adafruit_ili9341.ILI9341(display_bus,
                    width=TFT_WIDTH, height=TFT_HEIGHT,
                    rowstart=0, colstart=0)
display.rotation = 0

# Make the display context
splash = displayio.Group(max_size=10)
display.show(splash)

color_bitmap = displayio.Bitmap(display.width, display.height, 1)
color_palette = displayio.Palette(1)
color_palette[0] = 0x00FF00

bg_sprite = displayio.TileGrid(color_bitmap,
                               pixel_shader=color_palette, x=0, y=0)
splash.append(bg_sprite)

# Draw a smaller inner rectangle
inner_bitmap = displayio.Bitmap(display.width-2, display.height-2, 1)
inner_palette = displayio.Palette(1)
inner_palette[0] = 0x0000FF
inner_sprite = displayio.TileGrid(inner_bitmap,
                                  pixel_shader=inner_palette, x=1, y=1)
splash.append(inner_sprite)

# Draw a label
text_group1 = displayio.Group(max_size=10, scale=2, x=20, y=40)
text1 = "RPi Pico"
text_area1 = label.Label(terminalio.FONT, text=text1, color=0xFF0000)
text_group1.append(text_area1)  # Subgroup for text scaling
# Draw a label
text_group2 = displayio.Group(max_size=10, scale=1, x=20, y=60)
text2 = implementation[0] + ' ' + uname()[3]
text_area2 = label.Label(terminalio.FONT, text=text2, color=0xFFFFFF)
text_group2.append(text_area2)  # Subgroup for text scaling

# Draw a label
text_group3 = displayio.Group(max_size=10, scale=2, x=20, y=100)
text3 = adafruit_ili9341.__name__
text_area3 = label.Label(terminalio.FONT, text=text3, color=0xF0F0F0)
text_group3.append(text_area3)  # Subgroup for text scaling
# Draw a label
text_group4 = displayio.Group(max_size=10, scale=2, x=20, y=120)
text4 = adafruit_ili9341.__version__
text_area4 = label.Label(terminalio.FONT, text=text4, color=0xF0F0F0)
text_group4.append(text_area4)  # Subgroup for text scaling

splash.append(text_group1)
splash.append(text_group2)
splash.append(text_group3)
splash.append(text_group4)

rot = 0
print('rot: ', rot, '\t-', display.width," x ", display.height)
time.sleep(3.0)

while True:
    time.sleep(5.0)
    rot = rot + 90
    if (rot>=360):
        rot =0
    display.rotation = rot
    print('rot: ', rot, '\t-', display.width," x ", display.height)

print('- bye -')


cpyPico_spi_ILI9341_bitmap_20210416.py

"""
Example of CircuitPython/Raspberry Pi Pico
to display on 320x240 ili9341 SPI display
"""

import os
import board
import time
import terminalio
import displayio
import busio
from adafruit_display_text import label
import adafruit_ili9341

print("==============================")
print(os.uname())
print("Hello Raspberry Pi Pico/CircuitPython ILI8341 SPI Display")
print(adafruit_ili9341.__name__ + " version: " + adafruit_ili9341.__version__)
print()

# Release any resources currently in use for the displays
displayio.release_displays()

TFT_WIDTH = 320
TFT_HEIGHT = 240

tft_spi_clk = board.GP6
tft_spi_mosi = board.GP7
#tft_spi_miso = board.GP4
tft_cs = board.GP13
tft_dc = board.GP15
tft_res = board.GP14

tft_spi = busio.SPI(tft_spi_clk, MOSI=tft_spi_mosi)

display_bus = displayio.FourWire(
    tft_spi, command=tft_dc, chip_select=tft_cs, reset=tft_res)

display = adafruit_ili9341.ILI9341(display_bus,
                    width=TFT_WIDTH, height=TFT_HEIGHT)
display.rotation = 90
print('rot: ', display.rotation, '\t-', display.width," x ", display.height)

group = displayio.Group(max_size=10)
display.show(group)

bitmap = displayio.Bitmap(display.width, display.height, display.width)

palette = displayio.Palette(display.width)
for p in range(display.width):
    palette[p] = (0x010000*p) + (0x0100*p) + p

for y in range(display.height):
    for x in range(display.width):
        bitmap[x,y] = x
        
tileGrid = displayio.TileGrid(bitmap, pixel_shader=palette, x=0, y=0)
group.append(tileGrid)

time.sleep(3.0)

while True:
    for p in range(display.width):
        palette[p] = p
    time.sleep(3.0)

    for p in range(display.width):
        palette[p] = 0x0100 * p
    time.sleep(3.0)

    for p in range(display.width):
        palette[p] = 0x010000 * p
    time.sleep(3.0)
    
print('-bye -')

cpyPico_spi_ILI9341_touch_20210416.py

"""
Example of CircuitPython/Raspberry Pi Pico
to display on 320x240 ili9341 SPI display
with touch detection
"""

from sys import implementation
from os import uname
import board
import time
import terminalio
import displayio
import busio
from adafruit_display_text import label
import adafruit_ili9341
from cpy_xpt2046 import Touch

print("==============================")
print(implementation[0], uname()[3])
print("Hello Raspberry Pi Pico/CircuitPython ILI8341 SPI Display")
print("with touch")
print(adafruit_ili9341.__name__ + " version: " + adafruit_ili9341.__version__)
print()

# Release any resources currently in use for the displays
displayio.release_displays()

TFT_WIDTH = 320
TFT_HEIGHT = 240

tft_spi_clk = board.GP6
tft_spi_mosi = board.GP7
#tft_spi_miso = board.GP4
tft_cs = board.GP13
tft_dc = board.GP15
tft_res = board.GP14

touch_spi_clk = board.GP10
touch_spi_mosi = board.GP11
touch_spi_miso = board.GP8

touch_cs = board.GP12
#touch_int = board.GP0

touch_x_min = 64
touch_x_max = 1847
touch_y_min = 148
touch_y_max = 2047

touch_spi = busio.SPI(touch_spi_clk, MOSI=touch_spi_mosi, MISO=touch_spi_miso)
touch = Touch(touch_spi, cs=touch_cs,
              x_min=touch_x_min, x_max=touch_x_max,
              y_min=touch_y_min, y_max=touch_y_max)

tft_spi = busio.SPI(tft_spi_clk, MOSI=tft_spi_mosi)


display_bus = displayio.FourWire(
    tft_spi, command=tft_dc, chip_select=tft_cs, reset=tft_res)

display = adafruit_ili9341.ILI9341(display_bus,
                    width=TFT_WIDTH, height=TFT_HEIGHT)
display.rotation = 90
scrWidth = display.width
scrHeight = display.height
print('rot: ', display.rotation, '\t-', scrWidth," x ", scrHeight)


group = displayio.Group(max_size=10)
display.show(group)

bitmap = displayio.Bitmap(display.width, display.height, 5)

BLACK = 0
WHITE = 1
RED   = 2
GREEN = 3
BLUE  = 4
palette = displayio.Palette(5)
palette[0] = 0x000000
palette[1] = 0xFFFFFF
palette[2] = 0xFF0000
palette[3] = 0x00FF00
palette[4] = 0x0000FF

for y in range(display.height):
    for x in range(display.width):
        bitmap[x,y] = BLACK
        
tileGrid = displayio.TileGrid(bitmap, pixel_shader=palette, x=0, y=0)
group.append(tileGrid)

taskInterval_50ms = 0.050
NxTick = time.monotonic() + taskInterval_50ms

EVT_NO = const(0)
EVT_PenDown = const(1)
EVT_PenUp   = const(2)
EVT_PenRept = const(3)
touchEvent  = EVT_NO

touchSt_Idle_0     = const(0)
touchSt_DnDeb_1    = const(1)
touchSt_Touching_2 = const(2)
touchSt_UpDeb_3    = const(3)
touchSt = touchSt_Idle_0

touchDb_NUM = const(3)
touchDb = touchDb_NUM
touching = False
"""
state diagram for touch debounce

    touchStIdle_0       validXY!=None->     touchSt_DnDeb_1
                      <-validXY==None
    ^                                       validXY!=None
    |                                       |
    validXY==None                           V
                                          
    touchSt_UpDeb_3  <- validXY==None       touchSt_Touching_2
                        validXY!=None->
"""

"""
    None: no touch or invalid touch
    normailzedX, normailzedY: valid touch
"""
def validTouch():

    xy = touch.raw_touch()
    
    if xy == None:
        return None
    
    normailzedX, normailzedY = touch.normalize(*xy)
    if (normailzedX < 0 or normailzedX >= scrWidth
            or normailzedY < 0 or normailzedY >= scrHeight):
            return None
        
    return (normailzedX, normailzedY)

def TouchDetTask():
    global touch
    global touching
    global touchSt
    global touchEvent
    global touchedX, touchedY
    global touchDb
    
    validXY = validTouch()

    if touchSt == touchSt_Idle_0:
        if validXY != None:
            touchDb = touchDb_NUM
            touchSt = touchSt_DnDeb_1
    
    elif touchSt == touchSt_DnDeb_1:
        if validXY != None:
            touchDb = touchDb-1
            if touchDb==0:
                touchSt = touchSt_Touching_2
                touchEvent = EVT_PenDown
                touchedX, touchedY = validXY
                touching = True
        else:
            touchSt = touchSt_Idle_0
            
    elif touchSt == touchSt_Touching_2:
        if validXY != None:
            touchedX, touchedY = validXY
            touchEvent = EVT_PenRept
        else:
            touchDb=touchDb_NUM
            touchSt = touchSt_UpDeb_3
            
    elif touchSt == touchSt_UpDeb_3:
        if validXY != None:
            touchSt = touchSt_Touching_2
        else:
            touchDb=touchDb-1
            if touchDb==0:
                touchSt = touchSt_Idle_0
                touchEvent = EVT_PenUp
                touching = False
    
def drawCross(x, y, col):
    if y>=0 and y<scrHeight:
        for i in range(x-5, x+5):
            if i>=0 and i<scrWidth: 
                bitmap[i, y] = col
    
    if x>=0 and y<scrWidth:
        for i in range(y-5, y+5):
            if i>=0 and i<scrHeight:
                bitmap[x, i] = col
    
while True:
    
    curTick = time.monotonic()
    if curTick >= NxTick:
        NxTick = curTick + taskInterval_50ms
        #print(NxTick)
        TouchDetTask()
        
    #handle touch event
    if touchEvent != EVT_NO:
        if touchEvent == EVT_PenDown:
            print('ev PenDown - ', touchedX, " : ", touchedY)

            drawCross(touchedX, touchedY, WHITE)
        if touchEvent == EVT_PenUp:
            print('ev PenUp - ')
            drawCross(touchedX, touchedY, RED)
        if touchEvent == EVT_PenRept:
            if (touchedX>=0 and touchedX<scrWidth
                and touchedY>=0 and touchedY<scrHeight):
                bitmap[touchedX, touchedY] = GREEN
            
        touchEvent = EVT_NO
    
print('-bye -')



Remark about CircuitPython support for interrupt:

According to CircuitPython Frequently Asked Questions, CircuitPython does not currently support interrupts.



No comments: