|  | 
|  | 1 | +/* | 
|  | 2 | + * Copyright (c) 2025 Siratul Islam <[email protected]> | 
|  | 3 | + * SPDX-License-Identifier: Apache-2.0 | 
|  | 4 | + * | 
|  | 5 | + * Driver for 32x16 monochrome LED panels with HUB12 interface. | 
|  | 6 | + */ | 
|  | 7 | + | 
|  | 8 | +#include <zephyr/kernel.h> | 
|  | 9 | +#include <zephyr/device.h> | 
|  | 10 | +#include <zephyr/drivers/display.h> | 
|  | 11 | +#include <zephyr/drivers/gpio.h> | 
|  | 12 | +#include <zephyr/drivers/spi.h> | 
|  | 13 | +#include <zephyr/logging/log.h> | 
|  | 14 | +#include <zephyr/sys/util.h> | 
|  | 15 | +#include <string.h> | 
|  | 16 | + | 
|  | 17 | +LOG_MODULE_REGISTER(hub12, CONFIG_DISPLAY_LOG_LEVEL); | 
|  | 18 | + | 
|  | 19 | +#define DT_DRV_COMPAT hub12 | 
|  | 20 | + | 
|  | 21 | +/* Display dimensions and layout */ | 
|  | 22 | +#define HUB12_WIDTH         32 | 
|  | 23 | +#define HUB12_HEIGHT        16 | 
|  | 24 | +#define HUB12_ROWS          4 | 
|  | 25 | +#define HUB12_BYTES_PER_ROW 16 | 
|  | 26 | +#define HUB12_GROUP_SIZE    4 | 
|  | 27 | +#define HUB12_NUM_GROUPS    4 | 
|  | 28 | + | 
|  | 29 | +/* Brightness control parameters */ | 
|  | 30 | +#define HUB12_PWM_FREQ           1000 | 
|  | 31 | +#define HUB12_DEFAULT_BRIGHTNESS 5 | 
|  | 32 | +#define HUB12_MIN_BRIGHTNESS     1 | 
|  | 33 | +#define HUB12_MAX_BRIGHTNESS     50 | 
|  | 34 | + | 
|  | 35 | +struct hub12_config { | 
|  | 36 | +	struct gpio_dt_spec pa; | 
|  | 37 | +	struct gpio_dt_spec pb; | 
|  | 38 | +	struct gpio_dt_spec pe; | 
|  | 39 | +	struct gpio_dt_spec plat; | 
|  | 40 | +	struct spi_dt_spec spi; | 
|  | 41 | +}; | 
|  | 42 | + | 
|  | 43 | +struct hub12_data { | 
|  | 44 | +	uint8_t framebuffer[HUB12_WIDTH * HUB12_HEIGHT / 8]; | 
|  | 45 | +	uint8_t cache[HUB12_ROWS][HUB12_BYTES_PER_ROW]; | 
|  | 46 | +	uint8_t current_row; | 
|  | 47 | +	struct k_timer scan_timer; | 
|  | 48 | +	struct k_work scan_work; | 
|  | 49 | +	struct k_sem lock; | 
|  | 50 | +	const struct device *dev; | 
|  | 51 | +	uint8_t brightness_us; | 
|  | 52 | +}; | 
|  | 53 | + | 
|  | 54 | +static void hub12_update_cache(struct hub12_data *data, uint8_t row) | 
|  | 55 | +{ | 
|  | 56 | +	const uint8_t *fb = data->framebuffer; | 
|  | 57 | + | 
|  | 58 | +	for (int i = 0; i < HUB12_BYTES_PER_ROW; i++) { | 
|  | 59 | +		int group = i / HUB12_GROUP_SIZE; | 
|  | 60 | +		int offset = i % HUB12_GROUP_SIZE; | 
|  | 61 | +		int reverse_offset = (HUB12_GROUP_SIZE - 1) - offset; | 
|  | 62 | +		int fb_idx = reverse_offset * HUB12_NUM_GROUPS * HUB12_ROWS + | 
|  | 63 | +			     row * HUB12_NUM_GROUPS + group; | 
|  | 64 | + | 
|  | 65 | +		data->cache[row][i] = ~fb[fb_idx]; | 
|  | 66 | +	} | 
|  | 67 | +} | 
|  | 68 | + | 
|  | 69 | +static void hub12_scan_row(struct hub12_data *data, const struct hub12_config *config) | 
|  | 70 | +{ | 
|  | 71 | +	uint8_t row = data->current_row; | 
|  | 72 | +	int ret; | 
|  | 73 | + | 
|  | 74 | +	struct spi_buf tx_buf = {.buf = data->cache[row], .len = HUB12_BYTES_PER_ROW}; | 
|  | 75 | +	struct spi_buf_set tx = {.buffers = &tx_buf, .count = 1}; | 
|  | 76 | + | 
|  | 77 | +	ret = spi_write_dt(&config->spi, &tx); | 
|  | 78 | +	if (ret < 0) { | 
|  | 79 | +		LOG_ERR("SPI write failed: %d", ret); | 
|  | 80 | +		return; | 
|  | 81 | +	} | 
|  | 82 | + | 
|  | 83 | +	gpio_pin_set_dt(&config->pe, 0); | 
|  | 84 | + | 
|  | 85 | +	gpio_pin_set_dt(&config->plat, 1); | 
|  | 86 | +	k_busy_wait(1); | 
|  | 87 | +	gpio_pin_set_dt(&config->plat, 0); | 
|  | 88 | + | 
|  | 89 | +	gpio_pin_set_dt(&config->pa, (row & BIT(0)) ? 1 : 0); | 
|  | 90 | +	gpio_pin_set_dt(&config->pb, (row & BIT(1)) ? 1 : 0); | 
|  | 91 | + | 
|  | 92 | +	if (data->brightness_us > 0) { | 
|  | 93 | +		gpio_pin_set_dt(&config->pe, 1); | 
|  | 94 | +		k_busy_wait(data->brightness_us); | 
|  | 95 | +		gpio_pin_set_dt(&config->pe, 0); | 
|  | 96 | +	} | 
|  | 97 | + | 
|  | 98 | +	data->current_row = (data->current_row + 1) % HUB12_ROWS; | 
|  | 99 | + | 
|  | 100 | +	hub12_update_cache(data, data->current_row); | 
|  | 101 | +} | 
|  | 102 | + | 
|  | 103 | +static void hub12_scan_work_handler(struct k_work *work) | 
|  | 104 | +{ | 
|  | 105 | +	struct hub12_data *data = CONTAINER_OF(work, struct hub12_data, scan_work); | 
|  | 106 | +	const struct hub12_config *config = data->dev->config; | 
|  | 107 | + | 
|  | 108 | +	hub12_scan_row(data, config); | 
|  | 109 | +} | 
|  | 110 | + | 
|  | 111 | +static void hub12_scan_timer_handler(struct k_timer *timer) | 
|  | 112 | +{ | 
|  | 113 | +	struct hub12_data *data = CONTAINER_OF(timer, struct hub12_data, scan_timer); | 
|  | 114 | + | 
|  | 115 | +	k_work_submit(&data->scan_work); | 
|  | 116 | +} | 
|  | 117 | + | 
|  | 118 | +static int hub12_write(const struct device *dev, const uint16_t x, const uint16_t y, | 
|  | 119 | +		       const struct display_buffer_descriptor *desc, const void *buf) | 
|  | 120 | +{ | 
|  | 121 | +	struct hub12_data *data = dev->data; | 
|  | 122 | +	const uint8_t *src = buf; | 
|  | 123 | +	size_t fb_size = HUB12_WIDTH * HUB12_HEIGHT / 8; | 
|  | 124 | + | 
|  | 125 | +	if (x >= HUB12_WIDTH || y >= HUB12_HEIGHT) { | 
|  | 126 | +		return -EINVAL; | 
|  | 127 | +	} | 
|  | 128 | + | 
|  | 129 | +	if ((x + desc->width) > HUB12_WIDTH || (y + desc->height) > HUB12_HEIGHT) { | 
|  | 130 | +		return -EINVAL; | 
|  | 131 | +	} | 
|  | 132 | + | 
|  | 133 | +	if (desc->pitch != desc->width) { | 
|  | 134 | +		LOG_ERR("Unsupported pitch"); | 
|  | 135 | +		return -ENOTSUP; | 
|  | 136 | +	} | 
|  | 137 | + | 
|  | 138 | +	if (desc->buf_size < (desc->width * desc->height / 8)) { | 
|  | 139 | +		LOG_ERR("Buffer too small"); | 
|  | 140 | +		return -EINVAL; | 
|  | 141 | +	} | 
|  | 142 | + | 
|  | 143 | +	k_sem_take(&data->lock, K_FOREVER); | 
|  | 144 | + | 
|  | 145 | +	if (x == 0 && y == 0 && desc->width == HUB12_WIDTH && desc->height == HUB12_HEIGHT) { | 
|  | 146 | +		memcpy(data->framebuffer, src, fb_size); | 
|  | 147 | +	} else { | 
|  | 148 | +		LOG_WRN("Partial updates not optimized"); | 
|  | 149 | +		memcpy(data->framebuffer, src, fb_size); | 
|  | 150 | +	} | 
|  | 151 | + | 
|  | 152 | +	for (int i = 0; i < HUB12_ROWS; i++) { | 
|  | 153 | +		hub12_update_cache(data, i); | 
|  | 154 | +	} | 
|  | 155 | + | 
|  | 156 | +	k_sem_give(&data->lock); | 
|  | 157 | + | 
|  | 158 | +	return 0; | 
|  | 159 | +} | 
|  | 160 | + | 
|  | 161 | +static int hub12_read(const struct device *dev, const uint16_t x, const uint16_t y, | 
|  | 162 | +		      const struct display_buffer_descriptor *desc, void *buf) | 
|  | 163 | +{ | 
|  | 164 | +	return -ENOTSUP; | 
|  | 165 | +} | 
|  | 166 | + | 
|  | 167 | +static void *hub12_get_framebuffer(const struct device *dev) | 
|  | 168 | +{ | 
|  | 169 | +	struct hub12_data *data = dev->data; | 
|  | 170 | + | 
|  | 171 | +	return data->framebuffer; | 
|  | 172 | +} | 
|  | 173 | + | 
|  | 174 | +static int hub12_blanking_off(const struct device *dev) | 
|  | 175 | +{ | 
|  | 176 | +	return 0; | 
|  | 177 | +} | 
|  | 178 | + | 
|  | 179 | +static int hub12_blanking_on(const struct device *dev) | 
|  | 180 | +{ | 
|  | 181 | +	return 0; | 
|  | 182 | +} | 
|  | 183 | + | 
|  | 184 | +static int hub12_set_brightness(const struct device *dev, const uint8_t brightness) | 
|  | 185 | +{ | 
|  | 186 | +	struct hub12_data *data = dev->data; | 
|  | 187 | + | 
|  | 188 | +	if (brightness == 0) { | 
|  | 189 | +		data->brightness_us = 0; | 
|  | 190 | +	} else { | 
|  | 191 | +		uint32_t range = HUB12_MAX_BRIGHTNESS - HUB12_MIN_BRIGHTNESS; | 
|  | 192 | + | 
|  | 193 | +		data->brightness_us = HUB12_MIN_BRIGHTNESS + (uint8_t)((brightness * range) / 255U); | 
|  | 194 | +	} | 
|  | 195 | + | 
|  | 196 | +	LOG_INF("Brightness set to %u us", data->brightness_us); | 
|  | 197 | + | 
|  | 198 | +	return 0; | 
|  | 199 | +} | 
|  | 200 | + | 
|  | 201 | +static int hub12_set_contrast(const struct device *dev, const uint8_t contrast) | 
|  | 202 | +{ | 
|  | 203 | +	return -ENOTSUP; | 
|  | 204 | +} | 
|  | 205 | + | 
|  | 206 | +static void hub12_get_capabilities(const struct device *dev, struct display_capabilities *caps) | 
|  | 207 | +{ | 
|  | 208 | +	memset(caps, 0, sizeof(*caps)); | 
|  | 209 | +	caps->x_resolution = HUB12_WIDTH; | 
|  | 210 | +	caps->y_resolution = HUB12_HEIGHT; | 
|  | 211 | +	caps->supported_pixel_formats = PIXEL_FORMAT_MONO01; | 
|  | 212 | +	caps->current_pixel_format = PIXEL_FORMAT_MONO01; | 
|  | 213 | +	caps->screen_info = SCREEN_INFO_MONO_MSB_FIRST; | 
|  | 214 | +} | 
|  | 215 | + | 
|  | 216 | +static int hub12_set_pixel_format(const struct device *dev, const enum display_pixel_format pf) | 
|  | 217 | +{ | 
|  | 218 | +	if (pf == PIXEL_FORMAT_MONO01) { | 
|  | 219 | +		return 0; | 
|  | 220 | +	} | 
|  | 221 | + | 
|  | 222 | +	return -ENOTSUP; | 
|  | 223 | +} | 
|  | 224 | + | 
|  | 225 | +static int hub12_set_orientation(const struct device *dev, | 
|  | 226 | +				 const enum display_orientation orientation) | 
|  | 227 | +{ | 
|  | 228 | +	if (orientation == DISPLAY_ORIENTATION_NORMAL) { | 
|  | 229 | +		return 0; | 
|  | 230 | +	} | 
|  | 231 | + | 
|  | 232 | +	return -ENOTSUP; | 
|  | 233 | +} | 
|  | 234 | + | 
|  | 235 | +static const struct display_driver_api hub12_api = { | 
|  | 236 | +	.blanking_on = hub12_blanking_on, | 
|  | 237 | +	.blanking_off = hub12_blanking_off, | 
|  | 238 | +	.write = hub12_write, | 
|  | 239 | +	.read = hub12_read, | 
|  | 240 | +	.get_framebuffer = hub12_get_framebuffer, | 
|  | 241 | +	.set_brightness = hub12_set_brightness, | 
|  | 242 | +	.set_contrast = hub12_set_contrast, | 
|  | 243 | +	.get_capabilities = hub12_get_capabilities, | 
|  | 244 | +	.set_pixel_format = hub12_set_pixel_format, | 
|  | 245 | +	.set_orientation = hub12_set_orientation, | 
|  | 246 | +}; | 
|  | 247 | + | 
|  | 248 | +static int hub12_init(const struct device *dev) | 
|  | 249 | +{ | 
|  | 250 | +	struct hub12_data *data = dev->data; | 
|  | 251 | +	const struct hub12_config *config = dev->config; | 
|  | 252 | +	int ret; | 
|  | 253 | + | 
|  | 254 | +	data->dev = dev; | 
|  | 255 | + | 
|  | 256 | +	if (!gpio_is_ready_dt(&config->pa) || !gpio_is_ready_dt(&config->pb) || | 
|  | 257 | +	    !gpio_is_ready_dt(&config->pe) || !gpio_is_ready_dt(&config->plat)) { | 
|  | 258 | +		LOG_ERR("GPIO devices not ready"); | 
|  | 259 | +		return -ENODEV; | 
|  | 260 | +	} | 
|  | 261 | + | 
|  | 262 | +	ret = gpio_pin_configure_dt(&config->pa, GPIO_OUTPUT_INACTIVE); | 
|  | 263 | +	if (ret < 0) { | 
|  | 264 | +		return ret; | 
|  | 265 | +	} | 
|  | 266 | + | 
|  | 267 | +	ret = gpio_pin_configure_dt(&config->pb, GPIO_OUTPUT_INACTIVE); | 
|  | 268 | +	if (ret < 0) { | 
|  | 269 | +		return ret; | 
|  | 270 | +	} | 
|  | 271 | + | 
|  | 272 | +	ret = gpio_pin_configure_dt(&config->pe, GPIO_OUTPUT_INACTIVE); | 
|  | 273 | +	if (ret < 0) { | 
|  | 274 | +		return ret; | 
|  | 275 | +	} | 
|  | 276 | + | 
|  | 277 | +	ret = gpio_pin_configure_dt(&config->plat, GPIO_OUTPUT_INACTIVE); | 
|  | 278 | +	if (ret < 0) { | 
|  | 279 | +		return ret; | 
|  | 280 | +	} | 
|  | 281 | + | 
|  | 282 | +	if (!spi_is_ready_dt(&config->spi)) { | 
|  | 283 | +		LOG_ERR("SPI device not ready"); | 
|  | 284 | +		return -ENODEV; | 
|  | 285 | +	} | 
|  | 286 | + | 
|  | 287 | +	memset(data->framebuffer, 0, sizeof(data->framebuffer)); | 
|  | 288 | +	memset(data->cache, 0, sizeof(data->cache)); | 
|  | 289 | +	data->current_row = 0; | 
|  | 290 | +	data->brightness_us = HUB12_DEFAULT_BRIGHTNESS; | 
|  | 291 | + | 
|  | 292 | +	ret = k_sem_init(&data->lock, 1, 1); | 
|  | 293 | +	if (ret < 0) { | 
|  | 294 | +		LOG_ERR("Failed to initialize semaphore"); | 
|  | 295 | +		return ret; | 
|  | 296 | +	} | 
|  | 297 | + | 
|  | 298 | +	for (int i = 0; i < HUB12_ROWS; i++) { | 
|  | 299 | +		hub12_update_cache(data, i); | 
|  | 300 | +	} | 
|  | 301 | + | 
|  | 302 | +	k_work_init(&data->scan_work, hub12_scan_work_handler); | 
|  | 303 | +	k_timer_init(&data->scan_timer, hub12_scan_timer_handler, NULL); | 
|  | 304 | +	k_timer_start(&data->scan_timer, K_MSEC(1), K_MSEC(1)); | 
|  | 305 | + | 
|  | 306 | +	LOG_INF("HUB12 display initialized: %dx%d", HUB12_WIDTH, HUB12_HEIGHT); | 
|  | 307 | + | 
|  | 308 | +	return 0; | 
|  | 309 | +} | 
|  | 310 | + | 
|  | 311 | +#define HUB12_INIT(inst)                                                                           \ | 
|  | 312 | +	static struct hub12_data hub12_data_##inst;                                                \ | 
|  | 313 | +                                                                                                   \ | 
|  | 314 | +	static const struct hub12_config hub12_config_##inst = {                                   \ | 
|  | 315 | +		.pa = GPIO_DT_SPEC_INST_GET(inst, pa_gpios),                                       \ | 
|  | 316 | +		.pb = GPIO_DT_SPEC_INST_GET(inst, pb_gpios),                                       \ | 
|  | 317 | +		.pe = GPIO_DT_SPEC_INST_GET(inst, pe_gpios),                                       \ | 
|  | 318 | +		.plat = GPIO_DT_SPEC_INST_GET(inst, plat_gpios),                                   \ | 
|  | 319 | +		.spi = SPI_DT_SPEC_INST_GET(inst, SPI_OP_MODE_MASTER | SPI_WORD_SET(8) |           \ | 
|  | 320 | +							  SPI_TRANSFER_LSB),                       \ | 
|  | 321 | +	};                                                                                         \ | 
|  | 322 | +                                                                                                   \ | 
|  | 323 | +	DEVICE_DT_INST_DEFINE(inst, hub12_init, NULL, &hub12_data_##inst, &hub12_config_##inst,    \ | 
|  | 324 | +			      POST_KERNEL, CONFIG_DISPLAY_INIT_PRIORITY, &hub12_api); | 
|  | 325 | + | 
|  | 326 | +DT_INST_FOREACH_STATUS_OKAY(HUB12_INIT) | 
0 commit comments