| /* |
| * Copyright (c) 2022 Andreas Sandberg |
| * Copyright (c) 2018-2020 PHYTEC Messtechnik GmbH |
| * Copyright 2024 NXP |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define LOG_LEVEL CONFIG_DISPLAY_LOG_LEVEL |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_REGISTER(ssd16xx); |
| |
| #include <string.h> |
| #include <zephyr/device.h> |
| #include <zephyr/drivers/display.h> |
| #include <zephyr/init.h> |
| #include <zephyr/drivers/gpio.h> |
| #include <zephyr/drivers/mipi_dbi.h> |
| #include <zephyr/sys/byteorder.h> |
| |
| #include <zephyr/display/ssd16xx.h> |
| #include "ssd16xx_regs.h" |
| |
| /** |
| * SSD16xx compatible EPD controller driver. |
| */ |
| |
| #define EPD_PANEL_NUMOF_ROWS_PER_PAGE 8 |
| #define SSD16XX_PANEL_FIRST_PAGE 0 |
| #define SSD16XX_PANEL_FIRST_GATE 0 |
| #define SSD16XX_PIXELS_PER_BYTE 8 |
| #define SSD16XX_DEFAULT_TR_VALUE 25U |
| #define SSD16XX_TR_SCALE_FACTOR 256U |
| |
| |
| enum ssd16xx_profile_type { |
| SSD16XX_PROFILE_FULL = 0, |
| SSD16XX_PROFILE_PARTIAL, |
| SSD16XX_NUM_PROFILES, |
| SSD16XX_PROFILE_INVALID = SSD16XX_NUM_PROFILES, |
| }; |
| |
| struct ssd16xx_quirks { |
| /* Gates */ |
| uint16_t max_width; |
| /* Sources */ |
| uint16_t max_height; |
| /* Width (bits) of integer type representing an x coordinate */ |
| uint8_t pp_width_bits; |
| /* Width (bits) of integer type representing a y coordinate */ |
| uint8_t pp_height_bits; |
| |
| /* |
| * Device specific flags to be included in |
| * SSD16XX_CMD_UPDATE_CTRL2 for a full refresh. |
| */ |
| uint8_t ctrl2_full; |
| /* |
| * Device specific flags to be included in |
| * SSD16XX_CMD_UPDATE_CTRL2 for a partial refresh. |
| */ |
| uint8_t ctrl2_partial; |
| }; |
| |
| struct ssd16xx_data { |
| bool read_supported; |
| uint8_t scan_mode; |
| bool blanking_on; |
| enum ssd16xx_profile_type profile; |
| enum display_orientation orientation; |
| }; |
| |
| struct ssd16xx_dt_array { |
| uint8_t *data; |
| uint8_t len; |
| }; |
| |
| struct ssd16xx_profile { |
| struct ssd16xx_dt_array lut; |
| struct ssd16xx_dt_array gdv; |
| struct ssd16xx_dt_array sdv; |
| uint8_t vcom; |
| uint8_t bwf; |
| uint8_t dummy_line; |
| uint8_t gate_line_width; |
| |
| bool override_vcom; |
| bool override_bwf; |
| bool override_dummy_line; |
| bool override_gate_line_width; |
| }; |
| |
| struct ssd16xx_config { |
| const struct device *mipi_dev; |
| const struct mipi_dbi_config dbi_config; |
| struct gpio_dt_spec busy_gpio; |
| |
| const struct ssd16xx_quirks *quirks; |
| |
| struct ssd16xx_dt_array softstart; |
| |
| const struct ssd16xx_profile *profiles[SSD16XX_NUM_PROFILES]; |
| |
| uint16_t rotation; |
| uint16_t height; |
| uint16_t width; |
| uint8_t tssv; |
| }; |
| |
| static int ssd16xx_set_profile(const struct device *dev, |
| enum ssd16xx_profile_type type); |
| |
| static inline void ssd16xx_busy_wait(const struct device *dev) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| int pin = gpio_pin_get_dt(&config->busy_gpio); |
| |
| while (pin > 0) { |
| __ASSERT(pin >= 0, "Failed to get pin level"); |
| k_msleep(SSD16XX_BUSY_DELAY); |
| pin = gpio_pin_get_dt(&config->busy_gpio); |
| } |
| } |
| |
| static inline int ssd16xx_write_cmd(const struct device *dev, uint8_t cmd, |
| const uint8_t *data, size_t len) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| int err; |
| |
| ssd16xx_busy_wait(dev); |
| |
| err = mipi_dbi_command_write(config->mipi_dev, &config->dbi_config, |
| cmd, data, len); |
| mipi_dbi_release(config->mipi_dev, &config->dbi_config); |
| return err; |
| } |
| |
| static inline int ssd16xx_write_uint8(const struct device *dev, uint8_t cmd, |
| uint8_t data) |
| { |
| return ssd16xx_write_cmd(dev, cmd, &data, 1); |
| } |
| |
| static inline int ssd16xx_read_cmd(const struct device *dev, uint8_t cmd, |
| uint8_t *data, size_t len) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| const struct ssd16xx_data *dev_data = dev->data; |
| |
| if (!dev_data->read_supported) { |
| return -ENOTSUP; |
| } |
| |
| ssd16xx_busy_wait(dev); |
| |
| return mipi_dbi_command_read(config->mipi_dev, &config->dbi_config, |
| &cmd, 1, data, len); |
| } |
| |
| static inline size_t push_x_param(const struct device *dev, |
| uint8_t *data, uint16_t x) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| |
| if (config->quirks->pp_width_bits == 8) { |
| data[0] = (uint8_t)x; |
| return 1; |
| } |
| |
| if (config->quirks->pp_width_bits == 16) { |
| sys_put_le16(sys_cpu_to_le16(x), data); |
| return 2; |
| } |
| |
| LOG_ERR("Unsupported pp_width_bits %u", |
| config->quirks->pp_width_bits); |
| return 0; |
| } |
| |
| static inline size_t push_y_param(const struct device *dev, |
| uint8_t *data, uint16_t y) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| |
| if (config->quirks->pp_height_bits == 8) { |
| data[0] = (uint8_t)y; |
| return 1; |
| } |
| |
| if (config->quirks->pp_height_bits == 16) { |
| sys_put_le16(sys_cpu_to_le16(y), data); |
| return 2; |
| } |
| |
| LOG_ERR("Unsupported pp_height_bitsa %u", |
| config->quirks->pp_height_bits); |
| return 0; |
| } |
| |
| |
| |
| static inline int ssd16xx_set_ram_param(const struct device *dev, |
| uint16_t sx, uint16_t ex, |
| uint16_t sy, uint16_t ey) |
| { |
| int err; |
| uint8_t tmp[4]; |
| size_t len; |
| |
| len = push_x_param(dev, tmp, sx); |
| len += push_x_param(dev, tmp + len, ex); |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_RAM_XPOS_CTRL, tmp, len); |
| if (err < 0) { |
| return err; |
| } |
| |
| len = push_y_param(dev, tmp, sy); |
| len += push_y_param(dev, tmp + len, ey); |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_RAM_YPOS_CTRL, tmp, len); |
| if (err < 0) { |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| static inline int ssd16xx_set_ram_ptr(const struct device *dev, uint16_t x, |
| uint16_t y) |
| { |
| int err; |
| uint8_t tmp[2]; |
| size_t len; |
| |
| len = push_x_param(dev, tmp, x); |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_RAM_XPOS_CNTR, tmp, len); |
| if (err < 0) { |
| return err; |
| } |
| |
| len = push_y_param(dev, tmp, y); |
| return ssd16xx_write_cmd(dev, SSD16XX_CMD_RAM_YPOS_CNTR, tmp, len); |
| } |
| |
| static int ssd16xx_activate(const struct device *dev, uint8_t ctrl2) |
| { |
| int err; |
| |
| err = ssd16xx_write_uint8(dev, SSD16XX_CMD_UPDATE_CTRL2, ctrl2); |
| if (err < 0) { |
| return err; |
| } |
| |
| return ssd16xx_write_cmd(dev, SSD16XX_CMD_MASTER_ACTIVATION, NULL, 0); |
| } |
| |
| static int ssd16xx_update_display(const struct device *dev) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| const struct ssd16xx_data *data = dev->data; |
| const struct ssd16xx_profile *p = config->profiles[data->profile]; |
| const struct ssd16xx_quirks *quirks = config->quirks; |
| const bool load_lut = !p || p->lut.len == 0; |
| const bool load_temp = load_lut && config->tssv; |
| const bool partial = data->profile == SSD16XX_PROFILE_PARTIAL; |
| const uint8_t update_cmd = |
| SSD16XX_CTRL2_ENABLE_CLK | |
| SSD16XX_CTRL2_ENABLE_ANALOG | |
| (load_lut ? SSD16XX_CTRL2_LOAD_LUT : 0) | |
| (load_temp ? SSD16XX_CTRL2_LOAD_TEMPERATURE : 0) | |
| (partial ? quirks->ctrl2_partial : quirks->ctrl2_full) | |
| SSD16XX_CTRL2_DISABLE_ANALOG | |
| SSD16XX_CTRL2_DISABLE_CLK; |
| |
| return ssd16xx_activate(dev, update_cmd); |
| } |
| |
| static int ssd16xx_blanking_off(const struct device *dev) |
| { |
| struct ssd16xx_data *data = dev->data; |
| |
| if (data->blanking_on) { |
| data->blanking_on = false; |
| return ssd16xx_update_display(dev); |
| } |
| |
| return 0; |
| } |
| |
| static int ssd16xx_blanking_on(const struct device *dev) |
| { |
| struct ssd16xx_data *data = dev->data; |
| |
| if (!data->blanking_on) { |
| if (ssd16xx_set_profile(dev, SSD16XX_PROFILE_FULL)) { |
| return -EIO; |
| } |
| } |
| |
| data->blanking_on = true; |
| |
| return 0; |
| } |
| |
| static int ssd16xx_set_window(const struct device *dev, |
| const uint16_t x, const uint16_t y, |
| const struct display_buffer_descriptor *desc) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| const struct ssd16xx_data *data = dev->data; |
| int err; |
| uint16_t x_start; |
| uint16_t x_end; |
| uint16_t y_start; |
| uint16_t y_end; |
| uint16_t panel_h = config->height - |
| config->height % EPD_PANEL_NUMOF_ROWS_PER_PAGE; |
| |
| if (desc->pitch < desc->width) { |
| LOG_ERR("Pitch is smaller than width"); |
| return -EINVAL; |
| } |
| |
| if (desc->pitch > desc->width) { |
| LOG_ERR("Unsupported mode"); |
| return -ENOTSUP; |
| } |
| |
| if (data->orientation == DISPLAY_ORIENTATION_NORMAL || |
| data->orientation == DISPLAY_ORIENTATION_ROTATED_180) { |
| if ((y + desc->height) > panel_h) { |
| LOG_ERR("Buffer out of bounds (height)"); |
| return -EINVAL; |
| } |
| |
| if ((x + desc->width) > config->width) { |
| LOG_ERR("Buffer out of bounds (width)"); |
| return -EINVAL; |
| } |
| |
| if ((desc->height % EPD_PANEL_NUMOF_ROWS_PER_PAGE) != 0U) { |
| LOG_ERR("Buffer height not multiple of %d", EPD_PANEL_NUMOF_ROWS_PER_PAGE); |
| return -EINVAL; |
| } |
| |
| if ((y % EPD_PANEL_NUMOF_ROWS_PER_PAGE) != 0U) { |
| LOG_ERR("Y coordinate not multiple of %d", EPD_PANEL_NUMOF_ROWS_PER_PAGE); |
| return -EINVAL; |
| } |
| } else { |
| if ((y + desc->height) > config->width) { |
| LOG_ERR("Buffer out of bounds (height)"); |
| return -EINVAL; |
| } |
| |
| if ((x + desc->width) > panel_h) { |
| LOG_ERR("Buffer out of bounds (width)"); |
| return -EINVAL; |
| } |
| |
| if ((desc->width % SSD16XX_PIXELS_PER_BYTE) != 0U) { |
| LOG_ERR("Buffer width not multiple of %d", SSD16XX_PIXELS_PER_BYTE); |
| return -EINVAL; |
| } |
| |
| if ((x % SSD16XX_PIXELS_PER_BYTE) != 0U) { |
| LOG_ERR("X coordinate not multiple of %d", SSD16XX_PIXELS_PER_BYTE); |
| return -EINVAL; |
| } |
| } |
| |
| switch (data->orientation) { |
| case DISPLAY_ORIENTATION_NORMAL: |
| x_start = (panel_h - 1 - y) / SSD16XX_PIXELS_PER_BYTE; |
| x_end = (panel_h - 1 - (y + desc->height - 1)) / SSD16XX_PIXELS_PER_BYTE; |
| y_start = x; |
| y_end = (x + desc->width - 1); |
| break; |
| case DISPLAY_ORIENTATION_ROTATED_90: |
| x_start = (panel_h - 1 - x) / SSD16XX_PIXELS_PER_BYTE; |
| x_end = (panel_h - 1 - (x + desc->width - 1)) / SSD16XX_PIXELS_PER_BYTE; |
| y_start = (config->width - 1 - y); |
| y_end = (config->width - 1 - (y + desc->height - 1)); |
| break; |
| case DISPLAY_ORIENTATION_ROTATED_180: |
| x_start = y / SSD16XX_PIXELS_PER_BYTE; |
| x_end = (y + desc->height - 1) / SSD16XX_PIXELS_PER_BYTE; |
| y_start = (x + desc->width - 1); |
| y_end = x; |
| break; |
| case DISPLAY_ORIENTATION_ROTATED_270: |
| x_start = x / SSD16XX_PIXELS_PER_BYTE; |
| x_end = (x + desc->width - 1) / SSD16XX_PIXELS_PER_BYTE; |
| y_start = y; |
| y_end = (y + desc->height - 1); |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| err = ssd16xx_set_ram_param(dev, x_start, x_end, y_start, y_end); |
| if (err < 0) { |
| return err; |
| } |
| |
| err = ssd16xx_set_ram_ptr(dev, x_start, y_start); |
| if (err < 0) { |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| static int ssd16xx_write(const struct device *dev, const uint16_t x, |
| const uint16_t y, |
| const struct display_buffer_descriptor *desc, |
| const void *buf) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| const struct ssd16xx_data *data = dev->data; |
| const bool have_partial_refresh = |
| config->profiles[SSD16XX_PROFILE_PARTIAL] != NULL; |
| const bool partial_refresh = !data->blanking_on && have_partial_refresh; |
| const size_t buf_len = MIN(desc->buf_size, |
| desc->height * desc->width / 8); |
| int err; |
| |
| if (buf == NULL || buf_len == 0U) { |
| LOG_ERR("Display buffer is not available"); |
| return -EINVAL; |
| } |
| |
| if (partial_refresh) { |
| /* |
| * Request the partial profile. This operation becomes |
| * a no-op if the profile is already active. |
| */ |
| err = ssd16xx_set_profile(dev, SSD16XX_PROFILE_PARTIAL); |
| if (err < 0) { |
| return -EIO; |
| } |
| } |
| |
| err = ssd16xx_set_window(dev, x, y, desc); |
| if (err < 0) { |
| return err; |
| } |
| |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_WRITE_RAM, (uint8_t *)buf, |
| buf_len); |
| if (err < 0) { |
| return err; |
| } |
| |
| if (!data->blanking_on) { |
| err = ssd16xx_update_display(dev); |
| if (err < 0) { |
| return err; |
| } |
| } |
| |
| if (data->blanking_on && have_partial_refresh) { |
| /* |
| * We will trigger a full refresh when blanking is |
| * turned off. The controller won't keep track of the |
| * old frame buffer, which is needed to perform a |
| * partial update, when this happens. Maintain the old |
| * frame buffer manually here to make sure future |
| * partial updates will work as expected. |
| */ |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_WRITE_RED_RAM, |
| (uint8_t *)buf, buf_len); |
| if (err < 0) { |
| return err; |
| } |
| } else if (partial_refresh) { |
| /* |
| * We just performed a partial refresh. After the |
| * refresh, the controller swaps the black/red buffers |
| * containing the current and new image. We need to |
| * perform a second write here to ensure that future |
| * updates work on an up-to-date framebuffer. |
| */ |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_WRITE_RAM, |
| (uint8_t *)buf, buf_len); |
| if (err < 0) { |
| return err; |
| } |
| } |
| |
| return 0; |
| } |
| |
| int ssd16xx_read_ram(const struct device *dev, enum ssd16xx_ram ram_type, |
| const uint16_t x, const uint16_t y, |
| const struct display_buffer_descriptor *desc, |
| void *buf) |
| { |
| const struct ssd16xx_data *data = dev->data; |
| const size_t buf_len = MIN(desc->buf_size, |
| desc->height * desc->width / 8); |
| int err; |
| uint8_t ram_ctrl; |
| |
| if (!data->read_supported) { |
| return -ENOTSUP; |
| } |
| |
| switch (ram_type) { |
| case SSD16XX_RAM_BLACK: |
| ram_ctrl = SSD16XX_RAM_READ_CTRL_BLACK; |
| break; |
| |
| case SSD16XX_RAM_RED: |
| ram_ctrl = SSD16XX_RAM_READ_CTRL_RED; |
| break; |
| |
| default: |
| return -EINVAL; |
| } |
| |
| if (buf == NULL || buf_len == 0U) { |
| LOG_ERR("Display buffer is not available"); |
| return -EINVAL; |
| } |
| |
| err = ssd16xx_set_window(dev, x, y, desc); |
| if (err < 0) { |
| return err; |
| } |
| |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_RAM_READ_CTRL, |
| &ram_ctrl, sizeof(ram_ctrl)); |
| if (err < 0) { |
| return err; |
| } |
| |
| err = ssd16xx_read_cmd(dev, SSD16XX_CMD_READ_RAM, (uint8_t *)buf, |
| buf_len); |
| if (err < 0) { |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| static int ssd16xx_read(const struct device *dev, |
| const uint16_t x, const uint16_t y, |
| const struct display_buffer_descriptor *desc, |
| void *buf) |
| { |
| return ssd16xx_read_ram(dev, SSD16XX_RAM_BLACK, x, y, desc, buf); |
| } |
| |
| static void ssd16xx_get_capabilities(const struct device *dev, |
| struct display_capabilities *caps) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| struct ssd16xx_data *data = dev->data; |
| |
| memset(caps, 0, sizeof(struct display_capabilities)); |
| caps->x_resolution = config->width; |
| caps->y_resolution = config->height - |
| config->height % EPD_PANEL_NUMOF_ROWS_PER_PAGE; |
| caps->supported_pixel_formats = PIXEL_FORMAT_MONO10; |
| caps->current_pixel_format = PIXEL_FORMAT_MONO10; |
| caps->screen_info = SCREEN_INFO_MONO_MSB_FIRST | SCREEN_INFO_EPD; |
| |
| if (data->orientation == DISPLAY_ORIENTATION_NORMAL || |
| data->orientation == DISPLAY_ORIENTATION_ROTATED_180) { |
| caps->screen_info |= SCREEN_INFO_MONO_VTILED; |
| } |
| |
| caps->current_orientation = data->orientation; |
| } |
| |
| static int ssd16xx_set_pixel_format(const struct device *dev, |
| const enum display_pixel_format pf) |
| { |
| if (pf == PIXEL_FORMAT_MONO10) { |
| return 0; |
| } |
| |
| LOG_ERR("not supported"); |
| return -ENOTSUP; |
| } |
| |
| static int ssd16xx_set_orientation(const struct device *dev, |
| const enum display_orientation orientation) |
| { |
| struct ssd16xx_data *data = dev->data; |
| int err; |
| |
| if (orientation == DISPLAY_ORIENTATION_NORMAL) { |
| data->scan_mode = SSD16XX_DATA_ENTRY_XDYIY; |
| } else if (orientation == DISPLAY_ORIENTATION_ROTATED_90) { |
| data->scan_mode = SSD16XX_DATA_ENTRY_XDYDX; |
| } else if (orientation == DISPLAY_ORIENTATION_ROTATED_180) { |
| data->scan_mode = SSD16XX_DATA_ENTRY_XIYDY; |
| } else if (orientation == DISPLAY_ORIENTATION_ROTATED_270) { |
| data->scan_mode = SSD16XX_DATA_ENTRY_XIYIX; |
| } |
| |
| err = ssd16xx_write_uint8(dev, SSD16XX_CMD_ENTRY_MODE, data->scan_mode); |
| if (err < 0) { |
| return err; |
| } |
| |
| data->orientation = orientation; |
| |
| return 0; |
| } |
| |
| static int ssd16xx_clear_cntlr_mem(const struct device *dev, uint8_t ram_cmd) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| uint16_t panel_h = config->height / EPD_PANEL_NUMOF_ROWS_PER_PAGE; |
| uint16_t last_gate = config->width - 1; |
| uint8_t clear_page[64]; |
| int err; |
| |
| /* |
| * Clear unusable memory area when the resolution of the panel is not |
| * multiple of an octet. |
| */ |
| if (config->height % EPD_PANEL_NUMOF_ROWS_PER_PAGE) { |
| panel_h += 1; |
| } |
| |
| err = ssd16xx_write_uint8(dev, SSD16XX_CMD_ENTRY_MODE, |
| SSD16XX_DATA_ENTRY_XIYDY); |
| if (err < 0) { |
| return err; |
| } |
| |
| err = ssd16xx_set_ram_param(dev, SSD16XX_PANEL_FIRST_PAGE, |
| panel_h - 1, last_gate, |
| SSD16XX_PANEL_FIRST_GATE); |
| if (err < 0) { |
| return err; |
| } |
| |
| err = ssd16xx_set_ram_ptr(dev, SSD16XX_PANEL_FIRST_PAGE, last_gate); |
| if (err < 0) { |
| return err; |
| } |
| |
| memset(clear_page, 0xff, sizeof(clear_page)); |
| for (int h = 0; h < panel_h; h++) { |
| size_t x = config->width; |
| |
| while (x) { |
| size_t l = MIN(x, sizeof(clear_page)); |
| |
| x -= l; |
| err = ssd16xx_write_cmd(dev, ram_cmd, clear_page, l); |
| if (err < 0) { |
| return err; |
| } |
| } |
| } |
| |
| return 0; |
| } |
| |
| static inline int ssd16xx_load_ws_from_otp_tssv(const struct device *dev) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| |
| /* |
| * Controller has an integrated temperature sensor or external |
| * temperature sensor is connected to the controller. |
| */ |
| LOG_INF("Select and load WS from OTP"); |
| return ssd16xx_write_uint8(dev, SSD16XX_CMD_TSENSOR_SELECTION, |
| config->tssv); |
| } |
| |
| static inline int ssd16xx_load_ws_from_otp(const struct device *dev) |
| { |
| int16_t t = (SSD16XX_DEFAULT_TR_VALUE * SSD16XX_TR_SCALE_FACTOR); |
| uint8_t tmp[2]; |
| int err; |
| |
| LOG_INF("Load default WS (25 degrees Celsius) from OTP"); |
| |
| err = ssd16xx_activate(dev, SSD16XX_CTRL2_ENABLE_CLK); |
| if (err < 0) { |
| return err; |
| } |
| |
| /* Load temperature value */ |
| sys_put_be16(t, tmp); |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_TSENS_CTRL, tmp, 2); |
| if (err < 0) { |
| return err; |
| } |
| |
| err = ssd16xx_activate(dev, SSD16XX_CTRL2_DISABLE_CLK); |
| if (err < 0) { |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| |
| static int ssd16xx_load_lut(const struct device *dev, |
| const struct ssd16xx_dt_array *lut) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| |
| if (lut && lut->len) { |
| LOG_DBG("Using user-provided LUT"); |
| return ssd16xx_write_cmd(dev, SSD16XX_CMD_UPDATE_LUT, |
| lut->data, lut->len); |
| } else { |
| if (config->tssv) { |
| return ssd16xx_load_ws_from_otp_tssv(dev); |
| } else { |
| return ssd16xx_load_ws_from_otp(dev); |
| } |
| } |
| } |
| |
| static int ssd16xx_set_profile(const struct device *dev, |
| enum ssd16xx_profile_type type) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| struct ssd16xx_data *data = dev->data; |
| const struct ssd16xx_profile *p; |
| const uint16_t last_gate = config->width - 1; |
| uint8_t gdo[3]; |
| size_t gdo_len; |
| int err = 0; |
| |
| if (type >= SSD16XX_NUM_PROFILES) { |
| return -EINVAL; |
| } |
| |
| p = config->profiles[type]; |
| |
| /* |
| * The full profile is the only one that always exists. If it |
| * hasn't been specified, we use the defaults. |
| */ |
| if (!p && type != SSD16XX_PROFILE_FULL) { |
| return -ENOENT; |
| } |
| |
| if (type == data->profile) { |
| return 0; |
| } |
| |
| /* |
| * Perform a soft reset to make sure registers are reset. This |
| * will leave the RAM contents intact. |
| */ |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_SW_RESET, NULL, 0); |
| if (err < 0) { |
| return err; |
| } |
| |
| gdo_len = push_y_param(dev, gdo, last_gate); |
| gdo[gdo_len++] = 0U; |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_GDO_CTRL, gdo, gdo_len); |
| if (err < 0) { |
| return err; |
| } |
| |
| if (config->softstart.len) { |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_SOFTSTART, |
| config->softstart.data, |
| config->softstart.len); |
| if (err < 0) { |
| return err; |
| } |
| } |
| |
| err = ssd16xx_load_lut(dev, p ? &p->lut : NULL); |
| if (err < 0) { |
| return err; |
| } |
| |
| if (p && p->override_dummy_line) { |
| err = ssd16xx_write_uint8(dev, SSD16XX_CMD_DUMMY_LINE, |
| p->dummy_line); |
| if (err < 0) { |
| return err; |
| } |
| } |
| |
| if (p && p->override_gate_line_width) { |
| err = ssd16xx_write_uint8(dev, SSD16XX_CMD_GATE_LINE_WIDTH, |
| p->override_gate_line_width); |
| if (err < 0) { |
| return err; |
| } |
| } |
| |
| if (p && p->gdv.len) { |
| LOG_DBG("Setting GDV"); |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_GDV_CTRL, |
| p->gdv.data, p->gdv.len); |
| if (err < 0) { |
| return err; |
| } |
| } |
| |
| if (p && p->sdv.len) { |
| LOG_DBG("Setting SDV"); |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_SDV_CTRL, |
| p->sdv.data, p->sdv.len); |
| if (err < 0) { |
| return err; |
| } |
| } |
| |
| if (p && p->override_vcom) { |
| LOG_DBG("Setting VCOM"); |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_VCOM_VOLTAGE, |
| &p->vcom, 1); |
| if (err < 0) { |
| return err; |
| } |
| } |
| |
| if (p && p->override_bwf) { |
| LOG_DBG("Setting BWF"); |
| err = ssd16xx_write_cmd(dev, SSD16XX_CMD_BWF_CTRL, |
| &p->bwf, 1); |
| if (err < 0) { |
| return err; |
| } |
| } |
| |
| err = ssd16xx_write_uint8(dev, SSD16XX_CMD_ENTRY_MODE, data->scan_mode); |
| if (err < 0) { |
| return err; |
| } |
| |
| data->profile = type; |
| |
| return 0; |
| } |
| |
| static int ssd16xx_controller_init(const struct device *dev) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| struct ssd16xx_data *data = dev->data; |
| enum display_orientation orientation; |
| int err; |
| |
| LOG_DBG(""); |
| |
| data->blanking_on = false; |
| data->profile = SSD16XX_PROFILE_INVALID; |
| |
| err = mipi_dbi_reset(config->mipi_dev, SSD16XX_RESET_DELAY); |
| if (err < 0) { |
| return err; |
| } |
| |
| k_msleep(SSD16XX_RESET_DELAY); |
| |
| err = ssd16xx_set_profile(dev, SSD16XX_PROFILE_FULL); |
| if (err < 0) { |
| return err; |
| } |
| |
| err = ssd16xx_clear_cntlr_mem(dev, SSD16XX_CMD_WRITE_RAM); |
| if (err < 0) { |
| return err; |
| } |
| |
| err = ssd16xx_clear_cntlr_mem(dev, SSD16XX_CMD_WRITE_RED_RAM); |
| if (err < 0) { |
| return err; |
| } |
| |
| if (config->rotation == 0U) { |
| orientation = DISPLAY_ORIENTATION_NORMAL; |
| } else if (config->rotation == 90U) { |
| orientation = DISPLAY_ORIENTATION_ROTATED_90; |
| } else if (config->rotation == 180U) { |
| orientation = DISPLAY_ORIENTATION_ROTATED_180; |
| } else { |
| orientation = DISPLAY_ORIENTATION_ROTATED_270; |
| } |
| |
| err = ssd16xx_set_orientation(dev, orientation); |
| if (err < 0) { |
| return err; |
| } |
| |
| err = ssd16xx_update_display(dev); |
| if (err < 0) { |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| static int ssd16xx_init(const struct device *dev) |
| { |
| const struct ssd16xx_config *config = dev->config; |
| struct ssd16xx_data *data = dev->data; |
| int err; |
| |
| LOG_DBG(""); |
| |
| if (!device_is_ready(config->mipi_dev)) { |
| LOG_ERR("MIPI Device not ready"); |
| return -ENODEV; |
| } |
| |
| data->read_supported = |
| (config->dbi_config.config.operation & SPI_HALF_DUPLEX) != 0; |
| |
| if (!gpio_is_ready_dt(&config->busy_gpio)) { |
| LOG_ERR("Busy GPIO device not ready"); |
| return -ENODEV; |
| } |
| |
| err = gpio_pin_configure_dt(&config->busy_gpio, GPIO_INPUT); |
| if (err < 0) { |
| LOG_ERR("Failed to configure busy GPIO"); |
| return err; |
| } |
| |
| if (config->width > config->quirks->max_width || |
| config->height > config->quirks->max_height) { |
| LOG_ERR("Display size out of range."); |
| return -EINVAL; |
| } |
| |
| return ssd16xx_controller_init(dev); |
| } |
| |
| static const struct display_driver_api ssd16xx_driver_api = { |
| .blanking_on = ssd16xx_blanking_on, |
| .blanking_off = ssd16xx_blanking_off, |
| .write = ssd16xx_write, |
| .read = ssd16xx_read, |
| .get_capabilities = ssd16xx_get_capabilities, |
| .set_pixel_format = ssd16xx_set_pixel_format, |
| .set_orientation = ssd16xx_set_orientation, |
| }; |
| |
| #if DT_HAS_COMPAT_STATUS_OKAY(solomon_ssd1608) |
| static struct ssd16xx_quirks quirks_solomon_ssd1608 = { |
| .max_width = 320, |
| .max_height = 240, |
| .pp_width_bits = 16, |
| .pp_height_bits = 16, |
| .ctrl2_full = SSD16XX_GEN1_CTRL2_TO_PATTERN, |
| .ctrl2_partial = SSD16XX_GEN1_CTRL2_TO_PATTERN, |
| }; |
| #endif |
| |
| #if DT_HAS_COMPAT_STATUS_OKAY(solomon_ssd1673) |
| static struct ssd16xx_quirks quirks_solomon_ssd1673 = { |
| .max_width = 250, |
| .max_height = 150, |
| .pp_width_bits = 8, |
| .pp_height_bits = 8, |
| .ctrl2_full = SSD16XX_GEN1_CTRL2_TO_PATTERN, |
| .ctrl2_partial = SSD16XX_GEN1_CTRL2_TO_PATTERN, |
| }; |
| #endif |
| |
| #if DT_HAS_COMPAT_STATUS_OKAY(solomon_ssd1675a) |
| static struct ssd16xx_quirks quirks_solomon_ssd1675a = { |
| .max_width = 296, |
| .max_height = 160, |
| .pp_width_bits = 8, |
| .pp_height_bits = 16, |
| .ctrl2_full = SSD16XX_GEN1_CTRL2_TO_PATTERN, |
| .ctrl2_partial = SSD16XX_GEN1_CTRL2_TO_PATTERN, |
| }; |
| #endif |
| |
| #if DT_HAS_COMPAT_STATUS_OKAY(solomon_ssd1680) |
| static const struct ssd16xx_quirks quirks_solomon_ssd1680 = { |
| .max_width = 296, |
| .max_height = 176, |
| .pp_width_bits = 8, |
| .pp_height_bits = 16, |
| .ctrl2_full = SSD16XX_GEN2_CTRL2_DISPLAY, |
| .ctrl2_partial = SSD16XX_GEN2_CTRL2_DISPLAY | SSD16XX_GEN2_CTRL2_MODE2, |
| }; |
| #endif |
| |
| #if DT_HAS_COMPAT_STATUS_OKAY(solomon_ssd1681) |
| static struct ssd16xx_quirks quirks_solomon_ssd1681 = { |
| .max_width = 200, |
| .max_height = 200, |
| .pp_width_bits = 8, |
| .pp_height_bits = 16, |
| .ctrl2_full = SSD16XX_GEN2_CTRL2_DISPLAY, |
| .ctrl2_partial = SSD16XX_GEN2_CTRL2_DISPLAY | SSD16XX_GEN2_CTRL2_MODE2, |
| }; |
| #endif |
| |
| #define SOFTSTART_ASSIGN(n) \ |
| .softstart = { \ |
| .data = softstart_##n, \ |
| .len = sizeof(softstart_##n), \ |
| }, |
| |
| #define SSD16XX_MAKE_ARRAY_OPT(n, p) \ |
| static uint8_t data_ ## n ## _ ## p[] = DT_PROP_OR(n, p, {}) |
| |
| #define SSD16XX_ASSIGN_ARRAY(n, p) \ |
| { \ |
| .data = data_ ## n ## _ ## p, \ |
| .len = sizeof(data_ ## n ## _ ## p), \ |
| } |
| |
| #define SSD16XX_PROFILE(n) \ |
| SSD16XX_MAKE_ARRAY_OPT(n, lut); \ |
| SSD16XX_MAKE_ARRAY_OPT(n, gdv); \ |
| SSD16XX_MAKE_ARRAY_OPT(n, sdv); \ |
| \ |
| static const struct ssd16xx_profile ssd16xx_profile_ ## n = { \ |
| .lut = SSD16XX_ASSIGN_ARRAY(n, lut), \ |
| .gdv = SSD16XX_ASSIGN_ARRAY(n, gdv), \ |
| .sdv = SSD16XX_ASSIGN_ARRAY(n, sdv), \ |
| .vcom = DT_PROP_OR(n, vcom, 0), \ |
| .override_vcom = DT_NODE_HAS_PROP(n, vcom), \ |
| .bwf = DT_PROP_OR(n, border_waveform, 0), \ |
| .override_bwf = DT_NODE_HAS_PROP(n, border_waveform), \ |
| .dummy_line = DT_PROP_OR(n, dummy_line, 0), \ |
| .override_dummy_line = DT_NODE_HAS_PROP(n, dummy_line), \ |
| .gate_line_width = DT_PROP_OR(n, gate_line_width, 0), \ |
| .override_gate_line_width = DT_NODE_HAS_PROP( \ |
| n, gate_line_width), \ |
| }; |
| |
| |
| #define _SSD16XX_PROFILE_PTR(n) &ssd16xx_profile_ ## n |
| |
| #define SSD16XX_PROFILE_PTR(n) \ |
| COND_CODE_1(DT_NODE_EXISTS(n), \ |
| (_SSD16XX_PROFILE_PTR(n)), \ |
| NULL) |
| |
| #define SSD16XX_DEFINE(n, quirks_ptr) \ |
| SSD16XX_MAKE_ARRAY_OPT(n, softstart); \ |
| \ |
| DT_FOREACH_CHILD(n, SSD16XX_PROFILE); \ |
| \ |
| static const struct ssd16xx_config ssd16xx_cfg_ ## n = { \ |
| .mipi_dev = DEVICE_DT_GET(DT_PARENT(n)), \ |
| .dbi_config = { \ |
| .mode = MIPI_DBI_MODE_SPI_4WIRE, \ |
| .config = MIPI_DBI_SPI_CONFIG_DT(n, \ |
| SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | \ |
| SPI_HOLD_ON_CS | SPI_LOCK_ON, 0), \ |
| }, \ |
| .busy_gpio = GPIO_DT_SPEC_GET(n, busy_gpios), \ |
| .quirks = quirks_ptr, \ |
| .height = DT_PROP(n, height), \ |
| .width = DT_PROP(n, width), \ |
| .rotation = DT_PROP(n, rotation), \ |
| .tssv = DT_PROP_OR(n, tssv, 0), \ |
| .softstart = SSD16XX_ASSIGN_ARRAY(n, softstart), \ |
| .profiles = { \ |
| [SSD16XX_PROFILE_FULL] = \ |
| SSD16XX_PROFILE_PTR(DT_CHILD(n, full)), \ |
| [SSD16XX_PROFILE_PARTIAL] = \ |
| SSD16XX_PROFILE_PTR(DT_CHILD(n, partial)),\ |
| }, \ |
| }; \ |
| \ |
| static struct ssd16xx_data ssd16xx_data_ ## n; \ |
| \ |
| DEVICE_DT_DEFINE(n, \ |
| ssd16xx_init, NULL, \ |
| &ssd16xx_data_ ## n, \ |
| &ssd16xx_cfg_ ## n, \ |
| POST_KERNEL, \ |
| CONFIG_DISPLAY_INIT_PRIORITY, \ |
| &ssd16xx_driver_api) |
| |
| DT_FOREACH_STATUS_OKAY_VARGS(solomon_ssd1608, SSD16XX_DEFINE, |
| &quirks_solomon_ssd1608); |
| DT_FOREACH_STATUS_OKAY_VARGS(solomon_ssd1673, SSD16XX_DEFINE, |
| &quirks_solomon_ssd1673); |
| DT_FOREACH_STATUS_OKAY_VARGS(solomon_ssd1675a, SSD16XX_DEFINE, |
| &quirks_solomon_ssd1675a); |
| DT_FOREACH_STATUS_OKAY_VARGS(solomon_ssd1680, SSD16XX_DEFINE, |
| &quirks_solomon_ssd1680); |
| DT_FOREACH_STATUS_OKAY_VARGS(solomon_ssd1681, SSD16XX_DEFINE, |
| &quirks_solomon_ssd1681); |