diff mbox

[RFC,2/5] net: phy: sun8i-h3-ephy: Add driver for Allwinner H3 Ethernet PHY

Message ID 1459786954-12649-3-git-send-email-wens@csie.org
State RFC, archived
Delegated to: David Miller
Headers show

Commit Message

Chen-Yu Tsai April 4, 2016, 4:22 p.m. UTC
The Allwinner H3 SoC incorporates an Ethernet PHY. This is enabled and
configured through a memory mapped hardware register.

This same register also configures the MAC interface mode and TX clock
source. Also covered by the register, but not supported in this driver,
are TX/RX clock delay chains and inverters, and an RMII module.

Signed-off-by: Chen-Yu Tsai <wens@csie.org>
---
 drivers/net/phy/Kconfig         |   8 ++
 drivers/net/phy/Makefile        |   1 +
 drivers/net/phy/sun8i-h3-ephy.c | 266 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 275 insertions(+)
 create mode 100644 drivers/net/phy/sun8i-h3-ephy.c

Comments

Florian Fainelli April 11, 2016, 7:23 p.m. UTC | #1
On 04/04/16 09:22, Chen-Yu Tsai wrote:
> The Allwinner H3 SoC incorporates an Ethernet PHY. This is enabled and
> configured through a memory mapped hardware register.
> 
> This same register also configures the MAC interface mode and TX clock
> source. Also covered by the register, but not supported in this driver,
> are TX/RX clock delay chains and inverters, and an RMII module.

This is not really a PHY driver, more a driver for a special piece of
hardware responsible for properly configuring a more standard integrated
PHY which is then driven via standard MDIO accesses, right?

The intention to make this driver re-usable is fine, but still makes me
wonder if it should not be put in a file which is linked into the
Ethernet MAC driver, and utilized by this one in a way that may be more
"ad-hoc" than what you are proposing here.

One thing that is not obvious here, is how is the device parenting done?
Are we able to associate a phy_device to this SUN8I_H3_EPHY platform
device here?

Another thing is that the Ethernet MAC driver is fully aware of when
putting an Ethernet PHY into suspend, shutdown, or fully functional
power state should occur, if you have a separate platform driver here
which does not listen for such kinds of events (hint: none are generated
right now), then you cannot implement a working power state interface
between the MAC, SHIM and PHY here, even though you would want that.
Chen-Yu Tsai April 14, 2016, 2:04 a.m. UTC | #2
On Tue, Apr 12, 2016 at 3:23 AM, Florian Fainelli <f.fainelli@gmail.com> wrote:
> On 04/04/16 09:22, Chen-Yu Tsai wrote:
>> The Allwinner H3 SoC incorporates an Ethernet PHY. This is enabled and
>> configured through a memory mapped hardware register.
>>
>> This same register also configures the MAC interface mode and TX clock
>> source. Also covered by the register, but not supported in this driver,
>> are TX/RX clock delay chains and inverters, and an RMII module.
>
> This is not really a PHY driver, more a driver for a special piece of
> hardware responsible for properly configuring a more standard integrated
> PHY which is then driven via standard MDIO accesses, right?
>
> The intention to make this driver re-usable is fine, but still makes me
> wonder if it should not be put in a file which is linked into the
> Ethernet MAC driver, and utilized by this one in a way that may be more
> "ad-hoc" than what you are proposing here.
>
> One thing that is not obvious here, is how is the device parenting done?
> Are we able to associate a phy_device to this SUN8I_H3_EPHY platform
> device here?

AFAIK you can't tie a platform device to an MDIO bus and phy_device.
I looked around for any examples and found none.

We're going to run into a similar issue with an MFD device later on.
The X-Powers AC200 is a I2C based combo chip which includes an Ethernet
PHY.

> Another thing is that the Ethernet MAC driver is fully aware of when
> putting an Ethernet PHY into suspend, shutdown, or fully functional
> power state should occur, if you have a separate platform driver here
> which does not listen for such kinds of events (hint: none are generated
> right now), then you cannot implement a working power state interface
> between the MAC, SHIM and PHY here, even though you would want that.

