Compare commits

...

5 commits

Author SHA1 Message Date
Saji 0c1d924166 add cxxrtl platform
All checks were successful
Unit Tests / Test (push) Successful in 2m15s
2024-09-27 16:28:04 -05:00
Saji 3d49afe6ed rename bitslicer to hub75, rework hub75ctrl sig 2024-09-27 16:27:52 -05:00
Saji c328d174ad skip platform tests when toolchain missing 2024-09-27 16:27:38 -05:00
Saji ef3df3ae92 format code 2024-09-27 01:06:47 -05:00
Saji bcac3d0dcd make basic config module 2024-09-27 01:06:11 -05:00
15 changed files with 191 additions and 56 deletions

52
example-config.toml Normal file
View file

@ -0,0 +1,52 @@
# Groovylight configuration base file.
# make your edits using this, at the moment the defaults are not stored
# in the program so every value must be specified.
[hardware]
# The target platform.
# Valid options: "cxxrtl", "colorlight"
type = "cxxrtl"
# hardware-specific options, these are gently parsed i.e extra options are ignored.
# colorlight options
# Maps the strings array onto the colorlight connectors.
# it's fine (but will raise a warning) if the list is longer than the number of strings.
stringmapping = [ "J1", "J2", "J3", "J4", "J5", "J6", "J7", "J8" ]
# cxxrtl options
[network]
# Network port to listen on for UDP datagrams.
port = 9999
# IPv4 address to use for the Ethernet port.
# This setting is ignored for CXXRTL.
# 'dhcp' will get the IP at runtime.
ip = "dhcp" # Can also be e.g. "192.168.0.123"
[display]
strict = true # allows some wacky configurations, like panels that overlap.
[[display.strings]]
position = { x = 31, y = 0 }
dimensions = { width = 256, height = 64 }
rotation = "UPDOWN"
[[display.strings]]
position = { x = 32, y = 0 }
dimensions = { width = 256, height = 64 }
rotation = "LEFTRIGHT"
[[display.strings]]
position = { x = 32, y = 64 }
dimensions = { width = 256, height = 64 }
rotation = "LEFTRIGHT"
[[display.strings]]
position = { x = 32, y = 128 }
dimensions = { width = 256, height = 64 }
rotation = "LEFTRIGHT"
[[display.strings]]
position = { x = 32, y = 192 }
dimensions = { width = 256, height = 64 }
rotation = "LEFTRIGHT"

View file

@ -1,4 +1,4 @@
from amaranth import unsigned
from amaranth import Array, unsigned
from amaranth.lib import wiring, data
from amaranth.lib.wiring import Out
@ -42,13 +42,14 @@ class Hub75Ctrl(wiring.Signature):
"""
def __init__(self):
def __init__(self, n_strings: int = 1):
super().__init__(
{
"latch": Out(1),
"oe": Out(1),
"addr": Out(5),
"display_clk": Out(1),
"data": Out(Hub75Data()).array(n_strings),
}
)

25
src/groovylight/config.py Normal file
View file

@ -0,0 +1,25 @@
import tomllib
from pathlib import Path
from groovylight.geom import (
Coord,
DisplayDimensions,
DisplayGeometry,
DisplayRotation,
DisplayString,
)
class Config:
def __init__(self, file: Path) -> None:
with file.open("rb") as f:
self.conf = tomllib.load(f)
self.geom = DisplayGeometry(self.conf.display.strict)
for string in self.conf.display.strings:
pos = Coord(**string.position)
dims = DisplayDimensions(**string.dimensions)
rot = DisplayRotation[string.rotation]
disp = DisplayString(pos, dims, rot)
self.geom.add_string(disp)
self.network = self.conf.network # FIXME: parse this properly.

View file

@ -24,8 +24,30 @@
# This file contains code to generate these timing adjustments and
# control/quantify them.
# FIXME: This doesn't work. As much as I wish it did. Because the numbers are still
# added linearly. values that are between the powers of two (which are fitted to the log
# curve) are not even remotely close to matching.
# thankfully a gamma LUT is only 64 elements. we will need 3 muxes per color (1 per channel).
from math import pow
from amaranth import Module, Cat, Mux, ShapeLike, Signal, Assert
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.utils import ceil_log2
class GammaLUT(wiring.Component):
def __init__(self, n_bits: int, gamma: float = 2.2):
self.gamma = gamma
self.n_bits = n_bits
self.lut = [pow(x, gamma) for x in range(2**n_bits)]
# TODO: we can combo it maybe with the variable timing method.
super().__init__(wiring.Signature({}))
def _gammavec(vals: [float], g: float) -> [float]:
return [pow(x, g) for x in vals]

View file

@ -173,7 +173,8 @@ class DisplayGeometry:
will overlap with an existing string.
"""
for e in self._strings:
if e.intersects(s):
pass
if self.strict:
for e in self._strings:
if e.intersects(s):
raise RuntimeError(f"node {e} intersects with {s}")
self._strings.append(s)

