drivers: display: Added LPM013M126 display driver.
Add support for JDI LPM013M126 RGB memory display Co-developed-by: Elgin Perumbilly <elgin.perumbilly@siliconsignals.io> Signed-off-by: Elgin Perumbilly <elgin.perumbilly@siliconsignals.io> Signed-off-by: Bhavin Sharma <bhavin.sharma@siliconsignals.io>
This commit is contained in:
committed by
Chris Friedt
parent
ccf27a9701
commit
d368921ae6
@@ -41,6 +41,7 @@ zephyr_library_sources_ifdef(CONFIG_NT35510 display_nt35510.c)
|
||||
zephyr_library_sources_ifdef(CONFIG_RENESAS_RA_GLCDC display_renesas_ra.c)
|
||||
zephyr_library_sources_ifdef(CONFIG_ILI9806E_DSI display_ili9806e_dsi.c)
|
||||
zephyr_library_sources_ifdef(CONFIG_ST7701 display_st7701.c)
|
||||
zephyr_library_sources_ifdef(CONFIG_LPM013M126 display_lpm013m126.c)
|
||||
|
||||
zephyr_library_sources_ifdef(CONFIG_MICROBIT_DISPLAY
|
||||
mb_display.c
|
||||
|
||||
@@ -58,5 +58,6 @@ source "drivers/display/Kconfig.nt35510"
|
||||
source "drivers/display/Kconfig.renesas_ra"
|
||||
source "drivers/display/Kconfig.ili9806e_dsi"
|
||||
source "drivers/display/Kconfig.st7701"
|
||||
source "drivers/display/Kconfig.lpm013m126"
|
||||
|
||||
endif # DISPLAY
|
||||
|
||||
10
drivers/display/Kconfig.lpm013m126
Normal file
10
drivers/display/Kconfig.lpm013m126
Normal file
@@ -0,0 +1,10 @@
|
||||
# Copyright (c) 2025 Silicon Signals Pvt. Ltd.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
config LPM013M126
|
||||
bool "LPM013M126 memory display controller driver"
|
||||
default y
|
||||
depends on DT_HAS_JDI_LPM013M126_ENABLED
|
||||
select SPI
|
||||
help
|
||||
Enable driver for JDI memory display series LPM013M126
|
||||
269
drivers/display/display_lpm013m126.c
Normal file
269
drivers/display/display_lpm013m126.c
Normal file
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Copyright (c) 2025 Silicon Signals Pvt. Ltd.
|
||||
* Author: Bhavin Sharma <bhavin.sharma@siliconsignals.io>
|
||||
* Author: Elgin Perumbilly <elgin.perumbilly@siliconsignals.io>
|
||||
*/
|
||||
|
||||
#define DT_DRV_COMPAT jdi_lpm013m126
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
LOG_MODULE_REGISTER(lpm013m126, CONFIG_DISPLAY_LOG_LEVEL);
|
||||
|
||||
#include <string.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/drivers/display.h>
|
||||
#include <zephyr/init.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/drivers/spi.h>
|
||||
#include <zephyr/kernel.h>
|
||||
|
||||
/* Panel properties */
|
||||
#define LPM_BPP 3
|
||||
|
||||
/* Command bytes */
|
||||
#define LPM_WRITELINE_CMD 0x80
|
||||
#define LPM_ALLCLEAR_CMD 0x20
|
||||
|
||||
struct lpm013m126_data {
|
||||
struct k_timer vcom_timer;
|
||||
int vcom_state;
|
||||
};
|
||||
|
||||
struct lpm013m126_config {
|
||||
struct spi_dt_spec bus;
|
||||
struct gpio_dt_spec disp_gpio;
|
||||
struct gpio_dt_spec extcomin_gpio;
|
||||
uint32_t extcomin_freq;
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
};
|
||||
|
||||
/* bit-reversal of row address */
|
||||
static inline uint8_t bitrev8(uint8_t x)
|
||||
{
|
||||
x = ((x & 0xF0) >> 4) | ((x & 0x0F) << 4);
|
||||
x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2);
|
||||
x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1);
|
||||
return x;
|
||||
}
|
||||
|
||||
/* The native format (1 bit per channel) is rather unusual. LVGL and
|
||||
* other libraries don't support it. In addition, the format is not
|
||||
* very convenient for the application. So, we prefer to advertise
|
||||
* a well known format and convert it under the hood.
|
||||
* A native implementation of this format would allow to save memory
|
||||
* for the frame buffer (11kB instead of 30kB).
|
||||
*/
|
||||
static inline uint8_t rgb565_to_rgb3(uint16_t color)
|
||||
{
|
||||
uint8_t r = FIELD_GET(0xF800, color);
|
||||
uint8_t g = FIELD_GET(0x07E0, color);
|
||||
uint8_t b = FIELD_GET(0x001F, color);
|
||||
|
||||
r >>= 4;
|
||||
g >>= 5;
|
||||
b >>= 4;
|
||||
return (r << 2) | (g << 1) | b;
|
||||
}
|
||||
|
||||
/* Pack one row of RGB565 pixels into panel format */
|
||||
static void lpm_pack_row(const struct device *dev, uint8_t *dst, const uint16_t *src)
|
||||
{
|
||||
const struct lpm013m126_config *cfg = dev->config;
|
||||
|
||||
int bitpos = 0;
|
||||
uint8_t byte = 0;
|
||||
|
||||
for (int x = 0; x < cfg->width; x++) {
|
||||
uint8_t pix = rgb565_to_rgb3(src[x]);
|
||||
|
||||
for (int b = 2; b >= 0; b--) {
|
||||
byte |= ((pix >> b) & 0x1) << (7 - bitpos);
|
||||
bitpos++;
|
||||
|
||||
if (bitpos == 8) {
|
||||
*dst++ = byte;
|
||||
bitpos = 0;
|
||||
byte = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* flush final partial byte if needed */
|
||||
if (bitpos) {
|
||||
*dst++ = byte;
|
||||
}
|
||||
}
|
||||
|
||||
/* VCOM toggle callback */
|
||||
static void lpm_vcom_toggle(struct k_timer *timer)
|
||||
{
|
||||
const struct device *dev = k_timer_user_data_get(timer);
|
||||
const struct lpm013m126_config *cfg = dev->config;
|
||||
struct lpm013m126_data *data = dev->data;
|
||||
|
||||
data->vcom_state = !data->vcom_state;
|
||||
gpio_pin_set_dt(&cfg->extcomin_gpio, data->vcom_state);
|
||||
}
|
||||
|
||||
/* Send a line to the display */
|
||||
static int lpm_send_line(const struct device *dev, uint8_t line, uint8_t *buf, size_t len)
|
||||
{
|
||||
const struct lpm013m126_config *cfg = dev->config;
|
||||
|
||||
uint8_t cmd = LPM_WRITELINE_CMD;
|
||||
uint8_t addr = bitrev8(line);
|
||||
|
||||
struct spi_buf tx_bufs[] = {
|
||||
{ .buf = &cmd, .len = 1 },
|
||||
{ .buf = &addr, .len = 1 },
|
||||
{ .buf = buf, .len = len },
|
||||
};
|
||||
|
||||
struct spi_buf_set tx = {
|
||||
.buffers = tx_bufs,
|
||||
.count = ARRAY_SIZE(tx_bufs)
|
||||
};
|
||||
|
||||
return spi_write_dt(&cfg->bus, &tx);
|
||||
}
|
||||
|
||||
/* Send all-clear command */
|
||||
static int lpm_all_clear(const struct device *dev)
|
||||
{
|
||||
const struct lpm013m126_config *cfg = dev->config;
|
||||
|
||||
uint8_t cmd = LPM_ALLCLEAR_CMD;
|
||||
uint8_t dummy = 0x00;
|
||||
|
||||
struct spi_buf tx_bufs[] = {
|
||||
{ .buf = &cmd, .len = 1 },
|
||||
{ .buf = &dummy, .len = 1 },
|
||||
};
|
||||
|
||||
struct spi_buf_set tx = {
|
||||
.buffers = tx_bufs,
|
||||
.count = ARRAY_SIZE(tx_bufs)
|
||||
};
|
||||
|
||||
return spi_write_dt(&cfg->bus, &tx);
|
||||
}
|
||||
|
||||
/* Write buffer to panel */
|
||||
static int lpm_write(const struct device *dev, const uint16_t x, const uint16_t y,
|
||||
const struct display_buffer_descriptor *desc, const void *buf)
|
||||
{
|
||||
const struct lpm013m126_config *cfg = dev->config;
|
||||
|
||||
if (x != 0 || desc->width != cfg->width) {
|
||||
LOG_ERR("Only full-width writes supported");
|
||||
return -ENOTSUP;
|
||||
}
|
||||
if ((y + desc->height) > cfg->height) {
|
||||
LOG_ERR("Buffer out of bounds");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
const uint16_t *src = buf;
|
||||
size_t packed_len = DIV_ROUND_UP(cfg->width * LPM_BPP, 8);
|
||||
uint8_t linebuf[packed_len];
|
||||
|
||||
for (int row = 0; row < desc->height; row++) {
|
||||
lpm_pack_row(dev, linebuf, src);
|
||||
lpm_send_line(dev, y + row + 1, linebuf, packed_len);
|
||||
src += cfg->width;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void lpm_get_capabilities(const struct device *dev, struct display_capabilities *caps)
|
||||
{
|
||||
const struct lpm013m126_config *cfg = dev->config;
|
||||
|
||||
memset(caps, 0, sizeof(*caps));
|
||||
caps->x_resolution = cfg->width;
|
||||
caps->y_resolution = cfg->height;
|
||||
caps->supported_pixel_formats = PIXEL_FORMAT_RGB_565;
|
||||
caps->current_pixel_format = PIXEL_FORMAT_RGB_565;
|
||||
caps->screen_info = SCREEN_INFO_X_ALIGNMENT_WIDTH;
|
||||
}
|
||||
|
||||
static int lpm_set_pixel_format(const struct device *dev, enum display_pixel_format pf)
|
||||
{
|
||||
return (pf == PIXEL_FORMAT_RGB_565) ? 0 : -ENOTSUP;
|
||||
}
|
||||
|
||||
static int lpm_blanking_off(const struct device *dev)
|
||||
{
|
||||
const struct lpm013m126_config *cfg = dev->config;
|
||||
|
||||
return gpio_pin_set_dt(&cfg->disp_gpio, 1);
|
||||
}
|
||||
|
||||
static int lpm_blanking_on(const struct device *dev)
|
||||
{
|
||||
const struct lpm013m126_config *cfg = dev->config;
|
||||
|
||||
return gpio_pin_set_dt(&cfg->disp_gpio, 0);
|
||||
}
|
||||
|
||||
static int lpm_init(const struct device *dev)
|
||||
{
|
||||
const struct lpm013m126_config *cfg = dev->config;
|
||||
|
||||
struct lpm013m126_data *data = dev->data;
|
||||
|
||||
if (!spi_is_ready_dt(&cfg->bus)) {
|
||||
LOG_ERR("SPI not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
if (!gpio_is_ready_dt(&cfg->disp_gpio)) {
|
||||
LOG_ERR("DISP pin not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
if (!gpio_is_ready_dt(&cfg->extcomin_gpio)) {
|
||||
LOG_ERR("EXTCOMIN pin not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
gpio_pin_configure_dt(&cfg->disp_gpio, GPIO_OUTPUT_HIGH);
|
||||
gpio_pin_configure_dt(&cfg->extcomin_gpio, GPIO_OUTPUT_LOW);
|
||||
|
||||
lpm_all_clear(dev);
|
||||
|
||||
data->vcom_state = 0;
|
||||
k_timer_init(&data->vcom_timer, lpm_vcom_toggle, NULL);
|
||||
k_timer_user_data_set(&data->vcom_timer, (void *)dev);
|
||||
k_timer_start(&data->vcom_timer, K_MSEC(1000 / cfg->extcomin_freq / 2),
|
||||
K_MSEC(1000 / cfg->extcomin_freq / 2));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct display_driver_api lpm_api = {
|
||||
.blanking_on = lpm_blanking_on,
|
||||
.blanking_off = lpm_blanking_off,
|
||||
.write = lpm_write,
|
||||
.get_capabilities = lpm_get_capabilities,
|
||||
.set_pixel_format = lpm_set_pixel_format,
|
||||
};
|
||||
|
||||
#define LPM013M126_INIT(inst) \
|
||||
static const struct lpm013m126_config lpm_cfg_##inst = { \
|
||||
.bus = SPI_DT_SPEC_INST_GET(inst, SPI_OP_MODE_MASTER | \
|
||||
SPI_WORD_SET(8) | \
|
||||
SPI_TRANSFER_MSB, 0), \
|
||||
.disp_gpio = GPIO_DT_SPEC_INST_GET(inst, disp_gpios), \
|
||||
.extcomin_gpio = GPIO_DT_SPEC_INST_GET(inst, extcomin_gpios), \
|
||||
.extcomin_freq = DT_INST_PROP(inst, extcomin_frequency), \
|
||||
.width = DT_INST_PROP(inst, width), \
|
||||
.height = DT_INST_PROP(inst, height), \
|
||||
}; \
|
||||
static struct lpm013m126_data lpm_data_##inst; \
|
||||
DEVICE_DT_INST_DEFINE(inst, lpm_init, NULL, &lpm_data_##inst, \
|
||||
&lpm_cfg_##inst, POST_KERNEL, \
|
||||
CONFIG_DISPLAY_INIT_PRIORITY, &lpm_api);
|
||||
|
||||
DT_INST_FOREACH_STATUS_OKAY(LPM013M126_INIT);
|
||||
29
dts/bindings/display/jdi,lpm013m126.yaml
Normal file
29
dts/bindings/display/jdi,lpm013m126.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2025 Silicon Signals Pvt. Ltd.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
description: JDI LPM013M126 176x176 Color Memory LCD
|
||||
|
||||
compatible: "jdi,lpm013m126"
|
||||
|
||||
include: [spi-device.yaml, display-controller.yaml]
|
||||
|
||||
properties:
|
||||
disp-gpios:
|
||||
type: phandle-array
|
||||
description: |
|
||||
DISPLAY enable pin.
|
||||
Determines whether the LCD shows its memory content or a blank (white) screen.
|
||||
When defined, this pin is driven high during initialization.
|
||||
Blanking operations provided by the API can toggle this pin as needed.
|
||||
|
||||
extcomin-gpios:
|
||||
type: phandle-array
|
||||
description: |
|
||||
EXTCOMIN pin
|
||||
Provides a square wave on the EXTCOMIN pin to toggle VCOM (common electrode voltage)
|
||||
level.
|
||||
|
||||
extcomin-frequency:
|
||||
type: int
|
||||
description: |
|
||||
Frequency (Hz) to toggle the EXTCOMIN pin.
|
||||
Reference in New Issue
Block a user