blob: fd231b702432ba364136b5e5742e0bfa84da7d07 [file] [log] [blame]
/*
* 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), \
.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);