root/sys/dev/dwc/if_dwc.c
/*-
 * Copyright (c) 2014 Ruslan Bukin <br@bsdpad.com>
 *
 * This software was developed by SRI International and the University of
 * Cambridge Computer Laboratory under DARPA/AFRL contract (FA8750-10-C-0237)
 * ("CTSRD"), as part of the DARPA CRASH research programme.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * Ethernet media access controller (EMAC)
 * Chapter 17, Altera Cyclone V Device Handbook (CV-5V2 2014.07.22)
 *
 * EMAC is an instance of the Synopsys DesignWare 3504-0
 * Universal 10/100/1000 Ethernet MAC (DWC_gmac).
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/gpio.h>
#include <sys/kernel.h>
#include <sys/lock.h>
#include <sys/malloc.h>
#include <sys/mbuf.h>
#include <sys/module.h>
#include <sys/mutex.h>
#include <sys/rman.h>
#include <sys/socket.h>
#include <sys/sockio.h>

#include <net/bpf.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <net/if_dl.h>
#include <net/if_media.h>
#include <net/if_types.h>
#include <net/if_var.h>

#include <machine/bus.h>

#include <dev/clk/clk.h>
#include <dev/hwreset/hwreset.h>

#include <dev/mii/mii.h>
#include <dev/mii/miivar.h>
#include <dev/ofw/ofw_bus.h>
#include <dev/ofw/ofw_bus_subr.h>
#include <dev/mii/mii_fdt.h>

#include <dev/dwc/if_dwcvar.h>
#include <dev/dwc/dwc1000_core.h>
#include <dev/dwc/dwc1000_dma.h>

#include "if_dwc_if.h"
#include "gpio_if.h"
#include "miibus_if.h"

static struct resource_spec dwc_spec[] = {
        { SYS_RES_MEMORY,       0,      RF_ACTIVE },
        { SYS_RES_IRQ,          0,      RF_ACTIVE },
        { -1, 0 }
};

static void dwc_stop_locked(struct dwc_softc *sc);

static void dwc_tick(void *arg);

/*
 * Media functions
 */

static void
dwc_media_status(if_t ifp, struct ifmediareq *ifmr)
{
        struct dwc_softc *sc;
        struct mii_data *mii;

        sc = if_getsoftc(ifp);
        mii = sc->mii_softc;
        DWC_LOCK(sc);
        mii_pollstat(mii);
        ifmr->ifm_active = mii->mii_media_active;
        ifmr->ifm_status = mii->mii_media_status;
        DWC_UNLOCK(sc);
}

static int
dwc_media_change_locked(struct dwc_softc *sc)
{

        return (mii_mediachg(sc->mii_softc));
}

static int
dwc_media_change(if_t ifp)
{
        struct dwc_softc *sc;
        int error;

        sc = if_getsoftc(ifp);

        DWC_LOCK(sc);
        error = dwc_media_change_locked(sc);
        DWC_UNLOCK(sc);
        return (error);
}

/*
 * if_ functions
 */

static void
dwc_txstart_locked(struct dwc_softc *sc)
{
        if_t ifp;

        DWC_ASSERT_LOCKED(sc);

        if (!sc->link_is_up)
                return;

        ifp = sc->ifp;

        if ((if_getdrvflags(ifp) & (IFF_DRV_RUNNING|IFF_DRV_OACTIVE)) !=
            IFF_DRV_RUNNING)
                return;
        dma1000_txstart(sc);
}

static void
dwc_txstart(if_t ifp)
{
        struct dwc_softc *sc = if_getsoftc(ifp);

        DWC_LOCK(sc);
        dwc_txstart_locked(sc);
        DWC_UNLOCK(sc);
}

static void
dwc_init_locked(struct dwc_softc *sc)
{
        if_t ifp = sc->ifp;

        DWC_ASSERT_LOCKED(sc);

        if (if_getdrvflags(ifp) & IFF_DRV_RUNNING)
                return;

        /*
         * Call mii_mediachg() which will call back into dwc1000_miibus_statchg()
         * to set up the remaining config registers based on current media.
         */
        mii_mediachg(sc->mii_softc);

        dwc1000_setup_rxfilter(sc);
        dwc1000_core_setup(sc);
        dwc1000_enable_mac(sc, true);
        dwc1000_enable_csum_offload(sc);
        dma1000_start(sc);

        if_setdrvflagbits(ifp, IFF_DRV_RUNNING, IFF_DRV_OACTIVE);

        callout_reset(&sc->dwc_callout, hz, dwc_tick, sc);
}

