*/
 
 #include "bcm-phy-lib.h"
+#include <linux/bitfield.h>
 #include <linux/brcmphy.h>
 #include <linux/export.h>
 #include <linux/mdio.h>
 #include <linux/module.h>
 #include <linux/phy.h>
 #include <linux/ethtool.h>
+#include <linux/ethtool_netlink.h>
 
 #define MII_BCM_CHANNEL_WIDTH     0x2000
 #define BCM_CL45VEN_EEE_ADV       0x3c
 }
 EXPORT_SYMBOL_GPL(bcm_phy_enable_jumbo);
 
+int __bcm_phy_enable_rdb_access(struct phy_device *phydev)
+{
+       return __bcm_phy_write_exp(phydev, BCM54XX_EXP_REG7E, 0);
+}
+EXPORT_SYMBOL_GPL(__bcm_phy_enable_rdb_access);
+
+int __bcm_phy_enable_legacy_access(struct phy_device *phydev)
+{
+       return __bcm_phy_write_rdb(phydev, BCM54XX_RDB_REG0087,
+                                  BCM54XX_ACCESS_MODE_LEGACY_EN);
+}
+EXPORT_SYMBOL_GPL(__bcm_phy_enable_legacy_access);
+
+static int _bcm_phy_cable_test_start(struct phy_device *phydev, bool is_rdb)
+{
+       u16 mask, set;
+       int ret;
+
+       /* Auto-negotiation must be enabled for cable diagnostics to work, but
+        * don't advertise any capabilities.
+        */
+       phy_write(phydev, MII_BMCR, BMCR_ANENABLE);
+       phy_write(phydev, MII_ADVERTISE, ADVERTISE_CSMA);
+       phy_write(phydev, MII_CTRL1000, 0);
+
+       phy_lock_mdio_bus(phydev);
+       if (is_rdb) {
+               ret = __bcm_phy_enable_legacy_access(phydev);
+               if (ret)
+                       goto out;
+       }
+
+       mask = BCM54XX_ECD_CTRL_CROSS_SHORT_DIS | BCM54XX_ECD_CTRL_UNIT_MASK;
+       set = BCM54XX_ECD_CTRL_RUN | BCM54XX_ECD_CTRL_BREAK_LINK |
+             FIELD_PREP(BCM54XX_ECD_CTRL_UNIT_MASK,
+                        BCM54XX_ECD_CTRL_UNIT_CM);
+
+       ret = __bcm_phy_modify_exp(phydev, BCM54XX_EXP_ECD_CTRL, mask, set);
+
+out:
+       /* re-enable the RDB access even if there was an error */
+       if (is_rdb)
+               ret = __bcm_phy_enable_rdb_access(phydev) ? : ret;
+
+       phy_unlock_mdio_bus(phydev);
+
+       return ret;
+}
+
+static int bcm_phy_cable_test_report_trans(int result)
+{
+       switch (result) {
+       case BCM54XX_ECD_FAULT_TYPE_OK:
+               return ETHTOOL_A_CABLE_RESULT_CODE_OK;
+       case BCM54XX_ECD_FAULT_TYPE_OPEN:
+               return ETHTOOL_A_CABLE_RESULT_CODE_OPEN;
+       case BCM54XX_ECD_FAULT_TYPE_SAME_SHORT:
+               return ETHTOOL_A_CABLE_RESULT_CODE_SAME_SHORT;
+       case BCM54XX_ECD_FAULT_TYPE_CROSS_SHORT:
+               return ETHTOOL_A_CABLE_RESULT_CODE_CROSS_SHORT;
+       case BCM54XX_ECD_FAULT_TYPE_INVALID:
+       case BCM54XX_ECD_FAULT_TYPE_BUSY:
+       default:
+               return ETHTOOL_A_CABLE_RESULT_CODE_UNSPEC;
+       }
+}
+
+static bool bcm_phy_distance_valid(int result)
+{
+       switch (result) {
+       case BCM54XX_ECD_FAULT_TYPE_OPEN:
+       case BCM54XX_ECD_FAULT_TYPE_SAME_SHORT:
+       case BCM54XX_ECD_FAULT_TYPE_CROSS_SHORT:
+               return true;
+       }
+       return false;
+}
+
+static int bcm_phy_report_length(struct phy_device *phydev, int pair)
+{
+       int val;
+
+       val = __bcm_phy_read_exp(phydev,
+                                BCM54XX_EXP_ECD_PAIR_A_LENGTH_RESULTS + pair);
+       if (val < 0)
+               return val;
+
+       if (val == BCM54XX_ECD_LENGTH_RESULTS_INVALID)
+               return 0;
+
+       ethnl_cable_test_fault_length(phydev, pair, val);
+
+       return 0;
+}
+
+static int _bcm_phy_cable_test_get_status(struct phy_device *phydev,
+                                         bool *finished, bool is_rdb)
+{
+       int pair_a, pair_b, pair_c, pair_d, ret;
+
+       *finished = false;
+
+       phy_lock_mdio_bus(phydev);
+
+       if (is_rdb) {
+               ret = __bcm_phy_enable_legacy_access(phydev);
+               if (ret)
+                       goto out;
+       }
+
+       ret = __bcm_phy_read_exp(phydev, BCM54XX_EXP_ECD_CTRL);
+       if (ret < 0)
+               goto out;
+
+       if (ret & BCM54XX_ECD_CTRL_IN_PROGRESS) {
+               ret = 0;
+               goto out;
+       }
+
+       ret = __bcm_phy_read_exp(phydev, BCM54XX_EXP_ECD_FAULT_TYPE);
+       if (ret < 0)
+               goto out;
+
+       pair_a = FIELD_GET(BCM54XX_ECD_FAULT_TYPE_PAIR_A_MASK, ret);
+       pair_b = FIELD_GET(BCM54XX_ECD_FAULT_TYPE_PAIR_B_MASK, ret);
+       pair_c = FIELD_GET(BCM54XX_ECD_FAULT_TYPE_PAIR_C_MASK, ret);
+       pair_d = FIELD_GET(BCM54XX_ECD_FAULT_TYPE_PAIR_D_MASK, ret);
+
+       ethnl_cable_test_result(phydev, ETHTOOL_A_CABLE_PAIR_A,
+                               bcm_phy_cable_test_report_trans(pair_a));
+       ethnl_cable_test_result(phydev, ETHTOOL_A_CABLE_PAIR_B,
+                               bcm_phy_cable_test_report_trans(pair_b));
+       ethnl_cable_test_result(phydev, ETHTOOL_A_CABLE_PAIR_C,
+                               bcm_phy_cable_test_report_trans(pair_c));
+       ethnl_cable_test_result(phydev, ETHTOOL_A_CABLE_PAIR_D,
+                               bcm_phy_cable_test_report_trans(pair_d));
+
+       if (bcm_phy_distance_valid(pair_a))
+               bcm_phy_report_length(phydev, 0);
+       if (bcm_phy_distance_valid(pair_b))
+               bcm_phy_report_length(phydev, 1);
+       if (bcm_phy_distance_valid(pair_c))
+               bcm_phy_report_length(phydev, 2);
+       if (bcm_phy_distance_valid(pair_d))
+               bcm_phy_report_length(phydev, 3);
+
+       ret = 0;
+       *finished = true;
+out:
+       /* re-enable the RDB access even if there was an error */
+       if (is_rdb)
+               ret = __bcm_phy_enable_rdb_access(phydev) ? : ret;
+
+       phy_unlock_mdio_bus(phydev);
+
+       return ret;
+}
+
+int bcm_phy_cable_test_start(struct phy_device *phydev)
+{
+       return _bcm_phy_cable_test_start(phydev, false);
+}
+EXPORT_SYMBOL_GPL(bcm_phy_cable_test_start);
+
+int bcm_phy_cable_test_get_status(struct phy_device *phydev, bool *finished)
+{
+       return _bcm_phy_cable_test_get_status(phydev, finished, false);
+}
+EXPORT_SYMBOL_GPL(bcm_phy_cable_test_get_status);
+
+/* We assume that all PHYs which support RDB access can be switched to legacy
+ * mode. If, in the future, this is not true anymore, we have to re-implement
+ * this with RDB access.
+ */
+int bcm_phy_cable_test_start_rdb(struct phy_device *phydev)
+{
+       return _bcm_phy_cable_test_start(phydev, true);
+}
+EXPORT_SYMBOL_GPL(bcm_phy_cable_test_start_rdb);
+
+int bcm_phy_cable_test_get_status_rdb(struct phy_device *phydev,
+                                     bool *finished)
+{
+       return _bcm_phy_cable_test_get_status(phydev, finished, true);
+}
+EXPORT_SYMBOL_GPL(bcm_phy_cable_test_get_status_rdb);
+
 MODULE_DESCRIPTION("Broadcom PHY Library");
 MODULE_LICENSE("GPL v2");
 MODULE_AUTHOR("Broadcom Corporation");
 
 #define MII_BCM54XX_RDB_ADDR   0x1e
 #define MII_BCM54XX_RDB_DATA   0x1f
 
