Compare commits

..

3 commits

Author SHA1 Message Date
Saji 95ca2447cb add geom properties
All checks were successful
Unit Tests / Test (push) Successful in 2m19s
2024-09-28 14:36:51 -05:00
Saji 5ae230ac00 make dual-mode data driver for hub75
add tests
2024-09-28 14:36:51 -05:00
Saji 2c88d73ded update lockfile 2024-09-28 00:41:15 -05:00
5 changed files with 245 additions and 39 deletions

View file

@ -41,7 +41,7 @@ dependencies = [
[[package]]
name = "basedpyright"
version = "1.17.5"
version = "1.18.0"
requires_python = ">=3.8"
summary = "static type checking for Python (but based)"
groups = ["dev"]
@ -49,8 +49,8 @@ dependencies = [
"nodejs-wheel-binaries>=20.13.1",
]
files = [
{file = "basedpyright-1.17.5-py3-none-any.whl", hash = "sha256:7ea23951be3620fe4638f6f51625a3d5a65a57ce004e836c608058610153362b"},
{file = "basedpyright-1.17.5.tar.gz", hash = "sha256:8315f5f2fddfdad162513ef9d9a0b24bb00cc862e8883819ae98a3e28472874d"},
{file = "basedpyright-1.18.0-py3-none-any.whl", hash = "sha256:96238749b870809719d9e99587346056ca68169803095901fc5e028c733c9b54"},
{file = "basedpyright-1.18.0.tar.gz", hash = "sha256:82a0cceb7a20bbdd8b801a2ea41a035bc101d94bc634157bcc2ecf0f7aff52f8"},
]
[[package]]
@ -205,27 +205,27 @@ files = [
[[package]]
name = "ruff"
version = "0.6.7"
version = "0.6.8"
requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust."
groups = ["dev"]
files = [
{file = "ruff-0.6.7-py3-none-linux_armv6l.whl", hash = "sha256:08277b217534bfdcc2e1377f7f933e1c7957453e8a79764d004e44c40db923f2"},
{file = "ruff-0.6.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c6707a32e03b791f4448dc0dce24b636cbcdee4dd5607adc24e5ee73fd86c00a"},
{file = "ruff-0.6.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:533d66b7774ef224e7cf91506a7dafcc9e8ec7c059263ec46629e54e7b1f90ab"},
{file = "ruff-0.6.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17a86aac6f915932d259f7bec79173e356165518859f94649d8c50b81ff087e9"},
{file = "ruff-0.6.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3f8822defd260ae2460ea3832b24d37d203c3577f48b055590a426a722d50ef"},
{file = "ruff-0.6.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba4efe5c6dbbb58be58dd83feedb83b5e95c00091bf09987b4baf510fee5c99"},
{file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:525201b77f94d2b54868f0cbe5edc018e64c22563da6c5c2e5c107a4e85c1c0d"},
{file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8854450839f339e1049fdbe15d875384242b8e85d5c6947bb2faad33c651020b"},
{file = "ruff-0.6.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f0b62056246234d59cbf2ea66e84812dc9ec4540518e37553513392c171cb18"},
{file = "ruff-0.6.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b1462fa56c832dc0cea5b4041cfc9c97813505d11cce74ebc6d1aae068de36b"},
{file = "ruff-0.6.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:02b083770e4cdb1495ed313f5694c62808e71764ec6ee5db84eedd82fd32d8f5"},
{file = "ruff-0.6.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c05fd37013de36dfa883a3854fae57b3113aaa8abf5dea79202675991d48624"},
{file = "ruff-0.6.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f49c9caa28d9bbfac4a637ae10327b3db00f47d038f3fbb2195c4d682e925b14"},
{file = "ruff-0.6.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a0e1655868164e114ba43a908fd2d64a271a23660195017c17691fb6355d59bb"},
{file = "ruff-0.6.7-py3-none-win32.whl", hash = "sha256:a939ca435b49f6966a7dd64b765c9df16f1faed0ca3b6f16acdf7731969deb35"},
{file = "ruff-0.6.7-py3-none-win_amd64.whl", hash = "sha256:590445eec5653f36248584579c06252ad2e110a5d1f32db5420de35fb0e1c977"},
{file = "ruff-0.6.7-py3-none-win_arm64.whl", hash = "sha256:b28f0d5e2f771c1fe3c7a45d3f53916fc74a480698c4b5731f0bea61e52137c8"},
{file = "ruff-0.6.7.tar.gz", hash = "sha256:44e52129d82266fa59b587e2cd74def5637b730a69c4542525dfdecfaae38bd5"},
{file = "ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2"},
{file = "ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c"},
{file = "ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5"},
{file = "ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f"},
{file = "ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb"},
{file = "ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f"},
{file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0"},
{file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87"},
{file = "ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098"},
{file = "ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0"},
{file = "ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750"},
{file = "ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce"},
{file = "ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa"},
{file = "ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44"},
{file = "ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a"},
{file = "ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263"},
{file = "ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc"},
{file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"},
]

View file

@ -14,6 +14,7 @@ class RGBLayout(data.StructLayout):
)
Rgb888Layout = RGBLayout(8, 8, 8)
Rgb666Layout = RGBLayout(6, 6, 6)
Rgb111Layout = RGBLayout(1, 1, 1)

View file

@ -86,9 +86,14 @@ class DisplayDimensions:
height: int
mux: int = 2 # number of lines driven at once.
@property
def addr_bits(self) -> int:
return ceil(log2(self.height / self.mux))
@property
def size(self) -> int:
return self.length * self.height
class DisplayRotation(Enum):
"""Display rotation enums. The names indicate the general direction
@ -162,13 +167,25 @@ class DisplayGeometry:
self.strict = strict
pass
def add_string(self, s: DisplayString):
"""Add a new string to the display. This new string is located at
a specific point, and has a direction, along with dimension that reveal
the number of address lines (typically 64, with 1:32 selection so 5 address
bits) and the total length of the string which is used to size the line
buffers.
@property
def n_strings(self) -> int:
"""Returns the number of strings in the display"""
return len(self._strings)
@property
def framebuffer_size(self) -> int:
"""Returns the total size of the display, in pixels.
NOTE: This currently only works with strict=true.
"""
sum = 0
for s in self._strings:
sum += s.dimensions.size
return sum
def add_string(self, s: DisplayString):
"""Add a new string to the display.
When in strict mode, this method will throw an exception if this new string
will overlap with an existing string.
"""

View file

@ -1,11 +1,11 @@
from amaranth import Module, Cat, Mux, ShapeLike, Signal, Assert
from amaranth import Module, Cat, Mux, ShapeLike, Signal, Assert, Array
from amaranth.build import Platform
from amaranth.lib import wiring, data
from amaranth.lib.wiring import In, Out
from amaranth.lib.memory import Memory, ReadPort, WritePort
from amaranth.utils import ceil_log2
from .common import Rgb666Layout, Hub75Stream, Hub75Ctrl, Hub75Data
from .common import Rgb666Layout, Hub75Stream, Hub75Ctrl, Hub75Data, Rgb888Layout
class SwapBuffer(wiring.Component):
@ -80,6 +80,128 @@ class SwapBuffer(wiring.Component):
return m
class Hub75DataDriver(wiring.Component):
def __init__(
self,
panel_length: int = 128,
data_shape=Rgb888Layout,
double_fetch=True,
*,
src_loc_at=0,
):
self.panel_length = panel_length
self.double_fetch = double_fetch
# if we're using double_fetch then each bram cell is exactly one color,
# if we're doing single fetch, it's 2 colors stacked.
bram_shape = data_shape if double_fetch else data.ArrayLayout(data_shape, 2)
addr_width = (
ceil_log2(panel_length * 2) if double_fetch else ceil_log2(panel_length)
)
super().__init__(
{
"bcm_select": In(ceil_log2(data_shape["red"].shape.width)),
"done": Out(1),
"start": In(1),
"bram": In(
ReadPort.Signature(
addr_width=addr_width,
shape=bram_shape,
)
),
"data": Out(Hub75Data()),
},
src_loc_at=src_loc_at,
)
def elaborate(self, platform: Platform) -> Module:
m = Module()
counter = Signal(32)
pixnum = Signal(range(self.panel_length), init=self.panel_length - 1)
if self.double_fetch:
pixrow = Signal(1)
m.d.comb += self.bram.addr.eq(Cat(pixrow, pixnum))
ram_rgb_slice = Cat(
self.bram.data["red"].bit_select(self.bcm_select, 1),
self.bram.data["blue"].bit_select(self.bcm_select, 1),
self.bram.data["green"].bit_select(self.bcm_select, 1),
)
else:
ram_rgb_slice = Array(
[
Cat(
self.bram.data[0]["red"].bit_select(self.bcm_select, 1),
self.bram.data[0]["blue"].bit_select(self.bcm_select, 1),
self.bram.data[0]["green"].bit_select(self.bcm_select, 1),
),
Cat(
self.bram.data[1]["red"].bit_select(self.bcm_select, 1),
self.bram.data[1]["blue"].bit_select(self.bcm_select, 1),
self.bram.data[1]["green"].bit_select(self.bcm_select, 1),
),
]
)
m.d.comb += self.bram.addr.eq(pixnum)
with m.FSM():
with m.State("init"):
m.d.sync += [
self.done.eq(0),
counter.eq(0),
pixnum.eq(pixnum.reset),
]
if self.double_fetch:
m.d.sync += pixrow.eq(0)
with m.If(self.start == 1):
m.d.sync += self.bram.en.eq(1)
m.next = "prefetch" if self.double_fetch else "writerow"
with m.State("prefetch"):
# TODO: do we need this
m.next = "writerow"
with m.State("writerow"):
if self.double_fetch:
c = counter[0:1]
with m.If(c == 0):
m.d.sync += [
self.data.rgb0.eq(ram_rgb_slice),
pixrow.eq(1),
]
with m.If(c == 1):
m.d.sync += self.data.rgb1.eq(ram_rgb_slice)
with m.If(c == 2):
pass
with m.If(c == 3):
m.d.sync += [
counter.eq(0),
pixnum.eq(pixnum - 1),
pixrow.eq(0),
]
with m.If(pixnum == 0):
m.next = "done"
else:
with m.If(counter[0] == 0):
m.d.sync += [
self.data.rgb0.eq(ram_rgb_slice[0]),
self.data.rgb1.eq(ram_rgb_slice[1]),
]
with m.If(counter[0] == 1):
m.d.sync += [
pixnum.eq(pixnum - 1),
counter.eq(0),
]
with m.If(pixnum == 0):
m.next = "done"
with m.State("done"):
m.d.sync += [self.done.eq(1), self.bram.en.eq(0)]
m.next = "init"
return m
class Hub75StringDriver(wiring.Component):
"""A data driver for Hub75 panels. This accesses the line memory and feeds out the data.
It is controlled by a Hub75Coordinator to signal when it should run and what bit of the data
@ -126,7 +248,7 @@ class Hub75StringDriver(wiring.Component):
]
m.d.sync += Assert(self.bcm_select < 6)
pixnum = Signal(ceil_log2(self.panel_length), init=self.panel_length - 1)
pixnum = Signal(range(self.panel_length), init=self.panel_length - 1)
m.d.comb += self.bram_port.addr.eq(pixnum)
with m.FSM():
@ -180,11 +302,12 @@ class Hub75Coordinator(wiring.Component):
donearr = []
startStrings = Signal(1)
stringsDone = Signal(1)
for i in range(self.n_strings):
sb = SwapBuffer(depth=128, shape=data.ArrayLayout(Rgb666Layout, 2))
bufs += sb
bufs.append(sb)
stringdriver = Hub75StringDriver(128)
strings += stringdriver
strings.append(stringdriver)
wiring.connect(m, sb.read_port, stringdriver.bram_port)
m.d.comb += [
self.data[i].eq(stringdriver.display_out),
@ -192,12 +315,41 @@ class Hub75Coordinator(wiring.Component):
sb.selector.eq(swapline),
]
m.submodules += [sb, stringdriver]
donearr += stringdriver.done
donearr.append(stringdriver.done)
# combine the done signals into one signal with AND-reduction
m.d.comb += stringsDone.eq(Cat(*donearr).all())
self.addr = Signal(5)
# handle the fetch side.
# WIP: pass in fetcher/pixgen/geometry.
# right now we assume that it's just one panel,
# address is (string_number, hi/lo, addr)
for i in range(self.n_strings):
lookup_addr = Cat(i, self.addr, 0)
# generate a sequence of transfers.
with m.FSM():
with m.State("init"):
# go to preload.
pass
with m.State("preload"):
pass
with m.State("run"):
pass
with m.State("swap"):
pass
return m
# fetch line
#
class Hub75EDriver(wiring.Component):
"""An optimized driver for hub75 strings.
This version is faster than most implementations by merging the exposure

View file

@ -1,11 +1,12 @@
from amaranth import Array, Module, Cat, Signal, Assert, unsigned
from amaranth.build import Platform
from amaranth import Module
from amaranth.lib import wiring, data
from amaranth.lib.wiring import In, Out
from amaranth.lib.memory import Memory, WritePort
from amaranth.lib.memory import Memory
from amaranth.sim import Simulator
import pytest
from ..hub75 import Hub75Coordinator, Hub75StringDriver, Rgb666Layout
from groovylight.common import Rgb888Layout
from ..hub75 import Hub75Coordinator, Hub75DataDriver, Hub75StringDriver, Rgb666Layout
def test_stringdriver():
@ -38,6 +39,41 @@ def test_stringdriver():
sim.run_until(1e-6 * 1000)
def test_datadriver():
# the string driver test must
# 1. finish
# 2. strobe through all of the data in the array
# 3. slice the correct bit from the data.
m = Module()
m.submodules.dut = dut = Hub75DataDriver()
m.submodules.mem = mem = Memory(shape=Rgb888Layout, depth=256, init=[])
port = mem.read_port()
wiring.connect(m, port, dut.bram)
async def testbench(ctx):
# select a bit, strobe start, read values, test against known.
ctx.set(dut.bcm_select, 5)
ctx.set(dut.start, 1)
await ctx.tick()
ctx.set(dut.start, 0)
assert ctx.get(dut.bram_port.en) == 1
pass
sim = Simulator(m)
sim.add_clock(1e-6)
with sim.write_vcd("output.vcd"):
sim.run_until(1e-6 * 1000)
@pytest.mark.skip()
def test_hub75():
m = Module()
m.submodules.dut = dut = Hub75Coordinator(1)
sim = Simulator(m)
sim.add_clock(1e-6)
with sim.write_vcd("output.vcd"):
sim.run_until(1e-6 * 1000)