Merge pull request #27 from adafruit/make-package
Allow defining geometries (including more than 2 color lanes) via Python code
This commit is contained in:
commit
8d3355fca3
29 changed files with 628 additions and 199 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,3 @@
|
|||
/*.pio.h
|
||||
protodemo
|
||||
/build
|
||||
*.egg-info
|
||||
|
|
|
|||
8
docs/adafruit_blinka_raspberry_pi5_piomatter.rst
Normal file
8
docs/adafruit_blinka_raspberry_pi5_piomatter.rst
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
HUB75 matrix driver for Raspberry Pi 5 using PIO
|
||||
------------------------------------------------
|
||||
|
||||
.. autosummary::
|
||||
:toctree: _generate
|
||||
:recursive:
|
||||
|
||||
adafruit_blinka_raspberry_pi5_piomatter
|
||||
|
|
@ -1 +0,0 @@
|
|||
.. automodule:: adafruit_blinka_raspberry_pi5_piomatter
|
||||
20
docs/conf.py
20
docs/conf.py
|
|
@ -26,13 +26,27 @@
|
|||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"autoapi.extension",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.autosummary",
|
||||
"sphinx.ext.napoleon",
|
||||
]
|
||||
|
||||
autosummary_generate = True
|
||||
autoapi_keep_files = True
|
||||
autoapi_dirs = ["../src/adafruit_blinka_raspberry_pi5_piomatter"]
|
||||
autoapi_add_toctree_entry = True
|
||||
autoapi_options = [
|
||||
"members",
|
||||
"undoc-members",
|
||||
"show-inheritance",
|
||||
"special-members",
|
||||
"show-module-summary",
|
||||
]
|
||||
|
||||
autoapi_python_class_content = "both"
|
||||
autoapi_python_use_implicit_namespaces = True
|
||||
autoapi_template_dir = "autoapi/templates"
|
||||
autoapi_root = "api"
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
|
@ -50,7 +64,7 @@ master_doc = "index"
|
|||
|
||||
# General information about the project.
|
||||
project = "adafruit-blinka-pi5-piomatter"
|
||||
copyright = "2023 Jeff Epler"
|
||||
copyright = "2025 Jeff Epler"
|
||||
author = "Jeff Epler"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
sphinx
|
||||
sphinx-autoapi
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-jquery
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@ For help with commandline arguments, run `python fbmirror.py --help`
|
|||
"""
|
||||
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import click
|
||||
import numpy as np
|
||||
import piomatter_click
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import adafruit_blinka_raspberry_pi5_piomatter.click as piomatter_click
|
||||
from adafruit_blinka_raspberry_pi5_piomatter.pixelmappers import simple_multilane_mapper
|
||||
|
||||
with open("/sys/class/graphics/fb0/virtual_size") as f:
|
||||
screenx, screeny = [int(word) for word in f.read().split(",")]
|
||||
|
|
@ -33,12 +35,31 @@ with open("/sys/class/graphics/fb0/stride") as f:
|
|||
|
||||
linux_framebuffer = np.memmap('/dev/fb0',mode='r', shape=(screeny, stride // bytes_per_pixel), dtype=dtype)
|
||||
|
||||
def make_pixelmap_multilane(width, height, n_addr_lines, n_lanes):
|
||||
calc_height = n_lanes << n_addr_lines
|
||||
if height != calc_height:
|
||||
raise RuntimeError(f"Calculated height {calc_height} does not match requested height {height}")
|
||||
n_addr = 1 << n_addr_lines
|
||||
|
||||
m = []
|
||||
for addr in range(n_addr):
|
||||
for x in range(width):
|
||||
for lane in range(n_lanes):
|
||||
y = addr + lane * n_addr
|
||||
m.append(x + width * y)
|
||||
print(m)
|
||||
return m
|
||||
|
||||
@click.command
|
||||
@click.option("--x-offset", "xoffset", type=int, help="The x offset of top left corner of the region to mirror", default=0)
|
||||
@click.option("--y-offset", "yoffset", type=int, help="The y offset of top left corner of the region to mirror", default=0)
|
||||
@piomatter_click.standard_options
|
||||
def main(xoffset, yoffset, width, height, serpentine, rotation, pinout, n_planes, n_addr_lines):
|
||||
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, rotation=rotation)
|
||||
def main(xoffset, yoffset, width, height, serpentine, rotation, pinout, n_planes, n_temporal_planes, n_addr_lines, n_lanes):
|
||||
if n_lanes != 2:
|
||||
pixelmap = simple_multilane_mapper(width, height, n_addr_lines, n_lanes)
|
||||
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, n_temporal_planes=n_temporal_planes, n_lanes=n_lanes, map=pixelmap)
|
||||
else:
|
||||
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, n_temporal_planes=n_temporal_planes, rotation=rotation)
|
||||
framebuffer = np.zeros(shape=(geometry.height, geometry.width), dtype=dtype)
|
||||
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB565, pinout=pinout, framebuffer=framebuffer, geometry=geometry)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,11 +37,13 @@ or if `/boot/firmware/cmdline.txt` specifies a resolution such as
|
|||
`... video=HDMI-A-1:640x480M@60D`.
|
||||
"""
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import click
|
||||
import numpy as np
|
||||
import PIL.Image as Image
|
||||
import piomatter_click
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import adafruit_blinka_raspberry_pi5_piomatter.click as piomatter_click
|
||||
from adafruit_blinka_raspberry_pi5_piomatter.pixelmappers import simple_multilane_mapper
|
||||
|
||||
with open("/sys/class/graphics/fb0/virtual_size") as f:
|
||||
screenx, screeny = [int(word) for word in f.read().split(",")]
|
||||
|
|
@ -65,8 +67,12 @@ linux_framebuffer = np.memmap('/dev/fb0',mode='r', shape=(screeny, stride // byt
|
|||
@click.option("--y-offset", "yoffset", type=int, help="The y offset of top left corner of the region to mirror", default=0)
|
||||
@click.option("--scale", "scale", type=int, help="The scale factor to reduce the display down by.", default=3)
|
||||
@piomatter_click.standard_options
|
||||
def main(xoffset, yoffset, scale, width, height, serpentine, rotation, pinout, n_planes, n_addr_lines):
|
||||
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, rotation=rotation)
|
||||
def main(xoffset, yoffset, scale, width, height, serpentine, rotation, pinout, n_planes, n_temporal_planes, n_addr_lines, n_lanes):
|
||||
if n_lanes != 2:
|
||||
pixelmap = simple_multilane_mapper(width, height, n_addr_lines, n_lanes)
|
||||
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, n_temporal_planes=n_temporal_planes, n_lanes=n_lanes, map=pixelmap)
|
||||
else:
|
||||
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_temporal_planes=n_temporal_planes, n_addr_lines=n_addr_lines, rotation=rotation)
|
||||
matrix_framebuffer = np.zeros(shape=(geometry.height, geometry.width, 3), dtype=np.uint8)
|
||||
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=pinout, framebuffer=matrix_framebuffer, geometry=geometry)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ The animated gif is played repeatedly until interrupted with ctrl-c.
|
|||
|
||||
import time
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import numpy as np
|
||||
import PIL.Image as Image
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
|
||||
width = 64
|
||||
height = 32
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ import glob
|
|||
import sys
|
||||
import time
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import numpy as np
|
||||
import PIL.Image as Image
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
|
||||
images = sorted(glob.glob(sys.argv[1]))
|
||||
|
||||
geometry = piomatter.Geometry(width=64, height=32, n_addr_lines=4, rotation=piomatter.Orientation.Normal)
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@ $ python quote_scroller.py
|
|||
|
||||
"""
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import numpy as np
|
||||
import requests
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
|
||||
# 128px for 2x1 matrices. Change to 64 if you're using a single matrix.
|
||||
total_width = 128
|
||||
total_height = 32
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ Run like this:
|
|||
$ python rainbow_spiral.py
|
||||
|
||||
"""
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import numpy as np
|
||||
import rainbowio
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
|
||||
width = 64
|
||||
height = 32
|
||||
pen_radius = 1
|
||||
|
|
|
|||
114
examples/rainbow_spiral_active3.py
Normal file
114
examples/rainbow_spiral_active3.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/python3
|
||||
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
Display a spiral around the display drawn with a rainbow color.
|
||||
|
||||
Run like this:
|
||||
|
||||
$ python rainbow_spiral.py
|
||||
|
||||
"""
|
||||
import numpy as np
|
||||
import rainbowio
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
from adafruit_blinka_raspberry_pi5_piomatter.pixelmappers import simple_multilane_mapper
|
||||
|
||||
width = 64
|
||||
n_lanes = 6
|
||||
n_addr_lines = 5
|
||||
height = n_lanes << n_addr_lines
|
||||
pen_radius = 1
|
||||
|
||||
canvas = Image.new('RGB', (width, height), (0, 0, 0))
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
|
||||
pixelmap = simple_multilane_mapper(width, height, n_addr_lines, n_lanes)
|
||||
geometry = piomatter.Geometry(width=width, height=height, n_addr_lines=n_addr_lines, n_planes=10, n_temporal_planes=4, map=pixelmap, n_lanes=n_lanes)
|
||||
framebuffer = np.asarray(canvas) + 0 # Make a mutable copy
|
||||
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed,
|
||||
pinout=piomatter.Pinout.Active3,
|
||||
framebuffer=framebuffer,
|
||||
geometry=geometry)
|
||||
|
||||
color_index = 0
|
||||
|
||||
update_interval = 3
|
||||
update_counter = 0
|
||||
def update_matrix():
|
||||
global update_counter
|
||||
if (update_counter := update_counter + 1) >= update_interval:
|
||||
framebuffer[:] = np.asarray(canvas)
|
||||
matrix.show()
|
||||
update_counter = 0
|
||||
|
||||
def darken_color(hex_color, darkness_factor):
|
||||
# Convert hex color number to RGB
|
||||
r = (hex_color >> 16) & 0xFF
|
||||
g = (hex_color >> 8) & 0xFF
|
||||
b = hex_color & 0xFF
|
||||
|
||||
# Apply darkness factor
|
||||
r = int(r * (1 - darkness_factor))
|
||||
g = int(g * (1 - darkness_factor))
|
||||
b = int(b * (1 - darkness_factor))
|
||||
|
||||
# Ensure values are within the valid range
|
||||
r = max(0, min(255, r))
|
||||
g = max(0, min(255, g))
|
||||
b = max(0, min(255, b))
|
||||
|
||||
# Convert RGB back to hex number
|
||||
darkened_hex_color = (r << 16) + (g << 8) + b
|
||||
|
||||
return darkened_hex_color
|
||||
|
||||
step_count = 4
|
||||
darkness_factor = 0.5
|
||||
|
||||
clearing = False
|
||||
|
||||
try:
|
||||
# step_down_size = pen_radius * 2 + 2
|
||||
|
||||
while True:
|
||||
for step in range(step_count):
|
||||
step_down_size = step * (pen_radius* 2) + (2 * step)
|
||||
for x in range(pen_radius + step_down_size, width - pen_radius - step_down_size - 1):
|
||||
color_index = (color_index + 2) % 256
|
||||
color = darken_color(rainbowio.colorwheel(color_index), darkness_factor) if not clearing else 0x000000
|
||||
draw.circle((x, pen_radius + step_down_size), pen_radius, color)
|
||||
update_matrix()
|
||||
for y in range(pen_radius + step_down_size, height - pen_radius - step_down_size - 1):
|
||||
color_index = (color_index + 2) % 256
|
||||
color = darken_color(rainbowio.colorwheel(color_index), darkness_factor) if not clearing else 0x000000
|
||||
draw.circle((width - pen_radius - step_down_size -1, y), pen_radius, color)
|
||||
update_matrix()
|
||||
for x in range(width - pen_radius - step_down_size - 1, pen_radius + step_down_size, -1):
|
||||
color_index = (color_index + 2) % 256
|
||||
color = darken_color(rainbowio.colorwheel(color_index), darkness_factor) if not clearing else 0x000000
|
||||
draw.circle((x, height - pen_radius - step_down_size - 1), pen_radius, color)
|
||||
update_matrix()
|
||||
for y in range(height - pen_radius - step_down_size - 1, pen_radius + ((step+1) * (pen_radius* 2) + (2 * (step+1))) -1, -1):
|
||||
color_index = (color_index + 2) % 256
|
||||
color = darken_color(rainbowio.colorwheel(color_index), darkness_factor) if not clearing else 0x000000
|
||||
draw.circle((pen_radius + step_down_size, y), pen_radius, color)
|
||||
update_matrix()
|
||||
|
||||
if step != step_count-1:
|
||||
# connect to next iter
|
||||
for x in range(pen_radius + step_down_size, pen_radius + ((step+1) * (pen_radius* 2) + (2 * (step+1)))):
|
||||
color_index = (color_index + 2) % 256
|
||||
color = darken_color(rainbowio.colorwheel(color_index),
|
||||
darkness_factor) if not clearing else 0x000000
|
||||
draw.circle((x, pen_radius + ((step+1) * (pen_radius* 2) + (2 * (step+1)))), pen_radius, color)
|
||||
update_matrix()
|
||||
|
||||
print(matrix.fps)
|
||||
clearing = not clearing
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("Exiting")
|
||||
|
|
@ -13,10 +13,11 @@ The image is displayed until the user hits enter to exit.
|
|||
|
||||
import pathlib
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import numpy as np
|
||||
import PIL.Image as Image
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
|
||||
geometry = piomatter.Geometry(width=64, height=64, n_addr_lines=4, rotation=piomatter.Orientation.Normal)
|
||||
framebuffer = np.asarray(Image.open(pathlib.Path(__file__).parent / "blinka64x64.png"))
|
||||
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed,
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ The image is displayed until the user hits enter to exit.
|
|||
|
||||
import pathlib
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import numpy as np
|
||||
import PIL.Image as Image
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
|
||||
geometry = piomatter.Geometry(width=64, height=64, n_addr_lines=5, rotation=piomatter.Orientation.Normal, n_planes=8)
|
||||
framebuffer = np.asarray(Image.open(pathlib.Path(__file__).parent / "blinka64x64.png"))
|
||||
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=piomatter.Pinout.AdafruitMatrixBonnetBGR, framebuffer=framebuffer, geometry=geometry)
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ $ python simpletest.py
|
|||
|
||||
"""
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
|
||||
width = 64
|
||||
height = 32
|
||||
|
||||
|
|
|
|||
|
|
@ -26,12 +26,13 @@ Here's an example for running an emulator using a rom stored in "/tmp/snesrom.sm
|
|||
import shlex
|
||||
from subprocess import Popen
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import click
|
||||
import numpy as np
|
||||
import piomatter_click
|
||||
from pyvirtualdisplay.smartdisplay import SmartDisplay
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import adafruit_blinka_raspberry_pi5_piomatter.click as piomatter_click
|
||||
|
||||
|
||||
@click.command
|
||||
@click.option("--scale", type=float, help="The scale factor, larger numbers mean more virtual pixels", default=1)
|
||||
|
|
|
|||
|
|
@ -29,12 +29,14 @@ import termios
|
|||
import tty
|
||||
from subprocess import Popen, run
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import click
|
||||
import numpy as np
|
||||
import piomatter_click
|
||||
from pyvirtualdisplay.smartdisplay import SmartDisplay
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import adafruit_blinka_raspberry_pi5_piomatter.click as piomatter_click
|
||||
from adafruit_blinka_raspberry_pi5_piomatter.pixelmappers import simple_multilane_mapper
|
||||
|
||||
keyboard_debug = False
|
||||
keys_down = set()
|
||||
basic_characters = string.ascii_letters + string.digits
|
||||
|
|
@ -91,8 +93,8 @@ ctrl_modified_range = (1, 26)
|
|||
@click.option("--ctrl-c-interrupt/--no-ctrl-c-interrupt", help="If Ctrl+C should be handled as an interrupt.", default=True)
|
||||
@piomatter_click.standard_options
|
||||
@click.argument("command", nargs=-1)
|
||||
def main(scale, backend, use_xauth, extra_args, rfbport, width, height, serpentine, rotation, pinout, n_planes,
|
||||
n_addr_lines, ctrl_c_interrupt, command):
|
||||
def main(scale, backend, use_xauth, extra_args, rfbport, width, height, serpentine, rotation, pinout, n_planes, n_temporal_planes,
|
||||
n_addr_lines, n_lanes, ctrl_c_interrupt, command):
|
||||
def handle_key_event(evt_data):
|
||||
if evt_data in key_map.keys():
|
||||
keys_down.add(key_map[evt_data])
|
||||
|
|
@ -132,8 +134,11 @@ def main(scale, backend, use_xauth, extra_args, rfbport, width, height, serpenti
|
|||
if extra_args:
|
||||
kwargs['extra_args'] = shlex.split(extra_args)
|
||||
print("xauth", use_xauth)
|
||||
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines,
|
||||
rotation=rotation)
|
||||
if n_lanes != 2:
|
||||
pixelmap = simple_multilane_mapper(width, height, n_addr_lines, n_lanes)
|
||||
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, n_temporal_planes=n_temporal_planes, n_lanes=n_lanes, map=pixelmap)
|
||||
else:
|
||||
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_temporal_planes=n_temporal_planes, n_addr_lines=n_addr_lines, rotation=rotation)
|
||||
framebuffer = np.zeros(shape=(geometry.height, geometry.width, 3), dtype=np.uint8)
|
||||
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=pinout, framebuffer=framebuffer,
|
||||
geometry=geometry)
|
||||
|
|
|
|||
|
|
@ -9,14 +9,15 @@ build-backend = "setuptools.build_meta"
|
|||
[tool.setuptools_scm]
|
||||
|
||||
[tool.ruff]
|
||||
extend-select = [
|
||||
lint.extend-select = [
|
||||
"B", # flake8-bugbear
|
||||
"I", # isort
|
||||
"PGH", # pygrep-hooks
|
||||
"RUF", # Ruff-specific
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
extend-ignore = [
|
||||
lint.extend-ignore = [
|
||||
"E501", # Line too long
|
||||
"RUF002", # Yes I meant to type 'multiplication sign'!
|
||||
]
|
||||
target-version = "py311"
|
||||
|
|
|
|||
4
setup.py
4
setup.py
|
|
@ -11,7 +11,7 @@ __version__ = get_version()
|
|||
# say from a submodule.
|
||||
|
||||
ext_modules = [
|
||||
Pybind11Extension("adafruit_blinka_raspberry_pi5_piomatter",
|
||||
Pybind11Extension("adafruit_blinka_raspberry_pi5_piomatter._piomatter",
|
||||
["src/pymain.cpp", "src/piolib/piolib.c", "src/piolib/pio_rp1.c"],
|
||||
define_macros = [('VERSION_INFO', __version__)],
|
||||
include_dirs = ['./src/include', './src/piolib/include'],
|
||||
|
|
@ -33,6 +33,8 @@ setup(
|
|||
cmdclass={"build_ext": build_ext},
|
||||
zip_safe=False,
|
||||
python_requires=">=3.11",
|
||||
packages=['adafruit_blinka_raspberry_pi5_piomatter'],
|
||||
package_dir={'adafruit_blinka_raspberry_pi5_piomatter': 'src/adafruit_blinka_raspberry_pi5_piomatter'},
|
||||
extras_require={
|
||||
'docs': ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-jquery"],
|
||||
},
|
||||
|
|
|
|||
33
src/adafruit_blinka_raspberry_pi5_piomatter/__init__.py
Normal file
33
src/adafruit_blinka_raspberry_pi5_piomatter/__init__.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"""
|
||||
HUB75 matrix driver for Raspberry Pi 5 using PIO
|
||||
------------------------------------------------
|
||||
|
||||
.. currentmodule:: adafruit_blinka_raspberry_pi5_piomatter
|
||||
|
||||
.. autosummary::
|
||||
:toctree: _generate
|
||||
:recursive:
|
||||
:class: Orientation Pinout Colorspace Geometry PioMatter
|
||||
|
||||
Orientation
|
||||
Pinout
|
||||
Colorspace
|
||||
Geometry
|
||||
PioMatter
|
||||
"""
|
||||
|
||||
from ._piomatter import (
|
||||
Colorspace,
|
||||
Geometry,
|
||||
Orientation,
|
||||
Pinout,
|
||||
PioMatter,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'Colorspace',
|
||||
'Geometry',
|
||||
'Orientation',
|
||||
'Pinout',
|
||||
'PioMatter',
|
||||
]
|
||||
|
|
@ -5,11 +5,12 @@
|
|||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
import click
|
||||
|
||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||
|
||||
class PybindEnumChoice(click.Choice):
|
||||
|
||||
class _PybindEnumChoice(click.Choice):
|
||||
def __init__(self, enum, case_sensitive=False):
|
||||
self.enum = enum
|
||||
choices = [k for k, v in enum.__dict__.items() if isinstance(v, enum)]
|
||||
|
|
@ -25,6 +26,11 @@ class PybindEnumChoice(click.Choice):
|
|||
r = getattr(self.enum, value)
|
||||
return r
|
||||
|
||||
def _validate_temporal_planes(ctx, param, value):
|
||||
if value not in (0, 2, 4):
|
||||
raise click.BadParameter("must be 0, 2, or 4")
|
||||
return value
|
||||
|
||||
def standard_options(
|
||||
f: click.decorators.FC | None = None,
|
||||
*,
|
||||
|
|
@ -34,7 +40,9 @@ def standard_options(
|
|||
rotation=piomatter.Orientation.Normal,
|
||||
pinout=piomatter.Pinout.AdafruitMatrixBonnet,
|
||||
n_planes=10,
|
||||
n_temporal_planes=0,
|
||||
n_addr_lines=4,
|
||||
n_lanes=2,
|
||||
) -> Callable[[], None]:
|
||||
"""Add standard commandline flags, with the defaults given
|
||||
|
||||
|
|
@ -61,7 +69,7 @@ def standard_options(
|
|||
f = click.option(
|
||||
"--pinout",
|
||||
default=pinout,
|
||||
type=PybindEnumChoice(piomatter.Pinout),
|
||||
type=_PybindEnumChoice(piomatter.Pinout),
|
||||
help="The details of the electrical connection to the panels"
|
||||
)(f)
|
||||
if rotation is not None:
|
||||
|
|
@ -69,13 +77,17 @@ def standard_options(
|
|||
"--orientation",
|
||||
"rotation",
|
||||
default=rotation,
|
||||
type=PybindEnumChoice(piomatter.Orientation),
|
||||
type=_PybindEnumChoice(piomatter.Orientation),
|
||||
help="The overall orientation (rotation) of the panels"
|
||||
)(f)
|
||||
if n_planes is not None:
|
||||
f = click.option("--num-planes", "n_planes", default=n_planes, help="The number of bit planes (color depth. Lower values can improve refresh rate in frames per second")(f)
|
||||
f = click.option("--num-planes", "n_planes", default=n_planes, help="The number of bit planes (color depth). Lower values can improve refresh rate in frames per second")(f)
|
||||
if n_temporal_planes is not None:
|
||||
f = click.option("--num-temporal-planes", "n_temporal_planes", default=n_temporal_planes, callback=_validate_temporal_planes, help="The number of temporal bit-planes. May be 0, 2, or 4. Nonzero values improve frame rate but can cause some shimmer")(f)
|
||||
if n_addr_lines is not None:
|
||||
f = click.option("--num-address-lines", "n_addr_lines", default=n_addr_lines, help="The number of address lines used by the panels")(f)
|
||||
if n_lanes is not None:
|
||||
f = click.option("--num-lanes", "n_lanes", default=n_lanes, help="The number of lanes used by the panels. One 16-pin connector has two lanes (6 RGB pins)")(f)
|
||||
return f
|
||||
if f is None:
|
||||
return wrapper
|
||||
31
src/adafruit_blinka_raspberry_pi5_piomatter/pixelmappers.py
Normal file
31
src/adafruit_blinka_raspberry_pi5_piomatter/pixelmappers.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"""Functions to define the layout of complex setups, particularly multi-connector matrices"""
|
||||
|
||||
def simple_multilane_mapper(width, height, n_addr_lines, n_lanes):
|
||||
"""A simple mapper for 4+ pixel lanes
|
||||
|
||||
A framebuffer (width × height) is mapped onto a matrix where the lanes are stacked
|
||||
top-to-bottom. Panels within a lane may be cascaded left-to-right.
|
||||
|
||||
Rotation is not supported, and neither are more complicated arrangements of panels
|
||||
within a single chain (no support for serpentine or stacked panels within a segment)
|
||||
|
||||
.. code-block::
|
||||
|
||||
0 -> [panel] -> [panel]
|
||||
1 -> [panel] -> [panel]
|
||||
2 -> [panel] -> [panel]
|
||||
"""
|
||||
|
||||
calc_height = n_lanes << n_addr_lines
|
||||
if height != calc_height:
|
||||
raise RuntimeError(f"Calculated height {calc_height} does not match requested height {height}")
|
||||
n_addr = 1 << n_addr_lines
|
||||
|
||||
m = []
|
||||
for addr in range(n_addr):
|
||||
for x in range(width):
|
||||
for lane in range(n_lanes):
|
||||
y = addr + lane * n_addr
|
||||
m.append(x + width * y)
|
||||
print(m)
|
||||
return m
|
||||
|
|
@ -48,7 +48,8 @@ matrix_map make_matrixmap(size_t width, size_t height, size_t n_addr_lines,
|
|||
|
||||
size_t panel_height = 2 << n_addr_lines;
|
||||
if (height % panel_height != 0) {
|
||||
throw std::range_error("Height does not evenly divide panel height");
|
||||
throw std::range_error(
|
||||
"Overall height does not evenly divide calculated panel height");
|
||||
}
|
||||
|
||||
size_t half_panel_height = 1u << n_addr_lines;
|
||||
|
|
@ -80,23 +81,136 @@ matrix_map make_matrixmap(size_t width, size_t height, size_t n_addr_lines,
|
|||
return result;
|
||||
}
|
||||
|
||||
struct schedule_entry {
|
||||
uint32_t shift, active_time;
|
||||
};
|
||||
|
||||
using schedule = std::vector<schedule_entry>;
|
||||
using schedule_sequence = std::vector<schedule>;
|
||||
|
||||
schedule_sequence rescale_schedule(schedule_sequence ss, size_t pixels_across) {
|
||||
uint32_t max_active_time = 0;
|
||||
for (auto &s : ss) {
|
||||
for (auto &ent : s) {
|
||||
max_active_time = std::max(ent.active_time, max_active_time);
|
||||
}
|
||||
}
|
||||
if (max_active_time == 0 || max_active_time >= pixels_across) {
|
||||
return ss;
|
||||
}
|
||||
int scale = (pixels_across + max_active_time - 1) / max_active_time;
|
||||
for (auto &s : ss) {
|
||||
for (auto &ent : s) {
|
||||
ent.active_time *= scale;
|
||||
}
|
||||
}
|
||||
return ss;
|
||||
}
|
||||
|
||||
schedule_sequence make_simple_schedule(int n_planes, size_t pixels_across) {
|
||||
if (n_planes < 1 || n_planes > 10) {
|
||||
throw std::range_error("n_planes out of range");
|
||||
}
|
||||
schedule result;
|
||||
for (int i = 0; i < n_planes; i++) {
|
||||
result.emplace_back(9 - i, (1 << (n_planes - i - 1)));
|
||||
}
|
||||
return rescale_schedule({result}, pixels_across);
|
||||
}
|
||||
|
||||
// Make a temporal dither schedule. All the top `n_planes` are shown everytime,
|
||||
// but the lowest planes are done in a cycle of `n_temporal_planes`:
|
||||
// 2: {0, 1}; 4: {0, 1, 2, 3}
|
||||
schedule_sequence make_temporal_dither_schedule(int n_planes,
|
||||
size_t pixels_across,
|
||||
int n_temporal_planes) {
|
||||
if (n_planes < 1 || n_planes > 10) {
|
||||
throw std::range_error("n_planes out of range");
|
||||
}
|
||||
if (n_temporal_planes < 2) {
|
||||
// either 0 or 1 temporal planes are not really temporal at all
|
||||
return make_simple_schedule(n_planes, pixels_across);
|
||||
}
|
||||
if (n_temporal_planes >= n_planes) {
|
||||
throw std::range_error("n_temporal_planes can't exceed n_planes");
|
||||
}
|
||||
if (n_temporal_planes != 2 && n_temporal_planes != 4) {
|
||||
// the code can generate a schedule for 8 temporal planes, but it
|
||||
// flickers intolerably
|
||||
throw std::range_error("n_temporal_planes must be 0, 1, 2, or 4");
|
||||
}
|
||||
|
||||
int n_real_planes = n_planes - n_temporal_planes;
|
||||
|
||||
schedule base_sched;
|
||||
for (int j = 0; j < n_real_planes; j++) {
|
||||
base_sched.emplace_back(
|
||||
9 - j, (1 << (n_temporal_planes + n_real_planes - j - 1)) /
|
||||
n_temporal_planes);
|
||||
}
|
||||
|
||||
schedule_sequence result;
|
||||
|
||||
auto add_sched = [&result, &base_sched](int plane, int count) {
|
||||
auto sched = base_sched;
|
||||
sched.emplace_back(9 - plane, count);
|
||||
result.emplace_back(sched);
|
||||
};
|
||||
|
||||
for (int i = 0; i < n_temporal_planes; i++) {
|
||||
add_sched(n_real_planes + i, 1 << (n_temporal_planes - i - 1));
|
||||
}
|
||||
#if 0
|
||||
std::vector<uint32_t> counts(10, 0);
|
||||
for (auto s : result) {
|
||||
for(auto t: s) {
|
||||
counts[t.shift] += t.active_time;
|
||||
}
|
||||
}
|
||||
for (auto s : counts) {
|
||||
printf("%d ", s);
|
||||
}
|
||||
printf("\n");
|
||||
#endif
|
||||
|
||||
return rescale_schedule(result, pixels_across);
|
||||
;
|
||||
}
|
||||
|
||||
struct matrix_geometry {
|
||||
template <typename Cb>
|
||||
matrix_geometry(size_t pixels_across, size_t n_addr_lines, int n_planes,
|
||||
size_t width, size_t height, bool serpentine, const Cb &cb)
|
||||
int n_temporal_planes, size_t width, size_t height,
|
||||
bool serpentine, const Cb &cb)
|
||||
: matrix_geometry(
|
||||
pixels_across, n_addr_lines, n_planes, n_temporal_planes, width,
|
||||
height,
|
||||
make_matrixmap(width, height, n_addr_lines, serpentine, cb), 2) {}
|
||||
|
||||
matrix_geometry(size_t pixels_across, size_t n_addr_lines, int n_planes,
|
||||
int n_temporal_planes, size_t width, size_t height,
|
||||
matrix_map map, size_t n_lanes)
|
||||
: matrix_geometry(pixels_across, n_addr_lines, width, height, map,
|
||||
n_lanes,
|
||||
make_temporal_dither_schedule(n_planes, pixels_across,
|
||||
n_temporal_planes)) {}
|
||||
|
||||
matrix_geometry(size_t pixels_across, size_t n_addr_lines, size_t width,
|
||||
size_t height, matrix_map map, size_t n_lanes,
|
||||
const schedule_sequence &schedules)
|
||||
: pixels_across(pixels_across), n_addr_lines(n_addr_lines),
|
||||
n_planes(n_planes), width(width),
|
||||
height(height), map{make_matrixmap(width, height, n_addr_lines,
|
||||
serpentine, cb)} {
|
||||
size_t pixels_down = 2u << n_addr_lines;
|
||||
n_lanes(n_lanes), width(width), height(height),
|
||||
map(map), schedules{schedules} {
|
||||
size_t pixels_down = n_lanes << n_addr_lines;
|
||||
if (map.size() != pixels_down * pixels_across) {
|
||||
throw std::range_error(
|
||||
"map size does not match calculated pixel count");
|
||||
}
|
||||
}
|
||||
size_t pixels_across, n_addr_lines;
|
||||
int n_planes;
|
||||
|
||||
size_t pixels_across, n_addr_lines, n_lanes;
|
||||
size_t width, height;
|
||||
matrix_map map;
|
||||
schedule_sequence schedules;
|
||||
};
|
||||
} // namespace piomatter
|
||||
|
|
|
|||
|
|
@ -40,4 +40,42 @@ struct adafruit_matrix_bonnet_pinout_bgr {
|
|||
static constexpr uint32_t post_addr_delay = 500;
|
||||
};
|
||||
|
||||
struct active3_pinout {
|
||||
static constexpr pin_t PIN_RGB[] = {7, 27, 11, 10, 9, 8, 6, 5, 12,
|
||||
20, 13, 19, 3, 2, 14, 21, 16, 26};
|
||||
static constexpr pin_t PIN_ADDR[] = {22, 23, 24, 25, 15};
|
||||
static constexpr pin_t PIN_OE = 18; // /OE: output enable when LOW
|
||||
static constexpr pin_t PIN_CLK = 17; // SRCLK: clocks on RISING edge
|
||||
static constexpr pin_t PIN_LAT = 4; // RCLK: latches on RISING edge
|
||||
|
||||
static constexpr uint32_t clk_bit = 1u << PIN_CLK;
|
||||
static constexpr uint32_t lat_bit = 1u << PIN_LAT;
|
||||
static constexpr uint32_t oe_bit = 1u << PIN_OE;
|
||||
static constexpr uint32_t oe_active = 0;
|
||||
static constexpr uint32_t oe_inactive = oe_bit;
|
||||
|
||||
static constexpr uint32_t post_oe_delay = 0;
|
||||
static constexpr uint32_t post_latch_delay = 0;
|
||||
static constexpr uint32_t post_addr_delay = 500;
|
||||
};
|
||||
|
||||
struct active3_pinout_bgr {
|
||||
static constexpr pin_t PIN_RGB[] = {11, 27, 7, 8, 9, 10, 12, 5, 6,
|
||||
19, 13, 20, 14, 2, 3, 26, 16, 21};
|
||||
static constexpr pin_t PIN_ADDR[] = {22, 23, 24, 25, 15};
|
||||
static constexpr pin_t PIN_OE = 18; // /OE: output enable when LOW
|
||||
static constexpr pin_t PIN_CLK = 17; // SRCLK: clocks on RISING edge
|
||||
static constexpr pin_t PIN_LAT = 4; // RCLK: latches on RISING edge
|
||||
|
||||
static constexpr uint32_t clk_bit = 1u << PIN_CLK;
|
||||
static constexpr uint32_t lat_bit = 1u << PIN_LAT;
|
||||
static constexpr uint32_t oe_bit = 1u << PIN_OE;
|
||||
static constexpr uint32_t oe_active = 0;
|
||||
static constexpr uint32_t oe_inactive = oe_bit;
|
||||
|
||||
static constexpr uint32_t post_oe_delay = 0;
|
||||
static constexpr uint32_t post_latch_delay = 0;
|
||||
static constexpr uint32_t post_addr_delay = 500;
|
||||
};
|
||||
|
||||
} // namespace piomatter
|
||||
|
|
|
|||
|
|
@ -49,22 +49,31 @@ template <class pinout = adafruit_matrix_bonnet_pinout,
|
|||
class colorspace = colorspace_rgb888>
|
||||
struct piomatter : piomatter_base {
|
||||
using buffer_type = std::vector<uint32_t>;
|
||||
using bufseq_type = std::vector<buffer_type>;
|
||||
piomatter(std::span<typename colorspace::data_type const> framebuffer,
|
||||
const matrix_geometry &geometry)
|
||||
: framebuffer(framebuffer), geometry{geometry}, converter{},
|
||||
blitter_thread{&piomatter::blit_thread, this} {
|
||||
blitter_thread{} {
|
||||
if (geometry.n_addr_lines > std::size(pinout::PIN_ADDR)) {
|
||||
throw std::runtime_error("too many address lines requested");
|
||||
}
|
||||
program_init();
|
||||
blitter_thread = std::move(std::thread{&piomatter::blit_thread, this});
|
||||
show();
|
||||
}
|
||||
|
||||
void show() override {
|
||||
int buffer_idx = manager.get_free_buffer();
|
||||
auto &buffer = buffers[buffer_idx];
|
||||
auto &bufseq = buffers[buffer_idx];
|
||||
bufseq.resize(geometry.schedules.size());
|
||||
auto converted = converter.convert(framebuffer);
|
||||
protomatter_render_rgb10<pinout>(buffer, geometry, converted.data());
|
||||
auto old_active_time = geometry.schedules.back().back().active_time;
|
||||
for (size_t i = 0; i < geometry.schedules.size(); i++) {
|
||||
protomatter_render_rgb10<pinout>(bufseq[i], geometry,
|
||||
geometry.schedules[i],
|
||||
old_active_time, converted.data());
|
||||
old_active_time = geometry.schedules[i].back().active_time;
|
||||
}
|
||||
manager.put_filled_buffer(buffer_idx);
|
||||
}
|
||||
|
||||
|
|
@ -160,26 +169,27 @@ struct piomatter : piomatter_base {
|
|||
}
|
||||
|
||||
void blit_thread() {
|
||||
const uint32_t *databuf = nullptr;
|
||||
size_t datasize = 0;
|
||||
int old_buffer_idx = buffer_manager::no_buffer;
|
||||
int cur_buffer_idx = buffer_manager::no_buffer;
|
||||
int buffer_idx;
|
||||
int seq_idx = -1;
|
||||
uint64_t t0, t1;
|
||||
t0 = monotonicns64();
|
||||
while ((buffer_idx = manager.get_filled_buffer()) !=
|
||||
buffer_manager::exit_request) {
|
||||
if (buffer_idx != buffer_manager::no_buffer) {
|
||||
const auto &buffer = buffers[buffer_idx];
|
||||
databuf = &buffer[0];
|
||||
datasize = buffer.size() * sizeof(*databuf);
|
||||
if (old_buffer_idx != buffer_manager::no_buffer) {
|
||||
manager.put_free_buffer(old_buffer_idx);
|
||||
if (cur_buffer_idx != buffer_manager::no_buffer) {
|
||||
manager.put_free_buffer(cur_buffer_idx);
|
||||
}
|
||||
old_buffer_idx = buffer_idx;
|
||||
cur_buffer_idx = buffer_idx;
|
||||
}
|
||||
if (datasize) {
|
||||
if (cur_buffer_idx != buffer_manager::no_buffer) {
|
||||
const auto &cur_buf = buffers[cur_buffer_idx];
|
||||
seq_idx = (seq_idx + 1) % cur_buf.size();
|
||||
const auto &data = cur_buf[seq_idx];
|
||||
auto datasize = sizeof(uint32_t) * data.size();
|
||||
auto dataptr = const_cast<uint32_t *>(&data[0]);
|
||||
pio_sm_xfer_data_large(pio, sm, PIO_DIR_TO_SM, datasize,
|
||||
(uint32_t *)databuf);
|
||||
dataptr);
|
||||
t1 = monotonicns64();
|
||||
if (t0 != t1) {
|
||||
fps = 1e9 / (t1 - t0);
|
||||
|
|
@ -194,7 +204,7 @@ struct piomatter : piomatter_base {
|
|||
PIO pio = NULL;
|
||||
int sm = -1;
|
||||
std::span<typename colorspace::data_type const> framebuffer;
|
||||
buffer_type buffers[3];
|
||||
bufseq_type buffers[3];
|
||||
buffer_manager manager{};
|
||||
matrix_geometry geometry;
|
||||
colorspace converter;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
const int protomatter_wrap = 4;
|
||||
const int protomatter_wrap_target = 0;
|
||||
const int protomatter_sideset_pin_count = 1;
|
||||
const bool protomatter_sideset_enable = true;
|
||||
const bool protomatter_sideset_enable = 1;
|
||||
const uint16_t protomatter[] = {
|
||||
// ; data format (out-shift-right):
|
||||
// ; MSB ... LSB
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ struct colorspace_rgb10 {
|
|||
template <typename pinout>
|
||||
void protomatter_render_rgb10(std::vector<uint32_t> &result,
|
||||
const matrix_geometry &matrixmap,
|
||||
const schedule &sched, uint32_t old_active_time,
|
||||
const uint32_t *pixels) {
|
||||
result.clear();
|
||||
|
||||
|
|
@ -153,7 +154,7 @@ void protomatter_render_rgb10(std::vector<uint32_t> &result,
|
|||
data_count = n;
|
||||
};
|
||||
|
||||
int32_t active_time;
|
||||
int32_t active_time = old_active_time;
|
||||
|
||||
auto do_data_clk_active = [&active_time, &data_count, &result](uint32_t d) {
|
||||
bool active = active_time > 0;
|
||||
|
|
@ -183,68 +184,42 @@ void protomatter_render_rgb10(std::vector<uint32_t> &result,
|
|||
return data;
|
||||
};
|
||||
|
||||
auto add_pixels = [&do_data_clk_active,
|
||||
&result](uint32_t addr_bits, bool r0, bool g0, bool b0,
|
||||
bool r1, bool g1, bool b1) {
|
||||
uint32_t data = addr_bits;
|
||||
if (r0)
|
||||
data |= (1 << pinout::PIN_RGB[0]);
|
||||
if (g0)
|
||||
data |= (1 << pinout::PIN_RGB[1]);
|
||||
if (b0)
|
||||
data |= (1 << pinout::PIN_RGB[2]);
|
||||
if (r1)
|
||||
data |= (1 << pinout::PIN_RGB[3]);
|
||||
if (g1)
|
||||
data |= (1 << pinout::PIN_RGB[4]);
|
||||
if (b1)
|
||||
data |= (1 << pinout::PIN_RGB[5]);
|
||||
|
||||
do_data_clk_active(data);
|
||||
};
|
||||
|
||||
int last_bit = 0;
|
||||
// illuminate the right row for data in the shift register (the previous
|
||||
// address)
|
||||
|
||||
const size_t n_addr = 1u << matrixmap.n_addr_lines;
|
||||
const int n_planes = matrixmap.n_planes;
|
||||
constexpr size_t n_bits = 10u;
|
||||
unsigned offset = n_bits - n_planes;
|
||||
const size_t pixels_across = matrixmap.pixels_across;
|
||||
|
||||
size_t prev_addr = n_addr - 1;
|
||||
uint32_t addr_bits = calc_addr_bits(prev_addr);
|
||||
|
||||
for (size_t addr = 0; addr < n_addr; addr++) {
|
||||
// printf("addr=%zu/%zu\n", addr, n_addr);
|
||||
for (int bit = n_planes - 1; bit >= 0; bit--) {
|
||||
// printf("bit=%d/%d\n", bit, n_planes);
|
||||
uint32_t r = 1 << (20 + offset + bit);
|
||||
uint32_t g = 1 << (10 + offset + bit);
|
||||
uint32_t b = 1 << (0 + offset + bit);
|
||||
|
||||
// the shortest /OE we can do is one DATA_OVERHEAD...
|
||||
// TODO: should make sure desired duration of MSB is at least
|
||||
// `pixels_across`
|
||||
active_time = 1 << last_bit;
|
||||
last_bit = bit;
|
||||
for (auto &schedule_ent : sched) {
|
||||
uint32_t r_mask = 1 << (20 + schedule_ent.shift);
|
||||
uint32_t g_mask = 1 << (10 + schedule_ent.shift);
|
||||
uint32_t b_mask = 1 << (0 + schedule_ent.shift);
|
||||
|
||||
prep_data(pixels_across);
|
||||
auto mapiter = matrixmap.map.begin() + 2 * addr * pixels_across;
|
||||
auto mapiter = matrixmap.map.begin() +
|
||||
matrixmap.n_lanes * addr * pixels_across;
|
||||
for (size_t x = 0; x < pixels_across; x++) {
|
||||
uint32_t data = addr_bits;
|
||||
for (size_t px = 0; px < matrixmap.n_lanes; px++) {
|
||||
assert(mapiter != matrixmap.map.end());
|
||||
auto pixel0 = pixels[*mapiter++];
|
||||
auto r0 = pixel0 & r;
|
||||
auto g0 = pixel0 & g;
|
||||
auto b0 = pixel0 & b;
|
||||
assert(mapiter != matrixmap.map.end());
|
||||
auto pixel1 = pixels[*mapiter++];
|
||||
auto r1 = pixel1 & r;
|
||||
auto g1 = pixel1 & g;
|
||||
auto b1 = pixel1 & b;
|
||||
auto r_bit = pixel0 & r_mask;
|
||||
auto g_bit = pixel0 & g_mask;
|
||||
auto b_bit = pixel0 & b_mask;
|
||||
|
||||
add_pixels(addr_bits, r0, g0, b0, r1, g1, b1);
|
||||
if (r_bit)
|
||||
data |= (1 << pinout::PIN_RGB[px * 3 + 0]);
|
||||
if (g_bit)
|
||||
data |= (1 << pinout::PIN_RGB[px * 3 + 1]);
|
||||
if (b_bit)
|
||||
data |= (1 << pinout::PIN_RGB[px * 3 + 2]);
|
||||
}
|
||||
|
||||
do_data_clk_active(data);
|
||||
}
|
||||
|
||||
do_data_delay(addr_bits | pinout::oe_active,
|
||||
|
|
@ -256,6 +231,8 @@ void protomatter_render_rgb10(std::vector<uint32_t> &result,
|
|||
do_data_delay(addr_bits | pinout::oe_inactive | pinout::lat_bit,
|
||||
pinout::post_latch_delay);
|
||||
|
||||
active_time = schedule_ent.active_time;
|
||||
|
||||
// with oe inactive, set address bits to illuminate THIS line
|
||||
if (addr != prev_addr) {
|
||||
addr_bits = calc_addr_bits(addr);
|
||||
|
|
|
|||
|
|
@ -95,10 +95,56 @@ static uint64_t monotonicns64() {
|
|||
return tp.tv_sec * UINT64_C(1000000000) + tp.tv_nsec;
|
||||
}
|
||||
|
||||
static void print_dither_schedule(const piomatter::schedule_sequence &ss) {
|
||||
for (auto s : ss) {
|
||||
for (auto i : s) {
|
||||
printf("{%d %d} ", i.shift, i.active_time);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
static void test_simple_dither_schedule(int n_planes, int pixels_across) {
|
||||
auto ss = piomatter::make_simple_schedule(n_planes, pixels_across);
|
||||
print_dither_schedule(ss);
|
||||
}
|
||||
static void test_temporal_dither_schedule(int n_planes, int pixels_across,
|
||||
int n_temporal_frames) {
|
||||
auto ss = piomatter::make_temporal_dither_schedule(n_planes, pixels_across,
|
||||
n_temporal_frames);
|
||||
print_dither_schedule(ss);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int n = argc > 1 ? atoi(argv[1]) : 0;
|
||||
|
||||
piomatter::matrix_geometry geometry(128, 4, 10, 64, 64, true,
|
||||
test_simple_dither_schedule(5, 1);
|
||||
test_temporal_dither_schedule(5, 1, 0);
|
||||
test_temporal_dither_schedule(5, 1, 2);
|
||||
test_temporal_dither_schedule(5, 1, 4);
|
||||
|
||||
test_simple_dither_schedule(6, 1);
|
||||
test_temporal_dither_schedule(6, 1, 0);
|
||||
test_temporal_dither_schedule(6, 1, 2);
|
||||
test_temporal_dither_schedule(6, 1, 4);
|
||||
|
||||
test_simple_dither_schedule(5, 16);
|
||||
test_temporal_dither_schedule(5, 16, 2);
|
||||
test_temporal_dither_schedule(5, 16, 4);
|
||||
|
||||
test_simple_dither_schedule(5, 24);
|
||||
test_temporal_dither_schedule(5, 24, 2);
|
||||
test_temporal_dither_schedule(5, 24, 4);
|
||||
|
||||
test_simple_dither_schedule(10, 24);
|
||||
test_temporal_dither_schedule(10, 24, 8);
|
||||
|
||||
test_temporal_dither_schedule(5, 128, 4);
|
||||
test_temporal_dither_schedule(5, 192, 4);
|
||||
return 0;
|
||||
|
||||
piomatter::matrix_geometry geometry(128, 4, 10, 0, 64, 64, true,
|
||||
piomatter::orientation_normal);
|
||||
piomatter::piomatter p(std::span(&pixels[0][0], 64 * 64), geometry);
|
||||
|
||||
|
|
|
|||
160
src/pymain.cpp
160
src/pymain.cpp
|
|
@ -1,5 +1,6 @@
|
|||
#include <iostream>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
#include <string>
|
||||
|
||||
#include "piomatter/piomatter.h"
|
||||
|
|
@ -33,6 +34,14 @@ make_piomatter_pc(py::buffer buffer,
|
|||
const py::buffer_info info = buffer.request();
|
||||
const size_t buffer_size_in_bytes = info.size * info.itemsize;
|
||||
|
||||
if (geometry.n_lanes * 3 > std::size(pinout::PIN_RGB)) {
|
||||
throw std::runtime_error(
|
||||
py::str("Geometry lane count {} exceeds the pinout with {} rgb "
|
||||
"pins ({} lanes)")
|
||||
.attr("format")(geometry.n_lanes, std::size(pinout::PIN_RGB),
|
||||
std::size(pinout::PIN_RGB) / 3)
|
||||
.template cast<std::string>());
|
||||
}
|
||||
if (buffer_size_in_bytes != data_size_in_bytes) {
|
||||
throw std::runtime_error(
|
||||
py::str("Framebuffer size must be {} bytes ({} elements of {} "
|
||||
|
|
@ -54,6 +63,8 @@ enum Colorspace { RGB565, RGB888, RGB888Packed };
|
|||
enum Pinout {
|
||||
AdafruitMatrixBonnet,
|
||||
AdafruitMatrixBonnetBGR,
|
||||
Active3,
|
||||
Active3BGR,
|
||||
};
|
||||
|
||||
template <class pinout>
|
||||
|
|
@ -70,12 +81,10 @@ make_piomatter_p(Colorspace c, py::buffer buffer,
|
|||
case RGB888Packed:
|
||||
return make_piomatter_pc<pinout, piomatter::colorspace_rgb888_packed>(
|
||||
buffer, geometry);
|
||||
|
||||
default:
|
||||
}
|
||||
throw std::runtime_error(py::str("Invalid colorspace {!r}")
|
||||
.attr("format")(c)
|
||||
.template cast<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<PyPiomatter>
|
||||
|
|
@ -88,15 +97,19 @@ make_piomatter(Colorspace c, Pinout p, py::buffer buffer,
|
|||
case AdafruitMatrixBonnetBGR:
|
||||
return make_piomatter_p<piomatter::adafruit_matrix_bonnet_pinout_bgr>(
|
||||
c, buffer, geometry);
|
||||
default:
|
||||
case Active3:
|
||||
return make_piomatter_p<piomatter::active3_pinout>(c, buffer, geometry);
|
||||
case Active3BGR:
|
||||
return make_piomatter_p<piomatter::active3_pinout_bgr>(c, buffer,
|
||||
geometry);
|
||||
}
|
||||
throw std::runtime_error(py::str("Invalid pinout {!r}")
|
||||
.attr("format")(p)
|
||||
.template cast<std::string>());
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PYBIND11_MODULE(adafruit_blinka_raspberry_pi5_piomatter, m) {
|
||||
PYBIND11_MODULE(_piomatter, m) {
|
||||
py::options options;
|
||||
options.enable_enum_members_docstring();
|
||||
options.enable_function_signatures();
|
||||
|
|
@ -106,18 +119,7 @@ PYBIND11_MODULE(adafruit_blinka_raspberry_pi5_piomatter, m) {
|
|||
HUB75 matrix driver for Raspberry Pi 5 using PIO
|
||||
------------------------------------------------
|
||||
|
||||
.. currentmodule:: adafruit_blinka_raspberry_pi5_piomatter
|
||||
|
||||
.. autosummary::
|
||||
:toctree: _generate
|
||||
|
||||
Orientation
|
||||
Pinout
|
||||
Colorspace
|
||||
Geometry
|
||||
PioMatter
|
||||
AdafruitMatrixBonnetRGB888
|
||||
AdafruitMatrixBonnetRGB888Packed
|
||||
.. currentmodule:: adafruit_blinka_raspberry_pi5_piomatter._piomatter
|
||||
)pbdoc";
|
||||
|
||||
py::enum_<piomatter::orientation>(
|
||||
|
|
@ -138,7 +140,10 @@ PYBIND11_MODULE(adafruit_blinka_raspberry_pi5_piomatter, m) {
|
|||
.value("AdafruitMatrixHat", Pinout::AdafruitMatrixBonnet,
|
||||
"Adafruit Matrix Bonnet or Matrix Hat")
|
||||
.value("AdafruitMatrixHatBGR", Pinout::AdafruitMatrixBonnetBGR,
|
||||
"Adafruit Matrix Bonnet or Matrix Hat with BGR color order");
|
||||
"Adafruit Matrix Bonnet or Matrix Hat with BGR color order")
|
||||
.value("Active3", Pinout::Active3, "Active-3 or compatible board")
|
||||
.value("Active3BGR", Pinout::Active3BGR,
|
||||
"Active-3 or compatible board with BGR color order");
|
||||
|
||||
py::enum_<Colorspace>(
|
||||
m, "Colorspace",
|
||||
|
|
@ -157,19 +162,33 @@ Describe the geometry of a set of panels
|
|||
|
||||
The number of pixels in the shift register is automatically computed from these values.
|
||||
|
||||
``n_planes`` controls the color depth of the panel. This is separate from the framebuffer
|
||||
layout. Decreasing ``n_planes`` can increase FPS at the cost of reduced color fidelity.
|
||||
The default, 10, is the maximum value.
|
||||
|
||||
``n_temporal_planes`` controls temporal dithering of the panel. The acceptable values
|
||||
are 0 (the default), 2, and 4. A higher setting can increase FPS at the cost of
|
||||
slightly increasing the variation of brightness across subsequent frames.
|
||||
|
||||
For simple panels with just 1 connector (2 color lanes), the following constructor arguments are available:
|
||||
|
||||
``serpentine`` controls the arrangement of multiple panels when they are stacked in rows.
|
||||
If it is `True`, then each row goes in the opposite direction of the previous row.
|
||||
If this is specified, ``n_lanes`` cannot be, and 2 lanes are always used.
|
||||
|
||||
``rotation`` controls the orientation of the panel(s). Must be one of the ``Orientation``
|
||||
constants. Default is ``Orientation.Normal``.
|
||||
|
||||
``n_planes`` controls the color depth of the panel. This is separate from the framebuffer
|
||||
layout. Decreasing ``n_planes`` can increase FPS at the cost of reduced color fidelity.
|
||||
The default, 10, is the maximum value.
|
||||
For panels with more than 2 lanes, or using a custom pixel mapping, the following constructor arguments are available:
|
||||
|
||||
``n_lanes`` controls how many color lanes are used. A single 16-pin HUB75 connector has 2 color lanes.
|
||||
If 2 or 3 connectors are used, then there are 4 or 6 lanes.
|
||||
|
||||
``map`` is a Python list of integers giving the framebuffer pixel indices for each matrix pixel.
|
||||
)pbdoc")
|
||||
.def(py::init([](size_t width, size_t height, size_t n_addr_lines,
|
||||
bool serpentine, piomatter::orientation rotation,
|
||||
size_t n_planes) {
|
||||
size_t n_planes, size_t n_temporal_planes) {
|
||||
size_t n_lines = 2 << n_addr_lines;
|
||||
size_t pixels_across = width * height / n_lines;
|
||||
size_t odd = (width * height) % n_lines;
|
||||
|
|
@ -185,30 +204,51 @@ The default, 10, is the maximum value.
|
|||
switch (rotation) {
|
||||
case piomatter::orientation::normal:
|
||||
return piomatter::matrix_geometry(
|
||||
pixels_across, n_addr_lines, n_planes, width, height,
|
||||
serpentine, piomatter::orientation_normal);
|
||||
pixels_across, n_addr_lines, n_planes,
|
||||
n_temporal_planes, width, height, serpentine,
|
||||
piomatter::orientation_normal);
|
||||
|
||||
case piomatter::orientation::r180:
|
||||
return piomatter::matrix_geometry(
|
||||
pixels_across, n_addr_lines, n_planes, width, height,
|
||||
serpentine, piomatter::orientation_r180);
|
||||
pixels_across, n_addr_lines, n_planes,
|
||||
n_temporal_planes, width, height, serpentine,
|
||||
piomatter::orientation_r180);
|
||||
|
||||
case piomatter::orientation::ccw:
|
||||
return piomatter::matrix_geometry(
|
||||
pixels_across, n_addr_lines, n_planes, width, height,
|
||||
serpentine, piomatter::orientation_ccw);
|
||||
pixels_across, n_addr_lines, n_planes,
|
||||
n_temporal_planes, width, height, serpentine,
|
||||
piomatter::orientation_ccw);
|
||||
|
||||
case piomatter::orientation::cw:
|
||||
return piomatter::matrix_geometry(
|
||||
pixels_across, n_addr_lines, n_planes, width, height,
|
||||
serpentine, piomatter::orientation_cw);
|
||||
pixels_across, n_addr_lines, n_planes,
|
||||
n_temporal_planes, width, height, serpentine,
|
||||
piomatter::orientation_cw);
|
||||
}
|
||||
throw std::runtime_error("invalid rotation");
|
||||
}),
|
||||
py::arg("width"), py::arg("height"), py::arg("n_addr_lines"),
|
||||
py::arg("serpentine") = true,
|
||||
py::arg("rotation") = piomatter::orientation::normal,
|
||||
py::arg("n_planes") = 10u)
|
||||
py::arg("n_planes") = 10u, py::arg("n_temporal_planes") = 2)
|
||||
.def(py::init([](size_t width, size_t height, size_t n_addr_lines,
|
||||
piomatter::matrix_map map, size_t n_planes,
|
||||
size_t n_temporal_planes, size_t n_lanes) {
|
||||
size_t n_lines = n_lanes << n_addr_lines;
|
||||
size_t pixels_across = width * height / n_lines;
|
||||
for (auto el : map) {
|
||||
if ((size_t)el >= width * height) {
|
||||
throw std::out_of_range("Map element out of range");
|
||||
}
|
||||
}
|
||||
return piomatter::matrix_geometry(pixels_across, n_addr_lines,
|
||||
n_planes, n_temporal_planes,
|
||||
width, height, map, n_lanes);
|
||||
}),
|
||||
py::arg("width"), py::arg("height"), py::arg("n_addr_lines"),
|
||||
py::arg("map"), py::arg("n_planes") = 10u,
|
||||
py::arg("n_temporal_planes") = 0u, py::arg("n_lanes") = 2)
|
||||
.def_readonly("width", &piomatter::matrix_geometry::width)
|
||||
.def_readonly("height", &piomatter::matrix_geometry::height);
|
||||
|
||||
|
|
@ -216,16 +256,16 @@ The default, 10, is the maximum value.
|
|||
HUB75 matrix driver for Raspberry Pi 5 using PIO
|
||||
|
||||
``colorspace`` controls the colorspace that will be used for data to be displayed.
|
||||
It must be one of the ``Colorspace`` constants. Which to use depends on what data
|
||||
It must be one of the `Colorspace` constants. Which to use depends on what data
|
||||
your displaying and how it is processed before copying into the framebuffer.
|
||||
|
||||
``pinout`` defines which pins the panels are wired to. Different pinouts can
|
||||
support different hardware breakouts and panels with different color order. The
|
||||
value must be one of the ``Pinout`` constants.
|
||||
value must be one of the `Pinout` constants.
|
||||
|
||||
``framebuffer`` a numpy array that holds pixel data in the appropriate colorspace.
|
||||
|
||||
``geometry`` controls the size and shape of the panel. The value must be a ``Geometry``
|
||||
``geometry`` controls the size and shape of the panel. The value must be a `Geometry`
|
||||
instance.
|
||||
)pbdoc")
|
||||
.def(py::init(&make_piomatter), py::arg("colorspace"),
|
||||
|
|
@ -239,55 +279,5 @@ data is triple-buffered to prevent tearing.
|
|||
)pbdoc")
|
||||
.def_property_readonly("fps", &PyPiomatter::fps, R"pbdoc(
|
||||
The approximate number of matrix refreshes per second.
|
||||
)pbdoc");
|
||||
|
||||
m.def(
|
||||
"AdafruitMatrixBonnetRGB565",
|
||||
[](py::buffer buffer, const piomatter::matrix_geometry &geometry) {
|
||||
return make_piomatter(Colorspace::RGB565,
|
||||
Pinout::AdafruitMatrixBonnet, buffer,
|
||||
geometry);
|
||||
},
|
||||
py::arg("buffer"), py::arg("geometry"),
|
||||
R"pbdoc(
|
||||
Construct a PioMatter object to drive panels connected to an
|
||||
Adafruit Matrix Bonnet using the RGB565 memory layout (2 bytes per
|
||||
pixel)
|
||||
|
||||
This is deprecated shorthand for `PioMatter(Colorspace.RGB565, Pinout.AdafruitMatrixBonnet, ...)`.
|
||||
)pbdoc");
|
||||
|
||||
m.def(
|
||||
"AdafruitMatrixBonnetRGB888",
|
||||
[](py::buffer buffer, const piomatter::matrix_geometry &geometry) {
|
||||
return make_piomatter(Colorspace::RGB888,
|
||||
Pinout::AdafruitMatrixBonnet, buffer,
|
||||
geometry);
|
||||
},
|
||||
py::arg("framebuffer"), py::arg("geometry"),
|
||||
R"pbdoc(
|
||||
Construct a PioMatter object to drive panels connected to an
|
||||
Adafruit Matrix Bonnet using the RGB888 memory layout (4 bytes per
|
||||
pixel)
|
||||
|
||||
This is deprecated shorthand for `PioMatter(Colorspace.RGB888, Pinout.AdafruitMatrixBonnet, ...)`.
|
||||
)pbdoc")
|
||||
//.doc() =
|
||||
;
|
||||
|
||||
m.def(
|
||||
"AdafruitMatrixBonnetRGB888Packed",
|
||||
[](py::buffer buffer, const piomatter::matrix_geometry &geometry) {
|
||||
return make_piomatter(Colorspace::RGB888Packed,
|
||||
Pinout::AdafruitMatrixBonnet, buffer,
|
||||
geometry);
|
||||
},
|
||||
py::arg("framebuffer"), py::arg("geometry"),
|
||||
R"pbdoc(
|
||||
Construct a PioMatter object to drive panels connected to an
|
||||
Adafruit Matrix Bonnet using the RGB888 packed memory layout (3
|
||||
bytes per pixel)
|
||||
|
||||
This is deprecated shorthand for `PioMatter(Colorspace.RGB888Packed, Pinout.AdafruitMatrixBonnet, ...)`.
|
||||
)pbdoc");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue