[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <20250131064354.4163579-3-kikuchan98@gmail.com>
Date: Fri, 31 Jan 2025 15:43:54 +0900
From: Hironori KIKUCHI <kikuchan98@...il.com>
To: linux-kernel@...r.kernel.org
Cc: Hironori KIKUCHI <kikuchan98@...il.com>,
Neil Armstrong <neil.armstrong@...aro.org>,
Jessica Zhang <quic_jesszhan@...cinc.com>,
David Airlie <airlied@...il.com>,
Simona Vetter <simona@...ll.ch>,
Maarten Lankhorst <maarten.lankhorst@...ux.intel.com>,
Maxime Ripard <mripard@...nel.org>,
Thomas Zimmermann <tzimmermann@...e.de>,
Rob Herring <robh@...nel.org>,
Krzysztof Kozlowski <krzk+dt@...nel.org>,
Conor Dooley <conor+dt@...nel.org>,
dri-devel@...ts.freedesktop.org,
devicetree@...r.kernel.org
Subject: [PATCH 2/2] drm: panel: Add a driver for Generic MIPI-DSI/DPI(+SPI) panels
Although the MIPI specifications define how to communicate with a panel
to display an image, some panels still require a panel-specific
initialization sequence to be sent.
This is a driver for such generic MIPI-DSI/DPI panels that require
initialization with a simple command sequence before use.
Its fundamental approach is similar to `panel-mipi-dbi` driver,
which sends an initialization sequence stored in a firmware file.
Moreover, this driver allows display modes, timings, and panel
configuration parameters to be stored in the same file or in DT.
Signed-off-by: Hironori KIKUCHI <kikuchan98@...il.com>
---
drivers/gpu/drm/panel/Kconfig | 10 +
drivers/gpu/drm/panel/Makefile | 1 +
drivers/gpu/drm/panel/panel-mipi.c | 1355 ++++++++++++++++++++++++++++
3 files changed, 1366 insertions(+)
create mode 100644 drivers/gpu/drm/panel/panel-mipi.c
diff --git a/drivers/gpu/drm/panel/Kconfig b/drivers/gpu/drm/panel/Kconfig
index d7469c565d1..46eea1974a0 100644
--- a/drivers/gpu/drm/panel/Kconfig
+++ b/drivers/gpu/drm/panel/Kconfig
@@ -408,6 +408,16 @@ config DRM_PANEL_MANTIX_MLAF057WE51
has a resolution of 720x1440 pixels, a built in backlight and touch
controller.
+config DRM_PANEL_MIPI
+ tristate "Generic MIPI-DSI/DPI(+SPI) panel"
+ depends on OF
+ depends on SPI || DRM_MIPI_DSI
+ select DRM_MIPI_DBI if SPI
+ depends on BACKLIGHT_CLASS_DEVICE
+ help
+ Say Y here if you want to enable support for Generic MIPI-DSI /
+ MIPI-DPI(+SPI) panels.
+
config DRM_PANEL_NEC_NL8048HL11
tristate "NEC NL8048HL11 RGB panel"
depends on GPIOLIB && OF && SPI
diff --git a/drivers/gpu/drm/panel/Makefile b/drivers/gpu/drm/panel/Makefile
index 7dcf72646ca..22276255a7b 100644
--- a/drivers/gpu/drm/panel/Makefile
+++ b/drivers/gpu/drm/panel/Makefile
@@ -40,6 +40,7 @@ obj-$(CONFIG_DRM_PANEL_LG_LB035Q02) += panel-lg-lb035q02.o
obj-$(CONFIG_DRM_PANEL_LG_LG4573) += panel-lg-lg4573.o
obj-$(CONFIG_DRM_PANEL_LG_SW43408) += panel-lg-sw43408.o
obj-$(CONFIG_DRM_PANEL_MAGNACHIP_D53E6EA8966) += panel-magnachip-d53e6ea8966.o
+obj-$(CONFIG_DRM_PANEL_MIPI) += panel-mipi.o
obj-$(CONFIG_DRM_PANEL_NEC_NL8048HL11) += panel-nec-nl8048hl11.o
obj-$(CONFIG_DRM_PANEL_NEWVISION_NV3051D) += panel-newvision-nv3051d.o
obj-$(CONFIG_DRM_PANEL_NEWVISION_NV3052C) += panel-newvision-nv3052c.o
diff --git a/drivers/gpu/drm/panel/panel-mipi.c b/drivers/gpu/drm/panel/panel-mipi.c
new file mode 100644
index 00000000000..bcbaa15b62a
--- /dev/null
+++ b/drivers/gpu/drm/panel/panel-mipi.c
@@ -0,0 +1,1355 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Generic MIPI-DSI/DPI(+SPI) Panel Driver
+ *
+ * Supported panels:
+ * - A generic MIPI-DSI panel which implements basic DCS
+ * - A generic MIPI-DPI panel which implements basic DCS over SPI
+ *
+ * Copyright (C) 2025, Hironori KIKUCHI <kikuchan98@...il.com>
+ */
+
+#include <drm/drm_mipi_dbi.h>
+#include <drm/drm_mipi_dsi.h>
+#include <drm/drm_modes.h>
+#include <drm/drm_panel.h>
+#include <linux/backlight.h>
+#include <linux/bitfield.h>
+#include <linux/debugfs.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/firmware.h>
+#include <linux/gpio/consumer.h>
+#include <linux/media-bus-format.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regulator/consumer.h>
+#include <linux/spi/spi.h>
+#include <video/display_timing.h>
+#include <video/mipi_display.h>
+#include <video/of_display_timing.h>
+#include <video/videomode.h>
+
+/*
+ * The display panel configuration can be stored in a firmware file,
+ * under the firmware directory of the system.
+ *
+ * The name of the file is `panels/<firmware-name>.panel`, where the
+ * 'firmware-name' should be defined as the property of the device
+ * in the Device Tree, otherwise the first `compatible` string is used.
+ *
+ * File Layout:
+ * A binary file composed with the following data:
+ * <header>
+ * <config>
+ * <timings>
+ * <init-sequence>
+ *
+ * The 'header':
+ * A `struct panel_firmware_header`.
+ * The `file_format_version` must be `1`.
+ *
+ * The 'config':
+ * A `struct panel_firmware_config`.
+ * The values are in big-endian.
+ *
+ * The 'timings':
+ * An array of `struct panel_firmware_panel_timing`.
+ * Its length is `num_timings` in the config.
+ * The values are in big-endian.
+ *
+ * The 'init-sequence':
+ * MIPI commands to execute when the display pipeline is enabled.
+ * This is used to configure the display controller, and it may
+ * contains panel specific parameters as well.
+ *
+ * The commands are encoded and stored in a byte array:
+ *
+ * 0x00 : sleep 10 ms
+ * 0x01 <M> : MIPI command <M> with no arguments
+ * 0x02 <M> <a> : MIPI command <M> with 1 argument <a>
+ * 0x03 <M> <a> <b> : MIPI command <M> with 2 arguments <a> <b>
+ * :
+ * 0x7f <M> <...> : MIPI command <M> with 126 arguments <...>
+ * 0x80 : sleep 100 ms
+ * 0x81 - 0xff : reserved
+ *
+ * Example:
+ * command 0x11
+ * sleep 10ms
+ * command 0xb1 arguments 0x01 0x2c 0x2d
+ * command 0x29
+ * sleep 130ms
+ *
+ * Byte sequence:
+ * 0x01 0x11
+ * 0x00
+ * 0x04 0xb1 0x01 0x2c 0x2d
+ * 0x01 0x29
+ * 0x80 0x00 0x00 0x00
+ *
+ */
+static const u8 panel_firmware_magic[15] = {
+ 0x50, 0x41, 0x4e, 0x45, 0x4c, 0x2d, 0x46, 0x49,
+ 0x52, 0x4d, 0x57, 0x41, 0x52, 0x45, 0x00,
+};
+
+struct panel_firmware_header {
+ u8 magic[15];
+ u8 file_format_version; /* must be 1 */
+} __packed;
+
+struct panel_firmware_config {
+ u16 width_mm, height_mm;
+ u16 rotation;
+ u8 _reserved_1[2];
+ u8 _reserved_2[8];
+
+ u16 reset_delay; /* delay after the reset command, in ms */
+ u16 init_delay; /* delay for sending the initial command sequence, in ms */
+ u16 sleep_delay; /* delay after the sleep command, in ms */
+ u16 backlight_delay; /* delay for enabling the backlight, in ms */
+ u16 _reserved_3[4];
+
+ u16 dsi_lanes; /* unsigned int */
+ u16 dsi_format; /* enum mipi_dsi_pixel_format */
+ u32 dsi_mode_flags; /* unsigned long */
+ u32 bus_flags; /* struct drm_bus_flags */
+ u8 _reserved_4[2];
+ u8 preferred_timing;
+ u8 num_timings;
+} __packed;
+
+struct panel_firmware_panel_timing {
+ u16 hactive;
+ u16 hfp;
+ u16 hslen;
+ u16 hbp;
+
+ u16 vactive;
+ u16 vfp;
+ u16 vslen;
+ u16 vbp;
+
+ u32 dclk; /* in kHz */
+ u32 flags; /* include/drm/drm_modes.h DRM_MODE_FLAG_* */
+
+ u8 _reserved_1[8];
+} __packed;
+
+struct panel_commands {
+ const u8 *data;
+ size_t size;
+};
+
+struct panel_firmware {
+ const struct firmware *blob;
+ const struct panel_firmware_config *config;
+ const struct panel_firmware_panel_timing *timings;
+ struct panel_commands commands;
+};
+
+struct panel_mipi {
+ struct drm_panel panel;
+ struct mipi_dsi_device *dsi;
+ struct mipi_dbi dbi;
+
+ struct mutex lock;
+
+ struct regulator_bulk_data supplies[2];
+ struct gpio_desc *reset;
+
+ unsigned int reset_delay;
+ unsigned int init_delay;
+ unsigned int sleep_delay;
+ unsigned int backlight_delay;
+
+ unsigned int dsi_lanes;
+ enum mipi_dsi_pixel_format dsi_format;
+
+ /* Panel info */
+ u32 width_mm, height_mm;
+ u32 bus_format, bus_flags;
+ enum drm_panel_orientation orientation;
+
+ struct list_head mode_list;
+
+ struct panel_firmware *firmware;
+ struct panel_commands commands;
+
+ int (*write_command)(struct panel_mipi *mipi, const u8 *buf,
+ size_t len);
+ int (*read_command)(struct panel_mipi *mipi, u8 cmd, u8 *buf,
+ size_t len);
+
+ /* DMA-able buffer */
+ u8 initial_display_id[3];
+ u8 display_id[3];
+
+ /* debugfs */
+ struct dentry *debugfs;
+ u8 recvbuf[1024];
+ size_t recvlen;
+};
+
+static inline struct panel_mipi *to_panel_mipi(struct drm_panel *panel)
+{
+ return container_of(panel, struct panel_mipi, panel);
+}
+
+static int panel_mipi_dsi_write(struct panel_mipi *mipi, const u8 *buf,
+ size_t len)
+{
+ int ret;
+
+ ret = mipi_dsi_dcs_write_buffer(mipi->dsi, buf, len);
+ if (ret < 0)
+ return ret;
+
+ return 0;
+}
+
+static int panel_mipi_dbi_write(struct panel_mipi *mipi, const u8 *buf,
+ size_t len)
+{
+ return mipi_dbi_command_stackbuf(&mipi->dbi, buf[0], buf + 1, len - 1);
+}
+
+#define panel_mipi_write(mipi, ...) \
+ ({ \
+ u8 _buf[] = { __VA_ARGS__ }; \
+ mipi->write_command(mipi, _buf, ARRAY_SIZE(_buf)); \
+ })
+
+static int panel_mipi_dsi_read(struct panel_mipi *mipi, u8 cmd, u8 *buf,
+ size_t len)
+{
+ return mipi_dsi_dcs_read(mipi->dsi, cmd, buf, len);
+}
+
+static int panel_mipi_dbi_read(struct panel_mipi *mipi, u8 cmd, u8 *buf,
+ size_t len)
+{
+ return mipi_dbi_command_buf(&mipi->dbi, cmd, buf, len);
+}
+
+#define panel_mipi_read(mipi, cmd, buf, len) \
+ (mipi)->read_command((mipi), (cmd), (buf), (len))
+
+static unsigned long panel_mipi_dsi_set_mode_flags(struct panel_mipi *mipi,
+ unsigned long new_flags)
+{
+ unsigned long old_flags;
+
+ if (!mipi->dsi)
+ return 0;
+
+ old_flags = mipi->dsi->mode_flags;
+
+ mipi->dsi->mode_flags = new_flags;
+
+ return old_flags;
+}
+
+static bool panel_mipi_dsi_set_lpm(struct panel_mipi *mipi, bool set)
+{
+ unsigned long old_flags;
+
+ if (!mipi->dsi)
+ return false;
+
+ old_flags = panel_mipi_dsi_set_mode_flags(
+ mipi, set ? mipi->dsi->mode_flags | MIPI_DSI_MODE_LPM :
+ mipi->dsi->mode_flags & ~MIPI_DSI_MODE_LPM);
+
+ return old_flags & MIPI_DSI_MODE_LPM ? true : false;
+}
+
+#define PANEL_MIPI_TRANSACTION(mipi, stmt) \
+ do { \
+ mutex_lock(&(mipi)->lock); \
+ do { \
+ stmt; \
+ } while (0); \
+ mutex_unlock(&(mipi)->lock); \
+ } while (0)
+
+#define PANEL_MIPI_TRANSACTION_LPM(mipi, lpm, stmt) \
+ PANEL_MIPI_TRANSACTION( \
+ mipi, \
+ bool _lpm_backup = panel_mipi_dsi_set_lpm((mipi), (lpm)); \
+ do { stmt; } while (0); \
+ panel_mipi_dsi_set_lpm((mipi), _lpm_backup))
+
+static int panel_mipi_validate_commands(struct panel_mipi *mipi, const u8 *data,
+ size_t size, size_t *valid_len)
+{
+ size_t i = 0;
+
+ /* sanity check */
+ while (i < size) {
+ u8 inst = data[i];
+ bool ext = (inst & 0x80) ? true : false;
+ u8 len = inst & 0x7f;
+
+ if (ext && len > 0)
+ return -EINVAL; /* reserved */
+
+ if (i + 1 + len > size)
+ break;
+
+ i += 1 + len;
+ }
+
+ *valid_len = i;
+
+ return 0;
+}
+
+static int panel_mipi_load_commands(struct panel_mipi *mipi, const u8 *data,
+ size_t size)
+{
+ int err;
+ size_t valid_len;
+
+ err = panel_mipi_validate_commands(mipi, data, size, &valid_len);
+ if (err)
+ return err;
+
+ if (valid_len != size)
+ return -EINVAL;
+
+ mipi->commands.data = data;
+ mipi->commands.size = size;
+
+ return 0;
+}
+
+static int panel_mipi_write_commands(struct panel_mipi *mipi, const u8 *data,
+ size_t size)
+{
+ const u8 *read_commands;
+ unsigned int write_memory_bpw;
+ size_t i = 0;
+ int err = 0;
+
+ if (!data)
+ return 0;
+
+ /*
+ * Disable interpretation for special commands for DBI
+ *
+ * This is required because the vendor specific custom commands
+ * may change its command set to the other.
+ */
+ read_commands = mipi->dbi.read_commands;
+ write_memory_bpw = mipi->dbi.write_memory_bpw;
+ mipi->dbi.read_commands = NULL;
+ mipi->dbi.write_memory_bpw = 8;
+
+ while (i < size) {
+ u8 inst = data[i++];
+ bool ext = (inst & 0x80) ? true : false;
+ u8 len = inst & 0x7f;
+
+ if (len == 0x00) {
+ msleep(ext ? 100 : 10);
+ continue;
+ }
+
+ err = mipi->write_command(mipi, data + i, len);
+
+ if (err)
+ break;
+
+ i += len;
+ }
+
+ /* restore */
+ mipi->dbi.read_commands = read_commands;
+ mipi->dbi.write_memory_bpw = write_memory_bpw;
+
+ return err;
+}
+
+static int panel_mipi_read_firmware(const struct device *dev,
+ struct panel_mipi *mipi,
+ const struct panel_firmware *firmware)
+{
+ int rotation;
+ int err;
+
+ if (!firmware)
+ return 0;
+
+ err = panel_mipi_load_commands(mipi, firmware->commands.data,
+ firmware->commands.size);
+ if (err) {
+ dev_err(dev, "firmware: Malformed command sequence\n");
+ return err;
+ }
+
+ mipi->width_mm = be16_to_cpu(firmware->config->width_mm);
+ mipi->height_mm = be16_to_cpu(firmware->config->height_mm);
+
+ rotation = be16_to_cpu(firmware->config->rotation);
+ if (rotation == 0)
+ mipi->orientation = DRM_MODE_PANEL_ORIENTATION_NORMAL;
+ else if (rotation == 90)
+ mipi->orientation = DRM_MODE_PANEL_ORIENTATION_RIGHT_UP;
+ else if (rotation == 180)
+ mipi->orientation = DRM_MODE_PANEL_ORIENTATION_BOTTOM_UP;
+ else if (rotation == 270)
+ mipi->orientation = DRM_MODE_PANEL_ORIENTATION_LEFT_UP;
+ else {
+ dev_err(dev, "firmware: Invalid rotation %u\n", rotation);
+ return -EINVAL;
+ }
+
+ if (firmware->config->reset_delay)
+ mipi->reset_delay = be16_to_cpu(firmware->config->reset_delay);
+ if (firmware->config->init_delay)
+ mipi->init_delay = be16_to_cpu(firmware->config->init_delay);
+ if (firmware->config->sleep_delay)
+ mipi->sleep_delay = be16_to_cpu(firmware->config->sleep_delay);
+ if (firmware->config->backlight_delay)
+ mipi->backlight_delay =
+ be16_to_cpu(firmware->config->backlight_delay);
+
+ if (firmware->config->bus_flags)
+ mipi->bus_flags = be32_to_cpu(firmware->config->bus_flags);
+
+ return 0;
+}
+
+static struct panel_firmware *panel_mipi_load_firmware(struct device *dev)
+{
+ const struct firmware *blob;
+ const struct panel_firmware_header *hdr;
+ const struct panel_firmware_config *config;
+ const struct panel_firmware_panel_timing *timings;
+ struct panel_firmware *firmware;
+ const char *firmware_name;
+ size_t command_offset;
+ char filename[128];
+ int err;
+
+ err = of_property_read_string_index(dev->of_node, "compatible", 0,
+ &firmware_name);
+ if (err)
+ return NULL;
+
+ /* Use firmware-name instead if any */
+ of_property_read_string(dev->of_node, "firmware-name", &firmware_name);
+
+ snprintf(filename, sizeof(filename), "panels/%s.panel", firmware_name);
+ err = firmware_request_nowarn(&blob, filename, dev);
+ if (err)
+ return NULL;
+
+ hdr = (const struct panel_firmware_header *)blob->data;
+
+ if (blob->size < sizeof(*hdr)) {
+ dev_err(dev, "firmware: %s: file size %zu is too small\n",
+ filename, blob->size);
+ err = -EINVAL;
+ goto err;
+ }
+
+ if (memcmp(hdr->magic, panel_firmware_magic, sizeof(hdr->magic))) {
+ dev_err(dev, "firmware: %s: Bad magic %15ph\n", filename,
+ hdr->magic);
+ err = -EINVAL;
+ goto err;
+ }
+
+ if (hdr->file_format_version != 1) {
+ dev_err(dev, "firmware: %s: version %u is not supported\n",
+ filename, hdr->file_format_version);
+ err = -EINVAL;
+ goto err;
+ }
+
+ config = (struct panel_firmware_config *)(blob->data + sizeof(*hdr));
+ timings = (struct panel_firmware_panel_timing *)(blob->data +
+ sizeof(*hdr) +
+ sizeof(*config));
+ command_offset = sizeof(*hdr) + sizeof(*config) +
+ config->num_timings * sizeof(*timings);
+ if (blob->size < command_offset) {
+ dev_err(dev, "firmware: %s: file size %zu is too small\n",
+ filename, blob->size);
+ err = -EINVAL;
+ goto err;
+ }
+
+ firmware = devm_kzalloc(dev, sizeof(*firmware), GFP_KERNEL);
+ if (!firmware) {
+ err = -ENOMEM;
+ goto err;
+ }
+
+ firmware->blob = blob;
+ firmware->config = config;
+ firmware->timings = timings;
+ firmware->commands.data = blob->data + command_offset;
+ firmware->commands.size = blob->size - command_offset;
+
+ dev_info(dev, "firmware: %s: loaded successfully\n", filename);
+
+ return firmware;
+
+err:
+ release_firmware(blob);
+
+ return ERR_PTR(err);
+}
+
+static int panel_mipi_read_display_id(struct panel_mipi *mipi, u8 *dest)
+{
+ return panel_mipi_read(mipi, MIPI_DCS_GET_DISPLAY_ID, dest, 3);
+}
+
+static int panel_mipi_enable(struct drm_panel *panel)
+{
+ struct panel_mipi *mipi = to_panel_mipi(panel);
+ int err;
+
+ PANEL_MIPI_TRANSACTION(mipi, {
+ err = panel_mipi_write(mipi, MIPI_DCS_SET_DISPLAY_ON);
+ });
+ if (err)
+ return err;
+
+ if (panel->backlight) {
+ /* Wait for the picture to be ready before enabling backlight */
+ msleep(mipi->backlight_delay);
+ }
+
+ return 0;
+}
+
+static int panel_mipi_disable(struct drm_panel *panel)
+{
+ struct panel_mipi *mipi = to_panel_mipi(panel);
+ int err = 0;
+
+ PANEL_MIPI_TRANSACTION(mipi, {
+ err = panel_mipi_write(mipi, MIPI_DCS_SET_DISPLAY_OFF);
+ });
+
+ return err;
+}
+
+static int panel_mipi_prepare(struct drm_panel *panel)
+{
+ struct panel_mipi *mipi = to_panel_mipi(panel);
+ int err;
+
+ err = regulator_bulk_enable(ARRAY_SIZE(mipi->supplies), mipi->supplies);
+ if (err)
+ return err;
+
+ PANEL_MIPI_TRANSACTION(mipi, {
+ /* Reset the chip */
+ if (mipi->reset) {
+ gpiod_set_value_cansleep(mipi->reset, 1);
+ msleep(mipi->reset_delay);
+ gpiod_set_value_cansleep(mipi->reset, 0);
+ }
+
+ msleep(mipi->init_delay);
+
+ /* Read the Display ID */
+ if (!panel_mipi_read_display_id(mipi,
+ mipi->initial_display_id)) {
+ dev_info(panel->dev, "MIPI Display ID: %3phN\n",
+ mipi->initial_display_id);
+ }
+
+ /* Write the init-sequence */
+ err = panel_mipi_write_commands(mipi, mipi->commands.data,
+ mipi->commands.size);
+ if (err)
+ break;
+
+ /* Ensure to exit from a sleep mode */
+ err = panel_mipi_write(mipi, MIPI_DCS_EXIT_SLEEP_MODE);
+ if (err)
+ break;
+ msleep(mipi->sleep_delay);
+
+ /*
+ * Read the Display ID again,
+ * because the init-sequence might have changed the ID.
+ */
+ if (!panel_mipi_read_display_id(mipi, mipi->display_id) &&
+ memcmp(mipi->initial_display_id, mipi->display_id,
+ sizeof(mipi->display_id))) {
+ dev_info(panel->dev,
+ "MIPI Display ID: %3phN (changed)\n",
+ mipi->display_id);
+ }
+ });
+
+ if (err) {
+ regulator_bulk_disable(ARRAY_SIZE(mipi->supplies),
+ mipi->supplies);
+ return err;
+ }
+
+ return 0;
+}
+
+static int panel_mipi_unprepare(struct drm_panel *panel)
+{
+ struct panel_mipi *mipi = to_panel_mipi(panel);
+ int err;
+
+ PANEL_MIPI_TRANSACTION(mipi, {
+ err = panel_mipi_write(mipi, MIPI_DCS_ENTER_SLEEP_MODE);
+ if (err)
+ dev_warn(panel->dev, "Failed to enter a sleep mode\n");
+ msleep(mipi->sleep_delay);
+ });
+
+ if (mipi->reset) {
+ gpiod_set_value_cansleep(mipi->reset, 1);
+ msleep(mipi->reset_delay);
+ }
+
+ regulator_bulk_disable(ARRAY_SIZE(mipi->supplies), mipi->supplies);
+
+ return 0;
+}
+
+static int panel_mipi_get_modes(struct drm_panel *panel,
+ struct drm_connector *connector)
+{
+ struct panel_mipi *mipi = to_panel_mipi(panel);
+ struct drm_display_mode *mode;
+ unsigned int count = 0;
+
+ list_for_each_entry(mode, &mipi->mode_list, head) {
+ struct drm_display_mode *dmode;
+
+ dmode = drm_mode_duplicate(connector->dev, mode);
+ if (!dmode)
+ return -EINVAL;
+
+ drm_mode_probed_add(connector, dmode);
+ count++;
+ }
+
+ connector->display_info.bpc = 8;
+ connector->display_info.width_mm = mipi->width_mm;
+ connector->display_info.height_mm = mipi->height_mm;
+ connector->display_info.bus_flags = mipi->bus_flags;
+ drm_display_info_set_bus_formats(&connector->display_info,
+ &mipi->bus_format, 1);
+
+ /*
+ * TODO: Remove once all drm drivers call
+ * drm_connector_set_orientation_from_panel()
+ */
+ drm_connector_set_panel_orientation(connector, mipi->orientation);
+
+ return count;
+}
+
+static enum drm_panel_orientation
+panel_mipi_get_orientation(struct drm_panel *panel)
+{
+ struct panel_mipi *mipi = to_panel_mipi(panel);
+
+ return mipi->orientation;
+}
+
+#ifdef CONFIG_DEBUG_FS
+
+static int display_id_show(struct seq_file *s, void *data)
+{
+ struct drm_panel *panel = s->private;
+ struct panel_mipi *mipi = to_panel_mipi(panel);
+ int err = 0;
+
+ PANEL_MIPI_TRANSACTION(mipi, {
+ err = panel_mipi_read_display_id(mipi, mipi->display_id);
+ if (err)
+ break;
+
+ seq_printf(s, "%3phN\n", mipi->display_id);
+ });
+
+ return err;
+}
+DEFINE_SHOW_ATTRIBUTE(display_id);
+
+static int initial_display_id_show(struct seq_file *s, void *data)
+{
+ struct drm_panel *panel = s->private;
+ struct panel_mipi *mipi = to_panel_mipi(panel);
+
+ seq_printf(s, "%3phN\n", mipi->initial_display_id);
+
+ return 0;
+}
+
+DEFINE_SHOW_ATTRIBUTE(initial_display_id);
+
+static int commands_open(struct inode *inode, struct file *file)
+{
+ struct panel_mipi *mipi = inode->i_private;
+
+ mipi->recvlen = 0;
+ file->private_data = mipi;
+
+ return 0;
+}
+
+static ssize_t commands_write(struct file *file, const char __user *data,
+ size_t size, loff_t *pos)
+{
+ struct panel_mipi *mipi = file->private_data;
+ ssize_t s = min_t(u64, sizeof(mipi->recvbuf) - mipi->recvlen, size);
+ size_t valid_len;
+ int err;
+
+ if (!s || !mipi)
+ return -EINVAL;
+
+ if (copy_from_user(mipi->recvbuf + mipi->recvlen, data, s))
+ return -EINVAL;
+
+ mipi->recvlen += s;
+
+ err = panel_mipi_validate_commands(mipi, mipi->recvbuf, mipi->recvlen,
+ &valid_len);
+ if (err)
+ return err;
+
+ if (valid_len > 0) {
+ PANEL_MIPI_TRANSACTION(mipi, {
+ err = panel_mipi_write_commands(mipi, mipi->recvbuf,
+ valid_len);
+ });
+ if (err)
+ return err;
+
+ mipi->recvlen -= valid_len;
+ if (mipi->recvlen)
+ memmove(mipi->recvbuf, mipi->recvbuf + valid_len,
+ mipi->recvlen);
+ }
+
+ return s;
+}
+
+static int commands_release(struct inode *inode, struct file *file)
+{
+ struct panel_mipi *mipi = inode->i_private;
+
+ if (mipi->recvlen) {
+ dev_err(mipi->panel.dev, "Premature end of commands\n");
+ return -EIO;
+ }
+
+ return 0;
+}
+
+static const struct file_operations commands_fops = {
+ .owner = THIS_MODULE,
+ .open = commands_open,
+ .write = commands_write,
+ .release = commands_release,
+};
+
+#endif
+
+static void panel_mipi_debugfs_init(struct device *dev, struct panel_mipi *mipi)
+{
+#ifdef CONFIG_DEBUG_FS
+ struct dentry *dir;
+
+ mipi->debugfs = debugfs_lookup("panel-mipi", NULL);
+ if (!mipi->debugfs)
+ mipi->debugfs = debugfs_create_dir("panel-mipi", NULL);
+
+ dir = debugfs_create_dir(dev_name(dev), mipi->debugfs);
+ debugfs_create_file("display-id", 0600, dir, mipi, &display_id_fops);
+ debugfs_create_file("initial-display-id", 0600, dir, mipi,
+ &initial_display_id_fops);
+ debugfs_create_file("commands", 0600, dir, mipi, &commands_fops);
+#endif
+}
+
+static void panel_mipi_debugfs_remove(struct panel_mipi *mipi)
+{
+#ifdef CONFIG_DEBUG_FS
+ debugfs_remove_recursive(mipi->debugfs);
+ mipi->debugfs = NULL;
+#endif
+}
+
+static const struct drm_panel_funcs panel_mipi_funcs = {
+ .prepare = panel_mipi_prepare,
+ .unprepare = panel_mipi_unprepare,
+ .enable = panel_mipi_enable,
+ .disable = panel_mipi_disable,
+ .get_modes = panel_mipi_get_modes,
+ .get_orientation = panel_mipi_get_orientation,
+};
+
+static int panel_mipi_add_mode(struct device *dev, struct panel_mipi *mipi,
+ const struct drm_display_mode *src_mode,
+ const char *source)
+{
+ struct drm_display_mode *mode;
+
+ mode = devm_kzalloc(dev, sizeof(*mode), GFP_KERNEL);
+ if (!mode)
+ return -ENOMEM;
+
+ drm_mode_copy(mode, src_mode);
+
+ if (!mode->clock)
+ mode->clock = mode->htotal * mode->vtotal * 60 / 1000;
+
+ mode->type |= DRM_MODE_TYPE_DRIVER;
+
+ dev_info(dev, "Modeline " DRM_MODE_FMT " added by %s\n",
+ DRM_MODE_ARG(mode), source);
+
+ list_add_tail(&mode->head, &mipi->mode_list);
+
+ return 0;
+}
+
+static int panel_mipi_firmware_read_modes(const struct panel_firmware *firmware,
+ struct drm_display_mode *mode,
+ u32 *bus_flags, unsigned int i)
+{
+ const struct panel_firmware_panel_timing *timings =
+ &firmware->timings[i];
+
+ if (!firmware || i >= firmware->config->num_timings)
+ return -ENOENT;
+
+ if (!timings->hactive || !timings->vactive)
+ return -ENOENT;
+
+ memset(mode, 0, sizeof(*mode));
+
+ mode->clock = be32_to_cpu(timings->dclk);
+
+ mode->hdisplay = be16_to_cpu(timings->hactive);
+ mode->hsync_start = mode->hdisplay + be16_to_cpu(timings->hfp);
+ mode->hsync_end = mode->hsync_start + be16_to_cpu(timings->hslen);
+ mode->htotal = mode->hsync_end + be16_to_cpu(timings->hbp);
+
+ mode->vdisplay = be16_to_cpu(timings->vactive);
+ mode->vsync_start = mode->vdisplay + be16_to_cpu(timings->vfp);
+ mode->vsync_end = mode->vsync_start + be16_to_cpu(timings->vslen);
+ mode->vtotal = mode->vsync_end + be16_to_cpu(timings->vbp);
+
+ mode->flags = be32_to_cpu(timings->flags);
+
+ drm_mode_set_name(mode);
+
+ if (bus_flags && firmware->config->bus_flags)
+ *bus_flags = be32_to_cpu(firmware->config->bus_flags);
+
+ return 0;
+}
+
+static void drm_display_mode_from_timing(const struct display_timing *timing,
+ struct drm_display_mode *dmode,
+ u32 *bus_flags)
+{
+ struct videomode vm;
+
+ videomode_from_timing(timing, &vm);
+
+ memset(dmode, 0, sizeof(*dmode));
+ drm_display_mode_from_videomode(&vm, dmode);
+
+ if (bus_flags)
+ drm_bus_flags_from_videomode(&vm, bus_flags);
+}
+
+static int panel_mipi_read_of_display_timings(struct device *dev,
+ struct panel_mipi *mipi)
+{
+ struct device_node *np = dev->of_node;
+ struct device_node *timings_np;
+ struct display_timings *timings;
+ struct drm_display_mode mode = {};
+ u32 bus_flags;
+ unsigned int i;
+ int err;
+
+ /* To avoid the not found error message */
+ timings_np = of_get_child_by_name(np, "display-timings");
+ if (!timings_np)
+ return 0;
+ of_node_put(timings_np);
+
+ /* Then, load timings as usual */
+ timings = of_get_display_timings(np);
+ if (!timings)
+ return dev_err_probe(dev, -EINVAL,
+ "%pOF: Failed to get 'display-timings'\n",
+ dev->of_node);
+
+ for (i = 0; i < timings->num_timings; i++) {
+ drm_display_mode_from_timing(timings->timings[i], &mode,
+ &bus_flags);
+
+ if (timings->native_mode == i)
+ mode.type |= DRM_MODE_TYPE_PREFERRED;
+
+ err = panel_mipi_add_mode(dev, mipi, &mode, "display-timings");
+ if (err)
+ return err;
+ if (bus_flags)
+ mipi->bus_flags = bus_flags;
+ }
+
+ display_timings_release(timings);
+
+ return 0;
+}
+
+static int panel_mipi_probe_modes(struct device *dev, struct panel_mipi *mipi)
+{
+ unsigned int i = 0;
+ u32 bus_flags = 0;
+ struct drm_display_mode mode = {};
+ struct display_timing timing = {};
+ int err;
+
+ INIT_LIST_HEAD(&mipi->mode_list);
+
+ /* Read firmware first */
+ for (i = 0; mipi->firmware && i < mipi->firmware->config->num_timings;
+ i++) {
+ if (!panel_mipi_firmware_read_modes(mipi->firmware, &mode,
+ &bus_flags, i)) {
+ if (mipi->firmware->config->preferred_timing == i)
+ mode.type |= DRM_MODE_TYPE_PREFERRED;
+
+ err = panel_mipi_add_mode(dev, mipi, &mode, "firmware");
+ if (err)
+ return err;
+ if (bus_flags)
+ mipi->bus_flags = bus_flags;
+ }
+ }
+
+ /* Read `display-timings` and append if any */
+ err = panel_mipi_read_of_display_timings(dev, mipi);
+ if (err)
+ return err;
+
+ /* Read `panel-timing` and append if any */
+ err = of_get_display_timing(dev->of_node, "panel-timing", &timing);
+ if (err && err != -ENOENT)
+ return err;
+ else if (!err) {
+ drm_display_mode_from_timing(&timing, &mode, &bus_flags);
+ err = panel_mipi_add_mode(dev, mipi, &mode, "panel-timing");
+ if (err)
+ return err;
+ if (bus_flags)
+ mipi->bus_flags = bus_flags;
+ }
+
+ if (list_empty(&mipi->mode_list))
+ return dev_err_probe(dev, -EINVAL, "No modes defined\n");
+
+ return 0;
+}
+
+static void panel_mipi_cleanup(void *data)
+{
+ struct panel_mipi *mipi = (struct panel_mipi *)data;
+
+ panel_mipi_debugfs_remove(mipi);
+
+ drm_panel_remove(&mipi->panel);
+ drm_panel_disable(&mipi->panel);
+ drm_panel_unprepare(&mipi->panel);
+
+ if (mipi->firmware)
+ release_firmware(mipi->firmware->blob);
+}
+
+static int
+panel_mipi_backlight_update_status(struct backlight_device *backlight)
+{
+ struct panel_mipi *mipi = dev_get_drvdata(&backlight->dev);
+ int level = backlight_get_brightness(backlight);
+ int err;
+
+ PANEL_MIPI_TRANSACTION_LPM(mipi, false, {
+ err = panel_mipi_write(mipi, MIPI_DCS_SET_DISPLAY_BRIGHTNESS,
+ level, level);
+ });
+
+ return err;
+}
+
+static int
+panel_mipi_backlight_get_brightness(struct backlight_device *backlight)
+{
+ return backlight_get_brightness(backlight);
+}
+
+static const struct backlight_ops panel_mipi_backlight_ops = {
+ .get_brightness = panel_mipi_backlight_get_brightness,
+ .update_status = panel_mipi_backlight_update_status,
+};
+
+static int panel_mipi_set_backlight(struct drm_panel *panel, struct device *dev,
+ struct panel_mipi *mipi)
+{
+ int err;
+
+ err = drm_panel_of_backlight(panel);
+ if (err)
+ return err;
+
+ if (!panel->backlight) {
+ struct backlight_device *backlight = NULL;
+ struct backlight_properties props = {
+ .max_brightness = 255,
+ .type = BACKLIGHT_RAW,
+ };
+
+ backlight = devm_backlight_device_register(
+ dev, dev_name(dev), dev, mipi,
+ &panel_mipi_backlight_ops, &props);
+ if (IS_ERR(backlight))
+ return PTR_ERR(backlight);
+
+ panel->backlight = backlight;
+ }
+
+ return 0;
+}
+
+static int panel_mipi_probe(struct device *dev, int connector_type)
+{
+ struct panel_mipi *mipi;
+ int err;
+
+ mipi = devm_kzalloc(dev, sizeof(*mipi), GFP_KERNEL);
+ if (!mipi)
+ return -ENOMEM;
+
+ mutex_init(&mipi->lock);
+
+ /* Get `power-supply` and `io-supply` (if any) */
+ mipi->supplies[0].supply = "power";
+ mipi->supplies[1].supply = "io";
+ err = devm_regulator_bulk_get(dev, ARRAY_SIZE(mipi->supplies),
+ mipi->supplies);
+ if (err < 0) {
+ return dev_err_probe(dev, err,
+ "%pOF: Failed to get regulators\n",
+ dev->of_node);
+ }
+
+ /* GPIO for /RESET */
+ mipi->reset = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_LOW);
+ if (IS_ERR(mipi->reset))
+ return dev_err_probe(dev, PTR_ERR(mipi->reset),
+ "%pOF: Failed to get GPIO for RESET\n",
+ dev->of_node);
+
+ /* Default values */
+ mipi->bus_format = MEDIA_BUS_FMT_RGB888_1X24;
+ mipi->reset_delay = 1;
+ mipi->init_delay = 10;
+ mipi->sleep_delay = 120;
+ mipi->backlight_delay = 120;
+
+ /* Load from the firmware */
+ mipi->firmware = panel_mipi_load_firmware(dev);
+ if (IS_ERR(mipi->firmware))
+ return dev_err_probe(dev, PTR_ERR(mipi->firmware),
+ "Failed to load a firmware\n");
+ err = panel_mipi_read_firmware(dev, mipi, mipi->firmware);
+ if (err)
+ return err;
+
+ /* Load panel modes */
+ err = panel_mipi_probe_modes(dev, mipi);
+ if (err)
+ return err;
+
+ /* Load overrides from the DT */
+ const struct property *prop =
+ of_find_property(dev->of_node, "init-sequence", NULL);
+ if (prop && prop->value && prop->length > 0) {
+ err = panel_mipi_load_commands(mipi, prop->value, prop->length);
+ if (err)
+ return dev_err_probe(
+ dev, err, "%pOF: Malformed command sequence\n",
+ dev->of_node);
+ }
+ of_property_read_u32(dev->of_node, "width-mm", &mipi->width_mm);
+ of_property_read_u32(dev->of_node, "height-mm", &mipi->height_mm);
+ of_property_read_u32(dev->of_node, "reset-delay", &mipi->reset_delay);
+ of_property_read_u32(dev->of_node, "init-delay", &mipi->init_delay);
+ of_property_read_u32(dev->of_node, "sleep-delay", &mipi->sleep_delay);
+ of_property_read_u32(dev->of_node, "backlight-delay",
+ &mipi->backlight_delay);
+ if (of_find_property(dev->of_node, "rotation", NULL)) {
+ err = of_drm_get_panel_orientation(dev->of_node,
+ &mipi->orientation);
+ if (err)
+ return dev_err_probe(dev, err,
+ "%pOF: Invalid rotation\n",
+ dev->of_node);
+ }
+
+ /* DRM panel setup */
+ drm_panel_init(&mipi->panel, dev, &panel_mipi_funcs, connector_type);
+
+ err = panel_mipi_set_backlight(&mipi->panel, dev, mipi);
+ if (err)
+ return dev_err_probe(dev, err, "Failed to set backlight\n");
+
+ drm_panel_add(&mipi->panel);
+
+ dev_set_drvdata(dev, mipi);
+
+ panel_mipi_debugfs_init(dev, mipi);
+
+ return devm_add_action_or_reset(dev, panel_mipi_cleanup, mipi);
+}
+
+static unsigned long panel_mipi_read_dsi_mode_props(struct device_node *np)
+{
+ unsigned long mode = 0;
+
+ mode |= of_property_read_bool(np, "dsi-mode-video") ?
+ MIPI_DSI_MODE_VIDEO :
+ 0;
+ mode |= of_property_read_bool(np, "dsi-mode-video-burst") ?
+ MIPI_DSI_MODE_VIDEO_BURST :
+ 0;
+ mode |= of_property_read_bool(np, "dsi-mode-video-sync-pulse") ?
+ MIPI_DSI_MODE_VIDEO_SYNC_PULSE :
+ 0;
+ mode |= of_property_read_bool(np, "dsi-mode-video-auto-vert") ?
+ MIPI_DSI_MODE_VIDEO_AUTO_VERT :
+ 0;
+ mode |= of_property_read_bool(np, "dsi-mode-video-hse") ?
+ MIPI_DSI_MODE_VIDEO_HSE :
+ 0;
+ mode |= of_property_read_bool(np, "dsi-mode-video-no-hfp") ?
+ MIPI_DSI_MODE_VIDEO_NO_HFP :
+ 0;
+ mode |= of_property_read_bool(np, "dsi-mode-video-no-hbp") ?
+ MIPI_DSI_MODE_VIDEO_NO_HBP :
+ 0;
+ mode |= of_property_read_bool(np, "dsi-mode-video-no-hsa") ?
+ MIPI_DSI_MODE_VIDEO_NO_HSA :
+ 0;
+ mode |= of_property_read_bool(np, "dsi-mode-vsync-flush") ?
+ MIPI_DSI_MODE_VSYNC_FLUSH :
+ 0;
+ mode |= of_property_read_bool(np, "dsi-mode-no-eot-packet") ?
+ MIPI_DSI_MODE_NO_EOT_PACKET :
+ 0;
+ mode |= of_property_read_bool(np, "dsi-clock-non-continuous") ?
+ MIPI_DSI_CLOCK_NON_CONTINUOUS :
+ 0;
+ mode |= of_property_read_bool(np, "dsi-mode-lpm") ? MIPI_DSI_MODE_LPM :
+ 0;
+ mode |= of_property_read_bool(np, "dsi-hs-pkt-end-aligned") ?
+ MIPI_DSI_HS_PKT_END_ALIGNED :
+ 0;
+
+ return mode;
+}
+
+static int panel_mipi_dsi_probe(struct mipi_dsi_device *dsi)
+{
+ struct panel_mipi *mipi;
+ const char *format;
+ int err;
+
+ err = panel_mipi_probe(&dsi->dev, DRM_MODE_CONNECTOR_DSI);
+ if (err)
+ return err;
+
+ mipi = dev_get_drvdata(&dsi->dev);
+ mipi->dsi = dsi;
+ mipi->write_command = panel_mipi_dsi_write;
+ mipi->read_command = panel_mipi_dsi_read;
+
+ /* Default values */
+ dsi->lanes = 1;
+ dsi->format = MIPI_DSI_FMT_RGB888;
+ dsi->mode_flags = 0;
+
+ /* Read from the firmware */
+ if (mipi->firmware) {
+ dsi->lanes = be16_to_cpu(mipi->firmware->config->dsi_lanes);
+ dsi->format = be16_to_cpu(mipi->firmware->config->dsi_format);
+ dsi->mode_flags =
+ be32_to_cpu(mipi->firmware->config->dsi_mode_flags);
+ }
+
+ /* Load from the DT */
+ of_property_read_u32(dsi->dev.of_node, "dsi-lanes", &dsi->lanes);
+ if (!of_property_read_string(dsi->dev.of_node, "dsi-format", &format)) {
+ if (!strcmp(format, "rgb888"))
+ dsi->format = MIPI_DSI_FMT_RGB888;
+ else if (!strcmp(format, "rgb666"))
+ dsi->format = MIPI_DSI_FMT_RGB666;
+ else if (!strcmp(format, "rgb666-packed"))
+ dsi->format = MIPI_DSI_FMT_RGB666_PACKED;
+ else if (!strcmp(format, "rgb565"))
+ dsi->format = MIPI_DSI_FMT_RGB565;
+ else
+ return dev_err_probe(&dsi->dev, -EINVAL,
+ "%pOF: Unknown dsi-format '%s'\n",
+ dsi->dev.of_node, format);
+ }
+ dsi->mode_flags |= panel_mipi_read_dsi_mode_props(dsi->dev.of_node);
+
+ if (!dsi->lanes)
+ return dev_err_probe(&dsi->dev, -EINVAL,
+ "dsi-lanes == 0 for DSI panel\n");
+
+ /* Adjust bus_format */
+ switch (dsi->format) {
+ case MIPI_DSI_FMT_RGB888:
+ mipi->bus_format = MEDIA_BUS_FMT_RGB888_1X24;
+ break;
+ case MIPI_DSI_FMT_RGB666:
+ mipi->bus_format = MEDIA_BUS_FMT_RGB666_1X24_CPADHI;
+ break;
+ case MIPI_DSI_FMT_RGB666_PACKED:
+ mipi->bus_format = MEDIA_BUS_FMT_RGB666_1X18;
+ break;
+ case MIPI_DSI_FMT_RGB565:
+ mipi->bus_format = MEDIA_BUS_FMT_RGB565_1X16;
+ break;
+ }
+
+ err = mipi_dsi_attach(dsi);
+ if (err)
+ return dev_err_probe(&dsi->dev, err, "Failed to init DSI\n");
+
+ return 0;
+}
+
+static int panel_mipi_spi_probe(struct spi_device *spi)
+{
+ struct panel_mipi *mipi;
+ struct gpio_desc *dc;
+ int err;
+
+ err = panel_mipi_probe(&spi->dev, DRM_MODE_CONNECTOR_DPI);
+ if (err)
+ return err;
+
+ mipi = dev_get_drvdata(&spi->dev);
+ mipi->write_command = panel_mipi_dbi_write;
+ mipi->read_command = panel_mipi_dbi_read;
+
+ dc = devm_gpiod_get_optional(&spi->dev, "dc", GPIOD_OUT_LOW);
+ if (IS_ERR(dc))
+ return dev_err_probe(&spi->dev, PTR_ERR(dc),
+ "Failed to get GPIO for D/CX\n");
+
+ err = mipi_dbi_spi_init(spi, &mipi->dbi, dc);
+ if (err)
+ return dev_err_probe(&spi->dev, err, "Failed to init SPI\n");
+
+ return 0;
+}
+
+static void panel_mipi_dsi_remove(struct mipi_dsi_device *dsi)
+{
+ mipi_dsi_detach(dsi);
+}
+
+static const struct of_device_id panel_mipi_dsi_of_match[] = {
+ { .compatible = "panel-mipi-dsi" },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, panel_mipi_dsi_of_match);
+
+static const struct of_device_id panel_mipi_spi_of_match[] = {
+ { .compatible = "panel-mipi-dpi-spi" },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, panel_mipi_spi_of_match);
+
+static const struct spi_device_id panel_mipi_spi_ids[] = {
+ { "panel-mipi-dpi-spi" },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(spi, panel_mipi_spi_ids);
+
+static struct mipi_dsi_driver panel_mipi_dsi_driver = {
+ .probe = panel_mipi_dsi_probe,
+ .remove = panel_mipi_dsi_remove,
+ .driver = {
+ .name = "panel-mipi",
+ .of_match_table = panel_mipi_dsi_of_match,
+ },
+};
+
+static struct spi_driver panel_mipi_spi_driver = {
+ .probe = panel_mipi_spi_probe,
+ .id_table = panel_mipi_spi_ids,
+ .driver = {
+ .name = "panel-mipi",
+ .of_match_table = panel_mipi_spi_of_match,
+ },
+};
+
+static int __init panel_mipi_driver_init(void)
+{
+ int err;
+
+ if (IS_ENABLED(CONFIG_SPI)) {
+ err = spi_register_driver(&panel_mipi_spi_driver);
+ if (err)
+ return err;
+ }
+
+ if (IS_ENABLED(CONFIG_DRM_MIPI_DSI)) {
+ err = mipi_dsi_driver_register(&panel_mipi_dsi_driver);
+ if (err) {
+ if (IS_ENABLED(CONFIG_SPI))
+ spi_unregister_driver(&panel_mipi_spi_driver);
+ return err;
+ }
+ }
+
+ return 0;
+}
+module_init(panel_mipi_driver_init);
+
+static void __exit panel_mipi_driver_exit(void)
+{
+ if (IS_ENABLED(CONFIG_DRM_MIPI_DSI))
+ mipi_dsi_driver_unregister(&panel_mipi_dsi_driver);
+
+ if (IS_ENABLED(CONFIG_SPI))
+ spi_unregister_driver(&panel_mipi_spi_driver);
+}
+module_exit(panel_mipi_driver_exit);
+
+MODULE_DESCRIPTION("Generic MIPI-DSI/DPI(+SPI) Panel Driver");
+MODULE_AUTHOR("Hironori KIKUCHI <kikuchan98@...il.com>");
+MODULE_LICENSE("GPL v2");
--
2.48.1
Powered by blists - more mailing lists