背景:
最近买了几块ESP32模块,看了下mircopython支持还不错,所以买了个SPI触摸屏试试水,记录一下使用过程。
硬件相关:
SPI触摸屏
使用2.4寸屏幕,常见淘宝均可买到,驱动为ILI9341,具体参数如下图:
引脚描述:
ESP32模块
依旧轻松购买于淘宝,由于ESP32被很多家封装,因此模块各有不同,我的如下:
接口描述,网上找了个,差异点在于图中使用GIPOxx,而开发板印刷使用Dxx,例如,开发板上,D12,对应下图的GPIO12,其他的不需要关注:
接线
由于ESP32模块有两个可用SPI,而触摸屏显示和触摸都是使用SPI协议,刚好用完资源,
SPI | SCK | MOSI | MISO | CS | 用途 |
SPI1(HSPI) | GPIO14(D14) | GPIO13(D13) | GPIO12(D12) | GPIO15(D15) | 分配给触摸接口 |
SPI2(VSPI) | GPIO18(D18) | GPIO23(D23) | GPIO19(D19) | GPIO5(D5) | 分配给显示接口 |
图上还会看到VSPI 和 HSPI
具体接线如下:
触摸屏 | 分配(ESP32引脚名) | 归属 | |
显示设备 | VCC | 3V3 | ESP-IO |
GND | GND | ESP-IO | |
CS | D5 | SPI2 | |
RESET | D2 | ESP-IO | |
DC/RS | D21 | ESP-IO | |
SDI | D23 | SPI2 | |
SCK | D18 | SPI2 | |
LED | D4 | ESP-IO | |
SDO | D19 | SPI2 | |
触摸设备 | T_CLK | D14 | SPI1 |
T_CS | D15 | SPI1 | |
T_DIN | D13 | SPI1 | |
T_DO | D12 | SPI1 | |
T_IRQ | D33 | ESP-IO |
注意(不建议使用的引脚):
不建议使用或限制使用的引脚
不建议使用 Strapping引脚 ,SPI flash 引脚 以及 仅输入的引脚
Strapping 引脚
GPIO 0
GPIO 2
GPIO 4
GPIO 5 (启动时必须为高电平)
GPIO 12 (启动时必须为低电平)
GPIO 15 (启动时必须为高电平)
注意:
在硬件上要注意使用外接模块时不能将GPIO12拉高,否则将导致ESP32启动异常。还有一些GPIO在启动或重置时其状态更改为高或者输出PWM信号,在使用时需要注意。
集成在ESP-WROOM-32 的 SPI flash 引脚
GPIO 6 到 GPIO 11 在一些 ESP32 开发板中公开。但是,这些引脚连接到 ESP-WROOM-32 芯片上的集成 SPI 闪存,不推荐用于其他用途。所以,不要在你的项目中使用这些引脚:
GPIO 6 (SCK/CLK)
GPIO 7 (SDO/SD0)
GPIO 8 (SDI/SD1)
GPIO 9 (SHD/SD2)
GPIO 10 (SWP/SD3)
GPIO 11 (CSC/CMD)
仅输入引脚
GPIO 34 到 39 是 GPI – 仅输入引脚。这些引脚没有内部上拉或下拉电阻。它们不能用作输出,因此只能将这些引脚用作输入:
GPIO 34
GPIO 35
GPIO 36
GPIO 39
这些引脚都是ESP32用于引导加载程序或者烧录模式/在大多数内置USB/Serial的开发板上,不需要担心这些引脚的状态,开发板会把这些引脚设置为正确的状态,以便使用烧录或启动模式。
但是,如果你有外设连接到这些引脚上,当你在尝试上传新代码、用新固件烧写ESP32或重置电路板时可能会遇到麻烦,例如不明原因的错误和失败。可能是因为这些外设阻止ESP32进入正确的模式。
所以以上的引脚 不建议在项目中使用。
软件相关
目录结构
其中core/screen 文件夹,lib/ili9341.py 和 lib/xpt2046.py 以及main.py是我们本次测试文件.
效果
显示测试:
触摸测试:
代码
lib/ili9341.py
"""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
import ustruct # 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
ROTATE = {
0: 0x88,
90: 0xE8,
180: 0x48,
270: 0x28
}
def __init__(self, spi, cs, dc, rst,
width=240, height=320, rotation=0):
"""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
"""
self.spi = spi
self.cs = cs
self.dc = dc
self.rst = rst
self.width = width
self.height = height
if rotation not in self.ROTATE.keys():
raise RuntimeError('Rotation must be 0, 90, 180 or 270.')
else:
self.rotation = self.ROTATE[rotation]
# 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
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, *ustruct.pack(">HH", x0, x1))
self.write_cmd(self.SET_PAGE, *ustruct.pack(">HH", y0, y1))
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):
"""Clear display.
Args:
color (Optional int): RGB565 color value (Default: 0 = Black).
"""
w = self.width
h = self.height
# Clear display in 1024 byte blocks
if color:
line = color.to_bytes(2, 'big') * (w * 8)
else:
line = bytearray(w * 16)
for y in range(0, h, 8):
self.block(0, y, w - 1, y + 7, 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):
"""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)
"""
buf, w, h = font.get_letter(letter, color, background, landscape)
# 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, 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)
spacing (int): Pixels between letters (default: 1)
"""
for letter in text:
# Get letter array and letter dimensions
w, h = self.draw_letter(x, y, letter, font, color, background,
landscape)
# 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
# Rearrange color
r = (color & 0xF800) >> 8
g = (color & 0x07E0) >> 3
b = (color & 0x1F) << 3
buf = bytearray(w * 16)
fbuf = FrameBuffer(buf, w, h, RGB565)
if background != 0:
bg_r = (background & 0xF800) >> 8
bg_g = (background & 0x07E0) >> 3
bg_b = (background & 0x1F) << 3
fbuf.fill(color565(bg_b, bg_r, bg_g))
fbuf.text(text, 0, 0, color565(b, r, g))
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 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
"""
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)
print(top, middle, 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
lib/xpt2046.py
"""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)
core/screen/TLedCfg.py
class TLedConfig:
def __init__(self) -> None:
self.CS = 15 # 片选, 低电平使能
self.RESET = 2 # 低电平复位
self.DC = 21 # 液晶屏寄存器/数据选择信号,0:寄存器,1:数据
self.SDI = 23 # MOSI 写
self.SCK = 18 # 时钟
self.LED = 4 # 背光控制,高电平点亮
self.SDO = 19 # MISO 读
self.T_CLK = 14 # 触摸时钟
self.T_CS = 27 # 片选,低电平使能
self.T_DIN = 13 # 总线输入,接MOSI
self.T_DO = 12 # 总线输出,接MISO
self.T_IRQ = 33 # 中断,检测到触摸时为低电平
core/screen/test.py
from lib.ili9341 import Display, color565
from lib.xpt2046 import Touch
from machine import Pin, SPI, idle
from core.screen.TLedCfg import TLedConfig
from time import sleep
def ScreenTest():
ledCfg = TLedConfig()
power = Pin(ledCfg.LED, Pin.OUT)
power.value(1)
spi = SPI(2, baudrate=40000000, sck=Pin(ledCfg.SCK), mosi=Pin(ledCfg.SDI))
display = Display(spi, dc=Pin(ledCfg.DC), cs=Pin(ledCfg.CS), rst=Pin(ledCfg.RESET))
display.clear(color565(64, 0, 255))
sleep(1)
display.clear()
display.draw_hline(10, 319, 229, color565(255, 0, 255))
sleep(1)
display.draw_vline(10, 0, 319, color565(0, 255, 255))
sleep(1)
display.fill_hrect(23, 50, 30, 75, color565(255, 255, 255))
sleep(1)
display.draw_hline(0, 0, 222, color565(255, 0, 0))
sleep(1)
display.draw_line(127, 0, 64, 127, 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, 286, 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, 256, 16, 30, color565(255, 255, 0))
sleep(5)
display.cleanup()
class Demo(object):
"""Touchscreen simple demo."""
CYAN = color565(0, 255, 255)
PURPLE = color565(255, 0, 255)
WHITE = color565(255, 255, 255)
def __init__(self, display, spi2):
"""Initialize box.
Args:
display (ILI9341): display object
spi2 (SPI): SPI bus
"""
ledCfg = TLedConfig()
self.display = display
self.touch = Touch(spi2, cs=Pin(ledCfg.T_CS), int_pin=Pin(ledCfg.T_IRQ),
int_handler=self.touchscreen_press)
# Display initial message
self.display.draw_text8x8(self.display.width // 2 - 32,
self.display.height - 9,
"TOUCH ME",
self.WHITE,
background=self.PURPLE)
# A small 5x5 sprite for the dot
self.dot = bytearray(b'\x00\x00\x07\xE0\xF8\x00\x07\xE0\x00\x00\x07\xE0\xF8\x00\xF8\x00\xF8\x00\x07\xE0\xF8\x00\xF8\x00\xF8\x00\xF8\x00\xF8\x00\x07\xE0\xF8\x00\xF8\x00\xF8\x00\x07\xE0\x00\x00\x07\xE0\xF8\x00\x07\xE0\x00\x00')
def touchscreen_press(self, x, y):
"""Process touchscreen press events."""
print('touch once ... ...')
# Y needs to be flipped
# y = (self.display.height - 1) - y
# Display coordinates
self.display.draw_text8x8(self.display.width // 2 - 32,
self.display.height - 9,
"{0:03d}, {1:03d}".format(x, y),
self.CYAN)
# Draw dot
self.display.draw_sprite(self.dot, x ,y, 5, 5)
def TouchTest():
"""Test code."""
ledCfg = TLedConfig()
power = Pin(ledCfg.LED, Pin.OUT)
power.value(1)
spi1 = SPI(2, baudrate=32000000, sck=Pin(ledCfg.SCK), mosi=Pin(ledCfg.SDI))
spi2 = SPI(1, baudrate=1000000, sck=Pin(ledCfg.T_CLK), mosi=Pin(ledCfg.T_DIN))
display = Display(spi1, dc=Pin(ledCfg.DC), cs=Pin(ledCfg.CS), rst=Pin(ledCfg.RESET))
# display = Display(spi1, dc=Pin(4), cs=Pin(16), rst=Pin(17))
# spi2 = SPI(2, baudrate=1000000, sck=Pin(18), mosi=Pin(23), miso=Pin(19))
Demo(display, spi2)
try:
while True:
idle()
except KeyboardInterrupt:
print("\nCtrl-C pressed. Cleaning up and exiting...")
finally:
display.cleanup()
main.py
from core.WifiManager import WifiManager
from core.screen.TLedCfg import TLedConfig
from core.screen.test import ScreenTest,TouchTest
def main():
print("Welcome to MicroPython!")
# wifi = WifiManager()
# wifi.start_ap("esp32","12345678")
if __name__ == '__main__':
main()
ScreenTest()
# TouchTest()
main函数里,测试哪个就把另一个注释就好。
附录:
- 驱动和参考主要来自如下:https://github.com/rdagger/micropython-ili9341
- LVGL:https://github.com/lvgl/lvgl/tree/b66512183ab0ba3b36e8175504fa6fe9cd6e5312
- ESP32引脚参考大全ESP32 – GPIO 引脚参考大全 – 凌顺实验室 (lingshunlab.com)