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