static void
dwc_init(void *if_softc)
{
        struct dwc_softc *sc = if_softc;

        DWC_LOCK(sc);
        dwc_init_locked(sc);
        DWC_UNLOCK(sc);
}

static void
dwc_stop_locked(struct dwc_softc *sc)
{
        if_t ifp;

        DWC_ASSERT_LOCKED(sc);

        ifp = sc->ifp;
        if_setdrvflagbits(ifp, 0, IFF_DRV_RUNNING | IFF_DRV_OACTIVE);
        sc->tx_watchdog_count = 0;
        sc->stats_harvest_count = 0;

        callout_stop(&sc->dwc_callout);

        dma1000_stop(sc);
        dwc1000_enable_mac(sc, false);
}

static int
dwc_ioctl(if_t ifp, u_long cmd, caddr_t data)
{
        struct dwc_softc *sc;
        struct mii_data *mii;
        struct ifreq *ifr;
        int flags, mask, error;

        sc = if_getsoftc(ifp);
        ifr = (struct ifreq *)data;

        error = 0;
        switch (cmd) {
        case SIOCSIFFLAGS:
                DWC_LOCK(sc);
                if (if_getflags(ifp) & IFF_UP) {
                        if (if_getdrvflags(ifp) & IFF_DRV_RUNNING) {
                                flags = if_getflags(ifp) ^ sc->if_flags;
                                if ((flags & (IFF_PROMISC|IFF_ALLMULTI)) != 0)
                                        dwc1000_setup_rxfilter(sc);
                        } else {
                                if (!sc->is_detaching)
                                        dwc_init_locked(sc);
                        }
                } else {
                        if (if_getdrvflags(ifp) & IFF_DRV_RUNNING)
                                dwc_stop_locked(sc);
                }
                sc->if_flags = if_getflags(ifp);
                DWC_UNLOCK(sc);
                break;
        case SIOCADDMULTI:
        case SIOCDELMULTI:
                if (if_getdrvflags(ifp) & IFF_DRV_RUNNING) {
                        DWC_LOCK(sc);
                        dwc1000_setup_rxfilter(sc);
                        DWC_UNLOCK(sc);
                }
                break;
        case SIOCSIFMEDIA:
        case SIOCGIFMEDIA:
                mii = sc->mii_softc;
                error = ifmedia_ioctl(ifp, ifr, &mii->mii_media, cmd);
                break;
        case SIOCSIFCAP:
                mask = ifr->ifr_reqcap ^ if_getcapenable(ifp);
                if (mask & IFCAP_VLAN_MTU) {
                        /* No work to do except acknowledge the change took */
                        if_togglecapenable(ifp, IFCAP_VLAN_MTU);
                }
                if (mask & (IFCAP_RXCSUM | IFCAP_RXCSUM_IPV6))
                        if_togglecapenable(ifp, IFCAP_RXCSUM | IFCAP_RXCSUM_IPV6);
                if (mask & IFCAP_TXCSUM) {
                        if_togglecapenable(ifp, IFCAP_TXCSUM);
                        if_togglehwassist(ifp, CSUM_IP | CSUM_DELAY_DATA);
                }
                if (mask & IFCAP_TXCSUM_IPV6) {
                        if_togglecapenable(ifp, IFCAP_TXCSUM_IPV6);
                        if_togglehwassist(ifp, CSUM_DELAY_DATA_IPV6);
                }

                if (if_getdrvflags(ifp) & IFF_DRV_RUNNING) {
                        DWC_LOCK(sc);
                        dwc1000_enable_csum_offload(sc);
                        DWC_UNLOCK(sc);
                }
                break;

        default:
                error = ether_ioctl(ifp, cmd, data);
                break;
        }

        return (error);
}

/*
 * Interrupts functions
 */


static void
dwc_intr(void *arg)
{
        struct dwc_softc *sc;
        int rv;

        sc = arg;
        DWC_LOCK(sc);
        dwc1000_intr(sc);
        rv = dma1000_intr(sc);
        if (rv == EIO) {
                device_printf(sc->dev,
                  "Ethernet DMA error, restarting controller.\n");
                dwc_stop_locked(sc);
                dwc_init_locked(sc);
        }
        DWC_UNLOCK(sc);
}

