| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright 2021-2024 Cisco Systems, Inc. and/or its affiliates. All rights reserved. |
| */ |
| |
| /* |
| * Currently this driver does not fully support the serial port of the |
| * Extron, only the USB port is fully supported. |
| * |
| * Issues specific to using the serial port instead of the USB since the |
| * serial port doesn't detect if the device is powered off: |
| * |
| * - Some periodic ping mechanism is needed to detect when the Extron is |
| * powered off and when it is powered on again. |
| * - What to do when it is powered off and the driver is modprobed? Keep |
| * trying to contact the Extron indefinitely? |
| */ |
| |
| #include <linux/completion.h> |
| #include <linux/ctype.h> |
| #include <linux/delay.h> |
| #include <linux/init.h> |
| #include <linux/interrupt.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/slab.h> |
| #include <linux/time.h> |
| |
| #include "extron-da-hd-4k-plus.h" |
| |
| MODULE_AUTHOR("Hans Verkuil <hansverk@cisco.com>"); |
| MODULE_DESCRIPTION("Extron DA HD 4K PLUS HDMI CEC driver"); |
| MODULE_LICENSE("GPL"); |
| |
| static int debug; |
| module_param(debug, int, 0644); |
| MODULE_PARM_DESC(debug, "debug level (0-1)"); |
| |
| static unsigned int vendor_id; |
| module_param(vendor_id, uint, 0444); |
| MODULE_PARM_DESC(vendor_id, "CEC Vendor ID"); |
| |
| static char manufacturer_name[4]; |
| module_param_string(manufacturer_name, manufacturer_name, |
| sizeof(manufacturer_name), 0644); |
| MODULE_PARM_DESC(manufacturer_name, |
| "EDID Vendor String (3 uppercase characters)"); |
| |
| static bool hpd_never_low; |
| module_param(hpd_never_low, bool, 0644); |
| MODULE_PARM_DESC(hpd_never_low, "Input HPD will never go low (1), or go low if all output HPDs are low (0, default)"); |
| |
| #define EXTRON_TIMEOUT_SECS 6 |
| |
| static const u8 hdmi_edid[256] = { |
| 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, |
| 0x31, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x01, 0x20, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78, |
| 0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26, |
| 0x0f, 0x50, 0x54, 0x20, 0x00, 0x00, 0x01, 0x01, |
| 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, |
| 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x3a, |
| 0x80, 0x18, 0x71, 0x38, 0x2d, 0x40, 0x58, 0x2c, |
| 0x45, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e, |
| 0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x55, 0x18, |
| 0x87, 0x11, 0x00, 0x0a, 0x20, 0x20, 0x20, 0x20, |
| 0x20, 0x20, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x68, |
| 0x64, 0x6d, 0x69, 0x2d, 0x31, 0x30, 0x38, 0x30, |
| 0x70, 0x36, 0x30, 0x0a, 0x00, 0x00, 0x00, 0xfe, |
| 0x00, 0x73, 0x65, 0x72, 0x69, 0x6f, 0x0a, 0x20, |
| 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x95, |
| |
| 0x02, 0x03, 0x1b, 0xf1, 0x42, 0x10, 0x01, 0x23, |
| 0x09, 0x07, 0x07, 0x83, 0x01, 0x00, 0x00, 0x68, |
| 0x03, 0x0c, 0x00, 0x10, 0x00, 0x00, 0x21, 0x01, |
| 0xe2, 0x00, 0xca, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x89, |
| }; |
| |
| static const u8 hdmi_edid_4k_300[256] = { |
| 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, |
| 0x31, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x01, 0x20, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78, |
| 0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26, |
| 0x0f, 0x50, 0x54, 0x20, 0x00, 0x00, 0x01, 0x01, |
| 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, |
| 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x3a, |
| 0x80, 0x18, 0x71, 0x38, 0x2d, 0x40, 0x58, 0x2c, |
| 0x45, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e, |
| 0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x55, 0x18, |
| 0x87, 0x3c, 0x00, 0x0a, 0x20, 0x20, 0x20, 0x20, |
| 0x20, 0x20, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x68, |
| 0x64, 0x6d, 0x69, 0x2d, 0x34, 0x6b, 0x2d, 0x36, |
| 0x30, 0x30, 0x0a, 0x20, 0x00, 0x00, 0x00, 0xfe, |
| 0x00, 0x73, 0x65, 0x72, 0x69, 0x6f, 0x0a, 0x20, |
| 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x87, |
| |
| 0x02, 0x03, 0x1f, 0xf1, 0x43, 0x10, 0x5f, 0x01, |
| 0x23, 0x09, 0x07, 0x07, 0x83, 0x01, 0x00, 0x00, |
| 0x6b, 0x03, 0x0c, 0x00, 0x10, 0x00, 0x00, 0x3c, |
| 0x21, 0x00, 0x20, 0x01, 0xe2, 0x00, 0xca, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, |
| }; |
| |
| static const u8 hdmi_edid_4k_600[256] = { |
| 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, |
| 0x31, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x01, 0x20, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78, |
| 0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26, |
| 0x0f, 0x50, 0x54, 0x20, 0x00, 0x00, 0x01, 0x01, |
| 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, |
| 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x08, 0xe8, |
| 0x00, 0x30, 0xf2, 0x70, 0x5a, 0x80, 0xb0, 0x58, |
| 0x8a, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e, |
| 0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x55, 0x18, |
| 0x87, 0x3c, 0x00, 0x0a, 0x20, 0x20, 0x20, 0x20, |
| 0x20, 0x20, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x68, |
| 0x64, 0x6d, 0x69, 0x2d, 0x34, 0x6b, 0x2d, 0x36, |
| 0x30, 0x30, 0x0a, 0x20, 0x00, 0x00, 0x00, 0xfe, |
| 0x00, 0x73, 0x65, 0x72, 0x69, 0x6f, 0x0a, 0x20, |
| 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x4c, |
| |
| 0x02, 0x03, 0x28, 0xf1, 0x44, 0x61, 0x5f, 0x10, |
| 0x01, 0x23, 0x09, 0x07, 0x07, 0x83, 0x01, 0x00, |
| 0x00, 0x6b, 0x03, 0x0c, 0x00, 0x10, 0x00, 0x00, |
| 0x3c, 0x21, 0x00, 0x20, 0x01, 0x67, 0xd8, 0x5d, |
| 0xc4, 0x01, 0x78, 0x00, 0x00, 0xe2, 0x00, 0xca, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, |
| }; |
| |
| static int extron_send_byte(struct serio *serio, char byte) |
| { |
| int err, i; |
| |
| for (i = 0; i < 100; i++) { |
| err = serio_write(serio, byte); |
| if (!err) |
| break; |
| usleep_range(80, 120); |
| } |
| if (err) |
| dev_warn(&serio->dev, "unable to write byte after 100 attempts\n"); |
| return err ? -EIO : 0; |
| } |
| |
| static int extron_send_len(struct serio *serio, const char *command, |
| const unsigned char *bin, unsigned int len) |
| { |
| int err = 0; |
| |
| for (; !err && *command; command++) |
| err = extron_send_byte(serio, *command); |
| if (!err) |
| err = extron_send_byte(serio, '\r'); |
| if (bin) |
| for (; !err && len; len--) |
| err = extron_send_byte(serio, *bin++); |
| return err; |
| } |
| |
| static int extron_send_and_wait_len(struct extron *extron, struct extron_port *port, |
| const char *cmd, const unsigned char *bin, |
| unsigned int len, const char *response) |
| { |
| int timeout = EXTRON_TIMEOUT_SECS * HZ; |
| int err; |
| |
| if (debug) { |
| if (response) |
| dev_info(extron->dev, "transmit %s (response: %s)\n", |
| cmd, response); |
| else |
| dev_info(extron->dev, "transmit %s\n", cmd); |
| } |
| |
| mutex_lock(&extron->serio_lock); |
| if (port) { |
| init_completion(&port->cmd_done); |
| port->cmd_error = 0; |
| port->response = response; |
| } else { |
| init_completion(&extron->cmd_done); |
| extron->cmd_error = 0; |
| extron->response = response; |
| } |
| err = extron_send_len(extron->serio, cmd, bin, len); |
| |
| if (!err && response && |
| !wait_for_completion_timeout(port ? &port->cmd_done : &extron->cmd_done, timeout)) { |
| dev_info(extron->dev, "transmit %s failed with %s (expected: %s)\n", |
| cmd, extron->reply, response); |
| err = -ETIMEDOUT; |
| } |
| |
| if (!err && response && (port ? port->cmd_error : extron->cmd_error)) { |
| dev_info(extron->dev, "transmit %s failed with E%02u (expected: %s)\n", |
| cmd, port ? port->cmd_error : extron->cmd_error, response); |
| if (port) |
| port->cmd_error = 0; |
| else |
| extron->cmd_error = 0; |
| err = -EPROTO; |
| } |
| if (port) |
| port->response = NULL; |
| else |
| extron->response = NULL; |
| mutex_unlock(&extron->serio_lock); |
| return err; |
| } |
| |
| static int extron_send_and_wait(struct extron *extron, struct extron_port *port, |
| const char *cmd, const char *response) |
| { |
| return extron_send_and_wait_len(extron, port, cmd, NULL, 0, response); |
| } |
| |
| static void extron_parse_edid(struct extron_port *port) |
| { |
| const u8 *edid = port->edid; |
| unsigned int i, end; |
| u8 d; |
| |
| port->has_4kp30 = false; |
| port->has_4kp60 = false; |
| port->has_qy = false; |
| port->has_qs = false; |
| /* Store Established Timings 1 and 2 */ |
| port->est_i = edid[0x23]; |
| port->est_ii = edid[0x24]; |
| |
| // Check DTDs in base block |
| for (i = 0; i < 4; i++) { |
| const u8 *dtd = edid + 0x36 + i * 18; |
| unsigned int w, h; |
| unsigned int mhz; |
| u64 pclk; |
| |
| if (!dtd[0] && !dtd[1]) |
| continue; |
| w = dtd[2] + ((dtd[4] & 0xf0) << 4); |
| h = dtd[5] + ((dtd[7] & 0xf0) << 4); |
| if (w != 3840 || h != 2160) |
| continue; |
| |
| w += dtd[3] + ((dtd[4] & 0x0f) << 8); |
| h += dtd[6] + ((dtd[7] & 0x0f) << 8); |
| pclk = dtd[0] + (dtd[1] << 8); |
| pclk *= 100000; |
| mhz = div_u64(pclk, w * h); |
| if (mhz >= 297) |
| port->has_4kp30 = true; |
| if (mhz >= 594) |
| port->has_4kp60 = true; |
| } |
| |
| if (port->edid_blocks == 1) |
| return; |
| |
| edid += 128; |
| |
| /* Return if not a CTA-861 extension block */ |
| if (edid[0] != 0x02 || edid[1] != 0x03) |
| return; |
| |
| /* search Video Data Block (tag 2) */ |
| d = edid[2] & 0x7f; |
| /* Check if there are Data Blocks */ |
| if (d <= 4) |
| return; |
| |
| i = 4; |
| end = d; |
| |
| do { |
| u8 tag = edid[i] >> 5; |
| u8 len = edid[i] & 0x1f; |
| |
| /* Avoid buffer overrun in case the EDID is malformed */ |
| if (i + len + 1 > 0x7f) |
| return; |
| |
| switch (tag) { |
| case 2: /* Video Data Block */ |
| /* Search for VIC 97 */ |
| if (memchr(edid + i + 1, 97, len)) |
| port->has_4kp60 = true; |
| /* Search for VIC 95 */ |
| if (memchr(edid + i + 1, 95, len)) |
| port->has_4kp30 = true; |
| break; |
| |
| case 7: /* Use Extended Tag */ |
| switch (edid[i + 1]) { |
| case 0: /* Video Capability Data Block */ |
| if (edid[i + 2] & 0x80) |
| port->has_qy = true; |
| if (edid[i + 2] & 0x40) |
| port->has_qs = true; |
| break; |
| } |
| break; |
| } |
| i += len + 1; |
| } while (i < end); |
| } |
| |
| static int get_edid_tag_location(const u8 *edid, unsigned int size, |
| u8 want_tag, u8 ext_tag) |
| { |
| unsigned int offset = 128; |
| int i, end; |
| u8 d; |
| |
| edid += offset; |
| |
| /* Return if not a CTA-861 extension block */ |
| if (size < 256 || edid[0] != 0x02 || edid[1] != 0x03) |
| return -ENOENT; |
| |
| /* search tag */ |
| d = edid[0x02] & 0x7f; |
| if (d <= 4) |
| return -ENOENT; |
| |
| i = 0x04; |
| end = 0x00 + d; |
| |
| do { |
| unsigned char tag = edid[i] >> 5; |
| unsigned char len = edid[i] & 0x1f; |
| |
| if (tag != want_tag || i + len > end) { |
| i += len + 1; |
| continue; |
| } |
| |
| if (tag < 7 || (len >= 1 && edid[i + 1] == ext_tag)) |
| return offset + i; |
| i += len + 1; |
| } while (i < end); |
| return -ENOENT; |
| } |
| |
| static void extron_edid_crc(u8 *edid) |
| { |
| u8 sum = 0; |
| int offset; |
| |
| /* Update CRC */ |
| for (offset = 0; offset < 127; offset++) |
| sum += edid[offset]; |
| edid[127] = 256 - sum; |
| } |
| |
| /* |
| * Fill in EDID string. As per VESA EDID-1.3, strings are at most 13 chars |
| * long. If shorter then add a 0x0a character after the string and pad the |
| * remainder with spaces. |
| */ |
| static void extron_set_edid_string(u8 *start, const char *s) |
| { |
| const unsigned int max_len = 13; |
| int len = strlen(s); |
| |
| memset(start, ' ', max_len); |
| if (len > max_len) |
| len = max_len; |
| memcpy(start, s, len); |
| if (len < max_len) |
| start[len] = 0x0a; |
| } |
| |
| static void extron_update_edid(struct extron_port *port, unsigned int blocks) |
| { |
| int offset; |
| u8 c1, c2; |
| |
| c1 = ((manufacturer_name[0] - '@') << 2) | |
| (((manufacturer_name[1] - '@') >> 3) & 0x03); |
| c2 = (((manufacturer_name[1] - '@') & 0x07) << 5) | |
| ((manufacturer_name[2] - '@') & 0x1f); |
| |
| port->edid_tmp[8] = c1; |
| port->edid_tmp[9] = c2; |
| |
| /* Set Established Timings, but always enable VGA */ |
| port->edid_tmp[0x23] = port->est_i | 0x20; |
| port->edid_tmp[0x24] = port->est_ii; |
| |
| /* Set the Monitor Name to the unit name */ |
| extron_set_edid_string(port->edid_tmp + 0x5f, port->extron->unit_name); |
| /* Set the ASCII String to the CEC adapter name */ |
| extron_set_edid_string(port->edid_tmp + 0x71, port->adap->name); |
| |
| extron_edid_crc(port->edid_tmp); |
| |
| /* Find Video Capability Data Block */ |
| offset = get_edid_tag_location(port->edid_tmp, blocks * 128, 7, 0); |
| if (offset > 0) { |
| port->edid_tmp[offset + 2] &= ~0xc0; |
| if (port->has_qy) |
| port->edid_tmp[offset + 2] |= 0x80; |
| if (port->has_qs) |
| port->edid_tmp[offset + 2] |= 0x40; |
| } |
| |
| extron_edid_crc(port->edid_tmp + 128); |
| } |
| |
| static int extron_write_edid(struct extron_port *port, |
| const u8 *edid, unsigned int blocks) |
| { |
| struct extron *extron = port->extron; |
| u16 phys_addr = CEC_PHYS_ADDR_INVALID; |
| int ret; |
| |
| if (cec_get_edid_spa_location(edid, blocks * 128)) |
| phys_addr = 0; |
| |
| if (mutex_lock_interruptible(&extron->edid_lock)) |
| return -EINTR; |
| |
| memcpy(port->edid_tmp, edid, blocks * 128); |
| |
| if (manufacturer_name[0]) |
| extron_update_edid(port, blocks); |
| |
| ret = extron_send_and_wait_len(port->extron, port, "W+UF256,in.bin", |
| port->edid_tmp, sizeof(port->edid_tmp), |
| "Upl"); |
| if (ret) |
| goto unlock; |
| ret = extron_send_and_wait(port->extron, port, "WI1,in.binEDID", |
| "EdidI01"); |
| if (ret) |
| goto unlock; |
| |
| port->edid_blocks = blocks; |
| memcpy(port->edid, port->edid_tmp, blocks * 128); |
| port->read_edid = true; |
| mutex_unlock(&extron->edid_lock); |
| |
| cec_s_phys_addr(port->adap, phys_addr, false); |
| return 0; |
| |
| unlock: |
| mutex_unlock(&extron->edid_lock); |
| return ret; |
| } |
| |
| static void update_edid_work(struct work_struct *w) |
| { |
| struct extron *extron = container_of(w, struct extron, |
| work_update_edid.work); |
| struct extron_port *in = extron->ports[extron->num_out_ports]; |
| struct extron_port *p; |
| bool has_edid = false; |
| bool has_4kp30 = true; |
| bool has_4kp60 = true; |
| bool has_qy = true; |
| bool has_qs = true; |
| u8 est_i = 0xff; |
| u8 est_ii = 0xff; |
| unsigned int out; |
| |
| for (out = 0; has_4kp60 && out < extron->num_out_ports; out++) { |
| p = extron->ports[out]; |
| if (p->read_edid) { |
| has_4kp60 = p->has_4kp60; |
| est_i &= p->est_i; |
| est_ii &= p->est_ii; |
| has_edid = true; |
| } |
| } |
| for (out = 0; has_4kp30 && out < extron->num_out_ports; out++) |
| if (extron->ports[out]->read_edid) |
| has_4kp30 = extron->ports[out]->has_4kp30; |
| |
| for (out = 0; has_qy && out < extron->num_out_ports; out++) |
| if (extron->ports[out]->read_edid) |
| has_qy = extron->ports[out]->has_qy; |
| |
| for (out = 0; has_qs && out < extron->num_out_ports; out++) |
| if (extron->ports[out]->read_edid) |
| has_qs = extron->ports[out]->has_qs; |
| |
| /* exit if no output port had an EDID */ |
| if (!has_edid) |
| return; |
| |
| /* exit if the input EDID properties remained unchanged */ |
| if (has_4kp60 == in->has_4kp60 && has_4kp30 == in->has_4kp30 && |
| has_qy == in->has_qy && has_qs == in->has_qs && |
| est_i == in->est_i && est_ii == in->est_ii) |
| return; |
| |
| in->has_4kp60 = has_4kp60; |
| in->has_4kp30 = has_4kp30; |
| in->has_qy = has_qy; |
| in->has_qs = has_qs; |
| in->est_i = est_i; |
| in->est_ii = est_ii; |
| extron_write_edid(extron->ports[extron->num_out_ports], |
| has_4kp60 ? hdmi_edid_4k_600 : |
| (has_4kp30 ? hdmi_edid_4k_300 : hdmi_edid), 2); |
| } |
| |
| static void extron_read_edid(struct extron_port *port) |
| { |
| struct extron *extron = port->extron; |
| char cmd[10], reply[10]; |
| unsigned int idx; |
| |
| idx = port->port.port + (port->is_input ? 0 : extron->num_in_ports); |
| snprintf(cmd, sizeof(cmd), "WR%uEDID", idx); |
| snprintf(reply, sizeof(reply), "EdidR%u", idx); |
| if (mutex_lock_interruptible(&extron->edid_lock)) |
| return; |
| if (port->read_edid) |
| goto unlock; |
| extron->edid_bytes_read = 0; |
| extron->edid_port = port; |
| port->edid_blocks = 0; |
| if (!port->has_edid) |
| goto no_edid; |
| |
| extron->edid_reading = true; |
| |
| if (!extron_send_and_wait(extron, port, cmd, reply)) |
| wait_for_completion_killable_timeout(&extron->edid_completion, |
| msecs_to_jiffies(1000)); |
| if (port->edid_blocks) { |
| extron_parse_edid(port); |
| port->read_edid = true; |
| if (!port->is_input) |
| v4l2_ctrl_s_ctrl(port->ctrl_tx_edid_present, 1); |
| } |
| no_edid: |
| extron->edid_reading = false; |
| unlock: |
| mutex_unlock(&extron->edid_lock); |
| cancel_delayed_work_sync(&extron->work_update_edid); |
| if (manufacturer_name[0]) |
| schedule_delayed_work(&extron->work_update_edid, |
| msecs_to_jiffies(1000)); |
| } |
| |
| static void extron_irq_work_handler(struct work_struct *work) |
| { |
| struct extron_port *port = |
| container_of(work, struct extron_port, irq_work); |
| struct extron *extron = port->extron; |
| unsigned long flags; |
| bool update_pa; |
| u16 pa; |
| bool update_has_signal; |
| bool has_signal; |
| bool update_has_edid; |
| bool has_edid; |
| u32 status; |
| |
| spin_lock_irqsave(&port->msg_lock, flags); |
| while (port->rx_msg_num) { |
| spin_unlock_irqrestore(&port->msg_lock, flags); |
| cec_received_msg(port->adap, |
| &port->rx_msg[port->rx_msg_cur_idx]); |
| spin_lock_irqsave(&port->msg_lock, flags); |
| if (port->rx_msg_num) |
| port->rx_msg_num--; |
| port->rx_msg_cur_idx = |
| (port->rx_msg_cur_idx + 1) % NUM_MSGS; |
| } |
| update_pa = port->update_phys_addr; |
| pa = port->phys_addr; |
| port->update_phys_addr = false; |
| update_has_signal = port->update_has_signal; |
| has_signal = port->has_signal; |
| port->update_has_signal = false; |
| update_has_edid = port->update_has_edid; |
| has_edid = port->has_edid; |
| port->update_has_edid = false; |
| status = port->tx_done_status; |
| port->tx_done_status = 0; |
| spin_unlock_irqrestore(&port->msg_lock, flags); |
| |
| if (status) |
| cec_transmit_done(port->adap, status, 0, 0, 0, 0); |
| |
| if (update_has_signal && port->is_input) |
| v4l2_ctrl_s_ctrl(port->ctrl_rx_power_present, has_signal); |
| |
| if (update_has_edid && !port->is_input) { |
| v4l2_ctrl_s_ctrl(port->ctrl_tx_hotplug, |
| port->has_edid); |
| if (port->has_edid) { |
| port->port.found_sink = true; |
| port->port.lost_sink_ts = ktime_set(0, 0); |
| } else { |
| port->port.lost_sink_ts = ktime_get(); |
| } |
| if (!has_edid) { |
| port->edid_blocks = 0; |
| port->read_edid = false; |
| if (extron->edid_reading && !has_edid && |
| extron->edid_port == port) |
| extron->edid_reading = false; |
| v4l2_ctrl_s_ctrl(port->ctrl_tx_edid_present, 0); |
| } else if (!extron->edid_reading || extron->edid_port != port) { |
| extron_read_edid(port); |
| } |
| } |
| if (update_pa) |
| cec_s_phys_addr(port->adap, pa, false); |
| } |
| |
| static void extron_process_received(struct extron_port *port, const char *data) |
| { |
| struct cec_msg msg = {}; |
| unsigned int len = strlen(data); |
| unsigned long irq_flags; |
| unsigned int idx; |
| |
| if (!port || port->disconnected) |
| return; |
| |
| if (len < 5 || (len - 2) % 3 || data[len - 2] != '*') |
| goto malformed; |
| |
| while (*data != '*') { |
| int v = hex2bin(&msg.msg[msg.len], data + 1, 1); |
| |
| if (*data != '%' || v) |
| goto malformed; |
| msg.len++; |
| data += 3; |
| } |
| |
| spin_lock_irqsave(&port->msg_lock, irq_flags); |
| idx = (port->rx_msg_cur_idx + port->rx_msg_num) % |
| NUM_MSGS; |
| if (port->rx_msg_num == NUM_MSGS) { |
| dev_warn(port->dev, |
| "message queue is full, dropping %*ph\n", |
| msg.len, msg.msg); |
| spin_unlock_irqrestore(&port->msg_lock, |
| irq_flags); |
| return; |
| } |
| port->rx_msg_num++; |
| port->rx_msg[idx] = msg; |
| spin_unlock_irqrestore(&port->msg_lock, irq_flags); |
| if (!port->disconnected) |
| schedule_work(&port->irq_work); |
| return; |
| |
| malformed: |
| dev_info(port->extron->dev, "malformed msg received: '%s'\n", data); |
| } |
| |
| static void extron_port_signal_change(struct extron_port *port, bool has_sig) |
| { |
| unsigned long irq_flags; |
| bool update = false; |
| |
| if (!port) |
| return; |
| |
| spin_lock_irqsave(&port->msg_lock, irq_flags); |
| if (!port->update_has_signal && port->has_signal != has_sig) { |
| port->update_has_signal = true; |
| update = true; |
| } |
| port->has_signal = has_sig; |
| spin_unlock_irqrestore(&port->msg_lock, irq_flags); |
| if (update && !port->disconnected) |
| schedule_work(&port->irq_work); |
| } |
| |
| static void extron_process_signal_change(struct extron *extron, const char *data) |
| { |
| unsigned int i; |
| |
| extron_port_signal_change(extron->ports[extron->num_out_ports], |
| data[0] == '1'); |
| for (i = 0; i < extron->num_out_ports; i++) |
| extron_port_signal_change(extron->ports[i], |
| data[2 + 2 * i] != '0'); |
| } |
| |
| static void extron_port_edid_change(struct extron_port *port, bool has_edid) |
| { |
| unsigned long irq_flags; |
| bool update = false; |
| |
| if (!port) |
| return; |
| |
| spin_lock_irqsave(&port->msg_lock, irq_flags); |
| if (!port->update_has_edid && port->has_edid != has_edid) { |
| port->update_has_edid = true; |
| update = true; |
| } |
| port->has_edid = has_edid; |
| spin_unlock_irqrestore(&port->msg_lock, irq_flags); |
| if (update && !port->disconnected) |
| schedule_work(&port->irq_work); |
| } |
| |
| static void extron_process_edid_change(struct extron *extron, const char *data) |
| { |
| unsigned int i; |
| |
| /* |
| * Do nothing if the Extron isn't ready yet. Trying to do this |
| * while the Extron firmware is still settling will fail. |
| */ |
| if (!extron->is_ready) |
| return; |
| |
| for (i = 0; i < extron->num_out_ports; i++) |
| extron_port_edid_change(extron->ports[i], |
| data[2 + 2 * i] != '0'); |
| } |
| |
| static void extron_phys_addr_change(struct extron_port *port, u16 pa) |
| { |
| unsigned long irq_flags; |
| bool update = false; |
| |
| if (!port) |
| return; |
| |
| spin_lock_irqsave(&port->msg_lock, irq_flags); |
| if (!port->update_phys_addr && port->phys_addr != pa) { |
| update = true; |
| port->update_phys_addr = true; |
| } |
| port->phys_addr = pa; |
| spin_unlock_irqrestore(&port->msg_lock, irq_flags); |
| if (update && !port->disconnected) |
| schedule_work(&port->irq_work); |
| } |
| |
| static void extron_process_tx_done(struct extron_port *port, char status) |
| { |
| unsigned long irq_flags; |
| unsigned int tx_status; |
| |
| if (!port) |
| return; |
| |
| switch (status) { |
| case '0': |
| tx_status = CEC_TX_STATUS_NACK | CEC_TX_STATUS_MAX_RETRIES; |
| break; |
| case '1': |
| tx_status = CEC_TX_STATUS_OK; |
| break; |
| default: |
| tx_status = CEC_TX_STATUS_ERROR; |
| break; |
| } |
| spin_lock_irqsave(&port->msg_lock, irq_flags); |
| port->tx_done_status = tx_status; |
| spin_unlock_irqrestore(&port->msg_lock, irq_flags); |
| if (!port->disconnected) |
| schedule_work(&port->irq_work); |
| } |
| |
| static void extron_add_edid(struct extron_port *port, const char *hex) |
| { |
| struct extron *extron = port ? port->extron : NULL; |
| |
| if (!port || port != extron->edid_port) |
| return; |
| while (extron->edid_bytes_read < sizeof(port->edid) && *hex) { |
| int err = hex2bin(&port->edid[extron->edid_bytes_read], hex, 1); |
| |
| if (err) { |
| extron->edid_reading = false; |
| complete(&extron->edid_completion); |
| break; |
| } |
| extron->edid_bytes_read++; |
| hex += 2; |
| } |
| if (extron->edid_bytes_read == 128 && |
| port->edid[126] == 0) { |
| /* There are no extension blocks, we're done */ |
| port->edid_blocks = 1; |
| extron->edid_reading = false; |
| complete(&extron->edid_completion); |
| } |
| if (extron->edid_bytes_read < sizeof(port->edid)) |
| return; |
| if (!*hex) |
| port->edid_blocks = 2; |
| extron->edid_reading = false; |
| complete(&extron->edid_completion); |
| } |
| |
| static irqreturn_t extron_interrupt(struct serio *serio, unsigned char data, |
| unsigned int flags) |
| { |
| struct extron *extron = serio_get_drvdata(serio); |
| struct extron_port *port = NULL; |
| bool found_response; |
| unsigned int p; |
| |
| if (data == '\r' || data == '\n') { |
| if (extron->idx == 0) |
| return IRQ_HANDLED; |
| memcpy(extron->data, extron->buf, extron->idx); |
| extron->len = extron->idx; |
| extron->data[extron->len] = 0; |
| if (debug) |
| dev_info(extron->dev, "received %s\n", extron->data); |
| extron->idx = 0; |
| if (!memcmp(extron->data, "Sig", 3) && |
| extron->data[4] == '*') { |
| extron_process_signal_change(extron, extron->data + 3); |
| } else if (!memcmp(extron->data, "Hdcp", 4) && |
| extron->data[5] == '*') { |
| extron_process_edid_change(extron, extron->data + 4); |
| } else if (!memcmp(extron->data, "DcecI", 5) && |
| extron->data[5] >= '1' && |
| extron->data[5] < '1' + extron->num_in_ports) { |
| unsigned int p = extron->data[5] - '1'; |
| |
| p += extron->num_out_ports; |
| extron_process_tx_done(extron->ports[p], |
| extron->data[extron->len - 1]); |
| } else if (!memcmp(extron->data, "Ceci", 4) && |
| extron->data[4] >= '1' && |
| extron->data[4] < '1' + extron->num_in_ports && |
| extron->data[5] == '*') { |
| unsigned int p = extron->data[4] - '1'; |
| |
| p += extron->num_out_ports; |
| extron_process_received(extron->ports[p], |
| extron->data + 6); |
| } else if (!memcmp(extron->data, "DcecO", 5) && |
| extron->data[5] >= '1' && |
| extron->data[5] < '1' + extron->num_out_ports) { |
| unsigned int p = extron->data[5] - '1'; |
| |
| extron_process_tx_done(extron->ports[p], |
| extron->data[extron->len - 1]); |
| } else if (!memcmp(extron->data, "Ceco", 4) && |
| extron->data[4] >= '1' && |
| extron->data[4] < '1' + extron->num_out_ports && |
| extron->data[5] == '*') { |
| unsigned int p = extron->data[4] - '1'; |
| |
| extron_process_received(extron->ports[p], |
| extron->data + 6); |
| } else if (!memcmp(extron->data, "Pceco", 5) && |
| extron->data[5] >= '1' && |
| extron->data[5] < '1' + extron->num_out_ports) { |
| unsigned int p = extron->data[5] - '1'; |
| unsigned int tmp_pa[2] = { 0xff, 0xff }; |
| |
| if (sscanf(extron->data + 7, "%%%02x%%%02x", |
| &tmp_pa[0], &tmp_pa[1]) == 2) |
| extron_phys_addr_change(extron->ports[p], |
| tmp_pa[0] << 8 | tmp_pa[1]); |
| } else if (!memcmp(extron->data, "Pceci", 5) && |
| extron->data[5] >= '1' && |
| extron->data[5] < '1' + extron->num_in_ports) { |
| unsigned int p = extron->data[5] - '1'; |
| unsigned int tmp_pa[2] = { 0xff, 0xff }; |
| |
| p += extron->num_out_ports; |
| if (sscanf(extron->data + 7, "%%%02x%%%02x", |
| &tmp_pa[0], &tmp_pa[1]) == 2) |
| extron_phys_addr_change(extron->ports[p], |
| tmp_pa[0] << 8 | tmp_pa[1]); |
| } else if (!memcmp(extron->data, "EdidR", 5) && |
| extron->data[5] >= '1' && |
| extron->data[5] < '1' + extron->num_ports && |
| extron->data[6] == '*') { |
| unsigned int p = extron->data[5] - '1'; |
| |
| if (p) |
| p--; |
| else |
| p = extron->num_out_ports; |
| extron_add_edid(extron->ports[p], extron->data + 7); |
| } else if (extron->edid_reading && extron->len == 32 && |
| extron->edid_port) { |
| extron_add_edid(extron->edid_port, extron->data); |
| } |
| |
| found_response = false; |
| if (extron->response && |
| !strncmp(extron->response, extron->data, |
| strlen(extron->response))) |
| found_response = true; |
| |
| for (p = 0; !found_response && p < extron->num_ports; p++) { |
| port = extron->ports[p]; |
| if (port && port->response && |
| !strncmp(port->response, extron->data, |
| strlen(port->response))) |
| found_response = true; |
| } |
| |
| if (!found_response && extron->response && |
| extron->data[0] == 'E' && |
| isdigit(extron->data[1]) && |
| isdigit(extron->data[2]) && |
| !extron->data[3]) { |
| extron->cmd_error = (extron->data[1] - '0') * 10 + |
| extron->data[2] - '0'; |
| extron->response = NULL; |
| complete(&extron->cmd_done); |
| } |
| |
| if (!found_response) |
| return IRQ_HANDLED; |
| |
| memcpy(extron->reply, extron->data, extron->len); |
| extron->reply[extron->len] = 0; |
| if (!port) { |
| extron->response = NULL; |
| complete(&extron->cmd_done); |
| } else { |
| port->response = NULL; |
| complete(&port->cmd_done); |
| } |
| return IRQ_HANDLED; |
| } |
| |
| if (extron->idx >= DATA_SIZE - 1) { |
| dev_info(extron->dev, |
| "throwing away %d bytes of garbage\n", extron->idx); |
| extron->idx = 0; |
| } |
| extron->buf[extron->idx++] = (char)data; |
| return IRQ_HANDLED; |
| } |
| |
| static int extron_cec_adap_enable(struct cec_adapter *adap, bool enable) |
| { |
| struct extron_port *port = cec_get_drvdata(adap); |
| |
| return (port->disconnected && enable) ? -ENODEV : 0; |
| } |
| |
| static int extron_cec_adap_log_addr(struct cec_adapter *adap, u8 log_addr) |
| { |
| struct extron_port *port = cec_get_drvdata(adap); |
| char cmd[26]; |
| char resp[25]; |
| u8 la = log_addr == CEC_LOG_ADDR_INVALID ? 15 : log_addr; |
| int err; |
| |
| if (port->disconnected) |
| return -ENODEV; |
| snprintf(cmd, sizeof(cmd), "W%c%u*%uLCEC", |
| port->direction, port->port.port, la); |
| snprintf(resp, sizeof(resp), "Lcec%c%u*%u", |
| port->direction, port->port.port, la); |
| err = extron_send_and_wait(port->extron, port, cmd, resp); |
| return log_addr != CEC_LOG_ADDR_INVALID && err ? err : 0; |
| } |
| |
| static int extron_cec_adap_transmit(struct cec_adapter *adap, u8 attempts, |
| u32 signal_free_time, struct cec_msg *msg) |
| { |
| struct extron_port *port = cec_get_drvdata(adap); |
| char buf[CEC_MAX_MSG_SIZE * 3 + 1]; |
| char cmd[CEC_MAX_MSG_SIZE * 3 + 13]; |
| unsigned int i; |
| |
| if (port->disconnected) |
| return -ENODEV; |
| buf[0] = 0; |
| for (i = 0; i < msg->len - 1; i++) |
| sprintf(buf + i * 3, "%%%02X", msg->msg[i + 1]); |
| snprintf(cmd, sizeof(cmd), "W%c%u*%u*%u*%sDCEC", |
| port->direction, port->port.port, |
| cec_msg_initiator(msg), cec_msg_destination(msg), buf); |
| return extron_send_and_wait(port->extron, port, cmd, NULL); |
| } |
| |
| static void extron_cec_adap_unconfigured(struct cec_adapter *adap) |
| { |
| struct extron_port *port = cec_get_drvdata(adap); |
| |
| if (port->disconnected) |
| return; |
| if (debug) |
| dev_info(port->extron->dev, "unconfigured port %d (%s)\n", |
| port->port.port, |
| port->extron->splitter.is_standby ? "Off" : "On"); |
| if (!port->is_input) |
| cec_splitter_unconfigured_output(&port->port); |
| } |
| |
| static void extron_cec_configured(struct cec_adapter *adap) |
| { |
| struct extron_port *port = cec_get_drvdata(adap); |
| |
| if (port->disconnected) |
| return; |
| if (debug) |
| dev_info(port->extron->dev, "configured port %d (%s)\n", |
| port->port.port, |
| port->extron->splitter.is_standby ? "Off" : "On"); |
| if (!port->is_input) |
| cec_splitter_configured_output(&port->port); |
| } |
| |
| static void extron_cec_adap_nb_transmit_canceled(struct cec_adapter *adap, |
| const struct cec_msg *msg) |
| { |
| struct extron_port *port = cec_get_drvdata(adap); |
| struct cec_adapter *input_adap; |
| |
| if (!vendor_id) |
| return; |
| if (port->disconnected || port->is_input) |
| return; |
| input_adap = port->extron->ports[port->extron->num_out_ports]->adap; |
| cec_splitter_nb_transmit_canceled_output(&port->port, msg, input_adap); |
| } |
| |
| static int extron_received(struct cec_adapter *adap, struct cec_msg *msg) |
| { |
| struct extron_port *port = cec_get_drvdata(adap); |
| |
| if (!vendor_id) |
| return -ENOMSG; |
| if (port->disconnected) |
| return -ENOMSG; |
| if (port->is_input) |
| return cec_splitter_received_input(&port->port, msg); |
| return cec_splitter_received_output(&port->port, msg, |
| port->extron->ports[port->extron->num_out_ports]->adap); |
| } |
| |
| #define log_printf(adap, file, fmt, arg...) \ |
| do { \ |
| if (file) \ |
| seq_printf((file), fmt, ## arg); \ |
| else \ |
| pr_info("cec-%s: " fmt, (adap)->name, ## arg); \ |
| } while (0) |
| |
| static const char * const pwr_state[] = { |
| "on", |
| "standby", |
| "to on", |
| "to standby", |
| }; |
| |
| static void extron_adap_status_port(struct extron_port *port, struct seq_file *file) |
| { |
| struct cec_adapter *adap = port->adap; |
| |
| if (port->disconnected) { |
| log_printf(adap, file, |
| "\tport %u: disconnected\n", port->port.port); |
| return; |
| } |
| if (port->is_input) |
| log_printf(adap, file, |
| "\tport %u: %s signal, %s edid, %s 4kp30, %s 4kp60, %sQS/%sQY, is %s\n", |
| port->port.port, |
| port->has_signal ? "has" : "no", |
| port->has_edid ? "has" : "no", |
| port->has_4kp30 ? "has" : "no", |
| port->has_4kp60 ? "has" : "no", |
| port->has_qs ? "" : "no ", |
| port->has_qy ? "" : "no ", |
| !port->port.adap->is_configured ? "not configured" : |
| pwr_state[port->extron->splitter.is_standby]); |
| else |
| log_printf(adap, file, |
| "\tport %u: %s sink, %s signal, %s edid, %s 4kp30, %s 4kp60, %sQS/%sQY, is %sactive source, is %s\n", |
| port->port.port, |
| port->port.found_sink ? "found" : "no", |
| port->has_signal ? "has" : "no", |
| port->has_edid ? "has" : "no", |
| port->has_4kp30 ? "has" : "no", |
| port->has_4kp60 ? "has" : "no", |
| port->has_qs ? "" : "no ", |
| port->has_qy ? "" : "no ", |
| port->port.is_active_source ? "" : "not ", |
| !port->port.adap->is_configured ? "not configured" : |
| pwr_state[port->port.power_status & 3]); |
| if (port->port.out_give_device_power_status_seq) |
| log_printf(adap, file, |
| "\tport %u: querying power status (%u, %lldms)\n", |
| port->port.port, |
| port->port.out_give_device_power_status_seq & ~(1 << 31), |
| ktime_ms_delta(ktime_get(), |
| port->port.out_give_device_power_status_ts)); |
| if (port->port.out_request_current_latency_seq) |
| log_printf(adap, file, |
| "\tport %u: querying latency (%u, %lldms)\n", |
| port->port.port, |
| port->port.out_request_current_latency_seq & ~(1 << 31), |
| ktime_ms_delta(ktime_get(), |
| port->port.out_request_current_latency_ts)); |
| } |
| |
| static void extron_adap_status(struct cec_adapter *adap, struct seq_file *file) |
| { |
| struct extron_port *port = cec_get_drvdata(adap); |
| struct extron *extron = port->extron; |
| unsigned int i; |
| |
| log_printf(adap, file, "name: %s type: %s\n", |
| extron->unit_name, extron->unit_type); |
| log_printf(adap, file, "model: 60-160%c-01 (1 input, %u outputs)\n", |
| '6' + extron->num_out_ports / 2, extron->num_out_ports); |
| log_printf(adap, file, "firmware version: %s CEC engine version: %s\n", |
| extron->unit_fw_version, extron->unit_cec_engine_version); |
| if (extron->hpd_never_low) |
| log_printf(adap, file, "always keep input HPD high\n"); |
| else |
| log_printf(adap, file, |
| "pull input HPD low if all output HPDs are low\n"); |
| if (vendor_id) |
| log_printf(adap, file, |
| "splitter vendor ID: 0x%06x\n", vendor_id); |
| if (manufacturer_name[0]) |
| log_printf(adap, file, "splitter manufacturer name: %s\n", |
| manufacturer_name); |
| log_printf(adap, file, "splitter power status: %s\n", |
| pwr_state[extron->splitter.is_standby]); |
| log_printf(adap, file, "%s port: %d (%s)\n", |
| port->is_input ? "input" : "output", |
| port->port.port, port->name); |
| log_printf(adap, file, "splitter input port:\n"); |
| extron_adap_status_port(extron->ports[extron->num_out_ports], file); |
| |
| log_printf(adap, file, "splitter output ports:\n"); |
| for (i = 0; i < extron->num_out_ports; i++) |
| extron_adap_status_port(extron->ports[i], file); |
| |
| if (!port->has_edid || !port->read_edid) |
| return; |
| |
| for (i = 0; i < port->edid_blocks * 128; i += 16) { |
| if (i % 128 == 0) |
| log_printf(adap, file, "\n"); |
| log_printf(adap, file, "EDID: %*ph\n", 16, port->edid + i); |
| } |
| } |
| |
| static const struct cec_adap_ops extron_cec_adap_ops = { |
| .adap_enable = extron_cec_adap_enable, |
| .adap_log_addr = extron_cec_adap_log_addr, |
| .adap_transmit = extron_cec_adap_transmit, |
| .adap_nb_transmit_canceled = extron_cec_adap_nb_transmit_canceled, |
| .adap_unconfigured = extron_cec_adap_unconfigured, |
| .adap_status = extron_adap_status, |
| .configured = extron_cec_configured, |
| .received = extron_received, |
| }; |
| |
| static int extron_querycap(struct file *file, void *priv, |
| struct v4l2_capability *cap) |
| { |
| struct extron_port *port = video_drvdata(file); |
| |
| strscpy(cap->driver, "extron-da-hd-4k-plus-cec", sizeof(cap->driver)); |
| strscpy(cap->card, cap->driver, sizeof(cap->card)); |
| snprintf(cap->bus_info, sizeof(cap->bus_info), "serio:%s", port->name); |
| return 0; |
| } |
| |
| static int extron_enum_input(struct file *file, void *priv, struct v4l2_input *inp) |
| { |
| struct extron_port *port = video_drvdata(file); |
| |
| if (inp->index) |
| return -EINVAL; |
| inp->type = V4L2_INPUT_TYPE_CAMERA; |
| snprintf(inp->name, sizeof(inp->name), "HDMI IN %u", port->port.port); |
| inp->status = v4l2_ctrl_g_ctrl(port->ctrl_rx_power_present) ? |
| 0 : V4L2_IN_ST_NO_SIGNAL; |
| return 0; |
| } |
| |
| static int extron_g_input(struct file *file, void *priv, unsigned int *i) |
| { |
| *i = 0; |
| return 0; |
| } |
| |
| static int extron_s_input(struct file *file, void *priv, unsigned int i) |
| { |
| return i ? -EINVAL : 0; |
| } |
| |
| static int extron_enum_output(struct file *file, void *priv, struct v4l2_output *out) |
| { |
| struct extron_port *port = video_drvdata(file); |
| |
| if (out->index) |
| return -EINVAL; |
| out->type = V4L2_OUTPUT_TYPE_ANALOG; |
| snprintf(out->name, sizeof(out->name), "HDMI OUT %u", port->port.port); |
| return 0; |
| } |
| |
| static int extron_g_output(struct file *file, void *priv, unsigned int *o) |
| { |
| *o = 0; |
| return 0; |
| } |
| |
| static int extron_s_output(struct file *file, void *priv, unsigned int o) |
| { |
| return o ? -EINVAL : 0; |
| } |
| |
| static int extron_g_edid(struct file *file, void *_fh, |
| struct v4l2_edid *edid) |
| { |
| struct extron_port *port = video_drvdata(file); |
| |
| memset(edid->reserved, 0, sizeof(edid->reserved)); |
| if (port->disconnected) |
| return -ENODEV; |
| if (edid->pad) |
| return -EINVAL; |
| if (!port->has_edid) |
| return -ENODATA; |
| if (!port->read_edid) |
| extron_read_edid(port); |
| if (!port->read_edid) |
| return -ENODATA; |
| if (edid->start_block == 0 && edid->blocks == 0) { |
| edid->blocks = port->edid_blocks; |
| return 0; |
| } |
| if (edid->start_block >= port->edid_blocks) |
| return -EINVAL; |
| if (edid->blocks > port->edid_blocks - edid->start_block) |
| edid->blocks = port->edid_blocks - edid->start_block; |
| memcpy(edid->edid, port->edid + edid->start_block * 128, edid->blocks * 128); |
| return 0; |
| } |
| |
| static int extron_s_edid(struct file *file, void *_fh, struct v4l2_edid *edid) |
| { |
| struct extron_port *port = video_drvdata(file); |
| |
| memset(edid->reserved, 0, sizeof(edid->reserved)); |
| if (port->disconnected) |
| return -ENODEV; |
| if (edid->pad) |
| return -EINVAL; |
| |
| /* Unfortunately it is not possible to clear the EDID */ |
| if (edid->blocks == 0) |
| return -EINVAL; |
| |
| if (edid->blocks > MAX_EDID_BLOCKS) { |
| edid->blocks = MAX_EDID_BLOCKS; |
| return -E2BIG; |
| } |
| |
| if (cec_get_edid_spa_location(edid->edid, edid->blocks * 128)) |
| v4l2_set_edid_phys_addr(edid->edid, edid->blocks * 128, 0); |
| extron_parse_edid(port); |
| return extron_write_edid(port, edid->edid, edid->blocks); |
| } |
| |
| static int extron_log_status(struct file *file, void *priv) |
| { |
| struct extron_port *port = video_drvdata(file); |
| |
| extron_adap_status(port->adap, NULL); |
| return v4l2_ctrl_log_status(file, priv); |
| } |
| |
| static const struct v4l2_ioctl_ops extron_ioctl_ops = { |
| .vidioc_querycap = extron_querycap, |
| .vidioc_enum_input = extron_enum_input, |
| .vidioc_g_input = extron_g_input, |
| .vidioc_s_input = extron_s_input, |
| .vidioc_enum_output = extron_enum_output, |
| .vidioc_g_output = extron_g_output, |
| .vidioc_s_output = extron_s_output, |
| .vidioc_g_edid = extron_g_edid, |
| .vidioc_s_edid = extron_s_edid, |
| .vidioc_log_status = extron_log_status, |
| .vidioc_subscribe_event = v4l2_ctrl_subscribe_event, |
| .vidioc_unsubscribe_event = v4l2_event_unsubscribe, |
| }; |
| |
| static const struct v4l2_file_operations extron_fops = { |
| .owner = THIS_MODULE, |
| .open = v4l2_fh_open, |
| .release = v4l2_fh_release, |
| .poll = v4l2_ctrl_poll, |
| .unlocked_ioctl = video_ioctl2, |
| }; |
| |
| static const struct video_device extron_videodev = { |
| .name = "extron-da-hd-4k-plus-cec", |
| .vfl_dir = VFL_DIR_RX, |
| .fops = &extron_fops, |
| .ioctl_ops = &extron_ioctl_ops, |
| .minor = -1, |
| .release = video_device_release_empty, |
| }; |
| |
| static void extron_disconnect(struct serio *serio) |
| { |
| struct extron *extron = serio_get_drvdata(serio); |
| unsigned int p; |
| |
| kthread_stop(extron->kthread_setup); |
| |
| for (p = 0; p < extron->num_ports; p++) { |
| struct extron_port *port = extron->ports[p]; |
| |
| if (!port) |
| continue; |
| port->disconnected = true; |
| cancel_work_sync(&port->irq_work); |
| } |
| cancel_delayed_work_sync(&extron->work_update_edid); |
| for (p = 0; p < extron->num_ports; p++) { |
| struct extron_port *port = extron->ports[p]; |
| |
| if (!port) |
| continue; |
| |
| if (port->cec_was_registered) { |
| if (cec_is_registered(port->adap)) |
| cec_unregister_adapter(port->adap); |
| /* |
| * After registering the adapter, the |
| * extron_setup_thread() function took an extra |
| * reference to the device. We call the corresponding |
| * put here. |
| */ |
| cec_put_device(port->adap); |
| } else { |
| cec_delete_adapter(port->adap); |
| } |
| video_unregister_device(&port->vdev); |
| } |
| |
| complete(&extron->edid_completion); |
| |
| for (p = 0; p < extron->num_ports; p++) { |
| struct extron_port *port = extron->ports[p]; |
| |
| if (!port) |
| continue; |
| v4l2_ctrl_handler_free(&port->hdl); |
| mutex_destroy(&port->video_lock); |
| kfree(port); |
| } |
| mutex_destroy(&extron->edid_lock); |
| mutex_destroy(&extron->serio_lock); |
| extron->serio = NULL; |
| serio_set_drvdata(serio, NULL); |
| serio_close(serio); |
| } |
| |
| static int extron_setup(struct extron *extron) |
| { |
| struct serio *serio = extron->serio; |
| struct extron_port *port; |
| u8 *reply = extron->reply; |
| unsigned int p; |
| unsigned int major, minor; |
| int err; |
| |
| /* |
| * Attempt to disable CEC: avoid received CEC messages |
| * from interfering with the other serial port traffic. |
| */ |
| extron_send_and_wait(extron, NULL, "WI1*0CCEC", NULL); |
| extron_send_and_wait(extron, NULL, "WO0*CCEC", NULL); |
| |
| /* Obtain unit part number */ |
| err = extron_send_and_wait(extron, NULL, "N", "Pno"); |
| if (err) |
| return err; |
| dev_info(extron->dev, "Unit part number: %s\n", reply + 3); |
| if (strcmp(reply + 3, "60-1607-01") && |
| strcmp(reply + 3, "60-1608-01") && |
| strcmp(reply + 3, "60-1609-01")) { |
| dev_err(extron->dev, "Unsupported model\n"); |
| return -ENODEV; |
| } |
| /* Up to 6 output ports and one input port */ |
| extron->num_out_ports = 2 * (reply[9] - '6'); |
| extron->splitter.num_out_ports = extron->num_out_ports; |
| extron->splitter.ports = extron->splitter_ports; |
| extron->splitter.dev = extron->dev; |
| extron->num_in_ports = 1; |
| extron->num_ports = extron->num_out_ports + extron->num_in_ports; |
| dev_info(extron->dev, "Unit output ports: %d\n", extron->num_out_ports); |
| dev_info(extron->dev, "Unit input ports: %d\n", extron->num_in_ports); |
| |
| err = extron_send_and_wait(extron, NULL, "W CN", "Ipn "); |
| if (err) |
| return err; |
| dev_info(extron->dev, "Unit name: %s\n", reply + 4); |
| strscpy(extron->unit_name, reply + 4, sizeof(extron->unit_name)); |
| |
| err = extron_send_and_wait(extron, NULL, "*Q", "Bld"); |
| if (err) |
| return err; |
| dev_info(extron->dev, "Unit FW Version: %s\n", reply + 3); |
| strscpy(extron->unit_fw_version, reply + 3, |
| sizeof(extron->unit_fw_version)); |
| if (sscanf(reply + 3, "%u.%u.", &major, &minor) < 2 || |
| major < 1 || minor < 2) { |
| dev_err(extron->dev, |
| "Unsupported FW version (only 1.02 or up is supported)\n"); |
| return -ENODEV; |
| } |
| |
| err = extron_send_and_wait(extron, NULL, "2i", "Inf02*"); |
| if (err) |
| return err; |
| dev_info(extron->dev, "Unit Type: %s\n", reply + 6); |
| strscpy(extron->unit_type, reply + 6, sizeof(extron->unit_type)); |
| |
| err = extron_send_and_wait(extron, NULL, "39Q", "Ver39*"); |
| if (err) |
| return err; |
| dev_info(extron->dev, "CEC Engine Version: %s\n", reply + 6); |
| strscpy(extron->unit_cec_engine_version, reply + 6, |
| sizeof(extron->unit_cec_engine_version)); |
| |
| /* Disable CEC */ |
| err = extron_send_and_wait(extron, NULL, "WI1*0CCEC", "CcecI1*"); |
| if (err) |
| return err; |
| err = extron_send_and_wait(extron, NULL, "WO0*CCEC", "CcecO0"); |
| if (err) |
| return err; |
| |
| extron->hpd_never_low = hpd_never_low; |
| |
| /* Pull input port HPD low if all output ports also have a low HPD */ |
| if (hpd_never_low) { |
| dev_info(extron->dev, "Always keep input HPD high\n"); |
| } else { |
| dev_info(extron->dev, "Pull input HPD low if all output HPDs are low\n"); |
| extron_send_and_wait(extron, NULL, "W1ihpd", "Ihpd1"); |
| } |
| |
| for (p = 0; p < extron->num_ports; p++) { |
| u32 caps = CEC_CAP_DEFAULTS | CEC_CAP_MONITOR_ALL; |
| |
| if (vendor_id) |
| caps &= ~CEC_CAP_LOG_ADDRS; |
| port = kzalloc(sizeof(*port), GFP_KERNEL); |
| if (!port) |
| return -ENOMEM; |
| |
| INIT_WORK(&port->irq_work, extron_irq_work_handler); |
| spin_lock_init(&port->msg_lock); |
| mutex_init(&port->video_lock); |
| port->extron = extron; |
| port->is_input = p >= extron->num_out_ports; |
| port->direction = port->is_input ? 'I' : 'O'; |
| port->port.port = 1 + (port->is_input ? p - extron->num_out_ports : p); |
| port->port.splitter = &extron->splitter; |
| port->phys_addr = CEC_PHYS_ADDR_INVALID; |
| snprintf(port->name, sizeof(port->name), "%s-%s-%u", |
| dev_name(&serio->dev), port->is_input ? "in" : "out", |
| port->port.port); |
| |
| port->dev = extron->dev; |
| port->adap = cec_allocate_adapter(&extron_cec_adap_ops, port, |
| port->name, caps, 1); |
| err = PTR_ERR_OR_ZERO(port->adap); |
| if (err < 0) { |
| kfree(port); |
| return err; |
| } |
| |
| port->adap->xfer_timeout_ms = EXTRON_TIMEOUT_SECS * 1000; |
| port->port.adap = port->adap; |
| port->vdev = extron_videodev; |
| port->vdev.lock = &port->video_lock; |
| port->vdev.v4l2_dev = &extron->v4l2_dev; |
| port->vdev.ctrl_handler = &port->hdl; |
| port->vdev.device_caps = V4L2_CAP_EDID; |
| video_set_drvdata(&port->vdev, port); |
| |
| v4l2_ctrl_handler_init(&port->hdl, 2); |
| |
| if (port->is_input) { |
| port->vdev.vfl_dir = VFL_DIR_RX; |
| port->ctrl_rx_power_present = |
| v4l2_ctrl_new_std(&port->hdl, NULL, |
| V4L2_CID_DV_RX_POWER_PRESENT, |
| 0, 1, 0, 0); |
| port->has_edid = true; |
| } else { |
| port->vdev.vfl_dir = VFL_DIR_TX; |
| port->ctrl_tx_hotplug = |
| v4l2_ctrl_new_std(&port->hdl, NULL, |
| V4L2_CID_DV_TX_HOTPLUG, |
| 0, 1, 0, 0); |
| port->ctrl_tx_edid_present = |
| v4l2_ctrl_new_std(&port->hdl, NULL, |
| V4L2_CID_DV_TX_EDID_PRESENT, |
| 0, 1, 0, 0); |
| } |
| |
| err = port->hdl.error; |
| if (err < 0) { |
| cec_delete_adapter(port->adap); |
| kfree(port); |
| return err; |
| } |
| extron->ports[p] = port; |
| extron->splitter_ports[p] = &port->port; |
| if (port->is_input && manufacturer_name[0]) |
| extron_write_edid(port, hdmi_edid, 2); |
| } |
| |
| /* Enable CEC (manual mode, i.e. controlled by the driver) */ |
| err = extron_send_and_wait(extron, NULL, "WI1*20CCEC", "CcecI1*"); |
| if (err) |
| return err; |
| |
| err = extron_send_and_wait(extron, NULL, "WO20*CCEC", "CcecO20"); |
| if (err) |
| return err; |
| |
| /* Set logical addresses to 15 */ |
| err = extron_send_and_wait(extron, NULL, "WI1*15LCEC", "LcecI1*15"); |
| if (err) |
| return err; |
| |
| for (p = 0; p < extron->num_out_ports; p++) { |
| char cmd[20]; |
| char resp[20]; |
| |
| snprintf(cmd, sizeof(cmd), "WO%u*15LCEC", p + 1); |
| snprintf(resp, sizeof(resp), "LcecO%u*15", p + 1); |
| err = extron_send_and_wait(extron, extron->ports[p], cmd, resp); |
| if (err) |
| return err; |
| } |
| |
| /* |
| * The Extron is now ready for operation. Specifically it is now |
| * possible to retrieve EDIDs. |
| */ |
| extron->is_ready = true; |
| |
| /* Query HDCP and Signal states, used to update the initial state */ |
| err = extron_send_and_wait(extron, NULL, "WHDCP", "Hdcp"); |
| if (err) |
| return err; |
| |
| return extron_send_and_wait(extron, NULL, "WLS", "Sig"); |
| } |
| |
| static int extron_setup_thread(void *_extron) |
| { |
| struct extron *extron = _extron; |
| struct extron_port *port; |
| unsigned int p; |
| bool poll_splitter = false; |
| bool was_connected = true; |
| int err; |
| |
| while (1) { |
| if (kthread_should_stop()) |
| return 0; |
| err = extron_send_and_wait(extron, NULL, "W3CV", "Vrb3"); |
| // that should make it possible to detect a serio disconnect |
| // here by stopping the workqueue |
| if (err >= 0) |
| break; |
| was_connected = false; |
| ssleep(1); |
| } |
| |
| /* |
| * If the Extron was not connected at probe() time, i.e. it just got |
| * powered up and while the serial port is working, the firmware is |
| * still booting up, then wait 10 seconds for the firmware to settle. |
| * |
| * Trying to continue too soon means that some commands will not |
| * work yet. |
| */ |
| if (!was_connected) |
| ssleep(10); |
| |
| err = extron_setup(extron); |
| if (err) |
| goto disable_ports; |
| |
| for (p = 0; p < extron->num_ports; p++) { |
| struct cec_log_addrs log_addrs = {}; |
| |
| port = extron->ports[p]; |
| if (port->is_input && manufacturer_name[0]) |
| v4l2_disable_ioctl(&port->vdev, VIDIOC_S_EDID); |
| err = video_register_device(&port->vdev, VFL_TYPE_VIDEO, -1); |
| if (err) { |
| v4l2_err(&extron->v4l2_dev, "Failed to register video device\n"); |
| goto disable_ports; |
| } |
| |
| err = cec_register_adapter(port->adap, extron->dev); |
| if (err < 0) |
| goto disable_ports; |
| port->dev = &port->adap->devnode.dev; |
| port->cec_was_registered = true; |
| /* |
| * This driver is unusual in that the whole setup takes place |
| * in a thread since it can take such a long time before the |
| * Extron Splitter boots up, and you do not want to block the |
| * probe function on this driver. In addition, as soon as |
| * CEC adapters come online, they can be used, and you cannot |
| * just unregister them again if an error occurs, since that |
| * can delete the underlying CEC adapter, which might already |
| * be in use. |
| * |
| * So we take an additional reference to the adapter. This |
| * allows us to unregister the device node if needed, without |
| * deleting the actual adapter. |
| * |
| * In the disconnect function we will do the corresponding |
| * put call to ensure the adapter is deleted. |
| */ |
| cec_get_device(port->adap); |
| |
| /* |
| * If vendor_id wasn't set, then userspace configures the |
| * CEC devices. Otherwise the driver configures the CEC |
| * devices as TV (input) and Playback (outputs) devices |
| * and the driver processes all CEC messages. |
| */ |
| if (!vendor_id) |
| continue; |
| |
| log_addrs.cec_version = CEC_OP_CEC_VERSION_2_0; |
| log_addrs.num_log_addrs = 1; |
| log_addrs.vendor_id = vendor_id; |
| if (port->is_input) { |
| strscpy(log_addrs.osd_name, "Splitter In", |
| sizeof(log_addrs.osd_name)); |
| log_addrs.log_addr_type[0] = CEC_LOG_ADDR_TYPE_TV; |
| log_addrs.primary_device_type[0] = CEC_OP_PRIM_DEVTYPE_TV; |
| log_addrs.all_device_types[0] = CEC_OP_ALL_DEVTYPE_TV; |
| } else { |
| snprintf(log_addrs.osd_name, sizeof(log_addrs.osd_name), |
| "Splitter Out%u", port->port.port); |
| log_addrs.log_addr_type[0] = CEC_LOG_ADDR_TYPE_PLAYBACK; |
| log_addrs.primary_device_type[0] = CEC_OP_PRIM_DEVTYPE_PLAYBACK; |
| log_addrs.all_device_types[0] = CEC_OP_ALL_DEVTYPE_PLAYBACK; |
| } |
| err = cec_s_log_addrs(port->adap, &log_addrs, false); |
| if (err < 0) |
| goto disable_ports; |
| } |
| poll_splitter = true; |
| |
| port = extron->ports[extron->num_out_ports]; |
| while (!kthread_should_stop()) { |
| ssleep(1); |
| if (hpd_never_low != extron->hpd_never_low) { |
| /* |
| * Keep input port HPD high at all times, or pull it low |
| * if all output ports also have a low HPD |
| */ |
| if (hpd_never_low) { |
| dev_info(extron->dev, "Always keep input HPD high\n"); |
| extron_send_and_wait(extron, NULL, "W0ihpd", "Ihpd0"); |
| } else { |
| dev_info(extron->dev, "Pull input HPD low if all output HPDs are low\n"); |
| extron_send_and_wait(extron, NULL, "W1ihpd", "Ihpd1"); |
| } |
| extron->hpd_never_low = hpd_never_low; |
| } |
| if (poll_splitter && |
| cec_splitter_poll(&extron->splitter, port->adap, debug) && |
| manufacturer_name[0]) { |
| /* |
| * Sinks were lost, so see if the input edid needs to |
| * be updated. |
| */ |
| cancel_delayed_work_sync(&extron->work_update_edid); |
| schedule_delayed_work(&extron->work_update_edid, |
| msecs_to_jiffies(1000)); |
| } |
| } |
| return 0; |
| |
| disable_ports: |
| extron->is_ready = false; |
| for (p = 0; p < extron->num_ports; p++) { |
| struct extron_port *port = extron->ports[p]; |
| |
| if (!port) |
| continue; |
| port->disconnected = true; |
| cancel_work_sync(&port->irq_work); |
| video_unregister_device(&port->vdev); |
| if (port->cec_was_registered) |
| cec_unregister_adapter(port->adap); |
| } |
| cancel_delayed_work_sync(&extron->work_update_edid); |
| complete(&extron->edid_completion); |
| dev_err(extron->dev, "Setup failed with error %d\n", err); |
| while (!kthread_should_stop()) |
| ssleep(1); |
| return err; |
| } |
| |
| static int extron_connect(struct serio *serio, struct serio_driver *drv) |
| { |
| struct extron *extron; |
| int err = -ENOMEM; |
| |
| if (manufacturer_name[0] && |
| (!isupper(manufacturer_name[0]) || |
| !isupper(manufacturer_name[1]) || |
| !isupper(manufacturer_name[2]))) { |
| dev_warn(&serio->dev, "ignoring invalid manufacturer name\n"); |
| manufacturer_name[0] = 0; |
| } |
| |
| extron = kzalloc(sizeof(*extron), GFP_KERNEL); |
| |
| if (!extron) |
| return -ENOMEM; |
| |
| extron->serio = serio; |
| extron->dev = &serio->dev; |
| mutex_init(&extron->serio_lock); |
| mutex_init(&extron->edid_lock); |
| INIT_DELAYED_WORK(&extron->work_update_edid, update_edid_work); |
| |
| err = v4l2_device_register(extron->dev, &extron->v4l2_dev); |
| if (err) |
| goto free_device; |
| |
| err = serio_open(serio, drv); |
| if (err) |
| goto unreg_v4l2_dev; |
| |
| serio_set_drvdata(serio, extron); |
| init_completion(&extron->edid_completion); |
| |
| extron->kthread_setup = kthread_run(extron_setup_thread, extron, |
| "extron-da-hd-4k-plus-cec-%s", dev_name(&serio->dev)); |
| if (!IS_ERR(extron->kthread_setup)) |
| return 0; |
| |
| dev_err(extron->dev, "kthread_run() failed\n"); |
| err = PTR_ERR(extron->kthread_setup); |
| |
| extron->serio = NULL; |
| serio_set_drvdata(serio, NULL); |
| serio_close(serio); |
| unreg_v4l2_dev: |
| v4l2_device_unregister(&extron->v4l2_dev); |
| free_device: |
| mutex_destroy(&extron->edid_lock); |
| mutex_destroy(&extron->serio_lock); |
| kfree(extron); |
| return err; |
| } |
| |
| static const struct serio_device_id extron_serio_ids[] = { |
| { |
| .type = SERIO_RS232, |
| .proto = SERIO_EXTRON_DA_HD_4K_PLUS, |
| .id = SERIO_ANY, |
| .extra = SERIO_ANY, |
| }, |
| { 0 } |
| }; |
| |
| MODULE_DEVICE_TABLE(serio, extron_serio_ids); |
| |
| static struct serio_driver extron_drv = { |
| .driver = { |
| .name = "extron-da-hd-4k-plus-cec", |
| }, |
| .description = "Extron DA HD 4K PLUS HDMI CEC driver", |
| .id_table = extron_serio_ids, |
| .interrupt = extron_interrupt, |
| .connect = extron_connect, |
| .disconnect = extron_disconnect, |
| }; |
| |
| module_serio_driver(extron_drv); |