linux-kernel.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH] drivers: ddcci: upstream DDCCI driver
@ 2022-02-26  2:20 Yusuf Khan
  2022-02-26  2:33 ` Yusuf Khan
  2022-02-26  9:38 ` Arnd Bergmann
  0 siblings, 2 replies; 7+ messages in thread
From: Yusuf Khan @ 2022-02-26  2:20 UTC (permalink / raw)
  To: linux-kernel
  Cc: jasowang, mikelley, mst, gregkh, javier, arnd, will, axboe,
	bjorn.andersson, Yusuf Khan

This patch upstreams the DDCCI driver by Christoph Grenz into
the kernel. The original gitlab page is loacted at https://gitlab
.com/ddcci-driver-linux/ddcci-driver-linux/-/tree/master.

Signed-off-by: Yusuf Khan <yusisamerican@gmail.com>
---
 drivers/Kconfig                 |    2 +
 drivers/Makefile                |    1 +
 drivers/ddcci/Kconfig           |    3 +
 drivers/ddcci/Makefile          |    3 +
 drivers/ddcci/ddcci-backlight.c |  413 +++++++
 drivers/ddcci/ddcci.c           | 1895 +++++++++++++++++++++++++++++++
 include/linux/ddcci.h           |  164 +++
 7 files changed, 2481 insertions(+)
 create mode 100644 drivers/ddcci/Kconfig
 create mode 100644 drivers/ddcci/Makefile
 create mode 100644 drivers/ddcci/ddcci-backlight.c
 create mode 100644 drivers/ddcci/ddcci.c
 create mode 100644 include/linux/ddcci.h

diff --git a/drivers/Kconfig b/drivers/Kconfig
index 0d399ddaa185..3a58a29113f9 100644
--- a/drivers/Kconfig
+++ b/drivers/Kconfig
@@ -236,4 +236,6 @@ source "drivers/interconnect/Kconfig"
 source "drivers/counter/Kconfig"
 
 source "drivers/most/Kconfig"
+
+source "drivers/ddcci/Kconfig"
 endmenu
diff --git a/drivers/Makefile b/drivers/Makefile
index a110338c860c..c9181f1dc5aa 100644
--- a/drivers/Makefile
+++ b/drivers/Makefile
@@ -21,6 +21,7 @@ obj-y				+= pci/
 obj-$(CONFIG_PARISC)		+= parisc/
 obj-$(CONFIG_RAPIDIO)		+= rapidio/
 obj-y				+= video/
+obj-$(CONFIG_DDCCI)				+= ddcci/
 obj-y				+= idle/
 
 # IPMI must come before ACPI in order to provide IPMI opregion support
diff --git a/drivers/ddcci/Kconfig b/drivers/ddcci/Kconfig
new file mode 100644
index 000000000000..416881cc88e1
--- /dev/null
+++ b/drivers/ddcci/Kconfig
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-only
+menuconfig DDCCI
+	tristate "DDCCI display protocol support"
diff --git a/drivers/ddcci/Makefile b/drivers/ddcci/Makefile
new file mode 100644
index 000000000000..cce606be8038
--- /dev/null
+++ b/drivers/ddcci/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-$(CONFIG_DDCCI) += ddcci.o
+obj-$(CONFIG_DDCCI) += ddcci-backlight.o
diff --git a/drivers/ddcci/ddcci-backlight.c b/drivers/ddcci/ddcci-backlight.c
new file mode 100644
index 000000000000..7a9852207f0b
--- /dev/null
+++ b/drivers/ddcci/ddcci-backlight.c
@@ -0,0 +1,413 @@
+/*
+ *  DDC/CI monitor backlight driver
+ *
+ *  Copyright (c) 2015 Christoph Grenz
+ */
+
+/*
+ * 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.
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+#include <linux/backlight.h>
+#include <linux/module.h>
+#include <linux/fb.h>
+#include <linux/sysfs.h>
+
+#include <linux/ddcci.h>
+
+
+#define DDCCI_COMMAND_READ	0x01	/* read ctrl value */
+#define DDCCI_REPLY_READ	0x02	/* read ctrl value reply */
+#define DDCCI_COMMAND_WRITE	0x03	/* write ctrl value */
+#define DDCCI_COMMAND_SAVE	0x0c	/* save current settings */
+
+#define DDCCI_MONITOR_LUMINANCE	0x10
+#define DDCCI_MONITOR_BACKLIGHT	0x13
+#define DDCCI_MONITOR_BL_WHITE		0x6B
+
+static bool convenience_symlink = true;
+
+struct ddcci_monitor_drv_data {
+	struct ddcci_device *device;
+	struct backlight_device *bl_dev;
+	struct device *fb_dev;
+	unsigned char used_vcp;
+};
+
+static int ddcci_monitor_writectrl(struct ddcci_device *device,
+				   unsigned char ctrl, unsigned short value)
+{
+	unsigned char buf[4];
+	int ret;
+
+	buf[0] = DDCCI_COMMAND_WRITE;
+	buf[1] = ctrl;
+	buf[2] = (value >> 8);
+	buf[3] = (value & 255);
+
+	ret = ddcci_device_write(device, true, buf, sizeof(buf));
+
+	return ret;
+}
+
+static int ddcci_monitor_readctrl(struct ddcci_device *device,
+				  unsigned char ctrl, unsigned short *value,
+				  unsigned short *maximum)
+{
+	int ret;
+	unsigned char buf[10];
+
+	buf[0] = DDCCI_COMMAND_READ;
+	buf[1] = ctrl;
+
+	ret = ddcci_device_writeread(device, true, buf, 2, sizeof(buf));
+	if (ret < 0)
+		return ret;
+
+	if (ret == 0)
+		return -ENOTSUPP;
+
+	if (ret == 8 && buf[0] == DDCCI_REPLY_READ && buf[2] == ctrl) {
+		if (value)
+			*value = buf[6] * 256 + buf[7];
+
+		if (maximum)
+			*maximum = buf[4] * 256 + buf[5];
+
+		if (buf[1] == 1)
+			return -ENOTSUPP;
+		if (buf[1] != 0)
+			return -EIO;
+		return 0;
+	}
+
+	return -EIO;
+}
+
+static int ddcci_backlight_check_fb(struct backlight_device *bl,
+				   struct fb_info *info)
+{
+	struct ddcci_monitor_drv_data *drv_data = bl_get_data(bl);
+
+	return drv_data->fb_dev == NULL || drv_data->fb_dev == info->dev;
+}
+
+static int ddcci_backlight_update_status(struct backlight_device *bl)
+{
+	struct ddcci_monitor_drv_data *drv_data = bl_get_data(bl);
+	int brightness = bl->props.brightness;
+	int ret;
+
+	if (bl->props.power != FB_BLANK_UNBLANK ||
+	    bl->props.state & BL_CORE_FBBLANK)
+		brightness = 0;
+
+	ret = ddcci_monitor_writectrl(drv_data->device, drv_data->used_vcp,
+				      brightness);
+	if (ret > 0)
+		ret = 0;
+	return ret;
+}
+
+static int ddcci_backlight_get_brightness(struct backlight_device *bl)
+{
+	unsigned short value = 0, maxval = 0;
+	int ret;
+	struct ddcci_monitor_drv_data *drv_data = bl_get_data(bl);
+
+	ret = ddcci_monitor_readctrl(drv_data->device, drv_data->used_vcp,
+				     &value, &maxval);
+	if (ret < 0)
+		return ret;
+
+	bl->props.brightness = value;
+	bl->props.max_brightness = maxval;
+	ret = value;
+
+	return ret;
+}
+
+static const struct backlight_ops ddcci_backlight_ops = {
+	.options	= 0,
+	.update_status	= ddcci_backlight_update_status,
+	.get_brightness = ddcci_backlight_get_brightness,
+	.check_fb	= ddcci_backlight_check_fb,
+};
+
+static const char *ddcci_monitor_vcp_name(unsigned char vcp)
+{
+	switch (vcp) {
+		case DDCCI_MONITOR_BL_WHITE:
+			return "backlight";
+		case DDCCI_MONITOR_LUMINANCE:
+			return "luminance";
+		default:
+			return "???";
+	}
+}
+
+static const char *ddcci_monitor_next_vcp_item(const char *ptr)
+{
+	int depth = 0;
+
+	/* Sanity check */
+	if (unlikely(ptr == NULL || ptr[0] == '\0'))
+		return NULL;
+
+	/* Find next white space outside of parentheses */
+	while ((ptr = strpbrk(ptr, " ()"))) {
+		if (!ptr || depth == INT_MAX) {
+			return NULL;
+		} else if (*ptr == '(') {
+			depth++;
+		} else if (depth > 0) {
+			if (*ptr == ')')
+				depth--;
+		} else {
+			break;
+		}
+		++ptr;
+	}
+
+	/* Skip over whitespace */
+	ptr = skip_spaces(ptr);
+
+	/* Check if we're now at the end of the list */
+	if (unlikely(*ptr == '\0' || *ptr == ')'))
+		return NULL;
+
+	return ptr;
+}
+
+static bool ddcci_monitor_find_vcp(unsigned char vcp, const char *s)
+{
+	const char *ptr = s;
+	char vcp_hex[3];
+
+	/* Sanity check */
+	if (unlikely(s == NULL || s[0] == '\0'))
+		return false;
+
+	/* Create hex representation of VCP */
+	if (unlikely(snprintf(vcp_hex, 3, "%02hhX", vcp) != 2)) {
+		pr_err("snprintf failed to convert to hex. This should not happen.\n");
+		return false;
+	}
+
+	/* Search for it */
+	do {
+		if (strncasecmp(vcp_hex, ptr, 2) == 0) {
+			if (ptr[2] == ' ' || ptr[2] == '(' || ptr[2] == ')') {
+				return true;
+			}
+		}
+	} while ((ptr = ddcci_monitor_next_vcp_item(ptr)));
+
+	return false;
+}
+
+static int ddcci_backlight_create_symlink(struct ddcci_device *ddcci_dev)
+{
+	int i, result;
+	struct device *dev = &ddcci_dev->dev;
+	struct kernfs_node *dirent;
+	for (i = 0; i < 3; ++i) {
+		dev = dev->parent;
+		if (!dev) {
+			dev_dbg(&ddcci_dev->dev, "failed to create convenience symlink: ancestor device not found\n");
+			return -ENOENT;
+		}
+	}
+	dirent = sysfs_get_dirent(dev->kobj.sd, "ddcci_backlight");
+	if (dirent) {
+		sysfs_put(dirent);
+		dev_dbg(&ddcci_dev->dev, "failed to create convenience symlink: %s/ddcci_backlight already exists\n", dev_name(dev));
+		return -EEXIST;
+	}
+
+	result = sysfs_create_link(&dev->kobj, &ddcci_dev->dev.kobj, "ddcci_backlight");
+	if (result == 0) {
+		dev_dbg(&ddcci_dev->dev, "created symlink %s/ddcci_backlight\n", dev_name(dev));
+	} else {
+		dev_info(&ddcci_dev->dev, "failed to create convenience symlink: %d\n", result);
+	}
+	return result;
+}
+
+static int ddcci_backlight_remove_symlink(struct ddcci_device *ddcci_dev)
+{
+	int i;
+	struct device *dev = &ddcci_dev->dev;
+	struct kernfs_node *dirent;
+	for (i = 0; i < 3; ++i) {
+		dev = dev->parent;
+		if (!dev)
+			return -ENOENT;
+	}
+	dirent = sysfs_get_dirent(dev->kobj.sd, "ddcci_backlight");
+	if (!dirent) {
+		return -ENOENT;
+	}
+
+	if ((dirent->flags & KERNFS_LINK) == 0) {
+		sysfs_put(dirent);
+		dev_dbg(&ddcci_dev->dev, "won't remove %s/ddcci_backlight: not a symlink\n", dev_name(dev));
+		return -EINVAL;
+	}
+
+	if (dirent->symlink.target_kn != ddcci_dev->dev.kobj.sd) {
+		sysfs_put(dirent);
+		dev_dbg(&ddcci_dev->dev, "won't remove %s/ddcci_backlight: we are not the link target\n", dev_name(dev));
+		return -EINVAL;
+	}
+
+	sysfs_put(dirent);
+
+	sysfs_remove_link(&dev->kobj, "ddcci_backlight");
+	dev_dbg(&ddcci_dev->dev, "removed symlink %s/ddcci_backlight\n", dev_name(dev));
+	return 0;
+}
+
+static int ddcci_monitor_probe(struct ddcci_device *dev,
+			       const struct ddcci_device_id *id)
+{
+	struct ddcci_monitor_drv_data *drv_data;
+	struct backlight_properties props;
+	struct backlight_device *bl = NULL;
+	int ret = 0;
+	bool support_luminance, support_bl_white;
+	unsigned short brightness = 0, max_brightness = 0;
+	const char *vcps;
+
+	dev_dbg(&dev->dev, "probing monitor backlight device\n");
+
+	/* Get VCP list */
+	vcps = ddcci_find_capstr_item(dev->capabilities, "vcp", NULL);
+	if (IS_ERR(vcps)) {
+		dev_info(&dev->dev,
+			 "monitor doesn't provide a list of supported controls.\n");
+		support_bl_white = support_luminance = true;
+	} else {
+		/* Check VCP list for supported VCPs */
+		support_bl_white = ddcci_monitor_find_vcp(DDCCI_MONITOR_BL_WHITE, vcps);
+		support_luminance = ddcci_monitor_find_vcp(DDCCI_MONITOR_LUMINANCE, vcps);
+		/* Fallback to trying if no support is found */
+		if (!support_bl_white && !support_luminance) {
+			dev_info(&dev->dev,
+				 "monitor doesn't announce support for backlight or luminance controls.\n");
+			support_bl_white = support_luminance = true;
+		}
+	}
+
+	/* Initialize driver data structure */
+	drv_data = devm_kzalloc(&dev->dev, sizeof(struct ddcci_monitor_drv_data),
+				GFP_KERNEL);
+	if (!drv_data)
+		return -ENOMEM;
+	drv_data->device = dev;
+
+	if (support_bl_white) {
+		/* Try getting backlight level */
+		dev_dbg(&dev->dev,
+			"trying to access \"backlight level white\" control\n");
+		ret = ddcci_monitor_readctrl(drv_data->device, DDCCI_MONITOR_BL_WHITE,
+						&brightness, &max_brightness);
+		if (ret < 0) {
+			if (ret == -ENOTSUPP)
+				dev_info(&dev->dev,
+					"monitor does not support reading backlight level\n");
+			else
+				goto err_free;
+		} else {
+			drv_data->used_vcp = DDCCI_MONITOR_BL_WHITE;
+		}
+	}
+
+	if (support_luminance && !drv_data->used_vcp) {
+		/* Try getting luminance */
+		dev_dbg(&dev->dev,
+			"trying to access \"luminance\" control\n");
+		ret = ddcci_monitor_readctrl(drv_data->device, DDCCI_MONITOR_LUMINANCE,
+					     &brightness, &max_brightness);
+		if (ret < 0) {
+			if (ret == -ENOTSUPP)
+				dev_info(&dev->dev,
+					"monitor does not support reading luminance\n");
+			else
+				goto err_free;
+		} else {
+			drv_data->used_vcp = DDCCI_MONITOR_LUMINANCE;
+		}
+		drv_data->used_vcp = DDCCI_MONITOR_LUMINANCE;
+	}
+
+	if (!drv_data->used_vcp)
+		goto err_free;
+
+	/* Create brightness device */
+	memset(&props, 0, sizeof(props));
+	props.type = BACKLIGHT_RAW;
+	props.max_brightness = max_brightness;
+	props.brightness = brightness;
+	bl = devm_backlight_device_register(&dev->dev, dev_name(&dev->dev),
+					    &dev->dev, drv_data,
+					    &ddcci_backlight_ops, &props);
+	drv_data->bl_dev = bl;
+	if (IS_ERR(bl)) {
+		dev_err(&dev->dev, "failed to register backlight\n");
+		return PTR_ERR(bl);
+	}
+	dev_info(&dev->dev, "registered %s as backlight device %s\n",
+		 ddcci_monitor_vcp_name(drv_data->used_vcp),
+		 dev_name(&dev->dev));
+
+	if (convenience_symlink) {
+		ddcci_backlight_create_symlink(dev);
+	}
+
+	goto end;
+err_free:
+	devm_kfree(&dev->dev, drv_data);
+end:
+	return ret;
+}
+
+static int ddcci_monitor_remove(struct ddcci_device *dev)
+{
+	dev_dbg(&dev->dev, "removing device\n");
+	ddcci_backlight_remove_symlink(dev);
+	return 0;
+}
+
+static struct ddcci_device_id ddcci_monitor_idtable[] = {
+	{ "monitor", DDCCI_ANY_ID, DDCCI_ANY_ID, DDCCI_ANY_ID, DDCCI_ANY_ID, 0 },
+	{}
+};
+
+static struct ddcci_driver ddcci_backlight_driver = {
+	.driver = {
+		.name	= "ddcci-backlight",
+		.owner	= THIS_MODULE,
+	},
+
+	.id_table	= ddcci_monitor_idtable,
+	.probe		= ddcci_monitor_probe,
+	.remove		= ddcci_monitor_remove,
+};
+
+module_ddcci_driver(ddcci_backlight_driver);
+
+/* Module parameter description */
+module_param(convenience_symlink, bool, S_IRUGO|S_IWUSR);
+MODULE_PARM_DESC(convenience_symlink, "add convenience symlink \"ddcci_backlight\" to ancestor device in sysfs (default true)");
+
+MODULE_AUTHOR("Christoph Grenz");
+MODULE_DESCRIPTION("DDC/CI generic monitor backlight driver");
+MODULE_VERSION("0.4.2");
+MODULE_LICENSE("GPL");
+
+MODULE_ALIAS("ddcci:monitor-*-*-*-*");
diff --git a/drivers/ddcci/ddcci.c b/drivers/ddcci/ddcci.c
new file mode 100644
index 000000000000..e26d15cae948
--- /dev/null
+++ b/drivers/ddcci/ddcci.c
@@ -0,0 +1,1895 @@
+/*
+ *  DDC/CI sub-bus driver
+ *
+ *  Copyright (c) 2015 Christoph Grenz
+ */
+
+/*
+ * 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.
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+#include <asm-generic/fcntl.h>
+#include <linux/cdev.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/fs.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/rwsem.h>
+#include <linux/sem.h>
+#include <linux/slab.h>
+#include <linux/uaccess.h>
+#include <linux/version.h>
+
+#include <linux/ddcci.h>
+
+#define DDCCI_RECV_BUFFER_SIZE 130
+#define DEVICE_NAME "ddcci"
+#define DDCCI_MAX_CAP_CHUNKS 200
+
+static unsigned int delay = 60;
+static unsigned short autoprobe_addrs[127] = {0xF0, 0xF2, 0xF4, 0xF6, 0xF8};
+static int autoprobe_addr_count = 5;
+
+static dev_t ddcci_cdev_first;
+static dev_t ddcci_cdev_next;
+static dev_t ddcci_cdev_end;
+static DEFINE_SEMAPHORE(core_lock);
+
+struct bus_type ddcci_bus_type;
+EXPORT_SYMBOL_GPL(ddcci_bus_type);
+
+/* Assert neccessary string array sizes  */
+#ifndef sizeof_field
+# define sizeof_field(t,m) FIELD_SIZEOF(t,m)
+#endif
+static_assert(sizeof_field(struct ddcci_device, prot) > 8);
+static_assert(sizeof_field(struct ddcci_device, type) > 8);
+static_assert(sizeof_field(struct ddcci_device, model) > 8);
+static_assert(sizeof_field(struct ddcci_device, vendor) > 8);
+static_assert(sizeof_field(struct ddcci_device, module) > 8);
+
+/* Internal per-i2c-client driver data */
+struct ddcci_bus_drv_data {
+	unsigned long quirks;
+	struct i2c_client *i2c_dev;
+	struct semaphore sem;
+	unsigned char recv_buffer[DDCCI_RECV_BUFFER_SIZE];
+};
+
+/* Replace non-alphanumeric characters in a string (used for modalias) */
+static void ddcci_modalias_clean(char *string, size_t n, char replacement)
+{
+	int i;
+	for (i = 0; i < n; ++i) {
+		char c = string[i];
+		if (c == 0) {
+			return;
+		} else if (c < '0' || (c > '9' && c < 'A') || (c > 'Z' && c < 'a') || c > 'z') {
+			string[i] = replacement;
+		}
+	}
+}
+
+/* Write a message to the DDC/CI bus using i2c_smbus_write_byte() */
+static int __ddcci_write_bytewise(struct i2c_client *client, unsigned char addr,
+				  bool p_flag, const unsigned char * __restrict buf,
+				  unsigned char len)
+{
+	int ret = 0;
+	unsigned char outer_addr = (unsigned char)(client->addr << 1);
+	unsigned xor = outer_addr; /* initial xor value */
+
+	/* Consistency checks */
+	if (len > 127)
+		return -EINVAL;
+
+	/* Special case: sender to 0x6E is always 0x51 */
+	if (addr == DDCCI_DEFAULT_DEVICE_ADDR) {
+		addr = DDCCI_HOST_ADDR_ODD;
+	} else {
+		/* When sending the odd address is used */
+		addr = addr | 1;
+	}
+
+	/* first byte: sender address */
+	xor ^= addr;
+	ret = i2c_smbus_write_byte(client, addr);
+	if (ret < 0)
+		return ret;
+
+	/* second byte: protocol flag and message size */
+	xor ^= ((p_flag << 7) | len);
+	ret = i2c_smbus_write_byte(client, (p_flag << 7)|len);
+	if (ret < 0)
+		return ret;
+
+	/* send payload */
+	while (len--) {
+		xor ^= (*buf);
+		ret = i2c_smbus_write_byte(client, (*buf));
+		if (ret < 0)
+			return ret;
+		buf++;
+	}
+
+	/* send checksum */
+	ret = i2c_smbus_write_byte(client, xor);
+	return ret;
+}
+
+/* Write a message to the DDC/CI bus using i2c_master_send() */
+static int __ddcci_write_block(struct i2c_client *client, unsigned char addr,
+			       unsigned char *sendbuf, bool p_flag,
+			       const unsigned char *data, unsigned char len)
+{
+	unsigned char outer_addr = (unsigned char)(client->addr << 1);
+	unsigned xor = outer_addr;	/* initial xor value */
+	unsigned char *ptr = sendbuf;
+
+	/* Consistency checks */
+	if (len > 127)
+		return -EINVAL;
+
+	/* Special case: sender to 0x6E is always 0x51 */
+	if (addr == DDCCI_DEFAULT_DEVICE_ADDR) {
+		addr = DDCCI_HOST_ADDR_ODD;
+	} else {
+		/* When sending the odd address is used */
+		addr = addr | 1;
+	}
+
+	/* first byte: sender address */
+	xor ^= addr;
+	*(ptr++) = addr;
+	/* second byte: protocol flag and message size */
+	xor ^= ((p_flag << 7) | len);
+	*(ptr++) = (p_flag << 7)|len;
+	/* payload */
+	while (len--) {
+		xor ^= (*data);
+		*(ptr++) = (*data);
+		data++;
+	}
+	/* checksum */
+	(*ptr) = xor;
+
+	/* Send it */
+	return i2c_master_send(client, sendbuf, ptr - sendbuf + 1);
+}
+
+/*
+ * Write a message to the DDC/CI bus.
+ *
+ * You must hold the bus semaphore when calling this function.
+ */
+static int ddcci_write(struct i2c_client *client, unsigned char addr,
+		       bool p_flag, const unsigned char *data,
+		       unsigned char len)
+{
+	struct ddcci_bus_drv_data *drv_data;
+	unsigned char *sendbuf;
+	int ret;
+
+	drv_data = i2c_get_clientdata(client);
+
+
+	pr_debug("sending to %d:%02x:%02x: %*ph\n", client->adapter->nr,
+		 client->addr << 1, addr, len, data);
+	if (drv_data->quirks & DDCCI_QUIRK_WRITE_BYTEWISE) {
+		ret = __ddcci_write_bytewise(client, addr, p_flag, data, len);
+	} else {
+		sendbuf = drv_data->recv_buffer;
+		ret = __ddcci_write_block(client, addr, sendbuf, p_flag, data, len);
+	}
+
+	return ret;
+}
+
+/*
+ * Read a response from the DDC/CI bus with headers directly into a buffer.
+ * Always check for DDCCI_QUIRK_SKIP_FIRST_BYTE when using this function.
+ * The returned length contains the whole unmodified response.
+ * If -EMSGSIZE is returned, the buffer contains the response up to `len`.
+ * If any other negative error code is returned, the buffer content is
+ * unspecified.
+ */
+static int __ddcci_read(struct i2c_client *client, unsigned char addr,
+			bool p_flag, unsigned long quirks, unsigned char *buf,
+			unsigned char len)
+{
+	int i, payload_len, packet_length, ret;
+	unsigned char xor = DDCCI_HOST_ADDR_EVEN;
+
+	/* Consistency checks */
+	if (len < 3)
+		return -EINVAL;
+
+	/* Read frame */
+	ret = i2c_master_recv(client, buf, len);
+	if (ret < 0)
+		goto out_err;
+	packet_length = ret;
+
+	/* Skip first byte if quirk active */
+	if ((quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE) && ret > 0 && len > 0) {
+		ret--;
+		len--;
+		buf++;
+	}
+
+	/* If answer too short (= incomplete) break out */
+	if (ret < 3) {
+		ret = -EIO;
+		goto out_err;
+	}
+
+	/* validate first byte */
+	if (unlikely(buf[0] != addr)) {
+		ret = (buf[0] == '\0') ? -EAGAIN : -EIO;
+		goto out_err;
+	}
+
+	/* validate second byte (protocol flag) */
+	if (unlikely((buf[1] & 0x80) != (p_flag << 7))) {
+		if (!p_flag || !(quirks & DDCCI_QUIRK_NO_PFLAG)) {
+			ret = -EIO;
+			goto out_err;
+		}
+	}
+
+	/* get and check payload length */
+	payload_len = buf[1] & 0x7F;
+	if (3+payload_len > packet_length)
+		return -EBADMSG;
+	if (3+payload_len > len)
+		return -EMSGSIZE;
+
+	/* calculate checksum */
+	for (i = 0; i < 3+payload_len; i++)
+		xor ^= buf[i];
+
+	/* verify checksum */
+	if (xor != 0) {
+		dev_err(&client->dev, "invalid DDC/CI response, corrupted data - xor is 0x%02x, length 0x%02x\n",
+			xor, payload_len);
+		ret = -EBADMSG;
+		goto out_err;
+	}
+
+	/* return result */
+	ret = payload_len+3+((quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE)?1:0);
+
+out_err:
+	return ret;
+}
+
+/*
+ * Read a response from the DDC/CI bus
+ *
+ * You must hold the bus semaphore when calling this function.
+ */
+static int ddcci_read(struct i2c_client *client, unsigned char addr,
+		      bool p_flag, unsigned char *buf, unsigned char len)
+{
+	struct ddcci_bus_drv_data *drv_data;
+	unsigned char *recvbuf;
+	int ret;
+
+	drv_data = i2c_get_clientdata(client);
+	recvbuf = drv_data->recv_buffer;
+
+	/* Read frame */
+	ret = __ddcci_read(client, addr, p_flag,
+		drv_data->quirks, recvbuf, DDCCI_RECV_BUFFER_SIZE);
+	if (ret < 0)
+		return ret;
+
+	if (drv_data->quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE)
+		recvbuf++;
+
+	/* return result */
+	if (buf) {
+		if (ret > 3) {
+			ret = ret-3;
+			/* copy to caller buffer */
+			memcpy(buf, &recvbuf[2], (ret < len) ? ret : len);
+
+			if (ret > len) {
+				/* if message was truncated, return -EMSGSIZE */
+				pr_debug("received from %d:%02x:%02x: [%u/%u] %*ph ...\n",
+					 client->adapter->nr, client->addr << 1,
+					 addr, ret, len, len, buf);
+				ret = -EMSGSIZE;
+			} else {
+				pr_debug("received from %d:%02x:%02x: [%u/%u] %*ph\n",
+					 client->adapter->nr, client->addr << 1,
+					 addr, ret, len, ret, buf);
+			}
+		}
+	}
+	if (!(drv_data->quirks & DDCCI_QUIRK_WRITE_BYTEWISE)) {
+		/* second read to clear buffers, needed on some devices */
+		__ddcci_read(client, addr, true, drv_data->quirks, recvbuf, 1);
+	}
+	return ret;
+}
+
+/* Request the capability string for a device and put it into buf */
+static int ddcci_get_caps(struct i2c_client *client, unsigned char addr,
+			  unsigned char *buf, unsigned int len)
+{
+	int result = 0, counter = 0, offset = 0;
+	unsigned char cmd[3] = { DDCCI_COMMAND_CAPS, 0x00, 0x00 };
+	unsigned char *chunkbuf = kzalloc(35, GFP_KERNEL);
+
+	if (!chunkbuf)
+		return -ENOMEM;
+
+	do {
+		/* Send command */
+		result = ddcci_write(client, addr, true, cmd, sizeof(cmd));
+		if (result < 0)
+			goto err_free;
+		msleep(delay);
+		/* read result chunk */
+		result = ddcci_read(client, addr, true, chunkbuf,
+				    (len > 32) ? 35 : len+3);
+		if (result < 0)
+			goto err_free;
+
+		if (result > 0) {
+			/* check chunk header */
+			if (chunkbuf[0] != DDCCI_REPLY_CAPS) {
+				result = -EIO;
+				goto err_free;
+			}
+			if (chunkbuf[1] != cmd[1] || chunkbuf[2] != cmd[2]) {
+				result = -EIO;
+				goto err_free;
+			}
+			if (result < 3) {
+				result = -EIO;
+				goto err_free;
+			}
+			memcpy(buf, chunkbuf+3, min((unsigned int)result-3, len));
+
+			counter++;
+			/* adjust offset, etc. */
+			offset += result-3;
+			len -= result-3;
+			buf += result-3;
+			cmd[1] = offset >> 8;
+			cmd[2] = offset & 0xFF;
+			/* Another superfluous read to make some devices happy... */
+			ddcci_read(client, addr, true, NULL, 2);
+		}
+	} while (result > 3 && counter < DDCCI_MAX_CAP_CHUNKS);
+
+	kfree(chunkbuf);
+	return offset+result-3;
+err_free:
+	kfree(chunkbuf);
+	return result;
+}
+
+/*
+ * Request the device identification and put it into buf.
+ *
+ * Also detects all communication quirks and sets the corresponding flags
+ * in the ddcci_bus_drv_data structure associated with client.
+ *
+ * The identification command will fail on most DDC devices, as it is optional
+ * to support, but even the "failed" response suffices to detect quirks.
+ */
+static int ddcci_identify_device(struct i2c_client *client, unsigned char addr,
+				 unsigned char *buf, unsigned char len)
+{
+	int i, payload_len, ret = -ENODEV;
+	unsigned long quirks;
+	unsigned char cmd[1] = { DDCCI_COMMAND_ID };
+	unsigned char *buffer;
+	unsigned char xor = DDCCI_HOST_ADDR_EVEN;
+	struct ddcci_bus_drv_data *bus_drv_data;
+
+	bus_drv_data = i2c_get_clientdata(client);
+	quirks = bus_drv_data->quirks;
+	buffer = bus_drv_data->recv_buffer;
+
+	/* Send Identification command */
+	if (!(quirks & DDCCI_QUIRK_WRITE_BYTEWISE)) {
+		ret = __ddcci_write_block(client, addr, buffer, true, cmd, sizeof(cmd));
+		dev_dbg(&client->dev,
+			"[%02x:%02x] writing identification command in block mode: %d\n",
+			client->addr << 1, addr, ret);
+		if ((ret == -ENXIO)
+		    && i2c_check_functionality(client->adapter,
+					       I2C_FUNC_SMBUS_WRITE_BYTE)) {
+			quirks |= DDCCI_QUIRK_WRITE_BYTEWISE;
+			dev_info(&client->dev,
+				"DDC/CI bus quirk detected: writes must be done bytewise\n");
+			/* Some devices need writing twice after a failed blockwise write */
+			__ddcci_write_bytewise(client, addr, true, cmd, sizeof(cmd));
+			msleep(delay);
+		}
+	}
+	if (ret < 0 && (quirks & DDCCI_QUIRK_WRITE_BYTEWISE)) {
+		ret = __ddcci_write_bytewise(client, addr, true, cmd, sizeof(cmd));
+		dev_dbg(&client->dev,
+			"[%02x:%02x] writing identification command in bytewise mode: %d\n",
+			client->addr << 1, addr, ret);
+	}
+	if (ret < 0)
+		return -ENODEV;
+
+	/* Wait */
+	msleep(delay);
+
+	/* Receive response */
+	ret = i2c_master_recv(client, buffer, DDCCI_RECV_BUFFER_SIZE);
+	if (ret < 0) {
+		dev_dbg(&client->dev,
+			"[%02x:%02x] receiving identification response resulted in errno %d\n",
+			client->addr << 1, addr, ret);
+		return ret;
+	}
+
+	if (ret == 0) {
+		dev_dbg(&client->dev,
+			"[%02x:%02x] no identification response received\n",
+			client->addr << 1, addr);
+		return ret;
+	}
+
+	/* Skip first byte if quirk already active */
+	if (quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE && ret > 1) {
+		dev_dbg(&client->dev,
+			"[%02x:%02x] doubled first byte quirk in effect\n",
+			client->addr << 1, addr);
+		ret--;
+		buffer++;
+	}
+
+	/* If answer too short (= incomplete) break out */
+	if (ret < 3) {
+		dev_dbg(&client->dev,
+			"[%02x:%02x] identification response is too short (%d bytes)\n",
+			client->addr << 1, addr, ret);
+		return -EIO;
+	}
+
+	/* validate first byte */
+	if (buffer[0] != addr) {
+		dev_dbg(&client->dev,
+			"[%02x:%02x] identification response: %*ph\n",
+			client->addr << 1, addr, (ret > 32 ? 32 : ret), buffer);
+
+		dev_dbg(&client->dev,
+			"[%02x:%02x] identification response invalid (expected first byte %02x, got %02x)\n",
+			client->addr << 1, addr, addr, buffer[0]);
+		return -ENODEV;
+	}
+
+	/* Check if first byte is doubled (QUIRK_SKIP_FIRST_BYTE) */
+	if (!(quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE)) {
+		if (buffer[0] == buffer[1]) {
+			quirks |= DDCCI_QUIRK_SKIP_FIRST_BYTE;
+			dev_info(&client->dev,
+				"DDC/CI bus quirk detected: doubled first byte on read\n");
+			ret--;
+			buffer++;
+			if (ret < 3)
+				return -EIO;
+		}
+	}
+
+	/* validate second byte (protocol flag) */
+	if ((buffer[1] & 0x80) != 0x80 && !(quirks & DDCCI_QUIRK_NO_PFLAG)) {
+		dev_info(&client->dev,
+			"DDC/CI bus quirk detected: device omits protocol flag on responses\n");
+		quirks |= DDCCI_QUIRK_NO_PFLAG;
+	}
+
+	/* get and check payload length */
+	payload_len = buffer[1] & 0x7F;
+	if (3+payload_len > ret) {
+		dev_dbg(&client->dev,
+			"[%02x:%02x] identification response: %*ph ...\n",
+			client->addr << 1, addr, ret, buffer);
+		dev_dbg(&client->dev,
+			"[%02x:%02x] identification response was truncated (expected %d bytes, got %d)\n",
+			client->addr << 1, addr, 3+payload_len, ret);
+		return -EBADMSG;
+	}
+
+	dev_dbg(&client->dev,
+		"[%02x:%02x] identification response: %*ph\n",
+		client->addr << 1, addr, 3+payload_len, buffer);
+
+	/* calculate checksum */
+	for (i = 0; i < 3+payload_len; i++)
+		xor ^= buffer[i];
+
+	/* verify checksum */
+	if (xor != 0) {
+		dev_err(&client->dev,
+			"[%02x:%02x] invalid DDC/CI response, corrupted data - xor is 0x%02x, length 0x%02x\n",
+			client->addr << 1, addr, xor, payload_len);
+		return -EBADMSG;
+	}
+
+	/* save quirks */
+	bus_drv_data->quirks = quirks;
+
+	/* return result */
+	if (payload_len <= len) {
+		ret = payload_len;
+		memcpy(buf, &buffer[2], payload_len);
+	} else {
+		ret = -EMSGSIZE;
+		memcpy(buf, &buffer[2], len);
+	}
+	return ret;
+}
+
+/* Character device */
+
+/* Data structure for an open file handle */
+struct ddcci_fp_data {
+	struct ddcci_device *dev;
+	bool exclusive;
+	unsigned char buffer[129];
+};
+
+/* Called when the character device is opened */
+static int ddcci_cdev_open(struct inode *inode, struct file *filp)
+{
+	struct ddcci_device *dev = container_of(inode->i_cdev,
+						struct ddcci_device, cdev);
+	struct ddcci_fp_data *fp_data = NULL;
+
+	fp_data = kzalloc(sizeof(struct ddcci_fp_data), GFP_KERNEL);
+
+	if (!fp_data)
+		return -ENOMEM;
+
+	fp_data->exclusive = filp->f_flags & O_EXCL;
+
+	if (fp_data->exclusive) {
+		if (down_write_trylock(&dev->cdev_sem) == 0) {
+			kfree(fp_data);
+			return -EBUSY;
+		}
+	} else {
+		if (down_read_trylock(&dev->cdev_sem) == 0) {
+			kfree(fp_data);
+			return -EBUSY;
+		}
+	}
+
+	fp_data->dev = dev;
+	filp->private_data = fp_data;
+
+	return 0;
+}
+
+/* Called when the character device is closed */
+static int ddcci_cdev_close(struct inode *inode, struct file *filp)
+{
+	struct ddcci_fp_data *fp_data = filp->private_data;
+	struct ddcci_device *dev = fp_data->dev;
+
+	if (fp_data->exclusive)
+		up_write(&dev->cdev_sem);
+	else
+		up_read(&dev->cdev_sem);
+
+	filp->private_data = NULL;
+	kfree(fp_data);
+	return 0;
+}
+
+/* Called when reading from the character device */
+static ssize_t ddcci_cdev_read(struct file *filp, char __user *buffer,
+			       size_t length, loff_t *offset)
+{
+	struct ddcci_fp_data *fp_data = filp->private_data;
+	struct ddcci_device *dev = fp_data->dev;
+	unsigned char *buf = fp_data->buffer;
+	const bool nonblocking = (filp->f_flags & O_NONBLOCK) != 0;
+	int ret = 0;
+
+	if ((filp->f_mode & FMODE_READ) == 0)
+		return -EBADF;
+
+	/* Lock mutex */
+	if (nonblocking) {
+		if (down_trylock(&dev->bus_drv_data->sem))
+			return -EAGAIN;
+	} else {
+		if (down_interruptible(&dev->bus_drv_data->sem))
+			return -ERESTARTSYS;
+	}
+
+	/* Execute read */
+	ret = ddcci_read(dev->bus_drv_data->i2c_dev, dev->inner_addr, true, buf,
+			 length);
+
+	if (ret > 0) {
+		/* Copy data from user space */
+		if (copy_to_user(buffer, buf, ret)) {
+			ret = -EFAULT;
+			goto out;
+		}
+	}
+
+out:
+	up(&dev->bus_drv_data->sem);
+	return ret;
+}
+
+/* Called when writing to the character device */
+static ssize_t ddcci_cdev_write(struct file *filp, const char __user *buffer,
+				size_t count, loff_t *offset)
+{
+	struct ddcci_fp_data *fp_data = filp->private_data;
+	struct ddcci_device *dev = fp_data->dev;
+	unsigned char *buf = fp_data->buffer;
+	const bool nonblocking = (filp->f_flags & O_NONBLOCK) != 0;
+	int ret = 0;
+
+	if ((filp->f_mode & FMODE_WRITE) == 0)
+		return -EBADF;
+
+	if (count > 127)
+		return -EINVAL;
+
+	/* Lock mutex */
+	if (nonblocking) {
+		if (down_trylock(&dev->bus_drv_data->sem))
+			return -EAGAIN;
+	} else {
+		if (down_interruptible(&dev->bus_drv_data->sem))
+			return -ERESTARTSYS;
+	}
+
+	if (count > 0) {
+		/* Copy data from user space */
+		if (copy_from_user(buf, buffer, count)) {
+			ret = -EFAULT;
+			goto err_out;
+		}
+
+		/* Execute write */
+		ret = ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr,
+				  true, buf, count);
+	}
+
+	if (ret >= 0) {
+		msleep(delay);
+		up(&dev->bus_drv_data->sem);
+		return count;
+	}
+
+err_out:
+	up(&dev->bus_drv_data->sem);
+	return ret;
+}
+
+/* Called when seeking the character device */
+static loff_t ddcci_cdev_seek(struct file *filp, loff_t offset, int anchor)
+{
+	return -EINVAL;
+}
+
+static const struct file_operations ddcci_fops = {
+	.owner = THIS_MODULE,
+	.read = ddcci_cdev_read,
+	.write = ddcci_cdev_write,
+	.open = ddcci_cdev_open,
+	.release = ddcci_cdev_close,
+	.llseek = ddcci_cdev_seek
+};
+
+/* Set up the character device for a DDC/CI device */
+static int ddcci_setup_char_device(struct ddcci_device *device)
+{
+	int ret = -EINVAL;
+
+	/* Check if free minor exists */
+	if (ddcci_cdev_next == ddcci_cdev_end) {
+		dev_err(&device->dev, "no free major/minor\n");
+		ret = -ENFILE;
+		goto out;
+	}
+
+	/* Initialize rwsem */
+	init_rwsem(&device->cdev_sem);
+
+	/* Initialize character device node */
+	cdev_init(&device->cdev, &ddcci_fops);
+	device->cdev.owner = THIS_MODULE;
+
+	/* Publish char device */
+	device->dev.devt = ddcci_cdev_next;
+	ret = cdev_add(&device->cdev, ddcci_cdev_next, 1);
+	if (ret) {
+		device->dev.devt = 0;
+		goto out;
+	}
+
+	ddcci_cdev_next++;
+out:
+	return ret;
+}
+
+/* sysfs attributes */
+
+static ssize_t ddcci_attr_capabilities_show(struct device *dev,
+					    struct device_attribute *attr,
+					    char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+	size_t len;
+
+	if (likely(device != NULL)) {
+		len = device->capabilities_len;
+		if (unlikely(len > PAGE_SIZE))
+			len = PAGE_SIZE;
+		if (len == 0) {
+			ret = len;
+		} else {
+			memcpy(buf, device->capabilities, len);
+			if (likely(len < PAGE_SIZE)) {
+				buf[len] = '\n';
+				ret = len+1;
+			}
+		}
+	}
+
+	return ret;
+}
+
+static ssize_t ddcci_attr_prot_show(struct device *dev,
+				    struct device_attribute *attr, char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+	size_t len;
+
+	if (likely(device != NULL)) {
+		len = strnlen(device->prot, sizeof(device->prot));
+		strncpy(buf, device->prot, PAGE_SIZE);
+		if (len == 0) {
+			ret = len;
+		} else if (likely(len < PAGE_SIZE)) {
+			buf[len] = '\n';
+			ret = len+1;
+		} else {
+			ret = PAGE_SIZE;
+		}
+	}
+	return ret;
+}
+
+static ssize_t ddcci_attr_type_show(struct device *dev,
+				    struct device_attribute *attr, char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+	size_t len;
+
+	if (likely(device != NULL)) {
+		len = strnlen(device->type, sizeof(device->type));
+		strncpy(buf, device->type, PAGE_SIZE);
+		if (len == 0) {
+			ret = len;
+		} else if (likely(len < PAGE_SIZE)) {
+			buf[len] = '\n';
+			ret = len+1;
+		} else {
+			ret = PAGE_SIZE;
+		}
+	}
+	return ret;
+}
+
+static ssize_t ddcci_attr_model_show(struct device *dev,
+				     struct device_attribute *attr, char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+	size_t len;
+
+	if (likely(device != NULL)) {
+		len = strnlen(device->model, sizeof(device->model));
+		strncpy(buf, device->model, PAGE_SIZE);
+		if (len == 0) {
+			ret = len;
+		} else if (likely(len < PAGE_SIZE)) {
+			buf[len] = '\n';
+			ret = len+1;
+		} else {
+			ret = PAGE_SIZE;
+		}
+	}
+	return ret;
+}
+
+static ssize_t ddcci_attr_vendor_show(struct device *dev,
+				      struct device_attribute *attr, char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+	size_t len;
+
+	if (likely(device != NULL)) {
+		len = strnlen(device->vendor, sizeof(device->vendor));
+		strncpy(buf, device->vendor, PAGE_SIZE);
+		if (len == 0) {
+			ret = len;
+		} else if (likely(len < PAGE_SIZE)) {
+			buf[len] = '\n';
+			ret = len+1;
+		} else {
+			ret = PAGE_SIZE;
+		}
+	}
+	return ret;
+}
+
+static ssize_t ddcci_attr_module_show(struct device *dev,
+				      struct device_attribute *attr, char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+	size_t len;
+
+	if (likely(device != NULL)) {
+		len = strnlen(device->module, sizeof(device->module));
+		strncpy(buf, device->module, PAGE_SIZE);
+		if (len == 0) {
+			ret = len;
+		} else if (likely(len < PAGE_SIZE)) {
+			buf[len] = '\n';
+			ret = len+1;
+		} else {
+			ret = PAGE_SIZE;
+		}
+	}
+	return ret;
+}
+
+static ssize_t ddcci_attr_serial_show(struct device *dev,
+				      struct device_attribute *attr, char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+
+	if (likely(device != NULL))
+		ret = scnprintf(buf, PAGE_SIZE, "%d\n", device->device_number);
+
+	return ret;
+}
+
+static ssize_t ddcci_attr_modalias_show(struct device *dev,
+				      struct device_attribute *attr, char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+	char model[ARRAY_SIZE(device->model)];
+	char vendor[ARRAY_SIZE(device->model)];
+	char module[ARRAY_SIZE(device->model)];
+
+	if (likely(device != NULL)) {
+		memcpy(model, device->model, sizeof(model));
+		memcpy(vendor, device->vendor, sizeof(vendor));
+		memcpy(module, device->module, sizeof(module));
+		ddcci_modalias_clean(model, sizeof(model), '_');
+		ddcci_modalias_clean(vendor, sizeof(vendor), '_');
+		ddcci_modalias_clean(module, sizeof(module), '_');
+
+		ret = scnprintf(buf, PAGE_SIZE, "%s%s-%s-%s-%s-%s\n",
+			DDCCI_MODULE_PREFIX,
+			device->prot,
+			device->type,
+			model,
+			vendor,
+			module
+		);
+	}
+	return ret;
+}
+
+static DEVICE_ATTR(capabilities, S_IRUGO, ddcci_attr_capabilities_show, NULL);
+static DEVICE_ATTR(idProt, S_IRUGO, ddcci_attr_prot_show, NULL);
+static DEVICE_ATTR(idType, S_IRUGO, ddcci_attr_type_show, NULL);
+static DEVICE_ATTR(idModel, S_IRUGO, ddcci_attr_model_show, NULL);
+static DEVICE_ATTR(idVendor, S_IRUGO, ddcci_attr_vendor_show, NULL);
+static DEVICE_ATTR(idModule, S_IRUGO, ddcci_attr_module_show, NULL);
+static DEVICE_ATTR(idSerial, S_IRUGO, ddcci_attr_serial_show, NULL);
+static DEVICE_ATTR(modalias, S_IRUGO, ddcci_attr_modalias_show, NULL);
+
+static struct attribute *ddcci_char_device_attrs[] = {
+	&dev_attr_capabilities.attr,
+	&dev_attr_idProt.attr,
+	&dev_attr_idType.attr,
+	&dev_attr_idModel.attr,
+	&dev_attr_idVendor.attr,
+	&dev_attr_idModule.attr,
+	&dev_attr_idSerial.attr,
+	&dev_attr_modalias.attr,
+	NULL,
+};
+ATTRIBUTE_GROUPS(ddcci_char_device);
+
+/* DDC/CI bus */
+
+static int ddcci_device_uevent(struct device *dev, struct kobj_uevent_env *env)
+{
+	struct ddcci_device	*device = to_ddcci_device(dev);
+	char model[ARRAY_SIZE(device->model)];
+	char vendor[ARRAY_SIZE(device->vendor)];
+	char module[ARRAY_SIZE(device->module)];
+
+	memcpy(model, device->model, sizeof(model));
+	memcpy(vendor, device->vendor, sizeof(vendor));
+	memcpy(module, device->module, sizeof(module));
+	ddcci_modalias_clean(model, sizeof(model), '_');
+	ddcci_modalias_clean(vendor, sizeof(vendor), '_');
+	ddcci_modalias_clean(module, sizeof(module), '_');
+
+	if (add_uevent_var(env, "MODALIAS=%s%s-%s-%s-%s-%s",
+			   DDCCI_MODULE_PREFIX,
+			   device->prot,
+			   device->type,
+			   model,
+			   vendor,
+			   module
+		))
+		return -ENOMEM;
+
+	if (device->prot[0])
+		if (add_uevent_var(env, "DDCCI_PROT=%s", device->prot))
+			return -ENOMEM;
+
+	if (device->type[0])
+		if (add_uevent_var(env, "DDCCI_TYPE=%s", device->type))
+			return -ENOMEM;
+
+	if (device->model[0])
+		if (add_uevent_var(env, "DDCCI_MODEL=%s", device->model))
+			return -ENOMEM;
+
+	if (device->vendor[0]) {
+		if (add_uevent_var(env, "DDCCI_VENDOR=%s", device->vendor))
+			return -ENOMEM;
+
+		if (add_uevent_var(env, "DDCCI_MODULE=%s", device->module))
+			return -ENOMEM;
+
+		if (add_uevent_var(env, "DDCCI_UNIQ=%d", device->device_number))
+			return -ENOMEM;
+	}
+
+	return 0;
+}
+
+static void ddcci_device_release(struct device *dev)
+{
+	struct ddcci_device *device = to_ddcci_device(dev);
+	struct ddcci_driver *driver;
+
+	/* Notify driver */
+	if (dev->driver) {
+		driver = to_ddcci_driver(dev->driver);
+		if (driver->remove)
+			driver->remove(device);
+	}
+
+	/* Teardown chardev */
+	if (dev->devt) {
+		down(&core_lock);
+		if (device->cdev.dev == ddcci_cdev_next-1)
+			ddcci_cdev_next--;
+		cdev_del(&device->cdev);
+		up(&core_lock);
+	}
+
+	/* Free capability string */
+	if (device->capabilities) {
+		device->capabilities_len = 0;
+		kfree(device->capabilities);
+	}
+	/* Free device */
+	kfree(device);
+}
+
+static char *ddcci_devnode(struct device *dev,
+			 umode_t *mode, kuid_t *uid, kgid_t *gid)
+{
+	struct ddcci_device *device;
+
+	device = to_ddcci_device(dev);
+	return kasprintf(GFP_KERNEL, "bus/ddcci/%d/display",
+			 device->i2c_client->adapter->nr);
+}
+
+static char *ddcci_dependent_devnode(struct device *dev,
+			 umode_t *mode, kuid_t *uid, kgid_t *gid)
+{
+	struct ddcci_device *device;
+
+	device = to_ddcci_device(dev);
+	if (device->flags & DDCCI_FLAG_EXTERNAL) {
+		if (device->outer_addr == device->inner_addr)
+			return kasprintf(GFP_KERNEL, "bus/ddcci/%d/e%02x",
+					 device->i2c_client->adapter->nr,
+					 device->outer_addr);
+		else
+			return kasprintf(GFP_KERNEL, "bus/ddcci/%d/e%02x%02x",
+					 device->i2c_client->adapter->nr,
+					 device->outer_addr, device->inner_addr);
+	} else {
+		return kasprintf(GFP_KERNEL, "bus/ddcci/%d/i%02x",
+				 device->i2c_client->adapter->nr,
+				 device->inner_addr);
+	}
+}
+
+/* Device type for main DDC/CI devices*/
+static struct device_type ddcci_device_type = {
+	.name	= "ddcci-device",
+	.uevent		= ddcci_device_uevent,
+	.groups		= ddcci_char_device_groups,
+	.release	= ddcci_device_release,
+	.devnode	= ddcci_devnode
+};
+
+/* Device type for dependent DDC/CI devices*/
+static struct device_type ddcci_dependent_type = {
+	.name	= "ddcci-dependent-device",
+	.uevent		= ddcci_device_uevent,
+	.groups		= ddcci_char_device_groups,
+	.release	= ddcci_device_release,
+	.devnode	= ddcci_dependent_devnode
+};
+
+/**
+ * ddcci_verify_device - return parameter as ddcci_device, or NULL
+ * @dev: device, probably from some driver model iterator
+ */
+struct ddcci_device *ddcci_verify_device(struct device *dev)
+{
+	if (unlikely(!dev))
+		return NULL;
+	return (dev->type == &ddcci_device_type
+		|| dev->type == &ddcci_dependent_type)
+			? to_ddcci_device(dev)
+			: NULL;
+}
+EXPORT_SYMBOL(ddcci_verify_device);
+
+/**
+ * ddcci_quirks - Get quirks for DDC/CI device
+ * @dev: Target DDC/CI device
+ */
+unsigned long ddcci_quirks(struct ddcci_device *dev)
+{
+	if (unlikely(WARN_ON(!dev)))
+		return ~0L;
+	if (unlikely(WARN_ON(!dev->bus_drv_data)))
+		return ~0L;
+	return dev->bus_drv_data->quirks;
+}
+EXPORT_SYMBOL(ddcci_quirks);
+
+/**
+ * ddcci_register_driver - register DDC/CI driver
+ * @owner: the owning module
+ * @driver: the driver to register
+ */
+int ddcci_register_driver(struct module *owner, struct ddcci_driver *driver)
+{
+	int ret;
+
+	/* Can't register until after driver model init */
+	if (unlikely(WARN_ON(!ddcci_bus_type.p)))
+		return -EAGAIN;
+
+	pr_debug("registering driver [%s]\n", driver->driver.name);
+
+	/* add the driver to the list of ddcci drivers in the driver core */
+	driver->driver.owner = owner;
+	driver->driver.bus = &ddcci_bus_type;
+
+	/* When registration returns, the driver core
+	 * will have called probe() for all matching-but-unbound devices.
+	 */
+	ret = driver_register(&driver->driver);
+	if (ret)
+		return ret;
+
+	pr_debug("driver [%s] registered\n", driver->driver.name);
+
+	return 0;
+}
+EXPORT_SYMBOL(ddcci_register_driver);
+
+/**
+ * ddcci_del_driver - unregister DDC/CI driver
+ * @driver: the driver being unregistered
+ */
+void ddcci_del_driver(struct ddcci_driver *driver)
+{
+	driver_unregister(&driver->driver);
+	pr_debug("driver [%s] unregistered\n", driver->driver.name);
+}
+EXPORT_SYMBOL(ddcci_del_driver);
+
+/**
+ * ddcci_device_write - Write a message to a DDC/CI device
+ * @dev: Target DDC/CI device
+ * @p_flag: Protocol flag, true for standard control messages
+ * @data: Data that will be written to the device
+ * @length: How many bytes to write
+ *
+ * Writes the message to the device and sleeps (see module parameter 'delay')
+ */
+int ddcci_device_write(struct ddcci_device *dev, bool p_flag,
+		       unsigned char *data, unsigned char length)
+{
+	int ret;
+
+	if (down_interruptible(&dev->bus_drv_data->sem))
+		return -EAGAIN;
+
+	ret = ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, data, length);
+	msleep(delay);
+	up(&dev->bus_drv_data->sem);
+	return ret;
+}
+EXPORT_SYMBOL(ddcci_device_write);
+
+/**
+ * ddcci_device_read - Read a response from a DDC/CI device
+ * @dev: Target DDC/CI device
+ * @p_flag: Protocol flag, must match the corresponding write
+ * @buffer: Where to store data read from the device
+ * @length: Buffer size
+ */
+int ddcci_device_read(struct ddcci_device *dev, bool p_flag,
+		      unsigned char *buffer, unsigned char length)
+{
+	int ret;
+
+	if (down_interruptible(&dev->bus_drv_data->sem))
+		return -EAGAIN;
+
+	ret = ddcci_read(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, buffer, length);
+	up(&dev->bus_drv_data->sem);
+	return ret;
+}
+EXPORT_SYMBOL(ddcci_device_read);
+
+/**
+ * ddcci_device_writeread - Write a message to a device and read the response
+ * @dev: Target DDC/CI device
+ * @p_flag: Protocol flag, true for standard control messages
+ * @buffer: Buffer used for write and read
+ * @length: How many bytes to write
+ * @maxlength: Buffer size on read
+ *
+ * Writing, sleeping and reading are done without releasing the DDC/CI bus.
+ * This provides atomicity in respect to other DDC/CI accesses on the same bus.
+ */
+int ddcci_device_writeread(struct ddcci_device *dev, bool p_flag,
+			   unsigned char *buffer, unsigned char length,
+			   unsigned char maxlength)
+{
+	int ret;
+
+	if (down_interruptible(&dev->bus_drv_data->sem))
+		return -EAGAIN;
+
+	ret = ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, buffer, length);
+	if (ret < 0)
+		goto err;
+	msleep(delay);
+	ret = ddcci_read(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, buffer, maxlength);
+err:
+	up(&dev->bus_drv_data->sem);
+	return ret;
+}
+EXPORT_SYMBOL(ddcci_device_writeread);
+
+#define IS_ANY_ID(x) (((x)[0] == -1) && ((x)[7] == -1))
+
+/* Check if any device id in the array matches the device and return the matching id */
+static const struct ddcci_device_id *ddcci_match_id(const struct ddcci_device_id *id,
+						    const struct ddcci_device *device)
+{
+	while (id->prot[0] || id->type[0] || id->model[0] || id->vendor[0] || id->module[0]) {
+		if ((IS_ANY_ID(id->prot) || (strcmp(device->prot, id->prot) == 0))
+		 && (IS_ANY_ID(id->type) || (strcmp(device->type, id->type) == 0))
+		 && (IS_ANY_ID(id->model) || (strcmp(device->model, id->model) == 0))
+		 && (IS_ANY_ID(id->vendor) || (strcmp(device->vendor, id->vendor) == 0))
+		 && (IS_ANY_ID(id->module) || (strcmp(device->module, id->module) == 0))) {
+			return id;
+		}
+		id++;
+	}
+	return NULL;
+}
+
+static int ddcci_device_match(struct device *dev, struct device_driver *drv)
+{
+	struct ddcci_device	*device = ddcci_verify_device(dev);
+	struct ddcci_driver	*driver;
+
+	if (!device)
+		return 0;
+
+	driver = to_ddcci_driver(drv);
+	/* match on an id table if there is one */
+	if (driver->id_table)
+		return ddcci_match_id(driver->id_table, device) != NULL;
+
+	return 0;
+}
+
+static int ddcci_device_probe(struct device *dev)
+{
+	struct ddcci_device	*device = ddcci_verify_device(dev);
+	struct ddcci_driver	*driver;
+	const struct ddcci_device_id *id;
+	int ret = 0;
+
+	if (!device)
+		return -EINVAL;
+	driver = to_ddcci_driver(dev->driver);
+
+	id = ddcci_match_id(driver->id_table, device);
+	if (!id)
+		return -ENODEV;
+
+	if (driver->probe)
+		ret = driver->probe(device, id);
+
+	return ret;
+}
+
+static int ddcci_device_remove(struct device *dev)
+{
+	struct ddcci_device	*device = ddcci_verify_device(dev);
+	struct ddcci_driver	*driver;
+	int ret = 0;
+
+	if (!device)
+		return -EINVAL;
+	driver = to_ddcci_driver(dev->driver);
+
+	if (driver->remove)
+		ret = driver->remove(device);
+
+	return ret;
+}
+
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0)
+static void ddcci_device_remove_void(struct device *dev)
+{
+	ddcci_device_remove(dev);
+}
+#endif
+
+/**
+ * DDCCI bus type structure
+ */
+struct bus_type ddcci_bus_type = {
+	.name		= "ddcci",
+	.match		= ddcci_device_match,
+	.probe		= ddcci_device_probe,
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0)
+	.remove		= ddcci_device_remove_void
+#else
+	.remove		= ddcci_device_remove
+#endif
+};
+
+/* Main I2C driver */
+
+/* Get a pointer to the closing parenthesis */
+static char *ddcci_capstr_tok(const char *s, int depth)
+{
+	const char *ptr = s;
+	char *end;
+
+	if (s == NULL || s[0] == '\0')
+		return NULL;
+
+	while ((end = strpbrk(ptr, "()"))) {
+		if (!end || depth == INT_MAX)
+			return NULL;
+		if (*end == '(')
+			depth++;
+		else if (depth > 0)
+			depth--;
+		else
+			break;
+		ptr = end+1;
+	}
+	return end;
+}
+
+/**
+ * ddcci_find_capstr_item - Search capability string for a tag
+ * @capabilities: Capability string to search
+ * @tag: Tag to find
+ * @length: Buffer for the length of the found tag value (optional)
+ *
+ * Return a pointer to the start of the tag value (directly after the '(') on
+ * success and write the length of the value (excluding the ')') into `length`.
+ *
+ * If the tag is not found or another error occurs, an ERR_PTR is returned
+ * and `length` stays untouched.
+ */
+const char *ddcci_find_capstr_item(const char * capabilities,
+				   const char * __restrict tag,
+				   size_t *length)
+{
+	const char *src = capabilities, *ptr;
+	ptrdiff_t len;
+	int taglen = strnlen(tag, 1024);
+
+	/* Check length of requested tag */
+	if (unlikely(taglen <= 0 || taglen > 1023))
+		return ERR_PTR(-EINVAL);
+
+	/* Find tag */
+	while (src && (strncmp(src+1, tag, taglen) != 0 || src[1+taglen] != '('))
+		src = ddcci_capstr_tok(src+1, -1);
+	if (!src || src[0] == '\0')
+		return ERR_PTR(-ENOENT);
+
+	/* Locate end of value */
+	src += taglen+2;
+	ptr = ddcci_capstr_tok(src, 0);
+	if (unlikely(!ptr))
+		return ERR_PTR(-EOVERFLOW);
+
+	/* Check length of tag data */
+	len = ptr-src;
+	if (unlikely(len < 0 || len > 65535))
+		return ERR_PTR(-EMSGSIZE);
+
+	/* Return pointer and length */
+	if (likely(length != NULL))
+		*length = (size_t)len;
+	return src;
+}
+EXPORT_SYMBOL(ddcci_find_capstr_item);
+
+/* Search the capability string for a tag and copy the value to dest */
+static int ddcci_cpy_capstr_item(char *dest, const char *src,
+				  const char * __restrict tag, size_t maxlen)
+{
+	const char *ptr;
+	size_t len;
+
+	/* Find tag */
+	ptr = ddcci_find_capstr_item(src, tag, &len);
+	if (IS_ERR(ptr)) {
+		return PTR_ERR(ptr);
+	}
+
+	/* Copy value */
+	memcpy(dest, ptr, min(len, maxlen));
+	return 0;
+}
+
+/* Fill fields in device by parsing the capability string */
+static int ddcci_parse_capstring(struct ddcci_device *device)
+{
+	const char *capstr = device->capabilities;
+	int ret = 0;
+
+	if (!capstr)
+		return -EINVAL;
+
+	/* capability string start with a paren */
+	if (capstr[0] != '(')
+		return -EINVAL;
+
+	/* get prot(...) */
+	ret = ddcci_cpy_capstr_item(device->prot, capstr, "prot", sizeof(device->prot)-1);
+	if (ret) {
+		if (ret == -ENOENT) {
+			dev_warn(&device->dev, "malformed capability string: no protocol tag");
+			memset(device->prot, 0, sizeof(device->prot)-1);
+		} else {
+			return ret;
+		}
+	}
+
+	/* get type(...) */
+	ret = ddcci_cpy_capstr_item(device->type, capstr, "type", sizeof(device->type)-1);
+	if (ret) {
+		if (ret == -ENOENT) {
+			dev_warn(&device->dev, "malformed capability string: no type tag");
+			memset(device->type, 0, sizeof(device->type)-1);
+		} else {
+			return ret;
+		}
+	}
+
+	/* and then model(...) */
+	ret = ddcci_cpy_capstr_item(device->model, capstr, "model", sizeof(device->model)-1);
+	if (ret) {
+		if (ret == -ENOENT) {
+			dev_warn(&device->dev, "malformed capability string: no model tag");
+			memset(device->model, 0, sizeof(device->model)-1);
+		} else {
+			return ret;
+		}
+	}
+
+	/* if there is no protocol tag */
+	if (!device->prot[0]) {
+		/* and no type tag: give up. */
+		if (!device->type[0])
+			return -ENOENT;
+
+		/* Assume protocol "monitor" if type is "LCD" or "CRT" */
+		if (strncasecmp(device->type, "LCD", sizeof(device->type)-1) == 0
+		 || strncasecmp(device->type, "CRT", sizeof(device->type)-1) == 0) {
+			memcpy(device->prot, "monitor", 7);
+		}
+	}
+
+	/* skip the rest for now */
+
+	return 0;
+}
+
+/* Probe for a device on an inner address and create a ddcci_device for it */
+static int ddcci_detect_device(struct i2c_client *client, unsigned char addr,
+			       int dependent)
+{
+	int ret;
+	unsigned char outer_addr = client->addr << 1;
+	unsigned char *buffer = NULL;
+	struct ddcci_bus_drv_data *drv_data = i2c_get_clientdata(client);
+	struct ddcci_device *device = NULL;
+
+	down(&drv_data->sem);
+
+	/* Allocate buffer big enough for any capability string */
+	buffer = kmalloc(16384, GFP_KERNEL);
+	if (!buffer) {
+		ret = -ENOMEM;
+		goto err_end;
+	}
+
+	/* Allocate device struct */
+	device = kzalloc(sizeof(struct ddcci_device), GFP_KERNEL);
+	if (!device) {
+		ret = -ENOMEM;
+		goto err_end;
+	}
+
+	/* Initialize device */
+	device_initialize(&device->dev);
+	device->dev.parent = &client->dev;
+	device->dev.bus = &ddcci_bus_type;
+	device->outer_addr = outer_addr;
+	device->inner_addr = addr;
+	device->bus_drv_data = drv_data;
+	device->i2c_client = client;
+
+	if (!dependent) {
+		device->dev.type = &ddcci_device_type;
+		ret = dev_set_name(&device->dev, "ddcci%d", client->adapter->nr);
+	} else if (outer_addr == dependent) {
+		/* Internal dependent device */
+		device->dev.type = &ddcci_dependent_type;
+		device->flags = DDCCI_FLAG_DEPENDENT;
+		ret = dev_set_name(&device->dev, "ddcci%di%02x", client->adapter->nr, addr);
+	} else if (outer_addr == addr) {
+		/* External dependent device */
+		device->dev.type = &ddcci_dependent_type;
+		device->flags = DDCCI_FLAG_DEPENDENT | DDCCI_FLAG_EXTERNAL;
+		ret = dev_set_name(&device->dev, "ddcci%de%02x", client->adapter->nr, addr);
+	} else {
+		/* Dependent device of external dependent device
+		   Just in case something like this exists */
+		device->dev.type = &ddcci_dependent_type;
+		device->flags = DDCCI_FLAG_DEPENDENT | DDCCI_FLAG_EXTERNAL;
+		ret = dev_set_name(&device->dev, "ddcci%de%02x%02x", client->adapter->nr, outer_addr, addr);
+	}
+
+	if (ret)
+		goto err_free;
+
+	/* Read identification and check for quirks */
+	ret = ddcci_identify_device(client, addr, buffer, 29);
+	if (ret < 0) {
+		if (!dependent && (ret == -EBADMSG || ret == -EMSGSIZE)) {
+			dev_warn(&device->dev, "DDC/CI main device sent broken response on identification. Trying to detect solely based on capability information.\n");
+		} else {
+			goto err_free;
+		}
+	}
+
+	if (ret == 29 && buffer[0] == DDCCI_REPLY_ID) {
+		memcpy(device->vendor, &buffer[7], 8);
+		memcpy(device->module, &buffer[17], 8);
+		device->device_number = be32_to_cpu(*(__force __be32 *)&buffer[18]);
+	}
+
+	/* Read capabilities */
+	ret = ddcci_get_caps(client, addr, buffer, 16384);
+	if (ret > 0) {
+		/* Fixup unparenthesized capability strings, but only if the first
+		   character is an ascii lower case letter.
+		   This should still allow an early exit for completely garbled
+		   data but helps detecting devices where only the parentheses are
+		   missing, as the second char must be the first character of a
+		   keyword. */
+		if (ret > 2 && buffer[0] >= 'a' && buffer[0] <= 'z') {
+			dev_err(&device->dev, "DDC/CI device quirk detected: unparenthesized capability string\n");
+			device->capabilities = kzalloc(ret+3, GFP_KERNEL);
+			if (!device->capabilities) {
+				ret = -ENOMEM;
+				goto err_free;
+			}
+			device->capabilities_len = ret+2;
+			memcpy(&(device->capabilities[1]), buffer, ret);
+			device->capabilities[0] = '(';
+			device->capabilities[ret+1] = ')';
+		} else {
+			/* Standard case: simply copy the received string */
+			device->capabilities = kzalloc(ret+1, GFP_KERNEL);
+			if (!device->capabilities) {
+				ret = -ENOMEM;
+				goto err_free;
+			}
+			device->capabilities_len = ret;
+			memcpy(device->capabilities, buffer, ret);
+		}
+
+		ret = ddcci_parse_capstring(device);
+		if (ret) {
+			dev_err(&device->dev, "malformed capability string: \"%s\" errno %d\n", device->capabilities, ret);
+			ret = -EINVAL;
+			goto err_free;
+		}
+	}
+
+	/* Found a device if either identification or capabilities succeeded */
+	if (!device->capabilities && device->vendor[0] == '\0') {
+		dev_dbg(&client->dev,
+			"[%02x:%02x] got neither valid identification nor capability data\n",
+			client->addr << 1, addr);
+		ret = -ENODEV;
+		goto err_free;
+	}
+
+	/* Setup chardev */
+	down(&core_lock);
+	ret = ddcci_setup_char_device(device);
+	up(&core_lock);
+	if (ret)
+		goto err_free;
+
+	/* Release semaphore and add device to the tree */
+	up(&drv_data->sem);
+	pr_debug("found device at %d:%02x:%02x\n", client->adapter->nr, outer_addr, addr);
+	ret = device_add(&device->dev);
+	if (ret)
+		goto err_free;
+
+	goto end;
+err_free:
+	put_device(&device->dev);
+err_end:
+	up(&drv_data->sem);
+end:
+	kfree(buffer);
+	return ret;
+}
+
+/* I2C detect function: check if a main or external dependent device exists */
+static int ddcci_detect(struct i2c_client *client, struct i2c_board_info *info)
+{
+	int ret;
+	unsigned char outer_addr;
+	unsigned char inner_addr;
+	unsigned char buf[32];
+	unsigned char cmd_id[1] = { DDCCI_COMMAND_ID };
+	unsigned char cmd_caps[3] = { DDCCI_COMMAND_CAPS, 0x00, 0x00};
+	unsigned char *cmd;
+	unsigned int cmd_len;
+
+	/* Check for i2c_master_* functionality */
+	if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
+		pr_debug("i2c adapter %d unsuitable: no i2c_master functionality\n", client->adapter->nr);
+		return -ENODEV;
+	}
+
+	/* send Capabilities Request (for main) or Identification Request command (for dependent devices) */
+	outer_addr = client->addr << 1;
+	inner_addr = (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? DDCCI_HOST_ADDR_ODD : outer_addr | 1;
+	cmd = (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? cmd_caps : cmd_id;
+	cmd_len = (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? sizeof(cmd_caps) : sizeof(cmd_id);
+	pr_debug("detecting %d:%02x\n", client->adapter->nr, outer_addr);
+
+	ret = __ddcci_write_block(client, inner_addr, buf, true, cmd, cmd_len);
+
+	if (ret == -ENXIO || ret == -EIO) {
+		if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_WRITE_BYTE)) {
+			pr_debug("i2c write failed with ENXIO or EIO but bytewise writing is not supported\n");
+			return -ENODEV;
+		}
+		pr_debug("i2c write failed with ENXIO or EIO, trying bytewise writing\n");
+		ret = __ddcci_write_bytewise(client, inner_addr, true, cmd, cmd_len);
+		if (ret == 0) {
+			msleep(delay);
+			ret = __ddcci_write_bytewise(client, inner_addr, true, cmd, cmd_len);
+		}
+	}
+
+	if (ret < 0)
+		return -ENODEV;
+
+	/* wait for device */
+	msleep(delay);
+	/* receive answer */
+	ret = i2c_master_recv(client, buf, 32);
+	if (ret < 3) {
+		pr_debug("detection failed: no answer\n");
+		return -ENODEV;
+	}
+
+	/* check response starts with outer addr */
+	if (buf[0] != outer_addr) {
+		pr_debug("detection failed: invalid %s response (%02x != %02x)\n", (cmd == cmd_id) ? "identification" : "capabilities", buf[0], outer_addr);
+		pr_debug("received message was %*ph \n", ret, buf);
+		return -ENODEV;
+	}
+
+	pr_debug("detected %d:%02x\n", client->adapter->nr, outer_addr);
+
+	/* set device type */
+	strlcpy(info->type, (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? "ddcci" : "ddcci-dependent", I2C_NAME_SIZE);
+
+	return 0;
+}
+
+/* I2C probe function */
+static int ddcci_probe(struct i2c_client *client, const struct i2c_device_id *id)
+{
+	int i, ret = -ENODEV, tmp;
+	unsigned char main_addr, addr;
+	struct ddcci_bus_drv_data *drv_data;
+
+	/* Initialize driver data structure */
+	drv_data = devm_kzalloc(&client->dev, sizeof(struct ddcci_bus_drv_data), GFP_KERNEL);
+	if (!drv_data)
+		return -ENOMEM;
+	drv_data->i2c_dev = client;
+	sema_init(&drv_data->sem, 1);
+
+	/* Set i2c client data */
+	i2c_set_clientdata(client, drv_data);
+
+	if (id->driver_data == 0) {
+		/* Core device, probe at 0x6E */
+		main_addr = DDCCI_DEFAULT_DEVICE_ADDR;
+		dev_dbg(&client->dev, "probing core device [%02x]\n",
+			client->addr << 1);
+		ret = ddcci_detect_device(client, main_addr, 0);
+		if (ret) {
+			dev_info(&client->dev, "core device [%02x] probe failed: %d\n",
+				 client->addr << 1, ret);
+			if (ret == -EIO)
+				ret = -ENODEV;
+			goto err_free;
+		}
+
+		/* Detect internal dependent devices */
+		dev_dbg(&client->dev, "probing internal dependent devices\n");
+		for (i = 0; i < autoprobe_addr_count; ++i) {
+			addr = (unsigned short)autoprobe_addrs[i];
+			if ((addr & 1) == 0 && addr != main_addr) {
+				tmp = ddcci_detect_device(client, addr, main_addr);
+				if (tmp < 0 && tmp != -ENODEV) {
+					dev_info(&client->dev, "internal dependent device [%02x:%02x] probe failed: %d\n",
+						 client->addr << 1, addr, ret);
+				}
+			}
+		}
+	} else if (id->driver_data == 1) {
+		/* External dependent device */
+		main_addr = client->addr << 1;
+		dev_dbg(&client->dev, "probing external dependent device [%02x]\n", main_addr);
+		ret = ddcci_detect_device(client, main_addr, -1);
+		if (ret) {
+			dev_info(&client->dev, "external dependent device [%02x] probe failed: %d\n",
+				 main_addr, ret);
+			if (ret == -EIO)
+				ret = -ENODEV;
+			goto err_free;
+		}
+	} else {
+		dev_warn(&client->dev,
+			 "probe() called with invalid i2c device id\n");
+		ret = -EINVAL;
+	}
+
+	goto end;
+err_free:
+	devm_kfree(&client->dev, drv_data);
+end:
+	return ret;
+}
+
+/*
+ * Callback for bus_find_device() used in ddcci_remove()
+ *
+ * Find next device on i2c_client not flagged with
+ * DDCCI_FLAG_REMOVED and flag it.
+ */
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,3,0)
+static int ddcci_remove_helper(struct device *dev, const void *p)
+#else
+static int ddcci_remove_helper(struct device *dev, void *p)
+#endif
+{
+	struct ddcci_device *device;
+
+	device = ddcci_verify_device(dev);
+	if (!device || device->flags & DDCCI_FLAG_REMOVED)
+		return 0;
+
+	if (!p || (dev->parent == p)) {
+		device->flags |= DDCCI_FLAG_REMOVED;
+		wmb();
+		return 1;
+	}
+
+	return 0;
+}
+
+/* I2C driver remove callback: unregister all subdevices */
+static int ddcci_remove(struct i2c_client *client)
+{
+	struct ddcci_bus_drv_data *drv_data = i2c_get_clientdata(client);
+	struct device *dev;
+
+	down(&drv_data->sem);
+	while (1) {
+		dev = bus_find_device(&ddcci_bus_type, NULL, client,
+				      ddcci_remove_helper);
+		if (!dev)
+			break;
+		device_unregister(dev);
+		put_device(dev);
+	}
+	up(&drv_data->sem);
+	return 0;
+}
+
+/*
+ * I2C driver device identification table.
+ */
+static const struct i2c_device_id ddcci_idtable[] = {
+	{ "ddcci", 0 },
+	{ "ddcci-dependent", 1 },
+	{}
+};
+MODULE_DEVICE_TABLE(i2c, ddcci_idtable);
+
+/*
+ * I2C driver description structure
+ */
+static struct i2c_driver ddcci_driver = {
+	.driver = {
+		.name	= "ddcci",
+		.owner	= THIS_MODULE,
+	},
+
+	.id_table	= ddcci_idtable,
+	.probe		= ddcci_probe,
+	.remove		= ddcci_remove,
+	.class		= I2C_CLASS_DDC,
+	.detect		= ddcci_detect,
+	.address_list	= I2C_ADDRS(
+		DDCCI_DEFAULT_DEVICE_ADDR>>1
+	),
+};
+
+/*
+ * Module initialization function. Called when the module is inserted or
+ * (if builtin) at boot time.
+ */
+static int __init ddcci_module_init(void)
+{
+	int ret;
+
+	pr_debug("initializing ddcci driver\n");
+	/* Allocate a device number region for the character devices */
+	ret = alloc_chrdev_region(&ddcci_cdev_first, 0, 128, DEVICE_NAME);
+	if (ret < 0) {
+		pr_err("failed to register device region: error %d\n", ret);
+		goto err_chrdevreg;
+	}
+	ddcci_cdev_next = ddcci_cdev_first;
+	ddcci_cdev_end = MKDEV(MAJOR(ddcci_cdev_first), MINOR(ddcci_cdev_first)+128);
+
+	/* Register bus */
+	ret = bus_register(&ddcci_bus_type);
+	if (ret) {
+		pr_err("failed to register bus 'ddcci'\n");
+		goto err_busreg;
+	}
+
+	/* Register I2C driver */
+	ret = i2c_add_driver(&ddcci_driver);
+	if (ret) {
+		pr_err("failed to register i2c driver\n");
+		goto err_drvreg;
+	}
+
+	pr_debug("ddcci driver initialized\n");
+
+	return 0;
+
+err_drvreg:
+	bus_unregister(&ddcci_bus_type);
+err_busreg:
+	unregister_chrdev_region(ddcci_cdev_first, 128);
+err_chrdevreg:
+	return ret;
+}
+
+/*
+ * Module clean-up function. Called when the module is removed.
+ */
+static void __exit ddcci_module_exit(void)
+{
+	struct device *dev;
+
+	while (1) {
+		dev = bus_find_device(&ddcci_bus_type, NULL, NULL, ddcci_remove_helper);
+		if (!dev)
+			break;
+		device_unregister(dev);
+		put_device(dev);
+	}
+
+	i2c_del_driver(&ddcci_driver);
+	bus_unregister(&ddcci_bus_type);
+	unregister_chrdev_region(ddcci_cdev_first, 128);
+}
+
+/* Let the kernel know the calls for module init and exit */
+module_init(ddcci_module_init);
+module_exit(ddcci_module_exit);
+
+/* Module parameter description */
+module_param(delay, uint, S_IRUGO|S_IWUSR);
+MODULE_PARM_DESC(delay, "default delay after bus writes (in ms, default 60)");
+module_param_array(autoprobe_addrs, ushort, &autoprobe_addr_count, S_IRUGO|S_IWUSR);
+MODULE_PARM_DESC(autoprobe_addrs, "internal dependent device addresses to autoprobe");
+
+/* Module description */
+MODULE_AUTHOR("Christoph Grenz");
+MODULE_DESCRIPTION("DDC/CI bus driver");
+MODULE_VERSION("0.4.2");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/ddcci.h b/include/linux/ddcci.h
new file mode 100644
index 000000000000..a219f031e584
--- /dev/null
+++ b/include/linux/ddcci.h
@@ -0,0 +1,164 @@
+/*
+ *  DDC/CI bus driver
+ *
+ *  Copyright (c) 2015 Christoph Grenz
+ */
+
+/*
+ * 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.
+ */
+
+#ifndef _DDCCI_H
+#define _DDCCI_H
+
+#include <linux/mod_devicetable.h>
+#include <linux/device.h>
+#include <linux/cdev.h>
+
+#define DDCCI_MODULE_PREFIX "ddcci:"
+
+/* Special addresses */
+
+/* default device address (even) */
+#define DDCCI_DEFAULT_DEVICE_ADDR	0x6E
+/* receiving host address for communication with default device address */
+#define DDCCI_HOST_ADDR_EVEN	0x50
+/* sending host address for communication with default device address */
+#define DDCCI_HOST_ADDR_ODD	0x51
+
+/* Command codes */
+
+/* Identification Request */
+#define DDCCI_COMMAND_ID	0xf1
+/* Identification Reply */
+#define DDCCI_REPLY_ID	0xe1
+/* Capabilities Request */
+#define DDCCI_COMMAND_CAPS	0xf3
+/* Capabilities Reply */
+#define DDCCI_REPLY_CAPS	0xe3
+
+/* Quirks */
+
+/* Device always responds with unset protocol flag */
+#define DDCCI_QUIRK_NO_PFLAG BIT(1)
+/* Device needs writing one byte at a time  */
+#define DDCCI_QUIRK_WRITE_BYTEWISE BIT(2)
+/* Device repeats first byte on read */
+#define DDCCI_QUIRK_SKIP_FIRST_BYTE BIT(3)
+
+/* Flags */
+
+#define DDCCI_FLAG_REMOVED BIT(1)
+#define DDCCI_FLAG_DEPENDENT BIT(2)
+#define DDCCI_FLAG_EXTERNAL BIT(3)
+
+extern struct bus_type ddcci_bus_type;
+
+struct ddcci_bus_drv_data;
+
+/* struct ddcci_device_id - identifies DDC/CI devices for probing */
+struct ddcci_device_id {
+	char prot[9];
+	char type[9];
+	char model[9];
+	char vendor[9];
+	char module[9];
+	kernel_ulong_t driver_data;	/* Data private to the driver */
+};
+#define DDCCI_ANY_ID "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"
+
+/**
+ * struct ddcci_device - represent an DDC/CI device
+ * @outer_addr: Outer device address (I2C address << 1).
+ * @inner_addr: Inner device address.
+ * @flags: Device flags.
+ * @capabilities: Device capability string.
+ * @capabilities_len: Length of capability string.
+ * @i2c_client: Parent I2C device.
+ * @bus_drv_data: Driver internal data structure.
+ * @dev: Driver model device node for the slave.
+ * @cdev: Character device structure
+ * @cdev_sem: RW semaphore for exclusive access on character device.
+ * @prot: Device class ("protocol", from capability string)
+ * @type: Device subclass ("type", from capability string)
+ * @model: Device model (from capability string)
+ * @vendor: Device vendor (from identification command response)
+ * @module: Device module (from identification command response)
+ * @device_number: Device serial (from identification command response)
+ */
+struct ddcci_device {
+	unsigned short outer_addr;
+	unsigned short inner_addr;
+	int flags;
+	char *capabilities;
+	size_t capabilities_len;
+	struct i2c_client *i2c_client;
+	struct ddcci_bus_drv_data *bus_drv_data;
+	struct device dev;
+	struct cdev cdev;
+	struct rw_semaphore cdev_sem;
+	char prot[9];
+	char type[9];
+	char model[9];
+	char vendor[9];
+	char module[9];
+	int device_number;
+};
+#define to_ddcci_device(d) container_of(d, struct ddcci_device, dev)
+
+/**
+ * struct ddcci_driver - represent an DDC/CI device driver
+ * @probe: Callback for device binding
+ * @remove: Callback for device unbinding
+ * @driver: Device driver model driver
+ * @id_table: List of DDC/CI devices supported by this driver
+ *
+ * The driver.owner field should be set to the module owner of this driver.
+ * The driver.name field should be set to the name of this driver.
+ */
+struct ddcci_driver {
+	int (*probe)(struct ddcci_device *, const struct ddcci_device_id *);
+	int (*remove)(struct ddcci_device *);
+	struct device_driver driver;
+	struct ddcci_device_id *id_table;
+};
+#define to_ddcci_driver(d) container_of(d, struct ddcci_driver, driver)
+
+int ddcci_register_driver(struct module *owner, struct ddcci_driver *driver);
+#define ddcci_add_driver(driver) \
+	ddcci_register_driver(THIS_MODULE, driver)
+void ddcci_del_driver(struct ddcci_driver *driver);
+
+struct ddcci_device *ddcci_verify_device(struct device *dev);
+
+#define module_ddcci_driver(__ddcci_driver) \
+	module_driver(__ddcci_driver, ddcci_add_driver, \
+			ddcci_del_driver)
+
+int ddcci_device_write(struct ddcci_device *, bool p_flag, unsigned char *data,
+		       unsigned char length);
+int ddcci_device_read(struct ddcci_device *, bool p_flag, unsigned char *buffer,
+		      unsigned char length);
+int ddcci_device_writeread(struct ddcci_device *, bool p_flag,
+			   unsigned char *buffer, unsigned char length,
+			   unsigned char maxlength);
+
+static inline void *ddcci_get_drvdata(const struct ddcci_device *dev)
+{
+	return dev_get_drvdata(&dev->dev);
+}
+
+static inline void ddcci_set_drvdata(struct ddcci_device *dev, void *data)
+{
+	dev_set_drvdata(&dev->dev, data);
+}
+
+unsigned long ddcci_quirks(struct ddcci_device *dev);
+
+const char *ddcci_find_capstr_item(const char *capabilities, const char *tag,
+				   size_t *length);
+
+#endif
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* Re: [PATCH] drivers: ddcci: upstream DDCCI driver
  2022-02-26  2:20 [PATCH] drivers: ddcci: upstream DDCCI driver Yusuf Khan
@ 2022-02-26  2:33 ` Yusuf Khan
  2022-02-26  9:38 ` Arnd Bergmann
  1 sibling, 0 replies; 7+ messages in thread
From: Yusuf Khan @ 2022-02-26  2:33 UTC (permalink / raw)
  To: linux-kernel
  Cc: jasowang, mikelley, mst, gregkh, javier, arnd, will, axboe,
	bjorn.andersson

derp, I should have used the checkpatch command before I sent the
patch, v2 is coming.

On Fri, Feb 25, 2022 at 6:21 PM Yusuf Khan <yusisamerican@gmail.com> wrote:
>
> This patch upstreams the DDCCI driver by Christoph Grenz into
> the kernel. The original gitlab page is loacted at https://gitlab
> .com/ddcci-driver-linux/ddcci-driver-linux/-/tree/master.
>
> Signed-off-by: Yusuf Khan <yusisamerican@gmail.com>
> ---
>  drivers/Kconfig                 |    2 +
>  drivers/Makefile                |    1 +
>  drivers/ddcci/Kconfig           |    3 +
>  drivers/ddcci/Makefile          |    3 +
>  drivers/ddcci/ddcci-backlight.c |  413 +++++++
>  drivers/ddcci/ddcci.c           | 1895 +++++++++++++++++++++++++++++++
>  include/linux/ddcci.h           |  164 +++
>  7 files changed, 2481 insertions(+)
>  create mode 100644 drivers/ddcci/Kconfig
>  create mode 100644 drivers/ddcci/Makefile
>  create mode 100644 drivers/ddcci/ddcci-backlight.c
>  create mode 100644 drivers/ddcci/ddcci.c
>  create mode 100644 include/linux/ddcci.h
>
> diff --git a/drivers/Kconfig b/drivers/Kconfig
> index 0d399ddaa185..3a58a29113f9 100644
> --- a/drivers/Kconfig
> +++ b/drivers/Kconfig
> @@ -236,4 +236,6 @@ source "drivers/interconnect/Kconfig"
>  source "drivers/counter/Kconfig"
>
>  source "drivers/most/Kconfig"
> +
> +source "drivers/ddcci/Kconfig"
>  endmenu
> diff --git a/drivers/Makefile b/drivers/Makefile
> index a110338c860c..c9181f1dc5aa 100644
> --- a/drivers/Makefile
> +++ b/drivers/Makefile
> @@ -21,6 +21,7 @@ obj-y                         += pci/
>  obj-$(CONFIG_PARISC)           += parisc/
>  obj-$(CONFIG_RAPIDIO)          += rapidio/
>  obj-y                          += video/
> +obj-$(CONFIG_DDCCI)                            += ddcci/
>  obj-y                          += idle/
>
>  # IPMI must come before ACPI in order to provide IPMI opregion support
> diff --git a/drivers/ddcci/Kconfig b/drivers/ddcci/Kconfig
> new file mode 100644
> index 000000000000..416881cc88e1
> --- /dev/null
> +++ b/drivers/ddcci/Kconfig
> @@ -0,0 +1,3 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +menuconfig DDCCI
> +       tristate "DDCCI display protocol support"
> diff --git a/drivers/ddcci/Makefile b/drivers/ddcci/Makefile
> new file mode 100644
> index 000000000000..cce606be8038
> --- /dev/null
> +++ b/drivers/ddcci/Makefile
> @@ -0,0 +1,3 @@
> +# SPDX-License-Identifier: GPL-2.0
> +obj-$(CONFIG_DDCCI) += ddcci.o
> +obj-$(CONFIG_DDCCI) += ddcci-backlight.o
> diff --git a/drivers/ddcci/ddcci-backlight.c b/drivers/ddcci/ddcci-backlight.c
> new file mode 100644
> index 000000000000..7a9852207f0b
> --- /dev/null
> +++ b/drivers/ddcci/ddcci-backlight.c
> @@ -0,0 +1,413 @@
> +/*
> + *  DDC/CI monitor backlight driver
> + *
> + *  Copyright (c) 2015 Christoph Grenz
> + */
> +
> +/*
> + * 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.
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +#include <linux/backlight.h>
> +#include <linux/module.h>
> +#include <linux/fb.h>
> +#include <linux/sysfs.h>
> +
> +#include <linux/ddcci.h>
> +
> +
> +#define DDCCI_COMMAND_READ     0x01    /* read ctrl value */
> +#define DDCCI_REPLY_READ       0x02    /* read ctrl value reply */
> +#define DDCCI_COMMAND_WRITE    0x03    /* write ctrl value */
> +#define DDCCI_COMMAND_SAVE     0x0c    /* save current settings */
> +
> +#define DDCCI_MONITOR_LUMINANCE        0x10
> +#define DDCCI_MONITOR_BACKLIGHT        0x13
> +#define DDCCI_MONITOR_BL_WHITE         0x6B
> +
> +static bool convenience_symlink = true;
> +
> +struct ddcci_monitor_drv_data {
> +       struct ddcci_device *device;
> +       struct backlight_device *bl_dev;
> +       struct device *fb_dev;
> +       unsigned char used_vcp;
> +};
> +
> +static int ddcci_monitor_writectrl(struct ddcci_device *device,
> +                                  unsigned char ctrl, unsigned short value)
> +{
> +       unsigned char buf[4];
> +       int ret;
> +
> +       buf[0] = DDCCI_COMMAND_WRITE;
> +       buf[1] = ctrl;
> +       buf[2] = (value >> 8);
> +       buf[3] = (value & 255);
> +
> +       ret = ddcci_device_write(device, true, buf, sizeof(buf));
> +
> +       return ret;
> +}
> +
> +static int ddcci_monitor_readctrl(struct ddcci_device *device,
> +                                 unsigned char ctrl, unsigned short *value,
> +                                 unsigned short *maximum)
> +{
> +       int ret;
> +       unsigned char buf[10];
> +
> +       buf[0] = DDCCI_COMMAND_READ;
> +       buf[1] = ctrl;
> +
> +       ret = ddcci_device_writeread(device, true, buf, 2, sizeof(buf));
> +       if (ret < 0)
> +               return ret;
> +
> +       if (ret == 0)
> +               return -ENOTSUPP;
> +
> +       if (ret == 8 && buf[0] == DDCCI_REPLY_READ && buf[2] == ctrl) {
> +               if (value)
> +                       *value = buf[6] * 256 + buf[7];
> +
> +               if (maximum)
> +                       *maximum = buf[4] * 256 + buf[5];
> +
> +               if (buf[1] == 1)
> +                       return -ENOTSUPP;
> +               if (buf[1] != 0)
> +                       return -EIO;
> +               return 0;
> +       }
> +
> +       return -EIO;
> +}
> +
> +static int ddcci_backlight_check_fb(struct backlight_device *bl,
> +                                  struct fb_info *info)
> +{
> +       struct ddcci_monitor_drv_data *drv_data = bl_get_data(bl);
> +
> +       return drv_data->fb_dev == NULL || drv_data->fb_dev == info->dev;
> +}
> +
> +static int ddcci_backlight_update_status(struct backlight_device *bl)
> +{
> +       struct ddcci_monitor_drv_data *drv_data = bl_get_data(bl);
> +       int brightness = bl->props.brightness;
> +       int ret;
> +
> +       if (bl->props.power != FB_BLANK_UNBLANK ||
> +           bl->props.state & BL_CORE_FBBLANK)
> +               brightness = 0;
> +
> +       ret = ddcci_monitor_writectrl(drv_data->device, drv_data->used_vcp,
> +                                     brightness);
> +       if (ret > 0)
> +               ret = 0;
> +       return ret;
> +}
> +
> +static int ddcci_backlight_get_brightness(struct backlight_device *bl)
> +{
> +       unsigned short value = 0, maxval = 0;
> +       int ret;
> +       struct ddcci_monitor_drv_data *drv_data = bl_get_data(bl);
> +
> +       ret = ddcci_monitor_readctrl(drv_data->device, drv_data->used_vcp,
> +                                    &value, &maxval);
> +       if (ret < 0)
> +               return ret;
> +
> +       bl->props.brightness = value;
> +       bl->props.max_brightness = maxval;
> +       ret = value;
> +
> +       return ret;
> +}
> +
> +static const struct backlight_ops ddcci_backlight_ops = {
> +       .options        = 0,
> +       .update_status  = ddcci_backlight_update_status,
> +       .get_brightness = ddcci_backlight_get_brightness,
> +       .check_fb       = ddcci_backlight_check_fb,
> +};
> +
> +static const char *ddcci_monitor_vcp_name(unsigned char vcp)
> +{
> +       switch (vcp) {
> +               case DDCCI_MONITOR_BL_WHITE:
> +                       return "backlight";
> +               case DDCCI_MONITOR_LUMINANCE:
> +                       return "luminance";
> +               default:
> +                       return "???";
> +       }
> +}
> +
> +static const char *ddcci_monitor_next_vcp_item(const char *ptr)
> +{
> +       int depth = 0;
> +
> +       /* Sanity check */
> +       if (unlikely(ptr == NULL || ptr[0] == '\0'))
> +               return NULL;
> +
> +       /* Find next white space outside of parentheses */
> +       while ((ptr = strpbrk(ptr, " ()"))) {
> +               if (!ptr || depth == INT_MAX) {
> +                       return NULL;
> +               } else if (*ptr == '(') {
> +                       depth++;
> +               } else if (depth > 0) {
> +                       if (*ptr == ')')
> +                               depth--;
> +               } else {
> +                       break;
> +               }
> +               ++ptr;
> +       }
> +
> +       /* Skip over whitespace */
> +       ptr = skip_spaces(ptr);
> +
> +       /* Check if we're now at the end of the list */
> +       if (unlikely(*ptr == '\0' || *ptr == ')'))
> +               return NULL;
> +
> +       return ptr;
> +}
> +
> +static bool ddcci_monitor_find_vcp(unsigned char vcp, const char *s)
> +{
> +       const char *ptr = s;
> +       char vcp_hex[3];
> +
> +       /* Sanity check */
> +       if (unlikely(s == NULL || s[0] == '\0'))
> +               return false;
> +
> +       /* Create hex representation of VCP */
> +       if (unlikely(snprintf(vcp_hex, 3, "%02hhX", vcp) != 2)) {
> +               pr_err("snprintf failed to convert to hex. This should not happen.\n");
> +               return false;
> +       }
> +
> +       /* Search for it */
> +       do {
> +               if (strncasecmp(vcp_hex, ptr, 2) == 0) {
> +                       if (ptr[2] == ' ' || ptr[2] == '(' || ptr[2] == ')') {
> +                               return true;
> +                       }
> +               }
> +       } while ((ptr = ddcci_monitor_next_vcp_item(ptr)));
> +
> +       return false;
> +}
> +
> +static int ddcci_backlight_create_symlink(struct ddcci_device *ddcci_dev)
> +{
> +       int i, result;
> +       struct device *dev = &ddcci_dev->dev;
> +       struct kernfs_node *dirent;
> +       for (i = 0; i < 3; ++i) {
> +               dev = dev->parent;
> +               if (!dev) {
> +                       dev_dbg(&ddcci_dev->dev, "failed to create convenience symlink: ancestor device not found\n");
> +                       return -ENOENT;
> +               }
> +       }
> +       dirent = sysfs_get_dirent(dev->kobj.sd, "ddcci_backlight");
> +       if (dirent) {
> +               sysfs_put(dirent);
> +               dev_dbg(&ddcci_dev->dev, "failed to create convenience symlink: %s/ddcci_backlight already exists\n", dev_name(dev));
> +               return -EEXIST;
> +       }
> +
> +       result = sysfs_create_link(&dev->kobj, &ddcci_dev->dev.kobj, "ddcci_backlight");
> +       if (result == 0) {
> +               dev_dbg(&ddcci_dev->dev, "created symlink %s/ddcci_backlight\n", dev_name(dev));
> +       } else {
> +               dev_info(&ddcci_dev->dev, "failed to create convenience symlink: %d\n", result);
> +       }
> +       return result;
> +}
> +
> +static int ddcci_backlight_remove_symlink(struct ddcci_device *ddcci_dev)
> +{
> +       int i;
> +       struct device *dev = &ddcci_dev->dev;
> +       struct kernfs_node *dirent;
> +       for (i = 0; i < 3; ++i) {
> +               dev = dev->parent;
> +               if (!dev)
> +                       return -ENOENT;
> +       }
> +       dirent = sysfs_get_dirent(dev->kobj.sd, "ddcci_backlight");
> +       if (!dirent) {
> +               return -ENOENT;
> +       }
> +
> +       if ((dirent->flags & KERNFS_LINK) == 0) {
> +               sysfs_put(dirent);
> +               dev_dbg(&ddcci_dev->dev, "won't remove %s/ddcci_backlight: not a symlink\n", dev_name(dev));
> +               return -EINVAL;
> +       }
> +
> +       if (dirent->symlink.target_kn != ddcci_dev->dev.kobj.sd) {
> +               sysfs_put(dirent);
> +               dev_dbg(&ddcci_dev->dev, "won't remove %s/ddcci_backlight: we are not the link target\n", dev_name(dev));
> +               return -EINVAL;
> +       }
> +
> +       sysfs_put(dirent);
> +
> +       sysfs_remove_link(&dev->kobj, "ddcci_backlight");
> +       dev_dbg(&ddcci_dev->dev, "removed symlink %s/ddcci_backlight\n", dev_name(dev));
> +       return 0;
> +}
> +
> +static int ddcci_monitor_probe(struct ddcci_device *dev,
> +                              const struct ddcci_device_id *id)
> +{
> +       struct ddcci_monitor_drv_data *drv_data;
> +       struct backlight_properties props;
> +       struct backlight_device *bl = NULL;
> +       int ret = 0;
> +       bool support_luminance, support_bl_white;
> +       unsigned short brightness = 0, max_brightness = 0;
> +       const char *vcps;
> +
> +       dev_dbg(&dev->dev, "probing monitor backlight device\n");
> +
> +       /* Get VCP list */
> +       vcps = ddcci_find_capstr_item(dev->capabilities, "vcp", NULL);
> +       if (IS_ERR(vcps)) {
> +               dev_info(&dev->dev,
> +                        "monitor doesn't provide a list of supported controls.\n");
> +               support_bl_white = support_luminance = true;
> +       } else {
> +               /* Check VCP list for supported VCPs */
> +               support_bl_white = ddcci_monitor_find_vcp(DDCCI_MONITOR_BL_WHITE, vcps);
> +               support_luminance = ddcci_monitor_find_vcp(DDCCI_MONITOR_LUMINANCE, vcps);
> +               /* Fallback to trying if no support is found */
> +               if (!support_bl_white && !support_luminance) {
> +                       dev_info(&dev->dev,
> +                                "monitor doesn't announce support for backlight or luminance controls.\n");
> +                       support_bl_white = support_luminance = true;
> +               }
> +       }
> +
> +       /* Initialize driver data structure */
> +       drv_data = devm_kzalloc(&dev->dev, sizeof(struct ddcci_monitor_drv_data),
> +                               GFP_KERNEL);
> +       if (!drv_data)
> +               return -ENOMEM;
> +       drv_data->device = dev;
> +
> +       if (support_bl_white) {
> +               /* Try getting backlight level */
> +               dev_dbg(&dev->dev,
> +                       "trying to access \"backlight level white\" control\n");
> +               ret = ddcci_monitor_readctrl(drv_data->device, DDCCI_MONITOR_BL_WHITE,
> +                                               &brightness, &max_brightness);
> +               if (ret < 0) {
> +                       if (ret == -ENOTSUPP)
> +                               dev_info(&dev->dev,
> +                                       "monitor does not support reading backlight level\n");
> +                       else
> +                               goto err_free;
> +               } else {
> +                       drv_data->used_vcp = DDCCI_MONITOR_BL_WHITE;
> +               }
> +       }
> +
> +       if (support_luminance && !drv_data->used_vcp) {
> +               /* Try getting luminance */
> +               dev_dbg(&dev->dev,
> +                       "trying to access \"luminance\" control\n");
> +               ret = ddcci_monitor_readctrl(drv_data->device, DDCCI_MONITOR_LUMINANCE,
> +                                            &brightness, &max_brightness);
> +               if (ret < 0) {
> +                       if (ret == -ENOTSUPP)
> +                               dev_info(&dev->dev,
> +                                       "monitor does not support reading luminance\n");
> +                       else
> +                               goto err_free;
> +               } else {
> +                       drv_data->used_vcp = DDCCI_MONITOR_LUMINANCE;
> +               }
> +               drv_data->used_vcp = DDCCI_MONITOR_LUMINANCE;
> +       }
> +
> +       if (!drv_data->used_vcp)
> +               goto err_free;
> +
> +       /* Create brightness device */
> +       memset(&props, 0, sizeof(props));
> +       props.type = BACKLIGHT_RAW;
> +       props.max_brightness = max_brightness;
> +       props.brightness = brightness;
> +       bl = devm_backlight_device_register(&dev->dev, dev_name(&dev->dev),
> +                                           &dev->dev, drv_data,
> +                                           &ddcci_backlight_ops, &props);
> +       drv_data->bl_dev = bl;
> +       if (IS_ERR(bl)) {
> +               dev_err(&dev->dev, "failed to register backlight\n");
> +               return PTR_ERR(bl);
> +       }
> +       dev_info(&dev->dev, "registered %s as backlight device %s\n",
> +                ddcci_monitor_vcp_name(drv_data->used_vcp),
> +                dev_name(&dev->dev));
> +
> +       if (convenience_symlink) {
> +               ddcci_backlight_create_symlink(dev);
> +       }
> +
> +       goto end;
> +err_free:
> +       devm_kfree(&dev->dev, drv_data);
> +end:
> +       return ret;
> +}
> +
> +static int ddcci_monitor_remove(struct ddcci_device *dev)
> +{
> +       dev_dbg(&dev->dev, "removing device\n");
> +       ddcci_backlight_remove_symlink(dev);
> +       return 0;
> +}
> +
> +static struct ddcci_device_id ddcci_monitor_idtable[] = {
> +       { "monitor", DDCCI_ANY_ID, DDCCI_ANY_ID, DDCCI_ANY_ID, DDCCI_ANY_ID, 0 },
> +       {}
> +};
> +
> +static struct ddcci_driver ddcci_backlight_driver = {
> +       .driver = {
> +               .name   = "ddcci-backlight",
> +               .owner  = THIS_MODULE,
> +       },
> +
> +       .id_table       = ddcci_monitor_idtable,
> +       .probe          = ddcci_monitor_probe,
> +       .remove         = ddcci_monitor_remove,
> +};
> +
> +module_ddcci_driver(ddcci_backlight_driver);
> +
> +/* Module parameter description */
> +module_param(convenience_symlink, bool, S_IRUGO|S_IWUSR);
> +MODULE_PARM_DESC(convenience_symlink, "add convenience symlink \"ddcci_backlight\" to ancestor device in sysfs (default true)");
> +
> +MODULE_AUTHOR("Christoph Grenz");
> +MODULE_DESCRIPTION("DDC/CI generic monitor backlight driver");
> +MODULE_VERSION("0.4.2");
> +MODULE_LICENSE("GPL");
> +
> +MODULE_ALIAS("ddcci:monitor-*-*-*-*");
> diff --git a/drivers/ddcci/ddcci.c b/drivers/ddcci/ddcci.c
> new file mode 100644
> index 000000000000..e26d15cae948
> --- /dev/null
> +++ b/drivers/ddcci/ddcci.c
> @@ -0,0 +1,1895 @@
> +/*
> + *  DDC/CI sub-bus driver
> + *
> + *  Copyright (c) 2015 Christoph Grenz
> + */
> +
> +/*
> + * 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.
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +#include <asm-generic/fcntl.h>
> +#include <linux/cdev.h>
> +#include <linux/delay.h>
> +#include <linux/device.h>
> +#include <linux/fs.h>
> +#include <linux/i2c.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/rwsem.h>
> +#include <linux/sem.h>
> +#include <linux/slab.h>
> +#include <linux/uaccess.h>
> +#include <linux/version.h>
> +
> +#include <linux/ddcci.h>
> +
> +#define DDCCI_RECV_BUFFER_SIZE 130
> +#define DEVICE_NAME "ddcci"
> +#define DDCCI_MAX_CAP_CHUNKS 200
> +
> +static unsigned int delay = 60;
> +static unsigned short autoprobe_addrs[127] = {0xF0, 0xF2, 0xF4, 0xF6, 0xF8};
> +static int autoprobe_addr_count = 5;
> +
> +static dev_t ddcci_cdev_first;
> +static dev_t ddcci_cdev_next;
> +static dev_t ddcci_cdev_end;
> +static DEFINE_SEMAPHORE(core_lock);
> +
> +struct bus_type ddcci_bus_type;
> +EXPORT_SYMBOL_GPL(ddcci_bus_type);
> +
> +/* Assert neccessary string array sizes  */
> +#ifndef sizeof_field
> +# define sizeof_field(t,m) FIELD_SIZEOF(t,m)
> +#endif
> +static_assert(sizeof_field(struct ddcci_device, prot) > 8);
> +static_assert(sizeof_field(struct ddcci_device, type) > 8);
> +static_assert(sizeof_field(struct ddcci_device, model) > 8);
> +static_assert(sizeof_field(struct ddcci_device, vendor) > 8);
> +static_assert(sizeof_field(struct ddcci_device, module) > 8);
> +
> +/* Internal per-i2c-client driver data */
> +struct ddcci_bus_drv_data {
> +       unsigned long quirks;
> +       struct i2c_client *i2c_dev;
> +       struct semaphore sem;
> +       unsigned char recv_buffer[DDCCI_RECV_BUFFER_SIZE];
> +};
> +
> +/* Replace non-alphanumeric characters in a string (used for modalias) */
> +static void ddcci_modalias_clean(char *string, size_t n, char replacement)
> +{
> +       int i;
> +       for (i = 0; i < n; ++i) {
> +               char c = string[i];
> +               if (c == 0) {
> +                       return;
> +               } else if (c < '0' || (c > '9' && c < 'A') || (c > 'Z' && c < 'a') || c > 'z') {
> +                       string[i] = replacement;
> +               }
> +       }
> +}
> +
> +/* Write a message to the DDC/CI bus using i2c_smbus_write_byte() */
> +static int __ddcci_write_bytewise(struct i2c_client *client, unsigned char addr,
> +                                 bool p_flag, const unsigned char * __restrict buf,
> +                                 unsigned char len)
> +{
> +       int ret = 0;
> +       unsigned char outer_addr = (unsigned char)(client->addr << 1);
> +       unsigned xor = outer_addr; /* initial xor value */
> +
> +       /* Consistency checks */
> +       if (len > 127)
> +               return -EINVAL;
> +
> +       /* Special case: sender to 0x6E is always 0x51 */
> +       if (addr == DDCCI_DEFAULT_DEVICE_ADDR) {
> +               addr = DDCCI_HOST_ADDR_ODD;
> +       } else {
> +               /* When sending the odd address is used */
> +               addr = addr | 1;
> +       }
> +
> +       /* first byte: sender address */
> +       xor ^= addr;
> +       ret = i2c_smbus_write_byte(client, addr);
> +       if (ret < 0)
> +               return ret;
> +
> +       /* second byte: protocol flag and message size */
> +       xor ^= ((p_flag << 7) | len);
> +       ret = i2c_smbus_write_byte(client, (p_flag << 7)|len);
> +       if (ret < 0)
> +               return ret;
> +
> +       /* send payload */
> +       while (len--) {
> +               xor ^= (*buf);
> +               ret = i2c_smbus_write_byte(client, (*buf));
> +               if (ret < 0)
> +                       return ret;
> +               buf++;
> +       }
> +
> +       /* send checksum */
> +       ret = i2c_smbus_write_byte(client, xor);
> +       return ret;
> +}
> +
> +/* Write a message to the DDC/CI bus using i2c_master_send() */
> +static int __ddcci_write_block(struct i2c_client *client, unsigned char addr,
> +                              unsigned char *sendbuf, bool p_flag,
> +                              const unsigned char *data, unsigned char len)
> +{
> +       unsigned char outer_addr = (unsigned char)(client->addr << 1);
> +       unsigned xor = outer_addr;      /* initial xor value */
> +       unsigned char *ptr = sendbuf;
> +
> +       /* Consistency checks */
> +       if (len > 127)
> +               return -EINVAL;
> +
> +       /* Special case: sender to 0x6E is always 0x51 */
> +       if (addr == DDCCI_DEFAULT_DEVICE_ADDR) {
> +               addr = DDCCI_HOST_ADDR_ODD;
> +       } else {
> +               /* When sending the odd address is used */
> +               addr = addr | 1;
> +       }
> +
> +       /* first byte: sender address */
> +       xor ^= addr;
> +       *(ptr++) = addr;
> +       /* second byte: protocol flag and message size */
> +       xor ^= ((p_flag << 7) | len);
> +       *(ptr++) = (p_flag << 7)|len;
> +       /* payload */
> +       while (len--) {
> +               xor ^= (*data);
> +               *(ptr++) = (*data);
> +               data++;
> +       }
> +       /* checksum */
> +       (*ptr) = xor;
> +
> +       /* Send it */
> +       return i2c_master_send(client, sendbuf, ptr - sendbuf + 1);
> +}
> +
> +/*
> + * Write a message to the DDC/CI bus.
> + *
> + * You must hold the bus semaphore when calling this function.
> + */
> +static int ddcci_write(struct i2c_client *client, unsigned char addr,
> +                      bool p_flag, const unsigned char *data,
> +                      unsigned char len)
> +{
> +       struct ddcci_bus_drv_data *drv_data;
> +       unsigned char *sendbuf;
> +       int ret;
> +
> +       drv_data = i2c_get_clientdata(client);
> +
> +
> +       pr_debug("sending to %d:%02x:%02x: %*ph\n", client->adapter->nr,
> +                client->addr << 1, addr, len, data);
> +       if (drv_data->quirks & DDCCI_QUIRK_WRITE_BYTEWISE) {
> +               ret = __ddcci_write_bytewise(client, addr, p_flag, data, len);
> +       } else {
> +               sendbuf = drv_data->recv_buffer;
> +               ret = __ddcci_write_block(client, addr, sendbuf, p_flag, data, len);
> +       }
> +
> +       return ret;
> +}
> +
> +/*
> + * Read a response from the DDC/CI bus with headers directly into a buffer.
> + * Always check for DDCCI_QUIRK_SKIP_FIRST_BYTE when using this function.
> + * The returned length contains the whole unmodified response.
> + * If -EMSGSIZE is returned, the buffer contains the response up to `len`.
> + * If any other negative error code is returned, the buffer content is
> + * unspecified.
> + */
> +static int __ddcci_read(struct i2c_client *client, unsigned char addr,
> +                       bool p_flag, unsigned long quirks, unsigned char *buf,
> +                       unsigned char len)
> +{
> +       int i, payload_len, packet_length, ret;
> +       unsigned char xor = DDCCI_HOST_ADDR_EVEN;
> +
> +       /* Consistency checks */
> +       if (len < 3)
> +               return -EINVAL;
> +
> +       /* Read frame */
> +       ret = i2c_master_recv(client, buf, len);
> +       if (ret < 0)
> +               goto out_err;
> +       packet_length = ret;
> +
> +       /* Skip first byte if quirk active */
> +       if ((quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE) && ret > 0 && len > 0) {
> +               ret--;
> +               len--;
> +               buf++;
> +       }
> +
> +       /* If answer too short (= incomplete) break out */
> +       if (ret < 3) {
> +               ret = -EIO;
> +               goto out_err;
> +       }
> +
> +       /* validate first byte */
> +       if (unlikely(buf[0] != addr)) {
> +               ret = (buf[0] == '\0') ? -EAGAIN : -EIO;
> +               goto out_err;
> +       }
> +
> +       /* validate second byte (protocol flag) */
> +       if (unlikely((buf[1] & 0x80) != (p_flag << 7))) {
> +               if (!p_flag || !(quirks & DDCCI_QUIRK_NO_PFLAG)) {
> +                       ret = -EIO;
> +                       goto out_err;
> +               }
> +       }
> +
> +       /* get and check payload length */
> +       payload_len = buf[1] & 0x7F;
> +       if (3+payload_len > packet_length)
> +               return -EBADMSG;
> +       if (3+payload_len > len)
> +               return -EMSGSIZE;
> +
> +       /* calculate checksum */
> +       for (i = 0; i < 3+payload_len; i++)
> +               xor ^= buf[i];
> +
> +       /* verify checksum */
> +       if (xor != 0) {
> +               dev_err(&client->dev, "invalid DDC/CI response, corrupted data - xor is 0x%02x, length 0x%02x\n",
> +                       xor, payload_len);
> +               ret = -EBADMSG;
> +               goto out_err;
> +       }
> +
> +       /* return result */
> +       ret = payload_len+3+((quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE)?1:0);
> +
> +out_err:
> +       return ret;
> +}
> +
> +/*
> + * Read a response from the DDC/CI bus
> + *
> + * You must hold the bus semaphore when calling this function.
> + */
> +static int ddcci_read(struct i2c_client *client, unsigned char addr,
> +                     bool p_flag, unsigned char *buf, unsigned char len)
> +{
> +       struct ddcci_bus_drv_data *drv_data;
> +       unsigned char *recvbuf;
> +       int ret;
> +
> +       drv_data = i2c_get_clientdata(client);
> +       recvbuf = drv_data->recv_buffer;
> +
> +       /* Read frame */
> +       ret = __ddcci_read(client, addr, p_flag,
> +               drv_data->quirks, recvbuf, DDCCI_RECV_BUFFER_SIZE);
> +       if (ret < 0)
> +               return ret;
> +
> +       if (drv_data->quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE)
> +               recvbuf++;
> +
> +       /* return result */
> +       if (buf) {
> +               if (ret > 3) {
> +                       ret = ret-3;
> +                       /* copy to caller buffer */
> +                       memcpy(buf, &recvbuf[2], (ret < len) ? ret : len);
> +
> +                       if (ret > len) {
> +                               /* if message was truncated, return -EMSGSIZE */
> +                               pr_debug("received from %d:%02x:%02x: [%u/%u] %*ph ...\n",
> +                                        client->adapter->nr, client->addr << 1,
> +                                        addr, ret, len, len, buf);
> +                               ret = -EMSGSIZE;
> +                       } else {
> +                               pr_debug("received from %d:%02x:%02x: [%u/%u] %*ph\n",
> +                                        client->adapter->nr, client->addr << 1,
> +                                        addr, ret, len, ret, buf);
> +                       }
> +               }
> +       }
> +       if (!(drv_data->quirks & DDCCI_QUIRK_WRITE_BYTEWISE)) {
> +               /* second read to clear buffers, needed on some devices */
> +               __ddcci_read(client, addr, true, drv_data->quirks, recvbuf, 1);
> +       }
> +       return ret;
> +}
> +
> +/* Request the capability string for a device and put it into buf */
> +static int ddcci_get_caps(struct i2c_client *client, unsigned char addr,
> +                         unsigned char *buf, unsigned int len)
> +{
> +       int result = 0, counter = 0, offset = 0;
> +       unsigned char cmd[3] = { DDCCI_COMMAND_CAPS, 0x00, 0x00 };
> +       unsigned char *chunkbuf = kzalloc(35, GFP_KERNEL);
> +
> +       if (!chunkbuf)
> +               return -ENOMEM;
> +
> +       do {
> +               /* Send command */
> +               result = ddcci_write(client, addr, true, cmd, sizeof(cmd));
> +               if (result < 0)
> +                       goto err_free;
> +               msleep(delay);
> +               /* read result chunk */
> +               result = ddcci_read(client, addr, true, chunkbuf,
> +                                   (len > 32) ? 35 : len+3);
> +               if (result < 0)
> +                       goto err_free;
> +
> +               if (result > 0) {
> +                       /* check chunk header */
> +                       if (chunkbuf[0] != DDCCI_REPLY_CAPS) {
> +                               result = -EIO;
> +                               goto err_free;
> +                       }
> +                       if (chunkbuf[1] != cmd[1] || chunkbuf[2] != cmd[2]) {
> +                               result = -EIO;
> +                               goto err_free;
> +                       }
> +                       if (result < 3) {
> +                               result = -EIO;
> +                               goto err_free;
> +                       }
> +                       memcpy(buf, chunkbuf+3, min((unsigned int)result-3, len));
> +
> +                       counter++;
> +                       /* adjust offset, etc. */
> +                       offset += result-3;
> +                       len -= result-3;
> +                       buf += result-3;
> +                       cmd[1] = offset >> 8;
> +                       cmd[2] = offset & 0xFF;
> +                       /* Another superfluous read to make some devices happy... */
> +                       ddcci_read(client, addr, true, NULL, 2);
> +               }
> +       } while (result > 3 && counter < DDCCI_MAX_CAP_CHUNKS);
> +
> +       kfree(chunkbuf);
> +       return offset+result-3;
> +err_free:
> +       kfree(chunkbuf);
> +       return result;
> +}
> +
> +/*
> + * Request the device identification and put it into buf.
> + *
> + * Also detects all communication quirks and sets the corresponding flags
> + * in the ddcci_bus_drv_data structure associated with client.
> + *
> + * The identification command will fail on most DDC devices, as it is optional
> + * to support, but even the "failed" response suffices to detect quirks.
> + */
> +static int ddcci_identify_device(struct i2c_client *client, unsigned char addr,
> +                                unsigned char *buf, unsigned char len)
> +{
> +       int i, payload_len, ret = -ENODEV;
> +       unsigned long quirks;
> +       unsigned char cmd[1] = { DDCCI_COMMAND_ID };
> +       unsigned char *buffer;
> +       unsigned char xor = DDCCI_HOST_ADDR_EVEN;
> +       struct ddcci_bus_drv_data *bus_drv_data;
> +
> +       bus_drv_data = i2c_get_clientdata(client);
> +       quirks = bus_drv_data->quirks;
> +       buffer = bus_drv_data->recv_buffer;
> +
> +       /* Send Identification command */
> +       if (!(quirks & DDCCI_QUIRK_WRITE_BYTEWISE)) {
> +               ret = __ddcci_write_block(client, addr, buffer, true, cmd, sizeof(cmd));
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] writing identification command in block mode: %d\n",
> +                       client->addr << 1, addr, ret);
> +               if ((ret == -ENXIO)
> +                   && i2c_check_functionality(client->adapter,
> +                                              I2C_FUNC_SMBUS_WRITE_BYTE)) {
> +                       quirks |= DDCCI_QUIRK_WRITE_BYTEWISE;
> +                       dev_info(&client->dev,
> +                               "DDC/CI bus quirk detected: writes must be done bytewise\n");
> +                       /* Some devices need writing twice after a failed blockwise write */
> +                       __ddcci_write_bytewise(client, addr, true, cmd, sizeof(cmd));
> +                       msleep(delay);
> +               }
> +       }
> +       if (ret < 0 && (quirks & DDCCI_QUIRK_WRITE_BYTEWISE)) {
> +               ret = __ddcci_write_bytewise(client, addr, true, cmd, sizeof(cmd));
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] writing identification command in bytewise mode: %d\n",
> +                       client->addr << 1, addr, ret);
> +       }
> +       if (ret < 0)
> +               return -ENODEV;
> +
> +       /* Wait */
> +       msleep(delay);
> +
> +       /* Receive response */
> +       ret = i2c_master_recv(client, buffer, DDCCI_RECV_BUFFER_SIZE);
> +       if (ret < 0) {
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] receiving identification response resulted in errno %d\n",
> +                       client->addr << 1, addr, ret);
> +               return ret;
> +       }
> +
> +       if (ret == 0) {
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] no identification response received\n",
> +                       client->addr << 1, addr);
> +               return ret;
> +       }
> +
> +       /* Skip first byte if quirk already active */
> +       if (quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE && ret > 1) {
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] doubled first byte quirk in effect\n",
> +                       client->addr << 1, addr);
> +               ret--;
> +               buffer++;
> +       }
> +
> +       /* If answer too short (= incomplete) break out */
> +       if (ret < 3) {
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] identification response is too short (%d bytes)\n",
> +                       client->addr << 1, addr, ret);
> +               return -EIO;
> +       }
> +
> +       /* validate first byte */
> +       if (buffer[0] != addr) {
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] identification response: %*ph\n",
> +                       client->addr << 1, addr, (ret > 32 ? 32 : ret), buffer);
> +
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] identification response invalid (expected first byte %02x, got %02x)\n",
> +                       client->addr << 1, addr, addr, buffer[0]);
> +               return -ENODEV;
> +       }
> +
> +       /* Check if first byte is doubled (QUIRK_SKIP_FIRST_BYTE) */
> +       if (!(quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE)) {
> +               if (buffer[0] == buffer[1]) {
> +                       quirks |= DDCCI_QUIRK_SKIP_FIRST_BYTE;
> +                       dev_info(&client->dev,
> +                               "DDC/CI bus quirk detected: doubled first byte on read\n");
> +                       ret--;
> +                       buffer++;
> +                       if (ret < 3)
> +                               return -EIO;
> +               }
> +       }
> +
> +       /* validate second byte (protocol flag) */
> +       if ((buffer[1] & 0x80) != 0x80 && !(quirks & DDCCI_QUIRK_NO_PFLAG)) {
> +               dev_info(&client->dev,
> +                       "DDC/CI bus quirk detected: device omits protocol flag on responses\n");
> +               quirks |= DDCCI_QUIRK_NO_PFLAG;
> +       }
> +
> +       /* get and check payload length */
> +       payload_len = buffer[1] & 0x7F;
> +       if (3+payload_len > ret) {
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] identification response: %*ph ...\n",
> +                       client->addr << 1, addr, ret, buffer);
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] identification response was truncated (expected %d bytes, got %d)\n",
> +                       client->addr << 1, addr, 3+payload_len, ret);
> +               return -EBADMSG;
> +       }
> +
> +       dev_dbg(&client->dev,
> +               "[%02x:%02x] identification response: %*ph\n",
> +               client->addr << 1, addr, 3+payload_len, buffer);
> +
> +       /* calculate checksum */
> +       for (i = 0; i < 3+payload_len; i++)
> +               xor ^= buffer[i];
> +
> +       /* verify checksum */
> +       if (xor != 0) {
> +               dev_err(&client->dev,
> +                       "[%02x:%02x] invalid DDC/CI response, corrupted data - xor is 0x%02x, length 0x%02x\n",
> +                       client->addr << 1, addr, xor, payload_len);
> +               return -EBADMSG;
> +       }
> +
> +       /* save quirks */
> +       bus_drv_data->quirks = quirks;
> +
> +       /* return result */
> +       if (payload_len <= len) {
> +               ret = payload_len;
> +               memcpy(buf, &buffer[2], payload_len);
> +       } else {
> +               ret = -EMSGSIZE;
> +               memcpy(buf, &buffer[2], len);
> +       }
> +       return ret;
> +}
> +
> +/* Character device */
> +
> +/* Data structure for an open file handle */
> +struct ddcci_fp_data {
> +       struct ddcci_device *dev;
> +       bool exclusive;
> +       unsigned char buffer[129];
> +};
> +
> +/* Called when the character device is opened */
> +static int ddcci_cdev_open(struct inode *inode, struct file *filp)
> +{
> +       struct ddcci_device *dev = container_of(inode->i_cdev,
> +                                               struct ddcci_device, cdev);
> +       struct ddcci_fp_data *fp_data = NULL;
> +
> +       fp_data = kzalloc(sizeof(struct ddcci_fp_data), GFP_KERNEL);
> +
> +       if (!fp_data)
> +               return -ENOMEM;
> +
> +       fp_data->exclusive = filp->f_flags & O_EXCL;
> +
> +       if (fp_data->exclusive) {
> +               if (down_write_trylock(&dev->cdev_sem) == 0) {
> +                       kfree(fp_data);
> +                       return -EBUSY;
> +               }
> +       } else {
> +               if (down_read_trylock(&dev->cdev_sem) == 0) {
> +                       kfree(fp_data);
> +                       return -EBUSY;
> +               }
> +       }
> +
> +       fp_data->dev = dev;
> +       filp->private_data = fp_data;
> +
> +       return 0;
> +}
> +
> +/* Called when the character device is closed */
> +static int ddcci_cdev_close(struct inode *inode, struct file *filp)
> +{
> +       struct ddcci_fp_data *fp_data = filp->private_data;
> +       struct ddcci_device *dev = fp_data->dev;
> +
> +       if (fp_data->exclusive)
> +               up_write(&dev->cdev_sem);
> +       else
> +               up_read(&dev->cdev_sem);
> +
> +       filp->private_data = NULL;
> +       kfree(fp_data);
> +       return 0;
> +}
> +
> +/* Called when reading from the character device */
> +static ssize_t ddcci_cdev_read(struct file *filp, char __user *buffer,
> +                              size_t length, loff_t *offset)
> +{
> +       struct ddcci_fp_data *fp_data = filp->private_data;
> +       struct ddcci_device *dev = fp_data->dev;
> +       unsigned char *buf = fp_data->buffer;
> +       const bool nonblocking = (filp->f_flags & O_NONBLOCK) != 0;
> +       int ret = 0;
> +
> +       if ((filp->f_mode & FMODE_READ) == 0)
> +               return -EBADF;
> +
> +       /* Lock mutex */
> +       if (nonblocking) {
> +               if (down_trylock(&dev->bus_drv_data->sem))
> +                       return -EAGAIN;
> +       } else {
> +               if (down_interruptible(&dev->bus_drv_data->sem))
> +                       return -ERESTARTSYS;
> +       }
> +
> +       /* Execute read */
> +       ret = ddcci_read(dev->bus_drv_data->i2c_dev, dev->inner_addr, true, buf,
> +                        length);
> +
> +       if (ret > 0) {
> +               /* Copy data from user space */
> +               if (copy_to_user(buffer, buf, ret)) {
> +                       ret = -EFAULT;
> +                       goto out;
> +               }
> +       }
> +
> +out:
> +       up(&dev->bus_drv_data->sem);
> +       return ret;
> +}
> +
> +/* Called when writing to the character device */
> +static ssize_t ddcci_cdev_write(struct file *filp, const char __user *buffer,
> +                               size_t count, loff_t *offset)
> +{
> +       struct ddcci_fp_data *fp_data = filp->private_data;
> +       struct ddcci_device *dev = fp_data->dev;
> +       unsigned char *buf = fp_data->buffer;
> +       const bool nonblocking = (filp->f_flags & O_NONBLOCK) != 0;
> +       int ret = 0;
> +
> +       if ((filp->f_mode & FMODE_WRITE) == 0)
> +               return -EBADF;
> +
> +       if (count > 127)
> +               return -EINVAL;
> +
> +       /* Lock mutex */
> +       if (nonblocking) {
> +               if (down_trylock(&dev->bus_drv_data->sem))
> +                       return -EAGAIN;
> +       } else {
> +               if (down_interruptible(&dev->bus_drv_data->sem))
> +                       return -ERESTARTSYS;
> +       }
> +
> +       if (count > 0) {
> +               /* Copy data from user space */
> +               if (copy_from_user(buf, buffer, count)) {
> +                       ret = -EFAULT;
> +                       goto err_out;
> +               }
> +
> +               /* Execute write */
> +               ret = ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr,
> +                                 true, buf, count);
> +       }
> +
> +       if (ret >= 0) {
> +               msleep(delay);
> +               up(&dev->bus_drv_data->sem);
> +               return count;
> +       }
> +
> +err_out:
> +       up(&dev->bus_drv_data->sem);
> +       return ret;
> +}
> +
> +/* Called when seeking the character device */
> +static loff_t ddcci_cdev_seek(struct file *filp, loff_t offset, int anchor)
> +{
> +       return -EINVAL;
> +}
> +
> +static const struct file_operations ddcci_fops = {
> +       .owner = THIS_MODULE,
> +       .read = ddcci_cdev_read,
> +       .write = ddcci_cdev_write,
> +       .open = ddcci_cdev_open,
> +       .release = ddcci_cdev_close,
> +       .llseek = ddcci_cdev_seek
> +};
> +
> +/* Set up the character device for a DDC/CI device */
> +static int ddcci_setup_char_device(struct ddcci_device *device)
> +{
> +       int ret = -EINVAL;
> +
> +       /* Check if free minor exists */
> +       if (ddcci_cdev_next == ddcci_cdev_end) {
> +               dev_err(&device->dev, "no free major/minor\n");
> +               ret = -ENFILE;
> +               goto out;
> +       }
> +
> +       /* Initialize rwsem */
> +       init_rwsem(&device->cdev_sem);
> +
> +       /* Initialize character device node */
> +       cdev_init(&device->cdev, &ddcci_fops);
> +       device->cdev.owner = THIS_MODULE;
> +
> +       /* Publish char device */
> +       device->dev.devt = ddcci_cdev_next;
> +       ret = cdev_add(&device->cdev, ddcci_cdev_next, 1);
> +       if (ret) {
> +               device->dev.devt = 0;
> +               goto out;
> +       }
> +
> +       ddcci_cdev_next++;
> +out:
> +       return ret;
> +}
> +
> +/* sysfs attributes */
> +
> +static ssize_t ddcci_attr_capabilities_show(struct device *dev,
> +                                           struct device_attribute *attr,
> +                                           char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +       size_t len;
> +
> +       if (likely(device != NULL)) {
> +               len = device->capabilities_len;
> +               if (unlikely(len > PAGE_SIZE))
> +                       len = PAGE_SIZE;
> +               if (len == 0) {
> +                       ret = len;
> +               } else {
> +                       memcpy(buf, device->capabilities, len);
> +                       if (likely(len < PAGE_SIZE)) {
> +                               buf[len] = '\n';
> +                               ret = len+1;
> +                       }
> +               }
> +       }
> +
> +       return ret;
> +}
> +
> +static ssize_t ddcci_attr_prot_show(struct device *dev,
> +                                   struct device_attribute *attr, char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +       size_t len;
> +
> +       if (likely(device != NULL)) {
> +               len = strnlen(device->prot, sizeof(device->prot));
> +               strncpy(buf, device->prot, PAGE_SIZE);
> +               if (len == 0) {
> +                       ret = len;
> +               } else if (likely(len < PAGE_SIZE)) {
> +                       buf[len] = '\n';
> +                       ret = len+1;
> +               } else {
> +                       ret = PAGE_SIZE;
> +               }
> +       }
> +       return ret;
> +}
> +
> +static ssize_t ddcci_attr_type_show(struct device *dev,
> +                                   struct device_attribute *attr, char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +       size_t len;
> +
> +       if (likely(device != NULL)) {
> +               len = strnlen(device->type, sizeof(device->type));
> +               strncpy(buf, device->type, PAGE_SIZE);
> +               if (len == 0) {
> +                       ret = len;
> +               } else if (likely(len < PAGE_SIZE)) {
> +                       buf[len] = '\n';
> +                       ret = len+1;
> +               } else {
> +                       ret = PAGE_SIZE;
> +               }
> +       }
> +       return ret;
> +}
> +
> +static ssize_t ddcci_attr_model_show(struct device *dev,
> +                                    struct device_attribute *attr, char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +       size_t len;
> +
> +       if (likely(device != NULL)) {
> +               len = strnlen(device->model, sizeof(device->model));
> +               strncpy(buf, device->model, PAGE_SIZE);
> +               if (len == 0) {
> +                       ret = len;
> +               } else if (likely(len < PAGE_SIZE)) {
> +                       buf[len] = '\n';
> +                       ret = len+1;
> +               } else {
> +                       ret = PAGE_SIZE;
> +               }
> +       }
> +       return ret;
> +}
> +
> +static ssize_t ddcci_attr_vendor_show(struct device *dev,
> +                                     struct device_attribute *attr, char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +       size_t len;
> +
> +       if (likely(device != NULL)) {
> +               len = strnlen(device->vendor, sizeof(device->vendor));
> +               strncpy(buf, device->vendor, PAGE_SIZE);
> +               if (len == 0) {
> +                       ret = len;
> +               } else if (likely(len < PAGE_SIZE)) {
> +                       buf[len] = '\n';
> +                       ret = len+1;
> +               } else {
> +                       ret = PAGE_SIZE;
> +               }
> +       }
> +       return ret;
> +}
> +
> +static ssize_t ddcci_attr_module_show(struct device *dev,
> +                                     struct device_attribute *attr, char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +       size_t len;
> +
> +       if (likely(device != NULL)) {
> +               len = strnlen(device->module, sizeof(device->module));
> +               strncpy(buf, device->module, PAGE_SIZE);
> +               if (len == 0) {
> +                       ret = len;
> +               } else if (likely(len < PAGE_SIZE)) {
> +                       buf[len] = '\n';
> +                       ret = len+1;
> +               } else {
> +                       ret = PAGE_SIZE;
> +               }
> +       }
> +       return ret;
> +}
> +
> +static ssize_t ddcci_attr_serial_show(struct device *dev,
> +                                     struct device_attribute *attr, char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +
> +       if (likely(device != NULL))
> +               ret = scnprintf(buf, PAGE_SIZE, "%d\n", device->device_number);
> +
> +       return ret;
> +}
> +
> +static ssize_t ddcci_attr_modalias_show(struct device *dev,
> +                                     struct device_attribute *attr, char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +       char model[ARRAY_SIZE(device->model)];
> +       char vendor[ARRAY_SIZE(device->model)];
> +       char module[ARRAY_SIZE(device->model)];
> +
> +       if (likely(device != NULL)) {
> +               memcpy(model, device->model, sizeof(model));
> +               memcpy(vendor, device->vendor, sizeof(vendor));
> +               memcpy(module, device->module, sizeof(module));
> +               ddcci_modalias_clean(model, sizeof(model), '_');
> +               ddcci_modalias_clean(vendor, sizeof(vendor), '_');
> +               ddcci_modalias_clean(module, sizeof(module), '_');
> +
> +               ret = scnprintf(buf, PAGE_SIZE, "%s%s-%s-%s-%s-%s\n",
> +                       DDCCI_MODULE_PREFIX,
> +                       device->prot,
> +                       device->type,
> +                       model,
> +                       vendor,
> +                       module
> +               );
> +       }
> +       return ret;
> +}
> +
> +static DEVICE_ATTR(capabilities, S_IRUGO, ddcci_attr_capabilities_show, NULL);
> +static DEVICE_ATTR(idProt, S_IRUGO, ddcci_attr_prot_show, NULL);
> +static DEVICE_ATTR(idType, S_IRUGO, ddcci_attr_type_show, NULL);
> +static DEVICE_ATTR(idModel, S_IRUGO, ddcci_attr_model_show, NULL);
> +static DEVICE_ATTR(idVendor, S_IRUGO, ddcci_attr_vendor_show, NULL);
> +static DEVICE_ATTR(idModule, S_IRUGO, ddcci_attr_module_show, NULL);
> +static DEVICE_ATTR(idSerial, S_IRUGO, ddcci_attr_serial_show, NULL);
> +static DEVICE_ATTR(modalias, S_IRUGO, ddcci_attr_modalias_show, NULL);
> +
> +static struct attribute *ddcci_char_device_attrs[] = {
> +       &dev_attr_capabilities.attr,
> +       &dev_attr_idProt.attr,
> +       &dev_attr_idType.attr,
> +       &dev_attr_idModel.attr,
> +       &dev_attr_idVendor.attr,
> +       &dev_attr_idModule.attr,
> +       &dev_attr_idSerial.attr,
> +       &dev_attr_modalias.attr,
> +       NULL,
> +};
> +ATTRIBUTE_GROUPS(ddcci_char_device);
> +
> +/* DDC/CI bus */
> +
> +static int ddcci_device_uevent(struct device *dev, struct kobj_uevent_env *env)
> +{
> +       struct ddcci_device     *device = to_ddcci_device(dev);
> +       char model[ARRAY_SIZE(device->model)];
> +       char vendor[ARRAY_SIZE(device->vendor)];
> +       char module[ARRAY_SIZE(device->module)];
> +
> +       memcpy(model, device->model, sizeof(model));
> +       memcpy(vendor, device->vendor, sizeof(vendor));
> +       memcpy(module, device->module, sizeof(module));
> +       ddcci_modalias_clean(model, sizeof(model), '_');
> +       ddcci_modalias_clean(vendor, sizeof(vendor), '_');
> +       ddcci_modalias_clean(module, sizeof(module), '_');
> +
> +       if (add_uevent_var(env, "MODALIAS=%s%s-%s-%s-%s-%s",
> +                          DDCCI_MODULE_PREFIX,
> +                          device->prot,
> +                          device->type,
> +                          model,
> +                          vendor,
> +                          module
> +               ))
> +               return -ENOMEM;
> +
> +       if (device->prot[0])
> +               if (add_uevent_var(env, "DDCCI_PROT=%s", device->prot))
> +                       return -ENOMEM;
> +
> +       if (device->type[0])
> +               if (add_uevent_var(env, "DDCCI_TYPE=%s", device->type))
> +                       return -ENOMEM;
> +
> +       if (device->model[0])
> +               if (add_uevent_var(env, "DDCCI_MODEL=%s", device->model))
> +                       return -ENOMEM;
> +
> +       if (device->vendor[0]) {
> +               if (add_uevent_var(env, "DDCCI_VENDOR=%s", device->vendor))
> +                       return -ENOMEM;
> +
> +               if (add_uevent_var(env, "DDCCI_MODULE=%s", device->module))
> +                       return -ENOMEM;
> +
> +               if (add_uevent_var(env, "DDCCI_UNIQ=%d", device->device_number))
> +                       return -ENOMEM;
> +       }
> +
> +       return 0;
> +}
> +
> +static void ddcci_device_release(struct device *dev)
> +{
> +       struct ddcci_device *device = to_ddcci_device(dev);
> +       struct ddcci_driver *driver;
> +
> +       /* Notify driver */
> +       if (dev->driver) {
> +               driver = to_ddcci_driver(dev->driver);
> +               if (driver->remove)
> +                       driver->remove(device);
> +       }
> +
> +       /* Teardown chardev */
> +       if (dev->devt) {
> +               down(&core_lock);
> +               if (device->cdev.dev == ddcci_cdev_next-1)
> +                       ddcci_cdev_next--;
> +               cdev_del(&device->cdev);
> +               up(&core_lock);
> +       }
> +
> +       /* Free capability string */
> +       if (device->capabilities) {
> +               device->capabilities_len = 0;
> +               kfree(device->capabilities);
> +       }
> +       /* Free device */
> +       kfree(device);
> +}
> +
> +static char *ddcci_devnode(struct device *dev,
> +                        umode_t *mode, kuid_t *uid, kgid_t *gid)
> +{
> +       struct ddcci_device *device;
> +
> +       device = to_ddcci_device(dev);
> +       return kasprintf(GFP_KERNEL, "bus/ddcci/%d/display",
> +                        device->i2c_client->adapter->nr);
> +}
> +
> +static char *ddcci_dependent_devnode(struct device *dev,
> +                        umode_t *mode, kuid_t *uid, kgid_t *gid)
> +{
> +       struct ddcci_device *device;
> +
> +       device = to_ddcci_device(dev);
> +       if (device->flags & DDCCI_FLAG_EXTERNAL) {
> +               if (device->outer_addr == device->inner_addr)
> +                       return kasprintf(GFP_KERNEL, "bus/ddcci/%d/e%02x",
> +                                        device->i2c_client->adapter->nr,
> +                                        device->outer_addr);
> +               else
> +                       return kasprintf(GFP_KERNEL, "bus/ddcci/%d/e%02x%02x",
> +                                        device->i2c_client->adapter->nr,
> +                                        device->outer_addr, device->inner_addr);
> +       } else {
> +               return kasprintf(GFP_KERNEL, "bus/ddcci/%d/i%02x",
> +                                device->i2c_client->adapter->nr,
> +                                device->inner_addr);
> +       }
> +}
> +
> +/* Device type for main DDC/CI devices*/
> +static struct device_type ddcci_device_type = {
> +       .name   = "ddcci-device",
> +       .uevent         = ddcci_device_uevent,
> +       .groups         = ddcci_char_device_groups,
> +       .release        = ddcci_device_release,
> +       .devnode        = ddcci_devnode
> +};
> +
> +/* Device type for dependent DDC/CI devices*/
> +static struct device_type ddcci_dependent_type = {
> +       .name   = "ddcci-dependent-device",
> +       .uevent         = ddcci_device_uevent,
> +       .groups         = ddcci_char_device_groups,
> +       .release        = ddcci_device_release,
> +       .devnode        = ddcci_dependent_devnode
> +};
> +
> +/**
> + * ddcci_verify_device - return parameter as ddcci_device, or NULL
> + * @dev: device, probably from some driver model iterator
> + */
> +struct ddcci_device *ddcci_verify_device(struct device *dev)
> +{
> +       if (unlikely(!dev))
> +               return NULL;
> +       return (dev->type == &ddcci_device_type
> +               || dev->type == &ddcci_dependent_type)
> +                       ? to_ddcci_device(dev)
> +                       : NULL;
> +}
> +EXPORT_SYMBOL(ddcci_verify_device);
> +
> +/**
> + * ddcci_quirks - Get quirks for DDC/CI device
> + * @dev: Target DDC/CI device
> + */
> +unsigned long ddcci_quirks(struct ddcci_device *dev)
> +{
> +       if (unlikely(WARN_ON(!dev)))
> +               return ~0L;
> +       if (unlikely(WARN_ON(!dev->bus_drv_data)))
> +               return ~0L;
> +       return dev->bus_drv_data->quirks;
> +}
> +EXPORT_SYMBOL(ddcci_quirks);
> +
> +/**
> + * ddcci_register_driver - register DDC/CI driver
> + * @owner: the owning module
> + * @driver: the driver to register
> + */
> +int ddcci_register_driver(struct module *owner, struct ddcci_driver *driver)
> +{
> +       int ret;
> +
> +       /* Can't register until after driver model init */
> +       if (unlikely(WARN_ON(!ddcci_bus_type.p)))
> +               return -EAGAIN;
> +
> +       pr_debug("registering driver [%s]\n", driver->driver.name);
> +
> +       /* add the driver to the list of ddcci drivers in the driver core */
> +       driver->driver.owner = owner;
> +       driver->driver.bus = &ddcci_bus_type;
> +
> +       /* When registration returns, the driver core
> +        * will have called probe() for all matching-but-unbound devices.
> +        */
> +       ret = driver_register(&driver->driver);
> +       if (ret)
> +               return ret;
> +
> +       pr_debug("driver [%s] registered\n", driver->driver.name);
> +
> +       return 0;
> +}
> +EXPORT_SYMBOL(ddcci_register_driver);
> +
> +/**
> + * ddcci_del_driver - unregister DDC/CI driver
> + * @driver: the driver being unregistered
> + */
> +void ddcci_del_driver(struct ddcci_driver *driver)
> +{
> +       driver_unregister(&driver->driver);
> +       pr_debug("driver [%s] unregistered\n", driver->driver.name);
> +}
> +EXPORT_SYMBOL(ddcci_del_driver);
> +
> +/**
> + * ddcci_device_write - Write a message to a DDC/CI device
> + * @dev: Target DDC/CI device
> + * @p_flag: Protocol flag, true for standard control messages
> + * @data: Data that will be written to the device
> + * @length: How many bytes to write
> + *
> + * Writes the message to the device and sleeps (see module parameter 'delay')
> + */
> +int ddcci_device_write(struct ddcci_device *dev, bool p_flag,
> +                      unsigned char *data, unsigned char length)
> +{
> +       int ret;
> +
> +       if (down_interruptible(&dev->bus_drv_data->sem))
> +               return -EAGAIN;
> +
> +       ret = ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, data, length);
> +       msleep(delay);
> +       up(&dev->bus_drv_data->sem);
> +       return ret;
> +}
> +EXPORT_SYMBOL(ddcci_device_write);
> +
> +/**
> + * ddcci_device_read - Read a response from a DDC/CI device
> + * @dev: Target DDC/CI device
> + * @p_flag: Protocol flag, must match the corresponding write
> + * @buffer: Where to store data read from the device
> + * @length: Buffer size
> + */
> +int ddcci_device_read(struct ddcci_device *dev, bool p_flag,
> +                     unsigned char *buffer, unsigned char length)
> +{
> +       int ret;
> +
> +       if (down_interruptible(&dev->bus_drv_data->sem))
> +               return -EAGAIN;
> +
> +       ret = ddcci_read(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, buffer, length);
> +       up(&dev->bus_drv_data->sem);
> +       return ret;
> +}
> +EXPORT_SYMBOL(ddcci_device_read);
> +
> +/**
> + * ddcci_device_writeread - Write a message to a device and read the response
> + * @dev: Target DDC/CI device
> + * @p_flag: Protocol flag, true for standard control messages
> + * @buffer: Buffer used for write and read
> + * @length: How many bytes to write
> + * @maxlength: Buffer size on read
> + *
> + * Writing, sleeping and reading are done without releasing the DDC/CI bus.
> + * This provides atomicity in respect to other DDC/CI accesses on the same bus.
> + */
> +int ddcci_device_writeread(struct ddcci_device *dev, bool p_flag,
> +                          unsigned char *buffer, unsigned char length,
> +                          unsigned char maxlength)
> +{
> +       int ret;
> +
> +       if (down_interruptible(&dev->bus_drv_data->sem))
> +               return -EAGAIN;
> +
> +       ret = ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, buffer, length);
> +       if (ret < 0)
> +               goto err;
> +       msleep(delay);
> +       ret = ddcci_read(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, buffer, maxlength);
> +err:
> +       up(&dev->bus_drv_data->sem);
> +       return ret;
> +}
> +EXPORT_SYMBOL(ddcci_device_writeread);
> +
> +#define IS_ANY_ID(x) (((x)[0] == -1) && ((x)[7] == -1))
> +
> +/* Check if any device id in the array matches the device and return the matching id */
> +static const struct ddcci_device_id *ddcci_match_id(const struct ddcci_device_id *id,
> +                                                   const struct ddcci_device *device)
> +{
> +       while (id->prot[0] || id->type[0] || id->model[0] || id->vendor[0] || id->module[0]) {
> +               if ((IS_ANY_ID(id->prot) || (strcmp(device->prot, id->prot) == 0))
> +                && (IS_ANY_ID(id->type) || (strcmp(device->type, id->type) == 0))
> +                && (IS_ANY_ID(id->model) || (strcmp(device->model, id->model) == 0))
> +                && (IS_ANY_ID(id->vendor) || (strcmp(device->vendor, id->vendor) == 0))
> +                && (IS_ANY_ID(id->module) || (strcmp(device->module, id->module) == 0))) {
> +                       return id;
> +               }
> +               id++;
> +       }
> +       return NULL;
> +}
> +
> +static int ddcci_device_match(struct device *dev, struct device_driver *drv)
> +{
> +       struct ddcci_device     *device = ddcci_verify_device(dev);
> +       struct ddcci_driver     *driver;
> +
> +       if (!device)
> +               return 0;
> +
> +       driver = to_ddcci_driver(drv);
> +       /* match on an id table if there is one */
> +       if (driver->id_table)
> +               return ddcci_match_id(driver->id_table, device) != NULL;
> +
> +       return 0;
> +}
> +
> +static int ddcci_device_probe(struct device *dev)
> +{
> +       struct ddcci_device     *device = ddcci_verify_device(dev);
> +       struct ddcci_driver     *driver;
> +       const struct ddcci_device_id *id;
> +       int ret = 0;
> +
> +       if (!device)
> +               return -EINVAL;
> +       driver = to_ddcci_driver(dev->driver);
> +
> +       id = ddcci_match_id(driver->id_table, device);
> +       if (!id)
> +               return -ENODEV;
> +
> +       if (driver->probe)
> +               ret = driver->probe(device, id);
> +
> +       return ret;
> +}
> +
> +static int ddcci_device_remove(struct device *dev)
> +{
> +       struct ddcci_device     *device = ddcci_verify_device(dev);
> +       struct ddcci_driver     *driver;
> +       int ret = 0;
> +
> +       if (!device)
> +               return -EINVAL;
> +       driver = to_ddcci_driver(dev->driver);
> +
> +       if (driver->remove)
> +               ret = driver->remove(device);
> +
> +       return ret;
> +}
> +
> +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0)
> +static void ddcci_device_remove_void(struct device *dev)
> +{
> +       ddcci_device_remove(dev);
> +}
> +#endif
> +
> +/**
> + * DDCCI bus type structure
> + */
> +struct bus_type ddcci_bus_type = {
> +       .name           = "ddcci",
> +       .match          = ddcci_device_match,
> +       .probe          = ddcci_device_probe,
> +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0)
> +       .remove         = ddcci_device_remove_void
> +#else
> +       .remove         = ddcci_device_remove
> +#endif
> +};
> +
> +/* Main I2C driver */
> +
> +/* Get a pointer to the closing parenthesis */
> +static char *ddcci_capstr_tok(const char *s, int depth)
> +{
> +       const char *ptr = s;
> +       char *end;
> +
> +       if (s == NULL || s[0] == '\0')
> +               return NULL;
> +
> +       while ((end = strpbrk(ptr, "()"))) {
> +               if (!end || depth == INT_MAX)
> +                       return NULL;
> +               if (*end == '(')
> +                       depth++;
> +               else if (depth > 0)
> +                       depth--;
> +               else
> +                       break;
> +               ptr = end+1;
> +       }
> +       return end;
> +}
> +
> +/**
> + * ddcci_find_capstr_item - Search capability string for a tag
> + * @capabilities: Capability string to search
> + * @tag: Tag to find
> + * @length: Buffer for the length of the found tag value (optional)
> + *
> + * Return a pointer to the start of the tag value (directly after the '(') on
> + * success and write the length of the value (excluding the ')') into `length`.
> + *
> + * If the tag is not found or another error occurs, an ERR_PTR is returned
> + * and `length` stays untouched.
> + */
> +const char *ddcci_find_capstr_item(const char * capabilities,
> +                                  const char * __restrict tag,
> +                                  size_t *length)
> +{
> +       const char *src = capabilities, *ptr;
> +       ptrdiff_t len;
> +       int taglen = strnlen(tag, 1024);
> +
> +       /* Check length of requested tag */
> +       if (unlikely(taglen <= 0 || taglen > 1023))
> +               return ERR_PTR(-EINVAL);
> +
> +       /* Find tag */
> +       while (src && (strncmp(src+1, tag, taglen) != 0 || src[1+taglen] != '('))
> +               src = ddcci_capstr_tok(src+1, -1);
> +       if (!src || src[0] == '\0')
> +               return ERR_PTR(-ENOENT);
> +
> +       /* Locate end of value */
> +       src += taglen+2;
> +       ptr = ddcci_capstr_tok(src, 0);
> +       if (unlikely(!ptr))
> +               return ERR_PTR(-EOVERFLOW);
> +
> +       /* Check length of tag data */
> +       len = ptr-src;
> +       if (unlikely(len < 0 || len > 65535))
> +               return ERR_PTR(-EMSGSIZE);
> +
> +       /* Return pointer and length */
> +       if (likely(length != NULL))
> +               *length = (size_t)len;
> +       return src;
> +}
> +EXPORT_SYMBOL(ddcci_find_capstr_item);
> +
> +/* Search the capability string for a tag and copy the value to dest */
> +static int ddcci_cpy_capstr_item(char *dest, const char *src,
> +                                 const char * __restrict tag, size_t maxlen)
> +{
> +       const char *ptr;
> +       size_t len;
> +
> +       /* Find tag */
> +       ptr = ddcci_find_capstr_item(src, tag, &len);
> +       if (IS_ERR(ptr)) {
> +               return PTR_ERR(ptr);
> +       }
> +
> +       /* Copy value */
> +       memcpy(dest, ptr, min(len, maxlen));
> +       return 0;
> +}
> +
> +/* Fill fields in device by parsing the capability string */
> +static int ddcci_parse_capstring(struct ddcci_device *device)
> +{
> +       const char *capstr = device->capabilities;
> +       int ret = 0;
> +
> +       if (!capstr)
> +               return -EINVAL;
> +
> +       /* capability string start with a paren */
> +       if (capstr[0] != '(')
> +               return -EINVAL;
> +
> +       /* get prot(...) */
> +       ret = ddcci_cpy_capstr_item(device->prot, capstr, "prot", sizeof(device->prot)-1);
> +       if (ret) {
> +               if (ret == -ENOENT) {
> +                       dev_warn(&device->dev, "malformed capability string: no protocol tag");
> +                       memset(device->prot, 0, sizeof(device->prot)-1);
> +               } else {
> +                       return ret;
> +               }
> +       }
> +
> +       /* get type(...) */
> +       ret = ddcci_cpy_capstr_item(device->type, capstr, "type", sizeof(device->type)-1);
> +       if (ret) {
> +               if (ret == -ENOENT) {
> +                       dev_warn(&device->dev, "malformed capability string: no type tag");
> +                       memset(device->type, 0, sizeof(device->type)-1);
> +               } else {
> +                       return ret;
> +               }
> +       }
> +
> +       /* and then model(...) */
> +       ret = ddcci_cpy_capstr_item(device->model, capstr, "model", sizeof(device->model)-1);
> +       if (ret) {
> +               if (ret == -ENOENT) {
> +                       dev_warn(&device->dev, "malformed capability string: no model tag");
> +                       memset(device->model, 0, sizeof(device->model)-1);
> +               } else {
> +                       return ret;
> +               }
> +       }
> +
> +       /* if there is no protocol tag */
> +       if (!device->prot[0]) {
> +               /* and no type tag: give up. */
> +               if (!device->type[0])
> +                       return -ENOENT;
> +
> +               /* Assume protocol "monitor" if type is "LCD" or "CRT" */
> +               if (strncasecmp(device->type, "LCD", sizeof(device->type)-1) == 0
> +                || strncasecmp(device->type, "CRT", sizeof(device->type)-1) == 0) {
> +                       memcpy(device->prot, "monitor", 7);
> +               }
> +       }
> +
> +       /* skip the rest for now */
> +
> +       return 0;
> +}
> +
> +/* Probe for a device on an inner address and create a ddcci_device for it */
> +static int ddcci_detect_device(struct i2c_client *client, unsigned char addr,
> +                              int dependent)
> +{
> +       int ret;
> +       unsigned char outer_addr = client->addr << 1;
> +       unsigned char *buffer = NULL;
> +       struct ddcci_bus_drv_data *drv_data = i2c_get_clientdata(client);
> +       struct ddcci_device *device = NULL;
> +
> +       down(&drv_data->sem);
> +
> +       /* Allocate buffer big enough for any capability string */
> +       buffer = kmalloc(16384, GFP_KERNEL);
> +       if (!buffer) {
> +               ret = -ENOMEM;
> +               goto err_end;
> +       }
> +
> +       /* Allocate device struct */
> +       device = kzalloc(sizeof(struct ddcci_device), GFP_KERNEL);
> +       if (!device) {
> +               ret = -ENOMEM;
> +               goto err_end;
> +       }
> +
> +       /* Initialize device */
> +       device_initialize(&device->dev);
> +       device->dev.parent = &client->dev;
> +       device->dev.bus = &ddcci_bus_type;
> +       device->outer_addr = outer_addr;
> +       device->inner_addr = addr;
> +       device->bus_drv_data = drv_data;
> +       device->i2c_client = client;
> +
> +       if (!dependent) {
> +               device->dev.type = &ddcci_device_type;
> +               ret = dev_set_name(&device->dev, "ddcci%d", client->adapter->nr);
> +       } else if (outer_addr == dependent) {
> +               /* Internal dependent device */
> +               device->dev.type = &ddcci_dependent_type;
> +               device->flags = DDCCI_FLAG_DEPENDENT;
> +               ret = dev_set_name(&device->dev, "ddcci%di%02x", client->adapter->nr, addr);
> +       } else if (outer_addr == addr) {
> +               /* External dependent device */
> +               device->dev.type = &ddcci_dependent_type;
> +               device->flags = DDCCI_FLAG_DEPENDENT | DDCCI_FLAG_EXTERNAL;
> +               ret = dev_set_name(&device->dev, "ddcci%de%02x", client->adapter->nr, addr);
> +       } else {
> +               /* Dependent device of external dependent device
> +                  Just in case something like this exists */
> +               device->dev.type = &ddcci_dependent_type;
> +               device->flags = DDCCI_FLAG_DEPENDENT | DDCCI_FLAG_EXTERNAL;
> +               ret = dev_set_name(&device->dev, "ddcci%de%02x%02x", client->adapter->nr, outer_addr, addr);
> +       }
> +
> +       if (ret)
> +               goto err_free;
> +
> +       /* Read identification and check for quirks */
> +       ret = ddcci_identify_device(client, addr, buffer, 29);
> +       if (ret < 0) {
> +               if (!dependent && (ret == -EBADMSG || ret == -EMSGSIZE)) {
> +                       dev_warn(&device->dev, "DDC/CI main device sent broken response on identification. Trying to detect solely based on capability information.\n");
> +               } else {
> +                       goto err_free;
> +               }
> +       }
> +
> +       if (ret == 29 && buffer[0] == DDCCI_REPLY_ID) {
> +               memcpy(device->vendor, &buffer[7], 8);
> +               memcpy(device->module, &buffer[17], 8);
> +               device->device_number = be32_to_cpu(*(__force __be32 *)&buffer[18]);
> +       }
> +
> +       /* Read capabilities */
> +       ret = ddcci_get_caps(client, addr, buffer, 16384);
> +       if (ret > 0) {
> +               /* Fixup unparenthesized capability strings, but only if the first
> +                  character is an ascii lower case letter.
> +                  This should still allow an early exit for completely garbled
> +                  data but helps detecting devices where only the parentheses are
> +                  missing, as the second char must be the first character of a
> +                  keyword. */
> +               if (ret > 2 && buffer[0] >= 'a' && buffer[0] <= 'z') {
> +                       dev_err(&device->dev, "DDC/CI device quirk detected: unparenthesized capability string\n");
> +                       device->capabilities = kzalloc(ret+3, GFP_KERNEL);
> +                       if (!device->capabilities) {
> +                               ret = -ENOMEM;
> +                               goto err_free;
> +                       }
> +                       device->capabilities_len = ret+2;
> +                       memcpy(&(device->capabilities[1]), buffer, ret);
> +                       device->capabilities[0] = '(';
> +                       device->capabilities[ret+1] = ')';
> +               } else {
> +                       /* Standard case: simply copy the received string */
> +                       device->capabilities = kzalloc(ret+1, GFP_KERNEL);
> +                       if (!device->capabilities) {
> +                               ret = -ENOMEM;
> +                               goto err_free;
> +                       }
> +                       device->capabilities_len = ret;
> +                       memcpy(device->capabilities, buffer, ret);
> +               }
> +
> +               ret = ddcci_parse_capstring(device);
> +               if (ret) {
> +                       dev_err(&device->dev, "malformed capability string: \"%s\" errno %d\n", device->capabilities, ret);
> +                       ret = -EINVAL;
> +                       goto err_free;
> +               }
> +       }
> +
> +       /* Found a device if either identification or capabilities succeeded */
> +       if (!device->capabilities && device->vendor[0] == '\0') {
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] got neither valid identification nor capability data\n",
> +                       client->addr << 1, addr);
> +               ret = -ENODEV;
> +               goto err_free;
> +       }
> +
> +       /* Setup chardev */
> +       down(&core_lock);
> +       ret = ddcci_setup_char_device(device);
> +       up(&core_lock);
> +       if (ret)
> +               goto err_free;
> +
> +       /* Release semaphore and add device to the tree */
> +       up(&drv_data->sem);
> +       pr_debug("found device at %d:%02x:%02x\n", client->adapter->nr, outer_addr, addr);
> +       ret = device_add(&device->dev);
> +       if (ret)
> +               goto err_free;
> +
> +       goto end;
> +err_free:
> +       put_device(&device->dev);
> +err_end:
> +       up(&drv_data->sem);
> +end:
> +       kfree(buffer);
> +       return ret;
> +}
> +
> +/* I2C detect function: check if a main or external dependent device exists */
> +static int ddcci_detect(struct i2c_client *client, struct i2c_board_info *info)
> +{
> +       int ret;
> +       unsigned char outer_addr;
> +       unsigned char inner_addr;
> +       unsigned char buf[32];
> +       unsigned char cmd_id[1] = { DDCCI_COMMAND_ID };
> +       unsigned char cmd_caps[3] = { DDCCI_COMMAND_CAPS, 0x00, 0x00};
> +       unsigned char *cmd;
> +       unsigned int cmd_len;
> +
> +       /* Check for i2c_master_* functionality */
> +       if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
> +               pr_debug("i2c adapter %d unsuitable: no i2c_master functionality\n", client->adapter->nr);
> +               return -ENODEV;
> +       }
> +
> +       /* send Capabilities Request (for main) or Identification Request command (for dependent devices) */
> +       outer_addr = client->addr << 1;
> +       inner_addr = (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? DDCCI_HOST_ADDR_ODD : outer_addr | 1;
> +       cmd = (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? cmd_caps : cmd_id;
> +       cmd_len = (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? sizeof(cmd_caps) : sizeof(cmd_id);
> +       pr_debug("detecting %d:%02x\n", client->adapter->nr, outer_addr);
> +
> +       ret = __ddcci_write_block(client, inner_addr, buf, true, cmd, cmd_len);
> +
> +       if (ret == -ENXIO || ret == -EIO) {
> +               if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_WRITE_BYTE)) {
> +                       pr_debug("i2c write failed with ENXIO or EIO but bytewise writing is not supported\n");
> +                       return -ENODEV;
> +               }
> +               pr_debug("i2c write failed with ENXIO or EIO, trying bytewise writing\n");
> +               ret = __ddcci_write_bytewise(client, inner_addr, true, cmd, cmd_len);
> +               if (ret == 0) {
> +                       msleep(delay);
> +                       ret = __ddcci_write_bytewise(client, inner_addr, true, cmd, cmd_len);
> +               }
> +       }
> +
> +       if (ret < 0)
> +               return -ENODEV;
> +
> +       /* wait for device */
> +       msleep(delay);
> +       /* receive answer */
> +       ret = i2c_master_recv(client, buf, 32);
> +       if (ret < 3) {
> +               pr_debug("detection failed: no answer\n");
> +               return -ENODEV;
> +       }
> +
> +       /* check response starts with outer addr */
> +       if (buf[0] != outer_addr) {
> +               pr_debug("detection failed: invalid %s response (%02x != %02x)\n", (cmd == cmd_id) ? "identification" : "capabilities", buf[0], outer_addr);
> +               pr_debug("received message was %*ph \n", ret, buf);
> +               return -ENODEV;
> +       }
> +
> +       pr_debug("detected %d:%02x\n", client->adapter->nr, outer_addr);
> +
> +       /* set device type */
> +       strlcpy(info->type, (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? "ddcci" : "ddcci-dependent", I2C_NAME_SIZE);
> +
> +       return 0;
> +}
> +
> +/* I2C probe function */
> +static int ddcci_probe(struct i2c_client *client, const struct i2c_device_id *id)
> +{
> +       int i, ret = -ENODEV, tmp;
> +       unsigned char main_addr, addr;
> +       struct ddcci_bus_drv_data *drv_data;
> +
> +       /* Initialize driver data structure */
> +       drv_data = devm_kzalloc(&client->dev, sizeof(struct ddcci_bus_drv_data), GFP_KERNEL);
> +       if (!drv_data)
> +               return -ENOMEM;
> +       drv_data->i2c_dev = client;
> +       sema_init(&drv_data->sem, 1);
> +
> +       /* Set i2c client data */
> +       i2c_set_clientdata(client, drv_data);
> +
> +       if (id->driver_data == 0) {
> +               /* Core device, probe at 0x6E */
> +               main_addr = DDCCI_DEFAULT_DEVICE_ADDR;
> +               dev_dbg(&client->dev, "probing core device [%02x]\n",
> +                       client->addr << 1);
> +               ret = ddcci_detect_device(client, main_addr, 0);
> +               if (ret) {
> +                       dev_info(&client->dev, "core device [%02x] probe failed: %d\n",
> +                                client->addr << 1, ret);
> +                       if (ret == -EIO)
> +                               ret = -ENODEV;
> +                       goto err_free;
> +               }
> +
> +               /* Detect internal dependent devices */
> +               dev_dbg(&client->dev, "probing internal dependent devices\n");
> +               for (i = 0; i < autoprobe_addr_count; ++i) {
> +                       addr = (unsigned short)autoprobe_addrs[i];
> +                       if ((addr & 1) == 0 && addr != main_addr) {
> +                               tmp = ddcci_detect_device(client, addr, main_addr);
> +                               if (tmp < 0 && tmp != -ENODEV) {
> +                                       dev_info(&client->dev, "internal dependent device [%02x:%02x] probe failed: %d\n",
> +                                                client->addr << 1, addr, ret);
> +                               }
> +                       }
> +               }
> +       } else if (id->driver_data == 1) {
> +               /* External dependent device */
> +               main_addr = client->addr << 1;
> +               dev_dbg(&client->dev, "probing external dependent device [%02x]\n", main_addr);
> +               ret = ddcci_detect_device(client, main_addr, -1);
> +               if (ret) {
> +                       dev_info(&client->dev, "external dependent device [%02x] probe failed: %d\n",
> +                                main_addr, ret);
> +                       if (ret == -EIO)
> +                               ret = -ENODEV;
> +                       goto err_free;
> +               }
> +       } else {
> +               dev_warn(&client->dev,
> +                        "probe() called with invalid i2c device id\n");
> +               ret = -EINVAL;
> +       }
> +
> +       goto end;
> +err_free:
> +       devm_kfree(&client->dev, drv_data);
> +end:
> +       return ret;
> +}
> +
> +/*
> + * Callback for bus_find_device() used in ddcci_remove()
> + *
> + * Find next device on i2c_client not flagged with
> + * DDCCI_FLAG_REMOVED and flag it.
> + */
> +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,3,0)
> +static int ddcci_remove_helper(struct device *dev, const void *p)
> +#else
> +static int ddcci_remove_helper(struct device *dev, void *p)
> +#endif
> +{
> +       struct ddcci_device *device;
> +
> +       device = ddcci_verify_device(dev);
> +       if (!device || device->flags & DDCCI_FLAG_REMOVED)
> +               return 0;
> +
> +       if (!p || (dev->parent == p)) {
> +               device->flags |= DDCCI_FLAG_REMOVED;
> +               wmb();
> +               return 1;
> +       }
> +
> +       return 0;
> +}
> +
> +/* I2C driver remove callback: unregister all subdevices */
> +static int ddcci_remove(struct i2c_client *client)
> +{
> +       struct ddcci_bus_drv_data *drv_data = i2c_get_clientdata(client);
> +       struct device *dev;
> +
> +       down(&drv_data->sem);
> +       while (1) {
> +               dev = bus_find_device(&ddcci_bus_type, NULL, client,
> +                                     ddcci_remove_helper);
> +               if (!dev)
> +                       break;
> +               device_unregister(dev);
> +               put_device(dev);
> +       }
> +       up(&drv_data->sem);
> +       return 0;
> +}
> +
> +/*
> + * I2C driver device identification table.
> + */
> +static const struct i2c_device_id ddcci_idtable[] = {
> +       { "ddcci", 0 },
> +       { "ddcci-dependent", 1 },
> +       {}
> +};
> +MODULE_DEVICE_TABLE(i2c, ddcci_idtable);
> +
> +/*
> + * I2C driver description structure
> + */
> +static struct i2c_driver ddcci_driver = {
> +       .driver = {
> +               .name   = "ddcci",
> +               .owner  = THIS_MODULE,
> +       },
> +
> +       .id_table       = ddcci_idtable,
> +       .probe          = ddcci_probe,
> +       .remove         = ddcci_remove,
> +       .class          = I2C_CLASS_DDC,
> +       .detect         = ddcci_detect,
> +       .address_list   = I2C_ADDRS(
> +               DDCCI_DEFAULT_DEVICE_ADDR>>1
> +       ),
> +};
> +
> +/*
> + * Module initialization function. Called when the module is inserted or
> + * (if builtin) at boot time.
> + */
> +static int __init ddcci_module_init(void)
> +{
> +       int ret;
> +
> +       pr_debug("initializing ddcci driver\n");
> +       /* Allocate a device number region for the character devices */
> +       ret = alloc_chrdev_region(&ddcci_cdev_first, 0, 128, DEVICE_NAME);
> +       if (ret < 0) {
> +               pr_err("failed to register device region: error %d\n", ret);
> +               goto err_chrdevreg;
> +       }
> +       ddcci_cdev_next = ddcci_cdev_first;
> +       ddcci_cdev_end = MKDEV(MAJOR(ddcci_cdev_first), MINOR(ddcci_cdev_first)+128);
> +
> +       /* Register bus */
> +       ret = bus_register(&ddcci_bus_type);
> +       if (ret) {
> +               pr_err("failed to register bus 'ddcci'\n");
> +               goto err_busreg;
> +       }
> +
> +       /* Register I2C driver */
> +       ret = i2c_add_driver(&ddcci_driver);
> +       if (ret) {
> +               pr_err("failed to register i2c driver\n");
> +               goto err_drvreg;
> +       }
> +
> +       pr_debug("ddcci driver initialized\n");
> +
> +       return 0;
> +
> +err_drvreg:
> +       bus_unregister(&ddcci_bus_type);
> +err_busreg:
> +       unregister_chrdev_region(ddcci_cdev_first, 128);
> +err_chrdevreg:
> +       return ret;
> +}
> +
> +/*
> + * Module clean-up function. Called when the module is removed.
> + */
> +static void __exit ddcci_module_exit(void)
> +{
> +       struct device *dev;
> +
> +       while (1) {
> +               dev = bus_find_device(&ddcci_bus_type, NULL, NULL, ddcci_remove_helper);
> +               if (!dev)
> +                       break;
> +               device_unregister(dev);
> +               put_device(dev);
> +       }
> +
> +       i2c_del_driver(&ddcci_driver);
> +       bus_unregister(&ddcci_bus_type);
> +       unregister_chrdev_region(ddcci_cdev_first, 128);
> +}
> +
> +/* Let the kernel know the calls for module init and exit */
> +module_init(ddcci_module_init);
> +module_exit(ddcci_module_exit);
> +
> +/* Module parameter description */
> +module_param(delay, uint, S_IRUGO|S_IWUSR);
> +MODULE_PARM_DESC(delay, "default delay after bus writes (in ms, default 60)");
> +module_param_array(autoprobe_addrs, ushort, &autoprobe_addr_count, S_IRUGO|S_IWUSR);
> +MODULE_PARM_DESC(autoprobe_addrs, "internal dependent device addresses to autoprobe");
> +
> +/* Module description */
> +MODULE_AUTHOR("Christoph Grenz");
> +MODULE_DESCRIPTION("DDC/CI bus driver");
> +MODULE_VERSION("0.4.2");
> +MODULE_LICENSE("GPL");
> diff --git a/include/linux/ddcci.h b/include/linux/ddcci.h
> new file mode 100644
> index 000000000000..a219f031e584
> --- /dev/null
> +++ b/include/linux/ddcci.h
> @@ -0,0 +1,164 @@
> +/*
> + *  DDC/CI bus driver
> + *
> + *  Copyright (c) 2015 Christoph Grenz
> + */
> +
> +/*
> + * 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.
> + */
> +
> +#ifndef _DDCCI_H
> +#define _DDCCI_H
> +
> +#include <linux/mod_devicetable.h>
> +#include <linux/device.h>
> +#include <linux/cdev.h>
> +
> +#define DDCCI_MODULE_PREFIX "ddcci:"
> +
> +/* Special addresses */
> +
> +/* default device address (even) */
> +#define DDCCI_DEFAULT_DEVICE_ADDR      0x6E
> +/* receiving host address for communication with default device address */
> +#define DDCCI_HOST_ADDR_EVEN   0x50
> +/* sending host address for communication with default device address */
> +#define DDCCI_HOST_ADDR_ODD    0x51
> +
> +/* Command codes */
> +
> +/* Identification Request */
> +#define DDCCI_COMMAND_ID       0xf1
> +/* Identification Reply */
> +#define DDCCI_REPLY_ID 0xe1
> +/* Capabilities Request */
> +#define DDCCI_COMMAND_CAPS     0xf3
> +/* Capabilities Reply */
> +#define DDCCI_REPLY_CAPS       0xe3
> +
> +/* Quirks */
> +
> +/* Device always responds with unset protocol flag */
> +#define DDCCI_QUIRK_NO_PFLAG BIT(1)
> +/* Device needs writing one byte at a time  */
> +#define DDCCI_QUIRK_WRITE_BYTEWISE BIT(2)
> +/* Device repeats first byte on read */
> +#define DDCCI_QUIRK_SKIP_FIRST_BYTE BIT(3)
> +
> +/* Flags */
> +
> +#define DDCCI_FLAG_REMOVED BIT(1)
> +#define DDCCI_FLAG_DEPENDENT BIT(2)
> +#define DDCCI_FLAG_EXTERNAL BIT(3)
> +
> +extern struct bus_type ddcci_bus_type;
> +
> +struct ddcci_bus_drv_data;
> +
> +/* struct ddcci_device_id - identifies DDC/CI devices for probing */
> +struct ddcci_device_id {
> +       char prot[9];
> +       char type[9];
> +       char model[9];
> +       char vendor[9];
> +       char module[9];
> +       kernel_ulong_t driver_data;     /* Data private to the driver */
> +};
> +#define DDCCI_ANY_ID "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"
> +
> +/**
> + * struct ddcci_device - represent an DDC/CI device
> + * @outer_addr: Outer device address (I2C address << 1).
> + * @inner_addr: Inner device address.
> + * @flags: Device flags.
> + * @capabilities: Device capability string.
> + * @capabilities_len: Length of capability string.
> + * @i2c_client: Parent I2C device.
> + * @bus_drv_data: Driver internal data structure.
> + * @dev: Driver model device node for the slave.
> + * @cdev: Character device structure
> + * @cdev_sem: RW semaphore for exclusive access on character device.
> + * @prot: Device class ("protocol", from capability string)
> + * @type: Device subclass ("type", from capability string)
> + * @model: Device model (from capability string)
> + * @vendor: Device vendor (from identification command response)
> + * @module: Device module (from identification command response)
> + * @device_number: Device serial (from identification command response)
> + */
> +struct ddcci_device {
> +       unsigned short outer_addr;
> +       unsigned short inner_addr;
> +       int flags;
> +       char *capabilities;
> +       size_t capabilities_len;
> +       struct i2c_client *i2c_client;
> +       struct ddcci_bus_drv_data *bus_drv_data;
> +       struct device dev;
> +       struct cdev cdev;
> +       struct rw_semaphore cdev_sem;
> +       char prot[9];
> +       char type[9];
> +       char model[9];
> +       char vendor[9];
> +       char module[9];
> +       int device_number;
> +};
> +#define to_ddcci_device(d) container_of(d, struct ddcci_device, dev)
> +
> +/**
> + * struct ddcci_driver - represent an DDC/CI device driver
> + * @probe: Callback for device binding
> + * @remove: Callback for device unbinding
> + * @driver: Device driver model driver
> + * @id_table: List of DDC/CI devices supported by this driver
> + *
> + * The driver.owner field should be set to the module owner of this driver.
> + * The driver.name field should be set to the name of this driver.
> + */
> +struct ddcci_driver {
> +       int (*probe)(struct ddcci_device *, const struct ddcci_device_id *);
> +       int (*remove)(struct ddcci_device *);
> +       struct device_driver driver;
> +       struct ddcci_device_id *id_table;
> +};
> +#define to_ddcci_driver(d) container_of(d, struct ddcci_driver, driver)
> +
> +int ddcci_register_driver(struct module *owner, struct ddcci_driver *driver);
> +#define ddcci_add_driver(driver) \
> +       ddcci_register_driver(THIS_MODULE, driver)
> +void ddcci_del_driver(struct ddcci_driver *driver);
> +
> +struct ddcci_device *ddcci_verify_device(struct device *dev);
> +
> +#define module_ddcci_driver(__ddcci_driver) \
> +       module_driver(__ddcci_driver, ddcci_add_driver, \
> +                       ddcci_del_driver)
> +
> +int ddcci_device_write(struct ddcci_device *, bool p_flag, unsigned char *data,
> +                      unsigned char length);
> +int ddcci_device_read(struct ddcci_device *, bool p_flag, unsigned char *buffer,
> +                     unsigned char length);
> +int ddcci_device_writeread(struct ddcci_device *, bool p_flag,
> +                          unsigned char *buffer, unsigned char length,
> +                          unsigned char maxlength);
> +
> +static inline void *ddcci_get_drvdata(const struct ddcci_device *dev)
> +{
> +       return dev_get_drvdata(&dev->dev);
> +}
> +
> +static inline void ddcci_set_drvdata(struct ddcci_device *dev, void *data)
> +{
> +       dev_set_drvdata(&dev->dev, data);
> +}
> +
> +unsigned long ddcci_quirks(struct ddcci_device *dev);
> +
> +const char *ddcci_find_capstr_item(const char *capabilities, const char *tag,
> +                                  size_t *length);
> +
> +#endif
> --
> 2.25.1
>

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH] drivers: ddcci: upstream DDCCI driver
  2022-02-26  2:20 [PATCH] drivers: ddcci: upstream DDCCI driver Yusuf Khan
  2022-02-26  2:33 ` Yusuf Khan
@ 2022-02-26  9:38 ` Arnd Bergmann
  2022-02-26 10:48   ` Yusuf Khan
  1 sibling, 1 reply; 7+ messages in thread
From: Arnd Bergmann @ 2022-02-26  9:38 UTC (permalink / raw)
  To: Yusuf Khan
  Cc: Linux Kernel Mailing List, Jason Wang, Michael Kelley,
	Michael S. Tsirkin, gregkh, javier, Arnd Bergmann, Will Deacon,
	Jens Axboe, Bjorn Andersson

with the other ones.On Sat, Feb 26, 2022 at 3:20 AM Yusuf Khan
<yusisamerican@gmail.com> wrote:
>
> This patch upstreams the DDCCI driver by Christoph Grenz into
> the kernel. The original gitlab page is loacted at https://gitlab
> .com/ddcci-driver-linux/ddcci-driver-linux/-/tree/master.
>
> Signed-off-by: Yusuf Khan <yusisamerican@gmail.com>
> ---
>  drivers/Kconfig                 |    2 +
>  drivers/Makefile                |    1 +
>  drivers/ddcci/Kconfig           |    3 +
>  drivers/ddcci/Makefile          |    3 +
>  drivers/ddcci/ddcci-backlight.c |  413 +++++++
>  drivers/ddcci/ddcci.c           | 1895 +++++++++++++++++++++++++++++++
>  include/linux/ddcci.h           |  164 +++

If this is a backlight driver, I think it should go into
drivers/video/backlight/,
no need for a top-level subsystem.

> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +#include <asm-generic/fcntl.h>

Including the asm-generic version causes the build failures. If you need
the contents, use <linux/fcntl.h>, otherwise leave it out.

> +static dev_t ddcci_cdev_first;
> +static dev_t ddcci_cdev_next;
> +static dev_t ddcci_cdev_end;
> +static DEFINE_SEMAPHORE(core_lock);

No new semaphores please, this should probably be a mutex.


> +struct bus_type ddcci_bus_type;
> +EXPORT_SYMBOL_GPL(ddcci_bus_type);
> +
> +/* Assert neccessary string array sizes  */
> +#ifndef sizeof_field
> +# define sizeof_field(t,m) FIELD_SIZEOF(t,m)
> +#endif
> +static_assert(sizeof_field(struct ddcci_device, prot) > 8);
> +static_assert(sizeof_field(struct ddcci_device, type) > 8);
> +static_assert(sizeof_field(struct ddcci_device, model) > 8);
> +static_assert(sizeof_field(struct ddcci_device, vendor) > 8);
> +static_assert(sizeof_field(struct ddcci_device, module) > 8);
> +
> +/* Internal per-i2c-client driver data */
> +struct ddcci_bus_drv_data {
> +       unsigned long quirks;
> +       struct i2c_client *i2c_dev;
> +       struct semaphore sem;
> +       unsigned char recv_buffer[DDCCI_RECV_BUFFER_SIZE];
> +};

Same here.

> +static const struct file_operations ddcci_fops = {
> +       .owner = THIS_MODULE,
> +       .read = ddcci_cdev_read,
> +       .write = ddcci_cdev_write,
> +       .open = ddcci_cdev_open,
> +       .release = ddcci_cdev_close,
> +       .llseek = ddcci_cdev_seek
> +};

It looks like this adds low-level access to a bus that is already managed by
the drm (or older framebuffer) drivers. How do you prevent these two
from stepping on each other's toes?

        Arnd

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH] drivers: ddcci: upstream DDCCI driver
  2022-02-26  9:38 ` Arnd Bergmann
@ 2022-02-26 10:48   ` Yusuf Khan
  2022-02-26 21:58     ` Arnd Bergmann
  0 siblings, 1 reply; 7+ messages in thread
From: Yusuf Khan @ 2022-02-26 10:48 UTC (permalink / raw)
  To: Arnd Bergmann
  Cc: Linux Kernel Mailing List, Jason Wang, Michael Kelley,
	Michael S. Tsirkin, gregkh, javier, Will Deacon, Jens Axboe,
	Bjorn Andersson

Arnd, the driver includes a backlight driver, the main part of the
driver(ddcci.c) is a monitor communication protocol. Should
I leave the backlight driver(ddcci-backlight.c) in drivers/video/backlight?

On Sat, Feb 26, 2022 at 1:39 AM Arnd Bergmann <arnd@arndb.de> wrote:
>
> with the other ones.On Sat, Feb 26, 2022 at 3:20 AM Yusuf Khan
> <yusisamerican@gmail.com> wrote:
> >
> > This patch upstreams the DDCCI driver by Christoph Grenz into
> > the kernel. The original gitlab page is loacted at https://gitlab
> > .com/ddcci-driver-linux/ddcci-driver-linux/-/tree/master.
> >
> > Signed-off-by: Yusuf Khan <yusisamerican@gmail.com>
> > ---
> >  drivers/Kconfig                 |    2 +
> >  drivers/Makefile                |    1 +
> >  drivers/ddcci/Kconfig           |    3 +
> >  drivers/ddcci/Makefile          |    3 +
> >  drivers/ddcci/ddcci-backlight.c |  413 +++++++
> >  drivers/ddcci/ddcci.c           | 1895 +++++++++++++++++++++++++++++++
> >  include/linux/ddcci.h           |  164 +++
>
> If this is a backlight driver, I think it should go into
> drivers/video/backlight/,
> no need for a top-level subsystem.
>
> > + */
> > +
> > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > +#include <asm-generic/fcntl.h>
>
> Including the asm-generic version causes the build failures. If you need
> the contents, use <linux/fcntl.h>, otherwise leave it out.
>
> > +static dev_t ddcci_cdev_first;
> > +static dev_t ddcci_cdev_next;
> > +static dev_t ddcci_cdev_end;
> > +static DEFINE_SEMAPHORE(core_lock);
>
> No new semaphores please, this should probably be a mutex.
>
>
> > +struct bus_type ddcci_bus_type;
> > +EXPORT_SYMBOL_GPL(ddcci_bus_type);
> > +
> > +/* Assert neccessary string array sizes  */
> > +#ifndef sizeof_field
> > +# define sizeof_field(t,m) FIELD_SIZEOF(t,m)
> > +#endif
> > +static_assert(sizeof_field(struct ddcci_device, prot) > 8);
> > +static_assert(sizeof_field(struct ddcci_device, type) > 8);
> > +static_assert(sizeof_field(struct ddcci_device, model) > 8);
> > +static_assert(sizeof_field(struct ddcci_device, vendor) > 8);
> > +static_assert(sizeof_field(struct ddcci_device, module) > 8);
> > +
> > +/* Internal per-i2c-client driver data */
> > +struct ddcci_bus_drv_data {
> > +       unsigned long quirks;
> > +       struct i2c_client *i2c_dev;
> > +       struct semaphore sem;
> > +       unsigned char recv_buffer[DDCCI_RECV_BUFFER_SIZE];
> > +};
>
> Same here.
>
> > +static const struct file_operations ddcci_fops = {
> > +       .owner = THIS_MODULE,
> > +       .read = ddcci_cdev_read,
> > +       .write = ddcci_cdev_write,
> > +       .open = ddcci_cdev_open,
> > +       .release = ddcci_cdev_close,
> > +       .llseek = ddcci_cdev_seek
> > +};
>
> It looks like this adds low-level access to a bus that is already managed by
> the drm (or older framebuffer) drivers. How do you prevent these two
> from stepping on each other's toes?
>
>         Arnd

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH] drivers: ddcci: upstream DDCCI driver
  2022-02-26 10:48   ` Yusuf Khan
@ 2022-02-26 21:58     ` Arnd Bergmann
  0 siblings, 0 replies; 7+ messages in thread
From: Arnd Bergmann @ 2022-02-26 21:58 UTC (permalink / raw)
  To: Yusuf Khan
  Cc: Arnd Bergmann, Linux Kernel Mailing List, Jason Wang,
	Michael Kelley, Michael S. Tsirkin, gregkh, javier, Will Deacon,
	Jens Axboe, Bjorn Andersson

On Sat, Feb 26, 2022 at 11:48 AM Yusuf Khan <yusisamerican@gmail.com> wrote:
> On Sat, Feb 26, 2022 at 1:39 AM Arnd Bergmann <arnd@arndb.de> wrote:
> >
> > with the other ones.On Sat, Feb 26, 2022 at 3:20 AM Yusuf Khan
> > <yusisamerican@gmail.com> wrote:
> > >
> >
> > If this is a backlight driver, I think it should go into
> > drivers/video/backlight/,
> > no need for a top-level subsystem.
>
> Arnd, the driver includes a backlight driver, the main part of the
> driver(ddcci.c) is a monitor communication protocol. Should
> I leave the backlight driver(ddcci-backlight.c) in drivers/video/backlight?

It's more important to decide how the user space interface should be
handled, let's decide about that first.

> > > + */
> > > +
> > > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > > +#include <asm-generic/fcntl.h>
> >
> > Including the asm-generic version causes the build failures. If you need
> > the contents, use <linux/fcntl.h>, otherwise leave it out.
> >
> > > +static dev_t ddcci_cdev_first;
> > > +static dev_t ddcci_cdev_next;
> > > +static dev_t ddcci_cdev_end;
> > > +static DEFINE_SEMAPHORE(core_lock);
> >
> > No new semaphores please, this should probably be a mutex.
> >
> >
> > > +struct bus_type ddcci_bus_type;
> > > +EXPORT_SYMBOL_GPL(ddcci_bus_type);
> > > +
> > > +/* Assert neccessary string array sizes  */
> > > +#ifndef sizeof_field
> > > +# define sizeof_field(t,m) FIELD_SIZEOF(t,m)
> > > +#endif
> > > +static_assert(sizeof_field(struct ddcci_device, prot) > 8);
> > > +static_assert(sizeof_field(struct ddcci_device, type) > 8);
> > > +static_assert(sizeof_field(struct ddcci_device, model) > 8);
> > > +static_assert(sizeof_field(struct ddcci_device, vendor) > 8);
> > > +static_assert(sizeof_field(struct ddcci_device, module) > 8);
> > > +
> > > +/* Internal per-i2c-client driver data */
> > > +struct ddcci_bus_drv_data {
> > > +       unsigned long quirks;
> > > +       struct i2c_client *i2c_dev;
> > > +       struct semaphore sem;
> > > +       unsigned char recv_buffer[DDCCI_RECV_BUFFER_SIZE];
> > > +};
> >
> > Same here.
> >
> > > +static const struct file_operations ddcci_fops = {
> > > +       .owner = THIS_MODULE,
> > > +       .read = ddcci_cdev_read,
> > > +       .write = ddcci_cdev_write,
> > > +       .open = ddcci_cdev_open,
> > > +       .release = ddcci_cdev_close,
> > > +       .llseek = ddcci_cdev_seek
> > > +};
> >
> > It looks like this adds low-level access to a bus that is already managed by
> > the drm (or older framebuffer) drivers. How do you prevent these two
> > from stepping on each other's toes?

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH] drivers: ddcci: upstream DDCCI driver
  2022-02-26  3:57 Yusuf Khan
@ 2022-02-26  4:01 ` Yusuf Khan
  0 siblings, 0 replies; 7+ messages in thread
From: Yusuf Khan @ 2022-02-26  4:01 UTC (permalink / raw)
  To: linux-kernel
  Cc: jasowang, mikelley, mst, gregkh, javier, arnd, will, axboe,
	bjorn.andersson

Sorry for my [extened] clumsiness, I accidentally sent the same patch
twice instead of the v2! Im so sorry for my "extremely stupid"
behavior!

On Fri, Feb 25, 2022 at 7:57 PM Yusuf Khan <yusisamerican@gmail.com> wrote:
>
> This patch upstreams the DDCCI driver by Christoph Grenz into
> the kernel. The original gitlab page is loacted at https://gitlab
> .com/ddcci-driver-linux/ddcci-driver-linux/-/tree/master.
>
> Signed-off-by: Yusuf Khan <yusisamerican@gmail.com>
> ---
>  drivers/Kconfig                 |    2 +
>  drivers/Makefile                |    1 +
>  drivers/ddcci/Kconfig           |    3 +
>  drivers/ddcci/Makefile          |    3 +
>  drivers/ddcci/ddcci-backlight.c |  413 +++++++
>  drivers/ddcci/ddcci.c           | 1895 +++++++++++++++++++++++++++++++
>  include/linux/ddcci.h           |  164 +++
>  7 files changed, 2481 insertions(+)
>  create mode 100644 drivers/ddcci/Kconfig
>  create mode 100644 drivers/ddcci/Makefile
>  create mode 100644 drivers/ddcci/ddcci-backlight.c
>  create mode 100644 drivers/ddcci/ddcci.c
>  create mode 100644 include/linux/ddcci.h
>
> diff --git a/drivers/Kconfig b/drivers/Kconfig
> index 0d399ddaa185..3a58a29113f9 100644
> --- a/drivers/Kconfig
> +++ b/drivers/Kconfig
> @@ -236,4 +236,6 @@ source "drivers/interconnect/Kconfig"
>  source "drivers/counter/Kconfig"
>
>  source "drivers/most/Kconfig"
> +
> +source "drivers/ddcci/Kconfig"
>  endmenu
> diff --git a/drivers/Makefile b/drivers/Makefile
> index a110338c860c..c9181f1dc5aa 100644
> --- a/drivers/Makefile
> +++ b/drivers/Makefile
> @@ -21,6 +21,7 @@ obj-y                         += pci/
>  obj-$(CONFIG_PARISC)           += parisc/
>  obj-$(CONFIG_RAPIDIO)          += rapidio/
>  obj-y                          += video/
> +obj-$(CONFIG_DDCCI)                            += ddcci/
>  obj-y                          += idle/
>
>  # IPMI must come before ACPI in order to provide IPMI opregion support
> diff --git a/drivers/ddcci/Kconfig b/drivers/ddcci/Kconfig
> new file mode 100644
> index 000000000000..416881cc88e1
> --- /dev/null
> +++ b/drivers/ddcci/Kconfig
> @@ -0,0 +1,3 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +menuconfig DDCCI
> +       tristate "DDCCI display protocol support"
> diff --git a/drivers/ddcci/Makefile b/drivers/ddcci/Makefile
> new file mode 100644
> index 000000000000..cce606be8038
> --- /dev/null
> +++ b/drivers/ddcci/Makefile
> @@ -0,0 +1,3 @@
> +# SPDX-License-Identifier: GPL-2.0
> +obj-$(CONFIG_DDCCI) += ddcci.o
> +obj-$(CONFIG_DDCCI) += ddcci-backlight.o
> diff --git a/drivers/ddcci/ddcci-backlight.c b/drivers/ddcci/ddcci-backlight.c
> new file mode 100644
> index 000000000000..7a9852207f0b
> --- /dev/null
> +++ b/drivers/ddcci/ddcci-backlight.c
> @@ -0,0 +1,413 @@
> +/*
> + *  DDC/CI monitor backlight driver
> + *
> + *  Copyright (c) 2015 Christoph Grenz
> + */
> +
> +/*
> + * 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.
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +#include <linux/backlight.h>
> +#include <linux/module.h>
> +#include <linux/fb.h>
> +#include <linux/sysfs.h>
> +
> +#include <linux/ddcci.h>
> +
> +
> +#define DDCCI_COMMAND_READ     0x01    /* read ctrl value */
> +#define DDCCI_REPLY_READ       0x02    /* read ctrl value reply */
> +#define DDCCI_COMMAND_WRITE    0x03    /* write ctrl value */
> +#define DDCCI_COMMAND_SAVE     0x0c    /* save current settings */
> +
> +#define DDCCI_MONITOR_LUMINANCE        0x10
> +#define DDCCI_MONITOR_BACKLIGHT        0x13
> +#define DDCCI_MONITOR_BL_WHITE         0x6B
> +
> +static bool convenience_symlink = true;
> +
> +struct ddcci_monitor_drv_data {
> +       struct ddcci_device *device;
> +       struct backlight_device *bl_dev;
> +       struct device *fb_dev;
> +       unsigned char used_vcp;
> +};
> +
> +static int ddcci_monitor_writectrl(struct ddcci_device *device,
> +                                  unsigned char ctrl, unsigned short value)
> +{
> +       unsigned char buf[4];
> +       int ret;
> +
> +       buf[0] = DDCCI_COMMAND_WRITE;
> +       buf[1] = ctrl;
> +       buf[2] = (value >> 8);
> +       buf[3] = (value & 255);
> +
> +       ret = ddcci_device_write(device, true, buf, sizeof(buf));
> +
> +       return ret;
> +}
> +
> +static int ddcci_monitor_readctrl(struct ddcci_device *device,
> +                                 unsigned char ctrl, unsigned short *value,
> +                                 unsigned short *maximum)
> +{
> +       int ret;
> +       unsigned char buf[10];
> +
> +       buf[0] = DDCCI_COMMAND_READ;
> +       buf[1] = ctrl;
> +
> +       ret = ddcci_device_writeread(device, true, buf, 2, sizeof(buf));
> +       if (ret < 0)
> +               return ret;
> +
> +       if (ret == 0)
> +               return -ENOTSUPP;
> +
> +       if (ret == 8 && buf[0] == DDCCI_REPLY_READ && buf[2] == ctrl) {
> +               if (value)
> +                       *value = buf[6] * 256 + buf[7];
> +
> +               if (maximum)
> +                       *maximum = buf[4] * 256 + buf[5];
> +
> +               if (buf[1] == 1)
> +                       return -ENOTSUPP;
> +               if (buf[1] != 0)
> +                       return -EIO;
> +               return 0;
> +       }
> +
> +       return -EIO;
> +}
> +
> +static int ddcci_backlight_check_fb(struct backlight_device *bl,
> +                                  struct fb_info *info)
> +{
> +       struct ddcci_monitor_drv_data *drv_data = bl_get_data(bl);
> +
> +       return drv_data->fb_dev == NULL || drv_data->fb_dev == info->dev;
> +}
> +
> +static int ddcci_backlight_update_status(struct backlight_device *bl)
> +{
> +       struct ddcci_monitor_drv_data *drv_data = bl_get_data(bl);
> +       int brightness = bl->props.brightness;
> +       int ret;
> +
> +       if (bl->props.power != FB_BLANK_UNBLANK ||
> +           bl->props.state & BL_CORE_FBBLANK)
> +               brightness = 0;
> +
> +       ret = ddcci_monitor_writectrl(drv_data->device, drv_data->used_vcp,
> +                                     brightness);
> +       if (ret > 0)
> +               ret = 0;
> +       return ret;
> +}
> +
> +static int ddcci_backlight_get_brightness(struct backlight_device *bl)
> +{
> +       unsigned short value = 0, maxval = 0;
> +       int ret;
> +       struct ddcci_monitor_drv_data *drv_data = bl_get_data(bl);
> +
> +       ret = ddcci_monitor_readctrl(drv_data->device, drv_data->used_vcp,
> +                                    &value, &maxval);
> +       if (ret < 0)
> +               return ret;
> +
> +       bl->props.brightness = value;
> +       bl->props.max_brightness = maxval;
> +       ret = value;
> +
> +       return ret;
> +}
> +
> +static const struct backlight_ops ddcci_backlight_ops = {
> +       .options        = 0,
> +       .update_status  = ddcci_backlight_update_status,
> +       .get_brightness = ddcci_backlight_get_brightness,
> +       .check_fb       = ddcci_backlight_check_fb,
> +};
> +
> +static const char *ddcci_monitor_vcp_name(unsigned char vcp)
> +{
> +       switch (vcp) {
> +               case DDCCI_MONITOR_BL_WHITE:
> +                       return "backlight";
> +               case DDCCI_MONITOR_LUMINANCE:
> +                       return "luminance";
> +               default:
> +                       return "???";
> +       }
> +}
> +
> +static const char *ddcci_monitor_next_vcp_item(const char *ptr)
> +{
> +       int depth = 0;
> +
> +       /* Sanity check */
> +       if (unlikely(ptr == NULL || ptr[0] == '\0'))
> +               return NULL;
> +
> +       /* Find next white space outside of parentheses */
> +       while ((ptr = strpbrk(ptr, " ()"))) {
> +               if (!ptr || depth == INT_MAX) {
> +                       return NULL;
> +               } else if (*ptr == '(') {
> +                       depth++;
> +               } else if (depth > 0) {
> +                       if (*ptr == ')')
> +                               depth--;
> +               } else {
> +                       break;
> +               }
> +               ++ptr;
> +       }
> +
> +       /* Skip over whitespace */
> +       ptr = skip_spaces(ptr);
> +
> +       /* Check if we're now at the end of the list */
> +       if (unlikely(*ptr == '\0' || *ptr == ')'))
> +               return NULL;
> +
> +       return ptr;
> +}
> +
> +static bool ddcci_monitor_find_vcp(unsigned char vcp, const char *s)
> +{
> +       const char *ptr = s;
> +       char vcp_hex[3];
> +
> +       /* Sanity check */
> +       if (unlikely(s == NULL || s[0] == '\0'))
> +               return false;
> +
> +       /* Create hex representation of VCP */
> +       if (unlikely(snprintf(vcp_hex, 3, "%02hhX", vcp) != 2)) {
> +               pr_err("snprintf failed to convert to hex. This should not happen.\n");
> +               return false;
> +       }
> +
> +       /* Search for it */
> +       do {
> +               if (strncasecmp(vcp_hex, ptr, 2) == 0) {
> +                       if (ptr[2] == ' ' || ptr[2] == '(' || ptr[2] == ')') {
> +                               return true;
> +                       }
> +               }
> +       } while ((ptr = ddcci_monitor_next_vcp_item(ptr)));
> +
> +       return false;
> +}
> +
> +static int ddcci_backlight_create_symlink(struct ddcci_device *ddcci_dev)
> +{
> +       int i, result;
> +       struct device *dev = &ddcci_dev->dev;
> +       struct kernfs_node *dirent;
> +       for (i = 0; i < 3; ++i) {
> +               dev = dev->parent;
> +               if (!dev) {
> +                       dev_dbg(&ddcci_dev->dev, "failed to create convenience symlink: ancestor device not found\n");
> +                       return -ENOENT;
> +               }
> +       }
> +       dirent = sysfs_get_dirent(dev->kobj.sd, "ddcci_backlight");
> +       if (dirent) {
> +               sysfs_put(dirent);
> +               dev_dbg(&ddcci_dev->dev, "failed to create convenience symlink: %s/ddcci_backlight already exists\n", dev_name(dev));
> +               return -EEXIST;
> +       }
> +
> +       result = sysfs_create_link(&dev->kobj, &ddcci_dev->dev.kobj, "ddcci_backlight");
> +       if (result == 0) {
> +               dev_dbg(&ddcci_dev->dev, "created symlink %s/ddcci_backlight\n", dev_name(dev));
> +       } else {
> +               dev_info(&ddcci_dev->dev, "failed to create convenience symlink: %d\n", result);
> +       }
> +       return result;
> +}
> +
> +static int ddcci_backlight_remove_symlink(struct ddcci_device *ddcci_dev)
> +{
> +       int i;
> +       struct device *dev = &ddcci_dev->dev;
> +       struct kernfs_node *dirent;
> +       for (i = 0; i < 3; ++i) {
> +               dev = dev->parent;
> +               if (!dev)
> +                       return -ENOENT;
> +       }
> +       dirent = sysfs_get_dirent(dev->kobj.sd, "ddcci_backlight");
> +       if (!dirent) {
> +               return -ENOENT;
> +       }
> +
> +       if ((dirent->flags & KERNFS_LINK) == 0) {
> +               sysfs_put(dirent);
> +               dev_dbg(&ddcci_dev->dev, "won't remove %s/ddcci_backlight: not a symlink\n", dev_name(dev));
> +               return -EINVAL;
> +       }
> +
> +       if (dirent->symlink.target_kn != ddcci_dev->dev.kobj.sd) {
> +               sysfs_put(dirent);
> +               dev_dbg(&ddcci_dev->dev, "won't remove %s/ddcci_backlight: we are not the link target\n", dev_name(dev));
> +               return -EINVAL;
> +       }
> +
> +       sysfs_put(dirent);
> +
> +       sysfs_remove_link(&dev->kobj, "ddcci_backlight");
> +       dev_dbg(&ddcci_dev->dev, "removed symlink %s/ddcci_backlight\n", dev_name(dev));
> +       return 0;
> +}
> +
> +static int ddcci_monitor_probe(struct ddcci_device *dev,
> +                              const struct ddcci_device_id *id)
> +{
> +       struct ddcci_monitor_drv_data *drv_data;
> +       struct backlight_properties props;
> +       struct backlight_device *bl = NULL;
> +       int ret = 0;
> +       bool support_luminance, support_bl_white;
> +       unsigned short brightness = 0, max_brightness = 0;
> +       const char *vcps;
> +
> +       dev_dbg(&dev->dev, "probing monitor backlight device\n");
> +
> +       /* Get VCP list */
> +       vcps = ddcci_find_capstr_item(dev->capabilities, "vcp", NULL);
> +       if (IS_ERR(vcps)) {
> +               dev_info(&dev->dev,
> +                        "monitor doesn't provide a list of supported controls.\n");
> +               support_bl_white = support_luminance = true;
> +       } else {
> +               /* Check VCP list for supported VCPs */
> +               support_bl_white = ddcci_monitor_find_vcp(DDCCI_MONITOR_BL_WHITE, vcps);
> +               support_luminance = ddcci_monitor_find_vcp(DDCCI_MONITOR_LUMINANCE, vcps);
> +               /* Fallback to trying if no support is found */
> +               if (!support_bl_white && !support_luminance) {
> +                       dev_info(&dev->dev,
> +                                "monitor doesn't announce support for backlight or luminance controls.\n");
> +                       support_bl_white = support_luminance = true;
> +               }
> +       }
> +
> +       /* Initialize driver data structure */
> +       drv_data = devm_kzalloc(&dev->dev, sizeof(struct ddcci_monitor_drv_data),
> +                               GFP_KERNEL);
> +       if (!drv_data)
> +               return -ENOMEM;
> +       drv_data->device = dev;
> +
> +       if (support_bl_white) {
> +               /* Try getting backlight level */
> +               dev_dbg(&dev->dev,
> +                       "trying to access \"backlight level white\" control\n");
> +               ret = ddcci_monitor_readctrl(drv_data->device, DDCCI_MONITOR_BL_WHITE,
> +                                               &brightness, &max_brightness);
> +               if (ret < 0) {
> +                       if (ret == -ENOTSUPP)
> +                               dev_info(&dev->dev,
> +                                       "monitor does not support reading backlight level\n");
> +                       else
> +                               goto err_free;
> +               } else {
> +                       drv_data->used_vcp = DDCCI_MONITOR_BL_WHITE;
> +               }
> +       }
> +
> +       if (support_luminance && !drv_data->used_vcp) {
> +               /* Try getting luminance */
> +               dev_dbg(&dev->dev,
> +                       "trying to access \"luminance\" control\n");
> +               ret = ddcci_monitor_readctrl(drv_data->device, DDCCI_MONITOR_LUMINANCE,
> +                                            &brightness, &max_brightness);
> +               if (ret < 0) {
> +                       if (ret == -ENOTSUPP)
> +                               dev_info(&dev->dev,
> +                                       "monitor does not support reading luminance\n");
> +                       else
> +                               goto err_free;
> +               } else {
> +                       drv_data->used_vcp = DDCCI_MONITOR_LUMINANCE;
> +               }
> +               drv_data->used_vcp = DDCCI_MONITOR_LUMINANCE;
> +       }
> +
> +       if (!drv_data->used_vcp)
> +               goto err_free;
> +
> +       /* Create brightness device */
> +       memset(&props, 0, sizeof(props));
> +       props.type = BACKLIGHT_RAW;
> +       props.max_brightness = max_brightness;
> +       props.brightness = brightness;
> +       bl = devm_backlight_device_register(&dev->dev, dev_name(&dev->dev),
> +                                           &dev->dev, drv_data,
> +                                           &ddcci_backlight_ops, &props);
> +       drv_data->bl_dev = bl;
> +       if (IS_ERR(bl)) {
> +               dev_err(&dev->dev, "failed to register backlight\n");
> +               return PTR_ERR(bl);
> +       }
> +       dev_info(&dev->dev, "registered %s as backlight device %s\n",
> +                ddcci_monitor_vcp_name(drv_data->used_vcp),
> +                dev_name(&dev->dev));
> +
> +       if (convenience_symlink) {
> +               ddcci_backlight_create_symlink(dev);
> +       }
> +
> +       goto end;
> +err_free:
> +       devm_kfree(&dev->dev, drv_data);
> +end:
> +       return ret;
> +}
> +
> +static int ddcci_monitor_remove(struct ddcci_device *dev)
> +{
> +       dev_dbg(&dev->dev, "removing device\n");
> +       ddcci_backlight_remove_symlink(dev);
> +       return 0;
> +}
> +
> +static struct ddcci_device_id ddcci_monitor_idtable[] = {
> +       { "monitor", DDCCI_ANY_ID, DDCCI_ANY_ID, DDCCI_ANY_ID, DDCCI_ANY_ID, 0 },
> +       {}
> +};
> +
> +static struct ddcci_driver ddcci_backlight_driver = {
> +       .driver = {
> +               .name   = "ddcci-backlight",
> +               .owner  = THIS_MODULE,
> +       },
> +
> +       .id_table       = ddcci_monitor_idtable,
> +       .probe          = ddcci_monitor_probe,
> +       .remove         = ddcci_monitor_remove,
> +};
> +
> +module_ddcci_driver(ddcci_backlight_driver);
> +
> +/* Module parameter description */
> +module_param(convenience_symlink, bool, S_IRUGO|S_IWUSR);
> +MODULE_PARM_DESC(convenience_symlink, "add convenience symlink \"ddcci_backlight\" to ancestor device in sysfs (default true)");
> +
> +MODULE_AUTHOR("Christoph Grenz");
> +MODULE_DESCRIPTION("DDC/CI generic monitor backlight driver");
> +MODULE_VERSION("0.4.2");
> +MODULE_LICENSE("GPL");
> +
> +MODULE_ALIAS("ddcci:monitor-*-*-*-*");
> diff --git a/drivers/ddcci/ddcci.c b/drivers/ddcci/ddcci.c
> new file mode 100644
> index 000000000000..e26d15cae948
> --- /dev/null
> +++ b/drivers/ddcci/ddcci.c
> @@ -0,0 +1,1895 @@
> +/*
> + *  DDC/CI sub-bus driver
> + *
> + *  Copyright (c) 2015 Christoph Grenz
> + */
> +
> +/*
> + * 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.
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +#include <asm-generic/fcntl.h>
> +#include <linux/cdev.h>
> +#include <linux/delay.h>
> +#include <linux/device.h>
> +#include <linux/fs.h>
> +#include <linux/i2c.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/rwsem.h>
> +#include <linux/sem.h>
> +#include <linux/slab.h>
> +#include <linux/uaccess.h>
> +#include <linux/version.h>
> +
> +#include <linux/ddcci.h>
> +
> +#define DDCCI_RECV_BUFFER_SIZE 130
> +#define DEVICE_NAME "ddcci"
> +#define DDCCI_MAX_CAP_CHUNKS 200
> +
> +static unsigned int delay = 60;
> +static unsigned short autoprobe_addrs[127] = {0xF0, 0xF2, 0xF4, 0xF6, 0xF8};
> +static int autoprobe_addr_count = 5;
> +
> +static dev_t ddcci_cdev_first;
> +static dev_t ddcci_cdev_next;
> +static dev_t ddcci_cdev_end;
> +static DEFINE_SEMAPHORE(core_lock);
> +
> +struct bus_type ddcci_bus_type;
> +EXPORT_SYMBOL_GPL(ddcci_bus_type);
> +
> +/* Assert neccessary string array sizes  */
> +#ifndef sizeof_field
> +# define sizeof_field(t,m) FIELD_SIZEOF(t,m)
> +#endif
> +static_assert(sizeof_field(struct ddcci_device, prot) > 8);
> +static_assert(sizeof_field(struct ddcci_device, type) > 8);
> +static_assert(sizeof_field(struct ddcci_device, model) > 8);
> +static_assert(sizeof_field(struct ddcci_device, vendor) > 8);
> +static_assert(sizeof_field(struct ddcci_device, module) > 8);
> +
> +/* Internal per-i2c-client driver data */
> +struct ddcci_bus_drv_data {
> +       unsigned long quirks;
> +       struct i2c_client *i2c_dev;
> +       struct semaphore sem;
> +       unsigned char recv_buffer[DDCCI_RECV_BUFFER_SIZE];
> +};
> +
> +/* Replace non-alphanumeric characters in a string (used for modalias) */
> +static void ddcci_modalias_clean(char *string, size_t n, char replacement)
> +{
> +       int i;
> +       for (i = 0; i < n; ++i) {
> +               char c = string[i];
> +               if (c == 0) {
> +                       return;
> +               } else if (c < '0' || (c > '9' && c < 'A') || (c > 'Z' && c < 'a') || c > 'z') {
> +                       string[i] = replacement;
> +               }
> +       }
> +}
> +
> +/* Write a message to the DDC/CI bus using i2c_smbus_write_byte() */
> +static int __ddcci_write_bytewise(struct i2c_client *client, unsigned char addr,
> +                                 bool p_flag, const unsigned char * __restrict buf,
> +                                 unsigned char len)
> +{
> +       int ret = 0;
> +       unsigned char outer_addr = (unsigned char)(client->addr << 1);
> +       unsigned xor = outer_addr; /* initial xor value */
> +
> +       /* Consistency checks */
> +       if (len > 127)
> +               return -EINVAL;
> +
> +       /* Special case: sender to 0x6E is always 0x51 */
> +       if (addr == DDCCI_DEFAULT_DEVICE_ADDR) {
> +               addr = DDCCI_HOST_ADDR_ODD;
> +       } else {
> +               /* When sending the odd address is used */
> +               addr = addr | 1;
> +       }
> +
> +       /* first byte: sender address */
> +       xor ^= addr;
> +       ret = i2c_smbus_write_byte(client, addr);
> +       if (ret < 0)
> +               return ret;
> +
> +       /* second byte: protocol flag and message size */
> +       xor ^= ((p_flag << 7) | len);
> +       ret = i2c_smbus_write_byte(client, (p_flag << 7)|len);
> +       if (ret < 0)
> +               return ret;
> +
> +       /* send payload */
> +       while (len--) {
> +               xor ^= (*buf);
> +               ret = i2c_smbus_write_byte(client, (*buf));
> +               if (ret < 0)
> +                       return ret;
> +               buf++;
> +       }
> +
> +       /* send checksum */
> +       ret = i2c_smbus_write_byte(client, xor);
> +       return ret;
> +}
> +
> +/* Write a message to the DDC/CI bus using i2c_master_send() */
> +static int __ddcci_write_block(struct i2c_client *client, unsigned char addr,
> +                              unsigned char *sendbuf, bool p_flag,
> +                              const unsigned char *data, unsigned char len)
> +{
> +       unsigned char outer_addr = (unsigned char)(client->addr << 1);
> +       unsigned xor = outer_addr;      /* initial xor value */
> +       unsigned char *ptr = sendbuf;
> +
> +       /* Consistency checks */
> +       if (len > 127)
> +               return -EINVAL;
> +
> +       /* Special case: sender to 0x6E is always 0x51 */
> +       if (addr == DDCCI_DEFAULT_DEVICE_ADDR) {
> +               addr = DDCCI_HOST_ADDR_ODD;
> +       } else {
> +               /* When sending the odd address is used */
> +               addr = addr | 1;
> +       }
> +
> +       /* first byte: sender address */
> +       xor ^= addr;
> +       *(ptr++) = addr;
> +       /* second byte: protocol flag and message size */
> +       xor ^= ((p_flag << 7) | len);
> +       *(ptr++) = (p_flag << 7)|len;
> +       /* payload */
> +       while (len--) {
> +               xor ^= (*data);
> +               *(ptr++) = (*data);
> +               data++;
> +       }
> +       /* checksum */
> +       (*ptr) = xor;
> +
> +       /* Send it */
> +       return i2c_master_send(client, sendbuf, ptr - sendbuf + 1);
> +}
> +
> +/*
> + * Write a message to the DDC/CI bus.
> + *
> + * You must hold the bus semaphore when calling this function.
> + */
> +static int ddcci_write(struct i2c_client *client, unsigned char addr,
> +                      bool p_flag, const unsigned char *data,
> +                      unsigned char len)
> +{
> +       struct ddcci_bus_drv_data *drv_data;
> +       unsigned char *sendbuf;
> +       int ret;
> +
> +       drv_data = i2c_get_clientdata(client);
> +
> +
> +       pr_debug("sending to %d:%02x:%02x: %*ph\n", client->adapter->nr,
> +                client->addr << 1, addr, len, data);
> +       if (drv_data->quirks & DDCCI_QUIRK_WRITE_BYTEWISE) {
> +               ret = __ddcci_write_bytewise(client, addr, p_flag, data, len);
> +       } else {
> +               sendbuf = drv_data->recv_buffer;
> +               ret = __ddcci_write_block(client, addr, sendbuf, p_flag, data, len);
> +       }
> +
> +       return ret;
> +}
> +
> +/*
> + * Read a response from the DDC/CI bus with headers directly into a buffer.
> + * Always check for DDCCI_QUIRK_SKIP_FIRST_BYTE when using this function.
> + * The returned length contains the whole unmodified response.
> + * If -EMSGSIZE is returned, the buffer contains the response up to `len`.
> + * If any other negative error code is returned, the buffer content is
> + * unspecified.
> + */
> +static int __ddcci_read(struct i2c_client *client, unsigned char addr,
> +                       bool p_flag, unsigned long quirks, unsigned char *buf,
> +                       unsigned char len)
> +{
> +       int i, payload_len, packet_length, ret;
> +       unsigned char xor = DDCCI_HOST_ADDR_EVEN;
> +
> +       /* Consistency checks */
> +       if (len < 3)
> +               return -EINVAL;
> +
> +       /* Read frame */
> +       ret = i2c_master_recv(client, buf, len);
> +       if (ret < 0)
> +               goto out_err;
> +       packet_length = ret;
> +
> +       /* Skip first byte if quirk active */
> +       if ((quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE) && ret > 0 && len > 0) {
> +               ret--;
> +               len--;
> +               buf++;
> +       }
> +
> +       /* If answer too short (= incomplete) break out */
> +       if (ret < 3) {
> +               ret = -EIO;
> +               goto out_err;
> +       }
> +
> +       /* validate first byte */
> +       if (unlikely(buf[0] != addr)) {
> +               ret = (buf[0] == '\0') ? -EAGAIN : -EIO;
> +               goto out_err;
> +       }
> +
> +       /* validate second byte (protocol flag) */
> +       if (unlikely((buf[1] & 0x80) != (p_flag << 7))) {
> +               if (!p_flag || !(quirks & DDCCI_QUIRK_NO_PFLAG)) {
> +                       ret = -EIO;
> +                       goto out_err;
> +               }
> +       }
> +
> +       /* get and check payload length */
> +       payload_len = buf[1] & 0x7F;
> +       if (3+payload_len > packet_length)
> +               return -EBADMSG;
> +       if (3+payload_len > len)
> +               return -EMSGSIZE;
> +
> +       /* calculate checksum */
> +       for (i = 0; i < 3+payload_len; i++)
> +               xor ^= buf[i];
> +
> +       /* verify checksum */
> +       if (xor != 0) {
> +               dev_err(&client->dev, "invalid DDC/CI response, corrupted data - xor is 0x%02x, length 0x%02x\n",
> +                       xor, payload_len);
> +               ret = -EBADMSG;
> +               goto out_err;
> +       }
> +
> +       /* return result */
> +       ret = payload_len+3+((quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE)?1:0);
> +
> +out_err:
> +       return ret;
> +}
> +
> +/*
> + * Read a response from the DDC/CI bus
> + *
> + * You must hold the bus semaphore when calling this function.
> + */
> +static int ddcci_read(struct i2c_client *client, unsigned char addr,
> +                     bool p_flag, unsigned char *buf, unsigned char len)
> +{
> +       struct ddcci_bus_drv_data *drv_data;
> +       unsigned char *recvbuf;
> +       int ret;
> +
> +       drv_data = i2c_get_clientdata(client);
> +       recvbuf = drv_data->recv_buffer;
> +
> +       /* Read frame */
> +       ret = __ddcci_read(client, addr, p_flag,
> +               drv_data->quirks, recvbuf, DDCCI_RECV_BUFFER_SIZE);
> +       if (ret < 0)
> +               return ret;
> +
> +       if (drv_data->quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE)
> +               recvbuf++;
> +
> +       /* return result */
> +       if (buf) {
> +               if (ret > 3) {
> +                       ret = ret-3;
> +                       /* copy to caller buffer */
> +                       memcpy(buf, &recvbuf[2], (ret < len) ? ret : len);
> +
> +                       if (ret > len) {
> +                               /* if message was truncated, return -EMSGSIZE */
> +                               pr_debug("received from %d:%02x:%02x: [%u/%u] %*ph ...\n",
> +                                        client->adapter->nr, client->addr << 1,
> +                                        addr, ret, len, len, buf);
> +                               ret = -EMSGSIZE;
> +                       } else {
> +                               pr_debug("received from %d:%02x:%02x: [%u/%u] %*ph\n",
> +                                        client->adapter->nr, client->addr << 1,
> +                                        addr, ret, len, ret, buf);
> +                       }
> +               }
> +       }
> +       if (!(drv_data->quirks & DDCCI_QUIRK_WRITE_BYTEWISE)) {
> +               /* second read to clear buffers, needed on some devices */
> +               __ddcci_read(client, addr, true, drv_data->quirks, recvbuf, 1);
> +       }
> +       return ret;
> +}
> +
> +/* Request the capability string for a device and put it into buf */
> +static int ddcci_get_caps(struct i2c_client *client, unsigned char addr,
> +                         unsigned char *buf, unsigned int len)
> +{
> +       int result = 0, counter = 0, offset = 0;
> +       unsigned char cmd[3] = { DDCCI_COMMAND_CAPS, 0x00, 0x00 };
> +       unsigned char *chunkbuf = kzalloc(35, GFP_KERNEL);
> +
> +       if (!chunkbuf)
> +               return -ENOMEM;
> +
> +       do {
> +               /* Send command */
> +               result = ddcci_write(client, addr, true, cmd, sizeof(cmd));
> +               if (result < 0)
> +                       goto err_free;
> +               msleep(delay);
> +               /* read result chunk */
> +               result = ddcci_read(client, addr, true, chunkbuf,
> +                                   (len > 32) ? 35 : len+3);
> +               if (result < 0)
> +                       goto err_free;
> +
> +               if (result > 0) {
> +                       /* check chunk header */
> +                       if (chunkbuf[0] != DDCCI_REPLY_CAPS) {
> +                               result = -EIO;
> +                               goto err_free;
> +                       }
> +                       if (chunkbuf[1] != cmd[1] || chunkbuf[2] != cmd[2]) {
> +                               result = -EIO;
> +                               goto err_free;
> +                       }
> +                       if (result < 3) {
> +                               result = -EIO;
> +                               goto err_free;
> +                       }
> +                       memcpy(buf, chunkbuf+3, min((unsigned int)result-3, len));
> +
> +                       counter++;
> +                       /* adjust offset, etc. */
> +                       offset += result-3;
> +                       len -= result-3;
> +                       buf += result-3;
> +                       cmd[1] = offset >> 8;
> +                       cmd[2] = offset & 0xFF;
> +                       /* Another superfluous read to make some devices happy... */
> +                       ddcci_read(client, addr, true, NULL, 2);
> +               }
> +       } while (result > 3 && counter < DDCCI_MAX_CAP_CHUNKS);
> +
> +       kfree(chunkbuf);
> +       return offset+result-3;
> +err_free:
> +       kfree(chunkbuf);
> +       return result;
> +}
> +
> +/*
> + * Request the device identification and put it into buf.
> + *
> + * Also detects all communication quirks and sets the corresponding flags
> + * in the ddcci_bus_drv_data structure associated with client.
> + *
> + * The identification command will fail on most DDC devices, as it is optional
> + * to support, but even the "failed" response suffices to detect quirks.
> + */
> +static int ddcci_identify_device(struct i2c_client *client, unsigned char addr,
> +                                unsigned char *buf, unsigned char len)
> +{
> +       int i, payload_len, ret = -ENODEV;
> +       unsigned long quirks;
> +       unsigned char cmd[1] = { DDCCI_COMMAND_ID };
> +       unsigned char *buffer;
> +       unsigned char xor = DDCCI_HOST_ADDR_EVEN;
> +       struct ddcci_bus_drv_data *bus_drv_data;
> +
> +       bus_drv_data = i2c_get_clientdata(client);
> +       quirks = bus_drv_data->quirks;
> +       buffer = bus_drv_data->recv_buffer;
> +
> +       /* Send Identification command */
> +       if (!(quirks & DDCCI_QUIRK_WRITE_BYTEWISE)) {
> +               ret = __ddcci_write_block(client, addr, buffer, true, cmd, sizeof(cmd));
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] writing identification command in block mode: %d\n",
> +                       client->addr << 1, addr, ret);
> +               if ((ret == -ENXIO)
> +                   && i2c_check_functionality(client->adapter,
> +                                              I2C_FUNC_SMBUS_WRITE_BYTE)) {
> +                       quirks |= DDCCI_QUIRK_WRITE_BYTEWISE;
> +                       dev_info(&client->dev,
> +                               "DDC/CI bus quirk detected: writes must be done bytewise\n");
> +                       /* Some devices need writing twice after a failed blockwise write */
> +                       __ddcci_write_bytewise(client, addr, true, cmd, sizeof(cmd));
> +                       msleep(delay);
> +               }
> +       }
> +       if (ret < 0 && (quirks & DDCCI_QUIRK_WRITE_BYTEWISE)) {
> +               ret = __ddcci_write_bytewise(client, addr, true, cmd, sizeof(cmd));
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] writing identification command in bytewise mode: %d\n",
> +                       client->addr << 1, addr, ret);
> +       }
> +       if (ret < 0)
> +               return -ENODEV;
> +
> +       /* Wait */
> +       msleep(delay);
> +
> +       /* Receive response */
> +       ret = i2c_master_recv(client, buffer, DDCCI_RECV_BUFFER_SIZE);
> +       if (ret < 0) {
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] receiving identification response resulted in errno %d\n",
> +                       client->addr << 1, addr, ret);
> +               return ret;
> +       }
> +
> +       if (ret == 0) {
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] no identification response received\n",
> +                       client->addr << 1, addr);
> +               return ret;
> +       }
> +
> +       /* Skip first byte if quirk already active */
> +       if (quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE && ret > 1) {
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] doubled first byte quirk in effect\n",
> +                       client->addr << 1, addr);
> +               ret--;
> +               buffer++;
> +       }
> +
> +       /* If answer too short (= incomplete) break out */
> +       if (ret < 3) {
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] identification response is too short (%d bytes)\n",
> +                       client->addr << 1, addr, ret);
> +               return -EIO;
> +       }
> +
> +       /* validate first byte */
> +       if (buffer[0] != addr) {
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] identification response: %*ph\n",
> +                       client->addr << 1, addr, (ret > 32 ? 32 : ret), buffer);
> +
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] identification response invalid (expected first byte %02x, got %02x)\n",
> +                       client->addr << 1, addr, addr, buffer[0]);
> +               return -ENODEV;
> +       }
> +
> +       /* Check if first byte is doubled (QUIRK_SKIP_FIRST_BYTE) */
> +       if (!(quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE)) {
> +               if (buffer[0] == buffer[1]) {
> +                       quirks |= DDCCI_QUIRK_SKIP_FIRST_BYTE;
> +                       dev_info(&client->dev,
> +                               "DDC/CI bus quirk detected: doubled first byte on read\n");
> +                       ret--;
> +                       buffer++;
> +                       if (ret < 3)
> +                               return -EIO;
> +               }
> +       }
> +
> +       /* validate second byte (protocol flag) */
> +       if ((buffer[1] & 0x80) != 0x80 && !(quirks & DDCCI_QUIRK_NO_PFLAG)) {
> +               dev_info(&client->dev,
> +                       "DDC/CI bus quirk detected: device omits protocol flag on responses\n");
> +               quirks |= DDCCI_QUIRK_NO_PFLAG;
> +       }
> +
> +       /* get and check payload length */
> +       payload_len = buffer[1] & 0x7F;
> +       if (3+payload_len > ret) {
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] identification response: %*ph ...\n",
> +                       client->addr << 1, addr, ret, buffer);
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] identification response was truncated (expected %d bytes, got %d)\n",
> +                       client->addr << 1, addr, 3+payload_len, ret);
> +               return -EBADMSG;
> +       }
> +
> +       dev_dbg(&client->dev,
> +               "[%02x:%02x] identification response: %*ph\n",
> +               client->addr << 1, addr, 3+payload_len, buffer);
> +
> +       /* calculate checksum */
> +       for (i = 0; i < 3+payload_len; i++)
> +               xor ^= buffer[i];
> +
> +       /* verify checksum */
> +       if (xor != 0) {
> +               dev_err(&client->dev,
> +                       "[%02x:%02x] invalid DDC/CI response, corrupted data - xor is 0x%02x, length 0x%02x\n",
> +                       client->addr << 1, addr, xor, payload_len);
> +               return -EBADMSG;
> +       }
> +
> +       /* save quirks */
> +       bus_drv_data->quirks = quirks;
> +
> +       /* return result */
> +       if (payload_len <= len) {
> +               ret = payload_len;
> +               memcpy(buf, &buffer[2], payload_len);
> +       } else {
> +               ret = -EMSGSIZE;
> +               memcpy(buf, &buffer[2], len);
> +       }
> +       return ret;
> +}
> +
> +/* Character device */
> +
> +/* Data structure for an open file handle */
> +struct ddcci_fp_data {
> +       struct ddcci_device *dev;
> +       bool exclusive;
> +       unsigned char buffer[129];
> +};
> +
> +/* Called when the character device is opened */
> +static int ddcci_cdev_open(struct inode *inode, struct file *filp)
> +{
> +       struct ddcci_device *dev = container_of(inode->i_cdev,
> +                                               struct ddcci_device, cdev);
> +       struct ddcci_fp_data *fp_data = NULL;
> +
> +       fp_data = kzalloc(sizeof(struct ddcci_fp_data), GFP_KERNEL);
> +
> +       if (!fp_data)
> +               return -ENOMEM;
> +
> +       fp_data->exclusive = filp->f_flags & O_EXCL;
> +
> +       if (fp_data->exclusive) {
> +               if (down_write_trylock(&dev->cdev_sem) == 0) {
> +                       kfree(fp_data);
> +                       return -EBUSY;
> +               }
> +       } else {
> +               if (down_read_trylock(&dev->cdev_sem) == 0) {
> +                       kfree(fp_data);
> +                       return -EBUSY;
> +               }
> +       }
> +
> +       fp_data->dev = dev;
> +       filp->private_data = fp_data;
> +
> +       return 0;
> +}
> +
> +/* Called when the character device is closed */
> +static int ddcci_cdev_close(struct inode *inode, struct file *filp)
> +{
> +       struct ddcci_fp_data *fp_data = filp->private_data;
> +       struct ddcci_device *dev = fp_data->dev;
> +
> +       if (fp_data->exclusive)
> +               up_write(&dev->cdev_sem);
> +       else
> +               up_read(&dev->cdev_sem);
> +
> +       filp->private_data = NULL;
> +       kfree(fp_data);
> +       return 0;
> +}
> +
> +/* Called when reading from the character device */
> +static ssize_t ddcci_cdev_read(struct file *filp, char __user *buffer,
> +                              size_t length, loff_t *offset)
> +{
> +       struct ddcci_fp_data *fp_data = filp->private_data;
> +       struct ddcci_device *dev = fp_data->dev;
> +       unsigned char *buf = fp_data->buffer;
> +       const bool nonblocking = (filp->f_flags & O_NONBLOCK) != 0;
> +       int ret = 0;
> +
> +       if ((filp->f_mode & FMODE_READ) == 0)
> +               return -EBADF;
> +
> +       /* Lock mutex */
> +       if (nonblocking) {
> +               if (down_trylock(&dev->bus_drv_data->sem))
> +                       return -EAGAIN;
> +       } else {
> +               if (down_interruptible(&dev->bus_drv_data->sem))
> +                       return -ERESTARTSYS;
> +       }
> +
> +       /* Execute read */
> +       ret = ddcci_read(dev->bus_drv_data->i2c_dev, dev->inner_addr, true, buf,
> +                        length);
> +
> +       if (ret > 0) {
> +               /* Copy data from user space */
> +               if (copy_to_user(buffer, buf, ret)) {
> +                       ret = -EFAULT;
> +                       goto out;
> +               }
> +       }
> +
> +out:
> +       up(&dev->bus_drv_data->sem);
> +       return ret;
> +}
> +
> +/* Called when writing to the character device */
> +static ssize_t ddcci_cdev_write(struct file *filp, const char __user *buffer,
> +                               size_t count, loff_t *offset)
> +{
> +       struct ddcci_fp_data *fp_data = filp->private_data;
> +       struct ddcci_device *dev = fp_data->dev;
> +       unsigned char *buf = fp_data->buffer;
> +       const bool nonblocking = (filp->f_flags & O_NONBLOCK) != 0;
> +       int ret = 0;
> +
> +       if ((filp->f_mode & FMODE_WRITE) == 0)
> +               return -EBADF;
> +
> +       if (count > 127)
> +               return -EINVAL;
> +
> +       /* Lock mutex */
> +       if (nonblocking) {
> +               if (down_trylock(&dev->bus_drv_data->sem))
> +                       return -EAGAIN;
> +       } else {
> +               if (down_interruptible(&dev->bus_drv_data->sem))
> +                       return -ERESTARTSYS;
> +       }
> +
> +       if (count > 0) {
> +               /* Copy data from user space */
> +               if (copy_from_user(buf, buffer, count)) {
> +                       ret = -EFAULT;
> +                       goto err_out;
> +               }
> +
> +               /* Execute write */
> +               ret = ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr,
> +                                 true, buf, count);
> +       }
> +
> +       if (ret >= 0) {
> +               msleep(delay);
> +               up(&dev->bus_drv_data->sem);
> +               return count;
> +       }
> +
> +err_out:
> +       up(&dev->bus_drv_data->sem);
> +       return ret;
> +}
> +
> +/* Called when seeking the character device */
> +static loff_t ddcci_cdev_seek(struct file *filp, loff_t offset, int anchor)
> +{
> +       return -EINVAL;
> +}
> +
> +static const struct file_operations ddcci_fops = {
> +       .owner = THIS_MODULE,
> +       .read = ddcci_cdev_read,
> +       .write = ddcci_cdev_write,
> +       .open = ddcci_cdev_open,
> +       .release = ddcci_cdev_close,
> +       .llseek = ddcci_cdev_seek
> +};
> +
> +/* Set up the character device for a DDC/CI device */
> +static int ddcci_setup_char_device(struct ddcci_device *device)
> +{
> +       int ret = -EINVAL;
> +
> +       /* Check if free minor exists */
> +       if (ddcci_cdev_next == ddcci_cdev_end) {
> +               dev_err(&device->dev, "no free major/minor\n");
> +               ret = -ENFILE;
> +               goto out;
> +       }
> +
> +       /* Initialize rwsem */
> +       init_rwsem(&device->cdev_sem);
> +
> +       /* Initialize character device node */
> +       cdev_init(&device->cdev, &ddcci_fops);
> +       device->cdev.owner = THIS_MODULE;
> +
> +       /* Publish char device */
> +       device->dev.devt = ddcci_cdev_next;
> +       ret = cdev_add(&device->cdev, ddcci_cdev_next, 1);
> +       if (ret) {
> +               device->dev.devt = 0;
> +               goto out;
> +       }
> +
> +       ddcci_cdev_next++;
> +out:
> +       return ret;
> +}
> +
> +/* sysfs attributes */
> +
> +static ssize_t ddcci_attr_capabilities_show(struct device *dev,
> +                                           struct device_attribute *attr,
> +                                           char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +       size_t len;
> +
> +       if (likely(device != NULL)) {
> +               len = device->capabilities_len;
> +               if (unlikely(len > PAGE_SIZE))
> +                       len = PAGE_SIZE;
> +               if (len == 0) {
> +                       ret = len;
> +               } else {
> +                       memcpy(buf, device->capabilities, len);
> +                       if (likely(len < PAGE_SIZE)) {
> +                               buf[len] = '\n';
> +                               ret = len+1;
> +                       }
> +               }
> +       }
> +
> +       return ret;
> +}
> +
> +static ssize_t ddcci_attr_prot_show(struct device *dev,
> +                                   struct device_attribute *attr, char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +       size_t len;
> +
> +       if (likely(device != NULL)) {
> +               len = strnlen(device->prot, sizeof(device->prot));
> +               strncpy(buf, device->prot, PAGE_SIZE);
> +               if (len == 0) {
> +                       ret = len;
> +               } else if (likely(len < PAGE_SIZE)) {
> +                       buf[len] = '\n';
> +                       ret = len+1;
> +               } else {
> +                       ret = PAGE_SIZE;
> +               }
> +       }
> +       return ret;
> +}
> +
> +static ssize_t ddcci_attr_type_show(struct device *dev,
> +                                   struct device_attribute *attr, char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +       size_t len;
> +
> +       if (likely(device != NULL)) {
> +               len = strnlen(device->type, sizeof(device->type));
> +               strncpy(buf, device->type, PAGE_SIZE);
> +               if (len == 0) {
> +                       ret = len;
> +               } else if (likely(len < PAGE_SIZE)) {
> +                       buf[len] = '\n';
> +                       ret = len+1;
> +               } else {
> +                       ret = PAGE_SIZE;
> +               }
> +       }
> +       return ret;
> +}
> +
> +static ssize_t ddcci_attr_model_show(struct device *dev,
> +                                    struct device_attribute *attr, char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +       size_t len;
> +
> +       if (likely(device != NULL)) {
> +               len = strnlen(device->model, sizeof(device->model));
> +               strncpy(buf, device->model, PAGE_SIZE);
> +               if (len == 0) {
> +                       ret = len;
> +               } else if (likely(len < PAGE_SIZE)) {
> +                       buf[len] = '\n';
> +                       ret = len+1;
> +               } else {
> +                       ret = PAGE_SIZE;
> +               }
> +       }
> +       return ret;
> +}
> +
> +static ssize_t ddcci_attr_vendor_show(struct device *dev,
> +                                     struct device_attribute *attr, char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +       size_t len;
> +
> +       if (likely(device != NULL)) {
> +               len = strnlen(device->vendor, sizeof(device->vendor));
> +               strncpy(buf, device->vendor, PAGE_SIZE);
> +               if (len == 0) {
> +                       ret = len;
> +               } else if (likely(len < PAGE_SIZE)) {
> +                       buf[len] = '\n';
> +                       ret = len+1;
> +               } else {
> +                       ret = PAGE_SIZE;
> +               }
> +       }
> +       return ret;
> +}
> +
> +static ssize_t ddcci_attr_module_show(struct device *dev,
> +                                     struct device_attribute *attr, char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +       size_t len;
> +
> +       if (likely(device != NULL)) {
> +               len = strnlen(device->module, sizeof(device->module));
> +               strncpy(buf, device->module, PAGE_SIZE);
> +               if (len == 0) {
> +                       ret = len;
> +               } else if (likely(len < PAGE_SIZE)) {
> +                       buf[len] = '\n';
> +                       ret = len+1;
> +               } else {
> +                       ret = PAGE_SIZE;
> +               }
> +       }
> +       return ret;
> +}
> +
> +static ssize_t ddcci_attr_serial_show(struct device *dev,
> +                                     struct device_attribute *attr, char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +
> +       if (likely(device != NULL))
> +               ret = scnprintf(buf, PAGE_SIZE, "%d\n", device->device_number);
> +
> +       return ret;
> +}
> +
> +static ssize_t ddcci_attr_modalias_show(struct device *dev,
> +                                     struct device_attribute *attr, char *buf)
> +{
> +       struct ddcci_device *device = ddcci_verify_device(dev);
> +       ssize_t ret = -ENOENT;
> +       char model[ARRAY_SIZE(device->model)];
> +       char vendor[ARRAY_SIZE(device->model)];
> +       char module[ARRAY_SIZE(device->model)];
> +
> +       if (likely(device != NULL)) {
> +               memcpy(model, device->model, sizeof(model));
> +               memcpy(vendor, device->vendor, sizeof(vendor));
> +               memcpy(module, device->module, sizeof(module));
> +               ddcci_modalias_clean(model, sizeof(model), '_');
> +               ddcci_modalias_clean(vendor, sizeof(vendor), '_');
> +               ddcci_modalias_clean(module, sizeof(module), '_');
> +
> +               ret = scnprintf(buf, PAGE_SIZE, "%s%s-%s-%s-%s-%s\n",
> +                       DDCCI_MODULE_PREFIX,
> +                       device->prot,
> +                       device->type,
> +                       model,
> +                       vendor,
> +                       module
> +               );
> +       }
> +       return ret;
> +}
> +
> +static DEVICE_ATTR(capabilities, S_IRUGO, ddcci_attr_capabilities_show, NULL);
> +static DEVICE_ATTR(idProt, S_IRUGO, ddcci_attr_prot_show, NULL);
> +static DEVICE_ATTR(idType, S_IRUGO, ddcci_attr_type_show, NULL);
> +static DEVICE_ATTR(idModel, S_IRUGO, ddcci_attr_model_show, NULL);
> +static DEVICE_ATTR(idVendor, S_IRUGO, ddcci_attr_vendor_show, NULL);
> +static DEVICE_ATTR(idModule, S_IRUGO, ddcci_attr_module_show, NULL);
> +static DEVICE_ATTR(idSerial, S_IRUGO, ddcci_attr_serial_show, NULL);
> +static DEVICE_ATTR(modalias, S_IRUGO, ddcci_attr_modalias_show, NULL);
> +
> +static struct attribute *ddcci_char_device_attrs[] = {
> +       &dev_attr_capabilities.attr,
> +       &dev_attr_idProt.attr,
> +       &dev_attr_idType.attr,
> +       &dev_attr_idModel.attr,
> +       &dev_attr_idVendor.attr,
> +       &dev_attr_idModule.attr,
> +       &dev_attr_idSerial.attr,
> +       &dev_attr_modalias.attr,
> +       NULL,
> +};
> +ATTRIBUTE_GROUPS(ddcci_char_device);
> +
> +/* DDC/CI bus */
> +
> +static int ddcci_device_uevent(struct device *dev, struct kobj_uevent_env *env)
> +{
> +       struct ddcci_device     *device = to_ddcci_device(dev);
> +       char model[ARRAY_SIZE(device->model)];
> +       char vendor[ARRAY_SIZE(device->vendor)];
> +       char module[ARRAY_SIZE(device->module)];
> +
> +       memcpy(model, device->model, sizeof(model));
> +       memcpy(vendor, device->vendor, sizeof(vendor));
> +       memcpy(module, device->module, sizeof(module));
> +       ddcci_modalias_clean(model, sizeof(model), '_');
> +       ddcci_modalias_clean(vendor, sizeof(vendor), '_');
> +       ddcci_modalias_clean(module, sizeof(module), '_');
> +
> +       if (add_uevent_var(env, "MODALIAS=%s%s-%s-%s-%s-%s",
> +                          DDCCI_MODULE_PREFIX,
> +                          device->prot,
> +                          device->type,
> +                          model,
> +                          vendor,
> +                          module
> +               ))
> +               return -ENOMEM;
> +
> +       if (device->prot[0])
> +               if (add_uevent_var(env, "DDCCI_PROT=%s", device->prot))
> +                       return -ENOMEM;
> +
> +       if (device->type[0])
> +               if (add_uevent_var(env, "DDCCI_TYPE=%s", device->type))
> +                       return -ENOMEM;
> +
> +       if (device->model[0])
> +               if (add_uevent_var(env, "DDCCI_MODEL=%s", device->model))
> +                       return -ENOMEM;
> +
> +       if (device->vendor[0]) {
> +               if (add_uevent_var(env, "DDCCI_VENDOR=%s", device->vendor))
> +                       return -ENOMEM;
> +
> +               if (add_uevent_var(env, "DDCCI_MODULE=%s", device->module))
> +                       return -ENOMEM;
> +
> +               if (add_uevent_var(env, "DDCCI_UNIQ=%d", device->device_number))
> +                       return -ENOMEM;
> +       }
> +
> +       return 0;
> +}
> +
> +static void ddcci_device_release(struct device *dev)
> +{
> +       struct ddcci_device *device = to_ddcci_device(dev);
> +       struct ddcci_driver *driver;
> +
> +       /* Notify driver */
> +       if (dev->driver) {
> +               driver = to_ddcci_driver(dev->driver);
> +               if (driver->remove)
> +                       driver->remove(device);
> +       }
> +
> +       /* Teardown chardev */
> +       if (dev->devt) {
> +               down(&core_lock);
> +               if (device->cdev.dev == ddcci_cdev_next-1)
> +                       ddcci_cdev_next--;
> +               cdev_del(&device->cdev);
> +               up(&core_lock);
> +       }
> +
> +       /* Free capability string */
> +       if (device->capabilities) {
> +               device->capabilities_len = 0;
> +               kfree(device->capabilities);
> +       }
> +       /* Free device */
> +       kfree(device);
> +}
> +
> +static char *ddcci_devnode(struct device *dev,
> +                        umode_t *mode, kuid_t *uid, kgid_t *gid)
> +{
> +       struct ddcci_device *device;
> +
> +       device = to_ddcci_device(dev);
> +       return kasprintf(GFP_KERNEL, "bus/ddcci/%d/display",
> +                        device->i2c_client->adapter->nr);
> +}
> +
> +static char *ddcci_dependent_devnode(struct device *dev,
> +                        umode_t *mode, kuid_t *uid, kgid_t *gid)
> +{
> +       struct ddcci_device *device;
> +
> +       device = to_ddcci_device(dev);
> +       if (device->flags & DDCCI_FLAG_EXTERNAL) {
> +               if (device->outer_addr == device->inner_addr)
> +                       return kasprintf(GFP_KERNEL, "bus/ddcci/%d/e%02x",
> +                                        device->i2c_client->adapter->nr,
> +                                        device->outer_addr);
> +               else
> +                       return kasprintf(GFP_KERNEL, "bus/ddcci/%d/e%02x%02x",
> +                                        device->i2c_client->adapter->nr,
> +                                        device->outer_addr, device->inner_addr);
> +       } else {
> +               return kasprintf(GFP_KERNEL, "bus/ddcci/%d/i%02x",
> +                                device->i2c_client->adapter->nr,
> +                                device->inner_addr);
> +       }
> +}
> +
> +/* Device type for main DDC/CI devices*/
> +static struct device_type ddcci_device_type = {
> +       .name   = "ddcci-device",
> +       .uevent         = ddcci_device_uevent,
> +       .groups         = ddcci_char_device_groups,
> +       .release        = ddcci_device_release,
> +       .devnode        = ddcci_devnode
> +};
> +
> +/* Device type for dependent DDC/CI devices*/
> +static struct device_type ddcci_dependent_type = {
> +       .name   = "ddcci-dependent-device",
> +       .uevent         = ddcci_device_uevent,
> +       .groups         = ddcci_char_device_groups,
> +       .release        = ddcci_device_release,
> +       .devnode        = ddcci_dependent_devnode
> +};
> +
> +/**
> + * ddcci_verify_device - return parameter as ddcci_device, or NULL
> + * @dev: device, probably from some driver model iterator
> + */
> +struct ddcci_device *ddcci_verify_device(struct device *dev)
> +{
> +       if (unlikely(!dev))
> +               return NULL;
> +       return (dev->type == &ddcci_device_type
> +               || dev->type == &ddcci_dependent_type)
> +                       ? to_ddcci_device(dev)
> +                       : NULL;
> +}
> +EXPORT_SYMBOL(ddcci_verify_device);
> +
> +/**
> + * ddcci_quirks - Get quirks for DDC/CI device
> + * @dev: Target DDC/CI device
> + */
> +unsigned long ddcci_quirks(struct ddcci_device *dev)
> +{
> +       if (unlikely(WARN_ON(!dev)))
> +               return ~0L;
> +       if (unlikely(WARN_ON(!dev->bus_drv_data)))
> +               return ~0L;
> +       return dev->bus_drv_data->quirks;
> +}
> +EXPORT_SYMBOL(ddcci_quirks);
> +
> +/**
> + * ddcci_register_driver - register DDC/CI driver
> + * @owner: the owning module
> + * @driver: the driver to register
> + */
> +int ddcci_register_driver(struct module *owner, struct ddcci_driver *driver)
> +{
> +       int ret;
> +
> +       /* Can't register until after driver model init */
> +       if (unlikely(WARN_ON(!ddcci_bus_type.p)))
> +               return -EAGAIN;
> +
> +       pr_debug("registering driver [%s]\n", driver->driver.name);
> +
> +       /* add the driver to the list of ddcci drivers in the driver core */
> +       driver->driver.owner = owner;
> +       driver->driver.bus = &ddcci_bus_type;
> +
> +       /* When registration returns, the driver core
> +        * will have called probe() for all matching-but-unbound devices.
> +        */
> +       ret = driver_register(&driver->driver);
> +       if (ret)
> +               return ret;
> +
> +       pr_debug("driver [%s] registered\n", driver->driver.name);
> +
> +       return 0;
> +}
> +EXPORT_SYMBOL(ddcci_register_driver);
> +
> +/**
> + * ddcci_del_driver - unregister DDC/CI driver
> + * @driver: the driver being unregistered
> + */
> +void ddcci_del_driver(struct ddcci_driver *driver)
> +{
> +       driver_unregister(&driver->driver);
> +       pr_debug("driver [%s] unregistered\n", driver->driver.name);
> +}
> +EXPORT_SYMBOL(ddcci_del_driver);
> +
> +/**
> + * ddcci_device_write - Write a message to a DDC/CI device
> + * @dev: Target DDC/CI device
> + * @p_flag: Protocol flag, true for standard control messages
> + * @data: Data that will be written to the device
> + * @length: How many bytes to write
> + *
> + * Writes the message to the device and sleeps (see module parameter 'delay')
> + */
> +int ddcci_device_write(struct ddcci_device *dev, bool p_flag,
> +                      unsigned char *data, unsigned char length)
> +{
> +       int ret;
> +
> +       if (down_interruptible(&dev->bus_drv_data->sem))
> +               return -EAGAIN;
> +
> +       ret = ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, data, length);
> +       msleep(delay);
> +       up(&dev->bus_drv_data->sem);
> +       return ret;
> +}
> +EXPORT_SYMBOL(ddcci_device_write);
> +
> +/**
> + * ddcci_device_read - Read a response from a DDC/CI device
> + * @dev: Target DDC/CI device
> + * @p_flag: Protocol flag, must match the corresponding write
> + * @buffer: Where to store data read from the device
> + * @length: Buffer size
> + */
> +int ddcci_device_read(struct ddcci_device *dev, bool p_flag,
> +                     unsigned char *buffer, unsigned char length)
> +{
> +       int ret;
> +
> +       if (down_interruptible(&dev->bus_drv_data->sem))
> +               return -EAGAIN;
> +
> +       ret = ddcci_read(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, buffer, length);
> +       up(&dev->bus_drv_data->sem);
> +       return ret;
> +}
> +EXPORT_SYMBOL(ddcci_device_read);
> +
> +/**
> + * ddcci_device_writeread - Write a message to a device and read the response
> + * @dev: Target DDC/CI device
> + * @p_flag: Protocol flag, true for standard control messages
> + * @buffer: Buffer used for write and read
> + * @length: How many bytes to write
> + * @maxlength: Buffer size on read
> + *
> + * Writing, sleeping and reading are done without releasing the DDC/CI bus.
> + * This provides atomicity in respect to other DDC/CI accesses on the same bus.
> + */
> +int ddcci_device_writeread(struct ddcci_device *dev, bool p_flag,
> +                          unsigned char *buffer, unsigned char length,
> +                          unsigned char maxlength)
> +{
> +       int ret;
> +
> +       if (down_interruptible(&dev->bus_drv_data->sem))
> +               return -EAGAIN;
> +
> +       ret = ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, buffer, length);
> +       if (ret < 0)
> +               goto err;
> +       msleep(delay);
> +       ret = ddcci_read(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, buffer, maxlength);
> +err:
> +       up(&dev->bus_drv_data->sem);
> +       return ret;
> +}
> +EXPORT_SYMBOL(ddcci_device_writeread);
> +
> +#define IS_ANY_ID(x) (((x)[0] == -1) && ((x)[7] == -1))
> +
> +/* Check if any device id in the array matches the device and return the matching id */
> +static const struct ddcci_device_id *ddcci_match_id(const struct ddcci_device_id *id,
> +                                                   const struct ddcci_device *device)
> +{
> +       while (id->prot[0] || id->type[0] || id->model[0] || id->vendor[0] || id->module[0]) {
> +               if ((IS_ANY_ID(id->prot) || (strcmp(device->prot, id->prot) == 0))
> +                && (IS_ANY_ID(id->type) || (strcmp(device->type, id->type) == 0))
> +                && (IS_ANY_ID(id->model) || (strcmp(device->model, id->model) == 0))
> +                && (IS_ANY_ID(id->vendor) || (strcmp(device->vendor, id->vendor) == 0))
> +                && (IS_ANY_ID(id->module) || (strcmp(device->module, id->module) == 0))) {
> +                       return id;
> +               }
> +               id++;
> +       }
> +       return NULL;
> +}
> +
> +static int ddcci_device_match(struct device *dev, struct device_driver *drv)
> +{
> +       struct ddcci_device     *device = ddcci_verify_device(dev);
> +       struct ddcci_driver     *driver;
> +
> +       if (!device)
> +               return 0;
> +
> +       driver = to_ddcci_driver(drv);
> +       /* match on an id table if there is one */
> +       if (driver->id_table)
> +               return ddcci_match_id(driver->id_table, device) != NULL;
> +
> +       return 0;
> +}
> +
> +static int ddcci_device_probe(struct device *dev)
> +{
> +       struct ddcci_device     *device = ddcci_verify_device(dev);
> +       struct ddcci_driver     *driver;
> +       const struct ddcci_device_id *id;
> +       int ret = 0;
> +
> +       if (!device)
> +               return -EINVAL;
> +       driver = to_ddcci_driver(dev->driver);
> +
> +       id = ddcci_match_id(driver->id_table, device);
> +       if (!id)
> +               return -ENODEV;
> +
> +       if (driver->probe)
> +               ret = driver->probe(device, id);
> +
> +       return ret;
> +}
> +
> +static int ddcci_device_remove(struct device *dev)
> +{
> +       struct ddcci_device     *device = ddcci_verify_device(dev);
> +       struct ddcci_driver     *driver;
> +       int ret = 0;
> +
> +       if (!device)
> +               return -EINVAL;
> +       driver = to_ddcci_driver(dev->driver);
> +
> +       if (driver->remove)
> +               ret = driver->remove(device);
> +
> +       return ret;
> +}
> +
> +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0)
> +static void ddcci_device_remove_void(struct device *dev)
> +{
> +       ddcci_device_remove(dev);
> +}
> +#endif
> +
> +/**
> + * DDCCI bus type structure
> + */
> +struct bus_type ddcci_bus_type = {
> +       .name           = "ddcci",
> +       .match          = ddcci_device_match,
> +       .probe          = ddcci_device_probe,
> +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0)
> +       .remove         = ddcci_device_remove_void
> +#else
> +       .remove         = ddcci_device_remove
> +#endif
> +};
> +
> +/* Main I2C driver */
> +
> +/* Get a pointer to the closing parenthesis */
> +static char *ddcci_capstr_tok(const char *s, int depth)
> +{
> +       const char *ptr = s;
> +       char *end;
> +
> +       if (s == NULL || s[0] == '\0')
> +               return NULL;
> +
> +       while ((end = strpbrk(ptr, "()"))) {
> +               if (!end || depth == INT_MAX)
> +                       return NULL;
> +               if (*end == '(')
> +                       depth++;
> +               else if (depth > 0)
> +                       depth--;
> +               else
> +                       break;
> +               ptr = end+1;
> +       }
> +       return end;
> +}
> +
> +/**
> + * ddcci_find_capstr_item - Search capability string for a tag
> + * @capabilities: Capability string to search
> + * @tag: Tag to find
> + * @length: Buffer for the length of the found tag value (optional)
> + *
> + * Return a pointer to the start of the tag value (directly after the '(') on
> + * success and write the length of the value (excluding the ')') into `length`.
> + *
> + * If the tag is not found or another error occurs, an ERR_PTR is returned
> + * and `length` stays untouched.
> + */
> +const char *ddcci_find_capstr_item(const char * capabilities,
> +                                  const char * __restrict tag,
> +                                  size_t *length)
> +{
> +       const char *src = capabilities, *ptr;
> +       ptrdiff_t len;
> +       int taglen = strnlen(tag, 1024);
> +
> +       /* Check length of requested tag */
> +       if (unlikely(taglen <= 0 || taglen > 1023))
> +               return ERR_PTR(-EINVAL);
> +
> +       /* Find tag */
> +       while (src && (strncmp(src+1, tag, taglen) != 0 || src[1+taglen] != '('))
> +               src = ddcci_capstr_tok(src+1, -1);
> +       if (!src || src[0] == '\0')
> +               return ERR_PTR(-ENOENT);
> +
> +       /* Locate end of value */
> +       src += taglen+2;
> +       ptr = ddcci_capstr_tok(src, 0);
> +       if (unlikely(!ptr))
> +               return ERR_PTR(-EOVERFLOW);
> +
> +       /* Check length of tag data */
> +       len = ptr-src;
> +       if (unlikely(len < 0 || len > 65535))
> +               return ERR_PTR(-EMSGSIZE);
> +
> +       /* Return pointer and length */
> +       if (likely(length != NULL))
> +               *length = (size_t)len;
> +       return src;
> +}
> +EXPORT_SYMBOL(ddcci_find_capstr_item);
> +
> +/* Search the capability string for a tag and copy the value to dest */
> +static int ddcci_cpy_capstr_item(char *dest, const char *src,
> +                                 const char * __restrict tag, size_t maxlen)
> +{
> +       const char *ptr;
> +       size_t len;
> +
> +       /* Find tag */
> +       ptr = ddcci_find_capstr_item(src, tag, &len);
> +       if (IS_ERR(ptr)) {
> +               return PTR_ERR(ptr);
> +       }
> +
> +       /* Copy value */
> +       memcpy(dest, ptr, min(len, maxlen));
> +       return 0;
> +}
> +
> +/* Fill fields in device by parsing the capability string */
> +static int ddcci_parse_capstring(struct ddcci_device *device)
> +{
> +       const char *capstr = device->capabilities;
> +       int ret = 0;
> +
> +       if (!capstr)
> +               return -EINVAL;
> +
> +       /* capability string start with a paren */
> +       if (capstr[0] != '(')
> +               return -EINVAL;
> +
> +       /* get prot(...) */
> +       ret = ddcci_cpy_capstr_item(device->prot, capstr, "prot", sizeof(device->prot)-1);
> +       if (ret) {
> +               if (ret == -ENOENT) {
> +                       dev_warn(&device->dev, "malformed capability string: no protocol tag");
> +                       memset(device->prot, 0, sizeof(device->prot)-1);
> +               } else {
> +                       return ret;
> +               }
> +       }
> +
> +       /* get type(...) */
> +       ret = ddcci_cpy_capstr_item(device->type, capstr, "type", sizeof(device->type)-1);
> +       if (ret) {
> +               if (ret == -ENOENT) {
> +                       dev_warn(&device->dev, "malformed capability string: no type tag");
> +                       memset(device->type, 0, sizeof(device->type)-1);
> +               } else {
> +                       return ret;
> +               }
> +       }
> +
> +       /* and then model(...) */
> +       ret = ddcci_cpy_capstr_item(device->model, capstr, "model", sizeof(device->model)-1);
> +       if (ret) {
> +               if (ret == -ENOENT) {
> +                       dev_warn(&device->dev, "malformed capability string: no model tag");
> +                       memset(device->model, 0, sizeof(device->model)-1);
> +               } else {
> +                       return ret;
> +               }
> +       }
> +
> +       /* if there is no protocol tag */
> +       if (!device->prot[0]) {
> +               /* and no type tag: give up. */
> +               if (!device->type[0])
> +                       return -ENOENT;
> +
> +               /* Assume protocol "monitor" if type is "LCD" or "CRT" */
> +               if (strncasecmp(device->type, "LCD", sizeof(device->type)-1) == 0
> +                || strncasecmp(device->type, "CRT", sizeof(device->type)-1) == 0) {
> +                       memcpy(device->prot, "monitor", 7);
> +               }
> +       }
> +
> +       /* skip the rest for now */
> +
> +       return 0;
> +}
> +
> +/* Probe for a device on an inner address and create a ddcci_device for it */
> +static int ddcci_detect_device(struct i2c_client *client, unsigned char addr,
> +                              int dependent)
> +{
> +       int ret;
> +       unsigned char outer_addr = client->addr << 1;
> +       unsigned char *buffer = NULL;
> +       struct ddcci_bus_drv_data *drv_data = i2c_get_clientdata(client);
> +       struct ddcci_device *device = NULL;
> +
> +       down(&drv_data->sem);
> +
> +       /* Allocate buffer big enough for any capability string */
> +       buffer = kmalloc(16384, GFP_KERNEL);
> +       if (!buffer) {
> +               ret = -ENOMEM;
> +               goto err_end;
> +       }
> +
> +       /* Allocate device struct */
> +       device = kzalloc(sizeof(struct ddcci_device), GFP_KERNEL);
> +       if (!device) {
> +               ret = -ENOMEM;
> +               goto err_end;
> +       }
> +
> +       /* Initialize device */
> +       device_initialize(&device->dev);
> +       device->dev.parent = &client->dev;
> +       device->dev.bus = &ddcci_bus_type;
> +       device->outer_addr = outer_addr;
> +       device->inner_addr = addr;
> +       device->bus_drv_data = drv_data;
> +       device->i2c_client = client;
> +
> +       if (!dependent) {
> +               device->dev.type = &ddcci_device_type;
> +               ret = dev_set_name(&device->dev, "ddcci%d", client->adapter->nr);
> +       } else if (outer_addr == dependent) {
> +               /* Internal dependent device */
> +               device->dev.type = &ddcci_dependent_type;
> +               device->flags = DDCCI_FLAG_DEPENDENT;
> +               ret = dev_set_name(&device->dev, "ddcci%di%02x", client->adapter->nr, addr);
> +       } else if (outer_addr == addr) {
> +               /* External dependent device */
> +               device->dev.type = &ddcci_dependent_type;
> +               device->flags = DDCCI_FLAG_DEPENDENT | DDCCI_FLAG_EXTERNAL;
> +               ret = dev_set_name(&device->dev, "ddcci%de%02x", client->adapter->nr, addr);
> +       } else {
> +               /* Dependent device of external dependent device
> +                  Just in case something like this exists */
> +               device->dev.type = &ddcci_dependent_type;
> +               device->flags = DDCCI_FLAG_DEPENDENT | DDCCI_FLAG_EXTERNAL;
> +               ret = dev_set_name(&device->dev, "ddcci%de%02x%02x", client->adapter->nr, outer_addr, addr);
> +       }
> +
> +       if (ret)
> +               goto err_free;
> +
> +       /* Read identification and check for quirks */
> +       ret = ddcci_identify_device(client, addr, buffer, 29);
> +       if (ret < 0) {
> +               if (!dependent && (ret == -EBADMSG || ret == -EMSGSIZE)) {
> +                       dev_warn(&device->dev, "DDC/CI main device sent broken response on identification. Trying to detect solely based on capability information.\n");
> +               } else {
> +                       goto err_free;
> +               }
> +       }
> +
> +       if (ret == 29 && buffer[0] == DDCCI_REPLY_ID) {
> +               memcpy(device->vendor, &buffer[7], 8);
> +               memcpy(device->module, &buffer[17], 8);
> +               device->device_number = be32_to_cpu(*(__force __be32 *)&buffer[18]);
> +       }
> +
> +       /* Read capabilities */
> +       ret = ddcci_get_caps(client, addr, buffer, 16384);
> +       if (ret > 0) {
> +               /* Fixup unparenthesized capability strings, but only if the first
> +                  character is an ascii lower case letter.
> +                  This should still allow an early exit for completely garbled
> +                  data but helps detecting devices where only the parentheses are
> +                  missing, as the second char must be the first character of a
> +                  keyword. */
> +               if (ret > 2 && buffer[0] >= 'a' && buffer[0] <= 'z') {
> +                       dev_err(&device->dev, "DDC/CI device quirk detected: unparenthesized capability string\n");
> +                       device->capabilities = kzalloc(ret+3, GFP_KERNEL);
> +                       if (!device->capabilities) {
> +                               ret = -ENOMEM;
> +                               goto err_free;
> +                       }
> +                       device->capabilities_len = ret+2;
> +                       memcpy(&(device->capabilities[1]), buffer, ret);
> +                       device->capabilities[0] = '(';
> +                       device->capabilities[ret+1] = ')';
> +               } else {
> +                       /* Standard case: simply copy the received string */
> +                       device->capabilities = kzalloc(ret+1, GFP_KERNEL);
> +                       if (!device->capabilities) {
> +                               ret = -ENOMEM;
> +                               goto err_free;
> +                       }
> +                       device->capabilities_len = ret;
> +                       memcpy(device->capabilities, buffer, ret);
> +               }
> +
> +               ret = ddcci_parse_capstring(device);
> +               if (ret) {
> +                       dev_err(&device->dev, "malformed capability string: \"%s\" errno %d\n", device->capabilities, ret);
> +                       ret = -EINVAL;
> +                       goto err_free;
> +               }
> +       }
> +
> +       /* Found a device if either identification or capabilities succeeded */
> +       if (!device->capabilities && device->vendor[0] == '\0') {
> +               dev_dbg(&client->dev,
> +                       "[%02x:%02x] got neither valid identification nor capability data\n",
> +                       client->addr << 1, addr);
> +               ret = -ENODEV;
> +               goto err_free;
> +       }
> +
> +       /* Setup chardev */
> +       down(&core_lock);
> +       ret = ddcci_setup_char_device(device);
> +       up(&core_lock);
> +       if (ret)
> +               goto err_free;
> +
> +       /* Release semaphore and add device to the tree */
> +       up(&drv_data->sem);
> +       pr_debug("found device at %d:%02x:%02x\n", client->adapter->nr, outer_addr, addr);
> +       ret = device_add(&device->dev);
> +       if (ret)
> +               goto err_free;
> +
> +       goto end;
> +err_free:
> +       put_device(&device->dev);
> +err_end:
> +       up(&drv_data->sem);
> +end:
> +       kfree(buffer);
> +       return ret;
> +}
> +
> +/* I2C detect function: check if a main or external dependent device exists */
> +static int ddcci_detect(struct i2c_client *client, struct i2c_board_info *info)
> +{
> +       int ret;
> +       unsigned char outer_addr;
> +       unsigned char inner_addr;
> +       unsigned char buf[32];
> +       unsigned char cmd_id[1] = { DDCCI_COMMAND_ID };
> +       unsigned char cmd_caps[3] = { DDCCI_COMMAND_CAPS, 0x00, 0x00};
> +       unsigned char *cmd;
> +       unsigned int cmd_len;
> +
> +       /* Check for i2c_master_* functionality */
> +       if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
> +               pr_debug("i2c adapter %d unsuitable: no i2c_master functionality\n", client->adapter->nr);
> +               return -ENODEV;
> +       }
> +
> +       /* send Capabilities Request (for main) or Identification Request command (for dependent devices) */
> +       outer_addr = client->addr << 1;
> +       inner_addr = (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? DDCCI_HOST_ADDR_ODD : outer_addr | 1;
> +       cmd = (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? cmd_caps : cmd_id;
> +       cmd_len = (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? sizeof(cmd_caps) : sizeof(cmd_id);
> +       pr_debug("detecting %d:%02x\n", client->adapter->nr, outer_addr);
> +
> +       ret = __ddcci_write_block(client, inner_addr, buf, true, cmd, cmd_len);
> +
> +       if (ret == -ENXIO || ret == -EIO) {
> +               if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_WRITE_BYTE)) {
> +                       pr_debug("i2c write failed with ENXIO or EIO but bytewise writing is not supported\n");
> +                       return -ENODEV;
> +               }
> +               pr_debug("i2c write failed with ENXIO or EIO, trying bytewise writing\n");
> +               ret = __ddcci_write_bytewise(client, inner_addr, true, cmd, cmd_len);
> +               if (ret == 0) {
> +                       msleep(delay);
> +                       ret = __ddcci_write_bytewise(client, inner_addr, true, cmd, cmd_len);
> +               }
> +       }
> +
> +       if (ret < 0)
> +               return -ENODEV;
> +
> +       /* wait for device */
> +       msleep(delay);
> +       /* receive answer */
> +       ret = i2c_master_recv(client, buf, 32);
> +       if (ret < 3) {
> +               pr_debug("detection failed: no answer\n");
> +               return -ENODEV;
> +       }
> +
> +       /* check response starts with outer addr */
> +       if (buf[0] != outer_addr) {
> +               pr_debug("detection failed: invalid %s response (%02x != %02x)\n", (cmd == cmd_id) ? "identification" : "capabilities", buf[0], outer_addr);
> +               pr_debug("received message was %*ph \n", ret, buf);
> +               return -ENODEV;
> +       }
> +
> +       pr_debug("detected %d:%02x\n", client->adapter->nr, outer_addr);
> +
> +       /* set device type */
> +       strlcpy(info->type, (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? "ddcci" : "ddcci-dependent", I2C_NAME_SIZE);
> +
> +       return 0;
> +}
> +
> +/* I2C probe function */
> +static int ddcci_probe(struct i2c_client *client, const struct i2c_device_id *id)
> +{
> +       int i, ret = -ENODEV, tmp;
> +       unsigned char main_addr, addr;
> +       struct ddcci_bus_drv_data *drv_data;
> +
> +       /* Initialize driver data structure */
> +       drv_data = devm_kzalloc(&client->dev, sizeof(struct ddcci_bus_drv_data), GFP_KERNEL);
> +       if (!drv_data)
> +               return -ENOMEM;
> +       drv_data->i2c_dev = client;
> +       sema_init(&drv_data->sem, 1);
> +
> +       /* Set i2c client data */
> +       i2c_set_clientdata(client, drv_data);
> +
> +       if (id->driver_data == 0) {
> +               /* Core device, probe at 0x6E */
> +               main_addr = DDCCI_DEFAULT_DEVICE_ADDR;
> +               dev_dbg(&client->dev, "probing core device [%02x]\n",
> +                       client->addr << 1);
> +               ret = ddcci_detect_device(client, main_addr, 0);
> +               if (ret) {
> +                       dev_info(&client->dev, "core device [%02x] probe failed: %d\n",
> +                                client->addr << 1, ret);
> +                       if (ret == -EIO)
> +                               ret = -ENODEV;
> +                       goto err_free;
> +               }
> +
> +               /* Detect internal dependent devices */
> +               dev_dbg(&client->dev, "probing internal dependent devices\n");
> +               for (i = 0; i < autoprobe_addr_count; ++i) {
> +                       addr = (unsigned short)autoprobe_addrs[i];
> +                       if ((addr & 1) == 0 && addr != main_addr) {
> +                               tmp = ddcci_detect_device(client, addr, main_addr);
> +                               if (tmp < 0 && tmp != -ENODEV) {
> +                                       dev_info(&client->dev, "internal dependent device [%02x:%02x] probe failed: %d\n",
> +                                                client->addr << 1, addr, ret);
> +                               }
> +                       }
> +               }
> +       } else if (id->driver_data == 1) {
> +               /* External dependent device */
> +               main_addr = client->addr << 1;
> +               dev_dbg(&client->dev, "probing external dependent device [%02x]\n", main_addr);
> +               ret = ddcci_detect_device(client, main_addr, -1);
> +               if (ret) {
> +                       dev_info(&client->dev, "external dependent device [%02x] probe failed: %d\n",
> +                                main_addr, ret);
> +                       if (ret == -EIO)
> +                               ret = -ENODEV;
> +                       goto err_free;
> +               }
> +       } else {
> +               dev_warn(&client->dev,
> +                        "probe() called with invalid i2c device id\n");
> +               ret = -EINVAL;
> +       }
> +
> +       goto end;
> +err_free:
> +       devm_kfree(&client->dev, drv_data);
> +end:
> +       return ret;
> +}
> +
> +/*
> + * Callback for bus_find_device() used in ddcci_remove()
> + *
> + * Find next device on i2c_client not flagged with
> + * DDCCI_FLAG_REMOVED and flag it.
> + */
> +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,3,0)
> +static int ddcci_remove_helper(struct device *dev, const void *p)
> +#else
> +static int ddcci_remove_helper(struct device *dev, void *p)
> +#endif
> +{
> +       struct ddcci_device *device;
> +
> +       device = ddcci_verify_device(dev);
> +       if (!device || device->flags & DDCCI_FLAG_REMOVED)
> +               return 0;
> +
> +       if (!p || (dev->parent == p)) {
> +               device->flags |= DDCCI_FLAG_REMOVED;
> +               wmb();
> +               return 1;
> +       }
> +
> +       return 0;
> +}
> +
> +/* I2C driver remove callback: unregister all subdevices */
> +static int ddcci_remove(struct i2c_client *client)
> +{
> +       struct ddcci_bus_drv_data *drv_data = i2c_get_clientdata(client);
> +       struct device *dev;
> +
> +       down(&drv_data->sem);
> +       while (1) {
> +               dev = bus_find_device(&ddcci_bus_type, NULL, client,
> +                                     ddcci_remove_helper);
> +               if (!dev)
> +                       break;
> +               device_unregister(dev);
> +               put_device(dev);
> +       }
> +       up(&drv_data->sem);
> +       return 0;
> +}
> +
> +/*
> + * I2C driver device identification table.
> + */
> +static const struct i2c_device_id ddcci_idtable[] = {
> +       { "ddcci", 0 },
> +       { "ddcci-dependent", 1 },
> +       {}
> +};
> +MODULE_DEVICE_TABLE(i2c, ddcci_idtable);
> +
> +/*
> + * I2C driver description structure
> + */
> +static struct i2c_driver ddcci_driver = {
> +       .driver = {
> +               .name   = "ddcci",
> +               .owner  = THIS_MODULE,
> +       },
> +
> +       .id_table       = ddcci_idtable,
> +       .probe          = ddcci_probe,
> +       .remove         = ddcci_remove,
> +       .class          = I2C_CLASS_DDC,
> +       .detect         = ddcci_detect,
> +       .address_list   = I2C_ADDRS(
> +               DDCCI_DEFAULT_DEVICE_ADDR>>1
> +       ),
> +};
> +
> +/*
> + * Module initialization function. Called when the module is inserted or
> + * (if builtin) at boot time.
> + */
> +static int __init ddcci_module_init(void)
> +{
> +       int ret;
> +
> +       pr_debug("initializing ddcci driver\n");
> +       /* Allocate a device number region for the character devices */
> +       ret = alloc_chrdev_region(&ddcci_cdev_first, 0, 128, DEVICE_NAME);
> +       if (ret < 0) {
> +               pr_err("failed to register device region: error %d\n", ret);
> +               goto err_chrdevreg;
> +       }
> +       ddcci_cdev_next = ddcci_cdev_first;
> +       ddcci_cdev_end = MKDEV(MAJOR(ddcci_cdev_first), MINOR(ddcci_cdev_first)+128);
> +
> +       /* Register bus */
> +       ret = bus_register(&ddcci_bus_type);
> +       if (ret) {
> +               pr_err("failed to register bus 'ddcci'\n");
> +               goto err_busreg;
> +       }
> +
> +       /* Register I2C driver */
> +       ret = i2c_add_driver(&ddcci_driver);
> +       if (ret) {
> +               pr_err("failed to register i2c driver\n");
> +               goto err_drvreg;
> +       }
> +
> +       pr_debug("ddcci driver initialized\n");
> +
> +       return 0;
> +
> +err_drvreg:
> +       bus_unregister(&ddcci_bus_type);
> +err_busreg:
> +       unregister_chrdev_region(ddcci_cdev_first, 128);
> +err_chrdevreg:
> +       return ret;
> +}
> +
> +/*
> + * Module clean-up function. Called when the module is removed.
> + */
> +static void __exit ddcci_module_exit(void)
> +{
> +       struct device *dev;
> +
> +       while (1) {
> +               dev = bus_find_device(&ddcci_bus_type, NULL, NULL, ddcci_remove_helper);
> +               if (!dev)
> +                       break;
> +               device_unregister(dev);
> +               put_device(dev);
> +       }
> +
> +       i2c_del_driver(&ddcci_driver);
> +       bus_unregister(&ddcci_bus_type);
> +       unregister_chrdev_region(ddcci_cdev_first, 128);
> +}
> +
> +/* Let the kernel know the calls for module init and exit */
> +module_init(ddcci_module_init);
> +module_exit(ddcci_module_exit);
> +
> +/* Module parameter description */
> +module_param(delay, uint, S_IRUGO|S_IWUSR);
> +MODULE_PARM_DESC(delay, "default delay after bus writes (in ms, default 60)");
> +module_param_array(autoprobe_addrs, ushort, &autoprobe_addr_count, S_IRUGO|S_IWUSR);
> +MODULE_PARM_DESC(autoprobe_addrs, "internal dependent device addresses to autoprobe");
> +
> +/* Module description */
> +MODULE_AUTHOR("Christoph Grenz");
> +MODULE_DESCRIPTION("DDC/CI bus driver");
> +MODULE_VERSION("0.4.2");
> +MODULE_LICENSE("GPL");
> diff --git a/include/linux/ddcci.h b/include/linux/ddcci.h
> new file mode 100644
> index 000000000000..a219f031e584
> --- /dev/null
> +++ b/include/linux/ddcci.h
> @@ -0,0 +1,164 @@
> +/*
> + *  DDC/CI bus driver
> + *
> + *  Copyright (c) 2015 Christoph Grenz
> + */
> +
> +/*
> + * 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.
> + */
> +
> +#ifndef _DDCCI_H
> +#define _DDCCI_H
> +
> +#include <linux/mod_devicetable.h>
> +#include <linux/device.h>
> +#include <linux/cdev.h>
> +
> +#define DDCCI_MODULE_PREFIX "ddcci:"
> +
> +/* Special addresses */
> +
> +/* default device address (even) */
> +#define DDCCI_DEFAULT_DEVICE_ADDR      0x6E
> +/* receiving host address for communication with default device address */
> +#define DDCCI_HOST_ADDR_EVEN   0x50
> +/* sending host address for communication with default device address */
> +#define DDCCI_HOST_ADDR_ODD    0x51
> +
> +/* Command codes */
> +
> +/* Identification Request */
> +#define DDCCI_COMMAND_ID       0xf1
> +/* Identification Reply */
> +#define DDCCI_REPLY_ID 0xe1
> +/* Capabilities Request */
> +#define DDCCI_COMMAND_CAPS     0xf3
> +/* Capabilities Reply */
> +#define DDCCI_REPLY_CAPS       0xe3
> +
> +/* Quirks */
> +
> +/* Device always responds with unset protocol flag */
> +#define DDCCI_QUIRK_NO_PFLAG BIT(1)
> +/* Device needs writing one byte at a time  */
> +#define DDCCI_QUIRK_WRITE_BYTEWISE BIT(2)
> +/* Device repeats first byte on read */
> +#define DDCCI_QUIRK_SKIP_FIRST_BYTE BIT(3)
> +
> +/* Flags */
> +
> +#define DDCCI_FLAG_REMOVED BIT(1)
> +#define DDCCI_FLAG_DEPENDENT BIT(2)
> +#define DDCCI_FLAG_EXTERNAL BIT(3)
> +
> +extern struct bus_type ddcci_bus_type;
> +
> +struct ddcci_bus_drv_data;
> +
> +/* struct ddcci_device_id - identifies DDC/CI devices for probing */
> +struct ddcci_device_id {
> +       char prot[9];
> +       char type[9];
> +       char model[9];
> +       char vendor[9];
> +       char module[9];
> +       kernel_ulong_t driver_data;     /* Data private to the driver */
> +};
> +#define DDCCI_ANY_ID "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"
> +
> +/**
> + * struct ddcci_device - represent an DDC/CI device
> + * @outer_addr: Outer device address (I2C address << 1).
> + * @inner_addr: Inner device address.
> + * @flags: Device flags.
> + * @capabilities: Device capability string.
> + * @capabilities_len: Length of capability string.
> + * @i2c_client: Parent I2C device.
> + * @bus_drv_data: Driver internal data structure.
> + * @dev: Driver model device node for the slave.
> + * @cdev: Character device structure
> + * @cdev_sem: RW semaphore for exclusive access on character device.
> + * @prot: Device class ("protocol", from capability string)
> + * @type: Device subclass ("type", from capability string)
> + * @model: Device model (from capability string)
> + * @vendor: Device vendor (from identification command response)
> + * @module: Device module (from identification command response)
> + * @device_number: Device serial (from identification command response)
> + */
> +struct ddcci_device {
> +       unsigned short outer_addr;
> +       unsigned short inner_addr;
> +       int flags;
> +       char *capabilities;
> +       size_t capabilities_len;
> +       struct i2c_client *i2c_client;
> +       struct ddcci_bus_drv_data *bus_drv_data;
> +       struct device dev;
> +       struct cdev cdev;
> +       struct rw_semaphore cdev_sem;
> +       char prot[9];
> +       char type[9];
> +       char model[9];
> +       char vendor[9];
> +       char module[9];
> +       int device_number;
> +};
> +#define to_ddcci_device(d) container_of(d, struct ddcci_device, dev)
> +
> +/**
> + * struct ddcci_driver - represent an DDC/CI device driver
> + * @probe: Callback for device binding
> + * @remove: Callback for device unbinding
> + * @driver: Device driver model driver
> + * @id_table: List of DDC/CI devices supported by this driver
> + *
> + * The driver.owner field should be set to the module owner of this driver.
> + * The driver.name field should be set to the name of this driver.
> + */
> +struct ddcci_driver {
> +       int (*probe)(struct ddcci_device *, const struct ddcci_device_id *);
> +       int (*remove)(struct ddcci_device *);
> +       struct device_driver driver;
> +       struct ddcci_device_id *id_table;
> +};
> +#define to_ddcci_driver(d) container_of(d, struct ddcci_driver, driver)
> +
> +int ddcci_register_driver(struct module *owner, struct ddcci_driver *driver);
> +#define ddcci_add_driver(driver) \
> +       ddcci_register_driver(THIS_MODULE, driver)
> +void ddcci_del_driver(struct ddcci_driver *driver);
> +
> +struct ddcci_device *ddcci_verify_device(struct device *dev);
> +
> +#define module_ddcci_driver(__ddcci_driver) \
> +       module_driver(__ddcci_driver, ddcci_add_driver, \
> +                       ddcci_del_driver)
> +
> +int ddcci_device_write(struct ddcci_device *, bool p_flag, unsigned char *data,
> +                      unsigned char length);
> +int ddcci_device_read(struct ddcci_device *, bool p_flag, unsigned char *buffer,
> +                     unsigned char length);
> +int ddcci_device_writeread(struct ddcci_device *, bool p_flag,
> +                          unsigned char *buffer, unsigned char length,
> +                          unsigned char maxlength);
> +
> +static inline void *ddcci_get_drvdata(const struct ddcci_device *dev)
> +{
> +       return dev_get_drvdata(&dev->dev);
> +}
> +
> +static inline void ddcci_set_drvdata(struct ddcci_device *dev, void *data)
> +{
> +       dev_set_drvdata(&dev->dev, data);
> +}
> +
> +unsigned long ddcci_quirks(struct ddcci_device *dev);
> +
> +const char *ddcci_find_capstr_item(const char *capabilities, const char *tag,
> +                                  size_t *length);
> +
> +#endif
> --
> 2.25.1
>

^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH] drivers: ddcci: upstream DDCCI driver
@ 2022-02-26  3:57 Yusuf Khan
  2022-02-26  4:01 ` Yusuf Khan
  0 siblings, 1 reply; 7+ messages in thread
From: Yusuf Khan @ 2022-02-26  3:57 UTC (permalink / raw)
  To: linux-kernel
  Cc: jasowang, mikelley, mst, gregkh, javier, arnd, will, axboe,
	bjorn.andersson, Yusuf Khan

This patch upstreams the DDCCI driver by Christoph Grenz into
the kernel. The original gitlab page is loacted at https://gitlab
.com/ddcci-driver-linux/ddcci-driver-linux/-/tree/master.

Signed-off-by: Yusuf Khan <yusisamerican@gmail.com>
---
 drivers/Kconfig                 |    2 +
 drivers/Makefile                |    1 +
 drivers/ddcci/Kconfig           |    3 +
 drivers/ddcci/Makefile          |    3 +
 drivers/ddcci/ddcci-backlight.c |  413 +++++++
 drivers/ddcci/ddcci.c           | 1895 +++++++++++++++++++++++++++++++
 include/linux/ddcci.h           |  164 +++
 7 files changed, 2481 insertions(+)
 create mode 100644 drivers/ddcci/Kconfig
 create mode 100644 drivers/ddcci/Makefile
 create mode 100644 drivers/ddcci/ddcci-backlight.c
 create mode 100644 drivers/ddcci/ddcci.c
 create mode 100644 include/linux/ddcci.h

diff --git a/drivers/Kconfig b/drivers/Kconfig
index 0d399ddaa185..3a58a29113f9 100644
--- a/drivers/Kconfig
+++ b/drivers/Kconfig
@@ -236,4 +236,6 @@ source "drivers/interconnect/Kconfig"
 source "drivers/counter/Kconfig"
 
 source "drivers/most/Kconfig"
+
+source "drivers/ddcci/Kconfig"
 endmenu
diff --git a/drivers/Makefile b/drivers/Makefile
index a110338c860c..c9181f1dc5aa 100644
--- a/drivers/Makefile
+++ b/drivers/Makefile
@@ -21,6 +21,7 @@ obj-y				+= pci/
 obj-$(CONFIG_PARISC)		+= parisc/
 obj-$(CONFIG_RAPIDIO)		+= rapidio/
 obj-y				+= video/
+obj-$(CONFIG_DDCCI)				+= ddcci/
 obj-y				+= idle/
 
 # IPMI must come before ACPI in order to provide IPMI opregion support
diff --git a/drivers/ddcci/Kconfig b/drivers/ddcci/Kconfig
new file mode 100644
index 000000000000..416881cc88e1
--- /dev/null
+++ b/drivers/ddcci/Kconfig
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-only
+menuconfig DDCCI
+	tristate "DDCCI display protocol support"
diff --git a/drivers/ddcci/Makefile b/drivers/ddcci/Makefile
new file mode 100644
index 000000000000..cce606be8038
--- /dev/null
+++ b/drivers/ddcci/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-$(CONFIG_DDCCI) += ddcci.o
+obj-$(CONFIG_DDCCI) += ddcci-backlight.o
diff --git a/drivers/ddcci/ddcci-backlight.c b/drivers/ddcci/ddcci-backlight.c
new file mode 100644
index 000000000000..7a9852207f0b
--- /dev/null
+++ b/drivers/ddcci/ddcci-backlight.c
@@ -0,0 +1,413 @@
+/*
+ *  DDC/CI monitor backlight driver
+ *
+ *  Copyright (c) 2015 Christoph Grenz
+ */
+
+/*
+ * 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.
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+#include <linux/backlight.h>
+#include <linux/module.h>
+#include <linux/fb.h>
+#include <linux/sysfs.h>
+
+#include <linux/ddcci.h>
+
+
+#define DDCCI_COMMAND_READ	0x01	/* read ctrl value */
+#define DDCCI_REPLY_READ	0x02	/* read ctrl value reply */
+#define DDCCI_COMMAND_WRITE	0x03	/* write ctrl value */
+#define DDCCI_COMMAND_SAVE	0x0c	/* save current settings */
+
+#define DDCCI_MONITOR_LUMINANCE	0x10
+#define DDCCI_MONITOR_BACKLIGHT	0x13
+#define DDCCI_MONITOR_BL_WHITE		0x6B
+
+static bool convenience_symlink = true;
+
+struct ddcci_monitor_drv_data {
+	struct ddcci_device *device;
+	struct backlight_device *bl_dev;
+	struct device *fb_dev;
+	unsigned char used_vcp;
+};
+
+static int ddcci_monitor_writectrl(struct ddcci_device *device,
+				   unsigned char ctrl, unsigned short value)
+{
+	unsigned char buf[4];
+	int ret;
+
+	buf[0] = DDCCI_COMMAND_WRITE;
+	buf[1] = ctrl;
+	buf[2] = (value >> 8);
+	buf[3] = (value & 255);
+
+	ret = ddcci_device_write(device, true, buf, sizeof(buf));
+
+	return ret;
+}
+
+static int ddcci_monitor_readctrl(struct ddcci_device *device,
+				  unsigned char ctrl, unsigned short *value,
+				  unsigned short *maximum)
+{
+	int ret;
+	unsigned char buf[10];
+
+	buf[0] = DDCCI_COMMAND_READ;
+	buf[1] = ctrl;
+
+	ret = ddcci_device_writeread(device, true, buf, 2, sizeof(buf));
+	if (ret < 0)
+		return ret;
+
+	if (ret == 0)
+		return -ENOTSUPP;
+
+	if (ret == 8 && buf[0] == DDCCI_REPLY_READ && buf[2] == ctrl) {
+		if (value)
+			*value = buf[6] * 256 + buf[7];
+
+		if (maximum)
+			*maximum = buf[4] * 256 + buf[5];
+
+		if (buf[1] == 1)
+			return -ENOTSUPP;
+		if (buf[1] != 0)
+			return -EIO;
+		return 0;
+	}
+
+	return -EIO;
+}
+
+static int ddcci_backlight_check_fb(struct backlight_device *bl,
+				   struct fb_info *info)
+{
+	struct ddcci_monitor_drv_data *drv_data = bl_get_data(bl);
+
+	return drv_data->fb_dev == NULL || drv_data->fb_dev == info->dev;
+}
+
+static int ddcci_backlight_update_status(struct backlight_device *bl)
+{
+	struct ddcci_monitor_drv_data *drv_data = bl_get_data(bl);
+	int brightness = bl->props.brightness;
+	int ret;
+
+	if (bl->props.power != FB_BLANK_UNBLANK ||
+	    bl->props.state & BL_CORE_FBBLANK)
+		brightness = 0;
+
+	ret = ddcci_monitor_writectrl(drv_data->device, drv_data->used_vcp,
+				      brightness);
+	if (ret > 0)
+		ret = 0;
+	return ret;
+}
+
+static int ddcci_backlight_get_brightness(struct backlight_device *bl)
+{
+	unsigned short value = 0, maxval = 0;
+	int ret;
+	struct ddcci_monitor_drv_data *drv_data = bl_get_data(bl);
+
+	ret = ddcci_monitor_readctrl(drv_data->device, drv_data->used_vcp,
+				     &value, &maxval);
+	if (ret < 0)
+		return ret;
+
+	bl->props.brightness = value;
+	bl->props.max_brightness = maxval;
+	ret = value;
+
+	return ret;
+}
+
+static const struct backlight_ops ddcci_backlight_ops = {
+	.options	= 0,
+	.update_status	= ddcci_backlight_update_status,
+	.get_brightness = ddcci_backlight_get_brightness,
+	.check_fb	= ddcci_backlight_check_fb,
+};
+
+static const char *ddcci_monitor_vcp_name(unsigned char vcp)
+{
+	switch (vcp) {
+		case DDCCI_MONITOR_BL_WHITE:
+			return "backlight";
+		case DDCCI_MONITOR_LUMINANCE:
+			return "luminance";
+		default:
+			return "???";
+	}
+}
+
+static const char *ddcci_monitor_next_vcp_item(const char *ptr)
+{
+	int depth = 0;
+
+	/* Sanity check */
+	if (unlikely(ptr == NULL || ptr[0] == '\0'))
+		return NULL;
+
+	/* Find next white space outside of parentheses */
+	while ((ptr = strpbrk(ptr, " ()"))) {
+		if (!ptr || depth == INT_MAX) {
+			return NULL;
+		} else if (*ptr == '(') {
+			depth++;
+		} else if (depth > 0) {
+			if (*ptr == ')')
+				depth--;
+		} else {
+			break;
+		}
+		++ptr;
+	}
+
+	/* Skip over whitespace */
+	ptr = skip_spaces(ptr);
+
+	/* Check if we're now at the end of the list */
+	if (unlikely(*ptr == '\0' || *ptr == ')'))
+		return NULL;
+
+	return ptr;
+}
+
+static bool ddcci_monitor_find_vcp(unsigned char vcp, const char *s)
+{
+	const char *ptr = s;
+	char vcp_hex[3];
+
+	/* Sanity check */
+	if (unlikely(s == NULL || s[0] == '\0'))
+		return false;
+
+	/* Create hex representation of VCP */
+	if (unlikely(snprintf(vcp_hex, 3, "%02hhX", vcp) != 2)) {
+		pr_err("snprintf failed to convert to hex. This should not happen.\n");
+		return false;
+	}
+
+	/* Search for it */
+	do {
+		if (strncasecmp(vcp_hex, ptr, 2) == 0) {
+			if (ptr[2] == ' ' || ptr[2] == '(' || ptr[2] == ')') {
+				return true;
+			}
+		}
+	} while ((ptr = ddcci_monitor_next_vcp_item(ptr)));
+
+	return false;
+}
+
+static int ddcci_backlight_create_symlink(struct ddcci_device *ddcci_dev)
+{
+	int i, result;
+	struct device *dev = &ddcci_dev->dev;
+	struct kernfs_node *dirent;
+	for (i = 0; i < 3; ++i) {
+		dev = dev->parent;
+		if (!dev) {
+			dev_dbg(&ddcci_dev->dev, "failed to create convenience symlink: ancestor device not found\n");
+			return -ENOENT;
+		}
+	}
+	dirent = sysfs_get_dirent(dev->kobj.sd, "ddcci_backlight");
+	if (dirent) {
+		sysfs_put(dirent);
+		dev_dbg(&ddcci_dev->dev, "failed to create convenience symlink: %s/ddcci_backlight already exists\n", dev_name(dev));
+		return -EEXIST;
+	}
+
+	result = sysfs_create_link(&dev->kobj, &ddcci_dev->dev.kobj, "ddcci_backlight");
+	if (result == 0) {
+		dev_dbg(&ddcci_dev->dev, "created symlink %s/ddcci_backlight\n", dev_name(dev));
+	} else {
+		dev_info(&ddcci_dev->dev, "failed to create convenience symlink: %d\n", result);
+	}
+	return result;
+}
+
+static int ddcci_backlight_remove_symlink(struct ddcci_device *ddcci_dev)
+{
+	int i;
+	struct device *dev = &ddcci_dev->dev;
+	struct kernfs_node *dirent;
+	for (i = 0; i < 3; ++i) {
+		dev = dev->parent;
+		if (!dev)
+			return -ENOENT;
+	}
+	dirent = sysfs_get_dirent(dev->kobj.sd, "ddcci_backlight");
+	if (!dirent) {
+		return -ENOENT;
+	}
+
+	if ((dirent->flags & KERNFS_LINK) == 0) {
+		sysfs_put(dirent);
+		dev_dbg(&ddcci_dev->dev, "won't remove %s/ddcci_backlight: not a symlink\n", dev_name(dev));
+		return -EINVAL;
+	}
+
+	if (dirent->symlink.target_kn != ddcci_dev->dev.kobj.sd) {
+		sysfs_put(dirent);
+		dev_dbg(&ddcci_dev->dev, "won't remove %s/ddcci_backlight: we are not the link target\n", dev_name(dev));
+		return -EINVAL;
+	}
+
+	sysfs_put(dirent);
+
+	sysfs_remove_link(&dev->kobj, "ddcci_backlight");
+	dev_dbg(&ddcci_dev->dev, "removed symlink %s/ddcci_backlight\n", dev_name(dev));
+	return 0;
+}
+
+static int ddcci_monitor_probe(struct ddcci_device *dev,
+			       const struct ddcci_device_id *id)
+{
+	struct ddcci_monitor_drv_data *drv_data;
+	struct backlight_properties props;
+	struct backlight_device *bl = NULL;
+	int ret = 0;
+	bool support_luminance, support_bl_white;
+	unsigned short brightness = 0, max_brightness = 0;
+	const char *vcps;
+
+	dev_dbg(&dev->dev, "probing monitor backlight device\n");
+
+	/* Get VCP list */
+	vcps = ddcci_find_capstr_item(dev->capabilities, "vcp", NULL);
+	if (IS_ERR(vcps)) {
+		dev_info(&dev->dev,
+			 "monitor doesn't provide a list of supported controls.\n");
+		support_bl_white = support_luminance = true;
+	} else {
+		/* Check VCP list for supported VCPs */
+		support_bl_white = ddcci_monitor_find_vcp(DDCCI_MONITOR_BL_WHITE, vcps);
+		support_luminance = ddcci_monitor_find_vcp(DDCCI_MONITOR_LUMINANCE, vcps);
+		/* Fallback to trying if no support is found */
+		if (!support_bl_white && !support_luminance) {
+			dev_info(&dev->dev,
+				 "monitor doesn't announce support for backlight or luminance controls.\n");
+			support_bl_white = support_luminance = true;
+		}
+	}
+
+	/* Initialize driver data structure */
+	drv_data = devm_kzalloc(&dev->dev, sizeof(struct ddcci_monitor_drv_data),
+				GFP_KERNEL);
+	if (!drv_data)
+		return -ENOMEM;
+	drv_data->device = dev;
+
+	if (support_bl_white) {
+		/* Try getting backlight level */
+		dev_dbg(&dev->dev,
+			"trying to access \"backlight level white\" control\n");
+		ret = ddcci_monitor_readctrl(drv_data->device, DDCCI_MONITOR_BL_WHITE,
+						&brightness, &max_brightness);
+		if (ret < 0) {
+			if (ret == -ENOTSUPP)
+				dev_info(&dev->dev,
+					"monitor does not support reading backlight level\n");
+			else
+				goto err_free;
+		} else {
+			drv_data->used_vcp = DDCCI_MONITOR_BL_WHITE;
+		}
+	}
+
+	if (support_luminance && !drv_data->used_vcp) {
+		/* Try getting luminance */
+		dev_dbg(&dev->dev,
+			"trying to access \"luminance\" control\n");
+		ret = ddcci_monitor_readctrl(drv_data->device, DDCCI_MONITOR_LUMINANCE,
+					     &brightness, &max_brightness);
+		if (ret < 0) {
+			if (ret == -ENOTSUPP)
+				dev_info(&dev->dev,
+					"monitor does not support reading luminance\n");
+			else
+				goto err_free;
+		} else {
+			drv_data->used_vcp = DDCCI_MONITOR_LUMINANCE;
+		}
+		drv_data->used_vcp = DDCCI_MONITOR_LUMINANCE;
+	}
+
+	if (!drv_data->used_vcp)
+		goto err_free;
+
+	/* Create brightness device */
+	memset(&props, 0, sizeof(props));
+	props.type = BACKLIGHT_RAW;
+	props.max_brightness = max_brightness;
+	props.brightness = brightness;
+	bl = devm_backlight_device_register(&dev->dev, dev_name(&dev->dev),
+					    &dev->dev, drv_data,
+					    &ddcci_backlight_ops, &props);
+	drv_data->bl_dev = bl;
+	if (IS_ERR(bl)) {
+		dev_err(&dev->dev, "failed to register backlight\n");
+		return PTR_ERR(bl);
+	}
+	dev_info(&dev->dev, "registered %s as backlight device %s\n",
+		 ddcci_monitor_vcp_name(drv_data->used_vcp),
+		 dev_name(&dev->dev));
+
+	if (convenience_symlink) {
+		ddcci_backlight_create_symlink(dev);
+	}
+
+	goto end;
+err_free:
+	devm_kfree(&dev->dev, drv_data);
+end:
+	return ret;
+}
+
+static int ddcci_monitor_remove(struct ddcci_device *dev)
+{
+	dev_dbg(&dev->dev, "removing device\n");
+	ddcci_backlight_remove_symlink(dev);
+	return 0;
+}
+
+static struct ddcci_device_id ddcci_monitor_idtable[] = {
+	{ "monitor", DDCCI_ANY_ID, DDCCI_ANY_ID, DDCCI_ANY_ID, DDCCI_ANY_ID, 0 },
+	{}
+};
+
+static struct ddcci_driver ddcci_backlight_driver = {
+	.driver = {
+		.name	= "ddcci-backlight",
+		.owner	= THIS_MODULE,
+	},
+
+	.id_table	= ddcci_monitor_idtable,
+	.probe		= ddcci_monitor_probe,
+	.remove		= ddcci_monitor_remove,
+};
+
+module_ddcci_driver(ddcci_backlight_driver);
+
+/* Module parameter description */
+module_param(convenience_symlink, bool, S_IRUGO|S_IWUSR);
+MODULE_PARM_DESC(convenience_symlink, "add convenience symlink \"ddcci_backlight\" to ancestor device in sysfs (default true)");
+
+MODULE_AUTHOR("Christoph Grenz");
+MODULE_DESCRIPTION("DDC/CI generic monitor backlight driver");
+MODULE_VERSION("0.4.2");
+MODULE_LICENSE("GPL");
+
+MODULE_ALIAS("ddcci:monitor-*-*-*-*");
diff --git a/drivers/ddcci/ddcci.c b/drivers/ddcci/ddcci.c
new file mode 100644
index 000000000000..e26d15cae948
--- /dev/null
+++ b/drivers/ddcci/ddcci.c
@@ -0,0 +1,1895 @@
+/*
+ *  DDC/CI sub-bus driver
+ *
+ *  Copyright (c) 2015 Christoph Grenz
+ */
+
+/*
+ * 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.
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+#include <asm-generic/fcntl.h>
+#include <linux/cdev.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/fs.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/rwsem.h>
+#include <linux/sem.h>
+#include <linux/slab.h>
+#include <linux/uaccess.h>
+#include <linux/version.h>
+
+#include <linux/ddcci.h>
+
+#define DDCCI_RECV_BUFFER_SIZE 130
+#define DEVICE_NAME "ddcci"
+#define DDCCI_MAX_CAP_CHUNKS 200
+
+static unsigned int delay = 60;
+static unsigned short autoprobe_addrs[127] = {0xF0, 0xF2, 0xF4, 0xF6, 0xF8};
+static int autoprobe_addr_count = 5;
+
+static dev_t ddcci_cdev_first;
+static dev_t ddcci_cdev_next;
+static dev_t ddcci_cdev_end;
+static DEFINE_SEMAPHORE(core_lock);
+
+struct bus_type ddcci_bus_type;
+EXPORT_SYMBOL_GPL(ddcci_bus_type);
+
+/* Assert neccessary string array sizes  */
+#ifndef sizeof_field
+# define sizeof_field(t,m) FIELD_SIZEOF(t,m)
+#endif
+static_assert(sizeof_field(struct ddcci_device, prot) > 8);
+static_assert(sizeof_field(struct ddcci_device, type) > 8);
+static_assert(sizeof_field(struct ddcci_device, model) > 8);
+static_assert(sizeof_field(struct ddcci_device, vendor) > 8);
+static_assert(sizeof_field(struct ddcci_device, module) > 8);
+
+/* Internal per-i2c-client driver data */
+struct ddcci_bus_drv_data {
+	unsigned long quirks;
+	struct i2c_client *i2c_dev;
+	struct semaphore sem;
+	unsigned char recv_buffer[DDCCI_RECV_BUFFER_SIZE];
+};
+
+/* Replace non-alphanumeric characters in a string (used for modalias) */
+static void ddcci_modalias_clean(char *string, size_t n, char replacement)
+{
+	int i;
+	for (i = 0; i < n; ++i) {
+		char c = string[i];
+		if (c == 0) {
+			return;
+		} else if (c < '0' || (c > '9' && c < 'A') || (c > 'Z' && c < 'a') || c > 'z') {
+			string[i] = replacement;
+		}
+	}
+}
+
+/* Write a message to the DDC/CI bus using i2c_smbus_write_byte() */
+static int __ddcci_write_bytewise(struct i2c_client *client, unsigned char addr,
+				  bool p_flag, const unsigned char * __restrict buf,
+				  unsigned char len)
+{
+	int ret = 0;
+	unsigned char outer_addr = (unsigned char)(client->addr << 1);
+	unsigned xor = outer_addr; /* initial xor value */
+
+	/* Consistency checks */
+	if (len > 127)
+		return -EINVAL;
+
+	/* Special case: sender to 0x6E is always 0x51 */
+	if (addr == DDCCI_DEFAULT_DEVICE_ADDR) {
+		addr = DDCCI_HOST_ADDR_ODD;
+	} else {
+		/* When sending the odd address is used */
+		addr = addr | 1;
+	}
+
+	/* first byte: sender address */
+	xor ^= addr;
+	ret = i2c_smbus_write_byte(client, addr);
+	if (ret < 0)
+		return ret;
+
+	/* second byte: protocol flag and message size */
+	xor ^= ((p_flag << 7) | len);
+	ret = i2c_smbus_write_byte(client, (p_flag << 7)|len);
+	if (ret < 0)
+		return ret;
+
+	/* send payload */
+	while (len--) {
+		xor ^= (*buf);
+		ret = i2c_smbus_write_byte(client, (*buf));
+		if (ret < 0)
+			return ret;
+		buf++;
+	}
+
+	/* send checksum */
+	ret = i2c_smbus_write_byte(client, xor);
+	return ret;
+}
+
+/* Write a message to the DDC/CI bus using i2c_master_send() */
+static int __ddcci_write_block(struct i2c_client *client, unsigned char addr,
+			       unsigned char *sendbuf, bool p_flag,
+			       const unsigned char *data, unsigned char len)
+{
+	unsigned char outer_addr = (unsigned char)(client->addr << 1);
+	unsigned xor = outer_addr;	/* initial xor value */
+	unsigned char *ptr = sendbuf;
+
+	/* Consistency checks */
+	if (len > 127)
+		return -EINVAL;
+
+	/* Special case: sender to 0x6E is always 0x51 */
+	if (addr == DDCCI_DEFAULT_DEVICE_ADDR) {
+		addr = DDCCI_HOST_ADDR_ODD;
+	} else {
+		/* When sending the odd address is used */
+		addr = addr | 1;
+	}
+
+	/* first byte: sender address */
+	xor ^= addr;
+	*(ptr++) = addr;
+	/* second byte: protocol flag and message size */
+	xor ^= ((p_flag << 7) | len);
+	*(ptr++) = (p_flag << 7)|len;
+	/* payload */
+	while (len--) {
+		xor ^= (*data);
+		*(ptr++) = (*data);
+		data++;
+	}
+	/* checksum */
+	(*ptr) = xor;
+
+	/* Send it */
+	return i2c_master_send(client, sendbuf, ptr - sendbuf + 1);
+}
+
+/*
+ * Write a message to the DDC/CI bus.
+ *
+ * You must hold the bus semaphore when calling this function.
+ */
+static int ddcci_write(struct i2c_client *client, unsigned char addr,
+		       bool p_flag, const unsigned char *data,
+		       unsigned char len)
+{
+	struct ddcci_bus_drv_data *drv_data;
+	unsigned char *sendbuf;
+	int ret;
+
+	drv_data = i2c_get_clientdata(client);
+
+
+	pr_debug("sending to %d:%02x:%02x: %*ph\n", client->adapter->nr,
+		 client->addr << 1, addr, len, data);
+	if (drv_data->quirks & DDCCI_QUIRK_WRITE_BYTEWISE) {
+		ret = __ddcci_write_bytewise(client, addr, p_flag, data, len);
+	} else {
+		sendbuf = drv_data->recv_buffer;
+		ret = __ddcci_write_block(client, addr, sendbuf, p_flag, data, len);
+	}
+
+	return ret;
+}
+
+/*
+ * Read a response from the DDC/CI bus with headers directly into a buffer.
+ * Always check for DDCCI_QUIRK_SKIP_FIRST_BYTE when using this function.
+ * The returned length contains the whole unmodified response.
+ * If -EMSGSIZE is returned, the buffer contains the response up to `len`.
+ * If any other negative error code is returned, the buffer content is
+ * unspecified.
+ */
+static int __ddcci_read(struct i2c_client *client, unsigned char addr,
+			bool p_flag, unsigned long quirks, unsigned char *buf,
+			unsigned char len)
+{
+	int i, payload_len, packet_length, ret;
+	unsigned char xor = DDCCI_HOST_ADDR_EVEN;
+
+	/* Consistency checks */
+	if (len < 3)
+		return -EINVAL;
+
+	/* Read frame */
+	ret = i2c_master_recv(client, buf, len);
+	if (ret < 0)
+		goto out_err;
+	packet_length = ret;
+
+	/* Skip first byte if quirk active */
+	if ((quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE) && ret > 0 && len > 0) {
+		ret--;
+		len--;
+		buf++;
+	}
+
+	/* If answer too short (= incomplete) break out */
+	if (ret < 3) {
+		ret = -EIO;
+		goto out_err;
+	}
+
+	/* validate first byte */
+	if (unlikely(buf[0] != addr)) {
+		ret = (buf[0] == '\0') ? -EAGAIN : -EIO;
+		goto out_err;
+	}
+
+	/* validate second byte (protocol flag) */
+	if (unlikely((buf[1] & 0x80) != (p_flag << 7))) {
+		if (!p_flag || !(quirks & DDCCI_QUIRK_NO_PFLAG)) {
+			ret = -EIO;
+			goto out_err;
+		}
+	}
+
+	/* get and check payload length */
+	payload_len = buf[1] & 0x7F;
+	if (3+payload_len > packet_length)
+		return -EBADMSG;
+	if (3+payload_len > len)
+		return -EMSGSIZE;
+
+	/* calculate checksum */
+	for (i = 0; i < 3+payload_len; i++)
+		xor ^= buf[i];
+
+	/* verify checksum */
+	if (xor != 0) {
+		dev_err(&client->dev, "invalid DDC/CI response, corrupted data - xor is 0x%02x, length 0x%02x\n",
+			xor, payload_len);
+		ret = -EBADMSG;
+		goto out_err;
+	}
+
+	/* return result */
+	ret = payload_len+3+((quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE)?1:0);
+
+out_err:
+	return ret;
+}
+
+/*
+ * Read a response from the DDC/CI bus
+ *
+ * You must hold the bus semaphore when calling this function.
+ */
+static int ddcci_read(struct i2c_client *client, unsigned char addr,
+		      bool p_flag, unsigned char *buf, unsigned char len)
+{
+	struct ddcci_bus_drv_data *drv_data;
+	unsigned char *recvbuf;
+	int ret;
+
+	drv_data = i2c_get_clientdata(client);
+	recvbuf = drv_data->recv_buffer;
+
+	/* Read frame */
+	ret = __ddcci_read(client, addr, p_flag,
+		drv_data->quirks, recvbuf, DDCCI_RECV_BUFFER_SIZE);
+	if (ret < 0)
+		return ret;
+
+	if (drv_data->quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE)
+		recvbuf++;
+
+	/* return result */
+	if (buf) {
+		if (ret > 3) {
+			ret = ret-3;
+			/* copy to caller buffer */
+			memcpy(buf, &recvbuf[2], (ret < len) ? ret : len);
+
+			if (ret > len) {
+				/* if message was truncated, return -EMSGSIZE */
+				pr_debug("received from %d:%02x:%02x: [%u/%u] %*ph ...\n",
+					 client->adapter->nr, client->addr << 1,
+					 addr, ret, len, len, buf);
+				ret = -EMSGSIZE;
+			} else {
+				pr_debug("received from %d:%02x:%02x: [%u/%u] %*ph\n",
+					 client->adapter->nr, client->addr << 1,
+					 addr, ret, len, ret, buf);
+			}
+		}
+	}
+	if (!(drv_data->quirks & DDCCI_QUIRK_WRITE_BYTEWISE)) {
+		/* second read to clear buffers, needed on some devices */
+		__ddcci_read(client, addr, true, drv_data->quirks, recvbuf, 1);
+	}
+	return ret;
+}
+
+/* Request the capability string for a device and put it into buf */
+static int ddcci_get_caps(struct i2c_client *client, unsigned char addr,
+			  unsigned char *buf, unsigned int len)
+{
+	int result = 0, counter = 0, offset = 0;
+	unsigned char cmd[3] = { DDCCI_COMMAND_CAPS, 0x00, 0x00 };
+	unsigned char *chunkbuf = kzalloc(35, GFP_KERNEL);
+
+	if (!chunkbuf)
+		return -ENOMEM;
+
+	do {
+		/* Send command */
+		result = ddcci_write(client, addr, true, cmd, sizeof(cmd));
+		if (result < 0)
+			goto err_free;
+		msleep(delay);
+		/* read result chunk */
+		result = ddcci_read(client, addr, true, chunkbuf,
+				    (len > 32) ? 35 : len+3);
+		if (result < 0)
+			goto err_free;
+
+		if (result > 0) {
+			/* check chunk header */
+			if (chunkbuf[0] != DDCCI_REPLY_CAPS) {
+				result = -EIO;
+				goto err_free;
+			}
+			if (chunkbuf[1] != cmd[1] || chunkbuf[2] != cmd[2]) {
+				result = -EIO;
+				goto err_free;
+			}
+			if (result < 3) {
+				result = -EIO;
+				goto err_free;
+			}
+			memcpy(buf, chunkbuf+3, min((unsigned int)result-3, len));
+
+			counter++;
+			/* adjust offset, etc. */
+			offset += result-3;
+			len -= result-3;
+			buf += result-3;
+			cmd[1] = offset >> 8;
+			cmd[2] = offset & 0xFF;
+			/* Another superfluous read to make some devices happy... */
+			ddcci_read(client, addr, true, NULL, 2);
+		}
+	} while (result > 3 && counter < DDCCI_MAX_CAP_CHUNKS);
+
+	kfree(chunkbuf);
+	return offset+result-3;
+err_free:
+	kfree(chunkbuf);
+	return result;
+}
+
+/*
+ * Request the device identification and put it into buf.
+ *
+ * Also detects all communication quirks and sets the corresponding flags
+ * in the ddcci_bus_drv_data structure associated with client.
+ *
+ * The identification command will fail on most DDC devices, as it is optional
+ * to support, but even the "failed" response suffices to detect quirks.
+ */
+static int ddcci_identify_device(struct i2c_client *client, unsigned char addr,
+				 unsigned char *buf, unsigned char len)
+{
+	int i, payload_len, ret = -ENODEV;
+	unsigned long quirks;
+	unsigned char cmd[1] = { DDCCI_COMMAND_ID };
+	unsigned char *buffer;
+	unsigned char xor = DDCCI_HOST_ADDR_EVEN;
+	struct ddcci_bus_drv_data *bus_drv_data;
+
+	bus_drv_data = i2c_get_clientdata(client);
+	quirks = bus_drv_data->quirks;
+	buffer = bus_drv_data->recv_buffer;
+
+	/* Send Identification command */
+	if (!(quirks & DDCCI_QUIRK_WRITE_BYTEWISE)) {
+		ret = __ddcci_write_block(client, addr, buffer, true, cmd, sizeof(cmd));
+		dev_dbg(&client->dev,
+			"[%02x:%02x] writing identification command in block mode: %d\n",
+			client->addr << 1, addr, ret);
+		if ((ret == -ENXIO)
+		    && i2c_check_functionality(client->adapter,
+					       I2C_FUNC_SMBUS_WRITE_BYTE)) {
+			quirks |= DDCCI_QUIRK_WRITE_BYTEWISE;
+			dev_info(&client->dev,
+				"DDC/CI bus quirk detected: writes must be done bytewise\n");
+			/* Some devices need writing twice after a failed blockwise write */
+			__ddcci_write_bytewise(client, addr, true, cmd, sizeof(cmd));
+			msleep(delay);
+		}
+	}
+	if (ret < 0 && (quirks & DDCCI_QUIRK_WRITE_BYTEWISE)) {
+		ret = __ddcci_write_bytewise(client, addr, true, cmd, sizeof(cmd));
+		dev_dbg(&client->dev,
+			"[%02x:%02x] writing identification command in bytewise mode: %d\n",
+			client->addr << 1, addr, ret);
+	}
+	if (ret < 0)
+		return -ENODEV;
+
+	/* Wait */
+	msleep(delay);
+
+	/* Receive response */
+	ret = i2c_master_recv(client, buffer, DDCCI_RECV_BUFFER_SIZE);
+	if (ret < 0) {
+		dev_dbg(&client->dev,
+			"[%02x:%02x] receiving identification response resulted in errno %d\n",
+			client->addr << 1, addr, ret);
+		return ret;
+	}
+
+	if (ret == 0) {
+		dev_dbg(&client->dev,
+			"[%02x:%02x] no identification response received\n",
+			client->addr << 1, addr);
+		return ret;
+	}
+
+	/* Skip first byte if quirk already active */
+	if (quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE && ret > 1) {
+		dev_dbg(&client->dev,
+			"[%02x:%02x] doubled first byte quirk in effect\n",
+			client->addr << 1, addr);
+		ret--;
+		buffer++;
+	}
+
+	/* If answer too short (= incomplete) break out */
+	if (ret < 3) {
+		dev_dbg(&client->dev,
+			"[%02x:%02x] identification response is too short (%d bytes)\n",
+			client->addr << 1, addr, ret);
+		return -EIO;
+	}
+
+	/* validate first byte */
+	if (buffer[0] != addr) {
+		dev_dbg(&client->dev,
+			"[%02x:%02x] identification response: %*ph\n",
+			client->addr << 1, addr, (ret > 32 ? 32 : ret), buffer);
+
+		dev_dbg(&client->dev,
+			"[%02x:%02x] identification response invalid (expected first byte %02x, got %02x)\n",
+			client->addr << 1, addr, addr, buffer[0]);
+		return -ENODEV;
+	}
+
+	/* Check if first byte is doubled (QUIRK_SKIP_FIRST_BYTE) */
+	if (!(quirks & DDCCI_QUIRK_SKIP_FIRST_BYTE)) {
+		if (buffer[0] == buffer[1]) {
+			quirks |= DDCCI_QUIRK_SKIP_FIRST_BYTE;
+			dev_info(&client->dev,
+				"DDC/CI bus quirk detected: doubled first byte on read\n");
+			ret--;
+			buffer++;
+			if (ret < 3)
+				return -EIO;
+		}
+	}
+
+	/* validate second byte (protocol flag) */
+	if ((buffer[1] & 0x80) != 0x80 && !(quirks & DDCCI_QUIRK_NO_PFLAG)) {
+		dev_info(&client->dev,
+			"DDC/CI bus quirk detected: device omits protocol flag on responses\n");
+		quirks |= DDCCI_QUIRK_NO_PFLAG;
+	}
+
+	/* get and check payload length */
+	payload_len = buffer[1] & 0x7F;
+	if (3+payload_len > ret) {
+		dev_dbg(&client->dev,
+			"[%02x:%02x] identification response: %*ph ...\n",
+			client->addr << 1, addr, ret, buffer);
+		dev_dbg(&client->dev,
+			"[%02x:%02x] identification response was truncated (expected %d bytes, got %d)\n",
+			client->addr << 1, addr, 3+payload_len, ret);
+		return -EBADMSG;
+	}
+
+	dev_dbg(&client->dev,
+		"[%02x:%02x] identification response: %*ph\n",
+		client->addr << 1, addr, 3+payload_len, buffer);
+
+	/* calculate checksum */
+	for (i = 0; i < 3+payload_len; i++)
+		xor ^= buffer[i];
+
+	/* verify checksum */
+	if (xor != 0) {
+		dev_err(&client->dev,
+			"[%02x:%02x] invalid DDC/CI response, corrupted data - xor is 0x%02x, length 0x%02x\n",
+			client->addr << 1, addr, xor, payload_len);
+		return -EBADMSG;
+	}
+
+	/* save quirks */
+	bus_drv_data->quirks = quirks;
+
+	/* return result */
+	if (payload_len <= len) {
+		ret = payload_len;
+		memcpy(buf, &buffer[2], payload_len);
+	} else {
+		ret = -EMSGSIZE;
+		memcpy(buf, &buffer[2], len);
+	}
+	return ret;
+}
+
+/* Character device */
+
+/* Data structure for an open file handle */
+struct ddcci_fp_data {
+	struct ddcci_device *dev;
+	bool exclusive;
+	unsigned char buffer[129];
+};
+
+/* Called when the character device is opened */
+static int ddcci_cdev_open(struct inode *inode, struct file *filp)
+{
+	struct ddcci_device *dev = container_of(inode->i_cdev,
+						struct ddcci_device, cdev);
+	struct ddcci_fp_data *fp_data = NULL;
+
+	fp_data = kzalloc(sizeof(struct ddcci_fp_data), GFP_KERNEL);
+
+	if (!fp_data)
+		return -ENOMEM;
+
+	fp_data->exclusive = filp->f_flags & O_EXCL;
+
+	if (fp_data->exclusive) {
+		if (down_write_trylock(&dev->cdev_sem) == 0) {
+			kfree(fp_data);
+			return -EBUSY;
+		}
+	} else {
+		if (down_read_trylock(&dev->cdev_sem) == 0) {
+			kfree(fp_data);
+			return -EBUSY;
+		}
+	}
+
+	fp_data->dev = dev;
+	filp->private_data = fp_data;
+
+	return 0;
+}
+
+/* Called when the character device is closed */
+static int ddcci_cdev_close(struct inode *inode, struct file *filp)
+{
+	struct ddcci_fp_data *fp_data = filp->private_data;
+	struct ddcci_device *dev = fp_data->dev;
+
+	if (fp_data->exclusive)
+		up_write(&dev->cdev_sem);
+	else
+		up_read(&dev->cdev_sem);
+
+	filp->private_data = NULL;
+	kfree(fp_data);
+	return 0;
+}
+
+/* Called when reading from the character device */
+static ssize_t ddcci_cdev_read(struct file *filp, char __user *buffer,
+			       size_t length, loff_t *offset)
+{
+	struct ddcci_fp_data *fp_data = filp->private_data;
+	struct ddcci_device *dev = fp_data->dev;
+	unsigned char *buf = fp_data->buffer;
+	const bool nonblocking = (filp->f_flags & O_NONBLOCK) != 0;
+	int ret = 0;
+
+	if ((filp->f_mode & FMODE_READ) == 0)
+		return -EBADF;
+
+	/* Lock mutex */
+	if (nonblocking) {
+		if (down_trylock(&dev->bus_drv_data->sem))
+			return -EAGAIN;
+	} else {
+		if (down_interruptible(&dev->bus_drv_data->sem))
+			return -ERESTARTSYS;
+	}
+
+	/* Execute read */
+	ret = ddcci_read(dev->bus_drv_data->i2c_dev, dev->inner_addr, true, buf,
+			 length);
+
+	if (ret > 0) {
+		/* Copy data from user space */
+		if (copy_to_user(buffer, buf, ret)) {
+			ret = -EFAULT;
+			goto out;
+		}
+	}
+
+out:
+	up(&dev->bus_drv_data->sem);
+	return ret;
+}
+
+/* Called when writing to the character device */
+static ssize_t ddcci_cdev_write(struct file *filp, const char __user *buffer,
+				size_t count, loff_t *offset)
+{
+	struct ddcci_fp_data *fp_data = filp->private_data;
+	struct ddcci_device *dev = fp_data->dev;
+	unsigned char *buf = fp_data->buffer;
+	const bool nonblocking = (filp->f_flags & O_NONBLOCK) != 0;
+	int ret = 0;
+
+	if ((filp->f_mode & FMODE_WRITE) == 0)
+		return -EBADF;
+
+	if (count > 127)
+		return -EINVAL;
+
+	/* Lock mutex */
+	if (nonblocking) {
+		if (down_trylock(&dev->bus_drv_data->sem))
+			return -EAGAIN;
+	} else {
+		if (down_interruptible(&dev->bus_drv_data->sem))
+			return -ERESTARTSYS;
+	}
+
+	if (count > 0) {
+		/* Copy data from user space */
+		if (copy_from_user(buf, buffer, count)) {
+			ret = -EFAULT;
+			goto err_out;
+		}
+
+		/* Execute write */
+		ret = ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr,
+				  true, buf, count);
+	}
+
+	if (ret >= 0) {
+		msleep(delay);
+		up(&dev->bus_drv_data->sem);
+		return count;
+	}
+
+err_out:
+	up(&dev->bus_drv_data->sem);
+	return ret;
+}
+
+/* Called when seeking the character device */
+static loff_t ddcci_cdev_seek(struct file *filp, loff_t offset, int anchor)
+{
+	return -EINVAL;
+}
+
+static const struct file_operations ddcci_fops = {
+	.owner = THIS_MODULE,
+	.read = ddcci_cdev_read,
+	.write = ddcci_cdev_write,
+	.open = ddcci_cdev_open,
+	.release = ddcci_cdev_close,
+	.llseek = ddcci_cdev_seek
+};
+
+/* Set up the character device for a DDC/CI device */
+static int ddcci_setup_char_device(struct ddcci_device *device)
+{
+	int ret = -EINVAL;
+
+	/* Check if free minor exists */
+	if (ddcci_cdev_next == ddcci_cdev_end) {
+		dev_err(&device->dev, "no free major/minor\n");
+		ret = -ENFILE;
+		goto out;
+	}
+
+	/* Initialize rwsem */
+	init_rwsem(&device->cdev_sem);
+
+	/* Initialize character device node */
+	cdev_init(&device->cdev, &ddcci_fops);
+	device->cdev.owner = THIS_MODULE;
+
+	/* Publish char device */
+	device->dev.devt = ddcci_cdev_next;
+	ret = cdev_add(&device->cdev, ddcci_cdev_next, 1);
+	if (ret) {
+		device->dev.devt = 0;
+		goto out;
+	}
+
+	ddcci_cdev_next++;
+out:
+	return ret;
+}
+
+/* sysfs attributes */
+
+static ssize_t ddcci_attr_capabilities_show(struct device *dev,
+					    struct device_attribute *attr,
+					    char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+	size_t len;
+
+	if (likely(device != NULL)) {
+		len = device->capabilities_len;
+		if (unlikely(len > PAGE_SIZE))
+			len = PAGE_SIZE;
+		if (len == 0) {
+			ret = len;
+		} else {
+			memcpy(buf, device->capabilities, len);
+			if (likely(len < PAGE_SIZE)) {
+				buf[len] = '\n';
+				ret = len+1;
+			}
+		}
+	}
+
+	return ret;
+}
+
+static ssize_t ddcci_attr_prot_show(struct device *dev,
+				    struct device_attribute *attr, char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+	size_t len;
+
+	if (likely(device != NULL)) {
+		len = strnlen(device->prot, sizeof(device->prot));
+		strncpy(buf, device->prot, PAGE_SIZE);
+		if (len == 0) {
+			ret = len;
+		} else if (likely(len < PAGE_SIZE)) {
+			buf[len] = '\n';
+			ret = len+1;
+		} else {
+			ret = PAGE_SIZE;
+		}
+	}
+	return ret;
+}
+
+static ssize_t ddcci_attr_type_show(struct device *dev,
+				    struct device_attribute *attr, char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+	size_t len;
+
+	if (likely(device != NULL)) {
+		len = strnlen(device->type, sizeof(device->type));
+		strncpy(buf, device->type, PAGE_SIZE);
+		if (len == 0) {
+			ret = len;
+		} else if (likely(len < PAGE_SIZE)) {
+			buf[len] = '\n';
+			ret = len+1;
+		} else {
+			ret = PAGE_SIZE;
+		}
+	}
+	return ret;
+}
+
+static ssize_t ddcci_attr_model_show(struct device *dev,
+				     struct device_attribute *attr, char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+	size_t len;
+
+	if (likely(device != NULL)) {
+		len = strnlen(device->model, sizeof(device->model));
+		strncpy(buf, device->model, PAGE_SIZE);
+		if (len == 0) {
+			ret = len;
+		} else if (likely(len < PAGE_SIZE)) {
+			buf[len] = '\n';
+			ret = len+1;
+		} else {
+			ret = PAGE_SIZE;
+		}
+	}
+	return ret;
+}
+
+static ssize_t ddcci_attr_vendor_show(struct device *dev,
+				      struct device_attribute *attr, char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+	size_t len;
+
+	if (likely(device != NULL)) {
+		len = strnlen(device->vendor, sizeof(device->vendor));
+		strncpy(buf, device->vendor, PAGE_SIZE);
+		if (len == 0) {
+			ret = len;
+		} else if (likely(len < PAGE_SIZE)) {
+			buf[len] = '\n';
+			ret = len+1;
+		} else {
+			ret = PAGE_SIZE;
+		}
+	}
+	return ret;
+}
+
+static ssize_t ddcci_attr_module_show(struct device *dev,
+				      struct device_attribute *attr, char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+	size_t len;
+
+	if (likely(device != NULL)) {
+		len = strnlen(device->module, sizeof(device->module));
+		strncpy(buf, device->module, PAGE_SIZE);
+		if (len == 0) {
+			ret = len;
+		} else if (likely(len < PAGE_SIZE)) {
+			buf[len] = '\n';
+			ret = len+1;
+		} else {
+			ret = PAGE_SIZE;
+		}
+	}
+	return ret;
+}
+
+static ssize_t ddcci_attr_serial_show(struct device *dev,
+				      struct device_attribute *attr, char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+
+	if (likely(device != NULL))
+		ret = scnprintf(buf, PAGE_SIZE, "%d\n", device->device_number);
+
+	return ret;
+}
+
+static ssize_t ddcci_attr_modalias_show(struct device *dev,
+				      struct device_attribute *attr, char *buf)
+{
+	struct ddcci_device *device = ddcci_verify_device(dev);
+	ssize_t ret = -ENOENT;
+	char model[ARRAY_SIZE(device->model)];
+	char vendor[ARRAY_SIZE(device->model)];
+	char module[ARRAY_SIZE(device->model)];
+
+	if (likely(device != NULL)) {
+		memcpy(model, device->model, sizeof(model));
+		memcpy(vendor, device->vendor, sizeof(vendor));
+		memcpy(module, device->module, sizeof(module));
+		ddcci_modalias_clean(model, sizeof(model), '_');
+		ddcci_modalias_clean(vendor, sizeof(vendor), '_');
+		ddcci_modalias_clean(module, sizeof(module), '_');
+
+		ret = scnprintf(buf, PAGE_SIZE, "%s%s-%s-%s-%s-%s\n",
+			DDCCI_MODULE_PREFIX,
+			device->prot,
+			device->type,
+			model,
+			vendor,
+			module
+		);
+	}
+	return ret;
+}
+
+static DEVICE_ATTR(capabilities, S_IRUGO, ddcci_attr_capabilities_show, NULL);
+static DEVICE_ATTR(idProt, S_IRUGO, ddcci_attr_prot_show, NULL);
+static DEVICE_ATTR(idType, S_IRUGO, ddcci_attr_type_show, NULL);
+static DEVICE_ATTR(idModel, S_IRUGO, ddcci_attr_model_show, NULL);
+static DEVICE_ATTR(idVendor, S_IRUGO, ddcci_attr_vendor_show, NULL);
+static DEVICE_ATTR(idModule, S_IRUGO, ddcci_attr_module_show, NULL);
+static DEVICE_ATTR(idSerial, S_IRUGO, ddcci_attr_serial_show, NULL);
+static DEVICE_ATTR(modalias, S_IRUGO, ddcci_attr_modalias_show, NULL);
+
+static struct attribute *ddcci_char_device_attrs[] = {
+	&dev_attr_capabilities.attr,
+	&dev_attr_idProt.attr,
+	&dev_attr_idType.attr,
+	&dev_attr_idModel.attr,
+	&dev_attr_idVendor.attr,
+	&dev_attr_idModule.attr,
+	&dev_attr_idSerial.attr,
+	&dev_attr_modalias.attr,
+	NULL,
+};
+ATTRIBUTE_GROUPS(ddcci_char_device);
+
+/* DDC/CI bus */
+
+static int ddcci_device_uevent(struct device *dev, struct kobj_uevent_env *env)
+{
+	struct ddcci_device	*device = to_ddcci_device(dev);
+	char model[ARRAY_SIZE(device->model)];
+	char vendor[ARRAY_SIZE(device->vendor)];
+	char module[ARRAY_SIZE(device->module)];
+
+	memcpy(model, device->model, sizeof(model));
+	memcpy(vendor, device->vendor, sizeof(vendor));
+	memcpy(module, device->module, sizeof(module));
+	ddcci_modalias_clean(model, sizeof(model), '_');
+	ddcci_modalias_clean(vendor, sizeof(vendor), '_');
+	ddcci_modalias_clean(module, sizeof(module), '_');
+
+	if (add_uevent_var(env, "MODALIAS=%s%s-%s-%s-%s-%s",
+			   DDCCI_MODULE_PREFIX,
+			   device->prot,
+			   device->type,
+			   model,
+			   vendor,
+			   module
+		))
+		return -ENOMEM;
+
+	if (device->prot[0])
+		if (add_uevent_var(env, "DDCCI_PROT=%s", device->prot))
+			return -ENOMEM;
+
+	if (device->type[0])
+		if (add_uevent_var(env, "DDCCI_TYPE=%s", device->type))
+			return -ENOMEM;
+
+	if (device->model[0])
+		if (add_uevent_var(env, "DDCCI_MODEL=%s", device->model))
+			return -ENOMEM;
+
+	if (device->vendor[0]) {
+		if (add_uevent_var(env, "DDCCI_VENDOR=%s", device->vendor))
+			return -ENOMEM;
+
+		if (add_uevent_var(env, "DDCCI_MODULE=%s", device->module))
+			return -ENOMEM;
+
+		if (add_uevent_var(env, "DDCCI_UNIQ=%d", device->device_number))
+			return -ENOMEM;
+	}
+
+	return 0;
+}
+
+static void ddcci_device_release(struct device *dev)
+{
+	struct ddcci_device *device = to_ddcci_device(dev);
+	struct ddcci_driver *driver;
+
+	/* Notify driver */
+	if (dev->driver) {
+		driver = to_ddcci_driver(dev->driver);
+		if (driver->remove)
+			driver->remove(device);
+	}
+
+	/* Teardown chardev */
+	if (dev->devt) {
+		down(&core_lock);
+		if (device->cdev.dev == ddcci_cdev_next-1)
+			ddcci_cdev_next--;
+		cdev_del(&device->cdev);
+		up(&core_lock);
+	}
+
+	/* Free capability string */
+	if (device->capabilities) {
+		device->capabilities_len = 0;
+		kfree(device->capabilities);
+	}
+	/* Free device */
+	kfree(device);
+}
+
+static char *ddcci_devnode(struct device *dev,
+			 umode_t *mode, kuid_t *uid, kgid_t *gid)
+{
+	struct ddcci_device *device;
+
+	device = to_ddcci_device(dev);
+	return kasprintf(GFP_KERNEL, "bus/ddcci/%d/display",
+			 device->i2c_client->adapter->nr);
+}
+
+static char *ddcci_dependent_devnode(struct device *dev,
+			 umode_t *mode, kuid_t *uid, kgid_t *gid)
+{
+	struct ddcci_device *device;
+
+	device = to_ddcci_device(dev);
+	if (device->flags & DDCCI_FLAG_EXTERNAL) {
+		if (device->outer_addr == device->inner_addr)
+			return kasprintf(GFP_KERNEL, "bus/ddcci/%d/e%02x",
+					 device->i2c_client->adapter->nr,
+					 device->outer_addr);
+		else
+			return kasprintf(GFP_KERNEL, "bus/ddcci/%d/e%02x%02x",
+					 device->i2c_client->adapter->nr,
+					 device->outer_addr, device->inner_addr);
+	} else {
+		return kasprintf(GFP_KERNEL, "bus/ddcci/%d/i%02x",
+				 device->i2c_client->adapter->nr,
+				 device->inner_addr);
+	}
+}
+
+/* Device type for main DDC/CI devices*/
+static struct device_type ddcci_device_type = {
+	.name	= "ddcci-device",
+	.uevent		= ddcci_device_uevent,
+	.groups		= ddcci_char_device_groups,
+	.release	= ddcci_device_release,
+	.devnode	= ddcci_devnode
+};
+
+/* Device type for dependent DDC/CI devices*/
+static struct device_type ddcci_dependent_type = {
+	.name	= "ddcci-dependent-device",
+	.uevent		= ddcci_device_uevent,
+	.groups		= ddcci_char_device_groups,
+	.release	= ddcci_device_release,
+	.devnode	= ddcci_dependent_devnode
+};
+
+/**
+ * ddcci_verify_device - return parameter as ddcci_device, or NULL
+ * @dev: device, probably from some driver model iterator
+ */
+struct ddcci_device *ddcci_verify_device(struct device *dev)
+{
+	if (unlikely(!dev))
+		return NULL;
+	return (dev->type == &ddcci_device_type
+		|| dev->type == &ddcci_dependent_type)
+			? to_ddcci_device(dev)
+			: NULL;
+}
+EXPORT_SYMBOL(ddcci_verify_device);
+
+/**
+ * ddcci_quirks - Get quirks for DDC/CI device
+ * @dev: Target DDC/CI device
+ */
+unsigned long ddcci_quirks(struct ddcci_device *dev)
+{
+	if (unlikely(WARN_ON(!dev)))
+		return ~0L;
+	if (unlikely(WARN_ON(!dev->bus_drv_data)))
+		return ~0L;
+	return dev->bus_drv_data->quirks;
+}
+EXPORT_SYMBOL(ddcci_quirks);
+
+/**
+ * ddcci_register_driver - register DDC/CI driver
+ * @owner: the owning module
+ * @driver: the driver to register
+ */
+int ddcci_register_driver(struct module *owner, struct ddcci_driver *driver)
+{
+	int ret;
+
+	/* Can't register until after driver model init */
+	if (unlikely(WARN_ON(!ddcci_bus_type.p)))
+		return -EAGAIN;
+
+	pr_debug("registering driver [%s]\n", driver->driver.name);
+
+	/* add the driver to the list of ddcci drivers in the driver core */
+	driver->driver.owner = owner;
+	driver->driver.bus = &ddcci_bus_type;
+
+	/* When registration returns, the driver core
+	 * will have called probe() for all matching-but-unbound devices.
+	 */
+	ret = driver_register(&driver->driver);
+	if (ret)
+		return ret;
+
+	pr_debug("driver [%s] registered\n", driver->driver.name);
+
+	return 0;
+}
+EXPORT_SYMBOL(ddcci_register_driver);
+
+/**
+ * ddcci_del_driver - unregister DDC/CI driver
+ * @driver: the driver being unregistered
+ */
+void ddcci_del_driver(struct ddcci_driver *driver)
+{
+	driver_unregister(&driver->driver);
+	pr_debug("driver [%s] unregistered\n", driver->driver.name);
+}
+EXPORT_SYMBOL(ddcci_del_driver);
+
+/**
+ * ddcci_device_write - Write a message to a DDC/CI device
+ * @dev: Target DDC/CI device
+ * @p_flag: Protocol flag, true for standard control messages
+ * @data: Data that will be written to the device
+ * @length: How many bytes to write
+ *
+ * Writes the message to the device and sleeps (see module parameter 'delay')
+ */
+int ddcci_device_write(struct ddcci_device *dev, bool p_flag,
+		       unsigned char *data, unsigned char length)
+{
+	int ret;
+
+	if (down_interruptible(&dev->bus_drv_data->sem))
+		return -EAGAIN;
+
+	ret = ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, data, length);
+	msleep(delay);
+	up(&dev->bus_drv_data->sem);
+	return ret;
+}
+EXPORT_SYMBOL(ddcci_device_write);
+
+/**
+ * ddcci_device_read - Read a response from a DDC/CI device
+ * @dev: Target DDC/CI device
+ * @p_flag: Protocol flag, must match the corresponding write
+ * @buffer: Where to store data read from the device
+ * @length: Buffer size
+ */
+int ddcci_device_read(struct ddcci_device *dev, bool p_flag,
+		      unsigned char *buffer, unsigned char length)
+{
+	int ret;
+
+	if (down_interruptible(&dev->bus_drv_data->sem))
+		return -EAGAIN;
+
+	ret = ddcci_read(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, buffer, length);
+	up(&dev->bus_drv_data->sem);
+	return ret;
+}
+EXPORT_SYMBOL(ddcci_device_read);
+
+/**
+ * ddcci_device_writeread - Write a message to a device and read the response
+ * @dev: Target DDC/CI device
+ * @p_flag: Protocol flag, true for standard control messages
+ * @buffer: Buffer used for write and read
+ * @length: How many bytes to write
+ * @maxlength: Buffer size on read
+ *
+ * Writing, sleeping and reading are done without releasing the DDC/CI bus.
+ * This provides atomicity in respect to other DDC/CI accesses on the same bus.
+ */
+int ddcci_device_writeread(struct ddcci_device *dev, bool p_flag,
+			   unsigned char *buffer, unsigned char length,
+			   unsigned char maxlength)
+{
+	int ret;
+
+	if (down_interruptible(&dev->bus_drv_data->sem))
+		return -EAGAIN;
+
+	ret = ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, buffer, length);
+	if (ret < 0)
+		goto err;
+	msleep(delay);
+	ret = ddcci_read(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, buffer, maxlength);
+err:
+	up(&dev->bus_drv_data->sem);
+	return ret;
+}
+EXPORT_SYMBOL(ddcci_device_writeread);
+
+#define IS_ANY_ID(x) (((x)[0] == -1) && ((x)[7] == -1))
+
+/* Check if any device id in the array matches the device and return the matching id */
+static const struct ddcci_device_id *ddcci_match_id(const struct ddcci_device_id *id,
+						    const struct ddcci_device *device)
+{
+	while (id->prot[0] || id->type[0] || id->model[0] || id->vendor[0] || id->module[0]) {
+		if ((IS_ANY_ID(id->prot) || (strcmp(device->prot, id->prot) == 0))
+		 && (IS_ANY_ID(id->type) || (strcmp(device->type, id->type) == 0))
+		 && (IS_ANY_ID(id->model) || (strcmp(device->model, id->model) == 0))
+		 && (IS_ANY_ID(id->vendor) || (strcmp(device->vendor, id->vendor) == 0))
+		 && (IS_ANY_ID(id->module) || (strcmp(device->module, id->module) == 0))) {
+			return id;
+		}
+		id++;
+	}
+	return NULL;
+}
+
+static int ddcci_device_match(struct device *dev, struct device_driver *drv)
+{
+	struct ddcci_device	*device = ddcci_verify_device(dev);
+	struct ddcci_driver	*driver;
+
+	if (!device)
+		return 0;
+
+	driver = to_ddcci_driver(drv);
+	/* match on an id table if there is one */
+	if (driver->id_table)
+		return ddcci_match_id(driver->id_table, device) != NULL;
+
+	return 0;
+}
+
+static int ddcci_device_probe(struct device *dev)
+{
+	struct ddcci_device	*device = ddcci_verify_device(dev);
+	struct ddcci_driver	*driver;
+	const struct ddcci_device_id *id;
+	int ret = 0;
+
+	if (!device)
+		return -EINVAL;
+	driver = to_ddcci_driver(dev->driver);
+
+	id = ddcci_match_id(driver->id_table, device);
+	if (!id)
+		return -ENODEV;
+
+	if (driver->probe)
+		ret = driver->probe(device, id);
+
+	return ret;
+}
+
+static int ddcci_device_remove(struct device *dev)
+{
+	struct ddcci_device	*device = ddcci_verify_device(dev);
+	struct ddcci_driver	*driver;
+	int ret = 0;
+
+	if (!device)
+		return -EINVAL;
+	driver = to_ddcci_driver(dev->driver);
+
+	if (driver->remove)
+		ret = driver->remove(device);
+
+	return ret;
+}
+
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0)
+static void ddcci_device_remove_void(struct device *dev)
+{
+	ddcci_device_remove(dev);
+}
+#endif
+
+/**
+ * DDCCI bus type structure
+ */
+struct bus_type ddcci_bus_type = {
+	.name		= "ddcci",
+	.match		= ddcci_device_match,
+	.probe		= ddcci_device_probe,
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0)
+	.remove		= ddcci_device_remove_void
+#else
+	.remove		= ddcci_device_remove
+#endif
+};
+
+/* Main I2C driver */
+
+/* Get a pointer to the closing parenthesis */
+static char *ddcci_capstr_tok(const char *s, int depth)
+{
+	const char *ptr = s;
+	char *end;
+
+	if (s == NULL || s[0] == '\0')
+		return NULL;
+
+	while ((end = strpbrk(ptr, "()"))) {
+		if (!end || depth == INT_MAX)
+			return NULL;
+		if (*end == '(')
+			depth++;
+		else if (depth > 0)
+			depth--;
+		else
+			break;
+		ptr = end+1;
+	}
+	return end;
+}
+
+/**
+ * ddcci_find_capstr_item - Search capability string for a tag
+ * @capabilities: Capability string to search
+ * @tag: Tag to find
+ * @length: Buffer for the length of the found tag value (optional)
+ *
+ * Return a pointer to the start of the tag value (directly after the '(') on
+ * success and write the length of the value (excluding the ')') into `length`.
+ *
+ * If the tag is not found or another error occurs, an ERR_PTR is returned
+ * and `length` stays untouched.
+ */
+const char *ddcci_find_capstr_item(const char * capabilities,
+				   const char * __restrict tag,
+				   size_t *length)
+{
+	const char *src = capabilities, *ptr;
+	ptrdiff_t len;
+	int taglen = strnlen(tag, 1024);
+
+	/* Check length of requested tag */
+	if (unlikely(taglen <= 0 || taglen > 1023))
+		return ERR_PTR(-EINVAL);
+
+	/* Find tag */
+	while (src && (strncmp(src+1, tag, taglen) != 0 || src[1+taglen] != '('))
+		src = ddcci_capstr_tok(src+1, -1);
+	if (!src || src[0] == '\0')
+		return ERR_PTR(-ENOENT);
+
+	/* Locate end of value */
+	src += taglen+2;
+	ptr = ddcci_capstr_tok(src, 0);
+	if (unlikely(!ptr))
+		return ERR_PTR(-EOVERFLOW);
+
+	/* Check length of tag data */
+	len = ptr-src;
+	if (unlikely(len < 0 || len > 65535))
+		return ERR_PTR(-EMSGSIZE);
+
+	/* Return pointer and length */
+	if (likely(length != NULL))
+		*length = (size_t)len;
+	return src;
+}
+EXPORT_SYMBOL(ddcci_find_capstr_item);
+
+/* Search the capability string for a tag and copy the value to dest */
+static int ddcci_cpy_capstr_item(char *dest, const char *src,
+				  const char * __restrict tag, size_t maxlen)
+{
+	const char *ptr;
+	size_t len;
+
+	/* Find tag */
+	ptr = ddcci_find_capstr_item(src, tag, &len);
+	if (IS_ERR(ptr)) {
+		return PTR_ERR(ptr);
+	}
+
+	/* Copy value */
+	memcpy(dest, ptr, min(len, maxlen));
+	return 0;
+}
+
+/* Fill fields in device by parsing the capability string */
+static int ddcci_parse_capstring(struct ddcci_device *device)
+{
+	const char *capstr = device->capabilities;
+	int ret = 0;
+
+	if (!capstr)
+		return -EINVAL;
+
+	/* capability string start with a paren */
+	if (capstr[0] != '(')
+		return -EINVAL;
+
+	/* get prot(...) */
+	ret = ddcci_cpy_capstr_item(device->prot, capstr, "prot", sizeof(device->prot)-1);
+	if (ret) {
+		if (ret == -ENOENT) {
+			dev_warn(&device->dev, "malformed capability string: no protocol tag");
+			memset(device->prot, 0, sizeof(device->prot)-1);
+		} else {
+			return ret;
+		}
+	}
+
+	/* get type(...) */
+	ret = ddcci_cpy_capstr_item(device->type, capstr, "type", sizeof(device->type)-1);
+	if (ret) {
+		if (ret == -ENOENT) {
+			dev_warn(&device->dev, "malformed capability string: no type tag");
+			memset(device->type, 0, sizeof(device->type)-1);
+		} else {
+			return ret;
+		}
+	}
+
+	/* and then model(...) */
+	ret = ddcci_cpy_capstr_item(device->model, capstr, "model", sizeof(device->model)-1);
+	if (ret) {
+		if (ret == -ENOENT) {
+			dev_warn(&device->dev, "malformed capability string: no model tag");
+			memset(device->model, 0, sizeof(device->model)-1);
+		} else {
+			return ret;
+		}
+	}
+
+	/* if there is no protocol tag */
+	if (!device->prot[0]) {
+		/* and no type tag: give up. */
+		if (!device->type[0])
+			return -ENOENT;
+
+		/* Assume protocol "monitor" if type is "LCD" or "CRT" */
+		if (strncasecmp(device->type, "LCD", sizeof(device->type)-1) == 0
+		 || strncasecmp(device->type, "CRT", sizeof(device->type)-1) == 0) {
+			memcpy(device->prot, "monitor", 7);
+		}
+	}
+
+	/* skip the rest for now */
+
+	return 0;
+}
+
+/* Probe for a device on an inner address and create a ddcci_device for it */
+static int ddcci_detect_device(struct i2c_client *client, unsigned char addr,
+			       int dependent)
+{
+	int ret;
+	unsigned char outer_addr = client->addr << 1;
+	unsigned char *buffer = NULL;
+	struct ddcci_bus_drv_data *drv_data = i2c_get_clientdata(client);
+	struct ddcci_device *device = NULL;
+
+	down(&drv_data->sem);
+
+	/* Allocate buffer big enough for any capability string */
+	buffer = kmalloc(16384, GFP_KERNEL);
+	if (!buffer) {
+		ret = -ENOMEM;
+		goto err_end;
+	}
+
+	/* Allocate device struct */
+	device = kzalloc(sizeof(struct ddcci_device), GFP_KERNEL);
+	if (!device) {
+		ret = -ENOMEM;
+		goto err_end;
+	}
+
+	/* Initialize device */
+	device_initialize(&device->dev);
+	device->dev.parent = &client->dev;
+	device->dev.bus = &ddcci_bus_type;
+	device->outer_addr = outer_addr;
+	device->inner_addr = addr;
+	device->bus_drv_data = drv_data;
+	device->i2c_client = client;
+
+	if (!dependent) {
+		device->dev.type = &ddcci_device_type;
+		ret = dev_set_name(&device->dev, "ddcci%d", client->adapter->nr);
+	} else if (outer_addr == dependent) {
+		/* Internal dependent device */
+		device->dev.type = &ddcci_dependent_type;
+		device->flags = DDCCI_FLAG_DEPENDENT;
+		ret = dev_set_name(&device->dev, "ddcci%di%02x", client->adapter->nr, addr);
+	} else if (outer_addr == addr) {
+		/* External dependent device */
+		device->dev.type = &ddcci_dependent_type;
+		device->flags = DDCCI_FLAG_DEPENDENT | DDCCI_FLAG_EXTERNAL;
+		ret = dev_set_name(&device->dev, "ddcci%de%02x", client->adapter->nr, addr);
+	} else {
+		/* Dependent device of external dependent device
+		   Just in case something like this exists */
+		device->dev.type = &ddcci_dependent_type;
+		device->flags = DDCCI_FLAG_DEPENDENT | DDCCI_FLAG_EXTERNAL;
+		ret = dev_set_name(&device->dev, "ddcci%de%02x%02x", client->adapter->nr, outer_addr, addr);
+	}
+
+	if (ret)
+		goto err_free;
+
+	/* Read identification and check for quirks */
+	ret = ddcci_identify_device(client, addr, buffer, 29);
+	if (ret < 0) {
+		if (!dependent && (ret == -EBADMSG || ret == -EMSGSIZE)) {
+			dev_warn(&device->dev, "DDC/CI main device sent broken response on identification. Trying to detect solely based on capability information.\n");
+		} else {
+			goto err_free;
+		}
+	}
+
+	if (ret == 29 && buffer[0] == DDCCI_REPLY_ID) {
+		memcpy(device->vendor, &buffer[7], 8);
+		memcpy(device->module, &buffer[17], 8);
+		device->device_number = be32_to_cpu(*(__force __be32 *)&buffer[18]);
+	}
+
+	/* Read capabilities */
+	ret = ddcci_get_caps(client, addr, buffer, 16384);
+	if (ret > 0) {
+		/* Fixup unparenthesized capability strings, but only if the first
+		   character is an ascii lower case letter.
+		   This should still allow an early exit for completely garbled
+		   data but helps detecting devices where only the parentheses are
+		   missing, as the second char must be the first character of a
+		   keyword. */
+		if (ret > 2 && buffer[0] >= 'a' && buffer[0] <= 'z') {
+			dev_err(&device->dev, "DDC/CI device quirk detected: unparenthesized capability string\n");
+			device->capabilities = kzalloc(ret+3, GFP_KERNEL);
+			if (!device->capabilities) {
+				ret = -ENOMEM;
+				goto err_free;
+			}
+			device->capabilities_len = ret+2;
+			memcpy(&(device->capabilities[1]), buffer, ret);
+			device->capabilities[0] = '(';
+			device->capabilities[ret+1] = ')';
+		} else {
+			/* Standard case: simply copy the received string */
+			device->capabilities = kzalloc(ret+1, GFP_KERNEL);
+			if (!device->capabilities) {
+				ret = -ENOMEM;
+				goto err_free;
+			}
+			device->capabilities_len = ret;
+			memcpy(device->capabilities, buffer, ret);
+		}
+
+		ret = ddcci_parse_capstring(device);
+		if (ret) {
+			dev_err(&device->dev, "malformed capability string: \"%s\" errno %d\n", device->capabilities, ret);
+			ret = -EINVAL;
+			goto err_free;
+		}
+	}
+
+	/* Found a device if either identification or capabilities succeeded */
+	if (!device->capabilities && device->vendor[0] == '\0') {
+		dev_dbg(&client->dev,
+			"[%02x:%02x] got neither valid identification nor capability data\n",
+			client->addr << 1, addr);
+		ret = -ENODEV;
+		goto err_free;
+	}
+
+	/* Setup chardev */
+	down(&core_lock);
+	ret = ddcci_setup_char_device(device);
+	up(&core_lock);
+	if (ret)
+		goto err_free;
+
+	/* Release semaphore and add device to the tree */
+	up(&drv_data->sem);
+	pr_debug("found device at %d:%02x:%02x\n", client->adapter->nr, outer_addr, addr);
+	ret = device_add(&device->dev);
+	if (ret)
+		goto err_free;
+
+	goto end;
+err_free:
+	put_device(&device->dev);
+err_end:
+	up(&drv_data->sem);
+end:
+	kfree(buffer);
+	return ret;
+}
+
+/* I2C detect function: check if a main or external dependent device exists */
+static int ddcci_detect(struct i2c_client *client, struct i2c_board_info *info)
+{
+	int ret;
+	unsigned char outer_addr;
+	unsigned char inner_addr;
+	unsigned char buf[32];
+	unsigned char cmd_id[1] = { DDCCI_COMMAND_ID };
+	unsigned char cmd_caps[3] = { DDCCI_COMMAND_CAPS, 0x00, 0x00};
+	unsigned char *cmd;
+	unsigned int cmd_len;
+
+	/* Check for i2c_master_* functionality */
+	if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
+		pr_debug("i2c adapter %d unsuitable: no i2c_master functionality\n", client->adapter->nr);
+		return -ENODEV;
+	}
+
+	/* send Capabilities Request (for main) or Identification Request command (for dependent devices) */
+	outer_addr = client->addr << 1;
+	inner_addr = (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? DDCCI_HOST_ADDR_ODD : outer_addr | 1;
+	cmd = (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? cmd_caps : cmd_id;
+	cmd_len = (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? sizeof(cmd_caps) : sizeof(cmd_id);
+	pr_debug("detecting %d:%02x\n", client->adapter->nr, outer_addr);
+
+	ret = __ddcci_write_block(client, inner_addr, buf, true, cmd, cmd_len);
+
+	if (ret == -ENXIO || ret == -EIO) {
+		if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_WRITE_BYTE)) {
+			pr_debug("i2c write failed with ENXIO or EIO but bytewise writing is not supported\n");
+			return -ENODEV;
+		}
+		pr_debug("i2c write failed with ENXIO or EIO, trying bytewise writing\n");
+		ret = __ddcci_write_bytewise(client, inner_addr, true, cmd, cmd_len);
+		if (ret == 0) {
+			msleep(delay);
+			ret = __ddcci_write_bytewise(client, inner_addr, true, cmd, cmd_len);
+		}
+	}
+
+	if (ret < 0)
+		return -ENODEV;
+
+	/* wait for device */
+	msleep(delay);
+	/* receive answer */
+	ret = i2c_master_recv(client, buf, 32);
+	if (ret < 3) {
+		pr_debug("detection failed: no answer\n");
+		return -ENODEV;
+	}
+
+	/* check response starts with outer addr */
+	if (buf[0] != outer_addr) {
+		pr_debug("detection failed: invalid %s response (%02x != %02x)\n", (cmd == cmd_id) ? "identification" : "capabilities", buf[0], outer_addr);
+		pr_debug("received message was %*ph \n", ret, buf);
+		return -ENODEV;
+	}
+
+	pr_debug("detected %d:%02x\n", client->adapter->nr, outer_addr);
+
+	/* set device type */
+	strlcpy(info->type, (outer_addr == DDCCI_DEFAULT_DEVICE_ADDR) ? "ddcci" : "ddcci-dependent", I2C_NAME_SIZE);
+
+	return 0;
+}
+
+/* I2C probe function */
+static int ddcci_probe(struct i2c_client *client, const struct i2c_device_id *id)
+{
+	int i, ret = -ENODEV, tmp;
+	unsigned char main_addr, addr;
+	struct ddcci_bus_drv_data *drv_data;
+
+	/* Initialize driver data structure */
+	drv_data = devm_kzalloc(&client->dev, sizeof(struct ddcci_bus_drv_data), GFP_KERNEL);
+	if (!drv_data)
+		return -ENOMEM;
+	drv_data->i2c_dev = client;
+	sema_init(&drv_data->sem, 1);
+
+	/* Set i2c client data */
+	i2c_set_clientdata(client, drv_data);
+
+	if (id->driver_data == 0) {
+		/* Core device, probe at 0x6E */
+		main_addr = DDCCI_DEFAULT_DEVICE_ADDR;
+		dev_dbg(&client->dev, "probing core device [%02x]\n",
+			client->addr << 1);
+		ret = ddcci_detect_device(client, main_addr, 0);
+		if (ret) {
+			dev_info(&client->dev, "core device [%02x] probe failed: %d\n",
+				 client->addr << 1, ret);
+			if (ret == -EIO)
+				ret = -ENODEV;
+			goto err_free;
+		}
+
+		/* Detect internal dependent devices */
+		dev_dbg(&client->dev, "probing internal dependent devices\n");
+		for (i = 0; i < autoprobe_addr_count; ++i) {
+			addr = (unsigned short)autoprobe_addrs[i];
+			if ((addr & 1) == 0 && addr != main_addr) {
+				tmp = ddcci_detect_device(client, addr, main_addr);
+				if (tmp < 0 && tmp != -ENODEV) {
+					dev_info(&client->dev, "internal dependent device [%02x:%02x] probe failed: %d\n",
+						 client->addr << 1, addr, ret);
+				}
+			}
+		}
+	} else if (id->driver_data == 1) {
+		/* External dependent device */
+		main_addr = client->addr << 1;
+		dev_dbg(&client->dev, "probing external dependent device [%02x]\n", main_addr);
+		ret = ddcci_detect_device(client, main_addr, -1);
+		if (ret) {
+			dev_info(&client->dev, "external dependent device [%02x] probe failed: %d\n",
+				 main_addr, ret);
+			if (ret == -EIO)
+				ret = -ENODEV;
+			goto err_free;
+		}
+	} else {
+		dev_warn(&client->dev,
+			 "probe() called with invalid i2c device id\n");
+		ret = -EINVAL;
+	}
+
+	goto end;
+err_free:
+	devm_kfree(&client->dev, drv_data);
+end:
+	return ret;
+}
+
+/*
+ * Callback for bus_find_device() used in ddcci_remove()
+ *
+ * Find next device on i2c_client not flagged with
+ * DDCCI_FLAG_REMOVED and flag it.
+ */
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,3,0)
+static int ddcci_remove_helper(struct device *dev, const void *p)
+#else
+static int ddcci_remove_helper(struct device *dev, void *p)
+#endif
+{
+	struct ddcci_device *device;
+
+	device = ddcci_verify_device(dev);
+	if (!device || device->flags & DDCCI_FLAG_REMOVED)
+		return 0;
+
+	if (!p || (dev->parent == p)) {
+		device->flags |= DDCCI_FLAG_REMOVED;
+		wmb();
+		return 1;
+	}
+
+	return 0;
+}
+
+/* I2C driver remove callback: unregister all subdevices */
+static int ddcci_remove(struct i2c_client *client)
+{
+	struct ddcci_bus_drv_data *drv_data = i2c_get_clientdata(client);
+	struct device *dev;
+
+	down(&drv_data->sem);
+	while (1) {
+		dev = bus_find_device(&ddcci_bus_type, NULL, client,
+				      ddcci_remove_helper);
+		if (!dev)
+			break;
+		device_unregister(dev);
+		put_device(dev);
+	}
+	up(&drv_data->sem);
+	return 0;
+}
+
+/*
+ * I2C driver device identification table.
+ */
+static const struct i2c_device_id ddcci_idtable[] = {
+	{ "ddcci", 0 },
+	{ "ddcci-dependent", 1 },
+	{}
+};
+MODULE_DEVICE_TABLE(i2c, ddcci_idtable);
+
+/*
+ * I2C driver description structure
+ */
+static struct i2c_driver ddcci_driver = {
+	.driver = {
+		.name	= "ddcci",
+		.owner	= THIS_MODULE,
+	},
+
+	.id_table	= ddcci_idtable,
+	.probe		= ddcci_probe,
+	.remove		= ddcci_remove,
+	.class		= I2C_CLASS_DDC,
+	.detect		= ddcci_detect,
+	.address_list	= I2C_ADDRS(
+		DDCCI_DEFAULT_DEVICE_ADDR>>1
+	),
+};
+
+/*
+ * Module initialization function. Called when the module is inserted or
+ * (if builtin) at boot time.
+ */
+static int __init ddcci_module_init(void)
+{
+	int ret;
+
+	pr_debug("initializing ddcci driver\n");
+	/* Allocate a device number region for the character devices */
+	ret = alloc_chrdev_region(&ddcci_cdev_first, 0, 128, DEVICE_NAME);
+	if (ret < 0) {
+		pr_err("failed to register device region: error %d\n", ret);
+		goto err_chrdevreg;
+	}
+	ddcci_cdev_next = ddcci_cdev_first;
+	ddcci_cdev_end = MKDEV(MAJOR(ddcci_cdev_first), MINOR(ddcci_cdev_first)+128);
+
+	/* Register bus */
+	ret = bus_register(&ddcci_bus_type);
+	if (ret) {
+		pr_err("failed to register bus 'ddcci'\n");
+		goto err_busreg;
+	}
+
+	/* Register I2C driver */
+	ret = i2c_add_driver(&ddcci_driver);
+	if (ret) {
+		pr_err("failed to register i2c driver\n");
+		goto err_drvreg;
+	}
+
+	pr_debug("ddcci driver initialized\n");
+
+	return 0;
+
+err_drvreg:
+	bus_unregister(&ddcci_bus_type);
+err_busreg:
+	unregister_chrdev_region(ddcci_cdev_first, 128);
+err_chrdevreg:
+	return ret;
+}
+
+/*
+ * Module clean-up function. Called when the module is removed.
+ */
+static void __exit ddcci_module_exit(void)
+{
+	struct device *dev;
+
+	while (1) {
+		dev = bus_find_device(&ddcci_bus_type, NULL, NULL, ddcci_remove_helper);
+		if (!dev)
+			break;
+		device_unregister(dev);
+		put_device(dev);
+	}
+
+	i2c_del_driver(&ddcci_driver);
+	bus_unregister(&ddcci_bus_type);
+	unregister_chrdev_region(ddcci_cdev_first, 128);
+}
+
+/* Let the kernel know the calls for module init and exit */
+module_init(ddcci_module_init);
+module_exit(ddcci_module_exit);
+
+/* Module parameter description */
+module_param(delay, uint, S_IRUGO|S_IWUSR);
+MODULE_PARM_DESC(delay, "default delay after bus writes (in ms, default 60)");
+module_param_array(autoprobe_addrs, ushort, &autoprobe_addr_count, S_IRUGO|S_IWUSR);
+MODULE_PARM_DESC(autoprobe_addrs, "internal dependent device addresses to autoprobe");
+
+/* Module description */
+MODULE_AUTHOR("Christoph Grenz");
+MODULE_DESCRIPTION("DDC/CI bus driver");
+MODULE_VERSION("0.4.2");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/ddcci.h b/include/linux/ddcci.h
new file mode 100644
index 000000000000..a219f031e584
--- /dev/null
+++ b/include/linux/ddcci.h
@@ -0,0 +1,164 @@
+/*
+ *  DDC/CI bus driver
+ *
+ *  Copyright (c) 2015 Christoph Grenz
+ */
+
+/*
+ * 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.
+ */
+
+#ifndef _DDCCI_H
+#define _DDCCI_H
+
+#include <linux/mod_devicetable.h>
+#include <linux/device.h>
+#include <linux/cdev.h>
+
+#define DDCCI_MODULE_PREFIX "ddcci:"
+
+/* Special addresses */
+
+/* default device address (even) */
+#define DDCCI_DEFAULT_DEVICE_ADDR	0x6E
+/* receiving host address for communication with default device address */
+#define DDCCI_HOST_ADDR_EVEN	0x50
+/* sending host address for communication with default device address */
+#define DDCCI_HOST_ADDR_ODD	0x51
+
+/* Command codes */
+
+/* Identification Request */
+#define DDCCI_COMMAND_ID	0xf1
+/* Identification Reply */
+#define DDCCI_REPLY_ID	0xe1
+/* Capabilities Request */
+#define DDCCI_COMMAND_CAPS	0xf3
+/* Capabilities Reply */
+#define DDCCI_REPLY_CAPS	0xe3
+
+/* Quirks */
+
+/* Device always responds with unset protocol flag */
+#define DDCCI_QUIRK_NO_PFLAG BIT(1)
+/* Device needs writing one byte at a time  */
+#define DDCCI_QUIRK_WRITE_BYTEWISE BIT(2)
+/* Device repeats first byte on read */
+#define DDCCI_QUIRK_SKIP_FIRST_BYTE BIT(3)
+
+/* Flags */
+
+#define DDCCI_FLAG_REMOVED BIT(1)
+#define DDCCI_FLAG_DEPENDENT BIT(2)
+#define DDCCI_FLAG_EXTERNAL BIT(3)
+
+extern struct bus_type ddcci_bus_type;
+
+struct ddcci_bus_drv_data;
+
+/* struct ddcci_device_id - identifies DDC/CI devices for probing */
+struct ddcci_device_id {
+	char prot[9];
+	char type[9];
+	char model[9];
+	char vendor[9];
+	char module[9];
+	kernel_ulong_t driver_data;	/* Data private to the driver */
+};
+#define DDCCI_ANY_ID "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"
+
+/**
+ * struct ddcci_device - represent an DDC/CI device
+ * @outer_addr: Outer device address (I2C address << 1).
+ * @inner_addr: Inner device address.
+ * @flags: Device flags.
+ * @capabilities: Device capability string.
+ * @capabilities_len: Length of capability string.
+ * @i2c_client: Parent I2C device.
+ * @bus_drv_data: Driver internal data structure.
+ * @dev: Driver model device node for the slave.
+ * @cdev: Character device structure
+ * @cdev_sem: RW semaphore for exclusive access on character device.
+ * @prot: Device class ("protocol", from capability string)
+ * @type: Device subclass ("type", from capability string)
+ * @model: Device model (from capability string)
+ * @vendor: Device vendor (from identification command response)
+ * @module: Device module (from identification command response)
+ * @device_number: Device serial (from identification command response)
+ */
+struct ddcci_device {
+	unsigned short outer_addr;
+	unsigned short inner_addr;
+	int flags;
+	char *capabilities;
+	size_t capabilities_len;
+	struct i2c_client *i2c_client;
+	struct ddcci_bus_drv_data *bus_drv_data;
+	struct device dev;
+	struct cdev cdev;
+	struct rw_semaphore cdev_sem;
+	char prot[9];
+	char type[9];
+	char model[9];
+	char vendor[9];
+	char module[9];
+	int device_number;
+};
+#define to_ddcci_device(d) container_of(d, struct ddcci_device, dev)
+
+/**
+ * struct ddcci_driver - represent an DDC/CI device driver
+ * @probe: Callback for device binding
+ * @remove: Callback for device unbinding
+ * @driver: Device driver model driver
+ * @id_table: List of DDC/CI devices supported by this driver
+ *
+ * The driver.owner field should be set to the module owner of this driver.
+ * The driver.name field should be set to the name of this driver.
+ */
+struct ddcci_driver {
+	int (*probe)(struct ddcci_device *, const struct ddcci_device_id *);
+	int (*remove)(struct ddcci_device *);
+	struct device_driver driver;
+	struct ddcci_device_id *id_table;
+};
+#define to_ddcci_driver(d) container_of(d, struct ddcci_driver, driver)
+
+int ddcci_register_driver(struct module *owner, struct ddcci_driver *driver);
+#define ddcci_add_driver(driver) \
+	ddcci_register_driver(THIS_MODULE, driver)
+void ddcci_del_driver(struct ddcci_driver *driver);
+
+struct ddcci_device *ddcci_verify_device(struct device *dev);
+
+#define module_ddcci_driver(__ddcci_driver) \
+	module_driver(__ddcci_driver, ddcci_add_driver, \
+			ddcci_del_driver)
+
+int ddcci_device_write(struct ddcci_device *, bool p_flag, unsigned char *data,
+		       unsigned char length);
+int ddcci_device_read(struct ddcci_device *, bool p_flag, unsigned char *buffer,
+		      unsigned char length);
+int ddcci_device_writeread(struct ddcci_device *, bool p_flag,
+			   unsigned char *buffer, unsigned char length,
+			   unsigned char maxlength);
+
+static inline void *ddcci_get_drvdata(const struct ddcci_device *dev)
+{
+	return dev_get_drvdata(&dev->dev);
+}
+
+static inline void ddcci_set_drvdata(struct ddcci_device *dev, void *data)
+{
+	dev_set_drvdata(&dev->dev, data);
+}
+
+unsigned long ddcci_quirks(struct ddcci_device *dev);
+
+const char *ddcci_find_capstr_item(const char *capabilities, const char *tag,
+				   size_t *length);
+
+#endif
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2022-02-26 21:59 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-02-26  2:20 [PATCH] drivers: ddcci: upstream DDCCI driver Yusuf Khan
2022-02-26  2:33 ` Yusuf Khan
2022-02-26  9:38 ` Arnd Bergmann
2022-02-26 10:48   ` Yusuf Khan
2022-02-26 21:58     ` Arnd Bergmann
2022-02-26  3:57 Yusuf Khan
2022-02-26  4:01 ` Yusuf Khan

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).