+/* legacy access control via rdb/expansion register */
+#define BCM54XX_RDB_REG0087            0x0087
+#define BCM54XX_EXP_REG7E              (MII_BCM54XX_EXP_SEL_ER + 0x7E)
+#define BCM54XX_ACCESS_MODE_LEGACY_EN  BIT(15)
+
 /*
  * AUXILIARY CONTROL SHADOW ACCESS REGISTERS.  (PHY REG 0x18)
  */
 #define MII_BRCM_CORE_EXPB0    0xB0
 #define MII_BRCM_CORE_EXPB1    0xB1
 
+/* Enhanced Cable Diagnostics */
+#define BCM54XX_RDB_ECD_CTRL                   0x2a0
+#define BCM54XX_EXP_ECD_CTRL                   (MII_BCM54XX_EXP_SEL_ER + 0xc0)
+
+#define BCM54XX_ECD_CTRL_CABLE_TYPE_CAT3       1       /* CAT3 or worse */
+#define BCM54XX_ECD_CTRL_CABLE_TYPE_CAT5       0       /* CAT5 or better */
+#define BCM54XX_ECD_CTRL_CABLE_TYPE_MASK       BIT(0)  /* cable type */
+#define BCM54XX_ECD_CTRL_INVALID               BIT(3)  /* invalid result */
+#define BCM54XX_ECD_CTRL_UNIT_CM               0       /* centimeters */
+#define BCM54XX_ECD_CTRL_UNIT_M                        1       /* meters */
+#define BCM54XX_ECD_CTRL_UNIT_MASK             BIT(10) /* cable length unit */
+#define BCM54XX_ECD_CTRL_IN_PROGRESS           BIT(11) /* test in progress */
+#define BCM54XX_ECD_CTRL_BREAK_LINK            BIT(12) /* unconnect link
+                                                        * during test
+                                                        */
+#define BCM54XX_ECD_CTRL_CROSS_SHORT_DIS       BIT(13) /* disable inter-pair
+                                                        * short check
+                                                        */
+#define BCM54XX_ECD_CTRL_RUN                   BIT(15) /* run immediate */
+
+#define BCM54XX_RDB_ECD_FAULT_TYPE             0x2a1
+#define BCM54XX_EXP_ECD_FAULT_TYPE             (MII_BCM54XX_EXP_SEL_ER + 0xc1)
+#define BCM54XX_ECD_FAULT_TYPE_INVALID         0x0
+#define BCM54XX_ECD_FAULT_TYPE_OK              0x1
+#define BCM54XX_ECD_FAULT_TYPE_OPEN            0x2
+#define BCM54XX_ECD_FAULT_TYPE_SAME_SHORT      0x3 /* short same pair */
+#define BCM54XX_ECD_FAULT_TYPE_CROSS_SHORT     0x4 /* short different pairs */
+#define BCM54XX_ECD_FAULT_TYPE_BUSY            0x9
+#define BCM54XX_ECD_FAULT_TYPE_PAIR_D_MASK     GENMASK(3, 0)
+#define BCM54XX_ECD_FAULT_TYPE_PAIR_C_MASK     GENMASK(7, 4)
+#define BCM54XX_ECD_FAULT_TYPE_PAIR_B_MASK     GENMASK(11, 8)
+#define BCM54XX_ECD_FAULT_TYPE_PAIR_A_MASK     GENMASK(15, 12)
+#define BCM54XX_ECD_PAIR_A_LENGTH_RESULTS      0x2a2
+#define BCM54XX_ECD_PAIR_B_LENGTH_RESULTS      0x2a3
+#define BCM54XX_ECD_PAIR_C_LENGTH_RESULTS      0x2a4
+#define BCM54XX_ECD_PAIR_D_LENGTH_RESULTS      0x2a5
+
+#define BCM54XX_RDB_ECD_PAIR_A_LENGTH_RESULTS  0x2a2
+#define BCM54XX_EXP_ECD_PAIR_A_LENGTH_RESULTS  (MII_BCM54XX_EXP_SEL_ER + 0xc2)
+#define BCM54XX_RDB_ECD_PAIR_B_LENGTH_RESULTS  0x2a3
+#define BCM54XX_EXP_ECD_PAIR_B_LENGTH_RESULTS  (MII_BCM54XX_EXP_SEL_ER + 0xc3)
+#define BCM54XX_RDB_ECD_PAIR_C_LENGTH_RESULTS  0x2a4
+#define BCM54XX_EXP_ECD_PAIR_C_LENGTH_RESULTS  (MII_BCM54XX_EXP_SEL_ER + 0xc4)
+#define BCM54XX_RDB_ECD_PAIR_D_LENGTH_RESULTS  0x2a5
+#define BCM54XX_EXP_ECD_PAIR_D_LENGTH_RESULTS  (MII_BCM54XX_EXP_SEL_ER + 0xc5)
+#define BCM54XX_ECD_LENGTH_RESULTS_INVALID     0xffff
+
 #endif /* _LINUX_BRCMPHY_H */