/** * Copyright (c) 2023 Mr Beam Lasers GmbH. * Copyright (c) 2023 Amrith Venkat Kesavamoorthi * Copyright (c) 2023 Martin Kiepfer * SPDX-License-Identifier: Apache-2.0 */ #define DT_DRV_COMPAT galaxycore_gc9x01x #include "display_gc9x01x.h" #include #include #include #include #include #include #include LOG_MODULE_REGISTER(display_gc9x01x, CONFIG_DISPLAY_LOG_LEVEL); /* Maximum number of default init registers */ #define GC9X01X_NUM_DEFAULT_INIT_REGS 12U /* Display data struct */ struct gc9x01x_data { uint8_t bytes_per_pixel; enum display_pixel_format pixel_format; enum display_orientation orientation; }; /* Configuration data struct.*/ struct gc9x01x_config { const struct device *mipi_dev; struct mipi_dbi_config dbi_config; uint8_t pixel_format; uint16_t orientation; uint16_t x_resolution; uint16_t y_resolution; bool inversion; const void *regs; }; /* Initialization command data struct */ struct gc9x01x_default_init_regs { uint8_t cmd; uint8_t len; uint8_t data[GC9X01X_NUM_DEFAULT_INIT_REGS]; }; /* * Default initialization commands. There are a lot of undocumented commands * within the manufacturer sample code, that are essential for proper operation of * the display controller */ static const struct gc9x01x_default_init_regs default_init_regs[] = { { .cmd = 0xEBU, .len = 1U, .data = {0x14U}, }, { .cmd = 0x84U, .len = 1U, .data = {0x40U}, }, { .cmd = 0x85U, .len = 1U, .data = {0xFFU}, }, { .cmd = 0x86U, .len = 1U, .data = {0xFFU}, }, { .cmd = 0x87U, .len = 1U, .data = {0xFFU}, }, { .cmd = 0x88U, .len = 1U, .data = {0x0AU}, }, { .cmd = 0x89U, .len = 1U, .data = {0x21U}, }, { .cmd = 0x8AU, .len = 1U, .data = {0x00U}, }, { .cmd = 0x8BU, .len = 1U, .data = {0x80U}, }, { .cmd = 0x8CU, .len = 1U, .data = {0x01U}, }, { .cmd = 0x8DU, .len = 1U, .data = {0x01U}, }, { .cmd = 0x8EU, .len = 1U, .data = {0xFFU}, }, { .cmd = 0x8FU, .len = 1U, .data = {0xFFU}, }, { .cmd = 0xB6U, .len = 2U, .data = {0x00U, 0x20U}, }, { .cmd = 0x90U, .len = 4U, .data = {0x08U, 0x08U, 0x08U, 0x08U}, }, { .cmd = 0xBDU, .len = 1U, .data = {0x06U}, }, { .cmd = 0xBCU, .len = 1U, .data = {0x00U}, }, { .cmd = 0xFFU, .len = 3U, .data = {0x60U, 0x01U, 0x04U}, }, { .cmd = 0xBEU, .len = 1U, .data = {0x11U}, }, { .cmd = 0xE1U, .len = 2U, .data = {0x10U, 0x0EU}, }, { .cmd = 0xDFU, .len = 3U, .data = {0x21U, 0x0CU, 0x02U}, }, { .cmd = 0xEDU, .len = 2U, .data = {0x1BU, 0x0BU}, }, { .cmd = 0xAEU, .len = 1U, .data = {0x77U}, }, { .cmd = 0xCDU, .len = 1U, .data = {0x63U}, }, { .cmd = 0x70U, .len = 9U, .data = {0x07U, 0x07U, 0x04U, 0x0EU, 0x0FU, 0x09U, 0x07U, 0x08U, 0x03U}, }, { .cmd = 0x62U, .len = 12U, .data = {0x18U, 0x0DU, 0x71U, 0xEDU, 0x70U, 0x70U, 0x18U, 0x0FU, 0x71U, 0xEFU, 0x70U, 0x70U}, }, { .cmd = 0x63U, .len = 12U, .data = {0x18U, 0x11U, 0x71U, 0xF1U, 0x70U, 0x70U, 0x18U, 0x13U, 0x71U, 0xF3U, 0x70U, 0x70U}, }, { .cmd = 0x64U, .len = 7U, .data = {0x28U, 0x29U, 0xF1U, 0x01U, 0xF1U, 0x00U, 0x07U}, }, { .cmd = 0x66U, .len = 10U, .data = {0x3CU, 0x00U, 0xCDU, 0x67U, 0x45U, 0x45U, 0x10U, 0x00U, 0x00U, 0x00U}, }, { .cmd = 0x67U, .len = 10U, .data = {0x00U, 0x3CU, 0x00U, 0x00U, 0x00U, 0x01U, 0x54U, 0x10U, 0x32U, 0x98U}, }, { .cmd = 0x74U, .len = 7U, .data = {0x10U, 0x85U, 0x80U, 0x00U, 0x00U, 0x4EU, 0x00U}, }, { .cmd = 0x98U, .len = 2U, .data = {0x3EU, 0x07U}, }, }; static int gc9x01x_transmit(const struct device *dev, uint8_t cmd, const void *tx_data, size_t tx_len) { const struct gc9x01x_config *config = dev->config; return mipi_dbi_command_write(config->mipi_dev, &config->dbi_config, cmd, tx_data, tx_len); } static int gc9x01x_regs_init(const struct device *dev) { const struct gc9x01x_config *config = dev->config; const struct gc9x01x_regs *regs = config->regs; int ret; if (!device_is_ready(config->mipi_dev)) { return -ENODEV; } /* Enable inter-command mode */ ret = gc9x01x_transmit(dev, GC9X01X_CMD_INREGEN1, NULL, 0); if (ret < 0) { return ret; } ret = gc9x01x_transmit(dev, GC9X01X_CMD_INREGEN2, NULL, 0); if (ret < 0) { return ret; } /* Apply default init sequence */ for (int i = 0; (i < ARRAY_SIZE(default_init_regs)) && (ret == 0); i++) { ret = gc9x01x_transmit(dev, default_init_regs[i].cmd, default_init_regs[i].data, default_init_regs[i].len); if (ret < 0) { return ret; } } /* Apply generic configuration */ ret = gc9x01x_transmit(dev, GC9X01X_CMD_PWRCTRL2, regs->pwrctrl2, sizeof(regs->pwrctrl2)); if (ret < 0) { return ret; } ret = gc9x01x_transmit(dev, GC9X01X_CMD_PWRCTRL3, regs->pwrctrl3, sizeof(regs->pwrctrl3)); if (ret < 0) { return ret; } ret = gc9x01x_transmit(dev, GC9X01X_CMD_PWRCTRL4, regs->pwrctrl4, sizeof(regs->pwrctrl4)); if (ret < 0) { return ret; } ret = gc9x01x_transmit(dev, GC9X01X_CMD_GAMMA1, regs->gamma1, sizeof(regs->gamma1)); if (ret < 0) { return ret; } ret = gc9x01x_transmit(dev, GC9X01X_CMD_GAMMA2, regs->gamma2, sizeof(regs->gamma2)); if (ret < 0) { return ret; } ret = gc9x01x_transmit(dev, GC9X01X_CMD_GAMMA3, regs->gamma3, sizeof(regs->gamma3)); if (ret < 0) { return ret; } ret = gc9x01x_transmit(dev, GC9X01X_CMD_GAMMA4, regs->gamma4, sizeof(regs->gamma4)); if (ret < 0) { return ret; } ret = gc9x01x_transmit(dev, GC9X01X_CMD_FRAMERATE, regs->framerate, sizeof(regs->framerate)); if (ret < 0) { return ret; } /* Enable Tearing line */ ret = gc9x01x_transmit(dev, GC9X01X_CMD_TEON, NULL, 0); if (ret < 0) { return ret; } return 0; } static int gc9x01x_exit_sleep(const struct device *dev) { int ret; ret = gc9x01x_transmit(dev, GC9X01X_CMD_SLPOUT, NULL, 0); if (ret < 0) { return ret; } /* * Exit sleepmode and enable display. 30ms on top of the sleepout time to account for * any manufacturing defects. * This is to allow time for the supply voltages and clock circuits stabilize */ k_msleep(GC9X01X_SLEEP_IN_OUT_DURATION_MS + 30); return 0; } #ifdef CONFIG_PM_DEVICE static int gc9x01x_enter_sleep(const struct device *dev) { int ret; ret = gc9x01x_transmit(dev, GC9X01X_CMD_SLPIN, NULL, 0); if (ret < 0) { return ret; } /* * Exit sleepmode and enable display. 30ms on top of the sleepout time to account for * any manufacturing defects. */ k_msleep(GC9X01X_SLEEP_IN_OUT_DURATION_MS + 30); return 0; } #endif static int gc9x01x_hw_reset(const struct device *dev) { const struct gc9x01x_config *config = dev->config; int ret; ret = mipi_dbi_reset(config->mipi_dev, 100); if (ret < 0) { return ret; } k_msleep(10); return ret; } static int gc9x01x_display_blanking_off(const struct device *dev) { LOG_DBG("Turning display blanking off"); return gc9x01x_transmit(dev, GC9X01X_CMD_DISPON, NULL, 0); } static int gc9x01x_display_blanking_on(const struct device *dev) { LOG_DBG("Turning display blanking on"); return gc9x01x_transmit(dev, GC9X01X_CMD_DISPOFF, NULL, 0); } static int gc9x01x_set_pixel_format(const struct device *dev, const enum display_pixel_format pixel_format) { struct gc9x01x_data *data = dev->data; int ret; uint8_t tx_data; uint8_t bytes_per_pixel; if (pixel_format == PIXEL_FORMAT_RGB_565) { bytes_per_pixel = 2U; tx_data = GC9X01X_PIXFMT_VAL_MCU_16_BIT | GC9X01X_PIXFMT_VAL_RGB_16_BIT; } else if (pixel_format == PIXEL_FORMAT_RGB_888) { bytes_per_pixel = 3U; tx_data = GC9X01X_PIXFMT_VAL_MCU_18_BIT | GC9X01X_PIXFMT_VAL_RGB_18_BIT; } else { LOG_ERR("Unsupported pixel format"); return -ENOTSUP; } ret = gc9x01x_transmit(dev, GC9X01X_CMD_PIXFMT, &tx_data, 1U); if (ret < 0) { return ret; } data->pixel_format = pixel_format; data->bytes_per_pixel = bytes_per_pixel; return 0; } static int gc9x01x_set_orientation(const struct device *dev, const enum display_orientation orientation) { struct gc9x01x_data *data = dev->data; int ret; uint8_t tx_data = GC9X01X_MADCTL_VAL_BGR; if (orientation == DISPLAY_ORIENTATION_NORMAL) { /* works 0° - default */ } else if (orientation == DISPLAY_ORIENTATION_ROTATED_90) { /* works CW 90° */ tx_data |= GC9X01X_MADCTL_VAL_MV | GC9X01X_MADCTL_VAL_MY; } else if (orientation == DISPLAY_ORIENTATION_ROTATED_180) { /* works CW 180° */ tx_data |= GC9X01X_MADCTL_VAL_MY | GC9X01X_MADCTL_VAL_MX | GC9X01X_MADCTL_VAL_MH; } else if (orientation == DISPLAY_ORIENTATION_ROTATED_270) { /* works CW 270° */ tx_data |= GC9X01X_MADCTL_VAL_MV | GC9X01X_MADCTL_VAL_MX; } ret = gc9x01x_transmit(dev, GC9X01X_CMD_MADCTL, &tx_data, 1U); if (ret < 0) { return ret; } data->orientation = orientation; return 0; } static int gc9x01x_configure(const struct device *dev) { const struct gc9x01x_config *config = dev->config; int ret; /* Set all the required registers. */ ret = gc9x01x_regs_init(dev); if (ret < 0) { return ret; } /* Pixel format */ ret = gc9x01x_set_pixel_format(dev, config->pixel_format); if (ret < 0) { return ret; } /* Orientation */ ret = gc9x01x_set_orientation(dev, config->orientation); if (ret < 0) { return ret; } /* Display inversion mode. */ if (config->inversion) { ret = gc9x01x_transmit(dev, GC9X01X_CMD_INVON, NULL, 0); if (ret < 0) { return ret; } } return 0; } static int gc9x01x_init(const struct device *dev) { int ret; gc9x01x_hw_reset(dev); gc9x01x_display_blanking_on(dev); ret = gc9x01x_configure(dev); if (ret < 0) { LOG_ERR("Could not configure display (%d)", ret); return ret; } ret = gc9x01x_exit_sleep(dev); if (ret < 0) { LOG_ERR("Could not exit sleep mode (%d)", ret); return ret; } return 0; } static int gc9x01x_set_mem_area(const struct device *dev, const uint16_t x, const uint16_t y, const uint16_t w, const uint16_t h) { int ret; uint16_t spi_data[2]; spi_data[0] = sys_cpu_to_be16(x); spi_data[1] = sys_cpu_to_be16(x + w - 1U); ret = gc9x01x_transmit(dev, GC9X01X_CMD_COLSET, &spi_data[0], 4U); if (ret < 0) { return ret; } spi_data[0] = sys_cpu_to_be16(y); spi_data[1] = sys_cpu_to_be16(y + h - 1U); ret = gc9x01x_transmit(dev, GC9X01X_CMD_ROWSET, &spi_data[0], 4U); if (ret < 0) { return ret; } return 0; } static int gc9x01x_write(const struct device *dev, const uint16_t x, const uint16_t y, const struct display_buffer_descriptor *desc, const void *buf) { const struct gc9x01x_config *config = dev->config; struct gc9x01x_data *data = dev->data; int ret; const uint8_t *write_data_start = (const uint8_t *)buf; struct display_buffer_descriptor mipi_desc; uint16_t write_cnt; uint16_t nbr_of_writes; uint16_t write_h; __ASSERT(desc->width <= desc->pitch, "Pitch is smaller than width"); __ASSERT((desc->pitch * data->bytes_per_pixel * desc->height) <= desc->buf_size, "Input buffer to small"); LOG_DBG("Writing %dx%d (w,h) @ %dx%d (x,y)", desc->width, desc->height, x, y); ret = gc9x01x_set_mem_area(dev, x, y, desc->width, desc->height); if (ret < 0) { return ret; } if (desc->pitch > desc->width) { write_h = 1U; nbr_of_writes = desc->height; mipi_desc.height = 1; mipi_desc.buf_size = desc->width * data->bytes_per_pixel; } else { write_h = desc->height; mipi_desc.height = desc->height; mipi_desc.buf_size = desc->width * data->bytes_per_pixel * write_h; nbr_of_writes = 1U; } mipi_desc.width = desc->width; /* Per MIPI API, pitch must always match width */ mipi_desc.pitch = desc->width; mipi_desc.frame_incomplete = desc->frame_incomplete; ret = gc9x01x_transmit(dev, GC9X01X_CMD_MEMWR, NULL, 0); if (ret < 0) { return ret; } for (write_cnt = 0U; write_cnt < nbr_of_writes; ++write_cnt) { ret = mipi_dbi_write_display(config->mipi_dev, &config->dbi_config, write_data_start, &mipi_desc, data->pixel_format); if (ret < 0) { return ret; } write_data_start += desc->pitch * data->bytes_per_pixel; } return 0; } static void gc9x01x_get_capabilities(const struct device *dev, struct display_capabilities *capabilities) { struct gc9x01x_data *data = dev->data; const struct gc9x01x_config *config = dev->config; memset(capabilities, 0, sizeof(struct display_capabilities)); capabilities->supported_pixel_formats = PIXEL_FORMAT_RGB_565 | PIXEL_FORMAT_RGB_888; capabilities->current_pixel_format = data->pixel_format; if (data->orientation == DISPLAY_ORIENTATION_NORMAL || data->orientation == DISPLAY_ORIENTATION_ROTATED_180) { capabilities->x_resolution = config->x_resolution; capabilities->y_resolution = config->y_resolution; } else { capabilities->x_resolution = config->y_resolution; capabilities->y_resolution = config->x_resolution; } capabilities->current_orientation = data->orientation; } #ifdef CONFIG_PM_DEVICE static int gc9x01x_pm_action(const struct device *dev, enum pm_device_action action) { int ret; switch (action) { case PM_DEVICE_ACTION_RESUME: ret = gc9x01x_exit_sleep(dev); break; case PM_DEVICE_ACTION_SUSPEND: ret = gc9x01x_enter_sleep(dev); break; default: ret = -ENOTSUP; break; } return ret; } #endif /* CONFIG_PM_DEVICE */ /* Device driver API*/ static const struct display_driver_api gc9x01x_api = { .blanking_on = gc9x01x_display_blanking_on, .blanking_off = gc9x01x_display_blanking_off, .write = gc9x01x_write, .get_capabilities = gc9x01x_get_capabilities, .set_pixel_format = gc9x01x_set_pixel_format, .set_orientation = gc9x01x_set_orientation, }; #define GC9X01X_INIT(inst) \ GC9X01X_REGS_INIT(inst); \ static const struct gc9x01x_config gc9x01x_config_##inst = { \ .mipi_dev = DEVICE_DT_GET(DT_INST_PARENT(inst)), \ .dbi_config = { \ .mode = MIPI_DBI_MODE_SPI_4WIRE, \ .config = MIPI_DBI_SPI_CONFIG_DT_INST(inst, \ SPI_OP_MODE_MASTER | \ SPI_WORD_SET(8), 0), \ }, \ .pixel_format = DT_INST_PROP(inst, pixel_format), \ .orientation = DT_INST_ENUM_IDX(inst, orientation), \ .x_resolution = DT_INST_PROP(inst, width), \ .y_resolution = DT_INST_PROP(inst, height), \ .inversion = DT_INST_PROP(inst, display_inversion), \ .regs = &gc9x01x_regs_##inst, \ }; \ static struct gc9x01x_data gc9x01x_data_##inst; \ PM_DEVICE_DT_INST_DEFINE(inst, gc9x01x_pm_action); \ DEVICE_DT_INST_DEFINE(inst, &gc9x01x_init, PM_DEVICE_DT_INST_GET(inst), \ &gc9x01x_data_##inst, &gc9x01x_config_##inst, POST_KERNEL, \ CONFIG_DISPLAY_INIT_PRIORITY, &gc9x01x_api); DT_INST_FOREACH_STATUS_OKAY(GC9X01X_INIT)