Compare commits

..

No commits in common. "09485a9753082296029a91ea2951acbc06a7674b" and "5f54b8acd8978fd8281b3dc8359041049b94c747" have entirely different histories.

11 changed files with 184 additions and 186 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. strict = true # allows some wacky configurations, like panels that overlap.
[[display.strings]] [[display.strings]]
position = { x = 0, y = 0 } position = { x = 64, y = 0 }
dimensions = { length = 256, height = 64 } dimensions = { length = 256, height = 64 }
rotation = "R90" rotation = "UPDOWN"
[[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 = "R0" rotation = "LEFTRIGHT"
[[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 = "R0" rotation = "LEFTRIGHT"
[[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 = "R0" rotation = "LEFTRIGHT"
[[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 = "R0" rotation = "LEFTRIGHT"

View file

@ -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:70036fd7ee1fe6910ed441a5419d9194d1abc592cbf2f39deb1d7a8e77501d03" content_hash = "sha256:fbfe1db54d73aa2641413610d5e62d87b02de247293e2af3cd53ee0c283318db"
[[metadata.targets]] [[metadata.targets]]
requires_python = "==3.12.*" requires_python = "==3.12.*"
@ -39,18 +39,6 @@ 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"

View file

@ -8,7 +8,6 @@ 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"

View file

@ -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,6 +16,7 @@ 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)})
@ -52,7 +53,7 @@ class AddressGenerator(wiring.Component):
self.geom = geom self.geom = geom
super().__init__( super().__init__(
{ {
"output": Out( "coordstream": Out(
stream.Signature(data.ArrayLayout(CoordLayout, geom.dimensions.mux)) stream.Signature(data.ArrayLayout(CoordLayout, geom.dimensions.mux))
), ),
"start": In(1), "start": In(1),
@ -74,60 +75,41 @@ 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.output.payload.eq(translate.output) m.d.comb += self.coordstream.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.output.valid.eq(0)] m.d.comb += [self.done.eq(0), self.coordstream.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.output.valid.eq(1) m.d.comb += self.coordstream.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.output.ready & (counter == self.geom.dimensions.length - 1) self.coordstream.ready
& (counter == self.geom.dimensions.length - 1)
): ):
m.next = "done" m.next = "done"
with m.Elif(self.output.ready): with m.Elif(self.coordstream.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.output.valid.eq(0) m.d.comb += self.coordstream.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) -> dict rgb values. """A generic function-based fetcher. Takes a function of the form f(x,y: int) -> RGB."""
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, self, geom: DisplayString, dfunc, data_shape=Rgb888Layout, *, src_loc_at=0
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
@ -136,7 +118,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))
), ),
"output": Out( "pixstream": Out(
stream.Signature(data.ArrayLayout(data_shape, geom.dimensions.mux)) stream.Signature(data.ArrayLayout(data_shape, geom.dimensions.mux))
), ),
}, },
@ -146,25 +128,20 @@ class BasicFetcher(wiring.Component):
def elaborate(self, platform: Platform) -> Module: def elaborate(self, platform: Platform) -> Module:
m = Module() m = Module()
colors = self.output.payload # test mode - pass through, r = x + y, g = x - y, b = {y,x}
colors = self.pixstream.payload
m.d.comb += [ m.d.comb += [
self.output.valid.eq(self.input.valid), self.input.valid.eq(self.pixstream.valid),
self.input.ready.eq(self.output.ready), self.input.ready.eq(self.pixstream.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(output["red"]), colors[i].red.eq(inp.x + inp.y),
colors[i].green.eq(output["green"]), colors[i].green.eq(inp.x - inp.y),
colors[i].blue.eq(output["blue"]), colors[i].blue.eq(inp.x ^ inp.y),
] ]
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)

View file

@ -210,9 +210,6 @@ 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,12 +3,9 @@ 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
@ -37,10 +34,10 @@ class SwapBuffer(wiring.Component):
super().__init__( super().__init__(
{ {
"selector": In(1), "selector": In(1),
"read_port": Out( "read_port": In(
ReadPort.Signature(addr_width=ceil_log2(depth), shape=shape) ReadPort.Signature(addr_width=ceil_log2(depth), shape=shape)
), ),
"write_port": Out( "write_port": In(
WritePort.Signature(addr_width=ceil_log2(depth), shape=shape) WritePort.Signature(addr_width=ceil_log2(depth), shape=shape)
), ),
} }
@ -316,16 +313,14 @@ 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, geom: DisplayGeometry, *, double_fetch=True): def __init__(self, n_strings=1):
self.geom = geom self.n_strings = n_strings
self.double_fetch = double_fetch
super().__init__( super().__init__(
{ {
"hub75": Out(Hub75Ctrl(self.geom.n_strings)), "hub75": Out(Hub75Ctrl(n_strings)),
# TODO: fetching routine? maybe it's passed through. # TODO: fetching routine? maybe it's passed through.
} }
) )
@ -342,38 +337,34 @@ 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 idx, string in enumerate(self.geom.strings): for i in range(self.n_strings):
mdepth = string.dimensions.length sb = SwapBuffer(depth=128, shape=data.ArrayLayout(Rgb666Layout, 2))
if self.double_fetch:
mdepth = mdepth * 2
sb = SwapBuffer(depth=mdepth, shape=bram_shape)
bufs.append(sb) bufs.append(sb)
stringdriver = Hub75DataDriver( stringdriver = Hub75DataDriver(
string.dimensions.length, 128, data_shape=Rgb666Layout, double_fetch=False
data_shape=Rgb888Layout,
double_fetch=self.double_fetch,
) )
strings.append(stringdriver) strings.append(stringdriver)
wiring.connect(m, sb.read_port, stringdriver.bram) wiring.connect(m, sb.read_port, stringdriver.bram_port)
# wiring.connect(m, self.hub75.data[idx], stringdriver.data.flip())
m.d.comb += [ m.d.comb += [
self.hub75.data[idx].rgb0.eq(stringdriver.data.rgb0), self.data[i].eq(stringdriver.display_out),
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.
# right now we're just going to use a basicFetcher # WIP: pass in fetcher/pixgen/geometry.
# TODO: support SDRAM framebuffer # 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.
with m.FSM(): with m.FSM():
with m.State("init"): with m.State("init"):
@ -393,3 +384,131 @@ 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

@ -1,10 +1,8 @@
# 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)
@ -20,21 +18,9 @@ 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()
@ -55,9 +41,6 @@ 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",
@ -71,17 +54,8 @@ 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__":

View file

@ -7,16 +7,3 @@
# 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=[])

View file

@ -1,9 +1,10 @@
from amaranth import Module from amaranth.lib import wiring, data
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, chain_streams from groovylight.fetcher import AddressConverter, AddressGenerator, BasicFetcher
from groovylight.geom import DisplayString, Coord, DisplayDimensions, DisplayRotation from groovylight.geom import DisplayString, Coord, DisplayDimensions, DisplayRotation
ds_testdata = [ ds_testdata = [
@ -81,7 +82,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.output) payload = await stream_get(ctx, dut.coordstream)
assert expected.pop() == payload assert expected.pop() == payload
sim.add_testbench(runner) sim.add_testbench(runner)
@ -102,12 +103,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) dut = BasicFetcher(ds, None)
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.output.payload)[0] res = ctx.get(dut.pixstream.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"]
@ -116,34 +117,3 @@ 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()

View file

@ -7,7 +7,6 @@ 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,
@ -147,13 +146,10 @@ 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()
geom = DisplayGeometry() m.submodules.dut = dut = Hub75Coordinator(1)
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)

View file

@ -1,9 +0,0 @@
# Creates a top-level module based on the display configuration.
from amaranth import Signal
from amaranth.lib import wiring, data
from .hub75