From nobody Fri Jun 19 13:27:17 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 5C62CC433F5 for ; Sun, 3 Apr 2022 23:09:45 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1376625AbiDCXL0 (ORCPT ); Sun, 3 Apr 2022 19:11:26 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:34190 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1376618AbiDCXLX (ORCPT ); Sun, 3 Apr 2022 19:11:23 -0400 Received: from mail-pl1-x629.google.com (mail-pl1-x629.google.com [IPv6:2607:f8b0:4864:20::629]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 04A0B24F0F for ; Sun, 3 Apr 2022 16:09:26 -0700 (PDT) Received: by mail-pl1-x629.google.com with SMTP id j8so6735252pll.11 for ; Sun, 03 Apr 2022 16:09:25 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=6pInyxaD18PWfjdeovZjZKsCDrUv41lvyiFA8Mw663M=; b=F87NS6s02iwLeuBmu6yaxbIu+2ov77A/VRkbwJSlrpML8Z/0cw0Yr15drwtLKyW2bZ fdMsBUjTAHiUx83cmJMHLQVeDelcacreKnvDNNq5ilB8EVXibQBOvxW29+UfJzXJ5m/u HbS0VCsmQLOf+hj3xOEnJgdfd+z5dRxYyVXFOWbzVjLy87A0GcZk5Ap+J08TvQSPEG+Z RsEVuIg32COQMruZJqrryUPVaWnZDaHNFzHd/6l6GAFVMbeyd4RdMEU/olRLRZtG1YOp EkNUBpDyxmVa17EMue6lQXbmvVWGqq5wVXPmJjrFqqZpMz66AY0RJlcr7xAVBnAhLzYq zjKg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=6pInyxaD18PWfjdeovZjZKsCDrUv41lvyiFA8Mw663M=; b=yRiRjlllXPTOhdDrgB0DX5enZJNjHujH9yjNX9z3wTJyBpiOk3QFF3vxhNOqW7gt2i ziBSTIJZtjdXAYwyvDfKsFJerwq+ROXB3vD5w2Bnpa2KgqucUs8lzSW3R/gUoFCfyMWF W29AB/dN1PM9RKzNxfd+L3W8B2tAWXmuyZj9OgTHqVm/vD6RfYNcD1JAjZp9F5iPzBRG m7zPx497U91EBcIhqAlnzIJrVEqnqwkx4IerQuIyYceHUaqc/lcIMCVIzth3dbCmTITh NzlCn+D8uDnD0y7v5rn4PQGcsI4BIxpeNHodLveo7lINHLIHNzSTeBLcvLApGt8LNHYK zjSQ== X-Gm-Message-State: AOAM531QtZVbVCQmp14KuEtvPoQujMljgLFgwYlV59FDPHNMm3qBEHxj 9WAptm6ty4bYIiGAwIXyhOM/tD6A3K+Y2w== X-Google-Smtp-Source: ABdhPJzJ6MheTbY0wG5SJU5bTS2vCs4zhgZw69vT6TUvfafJUxJ2J+bSl8vV7G3nxxyho1l+ZxGfuw== X-Received: by 2002:a17:902:db05:b0:154:8219:ce1f with SMTP id m5-20020a170902db0500b001548219ce1fmr57339168plx.82.1649027364363; Sun, 03 Apr 2022 16:09:24 -0700 (PDT) Received: from yusufkhan-MS-7B17.lan ([24.17.200.29]) by smtp.gmail.com with ESMTPSA id p16-20020a056a000b5000b004faed463907sm10164503pfo.0.2022.04.03.16.09.23 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 03 Apr 2022 16:09:23 -0700 (PDT) From: Yusuf Khan To: linux-kernel@vger.kernel.org Cc: jasowang@redhat.com, mikelley@microsoft.com, mst@redhat.com, gregkh@linuxfoundation.org, javier@javigon.com, arnd@arndb.de, will@kernel.org, axboe@kernel.dk, Yusuf Khan , Christoph Grenz Subject: [PATCH v10 1/3] drivers: ddcci: add drivers for DDCCI Date: Sun, 3 Apr 2022 16:08:49 -0700 Message-Id: <20220403230850.2986-2-yusisamerican@gmail.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20220403230850.2986-1-yusisamerican@gmail.com> References: <20220403230850.2986-1-yusisamerican@gmail.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Content-Type: text/plain; charset="utf-8" This patch adds the main DDCCI driver. This interfaces with I2C to allow the kernel to communicate with DDCCI supporting monitors, it exports a chardev and sysfs interface for userspace. Signed-off-by: Yusuf Khan Signed-off-by: Christoph Grenz Reported-by: kernel test robot --- drivers/char/Kconfig | 11 + drivers/char/Makefile | 1 + drivers/char/ddcci.c | 1805 +++++++++++++++++++++++++++++++++++++++++ include/linux/ddcci.h | 163 ++++ 4 files changed, 1980 insertions(+) create mode 100644 drivers/char/ddcci.c create mode 100644 include/linux/ddcci.h diff --git a/drivers/char/Kconfig b/drivers/char/Kconfig index 740811893c57..2449c17e1080 100644 --- a/drivers/char/Kconfig +++ b/drivers/char/Kconfig @@ -451,4 +451,15 @@ config RANDOM_TRUST_BOOTLOADER pool. Otherwise, say N here so it will be regarded as device input that only mixes the entropy pool. =20 +config DDCCI + tristate "DDCCI display protocol support" + depends on I2C + help + Display Data Channel Command Interface is an + interface that allows the kernel to "talk" + to most displays made after 2005. Check your + display's specification to see if it has + support for this. This depends on I2C to + compile. + endmenu diff --git a/drivers/char/Makefile b/drivers/char/Makefile index 264eb398fdd4..eaa2d58d50df 100644 --- a/drivers/char/Makefile +++ b/drivers/char/Makefile @@ -3,6 +3,7 @@ # Makefile for the kernel character device drivers. # =20 +obj-$(CONFIG_DDCCI) +=3D ddcci.o obj-y +=3D mem.o random.o obj-$(CONFIG_TTY_PRINTK) +=3D ttyprintk.o obj-y +=3D misc.o diff --git a/drivers/char/ddcci.c b/drivers/char/ddcci.c new file mode 100644 index 000000000000..2861c537468f --- /dev/null +++ b/drivers/char/ddcci.c @@ -0,0 +1,1805 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * 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 F= ree + * Software Foundation; either version 2 of the License, or (at your optio= n) + * any later version. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define DDCCI_RECV_BUFFER_SIZE 130 +#define DEVICE_NAME "ddcci" +#define DDCCI_MAX_CAP_CHUNKS 200 + +static unsigned int delay =3D 60; +static unsigned short autoprobe_addrs[127] =3D {0xF0, 0xF2, 0xF4, 0xF6, 0x= F8}; +static int autoprobe_addr_count =3D 5; + +static dev_t ddcci_cdev_first; +static dev_t ddcci_cdev_next; +static dev_t ddcci_cdev_end; +static DEFINE_MUTEX(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 mutex mut; + 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 =3D 0; i < n; ++i) { + char c =3D string[i]; + + if (c =3D=3D 0) + return; + else if (isalpha(c)) + string[i] =3D 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 =3D 0; + unsigned char outer_addr =3D (unsigned char) (client->addr << 1); + unsigned int xor =3D outer_addr; /* initial xor value */ + + /* Consistency checks, maximum allowed by DDCCI spec */ + if (len > 32) + return -EINVAL; + + /* Special case: reply to 0x6E is always 0x51 */ + if (addr =3D=3D DDCCI_DEFAULT_DEVICE_ADDR) { + addr =3D DDCCI_HOST_ADDR_ODD; + } else { + /* When sending the odd address is used */ + addr =3D addr | 1; + } + + /* first byte: sender address */ + xor ^=3D addr; + ret =3D i2c_smbus_write_byte(client, addr); + if (ret < 0) + return ret; + + /* second byte: protocol flag and message size */ + xor ^=3D ((p_flag << 7) | len); + ret =3D i2c_smbus_write_byte(client, (p_flag << 7) | len); + if (ret < 0) + return ret; + + /* send payload */ + while (len--) { + xor ^=3D (*buf); + ret =3D i2c_smbus_write_byte(client, (*buf)); + if (ret < 0) + return ret; + buf++; + } + + /* send checksum */ + ret =3D 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 ad= dr, + unsigned char *sendbuf, bool p_flag, + const unsigned char *data, unsigned char len) +{ + unsigned char outer_addr =3D (unsigned char) (client->addr << 1); + unsigned int xor =3D outer_addr; /* initial xor value */ + unsigned char *ptr =3D sendbuf; + + /* Consistency checks, maximum allowed by DDCCI spec */ + if (len > 32) + return -EINVAL; + + /* Special case: sender to 0x6E is always 0x51 */ + if (addr =3D=3D DDCCI_DEFAULT_DEVICE_ADDR) { + addr =3D DDCCI_HOST_ADDR_ODD; + } else { + /* When sending the odd address is used */ + addr =3D addr | 1; + } + + /* first byte: sender address */ + xor ^=3D addr; + *(ptr++) =3D addr; + /* second byte: protocol flag and message size */ + xor ^=3D ((p_flag << 7) | len); + *(ptr++) =3D (p_flag << 7) | len; + /* payload */ + while (len--) { + xor ^=3D (*data); + *(ptr++) =3D (*data); + data++; + } + /* checksum */ + (*ptr) =3D 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 mutex 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 =3D 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 =3D __ddcci_write_bytewise(client, addr, p_flag, data, len); + } else { + sendbuf =3D drv_data->recv_buffer; + ret =3D __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 =3D DDCCI_HOST_ADDR_EVEN; + + /* Consistency checks */ + if (len < 3) + return -EINVAL; + + /* Read frame */ + ret =3D i2c_master_recv(client, buf, len); + if (ret < 0) + goto out_err; + packet_length =3D 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 (=3D incomplete) break out */ + if (ret < 3) { + ret =3D -EIO; + goto out_err; + } + + /* validate first byte */ + if (buf[0] !=3D addr) { + ret =3D (buf[0] =3D=3D '\0') ? -EAGAIN : -EIO; + goto out_err; + } + + /* validate second byte (protocol flag) */ + if ((buf[1] & 0x80) !=3D (p_flag << 7)) { + if (!p_flag || !(quirks & DDCCI_QUIRK_NO_PFLAG)) { + ret =3D -EIO; + goto out_err; + } + } + + /* get and check payload length */ + payload_len =3D buf[1] & 0x7F; + if (3 + payload_len > packet_length) + return -EBADMSG; + if (3 + payload_len > len) + return -EMSGSIZE; + + /* calculate checksum */ + for (i =3D 0; i < 3 + payload_len; i++) + xor ^=3D buf[i]; + + /* verify checksum */ + if (xor !=3D 0) { + dev_err(&client->dev, "invalid DDC/CI response, corrupted data - xor is = 0x%02x, length 0x%02x\n", + xor, payload_len); + ret =3D -EBADMSG; + goto out_err; + } + + /* return result */ + ret =3D 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 mutex 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 =3D i2c_get_clientdata(client); + recvbuf =3D drv_data->recv_buffer; + + /* Read frame */ + ret =3D __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 =3D 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 =3D -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 =3D 0, counter =3D 0, offset =3D 0; + unsigned char cmd[3] =3D { DDCCI_COMMAND_CAPS, 0x00, 0x00 }; + unsigned char *chunkbuf =3D kzalloc(35, GFP_KERNEL); + + if (!chunkbuf) + return -ENOMEM; + + do { + /* Send command */ + result =3D ddcci_write(client, addr, true, cmd, sizeof(cmd)); + if (result < 0) + goto err_free; + msleep(delay); + /* read result chunk */ + result =3D 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] !=3D DDCCI_REPLY_CAPS) { + result =3D -EIO; + goto err_free; + } + if (chunkbuf[1] !=3D cmd[1] || chunkbuf[2] !=3D cmd[2]) { + result =3D -EIO; + goto err_free; + } + if (result < 3) { + result =3D -EIO; + goto err_free; + } + memcpy(buf, chunkbuf + 3, min((unsigned int)result - 3, len)); + + counter++; + /* adjust offset, etc. */ + offset +=3D result-3; + len -=3D result-3; + buf +=3D result-3; + cmd[1] =3D offset >> 8; + cmd[2] =3D 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 opti= onal + * 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 =3D -ENODEV; + unsigned long quirks; + unsigned char cmd[1] =3D { DDCCI_COMMAND_ID }; + unsigned char *buffer; + unsigned char xor =3D DDCCI_HOST_ADDR_EVEN; + struct ddcci_bus_drv_data *bus_drv_data; + + bus_drv_data =3D i2c_get_clientdata(client); + quirks =3D bus_drv_data->quirks; + buffer =3D bus_drv_data->recv_buffer; + + /* Send Identification command */ + if (!(quirks & DDCCI_QUIRK_WRITE_BYTEWISE)) { + ret =3D __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 =3D=3D -ENXIO) + && i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_WRITE_BYTE)) { + quirks |=3D 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 =3D __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 =3D 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 =3D=3D 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 (=3D 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] !=3D 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] =3D=3D buffer[1]) { + quirks |=3D 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) !=3D 0x80 && !(quirks & DDCCI_QUIRK_NO_PFLAG)) { + dev_info(&client->dev, + "DDC/CI bus quirk detected: device omits protocol flag on responses\n"); + quirks |=3D DDCCI_QUIRK_NO_PFLAG; + } + + /* get and check payload length */ + payload_len =3D 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 =3D 0; i < 3 + payload_len; i++) + xor ^=3D buffer[i]; + + /* verify checksum */ + if (xor !=3D 0) { + dev_err(&client->dev, + "[%02x:%02x] invalid DDC/CI response, corrupted data - xor is 0x%02x, l= ength 0x%02x\n", + client->addr << 1, addr, xor, payload_len); + return -EBADMSG; + } + + /* save quirks */ + bus_drv_data->quirks =3D quirks; + + /* return result */ + if (payload_len <=3D len) { + ret =3D payload_len; + memcpy(buf, &buffer[2], payload_len); + } else { + ret =3D -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; + 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 =3D container_of(inode->i_cdev, + struct ddcci_device, cdev); + struct ddcci_fp_data *fp_data =3D NULL; + + fp_data =3D kzalloc(sizeof(struct ddcci_fp_data), GFP_KERNEL); + + if (!fp_data) + return -ENOMEM; + + fp_data->dev =3D dev; + filp->private_data =3D 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 =3D filp->private_data; + struct ddcci_device *dev =3D fp_data->dev; + unsigned char *buf =3D fp_data->buffer; + const bool nonblocking =3D (filp->f_flags & O_NONBLOCK) !=3D 0; + int ret =3D 0; + + if ((filp->f_mode & FMODE_READ) =3D=3D 0) + return -EBADF; + + /* Lock mutex */ + if (nonblocking) { + if (mutex_trylock(&dev->bus_drv_data->mut)) + return -EAGAIN; + } else { + if (mutex_lock_interruptible(&dev->bus_drv_data->mut)) + return -ERESTARTSYS; + } + + /* Execute read */ + ret =3D 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 =3D -EFAULT; + goto out; + } + } + +out: + mutex_unlock(&dev->bus_drv_data->mut); + return ret; +} + +/* Called when writing to the character device */ +static ssize_t ddcci_cdev_write(struct file *filp, const char __user *buff= er, + size_t count, loff_t *offset) +{ + struct ddcci_fp_data *fp_data =3D filp->private_data; + struct ddcci_device *dev =3D fp_data->dev; + unsigned char *buf =3D fp_data->buffer; + const bool nonblocking =3D (filp->f_flags & O_NONBLOCK) !=3D 0; + int ret =3D 0; + + if ((filp->f_mode & FMODE_WRITE) =3D=3D 0) + return -EBADF; + + if (count > 127) + return -EINVAL; + + /* Lock mutex */ + if (nonblocking) { + if (mutex_trylock(&dev->bus_drv_data->mut)) + return -EAGAIN; + } else { + if (mutex_lock_interruptible(&dev->bus_drv_data->mut)) + return -ERESTARTSYS; + } + + if (count > 0) { + /* Copy data from user space */ + if (copy_from_user(buf, buffer, count)) { + ret =3D -EFAULT; + goto err_out; + } + + /* Execute write */ + ret =3D ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr, + true, buf, count); + } + + if (ret >=3D 0) { + msleep(delay); + mutex_unlock(&dev->bus_drv_data->mut); + return count; + } + +err_out: + mutex_unlock(&dev->bus_drv_data->mut); + return ret; +} + +static const struct file_operations ddcci_fops =3D { + .owner =3D THIS_MODULE, + .read =3D ddcci_cdev_read, + .write =3D ddcci_cdev_write, + .open =3D ddcci_cdev_open, + .llseek =3D no_llseek +}; + +/* Set up the character device for a DDC/CI device */ +static int ddcci_setup_char_device(struct ddcci_device *device) +{ + int ret =3D -EINVAL; + + /* Check if free minor exists */ + if (ddcci_cdev_next =3D=3D ddcci_cdev_end) { + dev_err(&device->dev, "no free major/minor\n"); + ret =3D -ENFILE; + goto out; + } + + /* Initialize character device node */ + cdev_init(&device->cdev, &ddcci_fops); + device->cdev.owner =3D THIS_MODULE; + + /* Publish char device */ + device->dev.devt =3D ddcci_cdev_next; + ret =3D cdev_add(&device->cdev, ddcci_cdev_next, 1); + if (ret) { + device->dev.devt =3D 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 =3D ddcci_verify_device(dev); + ssize_t ret =3D -ENOENT; + size_t len; + + if (device !=3D NULL) { + len =3D device->capabilities_len; + if (len > PAGE_SIZE) + len =3D PAGE_SIZE; + if (len =3D=3D 0) { + ret =3D len; + } else { + memcpy(buf, device->capabilities, len); + if (len < PAGE_SIZE) { + buf[len] =3D '\n'; + ret =3D len + 1; + } + } + } + + return ret; +} + +static ssize_t ddcci_attr_prot_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ddcci_device *device =3D ddcci_verify_device(dev); + ssize_t ret =3D -ENOENT; + size_t len; + + if (device !=3D NULL) { + len =3D strnlen(device->prot, sizeof(device->prot)); + strncpy(buf, device->prot, PAGE_SIZE); + if (len =3D=3D 0) { + ret =3D len; + } else if (len < PAGE_SIZE) { + buf[len] =3D '\n'; + ret =3D len + 1; + } else { + ret =3D PAGE_SIZE; + } + } + return ret; +} + +static ssize_t ddcci_attr_type_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ddcci_device *device =3D ddcci_verify_device(dev); + ssize_t ret =3D -ENOENT; + size_t len; + + if (device !=3D NULL) { + len =3D strnlen(device->type, sizeof(device->type)); + strncpy(buf, device->type, PAGE_SIZE); + if (len =3D=3D 0) { + ret =3D len; + } else if (len < PAGE_SIZE) { + buf[len] =3D '\n'; + ret =3D len + 1; + } else { + ret =3D PAGE_SIZE; + } + } + return ret; +} + +static ssize_t ddcci_attr_model_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ddcci_device *device =3D ddcci_verify_device(dev); + ssize_t ret =3D -ENOENT; + size_t len; + + if (device !=3D NULL) { + len =3D strnlen(device->model, sizeof(device->model)); + strncpy(buf, device->model, PAGE_SIZE); + if (len =3D=3D 0) { + ret =3D len; + } else if (len < PAGE_SIZE) { + buf[len] =3D '\n'; + ret =3D len + 1; + } else { + ret =3D PAGE_SIZE; + } + } + return ret; +} + +static ssize_t ddcci_attr_vendor_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ddcci_device *device =3D ddcci_verify_device(dev); + ssize_t ret =3D -ENOENT; + + if (device !=3D NULL) + ret =3D sysfs_emit(buf, "%s\n", device->vendor); + + return ret; +} + +static ssize_t ddcci_attr_module_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ddcci_device *device =3D ddcci_verify_device(dev); + ssize_t ret =3D -ENOENT; + + if (device !=3D NULL) + ret =3D sysfs_emit(buf, "%s\n", device->module); + + return ret; +} + +static ssize_t ddcci_attr_serial_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ddcci_device *device =3D ddcci_verify_device(dev); + ssize_t ret =3D -ENOENT; + + if (device !=3D NULL) + ret =3D 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 =3D ddcci_verify_device(dev); + ssize_t ret =3D -ENOENT; + char model[ARRAY_SIZE(device->model)]; + char vendor[ARRAY_SIZE(device->model)]; + char module[ARRAY_SIZE(device->model)]; + + if (device !=3D 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 =3D 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, 0444, ddcci_attr_capabilities_show, NULL); +static DEVICE_ATTR(idProt, 0444, ddcci_attr_prot_show, NULL); +static DEVICE_ATTR(idType, 0444, ddcci_attr_type_show, NULL); +static DEVICE_ATTR(idModel, 0444, ddcci_attr_model_show, NULL); +static DEVICE_ATTR(idVendor, 0444, ddcci_attr_vendor_show, NULL); +static DEVICE_ATTR(idModule, 0444, ddcci_attr_module_show, NULL); +static DEVICE_ATTR(idSerial, 0444, ddcci_attr_serial_show, NULL); +static DEVICE_ATTR(modalias, 0444, ddcci_attr_modalias_show, NULL); + +static struct attribute *ddcci_char_device_attrs[] =3D { + &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 =3D 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=3D%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=3D%s", device->prot)) + return -ENOMEM; + + if (device->type[0]) + if (add_uevent_var(env, "DDCCI_TYPE=3D%s", device->type)) + return -ENOMEM; + + if (device->model[0]) + if (add_uevent_var(env, "DDCCI_MODEL=3D%s", device->model)) + return -ENOMEM; + + if (device->vendor[0]) { + if (add_uevent_var(env, "DDCCI_VENDOR=3D%s", device->vendor)) + return -ENOMEM; + + if (add_uevent_var(env, "DDCCI_MODULE=3D%s", device->module)) + return -ENOMEM; + + if (add_uevent_var(env, "DDCCI_UNIQ=3D%d", device->device_number)) + return -ENOMEM; + } + + return 0; +} + +static void ddcci_device_release(struct device *dev) +{ + struct ddcci_device *device =3D to_ddcci_device(dev); + struct ddcci_driver *driver; + + /* Notify driver */ + if (dev->driver) { + driver =3D to_ddcci_driver(dev->driver); + if (driver->remove) + driver->remove(device); + } + + /* Teardown chardev */ + if (dev->devt) { + mutex_lock(&core_lock); + if (device->cdev.dev =3D=3D ddcci_cdev_next-1) + ddcci_cdev_next--; + cdev_del(&device->cdev); + mutex_unlock(&core_lock); + } + + /* Free capability string */ + if (device->capabilities) { + device->capabilities_len =3D 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 =3D 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 =3D to_ddcci_device(dev); + if (device->flags & DDCCI_FLAG_EXTERNAL) { + if (device->outer_addr =3D=3D 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 =3D { + .name =3D "ddcci-device", + .uevent =3D ddcci_device_uevent, + .groups =3D ddcci_char_device_groups, + .release =3D ddcci_device_release, + .devnode =3D ddcci_devnode +}; + +/* Device type for dependent DDC/CI devices*/ +static struct device_type ddcci_dependent_type =3D { + .name =3D "ddcci-dependent-device", + .uevent =3D ddcci_device_uevent, + .groups =3D ddcci_char_device_groups, + .release =3D ddcci_device_release, + .devnode =3D 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 (!dev) + return NULL; + return (dev->type =3D=3D &ddcci_device_type + || dev->type =3D=3D &ddcci_dependent_type) + ? to_ddcci_device(dev) + : NULL; +} +EXPORT_SYMBOL_GPL(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 (WARN_ON(!dev)) + return ~0L; + if (WARN_ON(!dev->bus_drv_data)) + return ~0L; + return dev->bus_drv_data->quirks; +} +EXPORT_SYMBOL_GPL(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 *drive= r) +{ + int ret; + + 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 =3D owner; + driver->driver.bus =3D &ddcci_bus_type; + + /* When registration returns, the driver core + * will have called probe() for all matching-but-unbound devices. + */ + ret =3D driver_register(&driver->driver); + if (ret) + return ret; + + pr_debug("driver [%s] registered\n", driver->driver.name); + + return 0; +} +EXPORT_SYMBOL_GPL(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_GPL(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 for 'delay' milliseconds + */ +int ddcci_device_write(struct ddcci_device *dev, bool p_flag, + unsigned char *data, unsigned char length) +{ + int ret; + + if (mutex_lock_interruptible(&dev->bus_drv_data->mut)) + return -EAGAIN; + + ret =3D ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, = data, length); + msleep(delay); + mutex_unlock(&dev->bus_drv_data->mut); + return ret; +} +EXPORT_SYMBOL_GPL(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 (mutex_lock_interruptible(&dev->bus_drv_data->mut)) + return -EAGAIN; + + ret =3D ddcci_read(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, b= uffer, length); + mutex_unlock(&dev->bus_drv_data->mut); + return ret; +} +EXPORT_SYMBOL_GPL(ddcci_device_read); + +/** + * ddcci_device_writeread - Write a message to a device and read the respo= nse + * @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 (mutex_lock_interruptible(&dev->bus_drv_data->mut)) + return -EAGAIN; + + ret =3D ddcci_write(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, = buffer, length); + if (ret < 0) + goto err; + msleep(delay); + ret =3D ddcci_read(dev->bus_drv_data->i2c_dev, dev->inner_addr, p_flag, b= uffer, maxlength); +err: + mutex_unlock(&dev->bus_drv_data->mut); + return ret; +} +EXPORT_SYMBOL_GPL(ddcci_device_writeread); + +#define IS_ANY_ID(x) (((x)[0] =3D=3D '\xFF') && ((x)[7] =3D=3D '\xFF')) + +/* Check if any device id in the array matches the device and return the m= atching id */ +static const struct ddcci_device_id *ddcci_match_id(const struct ddcci_dev= ice_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) =3D=3D 0)) + && (IS_ANY_ID(id->type) || (strcmp(device->type, id->type) =3D=3D 0)) + && (IS_ANY_ID(id->model) || (strcmp(device->model, id->model) =3D=3D 0)) + && (IS_ANY_ID(id->vendor) || (strcmp(device->vendor, id->vendor) =3D=3D= 0)) + && (IS_ANY_ID(id->module) || (strcmp(device->module, id->module) =3D=3D= 0))) { + return id; + } + id++; + } + return NULL; +} + +static int ddcci_device_match(struct device *dev, struct device_driver *dr= v) +{ + struct ddcci_device *device =3D ddcci_verify_device(dev); + struct ddcci_driver *driver; + + if (!device) + return 0; + + driver =3D 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) !=3D NULL; + + return 0; +} + +static int ddcci_device_probe(struct device *dev) +{ + struct ddcci_device *device =3D ddcci_verify_device(dev); + struct ddcci_driver *driver; + const struct ddcci_device_id *id; + int ret =3D 0; + + if (!device) + return -EINVAL; + driver =3D to_ddcci_driver(dev->driver); + + id =3D ddcci_match_id(driver->id_table, device); + if (!id) + return -ENODEV; + + if (driver->probe) + ret =3D driver->probe(device, id); + + return ret; +} + +static void ddcci_device_remove(struct device *dev) +{ + struct ddcci_device *device =3D ddcci_verify_device(dev); + struct ddcci_driver *driver; + int ret =3D 0; + + if (!device) + return; + driver =3D to_ddcci_driver(dev->driver); + + if (driver->remove) + ret =3D driver->remove(device); +} + +/** + * DDCCI bus type structure + */ +struct bus_type ddcci_bus_type =3D { + .name =3D "ddcci", + .match =3D ddcci_device_match, + .probe =3D ddcci_device_probe, + .remove =3D ddcci_device_remove +}; + +/* Main I2C driver */ + +/* Get a pointer to the closing parenthesis */ +static char *ddcci_capstr_tok(const char *s, int depth) +{ + const char *ptr =3D s; + char *end; + + if (s =3D=3D NULL || s[0] =3D=3D '\0') + return NULL; + + while ((end =3D strpbrk(ptr, "()"))) { + if (!end || depth =3D=3D INT_MAX) + return NULL; + if (*end =3D=3D '(') + depth++; + else if (depth > 0) + depth--; + else + break; + ptr =3D 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 `len= gth`. + * + * 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 =3D capabilities, *ptr; + ptrdiff_t len; + int taglen =3D strnlen(tag, 1024); + + /* Check length of requested tag */ + if (taglen <=3D 0 || taglen > 1023) + return ERR_PTR(-EINVAL); + + /* Find tag */ + while (src && (strncmp(src + 1, tag, taglen) !=3D 0 || src[1 + taglen] != =3D '(')) + src =3D ddcci_capstr_tok(src + 1, -1); + if (!src || src[0] =3D=3D '\0') + return ERR_PTR(-ENOENT); + + /* Locate end of value */ + src +=3D taglen + 2; + ptr =3D ddcci_capstr_tok(src, 0); + if (!ptr) + return ERR_PTR(-EOVERFLOW); + + /* Check length of tag data */ + len =3D ptr-src; + if (len < 0 || len > 65535) + return ERR_PTR(-EMSGSIZE); + + /* Return pointer and length */ + if (length !=3D NULL) + *length =3D (size_t)len; + return src; +} +EXPORT_SYMBOL_GPL(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 =3D 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 =3D device->capabilities; + int ret =3D 0; + + if (!capstr) + return -EINVAL; + + /* capability string start with a paren */ + if (capstr[0] !=3D '(') + return -EINVAL; + + /* get prot(...) */ + ret =3D ddcci_cpy_capstr_item(device->prot, capstr, "prot", sizeof(device= ->prot)-1); + if (ret) { + if (ret =3D=3D -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 =3D ddcci_cpy_capstr_item(device->type, capstr, "type", sizeof(device= ->type)-1); + if (ret) { + if (ret =3D=3D -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 =3D ddcci_cpy_capstr_item(device->model, capstr, "model", sizeof(devi= ce->model)-1); + if (ret) { + if (ret =3D=3D -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) =3D=3D 0 + || strncasecmp(device->type, "CRT", sizeof(device->type)-1) =3D=3D 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 ad= dr, + int dependent) +{ + int ret; + unsigned char outer_addr =3D client->addr << 1; + unsigned char *buffer =3D NULL; + struct ddcci_bus_drv_data *drv_data =3D i2c_get_clientdata(client); + struct ddcci_device *device =3D NULL; + + mutex_lock(&drv_data->mut); + + /* Allocate buffer big enough for any capability string */ + buffer =3D kmalloc(16384, GFP_KERNEL); + if (!buffer) { + ret =3D -ENOMEM; + goto err_end; + } + + /* Allocate device struct */ + device =3D kzalloc(sizeof(struct ddcci_device), GFP_KERNEL); + if (!device) { + ret =3D -ENOMEM; + goto err_end; + } + + /* Initialize device */ + device_initialize(&device->dev); + device->dev.parent =3D &client->dev; + device->dev.bus =3D &ddcci_bus_type; + device->outer_addr =3D outer_addr; + device->inner_addr =3D addr; + device->bus_drv_data =3D drv_data; + device->i2c_client =3D client; + + if (!dependent) { + device->dev.type =3D &ddcci_device_type; + ret =3D dev_set_name(&device->dev, "ddcci%d", client->adapter->nr); + } else if (outer_addr =3D=3D dependent) { + /* Internal dependent device */ + device->dev.type =3D &ddcci_dependent_type; + device->flags =3D DDCCI_FLAG_DEPENDENT; + ret =3D dev_set_name(&device->dev, "ddcci%di%02x", client->adapter->nr, = addr); + } else if (outer_addr =3D=3D addr) { + /* External dependent device */ + device->dev.type =3D &ddcci_dependent_type; + device->flags =3D DDCCI_FLAG_DEPENDENT | DDCCI_FLAG_EXTERNAL; + ret =3D 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 =3D &ddcci_dependent_type; + device->flags =3D DDCCI_FLAG_DEPENDENT | DDCCI_FLAG_EXTERNAL; + ret =3D 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 =3D ddcci_identify_device(client, addr, buffer, 29); + if (ret < 0) { + if (!dependent && (ret =3D=3D -EBADMSG || ret =3D=3D -EMSGSIZE)) + dev_warn(&device->dev, "DDC/CI main device sent broken response on iden= tification. Trying to detect solely based on capability information.\n"); + else + goto err_free; + } + + if (ret =3D=3D 29 && buffer[0] =3D=3D DDCCI_REPLY_ID) { + memcpy(device->vendor, &buffer[7], 8); + memcpy(device->module, &buffer[17], 8); + device->device_number =3D be32_to_cpu(*(__force __be32 *)&buffer[18]); + } + + /* Read capabilities */ + ret =3D 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] >=3D 'a' && buffer[0] <=3D 'z') { + dev_err(&device->dev, "DDC/CI device quirk detected: unparenthesized ca= pability string\n"); + device->capabilities =3D kzalloc(ret + 3, GFP_KERNEL); + if (!device->capabilities) { + ret =3D -ENOMEM; + goto err_free; + } + device->capabilities_len =3D ret + 2; + memcpy(&(device->capabilities[1]), buffer, ret); + device->capabilities[0] =3D '('; + device->capabilities[ret + 1] =3D ')'; + } else { + /* Standard case: simply copy the received string */ + device->capabilities =3D kzalloc(ret + 1, GFP_KERNEL); + if (!device->capabilities) { + ret =3D -ENOMEM; + goto err_free; + } + device->capabilities_len =3D ret; + memcpy(device->capabilities, buffer, ret); + } + + ret =3D ddcci_parse_capstring(device); + if (ret) { + dev_err(&device->dev, "malformed capability string: \"%s\" errno %d\n",= device->capabilities, ret); + ret =3D -EINVAL; + goto err_free; + } + } + + /* Found a device if either identification or capabilities succeeded */ + if (!device->capabilities && device->vendor[0] =3D=3D '\0') { + dev_dbg(&client->dev, + "[%02x:%02x] got neither valid identification nor capability data\n", + client->addr << 1, addr); + ret =3D -ENODEV; + goto err_free; + } + + /* Setup chardev */ + mutex_lock(&core_lock); + ret =3D ddcci_setup_char_device(device); + mutex_unlock(&core_lock); + if (ret) + goto err_free; + + /* Release mutex and add device to the tree */ + mutex_unlock(&drv_data->mut); + pr_debug("found device at %d:%02x:%02x\n", client->adapter->nr, outer_add= r, addr); + ret =3D device_add(&device->dev); + if (ret) + goto err_free; + + goto end; +err_free: + put_device(&device->dev); +err_end: + mutex_unlock(&drv_data->mut); +end: + kfree(buffer); + return ret; +} + +/* I2C detect function: check if a main or external dependent device exist= s */ +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] =3D { DDCCI_COMMAND_ID }; + unsigned char cmd_caps[3] =3D { 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", cli= ent->adapter->nr); + return -ENODEV; + } + + /* send Capabilities Request (for main) or Identification Request command= (for dependent devices) */ + outer_addr =3D client->addr << 1; + inner_addr =3D (outer_addr =3D=3D DDCCI_DEFAULT_DEVICE_ADDR) ? DDCCI_HOST= _ADDR_ODD : outer_addr | 1; + cmd =3D (outer_addr =3D=3D DDCCI_DEFAULT_DEVICE_ADDR) ? cmd_caps : cmd_id; + cmd_len =3D (outer_addr =3D=3D DDCCI_DEFAULT_DEVICE_ADDR) ? sizeof(cmd_ca= ps) : sizeof(cmd_id); + pr_debug("detecting %d:%02x\n", client->adapter->nr, outer_addr); + + ret =3D __ddcci_write_block(client, inner_addr, buf, true, cmd, cmd_len); + + if (ret =3D=3D -ENXIO || ret =3D=3D -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 no= t supported\n"); + return -ENODEV; + } + pr_debug("i2c write failed with ENXIO or EIO, trying bytewise writing\n"= ); + ret =3D __ddcci_write_bytewise(client, inner_addr, true, cmd, cmd_len); + if (ret =3D=3D 0) { + msleep(delay); + ret =3D __ddcci_write_bytewise(client, inner_addr, true, cmd, cmd_len); + } + } + + if (ret < 0) + return -ENODEV; + + /* wait for device */ + msleep(delay); + /* receive answer */ + ret =3D 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] !=3D outer_addr) { + pr_debug("detection failed: invalid %s response (%02x !=3D %02x)\n", (cm= d =3D=3D 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 =3D=3D DDCCI_DEFAULT_DEVICE_ADDR) ? "ddcc= i" : "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 =3D -ENODEV, tmp; + unsigned char main_addr, addr; + struct ddcci_bus_drv_data *drv_data; + + /* Initialize driver data structure */ + drv_data =3D devm_kzalloc(&client->dev, sizeof(struct ddcci_bus_drv_data)= , GFP_KERNEL); + if (!drv_data) + return -ENOMEM; + drv_data->i2c_dev =3D client; + mutex_init(&drv_data->mut); + + /* Set i2c client data */ + i2c_set_clientdata(client, drv_data); + + if (id->driver_data =3D=3D 0) { + /* Core device, probe at 0x6E */ + main_addr =3D DDCCI_DEFAULT_DEVICE_ADDR; + dev_dbg(&client->dev, "probing core device [%02x]\n", + client->addr << 1); + ret =3D 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 =3D=3D -EIO) + ret =3D -ENODEV; + goto err_free; + } + + /* Detect internal dependent devices */ + dev_dbg(&client->dev, "probing internal dependent devices\n"); + for (i =3D 0; i < autoprobe_addr_count; ++i) { + addr =3D (unsigned short)autoprobe_addrs[i]; + if ((addr & 1) =3D=3D 0 && addr !=3D main_addr) { + tmp =3D ddcci_detect_device(client, addr, main_addr); + if (tmp < 0 && tmp !=3D -ENODEV) { + dev_info(&client->dev, "internal dependent device [%02x:%02x] probe f= ailed: %d\n", + client->addr << 1, addr, ret); + } + } + } + } else if (id->driver_data =3D=3D 1) { + /* External dependent device */ + main_addr =3D client->addr << 1; + dev_dbg(&client->dev, "probing external dependent device [%02x]\n", main= _addr); + ret =3D 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 =3D=3D -EIO) + ret =3D -ENODEV; + goto err_free; + } + } else { + dev_warn(&client->dev, + "probe() called with invalid i2c device id\n"); + ret =3D -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. + */ +static int ddcci_remove_helper(struct device *dev, const void *p) +{ + struct ddcci_device *device; + + device =3D ddcci_verify_device(dev); + if (!device || device->flags & DDCCI_FLAG_REMOVED) + return 0; + + if (!p || (dev->parent =3D=3D p)) { + device->flags |=3D DDCCI_FLAG_REMOVED; + /* Memory Barrier */ + 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 =3D i2c_get_clientdata(client); + struct device *dev; + + mutex_lock(&drv_data->mut); + while (1) { + dev =3D bus_find_device(&ddcci_bus_type, NULL, client, + ddcci_remove_helper); + if (!dev) + break; + device_unregister(dev); + put_device(dev); + } + mutex_unlock(&drv_data->mut); + return 0; +} + +/* + * I2C driver device identification table. + */ +static const struct i2c_device_id ddcci_idtable[] =3D { + { "ddcci", 0 }, + { "ddcci-dependent", 1 }, + {} +}; +MODULE_DEVICE_TABLE(i2c, ddcci_idtable); + +/* + * I2C driver description structure + */ +static struct i2c_driver ddcci_driver =3D { + .driver =3D { + .name =3D "ddcci", + .owner =3D THIS_MODULE, + }, + + .id_table =3D ddcci_idtable, + .probe =3D ddcci_probe, + .remove =3D ddcci_remove, + .class =3D I2C_CLASS_DDC, + .detect =3D ddcci_detect, + .address_list =3D 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 =3D 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 =3D ddcci_cdev_first; + ddcci_cdev_end =3D MKDEV(MAJOR(ddcci_cdev_first), MINOR(ddcci_cdev_first)= + 128); + + /* Register bus */ + ret =3D bus_register(&ddcci_bus_type); + if (ret) { + pr_err("failed to register bus 'ddcci'\n"); + goto err_busreg; + } + + /* Register I2C driver */ + ret =3D 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 =3D 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 description */ +MODULE_AUTHOR("Christoph Grenz "); +MODULE_DESCRIPTION("DDC/CI bus driver"); +MODULE_LICENSE("GPL"); diff --git a/include/linux/ddcci.h b/include/linux/ddcci.h new file mode 100644 index 000000000000..690a9ff85d43 --- /dev/null +++ b/include/linux/ddcci.h @@ -0,0 +1,163 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * 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 F= ree + * Software Foundation; either version 2 of the License, or (at your optio= n) + * any later version. + */ + +#ifndef _DDCCI_H +#define _DDCCI_H + +#include +#include +#include + +#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 + * @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; + 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 *drive= r); +#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 *b= uffer, + 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 *t= ag, + size_t *length); + +#endif --=20 2.25.1 From nobody Fri Jun 19 13:27:17 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 7990EC433FE for ; Sun, 3 Apr 2022 23:09:45 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1376633AbiDCXL3 (ORCPT ); Sun, 3 Apr 2022 19:11:29 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:34158 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1376617AbiDCXLW (ORCPT ); Sun, 3 Apr 2022 19:11:22 -0400 Received: from mail-pj1-x102e.google.com (mail-pj1-x102e.google.com [IPv6:2607:f8b0:4864:20::102e]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 8B50424F0A for ; Sun, 3 Apr 2022 16:09:27 -0700 (PDT) Received: by mail-pj1-x102e.google.com with SMTP id ch16-20020a17090af41000b001ca867ef52bso1519567pjb.0 for ; Sun, 03 Apr 2022 16:09:27 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=AazmYMEVI4+t31GypNH0a3+SYY9YIPzgFZxmTSXBvXI=; b=Px0NTwECYIoBDNYTVGoEzcgAC+SdXnZp3o/MviptdOXe9dOHQ6fNYQEvzMErHVNCfv Vys45j98dxnlAPTY9dONtEfe7XaDkW5owc8MYJDxAHIIMsVUg5xOTK15o1V01hz3hIc1 PJImyEe+MNYF1qRLzWpPEeOkvqqlTKvWo5hJN5UOgVDfKvqLm+mbHfUj0bdFQ/hgS7ks c6TxMyCR9pZz72+jcHjlY8viS/oGETUInzAcABr1NuN9a66VRSP1YEx5q7XmCBF65AHz GOPhFrVWVicjCDeRcVXmn0T7itglTVIY7LjfqNqBv25wV+mHOOls0SreUXrlcSUFUU0n Edjg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=AazmYMEVI4+t31GypNH0a3+SYY9YIPzgFZxmTSXBvXI=; b=H5waElaZAVxszMW8zKU3dnBk/LlrUyQGY/0VEJ0Y4mW3LttfLt4gwdwk1qjuheajbs e5GxDVOxCjEQitfIFSWDi8LBt33PXVRyZaJCa1wViw577TJ8g48ad1KV6emVX0w3igE6 RYnsFHHEc/NOtqREbOlyFIQ3irTHC2BoqsI/LHi0JcN2TwtQq7eUewAt66/UGX+bzaQa lZ3fwxCg0VBtdRzUOYE5mezdRO1HlIxIzb2a9zrmdrCrwI4bk+0FJCdPCM35DJprE6KH wsXg/mtOjmFeT0pNPSgi+BHeMIIXddCVG4m42GgWcw2JPh0elBry39e72t+/ZEXkeEbN sFwQ== X-Gm-Message-State: AOAM532Ws0H7Nx5rOz9WSAvGR2OGuvbEGV4t7TOVbreLdSV83xSt2PnV +qboMwZ2WAnGzSQnWqD+ZpudeaOsusq8LQ== X-Google-Smtp-Source: ABdhPJwu10LyWXjJNNLHbDslwYfk/gSPp782weeIVdS7z0wMaRI32tXvOrkUPNVqTNLzSiQqYKgj4w== X-Received: by 2002:a17:90b:4d0e:b0:1c6:3ea9:7b5f with SMTP id mw14-20020a17090b4d0e00b001c63ea97b5fmr23278861pjb.166.1649027366608; Sun, 03 Apr 2022 16:09:26 -0700 (PDT) Received: from yusufkhan-MS-7B17.lan ([24.17.200.29]) by smtp.gmail.com with ESMTPSA id p16-20020a056a000b5000b004faed463907sm10164503pfo.0.2022.04.03.16.09.25 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 03 Apr 2022 16:09:26 -0700 (PDT) From: Yusuf Khan To: linux-kernel@vger.kernel.org Cc: jasowang@redhat.com, mikelley@microsoft.com, mst@redhat.com, gregkh@linuxfoundation.org, javier@javigon.com, arnd@arndb.de, will@kernel.org, axboe@kernel.dk, Yusuf Khan , Christoph Grenz Subject: [PATCH v10 2/3] drivers: ddcci: add drivers for DDCCI Date: Sun, 3 Apr 2022 16:08:50 -0700 Message-Id: <20220403230850.2986-3-yusisamerican@gmail.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20220403230850.2986-1-yusisamerican@gmail.com> References: <20220403230850.2986-1-yusisamerican@gmail.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Content-Type: text/plain; charset="utf-8" This patch adds the backlight driver that utilizes the DDCCI driver from patch one to add a backlight driver. Signed-off-by: Yusuf Khan Signed-off-by: Christoph Grenz Reported-by: kernel test robot --- drivers/video/backlight/Kconfig | 11 + drivers/video/backlight/Makefile | 1 + drivers/video/backlight/ddcci-backlight.c | 411 ++++++++++++++++++++++ 3 files changed, 423 insertions(+) create mode 100644 drivers/video/backlight/ddcci-backlight.c diff --git a/drivers/video/backlight/Kconfig b/drivers/video/backlight/Kcon= fig index e32694c13da5..7a26088c3c3f 100644 --- a/drivers/video/backlight/Kconfig +++ b/drivers/video/backlight/Kconfig @@ -289,6 +289,17 @@ config BACKLIGHT_QCOM_WLED If you have the Qualcomm PMIC, say Y to enable a driver for the WLED block. Currently it supports PM8941 and PMI8998. =20 +config BACKLIGHT_DDCCI + tristate "DDCCI Backlight Driver" + depends on DDCCI + help + If you have a DDC/CI supporing monitor, say Y to enable a driver + to control its backlight using DDC/CI. This could be useful if + your monitor does not include a backlight driver. For this to be + useful you need to enable DDCCI support which can be found in + Device Drivers -> Character devices and that further depends on + I2C. + config BACKLIGHT_RT4831 tristate "Richtek RT4831 Backlight Driver" depends on MFD_RT4831 diff --git a/drivers/video/backlight/Makefile b/drivers/video/backlight/Mak= efile index cae2c83422ae..7bfb6e506b35 100644 --- a/drivers/video/backlight/Makefile +++ b/drivers/video/backlight/Makefile @@ -58,3 +58,4 @@ obj-$(CONFIG_BACKLIGHT_WM831X) +=3D wm831x_bl.o obj-$(CONFIG_BACKLIGHT_ARCXCNN) +=3D arcxcnn_bl.o obj-$(CONFIG_BACKLIGHT_RAVE_SP) +=3D rave-sp-backlight.o obj-$(CONFIG_BACKLIGHT_LED) +=3D led_bl.o +obj-$(CONFIG_BACKLIGHT_DDCCI) +=3D ddcci-backlight.o diff --git a/drivers/video/backlight/ddcci-backlight.c b/drivers/video/back= light/ddcci-backlight.c new file mode 100644 index 000000000000..d37eb142311d --- /dev/null +++ b/drivers/video/backlight/ddcci-backlight.c @@ -0,0 +1,411 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * 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 F= ree + * Software Foundation; either version 2 of the License, or (at your optio= n) + * any later version. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt +#include +#include +#include +#include + +#include + + +#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 =3D 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] =3D DDCCI_COMMAND_WRITE; + buf[1] =3D ctrl; + buf[2] =3D (value >> 8); + buf[3] =3D (value & 255); + + ret =3D 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] =3D DDCCI_COMMAND_READ; + buf[1] =3D ctrl; + + ret =3D ddcci_device_writeread(device, true, buf, 2, sizeof(buf)); + if (ret < 0) + return ret; + + if (ret =3D=3D 0) + return -ENOTSUPP; + + if (ret =3D=3D 8 && buf[0] =3D=3D DDCCI_REPLY_READ && buf[2] =3D=3D ctrl)= { + if (value) + *value =3D buf[6] * 256 + buf[7]; + + if (maximum) + *maximum =3D buf[4] * 256 + buf[5]; + + if (buf[1] =3D=3D 1) + return -ENOTSUPP; + if (buf[1] !=3D 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 =3D bl_get_data(bl); + + return drv_data->fb_dev =3D=3D NULL || drv_data->fb_dev =3D=3D info->dev; +} + +static int ddcci_backlight_update_status(struct backlight_device *bl) +{ + struct ddcci_monitor_drv_data *drv_data =3D bl_get_data(bl); + int brightness =3D bl->props.brightness; + int ret; + + if (bl->props.power !=3D FB_BLANK_UNBLANK || + bl->props.state & BL_CORE_FBBLANK) + brightness =3D 0; + + ret =3D ddcci_monitor_writectrl(drv_data->device, drv_data->used_vcp, + brightness); + if (ret > 0) + ret =3D 0; + return ret; +} + +static int ddcci_backlight_get_brightness(struct backlight_device *bl) +{ + unsigned short value =3D 0, maxval =3D 0; + int ret; + struct ddcci_monitor_drv_data *drv_data =3D bl_get_data(bl); + + ret =3D ddcci_monitor_readctrl(drv_data->device, drv_data->used_vcp, + &value, &maxval); + if (ret < 0) + return ret; + + bl->props.brightness =3D value; + bl->props.max_brightness =3D maxval; + ret =3D value; + + return ret; +} + +static const struct backlight_ops ddcci_backlight_ops =3D { + .options =3D 0, + .update_status =3D ddcci_backlight_update_status, + .get_brightness =3D ddcci_backlight_get_brightness, + .check_fb =3D 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 =3D 0; + + /* Sanity check */ + if (ptr =3D=3D NULL || ptr[0] =3D=3D '\0') + return NULL; + + /* Find next white space outside of parentheses */ + while ((ptr =3D strpbrk(ptr, " ()"))) { + if (!ptr || depth =3D=3D INT_MAX) + return NULL; + else if (*ptr =3D=3D '(') + depth++; + else if (depth > 0) { + if (*ptr =3D=3D ')') + depth--; + } else + break; + ++ptr; + } + + /* Skip over whitespace */ + ptr =3D skip_spaces(ptr); + + /* Check if we're now at the end of the list */ + if (*ptr =3D=3D '\0' || *ptr =3D=3D ')') + return NULL; + + return ptr; +} + +static bool ddcci_monitor_find_vcp(unsigned char vcp, const char *s) +{ + const char *ptr =3D s; + char vcp_hex[3]; + + /* Sanity check */ + if (s =3D=3D NULL || s[0] =3D=3D '\0') + return false; + + /* Create hex representation of VCP */ + if (snprintf(vcp_hex, 3, "%02hhX", vcp) !=3D 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) =3D=3D 0) { + if (ptr[2] =3D=3D ' ' || ptr[2] =3D=3D '(' || ptr[2] =3D=3D ')') + return true; + } + } while ((ptr =3D 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 =3D &ddcci_dev->dev; + struct kernfs_node *dirent; + + for (i =3D 0; i < 3; ++i) { + dev =3D dev->parent; + if (!dev) { + dev_dbg(&ddcci_dev->dev, "failed to create convenience symlink: ancesto= r device not found\n"); + return -ENOENT; + } + } + dirent =3D 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 =3D sysfs_create_link(&dev->kobj, &ddcci_dev->dev.kobj, "ddcci_bac= klight"); + if (result =3D=3D 0) + dev_dbg(&ddcci_dev->dev, "created symlink %s/ddcci_backlight\n", dev_nam= e(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 =3D &ddcci_dev->dev; + struct kernfs_node *dirent; + + for (i =3D 0; i < 3; ++i) { + dev =3D dev->parent; + if (!dev) + return -ENOENT; + } + dirent =3D sysfs_get_dirent(dev->kobj.sd, "ddcci_backlight"); + if (!dirent) + return -ENOENT; + + if ((dirent->flags & KERNFS_LINK) =3D=3D 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 !=3D ddcci_dev->dev.kobj.sd) { + sysfs_put(dirent); + dev_dbg(&ddcci_dev->dev, "won't remove %s/ddcci_backlight: we are not th= e 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 =3D NULL; + int ret =3D 0; + bool support_luminance, support_bl_white; + unsigned short brightness =3D 0, max_brightness =3D 0; + const char *vcps; + + dev_dbg(&dev->dev, "probing monitor backlight device\n"); + + /* Get VCP list */ + vcps =3D 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 =3D support_luminance =3D true; + } else { + /* Check VCP list for supported VCPs */ + support_bl_white =3D ddcci_monitor_find_vcp(DDCCI_MONITOR_BL_WHITE, vcps= ); + support_luminance =3D ddcci_monitor_find_vcp(DDCCI_MONITOR_LUMINANCE, vc= ps); + /* 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 =3D support_luminance =3D true; + } + } + + /* Initialize driver data structure */ + drv_data =3D devm_kzalloc(&dev->dev, sizeof(struct ddcci_monitor_drv_data= ), + GFP_KERNEL); + if (!drv_data) + return -ENOMEM; + drv_data->device =3D dev; + + if (support_bl_white) { + /* Try getting backlight level */ + dev_dbg(&dev->dev, + "trying to access \"backlight level white\" control\n"); + ret =3D ddcci_monitor_readctrl(drv_data->device, DDCCI_MONITOR_BL_WHITE, + &brightness, &max_brightness); + if (ret < 0) { + if (ret =3D=3D -ENOTSUPP) + dev_info(&dev->dev, + "monitor does not support reading backlight level\n"); + else + goto err_free; + } else { + drv_data->used_vcp =3D 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 =3D ddcci_monitor_readctrl(drv_data->device, DDCCI_MONITOR_LUMINANCE, + &brightness, &max_brightness); + if (ret < 0) { + if (ret =3D=3D -ENOTSUPP) + dev_info(&dev->dev, + "monitor does not support reading luminance\n"); + else + goto err_free; + } else { + drv_data->used_vcp =3D DDCCI_MONITOR_LUMINANCE; + } + drv_data->used_vcp =3D DDCCI_MONITOR_LUMINANCE; + } + + if (!drv_data->used_vcp) + goto err_free; + + /* Create brightness device */ + memset(&props, 0, sizeof(props)); + props.type =3D BACKLIGHT_RAW; + props.max_brightness =3D max_brightness; + props.brightness =3D brightness; + bl =3D devm_backlight_device_register(&dev->dev, dev_name(&dev->dev), + &dev->dev, drv_data, + &ddcci_backlight_ops, &props); + drv_data->bl_dev =3D 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[] =3D { + { "monitor", DDCCI_ANY_ID, DDCCI_ANY_ID, DDCCI_ANY_ID, DDCCI_ANY_ID, 0 }, + {} +}; + +static struct ddcci_driver ddcci_backlight_driver =3D { + .driver =3D { + .name =3D "ddcci-backlight", + .owner =3D THIS_MODULE, + }, + + .id_table =3D ddcci_monitor_idtable, + .probe =3D ddcci_monitor_probe, + .remove =3D ddcci_monitor_remove, +}; + +module_ddcci_driver(ddcci_backlight_driver); + +/* Module parameter description */ +module_param(convenience_symlink, bool, 0644); +MODULE_PARM_DESC(convenience_symlink, "add convenience symlink \"ddcci_bac= klight\" 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-*-*-*-*"); --=20 2.25.1 From nobody Fri Jun 19 13:27:17 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 88BBFC433EF for ; Sun, 3 Apr 2022 23:09:45 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1376637AbiDCXLe (ORCPT ); Sun, 3 Apr 2022 19:11:34 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:34336 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1376623AbiDCXLY (ORCPT ); Sun, 3 Apr 2022 19:11:24 -0400 Received: from mail-pj1-x1032.google.com (mail-pj1-x1032.google.com [IPv6:2607:f8b0:4864:20::1032]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id EF4E924F07 for ; Sun, 3 Apr 2022 16:09:29 -0700 (PDT) Received: by mail-pj1-x1032.google.com with SMTP id x14so3894677pjf.2 for ; Sun, 03 Apr 2022 16:09:29 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=MnWAMe9I06uRHHkAV77SrUCEzn8s1KKpVwsFGsGBqTc=; b=OENDJJLMrKrgibNyXZIHPx034c5IwHJkxSipgUCS4XAyLvLx/fovUcBWIE6F3zmtIf vDZjQnBPDv5CO5lTKwJaP/7wVTd062woHDn+Bf8pMO09NWRnvAwoqdEOcEcEs4evomTV kD36uvehsggyi6+7qV4HMy9Bq0CG9/GLwE3POkdZfRtz5A+fS5SNAuRVskV5/i/HjkCd yPMLqNCqFxPTFGG6G+6HPU+q4EBFdKmtNTaqj4FL487B3xXEI/Bxi+MLjbrPtbP6EBej mRT9OMAEa0Eb1lmJ90A65LobaBDRLs96hC1JZb0Cof7Y6aCdrTmtD88YMwK3AjoM9wBR wgrg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=MnWAMe9I06uRHHkAV77SrUCEzn8s1KKpVwsFGsGBqTc=; b=UJVbGSek/rgGBnNmpvYeL3at1bzGVGOYfbMS8o0gOHrYP53DXRarl6PU/RfsDFNvQE LpYipj22PBzb1v9U7A8wT5yBOHTzkzVf7u7H7fHPJLBh7yNtS7rN9QGCX9IqXaNFi2RD opQN0Vbg9xw3LUGQoqwj0saOMAa6nALN4UaFR9J7RXSV/pMfsrd0iPxChegJkonQFcYG S0YBlTfXW51+xfHOeJEWVIISNK7y2fvlPbv/nA48S7osZyx2CAgoBowssqUe99aL53DJ KVct9PM0OU4W8HXsBUXyscQ9pGeHduoJj8Con4LFBGR6ywzxNDOsq1rmRrXSvuoBoF1h VbEw== X-Gm-Message-State: AOAM533MU8irzM1p9CkjNsoIvaZkr+wX2pbCtTUviVjp4oHHV70Qlpoz e6HzKD2vCdY5z2o9PxYliJUKjOZIiq9Ylw== X-Google-Smtp-Source: ABdhPJyFp6sii1V9wLJ0I1w9QggEHc1o8nxpSQpDlwHpac0uZwpjQaSzFqIKY2Q2ACWyPFSZNgBIAQ== X-Received: by 2002:a17:903:110c:b0:14d:8859:5c8 with SMTP id n12-20020a170903110c00b0014d885905c8mr56137469plh.156.1649027369110; Sun, 03 Apr 2022 16:09:29 -0700 (PDT) Received: from yusufkhan-MS-7B17.lan ([24.17.200.29]) by smtp.gmail.com with ESMTPSA id p16-20020a056a000b5000b004faed463907sm10164503pfo.0.2022.04.03.16.09.28 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 03 Apr 2022 16:09:28 -0700 (PDT) From: Yusuf Khan To: linux-kernel@vger.kernel.org Cc: jasowang@redhat.com, mikelley@microsoft.com, mst@redhat.com, gregkh@linuxfoundation.org, javier@javigon.com, arnd@arndb.de, will@kernel.org, axboe@kernel.dk, Yusuf Khan , Christoph Grenz Subject: [PATCH v10 3/3] drivers: ddcci: add drivers for DDCCI Date: Sun, 3 Apr 2022 16:08:51 -0700 Message-Id: <20220403230850.2986-4-yusisamerican@gmail.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20220403230850.2986-1-yusisamerican@gmail.com> References: <20220403230850.2986-1-yusisamerican@gmail.com> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org This patch adds Documentation for the DDCCI drivers. Signed-off-by: Yusuf Khan Signed-off-by: Christoph Grenz Reported-by: kernel test robot --- Documentation/ABI/testing/sysfs-driver-ddcci | 57 +++++++++ Documentation/driver-api/ddcci.rst | 122 +++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 Documentation/ABI/testing/sysfs-driver-ddcci create mode 100644 Documentation/driver-api/ddcci.rst diff --git a/Documentation/ABI/testing/sysfs-driver-ddcci b/Documentation/A= BI/testing/sysfs-driver-ddcci new file mode 100644 index 000000000000..19f77ccf3ed0 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-driver-ddcci @@ -0,0 +1,57 @@ +What: /sys/bus/ddcci/ddccii +Date: March 2022 +KernelVersion: 5.18 +Contact: Christoph Grenz +Description: This file is a user interface for an internal + dependent device on the I2C bus, it exports the same + information as the master device(/sys/bus/ddcci/ + ddcci) that is referenced in this + document. + +What: /sys/bus/ddcci/ddccie +Date: March 2022 +KernelVersion: 5.18 +Contact: Christoph Grenz +Description: This file is a user interface for an external + dependent device on the I2C bus, it exports the same + information as the master device(/sys/bus/ddcci/ + ddcci) that is referenced in this + document. + +What: /sys/bus/ddcci/ddcci +Date: March 2022 +KernelVersion: 5.18 +Contact: Christoph Grenz +Description: This file provides the user interface for the + master device on the I2C bus. It exports the following + peices of information: + - idProt + ACCESS.bus protocol supported by the device. Usually + "monitor". + + - idType + ACCESS.bus device subtype. Usually "LCD" or "CRT". + + - idModel + ACCESS.bus device model identifier. Usually a + shortened form of the device model name. + + - idVendor + ACCESS.bus device vendor identifier. Empty if the + Identification command is not supported. + + - idModule + ACCESS.bus device module identifier. Empty if the + Identification command is not supported. + + - idSerial + 32 bit device number. A fixed serial number if it's + positive, a temporary serial number if negative and zero + if the Identification command is not supported. + + - modalias + A combined identifier for driver selection. It has the form: + ddcci:----. + All non-alphanumeric characters (including whitespace) + in the model, vendor or module parts are replaced by + underscores to prevent issues with software like systemd-udevd. diff --git a/Documentation/driver-api/ddcci.rst b/Documentation/driver-api/= ddcci.rst new file mode 100644 index 000000000000..2b7de1ac2656 --- /dev/null +++ b/Documentation/driver-api/ddcci.rst @@ -0,0 +1,122 @@ +.. SPDX-License-Identifier: GPL-2.0-or-later + +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D +DDC/CI +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +1. Introduction +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D +DDC/CI is a control protocol for monitor settings supported by most +monitors since about 2005. It is based on ACCESS.bus (an early USB predece= ssor). +This could be used to create drivers that communicate with the DDCCI compo= nent, +see ddcci-backlight for an example. + +2. sysfs interface +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D +Each detected DDC/CI device gets a directory in /sys/bus/ddcci/devices. +The main device on a bus is named ddcci[I=C2=B2C bus number]. +Internal dependent devices are named ddcci[I=C2=B2C bus number]i[hex addre= ss] +External dependent devices are named ddcci[I=C2=B2C bus number]e[hex addre= ss] +There the following files export information about the device: + +capabilities +The full ACCESS.bus capabilities string. It contains the protocol, +type and model of the device, a list of all supported command +codes, etc. See the ACCESS.bus spec for more information. + +- idProt +ACCESS.bus protocol supported by the device. Usually "monitor". + +- idType +ACCESS.bus device subtype. Usually "LCD" or "CRT". + +- idModel +ACCESS.bus device model identifier. Usually a shortened form of the +device model name. + +- idVendor +ACCESS.bus device vendor identifier. Empty if the Identification command +is not supported. + +- idModule +ACCESS.bus device module identifier. Empty if the Identification command +is not supported. + +- idSerial +32 bit device number. A fixed serial number if it's positive, a temporary +serial number if negative and zero if the +Identification command is not supported. + +- modalias +A combined identifier for driver selection. It has the form: +ddcci:----. +All non-alphanumeric characters (including whitespace) in the model, +vendor or module parts are replaced by underscores to prevent issues +with software like systemd-udevd. + +3. Character device interface +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D +For each DDC/CI device a character device in +/dev/bus/ddcci/[I=C2=B2C bus number]/ is created, +127 devices are assigned in total. + +The main device on the bus is named display. + +Internal dependent devices are named i[hex address] + +External dependent devices are named e[hex address] + +These character devices can be used to issue commands to a DDC/CI device +more easily than over i2c-dev devices. They should be opened unbuffered. +To send a command just write the command byte and the arguments with a +single write() operation. The length byte and checksum are automatically +calculated. + +To read a response use read() with a buffer big enough for the expected an= swer. + +NOTE: The maximum length of a DDC/CI message is 32 bytes. + +4. ddcci-backlight (monitor backlight driver) +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D +[This is not specific to the DDC/CI backlight driver, if you already dealt= with +backlight drivers, skip over this.] + +For each monitor that supports accessing the Backlight Level White +or the Luminance property, a backlight device of type "raw" named like the +corresponding ddcci device is created. You can find them in /sys/class/bac= klight/. +For convenience a symlink "ddcci_backlight" on the device associated with = the +display connector in /sys/class/drm/ to the backlight device is created, as +long as the graphics driver allows to make this association. + +5. Limitations +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +-Dependent devices (sub devices using DDC/CI directly wired to the monitor, +like Calibration devices, IR remotes, etc.) aren't automatically detected. +You can force detection of external dependent devices by writing +"ddcci-dependent [address]" into /sys/bus/i2c/i2c-?/new_device. + +There is no direct synchronization if you manually change the luminance +with the buttons on your monitor, as this can only be realized through pol= ling +and some monitors close their OSD every time a DDC/CI command is received. + +Monitor hotplugging is not detected. You need to detach/reattach the I=C2= =B2C driver +or reload the module. + +6. Debugging +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D +Both drivers use the dynamic debugging feature of the Linux kernel. +To get detailed debugging messages, set the dyndbg module parameter. +If you want to enable debugging permanently across reboots, create a file +/etc/modprobe.d/ddcci.conf containing lines like the following before load= ing the modules: + +options ddcci dyndbg +options ddcci-backlight dyndbg + +7. Origin +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D +This driver originally came from Christoph Grenz in DKMS form here: +https://gitlab.com/ddcci-driver-linux/ddcci-driver-linux +with multiple backups available on the wayback machine. It also +inlcudes a example program for the usage of this driver in +userland. --=20 2.25.1