Unable to get touch working on ESP32 ILI9488

I can’t seem to get the touch screen working on ILI9488. I’m trying to run the tpcal.py but when it loads touch won’t register (the countdown from 5 doesn’t decrease).

It’s also possible the screen is not refreshing. I’ve seen other post that had the same problem and solved it by removing MISO on the display but that doesn’t seem to change anything for me. I’v also tried running the display and touch on different SPIs but it gives me an error (It should work on the same bus). When I touch the display I can see the IRQ pin go to 0V, which means the hardware is working.

I made sure to use the newest version of lv_micropython and update the submodules. Is it possible I’m using the wrong version? Normally I don’t like asking for help but I’v been at it for days and don’t know what else to try. I even bought another ILI9488 but it has the same problem.

The code I’m using is below:

import sys
sys.path.append('')

import espidf as esp 
import lvgl as lv
lv.init()

from ili9XXX import ili9488
disp = ili9488(miso=19, mosi=23, clk=18, cs=4, dc=15, rst=2, spihost=esp.VSPI_HOST, mhz=40, factor=32, hybrid=True, width=320, height=480,
        invert=False, double_buffer=True, half_duplex=True)

HRES =  lv.scr_act().get_disp().driver.hor_res
VRES =  lv.scr_act().get_disp().driver.ver_res

from xpt2046 import xpt2046
touch = xpt2046(cs=21, spihost=esp.VSPI_HOST,cal_x0=0, cal_x1 = HRES, cal_y0=0, cal_y1 = VRES, transpose=True)

class Tpcal_point():
    def __init__(self, x, y, name):
        self.display_coordinates = lv.point_t({'x': x, 'y': y})
        self.touch_coordinate = None
        self.name = name

    def __repr__(self):
        return "%s: (%d, %d)" % (self.name, 
                self.touch_coordinate.x,
                self.touch_coordinate.y)

# Calibration helper class

