树莓派Pico + MicroPython驱动2.4寸SPI串口屏(ST7789) 您所在的位置:网站首页 指令寸止文本 树莓派Pico + MicroPython驱动2.4寸SPI串口屏(ST7789)

树莓派Pico + MicroPython驱动2.4寸SPI串口屏(ST7789)

2023-10-18 01:29| 来源: 网络整理| 查看: 265

  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

   可以根据文档进行一些简单的测试:

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个字节。

  ST7789芯片内部有数量众多的寄存器用于控制各种参数,在使用屏幕前需要对这些寄存器进行初始化配置。比较重要的配置有:睡眠设置、颜色格式、屏幕方向、RGB顺序、颜色反转、显示模式、伽马校正、显示开关等,可以先在初始化函数中添加尽量少的关键配置项,顺利点亮屏幕后再根据具体需求逐渐完善(如伽马校正等)。可以参考st7789.c中的初始化步骤:

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顺序

  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)。 

   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。

 

   下面是基于MicroPython的ST7789液晶屏驱动代码,实现了一些最基本的LCD控制功能以及基础图象绘制:

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语言编译的库要慢不少,连续画斜直线时可以看到肉眼可见的延迟。

   LCD_ST7789类的绘图函数中画像素点、填充矩形、画竖线或直线(特殊的填充矩形)、画矩形等方法比较简单直接对FrameBuffer进行简单操作就行,而像画斜直线、画圆、画椭圆、画贝塞尔曲线、画抗锯齿的直线等方法就更复杂一些,涉及计算机图形学相关知识。在屏幕上画直线有DDA(数值微分)算法、中点画线法、Bresenham算法等多种方法,Bresenham算法的优点如下:① 不必计算直线的斜率,因此不做除法。② 不用浮点数,只用整数。③ 只做整数加减运算和乘2运算,而乘2运算可以用移位操作实现。④ Bresenham算法的运算速度很快。程序中使用Bresenham算法来画直线,下面是该算法的简单介绍:

 

 

 

参考:

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



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有