static void
dwc_tick(void *arg)
{
        struct dwc_softc *sc;
        if_t ifp;
        int link_was_up;

        sc = arg;

        DWC_ASSERT_LOCKED(sc);

        ifp = sc->ifp;

        if ((if_getdrvflags(ifp) & IFF_DRV_RUNNING) == 0)
            return;

        /*
         * Typical tx watchdog.  If this fires it indicates that we enqueued
         * packets for output and never got a txdone interrupt for them.  Maybe
         * it's a missed interrupt somehow, just pretend we got one.
         */
        if (sc->tx_watchdog_count > 0) {
                if (--sc->tx_watchdog_count == 0) {
                        dma1000_txfinish_locked(sc);
                }
        }

        /* Gather stats from hardware counters. */
        dwc1000_harvest_stats(sc);

        /* Check the media status. */
        link_was_up = sc->link_is_up;
        mii_tick(sc->mii_softc);
        if (sc->link_is_up && !link_was_up)
                dwc_txstart_locked(sc);

        /* Schedule another check one second from now. */
        callout_reset(&sc->dwc_callout, hz, dwc_tick, sc);
}

static int
dwc_reset_phy(struct dwc_softc *sc)
{
        pcell_t gpio_prop[4];
        pcell_t delay_prop[3];
        phandle_t gpio_node;
        device_t gpio;
        uint32_t pin, flags;
        uint32_t pin_value;

        /*
         * All those properties are deprecated but still used in some DTS.
         * The new way to deal with this is to use the generic bindings
         * present in the ethernet-phy node.
         */
        if (OF_getencprop(sc->node, "snps,reset-gpio",
            gpio_prop, sizeof(gpio_prop)) <= 0)
                return (0);

        if (OF_getencprop(sc->node, "snps,reset-delays-us",
            delay_prop, sizeof(delay_prop)) <= 0) {
                device_printf(sc->dev,
                    "Wrong property for snps,reset-delays-us");
                return (ENXIO);
        }

        gpio_node = OF_node_from_xref(gpio_prop[0]);
        if ((gpio = OF_device_from_xref(gpio_prop[0])) == NULL) {
                device_printf(sc->dev,
                    "Can't find gpio controller for phy reset\n");
                return (ENXIO);
        }

        if (GPIO_MAP_GPIOS(gpio, sc->node, gpio_node,
            nitems(gpio_prop) - 1,
            gpio_prop + 1, &pin, &flags) != 0) {
                device_printf(sc->dev, "Can't map gpio for phy reset\n");
                return (ENXIO);
        }

        pin_value = GPIO_PIN_LOW;
        if (OF_hasprop(sc->node, "snps,reset-active-low"))
                pin_value = GPIO_PIN_HIGH;

        GPIO_PIN_SETFLAGS(gpio, pin, GPIO_PIN_OUTPUT);
        GPIO_PIN_SET(gpio, pin, pin_value);
        DELAY(delay_prop[0] * 5);
        GPIO_PIN_SET(gpio, pin, !pin_value);
        DELAY(delay_prop[1] * 5);
        GPIO_PIN_SET(gpio, pin, pin_value);
        DELAY(delay_prop[2] * 5);

        return (0);
}

static int
dwc_clock_init(struct dwc_softc *sc)
{
        int rv;
        int64_t freq;

        /* Required clock */
        rv = clk_get_by_ofw_name(sc->dev, 0, "stmmaceth", &sc->clk_stmmaceth);
        if (rv != 0) {
                device_printf(sc->dev, "Cannot get GMAC main clock\n");
                return (ENXIO);
        }
        if ((rv = clk_enable(sc->clk_stmmaceth)) != 0) {
                device_printf(sc->dev, "could not enable main clock\n");
                return (rv);
        }

        /* Optional clock */
        rv = clk_get_by_ofw_name(sc->dev, 0, "pclk", &sc->clk_pclk);
        if (rv != 0)
                return (0);
        if ((rv = clk_enable(sc->clk_pclk)) != 0) {
                device_printf(sc->dev, "could not enable peripheral clock\n");
                return (rv);
        }

        if (bootverbose) {
                clk_get_freq(sc->clk_stmmaceth, &freq);
                device_printf(sc->dev, "MAC clock(%s) freq: %jd\n",
                    clk_get_name(sc->clk_stmmaceth), (intmax_t)freq);
        }

        return (0);
}

