Livt packages
Eccelerators.Matrix
Version 1.0.1
Reusable RGB matrix drawing, color conversion, animation pacing, and HUB75 display components for Livt.
Published files (48)
src/Hub75/Hub75Controller.lvt
namespace Eccelerators.Matrix.Hub75
using Eccelerators.Matrix
/**
* Scan a binary-address HUB75 panel using two packed RGB lanes in the inherited
* clock/reset context. Owns scan timing and registered panel outputs; the caller
* owns the pixel source and must supply each requested pair within one phase.
* Channels are packed R,G,B, each COLOR_BITS wide; bitplanes use binary dwell.
* Reset blanks the panel and restarts the startup delay. No panel-specific driver initialization is performed.
*/
component Hub75Controller<SHIFT_COLUMNS: int = 64, ROW_BITS: int = 5,
PHASE_TIME: Time = 100ns, STARTUP_TIME: Time = 50ms, HOLD_TIME: Time = 50us, COLOR_BITS: int = 1, ROW_HOLD_TIME: Time = 0ns>
{
static assert SHIFT_COLUMNS >= 2 && SHIFT_COLUMNS <= 1024, "Shift columns must be 2..1024"
static assert ROW_BITS > 0 && ROW_BITS <= 5, "Binary row bits must be 1..5"
static assert COLOR_BITS >= 1 && COLOR_BITS <= 6, "Color bits must be 1..6"
static assert PHASE_TIME > 0ns, "Scan phase must be positive"
static assert STARTUP_TIME > 0ns, "Startup time must be positive"
static assert ROW_HOLD_TIME >= 0ns, "Row hold budget must not be negative"
static assert ROW_HOLD_TIME > 0ns || HOLD_TIME > 0ns, "Base hold time must be positive when no row budget is supplied"
// Public scan geometry; panel height is twice ROW_PAIRS.
const PIXEL_BITS: int = 3 * COLOR_BITS
const COLUMN_BITS: int = Bits.RequiredFor(SHIFT_COLUMNS - 1)
public const WIDTH: int = SHIFT_COLUMNS
public const ROW_PAIRS: int = 1 << ROW_BITS
// Scan-phase encodings; reset starts in the zero-valued startup phase.
const PHASE_STARTUP: int = 0
const PHASE_CAPTURE_PIXELS: int = 1
const PHASE_CLOCK_HIGH: int = 2
const PHASE_CLOCK_LOW: int = 3
const PHASE_SETTLE: int = 4
const PHASE_LATCH_HIGH: int = 5
const PHASE_LATCH_LOW: int = 6
const PHASE_ENABLE: int = 7
const PHASE_HOLD: int = 8
const PHASE_ADVANCE_ROW: int = 9
// Context-derived timing fields; constructor assignments avoid the installed
// backend defect in context-dependent member constants.
tickCycles: int
startupCycles: int
holdCycles: int
// Registered panel outputs. Keep OE inactive even during reset and construction.
public panelClock: out logic = 0b0
public latch: out logic = 0b0
public outputEnableN: out logic = 0b1
public row: out logic[ROW_BITS] = 0 as logic[ROW_BITS]
public rgbUpper: out logic[3] = 0b000
public rgbLower: out logic[3] = 0b000
// Pixel-source request and one-clock frame-boundary pulse.
public readColumn: out logic[COLUMN_BITS]
public readRow: out logic[ROW_BITS]
public frameBoundary: out logic = 0b0
// Pixel-source response; both lanes must settle before the next capture phase.
public pixelUpper: in logic[PIXEL_BITS]
public pixelLower: in logic[PIXEL_BITS]
// Scan counters and phase state, owned by Scan.
divider: int
phase: int
delay: int
column: int
rowIndex: int
bitPlane: int
/**
* Derive scan, startup and dwell counts from the inherited context.
* ROW_HOLD_TIME, when positive, sets the total enabled budget per row pair.
* Divide it among binary-weighted planes, rounding the base dwell down.
* Otherwise retain the legacy HOLD_TIME base-plane dwell.
* Require at least five clocks per phase and positive startup/dwell counts.
* Keep the panel blank until construction and the startup delay complete.
*/
new()
{
tickCycles = context.TicksFor(PHASE_TIME) as int
assert tickCycles >= 5, "Increase PHASE_TIME: pixel source requires at least five context ticks per phase"
startupCycles = context.TicksFor(STARTUP_TIME) as int
static if (ROW_HOLD_TIME > 0ns) {
holdCycles = (context.TicksFor(ROW_HOLD_TIME) as int) / ((1 << COLOR_BITS) - 1)
} else {
holdCycles = context.TicksFor(HOLD_TIME) as int
}
assert holdCycles <= (2147483647 >> (COLOR_BITS - 1)), "PWM dwell exceeds counter range"
assert startupCycles > 0 && holdCycles > 0, "Startup and hold must be positive"
}
/**
* Advance the complete scan transaction atomically on each context edge.
* Long delays count context ticks; shifting and blanking use phase ticks.
*/
process Scan()
{
state {
frameBoundary = 0b0
if (phase == PHASE_STARTUP) {
if (delay == startupCycles - 1) {
delay = 0
frameBoundary = 0b1
phase = PHASE_CAPTURE_PIXELS
} else {
delay++
}
} elif (phase == PHASE_HOLD) {
if (delay == (holdCycles << bitPlane) - 1) {
outputEnableN = 0b1
delay = 0
phase = PHASE_ADVANCE_ROW
} else {
delay++
}
} elif (divider < tickCycles - 1) {
divider++
} else {
divider = 0
match phase
{
PHASE_CAPTURE_PIXELS =>
{
rgbUpper = (((((pixelUpper as uint) >> (2 * COLOR_BITS + bitPlane)) & 1) << 2) +
((((pixelUpper as uint) >> (COLOR_BITS + bitPlane)) & 1) << 1) +
(((pixelUpper as uint) >> bitPlane) & 1)) as logic[3]
rgbLower = (((((pixelLower as uint) >> (2 * COLOR_BITS + bitPlane)) & 1) << 2) +
((((pixelLower as uint) >> (COLOR_BITS + bitPlane)) & 1) << 1) +
(((pixelLower as uint) >> bitPlane) & 1)) as logic[3]
phase = PHASE_CLOCK_HIGH
}
PHASE_CLOCK_HIGH =>
{
panelClock = 0b1
phase = PHASE_CLOCK_LOW
}
PHASE_CLOCK_LOW =>
{
panelClock = 0b0
if (column == WIDTH - 1) {
phase = PHASE_SETTLE
} else {
column++
readColumn = column as logic[COLUMN_BITS]
phase = PHASE_CAPTURE_PIXELS
}
}
PHASE_SETTLE =>
{
phase = PHASE_LATCH_HIGH
}
PHASE_LATCH_HIGH =>
{
latch = 0b1
phase = PHASE_LATCH_LOW
}
PHASE_LATCH_LOW =>
{
latch = 0b0
phase = PHASE_ENABLE
}
PHASE_ENABLE =>
{
outputEnableN = 0b0
phase = PHASE_HOLD
}
PHASE_ADVANCE_ROW =>
{
column = 0
readColumn = 0 as logic[COLUMN_BITS]
if (bitPlane < COLOR_BITS - 1) {
bitPlane++
} elif (rowIndex == ROW_PAIRS - 1) {
bitPlane = 0
rowIndex = 0
frameBoundary = 0b1
} else {
bitPlane = 0
rowIndex++
}
row = rowIndex as logic[ROW_BITS]
readRow = rowIndex as logic[ROW_BITS]
phase = PHASE_CAPTURE_PIXELS
}
_ =>
{
outputEnableN = 0b1
panelClock = 0b0
latch = 0b0
phase = PHASE_STARTUP
}
}
}
}
}
}