Compare commits

...

4 commits

Author SHA1 Message Date
saji 09485a9753 more work on cxxrtl simulator, start outputting things
Some checks failed
Unit Tests / Test (push) Failing after 2m22s
2024-11-02 09:57:16 -05:00
saji 300e8192fe start work on cxxrtl simulation backend 2024-11-02 00:16:47 -05:00
saji dd334e8bad cleanup; integrate geom into hub75 2024-11-02 00:06:21 -05:00
saji ae1ad4633c refactor streams for fetching/addrgen
write end to end test
2024-10-29 13:48:51 -05:00
11 changed files with 186 additions and 184 deletions

View file

@ -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 = 64, y = 0 }
position = { x = 0, y = 0 }
dimensions = { length = 256, height = 64 }
rotation = "UPDOWN"
rotation = "R90"
[[display.strings]]
position = { x = 65, y = 0 }
dimensions = { length = 256, height = 64 }
rotation = "LEFTRIGHT"
rotation = "R0"
[[display.strings]]
position = { x = 65, y = 65 }
dimensions = { length = 256, height = 64 }
rotation = "LEFTRIGHT"
rotation = "R0"
[[display.strings]]
position = { x = 65, y = 130 }
dimensions = { length = 256, height = 64 }
rotation = "LEFTRIGHT"
rotation = "R0"
[[display.strings]]
position = { x = 65, y = 195 }
dimensions = { length = 256, height = 64 }
rotation = "LEFTRIGHT"
rotation = "R0"

View file