static int
dwc_reset_deassert(struct dwc_softc *sc)
{
        int rv;

        /* Required reset */
        rv = hwreset_get_by_ofw_name(sc->dev, 0, "stmmaceth", &sc->rst_stmmaceth);
        if (rv != 0) {
                device_printf(sc->dev, "Cannot get GMAC reset\n");
                return (ENXIO);
        }
        rv = hwreset_deassert(sc->rst_stmmaceth);
        if (rv != 0) {
                device_printf(sc->dev, "could not de-assert GMAC reset\n");
                return (rv);
        }

        /* Optional reset */
        rv = hwreset_get_by_ofw_name(sc->dev, 0, "ahb", &sc->rst_ahb);
        if (rv != 0)
                return (0);
        rv = hwreset_deassert(sc->rst_ahb);
        if (rv != 0) {
                device_printf(sc->dev, "could not de-assert AHB reset\n");
                return (rv);
        }

        return (0);
}

/*
 * Probe/Attach functions
 */

static int
dwc_probe(device_t dev)
{

        if (!ofw_bus_status_okay(dev))
                return (ENXIO);

        if (!ofw_bus_is_compatible(dev, "snps,dwmac"))
                return (ENXIO);

        device_set_desc(dev, "Gigabit Ethernet Controller");
        return (BUS_PROBE_DEFAULT);
}

static int
dwc_attach(device_t dev)
{
        uint8_t macaddr[ETHER_ADDR_LEN];
        struct dwc_softc *sc;
        if_t ifp;
        int error;
        uint32_t pbl;

        sc = device_get_softc(dev);
        sc->dev = dev;
        sc->rx_idx = 0;
        sc->tx_desccount = TX_DESC_COUNT;
        sc->tx_mapcount = 0;

        sc->node = ofw_bus_get_node(dev);
        sc->phy_mode = mii_fdt_get_contype(sc->node);
        switch (sc->phy_mode) {
        case MII_CONTYPE_RGMII:
        case MII_CONTYPE_RGMII_ID:
        case MII_CONTYPE_RGMII_RXID:
        case MII_CONTYPE_RGMII_TXID:
        case MII_CONTYPE_RMII:
        case MII_CONTYPE_MII:
                break;
        default:
                device_printf(dev, "Unsupported MII type\n");
                return (ENXIO);
        }

        if (OF_getencprop(sc->node, "snps,pbl", &pbl, sizeof(uint32_t)) <= 0)
                pbl = DMA_DEFAULT_PBL;
        if (OF_getencprop(sc->node, "snps,txpbl", &sc->txpbl, sizeof(uint32_t)) <= 0)
                sc->txpbl = pbl;
        if (OF_getencprop(sc->node, "snps,rxpbl", &sc->rxpbl, sizeof(uint32_t)) <= 0)
                sc->rxpbl = pbl;
        if (OF_hasprop(sc->node, "snps,no-pbl-x8") == 1)
                sc->nopblx8 = true;
        if (OF_hasprop(sc->node, "snps,fixed-burst") == 1)
                sc->fixed_burst = true;
        if (OF_hasprop(sc->node, "snps,mixed-burst") == 1)
                sc->mixed_burst = true;
        if (OF_hasprop(sc->node, "snps,aal") == 1)
                sc->aal = true;

        error = clk_set_assigned(dev, ofw_bus_get_node(dev));
        if (error != 0 && error != ENOENT) {
                device_printf(dev, "clk_set_assigned failed\n");
                return (error);
        }

        /* Enable main clock */
        if ((error = dwc_clock_init(sc)) != 0)
                return (error);
        /* De-assert main reset */
        if ((error = dwc_reset_deassert(sc)) != 0)
                return (error);

        if (IF_DWC_INIT(dev) != 0)
                return (ENXIO);

        if ((sc->mii_clk = IF_DWC_MII_CLK(dev)) < 0) {
                device_printf(dev, "Cannot get mii clock value %d\n", -sc->mii_clk);
                return (ENXIO);
        }

        if (bus_alloc_resources(dev, dwc_spec, sc->res)) {
                device_printf(dev, "could not allocate resources\n");
                return (ENXIO);
        }

        /* Read MAC before reset */
        dwc1000_get_hwaddr(sc, macaddr);

        /* Reset the PHY if needed */
        if (dwc_reset_phy(sc) != 0) {
                device_printf(dev, "Can't reset the PHY\n");
                bus_release_resources(dev, dwc_spec, sc->res);
                return (ENXIO);
        }

        /* Reset */
        if ((error = dma1000_reset(sc)) != 0) {
                device_printf(sc->dev, "Can't reset DMA controller.\n");
                bus_release_resources(sc->dev, dwc_spec, sc->res);
                return (error);
        }

        if (dma1000_init(sc)) {
                bus_release_resources(dev, dwc_spec, sc->res);
                return (ENXIO);
        }

        mtx_init(&sc->mtx, device_get_nameunit(sc->dev),
            MTX_NETWORK_LOCK, MTX_DEF);

        callout_init_mtx(&sc->dwc_callout, &sc->mtx, 0);

        /* Setup interrupt handler. */
        error = bus_setup_intr(dev, sc->res[1], INTR_TYPE_NET | INTR_MPSAFE,
            NULL, dwc_intr, sc, &sc->intr_cookie);
        if (error != 0) {
                device_printf(dev, "could not setup interrupt handler.\n");
                bus_release_resources(dev, dwc_spec, sc->res);
                return (ENXIO);
        }

        /* Set up the ethernet interface. */
        sc->ifp = ifp = if_alloc(IFT_ETHER);

        if_setsoftc(ifp, sc);
        if_initname(ifp, device_get_name(dev), device_get_unit(dev));
        if_setflags(sc->ifp, IFF_BROADCAST | IFF_SIMPLEX | IFF_MULTICAST);
        if_setstartfn(ifp, dwc_txstart);
        if_setioctlfn(ifp, dwc_ioctl);
        if_setinitfn(ifp, dwc_init);
        if_setsendqlen(ifp, TX_MAP_COUNT - 1);
        if_setsendqready(sc->ifp);
        if_sethwassist(sc->ifp, CSUM_IP | CSUM_DELAY_DATA | CSUM_DELAY_DATA_IPV6);
        if_setcapabilities(sc->ifp, IFCAP_VLAN_MTU | IFCAP_HWCSUM | IFCAP_HWCSUM_IPV6);
        if_setcapenable(sc->ifp, if_getcapabilities(sc->ifp));

        /* Attach the mii driver. */
        error = mii_attach(dev, &sc->miibus, ifp, dwc_media_change,
            dwc_media_status, BMSR_DEFCAPMASK, MII_PHY_ANY,
            MII_OFFSET_ANY, 0);

        if (error != 0) {
                device_printf(dev, "PHY attach failed\n");
                bus_teardown_intr(dev, sc->res[1], sc->intr_cookie);
                bus_release_resources(dev, dwc_spec, sc->res);
                return (ENXIO);
        }
        sc->mii_softc = device_get_softc(sc->miibus);

        /* All ready to run, attach the ethernet interface. */
        ether_ifattach(ifp, macaddr);
        sc->is_attached = true;

        return (0);
}

