| /* | |
| * 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/errno.h> | |
| #include <linux/export.h> | |
| #include <linux/i2c.h> | |
| #include <linux/slab.h> | |
| #include <linux/string.h> | |
| #include <drm/drm_dp_dual_mode_helper.h> | |
| #include <drm/drmP.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: sizo 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) | |
| { | |
| struct i2c_msg msgs[] = { | |
| { | |
| .addr = DP_DUAL_MODE_SLAVE_ADDRESS, | |
| .flags = 0, | |
| .len = 1, | |
| .buf = &offset, | |
| }, | |
| { | |
| .addr = DP_DUAL_MODE_SLAVE_ADDRESS, | |
| .flags = I2C_M_RD, | |
| .len = size, | |
| .buf = buffer, | |
| }, | |
| }; | |
| int ret; | |
| 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_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: sizo 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] = | |
| "DP-HDMI ADAPTOR\x04"; | |
| return memcmp(hdmi_id, dp_dual_mode_hdmi_id, | |
| sizeof(dp_dual_mode_hdmi_id)) == 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 | |
| * @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(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_DEBUG_KMS("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; | |
| /* | |
| * Sigh. Some (maybe all?) type 1 adaptors are broken and ack | |
| * the offset but ignore it, and instead they just always return | |
| * data from the start of the HDMI ID buffer. So for a broken | |
| * type 1 HDMI adaptor a single byte read will always give us | |
| * 0x44, and for a type 1 DVI adaptor it should give 0x00 | |
| * (assuming it implements any registers). Fortunately neither | |
| * of those values will match the type 2 signature of the | |
| * DP_DUAL_MODE_ADAPTOR_ID register so we can proceed with | |
| * the type 2 adaptor detection safely even in the presence | |
| * of broken type 1 adaptors. | |
| */ | |
| ret = drm_dp_dual_mode_read(adapter, DP_DUAL_MODE_ADAPTOR_ID, | |
| &adaptor_id, sizeof(adaptor_id)); | |
| DRM_DEBUG_KMS("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 neither a proper type 1 ID nor a broken type 1 adaptor | |
| * as described above, assume type 1, but let the user know | |
| * that we may have misdetected the type. | |
| */ | |
| if (!is_type1_adaptor(adaptor_id) && adaptor_id != hdmi_id[0]) | |
| DRM_ERROR("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 | |
| * @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(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_DEBUG_KMS("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 | |
| * @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(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_DEBUG_KMS("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 | |
| * @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. As | |
| * some type 1 adaptors have problems with registers (see comments | |
| * in drm_dp_dual_mode_detect()) we avoid touching the register, | |
| * making this function a no-op on type 1 adaptors. | |
| * | |
| * Returns: | |
| * 0 on success, negative error code on failure | |
| */ | |
| int drm_dp_dual_mode_set_tmds_output(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; | |
| if (type < DRM_DP_DUAL_MODE_TYPE2_DVI) | |
| return 0; | |
| ret = drm_dp_dual_mode_write(adapter, DP_DUAL_MODE_TMDS_OEN, | |
| &tmds_oen, sizeof(tmds_oen)); | |
| if (ret) { | |
| DRM_DEBUG_KMS("Failed to %s TMDS output buffers\n", | |
| enable ? "enable" : "disable"); | |
| return ret; | |
| } | |
| return 0; | |
| } | |
| 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) | |
| * @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(struct i2c_adapter *adapter, | |
| enum drm_lspcon_mode *mode) | |
| { | |
| u8 data; | |
| int ret = 0; | |
| if (!mode) { | |
| DRM_ERROR("NULL input\n"); | |
| return -EINVAL; | |
| } | |
| /* Read Status: i2c over aux */ | |
| ret = drm_dp_dual_mode_read(adapter, DP_DUAL_MODE_LSPCON_CURRENT_MODE, | |
| &data, sizeof(data)); | |
| if (ret < 0) { | |
| DRM_ERROR("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) | |
| * @adapter: I2C-over-aux adapter | |
| * @mode: required mode of operation | |
| * | |
| * Returns: | |
| * 0 on success, -error on failure/timeout | |
| */ | |
| int drm_lspcon_set_mode(struct i2c_adapter *adapter, | |
| enum drm_lspcon_mode mode) | |
| { | |
| u8 data = 0; | |
| int ret; | |
| int time_out = 200; | |
| 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_ERROR("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(adapter, ¤t_mode); | |
| if (ret) { | |
| DRM_ERROR("can't confirm LSPCON mode change\n"); | |
| return ret; | |
| } else { | |
| if (current_mode != mode) { | |
| msleep(10); | |
| time_out -= 10; | |
| } else { | |
| DRM_DEBUG_KMS("LSPCON mode changed to %s\n", | |
| mode == DRM_LSPCON_MODE_LS ? | |
| "LS" : "PCON"); | |
| return 0; | |
| } | |
| } | |
| } while (time_out); | |
| DRM_ERROR("LSPCON mode change timed out\n"); | |
| return -ETIMEDOUT; | |
| } | |
| EXPORT_SYMBOL(drm_lspcon_set_mode); |