add displaystrobe, config/geom fixes

This commit is contained in:
saji 2024-10-05 00:24:16 -05:00 committed by Saji
parent 88426ac89a
commit 3187db40ae
8 changed files with 107 additions and 29 deletions

1
.gitignore vendored
View file

@ -207,3 +207,4 @@ cython_debug/
#.idea/ #.idea/
result result
.aider*

View file

@ -31,22 +31,22 @@ ip = "dhcp" # Can also be e.g. "192.168.0.123"
strict = true # allows some wacky configurations, like panels that overlap. strict = true # allows some wacky configurations, like panels that overlap.
[[display.strings]] [[display.strings]]
position = { x = 31, y = 0 } position = { x = 64, y = 0 }
dimensions = { width = 256, height = 64 } dimensions = { length = 256, height = 64 }
rotation = "UPDOWN" rotation = "UPDOWN"
[[display.strings]] [[display.strings]]
position = { x = 32, y = 0 } position = { x = 65, y = 0 }
dimensions = { width = 256, height = 64 } dimensions = { length = 256, height = 64 }
rotation = "LEFTRIGHT" rotation = "LEFTRIGHT"
[[display.strings]] [[display.strings]]
position = { x = 32, y = 64 } position = { x = 65, y = 65 }
dimensions = { width = 256, height = 64 } dimensions = { length = 256, height = 64 }
rotation = "LEFTRIGHT" rotation = "LEFTRIGHT"
[[display.strings]] [[display.strings]]
position = { x = 32, y = 128 } position = { x = 65, y = 130 }
dimensions = { width = 256, height = 64 } dimensions = { length = 256, height = 64 }
rotation = "LEFTRIGHT" rotation = "LEFTRIGHT"
[[display.strings]] [[display.strings]]
position = { x = 32, y = 192 } position = { x = 65, y = 195 }
dimensions = { width = 256, height = 64 } dimensions = { length = 256, height = 64 }
rotation = "LEFTRIGHT" rotation = "LEFTRIGHT"

View file

@ -25,8 +25,8 @@ Rgb111Layout = RGBLayout(1, 1, 1)
class RGBView(data.View): class RGBView(data.View):
def channel_size(self) -> int: def bit_depth(self) -> int:
return self.red.shape() return max(self.red.shape(), self.green.shape(), self.blue.shape())
def channel_slice(self, bit: int) -> Rgb111Layout: def channel_slice(self, bit: int) -> Rgb111Layout:
"""Select bits from each channel and use it to form an Rgb111Layout. """Select bits from each channel and use it to form an Rgb111Layout.
This is useful for BCM stuff, since the bits are sliced to form a bitplane. This is useful for BCM stuff, since the bits are sliced to form a bitplane.

View file

