Initial commit

This commit is contained in:
2025-04-03 06:57:23 +02:00
commit aa96f89a13
5 changed files with 801 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
.vscode
.micropico
+409
View File
@@ -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
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
exec(open("breakout.py").read(), globals())
+377
View File
@@ -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)