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

  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) << 8 | (green & 0xFC) << 3 | blue >> 3


def swap_bytes(val):
    ((val >> 8) & 0x00FF) | ((val << 8) & 0xFF00)


class LCD_ST7789:
    # Color definition
    BLACK =   0x0000
    BLUE  =   0x001F
    RED   =   0xF800
    GREEN =   0x07E0
    CYAN  =   0x07FF
    MAGENTA = 0xF81F
    YELLOW  = 0xFFE0
    WHITE   = 0xFFFF
    # Color order
    RGB = 0x00
    BGR = 0x08
    # Default st7789 display orientation tables
    # [madctl, width, height]
    ORIENTATIONS_240x320 = [
        [ 0x00, 240, 320 ], # Normal direction
        [ 0x60, 320, 240 ], # X-Y Exchange, X-Mirror
        [ 0xC0, 240, 320 ], # X-Mirror, Y-Mirror
        [ 0xA0, 320, 240 ]] # X-Y Exchange, Y-Mirror


    def __init__(self, spi, cs=17, dc=21, rst=20, backlight=16, width=240, height=320, rotation=0, color_order=RGB):
        self.spi = spi
        self.cs = Pin(cs, Pin.OUT, value=1)   # Chip selection pin. Low enable, High disable
        self.dc = Pin(dc, Pin.OUT, value=0)   # dc=0: command, dc=1: data
        self.rst = Pin(rst, Pin.OUT, value=1) # Reset signal is active low
        self.backlight = Pin(backlight, Pin.OUT, value=1)
        self.width = width
        self.height = height
        self.rotation = rotation
        self.color_order = color_order
        self.init()


    def send_command(self, command):
        # Pin objects are callable. The call method provides a shortcut to set and get the value of the pin.
        self.cs(0)
        self.dc(0)
        self.spi.write(bytearray([command]))
        self.cs(1)


    def send_data(self, data):
        self.cs(0)
        self.dc(1)
        self.spi.write(bytearray([data]))
        self.cs(1)


    def hard_reset(self):
        """Hard reset display"""
        self.rst(0)
        time.sleep_ms(50)
        self.rst(1)
        time.sleep_ms(50)


    def soft_reset(self):
        """Soft reset display"""
        self.send_command(0x01) # SWRESET (0x01): Software Reset
        time.sleep_ms(120)


    def init(self):
        """Initialize the lcd register"""
        # Performs a reset
        self.hard_reset()

        # Turn off sleep mode
        self.send_command(0x11) # SLPOUT (0x11): Sleep Out
        time.sleep_ms(120) # wait a little time before sending any new commands

        # Set pixel format
        self.send_command(0x3A) # COLMOD(0x3A): Interface Pixel Format
        self.send_data(0x55)    # 16-bit 65K RGB mode(RGB565)

        self.send_command(0xC6) # FRCTRL2 (0xC6): Frame Rate Control in Normal Mode
        self.send_data(0x01)    # 111Hz

#         self.send_command(0x36) # MADCTL (0x36): Memory Data Access Control
#         self.madctl_value = self.color_order | 0x00
#         self.send_data(self.madctl_value)
        self.set_rotation(self.rotation)

        self.send_command(0x20) # INVOFF (0x20): Display Inversion Off

        # Gamma setting
        # https://baike.baidu.com/item/%E4%BC%BD%E7%8E%9B%E6%A0%A1%E6%AD%A3/7257507?fr=aladdin
        # https://www.bilibili.com/video/av2586864/
        # https://www.zhihu.com/question/27467127
#         self.send_command(0xE0) # PVGAMCTRL (0xE0): Positive Voltage Gamma Control
#         self.send_data(0xD0)
#         self.send_data(0x05)
#         self.send_data(0x09)
#         self.send_data(0x09)
#         self.send_data(0x08)
#         self.send_data(0x14)
#         self.send_data(0x28)
#         self.send_data(0x33)
#         self.send_data(0x3F)
#         self.send_data(0x07)
#         self.send_data(0x13)
#         self.send_data(0x14)
#         self.send_data(0x28)
#         self.send_data(0x30)
#         self.send_command(0XE1) # NVGAMCTRL (0xE1): Negative Voltage Gamma Control
#         self.send_data(0xD0)
#         self.send_data(0x05)
#         self.send_data(0x09)
#         self.send_data(0x09)
#         self.send_data(0x08)
#         self.send_data(0x03)
#         self.send_data(0x24)
#         self.send_data(0x32)
#         self.send_data(0x32)
#         self.send_data(0x3B)
#         self.send_data(0x14)
#         self.send_data(0x13)
#         self.send_data(0x28)
#         self.send_data(0x2F)

        # Turns the display to normal mode (means partial mode off)
        self.send_command(0x13) # NORON (0x13): Normal Display Mode On

        self.send_command(0x29) # DISPON (0x29): Display On


    def set_window(self, Xstart, Ystart, Xend, Yend):
        """
        The address ranges are X=0 to X=239 (Efh) and Y=0 to Y=319 (13Fh).
        Addresses outside these ranges are not allowed. Before writing to the RAM, a window must be defined
        that will be written. The window is programmable via the command registers XS, YS designating the
        start address and XE, YE designating the end address. For example the whole display contents will be
        written, the window is defined by the following values: XS=0 (0h) YS=0 (0h) and XE=239 (Efh), YE=319 (13Fh).
        """
        if (Xstart > 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 <= x < self.width or
            not 0 <= y < self.height or
            not 0 < x + width <= self.width or
            not 0 < y + height <= self.height):
                raise ValueError("out of bounds")
        self.set_window(x, y, x + width - 1, y + height - 1)
        self.cs(0)
        self.dc(1)
        buffer_size  = 256
        limit = min(len(buffer), width * height * 2)
        chunks, rest = divmod(limit, buffer_size)
        if chunks:
            for i in range(chunks):
                self.spi.write(buffer[i*buffer_size: (i+1)*buffer_size])
        if rest:
            self.spi.write(buffer[i*buffer_size: i*buffer_size+rest])
        self.cs(1)


    def rect(self, x: int, y: int, w: int, h: int, color: int):
        """
        Draw a rectangle at the given location, size and color.
        Args:
            x (int): Top left corner x coordinate
            y (int): Top left corner y coordinate
            w (int): Width in pixels
            h (int): Height in pixels
            color (int): 565 encoded color
        """
        self.hline(x, y, w, color)
        self.vline(x, y, h, color)
        self.hline(x, y + h - 1, w, color)
        self.vline(x + w - 1, y, h, color)


    def fill_rect(self, x: int, y: int, width: int, height: int, color: int):
        """
        Draw a rectangle at the given location, size and filled with color.
        Args:
            x (int): Top left corner x coordinate
            y (int): Top left corner y coordinate
            width (int): Width in pixels
            height (int): Height in pixels
            color (int): 565 encoded color
        """
        if (x < self.width-1 and y < self.height-1):
            right = x + width - 1
            bottom = y + height - 1
            if (right > 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 << bi)
                cur_color = color if b else bg_color
                buffer[id] = (cur_color & 0xFF00) >> 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

posted @ 2022-03-27 18:30  XXX已失联  阅读(5351)  评论(2编辑  收藏  举报