ST7789是一种常用的液晶屏控制芯片(最大支持的分辨率为240×320),可与单片机之间通过SPI通信传送控制指令或者数据。在MicroPython环境下使用ESP32或者树莓派Pico可以直接下载st7789_mpy库预先编译好的固件firmware来尝试控制液晶屏,支持的各种单片机开发板如下表所示。这里使用树莓派Pico,将RP2文件夹中的firmware.uf2固件下载下来,然后按相关教程将其拖放到Pico上进行编程。
DirectoryFileDevice
GENERIC-7789
firmware.bin
Generic ESP32 devices
GENERIC_SPIRAM-7789
firmware.bin
Generic ESP32 devices with SPI Ram
GENERIC_C3
firmware.bin
Generic ESP32-C3 devices (JPG support not working)
PYBV11
firmware.dfu
Pyboard v1.1
RP2
firmware.uf2
Raspberry Pi Pico RP2040
T-DISPLAY
firmware.bin
LILYGO® TTGO T-Display
T-Watch-2020
firmware.bin
LILYGO® T-Watch 2020
可以根据文档进行一些简单的测试:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from machine import SPI, Pin
import st7789
import time
import math
import vga1_8x8 as font
spi = SPI(0, baudrate=40_000_000, polarity=0, phase=0, sck=Pin(18,Pin.OUT), mosi=Pin(19,Pin.OUT))
display = st7789.ST7789(spi, 240, 320,
reset=Pin(20, Pin.OUT),
dc=Pin(21, Pin.OUT),
cs=Pin(17, Pin.OUT),
backlight=Pin(16, Pin.OUT),
color_order = st7789.RGB,
inversion = False,
rotation = 2)
display.init()
COLORS = [
0xFFE0, # yellow
0x0000, # black
st7789.BLUE,
st7789.RED,
st7789.GREEN,
st7789.CYAN,
st7789.MAGENTA,
st7789.YELLOW,
st7789.WHITE,
st7789.BLACK]
for color in COLORS:
display.fill(color)
time.sleep_ms(400)
display.text(font, "Hello world", 0, 0, st7789.WHITE)
display.circle(120, 160, 50, st7789.WHITE)
display.vline(120, 160, 50, st7789.BLUE)
display.hline(120, 160, 50, st7789.RED)
display.rect(10, 20, 10, 20, st7789.YELLOW)
display.fill_rect(10, 50, 10, 20, st7789.GREEN)
display.fill_circle(120, 160, 5, st7789.WHITE)
pointlist=((0,0),(20,0),(30,30),(0,20),(0,0))
display.polygon(pointlist, 120, 50, st7789.WHITE, 0, 120, 50)
display.polygon(pointlist, 120, 50, st7789.GREEN, math.pi/2, 0, 0)
display.fill_polygon(((0,0),(10,0),(5,7),(0,0)), 115, 40, st7789.RED)
time.sleep_ms(200)
for x in range(240):
y = int(80*math.sin(0.125664*x)) + 160
display.pixel(x, y, st7789.MAGENTA)
for d in range(-90,-185,-5):
a = d * math.pi / 180
x1 = int(50*math.cos(a)+120)
y1 = int(50*math.sin(a)+160)
display.line(120, 160, x1, y1, st7789.CYAN)
time.sleep_ms(50)
time.sleep(5)
display.off() # Turn off the backlight pin if one was defined during init.
time.sleep(2)
display.on()
View Code
接下来尝试根据ST7789的数据手册自己编写MicroPython代码驱动液晶屏,以熟悉底层控制逻辑。与传统的SPI协议不同的地方是由于是只需要显示,故而不需要从机发往主机的数据线MISO。根据手册:RESX为复位信号,低电平有效,通常情况下置1;CSX为从机片选, 仅当CS为低电平时,芯片才会被使能。如果只有一个显示屏可以直接将CSX信号拉低,程序中不需对其进行控制;D/CX为芯片的数据/命令控制引脚,当DC = 0时写命令,当DC = 1时写数据;SDA为通过SPI的MOSI引脚传输的数据,即RGB数据;SCL为SPI通信时钟,空闲时低电平,第一个上升沿采样开始传输数据,一个时钟周期传输8bit数据,按位传输,高位在前,低位在后(编程时要注意字节顺序问题)。因此,SPI的时钟极性CPOL=0(空闲状态为低电平),时钟相位CPHA=0(第一时钟跳变沿数据被采集)。
ST7789支持12位,16位以及18位每像素的输入颜色格式,即RGB444,RGB565,RGB666三种颜色格式。这里使用RGB565这种最常用的RGB颜色格式,用两个字节16位来存储像素点的信息(可以表示2^16 = 65536种颜色 , 即65K),其中R通道占据5个字节,G通道占据6个字节,B通道占据5个字节。
![](https://img2022.cnblogs.com/blog/890966/202203/890966-20220327150750075-1768231213.png)
ST7789芯片内部有数量众多的寄存器用于控制各种参数,在使用屏幕前需要对这些寄存器进行初始化配置。比较重要的配置有:睡眠设置、颜色格式、屏幕方向、RGB顺序、颜色反转、显示模式、伽马校正、显示开关等,可以先在初始化函数中添加尽量少的关键配置项,顺利点亮屏幕后再根据具体需求逐渐完善(如伽马校正等)。可以参考st7789.c中的初始化步骤:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
STATIC mp_obj_t st7789_ST7789_init(mp_obj_t self_in)
{
st7789_ST7789_obj_t *self = MP_OBJ_TO_PTR(self_in);
st7789_ST7789_hard_reset(self_in);
st7789_ST7789_soft_reset(self_in);
write_cmd(self, ST7789_SLPOUT, NULL, 0);
const uint8_t color_mode[] = {COLOR_MODE_65K | COLOR_MODE_16BIT};
write_cmd(self, ST7789_COLMOD, color_mode, 1);
mp_hal_delay_ms(10);
set_rotation(self);
if (self->inversion) {
write_cmd(self, ST7789_INVON, NULL, 0);
} else {
write_cmd(self, ST7789_INVOFF, NULL, 0);
}
mp_hal_delay_ms(10);
write_cmd(self, ST7789_NORON, NULL, 0);
mp_hal_delay_ms(10);
const mp_obj_t args[] = {
self_in,
mp_obj_new_int(0),
mp_obj_new_int(0),
mp_obj_new_int(self->width),
mp_obj_new_int(self->height),
mp_obj_new_int(BLACK)};
st7789_ST7789_fill_rect(6, args);
if (self->backlight)
mp_hal_pin_write(self->backlight, 1);
write_cmd(self, ST7789_DISPON, NULL, 0);
mp_hal_delay_ms(150);
return mp_const_none;
}
View Code
下面介绍几个比较关键的寄存器:
1、MADCTL (36h) 控制屏幕显示/刷新方向和RGB顺序
![](https://img2022.cnblogs.com/blog/890966/202203/890966-20220327160141369-177081536.png)
MX=0 表示列地址的方向是从左往右,MX=1 表示列地址的方向是从右往左;MY=0 表示行地址的方向是从上往下,MY=1 表示行地址的方向是从下往上;MV=0表示正常模式,MV=1表示行列方向互换;通过控制MV、MX、MY这三个位的值就可以控制屏幕方向(参考ST7789芯片手册第125页)。MADCTL = 0x00是正常方向,MADCTL = 0x60是顺时针旋转90°的方向,MADCTL = 0xC0是屏幕上下颠倒的方向,MADCTL = 0xA0是逆时针旋转90°的方向。此外MADCTL寄存器的D3位还可以控制RGB颜色顺序,该位为0时代表颜色的两字节按R-G-B顺序解释,设置为1时按G-B-R顺序(默认为RGB)。
![](https://img2022.cnblogs.com/blog/890966/202203/890966-20220327160744583-1655613330.png)
2、CASET (2Ah): Column Address Set、 RASET (2Bh): Row Address Set 、RAMWR (2Ch): Memory Write 。CASET和RASET寄存器设置列与行像素坐标的范围,即设定一个绘图区域,超出该区域无效。对于宽高为240×320像素的屏幕来说,如果在全屏幕上绘图,则列方向像素的坐标范围是:XS=0(0h),XE=239(EFh);行方向像素的坐标范围是:YS=0(0h),YE=319(13Fh)。设置好绘图区域后即可发送RAMWR指令,开始将像素数据从MCU传送至ST7789的frame memory。
![](https://img2022.cnblogs.com/blog/890966/202203/890966-20220327162538721-364790039.png)
下面是基于MicroPython的ST7789液晶屏驱动代码,实现了一些最基本的LCD控制功能以及基础图象绘制:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from machine import SPI, Pin
import struct
import time
def color565(red: int, green: int = 0, blue: int = 0) -> int:
"""
Convert red, green and blue values (0-255) into a 16-bit 565 encoding.
"""
return (red & 0xF8) 3
def swap_bytes(val):
((val >> 8) & 0x00FF) | ((val Xend or Xend >= self.width):
return
if (Ystart > Yend or Yend >= self.height):
return
# Set the X coordinates
self.send_command(0x2A) # CASET (0x2A): Column Address Set
self.send_data(Xstart >> 8) # Set the horizontal starting point to the high octet
self.send_data(Xstart & 0xFF) # Set the horizontal starting point to the low octet
self.send_data(Xend >> 8) # Set the horizontal end to the high octet
self.send_data(Xend & 0xFF) # Set the horizontal end to the low octet
# Set the Y coordinates
self.send_command(0x2B) # RASET (0x2B): Row Address Set
self.send_data(Ystart >> 8)
self.send_data(Ystart & 0xFF)
self.send_data(Yend >> 8)
self.send_data(Yend & 0xFF)
# This command is used to transfer data from MCU to frame memory
# When this command is accepted, the column register and the page
# register are reset to the start column/start page positions.
self.send_command(0x2C) # RAMWR (0x2C): Memory Write
def on(self):
"""
Turn on the backlight pin if one was defined during init.
"""
self.backlight(1)
time.sleep_ms(10)
def off(self):
"""
Turn off the backlight pin if one was defined during init.
"""
self.backlight(0)
time.sleep_ms(10)
def set_color_order(self, color_order: int):
"""Change color order"""
if not color_order in [self.RGB, self.BGR]:
return
self.send_command(0x36) # MADCTL (0x36): Memory Data Access Control
self.color_order = color_order
self.madctl_value = color_order | (self.madctl_value & 0xF7)
self.send_data(self.madctl_value)
def inversion_mode(self, value: bool):
"""
Enable or disable display inversion mode.
Args:
value (bool): if True enable inversion mode. if False disable
inversion mode
"""
if value:
self.send_command(0x21) # INVON (0x21): Display Inversion On
else:
self.send_command(0x20) # INVOFF (0x20): Display Inversion Off
def sleep_mode(self, value: bool):
"""
Enable or disable display sleep mode.
Args:
value (bool): if True enable sleep mode. if False disable sleep mode
"""
if value:
# This command causes the LCD module to enter the minimum power consumption mode.
# MCU interface and memory are still working and the memory keeps its contents.
self.send_command(0x10) # SLPIN (0x10): Sleep in
else:
self.send_command(0x11) # SLPOUT (0x11): Sleep Out
time.sleep_ms(120) # wait a little time before sending any new commands
def set_rotation(self, rotation: int):
"""
Set display rotation.
Args:
rotation (int):
- 0-Portrait
- 1-Landscape
- 2-Inverted Portrait
- 3-Inverted Landscape
"""
if rotation < 0 or rotation > 3:
return
self.rotation = rotation
row = self.ORIENTATIONS_240x320[self.rotation]
self.madctl_value = self.color_order | row[0]
self.width = row[1]
self.height = row[2]
self.send_command(0x36) # MADCTL (0x36): Memory Data Access Control
self.send_data(self.madctl_value)
def pixel(self, x: int, y: int, color: int):
"""
Draw a pixel at the given location and color.
Args:
x (int): x coordinate
y (int): y coordinate
color (int): 565 encoded color
"""
self.set_window(x, y, x, y)
pixel = struct.pack('>H', color)
self.cs(0)
self.dc(1)
self.spi.write(pixel)
self.cs(1)
def blit_buffer(self, buffer: bytes, x: int, y: int, width: int, height: int):
"""
Copy buffer to display at the given location.
Args:
buffer (bytes): Data to copy to display
x (int): Top left corner x coordinate
y (int): Top left corner y coordinate
width (int): Width
height (int): Height
"""
if (not 0 self.width - 1):
right = self.width - 1
if (bottom > self.height - 1):
bottom = self.height - 1
self.set_window(x, y, right, bottom)
self.cs(0)
self.dc(1)
self.fill_color_buffer(color, 2*width*height)
self.cs(1)
def fill_color_buffer(self, color: int, length: int):
buffer_pixel_size = 256 # 256 pixels, 512 byte
chunks, rest = divmod(length, buffer_pixel_size*2)
pixel = struct.pack('>H', color)
buffer = pixel * buffer_pixel_size # a bytearray
if chunks:
for count in range(chunks):
self.spi.write(buffer)
if rest:
self.spi.write(pixel * rest)
def fill(self, color: int):
"""
Fill the entire FrameBuffer with the specified color.
Args:
color (int): RGB565 encoded color
"""
self.set_window(0, 0, self.width-1, self.height-1)
self.cs(0)
self.dc(1)
self.fill_color_buffer(color, 2*self.width*self.height)
self.cs(1)
def line(self, x0: int, y0: int, x1: int, y1: int, color: int):
"""
Draw a single pixel wide line starting at x0, y0 and ending at x1, y1.
Args:
x0 (int): Start point x coordinate
y0 (int): Start point y coordinate
x1 (int): End point x coordinate
y1 (int): End point y coordinate
color (int): 565 encoded color
"""
# http://members.chello.at/easyfilter/bresenham.html
# https://oldj.net/article/2010/08/27/bresenham-algorithm/
# https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
x, y = x0, y0
dx = abs(x1 - x0)
dy = abs(y1 - y0)
sx = 1 if x1 >= x0 else -1
sy = 1 if y1 >= y0 else -1
if dy > dx:
dx, dy = dy, dx
swap = 1
else:
swap = 0
f = 2 * dy - dx
self.pixel(x0, y0, color)
for i in range(dx):
if f >= 0:
if swap:
x += sx
y += sy
else:
x += sx
y += sy
self.pixel(x, y, color)
f = f + 2 * (dy - dx)
else:
if swap:
y += sy
else:
x += sx
f = f + 2 * dy
def vline(self, x: int, y: int, length: int, color: int):
"""
Draw vertical line at the given location and color.
Args:
x (int): x coordinate
y (int): y coordinate
length (int): length of line
color (int): 565 encoded color
"""
self.fill_rect(x, y, 1, length, color)
def hline(self, x: int, y: int, length: int, color: int):
"""
Draw horizontal line at the given location and color.
Args:
x (int): x coordinate
y (int): y coordinate
length (int): length of line
color (int): 565 encoded color
"""
self.fill_rect(x, y, length, 1, color)
def map_bitarray_to_rgb565(self, bitarray, buffer, width, color, bg_color):
"""
Convert a bitarray to the rgb565 color buffer that is suitable for blitting.
Bit 1 in bitarray is a pixel with color and 0 - with bg_color.
"""
row_pos = 0
length = len(bitarray)
id = 0
for i in range(length):
byte = bitarray[i]
for bi in range(7,-1,-1):
b = byte & (1 8
id += 1
buffer[id] = cur_color & 0xFF
id += 1
row_pos += 1
if (row_pos >= width):
row_pos = 0
break
if __name__ == "__main__":
# The RP2040 has 2 hardware SPI buses which is accessed via the machine.SPI class
spi = SPI(id=0, baudrate=40_000_000, polarity=0, phase=0, sck=Pin(18,Pin.OUT), mosi=Pin(19,Pin.OUT), bits=8, firstbit=SPI.MSB)
lcd = LCD_ST7789(spi, rotation = 0)
lcd.fill(LCD_ST7789.BLACK)
lcd.fill_rect(0, 0, 20, 40, LCD_ST7789.GREEN)
lcd.rect(40, 0, 20, 40, LCD_ST7789.WHITE)
lcd.hline(120, 160, 120, LCD_ST7789.RED)
lcd.vline(120, 160, 160, LCD_ST7789.BLUE)
# lcd.line(120, 160, 239, 0, LCD_ST7789.BLUE)
# lcd.line(120, 160, 0, 40, color565(231,107,138))
# lcd.line(0, 40, 239, 319, color565(207,60,244))
# lcd.line(120, 160, 0, 319, LCD_ST7789.GREEN)
# lcd.line(120, 160, 239, 319, LCD_ST7789.CYAN)
# lcd.line(120, 160, 239, 100, LCD_ST7789.YELLOW)
# time.sleep(2)
# lcd.sleep_mode(True)
# lcd.set_color_order(LCD_ST7789.BGR)
# lcd.fill_rect(0, 50, 20, 40, LCD_ST7789.RED)
# lcd.fill_rect(0, 100, 20, 40, LCD_ST7789.BLUE)
# time.sleep(2)
# lcd.sleep_mode(False)
# import math
# for x in range(240):
# y = int(80*math.sin(0.125664*x)) + 160
# lcd.pixel(x, y, LCD_ST7789.MAGENTA)
#
# lcd.set_rotation(1)
#
# for x in range(360):
# y = int(40*math.sin(0.125664*x)) + 120
# lcd.pixel(x, y, LCD_ST7789.CYAN)
# time.sleep(2)
# lcd.soft_reset()
# time.sleep(2)
# lcd.inversion_mode(True)
# time.sleep(1)
# lcd.off()
# time.sleep(1)
# lcd.on()
SPRITE_WIDTH = 16
SPRITE_HEIGHT = 16
SPRITE_BITMAPS = [
bytearray([
0b00000000, 0b00000000,
0b00000001, 0b11110000,
0b00000111, 0b11110000,
0b00001111, 0b11100000,
0b00001111, 0b11000000,
0b00011111, 0b10000000,
0b00011111, 0b00000000,
0b00011110, 0b00000000,
0b00011111, 0b00000000,
0b00011111, 0b10000000,
0b00001111, 0b11000000,
0b00001111, 0b11100000,
0b00000111, 0b11110000,
0b00000001, 0b11110000,
0b00000000, 0b00000000,
0b00000000, 0b00000000]),
bytearray([
0b00000000, 0b00000000,
0b00000011, 0b11100000,
0b00001111, 0b11111000,
0b00011111, 0b11111100,
0b00011111, 0b11111100,
0b00111111, 0b11110000,
0b00111111, 0b10000000,
0b00111100, 0b00000000,
0b00111111, 0b10000000,
0b00111111, 0b11110000,
0b00011111, 0b11111100,
0b00011111, 0b11111100,
0b00001111, 0b11111000,
0b00000011, 0b11100000,
0b00000000, 0b00000000,
0b00000000, 0b00000000]),
bytearray([
0b00000000, 0b00000000,
0b00000111, 0b11000000,
0b00011111, 0b11110000,
0b00111111, 0b11111000,
0b00111111, 0b11111000,
0b01111111, 0b11111100,
0b01111111, 0b11111100,
0b01111111, 0b11111100,
0b01111111, 0b11111100,
0b01111111, 0b11111100,
0b00111111, 0b11111000,
0b00111111, 0b11111000,
0b00011111, 0b11110000,
0b00000111, 0b11000000,
0b00000000, 0b00000000,
0b00000000, 0b00000000])]
# convert bitmaps into rgb565 blitable buffers
blitable = []
for sprite_bitmap in SPRITE_BITMAPS:
sprite = bytearray(512)
lcd.map_bitarray_to_rgb565(
sprite_bitmap,
sprite,
SPRITE_WIDTH,
LCD_ST7789.YELLOW,
LCD_ST7789.BLACK)
blitable.append(sprite)
import random
x = random.randint(0, lcd.width - SPRITE_WIDTH)
y = random.randint(0, lcd.height - SPRITE_HEIGHT)
# draw sprites
while True:
for i in range(0, len(blitable)):
lcd.blit_buffer(blitable[i], x, y, SPRITE_WIDTH, SPRITE_HEIGHT)
time.sleep_ms(200)
for i in range(len(blitable)-1,-1,-1):
lcd.blit_buffer(blitable[i], x, y, SPRITE_WIDTH, SPRITE_HEIGHT)
time.sleep_ms(200)
View Code
需要注意的是屏幕尺寸为240×320,对RGB565格式来说每个像素占2字节,那么整个屏幕需要240*320*2=153600字节(150Kb),而一般单片机的RAM都很有限,比如STM32F407只有192Kb的RAM,在程序中直接使用数组等缓冲区刷新屏幕时要注意数组大小,数组过大会产生栈溢出,可以设置一个合适的数组长度,分段进行数据刷新。
在当前文件中测试了基本的绘图函数及一些LCD控制命令(如睡眠、背光开关、颜色反转、软复位等),除此之外还使用blit_buffer函数根据给定的字节数组绘制了“吃豆人”,在屏幕上随机选择一个初始位置,连续绘制产生动画效果。由于例子是用MicroPython脚本语言编写的,比st7789_mpy这个由C语言编译的库要慢不少,连续画斜直线时可以看到肉眼可见的延迟。
![](https://img2022.cnblogs.com/blog/890966/202203/890966-20220327181639854-64927933.gif)
LCD_ST7789类的绘图函数中画像素点、填充矩形、画竖线或直线(特殊的填充矩形)、画矩形等方法比较简单直接对FrameBuffer进行简单操作就行,而像画斜直线、画圆、画椭圆、画贝塞尔曲线、画抗锯齿的直线等方法就更复杂一些,涉及计算机图形学相关知识。在屏幕上画直线有DDA(数值微分)算法、中点画线法、Bresenham算法等多种方法,Bresenham算法的优点如下:① 不必计算直线的斜率,因此不做除法。② 不用浮点数,只用整数。③ 只做整数加减运算和乘2运算,而乘2运算可以用移位操作实现。④ Bresenham算法的运算速度很快。程序中使用Bresenham算法来画直线,下面是该算法的简单介绍:
![](https://img2022.cnblogs.com/blog/890966/202203/890966-20220327153705804-1071969880.png)
参考:
ST7789VW 手册
st7789_mpy/st7789/st7789.c
st7789_mpy/examples/st7789.pyi
micropython_esp32_st7789
微雪 2.4inch LCD Module
伽玛校正
【分钟物理】电脑颜色是错的
Bresenham's line algorithm
The Beauty of Bresenham's Algorithm
MicroPython class SPI – a Serial Peripheral Interface bus protocol
|