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):
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

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 dataclasses import dataclass
from typing import Self
from math import ceil, log2
@dataclass(frozen=True, order=True)
@dataclass(frozen=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.
@ -18,6 +20,24 @@ class Coord:
if self.x < 0 or self.y < 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)
class BBox:
@ -32,7 +52,27 @@ class BBox:
raise RuntimeError("topleft must be strictly less than bottomright")
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)
@ -44,6 +84,10 @@ class DisplayDimensions:
length: 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):
@ -68,7 +112,7 @@ class DisplayRotation(Enum):
@dataclass(frozen=True)
class _DisplayString:
class DisplayString:
"""Internal class to represent a string of HUB75 displays.
position: (X,Y) coordinates of the local top-left of the display
@ -81,6 +125,7 @@ class _DisplayString:
position: Coord
dimensions: DisplayDimensions
rotation: DisplayRotation
# TODO: encode muxing
@property
def bbox(self) -> BBox:
@ -103,10 +148,13 @@ class _DisplayString:
"""Checks if the given coordinate is inside this display."""
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:
"""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):
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.sim import Simulator
from .bitslicer import Hub75StringDriver, Rgb666Layout
from ..bitslicer import Hub75StringDriver, Rgb666Layout
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.sim import Simulator
from .bitslicer import Hub75StringDriver, Rgb666Layout, SwapBuffer
from ..bitslicer import Hub75StringDriver, Rgb666Layout, SwapBuffer
def test_swapbuffer():