generated from saji/ecp5-template
Compare commits
No commits in common. "0c1d9241666fa4ff7741718934e7ad5a31be5bb9" and "7e41e6e2f3c1acbe57b30b3a3bba0ec42e559e5f" have entirely different histories.
0c1d924166
...
7e41e6e2f3
|
@ -1,52 +0,0 @@
|
||||||
# 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"
|
|
|
@ -8,6 +8,7 @@ from amaranth.utils import ceil_log2
|
||||||
from .common import Rgb666Layout, Hub75Stream, Hub75Ctrl, Hub75Data
|
from .common import Rgb666Layout, Hub75Stream, Hub75Ctrl, Hub75Data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SwapBuffer(wiring.Component):
|
class SwapBuffer(wiring.Component):
|
||||||
"""A pair of BRAMs for holdling line data that are swapped between using an external signal.
|
"""A pair of BRAMs for holdling line data that are swapped between using an external signal.
|
||||||
|
|
||||||
|
@ -163,7 +164,8 @@ class Hub75Coordinator(wiring.Component):
|
||||||
self.n_strings = n_strings
|
self.n_strings = n_strings
|
||||||
super().__init__(
|
super().__init__(
|
||||||
{
|
{
|
||||||
"hub75": Out(Hub75Ctrl(n_strings)),
|
"ctrl": Out(Hub75Ctrl),
|
||||||
|
"data": data.ArrayLayout(Hub75Data, n_strings),
|
||||||
# TODO: fetching routine? maybe it's passed through.
|
# TODO: fetching routine? maybe it's passed through.
|
||||||
}
|
}
|
||||||
)
|
)
|
|
@ -1,4 +1,4 @@
|
||||||
from amaranth import Array, unsigned
|
from amaranth import unsigned
|
||||||
from amaranth.lib import wiring, data
|
from amaranth.lib import wiring, data
|
||||||
from amaranth.lib.wiring import Out
|
from amaranth.lib.wiring import Out
|
||||||
|
|
||||||
|
@ -42,14 +42,13 @@ class Hub75Ctrl(wiring.Signature):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, n_strings: int = 1):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
{
|
{
|
||||||
"latch": Out(1),
|
"latch": Out(1),
|
||||||
"oe": Out(1),
|
"oe": Out(1),
|
||||||
"addr": Out(5),
|
"addr": Out(5),
|
||||||
"display_clk": Out(1),
|
"display_clk": Out(1),
|
||||||
"data": Out(Hub75Data()).array(n_strings),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
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.
|
|
|
@ -24,30 +24,8 @@
|
||||||
# This file contains code to generate these timing adjustments and
|
# This file contains code to generate these timing adjustments and
|
||||||
# control/quantify them.
|
# 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 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]:
|
def _gammavec(vals: [float], g: float) -> [float]:
|
||||||
return [pow(x, g) for x in vals]
|
return [pow(x, g) for x in vals]
|
||||||
|
|
|
@ -173,8 +173,7 @@ class DisplayGeometry:
|
||||||
will overlap with an existing string.
|
will overlap with an existing string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.strict:
|
|
||||||
for e in self._strings:
|
for e in self._strings:
|
||||||
if e.intersects(s):
|
if e.intersects(s):
|
||||||
raise RuntimeError(f"node {e} intersects with {s}")
|
pass
|
||||||
self._strings.append(s)
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
# main entry point for CLI applications.
|
# main entry point for CLI applications.
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__loader__.name)
|
logger = logging.getLogger(__loader__.name)
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,7 +12,9 @@ def setup_logger(args):
|
||||||
root_logger = logging.getLogger()
|
root_logger = logging.getLogger()
|
||||||
handler = logging.StreamHandler()
|
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)
|
handler.setFormatter(formatter)
|
||||||
|
|
||||||
root_logger.addHandler(handler)
|
root_logger.addHandler(handler)
|
||||||
|
@ -29,7 +33,6 @@ def main():
|
||||||
const=logging.DEBUG,
|
const=logging.DEBUG,
|
||||||
default=logging.INFO,
|
default=logging.INFO,
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-L",
|
"-L",
|
||||||
"--log-file",
|
"--log-file",
|
||||||
|
|
|
@ -23,9 +23,7 @@ class Colorlight_5A75B_R82Platform(LatticeECP5Platform):
|
||||||
Subsignal("copi", Pins("T8", dir="o")),
|
Subsignal("copi", Pins("T8", dir="o")),
|
||||||
Attrs(IO_TYPE="LVCMOS33"),
|
Attrs(IO_TYPE="LVCMOS33"),
|
||||||
),
|
),
|
||||||
*LEDResources(
|
*LEDResources(pins="T6", invert=True, attrs=Attrs(IO_TYPE="LVCMOS33", DRIVE="4")),
|
||||||
pins="T6", invert=True, attrs=Attrs(IO_TYPE="LVCMOS33", DRIVE="4")
|
|
||||||
),
|
|
||||||
*ButtonResources(
|
*ButtonResources(
|
||||||
pins="R7", invert=True, attrs=Attrs(IO_TYPE="LVCMOS33", PULLMODE="UP")
|
pins="R7", invert=True, attrs=Attrs(IO_TYPE="LVCMOS33", PULLMODE="UP")
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
# 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)
|
|
|
@ -13,13 +13,10 @@ from amaranth.lib.wiring import In, Out
|
||||||
# word size = 32
|
# word size = 32
|
||||||
# 8 megabytes data.
|
# 8 megabytes data.
|
||||||
|
|
||||||
|
|
||||||
class SDRAMSignature(wiring.Signature):
|
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):
|
def __init__(self, addr_width, data_width=32, bank_width=2):
|
||||||
super().__init__(
|
super().__init__({
|
||||||
{
|
|
||||||
"nCS": Out(1),
|
"nCS": Out(1),
|
||||||
"cke": Out(1),
|
"cke": Out(1),
|
||||||
"nRAS": Out(1),
|
"nRAS": Out(1),
|
||||||
|
@ -31,41 +28,31 @@ class SDRAMSignature(wiring.Signature):
|
||||||
# todo: use dqm
|
# todo: use dqm
|
||||||
"data_wren": Out(1),
|
"data_wren": Out(1),
|
||||||
"bank_cs": Out(bank_width),
|
"bank_cs": Out(bank_width),
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class _WriteBurstLength(enum.Enum, shape=1):
|
class _WriteBurstLength(enum.Enum, shape=1):
|
||||||
"""MRS Write burst mode"""
|
"""MRS Write burst mode"""
|
||||||
|
|
||||||
BURST = 0
|
BURST = 0
|
||||||
SINGLE_BIT = 1
|
SINGLE_BIT = 1
|
||||||
|
|
||||||
|
|
||||||
class _TestMode(enum.Enum, shape=2):
|
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
|
MODE_REGISTER_SET = 0
|
||||||
RESERVED0 = 1
|
RESERVED0 = 1
|
||||||
RESERVED1 = 2
|
RESERVED1 = 2
|
||||||
RESERVED2 = 3
|
RESERVED2 = 3
|
||||||
|
|
||||||
|
|
||||||
class _CASLatency(enum.Enum, shape=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
|
CYCL2 = 2
|
||||||
CYCL3 = 3
|
CYCL3 = 3
|
||||||
|
|
||||||
|
|
||||||
class _BurstType(enum.Enum, shape=1):
|
class _BurstType(enum.Enum, shape=1):
|
||||||
SEQUENTIAL = 0
|
SEQUENTIAL = 0
|
||||||
INTERLEAVED = 1
|
INTERLEAVED = 1
|
||||||
|
|
||||||
|
|
||||||
class _BurstLength(enum.IntEnum, shape=3):
|
class _BurstLength(enum.IntEnum, shape=3):
|
||||||
"""The size of the burst"""
|
""" The size of the burst """
|
||||||
|
|
||||||
SINGLE = 0
|
SINGLE = 0
|
||||||
DUAL = 1
|
DUAL = 1
|
||||||
QUAD = 2
|
QUAD = 2
|
||||||
|
@ -74,8 +61,7 @@ class _BurstLength(enum.IntEnum, shape=3):
|
||||||
|
|
||||||
|
|
||||||
class _Command(enum.Enum):
|
class _Command(enum.Enum):
|
||||||
"""Command set for SDRAM"""
|
""" Command set for SDRAM """
|
||||||
|
|
||||||
MRS_WRITE = 0
|
MRS_WRITE = 0
|
||||||
ACTIVATE = 1
|
ACTIVATE = 1
|
||||||
PRECHARGE = 2
|
PRECHARGE = 2
|
||||||
|
@ -87,6 +73,7 @@ class _Command(enum.Enum):
|
||||||
NOP = 8
|
NOP = 8
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BankController(wiring.Component):
|
class BankController(wiring.Component):
|
||||||
"""Manages a single Bank. Has a bank locking/state tracker,
|
"""Manages a single Bank. Has a bank locking/state tracker,
|
||||||
can issue commands"""
|
can issue commands"""
|
||||||
|
|
|
@ -2,12 +2,11 @@ from ..geom import Coord, BBox
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def test_coord_comparison():
|
def test_coord_comparison():
|
||||||
c1 = Coord(0, 0)
|
c1 = Coord(0,0)
|
||||||
c2 = Coord(0, 1)
|
c2 = Coord(0,1)
|
||||||
c3 = Coord(1, 1)
|
c3 = Coord(1,1)
|
||||||
c3_other = Coord(1, 1)
|
c3_other = Coord(1,1)
|
||||||
|
|
||||||
assert c1 < c3
|
assert c1 < c3
|
||||||
assert not c1 < c2, "both x,y must be greater/lt/eq"
|
assert not c1 < c2, "both x,y must be greater/lt/eq"
|
||||||
|
@ -16,24 +15,23 @@ def test_coord_comparison():
|
||||||
assert c3 == c3_other, "Coords with same numbers should equal each other"
|
assert c3 == c3_other, "Coords with same numbers should equal each other"
|
||||||
assert c3 != c2
|
assert c3 != c2
|
||||||
|
|
||||||
|
|
||||||
def test_coord_construction():
|
def test_coord_construction():
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
Coord(0, -1)
|
Coord(0,-1)
|
||||||
|
|
||||||
|
|
||||||
def test_bbox():
|
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.width == 2
|
||||||
assert b.height == 1
|
assert b.height == 1
|
||||||
|
|
||||||
assert b.contains(Coord(1, 2))
|
assert b.contains(Coord(1,2))
|
||||||
assert not b.contains(Coord(0, 0))
|
assert not b.contains(Coord(0,0))
|
||||||
|
|
||||||
# TODO: test .intersect(other)
|
# TODO: test .intersect(other)
|
||||||
|
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
BBox(Coord(0, 0), Coord(1, 0))
|
BBox(Coord(0,0), Coord(1,0))
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
BBox(Coord(1, 1), Coord(0, 0))
|
BBox(Coord(1,1), Coord(0,0))
|
||||||
|
|
|
@ -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 ..hub75 import Hub75Coordinator, Hub75StringDriver, Rgb666Layout
|
from ..bitslicer import Hub75StringDriver, Rgb666Layout
|
||||||
|
|
||||||
|
|
||||||
def test_stringdriver():
|
def test_stringdriver():
|
||||||
|
@ -36,8 +36,3 @@ def test_stringdriver():
|
||||||
|
|
||||||
with sim.write_vcd("output.vcd"):
|
with sim.write_vcd("output.vcd"):
|
||||||
sim.run_until(1e-6 * 1000)
|
sim.run_until(1e-6 * 1000)
|
||||||
|
|
||||||
|
|
||||||
def test_hub75():
|
|
||||||
m = Module()
|
|
||||||
m.submodules.dut = dut = Hub75Coordinator(1)
|
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
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 amaranth.sim import Simulator
|
||||||
|
|
||||||
from ..hub75 import Hub75StringDriver, Rgb666Layout, SwapBuffer
|
from ..bitslicer import Hub75StringDriver, Rgb666Layout, SwapBuffer
|
||||||
|
|
||||||
|
|
||||||
def test_swapbuffer():
|
def test_swapbuffer():
|
||||||
|
@ -20,9 +25,7 @@ def test_swapbuffer():
|
||||||
assert ctx.get(dut.read_port.data) == init_color
|
assert ctx.get(dut.read_port.data) == init_color
|
||||||
# swap buffer
|
# swap buffer
|
||||||
ctx.set(dut.selector, 1)
|
ctx.set(dut.selector, 1)
|
||||||
await ctx.tick().repeat(
|
await ctx.tick().repeat(2) # takes two clocks after switching selector to output data.
|
||||||
2
|
|
||||||
) # takes two clocks after switching selector to output data.
|
|
||||||
assert ctx.get(dut.read_port.data) == test_color
|
assert ctx.get(dut.read_port.data) == test_color
|
||||||
|
|
||||||
# TODO: add more assertions/verification
|
# TODO: add more assertions/verification
|
||||||
|
|
|
@ -1,16 +1,8 @@
|
||||||
import shutil
|
|
||||||
import pytest
|
|
||||||
from amaranth import Elaboratable, Module, Signal
|
from amaranth import Elaboratable, Module, Signal
|
||||||
from amaranth.lib.io import Buffer
|
from amaranth.lib.io import Buffer
|
||||||
from groovylight.platforms.colorlight_5a75b_v8_2 import Colorlight_5A75B_R82Platform
|
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):
|
class Blinky(Elaboratable):
|
||||||
def elaborate(self, platform):
|
def elaborate(self, platform):
|
||||||
m = Module()
|
m = Module()
|
||||||
|
@ -30,8 +22,5 @@ class Blinky(Elaboratable):
|
||||||
|
|
||||||
|
|
||||||
def test_platform():
|
def test_platform():
|
||||||
if not progs_exist(["yosys", "nextpnr-ecp5", "openFPGALoader"]):
|
|
||||||
pytest.skip("missing toolchain programs")
|
|
||||||
plat = Colorlight_5A75B_R82Platform()
|
plat = Colorlight_5A75B_R82Platform()
|
||||||
|
|
||||||
plat.build(Blinky())
|
plat.build(Blinky())
|
||||||
|
|
Loading…
Reference in a new issue