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

From: Chen-Yu Tsai
Date: Mon Apr 04 2016 - 12:23:30 EST


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@xxxxxxxx>
---
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

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@xxxxxxxx>
+ *
+ * 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@xxxxxxxx>");
+MODULE_LICENSE("GPL v2");
--
2.7.0