Compare commits

...

1 commit

Author SHA1 Message Date
saji 9a106156d0 add displaystrobe, config/geom fixes
Some checks failed
Unit Tests / Test (push) Failing after 2m12s
2024-10-08 13:14:27 -05:00
8 changed files with 107 additions and 29 deletions

1
.gitignore vendored
View file

@ -207,3 +207,4 @@ cython_debug/
#.idea/
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.
[[display.strings]]
position = { x = 31, y = 0 }
dimensions = { width = 256, height = 64 }
position = { x = 64, y = 0 }
dimensions = { length = 256, height = 64 }
rotation = "UPDOWN"
[[display.strings]]
position = { x = 32, y = 0 }
dimensions = { width = 256, height = 64 }
position = { x = 65, y = 0 }
dimensions = { length = 256, height = 64 }
rotation = "LEFTRIGHT"
[[display.strings]]
position = { x = 32, y = 64 }
dimensions = { width = 256, height = 64 }
position = { x = 65, y = 65 }
dimensions = { length = 256, height = 64 }
rotation = "LEFTRIGHT"
[[display.strings]]
position = { x = 32, y = 128 }
dimensions = { width = 256, height = 64 }
position = { x = 65, y = 130 }
dimensions = { length = 256, height = 64 }
rotation = "LEFTRIGHT"
[[display.strings]]
position = { x = 32, y = 192 }
dimensions = { width = 256, height = 64 }
position = { x = 65, y = 195 }
dimensions = { length = 256, height = 64 }
rotation = "LEFTRIGHT"

View file

@ -25,8 +25,8 @@ Rgb111Layout = RGBLayout(1, 1, 1)
class RGBView(data.View):
def channel_size(self) -> int:
return self.red.shape()
def bit_depth(self) -> int:
return max(self.red.shape(), self.green.shape(), self.blue.shape())
def channel_slice(self, bit: int) -> 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.

View file

@ -1,5 +1,4 @@
import tomllib
from pathlib import Path
from groovylight.geom import (
Coord,
@ -11,15 +10,15 @@ from groovylight.geom import (
class Config:
def __init__(self, file: Path) -> None:
with file.open("rb") as f:
self.conf = tomllib.load(f)
def __init__(self, file) -> None:
self.conf = tomllib.load(file)
print
self.geom = DisplayGeometry(self.conf.display.strict)
for string in self.conf.display.strings:
pos = Coord(**string.position)
dims = DisplayDimensions(**string.dimensions)
rot = DisplayRotation[string.rotation]
self.geom = DisplayGeometry(strict=self.conf["display"]["strict"])
for string in self.conf["display"]["strings"]:
pos = Coord(**string['position'])
dims = DisplayDimensions(**string["dimensions"])
rot = DisplayRotation[string["rotation"]]
disp = DisplayString(pos, dims, rot)
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:
for e in self._strings:
if e.intersects(s):
if e.intersects(s.bbox):
raise RuntimeError(f"node {e} intersects with {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.memory import Memory, ReadPort, WritePort
from amaranth.utils import ceil_log2
import logging
from .common import Rgb666Layout, Hub75Stream, Hub75Ctrl, Hub75Data, Rgb888Layout
logger = logging.getLogger(__name__)
class SwapBuffer(wiring.Component):
"""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__(
{
"selector": In(1),
@ -44,10 +49,10 @@ class SwapBuffer(wiring.Component):
m = Module()
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(
shape=self.data_shape, depth=self.depth, init=[]
shape=self.data_shape, depth=self.depth, init=self.mem_init
)
rd0 = self.bram0.read_port()
@ -93,6 +98,7 @@ class DisplayClock(wiring.Component):
if startup_delay is None:
self.startup_delay = 4 if double_fetch else 1 # FIXME: choose right values.
else:
logger.warn("Using custom startup delay. This is not tested")
self.startup_delay = startup_delay
super().__init__(
@ -138,6 +144,65 @@ class DisplayClock(wiring.Component):
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):
def __init__(
self,

View file

@ -2,6 +2,7 @@
import logging
import argparse
from groovylight.config import Config
logger = logging.getLogger(__loader__.name)
@ -10,7 +11,10 @@ def setup_logger(args):
root_logger = logging.getLogger()
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)
root_logger.addHandler(handler)
@ -38,10 +42,21 @@ def main():
metavar="FILE",
)
parser.add_argument(
"config",
help="Configuration file",
type=argparse.FileType("rb"),
metavar="FILE",
)
args = parser.parse_args()
setup_logger(args)
conf = Config(args.config)
print(conf)
if __name__ == "__main__":
main()

View file

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