generated from saji/ecp5-template
Compare commits
4 commits
5f54b8acd8
...
09485a9753
Author | SHA1 | Date | |
---|---|---|---|
saji | 09485a9753 | ||
saji | 300e8192fe | ||
saji | dd334e8bad | ||
saji | ae1ad4633c |
|
@ -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.
|
strict = true # allows some wacky configurations, like panels that overlap.
|
||||||
|
|
||||||
[[display.strings]]
|
[[display.strings]]
|
||||||
position = { x = 64, y = 0 }
|
position = { x = 0, y = 0 }
|
||||||
dimensions = { length = 256, height = 64 }
|
dimensions = { length = 256, height = 64 }
|
||||||
rotation = "UPDOWN"
|
rotation = "R90"
|
||||||
[[display.strings]]
|
[[display.strings]]
|
||||||
position = { x = 65, y = 0 }
|
position = { x = 65, y = 0 }
|
||||||
dimensions = { length = 256, height = 64 }
|
dimensions = { length = 256, height = 64 }
|
||||||
rotation = "LEFTRIGHT"
|
rotation = "R0"
|
||||||
[[display.strings]]
|
[[display.strings]]
|
||||||
position = { x = 65, y = 65 }
|
position = { x = 65, y = 65 }
|
||||||
dimensions = { length = 256, height = 64 }
|
dimensions = { length = 256, height = 64 }
|
||||||
rotation = "LEFTRIGHT"
|
rotation = "R0"
|
||||||
[[display.strings]]
|
[[display.strings]]
|
||||||
position = { x = 65, y = 130 }
|
position = { x = 65, y = 130 }
|
||||||
dimensions = { length = 256, height = 64 }
|
dimensions = { length = 256, height = 64 }
|
||||||
rotation = "LEFTRIGHT"
|
rotation = "R0"
|
||||||
[[display.strings]]
|
[[display.strings]]
|
||||||
position = { x = 65, y = 195 }
|
position = { x = 65, y = 195 }
|
||||||
dimensions = { length = 256, height = 64 }
|
dimensions = { length = 256, height = 64 }
|
||||||
rotation = "LEFTRIGHT"
|
rotation = "R0"
|
||||||
|
|
14
pdm.lock
14
pdm.lock
|
@ -5,7 +5,7 @@
|
||||||
groups = ["default", "dev"]
|
groups = ["default", "dev"]
|
||||||
strategy = ["inherit_metadata"]
|
strategy = ["inherit_metadata"]
|
||||||
lock_version = "4.5.0"
|
lock_version = "4.5.0"
|
||||||
content_hash = "sha256:fbfe1db54d73aa2641413610d5e62d87b02de247293e2af3cd53ee0c283318db"
|
content_hash = "sha256:70036fd7ee1fe6910ed441a5419d9194d1abc592cbf2f39deb1d7a8e77501d03"
|
||||||
|
|
||||||
[[metadata.targets]]
|
[[metadata.targets]]
|
||||||
requires_python = "==3.12.*"
|
requires_python = "==3.12.*"
|
||||||
|
@ -39,6 +39,18 @@ dependencies = [
|
||||||
"amaranth<0.7,>=0.4",
|
"amaranth<0.7,>=0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "amaranth-soc"
|
||||||
|
version = "0.1a1.dev24"
|
||||||
|
requires_python = "~=3.9"
|
||||||
|
git = "https://github.com/amaranth-lang/amaranth-soc.git"
|
||||||
|
revision = "5c43cf58f15d9cd9c69ff83c97997708d386b2dc"
|
||||||
|
summary = "System on Chip toolkit for Amaranth HDL"
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"amaranth<0.6,>=0.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "basedpyright"
|
name = "basedpyright"
|
||||||
version = "1.18.0"
|
version = "1.18.0"
|
||||||
|
|
|
@ -8,6 +8,7 @@ authors = [
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"amaranth>=0.5.1",
|
"amaranth>=0.5.1",
|
||||||
"amaranth-boards @ git+https://github.com/amaranth-lang/amaranth-boards.git",
|
"amaranth-boards @ git+https://github.com/amaranth-lang/amaranth-boards.git",
|
||||||
|
"amaranth-soc @ git+https://github.com/amaranth-lang/amaranth-soc.git",
|
||||||
]
|
]
|
||||||
requires-python = "==3.12.*"
|
requires-python = "==3.12.*"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
# to know its location.
|
# to know its location.
|
||||||
# during operation, it is given a row index, and responds with the data.
|
# during operation, it is given a row index, and responds with the data.
|
||||||
|
|
||||||
|
from amaranth import Module, Signal, unsigned
|
||||||
from amaranth import Module, Signal, unsigned, Cat
|
|
||||||
from amaranth.build import Platform
|
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 import stream
|
from amaranth.lib import stream
|
||||||
import logging
|
import logging
|
||||||
|
from itertools import pairwise
|
||||||
|
|
||||||
from .common import Rgb888Layout
|
from .common import Rgb888Layout
|
||||||
from .geom import DisplayString
|
from .geom import DisplayString
|
||||||
|
@ -16,7 +16,6 @@ from .geom import DisplayString
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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)})
|
||||||
|
|
||||||
|
@ -53,7 +52,7 @@ class AddressGenerator(wiring.Component):
|
||||||
self.geom = geom
|
self.geom = geom
|
||||||
super().__init__(
|
super().__init__(
|
||||||
{
|
{
|
||||||
"coordstream": Out(
|
"output": Out(
|
||||||
stream.Signature(data.ArrayLayout(CoordLayout, geom.dimensions.mux))
|
stream.Signature(data.ArrayLayout(CoordLayout, geom.dimensions.mux))
|
||||||
),
|
),
|
||||||
"start": In(1),
|
"start": In(1),
|
||||||
|
@ -75,41 +74,60 @@ class AddressGenerator(wiring.Component):
|
||||||
m.d.comb += translate.input_x.eq(counter)
|
m.d.comb += translate.input_x.eq(counter)
|
||||||
m.d.comb += translate.addr.eq(addr)
|
m.d.comb += translate.addr.eq(addr)
|
||||||
|
|
||||||
m.d.comb += self.coordstream.payload.eq(translate.output)
|
m.d.comb += self.output.payload.eq(translate.output)
|
||||||
|
|
||||||
with m.FSM():
|
with m.FSM():
|
||||||
with m.State("init"):
|
with m.State("init"):
|
||||||
m.d.comb += [self.done.eq(0), self.coordstream.valid.eq(0)]
|
m.d.comb += [self.done.eq(0), self.output.valid.eq(0)]
|
||||||
m.d.sync += [counter.eq(0), addr.eq(self.addr)]
|
m.d.sync += [counter.eq(0), addr.eq(self.addr)]
|
||||||
with m.If(self.start):
|
with m.If(self.start):
|
||||||
m.next = "run"
|
m.next = "run"
|
||||||
|
|
||||||
with m.State("run"):
|
with m.State("run"):
|
||||||
m.d.comb += self.coordstream.valid.eq(1)
|
m.d.comb += self.output.valid.eq(1)
|
||||||
# stream data out as long as it's valid.
|
# stream data out as long as it's valid.
|
||||||
|
|
||||||
with m.If(
|
with m.If(
|
||||||
self.coordstream.ready
|
self.output.ready & (counter == self.geom.dimensions.length - 1)
|
||||||
& (counter == self.geom.dimensions.length - 1)
|
|
||||||
):
|
):
|
||||||
m.next = "done"
|
m.next = "done"
|
||||||
with m.Elif(self.coordstream.ready):
|
with m.Elif(self.output.ready):
|
||||||
m.d.sync += counter.eq(counter + 1)
|
m.d.sync += counter.eq(counter + 1)
|
||||||
|
|
||||||
pass
|
pass
|
||||||
with m.State("done"):
|
with m.State("done"):
|
||||||
m.d.comb += self.coordstream.valid.eq(0)
|
m.d.comb += self.output.valid.eq(0)
|
||||||
m.d.comb += self.done.eq(1)
|
m.d.comb += self.done.eq(1)
|
||||||
m.next = "init"
|
m.next = "init"
|
||||||
|
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
"blue": x ^ y,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BasicFetcher(wiring.Component):
|
class BasicFetcher(wiring.Component):
|
||||||
"""A generic function-based fetcher. Takes a function of the form f(x,y: int) -> RGB."""
|
"""A generic function-based fetcher. Takes a function of the form f(x,y: int) -> dict rgb values.
|
||||||
|
|
||||||
|
If no function is provided it uses a basic coordinate-driven rgb transform where red = x+y,
|
||||||
|
green = x - y, and blue = x ^ y.
|
||||||
|
|
||||||
|
When providing a function, it must return a dictionary with the keys "red", "green", "blue"."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, geom: DisplayString, dfunc, data_shape=Rgb888Layout, *, src_loc_at=0
|
self,
|
||||||
|
geom: DisplayString,
|
||||||
|
dfunc=example_rgb_transform,
|
||||||
|
data_shape=Rgb888Layout,
|
||||||
|
*,
|
||||||
|
src_loc_at=0,
|
||||||
):
|
):
|
||||||
self.geom = geom
|
self.geom = geom
|
||||||
self.dfunc = dfunc
|
self.dfunc = dfunc
|
||||||
|
@ -118,7 +136,7 @@ class BasicFetcher(wiring.Component):
|
||||||
"input": In(
|
"input": In(
|
||||||
stream.Signature(data.ArrayLayout(CoordLayout, geom.dimensions.mux))
|
stream.Signature(data.ArrayLayout(CoordLayout, geom.dimensions.mux))
|
||||||
),
|
),
|
||||||
"pixstream": Out(
|
"output": Out(
|
||||||
stream.Signature(data.ArrayLayout(data_shape, geom.dimensions.mux))
|
stream.Signature(data.ArrayLayout(data_shape, geom.dimensions.mux))
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -128,20 +146,25 @@ class BasicFetcher(wiring.Component):
|
||||||
def elaborate(self, platform: Platform) -> Module:
|
def elaborate(self, platform: Platform) -> Module:
|
||||||
m = Module()
|
m = Module()
|
||||||
|
|
||||||
# test mode - pass through, r = x + y, g = x - y, b = {y,x}
|
colors = self.output.payload
|
||||||
|
|
||||||
colors = self.pixstream.payload
|
|
||||||
m.d.comb += [
|
m.d.comb += [
|
||||||
self.input.valid.eq(self.pixstream.valid),
|
self.output.valid.eq(self.input.valid),
|
||||||
self.input.ready.eq(self.pixstream.ready),
|
self.input.ready.eq(self.output.ready),
|
||||||
]
|
]
|
||||||
|
|
||||||
for i in range(self.geom.dimensions.mux):
|
for i in range(self.geom.dimensions.mux):
|
||||||
inp = self.input.payload[i]
|
inp = self.input.payload[i]
|
||||||
|
output = self.dfunc(inp.x, inp.y)
|
||||||
m.d.comb += [
|
m.d.comb += [
|
||||||
colors[i].red.eq(inp.x + inp.y),
|
colors[i].red.eq(output["red"]),
|
||||||
colors[i].green.eq(inp.x - inp.y),
|
colors[i].green.eq(output["green"]),
|
||||||
colors[i].blue.eq(inp.x ^ inp.y),
|
colors[i].blue.eq(output["blue"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
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
|
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.
|
||||||
|
|
|
@ -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)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -313,14 +316,16 @@ class Hub75DataDriver(wiring.Component):
|
||||||
|
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
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, *, double_fetch=True):
|
||||||
self.n_strings = n_strings
|
self.geom = geom
|
||||||
|
self.double_fetch = double_fetch
|
||||||
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.
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -337,34 +342,38 @@ class Hub75Coordinator(wiring.Component):
|
||||||
donearr = []
|
donearr = []
|
||||||
startStrings = Signal(1)
|
startStrings = Signal(1)
|
||||||
stringsDone = Signal(1)
|
stringsDone = Signal(1)
|
||||||
|
bram_shape = Rgb888Layout if self.double_fetch else data.ArrayLayout(Rgb888Layout, 2)
|
||||||
|
|
||||||
for i in range(self.n_strings):
|
for idx, string in enumerate(self.geom.strings):
|
||||||
sb = SwapBuffer(depth=128, shape=data.ArrayLayout(Rgb666Layout, 2))
|
mdepth = string.dimensions.length
|
||||||
|
if self.double_fetch:
|
||||||
|
mdepth = mdepth * 2
|
||||||
|
sb = SwapBuffer(depth=mdepth, shape=bram_shape)
|
||||||
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=self.double_fetch,
|
||||||
)
|
)
|
||||||
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 +393,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)
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
# main entry point for CLI applications.
|
# main entry point for CLI applications.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
from groovylight.config import Config
|
from groovylight.config import Config
|
||||||
|
from groovylight.platforms.cxxrtl_sim import emit_cxxrtl
|
||||||
|
|
||||||
logger = logging.getLogger(__loader__.name)
|
logger = logging.getLogger(__loader__.name)
|
||||||
|
|
||||||
|
@ -18,9 +20,21 @@ def setup_logger(args):
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
|
|
||||||
root_logger.addHandler(handler)
|
root_logger.addHandler(handler)
|
||||||
|
if args.log_file is not None:
|
||||||
|
hdlr = logging.FileHandler(args.log_file)
|
||||||
|
hdlr.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(formatter)
|
||||||
|
|
||||||
root_logger.setLevel(args.loglevel)
|
root_logger.setLevel(args.loglevel)
|
||||||
|
|
||||||
|
|
||||||
|
def dir_path(string):
|
||||||
|
if os.path.isdir(string):
|
||||||
|
return string
|
||||||
|
else:
|
||||||
|
raise NotADirectoryError(string)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
@ -41,6 +55,9 @@ def main():
|
||||||
type=argparse.FileType("w"),
|
type=argparse.FileType("w"),
|
||||||
metavar="FILE",
|
metavar="FILE",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-D", "--dump", help="Dump verilog to folder", type=dir_path, metavar="FOLDER"
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"config",
|
"config",
|
||||||
|
@ -54,8 +71,17 @@ def main():
|
||||||
setup_logger(args)
|
setup_logger(args)
|
||||||
|
|
||||||
conf = Config(args.config)
|
conf = Config(args.config)
|
||||||
print(conf)
|
|
||||||
|
|
||||||
|
# use the config to create the module.
|
||||||
|
|
||||||
|
if conf.conf["hardware"]["type"] == "cxxrtl":
|
||||||
|
logger.info("Generating CXXRTL based graphical simulator.")
|
||||||
|
emit_cxxrtl(conf)
|
||||||
|
|
||||||
|
elif conf.conf["hardware"]["type"] == "colorlight":
|
||||||
|
logger.debug("Generating colorlight code")
|
||||||
|
if args.dump:
|
||||||
|
logger.info(f"Dumping verilog to {args.dump}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -7,3 +7,16 @@
|
||||||
# black box the UDP streaming.
|
# black box the UDP streaming.
|
||||||
# provide code for display outputs to render onto SDL2 canvas.
|
# provide code for display outputs to render onto SDL2 canvas.
|
||||||
# compile code (optionally)
|
# compile code (optionally)
|
||||||
|
from amaranth.back import cxxrtl
|
||||||
|
from amaranth import Module
|
||||||
|
|
||||||
|
from groovylight import hub75
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def emit_cxxrtl(config):
|
||||||
|
m = Module()
|
||||||
|
m.submodules.coordinator = crd = hub75.Hub75Coordinator(config.geom)
|
||||||
|
cxxrtl.convert(m, ports=[])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
from amaranth.lib import wiring, data
|
from amaranth import Module
|
||||||
|
from amaranth.lib import wiring
|
||||||
from amaranth.sim import Simulator
|
from amaranth.sim import Simulator
|
||||||
import random
|
|
||||||
from random import randrange
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from groovylight.fetcher import AddressConverter, AddressGenerator, BasicFetcher
|
from groovylight.fetcher import AddressConverter, AddressGenerator, BasicFetcher, chain_streams
|
||||||
from groovylight.geom import DisplayString, Coord, DisplayDimensions, DisplayRotation
|
from groovylight.geom import DisplayString, Coord, DisplayDimensions, DisplayRotation
|
||||||
|
|
||||||
ds_testdata = [
|
ds_testdata = [
|
||||||
|
@ -82,7 +81,7 @@ def test_generator(addr, rot):
|
||||||
|
|
||||||
async def stream_checker(ctx):
|
async def stream_checker(ctx):
|
||||||
while ctx.get(dut.done) == 0:
|
while ctx.get(dut.done) == 0:
|
||||||
payload = await stream_get(ctx, dut.coordstream)
|
payload = await stream_get(ctx, dut.output)
|
||||||
assert expected.pop() == payload
|
assert expected.pop() == payload
|
||||||
|
|
||||||
sim.add_testbench(runner)
|
sim.add_testbench(runner)
|
||||||
|
@ -103,12 +102,12 @@ def test_basic_fetcher(inp, expected):
|
||||||
ds = DisplayString(
|
ds = DisplayString(
|
||||||
Coord(3, 0), DisplayDimensions(128, 64, mux=1), DisplayRotation.R0
|
Coord(3, 0), DisplayDimensions(128, 64, mux=1), DisplayRotation.R0
|
||||||
)
|
)
|
||||||
dut = BasicFetcher(ds, None)
|
dut = BasicFetcher(ds)
|
||||||
sim = Simulator(dut)
|
sim = Simulator(dut)
|
||||||
|
|
||||||
async def test(ctx):
|
async def test(ctx):
|
||||||
ctx.set(dut.input.payload[0], inp)
|
ctx.set(dut.input.payload[0], inp)
|
||||||
res = ctx.get(dut.pixstream.payload)[0]
|
res = ctx.get(dut.output.payload)[0]
|
||||||
assert res["red"] == expected["red"]
|
assert res["red"] == expected["red"]
|
||||||
assert res["green"] == expected["green"]
|
assert res["green"] == expected["green"]
|
||||||
assert res["blue"] == expected["blue"]
|
assert res["blue"] == expected["blue"]
|
||||||
|
@ -117,3 +116,34 @@ def test_basic_fetcher(inp, expected):
|
||||||
|
|
||||||
with sim.write_vcd("fetcher.vcd"):
|
with sim.write_vcd("fetcher.vcd"):
|
||||||
sim.run()
|
sim.run()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_stream_e2e():
|
||||||
|
ds = DisplayString(
|
||||||
|
Coord(3, 0), DisplayDimensions(128, 64, mux=1), DisplayRotation.R0
|
||||||
|
)
|
||||||
|
m = Module()
|
||||||
|
m.submodules.gen = addrgen = AddressGenerator(ds)
|
||||||
|
m.submodules.fetch = fetch = BasicFetcher(ds)
|
||||||
|
|
||||||
|
chain_streams(m, [addrgen, fetch])
|
||||||
|
|
||||||
|
sim = Simulator(m)
|
||||||
|
sim.add_clock(1e-6)
|
||||||
|
|
||||||
|
async def stim(ctx):
|
||||||
|
await ctx.tick()
|
||||||
|
ctx.set(addrgen.start, 1)
|
||||||
|
await ctx.tick()
|
||||||
|
ctx.set(addrgen.start, 0)
|
||||||
|
payload = await stream_get(ctx, fetch.output)
|
||||||
|
assert payload[0] == {"red": 3, "green": 3, "blue": 3}
|
||||||
|
|
||||||
|
|
||||||
|
sim.add_testbench(stim)
|
||||||
|
|
||||||
|
with sim.write_vcd("stream_e2e.vcd"):
|
||||||
|
sim.run()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
9
src/groovylight/top.py
Normal file
9
src/groovylight/top.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# Creates a top-level module based on the display configuration.
|
||||||
|
|
||||||
|
|
||||||
|
from amaranth import Signal
|
||||||
|
from amaranth.lib import wiring, data
|
||||||
|
|
||||||
|
from .hub75
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue