Boot Linux faster!

Check our new training course

Boot Linux faster!

Check our new training course
and Creative Commons CC-BY-SA
lecture and lab materials

Bootlin logo

Elixir Cross Referencer

/*
 * Copyright (c) 2016 Piotr Mienkowski
 * SPDX-License-Identifier: Apache-2.0
 */

/** @file
 * @brief Atmel SAM MCU family Ethernet PHY (GMAC) driver.
 */

#include <errno.h>
#include <kernel.h>
#include <net/mii.h>
#include "phy_sam_gmac.h"

#define LOG_MODULE_NAME eth_sam_phy
#define LOG_LEVEL CONFIG_ETHERNET_LOG_LEVEL

#include <logging/log.h>
LOG_MODULE_REGISTER(LOG_MODULE_NAME);

/* Maximum time to establish a link through auto-negotiation for
 * 10BASE-T, 100BASE-TX is 3.7s, to add an extra margin the timeout
 * is set at 4s.
 */
#define PHY_AUTONEG_TIMEOUT_MS   4000

/* Enable MDIO serial bus between MAC and PHY. */
static void mdio_bus_enable(Gmac *gmac)
{
	gmac->GMAC_NCR |= GMAC_NCR_MPE;
}

/* Disable MDIO serial bus between MAC and PHY. */
static void mdio_bus_disable(Gmac *gmac)
{
	gmac->GMAC_NCR &= ~GMAC_NCR_MPE;
}

/* Wait PHY operation complete. */
static int mdio_bus_wait(Gmac *gmac)
{
	u32_t retries = 100U;  /* will wait up to 1 s */

	while (!(gmac->GMAC_NSR & GMAC_NSR_IDLE))   {
		if (retries-- == 0U) {
			LOG_ERR("timeout");
			return -ETIMEDOUT;
		}

		k_sleep(K_MSEC(10));
	}

	return 0;
}

/* Send command to PHY over MDIO serial bus */
static int mdio_bus_send(Gmac *gmac, u8_t phy_addr, u8_t reg_addr,
			 u8_t rw, u16_t data)
{
	int retval;

	/* Write GMAC PHY maintenance register */
	gmac->GMAC_MAN =   GMAC_MAN_CLTTO
			 | (GMAC_MAN_OP(rw ? 0x2 : 0x1))
			 | GMAC_MAN_WTN(0x02)
			 | GMAC_MAN_PHYA(phy_addr)
			 | GMAC_MAN_REGA(reg_addr)
			 | GMAC_MAN_DATA(data);

	/* Wait until PHY is ready */
	retval = mdio_bus_wait(gmac);
	if (retval < 0) {
		return retval;
	}

	return 0;
}

/* Read PHY register. */
static int phy_read(const struct phy_sam_gmac_dev *phy, u8_t reg_addr,
		    u32_t *value)
{
	Gmac *const gmac = phy->regs;
	u8_t phy_addr = phy->address;
	int retval;

	retval = mdio_bus_send(gmac, phy_addr, reg_addr, 1, 0);
	if (retval < 0) {
		return retval;
	}

	/* Read data */
	*value = gmac->GMAC_MAN & GMAC_MAN_DATA_Msk;

	return 0;
}

/* Write PHY register. */
static int phy_write(const struct phy_sam_gmac_dev *phy, u8_t reg_addr,
		     u32_t value)
{
	Gmac *const gmac = phy->regs;
	u8_t phy_addr = phy->address;

	return mdio_bus_send(gmac, phy_addr, reg_addr, 0, value);
}

/* Issue a PHY soft reset. */
static int phy_soft_reset(const struct phy_sam_gmac_dev *phy)
{
	u32_t phy_reg;
	u32_t retries = 12U;
	int retval;

	/* Issue a soft reset */
	retval = phy_write(phy, MII_BMCR, MII_BMCR_RESET);
	if (retval < 0) {
		return retval;
	}

	/* Wait up to 0.6s for the reset sequence to finish. According to
	 * IEEE 802.3, Section 2, Subsection 22.2.4.1.1 a PHY reset may take
	 * up to 0.5 s.
	 */
	do {
		if (retries-- == 0U) {
			return -ETIMEDOUT;
		}

		k_sleep(K_MSEC(50));

		retval = phy_read(phy, MII_BMCR, &phy_reg);
		if (retval < 0) {
			return retval;
		}
	} while (phy_reg & MII_BMCR_RESET);

	return 0;
}

