| /* |
| * Copyright (c) 2020 PHYTEC Messtechnik GmbH |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define DT_DRV_COMPAT gooddisplay_gd7965 |
| |
| #include <string.h> |
| #include <device.h> |
| #include <init.h> |
| #include <drivers/display.h> |
| #include <drivers/gpio.h> |
| #include <drivers/spi.h> |
| #include <sys/byteorder.h> |
| |
| #include "gd7965_regs.h" |
| |
| #include <logging/log.h> |
| LOG_MODULE_REGISTER(gd7965, CONFIG_DISPLAY_LOG_LEVEL); |
| |
| /** |
| * GD7965 compatible EPD controller driver. |
| * |
| * Currently only the black/white pannels are supported (KW mode), |
| * also first gate/source should be 0. |
| */ |
| |
| #define GD7965_SPI_FREQ DT_INST_PROP(0, spi_max_frequency) |
| #define GD7965_BUS_NAME DT_INST_BUS_LABEL(0) |
| #define GD7965_DC_PIN DT_INST_GPIO_PIN(0, dc_gpios) |
| #define GD7965_DC_FLAGS DT_INST_GPIO_FLAGS(0, dc_gpios) |
| #define GD7965_DC_CNTRL DT_INST_GPIO_LABEL(0, dc_gpios) |
| #define GD7965_CS_PIN DT_INST_SPI_DEV_CS_GPIOS_PIN(0) |
| #define GD7965_CS_FLAGS DT_INST_SPI_DEV_CS_GPIOS_FLAGS(0) |
| #if DT_INST_SPI_DEV_HAS_CS_GPIOS(0) |
| #define GD7965_CS_CNTRL DT_INST_SPI_DEV_CS_GPIOS_LABEL(0) |
| #endif |
| #define GD7965_BUSY_PIN DT_INST_GPIO_PIN(0, busy_gpios) |
| #define GD7965_BUSY_CNTRL DT_INST_GPIO_LABEL(0, busy_gpios) |
| #define GD7965_BUSY_FLAGS DT_INST_GPIO_FLAGS(0, busy_gpios) |
| #define GD7965_RESET_PIN DT_INST_GPIO_PIN(0, reset_gpios) |
| #define GD7965_RESET_CNTRL DT_INST_GPIO_LABEL(0, reset_gpios) |
| #define GD7965_RESET_FLAGS DT_INST_GPIO_FLAGS(0, reset_gpios) |
| |
| #define EPD_PANEL_WIDTH DT_INST_PROP(0, width) |
| #define EPD_PANEL_HEIGHT DT_INST_PROP(0, height) |
| #define GD7965_PIXELS_PER_BYTE 8U |
| |
| /* Horizontally aligned page! */ |
| #define GD7965_NUMOF_PAGES (EPD_PANEL_WIDTH / \ |
| GD7965_PIXELS_PER_BYTE) |
| #define GD7965_PANEL_FIRST_GATE 0U |
| #define GD7965_PANEL_LAST_GATE (EPD_PANEL_HEIGHT - 1) |
| #define GD7965_PANEL_FIRST_PAGE 0U |
| #define GD7965_PANEL_LAST_PAGE (GD7965_NUMOF_PAGES - 1) |
| |
| |
| struct gd7965_data { |
| const struct device *reset; |
| const struct device *dc; |
| const struct device *busy; |
| const struct device *spi_dev; |
| struct spi_config spi_config; |
| #if defined(GD7965_CS_CNTRL) |
| struct spi_cs_control cs_ctrl; |
| #endif |
| }; |
| |
| static uint8_t gd7965_softstart[] = DT_INST_PROP(0, softstart); |
| static uint8_t gd7965_pwr[] = DT_INST_PROP(0, pwr); |
| |
| /* Border and data polarity settings */ |
| static uint8_t bdd_polarity; |
| |
| static bool blanking_on = true; |
| |
| static inline int gd7965_write_cmd(struct gd7965_data *driver, |
| uint8_t cmd, uint8_t *data, size_t len) |
| { |
| struct spi_buf buf = {.buf = &cmd, .len = sizeof(cmd)}; |
| struct spi_buf_set buf_set = {.buffers = &buf, .count = 1}; |
| |
| gpio_pin_set(driver->dc, GD7965_DC_PIN, 1); |
| if (spi_write(driver->spi_dev, &driver->spi_config, &buf_set)) { |
| return -EIO; |
| } |
| |
| if (data != NULL) { |
| buf.buf = data; |
| buf.len = len; |
| gpio_pin_set(driver->dc, GD7965_DC_PIN, 0); |
| if (spi_write(driver->spi_dev, &driver->spi_config, &buf_set)) { |
| return -EIO; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static inline void gd7965_busy_wait(struct gd7965_data *driver) |
| { |
| int pin = gpio_pin_get(driver->busy, GD7965_BUSY_PIN); |
| |
| while (pin > 0) { |
| __ASSERT(pin >= 0, "Failed to get pin level"); |
| LOG_DBG("wait %u", pin); |
| k_sleep(K_MSEC(GD7965_BUSY_DELAY)); |
| pin = gpio_pin_get(driver->busy, GD7965_BUSY_PIN); |
| } |
| } |
| |
| static int gd7965_update_display(const struct device *dev) |
| { |
| struct gd7965_data *driver = dev->data; |
| |
| LOG_DBG("Trigger update sequence"); |
| if (gd7965_write_cmd(driver, GD7965_CMD_DRF, NULL, 0)) { |
| return -EIO; |
| } |
| |
| k_sleep(K_MSEC(GD7965_BUSY_DELAY)); |
| |
| return 0; |
| } |
| |
| static int gd7965_blanking_off(const struct device *dev) |
| { |
| struct gd7965_data *driver = dev->data; |
| |
| if (blanking_on) { |
| /* Update EPD pannel in normal mode */ |
| gd7965_busy_wait(driver); |
| if (gd7965_update_display(dev)) { |
| return -EIO; |
| } |
| } |
| |
| blanking_on = false; |
| |
| return 0; |
| } |
| |
| static int gd7965_blanking_on(const struct device *dev) |
| { |
| blanking_on = true; |
| |
| return 0; |
| } |
| |
| static int gd7965_write(const struct device *dev, const uint16_t x, const uint16_t y, |
| const struct display_buffer_descriptor *desc, |
| const void *buf) |
| { |
| struct gd7965_data *driver = dev->data; |
| uint16_t x_end_idx = x + desc->width - 1; |
| uint16_t y_end_idx = y + desc->height - 1; |
| uint8_t ptl[GD7965_PTL_REG_LENGTH] = {0}; |
| size_t buf_len; |
| |
| LOG_DBG("x %u, y %u, height %u, width %u, pitch %u", |
| x, y, desc->height, desc->width, desc->pitch); |
| |
| buf_len = MIN(desc->buf_size, |
| desc->height * desc->width / GD7965_PIXELS_PER_BYTE); |
| __ASSERT(desc->width <= desc->pitch, "Pitch is smaller then width"); |
| __ASSERT(buf != NULL, "Buffer is not available"); |
| __ASSERT(buf_len != 0U, "Buffer of length zero"); |
| __ASSERT(!(desc->width % GD7965_PIXELS_PER_BYTE), |
| "Buffer width not multiple of %d", GD7965_PIXELS_PER_BYTE); |
| |
| if ((y_end_idx > (EPD_PANEL_HEIGHT - 1)) || |
| (x_end_idx > (EPD_PANEL_WIDTH - 1))) { |
| LOG_ERR("Position out of bounds"); |
| return -EINVAL; |
| } |
| |
| /* Setup Partial Window and enable Partial Mode */ |
| sys_put_be16(x, &ptl[GD7965_PTL_HRST_IDX]); |
| sys_put_be16(x_end_idx, &ptl[GD7965_PTL_HRED_IDX]); |
| sys_put_be16(y, &ptl[GD7965_PTL_VRST_IDX]); |
| sys_put_be16(y_end_idx, &ptl[GD7965_PTL_VRED_IDX]); |
| ptl[sizeof(ptl) - 1] = GD7965_PTL_PT_SCAN; |
| LOG_HEXDUMP_DBG(ptl, sizeof(ptl), "ptl"); |
| |
| gd7965_busy_wait(driver); |
| if (gd7965_write_cmd(driver, GD7965_CMD_PTIN, NULL, 0)) { |
| return -EIO; |
| } |
| |
| if (gd7965_write_cmd(driver, GD7965_CMD_PTL, ptl, sizeof(ptl))) { |
| return -EIO; |
| } |
| |
| /* Disable boarder output */ |
| bdd_polarity |= GD7965_CDI_BDZ; |
| if (gd7965_write_cmd(driver, GD7965_CMD_CDI, |
| &bdd_polarity, sizeof(bdd_polarity))) { |
| return -EIO; |
| } |
| |
| if (gd7965_write_cmd(driver, GD7965_CMD_DTM2, (uint8_t *)buf, buf_len)) { |
| return -EIO; |
| } |
| |
| /* Update partial window and disable Partial Mode */ |
| if (blanking_on == false) { |
| if (gd7965_update_display(dev)) { |
| return -EIO; |
| } |
| } |
| |
| /* Enable boarder output */ |
| bdd_polarity &= ~GD7965_CDI_BDZ; |
| if (gd7965_write_cmd(driver, GD7965_CMD_CDI, |
| &bdd_polarity, sizeof(bdd_polarity))) { |
| return -EIO; |
| } |
| |
| if (gd7965_write_cmd(driver, GD7965_CMD_PTOUT, NULL, 0)) { |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| static int gd7965_read(const struct device *dev, const uint16_t x, const uint16_t y, |
| const struct display_buffer_descriptor *desc, void *buf) |
| { |
| LOG_ERR("not supported"); |
| return -ENOTSUP; |
| } |
| |
| static void *gd7965_get_framebuffer(const struct device *dev) |
| { |
| LOG_ERR("not supported"); |
| return NULL; |
| } |
| |
| static int gd7965_set_brightness(const struct device *dev, |
| const uint8_t brightness) |
| { |
| LOG_WRN("not supported"); |
| return -ENOTSUP; |
| } |
| |
| static int gd7965_set_contrast(const struct device *dev, uint8_t contrast) |
| { |
| LOG_WRN("not supported"); |
| return -ENOTSUP; |
| } |
| |
| static void gd7965_get_capabilities(const struct device *dev, |
| struct display_capabilities *caps) |
| { |
| memset(caps, 0, sizeof(struct display_capabilities)); |
| caps->x_resolution = EPD_PANEL_WIDTH; |
| caps->y_resolution = EPD_PANEL_HEIGHT; |
| 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; |
| } |
| |
| static int gd7965_set_orientation(const struct device *dev, |
| const enum display_orientation |
| orientation) |
| { |
| LOG_ERR("Unsupported"); |
| return -ENOTSUP; |
| } |
| |
| static int gd7965_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 gd7965_clear_and_write_buffer(const struct device *dev, |
| uint8_t pattern, bool update) |
| { |
| struct display_buffer_descriptor desc = { |
| .buf_size = GD7965_NUMOF_PAGES, |
| .width = EPD_PANEL_WIDTH, |
| .height = 1, |
| .pitch = EPD_PANEL_WIDTH, |
| }; |
| uint8_t *line; |
| |
| line = k_malloc(GD7965_NUMOF_PAGES); |
| if (line == NULL) { |
| return -ENOMEM; |
| } |
| |
| memset(line, pattern, GD7965_NUMOF_PAGES); |
| for (int i = 0; i < EPD_PANEL_HEIGHT; i++) { |
| gd7965_write(dev, 0, i, &desc, line); |
| } |
| |
| k_free(line); |
| |
| if (update == true) { |
| if (gd7965_update_display(dev)) { |
| return -EIO; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int gd7965_controller_init(const struct device *dev) |
| { |
| struct gd7965_data *driver = dev->data; |
| uint8_t tmp[GD7965_TRES_REG_LENGTH]; |
| |
| gpio_pin_set(driver->reset, GD7965_RESET_PIN, 1); |
| k_sleep(K_MSEC(GD7965_RESET_DELAY)); |
| gpio_pin_set(driver->reset, GD7965_RESET_PIN, 0); |
| k_sleep(K_MSEC(GD7965_RESET_DELAY)); |
| gd7965_busy_wait(driver); |
| |
| LOG_DBG("Initialize GD7965 controller"); |
| |
| if (gd7965_write_cmd(driver, GD7965_CMD_PWR, gd7965_pwr, |
| sizeof(gd7965_pwr))) { |
| return -EIO; |
| } |
| |
| if (gd7965_write_cmd(driver, GD7965_CMD_BTST, |
| gd7965_softstart, sizeof(gd7965_softstart))) { |
| return -EIO; |
| } |
| |
| /* Turn on: booster, controller, regulators, and sensor. */ |
| if (gd7965_write_cmd(driver, GD7965_CMD_PON, NULL, 0)) { |
| return -EIO; |
| } |
| |
| k_sleep(K_MSEC(GD7965_PON_DELAY)); |
| gd7965_busy_wait(driver); |
| |
| /* Pannel settings, KW mode */ |
| tmp[0] = GD7965_PSR_KW_R | |
| GD7965_PSR_UD | |
| GD7965_PSR_SHL | |
| GD7965_PSR_SHD | |
| GD7965_PSR_RST; |
| if (gd7965_write_cmd(driver, GD7965_CMD_PSR, tmp, 1)) { |
| return -EIO; |
| } |
| |
| /* Set panel resolution */ |
| sys_put_be16(EPD_PANEL_WIDTH, &tmp[GD7965_TRES_HRES_IDX]); |
| sys_put_be16(EPD_PANEL_HEIGHT, &tmp[GD7965_TRES_VRES_IDX]); |
| LOG_HEXDUMP_DBG(tmp, sizeof(tmp), "TRES"); |
| if (gd7965_write_cmd(driver, GD7965_CMD_TRES, |
| tmp, GD7965_TRES_REG_LENGTH)) { |
| return -EIO; |
| } |
| |
| bdd_polarity = GD7965_CDI_BDV1 | |
| GD7965_CDI_N2OCP | GD7965_CDI_DDX0; |
| tmp[GD7965_CDI_BDZ_DDX_IDX] = bdd_polarity; |
| tmp[GD7965_CDI_CDI_IDX] = DT_INST_PROP(0, cdi); |
| LOG_HEXDUMP_DBG(tmp, GD7965_CDI_REG_LENGTH, "CDI"); |
| if (gd7965_write_cmd(driver, GD7965_CMD_CDI, tmp, |
| GD7965_CDI_REG_LENGTH)) { |
| return -EIO; |
| } |
| |
| tmp[0] = DT_INST_PROP(0, tcon); |
| if (gd7965_write_cmd(driver, GD7965_CMD_TCON, tmp, 1)) { |
| return -EIO; |
| } |
| |
| /* Enable Auto Sequence */ |
| tmp[0] = GD7965_AUTO_PON_DRF_POF; |
| if (gd7965_write_cmd(driver, GD7965_CMD_AUTO, tmp, 1)) { |
| return -EIO; |
| } |
| |
| if (gd7965_clear_and_write_buffer(dev, 0xff, false)) { |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| static int gd7965_init(const struct device *dev) |
| { |
| struct gd7965_data *driver = dev->data; |
| |
| LOG_DBG(""); |
| |
| driver->spi_dev = device_get_binding(GD7965_BUS_NAME); |
| if (driver->spi_dev == NULL) { |
| LOG_ERR("Could not get SPI device for GD7965"); |
| return -EIO; |
| } |
| |
| driver->spi_config.frequency = GD7965_SPI_FREQ; |
| driver->spi_config.operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8); |
| driver->spi_config.slave = DT_INST_REG_ADDR(0); |
| driver->spi_config.cs = NULL; |
| |
| driver->reset = device_get_binding(GD7965_RESET_CNTRL); |
| if (driver->reset == NULL) { |
| LOG_ERR("Could not get GPIO port for GD7965 reset"); |
| return -EIO; |
| } |
| |
| gpio_pin_configure(driver->reset, GD7965_RESET_PIN, |
| GPIO_OUTPUT_INACTIVE | GD7965_RESET_FLAGS); |
| |
| driver->dc = device_get_binding(GD7965_DC_CNTRL); |
| if (driver->dc == NULL) { |
| LOG_ERR("Could not get GPIO port for GD7965 DC signal"); |
| return -EIO; |
| } |
| |
| gpio_pin_configure(driver->dc, GD7965_DC_PIN, |
| GPIO_OUTPUT_INACTIVE | GD7965_DC_FLAGS); |
| |
| driver->busy = device_get_binding(GD7965_BUSY_CNTRL); |
| if (driver->busy == NULL) { |
| LOG_ERR("Could not get GPIO port for GD7965 busy signal"); |
| return -EIO; |
| } |
| |
| gpio_pin_configure(driver->busy, GD7965_BUSY_PIN, |
| GPIO_INPUT | GD7965_BUSY_FLAGS); |
| |
| #if defined(GD7965_CS_CNTRL) |
| driver->cs_ctrl.gpio_dev = device_get_binding(GD7965_CS_CNTRL); |
| if (!driver->cs_ctrl.gpio_dev) { |
| LOG_ERR("Unable to get SPI GPIO CS device"); |
| return -EIO; |
| } |
| |
| driver->cs_ctrl.gpio_pin = GD7965_CS_PIN; |
| driver->cs_ctrl.gpio_dt_flags = GD7965_CS_FLAGS; |
| driver->cs_ctrl.delay = 0U; |
| driver->spi_config.cs = &driver->cs_ctrl; |
| #endif |
| |
| return gd7965_controller_init(dev); |
| } |
| |
| static struct gd7965_data gd7965_driver; |
| |
| static struct display_driver_api gd7965_driver_api = { |
| .blanking_on = gd7965_blanking_on, |
| .blanking_off = gd7965_blanking_off, |
| .write = gd7965_write, |
| .read = gd7965_read, |
| .get_framebuffer = gd7965_get_framebuffer, |
| .set_brightness = gd7965_set_brightness, |
| .set_contrast = gd7965_set_contrast, |
| .get_capabilities = gd7965_get_capabilities, |
| .set_pixel_format = gd7965_set_pixel_format, |
| .set_orientation = gd7965_set_orientation, |
| }; |
| |
| |
| DEVICE_DT_INST_DEFINE(0, gd7965_init, device_pm_control_nop, |
| &gd7965_driver, NULL, |
| POST_KERNEL, CONFIG_APPLICATION_INIT_PRIORITY, |
| &gd7965_driver_api); |