diff --git a/src/groovylight/fetcher.py b/src/groovylight/fetcher.py index 353c325..a5cec72 100644 --- a/src/groovylight/fetcher.py +++ b/src/groovylight/fetcher.py @@ -19,6 +19,7 @@ logger = logging.getLogger(__name__) # FIXME: sizing should be based off of screen size. CoordLayout = data.StructLayout({"x": unsigned(10), "y": unsigned(10)}) + class AddressConverter(wiring.Component): """Translates display (x,y) into full screen (x,y) based on geometry""" @@ -103,6 +104,8 @@ class AddressGenerator(wiring.Component): def example_rgb_transform(x, y): + """A simple coordinate-RGB transformation that computes RGB values directly from the x-y position + of the pixel. This is used in the simple case and as a test for the rest of the system.""" return { "red": x + y, "green": x - y, @@ -162,5 +165,6 @@ class BasicFetcher(wiring.Component): def chain_streams(m, streams): + """For "stream combinators", this allows you to easily chain outputs to inputs.""" for pair in pairwise(streams): wiring.connect(m, pair[0].output, pair[1].input) diff --git a/src/groovylight/geom.py b/src/groovylight/geom.py index 51e4e42..7d912df 100644 --- a/src/groovylight/geom.py +++ b/src/groovylight/geom.py @@ -210,6 +210,9 @@ class DisplayGeometry: sum += s.dimensions.size return sum + @property + def strings(self) -> [DisplayString]: + return self._strings def add_string(self, s: DisplayString): """Add a new string to the display. diff --git a/src/groovylight/hub75.py b/src/groovylight/hub75.py index 23c10e9..f620428 100644 --- a/src/groovylight/hub75.py +++ b/src/groovylight/hub75.py @@ -3,9 +3,12 @@ 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_soc import wishbone from amaranth.utils import ceil_log2 import logging +from groovylight.geom import DisplayGeometry + from .common import Rgb666Layout, Hub75Stream, Hub75Ctrl, Hub75Data, Rgb888Layout @@ -34,10 +37,10 @@ class SwapBuffer(wiring.Component): super().__init__( { "selector": In(1), - "read_port": In( + "read_port": Out( ReadPort.Signature(addr_width=ceil_log2(depth), shape=shape) ), - "write_port": In( + "write_port": Out( WritePort.Signature(addr_width=ceil_log2(depth), shape=shape) ), } @@ -316,11 +319,11 @@ class Hub75DataDriver(wiring.Component): class Hub75Coordinator(wiring.Component): """A shared-control hub75 driver""" - def __init__(self, n_strings=1): - self.n_strings = n_strings + def __init__(self, geom: DisplayGeometry): + self.geom = geom super().__init__( { - "hub75": Out(Hub75Ctrl(n_strings)), + "hub75": Out(Hub75Ctrl(self.geom.n_strings)), # TODO: fetching routine? maybe it's passed through. } ) @@ -338,33 +341,31 @@ class Hub75Coordinator(wiring.Component): startStrings = Signal(1) stringsDone = Signal(1) - for i in range(self.n_strings): - sb = SwapBuffer(depth=128, shape=data.ArrayLayout(Rgb666Layout, 2)) + for idx, string in enumerate(self.geom.strings): + sb = SwapBuffer(depth=128, shape=data.ArrayLayout(Rgb888Layout, 2)) bufs.append(sb) stringdriver = Hub75DataDriver( - 128, data_shape=Rgb666Layout, double_fetch=False + string.dimensions.length, data_shape=Rgb888Layout, double_fetch=False ) strings.append(stringdriver) - wiring.connect(m, sb.read_port, stringdriver.bram_port) + wiring.connect(m, sb.read_port, stringdriver.bram) + # wiring.connect(m, self.hub75.data[idx], stringdriver.data.flip()) m.d.comb += [ - self.data[i].eq(stringdriver.display_out), + self.hub75.data[idx].rgb0.eq(stringdriver.data.rgb0), + self.hub75.data[idx].rgb1.eq(stringdriver.data.rgb1), stringdriver.start.eq(startStrings), sb.selector.eq(swapline), ] m.submodules += [sb, stringdriver] 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. + # right now we're just going to use a basicFetcher + # TODO: support SDRAM framebuffer with m.FSM(): with m.State("init"): @@ -384,131 +385,3 @@ class Hub75Coordinator(wiring.Component): # fetch line -# - - -class Hub75EDriver(wiring.Component): - """An optimized driver for hub75 strings. - This version is faster than most implementations by merging the exposure - period and the data-write period to happen simultaneously. As a result, - the display can be brighter due to higher duty cycle. - - - NOTICE: this is a direct port of the old verilog code. It isn't up to date with the - modified structure. Notably, it uses RGB888 with double-fetch (4xclocking) - """ - - start: In(1) - done: Out(1) - out: Out(Hub75Stream()) - buf_addr: Out(9) - buf_data: In(36) - - row_depth = 128 - bit_depth = 8 - bcm_len = 32 - - def elaborate(self, platform: Platform) -> Module: - m = Module() - - counter = Signal(32) - bcm_shift: Signal = Signal(4, init=7) - m.d.sync += counter.eq(counter + 1) - # construct helper signals. - ram_r = self.buf_data[16:23] - ram_b = self.buf_data[8:15] - ram_g = self.buf_data[0:7] - - ram_rgb_slice = Cat( - ram_r.bit_select(bcm_shift, 1), - ram_g.bit_select(bcm_shift, 1), - ram_b.bit_select(bcm_shift, 1), - ) - pixnum = Signal(8, reset=127) - pixrow = Signal(1, reset=0) - m.d.comb += self.buf_addr.eq(Cat(pixrow, pixnum)) - - should_clock = counter < (128 * 2 + 1) - should_expose = (counter < (32 << (bcm_shift + 1) + 1)).bool() & ( - bcm_shift != 7 - ).bool() - - with m.FSM(): - with m.State("init"): - m.d.sync += [ - bcm_shift.eq(7), - counter.eq(0), - self.done.eq(0), - pixnum.eq(127), - pixrow.eq(0), - ] - with m.If(self.start == 1): - m.next = "prefetch" - with m.State("prefetch"): - with m.If(counter == 0): - m.d.sync += pixrow.eq(0) - with m.Elif(counter == 1): - m.d.sync += pixrow.eq(1) - with m.Elif(counter == 2): - m.d.sync += self.out.rgb0.eq(ram_rgb_slice) - with m.Elif(counter == 3): - m.d.sync += [self.out.rgb1.eq(ram_rgb_slice), counter.eq(0)] - m.next = "writerow" - with m.State("writerow"): - # expose if we haven't done it for long enough yet. - m.d.sync += self.out.oe.eq(~(should_expose)) - with m.If(should_clock): - # clock is high on entry - m.d.sync += self.out.display_clk.eq(counter[1] == 0) - - with m.If(counter[0:1] == 0): - # rising edge of the clock - m.d.sync += pixrow.eq(0) - - with m.If(counter[0:1] == 1): - m.d.sync += [ - pixrow.eq(1), - self.out.rgb0.eq(ram_rgb_slice), - ] - with m.If(counter[0:1] == 2): - m.d.sync += [ - pixnum.eq(pixnum - 1), - pixrow.eq(0), - self.out.rgb1.eq(ram_rgb_slice), - ] - - with m.Elif(~(should_expose)): - # we are done both feeding in the new data and exposing the previous. - m.d.sync += [counter.eq(0), self.out.display_clk.eq(0)] - m.next = "latchout" - with m.State("latchout"): - m.d.sync += [ - pixnum.eq(127), - self.out.latch.eq(1), - ] - with m.If(counter > 3): - m.d.sync += self.out.latch.eq(0) - m.d.sync += counter.eq(0) - with m.If(bcm_shift == 0): - m.next = "finish" - with m.Else(): - m.d.sync += bcm_shift.eq(bcm_shift - 1) - m.next = "prefetch" - - with m.State("finish"): - m.d.sync += Assert(bcm_shift == 0, "finish without bcm shift 0") - - with m.If(counter < (32)): - m.d.sync += self.out.oe.eq(0) - with m.Else(): - m.d.sync += [self.out.oe.eq(1), self.done.eq(1)] - m.next = "init" - - return m - - -if __name__ == "__main__": - m = Hub75EDriver() - from amaranth.cli import main - - main(m) diff --git a/src/groovylight/tests/test_hub75.py b/src/groovylight/tests/test_hub75.py index 80664ef..85a3d23 100644 --- a/src/groovylight/tests/test_hub75.py +++ b/src/groovylight/tests/test_hub75.py @@ -7,6 +7,7 @@ from random import randrange import pytest from groovylight.common import Rgb888Layout, Rgb666Layout +from groovylight.geom import DisplayGeometry, DisplayString, DisplayRotation, DisplayDimensions, Coord from groovylight.hub75 import ( DisplayClock, @@ -146,10 +147,13 @@ def test_datadriver_single(bcm): with sim.write_vcd("output.vcd"): sim.run() -@pytest.mark.skip() + def test_hub75_coordinator(): m = Module() - m.submodules.dut = dut = Hub75Coordinator(1) + geom = DisplayGeometry() + geom.add_string(DisplayString(Coord(0,0), DisplayDimensions(128,64), DisplayRotation.R0)) + geom.add_string(DisplayString(Coord(65,0), DisplayDimensions(128,64), DisplayRotation.R0)) + m.submodules.dut = dut = Hub75Coordinator(geom) sim = Simulator(m) sim.add_clock(1e-6)