初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
"""Pure-Python reference model for the fixed WalnutPi HUB75 mapping.
|
||||
|
||||
The production refresh loop is C. This module is intentionally small and
|
||||
deterministic so mapping and bitplane contracts can be tested on any host.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
WIDTH = 64
|
||||
HEIGHT = 64
|
||||
SCAN_ROWS = 32
|
||||
PWM_BITS = 7
|
||||
PIO_BASE = 0x0300B000
|
||||
PI_BANK_OFFSET = 0x120
|
||||
PI_DATA_OFFSET = 0x10
|
||||
PWM_BASE = 0x0300A000
|
||||
HARDWARE_MAPPING = "walnutpi-pi-bank-pwm-oe-v2"
|
||||
OE_PWM_TICK_NS = 125
|
||||
INTERNAL_PWM_BITS = 11
|
||||
PWM_PLANE_OFFSET = INTERNAL_PWM_BITS - PWM_BITS
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Hub75Pin:
|
||||
signal: str
|
||||
physical: int
|
||||
gpio: str
|
||||
bit: int
|
||||
|
||||
|
||||
HUB75_PINS = (
|
||||
Hub75Pin("R1", 29, "PI0", 0),
|
||||
Hub75Pin("G1", 31, "PI1", 1),
|
||||
Hub75Pin("B1", 33, "PI2", 2),
|
||||
Hub75Pin("R2", 35, "PI3", 3),
|
||||
Hub75Pin("G2", 37, "PI4", 4),
|
||||
Hub75Pin("B2", 8, "PI5", 5),
|
||||
Hub75Pin("A", 10, "PI6", 6),
|
||||
Hub75Pin("B", 28, "PI9", 9),
|
||||
Hub75Pin("C", 27, "PI10", 10),
|
||||
Hub75Pin("D", 15, "PI11", 11),
|
||||
Hub75Pin("E", 16, "PI12", 12),
|
||||
Hub75Pin("CLK", 38, "PI13", 13),
|
||||
Hub75Pin("OE", 40, "PI14/PWM4", 14),
|
||||
Hub75Pin("LAT", 36, "PI15", 15),
|
||||
)
|
||||
|
||||
RESERVED_PI_BITS = frozenset({7, 8, 16})
|
||||
REFRESH_RATE_LIMITS = frozenset({15, 20, 30, 45, 60, 80, 100})
|
||||
|
||||
|
||||
def address_mask(row: int) -> int:
|
||||
if type(row) is not int or not 0 <= row < SCAN_ROWS:
|
||||
raise ValueError("row must be in 0..31")
|
||||
mapping = (6, 9, 10, 11, 12)
|
||||
return sum(1 << gpio_bit for row_bit, gpio_bit in enumerate(mapping) if row & (1 << row_bit))
|
||||
|
||||
|
||||
def map_channel(value: int, brightness: int) -> int:
|
||||
if type(value) is not int or not 0 <= value <= 255:
|
||||
raise ValueError("value must be in 0..255")
|
||||
if type(brightness) is not int or not 1 <= brightness <= 100:
|
||||
raise ValueError("brightness must be in 1..100")
|
||||
output_factor = (1 << INTERNAL_PWM_BITS) - 1
|
||||
v = value * brightness / 255.0
|
||||
luminance = v / 902.3 if v <= 8 else ((v + 16) / 116.0) ** 3
|
||||
return int(output_factor * luminance + 0.5)
|
||||
|
||||
|
||||
def build_bitplanes(frame: bytes, brightness: int = 100) -> tuple[int, ...]:
|
||||
if len(frame) != WIDTH * HEIGHT * 3:
|
||||
raise ValueError("frame must contain 64x64 RGB888 bytes")
|
||||
mapped = tuple(map_channel(value, brightness) for value in range(256))
|
||||
output = [0] * (SCAN_ROWS * PWM_BITS * WIDTH)
|
||||
for row in range(SCAN_ROWS):
|
||||
for plane in range(PWM_BITS):
|
||||
for column in range(WIDTH):
|
||||
top = (row * WIDTH + column) * 3
|
||||
bottom = ((row + SCAN_ROWS) * WIDTH + column) * 3
|
||||
word = 0
|
||||
for component in range(3):
|
||||
if mapped[frame[top + component]] & (1 << (plane + PWM_PLANE_OFFSET)):
|
||||
word |= 1 << component
|
||||
if mapped[frame[bottom + component]] & (1 << (plane + PWM_PLANE_OFFSET)):
|
||||
word |= 1 << (component + 3)
|
||||
index = ((row * PWM_BITS + plane) * WIDTH) + column
|
||||
output[index] = word
|
||||
return tuple(output)
|
||||
|
||||
|
||||
def bitplane_index(row: int, plane: int, column: int) -> int:
|
||||
if not 0 <= row < SCAN_ROWS or not 0 <= plane < PWM_BITS or not 0 <= column < WIDTH:
|
||||
raise ValueError("bitplane coordinate out of range")
|
||||
return ((row * PWM_BITS + plane) * WIDTH) + column
|
||||
|
||||
|
||||
def oe_ticks_for_plane(
|
||||
scan_slot_ns: int,
|
||||
brightness: int,
|
||||
plane: int,
|
||||
remainder: int,
|
||||
) -> tuple[int, int]:
|
||||
if type(scan_slot_ns) is not int or scan_slot_ns <= 0:
|
||||
raise ValueError("scan_slot_ns must be positive")
|
||||
if type(brightness) is not int or not 1 <= brightness <= 100:
|
||||
raise ValueError("brightness must be in 1..100")
|
||||
if type(plane) is not int or not 0 <= plane < PWM_BITS:
|
||||
raise ValueError("plane must be in 0..6")
|
||||
if type(remainder) is not int or remainder < 0:
|
||||
raise ValueError("remainder must be non-negative")
|
||||
return 1 << (plane + PWM_PLANE_OFFSET), 0
|
||||
Reference in New Issue
Block a user