|
@@ -0,0 +1,626 @@
|
|
|
+/*
|
|
|
+ * Broadcom Starfighter 2 DSA switch driver
|
|
|
+ *
|
|
|
+ * Copyright (C) 2014, Broadcom Corporation
|
|
|
+ *
|
|
|
+ * 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/list.h>
|
|
|
+#include <linux/module.h>
|
|
|
+#include <linux/netdevice.h>
|
|
|
+#include <linux/interrupt.h>
|
|
|
+#include <linux/platform_device.h>
|
|
|
+#include <linux/of.h>
|
|
|
+#include <linux/phy.h>
|
|
|
+#include <linux/phy_fixed.h>
|
|
|
+#include <linux/mii.h>
|
|
|
+#include <linux/of.h>
|
|
|
+#include <linux/of_irq.h>
|
|
|
+#include <linux/of_address.h>
|
|
|
+#include <net/dsa.h>
|
|
|
+
|
|
|
+#include "bcm_sf2.h"
|
|
|
+#include "bcm_sf2_regs.h"
|
|
|
+
|
|
|
+/* String, offset, and register size in bytes if different from 4 bytes */
|
|
|
+static const struct bcm_sf2_hw_stats bcm_sf2_mib[] = {
|
|
|
+ { "TxOctets", 0x000, 8 },
|
|
|
+ { "TxDropPkts", 0x020 },
|
|
|
+ { "TxQPKTQ0", 0x030 },
|
|
|
+ { "TxBroadcastPkts", 0x040 },
|
|
|
+ { "TxMulticastPkts", 0x050 },
|
|
|
+ { "TxUnicastPKts", 0x060 },
|
|
|
+ { "TxCollisions", 0x070 },
|
|
|
+ { "TxSingleCollision", 0x080 },
|
|
|
+ { "TxMultipleCollision", 0x090 },
|
|
|
+ { "TxDeferredCollision", 0x0a0 },
|
|
|
+ { "TxLateCollision", 0x0b0 },
|
|
|
+ { "TxExcessiveCollision", 0x0c0 },
|
|
|
+ { "TxFrameInDisc", 0x0d0 },
|
|
|
+ { "TxPausePkts", 0x0e0 },
|
|
|
+ { "TxQPKTQ1", 0x0f0 },
|
|
|
+ { "TxQPKTQ2", 0x100 },
|
|
|
+ { "TxQPKTQ3", 0x110 },
|
|
|
+ { "TxQPKTQ4", 0x120 },
|
|
|
+ { "TxQPKTQ5", 0x130 },
|
|
|
+ { "RxOctets", 0x140, 8 },
|
|
|
+ { "RxUndersizePkts", 0x160 },
|
|
|
+ { "RxPausePkts", 0x170 },
|
|
|
+ { "RxPkts64Octets", 0x180 },
|
|
|
+ { "RxPkts65to127Octets", 0x190 },
|
|
|
+ { "RxPkts128to255Octets", 0x1a0 },
|
|
|
+ { "RxPkts256to511Octets", 0x1b0 },
|
|
|
+ { "RxPkts512to1023Octets", 0x1c0 },
|
|
|
+ { "RxPkts1024toMaxPktsOctets", 0x1d0 },
|
|
|
+ { "RxOversizePkts", 0x1e0 },
|
|
|
+ { "RxJabbers", 0x1f0 },
|
|
|
+ { "RxAlignmentErrors", 0x200 },
|
|
|
+ { "RxFCSErrors", 0x210 },
|
|
|
+ { "RxGoodOctets", 0x220, 8 },
|
|
|
+ { "RxDropPkts", 0x240 },
|
|
|
+ { "RxUnicastPkts", 0x250 },
|
|
|
+ { "RxMulticastPkts", 0x260 },
|
|
|
+ { "RxBroadcastPkts", 0x270 },
|
|
|
+ { "RxSAChanges", 0x280 },
|
|
|
+ { "RxFragments", 0x290 },
|
|
|
+ { "RxJumboPkt", 0x2a0 },
|
|
|
+ { "RxSymblErr", 0x2b0 },
|
|
|
+ { "InRangeErrCount", 0x2c0 },
|
|
|
+ { "OutRangeErrCount", 0x2d0 },
|
|
|
+ { "EEELpiEvent", 0x2e0 },
|
|
|
+ { "EEELpiDuration", 0x2f0 },
|
|
|
+ { "RxDiscard", 0x300, 8 },
|
|
|
+ { "TxQPKTQ6", 0x320 },
|
|
|
+ { "TxQPKTQ7", 0x330 },
|
|
|
+ { "TxPkts64Octets", 0x340 },
|
|
|
+ { "TxPkts65to127Octets", 0x350 },
|
|
|
+ { "TxPkts128to255Octets", 0x360 },
|
|
|
+ { "TxPkts256to511Ocets", 0x370 },
|
|
|
+ { "TxPkts512to1023Ocets", 0x380 },
|
|
|
+ { "TxPkts1024toMaxPktOcets", 0x390 },
|
|
|
+};
|
|
|
+
|
|
|
+#define BCM_SF2_STATS_SIZE ARRAY_SIZE(bcm_sf2_mib)
|
|
|
+
|
|
|
+static void bcm_sf2_sw_get_strings(struct dsa_switch *ds,
|
|
|
+ int port, uint8_t *data)
|
|
|
+{
|
|
|
+ unsigned int i;
|
|
|
+
|
|
|
+ for (i = 0; i < BCM_SF2_STATS_SIZE; i++)
|
|
|
+ memcpy(data + i * ETH_GSTRING_LEN,
|
|
|
+ bcm_sf2_mib[i].string, ETH_GSTRING_LEN);
|
|
|
+}
|
|
|
+
|
|
|
+static void bcm_sf2_sw_get_ethtool_stats(struct dsa_switch *ds,
|
|
|
+ int port, uint64_t *data)
|
|
|
+{
|
|
|
+ struct bcm_sf2_priv *priv = ds_to_priv(ds);
|
|
|
+ const struct bcm_sf2_hw_stats *s;
|
|
|
+ unsigned int i;
|
|
|
+ u64 val = 0;
|
|
|
+ u32 offset;
|
|
|
+
|
|
|
+ mutex_lock(&priv->stats_mutex);
|
|
|
+
|
|
|
+ /* Now fetch the per-port counters */
|
|
|
+ for (i = 0; i < BCM_SF2_STATS_SIZE; i++) {
|
|
|
+ s = &bcm_sf2_mib[i];
|
|
|
+
|
|
|
+ /* Do a latched 64-bit read if needed */
|
|
|
+ offset = s->reg + CORE_P_MIB_OFFSET(port);
|
|
|
+ if (s->sizeof_stat == 8)
|
|
|
+ val = core_readq(priv, offset);
|
|
|
+ else
|
|
|
+ val = core_readl(priv, offset);
|
|
|
+
|
|
|
+ data[i] = (u64)val;
|
|
|
+ }
|
|
|
+
|
|
|
+ mutex_unlock(&priv->stats_mutex);
|
|
|
+}
|
|
|
+
|
|
|
+static int bcm_sf2_sw_get_sset_count(struct dsa_switch *ds)
|
|
|
+{
|
|
|
+ return BCM_SF2_STATS_SIZE;
|
|
|
+}
|
|
|
+
|
|
|
+static char *bcm_sf2_sw_probe(struct mii_bus *bus, int sw_addr)
|
|
|
+{
|
|
|
+ return "Broadcom Starfighter 2";
|
|
|
+}
|
|
|
+
|
|
|
+static void bcm_sf2_imp_setup(struct dsa_switch *ds, int port)
|
|
|
+{
|
|
|
+ struct bcm_sf2_priv *priv = ds_to_priv(ds);
|
|
|
+ unsigned int i;
|
|
|
+ u32 reg, val;
|
|
|
+
|
|
|
+ /* Enable the port memories */
|
|
|
+ reg = core_readl(priv, CORE_MEM_PSM_VDD_CTRL);
|
|
|
+ reg &= ~P_TXQ_PSM_VDD(port);
|
|
|
+ core_writel(priv, reg, CORE_MEM_PSM_VDD_CTRL);
|
|
|
+
|
|
|
+ /* Enable Broadcast, Multicast, Unicast forwarding to IMP port */
|
|
|
+ reg = core_readl(priv, CORE_IMP_CTL);
|
|
|
+ reg |= (RX_BCST_EN | RX_MCST_EN | RX_UCST_EN);
|
|
|
+ reg &= ~(RX_DIS | TX_DIS);
|
|
|
+ core_writel(priv, reg, CORE_IMP_CTL);
|
|
|
+
|
|
|
+ /* Enable forwarding */
|
|
|
+ core_writel(priv, SW_FWDG_EN, CORE_SWMODE);
|
|
|
+
|
|
|
+ /* Enable IMP port in dumb mode */
|
|
|
+ reg = core_readl(priv, CORE_SWITCH_CTRL);
|
|
|
+ reg |= MII_DUMB_FWDG_EN;
|
|
|
+ core_writel(priv, reg, CORE_SWITCH_CTRL);
|
|
|
+
|
|
|
+ /* Resolve which bit controls the Broadcom tag */
|
|
|
+ switch (port) {
|
|
|
+ case 8:
|
|
|
+ val = BRCM_HDR_EN_P8;
|
|
|
+ break;
|
|
|
+ case 7:
|
|
|
+ val = BRCM_HDR_EN_P7;
|
|
|
+ break;
|
|
|
+ case 5:
|
|
|
+ val = BRCM_HDR_EN_P5;
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ val = 0;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Enable Broadcom tags for IMP port */
|
|
|
+ reg = core_readl(priv, CORE_BRCM_HDR_CTRL);
|
|
|
+ reg |= val;
|
|
|
+ core_writel(priv, reg, CORE_BRCM_HDR_CTRL);
|
|
|
+
|
|
|
+ /* Enable reception Broadcom tag for CPU TX (switch RX) to
|
|
|
+ * allow us to tag outgoing frames
|
|
|
+ */
|
|
|
+ reg = core_readl(priv, CORE_BRCM_HDR_RX_DIS);
|
|
|
+ reg &= ~(1 << port);
|
|
|
+ core_writel(priv, reg, CORE_BRCM_HDR_RX_DIS);
|
|
|
+
|
|
|
+ /* Enable transmission of Broadcom tags from the switch (CPU RX) to
|
|
|
+ * allow delivering frames to the per-port net_devices
|
|
|
+ */
|
|
|
+ reg = core_readl(priv, CORE_BRCM_HDR_TX_DIS);
|
|
|
+ reg &= ~(1 << port);
|
|
|
+ core_writel(priv, reg, CORE_BRCM_HDR_TX_DIS);
|
|
|
+
|
|
|
+ /* Force link status for IMP port */
|
|
|
+ reg = core_readl(priv, CORE_STS_OVERRIDE_IMP);
|
|
|
+ reg |= (MII_SW_OR | LINK_STS);
|
|
|
+ core_writel(priv, reg, CORE_STS_OVERRIDE_IMP);
|
|
|
+
|
|
|
+ /* Enable the IMP Port to be in the same VLAN as the other ports
|
|
|
+ * on a per-port basis such that we only have Port i and IMP in
|
|
|
+ * the same VLAN.
|
|
|
+ */
|
|
|
+ for (i = 0; i < priv->hw_params.num_ports; i++) {
|
|
|
+ if (!((1 << i) & ds->phys_port_mask))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ reg = core_readl(priv, CORE_PORT_VLAN_CTL_PORT(i));
|
|
|
+ reg |= (1 << port);
|
|
|
+ core_writel(priv, reg, CORE_PORT_VLAN_CTL_PORT(i));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+static void bcm_sf2_port_setup(struct dsa_switch *ds, int port)
|
|
|
+{
|
|
|
+ struct bcm_sf2_priv *priv = ds_to_priv(ds);
|
|
|
+ u32 reg;
|
|
|
+
|
|
|
+ /* Clear the memory power down */
|
|
|
+ reg = core_readl(priv, CORE_MEM_PSM_VDD_CTRL);
|
|
|
+ reg &= ~P_TXQ_PSM_VDD(port);
|
|
|
+ core_writel(priv, reg, CORE_MEM_PSM_VDD_CTRL);
|
|
|
+
|
|
|
+ /* Clear the Rx and Tx disable bits and set to no spanning tree */
|
|
|
+ core_writel(priv, 0, CORE_G_PCTL_PORT(port));
|
|
|
+
|
|
|
+ /* Enable port 7 interrupts to get notified */
|
|
|
+ if (port == 7)
|
|
|
+ intrl2_1_mask_clear(priv, P_IRQ_MASK(P7_IRQ_OFF));
|
|
|
+
|
|
|
+ /* Set this port, and only this one to be in the default VLAN */
|
|
|
+ reg = core_readl(priv, CORE_PORT_VLAN_CTL_PORT(port));
|
|
|
+ reg &= ~PORT_VLAN_CTRL_MASK;
|
|
|
+ reg |= (1 << port);
|
|
|
+ core_writel(priv, reg, CORE_PORT_VLAN_CTL_PORT(port));
|
|
|
+}
|
|
|
+
|
|
|
+static void bcm_sf2_port_disable(struct dsa_switch *ds, int port)
|
|
|
+{
|
|
|
+ struct bcm_sf2_priv *priv = ds_to_priv(ds);
|
|
|
+ u32 off, reg;
|
|
|
+
|
|
|
+ if (dsa_is_cpu_port(ds, port))
|
|
|
+ off = CORE_IMP_CTL;
|
|
|
+ else
|
|
|
+ off = CORE_G_PCTL_PORT(port);
|
|
|
+
|
|
|
+ reg = core_readl(priv, off);
|
|
|
+ reg |= RX_DIS | TX_DIS;
|
|
|
+ core_writel(priv, reg, off);
|
|
|
+
|
|
|
+ /* Power down the port memory */
|
|
|
+ reg = core_readl(priv, CORE_MEM_PSM_VDD_CTRL);
|
|
|
+ reg |= P_TXQ_PSM_VDD(port);
|
|
|
+ core_writel(priv, reg, CORE_MEM_PSM_VDD_CTRL);
|
|
|
+}
|
|
|
+
|
|
|
+static irqreturn_t bcm_sf2_switch_0_isr(int irq, void *dev_id)
|
|
|
+{
|
|
|
+ struct bcm_sf2_priv *priv = dev_id;
|
|
|
+
|
|
|
+ priv->irq0_stat = intrl2_0_readl(priv, INTRL2_CPU_STATUS) &
|
|
|
+ ~priv->irq0_mask;
|
|
|
+ intrl2_0_writel(priv, priv->irq0_stat, INTRL2_CPU_CLEAR);
|
|
|
+
|
|
|
+ return IRQ_HANDLED;
|
|
|
+}
|
|
|
+
|
|
|
+static irqreturn_t bcm_sf2_switch_1_isr(int irq, void *dev_id)
|
|
|
+{
|
|
|
+ struct bcm_sf2_priv *priv = dev_id;
|
|
|
+
|
|
|
+ priv->irq1_stat = intrl2_1_readl(priv, INTRL2_CPU_STATUS) &
|
|
|
+ ~priv->irq1_mask;
|
|
|
+ intrl2_1_writel(priv, priv->irq1_stat, INTRL2_CPU_CLEAR);
|
|
|
+
|
|
|
+ if (priv->irq1_stat & P_LINK_UP_IRQ(P7_IRQ_OFF))
|
|
|
+ priv->port_sts[7].link = 1;
|
|
|
+ if (priv->irq1_stat & P_LINK_DOWN_IRQ(P7_IRQ_OFF))
|
|
|
+ priv->port_sts[7].link = 0;
|
|
|
+
|
|
|
+ return IRQ_HANDLED;
|
|
|
+}
|
|
|
+
|
|
|
+static int bcm_sf2_sw_setup(struct dsa_switch *ds)
|
|
|
+{
|
|
|
+ const char *reg_names[BCM_SF2_REGS_NUM] = BCM_SF2_REGS_NAME;
|
|
|
+ struct bcm_sf2_priv *priv = ds_to_priv(ds);
|
|
|
+ struct device_node *dn;
|
|
|
+ void __iomem **base;
|
|
|
+ unsigned int port;
|
|
|
+ unsigned int i;
|
|
|
+ u32 reg, rev;
|
|
|
+ int ret;
|
|
|
+
|
|
|
+ spin_lock_init(&priv->indir_lock);
|
|
|
+ mutex_init(&priv->stats_mutex);
|
|
|
+
|
|
|
+ /* All the interesting properties are at the parent device_node
|
|
|
+ * level
|
|
|
+ */
|
|
|
+ dn = ds->pd->of_node->parent;
|
|
|
+
|
|
|
+ priv->irq0 = irq_of_parse_and_map(dn, 0);
|
|
|
+ priv->irq1 = irq_of_parse_and_map(dn, 1);
|
|
|
+
|
|
|
+ base = &priv->core;
|
|
|
+ for (i = 0; i < BCM_SF2_REGS_NUM; i++) {
|
|
|
+ *base = of_iomap(dn, i);
|
|
|
+ if (*base == NULL) {
|
|
|
+ pr_err("unable to find register: %s\n", reg_names[i]);
|
|
|
+ return -ENODEV;
|
|
|
+ }
|
|
|
+ base++;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Disable all interrupts and request them */
|
|
|
+ intrl2_0_writel(priv, 0xffffffff, INTRL2_CPU_MASK_SET);
|
|
|
+ intrl2_0_writel(priv, 0xffffffff, INTRL2_CPU_CLEAR);
|
|
|
+ intrl2_0_writel(priv, 0, INTRL2_CPU_MASK_CLEAR);
|
|
|
+ intrl2_1_writel(priv, 0xffffffff, INTRL2_CPU_MASK_SET);
|
|
|
+ intrl2_1_writel(priv, 0xffffffff, INTRL2_CPU_CLEAR);
|
|
|
+ intrl2_1_writel(priv, 0, INTRL2_CPU_MASK_CLEAR);
|
|
|
+
|
|
|
+ ret = request_irq(priv->irq0, bcm_sf2_switch_0_isr, 0,
|
|
|
+ "switch_0", priv);
|
|
|
+ if (ret < 0) {
|
|
|
+ pr_err("failed to request switch_0 IRQ\n");
|
|
|
+ goto out_unmap;
|
|
|
+ }
|
|
|
+
|
|
|
+ ret = request_irq(priv->irq1, bcm_sf2_switch_1_isr, 0,
|
|
|
+ "switch_1", priv);
|
|
|
+ if (ret < 0) {
|
|
|
+ pr_err("failed to request switch_1 IRQ\n");
|
|
|
+ goto out_free_irq0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Reset the MIB counters */
|
|
|
+ reg = core_readl(priv, CORE_GMNCFGCFG);
|
|
|
+ reg |= RST_MIB_CNT;
|
|
|
+ core_writel(priv, reg, CORE_GMNCFGCFG);
|
|
|
+ reg &= ~RST_MIB_CNT;
|
|
|
+ core_writel(priv, reg, CORE_GMNCFGCFG);
|
|
|
+
|
|
|
+ /* Get the maximum number of ports for this switch */
|
|
|
+ priv->hw_params.num_ports = core_readl(priv, CORE_IMP0_PRT_ID) + 1;
|
|
|
+ if (priv->hw_params.num_ports > DSA_MAX_PORTS)
|
|
|
+ priv->hw_params.num_ports = DSA_MAX_PORTS;
|
|
|
+
|
|
|
+ /* Assume a single GPHY setup if we can't read that property */
|
|
|
+ if (of_property_read_u32(dn, "brcm,num-gphy",
|
|
|
+ &priv->hw_params.num_gphy))
|
|
|
+ priv->hw_params.num_gphy = 1;
|
|
|
+
|
|
|
+ /* Enable all valid ports and disable those unused */
|
|
|
+ for (port = 0; port < priv->hw_params.num_ports; port++) {
|
|
|
+ /* IMP port receives special treatment */
|
|
|
+ if ((1 << port) & ds->phys_port_mask)
|
|
|
+ bcm_sf2_port_setup(ds, port);
|
|
|
+ else if (dsa_is_cpu_port(ds, port))
|
|
|
+ bcm_sf2_imp_setup(ds, port);
|
|
|
+ else
|
|
|
+ bcm_sf2_port_disable(ds, port);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Include the pseudo-PHY address and the broadcast PHY address to
|
|
|
+ * divert reads towards our workaround
|
|
|
+ */
|
|
|
+ ds->phys_mii_mask |= ((1 << 30) | (1 << 0));
|
|
|
+
|
|
|
+ rev = reg_readl(priv, REG_SWITCH_REVISION);
|
|
|
+ priv->hw_params.top_rev = (rev >> SWITCH_TOP_REV_SHIFT) &
|
|
|
+ SWITCH_TOP_REV_MASK;
|
|
|
+ priv->hw_params.core_rev = (rev & SF2_REV_MASK);
|
|
|
+
|
|
|
+ pr_info("Starfighter 2 top: %x.%02x, core: %x.%02x base: 0x%p, IRQs: %d, %d\n",
|
|
|
+ priv->hw_params.top_rev >> 8, priv->hw_params.top_rev & 0xff,
|
|
|
+ priv->hw_params.core_rev >> 8, priv->hw_params.core_rev & 0xff,
|
|
|
+ priv->core, priv->irq0, priv->irq1);
|
|
|
+
|
|
|
+ return 0;
|
|
|
+
|
|
|
+out_free_irq0:
|
|
|
+ free_irq(priv->irq0, priv);
|
|
|
+out_unmap:
|
|
|
+ base = &priv->core;
|
|
|
+ for (i = 0; i < BCM_SF2_REGS_NUM; i++) {
|
|
|
+ iounmap(*base);
|
|
|
+ base++;
|
|
|
+ }
|
|
|
+ return ret;
|
|
|
+}
|
|
|
+
|
|
|
+static int bcm_sf2_sw_set_addr(struct dsa_switch *ds, u8 *addr)
|
|
|
+{
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+static int bcm_sf2_sw_indir_rw(struct dsa_switch *ds, int op, int addr,
|
|
|
+ int regnum, u16 val)
|
|
|
+{
|
|
|
+ struct bcm_sf2_priv *priv = ds_to_priv(ds);
|
|
|
+ int ret = 0;
|
|
|
+ u32 reg;
|
|
|
+
|
|
|
+ reg = reg_readl(priv, REG_SWITCH_CNTRL);
|
|
|
+ reg |= MDIO_MASTER_SEL;
|
|
|
+ reg_writel(priv, reg, REG_SWITCH_CNTRL);
|
|
|
+
|
|
|
+ /* Page << 8 | offset */
|
|
|
+ reg = 0x70;
|
|
|
+ reg <<= 2;
|
|
|
+ core_writel(priv, addr, reg);
|
|
|
+
|
|
|
+ /* Page << 8 | offset */
|
|
|
+ reg = 0x80 << 8 | regnum << 1;
|
|
|
+ reg <<= 2;
|
|
|
+
|
|
|
+ if (op)
|
|
|
+ ret = core_readl(priv, reg);
|
|
|
+ else
|
|
|
+ core_writel(priv, val, reg);
|
|
|
+
|
|
|
+ reg = reg_readl(priv, REG_SWITCH_CNTRL);
|
|
|
+ reg &= ~MDIO_MASTER_SEL;
|
|
|
+ reg_writel(priv, reg, REG_SWITCH_CNTRL);
|
|
|
+
|
|
|
+ return ret & 0xffff;
|
|
|
+}
|
|
|
+
|
|
|
+static int bcm_sf2_sw_phy_read(struct dsa_switch *ds, int addr, int regnum)
|
|
|
+{
|
|
|
+ /* Intercept reads from the MDIO broadcast address or Broadcom
|
|
|
+ * pseudo-PHY address
|
|
|
+ */
|
|
|
+ switch (addr) {
|
|
|
+ case 0:
|
|
|
+ case 30:
|
|
|
+ return bcm_sf2_sw_indir_rw(ds, 1, addr, regnum, 0);
|
|
|
+ default:
|
|
|
+ return 0xffff;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+static int bcm_sf2_sw_phy_write(struct dsa_switch *ds, int addr, int regnum,
|
|
|
+ u16 val)
|
|
|
+{
|
|
|
+ /* Intercept writes to the MDIO broadcast address or Broadcom
|
|
|
+ * pseudo-PHY address
|
|
|
+ */
|
|
|
+ switch (addr) {
|
|
|
+ case 0:
|
|
|
+ case 30:
|
|
|
+ bcm_sf2_sw_indir_rw(ds, 0, addr, regnum, val);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+static void bcm_sf2_sw_adjust_link(struct dsa_switch *ds, int port,
|
|
|
+ struct phy_device *phydev)
|
|
|
+{
|
|
|
+ struct bcm_sf2_priv *priv = ds_to_priv(ds);
|
|
|
+ u32 id_mode_dis = 0, port_mode;
|
|
|
+ const char *str = NULL;
|
|
|
+ u32 reg;
|
|
|
+
|
|
|
+ switch (phydev->interface) {
|
|
|
+ case PHY_INTERFACE_MODE_RGMII:
|
|
|
+ str = "RGMII (no delay)";
|
|
|
+ id_mode_dis = 1;
|
|
|
+ case PHY_INTERFACE_MODE_RGMII_TXID:
|
|
|
+ if (!str)
|
|
|
+ str = "RGMII (TX delay)";
|
|
|
+ port_mode = EXT_GPHY;
|
|
|
+ break;
|
|
|
+ case PHY_INTERFACE_MODE_MII:
|
|
|
+ str = "MII";
|
|
|
+ port_mode = EXT_EPHY;
|
|
|
+ break;
|
|
|
+ case PHY_INTERFACE_MODE_REVMII:
|
|
|
+ str = "Reverse MII";
|
|
|
+ port_mode = EXT_REVMII;
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ goto force_link;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Clear id_mode_dis bit, and the existing port mode, but
|
|
|
+ * make sure we enable the RGMII block for data to pass
|
|
|
+ */
|
|
|
+ reg = reg_readl(priv, REG_RGMII_CNTRL_P(port));
|
|
|
+ reg &= ~ID_MODE_DIS;
|
|
|
+ reg &= ~(PORT_MODE_MASK << PORT_MODE_SHIFT);
|
|
|
+ reg &= ~(RX_PAUSE_EN | TX_PAUSE_EN);
|
|
|
+
|
|
|
+ reg |= port_mode | RGMII_MODE_EN;
|
|
|
+ if (id_mode_dis)
|
|
|
+ reg |= ID_MODE_DIS;
|
|
|
+
|
|
|
+ if (phydev->pause) {
|
|
|
+ if (phydev->asym_pause)
|
|
|
+ reg |= TX_PAUSE_EN;
|
|
|
+ reg |= RX_PAUSE_EN;
|
|
|
+ }
|
|
|
+
|
|
|
+ reg_writel(priv, reg, REG_RGMII_CNTRL_P(port));
|
|
|
+
|
|
|
+ pr_info("Port %d configured for %s\n", port, str);
|
|
|
+
|
|
|
+force_link:
|
|
|
+ /* Force link settings detected from the PHY */
|
|
|
+ reg = SW_OVERRIDE;
|
|
|
+ switch (phydev->speed) {
|
|
|
+ case SPEED_1000:
|
|
|
+ reg |= SPDSTS_1000 << SPEED_SHIFT;
|
|
|
+ break;
|
|
|
+ case SPEED_100:
|
|
|
+ reg |= SPDSTS_100 << SPEED_SHIFT;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (phydev->link)
|
|
|
+ reg |= LINK_STS;
|
|
|
+ if (phydev->duplex == DUPLEX_FULL)
|
|
|
+ reg |= DUPLX_MODE;
|
|
|
+
|
|
|
+ core_writel(priv, reg, CORE_STS_OVERRIDE_GMIIP_PORT(port));
|
|
|
+}
|
|
|
+
|
|
|
+static void bcm_sf2_sw_fixed_link_update(struct dsa_switch *ds, int port,
|
|
|
+ struct fixed_phy_status *status)
|
|
|
+{
|
|
|
+ struct bcm_sf2_priv *priv = ds_to_priv(ds);
|
|
|
+ u32 link, duplex, pause, speed;
|
|
|
+ u32 reg;
|
|
|
+
|
|
|
+ link = core_readl(priv, CORE_LNKSTS);
|
|
|
+ duplex = core_readl(priv, CORE_DUPSTS);
|
|
|
+ pause = core_readl(priv, CORE_PAUSESTS);
|
|
|
+ speed = core_readl(priv, CORE_SPDSTS);
|
|
|
+
|
|
|
+ speed >>= (port * SPDSTS_SHIFT);
|
|
|
+ speed &= SPDSTS_MASK;
|
|
|
+
|
|
|
+ status->link = 0;
|
|
|
+
|
|
|
+ /* Port 7 is special as we do not get link status from CORE_LNKSTS,
|
|
|
+ * which means that we need to force the link at the port override
|
|
|
+ * level to get the data to flow. We do use what the interrupt handler
|
|
|
+ * did determine before.
|
|
|
+ */
|
|
|
+ if (port == 7) {
|
|
|
+ status->link = priv->port_sts[port].link;
|
|
|
+ reg = core_readl(priv, CORE_STS_OVERRIDE_GMIIP_PORT(7));
|
|
|
+ reg |= SW_OVERRIDE;
|
|
|
+ if (status->link)
|
|
|
+ reg |= LINK_STS;
|
|
|
+ else
|
|
|
+ reg &= ~LINK_STS;
|
|
|
+ core_writel(priv, reg, CORE_STS_OVERRIDE_GMIIP_PORT(7));
|
|
|
+ status->duplex = 1;
|
|
|
+ } else {
|
|
|
+ status->link = !!(link & (1 << port));
|
|
|
+ status->duplex = !!(duplex & (1 << port));
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (speed) {
|
|
|
+ case SPDSTS_10:
|
|
|
+ status->speed = SPEED_10;
|
|
|
+ break;
|
|
|
+ case SPDSTS_100:
|
|
|
+ status->speed = SPEED_100;
|
|
|
+ break;
|
|
|
+ case SPDSTS_1000:
|
|
|
+ status->speed = SPEED_1000;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ((pause & (1 << port)) &&
|
|
|
+ (pause & (1 << (port + PAUSESTS_TX_PAUSE_SHIFT)))) {
|
|
|
+ status->asym_pause = 1;
|
|
|
+ status->pause = 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (pause & (1 << port))
|
|
|
+ status->pause = 1;
|
|
|
+}
|
|
|
+
|
|
|
+static struct dsa_switch_driver bcm_sf2_switch_driver = {
|
|
|
+ .tag_protocol = htons(ETH_P_BRCMTAG),
|
|
|
+ .priv_size = sizeof(struct bcm_sf2_priv),
|
|
|
+ .probe = bcm_sf2_sw_probe,
|
|
|
+ .setup = bcm_sf2_sw_setup,
|
|
|
+ .set_addr = bcm_sf2_sw_set_addr,
|
|
|
+ .phy_read = bcm_sf2_sw_phy_read,
|
|
|
+ .phy_write = bcm_sf2_sw_phy_write,
|
|
|
+ .get_strings = bcm_sf2_sw_get_strings,
|
|
|
+ .get_ethtool_stats = bcm_sf2_sw_get_ethtool_stats,
|
|
|
+ .get_sset_count = bcm_sf2_sw_get_sset_count,
|
|
|
+ .adjust_link = bcm_sf2_sw_adjust_link,
|
|
|
+ .fixed_link_update = bcm_sf2_sw_fixed_link_update,
|
|
|
+};
|
|
|
+
|
|
|
+static int __init bcm_sf2_init(void)
|
|
|
+{
|
|
|
+ register_switch_driver(&bcm_sf2_switch_driver);
|
|
|
+
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+module_init(bcm_sf2_init);
|
|
|
+
|
|
|
+static void __exit bcm_sf2_exit(void)
|
|
|
+{
|
|
|
+ unregister_switch_driver(&bcm_sf2_switch_driver);
|
|
|
+}
|
|
|
+module_exit(bcm_sf2_exit);
|
|
|
+
|
|
|
+MODULE_AUTHOR("Broadcom Corporation");
|
|
|
+MODULE_DESCRIPTION("Driver for Broadcom Starfighter 2 ethernet switch chip");
|
|
|
+MODULE_LICENSE("GPL");
|
|
|
+MODULE_ALIAS("platform:brcm-sf2");
|