class Tpcal():
        
    # Create a screen with a button and a label

    CIRCLE_SIZE = const(20)
    CIRCLE_OFFSET = const(20)
    TP_MAX_VALUE = const(10000)

    LV_COORD_MAX = const((1 << (8 * 2 - 1)) - 1000)
    LV_RADIUS_CIRCLE = const(LV_COORD_MAX) # TODO use lv.RADIUS_CIRCLE constant when it's available!

    def __init__(self, points, calibrate, touch_count = 5):

        self.points = points
        self.calibrate = calibrate
        self.touch_count = touch_count

        self.med = [lv.point_t() for i in range(0,self.touch_count)] # Storage point to calculate median

        self.cur_point = 0
        self.cur_touch = 0

        self.scr = lv.obj(None)
        self.scr.set_size(TP_MAX_VALUE, TP_MAX_VALUE)
        lv.scr_load(self.scr)

        # Create a big transparent button screen to receive clicks
        style_transp = lv.style_t()
        style_transp.init()
        style_transp.set_bg_opa(lv.OPA.TRANSP)
        self.big_btn = lv.btn(lv.scr_act())
        self.big_btn.set_size(TP_MAX_VALUE, TP_MAX_VALUE)
        self.big_btn.add_style(style_transp, lv.PART.MAIN)
        self.big_btn.add_style(style_transp, lv.PART.MAIN)
        #self.big_btn.set_layout(lv.LAYOUT.OFF)
        self.big_btn.add_event_cb(lambda event, self=self: self.calibrate_clicked(),lv.EVENT.CLICKED, None) 

        # Create the screen, circle and label

        self.label_main = lv.label(lv.scr_act())

        style_circ = lv.style_t()
        style_circ.init()
        style_circ.set_radius(LV_RADIUS_CIRCLE)
                              
        self.circ_area = lv.obj(lv.scr_act())
        self.circ_area.set_size(CIRCLE_SIZE, CIRCLE_SIZE)
        self.circ_area.add_style(style_circ, lv.STATE.DEFAULT)
        self.circ_area.clear_flag(lv.obj.FLAG.CLICKABLE) # self.circ_area.set_click(False)

        self.show_circle()

    def show_text(self, txt):
        self.label_main.set_text(txt)
        # self.label_main.set_align(lv.label.ALIGN.CENTER)
        self.label_main.set_pos((HRES - self.label_main.get_width() ) // 2,
                           (VRES - self.label_main.get_height()) // 2)
    def show_circle(self):
        point = self.points[self.cur_point]
        self.show_text("Click the circle in\n" + \
              point.name + "\n" + \
              "%d left" % (self.touch_count - self.cur_touch))
        if point.display_coordinates.x < 0: point.display_coordinates.x += HRES
        if point.display_coordinates.y < 0: point.display_coordinates.y += VRES
        # print("Circle coordinates: x: %d, y: %d"%(point.display_coordinates.x - self.CIRCLE_SIZE // 2,
        #                                           point.display_coordinates.y - self.CIRCLE_SIZE // 2))
        self.circ_area.set_pos(point.display_coordinates.x - CIRCLE_SIZE // 2,
                               point.display_coordinates.y - CIRCLE_SIZE // 2)

    def calibrate_clicked(self):
        point = self.points[self.cur_point]
        indev = lv.indev_get_act()
        indev.get_point(self.med[self.cur_touch])
        # print("calibrate_clicked: x: %d, y: %d"%(self.med[self.cur_touch].x,self.med[self.cur_touch].y))        
        self.cur_touch += 1

        if self.cur_touch == self.touch_count:
            med_x = sorted([med.x for med in self.med])
            med_y = sorted([med.y for med in self.med])
            x = med_x[len(med_x) // 2]
            y = med_y[len(med_y) // 2]
            point.touch_coordinate = lv.point_t({
                'x': x,
                'y': y})

            self.cur_point += 1
            self.cur_touch = 0

        if self.cur_point == len(self.points):
            self.calibrate(self.points)
            self.cur_point = 0
            self.show_text("Click/drag on screen\n" + \
                           "to check calibration")
            self.big_btn.set_event_cb(lambda event, self=self: self.check(), lv.EVENT.PRESSING, None) 
        else:
            self.show_circle()

    def check(self):
        point = lv.point_t()
        indev = lv.indev_get_act()
        indev.get_point(point)
        # print("click position: x: %d, y: %d"%(point.x,point.y))
        self.circ_area.set_pos(point.x - CIRCLE_SIZE // 2,
                               point.y - CIRCLE_SIZE // 2)

# Calculate calibration, and calibrate

def calibrate(points):
    visual_width = points[1].display_coordinates.x - points[0].display_coordinates.x
    visual_height = points[1].display_coordinates.y - points[0].display_coordinates.y
    touch_width = points[1].touch_coordinate.x - points[0].touch_coordinate.x
    touch_height = points[1].touch_coordinate.y - points[0].touch_coordinate.y

    pixel_width = touch_width / visual_width
    pixel_height = touch_height / visual_height

    x0 = points[0].touch_coordinate.x - points[0].display_coordinates.x * pixel_width
    y0 = points[0].touch_coordinate.y - points[0].display_coordinates.y * pixel_height

    x1 = points[1].touch_coordinate.x + (HRES - points[1].display_coordinates.x) * pixel_width
    y1 = points[1].touch_coordinate.y + (VRES - points[1].display_coordinates.y) * pixel_height

    print("Calibration result: x0=%d, y0=%d, x1=%d, y1=%d" % (round(x0), round(y0), round(x1), round(y1)))
    touch.calibrate(round(x0), round(y0), round(x1), round(y1))
    
# Run calibration

tpcal = Tpcal([
        Tpcal_point(20,  20, "upper left-hand corner"),
        Tpcal_point(-40, -40, "lower right-hand corner"),
    ], calibrate)

# while True:
#     pass

Using NodeMCU ESP32 (ESP32 wroom 32 dev board)
Commands to make lv_micropython in Ubuntu 20.04.2.0:

git clone https://github.com/lvgl/lv_micropython.git
cd lv_micropython
git submodule update --init --recursive
cd ports/esp32
make submodules
make LV_CFLAGS="-DLV_COLOR_DEPTH=32"
make erase
make deploy

Hi @lpearl !

I’m using latest lv_micropython with ili9341+xpt2046 on the same HSPI host, so this seems to be related to ili9488.
Personally I’m not using ili9488 so it’s harder for me to say what the problem is.

Regarding tpcal… unfortunately, it is one of the last scripts that weren’t yet migrated to LVGL v8.
There is an open issue for it, tagged as “help wanted”. Currently I’m focusing on other things but I might get to it eventually.

tpcal is provided as an example for calibrating the touch screen. But it seems you are still struggling with more basic issues right?
Do you get any reading from xp2046? Do they make sense? Try simply to call get_coords() and see what you get.

Can you confirm the screen stops refreshing? Does it happen after wiring xpt2046? After powering it? Or only after initializing the driver?

Right. In principe it should work on the same bus, or on difference busses. Both modes are supported.

Today the driver is not using the IRQ. Do you get sensible readings from the driver?

Latest lv_micropython is the right version.

To really understand the problem, if you have the time and the patience, you can connect a logic analyzer to the busses and try to understand what happens, compare this to the device specifications etc.

On ili9488 you didn’t set power and backlight. You can set them to -1 if they are not in use.
Also try to lower mhz as it’s out of spec (although usually works on 40mhz)

Also see this issue, you can try setting the drivers to full duplex instead of half duplex which is the default.

@amirgon First of all thanks for your help and all the work you have put into this project!

I decided to first check to see if the display was even refreshing. My Python knowledge is pretty basic and I’m new at micropython so my code might not work in the first place.

Below is the code I used to check screen refresh:

import sys
sys.path.append('')

import espidf as esp 
import lvgl as lv
import time
lv.init()

from ili9XXX import ili9488
disp = ili9488(miso=19, mosi=23, clk=18, cs=4, dc=15, rst=2, spihost=esp.VSPI_HOST, mhz=20,power=-1,backlight=-1, factor=16, hybrid=True, width=320, height=480, invert=False, double_buffer=True, half_duplex=False)


from xpt2046 import xpt2046
touch = xpt2046(cs=21, spihost=esp.VSPI_HOST, transpose=True)

scr = lv.obj()
btn = lv.btn(scr)
btn.align_to(lv.scr_act(), lv.ALIGN.CENTER, 0, 0)
label = lv.label(btn)
while True:
	label.set_text("Hello World!")
	lv.scr_load(scr)
	time.sleep(1)
	label.set_text("TEST")
	lv.scr_load(scr)

With this code, it gets stuck on Hello World! and never changes to TEST. Again it could be a problem with my code. I tried removing the xpt2046 import and initialization line, but still no luck. I also tried your suggestions in the code of lowering the mhz and changing half-duplex to false (I assume that makes it run in full-duplex).

GOOD NEWS! The touch screen does seam to be working, I tested get_cords using code below and I get an x and y output when I touch the screen:

import sys
sys.path.append('')

import espidf as esp 
import lvgl as lv
import time
lv.init()

from ili9XXX import ili9488
disp = ili9488(miso=19, mosi=23, clk=18, cs=4, dc=15, rst=2, spihost=esp.VSPI_HOST, mhz=20,power=-1,backlight=-1, factor=16, hybrid=True, width=320, height=480, invert=False, double_buffer=True, half_duplex=False)


from xpt2046 import xpt2046
touch = xpt2046(cs=21, spihost=esp.VSPI_HOST, transpose=True)

scr = lv.obj()
btn = lv.btn(scr)
btn.align_to(lv.scr_act(), lv.ALIGN.CENTER, 0, 0)
label = lv.label(btn)
while True:
	print(touch.get_coords())

Are the only pins that need to be connected to specific pins are miso (technically not connected in my case), mosi and clk (SPI pins of esp32)? The other pins can be connected to any gpio, right?

I also ordered a ILI9341 which through the magic of amazon should arrive tomorrow, and i can do some testing with it.

The easiest way to see if screen is working is to display some animation.
Here is, for example an animated spinner.

In your code you shouldn’t to call sc_load every time, only once.
You usually create all the objects and load the screen once.
It’s ok to change the objects on the screen after it was loaded.

In your code you need to add a sleep after setting text to “TEST” otherwise it would change back to “Hello World” before the screen is refreshed.

In general, it’s less recommended to use “sleep” since it blocks all execution. (won’t work in the online simulator, for example).

A better approach is to use a timer.
Here is an example of changing a label with a timer, on the online simulator.

Unused pins of a device should usually be pulled up or down, according to the device’s specs.

Sorry about that, and thanks for the advice. It seems both the screen and the touch work fine. But I’m still unable to get the tpcal.py working, you said you never updated to LVGL 8, should I downgrade to v7? Is there some way to get the code working, am I doing something wrong?

That is great!

I don’t recommend that. v7 is old now, missing many features and not supported any more.
v8 is mature enough and stable, and is the recommended version for LVGL and lv_micropython.

tpcal is a demo, or an example, to show how to calibrate the touch screen.
It was not meant to be a fully functional utility.
It only displays some values you can write down and use on your code with the xpt2046.

You can also calibrate the screen manually without it - just measure the values on different points of the screen to determine the cal_x0/y0/x1/y1 values to provide to xpt2046 in the constructor or the calibrate function.

The driver uses the calibrated values to determine the final coordinates like this:

So here are some of your options:

  • Try migrating tpcal to v8
  • Determine the cal_x0/y0/x1/y1 values manually
  • Try the existing calibration values, or try to play with them.
  • Calibrate with some existing C code (like this for example), determine the values and use them on Micropython.
  • Write your own code to calibrate the screen

I used the touch calibration TFT_eSPI library and it seems to work. Just trying to figure out what the values represent. I’m sure I will figure it out.

// Use this calibration code in setup():
  uint16_t calData[5] = { 293, 3534, 333, 3287, 7 };
  tft.setTouch(calData);

Thanks for all your help!