Compare commits

..

3 commits

Author SHA1 Message Date
saji 8e046b442a initial coordinator
Some checks failed
Verilator Unit Tests / Test (push) Failing after 3m25s
2024-09-20 14:59:44 -05:00
saji fbb39a85e5 added geometry module 2024-09-20 14:59:17 -05:00
saji 66b492e147 refactor around rgb666
remove 4xclocking for double-fetch architecture (still using 2xclock for
better S/H)
2024-09-19 01:45:01 -05:00
5 changed files with 231 additions and 87 deletions

View file

@ -1,9 +1,8 @@
from amaranth import Array, Module, Cat, Mux, ShapeLike, Signal, Assert, unsigned
from amaranth import Module, Cat, Mux, ShapeLike, 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, ReadPort, WritePort
from amaranth.sim import Simulator
from amaranth.utils import ceil_log2
@ -18,7 +17,7 @@ class RGBLayout(data.StructLayout):
)
Rgb888Layout = RGBLayout(8, 8, 8)
Rgb666Layout = RGBLayout(6, 6, 6)
Rgb111Layout = RGBLayout(1, 1, 1)
@ -113,28 +112,31 @@ class SwapBuffer(wiring.Component):
shape=self.data_shape, depth=self.depth, init=[]
)
read0 = self.bram0.read_port()
write0 = self.bram0.write_port()
read1 = self.bram1.read_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]))
#
rd0 = self.bram0.read_port()
wr0 = self.bram0.write_port()
rd1 = self.bram1.read_port()
wr1 = self.bram1.write_port()
m.d.comb += [
write0.addr.eq(self.write_port.addr),
write1.addr.eq(self.write_port.addr),
write0.en.eq(~self.selector),
write1.en.eq(self.selector),
# self.write_port.data.eq(Mux(self.selector, write0.data, write1.data)),
write0.data.eq(self.write_port.data),
write1.data.eq(self.write_port.data),
read0.addr.eq(self.read_port.addr),
read1.addr.eq(self.read_port.addr),
read0.en.eq(~self.selector),
read1.en.eq(self.selector),
self.read_port.data.eq(Mux(self.selector, read1.data, read0.data)),
# wr addres
wr0.addr.eq(self.write_port.addr),
wr1.addr.eq(self.write_port.addr),
# write enables are based on selector
wr0.en.eq(~self.selector),
wr1.en.eq(self.selector),
# connect write data. This is FINE because
# there is one driver (the external writer)
# and we en based on selector so the other one isn't active
wr0.data.eq(self.write_port.data),
wr1.data.eq(self.write_port.data),
# connect rd address lines
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
@ -146,33 +148,48 @@ class Hub75StringDriver(wiring.Component):
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):
self.panel_length = panel_length
super().__init__(None, src_loc_at=src_loc_at)
super().__init__(
{
"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:
m = Module()
# add two memories
self._counter = counter = Signal(32)
self._counter = counter = Signal(32) # unused count is optimized out
m.d.sync += counter.eq(counter + 1)
ram_rgb_slice = Cat(
self.bram_port.data["red"].bit_select(self.bcm_select, 1),
self.bram_port.data["blue"].bit_select(self.bcm_select, 1),
self.bram_port.data["green"].bit_select(self.bcm_select, 1),
ram_rgb0_slice = Cat(
self.bram_port.data[0]["red"].bit_select(self.bcm_select, 1),
self.bram_port.data[0]["blue"].bit_select(self.bcm_select, 1),
self.bram_port.data[0]["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(range(self.panel_length), init=self.panel_length - 1)
pixrow = Signal(1, init=0)
m.d.comb += self.bram_port.addr.eq(Cat(pixrow, pixnum))
pixnum = Signal(ceil_log2(self.panel_length), init=self.panel_length - 1)
m.d.comb += self.bram_port.addr.eq(pixnum)
with m.FSM():
with m.State("init"):
@ -180,42 +197,22 @@ class Hub75StringDriver(wiring.Component):
self.done.eq(0),
counter.eq(0),
pixnum.eq(self.panel_length - 1),
pixrow.eq(0),
]
with m.If(self.start == 1):
m.d.sync += self.bram_port.en.eq(1)
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.If(counter[0:1] == 0):
# rising edge of the clock
m.d.sync += pixrow.eq(0)
with m.If(counter[0] == 0):
# do nothing
pass
with m.If(counter[0:1] == 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.If((counter[0] == 1) & (pixnum != 0)):
m.d.sync += pixnum.eq(pixnum - 1)
with m.Else():
m.next = "done"
with m.State("done"):
m.d.sync += self.done.eq(1)
m.d.sync += [self.done.eq(1), self.bram_port.en.eq(0)]
m.next = "init"
return m
@ -226,10 +223,40 @@ class Hub75Coordinator(wiring.Component):
def __init__(self, n_strings=1):
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:
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
@ -239,6 +266,10 @@ class Hub75EDriver(wiring.Component):
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)

124
src/groovylight/geom.py Normal file
View file

@ -0,0 +1,124 @@
# 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.
"""

View file

@ -1,12 +0,0 @@
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)

View file

@ -5,17 +5,19 @@ from amaranth.lib.wiring import In, Out
from amaranth.lib.memory import Memory, WritePort
from amaranth.sim import Simulator
from .bitslicer import Hub75StringDriver, Rgb888Layout
from .bitslicer import Hub75StringDriver, Rgb666Layout
def test_stringdriver():
# the string driver test must
# the string driver test must
# 1. finish
# 2. strobe through all of the data in the array
# 3. slice the correct bit from the data.
m = Module()
m.submodules.dut = dut = Hub75StringDriver()
m.submodules.mem = mem = Memory(shape=Rgb888Layout, depth=512, init=[])
m.submodules.mem = mem = Memory(
shape=data.ArrayLayout(Rgb666Layout, 2), depth=128, init=[]
)
port = mem.read_port()
wiring.connect(m, port, dut.bram_port)
@ -26,12 +28,11 @@ def test_stringdriver():
ctx.set(dut.start, 1)
await ctx.tick()
ctx.set(dut.start, 0)
assert(ctx.get(dut.bram_port.en) == 1)
assert ctx.get(dut.bram_port.en) == 1
pass
sim = Simulator(m)
sim.add_clock(1e-6)
with sim.write_vcd("output.vcd"):
sim.run_until(1e-6 * 1000)

View file

@ -5,11 +5,11 @@ from amaranth.lib.wiring import In, Out
from amaranth.lib.memory import Memory, WritePort
from amaranth.sim import Simulator
from .bitslicer import Hub75StringDriver, Rgb888Layout, SwapBuffer
from .bitslicer import Hub75StringDriver, Rgb666Layout, SwapBuffer
def test_swapbuffer():
dut = SwapBuffer(Rgb888Layout, 512)
dut = SwapBuffer(Rgb666Layout, 512)
sim = Simulator(dut)
sim.add_clock(1e-6)