diff options
Diffstat (limited to 'drivers/thunderbolt/debugfs.c')
| -rw-r--r-- | drivers/thunderbolt/debugfs.c | 1852 |
1 files changed, 1834 insertions, 18 deletions
diff --git a/drivers/thunderbolt/debugfs.c b/drivers/thunderbolt/debugfs.c index c850b0ac098c..45266ec72f88 100644 --- a/drivers/thunderbolt/debugfs.c +++ b/drivers/thunderbolt/debugfs.c @@ -7,18 +7,26 @@ * Mika Westerberg <mika.westerberg@linux.intel.com> */ +#include <linux/array_size.h> +#include <linux/bitfield.h> #include <linux/debugfs.h> +#include <linux/delay.h> #include <linux/pm_runtime.h> +#include <linux/string_choices.h> #include <linux/uaccess.h> #include "tb.h" +#include "sb_regs.h" -#define PORT_CAP_PCIE_LEN 1 +#define PORT_CAP_V1_PCIE_LEN 1 +#define PORT_CAP_V2_PCIE_LEN 2 #define PORT_CAP_POWER_LEN 2 #define PORT_CAP_LANE_LEN 3 #define PORT_CAP_USB3_LEN 5 -#define PORT_CAP_DP_LEN 8 -#define PORT_CAP_TMU_LEN 8 +#define PORT_CAP_DP_V1_LEN 9 +#define PORT_CAP_DP_V2_LEN 14 +#define PORT_CAP_TMU_V1_LEN 8 +#define PORT_CAP_TMU_V2_LEN 10 #define PORT_CAP_BASIC_LEN 9 #define PORT_CAP_USB4_LEN 20 @@ -29,6 +37,70 @@ #define COUNTER_SET_LEN 3 +/* + * USB4 spec doesn't specify dwell range, the range of 100 ms to 500 ms + * probed to give good results. + */ +#define MIN_DWELL_TIME 100 /* ms */ +#define MAX_DWELL_TIME 500 /* ms */ +#define DWELL_SAMPLE_INTERVAL 10 + +enum usb4_margin_cap_voltage_indp { + USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_2_3_MIN, + USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_2_3_HL, + USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_2_3_BOTH, + USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_4_MIN, + USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_4_BOTH, + USB4_MARGIN_CAP_VOLTAGE_INDP_UNKNOWN, +}; + +enum usb4_margin_cap_time_indp { + USB4_MARGIN_CAP_TIME_INDP_GEN_2_3_MIN, + USB4_MARGIN_CAP_TIME_INDP_GEN_2_3_LR, + USB4_MARGIN_CAP_TIME_INDP_GEN_2_3_BOTH, + USB4_MARGIN_CAP_TIME_INDP_GEN_4_MIN, + USB4_MARGIN_CAP_TIME_INDP_GEN_4_BOTH, + USB4_MARGIN_CAP_TIME_INDP_UNKNOWN, +}; + +/* Sideband registers and their sizes as defined in the USB4 spec */ +struct sb_reg { + unsigned int reg; + unsigned int size; +}; + +#define SB_MAX_SIZE 64 + +/* Sideband registers for router */ +static const struct sb_reg port_sb_regs[] = { + { USB4_SB_VENDOR_ID, 4 }, + { USB4_SB_PRODUCT_ID, 4 }, + { USB4_SB_DEBUG_CONF, 4 }, + { USB4_SB_DEBUG, 54 }, + { USB4_SB_LRD_TUNING, 4 }, + { USB4_SB_OPCODE, 4 }, + { USB4_SB_METADATA, 4 }, + { USB4_SB_LINK_CONF, 3 }, + { USB4_SB_GEN23_TXFFE, 4 }, + { USB4_SB_GEN4_TXFFE, 4 }, + { USB4_SB_VERSION, 4 }, + { USB4_SB_DATA, 64 }, +}; + +/* Sideband registers for retimer */ +static const struct sb_reg retimer_sb_regs[] = { + { USB4_SB_VENDOR_ID, 4 }, + { USB4_SB_PRODUCT_ID, 4 }, + { USB4_SB_FW_VERSION, 4 }, + { USB4_SB_LRD_TUNING, 4 }, + { USB4_SB_OPCODE, 4 }, + { USB4_SB_METADATA, 4 }, + { USB4_SB_GEN23_TXFFE, 4 }, + { USB4_SB_GEN4_TXFFE, 4 }, + { USB4_SB_VERSION, 4 }, + { USB4_SB_DATA, 64 }, +}; + #define DEBUGFS_ATTR(__space, __write) \ static int __space ## _open(struct inode *inode, struct file *file) \ { \ @@ -97,6 +169,13 @@ static bool parse_line(char **line, u32 *offs, u32 *val, int short_fmt_len, * offset relative_offset cap_id vs_cap_id value\n * v[0] v[1] v[2] v[3] v[4] * + * For Path configuration space: + * Short format is: offset value\n + * v[0] v[1] + * Long format as produced from the read side: + * offset relative_offset in_hop_id value\n + * v[0] v[1] v[2] v[3] + * * For Counter configuration space: * Short format is: offset\n * v[0] @@ -120,14 +199,33 @@ static bool parse_line(char **line, u32 *offs, u32 *val, int short_fmt_len, } #if IS_ENABLED(CONFIG_USB4_DEBUGFS_WRITE) +/* + * Path registers need to be written in double word pairs and they both must be + * read before written. This writes one double word in path config space + * following the spec flow. + */ +static int path_write_one(struct tb_port *port, u32 val, u32 offset) +{ + u32 index = offset % PATH_LEN; + u32 offs = offset - index; + u32 data[PATH_LEN]; + int ret; + + ret = tb_port_read(port, data, TB_CFG_HOPS, offs, PATH_LEN); + if (ret) + return ret; + data[index] = val; + return tb_port_write(port, data, TB_CFG_HOPS, offs, PATH_LEN); +} + static ssize_t regs_write(struct tb_switch *sw, struct tb_port *port, - const char __user *user_buf, size_t count, - loff_t *ppos) + enum tb_cfg_space space, const char __user *user_buf, + size_t count, loff_t *ppos) { + int long_fmt_len, ret = 0; struct tb *tb = sw->tb; char *line, *buf; u32 val, offset; - int ret = 0; buf = validate_and_copy_from_user(user_buf, &count); if (IS_ERR(buf)) @@ -143,12 +241,21 @@ static ssize_t regs_write(struct tb_switch *sw, struct tb_port *port, /* User did hardware changes behind the driver's back */ add_taint(TAINT_USER, LOCKDEP_STILL_OK); + if (space == TB_CFG_HOPS) + long_fmt_len = 4; + else + long_fmt_len = 5; + line = buf; - while (parse_line(&line, &offset, &val, 2, 5)) { - if (port) - ret = tb_port_write(port, &val, TB_CFG_PORT, offset, 1); - else + while (parse_line(&line, &offset, &val, 2, long_fmt_len)) { + if (port) { + if (space == TB_CFG_HOPS) + ret = path_write_one(port, val, offset); + else + ret = tb_port_write(port, &val, space, offset, 1); + } else { ret = tb_sw_write(sw, &val, TB_CFG_SWITCH, offset, 1); + } if (ret) break; } @@ -169,7 +276,16 @@ static ssize_t port_regs_write(struct file *file, const char __user *user_buf, struct seq_file *s = file->private_data; struct tb_port *port = s->private; - return regs_write(port->sw, port, user_buf, count, ppos); + return regs_write(port->sw, port, TB_CFG_PORT, user_buf, count, ppos); +} + +static ssize_t path_write(struct file *file, const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct tb_port *port = s->private; + + return regs_write(port->sw, port, TB_CFG_HOPS, user_buf, count, ppos); } static ssize_t switch_regs_write(struct file *file, const char __user *user_buf, @@ -178,15 +294,1579 @@ static ssize_t switch_regs_write(struct file *file, const char __user *user_buf, struct seq_file *s = file->private_data; struct tb_switch *sw = s->private; - return regs_write(sw, NULL, user_buf, count, ppos); + return regs_write(sw, NULL, TB_CFG_SWITCH, user_buf, count, ppos); +} + +static bool parse_sb_line(char **line, u8 *reg, u8 *data, size_t data_size, + size_t *bytes_read) +{ + char *field, *token; + int i; + + token = strsep(line, "\n"); + if (!token) + return false; + + /* Parse the register first */ + field = strsep(&token, " "); + if (!field) + return false; + if (kstrtou8(field, 0, reg)) + return false; + + /* Then the values for the register, up to data_size */ + for (i = 0; i < data_size; i++) { + field = strsep(&token, " "); + if (!field) + break; + if (kstrtou8(field, 0, &data[i])) + return false; + } + + *bytes_read = i; + return true; +} + +static ssize_t sb_regs_write(struct tb_port *port, const struct sb_reg *sb_regs, + size_t size, enum usb4_sb_target target, u8 index, + char *buf, size_t count, loff_t *ppos) +{ + u8 reg, data[SB_MAX_SIZE]; + size_t bytes_read; + char *line = buf; + + /* User did hardware changes behind the driver's back */ + add_taint(TAINT_USER, LOCKDEP_STILL_OK); + + /* + * For sideband registers we accept: + * reg b0 b1 b2...\n + * + * Here "reg" is the byte offset of the sideband register and "b0".. + * are the byte values. There can be less byte values than the register + * size. The leftovers will not be overwritten. + */ + while (parse_sb_line(&line, ®, data, ARRAY_SIZE(data), &bytes_read)) { + const struct sb_reg *sb_reg; + int ret; + + /* At least one byte must be passed */ + if (bytes_read < 1) + return -EINVAL; + + /* Find the register */ + sb_reg = NULL; + for (int i = 0; i < size; i++) { + if (sb_regs[i].reg == reg) { + sb_reg = &sb_regs[i]; + break; + } + } + + if (!sb_reg) + return -EINVAL; + + if (bytes_read > sb_regs->size) + return -E2BIG; + + ret = usb4_port_sb_write(port, target, index, sb_reg->reg, data, + bytes_read); + if (ret) + return ret; + } + + return 0; +} + +static ssize_t port_sb_regs_write(struct file *file, const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct tb_port *port = s->private; + struct tb_switch *sw = port->sw; + struct tb *tb = sw->tb; + char *buf; + int ret; + + buf = validate_and_copy_from_user(user_buf, &count); + if (IS_ERR(buf)) + return PTR_ERR(buf); + + pm_runtime_get_sync(&sw->dev); + + if (mutex_lock_interruptible(&tb->lock)) { + ret = -ERESTARTSYS; + goto out; + } + + ret = sb_regs_write(port, port_sb_regs, ARRAY_SIZE(port_sb_regs), + USB4_SB_TARGET_ROUTER, 0, buf, count, ppos); + + mutex_unlock(&tb->lock); +out: + pm_runtime_mark_last_busy(&sw->dev); + pm_runtime_put_autosuspend(&sw->dev); + free_page((unsigned long)buf); + + return ret < 0 ? ret : count; +} + +static ssize_t retimer_sb_regs_write(struct file *file, + const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct tb_retimer *rt = s->private; + struct tb *tb = rt->tb; + char *buf; + int ret; + + buf = validate_and_copy_from_user(user_buf, &count); + if (IS_ERR(buf)) + return PTR_ERR(buf); + + pm_runtime_get_sync(&rt->dev); + + if (mutex_lock_interruptible(&tb->lock)) { + ret = -ERESTARTSYS; + goto out; + } + + ret = sb_regs_write(rt->port, retimer_sb_regs, ARRAY_SIZE(retimer_sb_regs), + USB4_SB_TARGET_RETIMER, rt->index, buf, count, ppos); + + mutex_unlock(&tb->lock); +out: + pm_runtime_mark_last_busy(&rt->dev); + pm_runtime_put_autosuspend(&rt->dev); + free_page((unsigned long)buf); + + return ret < 0 ? ret : count; } #define DEBUGFS_MODE 0600 #else #define port_regs_write NULL +#define path_write NULL #define switch_regs_write NULL +#define port_sb_regs_write NULL +#define retimer_sb_regs_write NULL #define DEBUGFS_MODE 0400 #endif +#if IS_ENABLED(CONFIG_USB4_DEBUGFS_MARGINING) +/** + * struct tb_margining - Lane margining support + * @port: USB4 port through which the margining operations are run + * @target: Sideband target + * @index: Retimer index if taget is %USB4_SB_TARGET_RETIMER + * @dev: Pointer to the device that is the target (USB4 port or retimer) + * @gen: Link generation + * @asym_rx: %true% if @port supports asymmetric link with 3 Rx + * @caps: Port lane margining capabilities + * @results: Last lane margining results + * @lanes: %0, %1 or %7 (all) + * @min_ber_level: Minimum supported BER level contour value + * @max_ber_level: Maximum supported BER level contour value + * @ber_level: Current BER level contour value + * @voltage_steps: Number of mandatory voltage steps + * @max_voltage_offset: Maximum mandatory voltage offset (in mV) + * @voltage_steps_optional_range: Number of voltage steps for optional range + * @max_voltage_offset_optional_range: Maximum voltage offset for the optional + * range (in mV). + * @time_steps: Number of time margin steps + * @max_time_offset: Maximum time margin offset (in mUI) + * @voltage_time_offset: Offset for voltage / time for software margining + * @dwell_time: Dwell time for software margining (in ms) + * @error_counter: Error counter operation for software margining + * @optional_voltage_offset_range: Enable optional extended voltage range + * @software: %true if software margining is used instead of hardware + * @time: %true if time margining is used instead of voltage + * @right_high: %false if left/low margin test is performed, %true if + * right/high + * @upper_eye: %false if the lower PAM3 eye is used, %true if the upper + * eye is used + */ +struct tb_margining { + struct tb_port *port; + enum usb4_sb_target target; + u8 index; + struct device *dev; + unsigned int gen; + bool asym_rx; + u32 caps[3]; + u32 results[3]; + enum usb4_margining_lane lanes; + unsigned int min_ber_level; + unsigned int max_ber_level; + unsigned int ber_level; + unsigned int voltage_steps; + unsigned int max_voltage_offset; + unsigned int voltage_steps_optional_range; + unsigned int max_voltage_offset_optional_range; + unsigned int time_steps; + unsigned int max_time_offset; + unsigned int voltage_time_offset; + unsigned int dwell_time; + enum usb4_margin_sw_error_counter error_counter; + bool optional_voltage_offset_range; + bool software; + bool time; + bool right_high; + bool upper_eye; +}; + +static int margining_modify_error_counter(struct tb_margining *margining, + u32 lanes, enum usb4_margin_sw_error_counter error_counter) +{ + struct usb4_port_margining_params params = { 0 }; + struct tb_port *port = margining->port; + u32 result; + + if (error_counter != USB4_MARGIN_SW_ERROR_COUNTER_CLEAR && + error_counter != USB4_MARGIN_SW_ERROR_COUNTER_STOP) + return -EOPNOTSUPP; + + params.error_counter = error_counter; + params.lanes = lanes; + + return usb4_port_sw_margin(port, margining->target, margining->index, + ¶ms, &result); +} + +static bool supports_software(const struct tb_margining *margining) +{ + if (margining->gen < 4) + return margining->caps[0] & USB4_MARGIN_CAP_0_MODES_SW; + return margining->caps[2] & USB4_MARGIN_CAP_2_MODES_SW; +} + +static bool supports_hardware(const struct tb_margining *margining) +{ + if (margining->gen < 4) + return margining->caps[0] & USB4_MARGIN_CAP_0_MODES_HW; + return margining->caps[2] & USB4_MARGIN_CAP_2_MODES_HW; +} + +static bool all_lanes(const struct tb_margining *margining) +{ + return margining->caps[0] & USB4_MARGIN_CAP_0_ALL_LANES; +} + +static enum usb4_margin_cap_voltage_indp +independent_voltage_margins(const struct tb_margining *margining) +{ + if (margining->gen < 4) { + switch (FIELD_GET(USB4_MARGIN_CAP_0_VOLTAGE_INDP_MASK, margining->caps[0])) { + case USB4_MARGIN_CAP_0_VOLTAGE_MIN: + return USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_2_3_MIN; + case USB4_MARGIN_CAP_0_VOLTAGE_HL: + return USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_2_3_HL; + case USB4_MARGIN_CAP_1_TIME_BOTH: + return USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_2_3_BOTH; + } + } else { + switch (FIELD_GET(USB4_MARGIN_CAP_2_VOLTAGE_INDP_MASK, margining->caps[2])) { + case USB4_MARGIN_CAP_2_VOLTAGE_MIN: + return USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_4_MIN; + case USB4_MARGIN_CAP_2_VOLTAGE_BOTH: + return USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_4_BOTH; + } + } + return USB4_MARGIN_CAP_VOLTAGE_INDP_UNKNOWN; +} + +static bool supports_time(const struct tb_margining *margining) +{ + if (margining->gen < 4) + return margining->caps[0] & USB4_MARGIN_CAP_0_TIME; + return margining->caps[2] & USB4_MARGIN_CAP_2_TIME; +} + +/* Only applicable if supports_time() returns true */ +static enum usb4_margin_cap_time_indp +independent_time_margins(const struct tb_margining *margining) +{ + if (margining->gen < 4) { + switch (FIELD_GET(USB4_MARGIN_CAP_1_TIME_INDP_MASK, margining->caps[1])) { + case USB4_MARGIN_CAP_1_TIME_MIN: + return USB4_MARGIN_CAP_TIME_INDP_GEN_2_3_MIN; + case USB4_MARGIN_CAP_1_TIME_LR: + return USB4_MARGIN_CAP_TIME_INDP_GEN_2_3_LR; + case USB4_MARGIN_CAP_1_TIME_BOTH: + return USB4_MARGIN_CAP_TIME_INDP_GEN_2_3_BOTH; + } + } else { + switch (FIELD_GET(USB4_MARGIN_CAP_2_TIME_INDP_MASK, margining->caps[2])) { + case USB4_MARGIN_CAP_2_TIME_MIN: + return USB4_MARGIN_CAP_TIME_INDP_GEN_4_MIN; + case USB4_MARGIN_CAP_2_TIME_BOTH: + return USB4_MARGIN_CAP_TIME_INDP_GEN_4_BOTH; + } + } + return USB4_MARGIN_CAP_TIME_INDP_UNKNOWN; +} + +static bool +supports_optional_voltage_offset_range(const struct tb_margining *margining) +{ + return margining->caps[0] & USB4_MARGIN_CAP_0_OPT_VOLTAGE_SUPPORT; +} + +static ssize_t +margining_ber_level_write(struct file *file, const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + unsigned int val; + int ret = 0; + char *buf; + + if (mutex_lock_interruptible(&tb->lock)) + return -ERESTARTSYS; + + if (margining->software) { + ret = -EINVAL; + goto out_unlock; + } + + buf = validate_and_copy_from_user(user_buf, &count); + if (IS_ERR(buf)) { + ret = PTR_ERR(buf); + goto out_unlock; + } + + buf[count - 1] = '\0'; + + ret = kstrtouint(buf, 10, &val); + if (ret) + goto out_free; + + if (val < margining->min_ber_level || + val > margining->max_ber_level) { + ret = -EINVAL; + goto out_free; + } + + margining->ber_level = val; + +out_free: + free_page((unsigned long)buf); +out_unlock: + mutex_unlock(&tb->lock); + + return ret < 0 ? ret : count; +} + +static void ber_level_show(struct seq_file *s, unsigned int val) +{ + if (val % 2) + seq_printf(s, "3 * 1e%d (%u)\n", -12 + (val + 1) / 2, val); + else + seq_printf(s, "1e%d (%u)\n", -12 + val / 2, val); +} + +static int margining_ber_level_show(struct seq_file *s, void *not_used) +{ + const struct tb_margining *margining = s->private; + + if (margining->software) + return -EINVAL; + ber_level_show(s, margining->ber_level); + return 0; +} +DEBUGFS_ATTR_RW(margining_ber_level); + +static int margining_caps_show(struct seq_file *s, void *not_used) +{ + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + int ret = 0; + + if (mutex_lock_interruptible(&tb->lock)) + return -ERESTARTSYS; + + /* Dump the raw caps first */ + for (int i = 0; i < ARRAY_SIZE(margining->caps); i++) + seq_printf(s, "0x%08x\n", margining->caps[i]); + + seq_printf(s, "# software margining: %s\n", + str_yes_no(supports_software(margining))); + if (supports_hardware(margining)) { + seq_puts(s, "# hardware margining: yes\n"); + seq_puts(s, "# minimum BER level contour: "); + ber_level_show(s, margining->min_ber_level); + seq_puts(s, "# maximum BER level contour: "); + ber_level_show(s, margining->max_ber_level); + } else { + seq_puts(s, "# hardware margining: no\n"); + } + + seq_printf(s, "# all lanes simultaneously: %s\n", + str_yes_no(all_lanes(margining))); + seq_printf(s, "# voltage margin steps: %u\n", + margining->voltage_steps); + seq_printf(s, "# maximum voltage offset: %u mV\n", + margining->max_voltage_offset); + seq_printf(s, "# optional voltage offset range support: %s\n", + str_yes_no(supports_optional_voltage_offset_range(margining))); + if (supports_optional_voltage_offset_range(margining)) { + seq_printf(s, "# voltage margin steps, optional range: %u\n", + margining->voltage_steps_optional_range); + seq_printf(s, "# maximum voltage offset, optional range: %u mV\n", + margining->max_voltage_offset_optional_range); + } + + switch (independent_voltage_margins(margining)) { + case USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_2_3_MIN: + seq_puts(s, "# returns minimum between high and low voltage margins\n"); + break; + case USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_2_3_HL: + seq_puts(s, "# returns high or low voltage margin\n"); + break; + case USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_2_3_BOTH: + seq_puts(s, "# returns both high and low margins\n"); + break; + case USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_4_MIN: + seq_puts(s, "# returns minimum between high and low voltage margins in both lower and upper eye\n"); + break; + case USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_4_BOTH: + seq_puts(s, "# returns both high and low margins of both upper and lower eye\n"); + break; + case USB4_MARGIN_CAP_VOLTAGE_INDP_UNKNOWN: + tb_port_warn(margining->port, + "failed to parse independent voltage margining capabilities\n"); + ret = -EIO; + goto out; + } + + if (supports_time(margining)) { + seq_puts(s, "# time margining: yes\n"); + seq_printf(s, "# time margining is destructive: %s\n", + str_yes_no(margining->caps[1] & USB4_MARGIN_CAP_1_TIME_DESTR)); + + switch (independent_time_margins(margining)) { + case USB4_MARGIN_CAP_TIME_INDP_GEN_2_3_MIN: + seq_puts(s, "# returns minimum between left and right time margins\n"); + break; + case USB4_MARGIN_CAP_TIME_INDP_GEN_2_3_LR: + seq_puts(s, "# returns left or right margin\n"); + break; + case USB4_MARGIN_CAP_TIME_INDP_GEN_2_3_BOTH: + seq_puts(s, "# returns both left and right margins\n"); + break; + case USB4_MARGIN_CAP_TIME_INDP_GEN_4_MIN: + seq_puts(s, "# returns minimum between left and right time margins in both lower and upper eye\n"); + break; + case USB4_MARGIN_CAP_TIME_INDP_GEN_4_BOTH: + seq_puts(s, "# returns both left and right margins of both upper and lower eye\n"); + break; + case USB4_MARGIN_CAP_TIME_INDP_UNKNOWN: + tb_port_warn(margining->port, + "failed to parse independent time margining capabilities\n"); + ret = -EIO; + goto out; + } + + seq_printf(s, "# time margin steps: %u\n", + margining->time_steps); + seq_printf(s, "# maximum time offset: %u mUI\n", + margining->max_time_offset); + } else { + seq_puts(s, "# time margining: no\n"); + } + +out: + mutex_unlock(&tb->lock); + return ret; +} +DEBUGFS_ATTR_RO(margining_caps); + +static const struct { + enum usb4_margining_lane lane; + const char *name; +} lane_names[] = { + { + .lane = USB4_MARGINING_LANE_RX0, + .name = "0", + }, + { + .lane = USB4_MARGINING_LANE_RX1, + .name = "1", + }, + { + .lane = USB4_MARGINING_LANE_RX2, + .name = "2", + }, + { + .lane = USB4_MARGINING_LANE_ALL, + .name = "all", + }, +}; + +static ssize_t +margining_lanes_write(struct file *file, const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct tb_margining *margining = s->private; + struct tb_port *port = margining->port; + struct tb *tb = port->sw->tb; + int lane = -1; + char *buf; + + buf = validate_and_copy_from_user(user_buf, &count); + if (IS_ERR(buf)) + return PTR_ERR(buf); + + buf[count - 1] = '\0'; + + for (int i = 0; i < ARRAY_SIZE(lane_names); i++) { + if (!strcmp(buf, lane_names[i].name)) { + lane = lane_names[i].lane; + break; + } + } + + free_page((unsigned long)buf); + + if (lane == -1) + return -EINVAL; + + scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &tb->lock) { + if (lane == USB4_MARGINING_LANE_ALL && !all_lanes(margining)) + return -EINVAL; + /* + * Enabling on RX2 requires that it is supported by the + * USB4 port. + */ + if (lane == USB4_MARGINING_LANE_RX2 && !margining->asym_rx) + return -EINVAL; + + margining->lanes = lane; + } + + return count; +} + +static int margining_lanes_show(struct seq_file *s, void *not_used) +{ + struct tb_margining *margining = s->private; + struct tb_port *port = margining->port; + struct tb *tb = port->sw->tb; + + scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &tb->lock) { + for (int i = 0; i < ARRAY_SIZE(lane_names); i++) { + if (lane_names[i].lane == USB4_MARGINING_LANE_ALL && + !all_lanes(margining)) + continue; + if (lane_names[i].lane == USB4_MARGINING_LANE_RX2 && + !margining->asym_rx) + continue; + + if (i != 0) + seq_putc(s, ' '); + + if (lane_names[i].lane == margining->lanes) + seq_printf(s, "[%s]", lane_names[i].name); + else + seq_printf(s, "%s", lane_names[i].name); + } + seq_puts(s, "\n"); + } + + return 0; +} +DEBUGFS_ATTR_RW(margining_lanes); + +static ssize_t +margining_voltage_time_offset_write(struct file *file, + const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + unsigned int max_margin; + unsigned int val; + int ret; + + ret = kstrtouint_from_user(user_buf, count, 10, &val); + if (ret) + return ret; + + scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &tb->lock) { + if (!margining->software) + return -EOPNOTSUPP; + + if (margining->time) + max_margin = margining->time_steps; + else + if (margining->optional_voltage_offset_range) + max_margin = margining->voltage_steps_optional_range; + else + max_margin = margining->voltage_steps; + + margining->voltage_time_offset = clamp(val, 0, max_margin); + } + + return count; +} + +static int margining_voltage_time_offset_show(struct seq_file *s, + void *not_used) +{ + const struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + + scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &tb->lock) { + if (!margining->software) + return -EOPNOTSUPP; + + seq_printf(s, "%d\n", margining->voltage_time_offset); + } + + return 0; +} +DEBUGFS_ATTR_RW(margining_voltage_time_offset); + +static ssize_t +margining_error_counter_write(struct file *file, const char __user *user_buf, + size_t count, loff_t *ppos) +{ + enum usb4_margin_sw_error_counter error_counter; + struct seq_file *s = file->private_data; + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + char *buf; + + buf = validate_and_copy_from_user(user_buf, &count); + if (IS_ERR(buf)) + return PTR_ERR(buf); + + buf[count - 1] = '\0'; + + if (!strcmp(buf, "nop")) + error_counter = USB4_MARGIN_SW_ERROR_COUNTER_NOP; + else if (!strcmp(buf, "clear")) + error_counter = USB4_MARGIN_SW_ERROR_COUNTER_CLEAR; + else if (!strcmp(buf, "start")) + error_counter = USB4_MARGIN_SW_ERROR_COUNTER_START; + else if (!strcmp(buf, "stop")) + error_counter = USB4_MARGIN_SW_ERROR_COUNTER_STOP; + else + return -EINVAL; + + scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &tb->lock) { + if (!margining->software) + return -EOPNOTSUPP; + + margining->error_counter = error_counter; + } + + return count; +} + +static int margining_error_counter_show(struct seq_file *s, void *not_used) +{ + const struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + + scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &tb->lock) { + if (!margining->software) + return -EOPNOTSUPP; + + switch (margining->error_counter) { + case USB4_MARGIN_SW_ERROR_COUNTER_NOP: + seq_puts(s, "[nop] clear start stop\n"); + break; + case USB4_MARGIN_SW_ERROR_COUNTER_CLEAR: + seq_puts(s, "nop [clear] start stop\n"); + break; + case USB4_MARGIN_SW_ERROR_COUNTER_START: + seq_puts(s, "nop clear [start] stop\n"); + break; + case USB4_MARGIN_SW_ERROR_COUNTER_STOP: + seq_puts(s, "nop clear start [stop]\n"); + break; + } + } + + return 0; +} +DEBUGFS_ATTR_RW(margining_error_counter); + +static ssize_t +margining_dwell_time_write(struct file *file, const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + unsigned int val; + int ret; + + ret = kstrtouint_from_user(user_buf, count, 10, &val); + if (ret) + return ret; + + scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &tb->lock) { + if (!margining->software) + return -EOPNOTSUPP; + + margining->dwell_time = clamp(val, MIN_DWELL_TIME, MAX_DWELL_TIME); + } + + return count; +} + +static int margining_dwell_time_show(struct seq_file *s, void *not_used) +{ + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + + scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &tb->lock) { + if (!margining->software) + return -EOPNOTSUPP; + + seq_printf(s, "%d\n", margining->dwell_time); + } + + return 0; +} +DEBUGFS_ATTR_RW(margining_dwell_time); + +static ssize_t +margining_optional_voltage_offset_write(struct file *file, const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + bool val; + int ret; + + ret = kstrtobool_from_user(user_buf, count, &val); + if (ret) + return ret; + + scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &tb->lock) { + margining->optional_voltage_offset_range = val; + } + + return count; +} + +static int margining_optional_voltage_offset_show(struct seq_file *s, + void *not_used) +{ + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + + scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &tb->lock) { + seq_printf(s, "%u\n", margining->optional_voltage_offset_range); + } + + return 0; +} +DEBUGFS_ATTR_RW(margining_optional_voltage_offset); + +static ssize_t margining_mode_write(struct file *file, + const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + int ret = 0; + char *buf; + + buf = validate_and_copy_from_user(user_buf, &count); + if (IS_ERR(buf)) + return PTR_ERR(buf); + + buf[count - 1] = '\0'; + + if (mutex_lock_interruptible(&tb->lock)) { + ret = -ERESTARTSYS; + goto out_free; + } + + if (!strcmp(buf, "software")) { + if (supports_software(margining)) + margining->software = true; + else + ret = -EINVAL; + } else if (!strcmp(buf, "hardware")) { + if (supports_hardware(margining)) + margining->software = false; + else + ret = -EINVAL; + } else { + ret = -EINVAL; + } + + mutex_unlock(&tb->lock); + +out_free: + free_page((unsigned long)buf); + return ret ? ret : count; +} + +static int margining_mode_show(struct seq_file *s, void *not_used) +{ + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + const char *space = ""; + + if (mutex_lock_interruptible(&tb->lock)) + return -ERESTARTSYS; + + if (supports_software(margining)) { + if (margining->software) + seq_puts(s, "[software]"); + else + seq_puts(s, "software"); + space = " "; + } + if (supports_hardware(margining)) { + if (margining->software) + seq_printf(s, "%shardware", space); + else + seq_printf(s, "%s[hardware]", space); + } + + mutex_unlock(&tb->lock); + + seq_puts(s, "\n"); + return 0; +} +DEBUGFS_ATTR_RW(margining_mode); + +static int margining_run_sw(struct tb_margining *margining, + struct usb4_port_margining_params *params) +{ + u32 nsamples = margining->dwell_time / DWELL_SAMPLE_INTERVAL; + int ret, i; + + ret = usb4_port_sw_margin(margining->port, margining->target, margining->index, + params, margining->results); + if (ret) + goto out_stop; + + for (i = 0; i <= nsamples; i++) { + u32 errors = 0; + + ret = usb4_port_sw_margin_errors(margining->port, margining->target, + margining->index, &margining->results[1]); + if (ret) + break; + + if (margining->lanes == USB4_MARGINING_LANE_RX0) + errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_0_MASK, + margining->results[1]); + else if (margining->lanes == USB4_MARGINING_LANE_RX1) + errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_1_MASK, + margining->results[1]); + else if (margining->lanes == USB4_MARGINING_LANE_RX2) + errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_2_MASK, + margining->results[1]); + else if (margining->lanes == USB4_MARGINING_LANE_ALL) + errors = margining->results[1]; + + /* Any errors stop the test */ + if (errors) + break; + + fsleep(DWELL_SAMPLE_INTERVAL * USEC_PER_MSEC); + } + +out_stop: + /* + * Stop the counters but don't clear them to allow the + * different error counter configurations. + */ + margining_modify_error_counter(margining, margining->lanes, + USB4_MARGIN_SW_ERROR_COUNTER_STOP); + return ret; +} + +static int validate_margining(struct tb_margining *margining) +{ + /* + * For running on RX2 the link must be asymmetric with 3 + * receivers. Because this can change dynamically, check it + * here before we start the margining and report back error if + * expectations are not met. + */ + if (margining->lanes == USB4_MARGINING_LANE_RX2) { + int ret; + + ret = tb_port_get_link_width(margining->port); + if (ret < 0) + return ret; + if (ret != TB_LINK_WIDTH_ASYM_RX) { + tb_port_warn(margining->port, "link is %s expected %s", + tb_width_name(ret), + tb_width_name(TB_LINK_WIDTH_ASYM_RX)); + return -EINVAL; + } + } + + return 0; +} + +static int margining_run_write(void *data, u64 val) +{ + struct tb_margining *margining = data; + struct tb_port *port = margining->port; + struct device *dev = margining->dev; + struct tb_switch *sw = port->sw; + struct tb_switch *down_sw; + struct tb *tb = sw->tb; + int ret, clx; + + if (val != 1) + return -EINVAL; + + pm_runtime_get_sync(dev); + + if (mutex_lock_interruptible(&tb->lock)) { + ret = -ERESTARTSYS; + goto out_rpm_put; + } + + ret = validate_margining(margining); + if (ret) + goto out_unlock; + + if (tb_is_upstream_port(port)) + down_sw = sw; + else if (port->remote) + down_sw = port->remote->sw; + else + down_sw = NULL; + + if (down_sw) { + /* + * CL states may interfere with lane margining so + * disable them temporarily now. + */ + ret = tb_switch_clx_disable(down_sw); + if (ret < 0) { + tb_sw_warn(down_sw, "failed to disable CL states\n"); + goto out_unlock; + } + clx = ret; + } + + /* Clear the results */ + memset(margining->results, 0, sizeof(margining->results)); + + if (margining->software) { + struct usb4_port_margining_params params = { + .error_counter = USB4_MARGIN_SW_ERROR_COUNTER_CLEAR, + .lanes = margining->lanes, + .time = margining->time, + .voltage_time_offset = margining->voltage_time_offset, + .right_high = margining->right_high, + .upper_eye = margining->upper_eye, + .optional_voltage_offset_range = margining->optional_voltage_offset_range, + }; + + tb_port_dbg(port, + "running software %s lane margining for %s lanes %u\n", + margining->time ? "time" : "voltage", dev_name(dev), + margining->lanes); + + ret = margining_run_sw(margining, ¶ms); + } else { + struct usb4_port_margining_params params = { + .ber_level = margining->ber_level, + .lanes = margining->lanes, + .time = margining->time, + .right_high = margining->right_high, + .upper_eye = margining->upper_eye, + .optional_voltage_offset_range = margining->optional_voltage_offset_range, + }; + + tb_port_dbg(port, + "running hardware %s lane margining for %s lanes %u\n", + margining->time ? "time" : "voltage", dev_name(dev), + margining->lanes); + + ret = usb4_port_hw_margin(port, margining->target, margining->index, ¶ms, + margining->results, ARRAY_SIZE(margining->results)); + } + + if (down_sw) + tb_switch_clx_enable(down_sw, clx); +out_unlock: + mutex_unlock(&tb->lock); +out_rpm_put: + pm_runtime_mark_last_busy(dev); + pm_runtime_put_autosuspend(dev); + + return ret; +} +DEFINE_DEBUGFS_ATTRIBUTE(margining_run_fops, NULL, margining_run_write, + "%llu\n"); + +static ssize_t margining_results_write(struct file *file, + const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + + if (mutex_lock_interruptible(&tb->lock)) + return -ERESTARTSYS; + + /* Just clear the results */ + memset(margining->results, 0, sizeof(margining->results)); + + if (margining->software) { + /* Clear the error counters */ + margining_modify_error_counter(margining, + USB4_MARGINING_LANE_ALL, + USB4_MARGIN_SW_ERROR_COUNTER_CLEAR); + } + + mutex_unlock(&tb->lock); + return count; +} + +static void voltage_margin_show(struct seq_file *s, + const struct tb_margining *margining, u8 val) +{ + unsigned int tmp, voltage; + + tmp = FIELD_GET(USB4_MARGIN_HW_RES_MARGIN_MASK, val); + voltage = tmp * margining->max_voltage_offset / margining->voltage_steps; + seq_printf(s, "%u mV (%u)", voltage, tmp); + if (val & USB4_MARGIN_HW_RES_EXCEEDS) + seq_puts(s, " exceeds maximum"); + seq_puts(s, "\n"); + if (margining->optional_voltage_offset_range) + seq_puts(s, " optional voltage offset range enabled\n"); +} + +static void time_margin_show(struct seq_file *s, + const struct tb_margining *margining, u8 val) +{ + unsigned int tmp, interval; + + tmp = FIELD_GET(USB4_MARGIN_HW_RES_MARGIN_MASK, val); + interval = tmp * margining->max_time_offset / margining->time_steps; + seq_printf(s, "%u mUI (%u)", interval, tmp); + if (val & USB4_MARGIN_HW_RES_EXCEEDS) + seq_puts(s, " exceeds maximum"); + seq_puts(s, "\n"); +} + +static u8 margining_hw_result_val(const u32 *results, + enum usb4_margining_lane lane, + bool right_high) +{ + u32 val; + + if (lane == USB4_MARGINING_LANE_RX0) + val = results[1]; + else if (lane == USB4_MARGINING_LANE_RX1) + val = results[1] >> USB4_MARGIN_HW_RES_LANE_SHIFT; + else if (lane == USB4_MARGINING_LANE_RX2) + val = results[2]; + else + val = 0; + + return right_high ? val : val >> USB4_MARGIN_HW_RES_LL_SHIFT; +} + +static void margining_hw_result_format(struct seq_file *s, + const struct tb_margining *margining, + enum usb4_margining_lane lane) +{ + u8 val; + + if (margining->time) { + val = margining_hw_result_val(margining->results, lane, true); + seq_printf(s, "# lane %u right time margin: ", lane); + time_margin_show(s, margining, val); + val = margining_hw_result_val(margining->results, lane, false); + seq_printf(s, "# lane %u left time margin: ", lane); + time_margin_show(s, margining, val); + } else { + val = margining_hw_result_val(margining->results, lane, true); + seq_printf(s, "# lane %u high voltage margin: ", lane); + voltage_margin_show(s, margining, val); + val = margining_hw_result_val(margining->results, lane, false); + seq_printf(s, "# lane %u low voltage margin: ", lane); + voltage_margin_show(s, margining, val); + } +} + +static int margining_results_show(struct seq_file *s, void *not_used) +{ + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + + if (mutex_lock_interruptible(&tb->lock)) + return -ERESTARTSYS; + + /* Dump the raw results first */ + seq_printf(s, "0x%08x\n", margining->results[0]); + /* Only the hardware margining has two result dwords */ + if (!margining->software) { + for (int i = 1; i < ARRAY_SIZE(margining->results); i++) + seq_printf(s, "0x%08x\n", margining->results[i]); + + if (margining->lanes == USB4_MARGINING_LANE_ALL) { + margining_hw_result_format(s, margining, + USB4_MARGINING_LANE_RX0); + margining_hw_result_format(s, margining, + USB4_MARGINING_LANE_RX1); + if (margining->asym_rx) + margining_hw_result_format(s, margining, + USB4_MARGINING_LANE_RX2); + } else { + margining_hw_result_format(s, margining, + margining->lanes); + } + } else { + u32 lane_errors, result; + + seq_printf(s, "0x%08x\n", margining->results[1]); + + result = FIELD_GET(USB4_MARGIN_SW_LANES_MASK, margining->results[0]); + if (result == USB4_MARGINING_LANE_RX0 || + result == USB4_MARGINING_LANE_ALL) { + lane_errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_0_MASK, + margining->results[1]); + seq_printf(s, "# lane 0 errors: %u\n", lane_errors); + } + if (result == USB4_MARGINING_LANE_RX1 || + result == USB4_MARGINING_LANE_ALL) { + lane_errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_1_MASK, + margining->results[1]); + seq_printf(s, "# lane 1 errors: %u\n", lane_errors); + } + if (margining->asym_rx && + (result == USB4_MARGINING_LANE_RX2 || + result == USB4_MARGINING_LANE_ALL)) { + lane_errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_2_MASK, + margining->results[1]); + seq_printf(s, "# lane 2 errors: %u\n", lane_errors); + } + } + + mutex_unlock(&tb->lock); + return 0; +} +DEBUGFS_ATTR_RW(margining_results); + +static ssize_t margining_test_write(struct file *file, + const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + int ret = 0; + char *buf; + + buf = validate_and_copy_from_user(user_buf, &count); + if (IS_ERR(buf)) + return PTR_ERR(buf); + + buf[count - 1] = '\0'; + + if (mutex_lock_interruptible(&tb->lock)) { + ret = -ERESTARTSYS; + goto out_free; + } + + if (!strcmp(buf, "time") && supports_time(margining)) + margining->time = true; + else if (!strcmp(buf, "voltage")) + margining->time = false; + else + ret = -EINVAL; + + mutex_unlock(&tb->lock); + +out_free: + free_page((unsigned long)buf); + return ret ? ret : count; +} + +static int margining_test_show(struct seq_file *s, void *not_used) +{ + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + + if (mutex_lock_interruptible(&tb->lock)) + return -ERESTARTSYS; + + if (supports_time(margining)) { + if (margining->time) + seq_puts(s, "voltage [time]\n"); + else + seq_puts(s, "[voltage] time\n"); + } else { + seq_puts(s, "[voltage]\n"); + } + + mutex_unlock(&tb->lock); + return 0; +} +DEBUGFS_ATTR_RW(margining_test); + +static ssize_t margining_margin_write(struct file *file, + const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + int ret = 0; + char *buf; + + buf = validate_and_copy_from_user(user_buf, &count); + if (IS_ERR(buf)) + return PTR_ERR(buf); + + buf[count - 1] = '\0'; + + if (mutex_lock_interruptible(&tb->lock)) { + ret = -ERESTARTSYS; + goto out_free; + } + + if (margining->time) { + if (!strcmp(buf, "left")) + margining->right_high = false; + else if (!strcmp(buf, "right")) + margining->right_high = true; + else + ret = -EINVAL; + } else { + if (!strcmp(buf, "low")) + margining->right_high = false; + else if (!strcmp(buf, "high")) + margining->right_high = true; + else + ret = -EINVAL; + } + + mutex_unlock(&tb->lock); + +out_free: + free_page((unsigned long)buf); + return ret ? ret : count; +} + +static int margining_margin_show(struct seq_file *s, void *not_used) +{ + struct tb_margining *margining = s->private; + struct tb *tb = margining->port->sw->tb; + + if (mutex_lock_interruptible(&tb->lock)) + return -ERESTARTSYS; + + if (margining->time) { + if (margining->right_high) + seq_puts(s, "left [right]\n"); + else + seq_puts(s, "[left] right\n"); + } else { + if (margining->right_high) + seq_puts(s, "low [high]\n"); + else + seq_puts(s, "[low] high\n"); + } + + mutex_unlock(&tb->lock); + return 0; +} +DEBUGFS_ATTR_RW(margining_margin); + +static ssize_t margining_eye_write(struct file *file, + const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct tb_port *port = s->private; + struct usb4_port *usb4 = port->usb4; + struct tb *tb = port->sw->tb; + int ret = 0; + char *buf; + + buf = validate_and_copy_from_user(user_buf, &count); + if (IS_ERR(buf)) + return PTR_ERR(buf); + + buf[count - 1] = '\0'; + + scoped_cond_guard(mutex_intr, ret = -ERESTARTSYS, &tb->lock) { + if (!strcmp(buf, "lower")) + usb4->margining->upper_eye = false; + else if (!strcmp(buf, "upper")) + usb4->margining->upper_eye = true; + else + ret = -EINVAL; + } + + free_page((unsigned long)buf); + return ret ? ret : count; +} + +static int margining_eye_show(struct seq_file *s, void *not_used) +{ + struct tb_port *port = s->private; + struct usb4_port *usb4 = port->usb4; + struct tb *tb = port->sw->tb; + + scoped_guard(mutex_intr, &tb->lock) { + if (usb4->margining->upper_eye) + seq_puts(s, "lower [upper]\n"); + else + seq_puts(s, "[lower] upper\n"); + + return 0; + } + + return -ERESTARTSYS; +} +DEBUGFS_ATTR_RW(margining_eye); + +static struct tb_margining *margining_alloc(struct tb_port *port, + struct device *dev, + enum usb4_sb_target target, + u8 index, struct dentry *parent) +{ + struct tb_margining *margining; + struct dentry *dir; + unsigned int val; + int ret; + + ret = tb_port_get_link_generation(port); + if (ret < 0) { + tb_port_warn(port, "failed to read link generation\n"); + return NULL; + } + + margining = kzalloc(sizeof(*margining), GFP_KERNEL); + if (!margining) + return NULL; + + margining->port = port; + margining->target = target; + margining->index = index; + margining->dev = dev; + margining->gen = ret; + margining->asym_rx = tb_port_width_supported(port, TB_LINK_WIDTH_ASYM_RX); + + ret = usb4_port_margining_caps(port, target, index, margining->caps, + ARRAY_SIZE(margining->caps)); + if (ret) { + kfree(margining); + return NULL; + } + + /* Set the initial mode */ + if (supports_software(margining)) + margining->software = true; + + if (margining->gen < 4) { + val = FIELD_GET(USB4_MARGIN_CAP_0_VOLTAGE_STEPS_MASK, margining->caps[0]); + margining->voltage_steps = val; + val = FIELD_GET(USB4_MARGIN_CAP_0_MAX_VOLTAGE_OFFSET_MASK, margining->caps[0]); + margining->max_voltage_offset = 74 + val * 2; + } else { + val = FIELD_GET(USB4_MARGIN_CAP_2_VOLTAGE_STEPS_MASK, margining->caps[2]); + margining->voltage_steps = val; + val = FIELD_GET(USB4_MARGIN_CAP_2_MAX_VOLTAGE_OFFSET_MASK, margining->caps[2]); + margining->max_voltage_offset = 74 + val * 2; + } + + if (supports_optional_voltage_offset_range(margining)) { + val = FIELD_GET(USB4_MARGIN_CAP_0_VOLT_STEPS_OPT_MASK, + margining->caps[0]); + margining->voltage_steps_optional_range = val; + val = FIELD_GET(USB4_MARGIN_CAP_1_MAX_VOLT_OFS_OPT_MASK, + margining->caps[1]); + margining->max_voltage_offset_optional_range = 74 + val * 2; + } + + if (supports_time(margining)) { + val = FIELD_GET(USB4_MARGIN_CAP_1_TIME_STEPS_MASK, margining->caps[1]); + margining->time_steps = val; + val = FIELD_GET(USB4_MARGIN_CAP_1_TIME_OFFSET_MASK, margining->caps[1]); + /* + * Store it as mUI (milli Unit Interval) because we want + * to keep it as integer. + */ + margining->max_time_offset = 200 + 10 * val; + } + + dir = debugfs_create_dir("margining", parent); + if (supports_hardware(margining)) { + val = FIELD_GET(USB4_MARGIN_CAP_1_MIN_BER_MASK, margining->caps[1]); + margining->min_ber_level = val; + val = FIELD_GET(USB4_MARGIN_CAP_1_MAX_BER_MASK, margining->caps[1]); + margining->max_ber_level = val; + + /* Set the default to minimum */ + margining->ber_level = margining->min_ber_level; + + debugfs_create_file("ber_level_contour", 0400, dir, margining, + &margining_ber_level_fops); + } + debugfs_create_file("caps", 0400, dir, margining, &margining_caps_fops); + debugfs_create_file("lanes", 0600, dir, margining, &margining_lanes_fops); + debugfs_create_file("mode", 0600, dir, margining, &margining_mode_fops); + debugfs_create_file("run", 0600, dir, margining, &margining_run_fops); + debugfs_create_file("results", 0600, dir, margining, + &margining_results_fops); + debugfs_create_file("test", 0600, dir, margining, &margining_test_fops); + if (independent_voltage_margins(margining) == USB4_MARGIN_CAP_VOLTAGE_INDP_GEN_2_3_HL || + (supports_time(margining) && + independent_time_margins(margining) == USB4_MARGIN_CAP_TIME_INDP_GEN_2_3_LR)) + debugfs_create_file("margin", 0600, dir, margining, &margining_margin_fops); + + margining->error_counter = USB4_MARGIN_SW_ERROR_COUNTER_CLEAR; + margining->dwell_time = MIN_DWELL_TIME; + + if (supports_optional_voltage_offset_range(margining)) + debugfs_create_file("optional_voltage_offset", DEBUGFS_MODE, dir, margining, + &margining_optional_voltage_offset_fops); + + if (supports_software(margining)) { + debugfs_create_file("voltage_time_offset", DEBUGFS_MODE, dir, margining, + &margining_voltage_time_offset_fops); + debugfs_create_file("error_counter", DEBUGFS_MODE, dir, margining, + &margining_error_counter_fops); + debugfs_create_file("dwell_time", DEBUGFS_MODE, dir, margining, + &margining_dwell_time_fops); + } + + if (margining->gen >= 4) + debugfs_create_file("eye", 0600, dir, port, &margining_eye_fops); + + return margining; +} + +static void margining_port_init(struct tb_port *port) +{ + struct dentry *parent; + char dir_name[10]; + + if (!port->usb4) + return; + + snprintf(dir_name, sizeof(dir_name), "port%d", port->port); + parent = debugfs_lookup(dir_name, port->sw->debugfs_dir); + port->usb4->margining = margining_alloc(port, &port->usb4->dev, + USB4_SB_TARGET_ROUTER, 0, + parent); +} + +static void margining_port_remove(struct tb_port *port) +{ + struct dentry *parent; + char dir_name[10]; + + if (!port->usb4) + return; + + snprintf(dir_name, sizeof(dir_name), "port%d", port->port); + parent = debugfs_lookup(dir_name, port->sw->debugfs_dir); + if (parent) + debugfs_lookup_and_remove("margining", parent); + + kfree(port->usb4->margining); + port->usb4->margining = NULL; +} + +static void margining_switch_init(struct tb_switch *sw) +{ + struct tb_port *upstream, *downstream; + struct tb_switch *parent_sw; + u64 route = tb_route(sw); + + if (!route) + return; + + upstream = tb_upstream_port(sw); + parent_sw = tb_switch_parent(sw); + downstream = tb_port_at(route, parent_sw); + + margining_port_init(downstream); + margining_port_init(upstream); +} + +static void margining_switch_remove(struct tb_switch *sw) +{ + struct tb_port *upstream, *downstream; + struct tb_switch *parent_sw; + u64 route = tb_route(sw); + + if (!route) + return; + + upstream = tb_upstream_port(sw); + parent_sw = tb_switch_parent(sw); + downstream = tb_port_at(route, parent_sw); + + margining_port_remove(upstream); + margining_port_remove(downstream); +} + +static void margining_xdomain_init(struct tb_xdomain *xd) +{ + struct tb_switch *parent_sw; + struct tb_port *downstream; + + parent_sw = tb_xdomain_parent(xd); + downstream = tb_port_at(xd->route, parent_sw); + + margining_port_init(downstream); +} + +static void margining_xdomain_remove(struct tb_xdomain *xd) +{ + struct tb_switch *parent_sw; + struct tb_port *downstream; + + parent_sw = tb_xdomain_parent(xd); + downstream = tb_port_at(xd->route, parent_sw); + margining_port_remove(downstream); +} + +static void margining_retimer_init(struct tb_retimer *rt, struct dentry *debugfs_dir) +{ + rt->margining = margining_alloc(rt->port, &rt->dev, + USB4_SB_TARGET_RETIMER, rt->index, + debugfs_dir); +} + +static void margining_retimer_remove(struct tb_retimer *rt) +{ + kfree(rt->margining); + rt->margining = NULL; +} +#else +static inline void margining_switch_init(struct tb_switch *sw) { } +static inline void margining_switch_remove(struct tb_switch *sw) { } +static inline void margining_xdomain_init(struct tb_xdomain *xd) { } +static inline void margining_xdomain_remove(struct tb_xdomain *xd) { } +static inline void margining_retimer_init(struct tb_retimer *rt, + struct dentry *debugfs_dir) { } +static inline void margining_retimer_remove(struct tb_retimer *rt) { } +#endif + static int port_clear_all_counters(struct tb_port *port) { u32 *buf; @@ -325,7 +2005,10 @@ static void port_cap_show(struct tb_port *port, struct seq_file *s, break; case TB_PORT_CAP_TIME1: - length = PORT_CAP_TMU_LEN; + if (usb4_switch_version(port->sw) < 2) + length = PORT_CAP_TMU_V1_LEN; + else + length = PORT_CAP_TMU_V2_LEN; break; case TB_PORT_CAP_POWER: @@ -334,9 +2017,17 @@ static void port_cap_show(struct tb_port *port, struct seq_file *s, case TB_PORT_CAP_ADAP: if (tb_port_is_pcie_down(port) || tb_port_is_pcie_up(port)) { - length = PORT_CAP_PCIE_LEN; - } else if (tb_port_is_dpin(port) || tb_port_is_dpout(port)) { - length = PORT_CAP_DP_LEN; + if (usb4_switch_version(port->sw) < 2) + length = PORT_CAP_V1_PCIE_LEN; + else + length = PORT_CAP_V2_PCIE_LEN; + } else if (tb_port_is_dpin(port)) { + if (usb4_switch_version(port->sw) < 2) + length = PORT_CAP_DP_V1_LEN; + else + length = PORT_CAP_DP_V2_LEN; + } else if (tb_port_is_dpout(port)) { + length = PORT_CAP_DP_V1_LEN; } else if (tb_port_is_usb3_down(port) || tb_port_is_usb3_up(port)) { length = PORT_CAP_USB3_LEN; @@ -496,7 +2187,7 @@ static int switch_basic_regs_show(struct tb_switch *sw, struct seq_file *s) if (tb_switch_is_usb4(sw)) dwords = ARRAY_SIZE(data); else - dwords = 7; + dwords = 5; ret = tb_sw_read(sw, data, TB_CFG_SWITCH, 0, dwords); if (ret) @@ -598,7 +2289,7 @@ out_rpm_put: return ret; } -DEBUGFS_ATTR_RO(path); +DEBUGFS_ATTR_RW(path); static int counter_set_regs_show(struct tb_port *port, struct seq_file *s, int counter) @@ -654,6 +2345,60 @@ out: } DEBUGFS_ATTR_RW(counters); +static int sb_regs_show(struct tb_port *port, const struct sb_reg *sb_regs, + size_t size, enum usb4_sb_target target, u8 index, + struct seq_file *s) +{ + int ret, i; + + seq_puts(s, "# register value\n"); + + for (i = 0; i < size; i++) { + const struct sb_reg *regs = &sb_regs[i]; + u8 data[64]; + int j; + + memset(data, 0, sizeof(data)); + ret = usb4_port_sb_read(port, target, index, regs->reg, data, + regs->size); + if (ret) + return ret; + + seq_printf(s, "0x%02x", regs->reg); + for (j = 0; j < regs->size; j++) + seq_printf(s, " 0x%02x", data[j]); + seq_puts(s, "\n"); + } + + return 0; +} + +static int port_sb_regs_show(struct seq_file *s, void *not_used) +{ + struct tb_port *port = s->private; + struct tb_switch *sw = port->sw; + struct tb *tb = sw->tb; + int ret; + + pm_runtime_get_sync(&sw->dev); + + if (mutex_lock_interruptible(&tb->lock)) { + ret = -ERESTARTSYS; + goto out_rpm_put; + } + + ret = sb_regs_show(port, port_sb_regs, ARRAY_SIZE(port_sb_regs), + USB4_SB_TARGET_ROUTER, 0, s); + + mutex_unlock(&tb->lock); +out_rpm_put: + pm_runtime_mark_last_busy(&sw->dev); + pm_runtime_put_autosuspend(&sw->dev); + + return ret; +} +DEBUGFS_ATTR_RW(port_sb_regs); + /** * tb_switch_debugfs_init() - Add debugfs entries for router * @sw: Pointer to the router @@ -669,6 +2414,8 @@ void tb_switch_debugfs_init(struct tb_switch *sw) sw->debugfs_dir = debugfs_dir; debugfs_create_file("regs", DEBUGFS_MODE, debugfs_dir, sw, &switch_regs_fops); + if (sw->drom) + debugfs_create_blob("drom", 0400, debugfs_dir, &sw->drom_blob); tb_switch_for_each_port(sw, port) { struct dentry *debugfs_dir; @@ -688,7 +2435,12 @@ void tb_switch_debugfs_init(struct tb_switch *sw) if (port->config.counters_support) debugfs_create_file("counters", 0600, debugfs_dir, port, &counters_fops); + if (port->usb4) + debugfs_create_file("sb_regs", DEBUGFS_MODE, debugfs_dir, + port, &port_sb_regs_fops); } + + margining_switch_init(sw); } /** @@ -699,9 +2451,20 @@ void tb_switch_debugfs_init(struct tb_switch *sw) */ void tb_switch_debugfs_remove(struct tb_switch *sw) { + margining_switch_remove(sw); debugfs_remove_recursive(sw->debugfs_dir); } +void tb_xdomain_debugfs_init(struct tb_xdomain *xd) +{ + margining_xdomain_init(xd); +} + +void tb_xdomain_debugfs_remove(struct tb_xdomain *xd) +{ + margining_xdomain_remove(xd); +} + /** * tb_service_debugfs_init() - Add debugfs directory for service * @svc: Thunderbolt service pointer @@ -726,6 +2489,59 @@ void tb_service_debugfs_remove(struct tb_service *svc) svc->debugfs_dir = NULL; } +static int retimer_sb_regs_show(struct seq_file *s, void *not_used) +{ + struct tb_retimer *rt = s->private; + struct tb *tb = rt->tb; + int ret; + + pm_runtime_get_sync(&rt->dev); + + if (mutex_lock_interruptible(&tb->lock)) { + ret = -ERESTARTSYS; + goto out_rpm_put; + } + + ret = sb_regs_show(rt->port, retimer_sb_regs, ARRAY_SIZE(retimer_sb_regs), + USB4_SB_TARGET_RETIMER, rt->index, s); + + mutex_unlock(&tb->lock); +out_rpm_put: + pm_runtime_mark_last_busy(&rt->dev); + pm_runtime_put_autosuspend(&rt->dev); + + return ret; +} +DEBUGFS_ATTR_RW(retimer_sb_regs); + +/** + * tb_retimer_debugfs_init() - Add debugfs directory for retimer + * @rt: Pointer to retimer structure + * + * Adds and populates retimer debugfs directory. + */ +void tb_retimer_debugfs_init(struct tb_retimer *rt) +{ + struct dentry *debugfs_dir; + + debugfs_dir = debugfs_create_dir(dev_name(&rt->dev), tb_debugfs_root); + debugfs_create_file("sb_regs", DEBUGFS_MODE, debugfs_dir, rt, + &retimer_sb_regs_fops); + margining_retimer_init(rt, debugfs_dir); +} + +/** + * tb_retimer_debugfs_remove() - Remove retimer debugfs directory + * @rt: Pointer to retimer structure + * + * Removes the retimer debugfs directory along with its contents. + */ +void tb_retimer_debugfs_remove(struct tb_retimer *rt) +{ + debugfs_lookup_and_remove(dev_name(&rt->dev), tb_debugfs_root); + margining_retimer_remove(rt); +} + void tb_debugfs_init(void) { tb_debugfs_root = debugfs_create_dir("thunderbolt", NULL); |
