summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJakub Kicinski <kuba@kernel.org>2020-05-10 12:29:15 -0700
committerJakub Kicinski <kuba@kernel.org>2020-05-10 12:29:29 -0700
commitbed37f0ba6bc1ff702d87be727d9e740509809aa (patch)
tree663521d696ca521b454ae286dc01af09981bc26e
parentb9f96423bba6155cdf54f96f0b1e43fa6d0b0b74 (diff)
parent9896a4574ecb137d4e5b9283004aa34c688bc761 (diff)
downloadlinux-bed37f0ba6bc1ff702d87be727d9e740509809aa.tar.bz2
Merge branch 'Ethernet-Cable-test-support'
Andrew Lunn says: ==================== Ethernet Cable test support any copper Ethernet PHY have support for performing diagnostics of the cable. Are the cable shorted, broken, not plugged into anything at the other end? And they can report roughly how far along the cable any fault is. Add infrastructure in ethtool and phylib support for triggering a cable test and reporting the results. The Marvell 1G PHY driver is then extended to make use of this infrastructure. For testing, a modified ethtool(1) can be found here: https://github.com/lunn/ethtool.git feature/cable-test-v4. This also contains extra code for TDR dump, which will be added to the kernel in a later patch series. Thanks to Chris Healy for extensive testing. v2: See individual patches but: Remove _REPLY messages Change length into a u32 Grammar fixes Rename functions for consistency Extack for cable test already running Remove ethnl_cable_test_act_ops Add status attributes Rename pairs from numbers to letters v3: See individual patches but: Remove ETHTOOL_MSG_CABLE_TEST_ACT_REPLY from documentation Remove unused cable_test_get_policy Fixed example in document Add ETHTOOL_A_CABLE_NEST_* enum Add ETHTOOL_MSG_CABLE_TEST_NTF to documentation Poison phydev->skb Return -EMSGSIZE when ethnl_bcastmsg_put() fails Return valid error code when nla_nest_start() fails Use u8 for results Actually put u32 length into message s/mavell/marvell/g Remove include of <uapi/linux/ethtool_netlink.h> EMSGSIZE when ethnl_bcastmsg_put() fails Print an error message on failure, since this is a void function. v4: See individual patches but: Remove unwanted blank line ENOTSUPP->EOPNOTSUPP Move EINVAL->EMSGSIZE fix to correct patch ==================== Signed-off-by: Jakub Kicinski <kuba@kernel.org>
-rw-r--r--Documentation/networking/ethtool-netlink.rst57
-rw-r--r--drivers/net/phy/marvell.c201
-rw-r--r--drivers/net/phy/phy.c106
-rw-r--r--include/linux/ethtool_netlink.h33
-rw-r--r--include/linux/phy.h42
-rw-r--r--include/uapi/linux/ethtool_netlink.h71
-rw-r--r--net/Kconfig1
-rw-r--r--net/ethtool/Makefile2
-rw-r--r--net/ethtool/cabletest.c201
-rw-r--r--net/ethtool/netlink.c9
-rw-r--r--net/ethtool/netlink.h3
11 files changed, 722 insertions, 4 deletions
diff --git a/Documentation/networking/ethtool-netlink.rst b/Documentation/networking/ethtool-netlink.rst
index 8f5cefc539cf..eed46b6aa07d 100644
--- a/Documentation/networking/ethtool-netlink.rst
+++ b/Documentation/networking/ethtool-netlink.rst
@@ -204,6 +204,7 @@ Userspace to kernel:
``ETHTOOL_MSG_EEE_GET`` get EEE settings
``ETHTOOL_MSG_EEE_SET`` set EEE settings
``ETHTOOL_MSG_TSINFO_GET`` get timestamping info
+ ``ETHTOOL_MSG_CABLE_TEST_ACT`` action start cable test
===================================== ================================
Kernel to userspace:
@@ -235,6 +236,7 @@ Kernel to userspace:
``ETHTOOL_MSG_EEE_GET_REPLY`` EEE settings
``ETHTOOL_MSG_EEE_NTF`` EEE settings
``ETHTOOL_MSG_TSINFO_GET_REPLY`` timestamping info
+ ``ETHTOOL_MSG_CABLE_TEST_NTF`` Cable test results
===================================== =================================
``GET`` requests are sent by userspace applications to retrieve device
@@ -958,13 +960,65 @@ Kernel response contents:
is no special value for this case). The bitset attributes are omitted if they
would be empty (no bit set).
+CABLE_TEST
+==========
+
+Start a cable test.
+
+Request contents:
+
+ ==================================== ====== ==========================
+ ``ETHTOOL_A_CABLE_TEST_HEADER`` nested request header
+ ==================================== ====== ==========================
+
+Notification contents:
+
+An Ethernet cable typically contains 1, 2 or 4 pairs. The length of
+the pair can only be measured when there is a fault in the pair and
+hence a reflection. Information about the fault may not be available,
+depending on the specific hardware. Hence the contents of the notify
+message are mostly optional. The attributes can be repeated an
+arbitrary number of times, in an arbitrary order, for an arbitrary
+number of pairs.
+
+The example shows the notification sent when the test is completed for
+a T2 cable, i.e. two pairs. One pair is OK and hence has no length
+information. The second pair has a fault and does have length
+information.
+
+ +---------------------------------------------+--------+---------------------+
+ | ``ETHTOOL_A_CABLE_TEST_HEADER`` | nested | reply header |
+ +---------------------------------------------+--------+---------------------+
+ | ``ETHTOOL_A_CABLE_TEST_STATUS`` | u8 | completed |
+ +---------------------------------------------+--------+---------------------+
+ | ``ETHTOOL_A_CABLE_TEST_NTF_NEST`` | nested | all the results |
+ +-+-------------------------------------------+--------+---------------------+
+ | | ``ETHTOOL_A_CABLE_NEST_RESULT`` | nested | cable test result |
+ +-+-+-----------------------------------------+--------+---------------------+
+ | | | ``ETHTOOL_A_CABLE_RESULTS_PAIR`` | u8 | pair number |
+ +-+-+-----------------------------------------+--------+---------------------+
+ | | | ``ETHTOOL_A_CABLE_RESULTS_CODE`` | u8 | result code |
+ +-+-+-----------------------------------------+--------+---------------------+
+ | | ``ETHTOOL_A_CABLE_NEST_RESULT`` | nested | cable test results |
+ +-+-+-----------------------------------------+--------+---------------------+
+ | | | ``ETHTOOL_A_CABLE_RESULTS_PAIR`` | u8 | pair number |
+ +-+-+-----------------------------------------+--------+---------------------+
+ | | | ``ETHTOOL_A_CABLE_RESULTS_CODE`` | u8 | result code |
+ +-+-+-----------------------------------------+--------+---------------------+
+ | | ``ETHTOOL_A_CABLE_NEST_FAULT_LENGTH`` | nested | cable length |
+ +-+-+-----------------------------------------+--------+---------------------+
+ | | | ``ETHTOOL_A_CABLE_FAULT_LENGTH_PAIR`` | u8 | pair number |
+ +-+-+-----------------------------------------+--------+---------------------+
+ | | | ``ETHTOOL_A_CABLE_FAULT_LENGTH_CM`` | u32 | length in cm |
+ +-+-+-----------------------------------------+--------+---------------------+
Request translation
===================
The following table maps ioctl commands to netlink commands providing their
functionality. Entries with "n/a" in right column are commands which do not
-have their netlink replacement yet.
+have their netlink replacement yet. Entries which "n/a" in the left column
+are netlink only.
=================================== =====================================
ioctl command netlink command
@@ -1053,4 +1107,5 @@ have their netlink replacement yet.
``ETHTOOL_PHY_STUNABLE`` n/a
``ETHTOOL_GFECPARAM`` n/a
``ETHTOOL_SFECPARAM`` n/a
+ n/a ''ETHTOOL_MSG_CABLE_TEST_ACT''
=================================== =====================================
diff --git a/drivers/net/phy/marvell.c b/drivers/net/phy/marvell.c
index 7fc8e10c5f33..4bc7febf9248 100644
--- a/drivers/net/phy/marvell.c
+++ b/drivers/net/phy/marvell.c
@@ -27,6 +27,7 @@
#include <linux/module.h>
#include <linux/mii.h>
#include <linux/ethtool.h>
+#include <linux/ethtool_netlink.h>
#include <linux/phy.h>
#include <linux/marvell_phy.h>
#include <linux/bitfield.h>
@@ -42,6 +43,7 @@
#define MII_MARVELL_MSCR_PAGE 0x02
#define MII_MARVELL_LED_PAGE 0x03
#define MII_MARVELL_MISC_TEST_PAGE 0x06
+#define MII_MARVELL_VCT7_PAGE 0x07
#define MII_MARVELL_WOL_PAGE 0x11
#define MII_M1011_IEVENT 0x13
@@ -162,6 +164,36 @@
#define MII_88E1510_GEN_CTRL_REG_1_MODE_SGMII 0x1 /* SGMII to copper */
#define MII_88E1510_GEN_CTRL_REG_1_RESET 0x8000 /* Soft reset */
+#define MII_VCT7_PAIR_0_DISTANCE 0x10
+#define MII_VCT7_PAIR_1_DISTANCE 0x11
+#define MII_VCT7_PAIR_2_DISTANCE 0x12
+#define MII_VCT7_PAIR_3_DISTANCE 0x13
+
+#define MII_VCT7_RESULTS 0x14
+#define MII_VCT7_RESULTS_PAIR3_MASK 0xf000
+#define MII_VCT7_RESULTS_PAIR2_MASK 0x0f00
+#define MII_VCT7_RESULTS_PAIR1_MASK 0x00f0
+#define MII_VCT7_RESULTS_PAIR0_MASK 0x000f
+#define MII_VCT7_RESULTS_PAIR3_SHIFT 12
+#define MII_VCT7_RESULTS_PAIR2_SHIFT 8
+#define MII_VCT7_RESULTS_PAIR1_SHIFT 4
+#define MII_VCT7_RESULTS_PAIR0_SHIFT 0
+#define MII_VCT7_RESULTS_INVALID 0
+#define MII_VCT7_RESULTS_OK 1
+#define MII_VCT7_RESULTS_OPEN 2
+#define MII_VCT7_RESULTS_SAME_SHORT 3
+#define MII_VCT7_RESULTS_CROSS_SHORT 4
+#define MII_VCT7_RESULTS_BUSY 9
+
+#define MII_VCT7_CTRL 0x15
+#define MII_VCT7_CTRL_RUN_NOW BIT(15)
+#define MII_VCT7_CTRL_RUN_ANEG BIT(14)
+#define MII_VCT7_CTRL_DISABLE_CROSS BIT(13)
+#define MII_VCT7_CTRL_RUN_AFTER_BREAK_LINK BIT(12)
+#define MII_VCT7_CTRL_IN_PROGRESS BIT(11)
+#define MII_VCT7_CTRL_METERS BIT(10)
+#define MII_VCT7_CTRL_CENTIMETERS 0
+
#define LPA_PAUSE_FIBER 0x180
#define LPA_PAUSE_ASYM_FIBER 0x100
@@ -1658,6 +1690,163 @@ static void marvell_get_stats(struct phy_device *phydev,
data[i] = marvell_get_stat(phydev, i);
}
+static int marvell_vct7_cable_test_start(struct phy_device *phydev)
+{
+ int bmcr, bmsr, ret;
+
+ /* If auto-negotiation is enabled, but not complete, the cable
+ * test never completes. So disable auto-neg.
+ */
+ bmcr = phy_read(phydev, MII_BMCR);
+ if (bmcr < 0)
+ return bmcr;
+
+ bmsr = phy_read(phydev, MII_BMSR);
+
+ if (bmsr < 0)
+ return bmsr;
+
+ if (bmcr & BMCR_ANENABLE) {
+ ret = phy_modify(phydev, MII_BMCR, BMCR_ANENABLE, 0);
+ if (ret < 0)
+ return ret;
+ ret = genphy_soft_reset(phydev);
+ if (ret < 0)
+ return ret;
+ }
+
+ /* If the link is up, allow it some time to go down */
+ if (bmsr & BMSR_LSTATUS)
+ msleep(1500);
+
+ return phy_write_paged(phydev, MII_MARVELL_VCT7_PAGE,
+ MII_VCT7_CTRL,
+ MII_VCT7_CTRL_RUN_NOW |
+ MII_VCT7_CTRL_CENTIMETERS);
+}
+
+static int marvell_vct7_distance_to_length(int distance, bool meter)
+{
+ if (meter)
+ distance *= 100;
+
+ return distance;
+}
+
+static bool marvell_vct7_distance_valid(int result)
+{
+ switch (result) {
+ case MII_VCT7_RESULTS_OPEN:
+ case MII_VCT7_RESULTS_SAME_SHORT:
+ case MII_VCT7_RESULTS_CROSS_SHORT:
+ return true;
+ }
+ return false;
+}
+
+static int marvell_vct7_report_length(struct phy_device *phydev,
+ int pair, bool meter)
+{
+ int length;
+ int ret;
+
+ ret = phy_read_paged(phydev, MII_MARVELL_VCT7_PAGE,
+ MII_VCT7_PAIR_0_DISTANCE + pair);
+ if (ret < 0)
+ return ret;
+
+ length = marvell_vct7_distance_to_length(ret, meter);
+
+ ethnl_cable_test_fault_length(phydev, pair, length);
+
+ return 0;
+}
+
+static int marvell_vct7_cable_test_report_trans(int result)
+{
+ switch (result) {
+ case MII_VCT7_RESULTS_OK:
+ return ETHTOOL_A_CABLE_RESULT_CODE_OK;
+ case MII_VCT7_RESULTS_OPEN:
+ return ETHTOOL_A_CABLE_RESULT_CODE_OPEN;
+ case MII_VCT7_RESULTS_SAME_SHORT:
+ return ETHTOOL_A_CABLE_RESULT_CODE_SAME_SHORT;
+ case MII_VCT7_RESULTS_CROSS_SHORT:
+ return ETHTOOL_A_CABLE_RESULT_CODE_CROSS_SHORT;
+ default:
+ return ETHTOOL_A_CABLE_RESULT_CODE_UNSPEC;
+ }
+}
+
+static int marvell_vct7_cable_test_report(struct phy_device *phydev)
+{
+ int pair0, pair1, pair2, pair3;
+ bool meter;
+ int ret;
+
+ ret = phy_read_paged(phydev, MII_MARVELL_VCT7_PAGE,
+ MII_VCT7_RESULTS);
+ if (ret < 0)
+ return ret;
+
+ pair3 = (ret & MII_VCT7_RESULTS_PAIR3_MASK) >>
+ MII_VCT7_RESULTS_PAIR3_SHIFT;
+ pair2 = (ret & MII_VCT7_RESULTS_PAIR2_MASK) >>
+ MII_VCT7_RESULTS_PAIR2_SHIFT;
+ pair1 = (ret & MII_VCT7_RESULTS_PAIR1_MASK) >>
+ MII_VCT7_RESULTS_PAIR1_SHIFT;
+ pair0 = (ret & MII_VCT7_RESULTS_PAIR0_MASK) >>
+ MII_VCT7_RESULTS_PAIR0_SHIFT;
+
+ ethnl_cable_test_result(phydev, ETHTOOL_A_CABLE_PAIR_A,
+ marvell_vct7_cable_test_report_trans(pair0));
+ ethnl_cable_test_result(phydev, ETHTOOL_A_CABLE_PAIR_B,
+ marvell_vct7_cable_test_report_trans(pair1));
+ ethnl_cable_test_result(phydev, ETHTOOL_A_CABLE_PAIR_C,
+ marvell_vct7_cable_test_report_trans(pair2));
+ ethnl_cable_test_result(phydev, ETHTOOL_A_CABLE_PAIR_D,
+ marvell_vct7_cable_test_report_trans(pair3));
+
+ ret = phy_read_paged(phydev, MII_MARVELL_VCT7_PAGE, MII_VCT7_CTRL);
+ if (ret < 0)
+ return ret;
+
+ meter = ret & MII_VCT7_CTRL_METERS;
+
+ if (marvell_vct7_distance_valid(pair0))
+ marvell_vct7_report_length(phydev, 0, meter);
+ if (marvell_vct7_distance_valid(pair1))
+ marvell_vct7_report_length(phydev, 1, meter);
+ if (marvell_vct7_distance_valid(pair2))
+ marvell_vct7_report_length(phydev, 2, meter);
+ if (marvell_vct7_distance_valid(pair3))
+ marvell_vct7_report_length(phydev, 3, meter);
+
+ return 0;
+}
+
+static int marvell_vct7_cable_test_get_status(struct phy_device *phydev,
+ bool *finished)
+{
+ int ret;
+
+ *finished = false;
+
+ ret = phy_read_paged(phydev, MII_MARVELL_VCT7_PAGE,
+ MII_VCT7_CTRL);
+
+ if (ret < 0)
+ return ret;
+
+ if (!(ret & MII_VCT7_CTRL_IN_PROGRESS)) {
+ *finished = true;
+
+ return marvell_vct7_cable_test_report(phydev);
+ }
+
+ return 0;
+}
+
#ifdef CONFIG_HWMON
static int m88e1121_get_temp(struct phy_device *phydev, long *temp)
{
@@ -2353,6 +2542,7 @@ static struct phy_driver marvell_drivers[] = {
.phy_id_mask = MARVELL_PHY_ID_MASK,
.name = "Marvell 88E1510",
.features = PHY_GBIT_FIBRE_FEATURES,
+ .flags = PHY_POLL_CABLE_TEST,
.probe = &m88e1510_probe,
.config_init = &m88e1510_config_init,
.config_aneg = &m88e1510_config_aneg,
@@ -2372,12 +2562,15 @@ static struct phy_driver marvell_drivers[] = {
.set_loopback = genphy_loopback,
.get_tunable = m88e1011_get_tunable,
.set_tunable = m88e1011_set_tunable,
+ .cable_test_start = marvell_vct7_cable_test_start,
+ .cable_test_get_status = marvell_vct7_cable_test_get_status,
},
{
.phy_id = MARVELL_PHY_ID_88E1540,
.phy_id_mask = MARVELL_PHY_ID_MASK,
.name = "Marvell 88E1540",
/* PHY_GBIT_FEATURES */
+ .flags = PHY_POLL_CABLE_TEST,
.probe = m88e1510_probe,
.config_init = &marvell_config_init,
.config_aneg = &m88e1510_config_aneg,
@@ -2394,6 +2587,8 @@ static struct phy_driver marvell_drivers[] = {
.get_stats = marvell_get_stats,
.get_tunable = m88e1540_get_tunable,
.set_tunable = m88e1540_set_tunable,
+ .cable_test_start = marvell_vct7_cable_test_start,
+ .cable_test_get_status = marvell_vct7_cable_test_get_status,
},
{
.phy_id = MARVELL_PHY_ID_88E1545,
@@ -2401,6 +2596,7 @@ static struct phy_driver marvell_drivers[] = {
.name = "Marvell 88E1545",
.probe = m88e1510_probe,
/* PHY_GBIT_FEATURES */
+ .flags = PHY_POLL_CABLE_TEST,
.config_init = &marvell_config_init,
.config_aneg = &m88e1510_config_aneg,
.read_status = &marvell_read_status,
@@ -2416,6 +2612,8 @@ static struct phy_driver marvell_drivers[] = {
.get_stats = marvell_get_stats,
.get_tunable = m88e1540_get_tunable,
.set_tunable = m88e1540_set_tunable,
+ .cable_test_start = marvell_vct7_cable_test_start,
+ .cable_test_get_status = marvell_vct7_cable_test_get_status,
},
{
.phy_id = MARVELL_PHY_ID_88E3016,
@@ -2442,6 +2640,7 @@ static struct phy_driver marvell_drivers[] = {
.phy_id_mask = MARVELL_PHY_ID_MASK,
.name = "Marvell 88E6390",
/* PHY_GBIT_FEATURES */
+ .flags = PHY_POLL_CABLE_TEST,
.probe = m88e6390_probe,
.config_init = &marvell_config_init,
.config_aneg = &m88e6390_config_aneg,
@@ -2458,6 +2657,8 @@ static struct phy_driver marvell_drivers[] = {
.get_stats = marvell_get_stats,
.get_tunable = m88e1540_get_tunable,
.set_tunable = m88e1540_set_tunable,
+ .cable_test_start = marvell_vct7_cable_test_start,
+ .cable_test_get_status = marvell_vct7_cable_test_get_status,
},
};
diff --git a/drivers/net/phy/phy.c b/drivers/net/phy/phy.c
index 8c22d02b4218..9bdc924eea83 100644
--- a/drivers/net/phy/phy.c
+++ b/drivers/net/phy/phy.c
@@ -15,12 +15,14 @@
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/netdevice.h>
+#include <linux/netlink.h>
#include <linux/etherdevice.h>
#include <linux/skbuff.h>
#include <linux/mm.h>
#include <linux/module.h>
#include <linux/mii.h>
#include <linux/ethtool.h>
+#include <linux/ethtool_netlink.h>
#include <linux/phy.h>
#include <linux/phy_led_triggers.h>
#include <linux/sfp.h>
@@ -29,6 +31,9 @@
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/atomic.h>
+#include <net/netlink.h>
+#include <net/genetlink.h>
+#include <net/sock.h>
#define PHY_STATE_TIME HZ
@@ -44,6 +49,7 @@ static const char *phy_state_to_str(enum phy_state st)
PHY_STATE_STR(UP)
PHY_STATE_STR(RUNNING)
PHY_STATE_STR(NOLINK)
+ PHY_STATE_STR(CABLETEST)
PHY_STATE_STR(HALTED)
}
@@ -472,6 +478,80 @@ static void phy_trigger_machine(struct phy_device *phydev)
phy_queue_state_machine(phydev, 0);
}
+static void phy_abort_cable_test(struct phy_device *phydev)
+{
+ int err;
+
+ ethnl_cable_test_finished(phydev);
+
+ err = phy_init_hw(phydev);
+ if (err)
+ phydev_err(phydev, "Error while aborting cable test");
+}
+
+int phy_start_cable_test(struct phy_device *phydev,
+ struct netlink_ext_ack *extack)
+{
+ struct net_device *dev = phydev->attached_dev;
+ int err = -ENOMEM;
+
+ if (!(phydev->drv &&
+ phydev->drv->cable_test_start &&
+ phydev->drv->cable_test_get_status)) {
+ NL_SET_ERR_MSG(extack,
+ "PHY driver does not support cable testing");
+ return -EOPNOTSUPP;
+ }
+
+ mutex_lock(&phydev->lock);
+ if (phydev->state == PHY_CABLETEST) {
+ NL_SET_ERR_MSG(extack,
+ "PHY already performing a test");
+ err = -EBUSY;
+ goto out;
+ }
+
+ if (phydev->state < PHY_UP ||
+ phydev->state > PHY_CABLETEST) {
+ NL_SET_ERR_MSG(extack,
+ "PHY not configured. Try setting interface up");
+ err = -EBUSY;
+ goto out;
+ }
+
+ err = ethnl_cable_test_alloc(phydev);
+ if (err)
+ goto out;
+
+ /* Mark the carrier down until the test is complete */
+ phy_link_down(phydev, true);
+
+ netif_testing_on(dev);
+ err = phydev->drv->cable_test_start(phydev);
+ if (err) {
+ netif_testing_off(dev);
+ phy_link_up(phydev);
+ goto out_free;
+ }
+
+ phydev->state = PHY_CABLETEST;
+
+ if (phy_polling_mode(phydev))
+ phy_trigger_machine(phydev);
+
+ mutex_unlock(&phydev->lock);
+
+ return 0;
+
+out_free:
+ ethnl_cable_test_free(phydev);
+out:
+ mutex_unlock(&phydev->lock);
+
+ return err;
+}
+EXPORT_SYMBOL(phy_start_cable_test);
+
static int phy_config_aneg(struct phy_device *phydev)
{
if (phydev->drv->config_aneg)
@@ -802,6 +882,8 @@ EXPORT_SYMBOL(phy_free_interrupt);
*/
void phy_stop(struct phy_device *phydev)
{
+ struct net_device *dev = phydev->attached_dev;
+
if (!phy_is_started(phydev)) {
WARN(1, "called from state %s\n",
phy_state_to_str(phydev->state));
@@ -810,6 +892,11 @@ void phy_stop(struct phy_device *phydev)
mutex_lock(&phydev->lock);
+ if (phydev->state == PHY_CABLETEST) {
+ phy_abort_cable_test(phydev);
+ netif_testing_off(dev);
+ }
+
if (phydev->sfp_bus)
sfp_upstream_stop(phydev->sfp_bus);
@@ -870,8 +957,10 @@ void phy_state_machine(struct work_struct *work)
struct delayed_work *dwork = to_delayed_work(work);
struct phy_device *phydev =
container_of(dwork, struct phy_device, state_queue);
+ struct net_device *dev = phydev->attached_dev;
bool needs_aneg = false, do_suspend = false;
enum phy_state old_state;
+ bool finished = false;
int err = 0;
mutex_lock(&phydev->lock);
@@ -890,6 +979,23 @@ void phy_state_machine(struct work_struct *work)
case PHY_RUNNING:
err = phy_check_link_status(phydev);
break;
+ case PHY_CABLETEST:
+ err = phydev->drv->cable_test_get_status(phydev, &finished);
+ if (err) {
+ phy_abort_cable_test(phydev);
+ netif_testing_off(dev);
+ needs_aneg = true;
+ phydev->state = PHY_UP;
+ break;
+ }
+
+ if (finished) {
+ ethnl_cable_test_finished(phydev);
+ netif_testing_off(dev);
+ needs_aneg = true;
+ phydev->state = PHY_UP;
+ }
+ break;
case PHY_HALTED:
if (phydev->link) {
phydev->link = 0;
diff --git a/include/linux/ethtool_netlink.h b/include/linux/ethtool_netlink.h
index d01b77887f82..e317fc99565e 100644
--- a/include/linux/ethtool_netlink.h
+++ b/include/linux/ethtool_netlink.h
@@ -14,4 +14,37 @@ enum ethtool_multicast_groups {
ETHNL_MCGRP_MONITOR,
};
+struct phy_device;
+
+#if IS_ENABLED(CONFIG_ETHTOOL_NETLINK)
+int ethnl_cable_test_alloc(struct phy_device *phydev);
+void ethnl_cable_test_free(struct phy_device *phydev);
+void ethnl_cable_test_finished(struct phy_device *phydev);
+int ethnl_cable_test_result(struct phy_device *phydev, u8 pair, u8 result);
+int ethnl_cable_test_fault_length(struct phy_device *phydev, u8 pair, u32 cm);
+#else
+static inline int ethnl_cable_test_alloc(struct phy_device *phydev)
+{
+ return -EOPNOTSUPP;
+}
+
+static inline void ethnl_cable_test_free(struct phy_device *phydev)
+{
+}
+
+static inline void ethnl_cable_test_finished(struct phy_device *phydev)
+{
+}
+static inline int ethnl_cable_test_result(struct phy_device *phydev, u8 pair,
+ u8 result)
+{
+ return -EOPNOTSUPP;
+}
+
+static inline int ethnl_cable_test_fault_length(struct phy_device *phydev,
+ u8 pair, u32 cm)
+{
+ return -EOPNOTSUPP;
+}
+#endif /* IS_ENABLED(ETHTOOL_NETLINK) */
#endif /* _LINUX_ETHTOOL_NETLINK_H_ */
diff --git a/include/linux/phy.h b/include/linux/phy.h
index a2b91b5f9d0a..5d8ff5428010 100644
--- a/include/linux/phy.h
+++ b/include/linux/phy.h
@@ -15,6 +15,7 @@
#include <linux/spinlock.h>
#include <linux/ethtool.h>
#include <linux/linkmode.h>
+#include <linux/netlink.h>
#include <linux/mdio.h>
#include <linux/mii.h>
#include <linux/mii_timestamper.h>
@@ -78,6 +79,7 @@ extern const int phy_10gbit_features_array[1];
#define PHY_IS_INTERNAL 0x00000001
#define PHY_RST_AFTER_CLK_EN 0x00000002
+#define PHY_POLL_CABLE_TEST 0x00000004
#define MDIO_DEVICE_IS_PHY 0x80000000
/* Interface Mode definitions */
@@ -372,6 +374,12 @@ struct phy_device *mdiobus_scan(struct mii_bus *bus, int addr);
* - irq or timer will set NOLINK if link goes down
* - phy_stop moves to HALTED
*
+ * CABLETEST: PHY is performing a cable test. Packet reception/sending
+ * is not expected to work, carrier will be indicated as down. PHY will be
+ * poll once per second, or on interrupt for it current state.
+ * Once complete, move to UP to restart the PHY.
+ * - phy_stop aborts the running test and moves to HALTED
+ *
* HALTED: PHY is up, but no polling or interrupts are done. Or
* PHY is in an error state.
* - phy_start moves to UP
@@ -383,6 +391,7 @@ enum phy_state {
PHY_UP,
PHY_RUNNING,
PHY_NOLINK,
+ PHY_CABLETEST,
};
/**
@@ -514,6 +523,11 @@ struct phy_device {
/* For use by PHYs inside the same package that need a shared state. */
struct phy_package_shared *shared;
+ /* Reporting cable test results */
+ struct sk_buff *skb;
+ void *ehdr;
+ struct nlattr *nest;
+
/* Interrupt and Polling infrastructure */
struct delayed_work state_queue;
@@ -689,6 +703,13 @@ struct phy_driver {
int (*module_eeprom)(struct phy_device *dev,
struct ethtool_eeprom *ee, u8 *data);
+ /* Start a cable test */
+ int (*cable_test_start)(struct phy_device *dev);
+ /* Once per second, or on interrupt, request the status of the
+ * test.
+ */
+ int (*cable_test_get_status)(struct phy_device *dev, bool *finished);
+
/* Get statistics from the phy using ethtool */
int (*get_sset_count)(struct phy_device *dev);
void (*get_strings)(struct phy_device *dev, u8 *data);
@@ -1046,6 +1067,10 @@ static inline bool phy_interrupt_is_valid(struct phy_device *phydev)
*/
static inline bool phy_polling_mode(struct phy_device *phydev)
{
+ if (phydev->state == PHY_CABLETEST)
+ if (phydev->drv->flags & PHY_POLL_CABLE_TEST)
+ return true;
+
return phydev->irq == PHY_POLL;
}
@@ -1227,6 +1252,23 @@ int phy_speed_up(struct phy_device *phydev);
int phy_restart_aneg(struct phy_device *phydev);
int phy_reset_after_clk_enable(struct phy_device *phydev);
+#if IS_ENABLED(CONFIG_PHYLIB)
+int phy_start_cable_test(struct phy_device *phydev,
+ struct netlink_ext_ack *extack);
+#else
+static inline
+int phy_start_cable_test(struct phy_device *phydev,
+ struct netlink_ext_ack *extack)
+{
+ NL_SET_ERR_MSG(extack, "Kernel not compiled with PHYLIB support");
+ return -EOPNOTSUPP;
+}
+#endif
+
+int phy_cable_test_result(struct phy_device *phydev, u8 pair, u16 result);
+int phy_cable_test_fault_length(struct phy_device *phydev, u8 pair,
+ u16 cm);
+
static inline void phy_device_reset(struct phy_device *phydev, int value)
{
mdio_device_reset(&phydev->mdio, value);
diff --git a/include/uapi/linux/ethtool_netlink.h b/include/uapi/linux/ethtool_netlink.h
index bf1d310e20bc..2881af411f76 100644
--- a/include/uapi/linux/ethtool_netlink.h
+++ b/include/uapi/linux/ethtool_netlink.h
@@ -39,6 +39,7 @@ enum {
ETHTOOL_MSG_EEE_GET,
ETHTOOL_MSG_EEE_SET,
ETHTOOL_MSG_TSINFO_GET,
+ ETHTOOL_MSG_CABLE_TEST_ACT,
/* add new constants above here */
__ETHTOOL_MSG_USER_CNT,
@@ -74,6 +75,7 @@ enum {
ETHTOOL_MSG_EEE_GET_REPLY,
ETHTOOL_MSG_EEE_NTF,
ETHTOOL_MSG_TSINFO_GET_REPLY,
+ ETHTOOL_MSG_CABLE_TEST_NTF,
/* add new constants above here */
__ETHTOOL_MSG_KERNEL_CNT,
@@ -405,6 +407,75 @@ enum {
ETHTOOL_A_TSINFO_MAX = (__ETHTOOL_A_TSINFO_CNT - 1)
};
+/* CABLE TEST */
+
+enum {
+ ETHTOOL_A_CABLE_TEST_UNSPEC,
+ ETHTOOL_A_CABLE_TEST_HEADER, /* nest - _A_HEADER_* */
+
+ /* add new constants above here */
+ __ETHTOOL_A_CABLE_TEST_CNT,
+ ETHTOOL_A_CABLE_TEST_MAX = __ETHTOOL_A_CABLE_TEST_CNT - 1
+};
+
+/* CABLE TEST NOTIFY */
+enum {
+ ETHTOOL_A_CABLE_RESULT_CODE_UNSPEC,
+ ETHTOOL_A_CABLE_RESULT_CODE_OK,
+ ETHTOOL_A_CABLE_RESULT_CODE_OPEN,
+ ETHTOOL_A_CABLE_RESULT_CODE_SAME_SHORT,
+ ETHTOOL_A_CABLE_RESULT_CODE_CROSS_SHORT,
+};
+
+enum {
+ ETHTOOL_A_CABLE_PAIR_A,
+ ETHTOOL_A_CABLE_PAIR_B,
+ ETHTOOL_A_CABLE_PAIR_C,
+ ETHTOOL_A_CABLE_PAIR_D,
+};
+
+enum {
+ ETHTOOL_A_CABLE_RESULT_UNSPEC,
+ ETHTOOL_A_CABLE_RESULT_PAIR, /* u8 ETHTOOL_A_CABLE_PAIR_ */
+ ETHTOOL_A_CABLE_RESULT_CODE, /* u8 ETHTOOL_A_CABLE_RESULT_CODE_ */
+
+ __ETHTOOL_A_CABLE_RESULT_CNT,
+ ETHTOOL_A_CABLE_RESULT_MAX = (__ETHTOOL_A_CABLE_RESULT_CNT - 1)
+};
+
+enum {
+ ETHTOOL_A_CABLE_FAULT_LENGTH_UNSPEC,
+ ETHTOOL_A_CABLE_FAULT_LENGTH_PAIR, /* u8 ETHTOOL_A_CABLE_PAIR_ */
+ ETHTOOL_A_CABLE_FAULT_LENGTH_CM, /* u32 */
+
+ __ETHTOOL_A_CABLE_FAULT_LENGTH_CNT,
+ ETHTOOL_A_CABLE_FAULT_LENGTH_MAX = (__ETHTOOL_A_CABLE_FAULT_LENGTH_CNT - 1)
+};
+
+enum {
+ ETHTOOL_A_CABLE_TEST_NTF_STATUS_UNSPEC,
+ ETHTOOL_A_CABLE_TEST_NTF_STATUS_STARTED,
+ ETHTOOL_A_CABLE_TEST_NTF_STATUS_COMPLETED
+};
+
+enum {
+ ETHTOOL_A_CABLE_NEST_UNSPEC,
+ ETHTOOL_A_CABLE_NEST_RESULT, /* nest - ETHTOOL_A_CABLE_RESULT_ */
+ ETHTOOL_A_CABLE_NEST_FAULT_LENGTH, /* nest - ETHTOOL_A_CABLE_FAULT_LENGTH_ */
+ __ETHTOOL_A_CABLE_NEST_CNT,
+ ETHTOOL_A_CABLE_NEST_MAX = (__ETHTOOL_A_CABLE_NEST_CNT - 1)
+};
+
+enum {
+ ETHTOOL_A_CABLE_TEST_NTF_UNSPEC,
+ ETHTOOL_A_CABLE_TEST_NTF_HEADER, /* nest - ETHTOOL_A_HEADER_* */
+ ETHTOOL_A_CABLE_TEST_NTF_STATUS, /* u8 - _STARTED/_COMPLETE */
+ ETHTOOL_A_CABLE_TEST_NTF_NEST, /* nest - of results: */
+
+ __ETHTOOL_A_CABLE_TEST_NTF_CNT,
+ ETHTOOL_A_CABLE_TEST_NTF_MAX = (__ETHTOOL_A_CABLE_TEST_NTF_CNT - 1)
+};
+
/* generic netlink info */
#define ETHTOOL_GENL_NAME "ethtool"
#define ETHTOOL_GENL_VERSION 1
diff --git a/net/Kconfig b/net/Kconfig
index c5ba2d180c43..5c524c6ee75d 100644
--- a/net/Kconfig
+++ b/net/Kconfig
@@ -455,6 +455,7 @@ config FAILOVER
config ETHTOOL_NETLINK
bool "Netlink interface for ethtool"
default y
+ depends on PHYLIB=y || PHYLIB=n
help
An alternative userspace interface for ethtool based on generic
netlink. It provides better extensibility and some new features,
diff --git a/net/ethtool/Makefile b/net/ethtool/Makefile
index 6c360c9c9370..0c2b94f20499 100644
--- a/net/ethtool/Makefile
+++ b/net/ethtool/Makefile
@@ -6,4 +6,4 @@ obj-$(CONFIG_ETHTOOL_NETLINK) += ethtool_nl.o
ethtool_nl-y := netlink.o bitset.o strset.o linkinfo.o linkmodes.o \
linkstate.o debug.o wol.o features.o privflags.o rings.o \
- channels.o coalesce.o pause.o eee.o tsinfo.o
+ channels.o coalesce.o pause.o eee.o tsinfo.o cabletest.o
diff --git a/net/ethtool/cabletest.c b/net/ethtool/cabletest.c
new file mode 100644
index 000000000000..5ba06eabe8c2
--- /dev/null
+++ b/net/ethtool/cabletest.c
@@ -0,0 +1,201 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <linux/phy.h>
+#include <linux/ethtool_netlink.h>
+#include "netlink.h"
+#include "common.h"
+
+/* CABLE_TEST_ACT */
+
+static const struct nla_policy
+cable_test_act_policy[ETHTOOL_A_CABLE_TEST_MAX + 1] = {
+ [ETHTOOL_A_CABLE_TEST_UNSPEC] = { .type = NLA_REJECT },
+ [ETHTOOL_A_CABLE_TEST_HEADER] = { .type = NLA_NESTED },
+};
+
+static int ethnl_cable_test_started(struct phy_device *phydev)
+{
+ struct sk_buff *skb;
+ int err = -ENOMEM;
+ void *ehdr;
+
+ skb = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
+ if (!skb)
+ goto out;
+
+ ehdr = ethnl_bcastmsg_put(skb, ETHTOOL_MSG_CABLE_TEST_NTF);
+ if (!ehdr) {
+ err = -EMSGSIZE;
+ goto out;
+ }
+
+ err = ethnl_fill_reply_header(skb, phydev->attached_dev,
+ ETHTOOL_A_CABLE_TEST_NTF_HEADER);
+ if (err)
+ goto out;
+
+ err = nla_put_u8(skb, ETHTOOL_A_CABLE_TEST_NTF_STATUS,
+ ETHTOOL_A_CABLE_TEST_NTF_STATUS_STARTED);
+ if (err)
+ goto out;
+
+ genlmsg_end(skb, ehdr);
+
+ return ethnl_multicast(skb, phydev->attached_dev);
+
+out:
+ nlmsg_free(skb);
+ phydev_err(phydev, "%s: Error %pe\n", __func__, ERR_PTR(err));
+
+ return err;
+}
+
+int ethnl_act_cable_test(struct sk_buff *skb, struct genl_info *info)
+{
+ struct nlattr *tb[ETHTOOL_A_CABLE_TEST_MAX + 1];
+ struct ethnl_req_info req_info = {};
+ struct net_device *dev;
+ int ret;
+
+ ret = nlmsg_parse(info->nlhdr, GENL_HDRLEN, tb,
+ ETHTOOL_A_CABLE_TEST_MAX,
+ cable_test_act_policy, info->extack);
+ if (ret < 0)
+ return ret;
+
+ ret = ethnl_parse_header_dev_get(&req_info,
+ tb[ETHTOOL_A_CABLE_TEST_HEADER],
+ genl_info_net(info), info->extack,
+ true);
+ if (ret < 0)
+ return ret;
+
+ dev = req_info.dev;
+ if (!dev->phydev) {
+ ret = -EOPNOTSUPP;
+ goto out_dev_put;
+ }
+
+ rtnl_lock();
+ ret = ethnl_ops_begin(dev);
+ if (ret < 0)
+ goto out_rtnl;
+
+ ret = phy_start_cable_test(dev->phydev, info->extack);
+
+ ethnl_ops_complete(dev);
+
+ if (!ret)
+ ethnl_cable_test_started(dev->phydev);
+
+out_rtnl:
+ rtnl_unlock();
+out_dev_put:
+ dev_put(dev);
+ return ret;
+}
+
+int ethnl_cable_test_alloc(struct phy_device *phydev)
+{
+ int err = -ENOMEM;
+
+ phydev->skb = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
+ if (!phydev->skb)
+ goto out;
+
+ phydev->ehdr = ethnl_bcastmsg_put(phydev->skb,
+ ETHTOOL_MSG_CABLE_TEST_NTF);
+ if (!phydev->ehdr) {
+ err = -EMSGSIZE;
+ goto out;
+ }
+
+ err = ethnl_fill_reply_header(phydev->skb, phydev->attached_dev,
+ ETHTOOL_A_CABLE_TEST_NTF_HEADER);
+ if (err)
+ goto out;
+
+ err = nla_put_u8(phydev->skb, ETHTOOL_A_CABLE_TEST_NTF_STATUS,
+ ETHTOOL_A_CABLE_TEST_NTF_STATUS_COMPLETED);
+ if (err)
+ goto out;
+
+ phydev->nest = nla_nest_start(phydev->skb,
+ ETHTOOL_A_CABLE_TEST_NTF_NEST);
+ if (!phydev->nest) {
+ err = -EMSGSIZE;
+ goto out;
+ }
+
+ return 0;
+
+out:
+ nlmsg_free(phydev->skb);
+ phydev->skb = NULL;
+ return err;
+}
+EXPORT_SYMBOL_GPL(ethnl_cable_test_alloc);
+
+void ethnl_cable_test_free(struct phy_device *phydev)
+{
+ nlmsg_free(phydev->skb);
+ phydev->skb = NULL;
+}
+EXPORT_SYMBOL_GPL(ethnl_cable_test_free);
+
+void ethnl_cable_test_finished(struct phy_device *phydev)
+{
+ nla_nest_end(phydev->skb, phydev->nest);
+
+ genlmsg_end(phydev->skb, phydev->ehdr);
+
+ ethnl_multicast(phydev->skb, phydev->attached_dev);
+}
+EXPORT_SYMBOL_GPL(ethnl_cable_test_finished);
+
+int ethnl_cable_test_result(struct phy_device *phydev, u8 pair, u8 result)
+{
+ struct nlattr *nest;
+ int ret = -EMSGSIZE;
+
+ nest = nla_nest_start(phydev->skb, ETHTOOL_A_CABLE_NEST_RESULT);
+ if (!nest)
+ return -EMSGSIZE;
+
+ if (nla_put_u8(phydev->skb, ETHTOOL_A_CABLE_RESULT_PAIR, pair))
+ goto err;
+ if (nla_put_u8(phydev->skb, ETHTOOL_A_CABLE_RESULT_CODE, result))
+ goto err;
+
+ nla_nest_end(phydev->skb, nest);
+ return 0;
+
+err:
+ nla_nest_cancel(phydev->skb, nest);
+ return ret;
+}
+EXPORT_SYMBOL_GPL(ethnl_cable_test_result);
+
+int ethnl_cable_test_fault_length(struct phy_device *phydev, u8 pair, u32 cm)
+{
+ struct nlattr *nest;
+ int ret = -EMSGSIZE;
+
+ nest = nla_nest_start(phydev->skb,
+ ETHTOOL_A_CABLE_NEST_FAULT_LENGTH);
+ if (!nest)
+ return -EMSGSIZE;
+
+ if (nla_put_u8(phydev->skb, ETHTOOL_A_CABLE_FAULT_LENGTH_PAIR, pair))
+ goto err;
+ if (nla_put_u32(phydev->skb, ETHTOOL_A_CABLE_FAULT_LENGTH_CM, cm))
+ goto err;
+
+ nla_nest_end(phydev->skb, nest);
+ return 0;
+
+err:
+ nla_nest_cancel(phydev->skb, nest);
+ return ret;
+}
+EXPORT_SYMBOL_GPL(ethnl_cable_test_fault_length);
diff --git a/net/ethtool/netlink.c b/net/ethtool/netlink.c
index 0c772318c023..87bc02da74bc 100644
--- a/net/ethtool/netlink.c
+++ b/net/ethtool/netlink.c
@@ -181,13 +181,13 @@ err:
return NULL;
}
-static void *ethnl_bcastmsg_put(struct sk_buff *skb, u8 cmd)
+void *ethnl_bcastmsg_put(struct sk_buff *skb, u8 cmd)
{
return genlmsg_put(skb, 0, ++ethnl_bcast_seq, &ethtool_genl_family, 0,
cmd);
}
-static int ethnl_multicast(struct sk_buff *skb, struct net_device *dev)
+int ethnl_multicast(struct sk_buff *skb, struct net_device *dev)
{
return genlmsg_multicast_netns(&ethtool_genl_family, dev_net(dev), skb,
0, ETHNL_MCGRP_MONITOR, GFP_KERNEL);
@@ -839,6 +839,11 @@ static const struct genl_ops ethtool_genl_ops[] = {
.dumpit = ethnl_default_dumpit,
.done = ethnl_default_done,
},
+ {
+ .cmd = ETHTOOL_MSG_CABLE_TEST_ACT,
+ .flags = GENL_UNS_ADMIN_PERM,
+ .doit = ethnl_act_cable_test,
+ },
};
static const struct genl_multicast_group ethtool_nl_mcgrps[] = {
diff --git a/net/ethtool/netlink.h b/net/ethtool/netlink.h
index 81b8fa020bcb..b0eb5d920099 100644
--- a/net/ethtool/netlink.h
+++ b/net/ethtool/netlink.h
@@ -19,6 +19,8 @@ int ethnl_fill_reply_header(struct sk_buff *skb, struct net_device *dev,
struct sk_buff *ethnl_reply_init(size_t payload, struct net_device *dev, u8 cmd,
u16 hdr_attrtype, struct genl_info *info,
void **ehdrp);
+void *ethnl_bcastmsg_put(struct sk_buff *skb, u8 cmd);
+int ethnl_multicast(struct sk_buff *skb, struct net_device *dev);
/**
* ethnl_strz_size() - calculate attribute length for fixed size string
@@ -357,5 +359,6 @@ int ethnl_set_channels(struct sk_buff *skb, struct genl_info *info);
int ethnl_set_coalesce(struct sk_buff *skb, struct genl_info *info);
int ethnl_set_pause(struct sk_buff *skb, struct genl_info *info);
int ethnl_set_eee(struct sk_buff *skb, struct genl_info *info);
+int ethnl_act_cable_test(struct sk_buff *skb, struct genl_info *info);
#endif /* _NET_ETHTOOL_NETLINK_H */