diff options
Diffstat (limited to 'drivers/net/ksz8873.c')
-rw-r--r-- | drivers/net/ksz8873.c | 450 |
1 files changed, 450 insertions, 0 deletions
diff --git a/drivers/net/ksz8873.c b/drivers/net/ksz8873.c new file mode 100644 index 0000000000..03d3530754 --- /dev/null +++ b/drivers/net/ksz8873.c @@ -0,0 +1,450 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <common.h> +#include <complete.h> +#include <dsa.h> +#include <linux/gpio/consumer.h> +#include <linux/mii.h> +#include <linux/mdio.h> +#include <net.h> +#include <of_device.h> +#include <linux/regmap.h> + +#define KSZ8873_CHIP_ID0 0x00 +#define KSZ8873_CHIP_ID1 0x01 +#define KSZ88_CHIP_ID_M GENMASK(7, 4) +#define KSZ88_REV_ID_M GENMASK(3, 1) + +#define KSZ8873_GLOBAL_CTRL_1 0x03 +#define KSZ8873_PASS_ALL_FRAMES BIT(7) +#define KSZ8873_P3_TAIL_TAG_EN BIT(6) + +/* + * port specific registers. Should be used with ksz_pwrite/ksz_pread functions + */ +#define KSZ8873_PORTx_CTRL_1 0x01 +#define KSZ8873_PORTx_CTRL_12 0x0c + +#define PORT_AUTO_NEG_ENABLE BIT(7) +#define PORT_FORCE_100_MBIT BIT(6) +#define PORT_FORCE_FULL_DUPLEX BIT(5) +#define PORT_AUTO_NEG_100BTX_FD BIT(3) +#define PORT_AUTO_NEG_100BTX BIT(2) +#define PORT_AUTO_NEG_10BT_FD BIT(1) +#define PORT_AUTO_NEG_10BT BIT(0) + +#define KSZ8873_PORTx_CTRL_13 0x0d + +#define PORT_AUTO_NEG_RESTART BIT(5) +#define PORT_POWER_DOWN BIT(3) + +#define KSZ8873_PORTx_STATUS_0 0x0e + +#define PORT_AUTO_NEG_COMPLETE BIT(6) +#define PORT_STAT_LINK_GOOD BIT(5) +#define PORT_REMOTE_100BTX_FD BIT(3) +#define PORT_REMOTE_100BTX BIT(2) +#define PORT_REMOTE_10BT_FD BIT(1) +#define PORT_REMOTE_10BT BIT(0) + +#define KSZ8873_PORTx_STATUS_1 0x0f + +#define KSZ8795_ID_HI 0x0022 +#define KSZ8863_ID_LO 0x1430 + +#define PORT_CTRL_ADDR(port, addr) ((addr) + 0x10 + (port) * 0x10) + +struct ksz8873_dcfg { + unsigned int num_ports; + unsigned int phy_port_cnt; + u8 id0; + u8 id1; +}; + +struct ksz8873_switch { + struct phy_device *mdiodev; + struct dsa_switch ds; + struct device *dev; + const struct ksz8873_dcfg *dcfg; + struct regmap *regmap; +}; + +/* Serial Management Interface (SMI) uses the following frame format: + * + * preamble|start|Read/Write| PHY | REG |TA| Data bits | Idle + * |frame| OP code |address |address| | | + * read | 32x1´s | 01 | 00 | 1xRRR | RRRRR |Z0| 00000000DDDDDDDD | Z + * write| 32x1´s | 01 | 00 | 0xRRR | RRRRR |10| xxxxxxxxDDDDDDDD | Z + * + */ + +#define SMI_KSZ88XX_READ_PHY BIT(4) + +static int ksz8873_mdio_read(void *ctx, unsigned int reg, unsigned int *val) +{ + struct ksz8873_switch *priv = ctx; + struct phy_device *mdiodev = priv->mdiodev; + int ret; + + ret = mdiobus_read(mdiodev->bus, ((reg & 0xE0) >> 5) | + SMI_KSZ88XX_READ_PHY, reg); + if (ret < 0) + return ret; + + *val = ret; + + return 0; +} + +static int ksz8873_mdio_write(void *ctx, unsigned int reg, unsigned int val) +{ + struct ksz8873_switch *priv = ctx; + struct phy_device *mdiodev = priv->mdiodev; + + return mdiobus_write(mdiodev->bus, ((reg & 0xE0) >> 5), reg, val); +} + +static const struct regmap_bus ksz8873_regmap_smi = { + .reg_read = ksz8873_mdio_read, + .reg_write = ksz8873_mdio_write, +}; + +static const struct regmap_config ksz8873_regmap_config = { + .name = "#8", + .reg_bits = 8, + .pad_bits = 24, + .val_bits = 8, +}; + +static int ksz_read8(struct ksz8873_switch *priv, u32 reg, u8 *val) +{ + unsigned int value; + int ret = regmap_read(priv->regmap, reg, &value); + + *val = value & 0xff; + + return ret; +} + +static int ksz_write8(struct ksz8873_switch *priv, u32 reg, u8 value) +{ + return regmap_write(priv->regmap, reg, value); +} + +static int ksz_pread8(struct ksz8873_switch *priv, int port, int reg, u8 *val) +{ + return ksz_read8(priv, PORT_CTRL_ADDR(port, reg), val); +} + +static int ksz_pwrite8(struct ksz8873_switch *priv, int port, int reg, u8 val) +{ + return ksz_write8(priv, PORT_CTRL_ADDR(port, reg), val); +} + +static void ksz8_r_phy(struct ksz8873_switch *priv, u16 phy, u16 reg, u16 *val) +{ + u8 restart, ctrl, link; + int processed = true; + u16 data = 0; + u8 p = phy; + + switch (reg) { + case MII_BMCR: + ksz_pread8(priv, p, KSZ8873_PORTx_CTRL_13, &restart); + ksz_pread8(priv, p, KSZ8873_PORTx_CTRL_12, &ctrl); + if (ctrl & PORT_FORCE_100_MBIT) + data |= BMCR_SPEED100; + if ((ctrl & PORT_AUTO_NEG_ENABLE)) + data |= BMCR_ANENABLE; + if (restart & PORT_POWER_DOWN) + data |= BMCR_PDOWN; + if (restart & PORT_AUTO_NEG_RESTART) + data |= BMCR_ANRESTART; + if (ctrl & PORT_FORCE_FULL_DUPLEX) + data |= BMCR_FULLDPLX; + break; + case MII_BMSR: + ksz_pread8(priv, p, KSZ8873_PORTx_STATUS_0, &link); + data = BMSR_100FULL | + BMSR_100HALF | + BMSR_10FULL | + BMSR_10HALF | + BMSR_ANEGCAPABLE; + if (link & PORT_AUTO_NEG_COMPLETE) + data |= BMSR_ANEGCOMPLETE; + if (link & PORT_STAT_LINK_GOOD) + data |= BMSR_LSTATUS; + break; + case MII_PHYSID1: + data = KSZ8795_ID_HI; + break; + case MII_PHYSID2: + data = KSZ8863_ID_LO; + break; + case MII_ADVERTISE: + ksz_pread8(priv, p, KSZ8873_PORTx_CTRL_12, &ctrl); + data = ADVERTISE_CSMA; + if (ctrl & PORT_AUTO_NEG_100BTX_FD) + data |= ADVERTISE_100FULL; + if (ctrl & PORT_AUTO_NEG_100BTX) + data |= ADVERTISE_100HALF; + if (ctrl & PORT_AUTO_NEG_10BT_FD) + data |= ADVERTISE_10FULL; + if (ctrl & PORT_AUTO_NEG_10BT) + data |= ADVERTISE_10HALF; + break; + case MII_LPA: + ksz_pread8(priv, p, KSZ8873_PORTx_STATUS_0, &link); + data = LPA_SLCT; + if (link & PORT_REMOTE_100BTX_FD) + data |= LPA_100FULL; + if (link & PORT_REMOTE_100BTX) + data |= LPA_100HALF; + if (link & PORT_REMOTE_10BT_FD) + data |= LPA_10FULL; + if (link & PORT_REMOTE_10BT) + data |= LPA_10HALF; + if (data & ~LPA_SLCT) + data |= LPA_LPACK; + break; + default: + processed = false; + break; + } + if (processed) + *val = data; +} + +static void ksz8_w_phy(struct ksz8873_switch *priv, u16 phy, u16 reg, u16 val) +{ + u8 restart, ctrl, data; + u8 p = phy; + + switch (reg) { + case MII_BMCR: + ksz_pread8(priv, p, KSZ8873_PORTx_CTRL_12, &ctrl); + data = ctrl; + if ((val & BMCR_ANENABLE)) + data |= PORT_AUTO_NEG_ENABLE; + else + data &= ~PORT_AUTO_NEG_ENABLE; + + if (val & BMCR_SPEED100) + data |= PORT_FORCE_100_MBIT; + else + data &= ~PORT_FORCE_100_MBIT; + if (val & BMCR_FULLDPLX) + data |= PORT_FORCE_FULL_DUPLEX; + else + data &= ~PORT_FORCE_FULL_DUPLEX; + if (data != ctrl) + ksz_pwrite8(priv, p, KSZ8873_PORTx_CTRL_12, data); + ksz_pread8(priv, p, KSZ8873_PORTx_CTRL_13, &restart); + data = restart; + if (val & BMCR_ANRESTART) + data |= PORT_AUTO_NEG_RESTART; + else + data &= ~(PORT_AUTO_NEG_RESTART); + if (val & BMCR_PDOWN) + data |= PORT_POWER_DOWN; + else + data &= ~PORT_POWER_DOWN; + if (data != restart) + ksz_pwrite8(priv, p, KSZ8873_PORTx_CTRL_13, data); + break; + case MII_ADVERTISE: + ksz_pread8(priv, p, KSZ8873_PORTx_CTRL_12, &ctrl); + data = ctrl; + data &= ~(PORT_AUTO_NEG_100BTX_FD | + PORT_AUTO_NEG_100BTX | + PORT_AUTO_NEG_10BT_FD | + PORT_AUTO_NEG_10BT); + if (val & ADVERTISE_100FULL) + data |= PORT_AUTO_NEG_100BTX_FD; + if (val & ADVERTISE_100HALF) + data |= PORT_AUTO_NEG_100BTX; + if (val & ADVERTISE_10FULL) + data |= PORT_AUTO_NEG_10BT_FD; + if (val & ADVERTISE_10HALF) + data |= PORT_AUTO_NEG_10BT; + if (data != ctrl) + ksz_pwrite8(priv, p, KSZ8873_PORTx_CTRL_12, data); + break; + default: + break; + } +} + +static int ksz8873_phy_read16(struct dsa_switch *ds, int addr, int reg) +{ + struct device *dev = ds->dev; + struct ksz8873_switch *priv = dev_get_priv(dev); + u16 val = 0xffff; + + if (addr >= priv->dcfg->phy_port_cnt) + return val; + + ksz8_r_phy(priv, addr, reg, &val); + + return val; +} + +static int ksz8873_phy_write16(struct dsa_switch *ds, int addr, int reg, + u16 val) +{ + struct device *dev = ds->dev; + struct ksz8873_switch *priv = dev_get_priv(dev); + + /* No real PHY after this. */ + if (addr >= priv->dcfg->phy_port_cnt) + return 0; + + ksz8_w_phy(priv, addr, reg, val); + + return 0; +} + +static void ksz8873_cfg_port_member(struct ksz8873_switch *priv, int port, + u8 member) +{ + ksz_pwrite8(priv, port, KSZ8873_PORTx_CTRL_1, member); +} + +static int ksz8873_port_enable(struct dsa_port *dp, int port, + struct phy_device *phy) +{ + return 0; +} + +static int ksz8873_xmit(struct dsa_port *dp, int port, void *packet, int length) +{ + u8 *tag = packet + length - dp->ds->needed_tx_tailroom; + + *tag = BIT(dp->index); + + return 0; +} + +static int ksz8873_recv(struct dsa_switch *ds, int *port, void *packet, + int length) +{ + u8 *tag = packet + length - ds->needed_rx_tailroom; + + *port = *tag & 7; + + return 0; +}; + +static const struct dsa_switch_ops ksz8873_dsa_ops = { + .port_enable = ksz8873_port_enable, + .xmit = ksz8873_xmit, + .rcv = ksz8873_recv, + .phy_read = ksz8873_phy_read16, + .phy_write = ksz8873_phy_write16, +}; + +static int ksz8873_default_setup(struct ksz8873_switch *priv) +{ + int i; + + ksz_write8(priv, KSZ8873_GLOBAL_CTRL_1, KSZ8873_PASS_ALL_FRAMES | + KSZ8873_P3_TAIL_TAG_EN); + + for (i = 0; i < priv->ds.num_ports; i++) { + u8 member; + /* isolate all ports by default */ + member = BIT(priv->ds.cpu_port); + ksz8873_cfg_port_member(priv, i, member); + + member = dsa_user_ports(&priv->ds); + ksz8873_cfg_port_member(priv, priv->ds.cpu_port, member); + } + + return 0; +} + +static int ksz8873_probe_mdio(struct phy_device *mdiodev) +{ + struct device *dev = &mdiodev->dev; + const struct ksz8873_dcfg *dcfg; + struct ksz8873_switch *priv; + struct dsa_switch *ds; + struct gpio_desc *gpio; + int ret; + u8 id0, id1; + + priv = xzalloc(sizeof(*priv)); + + dcfg = of_device_get_match_data(dev); + if (!dcfg) + return -EINVAL; + + dev->priv = priv; + priv->dev = dev; + priv->dcfg = dcfg; + priv->mdiodev = mdiodev; + + priv->regmap = regmap_init(dev, &ksz8873_regmap_smi, priv, + &ksz8873_regmap_config); + if (IS_ERR(priv->regmap)) + return dev_err_probe(dev, PTR_ERR(priv->regmap), + "Failed to initialize regmap.\n"); + + gpio = gpiod_get_optional(dev, "reset", GPIOF_OUT_INIT_ACTIVE); + if (IS_ERR(gpio)) { + dev_warn(dev, "Failed to get 'reset' GPIO (ignored)\n"); + } else if (gpio) { + mdelay(1); + gpiod_set_value(gpio, false); + } + + ret = ksz_read8(priv, KSZ8873_CHIP_ID0, &id0); + if (ret) + return ret; + + ret = ksz_read8(priv, KSZ8873_CHIP_ID1, &id1); + if (ret) + return ret; + + if (id0 != dcfg->id0 || + (id1 & (KSZ88_CHIP_ID_M | KSZ88_REV_ID_M)) != dcfg->id1) + return -ENODEV; + + ds = &priv->ds; + ds->dev = dev; + ds->num_ports = dcfg->num_ports; + ds->ops = &ksz8873_dsa_ops; + ds->needed_rx_tailroom = 1; + ds->needed_tx_tailroom = 1; + ds->phys_mii_mask = 0x3; + + ret = dsa_register_switch(ds); + if (ret) + return ret; + + ksz8873_default_setup(priv); + + return 0; +} + +static const struct ksz8873_dcfg ksz8873_dcfg = { + .num_ports = 3, + .phy_port_cnt = 2, + .id0 = 0x88, + .id1 = 0x30, +}; + +static const struct of_device_id ksz8873_dt_ids[] = { + { .compatible = "microchip,ksz8873", .data = &ksz8873_dcfg }, + { } +}; +MODULE_DEVICE_TABLE(of, ksz8873_dt_ids); + +static struct phy_driver ksz8873_driver_mdio = { + .drv = { + .name = "KSZ8873 MDIO", + .of_compatible = DRV_OF_COMPAT(ksz8873_dt_ids), + }, + .probe = ksz8873_probe_mdio, +}; +device_mdio_driver(ksz8873_driver_mdio); |