| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * CLx support | |
| * | |
| * Copyright (C) 2020 - 2023, Intel Corporation | |
| * Authors: Gil Fine <gil.fine@intel.com> | |
| * Mika Westerberg <mika.westerberg@linux.intel.com> | |
| */ | |
| #include <linux/module.h> | |
| #include "tb.h" | |
| static bool clx_enabled = true; | |
| module_param_named(clx, clx_enabled, bool, 0444); | |
| MODULE_PARM_DESC(clx, "allow low power states on the high-speed lanes (default: true)"); | |
| static const char *clx_name(unsigned int clx) | |
| { | |
| switch (clx) { | |
| case TB_CL0S | TB_CL1 | TB_CL2: | |
| return "CL0s/CL1/CL2"; | |
| case TB_CL1 | TB_CL2: | |
| return "CL1/CL2"; | |
| case TB_CL0S | TB_CL2: | |
| return "CL0s/CL2"; | |
| case TB_CL0S | TB_CL1: | |
| return "CL0s/CL1"; | |
| case TB_CL0S: | |
| return "CL0s"; | |
| case 0: | |
| return "disabled"; | |
| default: | |
| return "unknown"; | |
| } | |
| } | |
| static int tb_port_pm_secondary_set(struct tb_port *port, bool secondary) | |
| { | |
| u32 phy; | |
| int ret; | |
| ret = tb_port_read(port, &phy, TB_CFG_PORT, | |
| port->cap_phy + LANE_ADP_CS_1, 1); | |
| if (ret) | |
| return ret; | |
| if (secondary) | |
| phy |= LANE_ADP_CS_1_PMS; | |
| else | |
| phy &= ~LANE_ADP_CS_1_PMS; | |
| return tb_port_write(port, &phy, TB_CFG_PORT, | |
| port->cap_phy + LANE_ADP_CS_1, 1); | |
| } | |
| static int tb_port_pm_secondary_enable(struct tb_port *port) | |
| { | |
| return tb_port_pm_secondary_set(port, true); | |
| } | |
| static int tb_port_pm_secondary_disable(struct tb_port *port) | |
| { | |
| return tb_port_pm_secondary_set(port, false); | |
| } | |
| /* Called for USB4 or Titan Ridge routers only */ | |
| static bool tb_port_clx_supported(struct tb_port *port, unsigned int clx) | |
| { | |
| u32 val, mask = 0; | |
| bool ret; | |
| /* Don't enable CLx in case of two single-lane links */ | |
| if (!port->bonded && port->dual_link_port) | |
| return false; | |
| /* Don't enable CLx in case of inter-domain link */ | |
| if (port->xdomain) | |
| return false; | |
| if (tb_switch_is_usb4(port->sw)) { | |
| if (!usb4_port_clx_supported(port)) | |
| return false; | |
| } else if (!tb_lc_is_clx_supported(port)) { | |
| return false; | |
| } | |
| if (clx & TB_CL0S) | |
| mask |= LANE_ADP_CS_0_CL0S_SUPPORT; | |
| if (clx & TB_CL1) | |
| mask |= LANE_ADP_CS_0_CL1_SUPPORT; | |
| if (clx & TB_CL2) | |
| mask |= LANE_ADP_CS_0_CL2_SUPPORT; | |
| ret = tb_port_read(port, &val, TB_CFG_PORT, | |
| port->cap_phy + LANE_ADP_CS_0, 1); | |
| if (ret) | |
| return false; | |
| return !!(val & mask); | |
| } | |
| static int tb_port_clx_set(struct tb_port *port, unsigned int clx, bool enable) | |
| { | |
| u32 phy, mask = 0; | |
| int ret; | |
| if (clx & TB_CL0S) | |
| mask |= LANE_ADP_CS_1_CL0S_ENABLE; | |
| if (clx & TB_CL1) | |
| mask |= LANE_ADP_CS_1_CL1_ENABLE; | |
| if (clx & TB_CL2) | |
| mask |= LANE_ADP_CS_1_CL2_ENABLE; | |
| if (!mask) | |
| return -EOPNOTSUPP; | |
| ret = tb_port_read(port, &phy, TB_CFG_PORT, | |
| port->cap_phy + LANE_ADP_CS_1, 1); | |
| if (ret) | |
| return ret; | |
| if (enable) | |
| phy |= mask; | |
| else | |
| phy &= ~mask; | |
| return tb_port_write(port, &phy, TB_CFG_PORT, | |
| port->cap_phy + LANE_ADP_CS_1, 1); | |
| } | |
| static int tb_port_clx_disable(struct tb_port *port, unsigned int clx) | |
| { | |
| return tb_port_clx_set(port, clx, false); | |
| } | |
| static int tb_port_clx_enable(struct tb_port *port, unsigned int clx) | |
| { | |
| return tb_port_clx_set(port, clx, true); | |
| } | |
| static int tb_port_clx(struct tb_port *port) | |
| { | |
| u32 val; | |
| int ret; | |
| if (!tb_port_clx_supported(port, TB_CL0S | TB_CL1 | TB_CL2)) | |
| return 0; | |
| ret = tb_port_read(port, &val, TB_CFG_PORT, | |
| port->cap_phy + LANE_ADP_CS_1, 1); | |
| if (ret) | |
| return ret; | |
| if (val & LANE_ADP_CS_1_CL0S_ENABLE) | |
| ret |= TB_CL0S; | |
| if (val & LANE_ADP_CS_1_CL1_ENABLE) | |
| ret |= TB_CL1; | |
| if (val & LANE_ADP_CS_1_CL2_ENABLE) | |
| ret |= TB_CL2; | |
| return ret; | |
| } | |
| /** | |
| * tb_port_clx_is_enabled() - Is given CL state enabled | |
| * @port: USB4 port to check | |
| * @clx: Mask of CL states to check | |
| * | |
| * Return: %true if any of the given CL states is enabled for @port, | |
| * %false otherwise. | |
| */ | |
| bool tb_port_clx_is_enabled(struct tb_port *port, unsigned int clx) | |
| { | |
| return !!(tb_port_clx(port) & clx); | |
| } | |
| /** | |
| * tb_switch_clx_is_supported() - Is CLx supported on this type of router | |
| * @sw: The router to check CLx support for | |
| * | |
| * Return: %true if CLx is supported, %false otherwise. | |
| */ | |
| static bool tb_switch_clx_is_supported(const struct tb_switch *sw) | |
| { | |
| if (!clx_enabled) | |
| return false; | |
| if (sw->quirks & QUIRK_NO_CLX) | |
| return false; | |
| /* | |
| * CLx is not enabled and validated on Intel USB4 platforms | |
| * before Alder Lake. | |
| */ | |
| if (tb_switch_is_tiger_lake(sw)) | |
| return false; | |
| return tb_switch_is_usb4(sw) || tb_switch_is_titan_ridge(sw); | |
| } | |
| /** | |
| * tb_switch_clx_init() - Initialize router CL states | |
| * @sw: Router | |
| * | |
| * Can be called for any router. Initializes the current CL state by | |
| * reading it from the hardware. | |
| * | |
| * Return: %0 on success, negative errno otherwise. | |
| */ | |
| int tb_switch_clx_init(struct tb_switch *sw) | |
| { | |
| struct tb_port *up, *down; | |
| unsigned int clx, tmp; | |
| if (tb_switch_is_icm(sw)) | |
| return 0; | |
| if (!tb_route(sw)) | |
| return 0; | |
| if (!tb_switch_clx_is_supported(sw)) | |
| return 0; | |
| up = tb_upstream_port(sw); | |
| down = tb_switch_downstream_port(sw); | |
| clx = tb_port_clx(up); | |
| tmp = tb_port_clx(down); | |
| if (clx != tmp) | |
| tb_sw_warn(sw, "CLx: inconsistent configuration %#x != %#x\n", | |
| clx, tmp); | |
| tb_sw_dbg(sw, "CLx: current mode: %s\n", clx_name(clx)); | |
| sw->clx = clx; | |
| return 0; | |
| } | |
| static int tb_switch_pm_secondary_resolve(struct tb_switch *sw) | |
| { | |
| struct tb_port *up, *down; | |
| int ret; | |
| if (!tb_route(sw)) | |
| return 0; | |
| up = tb_upstream_port(sw); | |
| down = tb_switch_downstream_port(sw); | |
| ret = tb_port_pm_secondary_enable(up); | |
| if (ret) | |
| return ret; | |
| return tb_port_pm_secondary_disable(down); | |
| } | |
| static int tb_switch_mask_clx_objections(struct tb_switch *sw) | |
| { | |
| int up_port = sw->config.upstream_port_number; | |
| u32 offset, val[2], mask_obj, unmask_obj; | |
| int ret, i; | |
| /* Only Titan Ridge of pre-USB4 devices support CLx states */ | |
| if (!tb_switch_is_titan_ridge(sw)) | |
| return 0; | |
| if (!tb_route(sw)) | |
| return 0; | |
| /* | |
| * In Titan Ridge there are only 2 dual-lane Thunderbolt ports: | |
| * Port A consists of lane adapters 1,2 and | |
| * Port B consists of lane adapters 3,4 | |
| * If upstream port is A, (lanes are 1,2), we mask objections from | |
| * port B (lanes 3,4) and unmask objections from Port A and vice-versa. | |
| */ | |
| if (up_port == 1) { | |
| mask_obj = TB_LOW_PWR_C0_PORT_B_MASK; | |
| unmask_obj = TB_LOW_PWR_C1_PORT_A_MASK; | |
| offset = TB_LOW_PWR_C1_CL1; | |
| } else { | |
| mask_obj = TB_LOW_PWR_C1_PORT_A_MASK; | |
| unmask_obj = TB_LOW_PWR_C0_PORT_B_MASK; | |
| offset = TB_LOW_PWR_C3_CL1; | |
| } | |
| ret = tb_sw_read(sw, &val, TB_CFG_SWITCH, | |
| sw->cap_lp + offset, ARRAY_SIZE(val)); | |
| if (ret) | |
| return ret; | |
| for (i = 0; i < ARRAY_SIZE(val); i++) { | |
| val[i] |= mask_obj; | |
| val[i] &= ~unmask_obj; | |
| } | |
| return tb_sw_write(sw, &val, TB_CFG_SWITCH, | |
| sw->cap_lp + offset, ARRAY_SIZE(val)); | |
| } | |
| static bool validate_mask(unsigned int clx) | |
| { | |
| /* Previous states need to be enabled */ | |
| if (clx & TB_CL1) | |
| return (clx & TB_CL0S) == TB_CL0S; | |
| return true; | |
| } | |
| /** | |
| * tb_switch_clx_enable() - Enable CLx on upstream port of specified router | |
| * @sw: Router to enable CLx for | |
| * @clx: The CLx state to enable | |
| * | |
| * CLx is enabled only if both sides of the link support CLx, and if both sides | |
| * of the link are not configured as two single lane links and only if the link | |
| * is not inter-domain link. The complete set of conditions is described in CM | |
| * Guide 1.0 section 8.1. | |
| * | |
| * Return: %0 on success, negative errno otherwise. | |
| */ | |
| int tb_switch_clx_enable(struct tb_switch *sw, unsigned int clx) | |
| { | |
| bool up_clx_support, down_clx_support; | |
| struct tb_switch *parent_sw; | |
| struct tb_port *up, *down; | |
| int ret; | |
| if (!clx || sw->clx == clx) | |
| return 0; | |
| if (!validate_mask(clx)) | |
| return -EINVAL; | |
| parent_sw = tb_switch_parent(sw); | |
| if (!parent_sw) | |
| return 0; | |
| if (!tb_switch_clx_is_supported(parent_sw) || | |
| !tb_switch_clx_is_supported(sw)) | |
| return 0; | |
| /* Only support CL2 for v2 routers */ | |
| if ((clx & TB_CL2) && | |
| (usb4_switch_version(parent_sw) < 2 || | |
| usb4_switch_version(sw) < 2)) | |
| return -EOPNOTSUPP; | |
| ret = tb_switch_pm_secondary_resolve(sw); | |
| if (ret) | |
| return ret; | |
| up = tb_upstream_port(sw); | |
| down = tb_switch_downstream_port(sw); | |
| up_clx_support = tb_port_clx_supported(up, clx); | |
| down_clx_support = tb_port_clx_supported(down, clx); | |
| tb_port_dbg(up, "CLx: %s %ssupported\n", clx_name(clx), | |
| up_clx_support ? "" : "not "); | |
| tb_port_dbg(down, "CLx: %s %ssupported\n", clx_name(clx), | |
| down_clx_support ? "" : "not "); | |
| if (!up_clx_support || !down_clx_support) | |
| return -EOPNOTSUPP; | |
| ret = tb_port_clx_enable(up, clx); | |
| if (ret) | |
| return ret; | |
| ret = tb_port_clx_enable(down, clx); | |
| if (ret) { | |
| tb_port_clx_disable(up, clx); | |
| return ret; | |
| } | |
| ret = tb_switch_mask_clx_objections(sw); | |
| if (ret) { | |
| tb_port_clx_disable(up, clx); | |
| tb_port_clx_disable(down, clx); | |
| return ret; | |
| } | |
| sw->clx |= clx; | |
| tb_sw_dbg(sw, "CLx: %s enabled\n", clx_name(clx)); | |
| return 0; | |
| } | |
| /** | |
| * tb_switch_clx_disable() - Disable CLx on upstream port of specified router | |
| * @sw: Router to disable CLx for | |
| * | |
| * Disables all CL states of the given router. Can be called on any | |
| * router and if the states were not enabled already does nothing. | |
| * | |
| * Return: CL states that were disabled or negative errno otherwise. | |
| */ | |
| int tb_switch_clx_disable(struct tb_switch *sw) | |
| { | |
| unsigned int clx = sw->clx; | |
| struct tb_port *up, *down; | |
| int ret; | |
| if (!tb_switch_clx_is_supported(sw)) | |
| return 0; | |
| if (!clx) | |
| return 0; | |
| if (sw->is_unplugged) | |
| return clx; | |
| up = tb_upstream_port(sw); | |
| down = tb_switch_downstream_port(sw); | |
| ret = tb_port_clx_disable(up, clx); | |
| if (ret) | |
| return ret; | |
| ret = tb_port_clx_disable(down, clx); | |
| if (ret) | |
| return ret; | |
| sw->clx = 0; | |
| tb_sw_dbg(sw, "CLx: %s disabled\n", clx_name(clx)); | |
| return clx; | |
| } |