Official Tutorial From TaoBao Factory : https://github.com/quantrpeter/2.8inch_ESP32-2432S028R
Remark: use offset 0x1000 to write the firmware and have to erase flash first, so:
Download micropython firmware here https://micropython.org/download/ESP32_GENERIC/

1. esptool.py erase_flash
2. esptool.py --baud 460800 write_flash 0x1000 ESP32_GENERIC-20250415-v1.25.0.bin
Download All Files in below links, or by the attachment in this posts
ILI9341 Display Driver
Download ili9341.py from: github.com/rdagger/micropython-ili9341
XPT2046 Touchscreen Driver
Download xpt2046.py from: github.com/rdagger/micropython-ili9341
Font Library
For custom fonts, download xglcd_font.py and font files (e.g., Unispace12x24.c) from: github.com/rdagger/micropython-ili9341
CYD-Specific Library
For simplified control, use the cydr.py library from: github.com/jtobinart/MicroPython_CYD_ESP32-2432S028R
https://github.com/rdagger/micropython-ili9341/blob/master/fonts/Unispace12x24.c
Run it
mpremote cp cydr.py :cydr.py
mpremote cp ili9341.py :ili9341.py
mpremote cp xglcd_font.py :xglcd_font.py
mpremote cp xpt2046.py :xpt2046.py
mpremote mkdir fonts
mpremote cp Unispace12x24.c :fonts/Unispace12x24.c
mpremote cp main.py :main.py
mpremote exec "import main"

Below example only work with official micropython firmware. lv-micropython firmware won't work.
Example 1 : Animation.py
from machine import Pin, SPI
from ili9341 import Display, color565
import time
# Initialize SPI for ILI9341 display
display_spi = SPI(1, baudrate=60000000, sck=Pin(14), mosi=Pin(13))
display = Display(display_spi, dc=Pin(2), cs=Pin(15), rst=Pin(15), width=320, height=240, rotation=90)
# Turn on backlight
backlight = Pin(21, Pin.OUT)
backlight.on()
# Clear display (white background)
display.clear(color565(255, 255, 255))
# Animation parameters
rect_width = 30
rect_height = 30
x = 0
y = 0
dx = 6 # Increased speed
dy = 6 # Increased speed
rect_color = color565(255, 0, 0) # Red rectangle
bg_color = color565(255, 255, 255) # White background
# Animation loop
while True:
# Clear previous rectangle (minimize cleared area)
display.fill_rectangle(x, y, rect_width, rect_height, bg_color)
# Update position
x += dx
y += dy
# Bounce off edges
if x <= 0 or x >= 320 - rect_width:
dx = -dx
if y <= 0 or y >= 240 - rect_height:
dy = -dy
# Draw new rectangle
display.fill_rectangle(x, y, rect_width, rect_height, rect_color)
# Reduced delay for faster animation
time.sleep(0.02) # ~50 FPS
Example 2 : Animation_complex.py
from machine import Pin, SPI
from ili9341 import Display, color565
from xglcd_font import XglcdFont
import time
import math
# Initialize SPI and ILI9341 display
display_spi = SPI(1, baudrate=60000000, sck=Pin(14), mosi=Pin(13))
display = Display(display_spi, dc=Pin(2), cs=Pin(15), rst=Pin(15), width=320, height=240, rotation=90)
# Turn on backlight
backlight = Pin(21, Pin.OUT)
backlight.on()
# Clear display (black background for contrast)
display.clear(color565(0, 0, 0))
# Load font
unispace = XglcdFont('fonts/Unispace12x24.c', 12, 24)
# Shape parameters (2 rectangles)
shapes = [
{'type': 'rect', 'x': 50, 'y': 50, 'w': 25, 'h': 25, 'dx': 5, 'dy': 3}, # Rectangle 1
{'type': 'rect', 'x': 100, 'y': 80, 'w': 20, 'h': 20, 'dx': -4, 'dy': 4} # Rectangle 2
]
# Text parameters
text = "CYD Demo!"
text_x = 0
text_dx = 3
text_color = color565(255, 255, 255) # White text
bg_color = color565(0, 0, 0) # Black background
# Color transition parameters
color_phase = 0
color_cycle_speed = 0.1 # Faster color change
# Animation loop
while True:
# Clear previous text
display.fill_rectangle(text_x, 0, unispace.measure_text(text), 24, bg_color)
# Update text position
text_x += text_dx
if text_x <= 0 or text_x >= 320 - unispace.measure_text(text):
text_dx = -text_dx
# Draw text
display.draw_text(text_x, 0, text, unispace, text_color)
# Update color phase (simplified)
color_phase = (color_phase + color_cycle_speed) % (2 * math.pi)
r = int(127 * (1 + math.sin(color_phase)))
g = int(127 * (1 + math.cos(color_phase)))
b = 128 # Fixed blue for simplicity
shape_color = color565(r, g, b)
# Update and draw shapes
for shape in shapes:
# Clear previous shape
display.fill_rectangle(shape['x'], shape['y'], shape['w'], shape['h'], bg_color)
# Update position
shape['x'] += shape['dx']
shape['y'] += shape['dy']
# Bounce off edges (avoid text area)
if shape['x'] <= 0 or shape['x'] >= 320 - shape['w']:
shape['dx'] = -shape['dx']
if shape['y'] <= 24 or shape['y'] >= 240 - shape['h']:
shape['dy'] = -shape['dy']
# Draw new shape
display.fill_rectangle(shape['x'], shape['y'], shape['w'], shape['h'], shape_color)
# Delay for ~66 FPS
time.sleep(0.015)