generated from saji/ecp5-template
Compare commits
No commits in common. "8e046b442aa5f91200420f32b828ee2198b4dd42" and "fe4a902bd87489f2e9e8bfc8a24fdf98f2a5b77c" have entirely different histories.
8e046b442a
...
fe4a902bd8
|
@ -1,8 +1,9 @@
|
||||||
from amaranth import Module, Cat, Mux, ShapeLike, Signal, Assert, unsigned
|
from amaranth import Array, Module, Cat, Mux, ShapeLike, Signal, Assert, unsigned
|
||||||
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.memory import Memory, ReadPort, WritePort
|
from amaranth.lib.memory import Memory, ReadPort, WritePort
|
||||||
|
from amaranth.sim import Simulator
|
||||||
from amaranth.utils import ceil_log2
|
from amaranth.utils import ceil_log2
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,7 +18,7 @@ class RGBLayout(data.StructLayout):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
Rgb666Layout = RGBLayout(6, 6, 6)
|
Rgb888Layout = RGBLayout(8, 8, 8)
|
||||||
|
|
||||||
Rgb111Layout = RGBLayout(1, 1, 1)
|
Rgb111Layout = RGBLayout(1, 1, 1)
|
||||||
|
|
||||||
|
@ -112,31 +113,28 @@ class SwapBuffer(wiring.Component):
|
||||||
shape=self.data_shape, depth=self.depth, init=[]
|
shape=self.data_shape, depth=self.depth, init=[]
|
||||||
)
|
)
|
||||||
|
|
||||||
rd0 = self.bram0.read_port()
|
read0 = self.bram0.read_port()
|
||||||
wr0 = self.bram0.write_port()
|
write0 = self.bram0.write_port()
|
||||||
rd1 = self.bram1.read_port()
|
read1 = self.bram1.read_port()
|
||||||
wr1 = self.bram1.write_port()
|
write1 = self.bram1.write_port()
|
||||||
|
|
||||||
|
# for name, member in self.write_port.signature.members.items():
|
||||||
|
# m.d.comb += self.write_port.members[name].eq(Mux(self.selector, write0[name], write1[name]))
|
||||||
|
#
|
||||||
|
|
||||||
m.d.comb += [
|
m.d.comb += [
|
||||||
# wr addres
|
write0.addr.eq(self.write_port.addr),
|
||||||
wr0.addr.eq(self.write_port.addr),
|
write1.addr.eq(self.write_port.addr),
|
||||||
wr1.addr.eq(self.write_port.addr),
|
write0.en.eq(~self.selector),
|
||||||
# write enables are based on selector
|
write1.en.eq(self.selector),
|
||||||
wr0.en.eq(~self.selector),
|
# self.write_port.data.eq(Mux(self.selector, write0.data, write1.data)),
|
||||||
wr1.en.eq(self.selector),
|
write0.data.eq(self.write_port.data),
|
||||||
# connect write data. This is FINE because
|
write1.data.eq(self.write_port.data),
|
||||||
# there is one driver (the external writer)
|
read0.addr.eq(self.read_port.addr),
|
||||||
# and we en based on selector so the other one isn't active
|
read1.addr.eq(self.read_port.addr),
|
||||||
wr0.data.eq(self.write_port.data),
|
read0.en.eq(~self.selector),
|
||||||
wr1.data.eq(self.write_port.data),
|
read1.en.eq(self.selector),
|
||||||
# connect rd address lines
|
self.read_port.data.eq(Mux(self.selector, read1.data, read0.data)),
|
||||||
rd0.addr.eq(self.read_port.addr),
|
|
||||||
rd1.addr.eq(self.read_port.addr),
|
|
||||||
rd0.en.eq(~self.selector),
|
|
||||||
rd1.en.eq(self.selector),
|
|
||||||
# we do this because the read_data lines are driven, this prevents
|
|
||||||
# double-driver situations even though we en using selector above
|
|
||||||
self.read_port.data.eq(Mux(self.selector, rd1.data, rd0.data)),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return m
|
return m
|
||||||
|
@ -148,48 +146,33 @@ class Hub75StringDriver(wiring.Component):
|
||||||
it should send.
|
it should send.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
bcm_select: In(3)
|
||||||
|
done: Out(1)
|
||||||
|
start: In(1)
|
||||||
|
bram_port: In(ReadPort.Signature(addr_width=9, shape=Rgb888Layout))
|
||||||
|
display_out: Out(Hub75Data()) # data signal output.
|
||||||
|
|
||||||
def __init__(self, panel_length: int = 128, *, src_loc_at=0):
|
def __init__(self, panel_length: int = 128, *, src_loc_at=0):
|
||||||
self.panel_length = panel_length
|
self.panel_length = panel_length
|
||||||
super().__init__(
|
super().__init__(None, src_loc_at=src_loc_at)
|
||||||
{
|
|
||||||
"bcm_select": In(3),
|
|
||||||
"done": Out(1),
|
|
||||||
"start": In(1),
|
|
||||||
"bram_port": In(
|
|
||||||
ReadPort.Signature(
|
|
||||||
addr_width=ceil_log2(panel_length),
|
|
||||||
shape=data.ArrayLayout(Rgb666Layout, 2),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"display_out": Out(Hub75Data()),
|
|
||||||
},
|
|
||||||
src_loc_at=src_loc_at,
|
|
||||||
)
|
|
||||||
|
|
||||||
def elaborate(self, platform: Platform) -> Module:
|
def elaborate(self, platform: Platform) -> Module:
|
||||||
m = Module()
|
m = Module()
|
||||||
|
|
||||||
self._counter = counter = Signal(32) # unused count is optimized out
|
# add two memories
|
||||||
|
|
||||||
|
self._counter = counter = Signal(32)
|
||||||
m.d.sync += counter.eq(counter + 1)
|
m.d.sync += counter.eq(counter + 1)
|
||||||
|
|
||||||
ram_rgb0_slice = Cat(
|
ram_rgb_slice = Cat(
|
||||||
self.bram_port.data[0]["red"].bit_select(self.bcm_select, 1),
|
self.bram_port.data["red"].bit_select(self.bcm_select, 1),
|
||||||
self.bram_port.data[0]["blue"].bit_select(self.bcm_select, 1),
|
self.bram_port.data["blue"].bit_select(self.bcm_select, 1),
|
||||||
self.bram_port.data[0]["green"].bit_select(self.bcm_select, 1),
|
self.bram_port.data["green"].bit_select(self.bcm_select, 1),
|
||||||
)
|
)
|
||||||
ram_rgb1_slice = Cat(
|
|
||||||
self.bram_port.data[1]["red"].bit_select(self.bcm_select, 1),
|
|
||||||
self.bram_port.data[1]["blue"].bit_select(self.bcm_select, 1),
|
|
||||||
self.bram_port.data[1]["green"].bit_select(self.bcm_select, 1),
|
|
||||||
)
|
|
||||||
m.d.comb += [
|
|
||||||
self.display_out.rgb0.eq(ram_rgb0_slice),
|
|
||||||
self.display_out.rgb1.eq(ram_rgb1_slice),
|
|
||||||
]
|
|
||||||
m.d.sync += Assert(self.bcm_select < 6)
|
|
||||||
|
|
||||||
pixnum = Signal(ceil_log2(self.panel_length), init=self.panel_length - 1)
|
pixnum = Signal(range(self.panel_length), init=self.panel_length - 1)
|
||||||
m.d.comb += self.bram_port.addr.eq(pixnum)
|
pixrow = Signal(1, init=0)
|
||||||
|
m.d.comb += self.bram_port.addr.eq(Cat(pixrow, pixnum))
|
||||||
|
|
||||||
with m.FSM():
|
with m.FSM():
|
||||||
with m.State("init"):
|
with m.State("init"):
|
||||||
|
@ -197,22 +180,42 @@ class Hub75StringDriver(wiring.Component):
|
||||||
self.done.eq(0),
|
self.done.eq(0),
|
||||||
counter.eq(0),
|
counter.eq(0),
|
||||||
pixnum.eq(self.panel_length - 1),
|
pixnum.eq(self.panel_length - 1),
|
||||||
|
pixrow.eq(0),
|
||||||
]
|
]
|
||||||
with m.If(self.start == 1):
|
with m.If(self.start == 1):
|
||||||
m.d.sync += self.bram_port.en.eq(1)
|
m.d.sync += self.bram_port.en.eq(1)
|
||||||
m.next = "writerow"
|
m.next = "writerow"
|
||||||
|
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.display_out.rgb0.eq(ram_rgb_slice)
|
||||||
|
with m.Elif(counter == 3):
|
||||||
|
m.d.sync += [self.display_out.rgb1.eq(ram_rgb_slice), counter.eq(0)]
|
||||||
|
m.next = "writerow"
|
||||||
with m.State("writerow"):
|
with m.State("writerow"):
|
||||||
with m.If(counter[0] == 0):
|
with m.If(counter[0:1] == 0):
|
||||||
# do nothing
|
# rising edge of the clock
|
||||||
pass
|
m.d.sync += pixrow.eq(0)
|
||||||
|
|
||||||
with m.If((counter[0] == 1) & (pixnum != 0)):
|
with m.If(counter[0:1] == 1):
|
||||||
m.d.sync += pixnum.eq(pixnum - 1)
|
m.d.sync += [
|
||||||
|
pixrow.eq(1),
|
||||||
|
self.display_out.rgb0.eq(ram_rgb_slice),
|
||||||
|
]
|
||||||
|
with m.If(counter[0:1] == 2):
|
||||||
|
m.d.sync += [
|
||||||
|
pixnum.eq(pixnum - 1),
|
||||||
|
pixrow.eq(0),
|
||||||
|
self.display_out.rgb1.eq(ram_rgb_slice),
|
||||||
|
]
|
||||||
|
with m.If(counter == 128 * 2 + 1):
|
||||||
|
|
||||||
with m.Else():
|
|
||||||
m.next = "done"
|
m.next = "done"
|
||||||
with m.State("done"):
|
with m.State("done"):
|
||||||
m.d.sync += [self.done.eq(1), self.bram_port.en.eq(0)]
|
m.d.sync += self.done.eq(1)
|
||||||
m.next = "init"
|
m.next = "init"
|
||||||
|
|
||||||
return m
|
return m
|
||||||
|
@ -223,40 +226,10 @@ class Hub75Coordinator(wiring.Component):
|
||||||
|
|
||||||
def __init__(self, n_strings=1):
|
def __init__(self, n_strings=1):
|
||||||
self.n_strings = n_strings
|
self.n_strings = n_strings
|
||||||
super().__init__(
|
super().__init__()
|
||||||
{
|
|
||||||
"ctrl": Out(Hub75Ctrl),
|
|
||||||
"data": data.ArrayLayout(Hub75Data, n_strings),
|
|
||||||
# TODO: fetching routine? maybe it's passed through.
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def elaborate(self, platform: Platform) -> Module:
|
def elaborate(self, platform: Platform) -> Module:
|
||||||
m = Module()
|
m = Module()
|
||||||
# swapline is which buffer we are using vs sending out.
|
|
||||||
swapline = Signal(1)
|
|
||||||
|
|
||||||
# for each string, spawn a swapbuffer + stringdriver and connect.
|
|
||||||
# don't worry about fetching for now.
|
|
||||||
self.strings = strings = []
|
|
||||||
self.buffers = bufs = []
|
|
||||||
donearr = []
|
|
||||||
startStrings = Signal(1)
|
|
||||||
stringsDone = Signal(1)
|
|
||||||
for i in range(self.n_strings):
|
|
||||||
sb = SwapBuffer(depth=128, shape=data.ArrayLayout(Rgb666Layout, 2))
|
|
||||||
bufs += sb
|
|
||||||
stringdriver = Hub75StringDriver(128)
|
|
||||||
strings += stringdriver
|
|
||||||
wiring.connect(m, sb.read_port, stringdriver.bram_port)
|
|
||||||
m.d.comb += [
|
|
||||||
self.data[i].eq(stringdriver.display_out),
|
|
||||||
stringdriver.start.eq(startStrings),
|
|
||||||
sb.selector.eq(swapline),
|
|
||||||
]
|
|
||||||
m.submodules += [sb, stringdriver]
|
|
||||||
donearr += stringdriver.done
|
|
||||||
m.d.comb += stringsDone.eq(Cat(*donearr).all())
|
|
||||||
|
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
@ -266,10 +239,6 @@ class Hub75EDriver(wiring.Component):
|
||||||
This version is faster than most implementations by merging the exposure
|
This version is faster than most implementations by merging the exposure
|
||||||
period and the data-write period to happen simultaneously. As a result,
|
period and the data-write period to happen simultaneously. As a result,
|
||||||
the display can be brighter due to higher duty cycle.
|
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)
|
start: In(1)
|
||||||
|
|
|
@ -1,124 +0,0 @@
|
||||||
# Geometry-related classes and functions. Manipulations and
|
|
||||||
# generation of panel-layout metadata for use in the HDL.
|
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, order=True)
|
|
||||||
class Coord:
|
|
||||||
"""Coordinate class. Uses computer-graphics standard coordinate system,
|
|
||||||
where X=0, Y=0 is top left. +X goes right. +Y goes down.
|
|
||||||
"""
|
|
||||||
|
|
||||||
x: int
|
|
||||||
y: int
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
if self.x < 0 or self.y < 0:
|
|
||||||
raise RuntimeError("x and y must both be >= 0")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class BBox:
|
|
||||||
"""Bounding box class. Captures the top left coordinate and bottom right coordinate
|
|
||||||
of an object"""
|
|
||||||
|
|
||||||
topleft: Coord
|
|
||||||
bottomright: Coord
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
if not self.topleft < self.bottomright:
|
|
||||||
raise RuntimeError("topleft must be strictly less than bottomright")
|
|
||||||
|
|
||||||
def contains(self, c: Coord) -> bool:
|
|
||||||
return c > self.topleft and c < self.bottomright
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class DisplayDimensions:
|
|
||||||
"""Represents the dimensions of a display string, in length x height. Notably
|
|
||||||
this is in local coordinates to the display. The display top left is 0,0.
|
|
||||||
Uses length/height notation to separate it from coord
|
|
||||||
"""
|
|
||||||
|
|
||||||
length: int
|
|
||||||
height: int
|
|
||||||
|
|
||||||
|
|
||||||
class DisplayRotation(Enum):
|
|
||||||
"""Display rotation enums. The names indicate the general direction
|
|
||||||
of data flow. most normal displays (LCDs, CRTs) are LEFTRIGHT,
|
|
||||||
since they scan left-to-right, top to bottom.
|
|
||||||
|
|
||||||
Note that the direction of subsequent lines is not always clear.
|
|
||||||
They are enumerated below:
|
|
||||||
LEFTRIGHT -> next line is below it (-Y)
|
|
||||||
UPDOWN -> next line is to the LEFT (-X)
|
|
||||||
DOWNUP -> next line is to the RIGHT (+X)
|
|
||||||
RIGHTLEFT -> next line is above it (+Y)
|
|
||||||
|
|
||||||
Generally, prefer LEFTRIGHT/UPDOWN over other rotations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
LEFTRIGHT = 0
|
|
||||||
UPDOWN = 1
|
|
||||||
DOWNUP = 2
|
|
||||||
RIGHTLEFT = 3 # why are you like this.
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class _DisplayString:
|
|
||||||
"""Internal class to represent a string of HUB75 displays.
|
|
||||||
|
|
||||||
position: (X,Y) coordinates of the local top-left of the display
|
|
||||||
|
|
||||||
dimensions: (length, height) local-coordinate dimensions of the display.
|
|
||||||
|
|
||||||
rotation: DisplayRotation: the orientation of the display.
|
|
||||||
"""
|
|
||||||
|
|
||||||
position: Coord
|
|
||||||
dimensions: DisplayDimensions
|
|
||||||
rotation: DisplayRotation
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bbox(self) -> BBox:
|
|
||||||
"""Returns the bounding box of the display based on the dimensions, position, and rotation."""
|
|
||||||
x = self.position.x
|
|
||||||
y = self.position.y
|
|
||||||
l = self.dimensions.length
|
|
||||||
h = self.dimensions.height
|
|
||||||
match self.rotation:
|
|
||||||
case DisplayRotation.LEFTRIGHT:
|
|
||||||
return BBox(Coord(x, y), Coord(x + l, y + h))
|
|
||||||
case DisplayRotation.UPDOWN:
|
|
||||||
return BBox(Coord(x - h, y), Coord(x, y + l))
|
|
||||||
case DisplayRotation.DOWNUP:
|
|
||||||
return BBox(Coord(x, y + l), Coord(x + h, y))
|
|
||||||
case DisplayRotation.RIGHTLEFT:
|
|
||||||
return BBox(Coord(x - l, y - h), Coord(x, y))
|
|
||||||
|
|
||||||
def contains_pix(self, coord: Coord) -> bool:
|
|
||||||
"""Checks if the given coordinate is inside this display."""
|
|
||||||
return self.bbox.contains(coord)
|
|
||||||
|
|
||||||
|
|
||||||
class DisplayGeometry:
|
|
||||||
"""Represents a display based on several strings in different positions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *, strict: bool = False):
|
|
||||||
self.strict = strict
|
|
||||||
pass
|
|
||||||
|
|
||||||
def add_string(self, position: (int, int), rot: int, dimensions: (int, int)):
|
|
||||||
"""Add a new string to the display. This new string is located at
|
|
||||||
a specific point, and has a direction, along with dimension that reveal
|
|
||||||
the number of address lines (typically 64, with 1:32 selection so 5 address
|
|
||||||
bits) and the total length of the string which is used to size the line
|
|
||||||
buffers.
|
|
||||||
|
|
||||||
When in strict mode, this method may throw an exception if this new string
|
|
||||||
will overlap with an existing string.
|
|
||||||
"""
|
|
12
src/groovylight/lineram.py
Normal file
12
src/groovylight/lineram.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from amaranth import Module, Cat, Signal, Assert, unsigned
|
||||||
|
from amaranth.build import Platform
|
||||||
|
|
||||||
|
from amaranth.lib import wiring, data
|
||||||
|
from amaranth.lib.wiring import In, Out
|
||||||
|
from amaranth.lib.memory import Memory
|
||||||
|
# file to wrap line ram using PDPw16KD
|
||||||
|
# this is tricky.
|
||||||
|
|
||||||
|
|
||||||
|
def lineram():
|
||||||
|
return Memory(shape=unsigned(24), depth=512)
|
|
@ -5,7 +5,7 @@ from amaranth.lib.wiring import In, Out
|
||||||
from amaranth.lib.memory import Memory, WritePort
|
from amaranth.lib.memory import Memory, WritePort
|
||||||
from amaranth.sim import Simulator
|
from amaranth.sim import Simulator
|
||||||
|
|
||||||
from .bitslicer import Hub75StringDriver, Rgb666Layout
|
from .bitslicer import Hub75StringDriver, Rgb888Layout
|
||||||
|
|
||||||
|
|
||||||
def test_stringdriver():
|
def test_stringdriver():
|
||||||
|
@ -15,9 +15,7 @@ def test_stringdriver():
|
||||||
# 3. slice the correct bit from the data.
|
# 3. slice the correct bit from the data.
|
||||||
m = Module()
|
m = Module()
|
||||||
m.submodules.dut = dut = Hub75StringDriver()
|
m.submodules.dut = dut = Hub75StringDriver()
|
||||||
m.submodules.mem = mem = Memory(
|
m.submodules.mem = mem = Memory(shape=Rgb888Layout, depth=512, init=[])
|
||||||
shape=data.ArrayLayout(Rgb666Layout, 2), depth=128, init=[]
|
|
||||||
)
|
|
||||||
port = mem.read_port()
|
port = mem.read_port()
|
||||||
|
|
||||||
wiring.connect(m, port, dut.bram_port)
|
wiring.connect(m, port, dut.bram_port)
|
||||||
|
@ -28,11 +26,12 @@ def test_stringdriver():
|
||||||
ctx.set(dut.start, 1)
|
ctx.set(dut.start, 1)
|
||||||
await ctx.tick()
|
await ctx.tick()
|
||||||
ctx.set(dut.start, 0)
|
ctx.set(dut.start, 0)
|
||||||
assert ctx.get(dut.bram_port.en) == 1
|
assert(ctx.get(dut.bram_port.en) == 1)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
sim = Simulator(m)
|
sim = Simulator(m)
|
||||||
sim.add_clock(1e-6)
|
sim.add_clock(1e-6)
|
||||||
|
|
||||||
with sim.write_vcd("output.vcd"):
|
with sim.write_vcd("output.vcd"):
|
||||||
|
|
||||||
sim.run_until(1e-6 * 1000)
|
sim.run_until(1e-6 * 1000)
|
||||||
|
|
|
@ -5,11 +5,11 @@ from amaranth.lib.wiring import In, Out
|
||||||
from amaranth.lib.memory import Memory, WritePort
|
from amaranth.lib.memory import Memory, WritePort
|
||||||
from amaranth.sim import Simulator
|
from amaranth.sim import Simulator
|
||||||
|
|
||||||
from .bitslicer import Hub75StringDriver, Rgb666Layout, SwapBuffer
|
from .bitslicer import Hub75StringDriver, Rgb888Layout, SwapBuffer
|
||||||
|
|
||||||
|
|
||||||
def test_swapbuffer():
|
def test_swapbuffer():
|
||||||
dut = SwapBuffer(Rgb666Layout, 512)
|
dut = SwapBuffer(Rgb888Layout, 512)
|
||||||
sim = Simulator(dut)
|
sim = Simulator(dut)
|
||||||
sim.add_clock(1e-6)
|
sim.add_clock(1e-6)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue