Compare commits

...

5 commits

Author SHA1 Message Date
saji dd47014029 fix geom, add unittest
Some checks failed
Verilator Unit Tests / Test (push) Failing after 3m25s
2024-09-20 23:29:50 -05:00
saji 477966b9c8 migrate tests 2024-09-20 23:29:42 -05:00
saji 75d6e15b42 add gamma module 2024-09-20 23:05:15 -05:00
saji 29464f91b4 add intersection 2024-09-20 19:05:18 -05:00
saji c6a81b5a76 initial coordinator 2024-09-20 15:51:59 -05:00
8 changed files with 171 additions and 9 deletions

View file

@ -223,10 +223,40 @@ 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

52
src/groovylight/gamma.py Normal file
View file

@ -0,0 +1,52 @@
# Gamma correction by adjusting the display OE/Expose timings.
# Most gamma correction is done on the values being displayed.
# i.e gammacorrect (RGB) -> RGB (adjusted). However this adds
# a complex look-up step which adds complexity and cycles.
# There is a simpler solution which uses some properties of the
# gamma function as well as the fact that we are manually doing
# color depth using BCM/PWM.
#
# Consider the default BCM timing layout:
# MSB MSB-1 MSB-2 MSB-3
# P*8 P*4 P*2 P
# that is, we have a baseline display, measured in clocks/us/whatever
# and then the next most significant bit is displayed for twice that,
# four times that, and so on.
# But, we can adjust the individual bit timings to adjust the brightness
# curve as we see fit. This has numerous advantages:
# 1. It's free, we don't have to do any math on the board, just adjusting
# an existing process.
# 2. We can go more granular that n-bits of color. This means that the gamma
# curve will be effective and accurate regardless of the color depth.
#
# This file contains code to generate these timing adjustments and
# control/quantify them.
from math import pow
def _gammavec(vals: [float], g: float) -> [float]:
return [pow(x,g) for x in vals]
def _nbit_scale(f, nbits:int) -> [float]:
"""Computes the equivalent linear value for each bit of n_bits.
That is, the list contains scalar values that are doubling as they progress,
[ x, 2x, 4x ] such that the sum(list) = 7x = f
"""
base = float(f) / (pow(2.0, nbits) - 1.0)
return [base * pow(2.0, x) for x in range(nbits)]
def gamma_timings(gamma:float = 2.2, nbits:int = 8, max_clocks: int = 4096):
"""Computes the clock cycle timings for a given gamma correction.
"""
linear_values = _nbit_scale(1.0, nbits)
gamma_values = _gammavec(linear_values, gamma)
bclk_ratio = max_clocks / gamma_values[-1]
result = [round(bclk_ratio * x) for x in gamma_values]
return result

View file

@ -3,9 +3,11 @@
from enum import Enum from enum import Enum
from dataclasses import dataclass from dataclasses import dataclass
from typing import Self
from math import ceil, log2
@dataclass(frozen=True, order=True) @dataclass(frozen=True)
class Coord: class Coord:
"""Coordinate class. Uses computer-graphics standard coordinate system, """Coordinate class. Uses computer-graphics standard coordinate system,
where X=0, Y=0 is top left. +X goes right. +Y goes down. where X=0, Y=0 is top left. +X goes right. +Y goes down.
@ -13,11 +15,29 @@ class Coord:
x: int x: int
y: int y: int
def __post_init__(self): def __post_init__(self):
if self.x < 0 or self.y < 0: if self.x < 0 or self.y < 0:
raise RuntimeError("x and y must both be >= 0") raise RuntimeError("x and y must both be >= 0")
def __lt__(self, other):
return self.x < other.x and self.y < other.y
def __gt__(self, other):
return self.x > other.x and self.y > other.y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __neq__(self, other):
return not (self.x == other.x and self.y == other.y)
def __le__(self, other):
return self.x <= other.x and self.y <= other.y
def __ge__(self, other):
return self.x >= other.x and self.y >= other.y
@dataclass(frozen=True) @dataclass(frozen=True)
class BBox: class BBox:
@ -32,7 +52,27 @@ class BBox:
raise RuntimeError("topleft must be strictly less than bottomright") raise RuntimeError("topleft must be strictly less than bottomright")
def contains(self, c: Coord) -> bool: def contains(self, c: Coord) -> bool:
return c > self.topleft and c < self.bottomright return c >= self.topleft and c <= self.bottomright
@property
def width(self) -> int:
return self.bottomright.x - self.topleft.x
@property
def height(self) -> int:
return self.bottomright.y - self.topleft.y
def intersects(self, other: Self) -> bool:
## other leftmost edge is right of our rightmost edge
x1 = other.topleft.x > self.bottomright.x
# our leftmost edge is to the right of other rightmost edge
x2 = self.topleft.x > other.bottomright.x
# other top edge is below (greater than!) our bottom edge
y1 = other.topleft.y > self.bottomright.y
# our top edge is below other bottom edge.
y2 = self.topleft.y > other.bottomright.y
return not (x1 or x2 or y1 or y2)
@dataclass(frozen=True) @dataclass(frozen=True)
@ -44,6 +84,10 @@ class DisplayDimensions:
length: int length: int
height: int height: int
mux: int = 2 # number of lines driven at once.
def addr_bits(self) -> int:
return ceil(log2(self.height / self.mux))
class DisplayRotation(Enum): class DisplayRotation(Enum):
@ -68,7 +112,7 @@ class DisplayRotation(Enum):
@dataclass(frozen=True) @dataclass(frozen=True)
class _DisplayString: class DisplayString:
"""Internal class to represent a string of HUB75 displays. """Internal class to represent a string of HUB75 displays.
position: (X,Y) coordinates of the local top-left of the display position: (X,Y) coordinates of the local top-left of the display
@ -81,6 +125,7 @@ class _DisplayString:
position: Coord position: Coord
dimensions: DisplayDimensions dimensions: DisplayDimensions
rotation: DisplayRotation rotation: DisplayRotation
# TODO: encode muxing
@property @property
def bbox(self) -> BBox: def bbox(self) -> BBox:
@ -103,10 +148,13 @@ class _DisplayString:
"""Checks if the given coordinate is inside this display.""" """Checks if the given coordinate is inside this display."""
return self.bbox.contains(coord) return self.bbox.contains(coord)
def intersects(self, box: BBox) -> bool:
"""Checks if the given BBox intersects with this display"""
return self.bbox.intersects(box)
class DisplayGeometry: class DisplayGeometry:
"""Represents a display based on several strings in different positions. """Represents a display based on several strings in different positions."""
"""
def __init__(self, *, strict: bool = False): def __init__(self, *, strict: bool = False):
self.strict = strict self.strict = strict

View file

View file

View file

@ -0,0 +1,32 @@
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)
assert c1 < c3
assert not c1 < c2, "both x,y must be greater/lt/eq"
assert c2 <= c3
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)
def test_bbox():
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))
# TODO: test .intersect(other)

View file

@ -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, Rgb666Layout
def test_stringdriver(): def test_stringdriver():

View file

@ -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, SwapBuffer from ..bitslicer import Hub75StringDriver, Rgb666Layout, SwapBuffer
def test_swapbuffer(): def test_swapbuffer():