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:
foamyguy 2025-03-11 09:40:55 -05:00 committed by GitHub
commit 8d3355fca3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 628 additions and 199 deletions

1
.gitignore vendored
View file

@ -1,4 +1,3 @@
/*.pio.h /*.pio.h
protodemo
/build /build
*.egg-info *.egg-info

View file

@ -0,0 +1,8 @@
HUB75 matrix driver for Raspberry Pi 5 using PIO
------------------------------------------------
.. autosummary::
:toctree: _generate
:recursive:
adafruit_blinka_raspberry_pi5_piomatter

View file

@ -1 +0,0 @@
.. automodule:: adafruit_blinka_raspberry_pi5_piomatter

View file

@ -26,13 +26,27 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
"sphinx.ext.autodoc", "autoapi.extension",
"sphinx.ext.intersphinx", "sphinx.ext.intersphinx",
"sphinx.ext.autosummary", "sphinx.ext.autosummary",
"sphinx.ext.napoleon", "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. # Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"] templates_path = ["_templates"]
@ -50,7 +64,7 @@ master_doc = "index"
# General information about the project. # General information about the project.
project = "adafruit-blinka-pi5-piomatter" project = "adafruit-blinka-pi5-piomatter"
copyright = "2023 Jeff Epler" copyright = "2025 Jeff Epler"
author = "Jeff Epler" author = "Jeff Epler"
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for

View file

@ -3,5 +3,6 @@
# SPDX-License-Identifier: Unlicense # SPDX-License-Identifier: Unlicense
sphinx sphinx
sphinx-autoapi
sphinx-rtd-theme sphinx-rtd-theme
sphinxcontrib-jquery sphinxcontrib-jquery

View file

@ -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 click
import numpy as np 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: with open("/sys/class/graphics/fb0/virtual_size") as f:
screenx, screeny = [int(word) for word in f.read().split(",")] 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) 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.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("--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) @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 @piomatter_click.standard_options
def main(xoffset, yoffset, width, height, serpentine, rotation, pinout, n_planes, n_addr_lines): def main(xoffset, yoffset, width, height, serpentine, rotation, pinout, n_planes, n_temporal_planes, n_addr_lines, n_lanes):
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_addr_lines=n_addr_lines, n_temporal_planes=n_temporal_planes, rotation=rotation)
framebuffer = np.zeros(shape=(geometry.height, geometry.width), dtype=dtype) framebuffer = np.zeros(shape=(geometry.height, geometry.width), dtype=dtype)
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB565, pinout=pinout, framebuffer=framebuffer, geometry=geometry) matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB565, pinout=pinout, framebuffer=framebuffer, geometry=geometry)

View file

@ -37,11 +37,13 @@ or if `/boot/firmware/cmdline.txt` specifies a resolution such as
`... video=HDMI-A-1:640x480M@60D`. `... video=HDMI-A-1:640x480M@60D`.
""" """
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
import click import click
import numpy as np import numpy as np
import PIL.Image as Image 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: with open("/sys/class/graphics/fb0/virtual_size") as f:
screenx, screeny = [int(word) for word in f.read().split(",")] 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("--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) @click.option("--scale", "scale", type=int, help="The scale factor to reduce the display down by.", default=3)
@piomatter_click.standard_options @piomatter_click.standard_options
def main(xoffset, yoffset, scale, width, height, serpentine, rotation, pinout, n_planes, n_addr_lines): def main(xoffset, yoffset, scale, width, height, serpentine, rotation, pinout, n_planes, n_temporal_planes, n_addr_lines, n_lanes):
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)
matrix_framebuffer = np.zeros(shape=(geometry.height, geometry.width, 3), dtype=np.uint8) 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) matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=pinout, framebuffer=matrix_framebuffer, geometry=geometry)

View file

@ -11,10 +11,11 @@ The animated gif is played repeatedly until interrupted with ctrl-c.
import time import time
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
import numpy as np import numpy as np
import PIL.Image as Image import PIL.Image as Image
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
width = 64 width = 64
height = 32 height = 32

View file

@ -13,10 +13,11 @@ import glob
import sys import sys
import time import time
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
import numpy as np import numpy as np
import PIL.Image as Image import PIL.Image as Image
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
images = sorted(glob.glob(sys.argv[1])) images = sorted(glob.glob(sys.argv[1]))
geometry = piomatter.Geometry(width=64, height=32, n_addr_lines=4, rotation=piomatter.Orientation.Normal) geometry = piomatter.Geometry(width=64, height=32, n_addr_lines=4, rotation=piomatter.Orientation.Normal)

View file

@ -14,11 +14,12 @@ $ python quote_scroller.py
""" """
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
import numpy as np import numpy as np
import requests import requests
from PIL import Image, ImageDraw, ImageFont 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. # 128px for 2x1 matrices. Change to 64 if you're using a single matrix.
total_width = 128 total_width = 128
total_height = 32 total_height = 32

View file

@ -10,11 +10,12 @@ Run like this:
$ python rainbow_spiral.py $ python rainbow_spiral.py
""" """
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
import numpy as np import numpy as np
import rainbowio import rainbowio
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
width = 64 width = 64
height = 32 height = 32
pen_radius = 1 pen_radius = 1

View 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")

View file

@ -13,10 +13,11 @@ The image is displayed until the user hits enter to exit.
import pathlib import pathlib
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
import numpy as np import numpy as np
import PIL.Image as Image 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) 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")) framebuffer = np.asarray(Image.open(pathlib.Path(__file__).parent / "blinka64x64.png"))
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed,

View file

@ -13,10 +13,11 @@ The image is displayed until the user hits enter to exit.
import pathlib import pathlib
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
import numpy as np import numpy as np
import PIL.Image as Image 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) 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")) 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) matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=piomatter.Pinout.AdafruitMatrixBonnetBGR, framebuffer=framebuffer, geometry=geometry)

View file

@ -11,10 +11,11 @@ $ python simpletest.py
""" """
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
import numpy as np import numpy as np
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
width = 64 width = 64
height = 32 height = 32

View file

@ -26,12 +26,13 @@ Here's an example for running an emulator using a rom stored in "/tmp/snesrom.sm
import shlex import shlex
from subprocess import Popen from subprocess import Popen
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
import click import click
import numpy as np import numpy as np
import piomatter_click
from pyvirtualdisplay.smartdisplay import SmartDisplay 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.command
@click.option("--scale", type=float, help="The scale factor, larger numbers mean more virtual pixels", default=1) @click.option("--scale", type=float, help="The scale factor, larger numbers mean more virtual pixels", default=1)

View file

@ -29,12 +29,14 @@ import termios
import tty import tty
from subprocess import Popen, run from subprocess import Popen, run
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
import click import click
import numpy as np import numpy as np
import piomatter_click
from pyvirtualdisplay.smartdisplay import SmartDisplay 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 keyboard_debug = False
keys_down = set() keys_down = set()
basic_characters = string.ascii_letters + string.digits 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) @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 @piomatter_click.standard_options
@click.argument("command", nargs=-1) @click.argument("command", nargs=-1)
def main(scale, backend, use_xauth, extra_args, rfbport, width, height, serpentine, rotation, pinout, n_planes, def main(scale, backend, use_xauth, extra_args, rfbport, width, height, serpentine, rotation, pinout, n_planes, n_temporal_planes,
n_addr_lines, ctrl_c_interrupt, command): n_addr_lines, n_lanes, ctrl_c_interrupt, command):
def handle_key_event(evt_data): def handle_key_event(evt_data):
if evt_data in key_map.keys(): if evt_data in key_map.keys():
keys_down.add(key_map[evt_data]) 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: if extra_args:
kwargs['extra_args'] = shlex.split(extra_args) kwargs['extra_args'] = shlex.split(extra_args)
print("xauth", use_xauth) print("xauth", use_xauth)
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, if n_lanes != 2:
rotation=rotation) 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) framebuffer = np.zeros(shape=(geometry.height, geometry.width, 3), dtype=np.uint8)
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=pinout, framebuffer=framebuffer, matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=pinout, framebuffer=framebuffer,
geometry=geometry) geometry=geometry)

View file

@ -9,14 +9,15 @@ build-backend = "setuptools.build_meta"
[tool.setuptools_scm] [tool.setuptools_scm]
[tool.ruff] [tool.ruff]
extend-select = [ lint.extend-select = [
"B", # flake8-bugbear "B", # flake8-bugbear
"I", # isort "I", # isort
"PGH", # pygrep-hooks "PGH", # pygrep-hooks
"RUF", # Ruff-specific "RUF", # Ruff-specific
"UP", # pyupgrade "UP", # pyupgrade
] ]
extend-ignore = [ lint.extend-ignore = [
"E501", # Line too long "E501", # Line too long
"RUF002", # Yes I meant to type 'multiplication sign'!
] ]
target-version = "py311" target-version = "py311"

View file

@ -11,7 +11,7 @@ __version__ = get_version()
# say from a submodule. # say from a submodule.
ext_modules = [ 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"], ["src/pymain.cpp", "src/piolib/piolib.c", "src/piolib/pio_rp1.c"],
define_macros = [('VERSION_INFO', __version__)], define_macros = [('VERSION_INFO', __version__)],
include_dirs = ['./src/include', './src/piolib/include'], include_dirs = ['./src/include', './src/piolib/include'],
@ -33,6 +33,8 @@ setup(
cmdclass={"build_ext": build_ext}, cmdclass={"build_ext": build_ext},
zip_safe=False, zip_safe=False,
python_requires=">=3.11", 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={ extras_require={
'docs': ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-jquery"], 'docs': ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-jquery"],
}, },

View 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',
]

View file

@ -5,11 +5,12 @@
from collections.abc import Callable from collections.abc import Callable
from typing import Any from typing import Any
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
import click 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): def __init__(self, enum, case_sensitive=False):
self.enum = enum self.enum = enum
choices = [k for k, v in enum.__dict__.items() if isinstance(v, 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) r = getattr(self.enum, value)
return r 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( def standard_options(
f: click.decorators.FC | None = None, f: click.decorators.FC | None = None,
*, *,
@ -34,7 +40,9 @@ def standard_options(
rotation=piomatter.Orientation.Normal, rotation=piomatter.Orientation.Normal,
pinout=piomatter.Pinout.AdafruitMatrixBonnet, pinout=piomatter.Pinout.AdafruitMatrixBonnet,
n_planes=10, n_planes=10,
n_temporal_planes=0,
n_addr_lines=4, n_addr_lines=4,
n_lanes=2,
) -> Callable[[], None]: ) -> Callable[[], None]:
"""Add standard commandline flags, with the defaults given """Add standard commandline flags, with the defaults given
@ -61,7 +69,7 @@ def standard_options(
f = click.option( f = click.option(
"--pinout", "--pinout",
default=pinout, default=pinout,
type=PybindEnumChoice(piomatter.Pinout), type=_PybindEnumChoice(piomatter.Pinout),
help="The details of the electrical connection to the panels" help="The details of the electrical connection to the panels"
)(f) )(f)
if rotation is not None: if rotation is not None:
@ -69,13 +77,17 @@ def standard_options(
"--orientation", "--orientation",
"rotation", "rotation",
default=rotation, default=rotation,
type=PybindEnumChoice(piomatter.Orientation), type=_PybindEnumChoice(piomatter.Orientation),
help="The overall orientation (rotation) of the panels" help="The overall orientation (rotation) of the panels"
)(f) )(f)
if n_planes is not None: 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: 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) 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 return f
if f is None: if f is None:
return wrapper return wrapper

View 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

View file

@ -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; size_t panel_height = 2 << n_addr_lines;
if (height % panel_height != 0) { 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; 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; 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 { struct matrix_geometry {
template <typename Cb> template <typename Cb>
matrix_geometry(size_t pixels_across, size_t n_addr_lines, int n_planes, 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), : pixels_across(pixels_across), n_addr_lines(n_addr_lines),
n_planes(n_planes), width(width), n_lanes(n_lanes), width(width), height(height),
height(height), map{make_matrixmap(width, height, n_addr_lines, map(map), schedules{schedules} {
serpentine, cb)} { size_t pixels_down = n_lanes << n_addr_lines;
size_t pixels_down = 2u << n_addr_lines;
if (map.size() != pixels_down * pixels_across) { if (map.size() != pixels_down * pixels_across) {
throw std::range_error( throw std::range_error(
"map size does not match calculated pixel count"); "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; size_t width, height;
matrix_map map; matrix_map map;
schedule_sequence schedules;
}; };
} // namespace piomatter } // namespace piomatter

View file

@ -40,4 +40,42 @@ struct adafruit_matrix_bonnet_pinout_bgr {
static constexpr uint32_t post_addr_delay = 500; 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 } // namespace piomatter

View file

@ -49,22 +49,31 @@ template <class pinout = adafruit_matrix_bonnet_pinout,
class colorspace = colorspace_rgb888> class colorspace = colorspace_rgb888>
struct piomatter : piomatter_base { struct piomatter : piomatter_base {
using buffer_type = std::vector<uint32_t>; using buffer_type = std::vector<uint32_t>;
using bufseq_type = std::vector<buffer_type>;
piomatter(std::span<typename colorspace::data_type const> framebuffer, piomatter(std::span<typename colorspace::data_type const> framebuffer,
const matrix_geometry &geometry) const matrix_geometry &geometry)
: framebuffer(framebuffer), geometry{geometry}, converter{}, : framebuffer(framebuffer), geometry{geometry}, converter{},
blitter_thread{&piomatter::blit_thread, this} { blitter_thread{} {
if (geometry.n_addr_lines > std::size(pinout::PIN_ADDR)) { if (geometry.n_addr_lines > std::size(pinout::PIN_ADDR)) {
throw std::runtime_error("too many address lines requested"); throw std::runtime_error("too many address lines requested");
} }
program_init(); program_init();
blitter_thread = std::move(std::thread{&piomatter::blit_thread, this});
show(); show();
} }
void show() override { void show() override {
int buffer_idx = manager.get_free_buffer(); 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); 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); manager.put_filled_buffer(buffer_idx);
} }
@ -160,26 +169,27 @@ struct piomatter : piomatter_base {
} }
void blit_thread() { void blit_thread() {
const uint32_t *databuf = nullptr; int cur_buffer_idx = buffer_manager::no_buffer;
size_t datasize = 0;
int old_buffer_idx = buffer_manager::no_buffer;
int buffer_idx; int buffer_idx;
int seq_idx = -1;
uint64_t t0, t1; uint64_t t0, t1;
t0 = monotonicns64(); t0 = monotonicns64();
while ((buffer_idx = manager.get_filled_buffer()) != while ((buffer_idx = manager.get_filled_buffer()) !=
buffer_manager::exit_request) { buffer_manager::exit_request) {
if (buffer_idx != buffer_manager::no_buffer) { if (buffer_idx != buffer_manager::no_buffer) {
const auto &buffer = buffers[buffer_idx]; if (cur_buffer_idx != buffer_manager::no_buffer) {
databuf = &buffer[0]; manager.put_free_buffer(cur_buffer_idx);
datasize = buffer.size() * sizeof(*databuf);
if (old_buffer_idx != buffer_manager::no_buffer) {
manager.put_free_buffer(old_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, pio_sm_xfer_data_large(pio, sm, PIO_DIR_TO_SM, datasize,
(uint32_t *)databuf); dataptr);
t1 = monotonicns64(); t1 = monotonicns64();
if (t0 != t1) { if (t0 != t1) {
fps = 1e9 / (t1 - t0); fps = 1e9 / (t1 - t0);
@ -194,7 +204,7 @@ struct piomatter : piomatter_base {
PIO pio = NULL; PIO pio = NULL;
int sm = -1; int sm = -1;
std::span<typename colorspace::data_type const> framebuffer; std::span<typename colorspace::data_type const> framebuffer;
buffer_type buffers[3]; bufseq_type buffers[3];
buffer_manager manager{}; buffer_manager manager{};
matrix_geometry geometry; matrix_geometry geometry;
colorspace converter; colorspace converter;

View file

@ -3,7 +3,7 @@
const int protomatter_wrap = 4; const int protomatter_wrap = 4;
const int protomatter_wrap_target = 0; const int protomatter_wrap_target = 0;
const int protomatter_sideset_pin_count = 1; const int protomatter_sideset_pin_count = 1;
const bool protomatter_sideset_enable = true; const bool protomatter_sideset_enable = 1;
const uint16_t protomatter[] = { const uint16_t protomatter[] = {
// ; data format (out-shift-right): // ; data format (out-shift-right):
// ; MSB ... LSB // ; MSB ... LSB

View file

@ -132,6 +132,7 @@ struct colorspace_rgb10 {
template <typename pinout> template <typename pinout>
void protomatter_render_rgb10(std::vector<uint32_t> &result, void protomatter_render_rgb10(std::vector<uint32_t> &result,
const matrix_geometry &matrixmap, const matrix_geometry &matrixmap,
const schedule &sched, uint32_t old_active_time,
const uint32_t *pixels) { const uint32_t *pixels) {
result.clear(); result.clear();
@ -153,7 +154,7 @@ void protomatter_render_rgb10(std::vector<uint32_t> &result,
data_count = n; 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) { auto do_data_clk_active = [&active_time, &data_count, &result](uint32_t d) {
bool active = active_time > 0; bool active = active_time > 0;
@ -183,68 +184,42 @@ void protomatter_render_rgb10(std::vector<uint32_t> &result,
return data; 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 // illuminate the right row for data in the shift register (the previous
// address) // address)
const size_t n_addr = 1u << matrixmap.n_addr_lines; 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; const size_t pixels_across = matrixmap.pixels_across;
size_t prev_addr = n_addr - 1; size_t prev_addr = n_addr - 1;
uint32_t addr_bits = calc_addr_bits(prev_addr); uint32_t addr_bits = calc_addr_bits(prev_addr);
for (size_t addr = 0; addr < n_addr; addr++) { for (size_t addr = 0; addr < n_addr; addr++) {
// printf("addr=%zu/%zu\n", addr, n_addr); for (auto &schedule_ent : sched) {
for (int bit = n_planes - 1; bit >= 0; bit--) { uint32_t r_mask = 1 << (20 + schedule_ent.shift);
// printf("bit=%d/%d\n", bit, n_planes); uint32_t g_mask = 1 << (10 + schedule_ent.shift);
uint32_t r = 1 << (20 + offset + bit); uint32_t b_mask = 1 << (0 + schedule_ent.shift);
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;
prep_data(pixels_across); 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++) { 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()); assert(mapiter != matrixmap.map.end());
auto pixel0 = pixels[*mapiter++]; auto pixel0 = pixels[*mapiter++];
auto r0 = pixel0 & r; auto r_bit = pixel0 & r_mask;
auto g0 = pixel0 & g; auto g_bit = pixel0 & g_mask;
auto b0 = pixel0 & b; auto b_bit = pixel0 & b_mask;
assert(mapiter != matrixmap.map.end());
auto pixel1 = pixels[*mapiter++];
auto r1 = pixel1 & r;
auto g1 = pixel1 & g;
auto b1 = pixel1 & b;
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, 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, do_data_delay(addr_bits | pinout::oe_inactive | pinout::lat_bit,
pinout::post_latch_delay); pinout::post_latch_delay);
active_time = schedule_ent.active_time;
// with oe inactive, set address bits to illuminate THIS line // with oe inactive, set address bits to illuminate THIS line
if (addr != prev_addr) { if (addr != prev_addr) {
addr_bits = calc_addr_bits(addr); addr_bits = calc_addr_bits(addr);

View file

@ -95,10 +95,56 @@ static uint64_t monotonicns64() {
return tp.tv_sec * UINT64_C(1000000000) + tp.tv_nsec; 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 main(int argc, char **argv) {
int n = argc > 1 ? atoi(argv[1]) : 0; 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::orientation_normal);
piomatter::piomatter p(std::span(&pixels[0][0], 64 * 64), geometry); piomatter::piomatter p(std::span(&pixels[0][0], 64 * 64), geometry);

View file

@ -1,5 +1,6 @@
#include <iostream> #include <iostream>
#include <pybind11/pybind11.h> #include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <string> #include <string>
#include "piomatter/piomatter.h" #include "piomatter/piomatter.h"
@ -33,6 +34,14 @@ make_piomatter_pc(py::buffer buffer,
const py::buffer_info info = buffer.request(); const py::buffer_info info = buffer.request();
const size_t buffer_size_in_bytes = info.size * info.itemsize; 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) { if (buffer_size_in_bytes != data_size_in_bytes) {
throw std::runtime_error( throw std::runtime_error(
py::str("Framebuffer size must be {} bytes ({} elements of {} " py::str("Framebuffer size must be {} bytes ({} elements of {} "
@ -54,6 +63,8 @@ enum Colorspace { RGB565, RGB888, RGB888Packed };
enum Pinout { enum Pinout {
AdafruitMatrixBonnet, AdafruitMatrixBonnet,
AdafruitMatrixBonnetBGR, AdafruitMatrixBonnetBGR,
Active3,
Active3BGR,
}; };
template <class pinout> template <class pinout>
@ -70,13 +81,11 @@ make_piomatter_p(Colorspace c, py::buffer buffer,
case RGB888Packed: case RGB888Packed:
return make_piomatter_pc<pinout, piomatter::colorspace_rgb888_packed>( return make_piomatter_pc<pinout, piomatter::colorspace_rgb888_packed>(
buffer, geometry); buffer, geometry);
}
default:
throw std::runtime_error(py::str("Invalid colorspace {!r}") throw std::runtime_error(py::str("Invalid colorspace {!r}")
.attr("format")(c) .attr("format")(c)
.template cast<std::string>()); .template cast<std::string>());
} }
}
std::unique_ptr<PyPiomatter> std::unique_ptr<PyPiomatter>
make_piomatter(Colorspace c, Pinout p, py::buffer buffer, make_piomatter(Colorspace c, Pinout p, py::buffer buffer,
@ -88,15 +97,19 @@ make_piomatter(Colorspace c, Pinout p, py::buffer buffer,
case AdafruitMatrixBonnetBGR: case AdafruitMatrixBonnetBGR:
return make_piomatter_p<piomatter::adafruit_matrix_bonnet_pinout_bgr>( return make_piomatter_p<piomatter::adafruit_matrix_bonnet_pinout_bgr>(
c, buffer, geometry); 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}") throw std::runtime_error(py::str("Invalid pinout {!r}")
.attr("format")(p) .attr("format")(p)
.template cast<std::string>()); .template cast<std::string>());
} }
}
} // namespace } // namespace
PYBIND11_MODULE(adafruit_blinka_raspberry_pi5_piomatter, m) { PYBIND11_MODULE(_piomatter, m) {
py::options options; py::options options;
options.enable_enum_members_docstring(); options.enable_enum_members_docstring();
options.enable_function_signatures(); 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 HUB75 matrix driver for Raspberry Pi 5 using PIO
------------------------------------------------ ------------------------------------------------
.. currentmodule:: adafruit_blinka_raspberry_pi5_piomatter .. currentmodule:: adafruit_blinka_raspberry_pi5_piomatter._piomatter
.. autosummary::
:toctree: _generate
Orientation
Pinout
Colorspace
Geometry
PioMatter
AdafruitMatrixBonnetRGB888
AdafruitMatrixBonnetRGB888Packed
)pbdoc"; )pbdoc";
py::enum_<piomatter::orientation>( py::enum_<piomatter::orientation>(
@ -138,7 +140,10 @@ PYBIND11_MODULE(adafruit_blinka_raspberry_pi5_piomatter, m) {
.value("AdafruitMatrixHat", Pinout::AdafruitMatrixBonnet, .value("AdafruitMatrixHat", Pinout::AdafruitMatrixBonnet,
"Adafruit Matrix Bonnet or Matrix Hat") "Adafruit Matrix Bonnet or Matrix Hat")
.value("AdafruitMatrixHatBGR", Pinout::AdafruitMatrixBonnetBGR, .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>( py::enum_<Colorspace>(
m, "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. 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. ``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 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`` ``rotation`` controls the orientation of the panel(s). Must be one of the ``Orientation``
constants. Default is ``Orientation.Normal``. constants. Default is ``Orientation.Normal``.
``n_planes`` controls the color depth of the panel. This is separate from the framebuffer For panels with more than 2 lanes, or using a custom pixel mapping, the following constructor arguments are available:
layout. Decreasing ``n_planes`` can increase FPS at the cost of reduced color fidelity.
The default, 10, is the maximum value. ``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") )pbdoc")
.def(py::init([](size_t width, size_t height, size_t n_addr_lines, .def(py::init([](size_t width, size_t height, size_t n_addr_lines,
bool serpentine, piomatter::orientation rotation, 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 n_lines = 2 << n_addr_lines;
size_t pixels_across = width * height / n_lines; size_t pixels_across = width * height / n_lines;
size_t odd = (width * height) % n_lines; size_t odd = (width * height) % n_lines;
@ -185,30 +204,51 @@ The default, 10, is the maximum value.
switch (rotation) { switch (rotation) {
case piomatter::orientation::normal: case piomatter::orientation::normal:
return piomatter::matrix_geometry( return piomatter::matrix_geometry(
pixels_across, n_addr_lines, n_planes, width, height, pixels_across, n_addr_lines, n_planes,
serpentine, piomatter::orientation_normal); n_temporal_planes, width, height, serpentine,
piomatter::orientation_normal);
case piomatter::orientation::r180: case piomatter::orientation::r180:
return piomatter::matrix_geometry( return piomatter::matrix_geometry(
pixels_across, n_addr_lines, n_planes, width, height, pixels_across, n_addr_lines, n_planes,
serpentine, piomatter::orientation_r180); n_temporal_planes, width, height, serpentine,
piomatter::orientation_r180);
case piomatter::orientation::ccw: case piomatter::orientation::ccw:
return piomatter::matrix_geometry( return piomatter::matrix_geometry(
pixels_across, n_addr_lines, n_planes, width, height, pixels_across, n_addr_lines, n_planes,
serpentine, piomatter::orientation_ccw); n_temporal_planes, width, height, serpentine,
piomatter::orientation_ccw);
case piomatter::orientation::cw: case piomatter::orientation::cw:
return piomatter::matrix_geometry( return piomatter::matrix_geometry(
pixels_across, n_addr_lines, n_planes, width, height, pixels_across, n_addr_lines, n_planes,
serpentine, piomatter::orientation_cw); n_temporal_planes, width, height, serpentine,
piomatter::orientation_cw);
} }
throw std::runtime_error("invalid rotation"); throw std::runtime_error("invalid rotation");
}), }),
py::arg("width"), py::arg("height"), py::arg("n_addr_lines"), py::arg("width"), py::arg("height"), py::arg("n_addr_lines"),
py::arg("serpentine") = true, py::arg("serpentine") = true,
py::arg("rotation") = piomatter::orientation::normal, 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("width", &piomatter::matrix_geometry::width)
.def_readonly("height", &piomatter::matrix_geometry::height); .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 HUB75 matrix driver for Raspberry Pi 5 using PIO
``colorspace`` controls the colorspace that will be used for data to be displayed. ``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. your displaying and how it is processed before copying into the framebuffer.
``pinout`` defines which pins the panels are wired to. Different pinouts can ``pinout`` defines which pins the panels are wired to. Different pinouts can
support different hardware breakouts and panels with different color order. The 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. ``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. instance.
)pbdoc") )pbdoc")
.def(py::init(&make_piomatter), py::arg("colorspace"), .def(py::init(&make_piomatter), py::arg("colorspace"),
@ -239,55 +279,5 @@ data is triple-buffered to prevent tearing.
)pbdoc") )pbdoc")
.def_property_readonly("fps", &PyPiomatter::fps, R"pbdoc( .def_property_readonly("fps", &PyPiomatter::fps, R"pbdoc(
The approximate number of matrix refreshes per second. 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"); )pbdoc");
} }