| /** | 
 |  * drivers/net/phy/rockchip.c | 
 |  * | 
 |  * Driver for ROCKCHIP Ethernet PHYs | 
 |  * | 
 |  * Copyright (c) 2017, Fuzhou Rockchip Electronics Co., Ltd | 
 |  * | 
 |  * David Wu <david.wu@rock-chips.com> | 
 |  * | 
 |  * This program is free software; you can redistribute it and/or modify | 
 |  * it under the terms of the GNU General Public License as published by | 
 |  * the Free Software Foundation; either version 2 of the License, or | 
 |  * (at your option) any later version. | 
 |  * | 
 |  */ | 
 |  | 
 | #include <linux/ethtool.h> | 
 | #include <linux/kernel.h> | 
 | #include <linux/module.h> | 
 | #include <linux/mii.h> | 
 | #include <linux/netdevice.h> | 
 | #include <linux/phy.h> | 
 |  | 
 | #define INTERNAL_EPHY_ID			0x1234d400 | 
 |  | 
 | #define MII_INTERNAL_CTRL_STATUS		17 | 
 | #define SMI_ADDR_TSTCNTL			20 | 
 | #define SMI_ADDR_TSTREAD1			21 | 
 | #define SMI_ADDR_TSTREAD2			22 | 
 | #define SMI_ADDR_TSTWRITE			23 | 
 | #define MII_SPECIAL_CONTROL_STATUS		31 | 
 |  | 
 | #define MII_AUTO_MDIX_EN			BIT(7) | 
 | #define MII_MDIX_EN				BIT(6) | 
 |  | 
 | #define MII_SPEED_10				BIT(2) | 
 | #define MII_SPEED_100				BIT(3) | 
 |  | 
 | #define TSTCNTL_RD				(BIT(15) | BIT(10)) | 
 | #define TSTCNTL_WR				(BIT(14) | BIT(10)) | 
 |  | 
 | #define TSTMODE_ENABLE				0x400 | 
 | #define TSTMODE_DISABLE				0x0 | 
 |  | 
 | #define WR_ADDR_A7CFG				0x18 | 
 |  | 
 | static int rockchip_init_tstmode(struct phy_device *phydev) | 
 | { | 
 | 	int ret; | 
 |  | 
 | 	/* Enable access to Analog and DSP register banks */ | 
 | 	ret = phy_write(phydev, SMI_ADDR_TSTCNTL, TSTMODE_ENABLE); | 
 | 	if (ret) | 
 | 		return ret; | 
 |  | 
 | 	ret = phy_write(phydev, SMI_ADDR_TSTCNTL, TSTMODE_DISABLE); | 
 | 	if (ret) | 
 | 		return ret; | 
 |  | 
 | 	return phy_write(phydev, SMI_ADDR_TSTCNTL, TSTMODE_ENABLE); | 
 | } | 
 |  | 
 | static int rockchip_close_tstmode(struct phy_device *phydev) | 
 | { | 
 | 	/* Back to basic register bank */ | 
 | 	return phy_write(phydev, SMI_ADDR_TSTCNTL, TSTMODE_DISABLE); | 
 | } | 
 |  | 
 | static int rockchip_integrated_phy_analog_init(struct phy_device *phydev) | 
 | { | 
 | 	int ret; | 
 |  | 
 | 	ret = rockchip_init_tstmode(phydev); | 
 | 	if (ret) | 
 | 		return ret; | 
 |  | 
 | 	/* | 
 | 	 * Adjust tx amplitude to make sginal better, | 
 | 	 * the default value is 0x8. | 
 | 	 */ | 
 | 	ret = phy_write(phydev, SMI_ADDR_TSTWRITE, 0xB); | 
 | 	if (ret) | 
 | 		return ret; | 
 | 	ret = phy_write(phydev, SMI_ADDR_TSTCNTL, TSTCNTL_WR | WR_ADDR_A7CFG); | 
 | 	if (ret) | 
 | 		return ret; | 
 |  | 
 | 	return rockchip_close_tstmode(phydev); | 
 | } | 
 |  | 
 | static int rockchip_integrated_phy_config_init(struct phy_device *phydev) | 
 | { | 
 | 	int val, ret; | 
 |  | 
 | 	/* | 
 | 	 * The auto MIDX has linked problem on some board, | 
 | 	 * workround to disable auto MDIX. | 
 | 	 */ | 
 | 	val = phy_read(phydev, MII_INTERNAL_CTRL_STATUS); | 
 | 	if (val < 0) | 
 | 		return val; | 
 | 	val &= ~MII_AUTO_MDIX_EN; | 
 | 	ret = phy_write(phydev, MII_INTERNAL_CTRL_STATUS, val); | 
 | 	if (ret) | 
 | 		return ret; | 
 |  | 
 | 	return rockchip_integrated_phy_analog_init(phydev); | 
 | } | 
 |  | 
 | static void rockchip_link_change_notify(struct phy_device *phydev) | 
 | { | 
 | 	int speed = SPEED_10; | 
 |  | 
 | 	if (phydev->autoneg == AUTONEG_ENABLE) { | 
 | 		int reg = phy_read(phydev, MII_SPECIAL_CONTROL_STATUS); | 
 |  | 
 | 		if (reg < 0) { | 
 | 			phydev_err(phydev, "phy_read err: %d.\n", reg); | 
 | 			return; | 
 | 		} | 
 |  | 
 | 		if (reg & MII_SPEED_100) | 
 | 			speed = SPEED_100; | 
 | 		else if (reg & MII_SPEED_10) | 
 | 			speed = SPEED_10; | 
 | 	} else { | 
 | 		int bmcr = phy_read(phydev, MII_BMCR); | 
 |  | 
 | 		if (bmcr < 0) { | 
 | 			phydev_err(phydev, "phy_read err: %d.\n", bmcr); | 
 | 			return; | 
 | 		} | 
 |  | 
 | 		if (bmcr & BMCR_SPEED100) | 
 | 			speed = SPEED_100; | 
 | 		else | 
 | 			speed = SPEED_10; | 
 | 	} | 
 |  | 
 | 	/* | 
 | 	 * If mode switch happens from 10BT to 100BT, all DSP/AFE | 
 | 	 * registers are set to default values. So any AFE/DSP | 
 | 	 * registers have to be re-initialized in this case. | 
 | 	 */ | 
 | 	if ((phydev->speed == SPEED_10) && (speed == SPEED_100)) { | 
 | 		int ret = rockchip_integrated_phy_analog_init(phydev); | 
 | 		if (ret) | 
 | 			phydev_err(phydev, "rockchip_integrated_phy_analog_init err: %d.\n", | 
 | 				   ret); | 
 | 	} | 
 | } | 
 |  | 
 | static int rockchip_set_polarity(struct phy_device *phydev, int polarity) | 
 | { | 
 | 	int reg, err, val; | 
 |  | 
 | 	/* get the current settings */ | 
 | 	reg = phy_read(phydev, MII_INTERNAL_CTRL_STATUS); | 
 | 	if (reg < 0) | 
 | 		return reg; | 
 |  | 
 | 	reg &= ~MII_AUTO_MDIX_EN; | 
 | 	val = reg; | 
 | 	switch (polarity) { | 
 | 	case ETH_TP_MDI: | 
 | 		val &= ~MII_MDIX_EN; | 
 | 		break; | 
 | 	case ETH_TP_MDI_X: | 
 | 		val |= MII_MDIX_EN; | 
 | 		break; | 
 | 	case ETH_TP_MDI_AUTO: | 
 | 	case ETH_TP_MDI_INVALID: | 
 | 	default: | 
 | 		return 0; | 
 | 	} | 
 |  | 
 | 	if (val != reg) { | 
 | 		/* Set the new polarity value in the register */ | 
 | 		err = phy_write(phydev, MII_INTERNAL_CTRL_STATUS, val); | 
 | 		if (err) | 
 | 			return err; | 
 | 	} | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static int rockchip_config_aneg(struct phy_device *phydev) | 
 | { | 
 | 	int err; | 
 |  | 
 | 	err = rockchip_set_polarity(phydev, phydev->mdix); | 
 | 	if (err < 0) | 
 | 		return err; | 
 |  | 
 | 	return genphy_config_aneg(phydev); | 
 | } | 
 |  | 
 | static int rockchip_phy_resume(struct phy_device *phydev) | 
 | { | 
 | 	genphy_resume(phydev); | 
 |  | 
 | 	return rockchip_integrated_phy_config_init(phydev); | 
 | } | 
 |  | 
 | static struct phy_driver rockchip_phy_driver[] = { | 
 | { | 
 | 	.phy_id			= INTERNAL_EPHY_ID, | 
 | 	.phy_id_mask		= 0xfffffff0, | 
 | 	.name			= "Rockchip integrated EPHY", | 
 | 	.features		= PHY_BASIC_FEATURES, | 
 | 	.flags			= 0, | 
 | 	.link_change_notify	= rockchip_link_change_notify, | 
 | 	.soft_reset		= genphy_soft_reset, | 
 | 	.config_init		= rockchip_integrated_phy_config_init, | 
 | 	.config_aneg		= rockchip_config_aneg, | 
 | 	.suspend		= genphy_suspend, | 
 | 	.resume			= rockchip_phy_resume, | 
 | }, | 
 | }; | 
 |  | 
 | module_phy_driver(rockchip_phy_driver); | 
 |  | 
 | static struct mdio_device_id __maybe_unused rockchip_phy_tbl[] = { | 
 | 	{ INTERNAL_EPHY_ID, 0xfffffff0 }, | 
 | 	{ } | 
 | }; | 
 |  | 
 | MODULE_DEVICE_TABLE(mdio, rockchip_phy_tbl); | 
 |  | 
 | MODULE_AUTHOR("David Wu <david.wu@rock-chips.com>"); | 
 | MODULE_DESCRIPTION("Rockchip Ethernet PHY driver"); | 
 | MODULE_LICENSE("GPL v2"); |