@ -1,5 +1,4 @@
import tomllib import tomllib
from pathlib import Path
from groovylight.geom import ( from groovylight.geom import (
Coord, Coord,
@ -11,15 +10,15 @@ from groovylight.geom import (
class Config: class Config:
def __init__(self, file: Path) -> None: def __init__(self, file) -> None:
with file.open("rb") as f: self.conf = tomllib.load(file)
self.conf = tomllib.load(f) print
self.geom = DisplayGeometry(self.conf.display.strict) self.geom = DisplayGeometry(strict=self.conf["display"]["strict"])
for string in self.conf.display.strings: for string in self.conf["display"]["strings"]:
pos = Coord(**string.position) pos = Coord(**string['position'])
dims = DisplayDimensions(**string.dimensions) dims = DisplayDimensions(**string["dimensions"])
rot = DisplayRotation[string.rotation] rot = DisplayRotation[string["rotation"]]
disp = DisplayString(pos, dims, rot) disp = DisplayString(pos, dims, rot)
self.geom.add_string(disp) self.geom.add_string(disp)
self.network = self.conf.network # FIXME: parse this properly. self.network = self.conf['network'] # FIXME: parse this properly.

View file

@ -192,6 +192,6 @@ class DisplayGeometry:
if self.strict: if self.strict:
for e in self._strings: for e in self._strings:
if e.intersects(s): if e.intersects(s.bbox):
raise RuntimeError(f"node {e} intersects with {s}") raise RuntimeError(f"node {e} intersects with {s}")
self._strings.append(s) self._strings.append(s)

View file

@ -4,10 +4,14 @@ from amaranth.lib import wiring, data
from amaranth.lib.wiring import In, Out from amaranth.lib.wiring import In, Out
from amaranth.lib.memory import Memory, ReadPort, WritePort from amaranth.lib.memory import Memory, ReadPort, WritePort
from amaranth.utils import ceil_log2 from amaranth.utils import ceil_log2
import logging
from .common import Rgb666Layout, Hub75Stream, Hub75Ctrl, Hub75Data, Rgb888Layout from .common import Rgb666Layout, Hub75Stream, Hub75Ctrl, Hub75Data, Rgb888Layout
logger = logging.getLogger(__name__)
class SwapBuffer(wiring.Component): class SwapBuffer(wiring.Component):
"""A pair of BRAMs for holdling line data that are swapped between using an external signal. """A pair of BRAMs for holdling line data that are swapped between using an external signal.
@ -25,7 +29,8 @@ class SwapBuffer(wiring.Component):
""" """
def __init__(self, shape: ShapeLike, depth: int): def __init__(self, shape: ShapeLike, depth: int, init=[]):
self.mem_init = init
super().__init__( super().__init__(
{ {
"selector": In(1), "selector": In(1),
@ -44,10 +49,10 @@ class SwapBuffer(wiring.Component):
m = Module() m = Module()
m.submodules.bram0 = self.bram0 = Memory( m.submodules.bram0 = self.bram0 = Memory(
shape=self.data_shape, depth=self.depth, init=[] shape=self.data_shape, depth=self.depth, init=self.mem_init
) )
m.submodules.bram1 = self.bram1 = Memory( m.submodules.bram1 = self.bram1 = Memory(
shape=self.data_shape, depth=self.depth, init=[] shape=self.data_shape, depth=self.depth, init=self.mem_init
) )
rd0 = self.bram0.read_port() rd0 = self.bram0.read_port()
@ -93,6 +98,7 @@ class DisplayClock(wiring.Component):
if startup_delay is None: if startup_delay is None:
self.startup_delay = 4 if double_fetch else 1 # FIXME: choose right values. self.startup_delay = 4 if double_fetch else 1 # FIXME: choose right values.
else: else:
logger.warn("Using custom startup delay. This is not tested")
self.startup_delay = startup_delay self.startup_delay = startup_delay
super().__init__( super().__init__(
@ -138,6 +144,65 @@ class DisplayClock(wiring.Component):
return m return m
class DisplayStrober(wiring.Component):
"""Generates ~OE/blank strobes to expose the latched-in data."""
def __init__(
self,
n_bits: int = 8,
base_expose_time: int = None,
*,
periods=None,
src_loc_at=0,
):
self.n_bits = n_bits
self.base_expose_time = base_expose_time
if periods is None:
# compute durations
self.periods = Array([base_expose_time << x for x in range(n_bits)])
else:
assert len(periods) == n_bits, "must have one period for each bit"
if base_expose_time is not None:
logger.warn(
"Received both base_expose_time and predifined periods. Ignoring base_expose_time"
)
self.periods = Array(periods)
logger.info("Strobe Clock Periods = %s", self.periods)
super().__init__(
{
"start": In(1),
"done": Out(1),
"blank": Out(1),
"bcm": In(ceil_log2(n_bits - 1)),
},
src_loc_at=src_loc_at,
)
def elaborate(self, platform: Platform) -> Module:
m = Module()
counter = Signal(32)
_bcm = Signal(self.bcm) # we save this value so that the coordinator can
# start the next line while this is running.
with m.FSM():
with m.State("init"):
m.d.sync += [counter.eq(0), self.done.eq(0), _bcm.eq(self.bcm)]
m.d.comb += self.blank.eq(1)
with m.If(self.start):
m.next = "run"
with m.State("run"):
m.d.comb += self.blank.eq(0)
m.d.sync += counter.eq(counter + 1)
with m.If(counter == self.periods[_bcm]):
m.next = "done"
with m.State("done"):
m.d.sync += self.done.eq(1)
m.d.comb += self.blank.eq(1)
m.next = "init"
return m
class Hub75DataDriver(wiring.Component): class Hub75DataDriver(wiring.Component):
def __init__( def __init__(
self, self,

View file

@ -2,6 +2,7 @@
import logging import logging
import argparse import argparse
from groovylight.config import Config
logger = logging.getLogger(__loader__.name) logger = logging.getLogger(__loader__.name)
@ -10,7 +11,10 @@ def setup_logger(args):
root_logger = logging.getLogger() root_logger = logging.getLogger()
handler = logging.StreamHandler() handler = logging.StreamHandler()
formatter = logging.Formatter(style="{", fmt="{levelname:s}: {name:s}: {message:s}") formatter = logging.Formatter(
style=r"{", fmt=r"{levelname:s}: {name:s}: {message:s}"
)
handler.setFormatter(formatter) handler.setFormatter(formatter)
root_logger.addHandler(handler) root_logger.addHandler(handler)
@ -38,10 +42,21 @@ def main():
metavar="FILE", metavar="FILE",
) )
parser.add_argument(
"config",
help="Configuration file",
type=argparse.FileType("rb"),
metavar="FILE",
)
args = parser.parse_args() args = parser.parse_args()
setup_logger(args) setup_logger(args)
conf = Config(args.config)
print(conf)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -90,7 +90,6 @@ def test_datadriver(bcm):
assert counter >= 0, "should not do more than 128 clocks" assert counter >= 0, "should not do more than 128 clocks"
e0 = ctx.get(mem.data[counter << 1]) e0 = ctx.get(mem.data[counter << 1])
e1 = ctx.get(mem.data[(counter << 1) + 1]) e1 = ctx.get(mem.data[(counter << 1) + 1])
print(counter)
for r, e in [(rgb0, e0), (rgb1, e1)]: for r, e in [(rgb0, e0), (rgb1, e1)]:
assert r.red == (e.red >> bcm) & 1 assert r.red == (e.red >> bcm) & 1
assert r.green == (e.green >> bcm) & 1 assert r.green == (e.green >> bcm) & 1
@ -133,7 +132,6 @@ def test_datadriver_single(bcm):
): ):
assert counter >= 0, "should not do more than 128 clocks" assert counter >= 0, "should not do more than 128 clocks"
e0, e1 = ctx.get(mem.data[counter]) e0, e1 = ctx.get(mem.data[counter])
print(counter)
for r, e in [(rgb0, e0), (rgb1, e1)]: for r, e in [(rgb0, e0), (rgb1, e1)]:
assert r.red == (e.red >> bcm) & 1 assert r.red == (e.red >> bcm) & 1
assert r.green == (e.green >> bcm) & 1 assert r.green == (e.green >> bcm) & 1