generated from saji/ecp5-template
cleanup; integrate geom into hub75
This commit is contained in:
parent
ae1ad4633c
commit
dd334e8bad
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue