1201 lines
41 KiB
C
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(¶meter, 0, sizeof(parameter));
|
|
parameter.sched_priority = context->realtime_priority;
|
|
code = pthread_setschedparam(pthread_self(), SCHED_FIFO, ¶meter);
|
|
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;
|
|
}
|