@ -5,7 +5,7 @@
groups = ["default", "dev"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:fbfe1db54d73aa2641413610d5e62d87b02de247293e2af3cd53ee0c283318db"
content_hash = "sha256:70036fd7ee1fe6910ed441a5419d9194d1abc592cbf2f39deb1d7a8e77501d03"
[[metadata.targets]]
requires_python = "==3.12.*"
@ -39,6 +39,18 @@ dependencies = [
"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]]
name = "basedpyright"
version = "1.18.0"

View file

@ -8,6 +8,7 @@ authors = [
dependencies = [
"amaranth>=0.5.1",
"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.*"
readme = "README.md"

View file

@ -2,13 +2,13 @@
# to know its location.
# during operation, it is given a row index, and responds with the data.
from amaranth import Module, Signal, unsigned, Cat
from amaranth import Module, Signal, unsigned
from amaranth.build import Platform
from amaranth.lib import wiring, data
from amaranth.lib.wiring import In, Out
from amaranth.lib import stream
import logging
from itertools import pairwise
from .common import Rgb888Layout
from .geom import DisplayString
@ -16,7 +16,6 @@ from .geom import DisplayString
logger = logging.getLogger(__name__)
# FIXME: sizing should be based off of screen size.
CoordLayout = data.StructLayout({"x": unsigned(10), "y": unsigned(10)})
@ -53,7 +52,7 @@ class AddressGenerator(wiring.Component):
self.geom = geom
super().__init__(
{
"coordstream": Out(
"output": Out(
stream.Signature(data.ArrayLayout(CoordLayout, geom.dimensions.mux))
),
"start": In(1),
@ -75,41 +74,60 @@ class AddressGenerator(wiring.Component):
m.d.comb += translate.input_x.eq(counter)
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.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)]
with m.If(self.start):
m.next = "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.
with m.If(
self.coordstream.ready
& (counter == self.geom.dimensions.length - 1)
self.output.ready & (counter == self.geom.dimensions.length - 1)
):
m.next = "done"
with m.Elif(self.coordstream.ready):
with m.Elif(self.output.ready):
m.d.sync += counter.eq(counter + 1)
pass
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.next = "init"
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):
"""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__(
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.dfunc = dfunc
@ -118,7 +136,7 @@ class BasicFetcher(wiring.Component):
"input": In(
stream.Signature(data.ArrayLayout(CoordLayout, geom.dimensions.mux))
),
"pixstream": Out(
"output": Out(
stream.Signature(data.ArrayLayout(data_shape, geom.dimensions.mux))
),
},
@ -128,20 +146,25 @@ class BasicFetcher(wiring.Component):
def elaborate(self, platform: Platform) -> Module:
m = Module()
# test mode - pass through, r = x + y, g = x - y, b = {y,x}
colors = self.pixstream.payload
colors = self.output.payload
m.d.comb += [
self.input.valid.eq(self.pixstream.valid),
self.input.ready.eq(self.pixstream.ready),
self.output.valid.eq(self.input.valid),
self.input.ready.eq(self.output.ready),
]
for i in range(self.geom.dimensions.mux):
inp = self.input.payload[i]
output = self.dfunc(inp.x, inp.y)
m.d.comb += [
colors[i].red.eq(inp.x + inp.y),
colors[i].green.eq(inp.x - inp.y),
colors[i].blue.eq(inp.x ^ inp.y),
colors[i].red.eq(output["red"]),
colors[i].green.eq(output["green"]),
colors[i].blue.eq(output["blue"]),
]
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)

View file

@ -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.

View file

@ -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)
),
}
@ -313,14 +316,16 @@ class Hub75DataDriver(wiring.Component):
return m
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, *, double_fetch=True):
self.geom = geom
self.double_fetch = double_fetch
super().__init__(
{
"hub75": Out(Hub75Ctrl(n_strings)),
"hub75": Out(Hub75Ctrl(self.geom.n_strings)),
# TODO: fetching routine? maybe it's passed through.
}
)
@ -337,34 +342,38 @@ class Hub75Coordinator(wiring.Component):
donearr = []
startStrings = Signal(1)
stringsDone = Signal(1)
bram_shape = Rgb888Layout if self.double_fetch else data.ArrayLayout(Rgb888Layout, 2)
for i in range(self.n_strings):
sb = SwapBuffer(depth=128, shape=data.ArrayLayout(Rgb666Layout, 2))
for idx, string in enumerate(self.geom.strings):
mdepth = string.dimensions.length
if self.double_fetch:
mdepth = mdepth * 2
sb = SwapBuffer(depth=mdepth, shape=bram_shape)
bufs.append(sb)
stringdriver = Hub75DataDriver(
128, data_shape=Rgb666Layout, double_fetch=False
string.dimensions.length,
data_shape=Rgb888Layout,
double_fetch=self.double_fetch,
)
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 +393,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)

View file

@ -1,8 +1,10 @@
# main entry point for CLI applications.
import logging
import os
import argparse
from groovylight.config import Config
from groovylight.platforms.cxxrtl_sim import emit_cxxrtl
logger = logging.getLogger(__loader__.name)
@ -18,9 +20,21 @@ def setup_logger(args):
handler.setFormatter(formatter)
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)
def dir_path(string):
if os.path.isdir(string):
return string
else:
raise NotADirectoryError(string)
def main():
parser = argparse.ArgumentParser()
@ -41,6 +55,9 @@ def main():
type=argparse.FileType("w"),
metavar="FILE",
)
parser.add_argument(
"-D", "--dump", help="Dump verilog to folder", type=dir_path, metavar="FOLDER"
)
parser.add_argument(
"config",
@ -54,8 +71,17 @@ def main():
setup_logger(args)
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__":

View file

@ -7,3 +7,16 @@
# black box the UDP streaming.
# provide code for display outputs to render onto SDL2 canvas.
# 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=[])

View file

@ -1,10 +1,9 @@
from amaranth.lib import wiring, data
from amaranth import Module
from amaranth.lib import wiring
from amaranth.sim import Simulator
import random
from random import randrange
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
ds_testdata = [
@ -82,7 +81,7 @@ def test_generator(addr, rot):
async def stream_checker(ctx):
while ctx.get(dut.done) == 0:
payload = await stream_get(ctx, dut.coordstream)
payload = await stream_get(ctx, dut.output)
assert expected.pop() == payload
sim.add_testbench(runner)
@ -103,12 +102,12 @@ def test_basic_fetcher(inp, expected):
ds = DisplayString(
Coord(3, 0), DisplayDimensions(128, 64, mux=1), DisplayRotation.R0
)
dut = BasicFetcher(ds, None)
dut = BasicFetcher(ds)
sim = Simulator(dut)
async def test(ctx):
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["green"] == expected["green"]
assert res["blue"] == expected["blue"]
@ -117,3 +116,34 @@ def test_basic_fetcher(inp, expected):
with sim.write_vcd("fetcher.vcd"):
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()

View file

@ -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)

9
src/groovylight/top.py Normal file
View 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