static int
dwc_detach(device_t dev)
{
        struct dwc_softc *sc;

        sc = device_get_softc(dev);

        /*
         * Disable and tear down interrupts before anything else, so we don't
         * race with the handler.
         */
        dwc1000_intr_disable(sc);
        if (sc->intr_cookie != NULL) {
                bus_teardown_intr(dev, sc->res[1], sc->intr_cookie);
        }

        if (sc->is_attached) {
                DWC_LOCK(sc);
                sc->is_detaching = true;
                dwc_stop_locked(sc);
                DWC_UNLOCK(sc);
                callout_drain(&sc->dwc_callout);
                ether_ifdetach(sc->ifp);
        }

        bus_generic_detach(dev);

        /* Free DMA descriptors */
        dma1000_free(sc);

        if (sc->ifp != NULL) {
                if_free(sc->ifp);
                sc->ifp = NULL;
        }

        bus_release_resources(dev, dwc_spec, sc->res);

        mtx_destroy(&sc->mtx);
        return (0);
}

static device_method_t dwc_methods[] = {
        DEVMETHOD(device_probe,         dwc_probe),
        DEVMETHOD(device_attach,        dwc_attach),
        DEVMETHOD(device_detach,        dwc_detach),

        /* MII Interface */
        DEVMETHOD(miibus_readreg,       dwc1000_miibus_read_reg),
        DEVMETHOD(miibus_writereg,      dwc1000_miibus_write_reg),
        DEVMETHOD(miibus_statchg,       dwc1000_miibus_statchg),

        DEVMETHOD_END
};

driver_t dwc_driver = {
        "dwc",
        dwc_methods,
        sizeof(struct dwc_softc),
};

DRIVER_MODULE(dwc, simplebus, dwc_driver, 0, 0);
DRIVER_MODULE(miibus, dwc, miibus_driver, 0, 0);

MODULE_DEPEND(dwc, ether, 1, 1, 1);
MODULE_DEPEND(dwc, miibus, 1, 1, 1);