diff --git a/.gitignore b/.gitignore index 93f5399..e0b56fc 100644 --- a/.gitignore +++ b/.gitignore @@ -207,3 +207,4 @@ cython_debug/ #.idea/ result +.aider* diff --git a/example-config.toml b/example-config.toml index 013fcb6..15133fc 100644 --- a/example-config.toml +++ b/example-config.toml @@ -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" diff --git a/src/groovylight/common.py b/src/groovylight/common.py index 6a34c49..b3b8c30 100644 --- a/src/groovylight/common.py +++ b/src/groovylight/common.py @@ -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. diff --git a/src/groovylight/config.py b/src/groovylight/config.py index f23443e..e7c5aef 100644 --- a/src/groovylight/config.py +++ b/src/groovylight/config.py @@ -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. diff --git a/src/groovylight/geom.py b/src/groovylight/geom.py index 91ca942..d80d6ac 100644 --- a/src/groovylight/geom.py +++ b/src/groovylight/geom.py @@ -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) diff --git a/src/groovylight/hub75.py b/src/groovylight/hub75.py index 43ac515..b2deb52 100644 --- a/src/groovylight/hub75.py +++ b/src/groovylight/hub75.py @@ -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, diff --git a/src/groovylight/main.py b/src/groovylight/main.py index 0cd9bda..4a4a0f2 100644 --- a/src/groovylight/main.py +++ b/src/groovylight/main.py @@ -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() diff --git a/src/groovylight/tests/test_hub75.py b/src/groovylight/tests/test_hub75.py index ba2ca35..b31951e 100644 --- a/src/groovylight/tests/test_hub75.py +++ b/src/groovylight/tests/test_hub75.py @@ -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