For this particular piece of hardware it would be possible to move the
code into the MAC driver itself. For the AC200, not so much. Using the
generic PHY framework to do power up / down would be a possibility.


Regards
ChenYu
diff mbox

Patch

diff --git a/drivers/net/phy/Kconfig b/drivers/net/phy/Kconfig
index 6dad9a9c356c..f4b9eb6e9114 100644
--- a/drivers/net/phy/Kconfig
+++ b/drivers/net/phy/Kconfig
@@ -276,3 +276,11 @@  endif # PHYLIB
 config MICREL_KS8995MA
 	tristate "Micrel KS8995MA 5-ports 10/100 managed Ethernet switch"
 	depends on SPI
+
+config SUN8I_H3_EPHY
+	tristate "Allwinner sun8i H3 Ethernet PHY"
+	depends on OF && HAS_IOMEM && COMMON_CLK && RESET_CONTROLLER
+	depends on MACH_SUN8I || COMPILE_TEST
+	help
+	  This modules provides a driver for the internal Ethernet PHY
+	  and Ethernet MAC controls found in the Allwinner H3 SoC.
diff --git a/drivers/net/phy/Makefile b/drivers/net/phy/Makefile
index fcdbb9299fab..7cec3aceb518 100644
--- a/drivers/net/phy/Makefile
+++ b/drivers/net/phy/Makefile
@@ -44,3 +44,4 @@  obj-$(CONFIG_MDIO_MOXART)	+= mdio-moxart.o
 obj-$(CONFIG_MDIO_BCM_UNIMAC)	+= mdio-bcm-unimac.o
 obj-$(CONFIG_MICROCHIP_PHY)	+= microchip.o
 obj-$(CONFIG_MDIO_BCM_IPROC)	+= mdio-bcm-iproc.o
