generated from saji/ecp5-template
Compare commits
1 commit
dd47014029
...
8e046b442a
Author | SHA1 | Date | |
---|---|---|---|
saji | 8e046b442a |
|
@ -1,52 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
|
@ -3,11 +3,9 @@
|
||||||
|
|
||||||
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)
|
@dataclass(frozen=True, order=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.
|
||||||
|
@ -20,24 +18,6 @@ class Coord:
|
||||||
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:
|
||||||
|
@ -52,27 +32,7 @@ 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)
|
||||||
|
@ -84,10 +44,6 @@ 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):
|
||||||
|
@ -112,7 +68,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
|
||||||
|
@ -125,7 +81,6 @@ 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:
|
||||||
|
@ -148,13 +103,10 @@ 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
|
||||||
|
|
|
@ -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():
|
|
@ -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():
|
|
@ -1,32 +0,0 @@
|
||||||
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)
|
|
Loading…
Reference in a new issue