Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
.vscode
|
||||||
|
.micropico
|
||||||
+409
@@ -0,0 +1,409 @@
|
|||||||
|
"""
|
||||||
|
Threaded bouncing boxes with frame buffer
|
||||||
|
|
||||||
|
Uses a single shot function for second core SPI handler.
|
||||||
|
This cleans itself when the function exits removing the
|
||||||
|
need for a garbage collection call.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from gc import collect
|
||||||
|
collect()
|
||||||
|
|
||||||
|
# import libraries
|
||||||
|
import math
|
||||||
|
import array
|
||||||
|
from machine import Pin, SPI
|
||||||
|
import framebuf
|
||||||
|
from random import random, seed, randint
|
||||||
|
from utime import sleep_us, ticks_cpu, ticks_us
|
||||||
|
import _thread
|
||||||
|
import st7789 as st7789
|
||||||
|
from joystick import Joystick
|
||||||
|
|
||||||
|
# ============================
|
||||||
|
# Helper Functions
|
||||||
|
# ============================
|
||||||
|
def color565(r, g, b):
|
||||||
|
"""Convert RGB888 to RGB565."""
|
||||||
|
return (((g & 0b00011100) << 3) + ((r & 0b11111000) >> 3) << 8) + (b & 0b11111000) + ((g & 0b11100000) >> 5)
|
||||||
|
|
||||||
|
|
||||||
|
RED = color565(0, 0, 255)
|
||||||
|
GREEN = color565(0, 255, 0)
|
||||||
|
YELLOW = color565(0, 255, 255)
|
||||||
|
BLACK = color565(0, 0, 0)
|
||||||
|
WHITE = color565(255, 255, 255)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_display():
|
||||||
|
"""Clear the display."""
|
||||||
|
global fbuf, display, buffer, buffer_width, buffer_height
|
||||||
|
fbuf.fill(BLACK)
|
||||||
|
display.blit_buffer(buffer, 0, 0, buffer_width, buffer_height)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================
|
||||||
|
# Constants and Configuration
|
||||||
|
# ============================
|
||||||
|
SCREEN_WIDTH = 135
|
||||||
|
SCREEN_HEIGHT = 240
|
||||||
|
SCREEN_ROTATION = 1
|
||||||
|
|
||||||
|
PADDLE_WIDTH = 70
|
||||||
|
PADDLE_HEIGHT = 10
|
||||||
|
PADDLE_COLOR = WHITE
|
||||||
|
PADDLE_SPEED = 10
|
||||||
|
|
||||||
|
BRICK_WIDTH = 30
|
||||||
|
BRICK_HEIGHT = 8
|
||||||
|
BRICK_PADDING = 4
|
||||||
|
BRICKS_PER_ROW = 7
|
||||||
|
ROWS = 4
|
||||||
|
|
||||||
|
BALL_SPEED = 3
|
||||||
|
|
||||||
|
SPLASH_WIDTH = 8
|
||||||
|
SPLASH_HEIGHT = 5
|
||||||
|
SPLASH_PADDING = 2
|
||||||
|
|
||||||
|
|
||||||
|
# ============================
|
||||||
|
# set up SPI and display
|
||||||
|
spi = SPI(1,
|
||||||
|
baudrate=31250000,
|
||||||
|
polarity=1,
|
||||||
|
phase=1,
|
||||||
|
bits=8,
|
||||||
|
firstbit=SPI.MSB,
|
||||||
|
sck=Pin(10),
|
||||||
|
mosi=Pin(11))
|
||||||
|
|
||||||
|
display = st7789.ST7789(
|
||||||
|
spi,
|
||||||
|
SCREEN_WIDTH,
|
||||||
|
SCREEN_HEIGHT,
|
||||||
|
reset=Pin(12, Pin.OUT),
|
||||||
|
cs=Pin(9, Pin.OUT),
|
||||||
|
dc=Pin(8, Pin.OUT),
|
||||||
|
backlight=Pin(13, Pin.OUT),
|
||||||
|
rotation=SCREEN_ROTATION)
|
||||||
|
|
||||||
|
|
||||||
|
# FrameBuffer needs 2 bytes for every RGB565 pixel
|
||||||
|
buffer_width = SCREEN_HEIGHT
|
||||||
|
buffer_height = SCREEN_WIDTH + 1
|
||||||
|
buffer_height = 136
|
||||||
|
buffer = bytearray(buffer_width * buffer_height * 2)
|
||||||
|
fbuf = framebuf.FrameBuffer(buffer, buffer_width, buffer_height, framebuf.RGB565)
|
||||||
|
|
||||||
|
render_frame = False
|
||||||
|
|
||||||
|
# ============================
|
||||||
|
# CLASSES
|
||||||
|
# ============================
|
||||||
|
|
||||||
|
class Paddle:
|
||||||
|
def __init__(self):
|
||||||
|
self.x = (SCREEN_HEIGHT - PADDLE_WIDTH) // 2
|
||||||
|
self.y = SCREEN_WIDTH - PADDLE_HEIGHT - 5
|
||||||
|
self.width = PADDLE_WIDTH
|
||||||
|
self.height = PADDLE_HEIGHT
|
||||||
|
self.speed = PADDLE_SPEED
|
||||||
|
|
||||||
|
def move(self, direction: int):
|
||||||
|
"""
|
||||||
|
Move paddle left or right.
|
||||||
|
Args: direction: -1 for left, 1 for right
|
||||||
|
"""
|
||||||
|
self.x += self.speed * direction
|
||||||
|
if self.x < 0:
|
||||||
|
self.x = 0
|
||||||
|
elif self.x > SCREEN_HEIGHT - self.width:
|
||||||
|
self.x = SCREEN_HEIGHT - self.width
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
"""Draw paddle."""
|
||||||
|
global fbuf
|
||||||
|
fbuf.fill_rect(self.x, self.y, self.width, self.height, PADDLE_COLOR)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Update paddle position."""
|
||||||
|
global joystick
|
||||||
|
if joystick.joy_left() == 0:
|
||||||
|
self.move(-1)
|
||||||
|
elif joystick.joy_right() == 0:
|
||||||
|
self.move(1)
|
||||||
|
self.draw()
|
||||||
|
|
||||||
|
def hit(self, ball: Ball) -> bool:
|
||||||
|
"""Check if the ball hits the paddle."""
|
||||||
|
return (
|
||||||
|
self.x < ball.x < self.x + self.width
|
||||||
|
and self.y < ball.y < self.y + self.height)
|
||||||
|
|
||||||
|
|
||||||
|
class Ball:
|
||||||
|
def __init__(self, paddle: Paddle, radius: int, color: int):
|
||||||
|
"""
|
||||||
|
Initialize the ball.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
paddle (Paddle): The paddle object to position the ball on.
|
||||||
|
radius (int): Radius of the ball.
|
||||||
|
color (int): RGB565 color value of the ball.
|
||||||
|
"""
|
||||||
|
self.radius = radius
|
||||||
|
self.color = color
|
||||||
|
self.reset_pos(paddle)
|
||||||
|
self.x_speed = BALL_SPEED
|
||||||
|
self.y_speed = -BALL_SPEED
|
||||||
|
|
||||||
|
# Position the ball in the middle of the paddle
|
||||||
|
self.x = paddle.x + (paddle.width // 2)
|
||||||
|
self.y = paddle.y - radius - 2 # Place the ball just above the paddle
|
||||||
|
|
||||||
|
def reset_pos(self, paddle: Paddle):
|
||||||
|
"""Reset ball position to the center of the screen.
|
||||||
|
Args: Paddle: The paddle object to position the ball on.
|
||||||
|
"""
|
||||||
|
self.x = SCREEN_HEIGHT // 2
|
||||||
|
self.y = SCREEN_WIDTH // 2
|
||||||
|
self.x_speed = BALL_SPEED
|
||||||
|
self.y_speed = -BALL_SPEED
|
||||||
|
|
||||||
|
def update_pos(self):
|
||||||
|
"""Update ball position."""
|
||||||
|
self.x += self.x_speed
|
||||||
|
self.y += self.y_speed
|
||||||
|
|
||||||
|
# Bounce off left or right screen edge
|
||||||
|
if self.x < 0:
|
||||||
|
self.x = 0
|
||||||
|
self.x_speed = -self.x_speed
|
||||||
|
elif self.x > SCREEN_HEIGHT:
|
||||||
|
self.x = SCREEN_HEIGHT
|
||||||
|
self.x_speed = -self.x_speed
|
||||||
|
|
||||||
|
# Bounce off top screen edge
|
||||||
|
if self.y < BRICK_PADDING + self.radius:
|
||||||
|
self.y = BRICK_PADDING + self.radius
|
||||||
|
self.y_speed = -self.y_speed
|
||||||
|
|
||||||
|
# Drop through bottom screen edge & return True to indicate we lose a life
|
||||||
|
if self.y > SCREEN_WIDTH:
|
||||||
|
self.y = SCREEN_WIDTH
|
||||||
|
self.y_speed = -self.y_speed
|
||||||
|
return True
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
"""Draw ball."""
|
||||||
|
global fbuf
|
||||||
|
fbuf.ellipse(self.x, self.y, self.radius, self.radius, self.color, True)
|
||||||
|
|
||||||
|
|
||||||
|
class Brick:
|
||||||
|
def __init__(self, x: int, y: int, width: int, height: int, color: int):
|
||||||
|
"""
|
||||||
|
Initialize a brick.
|
||||||
|
Args: x (int): x-coordinate of the brick.
|
||||||
|
y (int): y-coordinate of the brick.
|
||||||
|
width (int): width of the brick.
|
||||||
|
height (int): height of the brick.
|
||||||
|
color (int): color of the brick."""
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.color = color
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
"""Draw brick."""
|
||||||
|
global fbuf
|
||||||
|
fbuf.fill_rect(self.x, self.y, self.width, self.height, self.color)
|
||||||
|
|
||||||
|
|
||||||
|
class BrickRow:
|
||||||
|
def __init__(self, brick_width: int, brick_height: int, padding:int, offset_top: int, color: int):
|
||||||
|
"""
|
||||||
|
Initialize a row of bricks.
|
||||||
|
Args: brick_width (int): width of each brick.
|
||||||
|
brick_height (int): height of each brick.
|
||||||
|
padding (int): padding between bricks.
|
||||||
|
offset_top (int): y-coordinate of the top of the row.
|
||||||
|
color (int): color of the bricks.
|
||||||
|
"""
|
||||||
|
self.brick_width = brick_width
|
||||||
|
self.brick_height = brick_height
|
||||||
|
self.color = color
|
||||||
|
self.padding = padding
|
||||||
|
self.offset_top = offset_top
|
||||||
|
self.bricks = [Brick(padding + i * (brick_width + padding), offset_top, brick_width, brick_height, color) for i in range(BRICKS_PER_ROW)]
|
||||||
|
self.brick_x = [padding + i * (brick_width + padding) for i in range(BRICKS_PER_ROW)]
|
||||||
|
self.brick_y = [offset_top] * BRICKS_PER_ROW
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
global fbuf
|
||||||
|
for brick in self.bricks:
|
||||||
|
if brick is not None:
|
||||||
|
brick.draw()
|
||||||
|
|
||||||
|
def hit(self, ball: Ball) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the ball hits any brick in the row and remove it if hit.
|
||||||
|
Args: ball (Ball): The ball object to check for collision.
|
||||||
|
Returns: bool: True if the ball hits a brick, False otherwise.
|
||||||
|
"""
|
||||||
|
for i, brick in enumerate(self.bricks):
|
||||||
|
if brick is not None:
|
||||||
|
if (brick.x < ball.x < brick.x + brick.width) and (brick.y < ball.y < brick.y + brick.height):
|
||||||
|
# Remove the brick by setting it to None
|
||||||
|
self.bricks[i] = None
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def splash_screen(data_rows: list[int]):
|
||||||
|
"""
|
||||||
|
Display a splash screen using the bits in the data_rows.
|
||||||
|
Args: data_rows (list[int]): List of hex values to display as blocks.
|
||||||
|
"""
|
||||||
|
global fbuf, buffer, buffer_width, buffer_height, joystick, render_frame
|
||||||
|
fbuf.fill(BLACK)
|
||||||
|
|
||||||
|
start_x = 5
|
||||||
|
start_y = 20
|
||||||
|
|
||||||
|
for row_index, hex_value in enumerate(data_rows):
|
||||||
|
binary = bin(hex_value)[2:] # Convert to binary
|
||||||
|
binary = '{:0>22}'.format(binary) # Pad to 22 columns
|
||||||
|
if 0 <= row_index <= 1:
|
||||||
|
color = RED
|
||||||
|
elif 2 <= row_index <= 4:
|
||||||
|
color = YELLOW
|
||||||
|
else:
|
||||||
|
color = GREEN
|
||||||
|
for bit_index, bit in enumerate(binary):
|
||||||
|
if bit == '1': # Only draw a block for '1'
|
||||||
|
x = start_x + bit_index * (SPLASH_WIDTH + SPLASH_PADDING)
|
||||||
|
y = start_y + row_index * (SPLASH_WIDTH + SPLASH_PADDING)
|
||||||
|
fbuf.fill_rect(x, y, SPLASH_WIDTH, SPLASH_HEIGHT, color)
|
||||||
|
fbuf.text("Press A to start", 5, 100, WHITE)
|
||||||
|
fbuf.text("Press B to exit", 5, 120, WHITE)
|
||||||
|
|
||||||
|
# Wait for the frame to be rendered & update the display
|
||||||
|
while render_frame:
|
||||||
|
pass
|
||||||
|
display.blit_buffer(buffer, 0, 0, buffer_width, buffer_height)
|
||||||
|
|
||||||
|
|
||||||
|
def main_loop():
|
||||||
|
global fbuf, buffer, buffer_width, buffer_height, joystick
|
||||||
|
global render_frame
|
||||||
|
|
||||||
|
bricks = []
|
||||||
|
for row in range(ROWS):
|
||||||
|
if row == 0:
|
||||||
|
color = RED
|
||||||
|
elif row == 1:
|
||||||
|
color = YELLOW
|
||||||
|
else:
|
||||||
|
color = GREEN
|
||||||
|
bricks.append(
|
||||||
|
BrickRow(BRICK_WIDTH,
|
||||||
|
BRICK_HEIGHT,
|
||||||
|
BRICK_PADDING,
|
||||||
|
10 + row * (BRICK_HEIGHT + BRICK_PADDING),
|
||||||
|
color))
|
||||||
|
score = 0
|
||||||
|
lives = 3
|
||||||
|
|
||||||
|
paddle = Paddle()
|
||||||
|
ball = Ball(paddle, radius=5, color=WHITE)
|
||||||
|
# Create a list of small balls to represent lives
|
||||||
|
lives_balls = []
|
||||||
|
for i in range(0, lives):
|
||||||
|
life_ball = Ball(paddle, radius=3, color=WHITE)
|
||||||
|
life_ball.x = 5 + (i - 1) * 7
|
||||||
|
life_ball.y = 7
|
||||||
|
life_ball.x_speed = 0
|
||||||
|
lives_balls.append(life_ball)
|
||||||
|
|
||||||
|
render_frame = False
|
||||||
|
state = 0 # 0 = start screen, 1 = game, 2 = game over, 3 = game win
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if state == 0: # Startup screen
|
||||||
|
splash_screen([0x060046, 0x056B54, 0x054A64, 0x064A46, 0x054A62, 0x054A52, 0x074B56])
|
||||||
|
|
||||||
|
if joystick.button_a() == 0: # Transition to game state when A is pressed
|
||||||
|
state = 1
|
||||||
|
lives = 3
|
||||||
|
score = 0
|
||||||
|
|
||||||
|
elif state == 1 and lives > 0 and score < 28: # Game state
|
||||||
|
paddle.update()
|
||||||
|
if ball.update_pos(): # If ball is out of bounds, lose a life and reset ball position
|
||||||
|
lives -= 1
|
||||||
|
lives_balls.pop()
|
||||||
|
ball.reset_pos(paddle) # Reset ball position to the center of the screen
|
||||||
|
|
||||||
|
if paddle.hit(ball):
|
||||||
|
ball.y_speed = -ball.y_speed
|
||||||
|
for row in bricks:
|
||||||
|
if row.hit(ball):
|
||||||
|
ball.y_speed = -ball.y_speed
|
||||||
|
score += 1
|
||||||
|
break
|
||||||
|
for i in range(1, len(lives_balls)):
|
||||||
|
lives_balls[i].draw()
|
||||||
|
for row in bricks:
|
||||||
|
row.draw()
|
||||||
|
ball.draw()
|
||||||
|
paddle.draw()
|
||||||
|
|
||||||
|
while render_frame:
|
||||||
|
pass
|
||||||
|
render_frame = True
|
||||||
|
# Start SPI handler on core 1
|
||||||
|
spi_thread = _thread.start_new_thread(render_thread, ())
|
||||||
|
|
||||||
|
if state == 1 and (lives == 0 or score == 28): # Game over or win:
|
||||||
|
if lives > 0:
|
||||||
|
state= 3 # Winning state
|
||||||
|
else:
|
||||||
|
state = 2 # Losing state
|
||||||
|
if state == 2: # Game over screen
|
||||||
|
splash_screen([0x0276DC, 0x025490, 0x025494, 0x0256DC, 0x025298, 0x025294, 0x0376D4])
|
||||||
|
|
||||||
|
if state == 3: # Game win screen
|
||||||
|
splash_screen([0x04548, 0x04548, 0x04568, 0x05578, 0x05558, 0x05548, 0x03948])
|
||||||
|
|
||||||
|
if state != 1 and joystick.button_a() == 0: # Transition to start state when A is pressed
|
||||||
|
state = 0
|
||||||
|
sleep_us(1_000_000) # Debounce delay
|
||||||
|
|
||||||
|
if joystick.button_b() == 0: # Exit game when B is pressed
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def render_thread():
|
||||||
|
global fbuf, buffer, buffer_width, buffer_height, render_frame, spi
|
||||||
|
global display, SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_ROTATION
|
||||||
|
|
||||||
|
display.blit_buffer(buffer, 0, 0, buffer_width, buffer_height)
|
||||||
|
fbuf.fill(0)
|
||||||
|
|
||||||
|
render_frame = False
|
||||||
|
# thread will exit and self clean removing need for garbage collection
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
joystick = Joystick()
|
||||||
|
main_loop()
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
clear_display()
|
||||||
|
buffer = None
|
||||||
|
fbuf = None
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
from machine import Pin
|
||||||
|
class Joystick:
|
||||||
|
def __init__(self):
|
||||||
|
# Map buttons
|
||||||
|
self.button_a = Pin(15, Pin.IN, Pin.PULL_UP)
|
||||||
|
self.button_b = Pin(17, Pin.IN, Pin.PULL_UP)
|
||||||
|
# Map joystick
|
||||||
|
self.joy_up = Pin(2,Pin.IN, Pin.PULL_UP)
|
||||||
|
self.joy_down = Pin(18,Pin.IN, Pin.PULL_UP)
|
||||||
|
self.joy_left = Pin(16 ,Pin.IN, Pin.PULL_UP)
|
||||||
|
self.joy_right = Pin(20 ,Pin.IN, Pin.PULL_UP)
|
||||||
|
self.joy_click = Pin(3, Pin.IN, Pin.PULL_UP)
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2020, 2021 Russ Hughes
|
||||||
|
|
||||||
|
This file incorporates work covered by the following copyright and
|
||||||
|
permission notice and is licensed under the same terms:
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2019 Ivan Belokobylskiy
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
The driver is based on devbis' st7789py_mpy module from
|
||||||
|
https://github.com/devbis/st7789py_mpy.
|
||||||
|
|
||||||
|
This driver adds support for:
|
||||||
|
|
||||||
|
- 320x240, 240x240 and 135x240 pixel displays
|
||||||
|
- Display rotation
|
||||||
|
- Hardware based scrolling
|
||||||
|
- Drawing text using 8 and 16 bit wide bitmap fonts with heights that are
|
||||||
|
multiples of 8. Included are 12 bitmap fonts derived from classic pc
|
||||||
|
BIOS text mode fonts.
|
||||||
|
- Drawing text using converted TrueType fonts.
|
||||||
|
- Drawing converted bitmaps
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from micropython import const
|
||||||
|
import ustruct as struct
|
||||||
|
|
||||||
|
# commands
|
||||||
|
ST7789_NOP = const(0x00)
|
||||||
|
ST7789_SWRESET = const(0x01)
|
||||||
|
ST7789_RDDID = const(0x04)
|
||||||
|
ST7789_RDDST = const(0x09)
|
||||||
|
|
||||||
|
ST7789_SLPIN = const(0x10)
|
||||||
|
ST7789_SLPOUT = const(0x11)
|
||||||
|
ST7789_PTLON = const(0x12)
|
||||||
|
ST7789_NORON = const(0x13)
|
||||||
|
|
||||||
|
ST7789_INVOFF = const(0x20)
|
||||||
|
ST7789_INVON = const(0x21)
|
||||||
|
ST7789_DISPOFF = const(0x28)
|
||||||
|
ST7789_DISPON = const(0x29)
|
||||||
|
ST7789_CASET = const(0x2A)
|
||||||
|
ST7789_RASET = const(0x2B)
|
||||||
|
ST7789_RAMWR = const(0x2C)
|
||||||
|
ST7789_RAMRD = const(0x2E)
|
||||||
|
|
||||||
|
ST7789_PTLAR = const(0x30)
|
||||||
|
ST7789_VSCRDEF = const(0x33)
|
||||||
|
ST7789_COLMOD = const(0x3A)
|
||||||
|
ST7789_MADCTL = const(0x36)
|
||||||
|
ST7789_VSCSAD = const(0x37)
|
||||||
|
|
||||||
|
ST7789_MADCTL_MY = const(0x80)
|
||||||
|
ST7789_MADCTL_MX = const(0x40)
|
||||||
|
ST7789_MADCTL_MV = const(0x20)
|
||||||
|
ST7789_MADCTL_ML = const(0x10)
|
||||||
|
ST7789_MADCTL_BGR = const(0x08)
|
||||||
|
ST7789_MADCTL_MH = const(0x04)
|
||||||
|
ST7789_MADCTL_RGB = const(0x00)
|
||||||
|
|
||||||
|
ST7789_RDID1 = const(0xDA)
|
||||||
|
ST7789_RDID2 = const(0xDB)
|
||||||
|
ST7789_RDID3 = const(0xDC)
|
||||||
|
ST7789_RDID4 = const(0xDD)
|
||||||
|
|
||||||
|
COLOR_MODE_65K = const(0x50)
|
||||||
|
COLOR_MODE_262K = const(0x60)
|
||||||
|
COLOR_MODE_12BIT = const(0x03)
|
||||||
|
COLOR_MODE_16BIT = const(0x05)
|
||||||
|
COLOR_MODE_18BIT = const(0x06)
|
||||||
|
COLOR_MODE_16M = const(0x07)
|
||||||
|
|
||||||
|
# Color definitions
|
||||||
|
BLACK = const(0x0000)
|
||||||
|
BLUE = const(0x001F)
|
||||||
|
RED = const(0xF800)
|
||||||
|
GREEN = const(0x07E0)
|
||||||
|
CYAN = const(0x07FF)
|
||||||
|
MAGENTA = const(0xF81F)
|
||||||
|
YELLOW = const(0xFFE0)
|
||||||
|
WHITE = const(0xFFFF)
|
||||||
|
|
||||||
|
_ENCODE_PIXEL = ">H"
|
||||||
|
_ENCODE_POS = ">HH"
|
||||||
|
_DECODE_PIXEL = ">BBB"
|
||||||
|
|
||||||
|
_BUFFER_SIZE = const(256)
|
||||||
|
|
||||||
|
_BIT7 = const(0x80)
|
||||||
|
_BIT6 = const(0x40)
|
||||||
|
_BIT5 = const(0x20)
|
||||||
|
_BIT4 = const(0x10)
|
||||||
|
_BIT3 = const(0x08)
|
||||||
|
_BIT2 = const(0x04)
|
||||||
|
_BIT1 = const(0x02)
|
||||||
|
_BIT0 = const(0x01)
|
||||||
|
|
||||||
|
# Rotation tables (width, height, xstart, ystart)[rotation % 4]
|
||||||
|
|
||||||
|
WIDTH_320 = [(240, 320, 0, 0),
|
||||||
|
(320, 240, 0, 0),
|
||||||
|
(240, 320, 0, 0),
|
||||||
|
(320, 240, 0, 0)]
|
||||||
|
|
||||||
|
WIDTH_240 = [(240, 240, 0, 0),
|
||||||
|
(240, 240, 0, 0),
|
||||||
|
(240, 240, 0, 80),
|
||||||
|
(240, 240, 80, 0)]
|
||||||
|
|
||||||
|
WIDTH_135 = [(135, 240, 52, 40),
|
||||||
|
(240, 135, 40, 53),
|
||||||
|
(135, 240, 53, 40),
|
||||||
|
(240, 135, 40, 52)]
|
||||||
|
|
||||||
|
# MADCTL ROTATIONS[rotation % 4]
|
||||||
|
ROTATIONS = [0x00, 0x60, 0xc0, 0xa0]
|
||||||
|
|
||||||
|
|
||||||
|
def color565(red, green=0, blue=0):
|
||||||
|
"""
|
||||||
|
Convert red, green and blue values (0-255) into a 16-bit 565 encoding.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
red, green, blue = red # see if the first var is a tuple/list
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
return (red & 0xf8) << 8 | (green & 0xfc) << 3 | blue >> 3
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_pos(x, y):
|
||||||
|
"""Encode a postion into bytes."""
|
||||||
|
return struct.pack(_ENCODE_POS, x, y)
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_pixel(color):
|
||||||
|
"""Encode a pixel color into bytes."""
|
||||||
|
return struct.pack(_ENCODE_PIXEL, color)
|
||||||
|
|
||||||
|
|
||||||
|
class ST7789():
|
||||||
|
"""
|
||||||
|
ST7789 driver class
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spi (spi): spi object **Required**
|
||||||
|
width (int): display width **Required**
|
||||||
|
height (int): display height **Required**
|
||||||
|
reset (pin): reset pin
|
||||||
|
dc (pin): dc pin **Required**
|
||||||
|
cs (pin): cs pin
|
||||||
|
backlight(pin): backlight pin
|
||||||
|
rotation (int): display rotation
|
||||||
|
- 0-Portrait
|
||||||
|
- 1-Landscape
|
||||||
|
- 2-Inverted Portrait
|
||||||
|
- 3-Inverted Landscape
|
||||||
|
"""
|
||||||
|
def __init__(self, spi, width, height, reset=None, dc=None,
|
||||||
|
cs=None, backlight=None, rotation=0):
|
||||||
|
"""
|
||||||
|
Initialize display.
|
||||||
|
"""
|
||||||
|
if height != 240 or width not in [320, 240, 135]:
|
||||||
|
raise ValueError(
|
||||||
|
"Unsupported display. 320x240, 240x240 and 135x240 are supported."
|
||||||
|
)
|
||||||
|
|
||||||
|
if dc is None:
|
||||||
|
raise ValueError("dc pin is required.")
|
||||||
|
|
||||||
|
self._display_width = self.width = width
|
||||||
|
self._display_height = self.height = height
|
||||||
|
self.xstart = 0
|
||||||
|
self.ystart = 0
|
||||||
|
self.spi = spi
|
||||||
|
self.reset = reset
|
||||||
|
self.dc = dc
|
||||||
|
self.cs = cs
|
||||||
|
self.backlight = backlight
|
||||||
|
self._rotation = rotation % 4
|
||||||
|
|
||||||
|
self.hard_reset()
|
||||||
|
self.soft_reset()
|
||||||
|
self.sleep_mode(False)
|
||||||
|
|
||||||
|
self._set_color_mode(COLOR_MODE_65K | COLOR_MODE_16BIT)
|
||||||
|
time.sleep_ms(50)
|
||||||
|
self.rotation(self._rotation)
|
||||||
|
self.inversion_mode(True)
|
||||||
|
time.sleep_ms(10)
|
||||||
|
self._write(ST7789_NORON)
|
||||||
|
time.sleep_ms(10)
|
||||||
|
if backlight is not None:
|
||||||
|
backlight.value(1)
|
||||||
|
self._write(ST7789_DISPON)
|
||||||
|
time.sleep_ms(500)
|
||||||
|
|
||||||
|
def _write(self, command=None, data=None):
|
||||||
|
"""SPI write to the device: commands and data."""
|
||||||
|
if self.cs:
|
||||||
|
self.cs.off()
|
||||||
|
|
||||||
|
if command is not None:
|
||||||
|
self.dc.off()
|
||||||
|
self.spi.write(bytes([command]))
|
||||||
|
if data is not None:
|
||||||
|
self.dc.on()
|
||||||
|
self.spi.write(data)
|
||||||
|
if self.cs:
|
||||||
|
self.cs.on()
|
||||||
|
|
||||||
|
def hard_reset(self):
|
||||||
|
"""
|
||||||
|
Hard reset display.
|
||||||
|
"""
|
||||||
|
if self.cs:
|
||||||
|
self.cs.off()
|
||||||
|
if self.reset:
|
||||||
|
self.reset.on()
|
||||||
|
time.sleep_ms(50)
|
||||||
|
if self.reset:
|
||||||
|
self.reset.off()
|
||||||
|
time.sleep_ms(50)
|
||||||
|
if self.reset:
|
||||||
|
self.reset.on()
|
||||||
|
time.sleep_ms(150)
|
||||||
|
if self.cs:
|
||||||
|
self.cs.on()
|
||||||
|
|
||||||
|
def soft_reset(self):
|
||||||
|
"""
|
||||||
|
Soft reset display.
|
||||||
|
"""
|
||||||
|
self._write(ST7789_SWRESET)
|
||||||
|
time.sleep_ms(150)
|
||||||
|
|
||||||
|
def sleep_mode(self, value):
|
||||||
|
"""
|
||||||
|
Enable or disable display sleep mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (bool): if True enable sleep mode. if False disable sleep
|
||||||
|
mode
|
||||||
|
"""
|
||||||
|
if value:
|
||||||
|
self._write(ST7789_SLPIN)
|
||||||
|
else:
|
||||||
|
self._write(ST7789_SLPOUT)
|
||||||
|
|
||||||
|
def inversion_mode(self, value):
|
||||||
|
"""
|
||||||
|
Enable or disable display inversion mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (bool): if True enable inversion mode. if False disable
|
||||||
|
inversion mode
|
||||||
|
"""
|
||||||
|
if value:
|
||||||
|
self._write(ST7789_INVON)
|
||||||
|
else:
|
||||||
|
self._write(ST7789_INVOFF)
|
||||||
|
|
||||||
|
def _set_color_mode(self, mode):
|
||||||
|
"""
|
||||||
|
Set display color mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode (int): color mode
|
||||||
|
COLOR_MODE_65K, COLOR_MODE_262K, COLOR_MODE_12BIT,
|
||||||
|
COLOR_MODE_16BIT, COLOR_MODE_18BIT, COLOR_MODE_16M
|
||||||
|
"""
|
||||||
|
self._write(ST7789_COLMOD, bytes([mode & 0x77]))
|
||||||
|
|
||||||
|
def rotation(self, rotation):
|
||||||
|
"""
|
||||||
|
Set display rotation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rotation (int):
|
||||||
|
- 0-Portrait
|
||||||
|
- 1-Landscape
|
||||||
|
- 2-Inverted Portrait
|
||||||
|
- 3-Inverted Landscape
|
||||||
|
"""
|
||||||
|
|
||||||
|
rotation %= 4
|
||||||
|
self._rotation = rotation
|
||||||
|
madctl = ROTATIONS[rotation]
|
||||||
|
|
||||||
|
if self._display_width == 320:
|
||||||
|
table = WIDTH_320
|
||||||
|
elif self._display_width == 240:
|
||||||
|
table = WIDTH_240
|
||||||
|
elif self._display_width == 135:
|
||||||
|
table = WIDTH_135
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Unsupported display. 320x240, 240x240 and 135x240 are supported."
|
||||||
|
)
|
||||||
|
|
||||||
|
self.width, self.height, self.xstart, self.ystart = table[rotation]
|
||||||
|
self._write(ST7789_MADCTL, bytes([madctl]))
|
||||||
|
|
||||||
|
def _set_columns(self, start, end):
|
||||||
|
"""
|
||||||
|
Send CASET (column address set) command to display.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start (int): column start address
|
||||||
|
end (int): column end address
|
||||||
|
"""
|
||||||
|
if start <= end <= self.width:
|
||||||
|
self._write(ST7789_CASET, _encode_pos(
|
||||||
|
start+self.xstart, end + self.xstart))
|
||||||
|
|
||||||
|
def _set_rows(self, start, end):
|
||||||
|
"""
|
||||||
|
Send RASET (row address set) command to display.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start (int): row start address
|
||||||
|
end (int): row end address
|
||||||
|
"""
|
||||||
|
if start <= end <= self.height:
|
||||||
|
self._write(ST7789_RASET, _encode_pos(
|
||||||
|
start+self.ystart, end+self.ystart))
|
||||||
|
|
||||||
|
def _set_window(self, x0, y0, x1, y1):
|
||||||
|
"""
|
||||||
|
Set window to column and row address.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x0 (int): column start address
|
||||||
|
y0 (int): row start address
|
||||||
|
x1 (int): column end address
|
||||||
|
y1 (int): row end address
|
||||||
|
"""
|
||||||
|
self._set_columns(x0, x1)
|
||||||
|
self._set_rows(y0, y1)
|
||||||
|
self._write(ST7789_RAMWR)
|
||||||
|
|
||||||
|
|
||||||
|
def blit_buffer(self, buffer, x, y, width, height):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
self._set_window(x, y, x + width - 1, y + height - 1)
|
||||||
|
self._write(None, buffer)
|
||||||
Reference in New Issue
Block a user