初始化奇妙小屏幕控制器项目

This commit is contained in:
2026-09-08 22:56:52 +08:00
commit 8d368de3b5
491 changed files with 67678 additions and 0 deletions
@@ -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