|  | /* | 
|  | * 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); |