In this guide, you’ll learn how to get started with the ESP32 Cheap Yellow Display (and other compatible displays) using MicroPython. We’ll quickly introduce the board, and cover how to upload the right libraries, how to display static text and images, and control the on-board RGB LED and LDR (light-dependent resistor).
If you have a standalone TFT Touchscreen Display 2.8 inch with ILI9341 driver, you can read our LVGL guide for the ESP32.
Table of Contents:
In this guide, we’ll cover the following topics:
- Introducing the ESP32 Cheap Yellow Display
- MicroPython ILI9341 Library
- Draw Static Text
- Load Custom Font and Display Text
- Testing the Touchscreen
- Loading Image on the Display
- Draw Shapes on the Display
- Control the On-board RGB LED
- Read the On-board LDR
Prerequisites
To follow this tutorial you need MicroPython firmware installed in your ESP32 CYD board. You also need an IDE to write and upload the code to your board. We suggest using Thonny IDE:
Learn more about MicroPython: MicroPython Programming with ESP32 and ESP8266
Introducing the ESP32 Cheap Yellow Display – CYD (ESP32-2432S028R)
The ESP32-2432S028R development board has become known in the maker community as the “Cheap Yellow Display” or CYD for short. This development board, whose main chip is an ESP32-WROOM-32 module, comes with a 2.8-inch TFT touchscreen LCD, a microSD card interface, an RGB LED, and all the required circuitry to program and apply power to the board.
This is a very versatile board to build GUIs for your IoT projects and is much more convenient and practical than using a separate ESP32 board with a TFT screen.
Related content: Getting Started with ESP32 Cheap Yellow Display Board – CYD (ESP32-2432S028R) using Arduino IDE
Here’s a list of more detailed specifications of this development board:
- Dual-core MCU, integrated WI-FI and Bluetooth functions
- Frequency can reach 240MHz
- 520KB SRAM, 448KB ROM, Flash size is 4MB
- Module size 50.0×86.0mm
- Operating Voltage: 5V
- Power consumption: approximately 115mA
- Product weight: approximately 50g
- The module includes:
- 2.8-inch color TFT display screen with ILI9341 driver chip
- Display resolution: 240x320px with resistive touchscreen
- Backlight control circuit
- TF card interface for external storage
- Serial interface
- Temperature and humidity sensor interface (DHT11 interface) and reserved IO port interface
- It can be programmed with: Arduino IDE, MicroPython, ESP-IDF
In the Extended GPIO connectors, there are at least 4 GPIOs available: GPIO 35, GPIO 22, GPIO 21, and GPIO 27. It also has the TX/RX pins available (see previous image).
More information about the CYD board GPIOs: ESP32 Cheap Yellow Display (CYD) Pinout (ESP32-2432S028R).
Where to buy?
You can click the link below to check where to buy the ESP32 Cheap Yellow display and its price in different stores.
MicroPython ILI9341 Library
There are different libraries that make it easy to communicate with the CYD board. For MicroPython firmware, we’ll use the MicroPython ILI9341 Display and XPT2046 Touch Screen Drivers developed by rdagger GitHub user. You must follow the next steps to install the three required modules.
Download and Upload the ili9341.py
- Click here to download the ili9341.py code;
- Copy the code to a file on Thonny IDE;
- Go to File > Save as… and select MicroPython Device;
- Save the file with the name ili9341.py (don’t change the name).
"""ILI9341 LCD/Touch module."""
from time import sleep
from math import cos, sin, pi, radians
from sys import implementation
from framebuf import FrameBuffer, RGB565 # type: ignore
from micropython import const # type: ignore
def color565(r, g, b):
"""Return RGB565 color value.
Args:
r (int): Red value.
g (int): Green value.
b (int): Blue value.
"""
return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3
class Display(object):
"""Serial interface for 16-bit color (5-6-5 RGB) IL9341 display.
Note: All coordinates are zero based.
"""
# Command constants from ILI9341 datasheet
NOP = const(0x00) # No-op
SWRESET = const(0x01) # Software reset
RDDID = const(0x04) # Read display ID info
RDDST = const(0x09) # Read display status
SLPIN = const(0x10) # Enter sleep mode
SLPOUT = const(0x11) # Exit sleep mode
PTLON = const(0x12) # Partial mode on
NORON = const(0x13) # Normal display mode on
RDMODE = const(0x0A) # Read display power mode
RDMADCTL = const(0x0B) # Read display MADCTL
RDPIXFMT = const(0x0C) # Read display pixel format
RDIMGFMT = const(0x0D) # Read display image format
RDSELFDIAG = const(0x0F) # Read display self-diagnostic
INVOFF = const(0x20) # Display inversion off
INVON = const(0x21) # Display inversion on
GAMMASET = const(0x26) # Gamma set
DISPLAY_OFF = const(0x28) # Display off
DISPLAY_ON = const(0x29) # Display on
SET_COLUMN = const(0x2A) # Column address set
SET_PAGE = const(0x2B) # Page address set
WRITE_RAM = const(0x2C) # Memory write
READ_RAM = const(0x2E) # Memory read
PTLAR = const(0x30) # Partial area
VSCRDEF = const(0x33) # Vertical scrolling definition
MADCTL = const(0x36) # Memory access control
VSCRSADD = const(0x37) # Vertical scrolling start address
PIXFMT = const(0x3A) # COLMOD: Pixel format set
WRITE_DISPLAY_BRIGHTNESS = const(0x51) # Brightness hardware dependent!
READ_DISPLAY_BRIGHTNESS = const(0x52)
WRITE_CTRL_DISPLAY = const(0x53)
READ_CTRL_DISPLAY = const(0x54)
WRITE_CABC = const(0x55) # Write Content Adaptive Brightness Control
READ_CABC = const(0x56) # Read Content Adaptive Brightness Control
WRITE_CABC_MINIMUM = const(0x5E) # Write CABC Minimum Brightness
READ_CABC_MINIMUM = const(0x5F) # Read CABC Minimum Brightness
FRMCTR1 = const(0xB1) # Frame rate control (In normal mode/full colors)
FRMCTR2 = const(0xB2) # Frame rate control (In idle mode/8 colors)
FRMCTR3 = const(0xB3) # Frame rate control (In partial mode/full colors)
INVCTR = const(0xB4) # Display inversion control
DFUNCTR = const(0xB6) # Display function control
PWCTR1 = const(0xC0) # Power control 1
PWCTR2 = const(0xC1) # Power control 2
PWCTRA = const(0xCB) # Power control A
PWCTRB = const(0xCF) # Power control B
VMCTR1 = const(0xC5) # VCOM control 1
VMCTR2 = const(0xC7) # VCOM control 2
RDID1 = const(0xDA) # Read ID 1
RDID2 = const(0xDB) # Read ID 2
RDID3 = const(0xDC) # Read ID 3
RDID4 = const(0xDD) # Read ID 4
GMCTRP1 = const(0xE0) # Positive gamma correction
GMCTRN1 = const(0xE1) # Negative gamma correction
DTCA = const(0xE8) # Driver timing control A
DTCB = const(0xEA) # Driver timing control B
POSC = const(0xED) # Power on sequence control
ENABLE3G = const(0xF2) # Enable 3 gamma control
PUMPRC = const(0xF7) # Pump ratio control
MIRROR_ROTATE = { # MADCTL configurations for rotation and mirroring
(False, 0): 0x80, # 1000 0000
(False, 90): 0xE0, # 1110 0000
(False, 180): 0x40, # 0100 0000
(False, 270): 0x20, # 0010 0000
(True, 0): 0xC0, # 1100 0000
(True, 90): 0x60, # 0110 0000
(True, 180): 0x00, # 0000 0000
(True, 270): 0xA0 # 1010 0000
}
def __init__(self, spi, cs, dc, rst, width=240, height=320, rotation=0,
mirror=False, bgr=True, gamma=True):
"""Initialize OLED.
Args:
spi (Class Spi): SPI interface for OLED
cs (Class Pin): Chip select pin
dc (Class Pin): Data/Command pin
rst (Class Pin): Reset pin
width (Optional int): Screen width (default 240)
height (Optional int): Screen height (default 320)
rotation (Optional int): Rotation must be 0 default, 90. 180 or 270
mirror (Optional bool): Mirror display (default False)
bgr (Optional bool): Swaps red and blue colors (default True)
gamma (Optional bool): Custom gamma correction (default True)
"""
self.spi = spi
self.cs = cs
self.dc = dc
self.rst = rst
self.width = width
self.height = height
if (mirror, rotation) not in self.MIRROR_ROTATE:
raise ValueError('Rotation must be 0, 90, 180 or 270.')
else:
self.rotation = self.MIRROR_ROTATE[mirror, rotation]
if bgr: # Set BGR bit
self.rotation |= 0b00001000
# Initialize GPIO pins and set implementation specific methods
if implementation.name == 'circuitpython':
self.cs.switch_to_output(value=True)
self.dc.switch_to_output(value=False)
self.rst.switch_to_output(value=True)
self.reset = self.reset_cpy
self.write_cmd = self.write_cmd_cpy
self.write_data = self.write_data_cpy
else:
self.cs.init(self.cs.OUT, value=1)
self.dc.init(self.dc.OUT, value=0)
self.rst.init(self.rst.OUT, value=1)
self.reset = self.reset_mpy
self.write_cmd = self.write_cmd_mpy
self.write_data = self.write_data_mpy
self.reset()
# Send initialization commands
self.write_cmd(self.SWRESET) # Software reset
sleep(.1)
self.write_cmd(self.PWCTRB, 0x00, 0xC1, 0x30) # Pwr ctrl B
self.write_cmd(self.POSC, 0x64, 0x03, 0x12, 0x81) # Pwr on seq. ctrl
self.write_cmd(self.DTCA, 0x85, 0x00, 0x78) # Driver timing ctrl A
self.write_cmd(self.PWCTRA, 0x39, 0x2C, 0x00, 0x34, 0x02) # Pwr ctrl A
self.write_cmd(self.PUMPRC, 0x20) # Pump ratio control
self.write_cmd(self.DTCB, 0x00, 0x00) # Driver timing ctrl B
self.write_cmd(self.PWCTR1, 0x23) # Pwr ctrl 1
self.write_cmd(self.PWCTR2, 0x10) # Pwr ctrl 2
self.write_cmd(self.VMCTR1, 0x3E, 0x28) # VCOM ctrl 1
self.write_cmd(self.VMCTR2, 0x86) # VCOM ctrl 2
self.write_cmd(self.MADCTL, self.rotation) # Memory access ctrl
self.write_cmd(self.VSCRSADD, 0x00) # Vertical scrolling start address
self.write_cmd(self.PIXFMT, 0x55) # COLMOD: Pixel format
self.write_cmd(self.FRMCTR1, 0x00, 0x18) # Frame rate ctrl
self.write_cmd(self.DFUNCTR, 0x08, 0x82, 0x27)
self.write_cmd(self.ENABLE3G, 0x00) # Enable 3 gamma ctrl
self.write_cmd(self.GAMMASET, 0x01) # Gamma curve selected
if gamma: # Use custom gamma correction values
self.write_cmd(self.GMCTRP1, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08,
0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09,
0x00)
self.write_cmd(self.GMCTRN1, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07,
0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36,
0x0F)
self.write_cmd(self.SLPOUT) # Exit sleep
sleep(.1)
self.write_cmd(self.DISPLAY_ON) # Display on
sleep(.1)
self.clear()
def block(self, x0, y0, x1, y1, data):
"""Write a block of data to display.
Args:
x0 (int): Starting X position.
y0 (int): Starting Y position.
x1 (int): Ending X position.
y1 (int): Ending Y position.
data (bytes): Data buffer to write.
"""
self.write_cmd(self.SET_COLUMN,
x0 >> 8, x0 & 0xff, x1 >> 8, x1 & 0xff)
self.write_cmd(self.SET_PAGE,
y0 >> 8, y0 & 0xff, y1 >> 8, y1 & 0xff)
self.write_cmd(self.WRITE_RAM)
self.write_data(data)
def cleanup(self):
"""Clean up resources."""
self.clear()
self.display_off()
self.spi.deinit()
print('display off')
def clear(self, color=0, hlines=8):
"""Clear display.
Args:
color (Optional int): RGB565 color value (Default: 0 = Black).
hlines (Optional int): # of horizontal lines per chunk (Default: 8)
Note:
hlines was introduced to deal with memory allocation on some
boards. Smaller values allocate less memory but take longer
to execute. hlines must be a factor of the display height.
For example, for a 240 pixel height, valid values for hline
would be 1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 16, 20, 24, 30, 40, etc.
Higher values may result in memory allocation errors.
"""
w = self.width
h = self.height
assert hlines > 0 and h % hlines == 0, (
"hlines must be a non-zero factor of height.")
# Clear display
if color:
line = color.to_bytes(2, 'big') * (w * hlines)
else:
line = bytearray(w * 2 * hlines)
for y in range(0, h, hlines):
self.block(0, y, w - 1, y + hlines - 1, line)
def display_off(self):
"""Turn display off."""
self.write_cmd(self.DISPLAY_OFF)
def display_on(self):
"""Turn display on."""
self.write_cmd(self.DISPLAY_ON)
def draw_circle(self, x0, y0, r, color):
"""Draw a circle.
Args:
x0 (int): X coordinate of center point.
y0 (int): Y coordinate of center point.
r (int): Radius.
color (int): RGB565 color value.
"""
f = 1 - r
dx = 1
dy = -r - r
x = 0
y = r
self.draw_pixel(x0, y0 + r, color)
self.draw_pixel(x0, y0 - r, color)
self.draw_pixel(x0 + r, y0, color)
self.draw_pixel(x0 - r, y0, color)
while x < y:
if f >= 0:
y -= 1
dy += 2
f += dy
x += 1
dx += 2
f += dx
self.draw_pixel(x0 + x, y0 + y, color)
self.draw_pixel(x0 - x, y0 + y, color)
self.draw_pixel(x0 + x, y0 - y, color)
self.draw_pixel(x0 - x, y0 - y, color)
self.draw_pixel(x0 + y, y0 + x, color)
self.draw_pixel(x0 - y, y0 + x, color)
self.draw_pixel(x0 + y, y0 - x, color)
self.draw_pixel(x0 - y, y0 - x, color)
def draw_ellipse(self, x0, y0, a, b, color):
"""Draw an ellipse.
Args:
x0, y0 (int): Coordinates of center point.
a (int): Semi axis horizontal.
b (int): Semi axis vertical.
color (int): RGB565 color value.
Note:
The center point is the center of the x0,y0 pixel.
Since pixels are not divisible, the axes are integer rounded
up to complete on a full pixel. Therefore the major and
minor axes are increased by 1.
"""
a2 = a * a
b2 = b * b
twoa2 = a2 + a2
twob2 = b2 + b2
x = 0
y = b
px = 0
py = twoa2 * y
# Plot initial points
self.draw_pixel(x0 + x, y0 + y, color)
self.draw_pixel(x0 - x, y0 + y, color)
self.draw_pixel(x0 + x, y0 - y, color)
self.draw_pixel(x0 - x, y0 - y, color)
# Region 1
p = round(b2 - (a2 * b) + (0.25 * a2))
while px < py:
x += 1
px += twob2
if p < 0:
p += b2 + px
else:
y -= 1
py -= twoa2
p += b2 + px - py
self.draw_pixel(x0 + x, y0 + y, color)
self.draw_pixel(x0 - x, y0 + y, color)
self.draw_pixel(x0 + x, y0 - y, color)
self.draw_pixel(x0 - x, y0 - y, color)
# Region 2
p = round(b2 * (x + 0.5) * (x + 0.5) +
a2 * (y - 1) * (y - 1) - a2 * b2)
while y > 0:
y -= 1
py -= twoa2
if p > 0:
p += a2 - py
else:
x += 1
px += twob2
p += a2 - py + px
self.draw_pixel(x0 + x, y0 + y, color)
self.draw_pixel(x0 - x, y0 + y, color)
self.draw_pixel(x0 + x, y0 - y, color)
self.draw_pixel(x0 - x, y0 - y, color)
def draw_hline(self, x, y, w, color):
"""Draw a horizontal line.
Args:
x (int): Starting X position.
y (int): Starting Y position.
w (int): Width of line.
color (int): RGB565 color value.
"""
if self.is_off_grid(x, y, x + w - 1, y):
return
line = color.to_bytes(2, 'big') * w
self.block(x, y, x + w - 1, y, line)
def draw_image(self, path, x=0, y=0, w=320, h=240):
"""Draw image from flash.
Args:
path (string): Image file path.
x (int): X coordinate of image left. Default is 0.
y (int): Y coordinate of image top. Default is 0.
w (int): Width of image. Default is 320.
h (int): Height of image. Default is 240.
"""
x2 = x + w - 1
y2 = y + h - 1
if self.is_off_grid(x, y, x2, y2):
return
with open(path, "rb") as f:
chunk_height = 1024 // w
chunk_count, remainder = divmod(h, chunk_height)
chunk_size = chunk_height * w * 2
chunk_y = y
if chunk_count:
for c in range(0, chunk_count):
buf = f.read(chunk_size)
self.block(x, chunk_y,
x2, chunk_y + chunk_height - 1,
buf)
chunk_y += chunk_height
if remainder:
buf = f.read(remainder * w * 2)
self.block(x, chunk_y,
x2, chunk_y + remainder - 1,
buf)
def draw_letter(self, x, y, letter, font, color, background=0,
landscape=False, rotate_180=False):
"""Draw a letter.
Args:
x (int): Starting X position.
y (int): Starting Y position.
letter (string): Letter to draw.
font (XglcdFont object): Font.
color (int): RGB565 color value.
background (int): RGB565 background color (default: black)
landscape (bool): Orientation (default: False = portrait)
rotate_180 (bool): Rotate text by 180 degrees
"""
buf, w, h = font.get_letter(letter, color, background, landscape)
if rotate_180:
# Manually rotate the buffer by 180 degrees
# ensure bytes pairs for each pixel retain color565
new_buf = bytearray(len(buf))
num_pixels = len(buf) // 2
for i in range(num_pixels):
# The index for the new buffer's byte pair
new_idx = (num_pixels - 1 - i) * 2
# The index for the original buffer's byte pair
old_idx = i * 2
# Swap the pixels
new_buf[new_idx], new_buf[new_idx + 1] = buf[old_idx], buf[old_idx + 1]
buf = new_buf
# Check for errors (Font could be missing specified letter)
if w == 0:
return w, h
if landscape:
y -= w
if self.is_off_grid(x, y, x + h - 1, y + w - 1):
return 0, 0
self.block(x, y,
x + h - 1, y + w - 1,
buf)
else:
if self.is_off_grid(x, y, x + w - 1, y + h - 1):
return 0, 0
self.block(x, y,
x + w - 1, y + h - 1,
buf)
return w, h
def draw_line(self, x1, y1, x2, y2, color):
"""Draw a line using Bresenham's algorithm.
Args:
x1, y1 (int): Starting coordinates of the line
x2, y2 (int): Ending coordinates of the line
color (int): RGB565 color value.
"""
# Check for horizontal line
if y1 == y2:
if x1 > x2:
x1, x2 = x2, x1
self.draw_hline(x1, y1, x2 - x1 + 1, color)
return
# Check for vertical line
if x1 == x2:
if y1 > y2:
y1, y2 = y2, y1
self.draw_vline(x1, y1, y2 - y1 + 1, color)
return
# Confirm coordinates in boundary
if self.is_off_grid(min(x1, x2), min(y1, y2),
max(x1, x2), max(y1, y2)):
return
# Changes in x, y
dx = x2 - x1
dy = y2 - y1
# Determine how steep the line is
is_steep = abs(dy) > abs(dx)
# Rotate line
if is_steep:
x1, y1 = y1, x1
x2, y2 = y2, x2
# Swap start and end points if necessary
if x1 > x2:
x1, x2 = x2, x1
y1, y2 = y2, y1
# Recalculate differentials
dx = x2 - x1
dy = y2 - y1
# Calculate error
error = dx >> 1
ystep = 1 if y1 < y2 else -1
y = y1
for x in range(x1, x2 + 1):
# Had to reverse HW ????
if not is_steep:
self.draw_pixel(x, y, color)
else:
self.draw_pixel(y, x, color)
error -= abs(dy)
if error < 0:
y += ystep
error += dx
def draw_lines(self, coords, color):
"""Draw multiple lines.
Args:
coords ([[int, int],...]): Line coordinate X, Y pairs
color (int): RGB565 color value.
"""
# Starting point
x1, y1 = coords[0]
# Iterate through coordinates
for i in range(1, len(coords)):
x2, y2 = coords[i]
self.draw_line(x1, y1, x2, y2, color)
x1, y1 = x2, y2
def draw_pixel(self, x, y, color):
"""Draw a single pixel.
Args:
x (int): X position.
y (int): Y position.
color (int): RGB565 color value.
"""
if self.is_off_grid(x, y, x, y):
return
self.block(x, y, x, y, color.to_bytes(2, 'big'))
def draw_polygon(self, sides, x0, y0, r, color, rotate=0):
"""Draw an n-sided regular polygon.
Args:
sides (int): Number of polygon sides.
x0, y0 (int): Coordinates of center point.
r (int): Radius.
color (int): RGB565 color value.
rotate (Optional float): Rotation in degrees relative to origin.
Note:
The center point is the center of the x0,y0 pixel.
Since pixels are not divisible, the radius is integer rounded
up to complete on a full pixel. Therefore diameter = 2 x r + 1.
"""
coords = []
theta = radians(rotate)
n = sides + 1
for s in range(n):
t = 2.0 * pi * s / sides + theta
coords.append([int(r * cos(t) + x0), int(r * sin(t) + y0)])
# Cast to python float first to fix rounding errors
self.draw_lines(coords, color=color)
def draw_rectangle(self, x, y, w, h, color):
"""Draw a rectangle.
Args:
x (int): Starting X position.
y (int): Starting Y position.
w (int): Width of rectangle.
h (int): Height of rectangle.
color (int): RGB565 color value.
"""
x2 = x + w - 1
y2 = y + h - 1
self.draw_hline(x, y, w, color)
self.draw_hline(x, y2, w, color)
self.draw_vline(x, y, h, color)
self.draw_vline(x2, y, h, color)
def draw_sprite(self, buf, x, y, w, h):
"""Draw a sprite (optimized for horizontal drawing).
Args:
buf (bytearray): Buffer to draw.
x (int): Starting X position.
y (int): Starting Y position.
w (int): Width of drawing.
h (int): Height of drawing.
"""
x2 = x + w - 1
y2 = y + h - 1
if self.is_off_grid(x, y, x2, y2):
return
self.block(x, y, x2, y2, buf)
def draw_text(self, x, y, text, font, color, background=0,
landscape=False, rotate_180=False, spacing=1):
"""Draw text.
Args:
x (int): Starting X position
y (int): Starting Y position
text (string): Text to draw
font (XglcdFont object): Font
color (int): RGB565 color value
background (int): RGB565 background color (default: black)
landscape (bool): Orientation (default: False = portrait)
rotate_180 (bool): Rotate text by 180 degrees
spacing (int): Pixels between letters (default: 1)
"""
iterable_text = reversed(text) if rotate_180 else text
for letter in iterable_text:
# Get letter array and letter dimensions
w, h = self.draw_letter(x, y, letter, font, color, background,
landscape, rotate_180)
# Stop on error
if w == 0 or h == 0:
print('Invalid width {0} or height {1}'.format(w, h))
return
if landscape:
# Fill in spacing
if spacing:
self.fill_hrect(x, y - w - spacing, h, spacing, background)
# Position y for next letter
y -= (w + spacing)
else:
# Fill in spacing
if spacing:
self.fill_hrect(x + w, y, spacing, h, background)
# Position x for next letter
x += (w + spacing)
# # Fill in spacing
# if spacing:
# self.fill_vrect(x + w, y, spacing, h, background)
# # Position x for next letter
# x += w + spacing
def draw_text8x8(self, x, y, text, color, background=0,
rotate=0):
"""Draw text using built-in MicroPython 8x8 bit font.
Args:
x (int): Starting X position.
y (int): Starting Y position.
text (string): Text to draw.
color (int): RGB565 color value.
background (int): RGB565 background color (default: black).
rotate(int): 0, 90, 180, 270
"""
w = len(text) * 8
h = 8
# Confirm coordinates in boundary
if self.is_off_grid(x, y, x + 7, y + 7):
return
buf = bytearray(w * 16)
fbuf = FrameBuffer(buf, w, h, RGB565)
if background != 0:
# Swap background color bytes to correct for framebuf endianness
b_color = ((background & 0xFF) << 8) | ((background & 0xFF00) >> 8)
fbuf.fill(b_color)
# Swap text color bytes to correct for framebuf endianness
t_color = ((color & 0xFF) << 8) | ((color & 0xFF00) >> 8)
fbuf.text(text, 0, 0, t_color)
if rotate == 0:
self.block(x, y, x + w - 1, y + (h - 1), buf)
elif rotate == 90:
buf2 = bytearray(w * 16)
fbuf2 = FrameBuffer(buf2, h, w, RGB565)
for y1 in range(h):
for x1 in range(w):
fbuf2.pixel(y1, x1,
fbuf.pixel(x1, (h - 1) - y1))
self.block(x, y, x + (h - 1), y + w - 1, buf2)
elif rotate == 180:
buf2 = bytearray(w * 16)
fbuf2 = FrameBuffer(buf2, w, h, RGB565)
for y1 in range(h):
for x1 in range(w):
fbuf2.pixel(x1, y1,
fbuf.pixel((w - 1) - x1, (h - 1) - y1))
self.block(x, y, x + w - 1, y + (h - 1), buf2)
elif rotate == 270:
buf2 = bytearray(w * 16)
fbuf2 = FrameBuffer(buf2, h, w, RGB565)
for y1 in range(h):
for x1 in range(w):
fbuf2.pixel(y1, x1,
fbuf.pixel((w - 1) - x1, y1))
self.block(x, y, x + (h - 1), y + w - 1, buf2)
def draw_vline(self, x, y, h, color):
"""Draw a vertical line.
Args:
x (int): Starting X position.
y (int): Starting Y position.
h (int): Height of line.
color (int): RGB565 color value.
"""
# Confirm coordinates in boundary
if self.is_off_grid(x, y, x, y + h - 1):
return
line = color.to_bytes(2, 'big') * h
self.block(x, y, x, y + h - 1, line)
def fill_circle(self, x0, y0, r, color):
"""Draw a filled circle.
Args:
x0 (int): X coordinate of center point.
y0 (int): Y coordinate of center point.
r (int): Radius.
color (int): RGB565 color value.
"""
f = 1 - r
dx = 1
dy = -r - r
x = 0
y = r
self.draw_vline(x0, y0 - r, 2 * r + 1, color)
while x < y:
if f >= 0:
y -= 1
dy += 2
f += dy
x += 1
dx += 2
f += dx
self.draw_vline(x0 + x, y0 - y, 2 * y + 1, color)
self.draw_vline(x0 - x, y0 - y, 2 * y + 1, color)
self.draw_vline(x0 - y, y0 - x, 2 * x + 1, color)
self.draw_vline(x0 + y, y0 - x, 2 * x + 1, color)
def fill_ellipse(self, x0, y0, a, b, color):
"""Draw a filled ellipse.
Args:
x0, y0 (int): Coordinates of center point.
a (int): Semi axis horizontal.
b (int): Semi axis vertical.
color (int): RGB565 color value.
Note:
The center point is the center of the x0,y0 pixel.
Since pixels are not divisible, the axes are integer rounded
up to complete on a full pixel. Therefore the major and
minor axes are increased by 1.
"""
a2 = a * a
b2 = b * b
twoa2 = a2 + a2
twob2 = b2 + b2
x = 0
y = b
px = 0
py = twoa2 * y
# Plot initial points
self.draw_line(x0, y0 - y, x0, y0 + y, color)
# Region 1
p = round(b2 - (a2 * b) + (0.25 * a2))
while px < py:
x += 1
px += twob2
if p < 0:
p += b2 + px
else:
y -= 1
py -= twoa2
p += b2 + px - py
self.draw_line(x0 + x, y0 - y, x0 + x, y0 + y, color)
self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, color)
# Region 2
p = round(b2 * (x + 0.5) * (x + 0.5) +
a2 * (y - 1) * (y - 1) - a2 * b2)
while y > 0:
y -= 1
py -= twoa2
if p > 0:
p += a2 - py
else:
x += 1
px += twob2
p += a2 - py + px
self.draw_line(x0 + x, y0 - y, x0 + x, y0 + y, color)
self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, color)
def fill_hrect(self, x, y, w, h, color):
"""Draw a filled rectangle (optimized for horizontal drawing).
Args:
x (int): Starting X position.
y (int): Starting Y position.
w (int): Width of rectangle.
h (int): Height of rectangle.
color (int): RGB565 color value.
"""
if self.is_off_grid(x, y, x + w - 1, y + h - 1):
return
chunk_height = 1024 // w
chunk_count, remainder = divmod(h, chunk_height)
chunk_size = chunk_height * w
chunk_y = y
if chunk_count:
buf = color.to_bytes(2, 'big') * chunk_size
for c in range(0, chunk_count):
self.block(x, chunk_y,
x + w - 1, chunk_y + chunk_height - 1,
buf)
chunk_y += chunk_height
if remainder:
buf = color.to_bytes(2, 'big') * remainder * w
self.block(x, chunk_y,
x + w - 1, chunk_y + remainder - 1,
buf)
def fill_rectangle(self, x, y, w, h, color):
"""Draw a filled rectangle.
Args:
x (int): Starting X position.
y (int): Starting Y position.
w (int): Width of rectangle.
h (int): Height of rectangle.
color (int): RGB565 color value.
"""
if self.is_off_grid(x, y, x + w - 1, y + h - 1):
return
if w > h:
self.fill_hrect(x, y, w, h, color)
else:
self.fill_vrect(x, y, w, h, color)
def fill_polygon(self, sides, x0, y0, r, color, rotate=0):
"""Draw a filled n-sided regular polygon.
Args:
sides (int): Number of polygon sides.
x0, y0 (int): Coordinates of center point.
r (int): Radius.
color (int): RGB565 color value.
rotate (Optional float): Rotation in degrees relative to origin.
Note:
The center point is the center of the x0,y0 pixel.
Since pixels are not divisible, the radius is integer rounded
up to complete on a full pixel. Therefore diameter = 2 x r + 1.
"""
# Determine side coordinates
coords = []
theta = radians(rotate)
n = sides + 1
for s in range(n):
t = 2.0 * pi * s / sides + theta
coords.append([int(r * cos(t) + x0), int(r * sin(t) + y0)])
# Starting point
x1, y1 = coords[0]
# Minimum Maximum X dict
xdict = {y1: [x1, x1]}
# Iterate through coordinates
for row in coords[1:]:
x2, y2 = row
xprev, yprev = x2, y2
# Calculate perimeter
# Check for horizontal side
if y1 == y2:
if x1 > x2:
x1, x2 = x2, x1
if y1 in xdict:
xdict[y1] = [min(x1, xdict[y1][0]), max(x2, xdict[y1][1])]
else:
xdict[y1] = [x1, x2]
x1, y1 = xprev, yprev
continue
# Non horizontal side
# Changes in x, y
dx = x2 - x1
dy = y2 - y1
# Determine how steep the line is
is_steep = abs(dy) > abs(dx)
# Rotate line
if is_steep:
x1, y1 = y1, x1
x2, y2 = y2, x2
# Swap start and end points if necessary
if x1 > x2:
x1, x2 = x2, x1
y1, y2 = y2, y1
# Recalculate differentials
dx = x2 - x1
dy = y2 - y1
# Calculate error
error = dx >> 1
ystep = 1 if y1 < y2 else -1
y = y1
# Calcualte minimum and maximum x values
for x in range(x1, x2 + 1):
if is_steep:
if x in xdict:
xdict[x] = [min(y, xdict[x][0]), max(y, xdict[x][1])]
else:
xdict[x] = [y, y]
else:
if y in xdict:
xdict[y] = [min(x, xdict[y][0]), max(x, xdict[y][1])]
else:
xdict[y] = [x, x]
error -= abs(dy)
if error < 0:
y += ystep
error += dx
x1, y1 = xprev, yprev
# Fill polygon
for y, x in xdict.items():
self.draw_hline(x[0], y, x[1] - x[0] + 2, color)
def fill_vrect(self, x, y, w, h, color):
"""Draw a filled rectangle (optimized for vertical drawing).
Args:
x (int): Starting X position.
y (int): Starting Y position.
w (int): Width of rectangle.
h (int): Height of rectangle.
color (int): RGB565 color value.
"""
if self.is_off_grid(x, y, x + w - 1, y + h - 1):
return
chunk_width = 1024 // h
chunk_count, remainder = divmod(w, chunk_width)
chunk_size = chunk_width * h
chunk_x = x
if chunk_count:
buf = color.to_bytes(2, 'big') * chunk_size
for c in range(0, chunk_count):
self.block(chunk_x, y,
chunk_x + chunk_width - 1, y + h - 1,
buf)
chunk_x += chunk_width
if remainder:
buf = color.to_bytes(2, 'big') * remainder * h
self.block(chunk_x, y,
chunk_x + remainder - 1, y + h - 1,
buf)
def invert(self, enable=True):
"""Enables or disables inversion of display colors.
Args:
enable (Optional bool): True=enable, False=disable
"""
if enable:
self.write_cmd(self.INVON)
else:
self.write_cmd(self.INVOFF)
def is_off_grid(self, xmin, ymin, xmax, ymax):
"""Check if coordinates extend past display boundaries.
Args:
xmin (int): Minimum horizontal pixel.
ymin (int): Minimum vertical pixel.
xmax (int): Maximum horizontal pixel.
ymax (int): Maximum vertical pixel.
Returns:
boolean: False = Coordinates OK, True = Error.
"""
if xmin < 0:
print('x-coordinate: {0} below minimum of 0.'.format(xmin))
return True
if ymin < 0:
print('y-coordinate: {0} below minimum of 0.'.format(ymin))
return True
if xmax >= self.width:
print('x-coordinate: {0} above maximum of {1}.'.format(
xmax, self.width - 1))
return True
if ymax >= self.height:
print('y-coordinate: {0} above maximum of {1}.'.format(
ymax, self.height - 1))
return True
return False
def load_sprite(self, path, w, h):
"""Load sprite image.
Args:
path (string): Image file path.
w (int): Width of image.
h (int): Height of image.
Notes:
w x h cannot exceed 2048 on boards w/o PSRAM
"""
buf_size = w * h * 2
with open(path, "rb") as f:
return f.read(buf_size)
def reset_cpy(self):
"""Perform reset: Low=initialization, High=normal operation.
Notes: CircuitPython implemntation
"""
self.rst.value = False
sleep(.05)
self.rst.value = True
sleep(.05)
def reset_mpy(self):
"""Perform reset: Low=initialization, High=normal operation.
Notes: MicroPython implemntation
"""
self.rst(0)
sleep(.05)
self.rst(1)
sleep(.05)
def scroll(self, y):
"""Scroll display vertically.
Args:
y (int): Number of pixels to scroll display.
"""
self.write_cmd(self.VSCRSADD, y >> 8, y & 0xFF)
def set_scroll(self, top, bottom):
"""Set the height of the top and bottom scroll margins.
Args:
top (int): Height of top scroll margin
bottom (int): Height of bottom scroll margin
"""
if top + bottom <= self.height:
middle = self.height - (top + bottom)
self.write_cmd(self.VSCRDEF,
top >> 8,
top & 0xFF,
middle >> 8,
middle & 0xFF,
bottom >> 8,
bottom & 0xFF)
def sleep(self, enable=True):
"""Enters or exits sleep mode.
Args:
enable (bool): True (default)=Enter sleep mode, False=Exit sleep
"""
if enable:
self.write_cmd(self.SLPIN)
else:
self.write_cmd(self.SLPOUT)
def write_cmd_mpy(self, command, *args):
"""Write command to OLED (MicroPython).
Args:
command (byte): ILI9341 command code.
*args (optional bytes): Data to transmit.
"""
self.dc(0)
self.cs(0)
self.spi.write(bytearray([command]))
self.cs(1)
# Handle any passed data
if len(args) > 0:
self.write_data(bytearray(args))
def write_cmd_cpy(self, command, *args):
"""Write command to OLED (CircuitPython).
Args:
command (byte): ILI9341 command code.
*args (optional bytes): Data to transmit.
"""
self.dc.value = False
self.cs.value = False
# Confirm SPI locked before writing
while not self.spi.try_lock():
pass
self.spi.write(bytearray([command]))
self.spi.unlock()
self.cs.value = True
# Handle any passed data
if len(args) > 0:
self.write_data(bytearray(args))
def write_data_mpy(self, data):
"""Write data to OLED (MicroPython).
Args:
data (bytes): Data to transmit.
"""
self.dc(1)
self.cs(0)
self.spi.write(data)
self.cs(1)
def write_data_cpy(self, data):
"""Write data to OLED (CircuitPython).
Args:
data (bytes): Data to transmit.
"""
self.dc.value = True
self.cs.value = False
# Confirm SPI locked before writing
while not self.spi.try_lock():
pass
self.spi.write(data)
self.spi.unlock()
self.cs.value = True
Download and Upload the xpt2046.py
- Click here to download the xpt2046.py code;
- Copy the code to a file on Thonny IDE;
- Go to File > Save as… and select MicroPython Device;
- Save the file with the name xpt2046.py (don’t change the name).
"""XPT2046 Touch module."""
from time import sleep
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
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):
"""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.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
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):
"""Send X,Y values to passed interrupt handler."""
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.spi.write_readinto(self.tx_buf, self.rx_buf)
self.cs(1)
return (self.rx_buf[1] << 4) | (self.rx_buf[2] >> 4)
Download and Upload the xglcd_font.py
- Click here to download the xglcd_font.py code;
- Copy the code to a file on Thonny IDE;
- Go to File > Save as… and select MicroPython Device;
- Save the file with the name xglcd_font.py (don’t change the name).
"""XGLCD Font Utility."""
from math import ceil, floor
class XglcdFont(object):
"""Font data in X-GLCD format.
Attributes:
letters: A bytearray of letters (columns consist of bytes)
width: Maximum pixel width of font
height: Pixel height of font
start_letter: ASCII number of first letter
height_bytes: How many bytes comprises letter height
Note:
Font files can be generated with the free version of MikroElektronika
GLCD Font Creator: www.mikroe.com/glcd-font-creator
The font file must be in X-GLCD 'C' format.
To save text files from this font creator program in Win7 or higher
you must use XP compatibility mode or you can just use the clipboard.
"""
# Dict to tranlate bitwise values to byte position
BIT_POS = {1: 0, 2: 2, 4: 4, 8: 6, 16: 8, 32: 10, 64: 12, 128: 14, 256: 16}
def __init__(self, path, width, height, start_letter=32, letter_count=96):
"""Constructor for X-GLCD Font object.
Args:
path (string): Full path of font file
width (int): Maximum width in pixels of each letter
height (int): Height in pixels of each letter
start_letter (int): First ACII letter. Default is 32.
letter_count (int): Total number of letters. Default is 96.
"""
self.width = width
self.height = max(height, 8)
self.start_letter = start_letter
self.letter_count = letter_count
self.bytes_per_letter = (floor(
(self.height - 1) / 8) + 1) * self.width + 1
self.__load_xglcd_font(path)
def __load_xglcd_font(self, path):
"""Load X-GLCD font data from text file.
Args:
path (string): Full path of font file.
"""
bytes_per_letter = self.bytes_per_letter
# Buffer to hold letter byte values
self.letters = bytearray(bytes_per_letter * self.letter_count)
mv = memoryview(self.letters)
offset = 0
with open(path, 'r') as f:
for line in f:
# Skip lines that do not start with hex values
line = line.strip()
if len(line) == 0 or line[0:2] != '0x':
continue
# Remove comments
comment = line.find('//')
if comment != -1:
line = line[0:comment].strip()
# Remove trailing commas
if line.endswith(','):
line = line[0:len(line) - 1]
# Convert hex strings to bytearray and insert in to letters
mv[offset: offset + bytes_per_letter] = bytearray(
int(b, 16) for b in line.split(','))
offset += bytes_per_letter
def lit_bits(self, n):
"""Return positions of 1 bits only."""
while n:
b = n & (~n+1)
yield self.BIT_POS[b]
n ^= b
def get_letter(self, letter, color, background=0, landscape=False):
"""Convert letter byte data to pixels.
Args:
letter (string): Letter to return (must exist within font).
color (int): RGB565 color value.
background (int): RGB565 background color (default: black).
landscape (bool): Orientation (default: False = portrait)
Returns:
(bytearray): Pixel data.
(int, int): Letter width and height.
"""
# Get index of letter
letter_ord = ord(letter) - self.start_letter
# Confirm font contains letter
if letter_ord >= self.letter_count:
print('Font does not contain character: ' + letter)
return b'', 0, 0
bytes_per_letter = self.bytes_per_letter
offset = letter_ord * bytes_per_letter
mv = memoryview(self.letters[offset:offset + bytes_per_letter])
# Get width of letter (specified by first byte)
letter_width = mv[0]
letter_height = self.height
# Get size in bytes of specified letter
letter_size = letter_height * letter_width
# Create buffer (double size to accommodate 16 bit colors)
if background:
buf = bytearray(background.to_bytes(2, 'big') * letter_size)
else:
buf = bytearray(letter_size * 2)
msb, lsb = color.to_bytes(2, 'big')
if landscape:
# Populate buffer in order for landscape
pos = (letter_size * 2) - (letter_height * 2)
lh = letter_height
# Loop through letter byte data and convert to pixel data
for b in mv[1:]:
# Process only colored bits
for bit in self.lit_bits(b):
buf[bit + pos] = msb
buf[bit + pos + 1] = lsb
if lh > 8:
# Increment position by double byte
pos += 16
lh -= 8
else:
# Descrease position to start of previous column
pos -= (letter_height * 4) - (lh * 2)
lh = letter_height
else:
# Populate buffer in order for portrait
col = 0 # Set column to first column
bytes_per_letter = ceil(letter_height / 8)
letter_byte = 0
# Loop through letter byte data and convert to pixel data
for b in mv[1:]:
# Process only colored bits
segment_size = letter_byte * letter_width * 16
for bit in self.lit_bits(b):
pos = (bit * letter_width) + (col * 2) + segment_size
buf[pos] = msb
pos = (bit * letter_width) + (col * 2) + 1 + segment_size
buf[pos] = lsb
letter_byte += 1
if letter_byte + 1 > bytes_per_letter:
col += 1
letter_byte = 0
return buf, letter_width, letter_height
def measure_text(self, text, spacing=1):
"""Measure length of text string in pixels.
Args:
text (string): Text string to measure
spacing (optional int): Pixel spacing between letters. Default: 1.
Returns:
int: length of text
"""
length = 0
for letter in text:
# Get index of letter
letter_ord = ord(letter) - self.start_letter
offset = letter_ord * self.bytes_per_letter
# Add length of letter and spacing
length += self.letters[offset] + spacing
return length
With the modules loaded to the CYD board, now you can use the library functionalities in your code to control the display.
Before proceeding, you must have the following files loaded to your board. You can check that on the left sidebar of Thonny IDE, by going to View > Files.
Draw Static Text – Code
Displaying static text on the LCD is very simple. All you have to do is to load all required modules, initialize the display, select where you want the characters to be displayed on the screen, and the message content that will be displayed.
This example displays the “ESP32 says hello!” message in the (0, 0) coordinates, at the center of the display and text with rotation.
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-cheap-yellow-display-board-cyd-esp32-2432s028r/
from machine import Pin, SPI, ADC, idle
import os
from time import sleep
# Save this file as ili9341.py https://github.com/rdagger/micropython-ili9341/blob/master/ili9341.py
from ili9341 import Display, color565
# Save this file as xglcd_font.py https://github.com/rdagger/micropython-ili9341/blob/master/xglcd_font.py
from xglcd_font import XglcdFont
# Function to set up SPI for TFT display
display_spi = SPI(1, baudrate=60000000, sck=Pin(14), mosi=Pin(13))
# Set up display
display = Display(display_spi, dc=Pin(2), cs=Pin(15), rst=Pin(15),
width=320, height=240, rotation=90)
def draw_text():
# Set colors
white_color = color565(255, 255, 255) # white color
black_color = color565(0, 0, 0) # black color
# Turn on display backlight
backlight = Pin(21, Pin.OUT)
backlight.on()
# Clear display
display.clear(black_color)
# Draw the text on (0, 0) coordinates (x, y, text, font color, font background color, rotation)
display.draw_text8x8(0, 0, 'ESP32 says hello!', white_color, black_color, 0)
# Draw the text on the center of the display
font_size = 8
text_msg = 'Centered text'
x_center = int((display.width-len(text_msg)*font_size)/2)
y_center = int(((display.height)/2)-(font_size/2))
display.draw_text8x8(x_center, y_center, text_msg, white_color, black_color, 0)
# Draw the text on the right with rotation
display.draw_text8x8(display.width-font_size, 0, 'Text with rotation', white_color, black_color, 90)
try:
draw_text()
except Exception as e:
print('Error occured: ', e)
except KeyboardInterrupt:
print('Program Interrupted by the user')
display.cleanup()
How the Code Works
Let’s take a quick look at how the code works to see how to display text on the screen.
Importing libraries
First, you need to import the required libraries
from machine import Pin, SPI, ADC, idle
import os
from time import sleep
# Save this file as ili9341.py https://github.com/rdagger/micropython-ili9341/blob/master/ili9341.py
from ili9341 import Display, color565
# Save this file as xglcd_font.py https://github.com/rdagger/micropython-ili9341/blob/master/xglcd_font.py
from xglcd_font import XglcdFont
Initialize the SPI Bus and the Display
Then, you initialize an SPI bus to communicate with the display—double-check that your display also uses GPIO 14 as SCK and GPIO 13 as MOSI.
# Function to set up SPI for TFT display
display_spi = SPI(1, baudrate=60000000, sck=Pin(14), mosi=Pin(13))
Create a Display object called display and we pass as arguments, the SPI bus we just created, the DC, CS, and RST pins, as well as the display size and rotation. Change the following parameters if your display has a different Pinout or different dimensions.
# Set up display
display = Display(display_spi, dc=Pin(2), cs=Pin(15), rst=Pin(15),
width=320, height=240, rotation=90)
If your board is similar to the one used in this tutorial, you can check the pinout here: ESP32 Cheap Yellow Display (CYD) Pinout (ESP32-2432S028R).
After initializing and setting up the display, we create a function to draw some sample text.
def draw_text():
Creating Colors
We start by creating variables to refer to white and black colors:
white_color = color565(255, 255, 255) # white color
black_color = color565(0, 0, 0) # black color
Turn on the Backlight and Set the Background Color
Then, we initialize the pin that controls the backlight and set it to ON to light up the backlight.
# Turn on display backlight
backlight = Pin(21, Pin.OUT)
backlight.on()
Clear the display by setting the background color to black.
# Clear display
display.clear(black_color)
Displaying Text
Finally, to draw text, you just need to call the draw_8x8() function. This function accepts as arguments: the x and y coordinates, the message you want to display, the font color, the background color, and the rotation.
# Draw the text on (0, 0) coordinates (x, y, text, font color, font background color, rotation)
display.draw_text8x8(0, 0, 'ESP32 says hello!', white_color, black_color, 0)
Display Centered Text
The following lines calculate the center of the display to print centered text.
# Draw the text on the center of the display
font_size = 8
text_msg = 'Centered text'
x_center = int((display.width-len(text_msg)*font_size)/2)
y_center = int(((display.height)/2)-(font_size/2))
display.draw_text8x8(x_center, y_center, text_msg, white_color, black_color, 0)
Display Text with Rotation
This displays text with rotation. The rotation is the last argument of the draw_text8x8() function.
# Draw the text on the right with rotation
display.draw_text8x8(display.width-font_size, 0, 'Text with rotation', white_color, black_color, 90)
Finally, we call the draw_text() function we created previously to display the three text messages in different places on the screen.
try:
draw_text()
Testing the Example
Run the following code on your display. You should get something similar as shown in the following picture.
Note: if you want the code to run automatically when the ESP32 boots (for example, without being connected to your computer), you need to save the file to the board with the name main.py.
When you name a file main.py, the ESP32 will run that file automatically on boot. If you call it a different name, it will still be saved on the board filesystem, but it will not run automatically on boot.
Load Custom Font and Display Text – Code
The library allows you to choose different fonts to display your text (even though the options are limited). You can see all available font options here: Library Fonts Folder.
With a connection opened with your board on Thonny IDE, go to View > Files. A new tab on the left side will open with the files saved on the ESP32. Click on the icon next to the MicroPython device and click New Directory…
The new directory should be called fonts.
As an example, we’ll load the Unispace12x24 font. You can load any available font on the library repository. Follow the next steps:
- Click here to download the Unispace12x24.c code;
- Copy the content to a file on Thonny IDE;
- Go to File > Save as… and select MicroPython Device;
- Save the file with the name Unispace12x24.c (don’t change the name) inside the fonts folder.
Here’s the code that draws text using a custom font. This is in all similar to the previous examples, but uses a custom font.
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-cheap-yellow-display-board-cyd-esp32-2432s028r/
from machine import Pin, SPI, ADC, idle
import os
from time import sleep
# Save this file as ili9341.py https://github.com/rdagger/micropython-ili9341/blob/master/ili9341.py
from ili9341 import Display, color565
# Save this file as xglcd_font.py https://github.com/rdagger/micropython-ili9341/blob/master/xglcd_font.py
from xglcd_font import XglcdFont
# Function to set up SPI for TFT display
display_spi = SPI(1, baudrate=60000000, sck=Pin(14), mosi=Pin(13))
# Set up display
display = Display(display_spi, dc=Pin(2), cs=Pin(15), rst=Pin(15),
width=320, height=240, rotation=90)
def draw_text():
# Set colors
white_color = color565(255, 255, 255) # white color
black_color = color565(0, 0, 0) # black color
# Turn on display backlight
backlight = Pin(21, Pin.OUT)
backlight.on()
# Clear display
display.clear(white_color)
# Loading Unispace font
print('Loading Unispace font...')
unispace_font = XglcdFont('fonts/Unispace12x24.c', 12, 24)
# Draw the text on (0, 0) coordinates (x, y, text, font, font color, font background color,
# landscape=False, rotate_180=False, spacing=1)
display.draw_text(0, 0, 'ESP32 says hello!', unispace_font, black_color, white_color)
# Draw the text on the center of the display
font_size_w = unispace_font.width
font_size_h = unispace_font.height
text_msg = 'Centered text'
x_center = int((display.width-len(text_msg)*font_size_w)/2)
y_center = int(((display.height)/2)-(font_size_h/2))
display.draw_text(x_center, y_center, text_msg, unispace_font, black_color, white_color)
# Draw the text with rotation
display.draw_text(display.width-font_size_h, display.height-font_size_w, 'Text with rotation',
unispace_font, black_color, white_color, landscape=True)
try:
draw_text()
except Exception as e:
print('Error occured: ', e)
except KeyboardInterrupt:
print('Program Interrupted by the user')
display.cleanup()
How Does the Code Work?
First, you need to load the desired font as follows:
# Loading Unispace font
print('Loading Unispace font...')
unispace_font = XglcdFont('fonts/Unispace12x24.c', 12, 24)
The font is saved on the unispace_font variable.
Then, use the draw_text() function that accepts the font as the fourth argument.
# Draw the text on (0, 0) coordinates (x, y, text, font, font color, font background color,
# landscape=False, rotate_180=False, spacing=1)
display.draw_text(0, 0, 'ESP32 says hello!', unispace_font, black_color, white_color)
Testing the Example
Run the code on your board, or upload it as main.py. Your display will look like the following picture.
Testing the Touchscreen – Code
The following code tests the touchscreen. It detects touch and prints the coordinates where touch was detected.
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-cheap-yellow-display-board-cyd-esp32-2432s028r/
from machine import Pin, SPI, ADC, idle
import os
from time import sleep
# Save this file as ili9341.py https://github.com/rdagger/micropython-ili9341/blob/master/ili9341.py
from ili9341 import Display, color565
# Save this file as xpt2046.py https://github.com/rdagger/micropython-ili9341/blob/master/xpt2046.py
from xpt2046 import Touch
# Save this file as xglcd_font.py https://github.com/rdagger/micropython-ili9341/blob/master/xglcd_font.py
from xglcd_font import XglcdFont
# Function to set up SPI for TFT display
display_spi = SPI(1, baudrate=60000000, sck=Pin(14), mosi=Pin(13))
# Set up display
display = Display(display_spi, dc=Pin(2), cs=Pin(15),
rst=Pin(15), width=320, height=240, rotation=90)
print('Display height: ' + str(display.height))
print('Display width: ' + str(display.width))
# Set colors (foreground) and background color
white_color = color565(255, 255, 255) # white
black_color = color565(0, 0, 0) # Black
# Turn on display backlight
backlight = Pin(21, Pin.OUT)
backlight.on()
# Clear display
display.clear(black_color)
# Initial message
# Draw the text on the center of the display
font_size = 8
text_msg = 'Touch screen to test'
x_center = int((display.width-len(text_msg)*font_size)/2)
y_center = int(((display.height)/2)-(font_size/2))
display.draw_text8x8(x_center, y_center,text_msg, white_color, black_color, 0)
# SPI for touchscreen
touchscreen_spi = SPI(2, baudrate=1000000, sck=Pin(25), mosi=Pin(32), miso=Pin(39))
def touchscreen_press(x, y):
display.clear(black_color)
text_touch_coordinates = "Touch: X = " + str(x) + " | Y = " + str(y)
x_center = int((display.width-len(text_touch_coordinates)*font_size)/2)
display.draw_text8x8(x_center, y_center, text_touch_coordinates, white_color, black_color, 0)
print("Touch: X = " + str(x) + " | Y = " + str(y))
touchscreen = Touch(touchscreen_spi, cs=Pin(33), int_pin=Pin(36), int_handler=touchscreen_press)
try:
# Run the event loop indefinitely
while True:
# Loop to wait for touchscreen press
touchscreen.get_touch()
except Exception as e:
print('Error occured: ', e)
except KeyboardInterrupt:
print('Program Interrupted by the user')
finally:
display.cleanup()
How Does the Code Work?
First, you need to include the required libraries, including the xpt2046 from the Touch module.
from machine import Pin, SPI, ADC, idle
import os
from time import sleep
# Save this file as ili9341.py https://github.com/rdagger/micropython-ili9341/blob/master/ili9341.py
from ili9341 import Display, color565
# Save this file as xpt2046.py https://github.com/rdagger/micropython-ili9341/blob/master/xpt2046.py
from xpt2046 import Touch
# Save this file as xglcd_font.py https://github.com/rdagger/micropython-ili9341/blob/master/xglcd_font.py
from xglcd_font import XglcdFont
SPI Bus for the Touchscreen
We create an SPI bus for the touchscreen. Pass as arguments the GPIOs used by the touchscreen as shown below (SCK, MOSI, and MISO).
touchscreen_spi = SPI(2, baudrate=1000000, sck=Pin(25), mosi=Pin(32), miso=Pin(39))
Detecting Touch
To detect touch on the touschscreen, you need to create a Touch object and pass as arguments the touchscreen SPI bus, the CS and INT (interrupt pin) and the touchscreen handler function. This handler function will run when touch is detected. The library will automatically pass the x and y coordinates to that function. In our case, it will call the touchscreen_press function that must be defined earlier.
touchscreen = Touch(touchscreen_spi, cs=Pin(33), int_pin=Pin(36), int_handler=touchscreen_press)
touchscreen_press()
This function will run when touch is detected on the screen. In this case, we print the touch coordinates on the screen. You can do any other task.
def touchscreen_press(x, y):
display.clear(black_color)
text_touch_coordinates = "Touch: X = " + str(x) + " | Y = " + str(y)
x_center = int((display.width-len(text_touch_coordinates)*font_size)/2)
display.draw_text8x8(x_center, y_center, text_touch_coordinates, white_color, black_color, 0)
print("Touch: X = " + str(x) + " | Y = " + str(y))
Keep Detecting Touch
To keep detecting touch, you need to create a while loop that includes the touchscreen.get_touch() instruction as follows.
try:
# Run the event loop indefinitely
while True:
# Loop to wait for touchscreen press
touchscreen.get_touch()
Testing the Example
Run the code on your board, or upload it as main.py. Your display will look like the following picture.
It will display a message at first.
Then, touch on the screen. It will display the touched coordinates.
If you have the board connected to your computer, it will also display the coordinates on the shell.
Loading Image on the Display – Code
In this section, we’ll show you how to display an image on the screen. The images or icons to be displayed on the screen must be converted to .raw type. We already have a .raw file prepared to test the example.
However, if you want to display a custom image, you need to use the img2rgb565.py tool located in the Library utils folder to change image files like JPEG and PNG into the required raw RGB565 format.
- Click here to download the MicroPython128x128.raw file. Thonny IDE can’t open this kind of files. To upload it to your board, you need to follow the next instructions.
- In Thonny IDE, establish a connection with your board.
- Go to View > Files. A left sidebar will show up with the files on your computer and the files on the MicroPython device (ESP32).
- Browse your computer until your find the MicroPython128x128.raw file (don’t change the name);
- Right-click on the file and select the “Upload to /” option and upload it to your board.
After a few seconds, the raw image file will show up on the list of files of your Micropython device.
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-cheap-yellow-display-board-cyd-esp32-2432s028r/
from machine import Pin, SPI, ADC, idle
import os
from time import sleep
# Save this file as ili9341.py https://github.com/rdagger/micropython-ili9341/blob/master/ili9341.py
from ili9341 import Display, color565
# Save this file as xglcd_font.py https://github.com/rdagger/micropython-ili9341/blob/master/xglcd_font.py
from xglcd_font import XglcdFont
# Function to set up SPI for TFT display
display_spi = SPI(1, baudrate=60000000, sck=Pin(14), mosi=Pin(13))
# Set up display
display = Display(display_spi, dc=Pin(2), cs=Pin(15),
rst=Pin(15), width=320, height=240, rotation=90)
print('Display height: ' + str(display.height))
print('Display width: ' + str(display.width))
# Set colors (foreground) and background color
white_color = color565(255, 255, 255) # white
black_color = color565(0, 0, 0) # Black
# Turn on display backlight
backlight = Pin(21, Pin.OUT)
backlight.on()
# Clear display
display.clear(black_color)
def load_image():
display.draw_image('MicroPython128x128.raw', 0, 0, 128, 128)
try:
load_image()
except Exception as e:
print('Error occured: ', e)
except KeyboardInterrupt:
print('Program Interrupted by the user')
How does the Code Work?
To display an image on the display, you just need to call the following function.
def load_image():
display.draw_image('MicroPython128x128.raw', 0, 0, 128, 128)
You just need to call the draw_image() function on the display object. Pass as arguments the x and y coordinates where you want to display the image, and the image width and height.
Testing the Example
After uploading or running the code on the board, you’ll have an image displayed on the screen.
Draw Shapes on the Display – Code
The following example shows how to draw different shapes on the display.
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-cheap-yellow-display-board-cyd-esp32-2432s028r/
from machine import Pin, SPI, ADC, idle
import os
from time import sleep
# Save this file as ili9341.py https://github.com/rdagger/micropython-ili9341/blob/master/ili9341.py
from ili9341 import Display, color565
# Save this file as xglcd_font.py https://github.com/rdagger/micropython-ili9341/blob/master/xglcd_font.py
from xglcd_font import XglcdFont
# Function to set up SPI for TFT display
display_spi = SPI(1, baudrate=60000000, sck=Pin(14), mosi=Pin(13))
# Set up display
display = Display(display_spi, dc=Pin(2), cs=Pin(15), rst=Pin(15),
width=320, height=240, rotation=90)
print('Display height: ' + str(display.height))
print('Display width: ' + str(display.width))
# Set colors (foreground) and background color
white_color = color565(255, 255, 255) # white
black_color = color565(0, 0, 0) # Black
# Turn on display backlight
backlight = Pin(21, Pin.OUT)
backlight.on()
# Clear display
display.clear(black_color)
def draw_shapes():
display.draw_hline(10, 40, 70, color565(255, 0, 255))
sleep(1)
display.draw_vline(10, 0, 40, color565(0, 255, 255))
sleep(1)
display.fill_hrect(23, 50, 30, 75, color565(255, 255, 255))
sleep(1)
display.draw_hline(0, 0, 100, color565(255, 0, 0))
sleep(1)
display.draw_line(50, 0, 64, 40, color565(255, 255, 0))
sleep(2)
display.clear()
coords = [[0, 63], [78, 80], [122, 92], [50, 50], [78, 15], [0, 63]]
display.draw_lines(coords, color565(0, 255, 255))
sleep(1)
display.clear()
display.fill_polygon(7, 120, 120, 100, color565(0, 255, 0))
sleep(1)
display.fill_rectangle(0, 0, 15, 227, color565(255, 0, 0))
sleep(1)
display.clear()
display.fill_rectangle(0, 0, 163, 163, color565(128, 128, 255))
sleep(1)
display.draw_rectangle(0, 64, 163, 163, color565(255, 0, 255))
sleep(1)
display.fill_rectangle(64, 0, 163, 163, color565(128, 0, 255))
sleep(1)
display.draw_polygon(3, 120, 110, 30, color565(0, 64, 255), rotate=15)
sleep(3)
display.clear()
display.fill_circle(132, 132, 70, color565(0, 255, 0))
sleep(1)
display.draw_circle(132, 96, 70, color565(0, 0, 255))
sleep(1)
display.fill_ellipse(96, 96, 30, 16, color565(255, 0, 0))
sleep(1)
display.draw_ellipse(96, 85, 16, 30, color565(255, 255, 0))
sleep(5)
display.cleanup()
try:
draw_shapes()
except Exception as e:
print('Error occured: ', e)
except KeyboardInterrupt:
print('Program Interrupted by the user')
finally:
display.cleanup()
After initializing the display, we apply different methods on the display object to display different shapes. The names of the methods are self-explanatory.
def draw_shapes():
display.draw_hline(10, 40, 70, color565(255, 0, 255))
sleep(1)
display.draw_vline(10, 0, 40, color565(0, 255, 255))
sleep(1)
display.fill_hrect(23, 50, 30, 75, color565(255, 255, 255))
sleep(1)
display.draw_hline(0, 0, 100, color565(255, 0, 0))
sleep(1)
display.draw_line(50, 0, 64, 40, color565(255, 255, 0))
sleep(2)
display.clear()
coords = [[0, 63], [78, 80], [122, 92], [50, 50], [78, 15], [0, 63]]
display.draw_lines(coords, color565(0, 255, 255))
sleep(1)
display.clear()
display.fill_polygon(7, 120, 120, 100, color565(0, 255, 0))
sleep(1)
display.fill_rectangle(0, 0, 15, 227, color565(255, 0, 0))
sleep(1)
(...)
Testing the Example
This example will display multiple shapes on the screen and with different colors.
Control the On-board RGB LED – Code
The ESP32 CYD board comes with an RGB LED that might be useful for debugging. In this section, you’ll learn how to control that RBG LED.
Here’s the RGB LED pinout:
RGB LED | GPIO |
Red LED | GPIO 4 |
Green LED | GPIO 16 |
Blue LED | GPIO 17 |
The pinout and location of the LED might be different depending on your board model.
Important: the RGB LEDs work with inverted logic because they are active low. This means that if you set them to HIGH = OFF and LOW = ON.
The following examples turns the RGB LED in different colors: red, green and blue.
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-cheap-yellow-display-board-cyd-esp32-2432s028r/
from machine import Pin
import os
from time import sleep
# RGB LED at the back
red_led = Pin(4, Pin.OUT)
green_led = Pin(16, Pin.OUT)
blue_led = Pin(17, Pin.OUT)
# Turn on all LEDs (they are active low, so they work with inverted logic)
# Example: red_led.on() command turns the red LED off
red_led.on()
green_led.on()
blue_led.on()
sleep(3)
red_led.off()
sleep(3)
red_led.on()
green_led.off()
sleep(3)
green_led.on()
blue_led.off()
sleep(3)
blue_led.on()
How Does the Code Work?
First, we create variables to refer to the LEDs.
red_led = Pin(4, Pin.OUT)
green_led = Pin(16, Pin.OUT)
blue_led = Pin(17, Pin.OUT)
Then, we turn all the LEDs off (these LEDs work with inverted logic).
red_led.on()
green_led.on()
blue_led.on()
Then, use the the off() command to turn a certain LED on and the on() command to turn a specific LED off.
red_led.off()
sleep(3)
red_led.on()
green_led.off()
sleep(3)
green_led.on()
blue_led.off()
sleep(3)
blue_led.on()
Read the On-board LDR – Code
The CYD board comes with an LDR at the front, right next to the display. The LDR is connected to GPIO 34.
To read the value from the LDR, you just need to read the analog signal on GPIO 34.
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-cheap-yellow-display-board-cyd-esp32-2432s028r/
from machine import Pin, ADC
import os
from time import sleep
try:
# Run the event loop indefinitely
while True:
# Read light sensor
lightsensor = ADC(34, atten=ADC.ATTN_0DB)
print('LDR value: ' + str(lightsensor.read_uv()))
sleep(1)
except Exception as e:
print('Error occured: ', e)
except KeyboardInterrupt:
print('Program Interrupted by the user')
Recommended reading: ESP32/ESP8266 Analog Readings with MicroPython.
Note: on many CYD boards, the LDR will not work as expected without modifying the internal circuit. So, if you’re getting the same value regardless of the luminosity, it will be the case that the LDR on your board doesn’t work properly.
Wrapping Up
This tutorial was a quick getting started guide for the ESP32 CYD (Cheap Yellow Display) board with MicroPython. We’ve covered displaying text, images and shapes and how to control the RGB LED and read values from the LDR.
This tutorial should also be compatible with other TFT displays by setting the right pinout and dimensions in the code.
If you prefer to use Arduino IDE instead, check this guide for the CYD board with Arduino IDE.
If you would like to learn more about Micropython with the ESP32 board, check out our resources:
Thanks for reading.
It would be great if you could do an article on using LVGL with MicroPython. It’s allegedly possible but I’ve not been able to figure out how to make it work.
Hi.
Thanks for the suggestion.
We’ll take a look into that.
Regards,
Sara
SPI CS and SPI RST both use Pin(15). This is probably hardwired. Why is that so, and why does it work? Thanks and best regards – Kornel
Thanks for the again excelent guide! However, I have the 2USB version of the CYD which has the ST7789 screen. Do you know if a MicroPython ST7789 library is available that will work with the CYD?
Many Thanks in advance, Aart
github.com/pimoroni/st7789-python
Forked and modified by Pimoroni for their own “ST7789 based 240×240 pixel TFT SPI display”.
Maybe you can make it work with a few modifications.
Hat mir sehr gut gefallen! Bei der Konvertierung anderer Bilder in raw habe ich Probleme.
Mfg
So mouch thanks !! all operate fine.