樹莓派Pico + MicroPython驅動2.4寸SPI串口屏(ST7789)


  ST7789是一種常用的液晶屏控制芯片(最大支持的分辨率為240×320),可與單片機之間通過SPI通信傳送控制指令或者數據。在MicroPython環境下使用ESP32或者樹莓派Pico可以直接下載st7789_mpy庫預先編譯好的固件firmware來嘗試控制液晶屏,支持的各種單片機開發板如下表所示。這里使用樹莓派Pico,將RP2文件夾中的firmware.uf2固件下載下來,然后按相關教程將其拖放到Pico上進行編程。

Directory File Device
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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM