blob: 29fcaf625057e22f8950ebd1b3032a533ecfd316 [file]
/*
* Copyright (c) 2025 Siratul Islam <email@sirat.me>
* SPDX-License-Identifier: Apache-2.0
*
* Driver for 32x16 monochrome P10 LED panels with HUB12 interface.
*/
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/display.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/util.h>
#include <string.h>
LOG_MODULE_REGISTER(hub12, CONFIG_DISPLAY_LOG_LEVEL);
#define DT_DRV_COMPAT zephyr_hub12
/* Single panel constants */
#define HUB12_PANEL_WIDTH 32
#define HUB12_PANEL_HEIGHT 16
#define HUB12_ROWS 4
#define HUB12_BYTES_PER_PANEL 16
#define HUB12_GROUP_SIZE 4
#define HUB12_NUM_GROUPS 4
#define HUB12_PIXELS_PER_BYTE 8
/* Macros for cache and row calculations */
#define HUB12_BYTES_PER_ROW(width) (((width) / HUB12_PANEL_WIDTH) * HUB12_BYTES_PER_PANEL)
#define HUB12_CACHE_SIZE(width) (HUB12_ROWS * HUB12_BYTES_PER_ROW(width))
/* Brightness control parameters */
#define HUB12_DEFAULT_BRIGHTNESS 5
#define HUB12_MIN_BRIGHTNESS 1
#define HUB12_MAX_BRIGHTNESS 50
struct hub12_config {
struct gpio_dt_spec pa;
struct gpio_dt_spec pb;
struct gpio_dt_spec pe;
struct gpio_dt_spec plat;
struct spi_dt_spec spi;
uint16_t width;
uint16_t height;
uint16_t bytes_per_row;
uint8_t num_panels;
};
struct hub12_data {
uint8_t *framebuffer;
uint8_t *cache;
uint8_t current_row;
struct k_timer scan_timer;
struct k_work scan_work;
struct k_sem lock;
const struct device *dev;
uint8_t brightness_us;
};
static void hub12_update_cache(struct hub12_data *data, const struct hub12_config *config,
uint8_t row)
{
const uint8_t *fb = data->framebuffer;
uint8_t *cache_row = data->cache + (row * config->bytes_per_row);
for (uint8_t panel = 0; panel < config->num_panels; panel++) {
for (int i = 0; i < HUB12_BYTES_PER_PANEL; i++) {
/* Determine byte position within 4-byte groups */
int group = i / HUB12_GROUP_SIZE;
int offset = i % HUB12_GROUP_SIZE;
int reverse_offset = (HUB12_GROUP_SIZE - 1) - offset;
/* Calculate which framebuffer byte maps to this cache position */
int bytes_per_group_row = config->num_panels * HUB12_NUM_GROUPS;
int offset_base = reverse_offset * bytes_per_group_row * HUB12_ROWS;
int row_offset = row * bytes_per_group_row;
int panel_offset = panel * HUB12_NUM_GROUPS;
int fb_idx = offset_base + row_offset + panel_offset + group;
cache_row[panel * HUB12_BYTES_PER_PANEL + i] = ~fb[fb_idx];
}
}
}
static void hub12_scan_row(struct hub12_data *data, const struct hub12_config *config)
{
uint8_t row = data->current_row;
uint8_t *cache_row = data->cache + (row * config->bytes_per_row);
int ret;
struct spi_buf tx_buf = {.buf = cache_row, .len = config->bytes_per_row};
struct spi_buf_set tx = {.buffers = &tx_buf, .count = 1};
ret = spi_write_dt(&config->spi, &tx);
if (ret < 0) {
LOG_ERR("SPI write failed: %d", ret);
return;
}
gpio_pin_set_dt(&config->pe, 0);
gpio_pin_set_dt(&config->plat, 1);
k_busy_wait(1);
gpio_pin_set_dt(&config->plat, 0);
gpio_pin_set_dt(&config->pa, (row & BIT(0)) ? 1 : 0);
gpio_pin_set_dt(&config->pb, (row & BIT(1)) ? 1 : 0);
if (data->brightness_us > 0) {
gpio_pin_set_dt(&config->pe, 1);
k_busy_wait(data->brightness_us);
gpio_pin_set_dt(&config->pe, 0);
}
data->current_row = (data->current_row + 1) % HUB12_ROWS;
}
static void hub12_scan_work_handler(struct k_work *work)
{
struct hub12_data *data = CONTAINER_OF(work, struct hub12_data, scan_work);
const struct hub12_config *config = data->dev->config;
/* K_NO_WAIT to avoid blocking work queue */
if (k_sem_take(&data->lock, K_NO_WAIT) == 0) {
hub12_scan_row(data, config);
uint8_t next_row = data->current_row;
hub12_update_cache(data, config, next_row);
k_sem_give(&data->lock);
}
}
static void hub12_scan_timer_handler(struct k_timer *timer)
{
struct hub12_data *data = CONTAINER_OF(timer, struct hub12_data, scan_timer);
k_work_submit(&data->scan_work);
}
static int hub12_write(const struct device *dev, const uint16_t x, const uint16_t y,
const struct display_buffer_descriptor *desc, const void *buf)
{
struct hub12_data *data = dev->data;
const struct hub12_config *config = dev->config;
const uint8_t *src = buf;
size_t fb_size = config->width * config->height / HUB12_PIXELS_PER_BYTE;
if (x >= config->width || y >= config->height) {
return -EINVAL;
}
if ((x + desc->width) > config->width || (y + desc->height) > config->height) {
return -EINVAL;
}
if (desc->pitch != desc->width) {
LOG_ERR("Unsupported pitch");
return -ENOTSUP;
}
if (desc->buf_size < (desc->width * desc->height / HUB12_PIXELS_PER_BYTE)) {
LOG_ERR("Buffer too small");
return -EINVAL;
}
k_sem_take(&data->lock, K_FOREVER);
if (x == 0 && y == 0 && desc->width == config->width && desc->height == config->height) {
memcpy(data->framebuffer, src, fb_size);
} else {
/* Partial update */
size_t src_pitch_bytes = desc->pitch / HUB12_PIXELS_PER_BYTE;
size_t dest_pitch_bytes = config->width / HUB12_PIXELS_PER_BYTE;
for (uint16_t j = 0; j < desc->height; j++) {
uint16_t dest_y = y + j;
for (uint16_t i = 0; i < desc->width; i++) {
uint16_t dest_x = x + i;
size_t src_byte_idx =
(j * src_pitch_bytes) + (i / HUB12_PIXELS_PER_BYTE);
uint8_t src_bit_mask = BIT(7 - (i % HUB12_PIXELS_PER_BYTE));
bool bit_is_set = (src[src_byte_idx] & src_bit_mask);
size_t dest_byte_idx = (dest_y * dest_pitch_bytes) +
(dest_x / HUB12_PIXELS_PER_BYTE);
uint8_t dest_bit_mask = BIT(7 - (dest_x % HUB12_PIXELS_PER_BYTE));
if (bit_is_set) {
data->framebuffer[dest_byte_idx] |= dest_bit_mask;
} else {
data->framebuffer[dest_byte_idx] &= ~dest_bit_mask;
}
}
}
}
for (int i = 0; i < HUB12_ROWS; i++) {
hub12_update_cache(data, config, i);
}
k_sem_give(&data->lock);
return 0;
}
static int hub12_read(const struct device *dev, const uint16_t x, const uint16_t y,
const struct display_buffer_descriptor *desc, void *buf)
{
return -ENOTSUP;
}
static void *hub12_get_framebuffer(const struct device *dev)
{
struct hub12_data *data = dev->data;
return data->framebuffer;
}
static int hub12_blanking_off(const struct device *dev)
{
return 0;
}
static int hub12_blanking_on(const struct device *dev)
{
return 0;
}
static int hub12_set_brightness(const struct device *dev, const uint8_t brightness)
{
struct hub12_data *data = dev->data;
if (brightness == 0) {
data->brightness_us = 0;
} else {
uint32_t range = HUB12_MAX_BRIGHTNESS - HUB12_MIN_BRIGHTNESS;
data->brightness_us = HUB12_MIN_BRIGHTNESS + (uint8_t)((brightness * range) / 255U);
}
LOG_INF("Brightness set to %u us", data->brightness_us);
return 0;
}
static int hub12_set_contrast(const struct device *dev, const uint8_t contrast)
{
return -ENOTSUP;
}
static void hub12_get_capabilities(const struct device *dev, struct display_capabilities *caps)
{
const struct hub12_config *config = dev->config;
memset(caps, 0, sizeof(*caps));
caps->x_resolution = config->width;
caps->y_resolution = config->height;
caps->supported_pixel_formats = PIXEL_FORMAT_MONO01;
caps->current_pixel_format = PIXEL_FORMAT_MONO01;
caps->screen_info = SCREEN_INFO_MONO_MSB_FIRST;
}
static int hub12_set_pixel_format(const struct device *dev, const enum display_pixel_format pf)
{
if (pf == PIXEL_FORMAT_MONO01) {
return 0;
}
return -ENOTSUP;
}
static int hub12_set_orientation(const struct device *dev,
const enum display_orientation orientation)
{
if (orientation == DISPLAY_ORIENTATION_NORMAL) {
return 0;
}
return -ENOTSUP;
}
static const struct display_driver_api hub12_api = {
.blanking_on = hub12_blanking_on,
.blanking_off = hub12_blanking_off,
.write = hub12_write,
.read = hub12_read,
.get_framebuffer = hub12_get_framebuffer,
.set_brightness = hub12_set_brightness,
.set_contrast = hub12_set_contrast,
.get_capabilities = hub12_get_capabilities,
.set_pixel_format = hub12_set_pixel_format,
.set_orientation = hub12_set_orientation,
};
static int hub12_init(const struct device *dev)
{
struct hub12_data *data = dev->data;
const struct hub12_config *config = dev->config;
int ret;
data->dev = dev;
if (!gpio_is_ready_dt(&config->pa) || !gpio_is_ready_dt(&config->pb) ||
!gpio_is_ready_dt(&config->pe) || !gpio_is_ready_dt(&config->plat)) {
LOG_ERR("GPIO devices not ready");
return -ENODEV;
}
ret = gpio_pin_configure_dt(&config->pa, GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
return ret;
}
ret = gpio_pin_configure_dt(&config->pb, GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
return ret;
}
ret = gpio_pin_configure_dt(&config->pe, GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
return ret;
}
ret = gpio_pin_configure_dt(&config->plat, GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
return ret;
}
if (!spi_is_ready_dt(&config->spi)) {
LOG_ERR("SPI device not ready");
return -ENODEV;
}
memset(data->framebuffer, 0, (config->width * config->height) / HUB12_PIXELS_PER_BYTE);
memset(data->cache, 0, HUB12_ROWS * config->bytes_per_row);
data->current_row = 0;
data->brightness_us = HUB12_DEFAULT_BRIGHTNESS;
ret = k_sem_init(&data->lock, 1, 1);
if (ret < 0) {
LOG_ERR("Failed to initialize semaphore");
return ret;
}
for (int i = 0; i < HUB12_ROWS; i++) {
hub12_update_cache(data, config, i);
}
k_work_init(&data->scan_work, hub12_scan_work_handler);
k_timer_init(&data->scan_timer, hub12_scan_timer_handler, NULL);
k_timer_start(&data->scan_timer, K_MSEC(1), K_MSEC(1));
LOG_INF("HUB12 display initialized: %dx%d (%d panels)", config->width, config->height,
config->num_panels);
return 0;
}
#define HUB12_INIT(inst) \
BUILD_ASSERT((DT_INST_PROP(inst, width) % HUB12_PANEL_WIDTH) == 0, \
"HUB12 width must be a multiple of " STRINGIFY(HUB12_PANEL_WIDTH)); \
\
static uint8_t hub12_framebuffer_##inst[(DT_INST_PROP(inst, width) * \
DT_INST_PROP(inst, height)) / \
HUB12_PIXELS_PER_BYTE]; \
static uint8_t hub12_cache_##inst[HUB12_CACHE_SIZE(DT_INST_PROP(inst, width))]; \
static struct hub12_data hub12_data_##inst = { \
.framebuffer = hub12_framebuffer_##inst, \
.cache = hub12_cache_##inst, \
}; \
\
static const struct hub12_config hub12_config_##inst = { \
.pa = GPIO_DT_SPEC_INST_GET(inst, pa_gpios), \
.pb = GPIO_DT_SPEC_INST_GET(inst, pb_gpios), \
.pe = GPIO_DT_SPEC_INST_GET(inst, pe_gpios), \
.plat = GPIO_DT_SPEC_INST_GET(inst, plat_gpios), \
.spi = SPI_DT_SPEC_INST_GET(inst, SPI_OP_MODE_MASTER | SPI_WORD_SET(8)), \
.width = DT_INST_PROP(inst, width), \
.height = DT_INST_PROP(inst, height), \
.num_panels = DT_INST_PROP(inst, width) / HUB12_PANEL_WIDTH, \
.bytes_per_row = HUB12_BYTES_PER_ROW(DT_INST_PROP(inst, width)), \
}; \
\
DEVICE_DT_INST_DEFINE(inst, hub12_init, NULL, &hub12_data_##inst, &hub12_config_##inst, \
POST_KERNEL, CONFIG_DISPLAY_INIT_PRIORITY, &hub12_api);
DT_INST_FOREACH_STATUS_OKAY(HUB12_INIT)