int phy_sam_gmac_init(const struct phy_sam_gmac_dev *phy)
{
	Gmac *const gmac = phy->regs;
	int phy_id;

	mdio_bus_enable(gmac);

	LOG_INF("Soft Reset of ETH PHY");
	phy_soft_reset(phy);

	/* Verify that the PHY device is responding */
	phy_id = phy_sam_gmac_id_get(phy);
	if (phy_id == 0xFFFFFFFF) {
		LOG_ERR("Unable to detect a valid PHY");
		return -1;
	}

	LOG_INF("PHYID: 0x%X at addr: %d", phy_id, phy->address);

	mdio_bus_disable(gmac);

	return 0;
}

u32_t phy_sam_gmac_id_get(const struct phy_sam_gmac_dev *phy)
{
	Gmac *const gmac = phy->regs;
	u32_t phy_reg;
	u32_t phy_id;

	mdio_bus_enable(gmac);

	if (phy_read(phy, MII_PHYID1R, &phy_reg) < 0) {
		return 0xFFFFFFFF;
	}

	phy_id = (phy_reg & 0xFFFF) << 16;

	if (phy_read(phy, MII_PHYID2R, &phy_reg) < 0) {
		return 0xFFFFFFFF;
	}

	phy_id |= (phy_reg & 0xFFFF);

	mdio_bus_disable(gmac);

	return phy_id;
}

int phy_sam_gmac_auto_negotiate(const struct phy_sam_gmac_dev *phy,
				u32_t *status)
{
	Gmac *const gmac = phy->regs;
	u32_t val;
	u32_t ability_adv;
	u32_t ability_rcvd;
	u32_t retries = PHY_AUTONEG_TIMEOUT_MS / 100;
	int retval;

	mdio_bus_enable(gmac);

	LOG_DBG("Starting ETH PHY auto-negotiate sequence");

	/* Read PHY default advertising parameters */
	retval = phy_read(phy, MII_ANAR, &ability_adv);
	if (retval < 0) {
		goto auto_negotiate_exit;
	}

	/* Configure and start auto-negotiation process */
	retval = phy_read(phy, MII_BMCR, &val);
	if (retval < 0) {
		goto auto_negotiate_exit;
	}

	val |= MII_BMCR_AUTONEG_ENABLE | MII_BMCR_AUTONEG_RESTART;
	val &= ~MII_BMCR_ISOLATE;  /* Don't isolate the PHY */

	retval = phy_write(phy, MII_BMCR, val);
	if (retval < 0) {
		goto auto_negotiate_exit;
	}

	/* Wait for the auto-negotiation process to complete */
	do {
		if (retries-- == 0U) {
			retval = -ETIMEDOUT;
			goto auto_negotiate_exit;
		}

		k_sleep(K_MSEC(100));

		retval = phy_read(phy, MII_BMSR, &val);
		if (retval < 0) {
			goto auto_negotiate_exit;
		}
	} while (!(val & MII_BMSR_AUTONEG_COMPLETE));

	LOG_DBG("PHY auto-negotiate sequence completed");

	/* Read abilities of the remote device */
	retval = phy_read(phy, MII_ANLPAR, &ability_rcvd);
	if (retval < 0) {
		goto auto_negotiate_exit;
	}

	/* Determine the best possible mode of operation */
	if ((ability_adv & ability_rcvd) & MII_ADVERTISE_100_FULL) {
		*status = PHY_DUPLEX_FULL | PHY_SPEED_100M;
	} else if ((ability_adv & ability_rcvd) & MII_ADVERTISE_100_HALF) {
		*status = PHY_DUPLEX_HALF | PHY_SPEED_100M;
	} else if ((ability_adv & ability_rcvd) & MII_ADVERTISE_10_FULL) {
		*status = PHY_DUPLEX_FULL | PHY_SPEED_10M;
	} else {
		*status = PHY_DUPLEX_HALF | PHY_SPEED_10M;
	}

	LOG_INF("common abilities: speed %s Mb, %s duplex",
		*status & PHY_SPEED_100M ? "100" : "10",
		*status & PHY_DUPLEX_FULL ? "full" : "half");

auto_negotiate_exit:
	mdio_bus_disable(gmac);
	return retval;
}