summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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 */