#define _GNU_SOURCE #include "h618_hub75.h" #include #include #include #include #include #include #include #include #include #include #include #include #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; }