View file

@ -8,7 +8,6 @@ from amaranth.utils import ceil_log2
from .common import Rgb666Layout, Hub75Stream, Hub75Ctrl, Hub75Data
class SwapBuffer(wiring.Component):
"""A pair of BRAMs for holdling line data that are swapped between using an external signal.
@ -164,8 +163,7 @@ class Hub75Coordinator(wiring.Component):
self.n_strings = n_strings
super().__init__(
{
"ctrl": Out(Hub75Ctrl),
"data": data.ArrayLayout(Hub75Data, n_strings),
"hub75": Out(Hub75Ctrl(n_strings)),
# TODO: fetching routine? maybe it's passed through.
}
)

View file

@ -1,10 +1,8 @@
# main entry point for CLI applications.
import logging
import argparse
logger = logging.getLogger(__loader__.name)
@ -12,9 +10,7 @@ def setup_logger(args):
root_logger = logging.getLogger()
handler = logging.StreamHandler()
formatter = logging.Formatter(
style="{", fmt="{levelname:s}: {name:s}: {message:s}"
)
formatter = logging.Formatter(style="{", fmt="{levelname:s}: {name:s}: {message:s}")
handler.setFormatter(formatter)
root_logger.addHandler(handler)
@ -33,6 +29,7 @@ def main():
const=logging.DEBUG,
default=logging.INFO,
)
parser.add_argument(
"-L",
"--log-file",

View file

View file

@ -23,7 +23,9 @@ class Colorlight_5A75B_R82Platform(LatticeECP5Platform):
Subsignal("copi", Pins("T8", dir="o")),
Attrs(IO_TYPE="LVCMOS33"),
),
*LEDResources(pins="T6", invert=True, attrs=Attrs(IO_TYPE="LVCMOS33", DRIVE="4")),
*LEDResources(
pins="T6", invert=True, attrs=Attrs(IO_TYPE="LVCMOS33", DRIVE="4")
),
*ButtonResources(
pins="R7", invert=True, attrs=Attrs(IO_TYPE="LVCMOS33", PULLMODE="UP")
),

View file

@ -0,0 +1,9 @@
# CXXRTL-based simulator.
# Uses SDL2 + black-box network stack to simulate the entire design.
# Tasks:
# Generate header with network port information
# black box the UDP streaming.
# provide code for display outputs to render onto SDL2 canvas.
# compile code (optionally)

View file

@ -13,67 +13,80 @@ from amaranth.lib.wiring import In, Out
# word size = 32
# 8 megabytes data.
class SDRAMSignature(wiring.Signature):
""" Signature of a variable-size sdram. Data is split between in/out and has out_en"""
"""Signature of a variable-size sdram. Data is split between in/out and has out_en"""
def __init__(self, addr_width, data_width=32, bank_width=2):
super().__init__({
"nCS": Out(1),
"cke": Out(1),
"nRAS": Out(1),
"nCAS": Out(1),
"nWE": Out(1),
"addr": Out(addr_width),
"data_out": Out(data_width),
"data_in": In(data_width),
# todo: use dqm
"data_wren": Out(1),
"bank_cs": Out(bank_width),
})
super().__init__(
{
"nCS": Out(1),
"cke": Out(1),
"nRAS": Out(1),
"nCAS": Out(1),
"nWE": Out(1),
"addr": Out(addr_width),
"data_out": Out(data_width),
"data_in": In(data_width),
# todo: use dqm
"data_wren": Out(1),
"bank_cs": Out(bank_width),
}
)
class _WriteBurstLength(enum.Enum, shape=1):
"""MRS Write burst mode"""
BURST = 0
SINGLE_BIT = 1
class _TestMode(enum.Enum, shape=2):
""" The "test mode" of the sdram. This is always zero pretty much"""
"""The "test mode" of the sdram. This is always zero pretty much"""
MODE_REGISTER_SET = 0
RESERVED0 = 1
RESERVED1 = 2
RESERVED2 = 3
class _CASLatency(enum.Enum, shape=3):
""" How many cycles of latency for the column address select to complete """
"""How many cycles of latency for the column address select to complete"""
CYCL2 = 2
CYCL3 = 3
class _BurstType(enum.Enum, shape=1):
SEQUENTIAL = 0
INTERLEAVED = 1
class _BurstLength(enum.IntEnum, shape=3):
""" The size of the burst """
"""The size of the burst"""
SINGLE = 0
DUAL = 1
QUAD = 2
OCT = 3
FULL_PAGE = 7 # this is 256 words?
FULL_PAGE = 7 # this is 256 words?
class _Command(enum.Enum):
""" Command set for SDRAM """
"""Command set for SDRAM"""
MRS_WRITE = 0
ACTIVATE = 1
PRECHARGE = 2
WRITE = 3
READ = 4
CBR = 5 # auto refresh
CBR = 5 # auto refresh
SELF_REFRESH = 6
BRST_STOP = 7
NOP = 8
class BankController(wiring.Component):
"""Manages a single Bank. Has a bank locking/state tracker,
can issue commands"""

View file

@ -2,11 +2,12 @@ from ..geom import Coord, BBox
import pytest
def test_coord_comparison():
c1 = Coord(0,0)
c2 = Coord(0,1)
c3 = Coord(1,1)
c3_other = Coord(1,1)
c1 = Coord(0, 0)
c2 = Coord(0, 1)
c3 = Coord(1, 1)
c3_other = Coord(1, 1)
assert c1 < c3
assert not c1 < c2, "both x,y must be greater/lt/eq"
@ -15,23 +16,24 @@ def test_coord_comparison():
assert c3 == c3_other, "Coords with same numbers should equal each other"
assert c3 != c2
def test_coord_construction():
with pytest.raises(RuntimeError):
Coord(0,-1)
Coord(0, -1)
def test_bbox():
b = BBox(Coord(1,1), Coord(3,2))
b = BBox(Coord(1, 1), Coord(3, 2))
assert b.width == 2
assert b.height == 1
assert b.contains(Coord(1,2))
assert not b.contains(Coord(0,0))
assert b.contains(Coord(1, 2))
assert not b.contains(Coord(0, 0))
# TODO: test .intersect(other)
with pytest.raises(RuntimeError):
BBox(Coord(0,0), Coord(1,0))
BBox(Coord(0, 0), Coord(1, 0))
with pytest.raises(RuntimeError):
BBox(Coord(1,1), Coord(0,0))
BBox(Coord(1, 1), Coord(0, 0))

View file

@ -5,7 +5,7 @@ from amaranth.lib.wiring import In, Out
from amaranth.lib.memory import Memory, WritePort
from amaranth.sim import Simulator
from ..bitslicer import Hub75StringDriver, Rgb666Layout
from ..hub75 import Hub75Coordinator, Hub75StringDriver, Rgb666Layout
def test_stringdriver():
@ -36,3 +36,8 @@ def test_stringdriver():
with sim.write_vcd("output.vcd"):
sim.run_until(1e-6 * 1000)
def test_hub75():
m = Module()
m.submodules.dut = dut = Hub75Coordinator(1)

View file

@ -1,11 +1,6 @@
from amaranth import Array, 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, WritePort
from amaranth.sim import Simulator
from ..bitslicer import Hub75StringDriver, Rgb666Layout, SwapBuffer
from ..hub75 import Hub75StringDriver, Rgb666Layout, SwapBuffer
def test_swapbuffer():
@ -25,9 +20,11 @@ def test_swapbuffer():
assert ctx.get(dut.read_port.data) == init_color
# swap buffer
ctx.set(dut.selector, 1)
await ctx.tick().repeat(2) # takes two clocks after switching selector to output data.
await ctx.tick().repeat(
2
) # takes two clocks after switching selector to output data.
assert ctx.get(dut.read_port.data) == test_color
# TODO: add more assertions/verification
sim.add_testbench(testbench)
with sim.write_vcd("output.vcd"):

View file

@ -1,8 +1,16 @@
import shutil
import pytest
from amaranth import Elaboratable, Module, Signal
from amaranth.lib.io import Buffer
from groovylight.platforms.colorlight_5a75b_v8_2 import Colorlight_5A75B_R82Platform
def progs_exist(programs) -> bool:
for p in programs:
if shutil.which(p) is None:
return False
return True
class Blinky(Elaboratable):
def elaborate(self, platform):
m = Module()
@ -22,5 +30,8 @@ class Blinky(Elaboratable):
def test_platform():
if not progs_exist(["yosys", "nextpnr-ecp5", "openFPGALoader"]):
pytest.skip("missing toolchain programs")
plat = Colorlight_5A75B_R82Platform()
plat.build(Blinky())