Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
# SPDX-License-Identifier: MIT
config DRM_DISPLAY_DP_AUX_BUS
tristate
depends on DRM
depends on OF
config DRM_DISPLAY_HELPER
tristate
depends on DRM
select CEC_CORE if DRM_DISPLAY_DP_AUX_CEC || DRM_DISPLAY_HDMI_CEC_HELPER || CEC_NOTIFIER
help
DRM helpers for display adapters.
if DRM_DISPLAY_HELPER
config DRM_BRIDGE_CONNECTOR
bool
select DRM_DISPLAY_HDMI_AUDIO_HELPER
select DRM_DISPLAY_HDMI_CEC_HELPER
select DRM_DISPLAY_HDMI_STATE_HELPER
help
DRM connector implementation terminating DRM bridge chains.
config DRM_DISPLAY_DP_AUX_CEC
bool "Enable DisplayPort CEC-Tunneling-over-AUX HDMI support"
select DRM_DISPLAY_DP_HELPER
help
Choose this option if you want to enable HDMI CEC support for
DisplayPort/USB-C to HDMI adapters.
Note: not all adapters support this feature, and even for those
that do support this they often do not hook up the CEC pin.
config DRM_DISPLAY_DP_AUX_CHARDEV
bool "DRM DP AUX Interface"
select DRM_DISPLAY_DP_HELPER
help
Choose this option to enable a /dev/drm_dp_auxN node that allows to
read and write values to arbitrary DPCD registers on the DP aux
channel.
config DRM_DISPLAY_DP_HELPER
bool
help
DRM display helpers for DisplayPort.
config DRM_DISPLAY_DP_TUNNEL
bool
select DRM_DISPLAY_DP_HELPER
help
Enable support for DisplayPort tunnels. This allows drivers to use
DP tunnel features like the Bandwidth Allocation mode to maximize the
BW utilization for display streams on Thunderbolt links.
config DRM_DISPLAY_DP_TUNNEL_STATE_DEBUG
bool "Enable debugging the DP tunnel state"
depends on REF_TRACKER
depends on DRM_DISPLAY_DP_TUNNEL
depends on DEBUG_KERNEL
depends on EXPERT
help
Enables debugging the DP tunnel manager's state, including the
consistency of all managed tunnels' reference counting and the state of
streams contained in tunnels.
If in doubt, say "N".
config DRM_DISPLAY_DSC_HELPER
bool
depends on DRM_DISPLAY_HELPER
help
DRM display helpers for VESA DSC (used by DSI and DisplayPort).
config DRM_DISPLAY_HDCP_HELPER
bool
help
DRM display helpers for HDCP.
config DRM_DISPLAY_HDMI_AUDIO_HELPER
bool
help
DRM display helpers for HDMI Audio functionality (generic HDMI Codec
implementation).
config DRM_DISPLAY_HDMI_CEC_HELPER
bool
help
DRM display helpers for HDMI CEC implementation.
config DRM_DISPLAY_HDMI_CEC_NOTIFIER_HELPER
def_bool CEC_NOTIFIER
help
DRM display helpers for HDMI CEC notifiers implementation.
config DRM_DISPLAY_HDMI_HELPER
bool
help
DRM display helpers for HDMI.
config DRM_DISPLAY_HDMI_STATE_HELPER
bool
select DRM_DISPLAY_HDMI_AUDIO_HELPER
select DRM_DISPLAY_HDMI_HELPER
help
DRM KMS state helpers for HDMI.
endif # DRM_DISPLAY_HELPER
@@ -0,0 +1,31 @@
# SPDX-License-Identifier: MIT
obj-$(CONFIG_DRM_DISPLAY_DP_AUX_BUS) += drm_dp_aux_bus.o
drm_display_helper-y := drm_display_helper_mod.o
drm_display_helper-$(CONFIG_DRM_BRIDGE_CONNECTOR) += \
drm_bridge_connector.o
drm_display_helper-$(CONFIG_DRM_DISPLAY_DP_HELPER) += \
drm_dp_dual_mode_helper.o \
drm_dp_helper.o \
drm_dp_mst_topology.o
drm_display_helper-$(CONFIG_DRM_DISPLAY_DP_TUNNEL) += \
drm_dp_tunnel.o
drm_display_helper-$(CONFIG_DRM_DISPLAY_DSC_HELPER) += \
drm_dsc_helper.o
drm_display_helper-$(CONFIG_DRM_DISPLAY_HDCP_HELPER) += drm_hdcp_helper.o
drm_display_helper-$(CONFIG_DRM_DISPLAY_HDMI_AUDIO_HELPER) += \
drm_hdmi_audio_helper.o
drm_display_helper-$(CONFIG_DRM_DISPLAY_HDMI_CEC_HELPER) += \
drm_hdmi_cec_helper.o
drm_display_helper-$(CONFIG_DRM_DISPLAY_HDMI_CEC_NOTIFIER_HELPER) += \
drm_hdmi_cec_notifier_helper.o
drm_display_helper-$(CONFIG_DRM_DISPLAY_HDMI_HELPER) += \
drm_hdmi_helper.o \
drm_scdc_helper.o
drm_display_helper-$(CONFIG_DRM_DISPLAY_HDMI_STATE_HELPER) += \
drm_hdmi_state_helper.o
drm_display_helper-$(CONFIG_DRM_DISPLAY_DP_AUX_CHARDEV) += drm_dp_aux_dev.o
drm_display_helper-$(CONFIG_DRM_DISPLAY_DP_AUX_CEC) += drm_dp_cec.o
obj-$(CONFIG_DRM_DISPLAY_HELPER) += drm_display_helper.o
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
#include <linux/module.h>
#include "drm_dp_helper_internal.h"
MODULE_DESCRIPTION("DRM display adapter helper");
MODULE_LICENSE("GPL and additional rights");
static int __init drm_display_helper_module_init(void)
{
return drm_dp_aux_dev_init();
}
static void __exit drm_display_helper_module_exit(void)
{
/* Call exit functions from specific dp helpers here */
drm_dp_aux_dev_exit();
}
module_init(drm_display_helper_module_init);
module_exit(drm_display_helper_module_exit);
@@ -0,0 +1,392 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright 2021 Google Inc.
*
* The DP AUX bus is used for devices that are connected over a DisplayPort
* AUX bus. The device on the far side of the bus is referred to as an
* endpoint in this code.
*
* There is only one device connected to the DP AUX bus: an eDP panel.
* Though historically panels (even DP panels) have been modeled as simple
* platform devices, putting them under the DP AUX bus allows the panel driver
* to perform transactions on that bus.
*/
#include <linux/export.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of_device.h>
#include <linux/pm_domain.h>
#include <linux/pm_runtime.h>
#include <drm/display/drm_dp_aux_bus.h>
#include <drm/display/drm_dp_helper.h>
struct dp_aux_ep_device_with_data {
struct dp_aux_ep_device aux_ep;
int (*done_probing)(struct drm_dp_aux *aux);
};
/**
* dp_aux_ep_match() - The match function for the dp_aux_bus.
* @dev: The device to match.
* @drv: The driver to try to match against.
*
* At the moment, we just match on device tree.
*
* Return: True if this driver matches this device; false otherwise.
*/
static int dp_aux_ep_match(struct device *dev, const struct device_driver *drv)
{
return !!of_match_device(drv->of_match_table, dev);
}
/**
* dp_aux_ep_probe() - The probe function for the dp_aux_bus.
* @dev: The device to probe.
*
* Calls through to the endpoint driver probe.
*
* Return: 0 if no error or negative error code.
*/
static int dp_aux_ep_probe(struct device *dev)
{
struct dp_aux_ep_driver *aux_ep_drv = to_dp_aux_ep_drv(dev->driver);
struct dp_aux_ep_device *aux_ep = to_dp_aux_ep_dev(dev);
struct dp_aux_ep_device_with_data *aux_ep_with_data =
container_of(aux_ep, struct dp_aux_ep_device_with_data, aux_ep);
int ret;
ret = dev_pm_domain_attach(dev, PD_FLAG_ATTACH_POWER_ON);
if (ret)
return dev_err_probe(dev, ret, "Failed to attach to PM Domain\n");
ret = aux_ep_drv->probe(aux_ep);
if (ret)
goto err_attached;
if (aux_ep_with_data->done_probing) {
ret = aux_ep_with_data->done_probing(aux_ep->aux);
if (ret) {
/*
* The done_probing() callback should not return
* -EPROBE_DEFER to us. If it does, we treat it as an
* error. Passing it on as-is would cause the _panel_
* to defer.
*/
if (ret == -EPROBE_DEFER) {
dev_err(dev,
"DP AUX done_probing() can't defer\n");
ret = -EINVAL;
}
goto err_probed;
}
}
return 0;
err_probed:
if (aux_ep_drv->remove)
aux_ep_drv->remove(aux_ep);
err_attached:
dev_pm_domain_detach(dev, true);
return ret;
}
/**
* dp_aux_ep_remove() - The remove function for the dp_aux_bus.
* @dev: The device to remove.
*
* Calls through to the endpoint driver remove.
*/
static void dp_aux_ep_remove(struct device *dev)
{
struct dp_aux_ep_driver *aux_ep_drv = to_dp_aux_ep_drv(dev->driver);
struct dp_aux_ep_device *aux_ep = to_dp_aux_ep_dev(dev);
if (aux_ep_drv->remove)
aux_ep_drv->remove(aux_ep);
dev_pm_domain_detach(dev, true);
}
/**
* dp_aux_ep_shutdown() - The shutdown function for the dp_aux_bus.
* @dev: The device to shutdown.
*
* Calls through to the endpoint driver shutdown.
*/
static void dp_aux_ep_shutdown(struct device *dev)
{
struct dp_aux_ep_driver *aux_ep_drv;
if (!dev->driver)
return;
aux_ep_drv = to_dp_aux_ep_drv(dev->driver);
if (aux_ep_drv->shutdown)
aux_ep_drv->shutdown(to_dp_aux_ep_dev(dev));
}
static const struct bus_type dp_aux_bus_type = {
.name = "dp-aux",
.match = dp_aux_ep_match,
.probe = dp_aux_ep_probe,
.remove = dp_aux_ep_remove,
.shutdown = dp_aux_ep_shutdown,
};
static ssize_t modalias_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return of_device_modalias(dev, buf, PAGE_SIZE);
}
static DEVICE_ATTR_RO(modalias);
static struct attribute *dp_aux_ep_dev_attrs[] = {
&dev_attr_modalias.attr,
NULL,
};
ATTRIBUTE_GROUPS(dp_aux_ep_dev);
/**
* dp_aux_ep_dev_release() - Free memory for the dp_aux_ep device
* @dev: The device to free.
*/
static void dp_aux_ep_dev_release(struct device *dev)
{
struct dp_aux_ep_device *aux_ep = to_dp_aux_ep_dev(dev);
struct dp_aux_ep_device_with_data *aux_ep_with_data =
container_of(aux_ep, struct dp_aux_ep_device_with_data, aux_ep);
kfree(aux_ep_with_data);
}
static int dp_aux_ep_dev_modalias(const struct device *dev, struct kobj_uevent_env *env)
{
return of_device_uevent_modalias(dev, env);
}
static struct device_type dp_aux_device_type_type = {
.groups = dp_aux_ep_dev_groups,
.uevent = dp_aux_ep_dev_modalias,
.release = dp_aux_ep_dev_release,
};
/**
* of_dp_aux_ep_destroy() - Destroy an DP AUX endpoint device
* @dev: The device to destroy.
* @data: Not used
*
* This is just used as a callback by of_dp_aux_depopulate_bus() and
* is called for _all_ of the child devices of the device providing the AUX bus.
* We'll only act on those that are of type "dp_aux_bus_type".
*
* This function is effectively an inverse of what's in
* of_dp_aux_populate_bus(). NOTE: since we only populate one child
* then it's expected that only one device will match all the "if" tests in
* this function and get to the device_unregister().
*
* Return: 0 if no error or negative error code.
*/
static int of_dp_aux_ep_destroy(struct device *dev, void *data)
{
struct device_node *np = dev->of_node;
if (dev->bus != &dp_aux_bus_type)
return 0;
if (!of_node_check_flag(np, OF_POPULATED))
return 0;
of_node_clear_flag(np, OF_POPULATED);
of_node_put(np);
device_unregister(dev);
return 0;
}
/**
* of_dp_aux_depopulate_bus() - Undo of_dp_aux_populate_bus
* @aux: The AUX channel whose device we want to depopulate
*
* This will destroy the device that was created
* by of_dp_aux_populate_bus().
*/
void of_dp_aux_depopulate_bus(struct drm_dp_aux *aux)
{
device_for_each_child_reverse(aux->dev, NULL, of_dp_aux_ep_destroy);
}
EXPORT_SYMBOL_GPL(of_dp_aux_depopulate_bus);
/**
* of_dp_aux_populate_bus() - Populate the endpoint device on the DP AUX
* @aux: The AUX channel whose device we want to populate. It is required that
* drm_dp_aux_init() has already been called for this AUX channel.
* @done_probing: Callback functions to call after EP device finishes probing.
* Will not be called if there are no EP devices and this
* function will return -ENODEV.
*
* This will populate the device (expected to be an eDP panel) under the
* "aux-bus" node of the device providing the AUX channel (AKA aux->dev).
*
* When this function finishes, it is _possible_ (but not guaranteed) that
* our sub-device will have finished probing. It should be noted that if our
* sub-device returns -EPROBE_DEFER or is probing asynchronously for some
* reason that we will not return any error codes ourselves but our
* sub-device will _not_ have actually probed successfully yet.
*
* In many cases it's important for the caller of this function to be notified
* when our sub device finishes probing. Our sub device is expected to be an
* eDP panel and the caller is expected to be an eDP controller. The eDP
* controller needs to be able to get a reference to the panel when it finishes
* probing. For this reason the caller can pass in a function pointer that
* will be called when our sub-device finishes probing.
*
* If this function succeeds you should later make sure you call
* of_dp_aux_depopulate_bus() to undo it, or just use the devm version
* of this function.
*
* Return: 0 if no error or negative error code; returns -ENODEV if there are
* no children. The done_probing() function won't be called in that
* case.
*/
int of_dp_aux_populate_bus(struct drm_dp_aux *aux,
int (*done_probing)(struct drm_dp_aux *aux))
{
struct device_node *bus = NULL, *np = NULL;
struct dp_aux_ep_device *aux_ep;
struct dp_aux_ep_device_with_data *aux_ep_with_data;
int ret;
/* drm_dp_aux_init() should have been called already; warn if not */
WARN_ON_ONCE(!aux->ddc.algo);
if (!aux->dev->of_node)
return -ENODEV;
bus = of_get_child_by_name(aux->dev->of_node, "aux-bus");
if (!bus)
return -ENODEV;
np = of_get_next_available_child(bus, NULL);
of_node_put(bus);
if (!np)
return -ENODEV;
if (of_node_test_and_set_flag(np, OF_POPULATED)) {
dev_err(aux->dev, "DP AUX EP device already populated\n");
ret = -EINVAL;
goto err_did_get_np;
}
aux_ep_with_data = kzalloc_obj(*aux_ep_with_data);
if (!aux_ep_with_data) {
ret = -ENOMEM;
goto err_did_set_populated;
}
aux_ep_with_data->done_probing = done_probing;
aux_ep = &aux_ep_with_data->aux_ep;
aux_ep->aux = aux;
aux_ep->dev.parent = aux->dev;
aux_ep->dev.bus = &dp_aux_bus_type;
aux_ep->dev.type = &dp_aux_device_type_type;
device_set_node(&aux_ep->dev, of_fwnode_handle(of_node_get(np)));
dev_set_name(&aux_ep->dev, "aux-%s", dev_name(aux->dev));
ret = device_register(&aux_ep->dev);
if (ret) {
dev_err(aux->dev, "Failed to create AUX EP for %pOF: %d\n", np, ret);
/*
* As per docs of device_register(), call this instead
* of kfree() directly for error cases.
*/
put_device(&aux_ep->dev);
goto err_did_set_populated;
}
return 0;
err_did_set_populated:
of_node_clear_flag(np, OF_POPULATED);
err_did_get_np:
of_node_put(np);
return ret;
}
EXPORT_SYMBOL_GPL(of_dp_aux_populate_bus);
static void of_dp_aux_depopulate_bus_void(void *data)
{
of_dp_aux_depopulate_bus(data);
}
/**
* devm_of_dp_aux_populate_bus() - devm wrapper for of_dp_aux_populate_bus()
* @aux: The AUX channel whose device we want to populate
* @done_probing: Callback functions to call after EP device finishes probing.
* Will not be called if there are no EP devices and this
* function will return -ENODEV.
*
* Handles freeing w/ devm on the device "aux->dev".
*
* Return: 0 if no error or negative error code; returns -ENODEV if there are
* no children. The done_probing() function won't be called in that
* case.
*/
int devm_of_dp_aux_populate_bus(struct drm_dp_aux *aux,
int (*done_probing)(struct drm_dp_aux *aux))
{
int ret;
ret = of_dp_aux_populate_bus(aux, done_probing);
if (ret)
return ret;
return devm_add_action_or_reset(aux->dev,
of_dp_aux_depopulate_bus_void, aux);
}
EXPORT_SYMBOL_GPL(devm_of_dp_aux_populate_bus);
int __dp_aux_dp_driver_register(struct dp_aux_ep_driver *drv, struct module *owner)
{
drv->driver.owner = owner;
drv->driver.bus = &dp_aux_bus_type;
return driver_register(&drv->driver);
}
EXPORT_SYMBOL_GPL(__dp_aux_dp_driver_register);
void dp_aux_dp_driver_unregister(struct dp_aux_ep_driver *drv)
{
driver_unregister(&drv->driver);
}
EXPORT_SYMBOL_GPL(dp_aux_dp_driver_unregister);
static int __init dp_aux_bus_init(void)
{
int ret;
ret = bus_register(&dp_aux_bus_type);
if (ret)
return ret;
return 0;
}
static void __exit dp_aux_bus_exit(void)
{
bus_unregister(&dp_aux_bus_type);
}
subsys_initcall(dp_aux_bus_init);
module_exit(dp_aux_bus_exit);
MODULE_AUTHOR("Douglas Anderson <dianders@chromium.org>");
MODULE_DESCRIPTION("DRM DisplayPort AUX bus");
MODULE_LICENSE("GPL v2");
@@ -0,0 +1,354 @@
/*
* Copyright © 2015 Intel Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*
* Authors:
* Rafael Antognolli <rafael.antognolli@intel.com>
*
*/
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/sched/signal.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/uio.h>
#include <drm/display/drm_dp_helper.h>
#include <drm/display/drm_dp_mst_helper.h>
#include <drm/drm_crtc.h>
#include <drm/drm_print.h>
#include "drm_dp_helper_internal.h"
struct drm_dp_aux_dev {
unsigned index;
struct drm_dp_aux *aux;
struct device *dev;
struct kref refcount;
atomic_t usecount;
};
#define DRM_AUX_MINORS 256
#define AUX_MAX_OFFSET (1 << 20)
static DEFINE_IDR(aux_idr);
static DEFINE_MUTEX(aux_idr_mutex);
static struct class *drm_dp_aux_dev_class;
static int drm_dev_major = -1;
static struct drm_dp_aux_dev *drm_dp_aux_dev_get_by_minor(unsigned index)
{
struct drm_dp_aux_dev *aux_dev = NULL;
mutex_lock(&aux_idr_mutex);
aux_dev = idr_find(&aux_idr, index);
if (aux_dev && !kref_get_unless_zero(&aux_dev->refcount))
aux_dev = NULL;
mutex_unlock(&aux_idr_mutex);
return aux_dev;
}
static struct drm_dp_aux_dev *alloc_drm_dp_aux_dev(struct drm_dp_aux *aux)
{
struct drm_dp_aux_dev *aux_dev;
int index;
aux_dev = kzalloc_obj(*aux_dev);
if (!aux_dev)
return ERR_PTR(-ENOMEM);
aux_dev->aux = aux;
atomic_set(&aux_dev->usecount, 1);
kref_init(&aux_dev->refcount);
mutex_lock(&aux_idr_mutex);
index = idr_alloc(&aux_idr, aux_dev, 0, DRM_AUX_MINORS, GFP_KERNEL);
mutex_unlock(&aux_idr_mutex);
if (index < 0) {
kfree(aux_dev);
return ERR_PTR(index);
}
aux_dev->index = index;
return aux_dev;
}
static void release_drm_dp_aux_dev(struct kref *ref)
{
struct drm_dp_aux_dev *aux_dev =
container_of(ref, struct drm_dp_aux_dev, refcount);
kfree(aux_dev);
}
static ssize_t name_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
ssize_t res;
struct drm_dp_aux_dev *aux_dev =
drm_dp_aux_dev_get_by_minor(MINOR(dev->devt));
if (!aux_dev)
return -ENODEV;
res = sprintf(buf, "%s\n", aux_dev->aux->name);
kref_put(&aux_dev->refcount, release_drm_dp_aux_dev);
return res;
}
static DEVICE_ATTR_RO(name);
static struct attribute *drm_dp_aux_attrs[] = {
&dev_attr_name.attr,
NULL,
};
ATTRIBUTE_GROUPS(drm_dp_aux);
static int auxdev_open(struct inode *inode, struct file *file)
{
unsigned int minor = iminor(inode);
struct drm_dp_aux_dev *aux_dev;
aux_dev = drm_dp_aux_dev_get_by_minor(minor);
if (!aux_dev)
return -ENODEV;
file->private_data = aux_dev;
return 0;
}
static loff_t auxdev_llseek(struct file *file, loff_t offset, int whence)
{
return fixed_size_llseek(file, offset, whence, AUX_MAX_OFFSET);
}
static ssize_t auxdev_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
struct drm_dp_aux_dev *aux_dev = iocb->ki_filp->private_data;
loff_t pos = iocb->ki_pos;
ssize_t res = 0;
if (!atomic_inc_not_zero(&aux_dev->usecount))
return -ENODEV;
iov_iter_truncate(to, AUX_MAX_OFFSET - pos);
while (iov_iter_count(to)) {
uint8_t buf[DP_AUX_MAX_PAYLOAD_BYTES];
ssize_t todo = min(iov_iter_count(to), sizeof(buf));
if (signal_pending(current)) {
res = -ERESTARTSYS;
break;
}
res = drm_dp_dpcd_read(aux_dev->aux, pos, buf, todo);
if (res <= 0)
break;
if (copy_to_iter(buf, res, to) != res) {
res = -EFAULT;
break;
}
pos += res;
}
if (pos != iocb->ki_pos)
res = pos - iocb->ki_pos;
iocb->ki_pos = pos;
if (atomic_dec_and_test(&aux_dev->usecount))
wake_up_var(&aux_dev->usecount);
return res;
}
static ssize_t auxdev_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
struct drm_dp_aux_dev *aux_dev = iocb->ki_filp->private_data;
loff_t pos = iocb->ki_pos;
ssize_t res = 0;
if (!atomic_inc_not_zero(&aux_dev->usecount))
return -ENODEV;
iov_iter_truncate(from, AUX_MAX_OFFSET - pos);
while (iov_iter_count(from)) {
uint8_t buf[DP_AUX_MAX_PAYLOAD_BYTES];
ssize_t todo = min(iov_iter_count(from), sizeof(buf));
if (signal_pending(current)) {
res = -ERESTARTSYS;
break;
}
if (!copy_from_iter_full(buf, todo, from)) {
res = -EFAULT;
break;
}
res = drm_dp_dpcd_write(aux_dev->aux, pos, buf, todo);
if (res <= 0)
break;
pos += res;
}
if (pos != iocb->ki_pos)
res = pos - iocb->ki_pos;
iocb->ki_pos = pos;
if (atomic_dec_and_test(&aux_dev->usecount))
wake_up_var(&aux_dev->usecount);
return res;
}
static int auxdev_release(struct inode *inode, struct file *file)
{
struct drm_dp_aux_dev *aux_dev = file->private_data;
kref_put(&aux_dev->refcount, release_drm_dp_aux_dev);
return 0;
}
static const struct file_operations auxdev_fops = {
.owner = THIS_MODULE,
.llseek = auxdev_llseek,
.read_iter = auxdev_read_iter,
.write_iter = auxdev_write_iter,
.open = auxdev_open,
.release = auxdev_release,
};
#define to_auxdev(d) container_of(d, struct drm_dp_aux_dev, aux)
static struct drm_dp_aux_dev *drm_dp_aux_dev_get_by_aux(struct drm_dp_aux *aux)
{
struct drm_dp_aux_dev *iter, *aux_dev = NULL;
int id;
/* don't increase kref count here because this function should only be
* used by drm_dp_aux_unregister_devnode. Thus, it will always have at
* least one reference - the one that drm_dp_aux_register_devnode
* created
*/
mutex_lock(&aux_idr_mutex);
idr_for_each_entry(&aux_idr, iter, id) {
if (iter->aux == aux) {
aux_dev = iter;
break;
}
}
mutex_unlock(&aux_idr_mutex);
return aux_dev;
}
void drm_dp_aux_unregister_devnode(struct drm_dp_aux *aux)
{
struct drm_dp_aux_dev *aux_dev;
unsigned int minor;
aux_dev = drm_dp_aux_dev_get_by_aux(aux);
if (!aux_dev) /* attach must have failed */
return;
/*
* As some AUX adapters may exist as platform devices which outlive their respective DRM
* devices, we clear drm_dev to ensure that we never accidentally reference a stale pointer
*/
aux->drm_dev = NULL;
mutex_lock(&aux_idr_mutex);
idr_remove(&aux_idr, aux_dev->index);
mutex_unlock(&aux_idr_mutex);
atomic_dec(&aux_dev->usecount);
wait_var_event(&aux_dev->usecount, !atomic_read(&aux_dev->usecount));
minor = aux_dev->index;
if (aux_dev->dev)
device_destroy(drm_dp_aux_dev_class,
MKDEV(drm_dev_major, minor));
DRM_DEBUG("drm_dp_aux_dev: aux [%s] unregistering\n", aux->name);
kref_put(&aux_dev->refcount, release_drm_dp_aux_dev);
}
int drm_dp_aux_register_devnode(struct drm_dp_aux *aux)
{
struct drm_dp_aux_dev *aux_dev;
int res;
aux_dev = alloc_drm_dp_aux_dev(aux);
if (IS_ERR(aux_dev))
return PTR_ERR(aux_dev);
aux_dev->dev = device_create(drm_dp_aux_dev_class, aux->dev,
MKDEV(drm_dev_major, aux_dev->index), NULL,
"drm_dp_aux%d", aux_dev->index);
if (IS_ERR(aux_dev->dev)) {
res = PTR_ERR(aux_dev->dev);
aux_dev->dev = NULL;
goto error;
}
DRM_DEBUG("drm_dp_aux_dev: aux [%s] registered as minor %d\n",
aux->name, aux_dev->index);
return 0;
error:
drm_dp_aux_unregister_devnode(aux);
return res;
}
int drm_dp_aux_dev_init(void)
{
int res;
drm_dp_aux_dev_class = class_create("drm_dp_aux_dev");
if (IS_ERR(drm_dp_aux_dev_class)) {
return PTR_ERR(drm_dp_aux_dev_class);
}
drm_dp_aux_dev_class->dev_groups = drm_dp_aux_groups;
res = register_chrdev(0, "aux", &auxdev_fops);
if (res < 0)
goto out;
drm_dev_major = res;
return 0;
out:
class_destroy(drm_dp_aux_dev_class);
return res;
}
void drm_dp_aux_dev_exit(void)
{
unregister_chrdev(drm_dev_major, "aux");
class_destroy(drm_dp_aux_dev_class);
}
@@ -0,0 +1,460 @@
// SPDX-License-Identifier: GPL-2.0
/*
* DisplayPort CEC-Tunneling-over-AUX support
*
* Copyright 2018 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*/
#include <linux/export.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <media/cec.h>
#include <drm/display/drm_dp_helper.h>
#include <drm/drm_connector.h>
#include <drm/drm_device.h>
#include <drm/drm_edid.h>
/*
* Unfortunately it turns out that we have a chicken-and-egg situation
* here. Quite a few active (mini-)DP-to-HDMI or USB-C-to-HDMI adapters
* have a converter chip that supports CEC-Tunneling-over-AUX (usually the
* Parade PS176), but they do not wire up the CEC pin, thus making CEC
* useless. Note that MegaChips 2900-based adapters appear to have good
* support for CEC tunneling. Those adapters that I have tested using
* this chipset all have the CEC line connected.
*
* Sadly there is no way for this driver to know this. What happens is
* that a /dev/cecX device is created that is isolated and unable to see
* any of the other CEC devices. Quite literally the CEC wire is cut
* (or in this case, never connected in the first place).
*
* The reason so few adapters support this is that this tunneling protocol
* was never supported by any OS. So there was no easy way of testing it,
* and no incentive to correctly wire up the CEC pin.
*
* Hopefully by creating this driver it will be easier for vendors to
* finally fix their adapters and test the CEC functionality.
*
* I keep a list of known working adapters here:
*
* https://hverkuil.home.xs4all.nl/cec-status.txt
*
* Please mail me (hverkuil@kernel.org) if you find an adapter that works
* and is not yet listed there.
*
* Note that the current implementation does not support CEC over an MST hub.
* As far as I can see there is no mechanism defined in the DisplayPort
* standard to transport CEC interrupts over an MST device. It might be
* possible to do this through polling, but I have not been able to get that
* to work.
*/
/**
* DOC: dp cec helpers
*
* These functions take care of supporting the CEC-Tunneling-over-AUX
* feature of DisplayPort-to-HDMI adapters.
*/
/*
* When the EDID is unset because the HPD went low, then the CEC DPCD registers
* typically can no longer be read (true for a DP-to-HDMI adapter since it is
* powered by the HPD). However, some displays toggle the HPD off and on for a
* short period for one reason or another, and that would cause the CEC adapter
* to be removed and added again, even though nothing else changed.
*
* This module parameter sets a delay in seconds before the CEC adapter is
* actually unregistered. Only if the HPD does not return within that time will
* the CEC adapter be unregistered.
*
* If it is set to a value >= NEVER_UNREG_DELAY, then the CEC adapter will never
* be unregistered for as long as the connector remains registered.
*
* If it is set to 0, then the CEC adapter will be unregistered immediately as
* soon as the HPD disappears.
*
* The default is one second to prevent short HPD glitches from unregistering
* the CEC adapter.
*
* Note that for integrated HDMI branch devices that support CEC the DPCD
* registers remain available even if the HPD goes low since it is not powered
* by the HPD. In that case the CEC adapter will never be unregistered during
* the life time of the connector. At least, this is the theory since I do not
* have hardware with an integrated HDMI branch device that supports CEC.
*/
#define NEVER_UNREG_DELAY 1000
static unsigned int drm_dp_cec_unregister_delay = 1;
module_param(drm_dp_cec_unregister_delay, uint, 0600);
MODULE_PARM_DESC(drm_dp_cec_unregister_delay,
"CEC unregister delay in seconds, 0: no delay, >= 1000: never unregister");
static int drm_dp_cec_adap_enable(struct cec_adapter *adap, bool enable)
{
struct drm_dp_aux *aux = cec_get_drvdata(adap);
u32 val = enable ? DP_CEC_TUNNELING_ENABLE : 0;
ssize_t err = 0;
err = drm_dp_dpcd_write_byte(aux, DP_CEC_TUNNELING_CONTROL, val);
return (enable && err < 0) ? err : 0;
}
static int drm_dp_cec_adap_log_addr(struct cec_adapter *adap, u8 addr)
{
struct drm_dp_aux *aux = cec_get_drvdata(adap);
/* Bit 15 (logical address 15) should always be set */
u16 la_mask = 1 << CEC_LOG_ADDR_BROADCAST;
u8 mask[2];
ssize_t err;
if (addr != CEC_LOG_ADDR_INVALID)
la_mask |= adap->log_addrs.log_addr_mask | (1 << addr);
mask[0] = la_mask & 0xff;
mask[1] = la_mask >> 8;
err = drm_dp_dpcd_write_data(aux, DP_CEC_LOGICAL_ADDRESS_MASK, mask, 2);
return (addr != CEC_LOG_ADDR_INVALID && err < 0) ? err : 0;
}
static int drm_dp_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
u32 signal_free_time, struct cec_msg *msg)
{
struct drm_dp_aux *aux = cec_get_drvdata(adap);
unsigned int retries = min(5, attempts - 1);
ssize_t err;
err = drm_dp_dpcd_write_data(aux, DP_CEC_TX_MESSAGE_BUFFER,
msg->msg, msg->len);
if (err < 0)
return err;
return drm_dp_dpcd_write_byte(aux, DP_CEC_TX_MESSAGE_INFO,
(msg->len - 1) | (retries << 4) |
DP_CEC_TX_MESSAGE_SEND);
}
static int drm_dp_cec_adap_monitor_all_enable(struct cec_adapter *adap,
bool enable)
{
struct drm_dp_aux *aux = cec_get_drvdata(adap);
ssize_t err;
u8 val;
if (!(adap->capabilities & CEC_CAP_MONITOR_ALL))
return 0;
err = drm_dp_dpcd_read_byte(aux, DP_CEC_TUNNELING_CONTROL, &val);
if (!err) {
if (enable)
val |= DP_CEC_SNOOPING_ENABLE;
else
val &= ~DP_CEC_SNOOPING_ENABLE;
err = drm_dp_dpcd_write_byte(aux, DP_CEC_TUNNELING_CONTROL, val);
}
return (enable && err < 0) ? err : 0;
}
static void drm_dp_cec_adap_status(struct cec_adapter *adap,
struct seq_file *file)
{
struct drm_dp_aux *aux = cec_get_drvdata(adap);
struct drm_dp_desc desc;
struct drm_dp_dpcd_ident *id = &desc.ident;
if (drm_dp_read_desc(aux, &desc, true))
return;
seq_printf(file, "OUI: %*phD\n",
(int)sizeof(id->oui), id->oui);
seq_printf(file, "ID: %*pE\n",
(int)strnlen(id->device_id, sizeof(id->device_id)),
id->device_id);
seq_printf(file, "HW Rev: %d.%d\n", id->hw_rev >> 4, id->hw_rev & 0xf);
/*
* Show this both in decimal and hex: at least one vendor
* always reports this in hex.
*/
seq_printf(file, "FW/SW Rev: %d.%d (0x%02x.0x%02x)\n",
id->sw_major_rev, id->sw_minor_rev,
id->sw_major_rev, id->sw_minor_rev);
}
static const struct cec_adap_ops drm_dp_cec_adap_ops = {
.adap_enable = drm_dp_cec_adap_enable,
.adap_log_addr = drm_dp_cec_adap_log_addr,
.adap_transmit = drm_dp_cec_adap_transmit,
.adap_monitor_all_enable = drm_dp_cec_adap_monitor_all_enable,
.adap_status = drm_dp_cec_adap_status,
};
static int drm_dp_cec_received(struct drm_dp_aux *aux)
{
struct cec_adapter *adap = aux->cec.adap;
struct cec_msg msg;
u8 rx_msg_info;
ssize_t err;
err = drm_dp_dpcd_read_byte(aux, DP_CEC_RX_MESSAGE_INFO, &rx_msg_info);
if (err < 0)
return err;
if (!(rx_msg_info & DP_CEC_RX_MESSAGE_ENDED))
return 0;
msg.len = (rx_msg_info & DP_CEC_RX_MESSAGE_LEN_MASK) + 1;
err = drm_dp_dpcd_read_data(aux, DP_CEC_RX_MESSAGE_BUFFER, msg.msg, msg.len);
if (err < 0)
return err;
cec_received_msg(adap, &msg);
return 0;
}
static void drm_dp_cec_handle_irq(struct drm_dp_aux *aux)
{
struct cec_adapter *adap = aux->cec.adap;
u8 flags;
if (drm_dp_dpcd_read_byte(aux, DP_CEC_TUNNELING_IRQ_FLAGS, &flags) < 0)
return;
if (flags & DP_CEC_RX_MESSAGE_INFO_VALID)
drm_dp_cec_received(aux);
if (flags & DP_CEC_TX_MESSAGE_SENT)
cec_transmit_attempt_done(adap, CEC_TX_STATUS_OK);
else if (flags & DP_CEC_TX_LINE_ERROR)
cec_transmit_attempt_done(adap, CEC_TX_STATUS_ERROR |
CEC_TX_STATUS_MAX_RETRIES);
else if (flags &
(DP_CEC_TX_ADDRESS_NACK_ERROR | DP_CEC_TX_DATA_NACK_ERROR))
cec_transmit_attempt_done(adap, CEC_TX_STATUS_NACK |
CEC_TX_STATUS_MAX_RETRIES);
drm_dp_dpcd_write_byte(aux, DP_CEC_TUNNELING_IRQ_FLAGS, flags);
}
/**
* drm_dp_cec_irq() - handle CEC interrupt, if any
* @aux: DisplayPort AUX channel
*
* Should be called when handling an IRQ_HPD request. If CEC-tunneling-over-AUX
* is present, then it will check for a CEC_IRQ and handle it accordingly.
*/
void drm_dp_cec_irq(struct drm_dp_aux *aux)
{
u8 cec_irq;
int ret;
/* No transfer function was set, so not a DP connector */
if (!aux->transfer)
return;
mutex_lock(&aux->cec.lock);
if (!aux->cec.adap)
goto unlock;
ret = drm_dp_dpcd_read_byte(aux, DP_DEVICE_SERVICE_IRQ_VECTOR_ESI1,
&cec_irq);
if (ret < 0 || !(cec_irq & DP_CEC_IRQ))
goto unlock;
drm_dp_cec_handle_irq(aux);
drm_dp_dpcd_write_byte(aux, DP_DEVICE_SERVICE_IRQ_VECTOR_ESI1, DP_CEC_IRQ);
unlock:
mutex_unlock(&aux->cec.lock);
}
EXPORT_SYMBOL(drm_dp_cec_irq);
static bool drm_dp_cec_cap(struct drm_dp_aux *aux, u8 *cec_cap)
{
u8 cap = 0;
if (drm_dp_dpcd_read_byte(aux, DP_CEC_TUNNELING_CAPABILITY, &cap) < 0 ||
!(cap & DP_CEC_TUNNELING_CAPABLE))
return false;
if (cec_cap)
*cec_cap = cap;
return true;
}
/*
* Called if the HPD was low for more than drm_dp_cec_unregister_delay
* seconds. This unregisters the CEC adapter.
*/
static void drm_dp_cec_unregister_work(struct work_struct *work)
{
struct drm_dp_aux *aux = container_of(work, struct drm_dp_aux,
cec.unregister_work.work);
mutex_lock(&aux->cec.lock);
cec_unregister_adapter(aux->cec.adap);
aux->cec.adap = NULL;
mutex_unlock(&aux->cec.lock);
}
/*
* A new EDID is set. If there is no CEC adapter, then create one. If
* there was a CEC adapter, then check if the CEC adapter properties
* were unchanged and just update the CEC physical address. Otherwise
* unregister the old CEC adapter and create a new one.
*/
void drm_dp_cec_attach(struct drm_dp_aux *aux, u16 source_physical_address)
{
struct drm_connector *connector = aux->cec.connector;
u32 cec_caps = CEC_CAP_DEFAULTS | CEC_CAP_NEEDS_HPD |
CEC_CAP_CONNECTOR_INFO;
struct cec_connector_info conn_info;
unsigned int num_las = 1;
u8 cap;
/* No transfer function was set, so not a DP connector */
if (!aux->transfer)
return;
cancel_delayed_work_sync(&aux->cec.unregister_work);
mutex_lock(&aux->cec.lock);
if (!drm_dp_cec_cap(aux, &cap)) {
/* CEC is not supported, unregister any existing adapter */
cec_unregister_adapter(aux->cec.adap);
aux->cec.adap = NULL;
goto unlock;
}
if (cap & DP_CEC_SNOOPING_CAPABLE)
cec_caps |= CEC_CAP_MONITOR_ALL;
if (cap & DP_CEC_MULTIPLE_LA_CAPABLE)
num_las = CEC_MAX_LOG_ADDRS;
if (aux->cec.adap) {
/* Check if the adapter properties have changed */
if ((aux->cec.adap->capabilities & CEC_CAP_MONITOR_ALL) ==
(cec_caps & CEC_CAP_MONITOR_ALL) &&
aux->cec.adap->available_log_addrs == num_las) {
/* Unchanged, so just set the phys addr */
cec_s_phys_addr(aux->cec.adap, source_physical_address, false);
goto unlock;
}
/*
* The capabilities changed, so unregister the old
* adapter first.
*/
cec_unregister_adapter(aux->cec.adap);
}
/* Create a new adapter */
aux->cec.adap = cec_allocate_adapter(&drm_dp_cec_adap_ops,
aux, connector->name, cec_caps,
num_las);
if (IS_ERR(aux->cec.adap)) {
aux->cec.adap = NULL;
goto unlock;
}
cec_fill_conn_info_from_drm(&conn_info, connector);
cec_s_conn_info(aux->cec.adap, &conn_info);
if (cec_register_adapter(aux->cec.adap, connector->dev->dev)) {
cec_delete_adapter(aux->cec.adap);
aux->cec.adap = NULL;
} else {
/*
* Update the phys addr for the new CEC adapter. When called
* from drm_dp_cec_register_connector() edid == NULL, so in
* that case the phys addr is just invalidated.
*/
cec_s_phys_addr(aux->cec.adap, source_physical_address, false);
}
unlock:
mutex_unlock(&aux->cec.lock);
}
EXPORT_SYMBOL(drm_dp_cec_attach);
/*
* Note: Prefer calling drm_dp_cec_attach() with
* connector->display_info.source_physical_address if possible.
*/
void drm_dp_cec_set_edid(struct drm_dp_aux *aux, const struct edid *edid)
{
u16 pa = CEC_PHYS_ADDR_INVALID;
if (edid && edid->extensions)
pa = cec_get_edid_phys_addr((const u8 *)edid,
EDID_LENGTH * (edid->extensions + 1), NULL);
drm_dp_cec_attach(aux, pa);
}
EXPORT_SYMBOL(drm_dp_cec_set_edid);
/*
* The EDID disappeared (likely because of the HPD going down).
*/
void drm_dp_cec_unset_edid(struct drm_dp_aux *aux)
{
/* No transfer function was set, so not a DP connector */
if (!aux->transfer)
return;
cancel_delayed_work_sync(&aux->cec.unregister_work);
mutex_lock(&aux->cec.lock);
if (!aux->cec.adap)
goto unlock;
cec_phys_addr_invalidate(aux->cec.adap);
/*
* We're done if we want to keep the CEC device
* (drm_dp_cec_unregister_delay is >= NEVER_UNREG_DELAY) or if the
* DPCD still indicates the CEC capability (expected for an integrated
* HDMI branch device).
*/
if (drm_dp_cec_unregister_delay < NEVER_UNREG_DELAY &&
!drm_dp_cec_cap(aux, NULL)) {
/*
* Unregister the CEC adapter after drm_dp_cec_unregister_delay
* seconds. This to debounce short HPD off-and-on cycles from
* displays.
*/
schedule_delayed_work(&aux->cec.unregister_work,
drm_dp_cec_unregister_delay * HZ);
}
unlock:
mutex_unlock(&aux->cec.lock);
}
EXPORT_SYMBOL(drm_dp_cec_unset_edid);
/**
* drm_dp_cec_register_connector() - register a new connector
* @aux: DisplayPort AUX channel
* @connector: drm connector
*
* A new connector was registered with associated CEC adapter name and
* CEC adapter parent device. After registering the name and parent
* drm_dp_cec_set_edid() is called to check if the connector supports
* CEC and to register a CEC adapter if that is the case.
*/
void drm_dp_cec_register_connector(struct drm_dp_aux *aux,
struct drm_connector *connector)
{
WARN_ON(aux->cec.adap);
if (WARN_ON(!aux->transfer))
return;
aux->cec.connector = connector;
INIT_DELAYED_WORK(&aux->cec.unregister_work,
drm_dp_cec_unregister_work);
}
EXPORT_SYMBOL(drm_dp_cec_register_connector);
/**
* drm_dp_cec_unregister_connector() - unregister the CEC adapter, if any
* @aux: DisplayPort AUX channel
*/
void drm_dp_cec_unregister_connector(struct drm_dp_aux *aux)
{
if (!aux->cec.adap)
return;
cancel_delayed_work_sync(&aux->cec.unregister_work);
cec_unregister_adapter(aux->cec.adap);
aux->cec.adap = NULL;
}
EXPORT_SYMBOL(drm_dp_cec_unregister_connector);
@@ -0,0 +1,537 @@
/*
* Copyright © 2016 Intel Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER(S) OR AUTHOR(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#include <linux/delay.h>
#include <linux/errno.h>
#include <linux/export.h>
#include <linux/i2c.h>
#include <linux/slab.h>
#include <linux/string.h>
#include <drm/display/drm_dp_dual_mode_helper.h>
#include <drm/drm_device.h>
#include <drm/drm_print.h>
/**
* DOC: dp dual mode helpers
*
* Helper functions to deal with DP dual mode (aka. DP++) adaptors.
*
* Type 1:
* Adaptor registers (if any) and the sink DDC bus may be accessed via I2C.
*
* Type 2:
* Adaptor registers and sink DDC bus can be accessed either via I2C or
* I2C-over-AUX. Source devices may choose to implement either of these
* access methods.
*/
#define DP_DUAL_MODE_SLAVE_ADDRESS 0x40
/**
* drm_dp_dual_mode_read - Read from the DP dual mode adaptor register(s)
* @adapter: I2C adapter for the DDC bus
* @offset: register offset
* @buffer: buffer for return data
* @size: size of the buffer
*
* Reads @size bytes from the DP dual mode adaptor registers
* starting at @offset.
*
* Returns:
* 0 on success, negative error code on failure
*/
ssize_t drm_dp_dual_mode_read(struct i2c_adapter *adapter,
u8 offset, void *buffer, size_t size)
{
u8 zero = 0;
char *tmpbuf = NULL;
/*
* As sub-addressing is not supported by all adaptors,
* always explicitly read from the start and discard
* any bytes that come before the requested offset.
* This way, no matter whether the adaptor supports it
* or not, we'll end up reading the proper data.
*/
struct i2c_msg msgs[] = {
{
.addr = DP_DUAL_MODE_SLAVE_ADDRESS,
.flags = 0,
.len = 1,
.buf = &zero,
},
{
.addr = DP_DUAL_MODE_SLAVE_ADDRESS,
.flags = I2C_M_RD,
.len = size + offset,
.buf = buffer,
},
};
int ret;
if (offset) {
tmpbuf = kmalloc(size + offset, GFP_KERNEL);
if (!tmpbuf)
return -ENOMEM;
msgs[1].buf = tmpbuf;
}
ret = i2c_transfer(adapter, msgs, ARRAY_SIZE(msgs));
if (tmpbuf)
memcpy(buffer, tmpbuf + offset, size);
kfree(tmpbuf);
if (ret < 0)
return ret;
if (ret != ARRAY_SIZE(msgs))
return -EPROTO;
return 0;
}
EXPORT_SYMBOL(drm_dp_dual_mode_read);
/**
* drm_dp_dual_mode_write - Write to the DP dual mode adaptor register(s)
* @adapter: I2C adapter for the DDC bus
* @offset: register offset
* @buffer: buffer for write data
* @size: size of the buffer
*
* Writes @size bytes to the DP dual mode adaptor registers
* starting at @offset.
*
* Returns:
* 0 on success, negative error code on failure
*/
ssize_t drm_dp_dual_mode_write(struct i2c_adapter *adapter,
u8 offset, const void *buffer, size_t size)
{
struct i2c_msg msg = {
.addr = DP_DUAL_MODE_SLAVE_ADDRESS,
.flags = 0,
.len = 1 + size,
.buf = NULL,
};
void *data;
int ret;
data = kmalloc(msg.len, GFP_KERNEL);
if (!data)
return -ENOMEM;
msg.buf = data;
memcpy(data, &offset, 1);
memcpy(data + 1, buffer, size);
ret = i2c_transfer(adapter, &msg, 1);
kfree(data);
if (ret < 0)
return ret;
if (ret != 1)
return -EPROTO;
return 0;
}
EXPORT_SYMBOL(drm_dp_dual_mode_write);
static bool is_hdmi_adaptor(const char hdmi_id[DP_DUAL_MODE_HDMI_ID_LEN])
{
static const char dp_dual_mode_hdmi_id[DP_DUAL_MODE_HDMI_ID_LEN + 1] =
"DP-HDMI ADAPTOR\x04";
return memcmp(hdmi_id, dp_dual_mode_hdmi_id,
DP_DUAL_MODE_HDMI_ID_LEN) == 0;
}
static bool is_type1_adaptor(uint8_t adaptor_id)
{
return adaptor_id == 0 || adaptor_id == 0xff;
}
static bool is_type2_adaptor(uint8_t adaptor_id)
{
return adaptor_id == (DP_DUAL_MODE_TYPE_TYPE2 |
DP_DUAL_MODE_REV_TYPE2);
}
static bool is_lspcon_adaptor(const char hdmi_id[DP_DUAL_MODE_HDMI_ID_LEN],
const uint8_t adaptor_id)
{
return is_hdmi_adaptor(hdmi_id) &&
(adaptor_id == (DP_DUAL_MODE_TYPE_TYPE2 |
DP_DUAL_MODE_TYPE_HAS_DPCD));
}
/**
* drm_dp_dual_mode_detect - Identify the DP dual mode adaptor
* @dev: &drm_device to use
* @adapter: I2C adapter for the DDC bus
*
* Attempt to identify the type of the DP dual mode adaptor used.
*
* Note that when the answer is @DRM_DP_DUAL_MODE_UNKNOWN it's not
* certain whether we're dealing with a native HDMI port or
* a type 1 DVI dual mode adaptor. The driver will have to use
* some other hardware/driver specific mechanism to make that
* distinction.
*
* Returns:
* The type of the DP dual mode adaptor used
*/
enum drm_dp_dual_mode_type drm_dp_dual_mode_detect(const struct drm_device *dev,
struct i2c_adapter *adapter)
{
char hdmi_id[DP_DUAL_MODE_HDMI_ID_LEN] = {};
uint8_t adaptor_id = 0x00;
ssize_t ret;
/*
* Let's see if the adaptor is there the by reading the
* HDMI ID registers.
*
* Note that type 1 DVI adaptors are not required to implemnt
* any registers, and that presents a problem for detection.
* If the i2c transfer is nacked, we may or may not be dealing
* with a type 1 DVI adaptor. Some other mechanism of detecting
* the presence of the adaptor is required. One way would be
* to check the state of the CONFIG1 pin, Another method would
* simply require the driver to know whether the port is a DP++
* port or a native HDMI port. Both of these methods are entirely
* hardware/driver specific so we can't deal with them here.
*/
ret = drm_dp_dual_mode_read(adapter, DP_DUAL_MODE_HDMI_ID,
hdmi_id, sizeof(hdmi_id));
drm_dbg_kms(dev, "DP dual mode HDMI ID: %*pE (err %zd)\n",
ret ? 0 : (int)sizeof(hdmi_id), hdmi_id, ret);
if (ret)
return DRM_DP_DUAL_MODE_UNKNOWN;
ret = drm_dp_dual_mode_read(adapter, DP_DUAL_MODE_ADAPTOR_ID,
&adaptor_id, sizeof(adaptor_id));
drm_dbg_kms(dev, "DP dual mode adaptor ID: %02x (err %zd)\n", adaptor_id, ret);
if (ret == 0) {
if (is_lspcon_adaptor(hdmi_id, adaptor_id))
return DRM_DP_DUAL_MODE_LSPCON;
if (is_type2_adaptor(adaptor_id)) {
if (is_hdmi_adaptor(hdmi_id))
return DRM_DP_DUAL_MODE_TYPE2_HDMI;
else
return DRM_DP_DUAL_MODE_TYPE2_DVI;
}
/*
* If not a proper type 1 ID, still assume type 1, but let
* the user know that we may have misdetected the type.
*/
if (!is_type1_adaptor(adaptor_id))
drm_err(dev, "Unexpected DP dual mode adaptor ID %02x\n", adaptor_id);
}
if (is_hdmi_adaptor(hdmi_id))
return DRM_DP_DUAL_MODE_TYPE1_HDMI;
else
return DRM_DP_DUAL_MODE_TYPE1_DVI;
}
EXPORT_SYMBOL(drm_dp_dual_mode_detect);
/**
* drm_dp_dual_mode_max_tmds_clock - Max TMDS clock for DP dual mode adaptor
* @dev: &drm_device to use
* @type: DP dual mode adaptor type
* @adapter: I2C adapter for the DDC bus
*
* Determine the max TMDS clock the adaptor supports based on the
* type of the dual mode adaptor and the DP_DUAL_MODE_MAX_TMDS_CLOCK
* register (on type2 adaptors). As some type 1 adaptors have
* problems with registers (see comments in drm_dp_dual_mode_detect())
* we don't read the register on those, instead we simply assume
* a 165 MHz limit based on the specification.
*
* Returns:
* Maximum supported TMDS clock rate for the DP dual mode adaptor in kHz.
*/
int drm_dp_dual_mode_max_tmds_clock(const struct drm_device *dev, enum drm_dp_dual_mode_type type,
struct i2c_adapter *adapter)
{
uint8_t max_tmds_clock;
ssize_t ret;
/* native HDMI so no limit */
if (type == DRM_DP_DUAL_MODE_NONE)
return 0;
/*
* Type 1 adaptors are limited to 165MHz
* Type 2 adaptors can tells us their limit
*/
if (type < DRM_DP_DUAL_MODE_TYPE2_DVI)
return 165000;
ret = drm_dp_dual_mode_read(adapter, DP_DUAL_MODE_MAX_TMDS_CLOCK,
&max_tmds_clock, sizeof(max_tmds_clock));
if (ret || max_tmds_clock == 0x00 || max_tmds_clock == 0xff) {
drm_dbg_kms(dev, "Failed to query max TMDS clock\n");
return 165000;
}
return max_tmds_clock * 5000 / 2;
}
EXPORT_SYMBOL(drm_dp_dual_mode_max_tmds_clock);
/**
* drm_dp_dual_mode_get_tmds_output - Get the state of the TMDS output buffers in the DP dual mode adaptor
* @dev: &drm_device to use
* @type: DP dual mode adaptor type
* @adapter: I2C adapter for the DDC bus
* @enabled: current state of the TMDS output buffers
*
* Get the state of the TMDS output buffers in the adaptor. For
* type2 adaptors this is queried from the DP_DUAL_MODE_TMDS_OEN
* register. As some type 1 adaptors have problems with registers
* (see comments in drm_dp_dual_mode_detect()) we don't read the
* register on those, instead we simply assume that the buffers
* are always enabled.
*
* Returns:
* 0 on success, negative error code on failure
*/
int drm_dp_dual_mode_get_tmds_output(const struct drm_device *dev,
enum drm_dp_dual_mode_type type, struct i2c_adapter *adapter,
bool *enabled)
{
uint8_t tmds_oen;
ssize_t ret;
if (type < DRM_DP_DUAL_MODE_TYPE2_DVI) {
*enabled = true;
return 0;
}
ret = drm_dp_dual_mode_read(adapter, DP_DUAL_MODE_TMDS_OEN,
&tmds_oen, sizeof(tmds_oen));
if (ret) {
drm_dbg_kms(dev, "Failed to query state of TMDS output buffers\n");
return ret;
}
*enabled = !(tmds_oen & DP_DUAL_MODE_TMDS_DISABLE);
return 0;
}
EXPORT_SYMBOL(drm_dp_dual_mode_get_tmds_output);
/**
* drm_dp_dual_mode_set_tmds_output - Enable/disable TMDS output buffers in the DP dual mode adaptor
* @dev: &drm_device to use
* @type: DP dual mode adaptor type
* @adapter: I2C adapter for the DDC bus
* @enable: enable (as opposed to disable) the TMDS output buffers
*
* Set the state of the TMDS output buffers in the adaptor. For
* type2 this is set via the DP_DUAL_MODE_TMDS_OEN register.
* Type1 adaptors do not support any register writes.
*
* Returns:
* 0 on success, negative error code on failure
*/
int drm_dp_dual_mode_set_tmds_output(const struct drm_device *dev, enum drm_dp_dual_mode_type type,
struct i2c_adapter *adapter, bool enable)
{
uint8_t tmds_oen = enable ? 0 : DP_DUAL_MODE_TMDS_DISABLE;
ssize_t ret;
int retry;
if (type < DRM_DP_DUAL_MODE_TYPE2_DVI)
return 0;
/*
* LSPCON adapters in low-power state may ignore the first write, so
* read back and verify the written value a few times.
*/
for (retry = 0; retry < 3; retry++) {
uint8_t tmp;
ret = drm_dp_dual_mode_write(adapter, DP_DUAL_MODE_TMDS_OEN,
&tmds_oen, sizeof(tmds_oen));
if (ret) {
drm_dbg_kms(dev, "Failed to %s TMDS output buffers (%d attempts)\n",
enable ? "enable" : "disable", retry + 1);
return ret;
}
ret = drm_dp_dual_mode_read(adapter, DP_DUAL_MODE_TMDS_OEN,
&tmp, sizeof(tmp));
if (ret) {
drm_dbg_kms(dev,
"I2C read failed during TMDS output buffer %s (%d attempts)\n",
enable ? "enabling" : "disabling", retry + 1);
return ret;
}
if (tmp == tmds_oen)
return 0;
}
drm_dbg_kms(dev, "I2C write value mismatch during TMDS output buffer %s\n",
enable ? "enabling" : "disabling");
return -EIO;
}
EXPORT_SYMBOL(drm_dp_dual_mode_set_tmds_output);
/**
* drm_dp_get_dual_mode_type_name - Get the name of the DP dual mode adaptor type as a string
* @type: DP dual mode adaptor type
*
* Returns:
* String representation of the DP dual mode adaptor type
*/
const char *drm_dp_get_dual_mode_type_name(enum drm_dp_dual_mode_type type)
{
switch (type) {
case DRM_DP_DUAL_MODE_NONE:
return "none";
case DRM_DP_DUAL_MODE_TYPE1_DVI:
return "type 1 DVI";
case DRM_DP_DUAL_MODE_TYPE1_HDMI:
return "type 1 HDMI";
case DRM_DP_DUAL_MODE_TYPE2_DVI:
return "type 2 DVI";
case DRM_DP_DUAL_MODE_TYPE2_HDMI:
return "type 2 HDMI";
case DRM_DP_DUAL_MODE_LSPCON:
return "lspcon";
default:
WARN_ON(type != DRM_DP_DUAL_MODE_UNKNOWN);
return "unknown";
}
}
EXPORT_SYMBOL(drm_dp_get_dual_mode_type_name);
/**
* drm_lspcon_get_mode: Get LSPCON's current mode of operation by
* reading offset (0x80, 0x41)
* @dev: &drm_device to use
* @adapter: I2C-over-aux adapter
* @mode: current lspcon mode of operation output variable
*
* Returns:
* 0 on success, sets the current_mode value to appropriate mode
* -error on failure
*/
int drm_lspcon_get_mode(const struct drm_device *dev, struct i2c_adapter *adapter,
enum drm_lspcon_mode *mode)
{
u8 data;
int ret = 0;
int retry;
if (!mode) {
drm_err(dev, "NULL input\n");
return -EINVAL;
}
/* Read Status: i2c over aux */
for (retry = 0; retry < 6; retry++) {
if (retry)
usleep_range(500, 1000);
ret = drm_dp_dual_mode_read(adapter,
DP_DUAL_MODE_LSPCON_CURRENT_MODE,
&data, sizeof(data));
if (!ret)
break;
}
if (ret < 0) {
drm_dbg_kms(dev, "LSPCON read(0x80, 0x41) failed\n");
return -EFAULT;
}
if (data & DP_DUAL_MODE_LSPCON_MODE_PCON)
*mode = DRM_LSPCON_MODE_PCON;
else
*mode = DRM_LSPCON_MODE_LS;
return 0;
}
EXPORT_SYMBOL(drm_lspcon_get_mode);
/**
* drm_lspcon_set_mode: Change LSPCON's mode of operation by
* writing offset (0x80, 0x40)
* @dev: &drm_device to use
* @adapter: I2C-over-aux adapter
* @mode: required mode of operation
* @time_out: LSPCON mode change settle timeout
*
* Returns:
* 0 on success, -error on failure/timeout
*/
int drm_lspcon_set_mode(const struct drm_device *dev, struct i2c_adapter *adapter,
enum drm_lspcon_mode mode, int time_out)
{
u8 data = 0;
int ret;
enum drm_lspcon_mode current_mode;
if (mode == DRM_LSPCON_MODE_PCON)
data = DP_DUAL_MODE_LSPCON_MODE_PCON;
/* Change mode */
ret = drm_dp_dual_mode_write(adapter, DP_DUAL_MODE_LSPCON_MODE_CHANGE,
&data, sizeof(data));
if (ret < 0) {
drm_err(dev, "LSPCON mode change failed\n");
return ret;
}
/*
* Confirm mode change by reading the status bit.
* Sometimes, it takes a while to change the mode,
* so wait and retry until time out or done.
*/
do {
ret = drm_lspcon_get_mode(dev, adapter, &current_mode);
if (ret) {
drm_err(dev, "can't confirm LSPCON mode change\n");
return ret;
} else {
if (current_mode != mode) {
msleep(10);
time_out -= 10;
} else {
drm_dbg_kms(dev, "LSPCON mode changed to %s\n",
mode == DRM_LSPCON_MODE_LS ? "LS" : "PCON");
return 0;
}
}
} while (time_out);
drm_err(dev, "LSPCON mode change timed out\n");
return -ETIMEDOUT;
}
EXPORT_SYMBOL(drm_lspcon_set_mode);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,33 @@
/* SPDX-License-Identifier: MIT */
#ifndef DRM_DP_HELPER_INTERNAL_H
#define DRM_DP_HELPER_INTERNAL_H
struct drm_dp_aux;
#ifdef CONFIG_DRM_DISPLAY_DP_AUX_CHARDEV
int drm_dp_aux_dev_init(void);
void drm_dp_aux_dev_exit(void);
int drm_dp_aux_register_devnode(struct drm_dp_aux *aux);
void drm_dp_aux_unregister_devnode(struct drm_dp_aux *aux);
#else
static inline int drm_dp_aux_dev_init(void)
{
return 0;
}
static inline void drm_dp_aux_dev_exit(void)
{
}
static inline int drm_dp_aux_register_devnode(struct drm_dp_aux *aux)
{
return 0;
}
static inline void drm_dp_aux_unregister_devnode(struct drm_dp_aux *aux)
{
}
#endif
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
/* SPDX-License-Identifier: GPL-2.0-only
*
* Declarations for DP MST related functions which are only used in selftests
*
* Copyright © 2018 Red Hat
* Authors:
* Lyude Paul <lyude@redhat.com>
*/
#ifndef _DRM_DP_MST_HELPER_INTERNAL_H_
#define _DRM_DP_MST_HELPER_INTERNAL_H_
struct drm_dp_sideband_msg_req_body;
struct drm_dp_sideband_msg_tx;
struct drm_printer;
void
drm_dp_encode_sideband_req(const struct drm_dp_sideband_msg_req_body *req,
struct drm_dp_sideband_msg_tx *raw);
int drm_dp_decode_sideband_req(const struct drm_dp_sideband_msg_tx *raw,
struct drm_dp_sideband_msg_req_body *req);
void
drm_dp_dump_sideband_msg_req_body(const struct drm_dp_sideband_msg_req_body *req,
int indent, struct drm_printer *printer);
#endif /* !_DRM_DP_MST_HELPER_INTERNAL_H_ */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,421 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2019 Intel Corporation.
*
* Authors:
* Ramalingam C <ramalingam.c@intel.com>
*/
#include <linux/device.h>
#include <linux/err.h>
#include <linux/gfp.h>
#include <linux/export.h>
#include <linux/slab.h>
#include <linux/firmware.h>
#include <drm/display/drm_hdcp_helper.h>
#include <drm/drm_sysfs.h>
#include <drm/drm_print.h>
#include <drm/drm_device.h>
#include <drm/drm_property.h>
#include <drm/drm_mode_object.h>
#include <drm/drm_connector.h>
static inline void drm_hdcp_print_ksv(const u8 *ksv)
{
DRM_DEBUG("\t%#02x, %#02x, %#02x, %#02x, %#02x\n",
ksv[0], ksv[1], ksv[2], ksv[3], ksv[4]);
}
static u32 drm_hdcp_get_revoked_ksv_count(const u8 *buf, u32 vrls_length)
{
u32 parsed_bytes = 0, ksv_count = 0, vrl_ksv_cnt, vrl_sz;
while (parsed_bytes < vrls_length) {
vrl_ksv_cnt = *buf;
ksv_count += vrl_ksv_cnt;
vrl_sz = (vrl_ksv_cnt * DRM_HDCP_KSV_LEN) + 1;
buf += vrl_sz;
parsed_bytes += vrl_sz;
}
/*
* When vrls are not valid, ksvs are not considered.
* Hence SRM will be discarded.
*/
if (parsed_bytes != vrls_length)
ksv_count = 0;
return ksv_count;
}
static u32 drm_hdcp_get_revoked_ksvs(const u8 *buf, u8 **revoked_ksv_list,
u32 vrls_length)
{
u32 vrl_ksv_cnt, vrl_ksv_sz, vrl_idx = 0;
u32 parsed_bytes = 0, ksv_count = 0;
do {
vrl_ksv_cnt = *buf;
vrl_ksv_sz = vrl_ksv_cnt * DRM_HDCP_KSV_LEN;
buf++;
DRM_DEBUG("vrl: %d, Revoked KSVs: %d\n", vrl_idx++,
vrl_ksv_cnt);
memcpy((*revoked_ksv_list) + (ksv_count * DRM_HDCP_KSV_LEN),
buf, vrl_ksv_sz);
ksv_count += vrl_ksv_cnt;
buf += vrl_ksv_sz;
parsed_bytes += (vrl_ksv_sz + 1);
} while (parsed_bytes < vrls_length);
return ksv_count;
}
static inline u32 get_vrl_length(const u8 *buf)
{
return drm_hdcp_be24_to_cpu(buf);
}
static int drm_hdcp_parse_hdcp1_srm(const u8 *buf, size_t count,
u8 **revoked_ksv_list, u32 *revoked_ksv_cnt)
{
struct hdcp_srm_header *header;
u32 vrl_length, ksv_count;
if (count < (sizeof(struct hdcp_srm_header) +
DRM_HDCP_1_4_VRL_LENGTH_SIZE + DRM_HDCP_1_4_DCP_SIG_SIZE)) {
DRM_ERROR("Invalid blob length\n");
return -EINVAL;
}
header = (struct hdcp_srm_header *)buf;
DRM_DEBUG("SRM ID: 0x%x, SRM Ver: 0x%x, SRM Gen No: 0x%x\n",
header->srm_id,
be16_to_cpu(header->srm_version), header->srm_gen_no);
WARN_ON(header->reserved);
buf = buf + sizeof(*header);
vrl_length = get_vrl_length(buf);
if (count < (sizeof(struct hdcp_srm_header) + vrl_length) ||
vrl_length < (DRM_HDCP_1_4_VRL_LENGTH_SIZE +
DRM_HDCP_1_4_DCP_SIG_SIZE)) {
DRM_ERROR("Invalid blob length or vrl length\n");
return -EINVAL;
}
/* Length of the all vrls combined */
vrl_length -= (DRM_HDCP_1_4_VRL_LENGTH_SIZE +
DRM_HDCP_1_4_DCP_SIG_SIZE);
if (!vrl_length) {
DRM_ERROR("No vrl found\n");
return -EINVAL;
}
buf += DRM_HDCP_1_4_VRL_LENGTH_SIZE;
ksv_count = drm_hdcp_get_revoked_ksv_count(buf, vrl_length);
if (!ksv_count) {
DRM_DEBUG("Revoked KSV count is 0\n");
return 0;
}
*revoked_ksv_list = kcalloc(ksv_count, DRM_HDCP_KSV_LEN, GFP_KERNEL);
if (!*revoked_ksv_list) {
DRM_ERROR("Out of Memory\n");
return -ENOMEM;
}
if (drm_hdcp_get_revoked_ksvs(buf, revoked_ksv_list,
vrl_length) != ksv_count) {
*revoked_ksv_cnt = 0;
kfree(*revoked_ksv_list);
return -EINVAL;
}
*revoked_ksv_cnt = ksv_count;
return 0;
}
static int drm_hdcp_parse_hdcp2_srm(const u8 *buf, size_t count,
u8 **revoked_ksv_list, u32 *revoked_ksv_cnt)
{
struct hdcp_srm_header *header;
u32 vrl_length, ksv_count, ksv_sz;
if (count < (sizeof(struct hdcp_srm_header) +
DRM_HDCP_2_VRL_LENGTH_SIZE + DRM_HDCP_2_DCP_SIG_SIZE)) {
DRM_ERROR("Invalid blob length\n");
return -EINVAL;
}
header = (struct hdcp_srm_header *)buf;
DRM_DEBUG("SRM ID: 0x%x, SRM Ver: 0x%x, SRM Gen No: 0x%x\n",
header->srm_id & DRM_HDCP_SRM_ID_MASK,
be16_to_cpu(header->srm_version), header->srm_gen_no);
if (header->reserved)
return -EINVAL;
buf = buf + sizeof(*header);
vrl_length = get_vrl_length(buf);
if (count < (sizeof(struct hdcp_srm_header) + vrl_length) ||
vrl_length < (DRM_HDCP_2_VRL_LENGTH_SIZE +
DRM_HDCP_2_DCP_SIG_SIZE)) {
DRM_ERROR("Invalid blob length or vrl length\n");
return -EINVAL;
}
/* Length of the all vrls combined */
vrl_length -= (DRM_HDCP_2_VRL_LENGTH_SIZE +
DRM_HDCP_2_DCP_SIG_SIZE);
if (!vrl_length) {
DRM_ERROR("No vrl found\n");
return -EINVAL;
}
buf += DRM_HDCP_2_VRL_LENGTH_SIZE;
ksv_count = (*buf << 2) | DRM_HDCP_2_KSV_COUNT_2_LSBITS(*(buf + 1));
if (!ksv_count) {
DRM_DEBUG("Revoked KSV count is 0\n");
return 0;
}
*revoked_ksv_list = kcalloc(ksv_count, DRM_HDCP_KSV_LEN, GFP_KERNEL);
if (!*revoked_ksv_list) {
DRM_ERROR("Out of Memory\n");
return -ENOMEM;
}
ksv_sz = ksv_count * DRM_HDCP_KSV_LEN;
buf += DRM_HDCP_2_NO_OF_DEV_PLUS_RESERVED_SZ;
DRM_DEBUG("Revoked KSVs: %d\n", ksv_count);
memcpy(*revoked_ksv_list, buf, ksv_sz);
*revoked_ksv_cnt = ksv_count;
return 0;
}
static inline bool is_srm_version_hdcp1(const u8 *buf)
{
return *buf == (u8)(DRM_HDCP_1_4_SRM_ID << 4);
}
static inline bool is_srm_version_hdcp2(const u8 *buf)
{
return *buf == (u8)(DRM_HDCP_2_SRM_ID << 4 | DRM_HDCP_2_INDICATOR);
}
static int drm_hdcp_srm_update(const u8 *buf, size_t count,
u8 **revoked_ksv_list, u32 *revoked_ksv_cnt)
{
if (count < sizeof(struct hdcp_srm_header))
return -EINVAL;
if (is_srm_version_hdcp1(buf))
return drm_hdcp_parse_hdcp1_srm(buf, count, revoked_ksv_list,
revoked_ksv_cnt);
else if (is_srm_version_hdcp2(buf))
return drm_hdcp_parse_hdcp2_srm(buf, count, revoked_ksv_list,
revoked_ksv_cnt);
else
return -EINVAL;
}
static int drm_hdcp_request_srm(struct drm_device *drm_dev,
u8 **revoked_ksv_list, u32 *revoked_ksv_cnt)
{
char fw_name[36] = "display_hdcp_srm.bin";
const struct firmware *fw;
int ret;
ret = request_firmware_direct(&fw, (const char *)fw_name,
drm_dev->dev);
if (ret < 0) {
*revoked_ksv_cnt = 0;
*revoked_ksv_list = NULL;
ret = 0;
goto exit;
}
if (fw->size && fw->data)
ret = drm_hdcp_srm_update(fw->data, fw->size, revoked_ksv_list,
revoked_ksv_cnt);
exit:
release_firmware(fw);
return ret;
}
/**
* drm_hdcp_check_ksvs_revoked - Check the revoked status of the IDs
*
* @drm_dev: drm_device for which HDCP revocation check is requested
* @ksvs: List of KSVs (HDCP receiver IDs)
* @ksv_count: KSV count passed in through @ksvs
*
* This function reads the HDCP System renewability Message(SRM Table)
* from userspace as a firmware and parses it for the revoked HDCP
* KSVs(Receiver IDs) detected by DCP LLC. Once the revoked KSVs are known,
* revoked state of the KSVs in the list passed in by display drivers are
* decided and response is sent.
*
* SRM should be presented in the name of "display_hdcp_srm.bin".
*
* Format of the SRM table, that userspace needs to write into the binary file,
* is defined at:
* 1. Renewability chapter on 55th page of HDCP 1.4 specification
* https://www.digital-cp.com/sites/default/files/specifications/HDCP%20Specification%20Rev1_4_Secure.pdf
* 2. Renewability chapter on 63rd page of HDCP 2.2 specification
* https://www.digital-cp.com/sites/default/files/specifications/HDCP%20on%20HDMI%20Specification%20Rev2_2_Final1.pdf
*
* Returns:
* Count of the revoked KSVs or -ve error number in case of the failure.
*/
int drm_hdcp_check_ksvs_revoked(struct drm_device *drm_dev, u8 *ksvs,
u32 ksv_count)
{
u32 revoked_ksv_cnt = 0, i, j;
u8 *revoked_ksv_list = NULL;
int ret = 0;
ret = drm_hdcp_request_srm(drm_dev, &revoked_ksv_list,
&revoked_ksv_cnt);
if (ret)
return ret;
/* revoked_ksv_cnt will be zero when above function failed */
for (i = 0; i < revoked_ksv_cnt; i++)
for (j = 0; j < ksv_count; j++)
if (!memcmp(&ksvs[j * DRM_HDCP_KSV_LEN],
&revoked_ksv_list[i * DRM_HDCP_KSV_LEN],
DRM_HDCP_KSV_LEN)) {
DRM_DEBUG("Revoked KSV is ");
drm_hdcp_print_ksv(&ksvs[j * DRM_HDCP_KSV_LEN]);
ret++;
}
kfree(revoked_ksv_list);
return ret;
}
EXPORT_SYMBOL_GPL(drm_hdcp_check_ksvs_revoked);
static struct drm_prop_enum_list drm_cp_enum_list[] = {
{ DRM_MODE_CONTENT_PROTECTION_UNDESIRED, "Undesired" },
{ DRM_MODE_CONTENT_PROTECTION_DESIRED, "Desired" },
{ DRM_MODE_CONTENT_PROTECTION_ENABLED, "Enabled" },
};
DRM_ENUM_NAME_FN(drm_get_content_protection_name, drm_cp_enum_list)
static struct drm_prop_enum_list drm_hdcp_content_type_enum_list[] = {
{ DRM_MODE_HDCP_CONTENT_TYPE0, "HDCP Type0" },
{ DRM_MODE_HDCP_CONTENT_TYPE1, "HDCP Type1" },
};
DRM_ENUM_NAME_FN(drm_get_hdcp_content_type_name,
drm_hdcp_content_type_enum_list)
/**
* drm_connector_attach_content_protection_property - attach content protection
* property
*
* @connector: connector to attach CP property on.
* @hdcp_content_type: is HDCP Content Type property needed for connector
*
* This is used to add support for content protection on select connectors.
* Content Protection is intentionally vague to allow for different underlying
* technologies, however it is most implemented by HDCP.
*
* When hdcp_content_type is true enum property called HDCP Content Type is
* created (if it is not already) and attached to the connector.
*
* This property is used for sending the protected content's stream type
* from userspace to kernel on selected connectors. Protected content provider
* will decide their type of their content and declare the same to kernel.
*
* Content type will be used during the HDCP 2.2 authentication.
* Content type will be set to &drm_connector_state.hdcp_content_type.
*
* The content protection will be set to &drm_connector_state.content_protection
*
* When kernel triggered content protection state change like DESIRED->ENABLED
* and ENABLED->DESIRED, will use drm_hdcp_update_content_protection() to update
* the content protection state of a connector.
*
* Returns:
* Zero on success, negative errno on failure.
*/
int drm_connector_attach_content_protection_property(
struct drm_connector *connector, bool hdcp_content_type)
{
struct drm_device *dev = connector->dev;
struct drm_property *prop =
dev->mode_config.content_protection_property;
if (!prop)
prop = drm_property_create_enum(dev, 0, "Content Protection",
drm_cp_enum_list,
ARRAY_SIZE(drm_cp_enum_list));
if (!prop)
return -ENOMEM;
drm_object_attach_property(&connector->base, prop,
DRM_MODE_CONTENT_PROTECTION_UNDESIRED);
dev->mode_config.content_protection_property = prop;
if (!hdcp_content_type)
return 0;
prop = dev->mode_config.hdcp_content_type_property;
if (!prop)
prop = drm_property_create_enum(dev, 0, "HDCP Content Type",
drm_hdcp_content_type_enum_list,
ARRAY_SIZE(
drm_hdcp_content_type_enum_list));
if (!prop)
return -ENOMEM;
drm_object_attach_property(&connector->base, prop,
DRM_MODE_HDCP_CONTENT_TYPE0);
dev->mode_config.hdcp_content_type_property = prop;
return 0;
}
EXPORT_SYMBOL(drm_connector_attach_content_protection_property);
/**
* drm_hdcp_update_content_protection - Updates the content protection state
* of a connector
*
* @connector: drm_connector on which content protection state needs an update
* @val: New state of the content protection property
*
* This function can be used by display drivers, to update the kernel triggered
* content protection state changes of a drm_connector such as DESIRED->ENABLED
* and ENABLED->DESIRED. No uevent for DESIRED->UNDESIRED or ENABLED->UNDESIRED,
* as userspace is triggering such state change and kernel performs it without
* fail.This function update the new state of the property into the connector's
* state and generate an uevent to notify the userspace.
*/
void drm_hdcp_update_content_protection(struct drm_connector *connector,
u64 val)
{
struct drm_device *dev = connector->dev;
struct drm_connector_state *state = connector->state;
WARN_ON(!drm_modeset_is_locked(&dev->mode_config.connection_mutex));
if (state->content_protection == val)
return;
state->content_protection = val;
drm_sysfs_connector_property_event(connector,
dev->mode_config.content_protection_property);
}
EXPORT_SYMBOL(drm_hdcp_update_content_protection);
@@ -0,0 +1,195 @@
// SPDX-License-Identifier: MIT
/*
* Copyright (c) 2024 Linaro Ltd
*/
#include <linux/export.h>
#include <linux/mutex.h>
#include <linux/of_graph.h>
#include <linux/platform_device.h>
#include <drm/drm_connector.h>
#include <drm/drm_device.h>
#include <drm/display/drm_hdmi_audio_helper.h>
#include <sound/hdmi-codec.h>
static int drm_connector_hdmi_audio_startup(struct device *dev, void *data)
{
struct drm_connector *connector = data;
const struct drm_connector_hdmi_audio_funcs *funcs =
connector->hdmi_audio.funcs;
if (funcs->startup)
return funcs->startup(connector);
return 0;
}
static int drm_connector_hdmi_audio_prepare(struct device *dev, void *data,
struct hdmi_codec_daifmt *fmt,
struct hdmi_codec_params *hparms)
{
struct drm_connector *connector = data;
const struct drm_connector_hdmi_audio_funcs *funcs =
connector->hdmi_audio.funcs;
return funcs->prepare(connector, fmt, hparms);
}
static void drm_connector_hdmi_audio_shutdown(struct device *dev, void *data)
{
struct drm_connector *connector = data;
const struct drm_connector_hdmi_audio_funcs *funcs =
connector->hdmi_audio.funcs;
return funcs->shutdown(connector);
}
static int drm_connector_hdmi_audio_mute_stream(struct device *dev, void *data,
bool enable, int direction)
{
struct drm_connector *connector = data;
const struct drm_connector_hdmi_audio_funcs *funcs =
connector->hdmi_audio.funcs;
if (funcs->mute_stream)
return funcs->mute_stream(connector, enable, direction);
return -ENOTSUPP;
}
static int drm_connector_hdmi_audio_get_dai_id(struct snd_soc_component *comment,
struct device_node *endpoint,
void *data)
{
struct drm_connector *connector = data;
struct of_endpoint of_ep;
int ret;
if (connector->hdmi_audio.dai_port < 0)
return -ENOTSUPP;
ret = of_graph_parse_endpoint(endpoint, &of_ep);
if (ret < 0)
return ret;
if (of_ep.port == connector->hdmi_audio.dai_port)
return 0;
return -EINVAL;
}
static int drm_connector_hdmi_audio_get_eld(struct device *dev, void *data,
uint8_t *buf, size_t len)
{
struct drm_connector *connector = data;
mutex_lock(&connector->eld_mutex);
memcpy(buf, connector->eld, min(sizeof(connector->eld), len));
mutex_unlock(&connector->eld_mutex);
return 0;
}
static int drm_connector_hdmi_audio_hook_plugged_cb(struct device *dev,
void *data,
hdmi_codec_plugged_cb fn,
struct device *codec_dev)
{
struct drm_connector *connector = data;
mutex_lock(&connector->hdmi_audio.lock);
connector->hdmi_audio.plugged_cb = fn;
connector->hdmi_audio.plugged_cb_dev = codec_dev;
if (fn)
fn(codec_dev, connector->hdmi_audio.last_state);
mutex_unlock(&connector->hdmi_audio.lock);
return 0;
}
void drm_connector_hdmi_audio_plugged_notify(struct drm_connector *connector,
bool plugged)
{
mutex_lock(&connector->hdmi_audio.lock);
connector->hdmi_audio.last_state = plugged;
if (connector->hdmi_audio.plugged_cb &&
connector->hdmi_audio.plugged_cb_dev)
connector->hdmi_audio.plugged_cb(connector->hdmi_audio.plugged_cb_dev,
connector->hdmi_audio.last_state);
mutex_unlock(&connector->hdmi_audio.lock);
}
EXPORT_SYMBOL(drm_connector_hdmi_audio_plugged_notify);
static const struct hdmi_codec_ops drm_connector_hdmi_audio_ops = {
.audio_startup = drm_connector_hdmi_audio_startup,
.prepare = drm_connector_hdmi_audio_prepare,
.audio_shutdown = drm_connector_hdmi_audio_shutdown,
.mute_stream = drm_connector_hdmi_audio_mute_stream,
.get_eld = drm_connector_hdmi_audio_get_eld,
.get_dai_id = drm_connector_hdmi_audio_get_dai_id,
.hook_plugged_cb = drm_connector_hdmi_audio_hook_plugged_cb,
};
/**
* drm_connector_hdmi_audio_init - Initialize HDMI Codec device for the DRM connector
* @connector: A pointer to the connector to allocate codec for
* @hdmi_codec_dev: device to be used as a parent for the HDMI Codec
* @funcs: callbacks for this HDMI Codec
* @max_i2s_playback_channels: maximum number of playback I2S channels
* @i2s_formats: set of I2S formats (use 0 for a bus-specific set)
* @spdif_playback: set if HDMI codec has S/PDIF playback port
* @dai_port: sound DAI port, -1 if it is not enabled
*
* Create a HDMI codec device to be used with the specified connector.
*
* Returns:
* Zero on success, error code on failure.
*/
int drm_connector_hdmi_audio_init(struct drm_connector *connector,
struct device *hdmi_codec_dev,
const struct drm_connector_hdmi_audio_funcs *funcs,
unsigned int max_i2s_playback_channels,
u64 i2s_formats,
bool spdif_playback,
int dai_port)
{
struct hdmi_codec_pdata codec_pdata = {
.ops = &drm_connector_hdmi_audio_ops,
.max_i2s_channels = max_i2s_playback_channels,
.i2s = !!max_i2s_playback_channels,
.i2s_formats = i2s_formats,
.spdif = spdif_playback,
.no_i2s_capture = true,
.no_spdif_capture = true,
.data = connector,
};
struct platform_device *pdev;
if (!funcs ||
!funcs->prepare ||
!funcs->shutdown)
return -EINVAL;
connector->hdmi_audio.funcs = funcs;
connector->hdmi_audio.dai_port = dai_port;
pdev = platform_device_register_data(hdmi_codec_dev,
HDMI_CODEC_DRV_NAME,
PLATFORM_DEVID_AUTO,
&codec_pdata, sizeof(codec_pdata));
if (IS_ERR(pdev))
return PTR_ERR(pdev);
connector->hdmi_audio.codec_pdev = pdev;
return 0;
}
EXPORT_SYMBOL(drm_connector_hdmi_audio_init);
@@ -0,0 +1,193 @@
// SPDX-License-Identifier: MIT
/*
* Copyright (c) 2024 Linaro Ltd
*/
#include <drm/drm_bridge.h>
#include <drm/drm_connector.h>
#include <drm/drm_managed.h>
#include <drm/display/drm_hdmi_cec_helper.h>
#include <linux/export.h>
#include <linux/mutex.h>
#include <media/cec.h>
struct drm_connector_hdmi_cec_data {
struct cec_adapter *adapter;
const struct drm_connector_hdmi_cec_funcs *funcs;
};
static int drm_connector_hdmi_cec_adap_enable(struct cec_adapter *adap, bool enable)
{
struct drm_connector *connector = cec_get_drvdata(adap);
struct drm_connector_hdmi_cec_data *data = connector->cec.data;
return data->funcs->enable(connector, enable);
}
static int drm_connector_hdmi_cec_adap_log_addr(struct cec_adapter *adap, u8 logical_addr)
{
struct drm_connector *connector = cec_get_drvdata(adap);
struct drm_connector_hdmi_cec_data *data = connector->cec.data;
return data->funcs->log_addr(connector, logical_addr);
}
static int drm_connector_hdmi_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
u32 signal_free_time, struct cec_msg *msg)
{
struct drm_connector *connector = cec_get_drvdata(adap);
struct drm_connector_hdmi_cec_data *data = connector->cec.data;
return data->funcs->transmit(connector, attempts, signal_free_time, msg);
}
static const struct cec_adap_ops drm_connector_hdmi_cec_adap_ops = {
.adap_enable = drm_connector_hdmi_cec_adap_enable,
.adap_log_addr = drm_connector_hdmi_cec_adap_log_addr,
.adap_transmit = drm_connector_hdmi_cec_adap_transmit,
};
static void drm_connector_hdmi_cec_adapter_phys_addr_invalidate(struct drm_connector *connector)
{
struct drm_connector_hdmi_cec_data *data = connector->cec.data;
cec_phys_addr_invalidate(data->adapter);
}
static void drm_connector_hdmi_cec_adapter_phys_addr_set(struct drm_connector *connector,
u16 addr)
{
struct drm_connector_hdmi_cec_data *data = connector->cec.data;
cec_s_phys_addr(data->adapter, addr, false);
}
static void drm_connector_hdmi_cec_adapter_unregister(struct drm_device *dev, void *res)
{
struct drm_connector *connector = res;
struct drm_connector_hdmi_cec_data *data = connector->cec.data;
cec_unregister_adapter(data->adapter);
if (data->funcs->uninit)
data->funcs->uninit(connector);
kfree(data);
connector->cec.data = NULL;
}
static struct drm_connector_cec_funcs drm_connector_hdmi_cec_adapter_funcs = {
.phys_addr_invalidate = drm_connector_hdmi_cec_adapter_phys_addr_invalidate,
.phys_addr_set = drm_connector_hdmi_cec_adapter_phys_addr_set,
};
int drmm_connector_hdmi_cec_register(struct drm_connector *connector,
const struct drm_connector_hdmi_cec_funcs *funcs,
const char *name,
u8 available_las,
struct device *dev)
{
struct drm_connector_hdmi_cec_data *data;
struct cec_connector_info conn_info;
struct cec_adapter *cec_adap;
int ret;
if (!funcs->init || !funcs->enable || !funcs->log_addr || !funcs->transmit)
return -EINVAL;
data = kzalloc_obj(*data);
if (!data)
return -ENOMEM;
data->funcs = funcs;
cec_adap = cec_allocate_adapter(&drm_connector_hdmi_cec_adap_ops, connector, name,
CEC_CAP_DEFAULTS | CEC_CAP_CONNECTOR_INFO,
available_las ? : CEC_MAX_LOG_ADDRS);
ret = PTR_ERR_OR_ZERO(cec_adap);
if (ret < 0)
goto err_free;
cec_fill_conn_info_from_drm(&conn_info, connector);
cec_s_conn_info(cec_adap, &conn_info);
data->adapter = cec_adap;
mutex_lock(&connector->cec.mutex);
connector->cec.data = data;
connector->cec.funcs = &drm_connector_hdmi_cec_adapter_funcs;
ret = funcs->init(connector);
if (ret < 0)
goto err_delete_adapter;
/*
* NOTE: the CEC adapter will be unregistered by drmm cleanup from
* drm_managed_release(), which is called from drm_dev_release()
* during device unbind.
*
* However, the CEC framework cleans up the CEC adapter only when the
* last user has closed its file descriptor, so we don't need to handle
* it in DRM.
*
* Before that CEC framework makes sure that even if the userspace
* still holds CEC device open, all calls will be shortcut via
* cec_is_registered(), making sure that there is no access to the
* freed memory.
*/
ret = cec_register_adapter(cec_adap, dev);
if (ret < 0)
goto err_delete_adapter;
mutex_unlock(&connector->cec.mutex);
return drmm_add_action_or_reset(connector->dev,
drm_connector_hdmi_cec_adapter_unregister,
connector);
err_delete_adapter:
cec_delete_adapter(cec_adap);
connector->cec.data = NULL;
mutex_unlock(&connector->cec.mutex);
err_free:
kfree(data);
return ret;
}
EXPORT_SYMBOL(drmm_connector_hdmi_cec_register);
void drm_connector_hdmi_cec_received_msg(struct drm_connector *connector,
struct cec_msg *msg)
{
struct drm_connector_hdmi_cec_data *data = connector->cec.data;
cec_received_msg(data->adapter, msg);
}
EXPORT_SYMBOL(drm_connector_hdmi_cec_received_msg);
void drm_connector_hdmi_cec_transmit_attempt_done(struct drm_connector *connector,
u8 status)
{
struct drm_connector_hdmi_cec_data *data = connector->cec.data;
cec_transmit_attempt_done(data->adapter, status);
}
EXPORT_SYMBOL(drm_connector_hdmi_cec_transmit_attempt_done);
void drm_connector_hdmi_cec_transmit_done(struct drm_connector *connector,
u8 status,
u8 arb_lost_cnt, u8 nack_cnt,
u8 low_drive_cnt, u8 error_cnt)
{
struct drm_connector_hdmi_cec_data *data = connector->cec.data;
cec_transmit_done(data->adapter, status,
arb_lost_cnt, nack_cnt, low_drive_cnt, error_cnt);
}
EXPORT_SYMBOL(drm_connector_hdmi_cec_transmit_done);
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: MIT
/*
* Copyright (c) 2024 Linaro Ltd
*/
#include <drm/drm_bridge.h>
#include <drm/drm_connector.h>
#include <drm/drm_managed.h>
#include <drm/display/drm_hdmi_cec_helper.h>
#include <linux/export.h>
#include <linux/mutex.h>
#include <media/cec.h>
#include <media/cec-notifier.h>
static void drm_connector_hdmi_cec_notifier_phys_addr_invalidate(struct drm_connector *connector)
{
cec_notifier_phys_addr_invalidate(connector->cec.data);
}
static void drm_connector_hdmi_cec_notifier_phys_addr_set(struct drm_connector *connector,
u16 addr)
{
cec_notifier_set_phys_addr(connector->cec.data, addr);
}
static void drm_connector_hdmi_cec_notifier_unregister(struct drm_device *dev, void *res)
{
struct drm_connector *connector = res;
cec_notifier_conn_unregister(connector->cec.data);
connector->cec.data = NULL;
}
static const struct drm_connector_cec_funcs drm_connector_cec_notifier_funcs = {
.phys_addr_invalidate = drm_connector_hdmi_cec_notifier_phys_addr_invalidate,
.phys_addr_set = drm_connector_hdmi_cec_notifier_phys_addr_set,
};
int drmm_connector_hdmi_cec_notifier_register(struct drm_connector *connector,
const char *port_name,
struct device *dev)
{
struct cec_connector_info conn_info;
struct cec_notifier *notifier;
cec_fill_conn_info_from_drm(&conn_info, connector);
notifier = cec_notifier_conn_register(dev, port_name, &conn_info);
if (!notifier)
return -ENOMEM;
mutex_lock(&connector->cec.mutex);
connector->cec.data = notifier;
connector->cec.funcs = &drm_connector_cec_notifier_funcs;
mutex_unlock(&connector->cec.mutex);
return drmm_add_action_or_reset(connector->dev,
drm_connector_hdmi_cec_notifier_unregister,
connector);
}
EXPORT_SYMBOL(drmm_connector_hdmi_cec_notifier_register);
@@ -0,0 +1,428 @@
// SPDX-License-Identifier: MIT
#include <linux/export.h>
#include <linux/module.h>
#include <drm/display/drm_hdmi_helper.h>
#include <drm/drm_connector.h>
#include <drm/drm_edid.h>
#include <drm/drm_modes.h>
#include <drm/drm_print.h>
#include <drm/drm_property.h>
static inline bool is_eotf_supported(u8 output_eotf, u8 sink_eotf)
{
return sink_eotf & BIT(output_eotf);
}
/**
* drm_hdmi_infoframe_set_hdr_metadata() - fill an HDMI DRM infoframe with
* HDR metadata from userspace
* @frame: HDMI DRM infoframe
* @conn_state: Connector state containing HDR metadata
*
* Return: 0 on success or a negative error code on failure.
*/
int drm_hdmi_infoframe_set_hdr_metadata(struct hdmi_drm_infoframe *frame,
const struct drm_connector_state *conn_state)
{
struct drm_connector *connector;
struct hdr_output_metadata *hdr_metadata;
int err;
if (!frame || !conn_state)
return -EINVAL;
connector = conn_state->connector;
if (!conn_state->hdr_output_metadata)
return -EINVAL;
hdr_metadata = conn_state->hdr_output_metadata->data;
if (!hdr_metadata || !connector)
return -EINVAL;
/* Sink EOTF is Bit map while infoframe is absolute values */
if (!is_eotf_supported(hdr_metadata->hdmi_metadata_type1.eotf,
connector->display_info.hdr_sink_metadata.hdmi_type1.eotf))
DRM_DEBUG_KMS("Unknown EOTF %d\n", hdr_metadata->hdmi_metadata_type1.eotf);
err = hdmi_drm_infoframe_init(frame);
if (err < 0)
return err;
frame->eotf = hdr_metadata->hdmi_metadata_type1.eotf;
frame->metadata_type = hdr_metadata->hdmi_metadata_type1.metadata_type;
BUILD_BUG_ON(sizeof(frame->display_primaries) !=
sizeof(hdr_metadata->hdmi_metadata_type1.display_primaries));
BUILD_BUG_ON(sizeof(frame->white_point) !=
sizeof(hdr_metadata->hdmi_metadata_type1.white_point));
memcpy(&frame->display_primaries,
&hdr_metadata->hdmi_metadata_type1.display_primaries,
sizeof(frame->display_primaries));
memcpy(&frame->white_point,
&hdr_metadata->hdmi_metadata_type1.white_point,
sizeof(frame->white_point));
frame->max_display_mastering_luminance =
hdr_metadata->hdmi_metadata_type1.max_display_mastering_luminance;
frame->min_display_mastering_luminance =
hdr_metadata->hdmi_metadata_type1.min_display_mastering_luminance;
frame->max_fall = hdr_metadata->hdmi_metadata_type1.max_fall;
frame->max_cll = hdr_metadata->hdmi_metadata_type1.max_cll;
return 0;
}
EXPORT_SYMBOL(drm_hdmi_infoframe_set_hdr_metadata);
/* HDMI Colorspace Spec Definitions */
#define FULL_COLORIMETRY_MASK 0x1FF
#define NORMAL_COLORIMETRY_MASK 0x3
#define EXTENDED_COLORIMETRY_MASK 0x7
#define EXTENDED_ACE_COLORIMETRY_MASK 0xF
#define C(x) ((x) << 0)
#define EC(x) ((x) << 2)
#define ACE(x) ((x) << 5)
#define HDMI_COLORIMETRY_NO_DATA 0x0
#define HDMI_COLORIMETRY_SMPTE_170M_YCC (C(1) | EC(0) | ACE(0))
#define HDMI_COLORIMETRY_BT709_YCC (C(2) | EC(0) | ACE(0))
#define HDMI_COLORIMETRY_XVYCC_601 (C(3) | EC(0) | ACE(0))
#define HDMI_COLORIMETRY_XVYCC_709 (C(3) | EC(1) | ACE(0))
#define HDMI_COLORIMETRY_SYCC_601 (C(3) | EC(2) | ACE(0))
#define HDMI_COLORIMETRY_OPYCC_601 (C(3) | EC(3) | ACE(0))
#define HDMI_COLORIMETRY_OPRGB (C(3) | EC(4) | ACE(0))
#define HDMI_COLORIMETRY_BT2020_CYCC (C(3) | EC(5) | ACE(0))
#define HDMI_COLORIMETRY_BT2020_RGB (C(3) | EC(6) | ACE(0))
#define HDMI_COLORIMETRY_BT2020_YCC (C(3) | EC(6) | ACE(0))
#define HDMI_COLORIMETRY_DCI_P3_RGB_D65 (C(3) | EC(7) | ACE(0))
#define HDMI_COLORIMETRY_DCI_P3_RGB_THEATER (C(3) | EC(7) | ACE(1))
static const u32 hdmi_colorimetry_val[] = {
[DRM_MODE_COLORIMETRY_NO_DATA] = HDMI_COLORIMETRY_NO_DATA,
[DRM_MODE_COLORIMETRY_SMPTE_170M_YCC] = HDMI_COLORIMETRY_SMPTE_170M_YCC,
[DRM_MODE_COLORIMETRY_BT709_YCC] = HDMI_COLORIMETRY_BT709_YCC,
[DRM_MODE_COLORIMETRY_XVYCC_601] = HDMI_COLORIMETRY_XVYCC_601,
[DRM_MODE_COLORIMETRY_XVYCC_709] = HDMI_COLORIMETRY_XVYCC_709,
[DRM_MODE_COLORIMETRY_SYCC_601] = HDMI_COLORIMETRY_SYCC_601,
[DRM_MODE_COLORIMETRY_OPYCC_601] = HDMI_COLORIMETRY_OPYCC_601,
[DRM_MODE_COLORIMETRY_OPRGB] = HDMI_COLORIMETRY_OPRGB,
[DRM_MODE_COLORIMETRY_BT2020_CYCC] = HDMI_COLORIMETRY_BT2020_CYCC,
[DRM_MODE_COLORIMETRY_BT2020_RGB] = HDMI_COLORIMETRY_BT2020_RGB,
[DRM_MODE_COLORIMETRY_BT2020_YCC] = HDMI_COLORIMETRY_BT2020_YCC,
};
#undef C
#undef EC
#undef ACE
/**
* drm_hdmi_avi_infoframe_colorimetry() - fill the HDMI AVI infoframe
* colorimetry information
* @frame: HDMI AVI infoframe
* @conn_state: connector state
*/
void drm_hdmi_avi_infoframe_colorimetry(struct hdmi_avi_infoframe *frame,
const struct drm_connector_state *conn_state)
{
u32 colorimetry_val;
u32 colorimetry_index = conn_state->colorspace & FULL_COLORIMETRY_MASK;
if (colorimetry_index >= ARRAY_SIZE(hdmi_colorimetry_val))
colorimetry_val = HDMI_COLORIMETRY_NO_DATA;
else
colorimetry_val = hdmi_colorimetry_val[colorimetry_index];
frame->colorimetry = colorimetry_val & NORMAL_COLORIMETRY_MASK;
/*
* ToDo: Extend it for ACE formats as well. Modify the infoframe
* structure and extend it in drivers/video/hdmi
*/
frame->extended_colorimetry = (colorimetry_val >> 2) &
EXTENDED_COLORIMETRY_MASK;
}
EXPORT_SYMBOL(drm_hdmi_avi_infoframe_colorimetry);
/**
* drm_hdmi_avi_infoframe_bars() - fill the HDMI AVI infoframe
* bar information
* @frame: HDMI AVI infoframe
* @conn_state: connector state
*/
void drm_hdmi_avi_infoframe_bars(struct hdmi_avi_infoframe *frame,
const struct drm_connector_state *conn_state)
{
frame->right_bar = conn_state->tv.margins.right;
frame->left_bar = conn_state->tv.margins.left;
frame->top_bar = conn_state->tv.margins.top;
frame->bottom_bar = conn_state->tv.margins.bottom;
}
EXPORT_SYMBOL(drm_hdmi_avi_infoframe_bars);
/**
* drm_hdmi_avi_infoframe_content_type() - fill the HDMI AVI infoframe
* content type information, based
* on correspondent DRM property.
* @frame: HDMI AVI infoframe
* @conn_state: DRM display connector state
*
*/
void drm_hdmi_avi_infoframe_content_type(struct hdmi_avi_infoframe *frame,
const struct drm_connector_state *conn_state)
{
switch (conn_state->content_type) {
case DRM_MODE_CONTENT_TYPE_GRAPHICS:
frame->content_type = HDMI_CONTENT_TYPE_GRAPHICS;
break;
case DRM_MODE_CONTENT_TYPE_CINEMA:
frame->content_type = HDMI_CONTENT_TYPE_CINEMA;
break;
case DRM_MODE_CONTENT_TYPE_GAME:
frame->content_type = HDMI_CONTENT_TYPE_GAME;
break;
case DRM_MODE_CONTENT_TYPE_PHOTO:
frame->content_type = HDMI_CONTENT_TYPE_PHOTO;
break;
default:
/* Graphics is the default(0) */
frame->content_type = HDMI_CONTENT_TYPE_GRAPHICS;
}
frame->itc = conn_state->content_type != DRM_MODE_CONTENT_TYPE_NO_DATA;
}
EXPORT_SYMBOL(drm_hdmi_avi_infoframe_content_type);
/**
* drm_hdmi_compute_mode_clock() - Computes the TMDS Character Rate
* @mode: Display mode to compute the clock for
* @bpc: Bits per character
* @fmt: Output Pixel Format used
*
* Returns the TMDS Character Rate for a given mode, bpc count and output format.
*
* RETURNS:
* The TMDS Character Rate, in Hertz, or 0 on error.
*/
unsigned long long
drm_hdmi_compute_mode_clock(const struct drm_display_mode *mode,
unsigned int bpc,
enum drm_output_color_format fmt)
{
unsigned long long clock = mode->clock * 1000ULL;
unsigned int vic = drm_match_cea_mode(mode);
/*
* CTA-861-G Spec, section 5.4 - Color Coding and Quantization
* mandates that VIC 1 always uses 8 bpc.
*/
if (vic == 1 && bpc != 8)
return 0;
if (fmt == DRM_OUTPUT_COLOR_FORMAT_YCBCR422) {
/*
* HDMI 1.0 Spec, section 6.5 - Pixel Encoding states that
* YUV422 sends 24 bits over three channels, with Cb and Cr
* components being sent on odd and even pixels, respectively.
*
* If fewer than 12 bpc are sent, data are left justified.
*/
if (bpc > 12)
return 0;
/*
* HDMI 1.0 Spec, section 6.5 - Pixel Encoding
* specifies that YUV422 sends two 12-bits components over
* three TMDS channels per pixel clock, which is equivalent to
* three 8-bits components over three channels used by RGB as
* far as the clock rate goes.
*/
bpc = 8;
}
/*
* HDMI 2.0 Spec, Section 7.1 - YCbCr 4:2:0 Pixel Encoding
* specifies that YUV420 encoding is carried at a TMDS Character Rate
* equal to half the pixel clock rate.
*/
if (fmt == DRM_OUTPUT_COLOR_FORMAT_YCBCR420)
clock = clock / 2;
if (mode->flags & DRM_MODE_FLAG_DBLCLK)
clock = clock * 2;
return DIV_ROUND_CLOSEST_ULL(clock * bpc, 8);
}
EXPORT_SYMBOL(drm_hdmi_compute_mode_clock);
struct drm_hdmi_acr_n_cts_entry {
unsigned int n;
unsigned int cts;
};
struct drm_hdmi_acr_data {
unsigned long tmds_clock_khz;
struct drm_hdmi_acr_n_cts_entry n_cts_32k,
n_cts_44k1,
n_cts_48k;
};
static const struct drm_hdmi_acr_data hdmi_acr_n_cts[] = {
{
/* "Other" entry */
.n_cts_32k = { .n = 4096, },
.n_cts_44k1 = { .n = 6272, },
.n_cts_48k = { .n = 6144, },
}, {
.tmds_clock_khz = 25175,
.n_cts_32k = { .n = 4576, .cts = 28125, },
.n_cts_44k1 = { .n = 7007, .cts = 31250, },
.n_cts_48k = { .n = 6864, .cts = 28125, },
}, {
.tmds_clock_khz = 25200,
.n_cts_32k = { .n = 4096, .cts = 25200, },
.n_cts_44k1 = { .n = 6272, .cts = 28000, },
.n_cts_48k = { .n = 6144, .cts = 25200, },
}, {
.tmds_clock_khz = 27000,
.n_cts_32k = { .n = 4096, .cts = 27000, },
.n_cts_44k1 = { .n = 6272, .cts = 30000, },
.n_cts_48k = { .n = 6144, .cts = 27000, },
}, {
.tmds_clock_khz = 27027,
.n_cts_32k = { .n = 4096, .cts = 27027, },
.n_cts_44k1 = { .n = 6272, .cts = 30030, },
.n_cts_48k = { .n = 6144, .cts = 27027, },
}, {
.tmds_clock_khz = 54000,
.n_cts_32k = { .n = 4096, .cts = 54000, },
.n_cts_44k1 = { .n = 6272, .cts = 60000, },
.n_cts_48k = { .n = 6144, .cts = 54000, },
}, {
.tmds_clock_khz = 54054,
.n_cts_32k = { .n = 4096, .cts = 54054, },
.n_cts_44k1 = { .n = 6272, .cts = 60060, },
.n_cts_48k = { .n = 6144, .cts = 54054, },
}, {
.tmds_clock_khz = 74176,
.n_cts_32k = { .n = 11648, .cts = 210937, }, /* and 210938 */
.n_cts_44k1 = { .n = 17836, .cts = 234375, },
.n_cts_48k = { .n = 11648, .cts = 140625, },
}, {
.tmds_clock_khz = 74250,
.n_cts_32k = { .n = 4096, .cts = 74250, },
.n_cts_44k1 = { .n = 6272, .cts = 82500, },
.n_cts_48k = { .n = 6144, .cts = 74250, },
}, {
.tmds_clock_khz = 148352,
.n_cts_32k = { .n = 11648, .cts = 421875, },
.n_cts_44k1 = { .n = 8918, .cts = 234375, },
.n_cts_48k = { .n = 5824, .cts = 140625, },
}, {
.tmds_clock_khz = 148500,
.n_cts_32k = { .n = 4096, .cts = 148500, },
.n_cts_44k1 = { .n = 6272, .cts = 165000, },
.n_cts_48k = { .n = 6144, .cts = 148500, },
}, {
.tmds_clock_khz = 296703,
.n_cts_32k = { .n = 5824, .cts = 421875, },
.n_cts_44k1 = { .n = 4459, .cts = 234375, },
.n_cts_48k = { .n = 5824, .cts = 281250, },
}, {
.tmds_clock_khz = 297000,
.n_cts_32k = { .n = 3072, .cts = 222750, },
.n_cts_44k1 = { .n = 4704, .cts = 247500, },
.n_cts_48k = { .n = 5120, .cts = 247500, },
}, {
.tmds_clock_khz = 593407,
.n_cts_32k = { .n = 5824, .cts = 843750, },
.n_cts_44k1 = { .n = 8918, .cts = 937500, },
.n_cts_48k = { .n = 5824, .cts = 562500, },
}, {
.tmds_clock_khz = 594000,
.n_cts_32k = { .n = 3072, .cts = 445500, },
.n_cts_44k1 = { .n = 9408, .cts = 990000, },
.n_cts_48k = { .n = 6144, .cts = 594000, },
},
};
static int drm_hdmi_acr_find_tmds_entry(unsigned long tmds_clock_khz)
{
int i;
/* skip the "other" entry */
for (i = 1; i < ARRAY_SIZE(hdmi_acr_n_cts); i++) {
if (hdmi_acr_n_cts[i].tmds_clock_khz == tmds_clock_khz)
return i;
}
return 0;
}
/**
* drm_hdmi_acr_get_n_cts() - get N and CTS values for Audio Clock Regeneration
*
* @tmds_char_rate: TMDS clock (char rate) as used by the HDMI connector
* @sample_rate: audio sample rate
* @out_n: a pointer to write the N value
* @out_cts: a pointer to write the CTS value
*
* Get the N and CTS values (either by calculating them or by returning data
* from the tables. This follows the HDMI 1.4b Section 7.2 "Audio Sample Clock
* Capture and Regeneration".
*
* Note, @sample_rate corresponds to the Fs value, see sections 7.2.4 - 7.2.6
* on how to select Fs for non-L-PCM formats.
*/
void
drm_hdmi_acr_get_n_cts(unsigned long long tmds_char_rate,
unsigned int sample_rate,
unsigned int *out_n,
unsigned int *out_cts)
{
/* be a bit more tolerant, especially for the 1.001 entries */
unsigned long tmds_clock_khz = DIV_ROUND_CLOSEST_ULL(tmds_char_rate, 1000);
const struct drm_hdmi_acr_n_cts_entry *entry;
unsigned int n, cts, mult;
int tmds_idx;
tmds_idx = drm_hdmi_acr_find_tmds_entry(tmds_clock_khz);
/*
* Don't change the order, 192 kHz is divisible by 48k and 32k, but it
* should use 48k entry.
*/
if (sample_rate % 48000 == 0) {
entry = &hdmi_acr_n_cts[tmds_idx].n_cts_48k;
mult = sample_rate / 48000;
} else if (sample_rate % 44100 == 0) {
entry = &hdmi_acr_n_cts[tmds_idx].n_cts_44k1;
mult = sample_rate / 44100;
} else if (sample_rate % 32000 == 0) {
entry = &hdmi_acr_n_cts[tmds_idx].n_cts_32k;
mult = sample_rate / 32000;
} else {
entry = NULL;
}
if (entry) {
n = entry->n * mult;
cts = entry->cts;
} else {
/* Recommended optimal value, HDMI 1.4b, Section 7.2.1 */
n = 128 * sample_rate / 1000;
cts = 0;
}
if (!cts)
cts = DIV_ROUND_CLOSEST_ULL(tmds_char_rate * n,
128 * sample_rate);
*out_n = n;
*out_cts = cts;
}
EXPORT_SYMBOL(drm_hdmi_acr_get_n_cts);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,278 @@
/*
* Copyright (c) 2015 NVIDIA Corporation. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sub license,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the
* next paragraph) shall be included in all copies or substantial portions
* of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
#include <linux/export.h>
#include <linux/i2c.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <drm/display/drm_scdc_helper.h>
#include <drm/drm_connector.h>
#include <drm/drm_device.h>
#include <drm/drm_print.h>
/**
* DOC: scdc helpers
*
* Status and Control Data Channel (SCDC) is a mechanism introduced by the
* HDMI 2.0 specification. It is a point-to-point protocol that allows the
* HDMI source and HDMI sink to exchange data. The same I2C interface that
* is used to access EDID serves as the transport mechanism for SCDC.
*
* Note: The SCDC status is going to be lost when the display is
* disconnected. This can happen physically when the user disconnects
* the cable, but also when a display is switched on (such as waking up
* a TV).
*
* This is further complicated by the fact that, upon a disconnection /
* reconnection, KMS won't change the mode on its own. This means that
* one can't just rely on setting the SCDC status on enable, but also
* has to track the connector status changes using interrupts and
* restore the SCDC status. The typical solution for this is to trigger an
* empty modeset in drm_connector_helper_funcs.detect_ctx(), like what vc4 does
* in vc4_hdmi_reset_link().
*/
#define SCDC_I2C_SLAVE_ADDRESS 0x54
/**
* drm_scdc_read - read a block of data from SCDC
* @adapter: I2C controller
* @offset: start offset of block to read
* @buffer: return location for the block to read
* @size: size of the block to read
*
* Reads a block of data from SCDC, starting at a given offset.
*
* Returns:
* 0 on success, negative error code on failure.
*/
ssize_t drm_scdc_read(struct i2c_adapter *adapter, u8 offset, void *buffer,
size_t size)
{
int ret;
struct i2c_msg msgs[2] = {
{
.addr = SCDC_I2C_SLAVE_ADDRESS,
.flags = 0,
.len = 1,
.buf = &offset,
}, {
.addr = SCDC_I2C_SLAVE_ADDRESS,
.flags = I2C_M_RD,
.len = size,
.buf = buffer,
}
};
ret = i2c_transfer(adapter, msgs, ARRAY_SIZE(msgs));
if (ret < 0)
return ret;
if (ret != ARRAY_SIZE(msgs))
return -EPROTO;
return 0;
}
EXPORT_SYMBOL(drm_scdc_read);
/**
* drm_scdc_write - write a block of data to SCDC
* @adapter: I2C controller
* @offset: start offset of block to write
* @buffer: block of data to write
* @size: size of the block to write
*
* Writes a block of data to SCDC, starting at a given offset.
*
* Returns:
* 0 on success, negative error code on failure.
*/
ssize_t drm_scdc_write(struct i2c_adapter *adapter, u8 offset,
const void *buffer, size_t size)
{
struct i2c_msg msg = {
.addr = SCDC_I2C_SLAVE_ADDRESS,
.flags = 0,
.len = 1 + size,
.buf = NULL,
};
void *data;
int err;
data = kmalloc(1 + size, GFP_KERNEL);
if (!data)
return -ENOMEM;
msg.buf = data;
memcpy(data, &offset, sizeof(offset));
memcpy(data + 1, buffer, size);
err = i2c_transfer(adapter, &msg, 1);
kfree(data);
if (err < 0)
return err;
if (err != 1)
return -EPROTO;
return 0;
}
EXPORT_SYMBOL(drm_scdc_write);
/**
* drm_scdc_get_scrambling_status - what is status of scrambling?
* @connector: connector
*
* Reads the scrambler status over SCDC, and checks the
* scrambling status.
*
* Returns:
* True if the scrambling is enabled, false otherwise.
*/
bool drm_scdc_get_scrambling_status(struct drm_connector *connector)
{
u8 status;
int ret;
ret = drm_scdc_readb(connector->ddc, SCDC_SCRAMBLER_STATUS, &status);
if (ret < 0) {
drm_dbg_kms(connector->dev,
"[CONNECTOR:%d:%s] Failed to read scrambling status: %d\n",
connector->base.id, connector->name, ret);
return false;
}
return status & SCDC_SCRAMBLING_STATUS;
}
EXPORT_SYMBOL(drm_scdc_get_scrambling_status);
/**
* drm_scdc_set_scrambling - enable scrambling
* @connector: connector
* @enable: bool to indicate if scrambling is to be enabled/disabled
*
* Writes the TMDS config register over SCDC channel, and:
* enables scrambling when enable = 1
* disables scrambling when enable = 0
*
* Returns:
* True if scrambling is set/reset successfully, false otherwise.
*/
bool drm_scdc_set_scrambling(struct drm_connector *connector,
bool enable)
{
u8 config;
int ret;
ret = drm_scdc_readb(connector->ddc, SCDC_TMDS_CONFIG, &config);
if (ret < 0) {
drm_dbg_kms(connector->dev,
"[CONNECTOR:%d:%s] Failed to read TMDS config: %d\n",
connector->base.id, connector->name, ret);
return false;
}
if (enable)
config |= SCDC_SCRAMBLING_ENABLE;
else
config &= ~SCDC_SCRAMBLING_ENABLE;
ret = drm_scdc_writeb(connector->ddc, SCDC_TMDS_CONFIG, config);
if (ret < 0) {
drm_dbg_kms(connector->dev,
"[CONNECTOR:%d:%s] Failed to enable scrambling: %d\n",
connector->base.id, connector->name, ret);
return false;
}
return true;
}
EXPORT_SYMBOL(drm_scdc_set_scrambling);
/**
* drm_scdc_set_high_tmds_clock_ratio - set TMDS clock ratio
* @connector: connector
* @set: ret or reset the high clock ratio
*
*
* TMDS clock ratio calculations go like this:
* TMDS character = 10 bit TMDS encoded value
*
* TMDS character rate = The rate at which TMDS characters are
* transmitted (Mcsc)
*
* TMDS bit rate = 10x TMDS character rate
*
* As per the spec:
* TMDS clock rate for pixel clock < 340 MHz = 1x the character
* rate = 1/10 pixel clock rate
*
* TMDS clock rate for pixel clock > 340 MHz = 0.25x the character
* rate = 1/40 pixel clock rate
*
* Writes to the TMDS config register over SCDC channel, and:
* sets TMDS clock ratio to 1/40 when set = 1
*
* sets TMDS clock ratio to 1/10 when set = 0
*
* Returns:
* True if write is successful, false otherwise.
*/
bool drm_scdc_set_high_tmds_clock_ratio(struct drm_connector *connector,
bool set)
{
u8 config;
int ret;
ret = drm_scdc_readb(connector->ddc, SCDC_TMDS_CONFIG, &config);
if (ret < 0) {
drm_dbg_kms(connector->dev,
"[CONNECTOR:%d:%s] Failed to read TMDS config: %d\n",
connector->base.id, connector->name, ret);
return false;
}
if (set)
config |= SCDC_TMDS_BIT_CLOCK_RATIO_BY_40;
else
config &= ~SCDC_TMDS_BIT_CLOCK_RATIO_BY_40;
ret = drm_scdc_writeb(connector->ddc, SCDC_TMDS_CONFIG, config);
if (ret < 0) {
drm_dbg_kms(connector->dev,
"[CONNECTOR:%d:%s] Failed to set TMDS clock ratio: %d\n",
connector->base.id, connector->name, ret);
return false;
}
/*
* The spec says that a source should wait minimum 1ms and maximum
* 100ms after writing the TMDS config for clock ratio. Lets allow a
* wait of up to 2ms here.
*/
usleep_range(1000, 2000);
return true;
}
EXPORT_SYMBOL(drm_scdc_set_high_tmds_clock_ratio);