diff options
-rw-r--r-- | drivers/thunderbolt/debugfs.c | 295 | ||||
-rw-r--r-- | drivers/thunderbolt/sb_regs.h | 8 | ||||
-rw-r--r-- | drivers/thunderbolt/tb.h | 2 | ||||
-rw-r--r-- | drivers/thunderbolt/usb4.c | 1 |
4 files changed, 290 insertions, 16 deletions
diff --git a/drivers/thunderbolt/debugfs.c b/drivers/thunderbolt/debugfs.c index 9defa69ef3a7..350310bd0fee 100644 --- a/drivers/thunderbolt/debugfs.c +++ b/drivers/thunderbolt/debugfs.c @@ -9,6 +9,7 @@ #include <linux/bitfield.h> #include <linux/debugfs.h> +#include <linux/delay.h> #include <linux/pm_runtime.h> #include <linux/uaccess.h> @@ -34,6 +35,14 @@ #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 + /* Sideband registers and their sizes as defined in the USB4 spec */ struct sb_reg { unsigned int reg; @@ -399,6 +408,9 @@ out: * 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 @@ -422,12 +434,33 @@ struct tb_margining { 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; }; +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) { return margining->caps[0] & USB4_MARGIN_CAP_0_MODES_SW; @@ -689,9 +722,165 @@ static int margining_lanes_show(struct seq_file *s, void *not_used) DEBUGFS_ATTR_RW(margining_lanes); static ssize_t -margining_optional_voltage_offset_write(struct file *file, - const char __user *user_buf, - size_t count, loff_t *ppos) +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; @@ -796,6 +985,51 @@ static int margining_mode_show(struct seq_file *s, void *not_used) } 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_MARGIN_SW_LANE_0) + errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_0_MASK, + margining->results[1]); + else if (margining->lanes == USB4_MARGIN_SW_LANE_1) + errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_1_MASK, + margining->results[1]); + else if (margining->lanes == USB4_MARGIN_SW_ALL_LANES) + 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 margining_run_write(void *data, u64 val) { struct tb_margining *margining = data; @@ -836,11 +1070,15 @@ static int margining_run_write(void *data, u64 val) 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, .optional_voltage_offset_range = margining->optional_voltage_offset_range, }; @@ -850,14 +1088,7 @@ static int margining_run_write(void *data, u64 val) margining->time ? "time" : "voltage", dev_name(dev), margining->lanes); - ret = usb4_port_sw_margin(port, margining->target, margining->index, ¶ms, - &margining->results[0]); - if (ret) - goto out_clx; - - ret = usb4_port_sw_margin_errors(port, margining->target, - margining->index, - &margining->results[0]); + ret = margining_run_sw(margining, ¶ms); } else { struct usb4_port_margining_params params = { .ber_level = margining->ber_level, @@ -867,10 +1098,6 @@ static int margining_run_write(void *data, u64 val) .optional_voltage_offset_range = margining->optional_voltage_offset_range, }; - /* Clear the results */ - margining->results[0] = 0; - margining->results[1] = 0; - tb_port_dbg(port, "running hardware %s lane margining for %s lanes %u\n", margining->time ? "time" : "voltage", dev_name(dev), @@ -880,7 +1107,6 @@ static int margining_run_write(void *data, u64 val) margining->results); } -out_clx: if (down_sw) tb_switch_clx_enable(down_sw, clx); out_unlock: @@ -909,6 +1135,13 @@ static ssize_t margining_results_write(struct file *file, margining->results[0] = 0; margining->results[1] = 0; + if (margining->software) { + /* Clear the error counters */ + margining_modify_error_counter(margining, + USB4_MARGIN_SW_ALL_LANES, + USB4_MARGIN_SW_ERROR_COUNTER_CLEAR); + } + mutex_unlock(&tb->lock); return count; } @@ -998,6 +1231,24 @@ static int margining_results_show(struct seq_file *s, void *not_used) voltage_margin_show(s, margining, val); } } + } 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_MARGIN_SW_LANE_0 || + result == USB4_MARGIN_SW_ALL_LANES) { + 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_MARGIN_SW_LANE_1 || + result == USB4_MARGIN_SW_ALL_LANES) { + 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); + } } mutex_unlock(&tb->lock); @@ -1211,9 +1462,21 @@ static struct tb_margining *margining_alloc(struct tb_port *port, 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); + } return margining; } diff --git a/drivers/thunderbolt/sb_regs.h b/drivers/thunderbolt/sb_regs.h index 8bff0740222c..dbcad25ead50 100644 --- a/drivers/thunderbolt/sb_regs.h +++ b/drivers/thunderbolt/sb_regs.h @@ -86,9 +86,17 @@ enum usb4_sb_opcode { #define USB4_MARGIN_HW_RES_1_L1_LL_MARGIN_SHIFT 24 /* USB4_SB_OPCODE_RUN_SW_LANE_MARGINING */ +#define USB4_MARGIN_SW_LANES_MASK GENMASK(2, 0) +#define USB4_MARGIN_SW_LANE_0 0x0 +#define USB4_MARGIN_SW_LANE_1 0x1 +#define USB4_MARGIN_SW_ALL_LANES 0x7 #define USB4_MARGIN_SW_TIME BIT(3) #define USB4_MARGIN_SW_RH BIT(4) #define USB4_MARGIN_SW_OPT_VOLTAGE BIT(5) +#define USB4_MARGIN_SW_VT_MASK GENMASK(12, 6) #define USB4_MARGIN_SW_COUNTER_MASK GENMASK(14, 13) +#define USB4_MARGIN_SW_ERR_COUNTER_LANE_0_MASK GENMASK(3, 0) +#define USB4_MARGIN_SW_ERR_COUNTER_LANE_1_MASK GENMASK(7, 4) + #endif diff --git a/drivers/thunderbolt/tb.h b/drivers/thunderbolt/tb.h index 262c333924b8..6737188f2581 100644 --- a/drivers/thunderbolt/tb.h +++ b/drivers/thunderbolt/tb.h @@ -1372,6 +1372,7 @@ enum usb4_margin_sw_error_counter { * @error_counter: Error counter operation for software margining * @ber_level: Current BER level contour value * @lanes: %0, %1 or %7 (all) + * @voltage_time_offset: Offset for voltage / time for software margining * @optional_voltage_offset_range: Enable optional extended voltage range * @right_high: %false if left/low margin test is performed, %true if right/high * @time: %true if time margining is used instead of voltage @@ -1380,6 +1381,7 @@ struct usb4_port_margining_params { enum usb4_margin_sw_error_counter error_counter; u32 ber_level; u32 lanes; + u32 voltage_time_offset; bool optional_voltage_offset_range; bool right_high; bool time; diff --git a/drivers/thunderbolt/usb4.c b/drivers/thunderbolt/usb4.c index a2f81e17ad8d..0a9b4aeb3fa1 100644 --- a/drivers/thunderbolt/usb4.c +++ b/drivers/thunderbolt/usb4.c @@ -1723,6 +1723,7 @@ int usb4_port_sw_margin(struct tb_port *port, enum usb4_sb_target target, if (params->right_high) val |= USB4_MARGIN_SW_RH; val |= FIELD_PREP(USB4_MARGIN_SW_COUNTER_MASK, params->error_counter); + val |= FIELD_PREP(USB4_MARGIN_SW_VT_MASK, params->voltage_time_offset); ret = usb4_port_sb_write(port, target, index, USB4_SB_METADATA, &val, sizeof(val)); |