初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -O3 -std=c11 -Wall -Wextra -Werror -fPIC -pthread
|
||||
LDFLAGS ?= -pthread
|
||||
|
||||
LIBRARY := libh618_hub75.so
|
||||
BENCHMARK := hub75_benchmark
|
||||
UNIT_TEST := hub75_native_test
|
||||
SAFEOFF := hub75_safeoff
|
||||
|
||||
.PHONY: all clean test
|
||||
|
||||
all: $(LIBRARY) $(BENCHMARK) $(UNIT_TEST) $(SAFEOFF)
|
||||
|
||||
$(LIBRARY): h618_hub75.c h618_hub75.h
|
||||
$(CC) $(CFLAGS) -shared h618_hub75.c -o $@ $(LDFLAGS)
|
||||
|
||||
$(BENCHMARK): benchmark.c h618_hub75.c h618_hub75.h
|
||||
$(CC) $(CFLAGS) benchmark.c h618_hub75.c -o $@ $(LDFLAGS)
|
||||
|
||||
$(UNIT_TEST): native_test.c h618_hub75.c h618_hub75.h
|
||||
$(CC) $(CFLAGS) native_test.c h618_hub75.c -o $@ $(LDFLAGS)
|
||||
|
||||
$(SAFEOFF): safeoff.c h618_hub75.c h618_hub75.h
|
||||
$(CC) $(CFLAGS) safeoff.c h618_hub75.c -o $@ $(LDFLAGS)
|
||||
|
||||
test: $(UNIT_TEST) $(SAFEOFF)
|
||||
./$(UNIT_TEST)
|
||||
./$(SAFEOFF) --fake
|
||||
|
||||
clean:
|
||||
rm -f -- $(LIBRARY) $(BENCHMARK) $(UNIT_TEST) $(SAFEOFF)
|
||||
@@ -0,0 +1,30 @@
|
||||
# Third-party notices
|
||||
|
||||
The H618 PIO register layout and WalnutPi 40-pin physical mapping used by
|
||||
`h618_hub75.c` were derived from:
|
||||
|
||||
- Project: `walnutpi/gpioc`
|
||||
- Source: https://github.com/walnutpi/gpioc
|
||||
- Commit: `220a46267469cc9cb1cbc35cd72f35eb1699a6e8`
|
||||
- License: MIT
|
||||
- Copyright: Copyright (c) 2023 酥饼
|
||||
|
||||
## MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,188 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "h618_hub75.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static double monotonic_seconds(void) {
|
||||
struct timespec value;
|
||||
clock_gettime(CLOCK_MONOTONIC, &value);
|
||||
return (double)value.tv_sec + (double)value.tv_nsec / 1000000000.0;
|
||||
}
|
||||
|
||||
static int integer_argument(const char *value, const char *name) {
|
||||
char *end = NULL;
|
||||
long parsed = strtol(value, &end, 10);
|
||||
if (value[0] == '\0' || end == NULL || *end != '\0' || parsed < 0 || parsed > 1000000) {
|
||||
fprintf(stderr, "invalid %s: %s\n", name, value);
|
||||
exit(2);
|
||||
}
|
||||
return (int)parsed;
|
||||
}
|
||||
|
||||
static int hex_nibble(char value) {
|
||||
if (value >= '0' && value <= '9') return value - '0';
|
||||
if (value >= 'a' && value <= 'f') return value - 'a' + 10;
|
||||
if (value >= 'A' && value <= 'F') return value - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void color_argument(const char *value, uint8_t output[3]) {
|
||||
if (strlen(value) != 6) {
|
||||
fprintf(stderr, "invalid color: %s (expected RRGGBB)\n", value);
|
||||
exit(2);
|
||||
}
|
||||
for (unsigned channel = 0; channel < 3; ++channel) {
|
||||
int high = hex_nibble(value[channel * 2]);
|
||||
int low = hex_nibble(value[channel * 2 + 1]);
|
||||
if (high < 0 || low < 0) {
|
||||
fprintf(stderr, "invalid color: %s (expected RRGGBB)\n", value);
|
||||
exit(2);
|
||||
}
|
||||
output[channel] = (uint8_t)((high << 4) | low);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int duration = 10;
|
||||
int refresh_rate = 100;
|
||||
int brightness = 40;
|
||||
int use_dev_mem = 0;
|
||||
int use_static_color = 0;
|
||||
uint8_t static_color[3] = {0};
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
if (strcmp(argv[index], "--duration") == 0 && index + 1 < argc) {
|
||||
duration = integer_argument(argv[++index], "duration");
|
||||
} else if (strcmp(argv[index], "--refresh-rate") == 0 && index + 1 < argc) {
|
||||
refresh_rate = integer_argument(argv[++index], "refresh rate");
|
||||
} else if (strcmp(argv[index], "--brightness") == 0 && index + 1 < argc) {
|
||||
brightness = integer_argument(argv[++index], "brightness");
|
||||
} else if (strcmp(argv[index], "--dev-mem") == 0) {
|
||||
use_dev_mem = 1;
|
||||
} else if (strcmp(argv[index], "--color") == 0 && index + 1 < argc) {
|
||||
color_argument(argv[++index], static_color);
|
||||
use_static_color = 1;
|
||||
} else {
|
||||
fprintf(stderr, "usage: %s [--duration N] [--refresh-rate N] [--brightness N] [--color RRGGBB] [--dev-mem]\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
if (duration < 1) {
|
||||
fputs("duration must be at least one second\n", stderr);
|
||||
return 2;
|
||||
}
|
||||
|
||||
hub75_config config = {
|
||||
.brightness = brightness,
|
||||
.refresh_rate_hz = refresh_rate,
|
||||
.cpu_affinity = 3,
|
||||
.realtime_priority = 50,
|
||||
.use_dev_mem = use_dev_mem,
|
||||
};
|
||||
char error[512] = "";
|
||||
hub75_context *context = hub75_create(&config, error, sizeof(error));
|
||||
if (context == NULL) {
|
||||
fprintf(stderr, "create failed: %s\n", error);
|
||||
return 1;
|
||||
}
|
||||
if (hub75_start(context) != 0) {
|
||||
fputs("start failed\n", stderr);
|
||||
hub75_destroy(context);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t frame[HUB75_FRAME_BYTES];
|
||||
if (use_static_color) {
|
||||
for (unsigned pixel = 0; pixel < HUB75_WIDTH * HUB75_HEIGHT; ++pixel) {
|
||||
memcpy(frame + pixel * 3u, static_color, sizeof(static_color));
|
||||
}
|
||||
if (hub75_submit_rgb(context, frame, sizeof(frame)) != 0) {
|
||||
fputs("initial static submit failed\n", stderr);
|
||||
hub75_destroy(context);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
double started = monotonic_seconds();
|
||||
unsigned submitted = 0;
|
||||
while (monotonic_seconds() - started < duration) {
|
||||
if (!use_static_color) {
|
||||
for (unsigned pixel = 0; pixel < HUB75_WIDTH * HUB75_HEIGHT; ++pixel) {
|
||||
frame[pixel * 3] = (uint8_t)(pixel + submitted);
|
||||
frame[pixel * 3 + 1] = (uint8_t)(pixel * 3u + submitted);
|
||||
frame[pixel * 3 + 2] = (uint8_t)(pixel * 7u + submitted);
|
||||
}
|
||||
if (hub75_submit_rgb(context, frame, sizeof(frame)) != 0) {
|
||||
fputs("submit failed\n", stderr);
|
||||
hub75_destroy(context);
|
||||
return 1;
|
||||
}
|
||||
++submitted;
|
||||
}
|
||||
struct timespec delay = {.tv_sec = 0, .tv_nsec = 20000000};
|
||||
nanosleep(&delay, NULL);
|
||||
}
|
||||
|
||||
hub75_stats stats;
|
||||
if (hub75_get_stats(context, &stats) != 0) {
|
||||
fputs("stats failed\n", stderr);
|
||||
hub75_destroy(context);
|
||||
return 1;
|
||||
}
|
||||
hub75_destroy(context);
|
||||
int expected_scans = hub75_scans_per_frame_for_brightness(brightness);
|
||||
double miss_rate = stats.completed_frames == 0 ? 100.0 :
|
||||
(double)stats.deadline_misses * 100.0 / (double)stats.completed_frames;
|
||||
printf(
|
||||
"{\"mode\":\"%s\",\"configured_hz\":%d,\"actual_hz\":%.3f,"
|
||||
"\"panel_scan_hz\":%.3f,\"completed_frames\":%llu,"
|
||||
"\"completed_scans\":%llu,\"scans_per_frame\":%d,"
|
||||
"\"deadline_misses\":%llu,"
|
||||
"\"deadline_miss_percent\":%.6f,\"buffer_swaps\":%llu,"
|
||||
"\"submitted_frames\":%llu,\"oe_timing_backend\":\"%s\","
|
||||
"\"oe_pulse_faults\":%llu,\"oe_forced_blanks\":%llu,"
|
||||
"\"max_programmed_oe_ns\":%llu,\"oe_timing_error\":\"%s\","
|
||||
"\"cpu_affinity_active\":%s,"
|
||||
"\"realtime_priority_active\":%s,\"memory_locked\":%s,"
|
||||
"\"governor\":\"%s\",\"affinity_error\":\"%s\","
|
||||
"\"realtime_error\":\"%s\",\"memory_lock_error\":\"%s\","
|
||||
"\"last_error\":\"%s\"}\n",
|
||||
use_dev_mem ? "dev-mem" : "fake-registers",
|
||||
refresh_rate,
|
||||
stats.actual_refresh_rate_hz,
|
||||
stats.panel_scan_rate_hz,
|
||||
(unsigned long long)stats.completed_frames,
|
||||
(unsigned long long)stats.completed_scans,
|
||||
stats.scans_per_frame,
|
||||
(unsigned long long)stats.deadline_misses,
|
||||
miss_rate,
|
||||
(unsigned long long)stats.buffer_swaps,
|
||||
(unsigned long long)stats.submitted_frames,
|
||||
stats.oe_timing_backend,
|
||||
(unsigned long long)stats.oe_pulse_faults,
|
||||
(unsigned long long)stats.oe_forced_blanks,
|
||||
(unsigned long long)stats.max_programmed_oe_ns,
|
||||
stats.oe_timing_error,
|
||||
stats.cpu_affinity_active ? "true" : "false",
|
||||
stats.realtime_priority_active ? "true" : "false",
|
||||
stats.memory_locked ? "true" : "false",
|
||||
stats.governor,
|
||||
stats.affinity_error,
|
||||
stats.realtime_error,
|
||||
stats.memory_lock_error,
|
||||
stats.last_error
|
||||
);
|
||||
if (
|
||||
stats.completed_frames == 0 ||
|
||||
stats.actual_refresh_rate_hz <= 0.0 ||
|
||||
stats.panel_scan_rate_hz <= 0.0 ||
|
||||
stats.scans_per_frame != expected_scans ||
|
||||
stats.completed_scans != stats.completed_frames * (uint64_t)expected_scans ||
|
||||
stats.oe_pulse_faults != 0 ||
|
||||
strcmp(stats.oe_timing_backend, HUB75_OE_TIMING_BACKEND) != 0
|
||||
) return 1;
|
||||
return 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Native HUB75 refresh driver for WalnutPi ZeroW / Allwinner H618.
|
||||
*
|
||||
* GPIO register layout and physical-pin mapping are derived from WalnutPi's
|
||||
* MIT-licensed gpioc project, commit 220a46267469cc9cb1cbc35cd72f35eb1699a6e8:
|
||||
* https://github.com/walnutpi/gpioc
|
||||
*
|
||||
* This project keeps the upstream MIT license in THIRD_PARTY_NOTICES.md.
|
||||
*/
|
||||
#ifndef WALNUTPI_H618_HUB75_H
|
||||
#define WALNUTPI_H618_HUB75_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define HUB75_WIDTH 64
|
||||
#define HUB75_HEIGHT 64
|
||||
#define HUB75_SCAN_ROWS 32
|
||||
#define HUB75_PWM_BITS 7
|
||||
#define HUB75_INTERNAL_PWM_BITS 11
|
||||
#define HUB75_PWM_PLANE_OFFSET (HUB75_INTERNAL_PWM_BITS - HUB75_PWM_BITS)
|
||||
#define HUB75_SCANS_PER_FRAME 1
|
||||
#define HUB75_FRAME_BYTES (HUB75_WIDTH * HUB75_HEIGHT * 3)
|
||||
#define HUB75_BITPLANE_WORDS (HUB75_SCAN_ROWS * HUB75_PWM_BITS * HUB75_WIDTH)
|
||||
#define HUB75_HARDWARE_MAPPING "walnutpi-pi-bank-pwm-oe-v2"
|
||||
#define HUB75_OE_TIMING_BACKEND "h618-pwm4"
|
||||
#define HUB75_OE_PWM_TICK_NS 125u
|
||||
|
||||
typedef struct hub75_context hub75_context;
|
||||
|
||||
typedef struct hub75_config {
|
||||
int brightness;
|
||||
int refresh_rate_hz;
|
||||
int cpu_affinity;
|
||||
int realtime_priority;
|
||||
int use_dev_mem;
|
||||
int fake_pwm5_mode;
|
||||
int fake_pwm4_stuck;
|
||||
} hub75_config;
|
||||
|
||||
typedef struct hub75_stats {
|
||||
double actual_refresh_rate_hz;
|
||||
double panel_scan_rate_hz;
|
||||
uint64_t completed_frames;
|
||||
uint64_t completed_scans;
|
||||
uint64_t deadline_misses;
|
||||
uint64_t buffer_swaps;
|
||||
uint64_t submitted_frames;
|
||||
uint64_t oe_pulse_faults;
|
||||
uint64_t oe_forced_blanks;
|
||||
uint64_t max_programmed_oe_ns;
|
||||
int cpu_affinity;
|
||||
int cpu_affinity_active;
|
||||
int realtime_priority;
|
||||
int realtime_priority_active;
|
||||
int memory_locked;
|
||||
int running;
|
||||
int brightness;
|
||||
int refresh_rate_hz;
|
||||
int scans_per_frame;
|
||||
char governor[32];
|
||||
char affinity_error[128];
|
||||
char realtime_error[128];
|
||||
char memory_lock_error[128];
|
||||
char oe_timing_backend[32];
|
||||
char oe_timing_error[256];
|
||||
char safeoff_error[256];
|
||||
char last_error[256];
|
||||
} hub75_stats;
|
||||
|
||||
hub75_context *hub75_create(const hub75_config *config, char *error, size_t error_size);
|
||||
int hub75_start(hub75_context *context);
|
||||
int hub75_submit_rgb(hub75_context *context, const uint8_t *rgb, size_t length);
|
||||
int hub75_set_brightness(hub75_context *context, int brightness);
|
||||
int hub75_set_refresh_rate(hub75_context *context, int refresh_rate_hz);
|
||||
int hub75_clear(hub75_context *context);
|
||||
int hub75_get_stats(hub75_context *context, hub75_stats *stats);
|
||||
void hub75_destroy(hub75_context *context);
|
||||
const char *hub75_hardware_mapping(void);
|
||||
int hub75_force_safeoff(int use_dev_mem, char *error, size_t error_size);
|
||||
|
||||
/* Pure conversion hook used by tests; output words contain PI0..PI5 colors. */
|
||||
int hub75_build_bitplanes(
|
||||
const uint8_t *rgb,
|
||||
size_t length,
|
||||
uint16_t *output,
|
||||
size_t output_words
|
||||
);
|
||||
|
||||
/* Raspberry-Pi-compatible CIE1931 brightness mapping into 11-bit color. */
|
||||
int hub75_build_bitplanes_for_brightness(
|
||||
const uint8_t *rgb,
|
||||
size_t length,
|
||||
int brightness,
|
||||
uint16_t *output,
|
||||
size_t output_words
|
||||
);
|
||||
|
||||
/* Pure 8-bit RGB channel to 11-bit CIE1931 mapping hook. */
|
||||
uint16_t hub75_map_channel_for_brightness(uint8_t value, int brightness);
|
||||
|
||||
/* Pure scan-count hook used by native tests and the benchmark. */
|
||||
int hub75_scans_per_frame_for_brightness(int brightness);
|
||||
|
||||
/* Fixed binary OE weights matching internal bitplanes 4..10. */
|
||||
uint32_t hub75_oe_ticks_for_plane(
|
||||
uint64_t scan_slot_ns,
|
||||
int brightness,
|
||||
unsigned plane,
|
||||
uint64_t *remainder
|
||||
);
|
||||
|
||||
typedef struct hub75_fake_registers {
|
||||
uint32_t mux0;
|
||||
uint32_t mux1;
|
||||
uint32_t data;
|
||||
uint32_t pwm_pccr45;
|
||||
uint32_t pwm_per;
|
||||
uint32_t pwm_pcr4;
|
||||
uint32_t pwm_ppr4;
|
||||
uint32_t pwm_pcr5;
|
||||
uint32_t pwm_ppr5;
|
||||
} hub75_fake_registers;
|
||||
|
||||
int hub75_get_fake_registers(
|
||||
hub75_context *context,
|
||||
hub75_fake_registers *registers
|
||||
);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,195 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "h618_hub75.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
enum {
|
||||
PI_R1 = 1u << 0,
|
||||
PI_G1 = 1u << 1,
|
||||
PI_B1 = 1u << 2,
|
||||
PI_R2 = 1u << 3,
|
||||
PI_G2 = 1u << 4,
|
||||
PI_B2 = 1u << 5,
|
||||
};
|
||||
|
||||
static size_t word_index(unsigned row, unsigned plane, unsigned column) {
|
||||
return ((size_t)row * HUB75_PWM_BITS + plane) * HUB75_WIDTH + column;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
assert(strcmp(hub75_hardware_mapping(), HUB75_HARDWARE_MAPPING) == 0);
|
||||
assert(HUB75_INTERNAL_PWM_BITS == 11);
|
||||
assert(HUB75_PWM_PLANE_OFFSET == 4);
|
||||
assert(HUB75_SCANS_PER_FRAME == 1);
|
||||
assert(hub75_scans_per_frame_for_brightness(1) == 1);
|
||||
assert(hub75_scans_per_frame_for_brightness(20) == 1);
|
||||
assert(hub75_scans_per_frame_for_brightness(100) == 1);
|
||||
assert(hub75_scans_per_frame_for_brightness(0) == -1);
|
||||
assert(hub75_scans_per_frame_for_brightness(101) == -1);
|
||||
|
||||
uint16_t previous_mapped = 0;
|
||||
for (int brightness = 1; brightness <= 100; ++brightness) {
|
||||
uint16_t mapped = hub75_map_channel_for_brightness(255, brightness);
|
||||
assert(mapped >= previous_mapped);
|
||||
previous_mapped = mapped;
|
||||
for (unsigned plane = 0; plane < HUB75_PWM_BITS; ++plane) {
|
||||
uint64_t remainder = 123u;
|
||||
assert(hub75_oe_ticks_for_plane(
|
||||
10250000, brightness, plane, &remainder
|
||||
) == (1u << (plane + HUB75_PWM_PLANE_OFFSET)));
|
||||
assert(remainder == 0);
|
||||
}
|
||||
}
|
||||
assert(previous_mapped == 2047u);
|
||||
assert(hub75_map_channel_for_brightness(255, 0) == UINT16_MAX);
|
||||
uint64_t invalid_remainder = 0;
|
||||
assert(hub75_oe_ticks_for_plane(5000000, 0, 0, &invalid_remainder) == UINT32_MAX);
|
||||
assert(hub75_oe_ticks_for_plane(5000000, 1, HUB75_PWM_BITS, &invalid_remainder) == UINT32_MAX);
|
||||
uint64_t full_scale_remainder = 0;
|
||||
assert(hub75_oe_ticks_for_plane(
|
||||
10250000, 100, HUB75_PWM_BITS - 1, &full_scale_remainder
|
||||
) == 1024u);
|
||||
uint8_t *frame = calloc(1, HUB75_FRAME_BYTES);
|
||||
uint16_t *planes = calloc(HUB75_BITPLANE_WORDS, sizeof(*planes));
|
||||
assert(frame != NULL);
|
||||
assert(planes != NULL);
|
||||
|
||||
/* Compare extracted planes with the Raspberry-compatible 11-bit map. */
|
||||
frame[0] = 0xff;
|
||||
frame[1] = 0x80;
|
||||
frame[2] = 0x40;
|
||||
size_t bottom = (size_t)HUB75_SCAN_ROWS * HUB75_WIDTH * 3u;
|
||||
frame[bottom] = 0x20;
|
||||
frame[bottom + 1] = 0x10;
|
||||
frame[bottom + 2] = 0x08;
|
||||
assert(hub75_build_bitplanes(
|
||||
frame,
|
||||
HUB75_FRAME_BYTES,
|
||||
planes,
|
||||
HUB75_BITPLANE_WORDS
|
||||
) == 0);
|
||||
const uint8_t values[] = {0xff, 0x80, 0x40, 0x20, 0x10, 0x08};
|
||||
const uint16_t bits[] = {PI_R1, PI_G1, PI_B1, PI_R2, PI_G2, PI_B2};
|
||||
for (unsigned plane = 0; plane < HUB75_PWM_BITS; ++plane) {
|
||||
uint16_t expected = 0;
|
||||
for (unsigned component = 0; component < 6; ++component) {
|
||||
uint16_t mapped = hub75_map_channel_for_brightness(values[component], 100);
|
||||
if ((mapped >> (plane + HUB75_PWM_PLANE_OFFSET)) & 1u) {
|
||||
expected |= bits[component];
|
||||
}
|
||||
}
|
||||
assert(planes[word_index(0, plane, 0)] == expected);
|
||||
}
|
||||
|
||||
memset(frame, 0xff, HUB75_FRAME_BYTES);
|
||||
assert(hub75_build_bitplanes(
|
||||
frame,
|
||||
HUB75_FRAME_BYTES,
|
||||
planes,
|
||||
HUB75_BITPLANE_WORDS
|
||||
) == 0);
|
||||
for (size_t index = 0; index < HUB75_BITPLANE_WORDS; ++index) {
|
||||
assert(planes[index] == (PI_R1 | PI_G1 | PI_B1 | PI_R2 | PI_G2 | PI_B2));
|
||||
}
|
||||
|
||||
/* Brightness changes bitplane data rather than OE pulse duration. */
|
||||
memset(frame, 0, HUB75_FRAME_BYTES);
|
||||
frame[0] = 0xff;
|
||||
frame[1] = 0x4f;
|
||||
frame[2] = 0xa3;
|
||||
uint16_t low_planes[HUB75_BITPLANE_WORDS];
|
||||
assert(hub75_build_bitplanes_for_brightness(
|
||||
frame, HUB75_FRAME_BYTES, 100, planes, HUB75_BITPLANE_WORDS
|
||||
) == 0);
|
||||
assert(hub75_build_bitplanes_for_brightness(
|
||||
frame, HUB75_FRAME_BYTES, 20, low_planes, HUB75_BITPLANE_WORDS
|
||||
) == 0);
|
||||
assert(memcmp(low_planes, planes, sizeof(low_planes)) != 0);
|
||||
|
||||
assert(hub75_build_bitplanes(frame, 1, planes, HUB75_BITPLANE_WORDS) == -1);
|
||||
assert(hub75_build_bitplanes(frame, HUB75_FRAME_BYTES, planes, 1) == -1);
|
||||
|
||||
hub75_config config = {
|
||||
.brightness = 40,
|
||||
.refresh_rate_hz = 100,
|
||||
.cpu_affinity = 0,
|
||||
.realtime_priority = 1,
|
||||
.use_dev_mem = 0,
|
||||
};
|
||||
char error[512] = {0};
|
||||
hub75_context *context = hub75_create(&config, error, sizeof(error));
|
||||
assert(context != NULL);
|
||||
hub75_fake_registers registers;
|
||||
assert(hub75_get_fake_registers(context, ®isters) == 0);
|
||||
assert(((registers.mux1 >> ((14u - 8u) * 4u)) & 0xfu) == 0x5u);
|
||||
assert(((registers.mux1 >> ((15u - 8u) * 4u)) & 0xfu) == 0x1u);
|
||||
assert((registers.pwm_pccr45 & (1u << 4)) != 0);
|
||||
assert((registers.pwm_pccr45 & ((0xfu) | (1u << 5) | (0x3u << 7))) == 0);
|
||||
assert((registers.pwm_per & (1u << 4)) != 0);
|
||||
assert((registers.pwm_pcr4 & 0x1ffu) == 2u);
|
||||
assert((registers.pwm_pcr4 & (1u << 9)) != 0);
|
||||
assert(registers.pwm_ppr4 == (65535u << 16));
|
||||
hub75_stats stats;
|
||||
assert(hub75_get_stats(context, &stats) == 0);
|
||||
assert(strcmp(stats.oe_timing_backend, HUB75_OE_TIMING_BACKEND) == 0);
|
||||
assert(stats.oe_pulse_faults == 0);
|
||||
memset(frame, 0xff, HUB75_FRAME_BYTES);
|
||||
assert(hub75_submit_rgb(context, frame, HUB75_FRAME_BYTES) == 0);
|
||||
assert(hub75_set_brightness(context, 100) == 0);
|
||||
assert(hub75_start(context) == 0);
|
||||
struct timespec pulse_test_delay = {.tv_sec = 0, .tv_nsec = 100000000};
|
||||
nanosleep(&pulse_test_delay, NULL);
|
||||
assert(hub75_get_stats(context, &stats) == 0);
|
||||
assert(stats.max_programmed_oe_ns == 128000u);
|
||||
assert(stats.oe_pulse_faults == 0);
|
||||
assert(hub75_get_fake_registers(context, ®isters) == 0);
|
||||
assert((registers.pwm_pcr4 & (1u << 9)) != 0);
|
||||
assert((registers.pwm_pcr4 & (1u << 10)) == 0);
|
||||
assert((registers.pwm_per & (1u << 4)) != 0);
|
||||
assert((registers.pwm_ppr4 & 0xffffu) != 0);
|
||||
assert((registers.pwm_ppr4 >> 16) + 1u ==
|
||||
(registers.pwm_ppr4 & 0xffffu));
|
||||
hub75_destroy(context);
|
||||
|
||||
config.fake_pwm4_stuck = 1;
|
||||
context = hub75_create(&config, error, sizeof(error));
|
||||
assert(context != NULL);
|
||||
assert(hub75_start(context) == 0);
|
||||
struct timespec stuck_test_delay = {.tv_sec = 0, .tv_nsec = 100000000};
|
||||
nanosleep(&stuck_test_delay, NULL);
|
||||
assert(hub75_get_stats(context, &stats) == 0);
|
||||
assert(stats.oe_pulse_faults == 1);
|
||||
assert(stats.oe_forced_blanks == 1);
|
||||
assert(strstr(stats.oe_timing_error, "start bit did not clear") != NULL);
|
||||
assert(hub75_get_fake_registers(context, ®isters) == 0);
|
||||
assert((registers.pwm_per & (1u << 4)) == 0);
|
||||
assert(((registers.mux1 >> ((14u - 8u) * 4u)) & 0xfu) == 0x1u);
|
||||
hub75_destroy(context);
|
||||
config.fake_pwm4_stuck = 0;
|
||||
|
||||
config.fake_pwm5_mode = 1;
|
||||
context = hub75_create(&config, error, sizeof(error));
|
||||
assert(context != NULL);
|
||||
assert(hub75_get_fake_registers(context, ®isters) == 0);
|
||||
assert((registers.pwm_per & (1u << 5)) != 0);
|
||||
assert((registers.pwm_pccr45 & (1u << 6)) != 0);
|
||||
assert(registers.pwm_pcr5 == 0x100u);
|
||||
assert(registers.pwm_ppr5 == 0x000b0006u);
|
||||
hub75_destroy(context);
|
||||
|
||||
config.fake_pwm5_mode = 2;
|
||||
context = hub75_create(&config, error, sizeof(error));
|
||||
assert(context == NULL);
|
||||
assert(strstr(error, "incompatible shared PWM4/5 clock") != NULL);
|
||||
assert(hub75_force_safeoff(0, error, sizeof(error)) == 0);
|
||||
|
||||
free(planes);
|
||||
free(frame);
|
||||
puts("native_bitplane_tests=PASS");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only H618 PI-bank snapshot for no-load safety verification."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import mmap
|
||||
import os
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
PIO_BASE = 0x0300B000
|
||||
PIO_LENGTH = 0x400
|
||||
PI_OFFSET = 0x120
|
||||
DATA_OFFSET = 0x10
|
||||
OWNED_BITS = tuple([*range(0, 7), *range(9, 16)])
|
||||
RESERVED_BITS = (7, 8, 16)
|
||||
|
||||
|
||||
def read_u32(mapped: mmap.mmap, offset: int) -> int:
|
||||
return struct.unpack_from("<I", mapped, offset)[0]
|
||||
|
||||
|
||||
def mux_mode(mapped: mmap.mmap, pin: int) -> int:
|
||||
register = PI_OFFSET + (pin // 8) * 4
|
||||
return (read_u32(mapped, register) >> ((pin % 8) * 4)) & 0xF
|
||||
|
||||
|
||||
def device_tree_contract() -> dict:
|
||||
node = Path("/proc/device-tree/soc/pinctrl@300b000")
|
||||
compatible = (node / "compatible").read_bytes().rstrip(b"\0").decode("ascii")
|
||||
values = struct.unpack(">II", (node / "reg").read_bytes())
|
||||
if compatible != "allwinner,sun50i-h616-pinctrl" or values != (PIO_BASE, PIO_LENGTH):
|
||||
raise RuntimeError(f"unexpected pinctrl contract: {compatible=} {values=}")
|
||||
return {"compatible": compatible, "base": f"0x{values[0]:08X}", "length": values[1]}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
contract = device_tree_contract()
|
||||
descriptor = os.open("/dev/mem", os.O_RDONLY | os.O_SYNC)
|
||||
try:
|
||||
with mmap.mmap(descriptor, PIO_LENGTH, access=mmap.ACCESS_READ, offset=PIO_BASE) as mapped:
|
||||
data = read_u32(mapped, PI_OFFSET + DATA_OFFSET)
|
||||
modes = {f"PI{pin}": mux_mode(mapped, pin) for pin in range(17)}
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
owned_mask = sum(1 << bit for bit in OWNED_BITS)
|
||||
reserved_mask = sum(1 << bit for bit in RESERVED_BITS)
|
||||
print(json.dumps({
|
||||
"device_tree": contract,
|
||||
"data_register": f"0x{data:08X}",
|
||||
"owned_data_bits": f"0x{data & owned_mask:08X}",
|
||||
"reserved_data_bits": f"0x{data & reserved_mask:08X}",
|
||||
"modes": modes,
|
||||
}, sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "h618_hub75.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int use_dev_mem = 1;
|
||||
if (argc == 2 && strcmp(argv[1], "--fake") == 0) {
|
||||
use_dev_mem = 0;
|
||||
} else if (argc != 1) {
|
||||
fprintf(stderr, "usage: %s [--fake]\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
char error[512] = {0};
|
||||
if (hub75_force_safeoff(use_dev_mem, error, sizeof(error)) != 0) {
|
||||
fprintf(stderr, "HUB75 safeoff failed: %s\n", error[0] ? error : "unknown error");
|
||||
return 1;
|
||||
}
|
||||
puts("HUB75 safeoff complete");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user