| // SPDX-License-Identifier: GPL-2.0+ | |
| /* | |
| * Copyright (c) 2017 Microchip Technology Inc. All rights reserved. | |
| */ | |
| #include <dm.h> | |
| #include <malloc.h> | |
| #include <miiphy.h> | |
| #include <memalign.h> | |
| #include <usb.h> | |
| #include <linux/ethtool.h> | |
| #include <linux/mii.h> | |
| #include "usb_ether.h" | |
| #include "lan7x.h" | |
| /* | |
| * Lan7x infrastructure commands | |
| */ | |
| int lan7x_write_reg(struct usb_device *udev, u32 index, u32 data) | |
| { | |
| int len; | |
| ALLOC_CACHE_ALIGN_BUFFER(u32, tmpbuf, 1); | |
| cpu_to_le32s(&data); | |
| tmpbuf[0] = data; | |
| len = usb_control_msg(udev, usb_sndctrlpipe(udev, 0), | |
| USB_VENDOR_REQUEST_WRITE_REGISTER, | |
| USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_DEVICE, | |
| 0, index, tmpbuf, sizeof(data), | |
| USB_CTRL_SET_TIMEOUT_MS); | |
| if (len != sizeof(data)) { | |
| debug("%s failed: index=%d, data=%d, len=%d", | |
| __func__, index, data, len); | |
| return -EIO; | |
| } | |
| return 0; | |
| } | |
| int lan7x_read_reg(struct usb_device *udev, u32 index, u32 *data) | |
| { | |
| int len; | |
| ALLOC_CACHE_ALIGN_BUFFER(u32, tmpbuf, 1); | |
| len = usb_control_msg(udev, usb_rcvctrlpipe(udev, 0), | |
| USB_VENDOR_REQUEST_READ_REGISTER, | |
| USB_DIR_IN | USB_TYPE_VENDOR | USB_RECIP_DEVICE, | |
| 0, index, tmpbuf, sizeof(*data), | |
| USB_CTRL_GET_TIMEOUT_MS); | |
| *data = tmpbuf[0]; | |
| if (len != sizeof(*data)) { | |
| debug("%s failed: index=%d, len=%d", __func__, index, len); | |
| return -EIO; | |
| } | |
| le32_to_cpus(data); | |
| return 0; | |
| } | |
| static int lan7x_phy_wait_not_busy(struct usb_device *udev) | |
| { | |
| return lan7x_wait_for_bit(udev, __func__, | |
| MII_ACC, MII_ACC_MII_BUSY, | |
| false, 100, 0); | |
| } | |
| int lan7x_mdio_read(struct usb_device *udev, int phy_id, int idx) | |
| { | |
| u32 val, addr; | |
| /* confirm MII not busy */ | |
| if (lan7x_phy_wait_not_busy(udev)) { | |
| debug("MII is busy in %s\n", __func__); | |
| return -ETIMEDOUT; | |
| } | |
| /* set the address, index & direction (read from PHY) */ | |
| addr = (phy_id << 11) | (idx << 6) | | |
| MII_ACC_MII_READ | MII_ACC_MII_BUSY; | |
| lan7x_write_reg(udev, MII_ACC, addr); | |
| if (lan7x_phy_wait_not_busy(udev)) { | |
| debug("Timed out reading MII reg %02X\n", idx); | |
| return -ETIMEDOUT; | |
| } | |
| lan7x_read_reg(udev, MII_DATA, &val); | |
| return val & 0xFFFF; | |
| } | |
| void lan7x_mdio_write(struct usb_device *udev, int phy_id, int idx, int regval) | |
| { | |
| u32 addr; | |
| /* confirm MII not busy */ | |
| if (lan7x_phy_wait_not_busy(udev)) { | |
| debug("MII is busy in %s\n", __func__); | |
| return; | |
| } | |
| lan7x_write_reg(udev, MII_DATA, regval); | |
| /* set the address, index & direction (write to PHY) */ | |
| addr = (phy_id << 11) | (idx << 6) | | |
| MII_ACC_MII_WRITE | MII_ACC_MII_BUSY; | |
| lan7x_write_reg(udev, MII_ACC, addr); | |
| if (lan7x_phy_wait_not_busy(udev)) | |
| debug("Timed out writing MII reg %02X\n", idx); | |
| } | |
| /* | |
| * Lan7x phylib wrappers | |
| */ | |
| static int lan7x_phylib_mdio_read(struct mii_dev *bus, | |
| int addr, int devad, int reg) | |
| { | |
| struct usb_device *udev = dev_get_parent_priv(bus->priv); | |
| return lan7x_mdio_read(udev, addr, reg); | |
| } | |
| static int lan7x_phylib_mdio_write(struct mii_dev *bus, | |
| int addr, int devad, int reg, u16 val) | |
| { | |
| struct usb_device *udev = dev_get_parent_priv(bus->priv); | |
| lan7x_mdio_write(udev, addr, reg, (int)val); | |
| return 0; | |
| } | |
| /* | |
| * Lan7x eeprom functions | |
| */ | |
| static int lan7x_eeprom_confirm_not_busy(struct usb_device *udev) | |
| { | |
| return lan7x_wait_for_bit(udev, __func__, | |
| E2P_CMD, E2P_CMD_EPC_BUSY, | |
| false, 100, 0); | |
| } | |
| static int lan7x_wait_eeprom(struct usb_device *udev) | |
| { | |
| return lan7x_wait_for_bit(udev, __func__, | |
| E2P_CMD, | |
| (E2P_CMD_EPC_BUSY | E2P_CMD_EPC_TIMEOUT), | |
| false, 100, 0); | |
| } | |
| static int lan7x_read_eeprom(struct usb_device *udev, | |
| u32 offset, u32 length, u8 *data) | |
| { | |
| u32 val; | |
| int i, ret; | |
| ret = lan7x_eeprom_confirm_not_busy(udev); | |
| if (ret) | |
| return ret; | |
| for (i = 0; i < length; i++) { | |
| val = E2P_CMD_EPC_BUSY | E2P_CMD_EPC_CMD_READ | | |
| (offset & E2P_CMD_EPC_ADDR_MASK); | |
| lan7x_write_reg(udev, E2P_CMD, val); | |
| ret = lan7x_wait_eeprom(udev); | |
| if (ret) | |
| return ret; | |
| lan7x_read_reg(udev, E2P_DATA, &val); | |
| data[i] = val & 0xFF; | |
| offset++; | |
| } | |
| return ret; | |
| } | |
| /* | |
| * Lan7x phylib functions | |
| */ | |
| int lan7x_phylib_register(struct udevice *udev) | |
| { | |
| struct usb_device *usbdev = dev_get_parent_priv(udev); | |
| struct lan7x_private *priv = dev_get_priv(udev); | |
| int ret; | |
| priv->mdiobus = mdio_alloc(); | |
| if (!priv->mdiobus) { | |
| printf("mdio_alloc failed\n"); | |
| return -ENOMEM; | |
| } | |
| priv->mdiobus->read = lan7x_phylib_mdio_read; | |
| priv->mdiobus->write = lan7x_phylib_mdio_write; | |
| sprintf(priv->mdiobus->name, | |
| "lan7x_mdiobus-d%hu-p%hu", usbdev->devnum, usbdev->portnr); | |
| priv->mdiobus->priv = (void *)udev; | |
| ret = mdio_register(priv->mdiobus); | |
| if (ret) { | |
| printf("mdio_register failed\n"); | |
| free(priv->mdiobus); | |
| return -ENOMEM; | |
| } | |
| return 0; | |
| } | |
| int lan7x_eth_phylib_connect(struct udevice *udev, struct ueth_data *dev) | |
| { | |
| struct lan7x_private *priv = dev_get_priv(udev); | |
| priv->phydev = phy_connect(priv->mdiobus, dev->phy_id, | |
| udev, PHY_INTERFACE_MODE_MII); | |
| if (!priv->phydev) { | |
| printf("phy_connect failed\n"); | |
| return -ENODEV; | |
| } | |
| return 0; | |
| } | |
| int lan7x_eth_phylib_config_start(struct udevice *udev) | |
| { | |
| struct lan7x_private *priv = dev_get_priv(udev); | |
| int ret; | |
| /* configure supported modes */ | |
| priv->phydev->supported = PHY_BASIC_FEATURES | | |
| SUPPORTED_1000baseT_Full | | |
| SUPPORTED_Pause | | |
| SUPPORTED_Asym_Pause; | |
| priv->phydev->advertising = ADVERTISED_10baseT_Half | | |
| ADVERTISED_10baseT_Full | | |
| ADVERTISED_100baseT_Half | | |
| ADVERTISED_100baseT_Full | | |
| ADVERTISED_1000baseT_Full | | |
| ADVERTISED_Pause | | |
| ADVERTISED_Asym_Pause | | |
| ADVERTISED_Autoneg; | |
| priv->phydev->autoneg = AUTONEG_ENABLE; | |
| ret = genphy_config_aneg(priv->phydev); | |
| if (ret) { | |
| printf("genphy_config_aneg failed\n"); | |
| return ret; | |
| } | |
| ret = phy_startup(priv->phydev); | |
| if (ret) { | |
| printf("phy_startup failed\n"); | |
| return ret; | |
| } | |
| debug("** %s() speed %i duplex %i adv %X supp %X\n", __func__, | |
| priv->phydev->speed, priv->phydev->duplex, | |
| priv->phydev->advertising, priv->phydev->supported); | |
| return 0; | |
| } | |
| int lan7x_update_flowcontrol(struct usb_device *udev, | |
| struct ueth_data *dev, | |
| uint32_t *flow, uint32_t *fct_flow) | |
| { | |
| uint32_t lcladv, rmtadv; | |
| u8 cap = 0; | |
| struct lan7x_private *priv = dev_get_priv(udev->dev); | |
| debug("** %s()\n", __func__); | |
| debug("** %s() priv->phydev->speed %i duplex %i\n", __func__, | |
| priv->phydev->speed, priv->phydev->duplex); | |
| if (priv->phydev->duplex == DUPLEX_FULL) { | |
| lcladv = lan7x_mdio_read(udev, dev->phy_id, MII_ADVERTISE); | |
| rmtadv = lan7x_mdio_read(udev, dev->phy_id, MII_LPA); | |
| cap = mii_resolve_flowctrl_fdx(lcladv, rmtadv); | |
| debug("TX Flow "); | |
| if (cap & FLOW_CTRL_TX) { | |
| *flow = (FLOW_CR_TX_FCEN | 0xFFFF); | |
| /* set fct_flow thresholds to 20% and 80% */ | |
| *fct_flow = ((MAX_RX_FIFO_SIZE * 2) / (10 * 512)) | |
| & 0x7FUL; | |
| *fct_flow <<= 8UL; | |
| *fct_flow |= ((MAX_RX_FIFO_SIZE * 8) / (10 * 512)) | |
| & 0x7FUL; | |
| debug("EN "); | |
| } else { | |
| debug("DIS "); | |
| } | |
| debug("RX Flow "); | |
| if (cap & FLOW_CTRL_RX) { | |
| *flow |= FLOW_CR_RX_FCEN; | |
| debug("EN"); | |
| } else { | |
| debug("DIS"); | |
| } | |
| } | |
| debug("\n"); | |
| return 0; | |
| } | |
| int lan7x_read_eeprom_mac(unsigned char *enetaddr, struct usb_device *udev) | |
| { | |
| int ret; | |
| memset(enetaddr, 0, 6); | |
| ret = lan7x_read_eeprom(udev, 0, 1, enetaddr); | |
| if ((ret == 0) && (enetaddr[0] == EEPROM_INDICATOR)) { | |
| ret = lan7x_read_eeprom(udev, | |
| EEPROM_MAC_OFFSET, ETH_ALEN, | |
| enetaddr); | |
| if ((ret == 0) && is_valid_ethaddr(enetaddr)) { | |
| /* eeprom values are valid so use them */ | |
| debug("MAC address read from EEPROM %pM\n", | |
| enetaddr); | |
| return 0; | |
| } | |
| } | |
| debug("MAC address read from EEPROM invalid %pM\n", enetaddr); | |
| memset(enetaddr, 0, 6); | |
| return -EINVAL; | |
| } | |
| int lan7x_pmt_phy_reset(struct usb_device *udev, | |
| struct ueth_data *dev) | |
| { | |
| int ret; | |
| u32 data; | |
| ret = lan7x_read_reg(udev, PMT_CTL, &data); | |
| if (ret) | |
| return ret; | |
| ret = lan7x_write_reg(udev, PMT_CTL, data | PMT_CTL_PHY_RST); | |
| if (ret) | |
| return ret; | |
| /* for LAN7x, we need to check PMT_CTL_READY asserted */ | |
| ret = lan7x_wait_for_bit(udev, "PMT_CTL_PHY_RST", | |
| PMT_CTL, PMT_CTL_PHY_RST, | |
| false, 1000, 0); /* could take over 125mS */ | |
| if (ret) | |
| return ret; | |
| return lan7x_wait_for_bit(udev, "PMT_CTL_READY", | |
| PMT_CTL, PMT_CTL_READY, | |
| true, 1000, 0); | |
| } | |
| int lan7x_basic_reset(struct usb_device *udev, | |
| struct ueth_data *dev) | |
| { | |
| int ret; | |
| dev->phy_id = LAN7X_INTERNAL_PHY_ID; /* fixed phy id */ | |
| ret = lan7x_write_reg(udev, HW_CFG, HW_CFG_LRST); | |
| if (ret) | |
| return ret; | |
| ret = lan7x_wait_for_bit(udev, "HW_CFG_LRST", | |
| HW_CFG, HW_CFG_LRST, | |
| false, 1000, 0); | |
| if (ret) | |
| return ret; | |
| debug("USB devnum %d portnr %d\n", udev->devnum, udev->portnr); | |
| return lan7x_pmt_phy_reset(udev, dev); | |
| } | |
| void lan7x_eth_stop(struct udevice *dev) | |
| { | |
| debug("** %s()\n", __func__); | |
| } | |
| int lan7x_eth_send(struct udevice *dev, void *packet, int length) | |
| { | |
| struct lan7x_private *priv = dev_get_priv(dev); | |
| struct ueth_data *ueth = &priv->ueth; | |
| int err; | |
| int actual_len; | |
| u32 tx_cmd_a; | |
| u32 tx_cmd_b; | |
| ALLOC_CACHE_ALIGN_BUFFER(unsigned char, msg, | |
| PKTSIZE + sizeof(tx_cmd_a) + sizeof(tx_cmd_b)); | |
| debug("** %s(), len %d, buf %#x\n", __func__, length, | |
| (unsigned int)(ulong) msg); | |
| if (length > PKTSIZE) | |
| return -ENOSPC; | |
| /* LAN7x disable all TX offload features for u-boot */ | |
| tx_cmd_a = (u32) (length & TX_CMD_A_LEN_MASK) | TX_CMD_A_FCS; | |
| tx_cmd_b = 0; | |
| cpu_to_le32s(&tx_cmd_a); | |
| cpu_to_le32s(&tx_cmd_b); | |
| /* prepend cmd_a and cmd_b */ | |
| memcpy(msg, &tx_cmd_a, sizeof(tx_cmd_a)); | |
| memcpy(msg + sizeof(tx_cmd_a), &tx_cmd_b, sizeof(tx_cmd_b)); | |
| memcpy(msg + sizeof(tx_cmd_a) + sizeof(tx_cmd_b), (void *)packet, | |
| length); | |
| err = usb_bulk_msg(ueth->pusb_dev, | |
| usb_sndbulkpipe(ueth->pusb_dev, ueth->ep_out), | |
| (void *)msg, | |
| length + sizeof(tx_cmd_a) + | |
| sizeof(tx_cmd_b), | |
| &actual_len, USB_BULK_SEND_TIMEOUT_MS); | |
| debug("Tx: len = %u, actual = %u, err = %d\n", | |
| (unsigned int)(length + sizeof(tx_cmd_a) + sizeof(tx_cmd_b)), | |
| (unsigned int)actual_len, err); | |
| return err; | |
| } | |
| int lan7x_eth_recv(struct udevice *dev, int flags, uchar **packetp) | |
| { | |
| struct lan7x_private *priv = dev_get_priv(dev); | |
| struct ueth_data *ueth = &priv->ueth; | |
| uint8_t *ptr; | |
| int ret, len; | |
| u32 packet_len = 0; | |
| u32 rx_cmd_a = 0; | |
| len = usb_ether_get_rx_bytes(ueth, &ptr); | |
| debug("%s: first try, len=%d\n", __func__, len); | |
| if (!len) { | |
| if (!(flags & ETH_RECV_CHECK_DEVICE)) | |
| return -EAGAIN; | |
| ret = usb_ether_receive(ueth, RX_URB_SIZE); | |
| if (ret == -EAGAIN) | |
| return ret; | |
| len = usb_ether_get_rx_bytes(ueth, &ptr); | |
| debug("%s: second try, len=%d\n", __func__, len); | |
| } | |
| /* | |
| * 1st 4 bytes contain the length of the actual data plus error info. | |
| * Extract data length. | |
| */ | |
| if (len < sizeof(packet_len)) { | |
| debug("Rx: incomplete packet length\n"); | |
| goto err; | |
| } | |
| memcpy(&rx_cmd_a, ptr, sizeof(rx_cmd_a)); | |
| le32_to_cpus(&rx_cmd_a); | |
| if (rx_cmd_a & RX_CMD_A_RXE) { | |
| debug("Rx: Error header=%#x", rx_cmd_a); | |
| goto err; | |
| } | |
| packet_len = (u16) (rx_cmd_a & RX_CMD_A_LEN_MASK); | |
| if (packet_len > len - sizeof(packet_len)) { | |
| debug("Rx: too large packet: %d\n", packet_len); | |
| goto err; | |
| } | |
| /* | |
| * For LAN7x, the length in command A does not | |
| * include command A, B, and C length. | |
| * So use it as is. | |
| */ | |
| *packetp = ptr + 10; | |
| return packet_len; | |
| err: | |
| usb_ether_advance_rxbuf(ueth, -1); | |
| return -EINVAL; | |
| } | |
| int lan7x_free_pkt(struct udevice *dev, uchar *packet, int packet_len) | |
| { | |
| struct lan7x_private *priv = dev_get_priv(dev); | |
| packet_len = ALIGN(packet_len, 4); | |
| usb_ether_advance_rxbuf(&priv->ueth, sizeof(u32) + packet_len); | |
| return 0; | |
| } | |
| int lan7x_eth_remove(struct udevice *dev) | |
| { | |
| struct lan7x_private *priv = dev_get_priv(dev); | |
| debug("** %s()\n", __func__); | |
| free(priv->phydev); | |
| mdio_unregister(priv->mdiobus); | |
| mdio_free(priv->mdiobus); | |
| return 0; | |
| } |