cleanup; integrate geom into hub75

This commit is contained in:
saji 2024-10-29 16:13:33 -05:00
parent ae1ad4633c
commit dd334e8bad
4 changed files with 31 additions and 147 deletions

View file

@ -19,6 +19,7 @@ logger = logging.getLogger(__name__)
# FIXME: sizing should be based off of screen size. # FIXME: sizing should be based off of screen size.
CoordLayout = data.StructLayout({"x": unsigned(10), "y": unsigned(10)}) CoordLayout = data.StructLayout({"x": unsigned(10), "y": unsigned(10)})
class AddressConverter(wiring.Component): class AddressConverter(wiring.Component):
"""Translates display (x,y) into full screen (x,y) based on geometry""" """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): 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 { return {
"red": x + y, "red": x + y,
"green": x - y, "green": x - y,
@ -162,5 +165,6 @@ class BasicFetcher(wiring.Component):
def chain_streams(m, streams): def chain_streams(m, streams):
"""For "stream combinators", this allows you to easily chain outputs to inputs."""
for pair in pairwise(streams): for pair in pairwise(streams):
wiring.connect(m, pair[0].output, pair[1].input) wiring.connect(m, pair[0].output, pair[1].input)

View file

@ -210,6 +210,9 @@ class DisplayGeometry:
sum += s.dimensions.size sum += s.dimensions.size
return sum return sum
@property
def strings(self) -> [DisplayString]:
return self._strings
def add_string(self, s: DisplayString): def add_string(self, s: DisplayString):
"""Add a new string to the display. """Add a new string to the display.

View file

@ -3,9 +3,12 @@ from amaranth.build import Platform
from amaranth.lib import wiring, data 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_soc import wishbone
from amaranth.utils import ceil_log2 from amaranth.utils import ceil_log2
import logging import logging
from groovylight.geom import DisplayGeometry
from .common import Rgb666Layout, Hub75Stream, Hub75Ctrl, Hub75Data, Rgb888Layout from .common import Rgb666Layout, Hub75Stream, Hub75Ctrl, Hub75Data, Rgb888Layout
@ -34,10 +37,10 @@ class SwapBuffer(wiring.Component):
super().__init__( super().__init__(
{ {
"selector": In(1), "selector": In(1),
"read_port": In( "read_port": Out(
ReadPort.Signature(addr_width=ceil_log2(depth), shape=shape) ReadPort.Signature(addr_width=ceil_log2(depth), shape=shape)
), ),
"write_port": In( "write_port": Out(
WritePort.Signature(addr_width=ceil_log2(depth), shape=shape) WritePort.Signature(addr_width=ceil_log2(depth), shape=shape)
), ),
} }
@ -316,11 +319,11 @@ class Hub75DataDriver(wiring.Component):
class Hub75Coordinator(wiring.Component): class Hub75Coordinator(wiring.Component):
"""A shared-control hub75 driver""" """A shared-control hub75 driver"""
def __init__(self, n_strings=1): def __init__(self, geom: DisplayGeometry):
self.n_strings = n_strings self.geom = geom
super().__init__( super().__init__(
{ {
"hub75": Out(Hub75Ctrl(n_strings)), "hub75": Out(Hub75Ctrl(self.geom.n_strings)),
# TODO: fetching routine? maybe it's passed through. # TODO: fetching routine? maybe it's passed through.
} }
) )
@ -338,33 +341,31 @@ class Hub75Coordinator(wiring.Component):
startStrings = Signal(1) startStrings = Signal(1)
stringsDone = Signal(1) stringsDone = Signal(1)
for i in range(self.n_strings): for idx, string in enumerate(self.geom.strings):
sb = SwapBuffer(depth=128, shape=data.ArrayLayout(Rgb666Layout, 2)) sb = SwapBuffer(depth=128, shape=data.ArrayLayout(Rgb888Layout, 2))
bufs.append(sb) bufs.append(sb)
stringdriver = Hub75DataDriver( stringdriver = Hub75DataDriver(
128, data_shape=Rgb666Layout, double_fetch=False string.dimensions.length, data_shape=Rgb888Layout, double_fetch=False
) )
strings.append(stringdriver) 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 += [ 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), stringdriver.start.eq(startStrings),
sb.selector.eq(swapline), sb.selector.eq(swapline),
] ]
m.submodules += [sb, stringdriver] m.submodules += [sb, stringdriver]
donearr.append(stringdriver.done) donearr.append(stringdriver.done)
# combine the done signals into one signal with AND-reduction # combine the done signals into one signal with AND-reduction
m.d.comb += stringsDone.eq(Cat(*donearr).all()) m.d.comb += stringsDone.eq(Cat(*donearr).all())
self.addr = Signal(5) self.addr = Signal(5)
# handle the fetch side. # handle the fetch side.
# WIP: pass in fetcher/pixgen/geometry. # right now we're just going to use a basicFetcher
# right now we assume that it's just one panel, # TODO: support SDRAM framebuffer
# 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.FSM():
with m.State("init"): with m.State("init"):
@ -384,131 +385,3 @@ class Hub75Coordinator(wiring.Component):
# fetch line # 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)

View file

@ -7,6 +7,7 @@ from random import randrange
import pytest import pytest
from groovylight.common import Rgb888Layout, Rgb666Layout from groovylight.common import Rgb888Layout, Rgb666Layout
from groovylight.geom import DisplayGeometry, DisplayString, DisplayRotation, DisplayDimensions, Coord
from groovylight.hub75 import ( from groovylight.hub75 import (
DisplayClock, DisplayClock,
@ -146,10 +147,13 @@ def test_datadriver_single(bcm):
with sim.write_vcd("output.vcd"): with sim.write_vcd("output.vcd"):
sim.run() sim.run()
@pytest.mark.skip()
def test_hub75_coordinator(): def test_hub75_coordinator():
m = Module() 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 = Simulator(m)
sim.add_clock(1e-6) sim.add_clock(1e-6)