diff options
Diffstat (limited to 'drivers/gpu/drm/tinydrm')
-rw-r--r-- | drivers/gpu/drm/tinydrm/Kconfig | 75 | ||||
-rw-r--r-- | drivers/gpu/drm/tinydrm/Makefile | 12 | ||||
-rw-r--r-- | drivers/gpu/drm/tinydrm/core/Makefile | 3 | ||||
-rw-r--r-- | drivers/gpu/drm/tinydrm/core/tinydrm-core.c | 267 | ||||
-rw-r--r-- | drivers/gpu/drm/tinydrm/core/tinydrm-helpers.c | 449 | ||||
-rw-r--r-- | drivers/gpu/drm/tinydrm/core/tinydrm-pipe.c | 207 | ||||
-rw-r--r-- | drivers/gpu/drm/tinydrm/ili9225.c | 454 | ||||
-rw-r--r-- | drivers/gpu/drm/tinydrm/ili9341.c | 232 | ||||
-rw-r--r-- | drivers/gpu/drm/tinydrm/mi0283qt.c | 267 | ||||
-rw-r--r-- | drivers/gpu/drm/tinydrm/mipi-dbi.c | 1099 | ||||
-rw-r--r-- | drivers/gpu/drm/tinydrm/repaper.c | 1099 | ||||
-rw-r--r-- | drivers/gpu/drm/tinydrm/st7586.c | 400 | ||||
-rw-r--r-- | drivers/gpu/drm/tinydrm/st7735r.c | 211 |
13 files changed, 4775 insertions, 0 deletions
diff --git a/drivers/gpu/drm/tinydrm/Kconfig b/drivers/gpu/drm/tinydrm/Kconfig new file mode 100644 index 000000000..16f4b5c91 --- /dev/null +++ b/drivers/gpu/drm/tinydrm/Kconfig @@ -0,0 +1,75 @@ +menuconfig DRM_TINYDRM + tristate "Support for simple displays" + depends on DRM + select DRM_KMS_HELPER + select DRM_KMS_CMA_HELPER + help + Choose this option if you have a tinydrm supported display. + If M is selected the module will be called tinydrm. + +config TINYDRM_MIPI_DBI + tristate + +config TINYDRM_ILI9225 + tristate "DRM support for ILI9225 display panels" + depends on DRM_TINYDRM && SPI + select TINYDRM_MIPI_DBI + help + DRM driver for the following Ilitek ILI9225 panels: + * No-name 2.2" color screen module + + If M is selected the module will be called ili9225. + +config TINYDRM_ILI9341 + tristate "DRM support for ILI9341 display panels" + depends on DRM_TINYDRM && SPI + depends on BACKLIGHT_CLASS_DEVICE + select TINYDRM_MIPI_DBI + help + DRM driver for the following Ilitek ILI9341 panels: + * YX240QV29-T 2.4" 240x320 TFT (Adafruit 2.4") + + If M is selected the module will be called ili9341. + +config TINYDRM_MI0283QT + tristate "DRM support for MI0283QT" + depends on DRM_TINYDRM && SPI + depends on BACKLIGHT_CLASS_DEVICE + select TINYDRM_MIPI_DBI + help + DRM driver for the Multi-Inno MI0283QT display panel + If M is selected the module will be called mi0283qt. + +config TINYDRM_REPAPER + tristate "DRM support for Pervasive Displays RePaper panels (V231)" + depends on DRM_TINYDRM && SPI + depends on THERMAL || !THERMAL + help + DRM driver for the following Pervasive Displays panels: + 1.44" TFT EPD Panel (E1144CS021) + 1.90" TFT EPD Panel (E1190CS021) + 2.00" TFT EPD Panel (E2200CS021) + 2.71" TFT EPD Panel (E2271CS021) + + If M is selected the module will be called repaper. + +config TINYDRM_ST7586 + tristate "DRM support for Sitronix ST7586 display panels" + depends on DRM_TINYDRM && SPI + select TINYDRM_MIPI_DBI + help + DRM driver for the following Sitronix ST7586 panels: + * LEGO MINDSTORMS EV3 + + If M is selected the module will be called st7586. + +config TINYDRM_ST7735R + tristate "DRM support for Sitronix ST7735R display panels" + depends on DRM_TINYDRM && SPI + depends on BACKLIGHT_CLASS_DEVICE + select TINYDRM_MIPI_DBI + help + DRM driver Sitronix ST7735R with one of the following LCDs: + * JD-T18003-T01 1.8" 128x160 TFT + + If M is selected the module will be called st7735r. diff --git a/drivers/gpu/drm/tinydrm/Makefile b/drivers/gpu/drm/tinydrm/Makefile new file mode 100644 index 000000000..14d990806 --- /dev/null +++ b/drivers/gpu/drm/tinydrm/Makefile @@ -0,0 +1,12 @@ +obj-$(CONFIG_DRM_TINYDRM) += core/ + +# Controllers +obj-$(CONFIG_TINYDRM_MIPI_DBI) += mipi-dbi.o + +# Displays +obj-$(CONFIG_TINYDRM_ILI9225) += ili9225.o +obj-$(CONFIG_TINYDRM_ILI9341) += ili9341.o +obj-$(CONFIG_TINYDRM_MI0283QT) += mi0283qt.o +obj-$(CONFIG_TINYDRM_REPAPER) += repaper.o +obj-$(CONFIG_TINYDRM_ST7586) += st7586.o +obj-$(CONFIG_TINYDRM_ST7735R) += st7735r.o diff --git a/drivers/gpu/drm/tinydrm/core/Makefile b/drivers/gpu/drm/tinydrm/core/Makefile new file mode 100644 index 000000000..fb221e6f8 --- /dev/null +++ b/drivers/gpu/drm/tinydrm/core/Makefile @@ -0,0 +1,3 @@ +tinydrm-y := tinydrm-core.o tinydrm-pipe.o tinydrm-helpers.o + +obj-$(CONFIG_DRM_TINYDRM) += tinydrm.o diff --git a/drivers/gpu/drm/tinydrm/core/tinydrm-core.c b/drivers/gpu/drm/tinydrm/core/tinydrm-core.c new file mode 100644 index 000000000..19c7f70ad --- /dev/null +++ b/drivers/gpu/drm/tinydrm/core/tinydrm-core.c @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2016 Noralf Trønnes + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include <drm/drm_atomic.h> +#include <drm/drm_atomic_helper.h> +#include <drm/drm_crtc_helper.h> +#include <drm/drm_fb_helper.h> +#include <drm/drm_gem_framebuffer_helper.h> +#include <drm/tinydrm/tinydrm.h> +#include <linux/device.h> +#include <linux/dma-buf.h> + +/** + * DOC: overview + * + * This library provides driver helpers for very simple display hardware. + * + * It is based on &drm_simple_display_pipe coupled with a &drm_connector which + * has only one fixed &drm_display_mode. The framebuffers are backed by the + * cma helper and have support for framebuffer flushing (dirty). + * fbdev support is also included. + * + */ + +/** + * DOC: core + * + * The driver allocates &tinydrm_device, initializes it using + * devm_tinydrm_init(), sets up the pipeline using tinydrm_display_pipe_init() + * and registers the DRM device using devm_tinydrm_register(). + */ + +/** + * tinydrm_gem_cma_prime_import_sg_table - Produce a CMA GEM object from + * another driver's scatter/gather table of pinned pages + * @drm: DRM device to import into + * @attach: DMA-BUF attachment + * @sgt: Scatter/gather table of pinned pages + * + * This function imports a scatter/gather table exported via DMA-BUF by + * another driver using drm_gem_cma_prime_import_sg_table(). It sets the + * kernel virtual address on the CMA object. Drivers should use this as their + * &drm_driver->gem_prime_import_sg_table callback if they need the virtual + * address. tinydrm_gem_cma_free_object() should be used in combination with + * this function. + * + * Returns: + * A pointer to a newly created GEM object or an ERR_PTR-encoded negative + * error code on failure. + */ +struct drm_gem_object * +tinydrm_gem_cma_prime_import_sg_table(struct drm_device *drm, + struct dma_buf_attachment *attach, + struct sg_table *sgt) +{ + struct drm_gem_cma_object *cma_obj; + struct drm_gem_object *obj; + void *vaddr; + + vaddr = dma_buf_vmap(attach->dmabuf); + if (!vaddr) { + DRM_ERROR("Failed to vmap PRIME buffer\n"); + return ERR_PTR(-ENOMEM); + } + + obj = drm_gem_cma_prime_import_sg_table(drm, attach, sgt); + if (IS_ERR(obj)) { + dma_buf_vunmap(attach->dmabuf, vaddr); + return obj; + } + + cma_obj = to_drm_gem_cma_obj(obj); + cma_obj->vaddr = vaddr; + + return obj; +} +EXPORT_SYMBOL(tinydrm_gem_cma_prime_import_sg_table); + +/** + * tinydrm_gem_cma_free_object - Free resources associated with a CMA GEM + * object + * @gem_obj: GEM object to free + * + * This function frees the backing memory of the CMA GEM object, cleans up the + * GEM object state and frees the memory used to store the object itself using + * drm_gem_cma_free_object(). It also handles PRIME buffers which has the kernel + * virtual address set by tinydrm_gem_cma_prime_import_sg_table(). Drivers + * can use this as their &drm_driver->gem_free_object_unlocked callback. + */ +void tinydrm_gem_cma_free_object(struct drm_gem_object *gem_obj) +{ + if (gem_obj->import_attach) { + struct drm_gem_cma_object *cma_obj; + + cma_obj = to_drm_gem_cma_obj(gem_obj); + dma_buf_vunmap(gem_obj->import_attach->dmabuf, cma_obj->vaddr); + cma_obj->vaddr = NULL; + } + + drm_gem_cma_free_object(gem_obj); +} +EXPORT_SYMBOL_GPL(tinydrm_gem_cma_free_object); + +static struct drm_framebuffer * +tinydrm_fb_create(struct drm_device *drm, struct drm_file *file_priv, + const struct drm_mode_fb_cmd2 *mode_cmd) +{ + struct tinydrm_device *tdev = drm->dev_private; + + return drm_gem_fb_create_with_funcs(drm, file_priv, mode_cmd, + tdev->fb_funcs); +} + +static const struct drm_mode_config_funcs tinydrm_mode_config_funcs = { + .fb_create = tinydrm_fb_create, + .atomic_check = drm_atomic_helper_check, + .atomic_commit = drm_atomic_helper_commit, +}; + +static int tinydrm_init(struct device *parent, struct tinydrm_device *tdev, + const struct drm_framebuffer_funcs *fb_funcs, + struct drm_driver *driver) +{ + struct drm_device *drm; + + mutex_init(&tdev->dirty_lock); + tdev->fb_funcs = fb_funcs; + + /* + * We don't embed drm_device, because that prevent us from using + * devm_kzalloc() to allocate tinydrm_device in the driver since + * drm_dev_unref() frees the structure. The devm_ functions provide + * for easy error handling. + */ + drm = drm_dev_alloc(driver, parent); + if (IS_ERR(drm)) + return PTR_ERR(drm); + + tdev->drm = drm; + drm->dev_private = tdev; + drm_mode_config_init(drm); + drm->mode_config.funcs = &tinydrm_mode_config_funcs; + + return 0; +} + +static void tinydrm_fini(struct tinydrm_device *tdev) +{ + drm_mode_config_cleanup(tdev->drm); + mutex_destroy(&tdev->dirty_lock); + tdev->drm->dev_private = NULL; + drm_dev_unref(tdev->drm); +} + +static void devm_tinydrm_release(void *data) +{ + tinydrm_fini(data); +} + +/** + * devm_tinydrm_init - Initialize tinydrm device + * @parent: Parent device object + * @tdev: tinydrm device + * @fb_funcs: Framebuffer functions + * @driver: DRM driver + * + * This function initializes @tdev, the underlying DRM device and it's + * mode_config. Resources will be automatically freed on driver detach (devres) + * using drm_mode_config_cleanup() and drm_dev_unref(). + * + * Returns: + * Zero on success, negative error code on failure. + */ +int devm_tinydrm_init(struct device *parent, struct tinydrm_device *tdev, + const struct drm_framebuffer_funcs *fb_funcs, + struct drm_driver *driver) +{ + int ret; + + ret = tinydrm_init(parent, tdev, fb_funcs, driver); + if (ret) + return ret; + + ret = devm_add_action(parent, devm_tinydrm_release, tdev); + if (ret) + tinydrm_fini(tdev); + + return ret; +} +EXPORT_SYMBOL(devm_tinydrm_init); + +static int tinydrm_register(struct tinydrm_device *tdev) +{ + struct drm_device *drm = tdev->drm; + int ret; + + ret = drm_dev_register(tdev->drm, 0); + if (ret) + return ret; + + ret = drm_fbdev_generic_setup(drm, 0); + if (ret) + DRM_ERROR("Failed to initialize fbdev: %d\n", ret); + + return 0; +} + +static void tinydrm_unregister(struct tinydrm_device *tdev) +{ + drm_atomic_helper_shutdown(tdev->drm); + drm_dev_unregister(tdev->drm); +} + +static void devm_tinydrm_register_release(void *data) +{ + tinydrm_unregister(data); +} + +/** + * devm_tinydrm_register - Register tinydrm device + * @tdev: tinydrm device + * + * This function registers the underlying DRM device and fbdev. + * These resources will be automatically unregistered on driver detach (devres) + * and the display pipeline will be disabled. + * + * Returns: + * Zero on success, negative error code on failure. + */ +int devm_tinydrm_register(struct tinydrm_device *tdev) +{ + struct device *dev = tdev->drm->dev; + int ret; + + ret = tinydrm_register(tdev); + if (ret) + return ret; + + ret = devm_add_action(dev, devm_tinydrm_register_release, tdev); + if (ret) + tinydrm_unregister(tdev); + + return ret; +} +EXPORT_SYMBOL(devm_tinydrm_register); + +/** + * tinydrm_shutdown - Shutdown tinydrm + * @tdev: tinydrm device + * + * This function makes sure that the display pipeline is disabled. + * Used by drivers in their shutdown callback to turn off the display + * on machine shutdown and reboot. + */ +void tinydrm_shutdown(struct tinydrm_device *tdev) +{ + drm_atomic_helper_shutdown(tdev->drm); +} +EXPORT_SYMBOL(tinydrm_shutdown); + +MODULE_LICENSE("GPL"); diff --git a/drivers/gpu/drm/tinydrm/core/tinydrm-helpers.c b/drivers/gpu/drm/tinydrm/core/tinydrm-helpers.c new file mode 100644 index 000000000..dcd390163 --- /dev/null +++ b/drivers/gpu/drm/tinydrm/core/tinydrm-helpers.c @@ -0,0 +1,449 @@ +/* + * Copyright (C) 2016 Noralf Trønnes + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include <linux/backlight.h> +#include <linux/dma-buf.h> +#include <linux/pm.h> +#include <linux/spi/spi.h> +#include <linux/swab.h> + +#include <drm/tinydrm/tinydrm.h> +#include <drm/tinydrm/tinydrm-helpers.h> + +static unsigned int spi_max; +module_param(spi_max, uint, 0400); +MODULE_PARM_DESC(spi_max, "Set a lower SPI max transfer size"); + +/** + * tinydrm_merge_clips - Merge clip rectangles + * @dst: Destination clip rectangle + * @src: Source clip rectangle(s) + * @num_clips: Number of @src clip rectangles + * @flags: Dirty fb ioctl flags + * @max_width: Maximum width of @dst + * @max_height: Maximum height of @dst + * + * This function merges @src clip rectangle(s) into @dst. If @src is NULL, + * @max_width and @min_width is used to set a full @dst clip rectangle. + * + * Returns: + * true if it's a full clip, false otherwise + */ +bool tinydrm_merge_clips(struct drm_clip_rect *dst, + struct drm_clip_rect *src, unsigned int num_clips, + unsigned int flags, u32 max_width, u32 max_height) +{ + unsigned int i; + + if (!src || !num_clips) { + dst->x1 = 0; + dst->x2 = max_width; + dst->y1 = 0; + dst->y2 = max_height; + return true; + } + + dst->x1 = ~0; + dst->y1 = ~0; + dst->x2 = 0; + dst->y2 = 0; + + for (i = 0; i < num_clips; i++) { + if (flags & DRM_MODE_FB_DIRTY_ANNOTATE_COPY) + i++; + dst->x1 = min(dst->x1, src[i].x1); + dst->x2 = max(dst->x2, src[i].x2); + dst->y1 = min(dst->y1, src[i].y1); + dst->y2 = max(dst->y2, src[i].y2); + } + + if (dst->x2 > max_width || dst->y2 > max_height || + dst->x1 >= dst->x2 || dst->y1 >= dst->y2) { + DRM_DEBUG_KMS("Illegal clip: x1=%u, x2=%u, y1=%u, y2=%u\n", + dst->x1, dst->x2, dst->y1, dst->y2); + dst->x1 = 0; + dst->y1 = 0; + dst->x2 = max_width; + dst->y2 = max_height; + } + + return (dst->x2 - dst->x1) == max_width && + (dst->y2 - dst->y1) == max_height; +} +EXPORT_SYMBOL(tinydrm_merge_clips); + +int tinydrm_fb_dirty(struct drm_framebuffer *fb, + struct drm_file *file_priv, + unsigned int flags, unsigned int color, + struct drm_clip_rect *clips, + unsigned int num_clips) +{ + struct tinydrm_device *tdev = fb->dev->dev_private; + struct drm_plane *plane = &tdev->pipe.plane; + int ret = 0; + + drm_modeset_lock(&plane->mutex, NULL); + + /* fbdev can flush even when we're not interested */ + if (plane->state->fb == fb) { + mutex_lock(&tdev->dirty_lock); + ret = tdev->fb_dirty(fb, file_priv, flags, + color, clips, num_clips); + mutex_unlock(&tdev->dirty_lock); + } + + drm_modeset_unlock(&plane->mutex); + + if (ret) + dev_err_once(fb->dev->dev, + "Failed to update display %d\n", ret); + + return ret; +} +EXPORT_SYMBOL(tinydrm_fb_dirty); + +/** + * tinydrm_memcpy - Copy clip buffer + * @dst: Destination buffer + * @vaddr: Source buffer + * @fb: DRM framebuffer + * @clip: Clip rectangle area to copy + */ +void tinydrm_memcpy(void *dst, void *vaddr, struct drm_framebuffer *fb, + struct drm_clip_rect *clip) +{ + unsigned int cpp = drm_format_plane_cpp(fb->format->format, 0); + unsigned int pitch = fb->pitches[0]; + void *src = vaddr + (clip->y1 * pitch) + (clip->x1 * cpp); + size_t len = (clip->x2 - clip->x1) * cpp; + unsigned int y; + + for (y = clip->y1; y < clip->y2; y++) { + memcpy(dst, src, len); + src += pitch; + dst += len; + } +} +EXPORT_SYMBOL(tinydrm_memcpy); + +/** + * tinydrm_swab16 - Swap bytes into clip buffer + * @dst: RGB565 destination buffer + * @vaddr: RGB565 source buffer + * @fb: DRM framebuffer + * @clip: Clip rectangle area to copy + */ +void tinydrm_swab16(u16 *dst, void *vaddr, struct drm_framebuffer *fb, + struct drm_clip_rect *clip) +{ + size_t len = (clip->x2 - clip->x1) * sizeof(u16); + unsigned int x, y; + u16 *src, *buf; + + /* + * The cma memory is write-combined so reads are uncached. + * Speed up by fetching one line at a time. + */ + buf = kmalloc(len, GFP_KERNEL); + if (!buf) + return; + + for (y = clip->y1; y < clip->y2; y++) { + src = vaddr + (y * fb->pitches[0]); + src += clip->x1; + memcpy(buf, src, len); + src = buf; + for (x = clip->x1; x < clip->x2; x++) + *dst++ = swab16(*src++); + } + + kfree(buf); +} +EXPORT_SYMBOL(tinydrm_swab16); + +/** + * tinydrm_xrgb8888_to_rgb565 - Convert XRGB8888 to RGB565 clip buffer + * @dst: RGB565 destination buffer + * @vaddr: XRGB8888 source buffer + * @fb: DRM framebuffer + * @clip: Clip rectangle area to copy + * @swap: Swap bytes + * + * Drivers can use this function for RGB565 devices that don't natively + * support XRGB8888. + */ +void tinydrm_xrgb8888_to_rgb565(u16 *dst, void *vaddr, + struct drm_framebuffer *fb, + struct drm_clip_rect *clip, bool swap) +{ + size_t len = (clip->x2 - clip->x1) * sizeof(u32); + unsigned int x, y; + u32 *src, *buf; + u16 val16; + + buf = kmalloc(len, GFP_KERNEL); + if (!buf) + return; + + for (y = clip->y1; y < clip->y2; y++) { + src = vaddr + (y * fb->pitches[0]); + src += clip->x1; + memcpy(buf, src, len); + src = buf; + for (x = clip->x1; x < clip->x2; x++) { + val16 = ((*src & 0x00F80000) >> 8) | + ((*src & 0x0000FC00) >> 5) | + ((*src & 0x000000F8) >> 3); + src++; + if (swap) + *dst++ = swab16(val16); + else + *dst++ = val16; + } + } + + kfree(buf); +} +EXPORT_SYMBOL(tinydrm_xrgb8888_to_rgb565); + +/** + * tinydrm_xrgb8888_to_gray8 - Convert XRGB8888 to grayscale + * @dst: 8-bit grayscale destination buffer + * @vaddr: XRGB8888 source buffer + * @fb: DRM framebuffer + * @clip: Clip rectangle area to copy + * + * Drm doesn't have native monochrome or grayscale support. + * Such drivers can announce the commonly supported XR24 format to userspace + * and use this function to convert to the native format. + * + * Monochrome drivers will use the most significant bit, + * where 1 means foreground color and 0 background color. + * + * ITU BT.601 is used for the RGB -> luma (brightness) conversion. + */ +void tinydrm_xrgb8888_to_gray8(u8 *dst, void *vaddr, struct drm_framebuffer *fb, + struct drm_clip_rect *clip) +{ + unsigned int len = (clip->x2 - clip->x1) * sizeof(u32); + unsigned int x, y; + void *buf; + u32 *src; + + if (WARN_ON(fb->format->format != DRM_FORMAT_XRGB8888)) + return; + /* + * The cma memory is write-combined so reads are uncached. + * Speed up by fetching one line at a time. + */ + buf = kmalloc(len, GFP_KERNEL); + if (!buf) + return; + + for (y = clip->y1; y < clip->y2; y++) { + src = vaddr + (y * fb->pitches[0]); + src += clip->x1; + memcpy(buf, src, len); + src = buf; + for (x = clip->x1; x < clip->x2; x++) { + u8 r = (*src & 0x00ff0000) >> 16; + u8 g = (*src & 0x0000ff00) >> 8; + u8 b = *src & 0x000000ff; + + /* ITU BT.601: Y = 0.299 R + 0.587 G + 0.114 B */ + *dst++ = (3 * r + 6 * g + b) / 10; + src++; + } + } + + kfree(buf); +} +EXPORT_SYMBOL(tinydrm_xrgb8888_to_gray8); + +#if IS_ENABLED(CONFIG_SPI) + +/** + * tinydrm_spi_max_transfer_size - Determine max SPI transfer size + * @spi: SPI device + * @max_len: Maximum buffer size needed (optional) + * + * This function returns the maximum size to use for SPI transfers. It checks + * the SPI master, the optional @max_len and the module parameter spi_max and + * returns the smallest. + * + * Returns: + * Maximum size for SPI transfers + */ +size_t tinydrm_spi_max_transfer_size(struct spi_device *spi, size_t max_len) +{ + size_t ret; + + ret = min(spi_max_transfer_size(spi), spi->master->max_dma_len); + if (max_len) + ret = min(ret, max_len); + if (spi_max) + ret = min_t(size_t, ret, spi_max); + ret &= ~0x3; + if (ret < 4) + ret = 4; + + return ret; +} +EXPORT_SYMBOL(tinydrm_spi_max_transfer_size); + +/** + * tinydrm_spi_bpw_supported - Check if bits per word is supported + * @spi: SPI device + * @bpw: Bits per word + * + * This function checks to see if the SPI master driver supports @bpw. + * + * Returns: + * True if @bpw is supported, false otherwise. + */ +bool tinydrm_spi_bpw_supported(struct spi_device *spi, u8 bpw) +{ + u32 bpw_mask = spi->master->bits_per_word_mask; + + if (bpw == 8) + return true; + + if (!bpw_mask) { + dev_warn_once(&spi->dev, + "bits_per_word_mask not set, assume 8-bit only\n"); + return false; + } + + if (bpw_mask & SPI_BPW_MASK(bpw)) + return true; + + return false; +} +EXPORT_SYMBOL(tinydrm_spi_bpw_supported); + +static void +tinydrm_dbg_spi_print(struct spi_device *spi, struct spi_transfer *tr, + const void *buf, int idx, bool tx) +{ + u32 speed_hz = tr->speed_hz ? tr->speed_hz : spi->max_speed_hz; + char linebuf[3 * 32]; + + hex_dump_to_buffer(buf, tr->len, 16, + DIV_ROUND_UP(tr->bits_per_word, 8), + linebuf, sizeof(linebuf), false); + + printk(KERN_DEBUG + " tr(%i): speed=%u%s, bpw=%i, len=%u, %s_buf=[%s%s]\n", idx, + speed_hz > 1000000 ? speed_hz / 1000000 : speed_hz / 1000, + speed_hz > 1000000 ? "MHz" : "kHz", tr->bits_per_word, tr->len, + tx ? "tx" : "rx", linebuf, tr->len > 16 ? " ..." : ""); +} + +/* called through tinydrm_dbg_spi_message() */ +void _tinydrm_dbg_spi_message(struct spi_device *spi, struct spi_message *m) +{ + struct spi_transfer *tmp; + int i = 0; + + list_for_each_entry(tmp, &m->transfers, transfer_list) { + + if (tmp->tx_buf) + tinydrm_dbg_spi_print(spi, tmp, tmp->tx_buf, i, true); + if (tmp->rx_buf) + tinydrm_dbg_spi_print(spi, tmp, tmp->rx_buf, i, false); + i++; + } +} +EXPORT_SYMBOL(_tinydrm_dbg_spi_message); + +/** + * tinydrm_spi_transfer - SPI transfer helper + * @spi: SPI device + * @speed_hz: Override speed (optional) + * @header: Optional header transfer + * @bpw: Bits per word + * @buf: Buffer to transfer + * @len: Buffer length + * + * This SPI transfer helper breaks up the transfer of @buf into chunks which + * the SPI master driver can handle. If the machine is Little Endian and the + * SPI master driver doesn't support 16 bits per word, it swaps the bytes and + * does a 8-bit transfer. + * If @header is set, it is prepended to each SPI message. + * + * Returns: + * Zero on success, negative error code on failure. + */ +int tinydrm_spi_transfer(struct spi_device *spi, u32 speed_hz, + struct spi_transfer *header, u8 bpw, const void *buf, + size_t len) +{ + struct spi_transfer tr = { + .bits_per_word = bpw, + .speed_hz = speed_hz, + }; + struct spi_message m; + u16 *swap_buf = NULL; + size_t max_chunk; + size_t chunk; + int ret = 0; + + if (WARN_ON_ONCE(bpw != 8 && bpw != 16)) + return -EINVAL; + + max_chunk = tinydrm_spi_max_transfer_size(spi, 0); + + if (drm_debug & DRM_UT_DRIVER) + pr_debug("[drm:%s] bpw=%u, max_chunk=%zu, transfers:\n", + __func__, bpw, max_chunk); + + if (bpw == 16 && !tinydrm_spi_bpw_supported(spi, 16)) { + tr.bits_per_word = 8; + if (tinydrm_machine_little_endian()) { + swap_buf = kmalloc(min(len, max_chunk), GFP_KERNEL); + if (!swap_buf) + return -ENOMEM; + } + } + + spi_message_init(&m); + if (header) + spi_message_add_tail(header, &m); + spi_message_add_tail(&tr, &m); + + while (len) { + chunk = min(len, max_chunk); + + tr.tx_buf = buf; + tr.len = chunk; + + if (swap_buf) { + const u16 *buf16 = buf; + unsigned int i; + + for (i = 0; i < chunk / 2; i++) + swap_buf[i] = swab16(buf16[i]); + + tr.tx_buf = swap_buf; + } + + buf += chunk; + len -= chunk; + + tinydrm_dbg_spi_message(spi, &m); + ret = spi_sync(spi, &m); + if (ret) + return ret; + } + + return 0; +} +EXPORT_SYMBOL(tinydrm_spi_transfer); + +#endif /* CONFIG_SPI */ diff --git a/drivers/gpu/drm/tinydrm/core/tinydrm-pipe.c b/drivers/gpu/drm/tinydrm/core/tinydrm-pipe.c new file mode 100644 index 000000000..7e8e24d0b --- /dev/null +++ b/drivers/gpu/drm/tinydrm/core/tinydrm-pipe.c @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2016 Noralf Trønnes + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include <drm/drm_atomic_helper.h> +#include <drm/drm_crtc_helper.h> +#include <drm/drm_gem_framebuffer_helper.h> +#include <drm/drm_modes.h> +#include <drm/tinydrm/tinydrm.h> + +struct tinydrm_connector { + struct drm_connector base; + struct drm_display_mode mode; +}; + +static inline struct tinydrm_connector * +to_tinydrm_connector(struct drm_connector *connector) +{ + return container_of(connector, struct tinydrm_connector, base); +} + +static int tinydrm_connector_get_modes(struct drm_connector *connector) +{ + struct tinydrm_connector *tconn = to_tinydrm_connector(connector); + struct drm_display_mode *mode; + + mode = drm_mode_duplicate(connector->dev, &tconn->mode); + if (!mode) { + DRM_ERROR("Failed to duplicate mode\n"); + return 0; + } + + if (mode->name[0] == '\0') + drm_mode_set_name(mode); + + mode->type |= DRM_MODE_TYPE_PREFERRED; + drm_mode_probed_add(connector, mode); + + if (mode->width_mm) { + connector->display_info.width_mm = mode->width_mm; + connector->display_info.height_mm = mode->height_mm; + } + + return 1; +} + +static const struct drm_connector_helper_funcs tinydrm_connector_hfuncs = { + .get_modes = tinydrm_connector_get_modes, +}; + +static enum drm_connector_status +tinydrm_connector_detect(struct drm_connector *connector, bool force) +{ + if (drm_dev_is_unplugged(connector->dev)) + return connector_status_disconnected; + + return connector->status; +} + +static void tinydrm_connector_destroy(struct drm_connector *connector) +{ + struct tinydrm_connector *tconn = to_tinydrm_connector(connector); + + drm_connector_cleanup(connector); + kfree(tconn); +} + +static const struct drm_connector_funcs tinydrm_connector_funcs = { + .reset = drm_atomic_helper_connector_reset, + .detect = tinydrm_connector_detect, + .fill_modes = drm_helper_probe_single_connector_modes, + .destroy = tinydrm_connector_destroy, + .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state, + .atomic_destroy_state = drm_atomic_helper_connector_destroy_state, +}; + +struct drm_connector * +tinydrm_connector_create(struct drm_device *drm, + const struct drm_display_mode *mode, + int connector_type) +{ + struct tinydrm_connector *tconn; + struct drm_connector *connector; + int ret; + + tconn = kzalloc(sizeof(*tconn), GFP_KERNEL); + if (!tconn) + return ERR_PTR(-ENOMEM); + + drm_mode_copy(&tconn->mode, mode); + connector = &tconn->base; + + drm_connector_helper_add(connector, &tinydrm_connector_hfuncs); + ret = drm_connector_init(drm, connector, &tinydrm_connector_funcs, + connector_type); + if (ret) { + kfree(tconn); + return ERR_PTR(ret); + } + + connector->status = connector_status_connected; + + return connector; +} + +/** + * tinydrm_display_pipe_update - Display pipe update helper + * @pipe: Simple display pipe + * @old_state: Old plane state + * + * This function does a full framebuffer flush if the plane framebuffer + * has changed. It also handles vblank events. Drivers can use this as their + * &drm_simple_display_pipe_funcs->update callback. + */ +void tinydrm_display_pipe_update(struct drm_simple_display_pipe *pipe, + struct drm_plane_state *old_state) +{ + struct tinydrm_device *tdev = pipe_to_tinydrm(pipe); + struct drm_framebuffer *fb = pipe->plane.state->fb; + struct drm_crtc *crtc = &tdev->pipe.crtc; + + if (fb && (fb != old_state->fb)) { + if (tdev->fb_dirty) + tdev->fb_dirty(fb, NULL, 0, 0, NULL, 0); + } + + if (crtc->state->event) { + spin_lock_irq(&crtc->dev->event_lock); + drm_crtc_send_vblank_event(crtc, crtc->state->event); + spin_unlock_irq(&crtc->dev->event_lock); + crtc->state->event = NULL; + } +} +EXPORT_SYMBOL(tinydrm_display_pipe_update); + +static int tinydrm_rotate_mode(struct drm_display_mode *mode, + unsigned int rotation) +{ + if (rotation == 0 || rotation == 180) { + return 0; + } else if (rotation == 90 || rotation == 270) { + swap(mode->hdisplay, mode->vdisplay); + swap(mode->hsync_start, mode->vsync_start); + swap(mode->hsync_end, mode->vsync_end); + swap(mode->htotal, mode->vtotal); + swap(mode->width_mm, mode->height_mm); + return 0; + } else { + return -EINVAL; + } +} + +/** + * tinydrm_display_pipe_init - Initialize display pipe + * @tdev: tinydrm device + * @funcs: Display pipe functions + * @connector_type: Connector type + * @formats: Array of supported formats (DRM_FORMAT\_\*) + * @format_count: Number of elements in @formats + * @mode: Supported mode + * @rotation: Initial @mode rotation in degrees Counter Clock Wise + * + * This function sets up a &drm_simple_display_pipe with a &drm_connector that + * has one fixed &drm_display_mode which is rotated according to @rotation. + * + * Returns: + * Zero on success, negative error code on failure. + */ +int +tinydrm_display_pipe_init(struct tinydrm_device *tdev, + const struct drm_simple_display_pipe_funcs *funcs, + int connector_type, + const uint32_t *formats, + unsigned int format_count, + const struct drm_display_mode *mode, + unsigned int rotation) +{ + struct drm_device *drm = tdev->drm; + struct drm_display_mode mode_copy; + struct drm_connector *connector; + int ret; + + drm_mode_copy(&mode_copy, mode); + ret = tinydrm_rotate_mode(&mode_copy, rotation); + if (ret) { + DRM_ERROR("Illegal rotation value %u\n", rotation); + return -EINVAL; + } + + drm->mode_config.min_width = mode_copy.hdisplay; + drm->mode_config.max_width = mode_copy.hdisplay; + drm->mode_config.min_height = mode_copy.vdisplay; + drm->mode_config.max_height = mode_copy.vdisplay; + + connector = tinydrm_connector_create(drm, &mode_copy, connector_type); + if (IS_ERR(connector)) + return PTR_ERR(connector); + + return drm_simple_display_pipe_init(drm, &tdev->pipe, funcs, formats, + format_count, NULL, connector); +} +EXPORT_SYMBOL(tinydrm_display_pipe_init); diff --git a/drivers/gpu/drm/tinydrm/ili9225.c b/drivers/gpu/drm/tinydrm/ili9225.c new file mode 100644 index 000000000..6044a0106 --- /dev/null +++ b/drivers/gpu/drm/tinydrm/ili9225.c @@ -0,0 +1,454 @@ +/* + * DRM driver for Ilitek ILI9225 panels + * + * Copyright 2017 David Lechner <david@lechnology.com> + * + * Some code copied from mipi-dbi.c + * Copyright 2016 Noralf Trønnes + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include <linux/delay.h> +#include <linux/dma-buf.h> +#include <linux/gpio/consumer.h> +#include <linux/module.h> +#include <linux/property.h> +#include <linux/spi/spi.h> +#include <video/mipi_display.h> + +#include <drm/drm_fb_helper.h> +#include <drm/drm_gem_framebuffer_helper.h> +#include <drm/tinydrm/mipi-dbi.h> +#include <drm/tinydrm/tinydrm-helpers.h> + +#define ILI9225_DRIVER_READ_CODE 0x00 +#define ILI9225_DRIVER_OUTPUT_CONTROL 0x01 +#define ILI9225_LCD_AC_DRIVING_CONTROL 0x02 +#define ILI9225_ENTRY_MODE 0x03 +#define ILI9225_DISPLAY_CONTROL_1 0x07 +#define ILI9225_BLANK_PERIOD_CONTROL_1 0x08 +#define ILI9225_FRAME_CYCLE_CONTROL 0x0b +#define ILI9225_INTERFACE_CONTROL 0x0c +#define ILI9225_OSCILLATION_CONTROL 0x0f +#define ILI9225_POWER_CONTROL_1 0x10 +#define ILI9225_POWER_CONTROL_2 0x11 +#define ILI9225_POWER_CONTROL_3 0x12 +#define ILI9225_POWER_CONTROL_4 0x13 +#define ILI9225_POWER_CONTROL_5 0x14 +#define ILI9225_VCI_RECYCLING 0x15 +#define ILI9225_RAM_ADDRESS_SET_1 0x20 +#define ILI9225_RAM_ADDRESS_SET_2 0x21 +#define ILI9225_WRITE_DATA_TO_GRAM 0x22 +#define ILI9225_SOFTWARE_RESET 0x28 +#define ILI9225_GATE_SCAN_CONTROL 0x30 +#define ILI9225_VERTICAL_SCROLL_1 0x31 +#define ILI9225_VERTICAL_SCROLL_2 0x32 +#define ILI9225_VERTICAL_SCROLL_3 0x33 +#define ILI9225_PARTIAL_DRIVING_POS_1 0x34 +#define ILI9225_PARTIAL_DRIVING_POS_2 0x35 +#define ILI9225_HORIZ_WINDOW_ADDR_1 0x36 +#define ILI9225_HORIZ_WINDOW_ADDR_2 0x37 +#define ILI9225_VERT_WINDOW_ADDR_1 0x38 +#define ILI9225_VERT_WINDOW_ADDR_2 0x39 +#define ILI9225_GAMMA_CONTROL_1 0x50 +#define ILI9225_GAMMA_CONTROL_2 0x51 +#define ILI9225_GAMMA_CONTROL_3 0x52 +#define ILI9225_GAMMA_CONTROL_4 0x53 +#define ILI9225_GAMMA_CONTROL_5 0x54 +#define ILI9225_GAMMA_CONTROL_6 0x55 +#define ILI9225_GAMMA_CONTROL_7 0x56 +#define ILI9225_GAMMA_CONTROL_8 0x57 +#define ILI9225_GAMMA_CONTROL_9 0x58 +#define ILI9225_GAMMA_CONTROL_10 0x59 + +static inline int ili9225_command(struct mipi_dbi *mipi, u8 cmd, u16 data) +{ + u8 par[2] = { data >> 8, data & 0xff }; + + return mipi_dbi_command_buf(mipi, cmd, par, 2); +} + +static int ili9225_fb_dirty(struct drm_framebuffer *fb, + struct drm_file *file_priv, unsigned int flags, + unsigned int color, struct drm_clip_rect *clips, + unsigned int num_clips) +{ + struct drm_gem_cma_object *cma_obj = drm_fb_cma_get_gem_obj(fb, 0); + struct tinydrm_device *tdev = fb->dev->dev_private; + struct mipi_dbi *mipi = mipi_dbi_from_tinydrm(tdev); + bool swap = mipi->swap_bytes; + struct drm_clip_rect clip; + u16 x_start, y_start; + u16 x1, x2, y1, y2; + int ret = 0; + bool full; + void *tr; + + if (!mipi->enabled) + return 0; + + full = tinydrm_merge_clips(&clip, clips, num_clips, flags, + fb->width, fb->height); + + DRM_DEBUG("Flushing [FB:%d] x1=%u, x2=%u, y1=%u, y2=%u\n", fb->base.id, + clip.x1, clip.x2, clip.y1, clip.y2); + + if (!mipi->dc || !full || swap || + fb->format->format == DRM_FORMAT_XRGB8888) { + tr = mipi->tx_buf; + ret = mipi_dbi_buf_copy(mipi->tx_buf, fb, &clip, swap); + if (ret) + return ret; + } else { + tr = cma_obj->vaddr; + } + + switch (mipi->rotation) { + default: + x1 = clip.x1; + x2 = clip.x2 - 1; + y1 = clip.y1; + y2 = clip.y2 - 1; + x_start = x1; + y_start = y1; + break; + case 90: + x1 = clip.y1; + x2 = clip.y2 - 1; + y1 = fb->width - clip.x2; + y2 = fb->width - clip.x1 - 1; + x_start = x1; + y_start = y2; + break; + case 180: + x1 = fb->width - clip.x2; + x2 = fb->width - clip.x1 - 1; + y1 = fb->height - clip.y2; + y2 = fb->height - clip.y1 - 1; + x_start = x2; + y_start = y2; + break; + case 270: + x1 = fb->height - clip.y2; + x2 = fb->height - clip.y1 - 1; + y1 = clip.x1; + y2 = clip.x2 - 1; + x_start = x2; + y_start = y1; + break; + } + + ili9225_command(mipi, ILI9225_HORIZ_WINDOW_ADDR_1, x2); + ili9225_command(mipi, ILI9225_HORIZ_WINDOW_ADDR_2, x1); + ili9225_command(mipi, ILI9225_VERT_WINDOW_ADDR_1, y2); + ili9225_command(mipi, ILI9225_VERT_WINDOW_ADDR_2, y1); + + ili9225_command(mipi, ILI9225_RAM_ADDRESS_SET_1, x_start); + ili9225_command(mipi, ILI9225_RAM_ADDRESS_SET_2, y_start); + + ret = mipi_dbi_command_buf(mipi, ILI9225_WRITE_DATA_TO_GRAM, tr, + (clip.x2 - clip.x1) * (clip.y2 - clip.y1) * 2); + + return ret; +} + +static const struct drm_framebuffer_funcs ili9225_fb_funcs = { + .destroy = drm_gem_fb_destroy, + .create_handle = drm_gem_fb_create_handle, + .dirty = tinydrm_fb_dirty, +}; + +static void ili9225_pipe_enable(struct drm_simple_display_pipe *pipe, + struct drm_crtc_state *crtc_state, + struct drm_plane_state *plane_state) +{ + struct tinydrm_device *tdev = pipe_to_tinydrm(pipe); + struct mipi_dbi *mipi = mipi_dbi_from_tinydrm(tdev); + struct device *dev = tdev->drm->dev; + int ret; + u8 am_id; + + DRM_DEBUG_KMS("\n"); + + mipi_dbi_hw_reset(mipi); + + /* + * There don't seem to be two example init sequences that match, so + * using the one from the popular Arduino library for this display. + * https://github.com/Nkawu/TFT_22_ILI9225/blob/master/src/TFT_22_ILI9225.cpp + */ + + ret = ili9225_command(mipi, ILI9225_POWER_CONTROL_1, 0x0000); + if (ret) { + DRM_DEV_ERROR(dev, "Error sending command %d\n", ret); + return; + } + ili9225_command(mipi, ILI9225_POWER_CONTROL_2, 0x0000); + ili9225_command(mipi, ILI9225_POWER_CONTROL_3, 0x0000); + ili9225_command(mipi, ILI9225_POWER_CONTROL_4, 0x0000); + ili9225_command(mipi, ILI9225_POWER_CONTROL_5, 0x0000); + + msleep(40); + + ili9225_command(mipi, ILI9225_POWER_CONTROL_2, 0x0018); + ili9225_command(mipi, ILI9225_POWER_CONTROL_3, 0x6121); + ili9225_command(mipi, ILI9225_POWER_CONTROL_4, 0x006f); + ili9225_command(mipi, ILI9225_POWER_CONTROL_5, 0x495f); + ili9225_command(mipi, ILI9225_POWER_CONTROL_1, 0x0800); + + msleep(10); + + ili9225_command(mipi, ILI9225_POWER_CONTROL_2, 0x103b); + + msleep(50); + + switch (mipi->rotation) { + default: + am_id = 0x30; + break; + case 90: + am_id = 0x18; + break; + case 180: + am_id = 0x00; + break; + case 270: + am_id = 0x28; + break; + } + ili9225_command(mipi, ILI9225_DRIVER_OUTPUT_CONTROL, 0x011c); + ili9225_command(mipi, ILI9225_LCD_AC_DRIVING_CONTROL, 0x0100); + ili9225_command(mipi, ILI9225_ENTRY_MODE, 0x1000 | am_id); + ili9225_command(mipi, ILI9225_DISPLAY_CONTROL_1, 0x0000); + ili9225_command(mipi, ILI9225_BLANK_PERIOD_CONTROL_1, 0x0808); + ili9225_command(mipi, ILI9225_FRAME_CYCLE_CONTROL, 0x1100); + ili9225_command(mipi, ILI9225_INTERFACE_CONTROL, 0x0000); + ili9225_command(mipi, ILI9225_OSCILLATION_CONTROL, 0x0d01); + ili9225_command(mipi, ILI9225_VCI_RECYCLING, 0x0020); + ili9225_command(mipi, ILI9225_RAM_ADDRESS_SET_1, 0x0000); + ili9225_command(mipi, ILI9225_RAM_ADDRESS_SET_2, 0x0000); + + ili9225_command(mipi, ILI9225_GATE_SCAN_CONTROL, 0x0000); + ili9225_command(mipi, ILI9225_VERTICAL_SCROLL_1, 0x00db); + ili9225_command(mipi, ILI9225_VERTICAL_SCROLL_2, 0x0000); + ili9225_command(mipi, ILI9225_VERTICAL_SCROLL_3, 0x0000); + ili9225_command(mipi, ILI9225_PARTIAL_DRIVING_POS_1, 0x00db); + ili9225_command(mipi, ILI9225_PARTIAL_DRIVING_POS_2, 0x0000); + + ili9225_command(mipi, ILI9225_GAMMA_CONTROL_1, 0x0000); + ili9225_command(mipi, ILI9225_GAMMA_CONTROL_2, 0x0808); + ili9225_command(mipi, ILI9225_GAMMA_CONTROL_3, 0x080a); + ili9225_command(mipi, ILI9225_GAMMA_CONTROL_4, 0x000a); + ili9225_command(mipi, ILI9225_GAMMA_CONTROL_5, 0x0a08); + ili9225_command(mipi, ILI9225_GAMMA_CONTROL_6, 0x0808); + ili9225_command(mipi, ILI9225_GAMMA_CONTROL_7, 0x0000); + ili9225_command(mipi, ILI9225_GAMMA_CONTROL_8, 0x0a00); + ili9225_command(mipi, ILI9225_GAMMA_CONTROL_9, 0x0710); + ili9225_command(mipi, ILI9225_GAMMA_CONTROL_10, 0x0710); + + ili9225_command(mipi, ILI9225_DISPLAY_CONTROL_1, 0x0012); + + msleep(50); + + ili9225_command(mipi, ILI9225_DISPLAY_CONTROL_1, 0x1017); + + mipi_dbi_enable_flush(mipi, crtc_state, plane_state); +} + +static void ili9225_pipe_disable(struct drm_simple_display_pipe *pipe) +{ + struct tinydrm_device *tdev = pipe_to_tinydrm(pipe); + struct mipi_dbi *mipi = mipi_dbi_from_tinydrm(tdev); + + DRM_DEBUG_KMS("\n"); + + if (!mipi->enabled) + return; + + ili9225_command(mipi, ILI9225_DISPLAY_CONTROL_1, 0x0000); + msleep(50); + ili9225_command(mipi, ILI9225_POWER_CONTROL_2, 0x0007); + msleep(50); + ili9225_command(mipi, ILI9225_POWER_CONTROL_1, 0x0a02); + + mipi->enabled = false; +} + +static int ili9225_dbi_command(struct mipi_dbi *mipi, u8 *cmd, u8 *par, + size_t num) +{ + struct spi_device *spi = mipi->spi; + unsigned int bpw = 8; + u32 speed_hz; + int ret; + + gpiod_set_value_cansleep(mipi->dc, 0); + speed_hz = mipi_dbi_spi_cmd_max_speed(spi, 1); + ret = tinydrm_spi_transfer(spi, speed_hz, NULL, 8, cmd, 1); + if (ret || !num) + return ret; + + if (*cmd == ILI9225_WRITE_DATA_TO_GRAM && !mipi->swap_bytes) + bpw = 16; + + gpiod_set_value_cansleep(mipi->dc, 1); + speed_hz = mipi_dbi_spi_cmd_max_speed(spi, num); + + return tinydrm_spi_transfer(spi, speed_hz, NULL, bpw, par, num); +} + +static const u32 ili9225_formats[] = { + DRM_FORMAT_RGB565, + DRM_FORMAT_XRGB8888, +}; + +static int ili9225_init(struct device *dev, struct mipi_dbi *mipi, + const struct drm_simple_display_pipe_funcs *pipe_funcs, + struct drm_driver *driver, + const struct drm_display_mode *mode, + unsigned int rotation) +{ + size_t bufsize = mode->vdisplay * mode->hdisplay * sizeof(u16); + struct tinydrm_device *tdev = &mipi->tinydrm; + int ret; + + if (!mipi->command) + return -EINVAL; + + mutex_init(&mipi->cmdlock); + + mipi->tx_buf = devm_kmalloc(dev, bufsize, GFP_KERNEL); + if (!mipi->tx_buf) + return -ENOMEM; + + ret = devm_tinydrm_init(dev, tdev, &ili9225_fb_funcs, driver); + if (ret) + return ret; + + tdev->fb_dirty = ili9225_fb_dirty; + + ret = tinydrm_display_pipe_init(tdev, pipe_funcs, + DRM_MODE_CONNECTOR_VIRTUAL, + ili9225_formats, + ARRAY_SIZE(ili9225_formats), mode, + rotation); + if (ret) + return ret; + + tdev->drm->mode_config.preferred_depth = 16; + mipi->rotation = rotation; + + drm_mode_config_reset(tdev->drm); + + DRM_DEBUG_KMS("preferred_depth=%u, rotation = %u\n", + tdev->drm->mode_config.preferred_depth, rotation); + + return 0; +} + +static const struct drm_simple_display_pipe_funcs ili9225_pipe_funcs = { + .enable = ili9225_pipe_enable, + .disable = ili9225_pipe_disable, + .update = tinydrm_display_pipe_update, + .prepare_fb = drm_gem_fb_simple_display_pipe_prepare_fb, +}; + +static const struct drm_display_mode ili9225_mode = { + TINYDRM_MODE(176, 220, 35, 44), +}; + +DEFINE_DRM_GEM_CMA_FOPS(ili9225_fops); + +static struct drm_driver ili9225_driver = { + .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_PRIME | + DRIVER_ATOMIC, + .fops = &ili9225_fops, + TINYDRM_GEM_DRIVER_OPS, + .name = "ili9225", + .desc = "Ilitek ILI9225", + .date = "20171106", + .major = 1, + .minor = 0, +}; + +static const struct of_device_id ili9225_of_match[] = { + { .compatible = "vot,v220hf01a-t" }, + {}, +}; +MODULE_DEVICE_TABLE(of, ili9225_of_match); + +static const struct spi_device_id ili9225_id[] = { + { "v220hf01a-t", 0 }, + { }, +}; +MODULE_DEVICE_TABLE(spi, ili9225_id); + +static int ili9225_probe(struct spi_device *spi) +{ + struct device *dev = &spi->dev; + struct mipi_dbi *mipi; + struct gpio_desc *rs; + u32 rotation = 0; + int ret; + + mipi = devm_kzalloc(dev, sizeof(*mipi), GFP_KERNEL); + if (!mipi) + return -ENOMEM; + + mipi->reset = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH); + if (IS_ERR(mipi->reset)) { + DRM_DEV_ERROR(dev, "Failed to get gpio 'reset'\n"); + return PTR_ERR(mipi->reset); + } + + rs = devm_gpiod_get(dev, "rs", GPIOD_OUT_LOW); + if (IS_ERR(rs)) { + DRM_DEV_ERROR(dev, "Failed to get gpio 'rs'\n"); + return PTR_ERR(rs); + } + + device_property_read_u32(dev, "rotation", &rotation); + + ret = mipi_dbi_spi_init(spi, mipi, rs); + if (ret) + return ret; + + /* override the command function set in mipi_dbi_spi_init() */ + mipi->command = ili9225_dbi_command; + + ret = ili9225_init(&spi->dev, mipi, &ili9225_pipe_funcs, + &ili9225_driver, &ili9225_mode, rotation); + if (ret) + return ret; + + spi_set_drvdata(spi, mipi); + + return devm_tinydrm_register(&mipi->tinydrm); +} + +static void ili9225_shutdown(struct spi_device *spi) +{ + struct mipi_dbi *mipi = spi_get_drvdata(spi); + + tinydrm_shutdown(&mipi->tinydrm); +} + +static struct spi_driver ili9225_spi_driver = { + .driver = { + .name = "ili9225", + .owner = THIS_MODULE, + .of_match_table = ili9225_of_match, + }, + .id_table = ili9225_id, + .probe = ili9225_probe, + .shutdown = ili9225_shutdown, +}; +module_spi_driver(ili9225_spi_driver); + +MODULE_DESCRIPTION("Ilitek ILI9225 DRM driver"); +MODULE_AUTHOR("David Lechner <david@lechnology.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/gpu/drm/tinydrm/ili9341.c b/drivers/gpu/drm/tinydrm/ili9341.c new file mode 100644 index 000000000..670103774 --- /dev/null +++ b/drivers/gpu/drm/tinydrm/ili9341.c @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * DRM driver for Ilitek ILI9341 panels + * + * Copyright 2018 David Lechner <david@lechnology.com> + * + * Based on mi0283qt.c: + * Copyright 2016 Noralf Trønnes + */ + +#include <linux/backlight.h> +#include <linux/delay.h> +#include <linux/gpio/consumer.h> +#include <linux/module.h> +#include <linux/property.h> +#include <linux/spi/spi.h> + +#include <drm/drm_fb_helper.h> +#include <drm/drm_gem_framebuffer_helper.h> +#include <drm/drm_modeset_helper.h> +#include <drm/tinydrm/mipi-dbi.h> +#include <drm/tinydrm/tinydrm-helpers.h> +#include <video/mipi_display.h> + +#define ILI9341_FRMCTR1 0xb1 +#define ILI9341_DISCTRL 0xb6 +#define ILI9341_ETMOD 0xb7 + +#define ILI9341_PWCTRL1 0xc0 +#define ILI9341_PWCTRL2 0xc1 +#define ILI9341_VMCTRL1 0xc5 +#define ILI9341_VMCTRL2 0xc7 +#define ILI9341_PWCTRLA 0xcb +#define ILI9341_PWCTRLB 0xcf + +#define ILI9341_PGAMCTRL 0xe0 +#define ILI9341_NGAMCTRL 0xe1 +#define ILI9341_DTCTRLA 0xe8 +#define ILI9341_DTCTRLB 0xea +#define ILI9341_PWRSEQ 0xed + +#define ILI9341_EN3GAM 0xf2 +#define ILI9341_PUMPCTRL 0xf7 + +#define ILI9341_MADCTL_BGR BIT(3) +#define ILI9341_MADCTL_MV BIT(5) +#define ILI9341_MADCTL_MX BIT(6) +#define ILI9341_MADCTL_MY BIT(7) + +static void yx240qv29_enable(struct drm_simple_display_pipe *pipe, + struct drm_crtc_state *crtc_state, + struct drm_plane_state *plane_state) +{ + struct tinydrm_device *tdev = pipe_to_tinydrm(pipe); + struct mipi_dbi *mipi = mipi_dbi_from_tinydrm(tdev); + u8 addr_mode; + int ret; + + DRM_DEBUG_KMS("\n"); + + ret = mipi_dbi_poweron_conditional_reset(mipi); + if (ret < 0) + return; + if (ret == 1) + goto out_enable; + + mipi_dbi_command(mipi, MIPI_DCS_SET_DISPLAY_OFF); + + mipi_dbi_command(mipi, ILI9341_PWCTRLB, 0x00, 0xc1, 0x30); + mipi_dbi_command(mipi, ILI9341_PWRSEQ, 0x64, 0x03, 0x12, 0x81); + mipi_dbi_command(mipi, ILI9341_DTCTRLA, 0x85, 0x00, 0x78); + mipi_dbi_command(mipi, ILI9341_PWCTRLA, 0x39, 0x2c, 0x00, 0x34, 0x02); + mipi_dbi_command(mipi, ILI9341_PUMPCTRL, 0x20); + mipi_dbi_command(mipi, ILI9341_DTCTRLB, 0x00, 0x00); + + /* Power Control */ + mipi_dbi_command(mipi, ILI9341_PWCTRL1, 0x23); + mipi_dbi_command(mipi, ILI9341_PWCTRL2, 0x10); + /* VCOM */ + mipi_dbi_command(mipi, ILI9341_VMCTRL1, 0x3e, 0x28); + mipi_dbi_command(mipi, ILI9341_VMCTRL2, 0x86); + + /* Memory Access Control */ + mipi_dbi_command(mipi, MIPI_DCS_SET_PIXEL_FORMAT, MIPI_DCS_PIXEL_FMT_16BIT); + + /* Frame Rate */ + mipi_dbi_command(mipi, ILI9341_FRMCTR1, 0x00, 0x1b); + + /* Gamma */ + mipi_dbi_command(mipi, ILI9341_EN3GAM, 0x00); + mipi_dbi_command(mipi, MIPI_DCS_SET_GAMMA_CURVE, 0x01); + mipi_dbi_command(mipi, ILI9341_PGAMCTRL, + 0x0f, 0x31, 0x2b, 0x0c, 0x0e, 0x08, 0x4e, 0xf1, + 0x37, 0x07, 0x10, 0x03, 0x0e, 0x09, 0x00); + mipi_dbi_command(mipi, ILI9341_NGAMCTRL, + 0x00, 0x0e, 0x14, 0x03, 0x11, 0x07, 0x31, 0xc1, + 0x48, 0x08, 0x0f, 0x0c, 0x31, 0x36, 0x0f); + + /* DDRAM */ + mipi_dbi_command(mipi, ILI9341_ETMOD, 0x07); + + /* Display */ + mipi_dbi_command(mipi, ILI9341_DISCTRL, 0x08, 0x82, 0x27, 0x00); + mipi_dbi_command(mipi, MIPI_DCS_EXIT_SLEEP_MODE); + msleep(100); + + mipi_dbi_command(mipi, MIPI_DCS_SET_DISPLAY_ON); + msleep(100); + +out_enable: + switch (mipi->rotation) { + default: + addr_mode = ILI9341_MADCTL_MX; + break; + case 90: + addr_mode = ILI9341_MADCTL_MV; + break; + case 180: + addr_mode = ILI9341_MADCTL_MY; + break; + case 270: + addr_mode = ILI9341_MADCTL_MV | ILI9341_MADCTL_MY | + ILI9341_MADCTL_MX; + break; + } + addr_mode |= ILI9341_MADCTL_BGR; + mipi_dbi_command(mipi, MIPI_DCS_SET_ADDRESS_MODE, addr_mode); + mipi_dbi_enable_flush(mipi, crtc_state, plane_state); +} + +static const struct drm_simple_display_pipe_funcs ili9341_pipe_funcs = { + .enable = yx240qv29_enable, + .disable = mipi_dbi_pipe_disable, + .update = tinydrm_display_pipe_update, + .prepare_fb = drm_gem_fb_simple_display_pipe_prepare_fb, +}; + +static const struct drm_display_mode yx240qv29_mode = { + TINYDRM_MODE(240, 320, 37, 49), +}; + +DEFINE_DRM_GEM_CMA_FOPS(ili9341_fops); + +static struct drm_driver ili9341_driver = { + .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_PRIME | DRIVER_ATOMIC, + .fops = &ili9341_fops, + TINYDRM_GEM_DRIVER_OPS, + .debugfs_init = mipi_dbi_debugfs_init, + .name = "ili9341", + .desc = "Ilitek ILI9341", + .date = "20180514", + .major = 1, + .minor = 0, +}; + +static const struct of_device_id ili9341_of_match[] = { + { .compatible = "adafruit,yx240qv29" }, + { } +}; +MODULE_DEVICE_TABLE(of, ili9341_of_match); + +static const struct spi_device_id ili9341_id[] = { + { "yx240qv29", 0 }, + { } +}; +MODULE_DEVICE_TABLE(spi, ili9341_id); + +static int ili9341_probe(struct spi_device *spi) +{ + struct device *dev = &spi->dev; + struct mipi_dbi *mipi; + struct gpio_desc *dc; + u32 rotation = 0; + int ret; + + mipi = devm_kzalloc(dev, sizeof(*mipi), GFP_KERNEL); + if (!mipi) + return -ENOMEM; + + mipi->reset = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH); + if (IS_ERR(mipi->reset)) { + DRM_DEV_ERROR(dev, "Failed to get gpio 'reset'\n"); + return PTR_ERR(mipi->reset); + } + + dc = devm_gpiod_get_optional(dev, "dc", GPIOD_OUT_LOW); + if (IS_ERR(dc)) { + DRM_DEV_ERROR(dev, "Failed to get gpio 'dc'\n"); + return PTR_ERR(dc); + } + + mipi->backlight = devm_of_find_backlight(dev); + if (IS_ERR(mipi->backlight)) + return PTR_ERR(mipi->backlight); + + device_property_read_u32(dev, "rotation", &rotation); + + ret = mipi_dbi_spi_init(spi, mipi, dc); + if (ret) + return ret; + + ret = mipi_dbi_init(&spi->dev, mipi, &ili9341_pipe_funcs, + &ili9341_driver, &yx240qv29_mode, rotation); + if (ret) + return ret; + + spi_set_drvdata(spi, mipi); + + return devm_tinydrm_register(&mipi->tinydrm); +} + +static void ili9341_shutdown(struct spi_device *spi) +{ + struct mipi_dbi *mipi = spi_get_drvdata(spi); + + tinydrm_shutdown(&mipi->tinydrm); +} + +static struct spi_driver ili9341_spi_driver = { + .driver = { + .name = "ili9341", + .of_match_table = ili9341_of_match, + }, + .id_table = ili9341_id, + .probe = ili9341_probe, + .shutdown = ili9341_shutdown, +}; +module_spi_driver(ili9341_spi_driver); + +MODULE_DESCRIPTION("Ilitek ILI9341 DRM driver"); +MODULE_AUTHOR("David Lechner <david@lechnology.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/gpu/drm/tinydrm/mi0283qt.c b/drivers/gpu/drm/tinydrm/mi0283qt.c new file mode 100644 index 000000000..d7bb4c5e6 --- /dev/null +++ b/drivers/gpu/drm/tinydrm/mi0283qt.c @@ -0,0 +1,267 @@ +/* + * DRM driver for Multi-Inno MI0283QT panels + * + * Copyright 2016 Noralf Trønnes + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include <linux/backlight.h> +#include <linux/delay.h> +#include <linux/gpio/consumer.h> +#include <linux/module.h> +#include <linux/property.h> +#include <linux/regulator/consumer.h> +#include <linux/spi/spi.h> + +#include <drm/drm_fb_helper.h> +#include <drm/drm_modeset_helper.h> +#include <drm/drm_gem_framebuffer_helper.h> +#include <drm/tinydrm/mipi-dbi.h> +#include <drm/tinydrm/tinydrm-helpers.h> +#include <video/mipi_display.h> + +#define ILI9341_FRMCTR1 0xb1 +#define ILI9341_DISCTRL 0xb6 +#define ILI9341_ETMOD 0xb7 + +#define ILI9341_PWCTRL1 0xc0 +#define ILI9341_PWCTRL2 0xc1 +#define ILI9341_VMCTRL1 0xc5 +#define ILI9341_VMCTRL2 0xc7 +#define ILI9341_PWCTRLA 0xcb +#define ILI9341_PWCTRLB 0xcf + +#define ILI9341_PGAMCTRL 0xe0 +#define ILI9341_NGAMCTRL 0xe1 +#define ILI9341_DTCTRLA 0xe8 +#define ILI9341_DTCTRLB 0xea +#define ILI9341_PWRSEQ 0xed + +#define ILI9341_EN3GAM 0xf2 +#define ILI9341_PUMPCTRL 0xf7 + +#define ILI9341_MADCTL_BGR BIT(3) +#define ILI9341_MADCTL_MV BIT(5) +#define ILI9341_MADCTL_MX BIT(6) +#define ILI9341_MADCTL_MY BIT(7) + +static void mi0283qt_enable(struct drm_simple_display_pipe *pipe, + struct drm_crtc_state *crtc_state, + struct drm_plane_state *plane_state) +{ + struct tinydrm_device *tdev = pipe_to_tinydrm(pipe); + struct mipi_dbi *mipi = mipi_dbi_from_tinydrm(tdev); + u8 addr_mode; + int ret; + + DRM_DEBUG_KMS("\n"); + + ret = mipi_dbi_poweron_conditional_reset(mipi); + if (ret < 0) + return; + if (ret == 1) + goto out_enable; + + mipi_dbi_command(mipi, MIPI_DCS_SET_DISPLAY_OFF); + + mipi_dbi_command(mipi, ILI9341_PWCTRLB, 0x00, 0x83, 0x30); + mipi_dbi_command(mipi, ILI9341_PWRSEQ, 0x64, 0x03, 0x12, 0x81); + mipi_dbi_command(mipi, ILI9341_DTCTRLA, 0x85, 0x01, 0x79); + mipi_dbi_command(mipi, ILI9341_PWCTRLA, 0x39, 0x2c, 0x00, 0x34, 0x02); + mipi_dbi_command(mipi, ILI9341_PUMPCTRL, 0x20); + mipi_dbi_command(mipi, ILI9341_DTCTRLB, 0x00, 0x00); + + /* Power Control */ + mipi_dbi_command(mipi, ILI9341_PWCTRL1, 0x26); + mipi_dbi_command(mipi, ILI9341_PWCTRL2, 0x11); + /* VCOM */ + mipi_dbi_command(mipi, ILI9341_VMCTRL1, 0x35, 0x3e); + mipi_dbi_command(mipi, ILI9341_VMCTRL2, 0xbe); + + /* Memory Access Control */ + mipi_dbi_command(mipi, MIPI_DCS_SET_PIXEL_FORMAT, MIPI_DCS_PIXEL_FMT_16BIT); + + /* Frame Rate */ + mipi_dbi_command(mipi, ILI9341_FRMCTR1, 0x00, 0x1b); + + /* Gamma */ + mipi_dbi_command(mipi, ILI9341_EN3GAM, 0x08); + mipi_dbi_command(mipi, MIPI_DCS_SET_GAMMA_CURVE, 0x01); + mipi_dbi_command(mipi, ILI9341_PGAMCTRL, + 0x1f, 0x1a, 0x18, 0x0a, 0x0f, 0x06, 0x45, 0x87, + 0x32, 0x0a, 0x07, 0x02, 0x07, 0x05, 0x00); + mipi_dbi_command(mipi, ILI9341_NGAMCTRL, + 0x00, 0x25, 0x27, 0x05, 0x10, 0x09, 0x3a, 0x78, + 0x4d, 0x05, 0x18, 0x0d, 0x38, 0x3a, 0x1f); + + /* DDRAM */ + mipi_dbi_command(mipi, ILI9341_ETMOD, 0x07); + + /* Display */ + mipi_dbi_command(mipi, ILI9341_DISCTRL, 0x0a, 0x82, 0x27, 0x00); + mipi_dbi_command(mipi, MIPI_DCS_EXIT_SLEEP_MODE); + msleep(100); + + mipi_dbi_command(mipi, MIPI_DCS_SET_DISPLAY_ON); + msleep(100); + +out_enable: + /* The PiTFT (ili9340) has a hardware reset circuit that + * resets only on power-on and not on each reboot through + * a gpio like the rpi-display does. + * As a result, we need to always apply the rotation value + * regardless of the display "on/off" state. + */ + switch (mipi->rotation) { + default: + addr_mode = ILI9341_MADCTL_MV | ILI9341_MADCTL_MY | + ILI9341_MADCTL_MX; + break; + case 90: + addr_mode = ILI9341_MADCTL_MY; + break; + case 180: + addr_mode = ILI9341_MADCTL_MV; + break; + case 270: + addr_mode = ILI9341_MADCTL_MX; + break; + } + addr_mode |= ILI9341_MADCTL_BGR; + mipi_dbi_command(mipi, MIPI_DCS_SET_ADDRESS_MODE, addr_mode); + mipi_dbi_enable_flush(mipi, crtc_state, plane_state); +} + +static const struct drm_simple_display_pipe_funcs mi0283qt_pipe_funcs = { + .enable = mi0283qt_enable, + .disable = mipi_dbi_pipe_disable, + .update = tinydrm_display_pipe_update, + .prepare_fb = drm_gem_fb_simple_display_pipe_prepare_fb, +}; + +static const struct drm_display_mode mi0283qt_mode = { + TINYDRM_MODE(320, 240, 58, 43), +}; + +DEFINE_DRM_GEM_CMA_FOPS(mi0283qt_fops); + +static struct drm_driver mi0283qt_driver = { + .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_PRIME | + DRIVER_ATOMIC, + .fops = &mi0283qt_fops, + TINYDRM_GEM_DRIVER_OPS, + .debugfs_init = mipi_dbi_debugfs_init, + .name = "mi0283qt", + .desc = "Multi-Inno MI0283QT", + .date = "20160614", + .major = 1, + .minor = 0, +}; + +static const struct of_device_id mi0283qt_of_match[] = { + { .compatible = "multi-inno,mi0283qt" }, + {}, +}; +MODULE_DEVICE_TABLE(of, mi0283qt_of_match); + +static const struct spi_device_id mi0283qt_id[] = { + { "mi0283qt", 0 }, + { }, +}; +MODULE_DEVICE_TABLE(spi, mi0283qt_id); + +static int mi0283qt_probe(struct spi_device *spi) +{ + struct device *dev = &spi->dev; + struct mipi_dbi *mipi; + struct gpio_desc *dc; + u32 rotation = 0; + int ret; + + mipi = devm_kzalloc(dev, sizeof(*mipi), GFP_KERNEL); + if (!mipi) + return -ENOMEM; + + mipi->reset = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH); + if (IS_ERR(mipi->reset)) { + DRM_DEV_ERROR(dev, "Failed to get gpio 'reset'\n"); + return PTR_ERR(mipi->reset); + } + + dc = devm_gpiod_get_optional(dev, "dc", GPIOD_OUT_LOW); + if (IS_ERR(dc)) { + DRM_DEV_ERROR(dev, "Failed to get gpio 'dc'\n"); + return PTR_ERR(dc); + } + + mipi->regulator = devm_regulator_get(dev, "power"); + if (IS_ERR(mipi->regulator)) + return PTR_ERR(mipi->regulator); + + mipi->backlight = devm_of_find_backlight(dev); + if (IS_ERR(mipi->backlight)) + return PTR_ERR(mipi->backlight); + + device_property_read_u32(dev, "rotation", &rotation); + + ret = mipi_dbi_spi_init(spi, mipi, dc); + if (ret) + return ret; + + ret = mipi_dbi_init(&spi->dev, mipi, &mi0283qt_pipe_funcs, + &mi0283qt_driver, &mi0283qt_mode, rotation); + if (ret) + return ret; + + spi_set_drvdata(spi, mipi); + + return devm_tinydrm_register(&mipi->tinydrm); +} + +static void mi0283qt_shutdown(struct spi_device *spi) +{ + struct mipi_dbi *mipi = spi_get_drvdata(spi); + + tinydrm_shutdown(&mipi->tinydrm); +} + +static int __maybe_unused mi0283qt_pm_suspend(struct device *dev) +{ + struct mipi_dbi *mipi = dev_get_drvdata(dev); + + return drm_mode_config_helper_suspend(mipi->tinydrm.drm); +} + +static int __maybe_unused mi0283qt_pm_resume(struct device *dev) +{ + struct mipi_dbi *mipi = dev_get_drvdata(dev); + + drm_mode_config_helper_resume(mipi->tinydrm.drm); + + return 0; +} + +static const struct dev_pm_ops mi0283qt_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(mi0283qt_pm_suspend, mi0283qt_pm_resume) +}; + +static struct spi_driver mi0283qt_spi_driver = { + .driver = { + .name = "mi0283qt", + .owner = THIS_MODULE, + .of_match_table = mi0283qt_of_match, + .pm = &mi0283qt_pm_ops, + }, + .id_table = mi0283qt_id, + .probe = mi0283qt_probe, + .shutdown = mi0283qt_shutdown, +}; +module_spi_driver(mi0283qt_spi_driver); + +MODULE_DESCRIPTION("Multi-Inno MI0283QT DRM driver"); +MODULE_AUTHOR("Noralf Trønnes"); +MODULE_LICENSE("GPL"); diff --git a/drivers/gpu/drm/tinydrm/mipi-dbi.c b/drivers/gpu/drm/tinydrm/mipi-dbi.c new file mode 100644 index 000000000..e772a8a9d --- /dev/null +++ b/drivers/gpu/drm/tinydrm/mipi-dbi.c @@ -0,0 +1,1099 @@ +/* + * MIPI Display Bus Interface (DBI) LCD controller support + * + * Copyright 2016 Noralf Trønnes + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include <drm/drm_gem_framebuffer_helper.h> +#include <drm/tinydrm/mipi-dbi.h> +#include <drm/tinydrm/tinydrm-helpers.h> +#include <linux/debugfs.h> +#include <linux/dma-buf.h> +#include <linux/gpio/consumer.h> +#include <linux/module.h> +#include <linux/regulator/consumer.h> +#include <linux/spi/spi.h> +#include <video/mipi_display.h> + +#define MIPI_DBI_MAX_SPI_READ_SPEED 2000000 /* 2MHz */ + +#define DCS_POWER_MODE_DISPLAY BIT(2) +#define DCS_POWER_MODE_DISPLAY_NORMAL_MODE BIT(3) +#define DCS_POWER_MODE_SLEEP_MODE BIT(4) +#define DCS_POWER_MODE_PARTIAL_MODE BIT(5) +#define DCS_POWER_MODE_IDLE_MODE BIT(6) +#define DCS_POWER_MODE_RESERVED_MASK (BIT(0) | BIT(1) | BIT(7)) + +/** + * DOC: overview + * + * This library provides helpers for MIPI Display Bus Interface (DBI) + * compatible display controllers. + * + * Many controllers for tiny lcd displays are MIPI compliant and can use this + * library. If a controller uses registers 0x2A and 0x2B to set the area to + * update and uses register 0x2C to write to frame memory, it is most likely + * MIPI compliant. + * + * Only MIPI Type 1 displays are supported since a full frame memory is needed. + * + * There are 3 MIPI DBI implementation types: + * + * A. Motorola 6800 type parallel bus + * + * B. Intel 8080 type parallel bus + * + * C. SPI type with 3 options: + * + * 1. 9-bit with the Data/Command signal as the ninth bit + * 2. Same as above except it's sent as 16 bits + * 3. 8-bit with the Data/Command signal as a separate D/CX pin + * + * Currently mipi_dbi only supports Type C options 1 and 3 with + * mipi_dbi_spi_init(). + */ + +#define MIPI_DBI_DEBUG_COMMAND(cmd, data, len) \ +({ \ + if (!len) \ + DRM_DEBUG_DRIVER("cmd=%02x\n", cmd); \ + else if (len <= 32) \ + DRM_DEBUG_DRIVER("cmd=%02x, par=%*ph\n", cmd, (int)len, data);\ + else \ + DRM_DEBUG_DRIVER("cmd=%02x, len=%zu\n", cmd, len); \ +}) + +static const u8 mipi_dbi_dcs_read_commands[] = { + MIPI_DCS_GET_DISPLAY_ID, + MIPI_DCS_GET_RED_CHANNEL, + MIPI_DCS_GET_GREEN_CHANNEL, + MIPI_DCS_GET_BLUE_CHANNEL, + MIPI_DCS_GET_DISPLAY_STATUS, + MIPI_DCS_GET_POWER_MODE, + MIPI_DCS_GET_ADDRESS_MODE, + MIPI_DCS_GET_PIXEL_FORMAT, + MIPI_DCS_GET_DISPLAY_MODE, + MIPI_DCS_GET_SIGNAL_MODE, + MIPI_DCS_GET_DIAGNOSTIC_RESULT, + MIPI_DCS_READ_MEMORY_START, + MIPI_DCS_READ_MEMORY_CONTINUE, + MIPI_DCS_GET_SCANLINE, + MIPI_DCS_GET_DISPLAY_BRIGHTNESS, + MIPI_DCS_GET_CONTROL_DISPLAY, + MIPI_DCS_GET_POWER_SAVE, + MIPI_DCS_GET_CABC_MIN_BRIGHTNESS, + MIPI_DCS_READ_DDB_START, + MIPI_DCS_READ_DDB_CONTINUE, + 0, /* sentinel */ +}; + +static bool mipi_dbi_command_is_read(struct mipi_dbi *mipi, u8 cmd) +{ + unsigned int i; + + if (!mipi->read_commands) + return false; + + for (i = 0; i < 0xff; i++) { + if (!mipi->read_commands[i]) + return false; + if (cmd == mipi->read_commands[i]) + return true; + } + + return false; +} + +/** + * mipi_dbi_command_read - MIPI DCS read command + * @mipi: MIPI structure + * @cmd: Command + * @val: Value read + * + * Send MIPI DCS read command to the controller. + * + * Returns: + * Zero on success, negative error code on failure. + */ +int mipi_dbi_command_read(struct mipi_dbi *mipi, u8 cmd, u8 *val) +{ + if (!mipi->read_commands) + return -EACCES; + + if (!mipi_dbi_command_is_read(mipi, cmd)) + return -EINVAL; + + return mipi_dbi_command_buf(mipi, cmd, val, 1); +} +EXPORT_SYMBOL(mipi_dbi_command_read); + +/** + * mipi_dbi_command_buf - MIPI DCS command with parameter(s) in an array + * @mipi: MIPI structure + * @cmd: Command + * @data: Parameter buffer + * @len: Buffer length + * + * Returns: + * Zero on success, negative error code on failure. + */ +int mipi_dbi_command_buf(struct mipi_dbi *mipi, u8 cmd, u8 *data, size_t len) +{ + u8 *cmdbuf; + int ret; + + /* SPI requires dma-safe buffers */ + cmdbuf = kmemdup(&cmd, 1, GFP_KERNEL); + if (!cmdbuf) + return -ENOMEM; + + mutex_lock(&mipi->cmdlock); + ret = mipi->command(mipi, cmdbuf, data, len); + mutex_unlock(&mipi->cmdlock); + + kfree(cmdbuf); + + return ret; +} +EXPORT_SYMBOL(mipi_dbi_command_buf); + +/* This should only be used by mipi_dbi_command() */ +int mipi_dbi_command_stackbuf(struct mipi_dbi *mipi, u8 cmd, u8 *data, size_t len) +{ + u8 *buf; + int ret; + + buf = kmemdup(data, len, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + ret = mipi_dbi_command_buf(mipi, cmd, buf, len); + + kfree(buf); + + return ret; +} +EXPORT_SYMBOL(mipi_dbi_command_stackbuf); + +/** + * mipi_dbi_buf_copy - Copy a framebuffer, transforming it if necessary + * @dst: The destination buffer + * @fb: The source framebuffer + * @clip: Clipping rectangle of the area to be copied + * @swap: When true, swap MSB/LSB of 16-bit values + * + * Returns: + * Zero on success, negative error code on failure. + */ +int mipi_dbi_buf_copy(void *dst, struct drm_framebuffer *fb, + struct drm_clip_rect *clip, bool swap) +{ + struct drm_gem_cma_object *cma_obj = drm_fb_cma_get_gem_obj(fb, 0); + struct dma_buf_attachment *import_attach = cma_obj->base.import_attach; + struct drm_format_name_buf format_name; + void *src = cma_obj->vaddr; + int ret = 0; + + if (import_attach) { + ret = dma_buf_begin_cpu_access(import_attach->dmabuf, + DMA_FROM_DEVICE); + if (ret) + return ret; + } + + switch (fb->format->format) { + case DRM_FORMAT_RGB565: + if (swap) + tinydrm_swab16(dst, src, fb, clip); + else + tinydrm_memcpy(dst, src, fb, clip); + break; + case DRM_FORMAT_XRGB8888: + tinydrm_xrgb8888_to_rgb565(dst, src, fb, clip, swap); + break; + default: + dev_err_once(fb->dev->dev, "Format is not supported: %s\n", + drm_get_format_name(fb->format->format, + &format_name)); + return -EINVAL; + } + + if (import_attach) + ret = dma_buf_end_cpu_access(import_attach->dmabuf, + DMA_FROM_DEVICE); + return ret; +} +EXPORT_SYMBOL(mipi_dbi_buf_copy); + +static int mipi_dbi_fb_dirty(struct drm_framebuffer *fb, + struct drm_file *file_priv, + unsigned int flags, unsigned int color, + struct drm_clip_rect *clips, + unsigned int num_clips) +{ + struct drm_gem_cma_object *cma_obj = drm_fb_cma_get_gem_obj(fb, 0); + struct tinydrm_device *tdev = fb->dev->dev_private; + struct mipi_dbi *mipi = mipi_dbi_from_tinydrm(tdev); + bool swap = mipi->swap_bytes; + struct drm_clip_rect clip; + int ret = 0; + bool full; + void *tr; + + if (!mipi->enabled) + return 0; + + full = tinydrm_merge_clips(&clip, clips, num_clips, flags, + fb->width, fb->height); + + DRM_DEBUG("Flushing [FB:%d] x1=%u, x2=%u, y1=%u, y2=%u\n", fb->base.id, + clip.x1, clip.x2, clip.y1, clip.y2); + + if (!mipi->dc || !full || swap || + fb->format->format == DRM_FORMAT_XRGB8888) { + tr = mipi->tx_buf; + ret = mipi_dbi_buf_copy(mipi->tx_buf, fb, &clip, swap); + if (ret) + return ret; + } else { + tr = cma_obj->vaddr; + } + + mipi_dbi_command(mipi, MIPI_DCS_SET_COLUMN_ADDRESS, + (clip.x1 >> 8) & 0xFF, clip.x1 & 0xFF, + (clip.x2 >> 8) & 0xFF, (clip.x2 - 1) & 0xFF); + mipi_dbi_command(mipi, MIPI_DCS_SET_PAGE_ADDRESS, + (clip.y1 >> 8) & 0xFF, clip.y1 & 0xFF, + (clip.y2 >> 8) & 0xFF, (clip.y2 - 1) & 0xFF); + + ret = mipi_dbi_command_buf(mipi, MIPI_DCS_WRITE_MEMORY_START, tr, + (clip.x2 - clip.x1) * (clip.y2 - clip.y1) * 2); + + return ret; +} + +static const struct drm_framebuffer_funcs mipi_dbi_fb_funcs = { + .destroy = drm_gem_fb_destroy, + .create_handle = drm_gem_fb_create_handle, + .dirty = tinydrm_fb_dirty, +}; + +/** + * mipi_dbi_enable_flush - MIPI DBI enable helper + * @mipi: MIPI DBI structure + * @crtc_state: CRTC state + * @plane_state: Plane state + * + * This function sets &mipi_dbi->enabled, flushes the whole framebuffer and + * enables the backlight. Drivers can use this in their + * &drm_simple_display_pipe_funcs->enable callback. + */ +void mipi_dbi_enable_flush(struct mipi_dbi *mipi, + struct drm_crtc_state *crtc_state, + struct drm_plane_state *plane_state) +{ + struct tinydrm_device *tdev = &mipi->tinydrm; + struct drm_framebuffer *fb = plane_state->fb; + + mipi->enabled = true; + if (fb) + tdev->fb_dirty(fb, NULL, 0, 0, NULL, 0); + + backlight_enable(mipi->backlight); +} +EXPORT_SYMBOL(mipi_dbi_enable_flush); + +static void mipi_dbi_blank(struct mipi_dbi *mipi) +{ + struct drm_device *drm = mipi->tinydrm.drm; + u16 height = drm->mode_config.min_height; + u16 width = drm->mode_config.min_width; + size_t len = width * height * 2; + + memset(mipi->tx_buf, 0, len); + + mipi_dbi_command(mipi, MIPI_DCS_SET_COLUMN_ADDRESS, 0, 0, + (width >> 8) & 0xFF, (width - 1) & 0xFF); + mipi_dbi_command(mipi, MIPI_DCS_SET_PAGE_ADDRESS, 0, 0, + (height >> 8) & 0xFF, (height - 1) & 0xFF); + mipi_dbi_command_buf(mipi, MIPI_DCS_WRITE_MEMORY_START, + (u8 *)mipi->tx_buf, len); +} + +/** + * mipi_dbi_pipe_disable - MIPI DBI pipe disable helper + * @pipe: Display pipe + * + * This function disables backlight if present, if not the display memory is + * blanked. The regulator is disabled if in use. Drivers can use this as their + * &drm_simple_display_pipe_funcs->disable callback. + */ +void mipi_dbi_pipe_disable(struct drm_simple_display_pipe *pipe) +{ + struct tinydrm_device *tdev = pipe_to_tinydrm(pipe); + struct mipi_dbi *mipi = mipi_dbi_from_tinydrm(tdev); + + DRM_DEBUG_KMS("\n"); + + mipi->enabled = false; + + if (mipi->backlight) + backlight_disable(mipi->backlight); + else + mipi_dbi_blank(mipi); + + if (mipi->regulator) + regulator_disable(mipi->regulator); +} +EXPORT_SYMBOL(mipi_dbi_pipe_disable); + +static const uint32_t mipi_dbi_formats[] = { + DRM_FORMAT_RGB565, + DRM_FORMAT_XRGB8888, +}; + +/** + * mipi_dbi_init - MIPI DBI initialization + * @dev: Parent device + * @mipi: &mipi_dbi structure to initialize + * @pipe_funcs: Display pipe functions + * @driver: DRM driver + * @mode: Display mode + * @rotation: Initial rotation in degrees Counter Clock Wise + * + * This function initializes a &mipi_dbi structure and it's underlying + * @tinydrm_device. It also sets up the display pipeline. + * + * Supported formats: Native RGB565 and emulated XRGB8888. + * + * Objects created by this function will be automatically freed on driver + * detach (devres). + * + * Returns: + * Zero on success, negative error code on failure. + */ +int mipi_dbi_init(struct device *dev, struct mipi_dbi *mipi, + const struct drm_simple_display_pipe_funcs *pipe_funcs, + struct drm_driver *driver, + const struct drm_display_mode *mode, unsigned int rotation) +{ + size_t bufsize = mode->vdisplay * mode->hdisplay * sizeof(u16); + struct tinydrm_device *tdev = &mipi->tinydrm; + int ret; + + if (!mipi->command) + return -EINVAL; + + mutex_init(&mipi->cmdlock); + + mipi->tx_buf = devm_kmalloc(dev, bufsize, GFP_KERNEL); + if (!mipi->tx_buf) + return -ENOMEM; + + ret = devm_tinydrm_init(dev, tdev, &mipi_dbi_fb_funcs, driver); + if (ret) + return ret; + + tdev->fb_dirty = mipi_dbi_fb_dirty; + + /* TODO: Maybe add DRM_MODE_CONNECTOR_SPI */ + ret = tinydrm_display_pipe_init(tdev, pipe_funcs, + DRM_MODE_CONNECTOR_VIRTUAL, + mipi_dbi_formats, + ARRAY_SIZE(mipi_dbi_formats), mode, + rotation); + if (ret) + return ret; + + tdev->drm->mode_config.preferred_depth = 16; + mipi->rotation = rotation; + + drm_mode_config_reset(tdev->drm); + + DRM_DEBUG_KMS("preferred_depth=%u, rotation = %u\n", + tdev->drm->mode_config.preferred_depth, rotation); + + return 0; +} +EXPORT_SYMBOL(mipi_dbi_init); + +/** + * mipi_dbi_hw_reset - Hardware reset of controller + * @mipi: MIPI DBI structure + * + * Reset controller if the &mipi_dbi->reset gpio is set. + */ +void mipi_dbi_hw_reset(struct mipi_dbi *mipi) +{ + if (!mipi->reset) + return; + + gpiod_set_value_cansleep(mipi->reset, 0); + usleep_range(20, 1000); + gpiod_set_value_cansleep(mipi->reset, 1); + msleep(120); +} +EXPORT_SYMBOL(mipi_dbi_hw_reset); + +/** + * mipi_dbi_display_is_on - Check if display is on + * @mipi: MIPI DBI structure + * + * This function checks the Power Mode register (if readable) to see if + * display output is turned on. This can be used to see if the bootloader + * has already turned on the display avoiding flicker when the pipeline is + * enabled. + * + * Returns: + * true if the display can be verified to be on, false otherwise. + */ +bool mipi_dbi_display_is_on(struct mipi_dbi *mipi) +{ + u8 val; + + if (mipi_dbi_command_read(mipi, MIPI_DCS_GET_POWER_MODE, &val)) + return false; + + val &= ~DCS_POWER_MODE_RESERVED_MASK; + + /* The poweron/reset value is 08h DCS_POWER_MODE_DISPLAY_NORMAL_MODE */ + if (val != (DCS_POWER_MODE_DISPLAY | + DCS_POWER_MODE_DISPLAY_NORMAL_MODE | DCS_POWER_MODE_SLEEP_MODE)) + return false; + + DRM_DEBUG_DRIVER("Display is ON\n"); + + return true; +} +EXPORT_SYMBOL(mipi_dbi_display_is_on); + +static int mipi_dbi_poweron_reset_conditional(struct mipi_dbi *mipi, bool cond) +{ + struct device *dev = mipi->tinydrm.drm->dev; + int ret; + + if (mipi->regulator) { + ret = regulator_enable(mipi->regulator); + if (ret) { + DRM_DEV_ERROR(dev, "Failed to enable regulator (%d)\n", ret); + return ret; + } + } + + if (cond && mipi_dbi_display_is_on(mipi)) + return 1; + + mipi_dbi_hw_reset(mipi); + ret = mipi_dbi_command(mipi, MIPI_DCS_SOFT_RESET); + if (ret) { + DRM_DEV_ERROR(dev, "Failed to send reset command (%d)\n", ret); + if (mipi->regulator) + regulator_disable(mipi->regulator); + return ret; + } + + /* + * If we did a hw reset, we know the controller is in Sleep mode and + * per MIPI DSC spec should wait 5ms after soft reset. If we didn't, + * we assume worst case and wait 120ms. + */ + if (mipi->reset) + usleep_range(5000, 20000); + else + msleep(120); + + return 0; +} + +/** + * mipi_dbi_poweron_reset - MIPI DBI poweron and reset + * @mipi: MIPI DBI structure + * + * This function enables the regulator if used and does a hardware and software + * reset. + * + * Returns: + * Zero on success, or a negative error code. + */ +int mipi_dbi_poweron_reset(struct mipi_dbi *mipi) +{ + return mipi_dbi_poweron_reset_conditional(mipi, false); +} +EXPORT_SYMBOL(mipi_dbi_poweron_reset); + +/** + * mipi_dbi_poweron_conditional_reset - MIPI DBI poweron and conditional reset + * @mipi: MIPI DBI structure + * + * This function enables the regulator if used and if the display is off, it + * does a hardware and software reset. If mipi_dbi_display_is_on() determines + * that the display is on, no reset is performed. + * + * Returns: + * Zero if the controller was reset, 1 if the display was already on, or a + * negative error code. + */ +int mipi_dbi_poweron_conditional_reset(struct mipi_dbi *mipi) +{ + return mipi_dbi_poweron_reset_conditional(mipi, true); +} +EXPORT_SYMBOL(mipi_dbi_poweron_conditional_reset); + +#if IS_ENABLED(CONFIG_SPI) + +/** + * mipi_dbi_spi_cmd_max_speed - get the maximum SPI bus speed + * @spi: SPI device + * @len: The transfer buffer length. + * + * Many controllers have a max speed of 10MHz, but can be pushed way beyond + * that. Increase reliability by running pixel data at max speed and the rest + * at 10MHz, preventing transfer glitches from messing up the init settings. + */ +u32 mipi_dbi_spi_cmd_max_speed(struct spi_device *spi, size_t len) +{ + if (len > 64) + return 0; /* use default */ + + return min_t(u32, 10000000, spi->max_speed_hz); +} +EXPORT_SYMBOL(mipi_dbi_spi_cmd_max_speed); + +/* + * MIPI DBI Type C Option 1 + * + * If the SPI controller doesn't have 9 bits per word support, + * use blocks of 9 bytes to send 8x 9-bit words using a 8-bit SPI transfer. + * Pad partial blocks with MIPI_DCS_NOP (zero). + * This is how the D/C bit (x) is added: + * x7654321 + * 0x765432 + * 10x76543 + * 210x7654 + * 3210x765 + * 43210x76 + * 543210x7 + * 6543210x + * 76543210 + */ + +static int mipi_dbi_spi1e_transfer(struct mipi_dbi *mipi, int dc, + const void *buf, size_t len, + unsigned int bpw) +{ + bool swap_bytes = (bpw == 16 && tinydrm_machine_little_endian()); + size_t chunk, max_chunk = mipi->tx_buf9_len; + struct spi_device *spi = mipi->spi; + struct spi_transfer tr = { + .tx_buf = mipi->tx_buf9, + .bits_per_word = 8, + }; + struct spi_message m; + const u8 *src = buf; + int i, ret; + u8 *dst; + + if (drm_debug & DRM_UT_DRIVER) + pr_debug("[drm:%s] dc=%d, max_chunk=%zu, transfers:\n", + __func__, dc, max_chunk); + + tr.speed_hz = mipi_dbi_spi_cmd_max_speed(spi, len); + spi_message_init_with_transfers(&m, &tr, 1); + + if (!dc) { + if (WARN_ON_ONCE(len != 1)) + return -EINVAL; + + /* Command: pad no-op's (zeroes) at beginning of block */ + dst = mipi->tx_buf9; + memset(dst, 0, 9); + dst[8] = *src; + tr.len = 9; + + tinydrm_dbg_spi_message(spi, &m); + + return spi_sync(spi, &m); + } + + /* max with room for adding one bit per byte */ + max_chunk = max_chunk / 9 * 8; + /* but no bigger than len */ + max_chunk = min(max_chunk, len); + /* 8 byte blocks */ + max_chunk = max_t(size_t, 8, max_chunk & ~0x7); + + while (len) { + size_t added = 0; + + chunk = min(len, max_chunk); + len -= chunk; + dst = mipi->tx_buf9; + + if (chunk < 8) { + u8 val, carry = 0; + + /* Data: pad no-op's (zeroes) at end of block */ + memset(dst, 0, 9); + + if (swap_bytes) { + for (i = 1; i < (chunk + 1); i++) { + val = src[1]; + *dst++ = carry | BIT(8 - i) | (val >> i); + carry = val << (8 - i); + i++; + val = src[0]; + *dst++ = carry | BIT(8 - i) | (val >> i); + carry = val << (8 - i); + src += 2; + } + *dst++ = carry; + } else { + for (i = 1; i < (chunk + 1); i++) { + val = *src++; + *dst++ = carry | BIT(8 - i) | (val >> i); + carry = val << (8 - i); + } + *dst++ = carry; + } + + chunk = 8; + added = 1; + } else { + for (i = 0; i < chunk; i += 8) { + if (swap_bytes) { + *dst++ = BIT(7) | (src[1] >> 1); + *dst++ = (src[1] << 7) | BIT(6) | (src[0] >> 2); + *dst++ = (src[0] << 6) | BIT(5) | (src[3] >> 3); + *dst++ = (src[3] << 5) | BIT(4) | (src[2] >> 4); + *dst++ = (src[2] << 4) | BIT(3) | (src[5] >> 5); + *dst++ = (src[5] << 3) | BIT(2) | (src[4] >> 6); + *dst++ = (src[4] << 2) | BIT(1) | (src[7] >> 7); + *dst++ = (src[7] << 1) | BIT(0); + *dst++ = src[6]; + } else { + *dst++ = BIT(7) | (src[0] >> 1); + *dst++ = (src[0] << 7) | BIT(6) | (src[1] >> 2); + *dst++ = (src[1] << 6) | BIT(5) | (src[2] >> 3); + *dst++ = (src[2] << 5) | BIT(4) | (src[3] >> 4); + *dst++ = (src[3] << 4) | BIT(3) | (src[4] >> 5); + *dst++ = (src[4] << 3) | BIT(2) | (src[5] >> 6); + *dst++ = (src[5] << 2) | BIT(1) | (src[6] >> 7); + *dst++ = (src[6] << 1) | BIT(0); + *dst++ = src[7]; + } + + src += 8; + added++; + } + } + + tr.len = chunk + added; + + tinydrm_dbg_spi_message(spi, &m); + ret = spi_sync(spi, &m); + if (ret) + return ret; + } + + return 0; +} + +static int mipi_dbi_spi1_transfer(struct mipi_dbi *mipi, int dc, + const void *buf, size_t len, + unsigned int bpw) +{ + struct spi_device *spi = mipi->spi; + struct spi_transfer tr = { + .bits_per_word = 9, + }; + const u16 *src16 = buf; + const u8 *src8 = buf; + struct spi_message m; + size_t max_chunk; + u16 *dst16; + int ret; + + if (!tinydrm_spi_bpw_supported(spi, 9)) + return mipi_dbi_spi1e_transfer(mipi, dc, buf, len, bpw); + + tr.speed_hz = mipi_dbi_spi_cmd_max_speed(spi, len); + max_chunk = mipi->tx_buf9_len; + dst16 = mipi->tx_buf9; + + if (drm_debug & DRM_UT_DRIVER) + pr_debug("[drm:%s] dc=%d, max_chunk=%zu, transfers:\n", + __func__, dc, max_chunk); + + max_chunk = min(max_chunk / 2, len); + + spi_message_init_with_transfers(&m, &tr, 1); + tr.tx_buf = dst16; + + while (len) { + size_t chunk = min(len, max_chunk); + unsigned int i; + + if (bpw == 16 && tinydrm_machine_little_endian()) { + for (i = 0; i < (chunk * 2); i += 2) { + dst16[i] = *src16 >> 8; + dst16[i + 1] = *src16++ & 0xFF; + if (dc) { + dst16[i] |= 0x0100; + dst16[i + 1] |= 0x0100; + } + } + } else { + for (i = 0; i < chunk; i++) { + dst16[i] = *src8++; + if (dc) + dst16[i] |= 0x0100; + } + } + + tr.len = chunk; + len -= chunk; + + tinydrm_dbg_spi_message(spi, &m); + ret = spi_sync(spi, &m); + if (ret) + return ret; + } + + return 0; +} + +static int mipi_dbi_typec1_command(struct mipi_dbi *mipi, u8 *cmd, + u8 *parameters, size_t num) +{ + unsigned int bpw = (*cmd == MIPI_DCS_WRITE_MEMORY_START) ? 16 : 8; + int ret; + + if (mipi_dbi_command_is_read(mipi, *cmd)) + return -ENOTSUPP; + + MIPI_DBI_DEBUG_COMMAND(*cmd, parameters, num); + + ret = mipi_dbi_spi1_transfer(mipi, 0, cmd, 1, 8); + if (ret || !num) + return ret; + + return mipi_dbi_spi1_transfer(mipi, 1, parameters, num, bpw); +} + +/* MIPI DBI Type C Option 3 */ + +static int mipi_dbi_typec3_command_read(struct mipi_dbi *mipi, u8 *cmd, + u8 *data, size_t len) +{ + struct spi_device *spi = mipi->spi; + u32 speed_hz = min_t(u32, MIPI_DBI_MAX_SPI_READ_SPEED, + spi->max_speed_hz / 2); + struct spi_transfer tr[2] = { + { + .speed_hz = speed_hz, + .tx_buf = cmd, + .len = 1, + }, { + .speed_hz = speed_hz, + .len = len, + }, + }; + struct spi_message m; + u8 *buf; + int ret; + + if (!len) + return -EINVAL; + + /* + * Support non-standard 24-bit and 32-bit Nokia read commands which + * start with a dummy clock, so we need to read an extra byte. + */ + if (*cmd == MIPI_DCS_GET_DISPLAY_ID || + *cmd == MIPI_DCS_GET_DISPLAY_STATUS) { + if (!(len == 3 || len == 4)) + return -EINVAL; + + tr[1].len = len + 1; + } + + buf = kmalloc(tr[1].len, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + tr[1].rx_buf = buf; + gpiod_set_value_cansleep(mipi->dc, 0); + + spi_message_init_with_transfers(&m, tr, ARRAY_SIZE(tr)); + ret = spi_sync(spi, &m); + if (ret) + goto err_free; + + tinydrm_dbg_spi_message(spi, &m); + + if (tr[1].len == len) { + memcpy(data, buf, len); + } else { + unsigned int i; + + for (i = 0; i < len; i++) + data[i] = (buf[i] << 1) | !!(buf[i + 1] & BIT(7)); + } + + MIPI_DBI_DEBUG_COMMAND(*cmd, data, len); + +err_free: + kfree(buf); + + return ret; +} + +static int mipi_dbi_typec3_command(struct mipi_dbi *mipi, u8 *cmd, + u8 *par, size_t num) +{ + struct spi_device *spi = mipi->spi; + unsigned int bpw = 8; + u32 speed_hz; + int ret; + + if (mipi_dbi_command_is_read(mipi, *cmd)) + return mipi_dbi_typec3_command_read(mipi, cmd, par, num); + + MIPI_DBI_DEBUG_COMMAND(*cmd, par, num); + + gpiod_set_value_cansleep(mipi->dc, 0); + speed_hz = mipi_dbi_spi_cmd_max_speed(spi, 1); + ret = tinydrm_spi_transfer(spi, speed_hz, NULL, 8, cmd, 1); + if (ret || !num) + return ret; + + if (*cmd == MIPI_DCS_WRITE_MEMORY_START && !mipi->swap_bytes) + bpw = 16; + + gpiod_set_value_cansleep(mipi->dc, 1); + speed_hz = mipi_dbi_spi_cmd_max_speed(spi, num); + + return tinydrm_spi_transfer(spi, speed_hz, NULL, bpw, par, num); +} + +/** + * mipi_dbi_spi_init - Initialize MIPI DBI SPI interfaced controller + * @spi: SPI device + * @mipi: &mipi_dbi structure to initialize + * @dc: D/C gpio (optional) + * + * This function sets &mipi_dbi->command, enables &mipi->read_commands for the + * usual read commands. It should be followed by a call to mipi_dbi_init() or + * a driver-specific init. + * + * If @dc is set, a Type C Option 3 interface is assumed, if not + * Type C Option 1. + * + * If the SPI master driver doesn't support the necessary bits per word, + * the following transformation is used: + * + * - 9-bit: reorder buffer as 9x 8-bit words, padded with no-op command. + * - 16-bit: if big endian send as 8-bit, if little endian swap bytes + * + * Returns: + * Zero on success, negative error code on failure. + */ +int mipi_dbi_spi_init(struct spi_device *spi, struct mipi_dbi *mipi, + struct gpio_desc *dc) +{ + size_t tx_size = tinydrm_spi_max_transfer_size(spi, 0); + struct device *dev = &spi->dev; + int ret; + + if (tx_size < 16) { + DRM_ERROR("SPI transmit buffer too small: %zu\n", tx_size); + return -EINVAL; + } + + /* + * Even though it's not the SPI device that does DMA (the master does), + * the dma mask is necessary for the dma_alloc_wc() in + * drm_gem_cma_create(). The dma_addr returned will be a physical + * adddress which might be different from the bus address, but this is + * not a problem since the address will not be used. + * The virtual address is used in the transfer and the SPI core + * re-maps it on the SPI master device using the DMA streaming API + * (spi_map_buf()). + */ + if (!dev->coherent_dma_mask) { + ret = dma_coerce_mask_and_coherent(dev, DMA_BIT_MASK(32)); + if (ret) { + dev_warn(dev, "Failed to set dma mask %d\n", ret); + return ret; + } + } + + mipi->spi = spi; + mipi->read_commands = mipi_dbi_dcs_read_commands; + + if (dc) { + mipi->command = mipi_dbi_typec3_command; + mipi->dc = dc; + if (tinydrm_machine_little_endian() && + !tinydrm_spi_bpw_supported(spi, 16)) + mipi->swap_bytes = true; + } else { + mipi->command = mipi_dbi_typec1_command; + mipi->tx_buf9_len = tx_size; + mipi->tx_buf9 = devm_kmalloc(dev, tx_size, GFP_KERNEL); + if (!mipi->tx_buf9) + return -ENOMEM; + } + + DRM_DEBUG_DRIVER("SPI speed: %uMHz\n", spi->max_speed_hz / 1000000); + + return 0; +} +EXPORT_SYMBOL(mipi_dbi_spi_init); + +#endif /* CONFIG_SPI */ + +#ifdef CONFIG_DEBUG_FS + +static ssize_t mipi_dbi_debugfs_command_write(struct file *file, + const char __user *ubuf, + size_t count, loff_t *ppos) +{ + struct seq_file *m = file->private_data; + struct mipi_dbi *mipi = m->private; + u8 val, cmd = 0, parameters[64]; + char *buf, *pos, *token; + unsigned int i; + int ret; + + buf = memdup_user_nul(ubuf, count); + if (IS_ERR(buf)) + return PTR_ERR(buf); + + /* strip trailing whitespace */ + for (i = count - 1; i > 0; i--) + if (isspace(buf[i])) + buf[i] = '\0'; + else + break; + i = 0; + pos = buf; + while (pos) { + token = strsep(&pos, " "); + if (!token) { + ret = -EINVAL; + goto err_free; + } + + ret = kstrtou8(token, 16, &val); + if (ret < 0) + goto err_free; + + if (token == buf) + cmd = val; + else + parameters[i++] = val; + + if (i == 64) { + ret = -E2BIG; + goto err_free; + } + } + + ret = mipi_dbi_command_buf(mipi, cmd, parameters, i); + +err_free: + kfree(buf); + + return ret < 0 ? ret : count; +} + +static int mipi_dbi_debugfs_command_show(struct seq_file *m, void *unused) +{ + struct mipi_dbi *mipi = m->private; + u8 cmd, val[4]; + size_t len; + int ret; + + for (cmd = 0; cmd < 255; cmd++) { + if (!mipi_dbi_command_is_read(mipi, cmd)) + continue; + + switch (cmd) { + case MIPI_DCS_READ_MEMORY_START: + case MIPI_DCS_READ_MEMORY_CONTINUE: + len = 2; + break; + case MIPI_DCS_GET_DISPLAY_ID: + len = 3; + break; + case MIPI_DCS_GET_DISPLAY_STATUS: + len = 4; + break; + default: + len = 1; + break; + } + + seq_printf(m, "%02x: ", cmd); + ret = mipi_dbi_command_buf(mipi, cmd, val, len); + if (ret) { + seq_puts(m, "XX\n"); + continue; + } + seq_printf(m, "%*phN\n", (int)len, val); + } + + return 0; +} + +static int mipi_dbi_debugfs_command_open(struct inode *inode, + struct file *file) +{ + return single_open(file, mipi_dbi_debugfs_command_show, + inode->i_private); +} + +static const struct file_operations mipi_dbi_debugfs_command_fops = { + .owner = THIS_MODULE, + .open = mipi_dbi_debugfs_command_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, + .write = mipi_dbi_debugfs_command_write, +}; + +/** + * mipi_dbi_debugfs_init - Create debugfs entries + * @minor: DRM minor + * + * This function creates a 'command' debugfs file for sending commands to the + * controller or getting the read command values. + * Drivers can use this as their &drm_driver->debugfs_init callback. + * + * Returns: + * Zero on success, negative error code on failure. + */ +int mipi_dbi_debugfs_init(struct drm_minor *minor) +{ + struct tinydrm_device *tdev = minor->dev->dev_private; + struct mipi_dbi *mipi = mipi_dbi_from_tinydrm(tdev); + umode_t mode = S_IFREG | S_IWUSR; + + if (mipi->read_commands) + mode |= S_IRUGO; + debugfs_create_file("command", mode, minor->debugfs_root, mipi, + &mipi_dbi_debugfs_command_fops); + + return 0; +} +EXPORT_SYMBOL(mipi_dbi_debugfs_init); + +#endif + +MODULE_LICENSE("GPL"); diff --git a/drivers/gpu/drm/tinydrm/repaper.c b/drivers/gpu/drm/tinydrm/repaper.c new file mode 100644 index 000000000..50a1d4216 --- /dev/null +++ b/drivers/gpu/drm/tinydrm/repaper.c @@ -0,0 +1,1099 @@ +/* + * DRM driver for Pervasive Displays RePaper branded e-ink panels + * + * Copyright 2013-2017 Pervasive Displays, Inc. + * Copyright 2017 Noralf Trønnes + * + * The driver supports: + * Material Film: Aurora Mb (V231) + * Driver IC: G2 (eTC) + * + * The controller code was taken from the userspace driver: + * https://github.com/repaper/gratis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include <linux/delay.h> +#include <linux/dma-buf.h> +#include <linux/gpio/consumer.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/sched/clock.h> +#include <linux/spi/spi.h> +#include <linux/thermal.h> + +#include <drm/drm_gem_framebuffer_helper.h> +#include <drm/tinydrm/tinydrm.h> +#include <drm/tinydrm/tinydrm-helpers.h> + +#define REPAPER_RID_G2_COG_ID 0x12 + +enum repaper_model { + E1144CS021 = 1, + E1190CS021, + E2200CS021, + E2271CS021, +}; + +enum repaper_stage { /* Image pixel -> Display pixel */ + REPAPER_COMPENSATE, /* B -> W, W -> B (Current Image) */ + REPAPER_WHITE, /* B -> N, W -> W (Current Image) */ + REPAPER_INVERSE, /* B -> N, W -> B (New Image) */ + REPAPER_NORMAL /* B -> B, W -> W (New Image) */ +}; + +enum repaper_epd_border_byte { + REPAPER_BORDER_BYTE_NONE, + REPAPER_BORDER_BYTE_ZERO, + REPAPER_BORDER_BYTE_SET, +}; + +struct repaper_epd { + struct tinydrm_device tinydrm; + struct spi_device *spi; + + struct gpio_desc *panel_on; + struct gpio_desc *border; + struct gpio_desc *discharge; + struct gpio_desc *reset; + struct gpio_desc *busy; + + struct thermal_zone_device *thermal; + + unsigned int height; + unsigned int width; + unsigned int bytes_per_scan; + const u8 *channel_select; + unsigned int stage_time; + unsigned int factored_stage_time; + bool middle_scan; + bool pre_border_byte; + enum repaper_epd_border_byte border_byte; + + u8 *line_buffer; + void *current_frame; + + bool enabled; + bool cleared; + bool partial; +}; + +static inline struct repaper_epd * +epd_from_tinydrm(struct tinydrm_device *tdev) +{ + return container_of(tdev, struct repaper_epd, tinydrm); +} + +static int repaper_spi_transfer(struct spi_device *spi, u8 header, + const void *tx, void *rx, size_t len) +{ + void *txbuf = NULL, *rxbuf = NULL; + struct spi_transfer tr[2] = {}; + u8 *headerbuf; + int ret; + + headerbuf = kmalloc(1, GFP_KERNEL); + if (!headerbuf) + return -ENOMEM; + + headerbuf[0] = header; + tr[0].tx_buf = headerbuf; + tr[0].len = 1; + + /* Stack allocated tx? */ + if (tx && len <= 32) { + txbuf = kmalloc(len, GFP_KERNEL); + if (!txbuf) { + ret = -ENOMEM; + goto out_free; + } + memcpy(txbuf, tx, len); + } + + if (rx) { + rxbuf = kmalloc(len, GFP_KERNEL); + if (!rxbuf) { + ret = -ENOMEM; + goto out_free; + } + } + + tr[1].tx_buf = txbuf ? txbuf : tx; + tr[1].rx_buf = rxbuf; + tr[1].len = len; + + ndelay(80); + ret = spi_sync_transfer(spi, tr, 2); + if (rx && !ret) + memcpy(rx, rxbuf, len); + +out_free: + kfree(headerbuf); + kfree(txbuf); + kfree(rxbuf); + + return ret; +} + +static int repaper_write_buf(struct spi_device *spi, u8 reg, + const u8 *buf, size_t len) +{ + int ret; + + ret = repaper_spi_transfer(spi, 0x70, ®, NULL, 1); + if (ret) + return ret; + + return repaper_spi_transfer(spi, 0x72, buf, NULL, len); +} + +static int repaper_write_val(struct spi_device *spi, u8 reg, u8 val) +{ + return repaper_write_buf(spi, reg, &val, 1); +} + +static int repaper_read_val(struct spi_device *spi, u8 reg) +{ + int ret; + u8 val; + + ret = repaper_spi_transfer(spi, 0x70, ®, NULL, 1); + if (ret) + return ret; + + ret = repaper_spi_transfer(spi, 0x73, NULL, &val, 1); + + return ret ? ret : val; +} + +static int repaper_read_id(struct spi_device *spi) +{ + int ret; + u8 id; + + ret = repaper_spi_transfer(spi, 0x71, NULL, &id, 1); + + return ret ? ret : id; +} + +static void repaper_spi_mosi_low(struct spi_device *spi) +{ + const u8 buf[1] = { 0 }; + + spi_write(spi, buf, 1); +} + +/* pixels on display are numbered from 1 so even is actually bits 1,3,5,... */ +static void repaper_even_pixels(struct repaper_epd *epd, u8 **pp, + const u8 *data, u8 fixed_value, const u8 *mask, + enum repaper_stage stage) +{ + unsigned int b; + + for (b = 0; b < (epd->width / 8); b++) { + if (data) { + u8 pixels = data[b] & 0xaa; + u8 pixel_mask = 0xff; + u8 p1, p2, p3, p4; + + if (mask) { + pixel_mask = (mask[b] ^ pixels) & 0xaa; + pixel_mask |= pixel_mask >> 1; + } + + switch (stage) { + case REPAPER_COMPENSATE: /* B -> W, W -> B (Current) */ + pixels = 0xaa | ((pixels ^ 0xaa) >> 1); + break; + case REPAPER_WHITE: /* B -> N, W -> W (Current) */ + pixels = 0x55 + ((pixels ^ 0xaa) >> 1); + break; + case REPAPER_INVERSE: /* B -> N, W -> B (New) */ + pixels = 0x55 | (pixels ^ 0xaa); + break; + case REPAPER_NORMAL: /* B -> B, W -> W (New) */ + pixels = 0xaa | (pixels >> 1); + break; + } + + pixels = (pixels & pixel_mask) | (~pixel_mask & 0x55); + p1 = (pixels >> 6) & 0x03; + p2 = (pixels >> 4) & 0x03; + p3 = (pixels >> 2) & 0x03; + p4 = (pixels >> 0) & 0x03; + pixels = (p1 << 0) | (p2 << 2) | (p3 << 4) | (p4 << 6); + *(*pp)++ = pixels; + } else { + *(*pp)++ = fixed_value; + } + } +} + +/* pixels on display are numbered from 1 so odd is actually bits 0,2,4,... */ +static void repaper_odd_pixels(struct repaper_epd *epd, u8 **pp, + const u8 *data, u8 fixed_value, const u8 *mask, + enum repaper_stage stage) +{ + unsigned int b; + + for (b = epd->width / 8; b > 0; b--) { + if (data) { + u8 pixels = data[b - 1] & 0x55; + u8 pixel_mask = 0xff; + + if (mask) { + pixel_mask = (mask[b - 1] ^ pixels) & 0x55; + pixel_mask |= pixel_mask << 1; + } + + switch (stage) { + case REPAPER_COMPENSATE: /* B -> W, W -> B (Current) */ + pixels = 0xaa | (pixels ^ 0x55); + break; + case REPAPER_WHITE: /* B -> N, W -> W (Current) */ + pixels = 0x55 + (pixels ^ 0x55); + break; + case REPAPER_INVERSE: /* B -> N, W -> B (New) */ + pixels = 0x55 | ((pixels ^ 0x55) << 1); + break; + case REPAPER_NORMAL: /* B -> B, W -> W (New) */ + pixels = 0xaa | pixels; + break; + } + + pixels = (pixels & pixel_mask) | (~pixel_mask & 0x55); + *(*pp)++ = pixels; + } else { + *(*pp)++ = fixed_value; + } + } +} + +/* interleave bits: (byte)76543210 -> (16 bit).7.6.5.4.3.2.1 */ +static inline u16 repaper_interleave_bits(u16 value) +{ + value = (value | (value << 4)) & 0x0f0f; + value = (value | (value << 2)) & 0x3333; + value = (value | (value << 1)) & 0x5555; + + return value; +} + +/* pixels on display are numbered from 1 */ +static void repaper_all_pixels(struct repaper_epd *epd, u8 **pp, + const u8 *data, u8 fixed_value, const u8 *mask, + enum repaper_stage stage) +{ + unsigned int b; + + for (b = epd->width / 8; b > 0; b--) { + if (data) { + u16 pixels = repaper_interleave_bits(data[b - 1]); + u16 pixel_mask = 0xffff; + + if (mask) { + pixel_mask = repaper_interleave_bits(mask[b - 1]); + + pixel_mask = (pixel_mask ^ pixels) & 0x5555; + pixel_mask |= pixel_mask << 1; + } + + switch (stage) { + case REPAPER_COMPENSATE: /* B -> W, W -> B (Current) */ + pixels = 0xaaaa | (pixels ^ 0x5555); + break; + case REPAPER_WHITE: /* B -> N, W -> W (Current) */ + pixels = 0x5555 + (pixels ^ 0x5555); + break; + case REPAPER_INVERSE: /* B -> N, W -> B (New) */ + pixels = 0x5555 | ((pixels ^ 0x5555) << 1); + break; + case REPAPER_NORMAL: /* B -> B, W -> W (New) */ + pixels = 0xaaaa | pixels; + break; + } + + pixels = (pixels & pixel_mask) | (~pixel_mask & 0x5555); + *(*pp)++ = pixels >> 8; + *(*pp)++ = pixels; + } else { + *(*pp)++ = fixed_value; + *(*pp)++ = fixed_value; + } + } +} + +/* output one line of scan and data bytes to the display */ +static void repaper_one_line(struct repaper_epd *epd, unsigned int line, + const u8 *data, u8 fixed_value, const u8 *mask, + enum repaper_stage stage) +{ + u8 *p = epd->line_buffer; + unsigned int b; + + repaper_spi_mosi_low(epd->spi); + + if (epd->pre_border_byte) + *p++ = 0x00; + + if (epd->middle_scan) { + /* data bytes */ + repaper_odd_pixels(epd, &p, data, fixed_value, mask, stage); + + /* scan line */ + for (b = epd->bytes_per_scan; b > 0; b--) { + if (line / 4 == b - 1) + *p++ = 0x03 << (2 * (line & 0x03)); + else + *p++ = 0x00; + } + + /* data bytes */ + repaper_even_pixels(epd, &p, data, fixed_value, mask, stage); + } else { + /* + * even scan line, but as lines on display are numbered from 1, + * line: 1,3,5,... + */ + for (b = 0; b < epd->bytes_per_scan; b++) { + if (0 != (line & 0x01) && line / 8 == b) + *p++ = 0xc0 >> (line & 0x06); + else + *p++ = 0x00; + } + + /* data bytes */ + repaper_all_pixels(epd, &p, data, fixed_value, mask, stage); + + /* + * odd scan line, but as lines on display are numbered from 1, + * line: 0,2,4,6,... + */ + for (b = epd->bytes_per_scan; b > 0; b--) { + if (0 == (line & 0x01) && line / 8 == b - 1) + *p++ = 0x03 << (line & 0x06); + else + *p++ = 0x00; + } + } + + switch (epd->border_byte) { + case REPAPER_BORDER_BYTE_NONE: + break; + + case REPAPER_BORDER_BYTE_ZERO: + *p++ = 0x00; + break; + + case REPAPER_BORDER_BYTE_SET: + switch (stage) { + case REPAPER_COMPENSATE: + case REPAPER_WHITE: + case REPAPER_INVERSE: + *p++ = 0x00; + break; + case REPAPER_NORMAL: + *p++ = 0xaa; + break; + } + break; + } + + repaper_write_buf(epd->spi, 0x0a, epd->line_buffer, + p - epd->line_buffer); + + /* Output data to panel */ + repaper_write_val(epd->spi, 0x02, 0x07); + + repaper_spi_mosi_low(epd->spi); +} + +static void repaper_frame_fixed(struct repaper_epd *epd, u8 fixed_value, + enum repaper_stage stage) +{ + unsigned int line; + + for (line = 0; line < epd->height; line++) + repaper_one_line(epd, line, NULL, fixed_value, NULL, stage); +} + +static void repaper_frame_data(struct repaper_epd *epd, const u8 *image, + const u8 *mask, enum repaper_stage stage) +{ + unsigned int line; + + if (!mask) { + for (line = 0; line < epd->height; line++) { + repaper_one_line(epd, line, + &image[line * (epd->width / 8)], + 0, NULL, stage); + } + } else { + for (line = 0; line < epd->height; line++) { + size_t n = line * epd->width / 8; + + repaper_one_line(epd, line, &image[n], 0, &mask[n], + stage); + } + } +} + +static void repaper_frame_fixed_repeat(struct repaper_epd *epd, u8 fixed_value, + enum repaper_stage stage) +{ + u64 start = local_clock(); + u64 end = start + (epd->factored_stage_time * 1000 * 1000); + + do { + repaper_frame_fixed(epd, fixed_value, stage); + } while (local_clock() < end); +} + +static void repaper_frame_data_repeat(struct repaper_epd *epd, const u8 *image, + const u8 *mask, enum repaper_stage stage) +{ + u64 start = local_clock(); + u64 end = start + (epd->factored_stage_time * 1000 * 1000); + + do { + repaper_frame_data(epd, image, mask, stage); + } while (local_clock() < end); +} + +static void repaper_get_temperature(struct repaper_epd *epd) +{ + int ret, temperature = 0; + unsigned int factor10x; + + if (!epd->thermal) + return; + + ret = thermal_zone_get_temp(epd->thermal, &temperature); + if (ret) { + DRM_DEV_ERROR(&epd->spi->dev, "Failed to get temperature (%d)\n", ret); + return; + } + + temperature /= 1000; + + if (temperature <= -10) + factor10x = 170; + else if (temperature <= -5) + factor10x = 120; + else if (temperature <= 5) + factor10x = 80; + else if (temperature <= 10) + factor10x = 40; + else if (temperature <= 15) + factor10x = 30; + else if (temperature <= 20) + factor10x = 20; + else if (temperature <= 40) + factor10x = 10; + else + factor10x = 7; + + epd->factored_stage_time = epd->stage_time * factor10x / 10; +} + +static void repaper_gray8_to_mono_reversed(u8 *buf, u32 width, u32 height) +{ + u8 *gray8 = buf, *mono = buf; + int y, xb, i; + + for (y = 0; y < height; y++) + for (xb = 0; xb < width / 8; xb++) { + u8 byte = 0x00; + + for (i = 0; i < 8; i++) { + int x = xb * 8 + i; + + byte >>= 1; + if (gray8[y * width + x] >> 7) + byte |= BIT(7); + } + *mono++ = byte; + } +} + +static int repaper_fb_dirty(struct drm_framebuffer *fb, + struct drm_file *file_priv, + unsigned int flags, unsigned int color, + struct drm_clip_rect *clips, + unsigned int num_clips) +{ + struct drm_gem_cma_object *cma_obj = drm_fb_cma_get_gem_obj(fb, 0); + struct dma_buf_attachment *import_attach = cma_obj->base.import_attach; + struct tinydrm_device *tdev = fb->dev->dev_private; + struct repaper_epd *epd = epd_from_tinydrm(tdev); + struct drm_clip_rect clip; + u8 *buf = NULL; + int ret = 0; + + /* repaper can't do partial updates */ + clip.x1 = 0; + clip.x2 = fb->width; + clip.y1 = 0; + clip.y2 = fb->height; + + if (!epd->enabled) + return 0; + + repaper_get_temperature(epd); + + DRM_DEBUG("Flushing [FB:%d] st=%ums\n", fb->base.id, + epd->factored_stage_time); + + buf = kmalloc_array(fb->width, fb->height, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + if (import_attach) { + ret = dma_buf_begin_cpu_access(import_attach->dmabuf, + DMA_FROM_DEVICE); + if (ret) + goto out_free; + } + + tinydrm_xrgb8888_to_gray8(buf, cma_obj->vaddr, fb, &clip); + + if (import_attach) { + ret = dma_buf_end_cpu_access(import_attach->dmabuf, + DMA_FROM_DEVICE); + if (ret) + goto out_free; + } + + repaper_gray8_to_mono_reversed(buf, fb->width, fb->height); + + if (epd->partial) { + repaper_frame_data_repeat(epd, buf, epd->current_frame, + REPAPER_NORMAL); + } else if (epd->cleared) { + repaper_frame_data_repeat(epd, epd->current_frame, NULL, + REPAPER_COMPENSATE); + repaper_frame_data_repeat(epd, epd->current_frame, NULL, + REPAPER_WHITE); + repaper_frame_data_repeat(epd, buf, NULL, REPAPER_INVERSE); + repaper_frame_data_repeat(epd, buf, NULL, REPAPER_NORMAL); + + epd->partial = true; + } else { + /* Clear display (anything -> white) */ + repaper_frame_fixed_repeat(epd, 0xff, REPAPER_COMPENSATE); + repaper_frame_fixed_repeat(epd, 0xff, REPAPER_WHITE); + repaper_frame_fixed_repeat(epd, 0xaa, REPAPER_INVERSE); + repaper_frame_fixed_repeat(epd, 0xaa, REPAPER_NORMAL); + + /* Assuming a clear (white) screen output an image */ + repaper_frame_fixed_repeat(epd, 0xaa, REPAPER_COMPENSATE); + repaper_frame_fixed_repeat(epd, 0xaa, REPAPER_WHITE); + repaper_frame_data_repeat(epd, buf, NULL, REPAPER_INVERSE); + repaper_frame_data_repeat(epd, buf, NULL, REPAPER_NORMAL); + + epd->cleared = true; + epd->partial = true; + } + + memcpy(epd->current_frame, buf, fb->width * fb->height / 8); + + /* + * An extra frame write is needed if pixels are set in the bottom line, + * or else grey lines rises up from the pixels + */ + if (epd->pre_border_byte) { + unsigned int x; + + for (x = 0; x < (fb->width / 8); x++) + if (buf[x + (fb->width * (fb->height - 1) / 8)]) { + repaper_frame_data_repeat(epd, buf, + epd->current_frame, + REPAPER_NORMAL); + break; + } + } + +out_free: + kfree(buf); + + return ret; +} + +static const struct drm_framebuffer_funcs repaper_fb_funcs = { + .destroy = drm_gem_fb_destroy, + .create_handle = drm_gem_fb_create_handle, + .dirty = tinydrm_fb_dirty, +}; + +static void power_off(struct repaper_epd *epd) +{ + /* Turn off power and all signals */ + gpiod_set_value_cansleep(epd->reset, 0); + gpiod_set_value_cansleep(epd->panel_on, 0); + if (epd->border) + gpiod_set_value_cansleep(epd->border, 0); + + /* Ensure SPI MOSI and CLOCK are Low before CS Low */ + repaper_spi_mosi_low(epd->spi); + + /* Discharge pulse */ + gpiod_set_value_cansleep(epd->discharge, 1); + msleep(150); + gpiod_set_value_cansleep(epd->discharge, 0); +} + +static void repaper_pipe_enable(struct drm_simple_display_pipe *pipe, + struct drm_crtc_state *crtc_state, + struct drm_plane_state *plane_state) +{ + struct tinydrm_device *tdev = pipe_to_tinydrm(pipe); + struct repaper_epd *epd = epd_from_tinydrm(tdev); + struct spi_device *spi = epd->spi; + struct device *dev = &spi->dev; + bool dc_ok = false; + int i, ret; + + DRM_DEBUG_DRIVER("\n"); + + /* Power up sequence */ + gpiod_set_value_cansleep(epd->reset, 0); + gpiod_set_value_cansleep(epd->panel_on, 0); + gpiod_set_value_cansleep(epd->discharge, 0); + if (epd->border) + gpiod_set_value_cansleep(epd->border, 0); + repaper_spi_mosi_low(spi); + usleep_range(5000, 10000); + + gpiod_set_value_cansleep(epd->panel_on, 1); + /* + * This delay comes from the repaper.org userspace driver, it's not + * mentioned in the datasheet. + */ + usleep_range(10000, 15000); + gpiod_set_value_cansleep(epd->reset, 1); + if (epd->border) + gpiod_set_value_cansleep(epd->border, 1); + usleep_range(5000, 10000); + gpiod_set_value_cansleep(epd->reset, 0); + usleep_range(5000, 10000); + gpiod_set_value_cansleep(epd->reset, 1); + usleep_range(5000, 10000); + + /* Wait for COG to become ready */ + for (i = 100; i > 0; i--) { + if (!gpiod_get_value_cansleep(epd->busy)) + break; + + usleep_range(10, 100); + } + + if (!i) { + DRM_DEV_ERROR(dev, "timeout waiting for panel to become ready.\n"); + power_off(epd); + return; + } + + repaper_read_id(spi); + ret = repaper_read_id(spi); + if (ret != REPAPER_RID_G2_COG_ID) { + if (ret < 0) + dev_err(dev, "failed to read chip (%d)\n", ret); + else + dev_err(dev, "wrong COG ID 0x%02x\n", ret); + power_off(epd); + return; + } + + /* Disable OE */ + repaper_write_val(spi, 0x02, 0x40); + + ret = repaper_read_val(spi, 0x0f); + if (ret < 0 || !(ret & 0x80)) { + if (ret < 0) + DRM_DEV_ERROR(dev, "failed to read chip (%d)\n", ret); + else + DRM_DEV_ERROR(dev, "panel is reported broken\n"); + power_off(epd); + return; + } + + /* Power saving mode */ + repaper_write_val(spi, 0x0b, 0x02); + /* Channel select */ + repaper_write_buf(spi, 0x01, epd->channel_select, 8); + /* High power mode osc */ + repaper_write_val(spi, 0x07, 0xd1); + /* Power setting */ + repaper_write_val(spi, 0x08, 0x02); + /* Vcom level */ + repaper_write_val(spi, 0x09, 0xc2); + /* Power setting */ + repaper_write_val(spi, 0x04, 0x03); + /* Driver latch on */ + repaper_write_val(spi, 0x03, 0x01); + /* Driver latch off */ + repaper_write_val(spi, 0x03, 0x00); + usleep_range(5000, 10000); + + /* Start chargepump */ + for (i = 0; i < 4; ++i) { + /* Charge pump positive voltage on - VGH/VDL on */ + repaper_write_val(spi, 0x05, 0x01); + msleep(240); + + /* Charge pump negative voltage on - VGL/VDL on */ + repaper_write_val(spi, 0x05, 0x03); + msleep(40); + + /* Charge pump Vcom on - Vcom driver on */ + repaper_write_val(spi, 0x05, 0x0f); + msleep(40); + + /* check DC/DC */ + ret = repaper_read_val(spi, 0x0f); + if (ret < 0) { + DRM_DEV_ERROR(dev, "failed to read chip (%d)\n", ret); + power_off(epd); + return; + } + + if (ret & 0x40) { + dc_ok = true; + break; + } + } + + if (!dc_ok) { + DRM_DEV_ERROR(dev, "dc/dc failed\n"); + power_off(epd); + return; + } + + /* + * Output enable to disable + * The userspace driver sets this to 0x04, but the datasheet says 0x06 + */ + repaper_write_val(spi, 0x02, 0x04); + + epd->enabled = true; + epd->partial = false; +} + +static void repaper_pipe_disable(struct drm_simple_display_pipe *pipe) +{ + struct tinydrm_device *tdev = pipe_to_tinydrm(pipe); + struct repaper_epd *epd = epd_from_tinydrm(tdev); + struct spi_device *spi = epd->spi; + unsigned int line; + + DRM_DEBUG_DRIVER("\n"); + + mutex_lock(&tdev->dirty_lock); + epd->enabled = false; + mutex_unlock(&tdev->dirty_lock); + + /* Nothing frame */ + for (line = 0; line < epd->height; line++) + repaper_one_line(epd, 0x7fffu, NULL, 0x00, NULL, + REPAPER_COMPENSATE); + + /* 2.7" */ + if (epd->border) { + /* Dummy line */ + repaper_one_line(epd, 0x7fffu, NULL, 0x00, NULL, + REPAPER_COMPENSATE); + msleep(25); + gpiod_set_value_cansleep(epd->border, 0); + msleep(200); + gpiod_set_value_cansleep(epd->border, 1); + } else { + /* Border dummy line */ + repaper_one_line(epd, 0x7fffu, NULL, 0x00, NULL, + REPAPER_NORMAL); + msleep(200); + } + + /* not described in datasheet */ + repaper_write_val(spi, 0x0b, 0x00); + /* Latch reset turn on */ + repaper_write_val(spi, 0x03, 0x01); + /* Power off charge pump Vcom */ + repaper_write_val(spi, 0x05, 0x03); + /* Power off charge pump neg voltage */ + repaper_write_val(spi, 0x05, 0x01); + msleep(120); + /* Discharge internal */ + repaper_write_val(spi, 0x04, 0x80); + /* turn off all charge pumps */ + repaper_write_val(spi, 0x05, 0x00); + /* Turn off osc */ + repaper_write_val(spi, 0x07, 0x01); + msleep(50); + + power_off(epd); +} + +static const struct drm_simple_display_pipe_funcs repaper_pipe_funcs = { + .enable = repaper_pipe_enable, + .disable = repaper_pipe_disable, + .update = tinydrm_display_pipe_update, + .prepare_fb = drm_gem_fb_simple_display_pipe_prepare_fb, +}; + +static const uint32_t repaper_formats[] = { + DRM_FORMAT_XRGB8888, +}; + +static const struct drm_display_mode repaper_e1144cs021_mode = { + TINYDRM_MODE(128, 96, 29, 22), +}; + +static const u8 repaper_e1144cs021_cs[] = { 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0f, 0xff, 0x00 }; + +static const struct drm_display_mode repaper_e1190cs021_mode = { + TINYDRM_MODE(144, 128, 36, 32), +}; + +static const u8 repaper_e1190cs021_cs[] = { 0x00, 0x00, 0x00, 0x03, + 0xfc, 0x00, 0x00, 0xff }; + +static const struct drm_display_mode repaper_e2200cs021_mode = { + TINYDRM_MODE(200, 96, 46, 22), +}; + +static const u8 repaper_e2200cs021_cs[] = { 0x00, 0x00, 0x00, 0x00, + 0x01, 0xff, 0xe0, 0x00 }; + +static const struct drm_display_mode repaper_e2271cs021_mode = { + TINYDRM_MODE(264, 176, 57, 38), +}; + +static const u8 repaper_e2271cs021_cs[] = { 0x00, 0x00, 0x00, 0x7f, + 0xff, 0xfe, 0x00, 0x00 }; + +DEFINE_DRM_GEM_CMA_FOPS(repaper_fops); + +static struct drm_driver repaper_driver = { + .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_PRIME | + DRIVER_ATOMIC, + .fops = &repaper_fops, + TINYDRM_GEM_DRIVER_OPS, + .name = "repaper", + .desc = "Pervasive Displays RePaper e-ink panels", + .date = "20170405", + .major = 1, + .minor = 0, +}; + +static const struct of_device_id repaper_of_match[] = { + { .compatible = "pervasive,e1144cs021", .data = (void *)E1144CS021 }, + { .compatible = "pervasive,e1190cs021", .data = (void *)E1190CS021 }, + { .compatible = "pervasive,e2200cs021", .data = (void *)E2200CS021 }, + { .compatible = "pervasive,e2271cs021", .data = (void *)E2271CS021 }, + {}, +}; +MODULE_DEVICE_TABLE(of, repaper_of_match); + +static const struct spi_device_id repaper_id[] = { + { "e1144cs021", E1144CS021 }, + { "e1190cs021", E1190CS021 }, + { "e2200cs021", E2200CS021 }, + { "e2271cs021", E2271CS021 }, + { }, +}; +MODULE_DEVICE_TABLE(spi, repaper_id); + +static int repaper_probe(struct spi_device *spi) +{ + const struct drm_display_mode *mode; + const struct spi_device_id *spi_id; + const struct of_device_id *match; + struct device *dev = &spi->dev; + struct tinydrm_device *tdev; + enum repaper_model model; + const char *thermal_zone; + struct repaper_epd *epd; + size_t line_buffer_size; + int ret; + + match = of_match_device(repaper_of_match, dev); + if (match) { + model = (enum repaper_model)match->data; + } else { + spi_id = spi_get_device_id(spi); + model = spi_id->driver_data; + } + + /* The SPI device is used to allocate dma memory */ + if (!dev->coherent_dma_mask) { + ret = dma_coerce_mask_and_coherent(dev, DMA_BIT_MASK(32)); + if (ret) { + dev_warn(dev, "Failed to set dma mask %d\n", ret); + return ret; + } + } + + epd = devm_kzalloc(dev, sizeof(*epd), GFP_KERNEL); + if (!epd) + return -ENOMEM; + + epd->spi = spi; + + epd->panel_on = devm_gpiod_get(dev, "panel-on", GPIOD_OUT_LOW); + if (IS_ERR(epd->panel_on)) { + ret = PTR_ERR(epd->panel_on); + if (ret != -EPROBE_DEFER) + DRM_DEV_ERROR(dev, "Failed to get gpio 'panel-on'\n"); + return ret; + } + + epd->discharge = devm_gpiod_get(dev, "discharge", GPIOD_OUT_LOW); + if (IS_ERR(epd->discharge)) { + ret = PTR_ERR(epd->discharge); + if (ret != -EPROBE_DEFER) + DRM_DEV_ERROR(dev, "Failed to get gpio 'discharge'\n"); + return ret; + } + + epd->reset = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW); + if (IS_ERR(epd->reset)) { + ret = PTR_ERR(epd->reset); + if (ret != -EPROBE_DEFER) + DRM_DEV_ERROR(dev, "Failed to get gpio 'reset'\n"); + return ret; + } + + epd->busy = devm_gpiod_get(dev, "busy", GPIOD_IN); + if (IS_ERR(epd->busy)) { + ret = PTR_ERR(epd->busy); + if (ret != -EPROBE_DEFER) + DRM_DEV_ERROR(dev, "Failed to get gpio 'busy'\n"); + return ret; + } + + if (!device_property_read_string(dev, "pervasive,thermal-zone", + &thermal_zone)) { + epd->thermal = thermal_zone_get_zone_by_name(thermal_zone); + if (IS_ERR(epd->thermal)) { + DRM_DEV_ERROR(dev, "Failed to get thermal zone: %s\n", thermal_zone); + return PTR_ERR(epd->thermal); + } + } + + switch (model) { + case E1144CS021: + mode = &repaper_e1144cs021_mode; + epd->channel_select = repaper_e1144cs021_cs; + epd->stage_time = 480; + epd->bytes_per_scan = 96 / 4; + epd->middle_scan = true; /* data-scan-data */ + epd->pre_border_byte = false; + epd->border_byte = REPAPER_BORDER_BYTE_ZERO; + break; + + case E1190CS021: + mode = &repaper_e1190cs021_mode; + epd->channel_select = repaper_e1190cs021_cs; + epd->stage_time = 480; + epd->bytes_per_scan = 128 / 4 / 2; + epd->middle_scan = false; /* scan-data-scan */ + epd->pre_border_byte = false; + epd->border_byte = REPAPER_BORDER_BYTE_SET; + break; + + case E2200CS021: + mode = &repaper_e2200cs021_mode; + epd->channel_select = repaper_e2200cs021_cs; + epd->stage_time = 480; + epd->bytes_per_scan = 96 / 4; + epd->middle_scan = true; /* data-scan-data */ + epd->pre_border_byte = true; + epd->border_byte = REPAPER_BORDER_BYTE_NONE; + break; + + case E2271CS021: + epd->border = devm_gpiod_get(dev, "border", GPIOD_OUT_LOW); + if (IS_ERR(epd->border)) { + ret = PTR_ERR(epd->border); + if (ret != -EPROBE_DEFER) + DRM_DEV_ERROR(dev, "Failed to get gpio 'border'\n"); + return ret; + } + + mode = &repaper_e2271cs021_mode; + epd->channel_select = repaper_e2271cs021_cs; + epd->stage_time = 630; + epd->bytes_per_scan = 176 / 4; + epd->middle_scan = true; /* data-scan-data */ + epd->pre_border_byte = true; + epd->border_byte = REPAPER_BORDER_BYTE_NONE; + break; + + default: + return -ENODEV; + } + + epd->width = mode->hdisplay; + epd->height = mode->vdisplay; + epd->factored_stage_time = epd->stage_time; + + line_buffer_size = 2 * epd->width / 8 + epd->bytes_per_scan + 2; + epd->line_buffer = devm_kzalloc(dev, line_buffer_size, GFP_KERNEL); + if (!epd->line_buffer) + return -ENOMEM; + + epd->current_frame = devm_kzalloc(dev, epd->width * epd->height / 8, + GFP_KERNEL); + if (!epd->current_frame) + return -ENOMEM; + + tdev = &epd->tinydrm; + + ret = devm_tinydrm_init(dev, tdev, &repaper_fb_funcs, &repaper_driver); + if (ret) + return ret; + + tdev->fb_dirty = repaper_fb_dirty; + + ret = tinydrm_display_pipe_init(tdev, &repaper_pipe_funcs, + DRM_MODE_CONNECTOR_VIRTUAL, + repaper_formats, + ARRAY_SIZE(repaper_formats), mode, 0); + if (ret) + return ret; + + drm_mode_config_reset(tdev->drm); + spi_set_drvdata(spi, tdev); + + DRM_DEBUG_DRIVER("SPI speed: %uMHz\n", spi->max_speed_hz / 1000000); + + return devm_tinydrm_register(tdev); +} + +static void repaper_shutdown(struct spi_device *spi) +{ + struct tinydrm_device *tdev = spi_get_drvdata(spi); + + tinydrm_shutdown(tdev); +} + +static struct spi_driver repaper_spi_driver = { + .driver = { + .name = "repaper", + .owner = THIS_MODULE, + .of_match_table = repaper_of_match, + }, + .id_table = repaper_id, + .probe = repaper_probe, + .shutdown = repaper_shutdown, +}; +module_spi_driver(repaper_spi_driver); + +MODULE_DESCRIPTION("Pervasive Displays RePaper DRM driver"); +MODULE_AUTHOR("Noralf Trønnes"); +MODULE_LICENSE("GPL"); diff --git a/drivers/gpu/drm/tinydrm/st7586.c b/drivers/gpu/drm/tinydrm/st7586.c new file mode 100644 index 000000000..2fcbc3067 --- /dev/null +++ b/drivers/gpu/drm/tinydrm/st7586.c @@ -0,0 +1,400 @@ +/* + * DRM driver for Sitronix ST7586 panels + * + * Copyright 2017 David Lechner <david@lechnology.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include <linux/delay.h> +#include <linux/dma-buf.h> +#include <linux/gpio/consumer.h> +#include <linux/module.h> +#include <linux/property.h> +#include <linux/spi/spi.h> +#include <video/mipi_display.h> + +#include <drm/drm_fb_helper.h> +#include <drm/drm_gem_framebuffer_helper.h> +#include <drm/tinydrm/mipi-dbi.h> +#include <drm/tinydrm/tinydrm-helpers.h> + +/* controller-specific commands */ +#define ST7586_DISP_MODE_GRAY 0x38 +#define ST7586_DISP_MODE_MONO 0x39 +#define ST7586_ENABLE_DDRAM 0x3a +#define ST7586_SET_DISP_DUTY 0xb0 +#define ST7586_SET_PART_DISP 0xb4 +#define ST7586_SET_NLINE_INV 0xb5 +#define ST7586_SET_VOP 0xc0 +#define ST7586_SET_BIAS_SYSTEM 0xc3 +#define ST7586_SET_BOOST_LEVEL 0xc4 +#define ST7586_SET_VOP_OFFSET 0xc7 +#define ST7586_ENABLE_ANALOG 0xd0 +#define ST7586_AUTO_READ_CTRL 0xd7 +#define ST7586_OTP_RW_CTRL 0xe0 +#define ST7586_OTP_CTRL_OUT 0xe1 +#define ST7586_OTP_READ 0xe3 + +#define ST7586_DISP_CTRL_MX BIT(6) +#define ST7586_DISP_CTRL_MY BIT(7) + +/* + * The ST7586 controller has an unusual pixel format where 2bpp grayscale is + * packed 3 pixels per byte with the first two pixels using 3 bits and the 3rd + * pixel using only 2 bits. + * + * | D7 | D6 | D5 || | || 2bpp | + * | (D4) | (D3) | (D2) || D1 | D0 || GRAY | + * +------+------+------++------+------++------+ + * | 1 | 1 | 1 || 1 | 1 || 0 0 | black + * | 1 | 0 | 0 || 1 | 0 || 0 1 | dark gray + * | 0 | 1 | 0 || 0 | 1 || 1 0 | light gray + * | 0 | 0 | 0 || 0 | 0 || 1 1 | white + */ + +static const u8 st7586_lookup[] = { 0x7, 0x4, 0x2, 0x0 }; + +static void st7586_xrgb8888_to_gray332(u8 *dst, void *vaddr, + struct drm_framebuffer *fb, + struct drm_clip_rect *clip) +{ + size_t len = (clip->x2 - clip->x1) * (clip->y2 - clip->y1); + unsigned int x, y; + u8 *src, *buf, val; + + buf = kmalloc(len, GFP_KERNEL); + if (!buf) + return; + + tinydrm_xrgb8888_to_gray8(buf, vaddr, fb, clip); + src = buf; + + for (y = clip->y1; y < clip->y2; y++) { + for (x = clip->x1; x < clip->x2; x += 3) { + val = st7586_lookup[*src++ >> 6] << 5; + val |= st7586_lookup[*src++ >> 6] << 2; + val |= st7586_lookup[*src++ >> 6] >> 1; + *dst++ = val; + } + } + + kfree(buf); +} + +static int st7586_buf_copy(void *dst, struct drm_framebuffer *fb, + struct drm_clip_rect *clip) +{ + struct drm_gem_cma_object *cma_obj = drm_fb_cma_get_gem_obj(fb, 0); + struct dma_buf_attachment *import_attach = cma_obj->base.import_attach; + void *src = cma_obj->vaddr; + int ret = 0; + + if (import_attach) { + ret = dma_buf_begin_cpu_access(import_attach->dmabuf, + DMA_FROM_DEVICE); + if (ret) + return ret; + } + + st7586_xrgb8888_to_gray332(dst, src, fb, clip); + + if (import_attach) + ret = dma_buf_end_cpu_access(import_attach->dmabuf, + DMA_FROM_DEVICE); + + return ret; +} + +static int st7586_fb_dirty(struct drm_framebuffer *fb, + struct drm_file *file_priv, unsigned int flags, + unsigned int color, struct drm_clip_rect *clips, + unsigned int num_clips) +{ + struct tinydrm_device *tdev = fb->dev->dev_private; + struct mipi_dbi *mipi = mipi_dbi_from_tinydrm(tdev); + struct drm_clip_rect clip; + int start, end; + int ret = 0; + + if (!mipi->enabled) + return 0; + + tinydrm_merge_clips(&clip, clips, num_clips, flags, fb->width, + fb->height); + + /* 3 pixels per byte, so grow clip to nearest multiple of 3 */ + clip.x1 = rounddown(clip.x1, 3); + clip.x2 = roundup(clip.x2, 3); + + DRM_DEBUG("Flushing [FB:%d] x1=%u, x2=%u, y1=%u, y2=%u\n", fb->base.id, + clip.x1, clip.x2, clip.y1, clip.y2); + + ret = st7586_buf_copy(mipi->tx_buf, fb, &clip); + if (ret) + return ret; + + /* Pixels are packed 3 per byte */ + start = clip.x1 / 3; + end = clip.x2 / 3; + + mipi_dbi_command(mipi, MIPI_DCS_SET_COLUMN_ADDRESS, + (start >> 8) & 0xFF, start & 0xFF, + (end >> 8) & 0xFF, (end - 1) & 0xFF); + mipi_dbi_command(mipi, MIPI_DCS_SET_PAGE_ADDRESS, + (clip.y1 >> 8) & 0xFF, clip.y1 & 0xFF, + (clip.y2 >> 8) & 0xFF, (clip.y2 - 1) & 0xFF); + + ret = mipi_dbi_command_buf(mipi, MIPI_DCS_WRITE_MEMORY_START, + (u8 *)mipi->tx_buf, + (end - start) * (clip.y2 - clip.y1)); + + return ret; +} + +static const struct drm_framebuffer_funcs st7586_fb_funcs = { + .destroy = drm_gem_fb_destroy, + .create_handle = drm_gem_fb_create_handle, + .dirty = tinydrm_fb_dirty, +}; + +static void st7586_pipe_enable(struct drm_simple_display_pipe *pipe, + struct drm_crtc_state *crtc_state, + struct drm_plane_state *plane_state) +{ + struct tinydrm_device *tdev = pipe_to_tinydrm(pipe); + struct mipi_dbi *mipi = mipi_dbi_from_tinydrm(tdev); + int ret; + u8 addr_mode; + + DRM_DEBUG_KMS("\n"); + + ret = mipi_dbi_poweron_reset(mipi); + if (ret) + return; + + mipi_dbi_command(mipi, ST7586_AUTO_READ_CTRL, 0x9f); + mipi_dbi_command(mipi, ST7586_OTP_RW_CTRL, 0x00); + + msleep(10); + + mipi_dbi_command(mipi, ST7586_OTP_READ); + + msleep(20); + + mipi_dbi_command(mipi, ST7586_OTP_CTRL_OUT); + mipi_dbi_command(mipi, MIPI_DCS_EXIT_SLEEP_MODE); + mipi_dbi_command(mipi, MIPI_DCS_SET_DISPLAY_OFF); + + msleep(50); + + mipi_dbi_command(mipi, ST7586_SET_VOP_OFFSET, 0x00); + mipi_dbi_command(mipi, ST7586_SET_VOP, 0xe3, 0x00); + mipi_dbi_command(mipi, ST7586_SET_BIAS_SYSTEM, 0x02); + mipi_dbi_command(mipi, ST7586_SET_BOOST_LEVEL, 0x04); + mipi_dbi_command(mipi, ST7586_ENABLE_ANALOG, 0x1d); + mipi_dbi_command(mipi, ST7586_SET_NLINE_INV, 0x00); + mipi_dbi_command(mipi, ST7586_DISP_MODE_GRAY); + mipi_dbi_command(mipi, ST7586_ENABLE_DDRAM, 0x02); + + switch (mipi->rotation) { + default: + addr_mode = 0x00; + break; + case 90: + addr_mode = ST7586_DISP_CTRL_MY; + break; + case 180: + addr_mode = ST7586_DISP_CTRL_MX | ST7586_DISP_CTRL_MY; + break; + case 270: + addr_mode = ST7586_DISP_CTRL_MX; + break; + } + mipi_dbi_command(mipi, MIPI_DCS_SET_ADDRESS_MODE, addr_mode); + + mipi_dbi_command(mipi, ST7586_SET_DISP_DUTY, 0x7f); + mipi_dbi_command(mipi, ST7586_SET_PART_DISP, 0xa0); + mipi_dbi_command(mipi, MIPI_DCS_SET_PARTIAL_AREA, 0x00, 0x00, 0x00, 0x77); + mipi_dbi_command(mipi, MIPI_DCS_EXIT_INVERT_MODE); + + msleep(100); + + mipi_dbi_command(mipi, MIPI_DCS_SET_DISPLAY_ON); + + mipi_dbi_enable_flush(mipi, crtc_state, plane_state); +} + +static void st7586_pipe_disable(struct drm_simple_display_pipe *pipe) +{ + struct tinydrm_device *tdev = pipe_to_tinydrm(pipe); + struct mipi_dbi *mipi = mipi_dbi_from_tinydrm(tdev); + + DRM_DEBUG_KMS("\n"); + + if (!mipi->enabled) + return; + + mipi_dbi_command(mipi, MIPI_DCS_SET_DISPLAY_OFF); + mipi->enabled = false; +} + +static const u32 st7586_formats[] = { + DRM_FORMAT_XRGB8888, +}; + +static int st7586_init(struct device *dev, struct mipi_dbi *mipi, + const struct drm_simple_display_pipe_funcs *pipe_funcs, + struct drm_driver *driver, const struct drm_display_mode *mode, + unsigned int rotation) +{ + size_t bufsize = (mode->vdisplay + 2) / 3 * mode->hdisplay; + struct tinydrm_device *tdev = &mipi->tinydrm; + int ret; + + mutex_init(&mipi->cmdlock); + + mipi->tx_buf = devm_kmalloc(dev, bufsize, GFP_KERNEL); + if (!mipi->tx_buf) + return -ENOMEM; + + ret = devm_tinydrm_init(dev, tdev, &st7586_fb_funcs, driver); + if (ret) + return ret; + + tdev->fb_dirty = st7586_fb_dirty; + + ret = tinydrm_display_pipe_init(tdev, pipe_funcs, + DRM_MODE_CONNECTOR_VIRTUAL, + st7586_formats, + ARRAY_SIZE(st7586_formats), + mode, rotation); + if (ret) + return ret; + + tdev->drm->mode_config.preferred_depth = 32; + mipi->rotation = rotation; + + drm_mode_config_reset(tdev->drm); + + DRM_DEBUG_KMS("preferred_depth=%u, rotation = %u\n", + tdev->drm->mode_config.preferred_depth, rotation); + + return 0; +} + +static const struct drm_simple_display_pipe_funcs st7586_pipe_funcs = { + .enable = st7586_pipe_enable, + .disable = st7586_pipe_disable, + .update = tinydrm_display_pipe_update, + .prepare_fb = drm_gem_fb_simple_display_pipe_prepare_fb, +}; + +static const struct drm_display_mode st7586_mode = { + TINYDRM_MODE(178, 128, 37, 27), +}; + +DEFINE_DRM_GEM_CMA_FOPS(st7586_fops); + +static struct drm_driver st7586_driver = { + .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_PRIME | + DRIVER_ATOMIC, + .fops = &st7586_fops, + TINYDRM_GEM_DRIVER_OPS, + .debugfs_init = mipi_dbi_debugfs_init, + .name = "st7586", + .desc = "Sitronix ST7586", + .date = "20170801", + .major = 1, + .minor = 0, +}; + +static const struct of_device_id st7586_of_match[] = { + { .compatible = "lego,ev3-lcd" }, + {}, +}; +MODULE_DEVICE_TABLE(of, st7586_of_match); + +static const struct spi_device_id st7586_id[] = { + { "ev3-lcd", 0 }, + { }, +}; +MODULE_DEVICE_TABLE(spi, st7586_id); + +static int st7586_probe(struct spi_device *spi) +{ + struct device *dev = &spi->dev; + struct mipi_dbi *mipi; + struct gpio_desc *a0; + u32 rotation = 0; + int ret; + + mipi = devm_kzalloc(dev, sizeof(*mipi), GFP_KERNEL); + if (!mipi) + return -ENOMEM; + + mipi->reset = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH); + if (IS_ERR(mipi->reset)) { + DRM_DEV_ERROR(dev, "Failed to get gpio 'reset'\n"); + return PTR_ERR(mipi->reset); + } + + a0 = devm_gpiod_get(dev, "a0", GPIOD_OUT_LOW); + if (IS_ERR(a0)) { + DRM_DEV_ERROR(dev, "Failed to get gpio 'a0'\n"); + return PTR_ERR(a0); + } + + device_property_read_u32(dev, "rotation", &rotation); + + ret = mipi_dbi_spi_init(spi, mipi, a0); + if (ret) + return ret; + + /* Cannot read from this controller via SPI */ + mipi->read_commands = NULL; + + /* + * we are using 8-bit data, so we are not actually swapping anything, + * but setting mipi->swap_bytes makes mipi_dbi_typec3_command() do the + * right thing and not use 16-bit transfers (which results in swapped + * bytes on little-endian systems and causes out of order data to be + * sent to the display). + */ + mipi->swap_bytes = true; + + ret = st7586_init(&spi->dev, mipi, &st7586_pipe_funcs, &st7586_driver, + &st7586_mode, rotation); + if (ret) + return ret; + + spi_set_drvdata(spi, mipi); + + return devm_tinydrm_register(&mipi->tinydrm); +} + +static void st7586_shutdown(struct spi_device *spi) +{ + struct mipi_dbi *mipi = spi_get_drvdata(spi); + + tinydrm_shutdown(&mipi->tinydrm); +} + +static struct spi_driver st7586_spi_driver = { + .driver = { + .name = "st7586", + .owner = THIS_MODULE, + .of_match_table = st7586_of_match, + }, + .id_table = st7586_id, + .probe = st7586_probe, + .shutdown = st7586_shutdown, +}; +module_spi_driver(st7586_spi_driver); + +MODULE_DESCRIPTION("Sitronix ST7586 DRM driver"); +MODULE_AUTHOR("David Lechner <david@lechnology.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/gpu/drm/tinydrm/st7735r.c b/drivers/gpu/drm/tinydrm/st7735r.c new file mode 100644 index 000000000..3081bc57c --- /dev/null +++ b/drivers/gpu/drm/tinydrm/st7735r.c @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * DRM driver for Sitronix ST7735R panels + * + * Copyright 2017 David Lechner <david@lechnology.com> + */ + +#include <linux/backlight.h> +#include <linux/delay.h> +#include <linux/dma-buf.h> +#include <linux/gpio/consumer.h> +#include <linux/module.h> +#include <linux/property.h> +#include <linux/spi/spi.h> +#include <video/mipi_display.h> + +#include <drm/drm_fb_helper.h> +#include <drm/drm_gem_framebuffer_helper.h> +#include <drm/tinydrm/mipi-dbi.h> +#include <drm/tinydrm/tinydrm-helpers.h> + +#define ST7735R_FRMCTR1 0xb1 +#define ST7735R_FRMCTR2 0xb2 +#define ST7735R_FRMCTR3 0xb3 +#define ST7735R_INVCTR 0xb4 +#define ST7735R_PWCTR1 0xc0 +#define ST7735R_PWCTR2 0xc1 +#define ST7735R_PWCTR3 0xc2 +#define ST7735R_PWCTR4 0xc3 +#define ST7735R_PWCTR5 0xc4 +#define ST7735R_VMCTR1 0xc5 +#define ST7735R_GAMCTRP1 0xe0 +#define ST7735R_GAMCTRN1 0xe1 + +#define ST7735R_MY BIT(7) +#define ST7735R_MX BIT(6) +#define ST7735R_MV BIT(5) + +static void jd_t18003_t01_pipe_enable(struct drm_simple_display_pipe *pipe, + struct drm_crtc_state *crtc_state, + struct drm_plane_state *plane_state) +{ + struct tinydrm_device *tdev = pipe_to_tinydrm(pipe); + struct mipi_dbi *mipi = mipi_dbi_from_tinydrm(tdev); + int ret; + u8 addr_mode; + + DRM_DEBUG_KMS("\n"); + + ret = mipi_dbi_poweron_reset(mipi); + if (ret) + return; + + msleep(150); + + mipi_dbi_command(mipi, MIPI_DCS_EXIT_SLEEP_MODE); + msleep(500); + + mipi_dbi_command(mipi, ST7735R_FRMCTR1, 0x01, 0x2c, 0x2d); + mipi_dbi_command(mipi, ST7735R_FRMCTR2, 0x01, 0x2c, 0x2d); + mipi_dbi_command(mipi, ST7735R_FRMCTR3, 0x01, 0x2c, 0x2d, 0x01, 0x2c, + 0x2d); + mipi_dbi_command(mipi, ST7735R_INVCTR, 0x07); + mipi_dbi_command(mipi, ST7735R_PWCTR1, 0xa2, 0x02, 0x84); + mipi_dbi_command(mipi, ST7735R_PWCTR2, 0xc5); + mipi_dbi_command(mipi, ST7735R_PWCTR3, 0x0a, 0x00); + mipi_dbi_command(mipi, ST7735R_PWCTR4, 0x8a, 0x2a); + mipi_dbi_command(mipi, ST7735R_PWCTR5, 0x8a, 0xee); + mipi_dbi_command(mipi, ST7735R_VMCTR1, 0x0e); + mipi_dbi_command(mipi, MIPI_DCS_EXIT_INVERT_MODE); + switch (mipi->rotation) { + default: + addr_mode = ST7735R_MX | ST7735R_MY; + break; + case 90: + addr_mode = ST7735R_MX | ST7735R_MV; + break; + case 180: + addr_mode = 0; + break; + case 270: + addr_mode = ST7735R_MY | ST7735R_MV; + break; + } + mipi_dbi_command(mipi, MIPI_DCS_SET_ADDRESS_MODE, addr_mode); + mipi_dbi_command(mipi, MIPI_DCS_SET_PIXEL_FORMAT, + MIPI_DCS_PIXEL_FMT_16BIT); + mipi_dbi_command(mipi, ST7735R_GAMCTRP1, 0x02, 0x1c, 0x07, 0x12, 0x37, + 0x32, 0x29, 0x2d, 0x29, 0x25, 0x2b, 0x39, 0x00, 0x01, + 0x03, 0x10); + mipi_dbi_command(mipi, ST7735R_GAMCTRN1, 0x03, 0x1d, 0x07, 0x06, 0x2e, + 0x2c, 0x29, 0x2d, 0x2e, 0x2e, 0x37, 0x3f, 0x00, 0x00, + 0x02, 0x10); + mipi_dbi_command(mipi, MIPI_DCS_SET_DISPLAY_ON); + + msleep(100); + + mipi_dbi_command(mipi, MIPI_DCS_ENTER_NORMAL_MODE); + + msleep(20); + + mipi_dbi_enable_flush(mipi, crtc_state, plane_state); +} + +static const struct drm_simple_display_pipe_funcs jd_t18003_t01_pipe_funcs = { + .enable = jd_t18003_t01_pipe_enable, + .disable = mipi_dbi_pipe_disable, + .update = tinydrm_display_pipe_update, + .prepare_fb = drm_gem_fb_simple_display_pipe_prepare_fb, +}; + +static const struct drm_display_mode jd_t18003_t01_mode = { + TINYDRM_MODE(128, 160, 28, 35), +}; + +DEFINE_DRM_GEM_CMA_FOPS(st7735r_fops); + +static struct drm_driver st7735r_driver = { + .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_PRIME | + DRIVER_ATOMIC, + .fops = &st7735r_fops, + TINYDRM_GEM_DRIVER_OPS, + .debugfs_init = mipi_dbi_debugfs_init, + .name = "st7735r", + .desc = "Sitronix ST7735R", + .date = "20171128", + .major = 1, + .minor = 0, +}; + +static const struct of_device_id st7735r_of_match[] = { + { .compatible = "jianda,jd-t18003-t01" }, + { }, +}; +MODULE_DEVICE_TABLE(of, st7735r_of_match); + +static const struct spi_device_id st7735r_id[] = { + { "jd-t18003-t01", 0 }, + { }, +}; +MODULE_DEVICE_TABLE(spi, st7735r_id); + +static int st7735r_probe(struct spi_device *spi) +{ + struct device *dev = &spi->dev; + struct mipi_dbi *mipi; + struct gpio_desc *dc; + u32 rotation = 0; + int ret; + + mipi = devm_kzalloc(dev, sizeof(*mipi), GFP_KERNEL); + if (!mipi) + return -ENOMEM; + + mipi->reset = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH); + if (IS_ERR(mipi->reset)) { + DRM_DEV_ERROR(dev, "Failed to get gpio 'reset'\n"); + return PTR_ERR(mipi->reset); + } + + dc = devm_gpiod_get(dev, "dc", GPIOD_OUT_LOW); + if (IS_ERR(dc)) { + DRM_DEV_ERROR(dev, "Failed to get gpio 'dc'\n"); + return PTR_ERR(dc); + } + + mipi->backlight = devm_of_find_backlight(dev); + if (IS_ERR(mipi->backlight)) + return PTR_ERR(mipi->backlight); + + device_property_read_u32(dev, "rotation", &rotation); + + ret = mipi_dbi_spi_init(spi, mipi, dc); + if (ret) + return ret; + + /* Cannot read from Adafruit 1.8" display via SPI */ + mipi->read_commands = NULL; + + ret = mipi_dbi_init(&spi->dev, mipi, &jd_t18003_t01_pipe_funcs, + &st7735r_driver, &jd_t18003_t01_mode, rotation); + if (ret) + return ret; + + spi_set_drvdata(spi, mipi); + + return devm_tinydrm_register(&mipi->tinydrm); +} + +static void st7735r_shutdown(struct spi_device *spi) +{ + struct mipi_dbi *mipi = spi_get_drvdata(spi); + + tinydrm_shutdown(&mipi->tinydrm); +} + +static struct spi_driver st7735r_spi_driver = { + .driver = { + .name = "st7735r", + .owner = THIS_MODULE, + .of_match_table = st7735r_of_match, + }, + .id_table = st7735r_id, + .probe = st7735r_probe, + .shutdown = st7735r_shutdown, +}; +module_spi_driver(st7735r_spi_driver); + +MODULE_DESCRIPTION("Sitronix ST7735R DRM driver"); +MODULE_AUTHOR("David Lechner <david@lechnology.com>"); +MODULE_LICENSE("GPL"); |