Files
matrix-screen-controller/核桃派软件源代码/app/display/native/h618_hub75.c
T

1201 lines
41 KiB
C

#define _GNU_SOURCE
#include "h618_hub75.h"
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <sched.h>
#include <stdarg.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#define H618_PIO_BASE ((off_t)0x0300B000)
#define H618_PIO_MAP_BYTES ((size_t)0x1000)
#define H618_PWM_BASE ((off_t)0x0300A000)
#define H618_PWM_MAP_BYTES ((size_t)0x1000)
#define H618_PI_BANK_OFFSET ((size_t)0x120)
#define SUNXI_MUX0_OFFSET ((size_t)0x00)
#define SUNXI_MUX1_OFFSET ((size_t)0x04)
#define SUNXI_DATA_OFFSET ((size_t)0x10)
#define PWM_PCCR45_OFFSET ((size_t)0x28)
#define PWM_PER_OFFSET ((size_t)0x40)
#define PWM_CHANNEL_STRIDE ((size_t)0x20)
#define PWM_PCR_OFFSET(channel) ((size_t)0x60 + PWM_CHANNEL_STRIDE * (channel))
#define PWM_PPR_OFFSET(channel) (PWM_PCR_OFFSET(channel) + (size_t)0x04)
#define PWM_PCNTR_OFFSET(channel) (PWM_PCR_OFFSET(channel) + (size_t)0x08)
#define PWM_CHANNEL 4u
#define PWM_CHANNEL_ENABLE BIT(PWM_CHANNEL)
#define PWM5_CHANNEL_ENABLE BIT(5)
#define PWM_PCCR_DIV_M_MASK 0x0fu
#define PWM_PCCR_GATE BIT(4)
#define PWM_PCCR_PWM4_BYPASS BIT(5)
#define PWM_PCCR_PWM5_BYPASS BIT(6)
#define PWM_PCCR_CLOCK_SOURCE_MASK (0x3u << 7)
#define PWM_PCR_PRESCALE_MASK 0xffu
#define PWM_PCR_ACTIVE_STATE BIT(8)
#define PWM_PCR_SINGLE_PULSE_MODE BIT(9)
#define PWM_PCR_PULSE_START BIT(10)
#define PWM_PCR_PERIOD_BUSY BIT(11)
#define PWM_PRESCALE_125NS 2u
#define PWM_PERIOD_CYCLES 65536u
#define PWM_PERIOD_NS ((uint64_t)PWM_PERIOD_CYCLES * HUB75_OE_PWM_TICK_NS)
#define PI14_MUX_PWM4 0x5u
#define BIT(pin) (1u << (pin))
#define PI_R1 BIT(0)
#define PI_G1 BIT(1)
#define PI_B1 BIT(2)
#define PI_R2 BIT(3)
#define PI_G2 BIT(4)
#define PI_B2 BIT(5)
#define PI_A BIT(6)
#define PI_B BIT(9)
#define PI_C BIT(10)
#define PI_D BIT(11)
#define PI_E BIT(12)
#define PI_CLK BIT(13)
#define PI_OE BIT(14)
#define PI_LAT BIT(15)
#define COLOR_MASK (PI_R1 | PI_G1 | PI_B1 | PI_R2 | PI_G2 | PI_B2)
#define ADDRESS_MASK (PI_A | PI_B | PI_C | PI_D | PI_E)
#define CONTROL_MASK (PI_CLK | PI_LAT | PI_OE)
#define OWNED_MASK (COLOR_MASK | ADDRESS_MASK | CONTROL_MASK)
#define STATS_WINDOW_NS 5000000000ull
#define HUNDRED_HZ_GUARD_NS 250000ull
struct hub75_context {
pthread_mutex_t lock;
pthread_t thread;
int thread_created;
atomic_int stop_requested;
int mem_fd;
uint8_t *mapped;
int mapped_is_heap;
uint8_t *pwm_mapped;
int pwm_mapped_is_heap;
int fake_pwm4_stuck;
volatile uint32_t *mux0;
volatile uint32_t *mux1;
volatile uint32_t *data;
volatile uint32_t *pwm_pccr45;
volatile uint32_t *pwm_per;
volatile uint32_t *pwm_pcr4;
volatile uint32_t *pwm_ppr4;
volatile uint32_t *pwm_pcntr4;
int gpio_configured;
int pwm_configured;
uint32_t saved_mux0_owned;
uint32_t saved_mux1_owned;
uint32_t saved_data_owned;
uint32_t preserved_data_unowned;
uint32_t saved_pwm_pccr45;
uint32_t saved_pwm_per;
uint32_t saved_pwm_pcr4;
uint32_t saved_pwm_ppr4;
uint32_t saved_pwm_pcr5;
uint32_t saved_pwm_ppr5;
uint8_t frames[2][HUB75_FRAME_BYTES];
uint16_t bitplanes[2][HUB75_BITPLANE_WORDS];
int front_index;
int back_index;
int swap_pending;
int brightness;
int refresh_rate_hz;
int cpu_affinity;
int realtime_priority;
hub75_stats stats;
};
static int refresh_rate_valid(int value) {
switch (value) {
case 15:
case 20:
case 30:
case 45:
case 60:
case 80:
case 100:
return 1;
default:
return 0;
}
}
int hub75_scans_per_frame_for_brightness(int brightness) {
if (brightness < 1 || brightness > 100) return -1;
return HUB75_SCANS_PER_FRAME;
}
const char *hub75_hardware_mapping(void) {
return HUB75_HARDWARE_MAPPING;
}
uint32_t hub75_oe_ticks_for_plane(
uint64_t scan_slot_ns,
int brightness,
unsigned plane,
uint64_t *remainder
) {
if (brightness < 1 || brightness > 100 || plane >= HUB75_PWM_BITS ||
remainder == NULL) {
return UINT32_MAX;
}
if (scan_slot_ns == 0) return UINT32_MAX;
/*
* Match rpi-rgb-led-matrix with kBitPlanes=11, pwm_bits=7 and an LSB
* pulse near 130 ns. The seven transmitted planes are internal planes
* 4..10, so their fixed 125 ns PWM4 weights are 2..128 us. Brightness is
* encoded into pixel bitplanes and never temporally dithered through OE.
*/
(void)brightness;
*remainder = 0;
return 1u << (plane + HUB75_PWM_PLANE_OFFSET);
}
static void copy_error(char *destination, size_t size, const char *format, ...) {
if (destination == NULL || size == 0) {
return;
}
va_list args;
va_start(args, format);
vsnprintf(destination, size, format, args);
va_end(args);
destination[size - 1] = '\0';
}
static void set_last_error(hub75_context *context, const char *format, ...) {
va_list args;
pthread_mutex_lock(&context->lock);
va_start(args, format);
vsnprintf(
context->stats.last_error,
sizeof(context->stats.last_error),
format,
args
);
va_end(args);
context->stats.last_error[sizeof(context->stats.last_error) - 1] = '\0';
pthread_mutex_unlock(&context->lock);
}
static uint64_t timespec_ns(const struct timespec *value) {
return (uint64_t)value->tv_sec * 1000000000ull + (uint64_t)value->tv_nsec;
}
static struct timespec ns_timespec(uint64_t value) {
struct timespec result;
result.tv_sec = (time_t)(value / 1000000000ull);
result.tv_nsec = (long)(value % 1000000000ull);
return result;
}
static uint64_t monotonic_ns(void) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return timespec_ns(&now);
}
static void busy_wait_ns(uint64_t duration_ns) {
if (duration_ns == 0) {
return;
}
uint64_t end = monotonic_ns() + duration_ns;
while (monotonic_ns() < end) {
__asm__ __volatile__("" ::: "memory");
}
}
static void sleep_until_ns(uint64_t target_ns) {
struct timespec target = ns_timespec(target_ns);
while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &target, NULL) == EINTR) {
}
}
static uint32_t address_bits(unsigned row) {
uint32_t value = 0;
if (row & 0x01u) value |= PI_A;
if (row & 0x02u) value |= PI_B;
if (row & 0x04u) value |= PI_C;
if (row & 0x08u) value |= PI_D;
if (row & 0x10u) value |= PI_E;
return value;
}
static inline void write_owned(hub75_context *context, uint32_t value) {
/*
* The dedicated-host profile stops every other userspace GPIO writer.
* Cache all non-HUB75 bits once so each clock edge needs one MMIO write
* instead of an MMIO read-modify-write. PI7/PI8/PI16 remain unchanged.
*/
*context->data = context->preserved_data_unowned | (value & OWNED_MASK);
atomic_signal_fence(memory_order_seq_cst);
}
static uint16_t cie1931_channel(uint8_t value, int brightness) {
const double output_factor = (1u << HUB75_INTERNAL_PWM_BITS) - 1u;
const double v = (double)value * (double)brightness / 255.0;
double luminance;
if (v <= 8.0) {
luminance = v / 902.3;
} else {
const double ratio = (v + 16.0) / 116.0;
luminance = ratio * ratio * ratio;
}
return (uint16_t)(output_factor * luminance + 0.5);
}
uint16_t hub75_map_channel_for_brightness(uint8_t value, int brightness) {
if (brightness < 1 || brightness > 100) return UINT16_MAX;
return cie1931_channel(value, brightness);
}
static uint32_t mux_mask(unsigned first, unsigned last, unsigned register_base) {
uint32_t result = 0;
for (unsigned pin = first; pin <= last; ++pin) {
result |= 0xFu << ((pin - register_base) * 4u);
}
return result;
}
static uint32_t mux_output_value(unsigned first, unsigned last, unsigned register_base) {
uint32_t result = 0;
for (unsigned pin = first; pin <= last; ++pin) {
result |= 0x1u << ((pin - register_base) * 4u);
}
return result;
}
static uint32_t mux_set_pin(uint32_t value, unsigned pin, unsigned function) {
unsigned shift = (pin - 8u) * 4u;
return (value & ~(0xfu << shift)) | ((function & 0xfu) << shift);
}
static void configure_gpio(hub75_context *context) {
const uint32_t mux0_owned = mux_mask(0, 6, 0);
const uint32_t mux1_owned = mux_mask(9, 15, 8);
context->saved_mux0_owned = *context->mux0 & mux0_owned;
context->saved_mux1_owned = *context->mux1 & mux1_owned;
context->saved_data_owned = *context->data & OWNED_MASK;
context->preserved_data_unowned = *context->data & ~OWNED_MASK;
write_owned(context, PI_OE);
*context->mux0 = (*context->mux0 & ~mux0_owned) |
mux_output_value(0, 6, 0);
*context->mux1 = (*context->mux1 & ~mux1_owned) |
mux_output_value(9, 15, 8);
atomic_thread_fence(memory_order_seq_cst);
write_owned(context, PI_OE);
context->gpio_configured = 1;
}
static void switch_oe_to_pwm4(hub75_context *context) {
write_owned(context, PI_OE);
*context->mux1 = mux_set_pin(*context->mux1, 14, PI14_MUX_PWM4);
atomic_thread_fence(memory_order_seq_cst);
}
static void switch_oe_to_gpio_high(hub75_context *context) {
*context->mux1 = mux_set_pin(*context->mux1, 14, 0x1u);
atomic_thread_fence(memory_order_seq_cst);
write_owned(context, PI_OE);
}
static inline void pwm4_disable(hub75_context *context) {
uint32_t enabled = *context->pwm_per;
*context->pwm_per = enabled & ~PWM_CHANNEL_ENABLE;
atomic_thread_fence(memory_order_seq_cst);
}
static void pwm_force_blank(hub75_context *context, int count_forced) {
if (context->pwm_per == NULL) {
return;
}
pwm4_disable(context);
switch_oe_to_gpio_high(context);
if (count_forced) {
pthread_mutex_lock(&context->lock);
context->stats.oe_forced_blanks += 1;
pthread_mutex_unlock(&context->lock);
}
}
static int pwm4_wait_period_ready(hub75_context *context) {
uint64_t started = monotonic_ns();
while ((*context->pwm_pcr4 & PWM_PCR_PERIOD_BUSY) != 0) {
if (context->pwm_mapped_is_heap) {
*context->pwm_pcr4 &= ~PWM_PCR_PERIOD_BUSY;
continue;
}
if (monotonic_ns() - started >= PWM_PERIOD_NS) {
return -1;
}
}
return 0;
}
static void pwm4_fake_begin_period_update(hub75_context *context) {
if (context->pwm_mapped_is_heap) {
*context->pwm_pcr4 |= PWM_PCR_PERIOD_BUSY;
}
}
static int configure_pwm4(hub75_context *context, char *error, size_t error_size) {
uint32_t per = *context->pwm_per;
if (per & PWM_CHANNEL_ENABLE) {
copy_error(error, error_size, "PWM4 is already active; hardware timing resource is occupied");
return -1;
}
context->saved_pwm_pccr45 = *context->pwm_pccr45;
context->saved_pwm_per = per;
context->saved_pwm_pcr4 = *context->pwm_pcr4;
context->saved_pwm_ppr4 = *context->pwm_ppr4;
context->saved_pwm_pcr5 = *(volatile uint32_t *)(
context->pwm_mapped + PWM_PCR_OFFSET(5u)
);
context->saved_pwm_ppr5 = *(volatile uint32_t *)(
context->pwm_mapped + PWM_PPR_OFFSET(5u)
);
context->pwm_configured = 1;
uint32_t clock = context->saved_pwm_pccr45;
clock &= ~(PWM_PCCR_DIV_M_MASK | PWM_PCCR_CLOCK_SOURCE_MASK |
PWM_PCCR_PWM4_BYPASS);
clock |= PWM_PCCR_GATE;
uint32_t shared_mask = PWM_PCCR_DIV_M_MASK |
PWM_PCCR_CLOCK_SOURCE_MASK | PWM_PCCR_GATE;
if ((per & PWM5_CHANNEL_ENABLE) &&
(context->saved_pwm_pccr45 & shared_mask) != (clock & shared_mask)) {
copy_error(
error,
error_size,
"active PWM5 uses an incompatible shared PWM4/5 clock"
);
return -1;
}
*context->pwm_pccr45 = clock;
uint32_t control = context->saved_pwm_pcr4;
control &= ~(PWM_PCR_PRESCALE_MASK | PWM_PCR_ACTIVE_STATE |
PWM_PCR_PULSE_START | PWM_PCR_PERIOD_BUSY);
control |= PWM_PRESCALE_125NS | PWM_PCR_SINGLE_PULSE_MODE;
*context->pwm_pcr4 = control;
*context->pwm_ppr4 = (PWM_PERIOD_CYCLES - 1u) << 16;
pwm4_fake_begin_period_update(context);
atomic_thread_fence(memory_order_seq_cst);
busy_wait_ns(1000u);
if (pwm4_wait_period_ready(context) != 0) {
copy_error(error, error_size, "PWM4 initial period register stayed busy");
return -1;
}
*context->pwm_per = per | PWM_CHANNEL_ENABLE;
atomic_thread_fence(memory_order_seq_cst);
uint32_t clock_readback = *context->pwm_pccr45;
uint32_t control_readback = *context->pwm_pcr4;
uint32_t period_readback = *context->pwm_ppr4;
if ((clock_readback & (PWM_PCCR_DIV_M_MASK | PWM_PCCR_CLOCK_SOURCE_MASK |
PWM_PCCR_GATE | PWM_PCCR_PWM4_BYPASS | PWM_PCCR_PWM5_BYPASS)) !=
(clock & (PWM_PCCR_DIV_M_MASK | PWM_PCCR_CLOCK_SOURCE_MASK |
PWM_PCCR_GATE | PWM_PCCR_PWM4_BYPASS | PWM_PCCR_PWM5_BYPASS)) ||
(control_readback & (PWM_PCR_PRESCALE_MASK | PWM_PCR_ACTIVE_STATE |
PWM_PCR_SINGLE_PULSE_MODE)) !=
(PWM_PRESCALE_125NS | PWM_PCR_SINGLE_PULSE_MODE) ||
period_readback != ((PWM_PERIOD_CYCLES - 1u) << 16) ||
(*context->pwm_per & PWM_CHANNEL_ENABLE) == 0) {
copy_error(error, error_size, "PWM4 initialization register readback mismatch");
return -1;
}
snprintf(
context->stats.oe_timing_backend,
sizeof(context->stats.oe_timing_backend),
"%s",
HUB75_OE_TIMING_BACKEND
);
return 0;
}
static int pwm4_shared_clock_valid(hub75_context *context) {
uint32_t expected = context->saved_pwm_pccr45;
expected &= ~(PWM_PCCR_DIV_M_MASK | PWM_PCCR_CLOCK_SOURCE_MASK |
PWM_PCCR_PWM4_BYPASS);
expected |= PWM_PCCR_GATE;
uint32_t mask = PWM_PCCR_DIV_M_MASK | PWM_PCCR_CLOCK_SOURCE_MASK |
PWM_PCCR_GATE | PWM_PCCR_PWM4_BYPASS | PWM_PCCR_PWM5_BYPASS;
return ((*context->pwm_pccr45 & mask) == (expected & mask)) &&
((*context->pwm_per & PWM_CHANNEL_ENABLE) != 0) &&
((*context->pwm_per & PWM5_CHANNEL_ENABLE) ==
(context->saved_pwm_per & PWM5_CHANNEL_ENABLE)) &&
(*(volatile uint32_t *)(context->pwm_mapped + PWM_PCR_OFFSET(5u)) ==
context->saved_pwm_pcr5) &&
(*(volatile uint32_t *)(context->pwm_mapped + PWM_PPR_OFFSET(5u)) ==
context->saved_pwm_ppr5);
}
static int pwm4_program_pulse(hub75_context *context, uint32_t ticks) {
if (ticks == 0 || ticks >= PWM_PERIOD_CYCLES) {
return -1;
}
uint64_t ready_started = monotonic_ns();
while ((*context->pwm_pcr4 &
(PWM_PCR_PULSE_START | PWM_PCR_PERIOD_BUSY)) != 0) {
if (monotonic_ns() - ready_started >= PWM_PERIOD_NS) {
return -1;
}
}
uint32_t programmed = ((ticks - 1u) << 16) | ticks;
/*
* H618 single-pulse mode reloads its read-only counter for every PUL_START
* command and clears PUL_START automatically at the end. Equal active and
* entire cycle counts remove the leading inactive portion, yielding one
* immediate low-active OE pulse with no periodic repeat after a stall.
*/
*context->pwm_ppr4 = programmed;
pwm4_fake_begin_period_update(context);
atomic_thread_fence(memory_order_seq_cst);
if (*context->pwm_ppr4 != programmed) {
return -1;
}
/*
* PERIOD_RDY is read-only: 1 means the period register is still busy.
* Allow the APB write to reach the PWM clock domain, then wait for the
* new active-cycle count to be committed before issuing PUL_START.
*/
busy_wait_ns(1000u);
if (pwm4_wait_period_ready(context) != 0) {
return -1;
}
uint32_t control = *context->pwm_pcr4;
if ((control & (PWM_PCR_SINGLE_PULSE_MODE | PWM_PCR_ACTIVE_STATE)) !=
PWM_PCR_SINGLE_PULSE_MODE) {
return -1;
}
*context->pwm_pcr4 = control | PWM_PCR_PULSE_START;
atomic_thread_fence(memory_order_seq_cst);
if (context->pwm_mapped_is_heap && !context->fake_pwm4_stuck) {
*context->pwm_pcr4 = control;
}
return (*context->pwm_per & PWM_CHANNEL_ENABLE) ? 0 : -1;
}
static void restore_pwm4(hub75_context *context) {
if (!context->pwm_configured || context->pwm_per == NULL) {
return;
}
pwm4_disable(context);
*context->pwm_ppr4 = context->saved_pwm_ppr4;
*context->pwm_pcr4 = context->saved_pwm_pcr4;
if ((*context->pwm_per & PWM5_CHANNEL_ENABLE) ==
(context->saved_pwm_per & PWM5_CHANNEL_ENABLE)) {
*context->pwm_pccr45 = context->saved_pwm_pccr45;
}
atomic_thread_fence(memory_order_seq_cst);
context->pwm_configured = 0;
}
static void restore_gpio_safe(hub75_context *context) {
if (!context->gpio_configured || context->data == NULL) {
return;
}
const uint32_t mux0_owned = mux_mask(0, 6, 0);
const uint32_t mux1_owned = mux_mask(9, 15, 8);
switch_oe_to_gpio_high(context);
write_owned(context, PI_OE);
*context->mux0 = (*context->mux0 & ~mux0_owned) | context->saved_mux0_owned;
*context->mux1 = (*context->mux1 & ~mux1_owned) | context->saved_mux1_owned;
atomic_thread_fence(memory_order_seq_cst);
write_owned(context, context->saved_data_owned);
context->gpio_configured = 0;
}
static int map_registers(hub75_context *context, int use_dev_mem, char *error, size_t error_size) {
if (use_dev_mem) {
context->mem_fd = open("/dev/mem", O_RDWR | O_SYNC | O_CLOEXEC);
if (context->mem_fd < 0) {
copy_error(error, error_size, "open /dev/mem failed: %s", strerror(errno));
return -1;
}
context->mapped = mmap(
NULL,
H618_PIO_MAP_BYTES,
PROT_READ | PROT_WRITE,
MAP_SHARED,
context->mem_fd,
H618_PIO_BASE
);
if (context->mapped == MAP_FAILED) {
context->mapped = NULL;
copy_error(error, error_size, "mmap H618 PIO failed: %s", strerror(errno));
return -1;
}
context->pwm_mapped = mmap(
NULL,
H618_PWM_MAP_BYTES,
PROT_READ | PROT_WRITE,
MAP_SHARED,
context->mem_fd,
H618_PWM_BASE
);
if (context->pwm_mapped == MAP_FAILED) {
context->pwm_mapped = NULL;
copy_error(error, error_size, "mmap H618 PWM failed: %s", strerror(errno));
return -1;
}
} else {
context->mapped = calloc(1, H618_PIO_MAP_BYTES);
if (context->mapped == NULL) {
copy_error(error, error_size, "allocating fake PIO registers failed");
return -1;
}
context->mapped_is_heap = 1;
context->pwm_mapped = calloc(1, H618_PWM_MAP_BYTES);
if (context->pwm_mapped == NULL) {
copy_error(error, error_size, "allocating fake PWM registers failed");
return -1;
}
context->pwm_mapped_is_heap = 1;
}
uint8_t *pi = context->mapped + H618_PI_BANK_OFFSET;
context->mux0 = (volatile uint32_t *)(pi + SUNXI_MUX0_OFFSET);
context->mux1 = (volatile uint32_t *)(pi + SUNXI_MUX1_OFFSET);
context->data = (volatile uint32_t *)(pi + SUNXI_DATA_OFFSET);
context->pwm_pccr45 = (volatile uint32_t *)(context->pwm_mapped + PWM_PCCR45_OFFSET);
context->pwm_per = (volatile uint32_t *)(context->pwm_mapped + PWM_PER_OFFSET);
context->pwm_pcr4 = (volatile uint32_t *)(context->pwm_mapped + PWM_PCR_OFFSET(PWM_CHANNEL));
context->pwm_ppr4 = (volatile uint32_t *)(context->pwm_mapped + PWM_PPR_OFFSET(PWM_CHANNEL));
context->pwm_pcntr4 = (volatile uint32_t *)(context->pwm_mapped + PWM_PCNTR_OFFSET(PWM_CHANNEL));
return 0;
}
static void unmap_registers(hub75_context *context) {
if (context->mapped != NULL) {
if (context->mapped_is_heap) {
free(context->mapped);
} else {
munmap(context->mapped, H618_PIO_MAP_BYTES);
}
context->mapped = NULL;
}
if (context->pwm_mapped != NULL) {
if (context->pwm_mapped_is_heap) {
free(context->pwm_mapped);
} else {
munmap(context->pwm_mapped, H618_PWM_MAP_BYTES);
}
context->pwm_mapped = NULL;
}
if (context->mem_fd >= 0) {
close(context->mem_fd);
context->mem_fd = -1;
}
context->mux0 = NULL;
context->mux1 = NULL;
context->data = NULL;
context->pwm_pccr45 = NULL;
context->pwm_per = NULL;
context->pwm_pcr4 = NULL;
context->pwm_ppr4 = NULL;
context->pwm_pcntr4 = NULL;
}
int hub75_build_bitplanes_for_brightness(
const uint8_t *rgb,
size_t length,
int brightness,
uint16_t *output,
size_t output_words
) {
if (rgb == NULL || output == NULL || length != HUB75_FRAME_BYTES ||
output_words != HUB75_BITPLANE_WORDS || brightness < 1 ||
brightness > 100) {
return -1;
}
uint16_t mapped[256];
for (unsigned value = 0; value < 256; ++value) {
mapped[value] = cie1931_channel((uint8_t)value, brightness);
}
for (unsigned row = 0; row < HUB75_SCAN_ROWS; ++row) {
for (unsigned plane = 0; plane < HUB75_PWM_BITS; ++plane) {
const unsigned internal_plane = plane + HUB75_PWM_PLANE_OFFSET;
for (unsigned column = 0; column < HUB75_WIDTH; ++column) {
size_t top = ((size_t)row * HUB75_WIDTH + column) * 3u;
size_t bottom = ((size_t)(row + HUB75_SCAN_ROWS) * HUB75_WIDTH + column) * 3u;
uint16_t word = 0;
if ((mapped[rgb[top]] >> internal_plane) & 1u) word |= PI_R1;
if ((mapped[rgb[top + 1]] >> internal_plane) & 1u) word |= PI_G1;
if ((mapped[rgb[top + 2]] >> internal_plane) & 1u) word |= PI_B1;
if ((mapped[rgb[bottom]] >> internal_plane) & 1u) word |= PI_R2;
if ((mapped[rgb[bottom + 1]] >> internal_plane) & 1u) word |= PI_G2;
if ((mapped[rgb[bottom + 2]] >> internal_plane) & 1u) word |= PI_B2;
size_t index = ((size_t)row * HUB75_PWM_BITS + plane) * HUB75_WIDTH + column;
output[index] = word;
}
}
}
return 0;
}
int hub75_build_bitplanes(
const uint8_t *rgb,
size_t length,
uint16_t *output,
size_t output_words
) {
return hub75_build_bitplanes_for_brightness(
rgb, length, 100, output, output_words
);
}
static void read_governor(hub75_context *context) {
const char *path = "/sys/devices/system/cpu/cpu3/cpufreq/scaling_governor";
FILE *file = fopen(path, "r");
char value[sizeof(context->stats.governor)] = "unavailable";
if (file != NULL) {
if (fgets(value, sizeof(value), file) == NULL) {
snprintf(value, sizeof(value), "unavailable");
}
fclose(file);
value[strcspn(value, "\r\n")] = '\0';
}
pthread_mutex_lock(&context->lock);
snprintf(context->stats.governor, sizeof(context->stats.governor), "%s", value);
pthread_mutex_unlock(&context->lock);
}
static void apply_thread_optimizations(hub75_context *context) {
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(context->cpu_affinity, &cpu_set);
int code = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set), &cpu_set);
pthread_mutex_lock(&context->lock);
if (code == 0) {
context->stats.cpu_affinity_active = 1;
} else {
snprintf(
context->stats.affinity_error,
sizeof(context->stats.affinity_error),
"%s",
strerror(code)
);
}
pthread_mutex_unlock(&context->lock);
struct sched_param parameter;
memset(&parameter, 0, sizeof(parameter));
parameter.sched_priority = context->realtime_priority;
code = pthread_setschedparam(pthread_self(), SCHED_FIFO, &parameter);
pthread_mutex_lock(&context->lock);
if (code == 0) {
context->stats.realtime_priority_active = 1;
} else {
snprintf(
context->stats.realtime_error,
sizeof(context->stats.realtime_error),
"%s",
strerror(code)
);
}
pthread_mutex_unlock(&context->lock);
if (mlockall(MCL_CURRENT | MCL_FUTURE) == 0) {
pthread_mutex_lock(&context->lock);
context->stats.memory_locked = 1;
pthread_mutex_unlock(&context->lock);
} else {
pthread_mutex_lock(&context->lock);
snprintf(
context->stats.memory_lock_error,
sizeof(context->stats.memory_lock_error),
"%s",
strerror(errno)
);
pthread_mutex_unlock(&context->lock);
}
read_governor(context);
}
static int scan_frame(
hub75_context *context,
const uint16_t *planes,
uint64_t period_ns
) {
uint64_t local_max_programmed_ns = 0;
if (!pwm4_shared_clock_valid(context)) {
pwm_force_blank(context, 1);
pthread_mutex_lock(&context->lock);
context->stats.oe_pulse_faults += 1;
snprintf(
context->stats.oe_timing_error,
sizeof(context->stats.oe_timing_error),
"PWM4/5 shared clock changed or PWM5 became active"
);
pthread_mutex_unlock(&context->lock);
return -1;
}
/* Prime the shift register and latch row 0 / plane 0 while OE is blank. */
uint32_t output_address = address_bits(0);
write_owned(context, output_address);
for (unsigned column = 0; column < HUB75_WIDTH; ++column) {
uint32_t value = output_address | planes[column];
write_owned(context, value);
write_owned(context, value | PI_CLK);
}
write_owned(context, output_address);
write_owned(context, output_address | PI_LAT);
write_owned(context, output_address);
const unsigned total_planes = HUB75_SCAN_ROWS * HUB75_PWM_BITS;
for (unsigned item = 0; item < total_planes; ++item) {
unsigned plane = item % HUB75_PWM_BITS;
uint32_t ticks = hub75_oe_ticks_for_plane(
period_ns,
100,
plane,
&(uint64_t){0}
);
uint64_t pulse_enabled_at = 0;
if (ticks == UINT32_MAX ||
(ticks != 0 && pwm4_program_pulse(context, ticks) != 0)) {
pwm_force_blank(context, 1);
pthread_mutex_lock(&context->lock);
context->stats.oe_pulse_faults += 1;
snprintf(
context->stats.oe_timing_error,
sizeof(context->stats.oe_timing_error),
"PWM4 pulse programming or readback failed"
);
pthread_mutex_unlock(&context->lock);
return -1;
}
if (ticks != 0) {
/* Timestamp after the enable readback so the wait is conservative. */
pulse_enabled_at = monotonic_ns();
}
uint64_t programmed_ns = (uint64_t)ticks * HUB75_OE_PWM_TICK_NS;
if (programmed_ns > local_max_programmed_ns) {
local_max_programmed_ns = programmed_ns;
}
/*
* While hardware bounds OE for the current latch, prepare the next
* bitplane. The row address remains unchanged until OE is inactive.
*/
if (item + 1u < total_planes) {
unsigned next_item = item + 1u;
size_t next_start = (size_t)next_item * HUB75_WIDTH;
for (unsigned column = 0; column < HUB75_WIDTH; ++column) {
uint32_t value = output_address | planes[next_start + column];
write_owned(context, value);
write_owned(context, value | PI_CLK);
}
}
if (ticks != 0) {
uint64_t pulse_deadline = pulse_enabled_at + programmed_ns +
HUB75_OE_PWM_TICK_NS / 2u;
uint64_t now = monotonic_ns();
if (now < pulse_deadline) {
busy_wait_ns(pulse_deadline - now);
}
uint64_t idle_started = monotonic_ns();
while ((*context->pwm_pcr4 & PWM_PCR_PULSE_START) != 0) {
if (monotonic_ns() - idle_started >= PWM_PERIOD_NS) {
pwm_force_blank(context, 1);
pthread_mutex_lock(&context->lock);
context->stats.oe_pulse_faults += 1;
snprintf(
context->stats.oe_timing_error,
sizeof(context->stats.oe_timing_error),
"PWM4 single-pulse start bit did not clear"
);
pthread_mutex_unlock(&context->lock);
return -1;
}
}
}
if (item + 1u < total_planes) {
unsigned next_row = (item + 1u) / HUB75_PWM_BITS;
output_address = address_bits(next_row);
write_owned(context, output_address);
write_owned(context, output_address | PI_LAT);
write_owned(context, output_address);
}
}
pthread_mutex_lock(&context->lock);
if (local_max_programmed_ns > context->stats.max_programmed_oe_ns) {
context->stats.max_programmed_oe_ns = local_max_programmed_ns;
}
pthread_mutex_unlock(&context->lock);
return 0;
}
static void *refresh_thread(void *argument) {
hub75_context *context = argument;
apply_thread_optimizations(context);
uint64_t window_start = monotonic_ns();
uint64_t window_frames = 0;
uint64_t window_scans = 0;
pthread_mutex_lock(&context->lock);
context->stats.running = 1;
pthread_mutex_unlock(&context->lock);
while (!atomic_load_explicit(&context->stop_requested, memory_order_acquire)) {
uint64_t frame_start = monotonic_ns();
int front;
int brightness;
int refresh_rate;
pthread_mutex_lock(&context->lock);
if (context->swap_pending) {
int previous_front = context->front_index;
context->front_index = context->back_index;
context->back_index = previous_front;
context->swap_pending = 0;
context->stats.buffer_swaps += 1;
}
front = context->front_index;
brightness = context->brightness;
refresh_rate = context->refresh_rate_hz;
pthread_mutex_unlock(&context->lock);
uint64_t period_ns = 1000000000ull / (uint64_t)refresh_rate;
if (refresh_rate == 100) {
/* 100 Hz is an upper limit; reserve bounded MMIO/jitter headroom. */
period_ns += HUNDRED_HZ_GUARD_NS;
}
int scans_per_frame = hub75_scans_per_frame_for_brightness(brightness);
uint64_t scan_slot_ns = period_ns / (uint64_t)scans_per_frame;
uint64_t frame_scans = 0;
for (int scan = 0; scan < scans_per_frame; ++scan) {
uint64_t scan_start = frame_start +
period_ns * (uint64_t)scan / (uint64_t)scans_per_frame;
uint64_t before_scan = monotonic_ns();
if (before_scan < scan_start) {
sleep_until_ns(scan_start);
}
if (scan_frame(
context,
context->bitplanes[front],
scan_slot_ns
) != 0) {
atomic_store_explicit(&context->stop_requested, 1, memory_order_release);
set_last_error(context, "PWM4 OE timing fault; output forced blank");
break;
}
++frame_scans;
}
uint64_t deadline = frame_start + period_ns;
uint64_t after_scan = monotonic_ns();
int missed = after_scan > deadline;
if (!missed) {
sleep_until_ns(deadline);
}
uint64_t now = monotonic_ns();
++window_frames;
window_scans += frame_scans;
pthread_mutex_lock(&context->lock);
context->stats.completed_frames += 1;
context->stats.completed_scans += frame_scans;
context->stats.scans_per_frame = scans_per_frame;
if (missed) {
context->stats.deadline_misses += 1;
}
if (now - window_start >= STATS_WINDOW_NS) {
context->stats.actual_refresh_rate_hz =
(double)window_frames * 1000000000.0 /
(double)(now - window_start);
context->stats.panel_scan_rate_hz =
(double)window_scans * 1000000000.0 /
(double)(now - window_start);
window_start = now;
window_frames = 0;
window_scans = 0;
}
pthread_mutex_unlock(&context->lock);
}
pwm_force_blank(context, 0);
pthread_mutex_lock(&context->lock);
context->stats.running = 0;
pthread_mutex_unlock(&context->lock);
return NULL;
}
hub75_context *hub75_create(const hub75_config *config, char *error, size_t error_size) {
if (config == NULL) {
copy_error(error, error_size, "config is required");
return NULL;
}
if (config->brightness < 1 || config->brightness > 100) {
copy_error(error, error_size, "brightness must be in 1..100");
return NULL;
}
if (!refresh_rate_valid(config->refresh_rate_hz)) {
copy_error(error, error_size, "unsupported refresh rate: %d", config->refresh_rate_hz);
return NULL;
}
if (config->cpu_affinity < 0 || config->realtime_priority < 1 ||
config->realtime_priority > 99 || config->fake_pwm5_mode < 0 ||
config->fake_pwm5_mode > 2 || config->fake_pwm4_stuck < 0 ||
config->fake_pwm4_stuck > 1 ||
(config->use_dev_mem && config->fake_pwm4_stuck != 0)) {
copy_error(error, error_size, "invalid CPU affinity or realtime priority");
return NULL;
}
hub75_context *context = calloc(1, sizeof(*context));
if (context == NULL) {
copy_error(error, error_size, "allocating driver context failed");
return NULL;
}
context->mem_fd = -1;
context->fake_pwm4_stuck = config->fake_pwm4_stuck;
pthread_mutexattr_t lock_attributes;
int lock_code = pthread_mutexattr_init(&lock_attributes);
int lock_attributes_ready = lock_code == 0;
if (lock_code == 0) {
lock_code = pthread_mutexattr_setprotocol(
&lock_attributes,
PTHREAD_PRIO_INHERIT
);
}
if (lock_code == 0) {
lock_code = pthread_mutex_init(&context->lock, &lock_attributes);
}
if (lock_attributes_ready) {
pthread_mutexattr_destroy(&lock_attributes);
}
if (lock_code != 0) {
copy_error(error, error_size, "initializing driver lock failed");
free(context);
return NULL;
}
context->brightness = config->brightness;
context->refresh_rate_hz = config->refresh_rate_hz;
context->cpu_affinity = config->cpu_affinity;
context->realtime_priority = config->realtime_priority;
context->front_index = 0;
context->back_index = 1;
context->stats.cpu_affinity = config->cpu_affinity;
context->stats.realtime_priority = config->realtime_priority;
context->stats.brightness = config->brightness;
context->stats.refresh_rate_hz = config->refresh_rate_hz;
context->stats.scans_per_frame =
hub75_scans_per_frame_for_brightness(config->brightness);
snprintf(context->stats.governor, sizeof(context->stats.governor), "unknown");
snprintf(
context->stats.oe_timing_backend,
sizeof(context->stats.oe_timing_backend),
"%s",
HUB75_OE_TIMING_BACKEND
);
if (map_registers(context, config->use_dev_mem, error, error_size) != 0) {
unmap_registers(context);
pthread_mutex_destroy(&context->lock);
free(context);
return NULL;
}
if (!config->use_dev_mem && config->fake_pwm5_mode != 0) {
*context->pwm_pccr45 = config->fake_pwm5_mode == 1 ? 0x50u : 0xd0u;
*context->pwm_per = PWM5_CHANNEL_ENABLE;
*(volatile uint32_t *)(context->pwm_mapped + PWM_PCR_OFFSET(5u)) = 0x100u;
*(volatile uint32_t *)(context->pwm_mapped + PWM_PPR_OFFSET(5u)) = 0x000b0006u;
}
configure_gpio(context);
if (configure_pwm4(context, error, error_size) != 0) {
snprintf(
context->stats.oe_timing_error,
sizeof(context->stats.oe_timing_error),
"%s",
error != NULL ? error : "PWM4 initialization failed"
);
restore_pwm4(context);
restore_gpio_safe(context);
unmap_registers(context);
pthread_mutex_destroy(&context->lock);
free(context);
return NULL;
}
switch_oe_to_pwm4(context);
memset(context->frames, 0, sizeof(context->frames));
memset(context->bitplanes, 0, sizeof(context->bitplanes));
return context;
}
int hub75_start(hub75_context *context) {
if (context == NULL) return -1;
pthread_mutex_lock(&context->lock);
if (context->thread_created) {
pthread_mutex_unlock(&context->lock);
return 0;
}
atomic_store_explicit(&context->stop_requested, 0, memory_order_release);
int code = pthread_create(&context->thread, NULL, refresh_thread, context);
if (code != 0) {
snprintf(
context->stats.last_error,
sizeof(context->stats.last_error),
"pthread_create failed: %s",
strerror(code)
);
pthread_mutex_unlock(&context->lock);
return -1;
}
context->thread_created = 1;
pthread_mutex_unlock(&context->lock);
return 0;
}
int hub75_submit_rgb(hub75_context *context, const uint8_t *rgb, size_t length) {
if (context == NULL || rgb == NULL || length != HUB75_FRAME_BYTES) {
if (context != NULL) set_last_error(context, "RGB frame must contain %u bytes", HUB75_FRAME_BYTES);
return -1;
}
uint16_t prepared[HUB75_BITPLANE_WORDS];
int brightness;
pthread_mutex_lock(&context->lock);
brightness = context->brightness;
pthread_mutex_unlock(&context->lock);
int result = hub75_build_bitplanes_for_brightness(
rgb,
HUB75_FRAME_BYTES,
brightness,
prepared,
HUB75_BITPLANE_WORDS
);
if (result == 0) {
pthread_mutex_lock(&context->lock);
int back = context->back_index;
memcpy(context->frames[back], rgb, HUB75_FRAME_BYTES);
memcpy(context->bitplanes[back], prepared, sizeof(prepared));
context->swap_pending = 1;
context->stats.submitted_frames += 1;
pthread_mutex_unlock(&context->lock);
}
return result;
}
int hub75_set_brightness(hub75_context *context, int brightness) {
if (context == NULL || brightness < 1 || brightness > 100) return -1;
pthread_mutex_lock(&context->lock);
context->brightness = brightness;
context->stats.brightness = brightness;
pthread_mutex_unlock(&context->lock);
return 0;
}
int hub75_set_refresh_rate(hub75_context *context, int refresh_rate_hz) {
if (context == NULL || !refresh_rate_valid(refresh_rate_hz)) return -1;
pthread_mutex_lock(&context->lock);
context->refresh_rate_hz = refresh_rate_hz;
context->stats.refresh_rate_hz = refresh_rate_hz;
pthread_mutex_unlock(&context->lock);
return 0;
}
int hub75_clear(hub75_context *context) {
if (context == NULL) return -1;
uint8_t black[HUB75_FRAME_BYTES] = {0};
return hub75_submit_rgb(context, black, sizeof(black));
}
int hub75_get_stats(hub75_context *context, hub75_stats *stats) {
if (context == NULL || stats == NULL) return -1;
pthread_mutex_lock(&context->lock);
*stats = context->stats;
pthread_mutex_unlock(&context->lock);
return 0;
}
void hub75_destroy(hub75_context *context) {
if (context == NULL) return;
atomic_store_explicit(&context->stop_requested, 1, memory_order_release);
if (context->thread_created) {
pthread_join(context->thread, NULL);
context->thread_created = 0;
}
pwm_force_blank(context, 0);
restore_pwm4(context);
restore_gpio_safe(context);
if (context->stats.memory_locked) {
munlockall();
}
unmap_registers(context);
pthread_mutex_destroy(&context->lock);
free(context);
}
int hub75_get_fake_registers(
hub75_context *context,
hub75_fake_registers *registers
) {
if (context == NULL || registers == NULL || !context->mapped_is_heap ||
!context->pwm_mapped_is_heap) {
return -1;
}
registers->mux0 = *context->mux0;
registers->mux1 = *context->mux1;
registers->data = *context->data;
registers->pwm_pccr45 = *context->pwm_pccr45;
registers->pwm_per = *context->pwm_per;
registers->pwm_pcr4 = *context->pwm_pcr4;
registers->pwm_ppr4 = *context->pwm_ppr4;
registers->pwm_pcr5 = *(volatile uint32_t *)(
context->pwm_mapped + PWM_PCR_OFFSET(5u)
);
registers->pwm_ppr5 = *(volatile uint32_t *)(
context->pwm_mapped + PWM_PPR_OFFSET(5u)
);
return 0;
}
static void force_all_owned_gpio_inputs(hub75_context *context) {
const uint32_t mux0_owned = mux_mask(0, 6, 0);
const uint32_t mux1_owned = mux_mask(9, 15, 8);
switch_oe_to_gpio_high(context);
write_owned(context, PI_OE);
*context->mux0 &= ~mux0_owned;
*context->mux1 &= ~mux1_owned;
atomic_thread_fence(memory_order_seq_cst);
context->gpio_configured = 0;
}
int hub75_force_safeoff(int use_dev_mem, char *error, size_t error_size) {
hub75_context context;
memset(&context, 0, sizeof(context));
context.mem_fd = -1;
if (pthread_mutex_init(&context.lock, NULL) != 0) {
copy_error(error, error_size, "initializing safeoff lock failed");
return -1;
}
if (map_registers(&context, use_dev_mem, error, error_size) != 0) {
unmap_registers(&context);
pthread_mutex_destroy(&context.lock);
return -1;
}
configure_gpio(&context);
pwm4_disable(&context);
force_all_owned_gpio_inputs(&context);
int failed = (*context.pwm_per & PWM_CHANNEL_ENABLE) != 0;
if (failed) {
copy_error(error, error_size, "safeoff could not disable PWM4");
}
unmap_registers(&context);
pthread_mutex_destroy(&context.lock);
return failed ? -1 : 0;
}