Skip to content

Commit 0974e9b

Browse files
heronetjhedberg
authored andcommitted
drivers: display: add HUB12 LED matrix driver
Add driver for HUB12 interface monochrome LED matrix displays. Features: - 32x16 pixel resolution, 1-bit monochrome (PIXEL_FORMAT_MONO01) - SPI-based data transfer with shift registers - Configurable brightness control - Thread-safe framebuffer access with semaphore The driver implements the standard Zephyr display API Signed-off-by: Siratul Islam <[email protected]>
1 parent 1230358 commit 0974e9b

File tree

4 files changed

+379
-0
lines changed

4 files changed

+379
-0
lines changed

drivers/display/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ zephyr_library_sources_ifdef(CONFIG_ST7796S display_st7796s.c)
3434
zephyr_library_sources_ifdef(CONFIG_STM32_LTDC display_stm32_ltdc.c)
3535
zephyr_library_sources_ifdef(CONFIG_RM68200 display_rm68200.c)
3636
zephyr_library_sources_ifdef(CONFIG_RM67162 display_rm67162.c)
37+
zephyr_library_sources_ifdef(CONFIG_HUB12 display_hub12.c)
3738
zephyr_library_sources_ifdef(CONFIG_HX8379C display_hx8379c.c)
3839
zephyr_library_sources_ifdef(CONFIG_HX8394 display_hx8394.c)
3940
zephyr_library_sources_ifdef(CONFIG_GC9X01X display_gc9x01x.c)

drivers/display/Kconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ source "drivers/display/Kconfig.max7219"
5151
source "drivers/display/Kconfig.intel_multibootfb"
5252
source "drivers/display/Kconfig.mcux_dcnano_lcdif"
5353
source "drivers/display/Kconfig.otm8009a"
54+
source "drivers/display/Kconfig.hub12"
5455
source "drivers/display/Kconfig.hx8379c"
5556
source "drivers/display/Kconfig.hx8394"
5657
source "drivers/display/Kconfig.gc9x01x"

drivers/display/Kconfig.hub12

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright (c) 2025 Siratul Islam <[email protected]>
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
config HUB12
5+
bool "HUB12 LED Panel Display Controller"
6+
default y
7+
depends on DT_HAS_ZEPHYR_HUB12_ENABLED
8+
select SPI
9+
select GPIO
10+
help
11+
HUB12 LED panel controller for 32x16 monochrome displays

drivers/display/display_hub12.c

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

0 commit comments

Comments
 (0)