+obj-$(CONFIG_SUN8I_H3_EPHY)	+= sun8i-h3-ephy.o
diff --git a/drivers/net/phy/sun8i-h3-ephy.c b/drivers/net/phy/sun8i-h3-ephy.c
new file mode 100644
index 000000000000..bf2703bee024
--- /dev/null
+++ b/drivers/net/phy/sun8i-h3-ephy.c
@@ -0,0 +1,266 @@ 
+/*
+ * Allwinner sun8i H3 E(thernet) PHY driver
+ *
+ * Copyright (C) 2016 Chen-Yu Tsai <wens@csie.org>
+ *
+ * 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/err.h>
+#include <linux/io.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/reset.h>
+#include <linux/spinlock.h>
+
+#define REG_PHY_ADDR_SHIFT	20
+#define REG_PHY_ADDR_MASK	GENMASK(4, 0)
+#define REG_LED_POL		BIT(17)	/* 1: active low, 0: active high */
+#define REG_SHUTDOWN		BIT(16)	/* 1: shutdown, 0: power up */
+#define REG_PHY_SELECT		BIT(15) /* 1: internal PHY, 0: external PHY */
+
+#define REG_DEFAULT_VALUE	0x58000
+#define REG_DEFAULT_MASK	GENMASK(31, 15)
+
+#define REG_GPIT		2
+#define REG_ETCS_MASK		0x3
+#define SUN8I_H3_EMAC_PARENTS	2
+#define MII_PHY_CLK_RATE	25000000
+#define INT_TX_CLK_RATE		125000000
+#define MII_PHY_CLK_NAME	"mii_phy_tx"
+#define INT_TX_CLK_NAME		"int_tx"
+
+struct sun8i_h3_ephy {
+	struct device *dev;
+	void __iomem *reg;
+	struct clk *clk;
+	struct reset_control *reset;
+	struct clk *mac_clks[SUN8I_H3_EMAC_PARENTS + 1];
+};
+
+static DEFINE_SPINLOCK(sun8i_h3_ephy_lock);
+
+/* The EMAC clock/interface controls are much like those found on the A20,
+ * which is supported by sun7i-a20-gmac-clk. The H3 adds an RMII module,
+ * enabled by bit 13. Unfortunately vendor code and documentation don't
+ * explain  how it should work with the other bits, and no hardware
+ * actually uses it.
+ */
+static u32 sun8i_h3_emac_mux_table[SUN8I_H3_EMAC_PARENTS] = {
+	0x0,	/* Select TX clock from MII PHY */
+	0x2,	/* Select internal TX clock (for RGMII) */
+};
+
+static const char *sun8i_h3_emac_mux_parents[SUN8I_H3_EMAC_PARENTS] = {
+	MII_PHY_CLK_NAME,
+	INT_TX_CLK_NAME,
+};
+
+int sun8i_h3_ephy_register_clocks(struct sun8i_h3_ephy *phy)
+{
+	struct device *dev = phy->dev;
+	struct device_node *np = dev->of_node;
+	const char *clk_name;
+	struct clk_mux *mux;
+	struct clk_gate *gate;
+	int ret;
+
+	if (of_property_read_string(np, "clock-output-names", &clk_name)) {
+		dev_err(phy->dev, "No clock output name given\n");
+		return -EINVAL;
+	}
+
+	/* allocate mux and gate clock structs */
+	mux = devm_kzalloc(dev, sizeof(struct clk_mux), GFP_KERNEL);
+	if (!mux)
+		return -ENOMEM;
+
+	gate = devm_kzalloc(dev, sizeof(struct clk_gate), GFP_KERNEL);
+	if (!gate)
+		return -ENOMEM;
+
+	/* Register fixed clocks to use as parents */
+	phy->mac_clks[0] = clk_register_fixed_rate(dev, MII_PHY_CLK_NAME,
+						   NULL, 0, MII_PHY_CLK_RATE);
+	if (IS_ERR(phy->mac_clks[0]))
+		return PTR_ERR(phy->mac_clks[0]);
+
+	phy->mac_clks[1] = clk_register_fixed_rate(dev, INT_TX_CLK_NAME,
+						   NULL, 0, INT_TX_CLK_RATE);
+	if (IS_ERR(phy->mac_clks[1])) {
+		ret = PTR_ERR(phy->mac_clks[1]);
+		goto err_int_tx_clk;
+	}
+
+	/* set up gate and mux properties */
+	gate->reg = phy->reg;
+	gate->bit_idx = REG_GPIT;
+	gate->lock = &sun8i_h3_ephy_lock;
+	mux->reg = phy->reg;
+	mux->mask = REG_ETCS_MASK;
+	mux->table = sun8i_h3_emac_mux_table;
+	mux->lock = &sun8i_h3_ephy_lock;
+
+	phy->mac_clks[2] = clk_register_composite(dev, clk_name,
+						  sun8i_h3_emac_mux_parents,
+						  SUN8I_H3_EMAC_PARENTS,
+						  &mux->hw, &clk_mux_ops,
+						  NULL, NULL,
+						  &gate->hw, &clk_gate_ops,
+						  0);
+	if (IS_ERR(phy->mac_clks[2])) {
+		ret = PTR_ERR(phy->mac_clks[2]);
+		goto err_tx_clk;
+	}
+
+	ret = of_clk_add_provider(dev->of_node, of_clk_src_simple_get,
+				  phy->mac_clks[2]);
+	if (ret)
+		goto err_of_clk;
+
+	return 0;
+
+err_of_clk:
+	/* TODO: switch to clk_unregister_composite when it's available */
+	clk_unregister(phy->mac_clks[2]);
+err_tx_clk:
+	clk_unregister_fixed_rate(phy->mac_clks[1]);
+err_int_tx_clk:
+	clk_unregister_fixed_rate(phy->mac_clks[0]);
+
+	return ret;
+}
+
+static void sun8i_h3_ephy_init_phy(struct sun8i_h3_ephy *phy, u32 addr)
+{
+	u32 val = readl(phy->reg);
+
+	/* set default values, but don't touch clock settings */
+	val = (val & ~REG_DEFAULT_MASK) | REG_DEFAULT_VALUE;
+
+	if (addr < REG_PHY_ADDR_MASK) {
+		dev_info(phy->dev, "Using interal PHY at 0x%x\n", addr);
+
+		val |= addr << REG_PHY_ADDR_SHIFT;
+		val &= ~REG_SHUTDOWN;
+		val |= REG_PHY_SELECT;
+
+		if (of_property_read_bool(phy->dev->of_node,
+					  "allwinner,leds-active-low"))
+			val |= REG_LED_POL;
+	} else {
+		dev_info(phy->dev, "Using external PHY\n");
+	}
+
+	writel(val, phy->reg);
+}
+
+static void sun8i_h3_ephy_reset_phy(struct sun8i_h3_ephy *phy)
+{
+	u32 val;
+
+	val = readl(phy->reg);
+	val = (val & ~REG_DEFAULT_MASK) | REG_DEFAULT_VALUE;
+	writel(val, phy->reg);
+}
+
+int sun8i_h3_ephy_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct device_node *np = dev->of_node;
+	struct sun8i_h3_ephy *phy;
+	struct resource *res;
+	u32 addr = REG_PHY_ADDR_MASK;
+	int ret;
+
+	ret = of_property_read_u32(np, "allwinner,ephy-addr", &addr);
+	if (!ret && addr > REG_PHY_ADDR_MASK) {
+		dev_err(dev, "invalid PHY address: 0x%x\n", addr);
+		return -EINVAL;
+	} else if (ret && ret != -EINVAL) {
+		dev_err(dev, "could not get PHY address: %d\n", ret);
+		return ret;
+	}
+
+	phy = devm_kzalloc(dev, sizeof(*phy), GFP_KERNEL);
+	if (!phy)
+		return -ENOMEM;
+
+	phy->dev = dev;
+	platform_set_drvdata(pdev, phy);
+
+	phy->clk = devm_clk_get(dev, NULL);
+	if (IS_ERR(phy->clk)) {
+		dev_err(dev, "failed to get clock\n");
+		return PTR_ERR(phy->clk);
+	}
+
+	phy->reset = devm_reset_control_get(dev, NULL);
+	if (IS_ERR(phy->reset)) {
+		dev_err(dev, "failed to get reset control\n");
+		return PTR_ERR(phy->reset);
+	}
+
+	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+	phy->reg = devm_ioremap_resource(dev, res);
+	if (IS_ERR(phy->reg)) {
+		dev_err(dev, "failed to map registers\n");
+		return PTR_ERR(phy->reg);
+	}
+
+	ret = clk_prepare_enable(phy->clk);
+	if (ret) {
+		dev_err(dev, "failed to enable clock\n");
+		return ret;
+	}
+
+	ret = reset_control_deassert(phy->reset);
+	if (ret) {
+		dev_err(dev, "failed to deassert reset control\n");
+		goto err_reset;
+	}
+
+	sun8i_h3_ephy_init_phy(phy, addr);
+
+	ret = sun8i_h3_ephy_register_clocks(phy);
+	if (ret)
+		goto err_tx_clk;
+
+	return 0;
+
+err_tx_clk:
+	sun8i_h3_ephy_reset_phy(phy);
+	reset_control_assert(phy->reset);
+err_reset:
+	clk_disable_unprepare(phy->clk);
+	return ret;
+}
+
+static const struct of_device_id sun8i_h3_ephy_of_match[] = {
+	{ .compatible = "allwinner,sun8i-h3-ephy", },
+	{ },
+};
+MODULE_DEVICE_TABLE(of, sun8i_h3_ephy_of_match);
+
+static struct platform_driver sun8i_h3_ephy_driver = {
+	.probe	= sun8i_h3_ephy_probe,
+	.driver = {
+		.of_match_table	= sun8i_h3_ephy_of_match,
+		.name  = "sun8i-h3-ephy",
+	}
+};
+module_platform_driver(sun8i_h3_ephy_driver);
+
+MODULE_DESCRIPTION("Allwinner sun8i H3 Ethernet PHY driver");
+MODULE_AUTHOR("Chen-Yu Tsai <wens@csie.org>");
+MODULE_LICENSE("GPL v2");