| // SPDX-License-Identifier: GPL-2.0+ | |
| /* | |
| * Microchip's LAN865x 10BASE-T1S MAC-PHY driver | |
| * | |
| * Author: Parthiban Veerasooran <parthiban.veerasooran@microchip.com> | |
| */ | |
| #include <linux/module.h> | |
| #include <linux/kernel.h> | |
| #include <linux/phy.h> | |
| #include <linux/oa_tc6.h> | |
| #define DRV_NAME "lan8650" | |
| /* MAC Network Control Register */ | |
| #define LAN865X_REG_MAC_NET_CTL 0x00010000 | |
| #define MAC_NET_CTL_TXEN BIT(3) /* Transmit Enable */ | |
| #define MAC_NET_CTL_RXEN BIT(2) /* Receive Enable */ | |
| /* MAC Network Configuration Reg */ | |
| #define LAN865X_REG_MAC_NET_CFG 0x00010001 | |
| #define MAC_NET_CFG_PROMISCUOUS_MODE BIT(4) | |
| #define MAC_NET_CFG_MULTICAST_MODE BIT(6) | |
| #define MAC_NET_CFG_UNICAST_MODE BIT(7) | |
| /* MAC Hash Register Bottom */ | |
| #define LAN865X_REG_MAC_L_HASH 0x00010020 | |
| /* MAC Hash Register Top */ | |
| #define LAN865X_REG_MAC_H_HASH 0x00010021 | |
| /* MAC Specific Addr 1 Bottom Reg */ | |
| #define LAN865X_REG_MAC_L_SADDR1 0x00010022 | |
| /* MAC Specific Addr 1 Top Reg */ | |
| #define LAN865X_REG_MAC_H_SADDR1 0x00010023 | |
| /* MAC TSU Timer Increment Register */ | |
| #define LAN865X_REG_MAC_TSU_TIMER_INCR 0x00010077 | |
| #define MAC_TSU_TIMER_INCR_COUNT_NANOSECONDS 0x0028 | |
| struct lan865x_priv { | |
| struct work_struct multicast_work; | |
| struct net_device *netdev; | |
| struct spi_device *spi; | |
| struct oa_tc6 *tc6; | |
| }; | |
| static int lan865x_set_hw_macaddr_low_bytes(struct oa_tc6 *tc6, const u8 *mac) | |
| { | |
| u32 regval; | |
| regval = (mac[3] << 24) | (mac[2] << 16) | (mac[1] << 8) | mac[0]; | |
| return oa_tc6_write_register(tc6, LAN865X_REG_MAC_L_SADDR1, regval); | |
| } | |
| static int lan865x_set_hw_macaddr(struct lan865x_priv *priv, const u8 *mac) | |
| { | |
| int restore_ret; | |
| u32 regval; | |
| int ret; | |
| /* Configure MAC address low bytes */ | |
| ret = lan865x_set_hw_macaddr_low_bytes(priv->tc6, mac); | |
| if (ret) | |
| return ret; | |
| /* Prepare and configure MAC address high bytes */ | |
| regval = (mac[5] << 8) | mac[4]; | |
| ret = oa_tc6_write_register(priv->tc6, LAN865X_REG_MAC_H_SADDR1, | |
| regval); | |
| if (!ret) | |
| return 0; | |
| /* Restore the old MAC address low bytes from netdev if the new MAC | |
| * address high bytes setting failed. | |
| */ | |
| restore_ret = lan865x_set_hw_macaddr_low_bytes(priv->tc6, | |
| priv->netdev->dev_addr); | |
| if (restore_ret) | |
| return restore_ret; | |
| return ret; | |
| } | |
| static const struct ethtool_ops lan865x_ethtool_ops = { | |
| .get_link_ksettings = phy_ethtool_get_link_ksettings, | |
| .set_link_ksettings = phy_ethtool_set_link_ksettings, | |
| }; | |
| static int lan865x_set_mac_address(struct net_device *netdev, void *addr) | |
| { | |
| struct lan865x_priv *priv = netdev_priv(netdev); | |
| struct sockaddr *address = addr; | |
| int ret; | |
| ret = eth_prepare_mac_addr_change(netdev, addr); | |
| if (ret < 0) | |
| return ret; | |
| if (ether_addr_equal(address->sa_data, netdev->dev_addr)) | |
| return 0; | |
| ret = lan865x_set_hw_macaddr(priv, address->sa_data); | |
| if (ret) | |
| return ret; | |
| eth_commit_mac_addr_change(netdev, addr); | |
| return 0; | |
| } | |
| static u32 get_address_bit(u8 addr[ETH_ALEN], u32 bit) | |
| { | |
| return ((addr[bit / 8]) >> (bit % 8)) & 1; | |
| } | |
| static u32 lan865x_hash(u8 addr[ETH_ALEN]) | |
| { | |
| u32 hash_index = 0; | |
| for (int i = 0; i < 6; i++) { | |
| u32 hash = 0; | |
| for (int j = 0; j < 8; j++) | |
| hash ^= get_address_bit(addr, (j * 6) + i); | |
| hash_index |= (hash << i); | |
| } | |
| return hash_index; | |
| } | |
| static int lan865x_set_specific_multicast_addr(struct lan865x_priv *priv) | |
| { | |
| struct netdev_hw_addr *ha; | |
| u32 hash_lo = 0; | |
| u32 hash_hi = 0; | |
| int ret; | |
| netdev_for_each_mc_addr(ha, priv->netdev) { | |
| u32 bit_num = lan865x_hash(ha->addr); | |
| if (bit_num >= BIT(5)) | |
| hash_hi |= (1 << (bit_num - BIT(5))); | |
| else | |
| hash_lo |= (1 << bit_num); | |
| } | |
| /* Enabling specific multicast addresses */ | |
| ret = oa_tc6_write_register(priv->tc6, LAN865X_REG_MAC_H_HASH, hash_hi); | |
| if (ret) { | |
| netdev_err(priv->netdev, "Failed to write reg_hashh: %d\n", | |
| ret); | |
| return ret; | |
| } | |
| ret = oa_tc6_write_register(priv->tc6, LAN865X_REG_MAC_L_HASH, hash_lo); | |
| if (ret) | |
| netdev_err(priv->netdev, "Failed to write reg_hashl: %d\n", | |
| ret); | |
| return ret; | |
| } | |
| static int lan865x_set_all_multicast_addr(struct lan865x_priv *priv) | |
| { | |
| int ret; | |
| /* Enabling all multicast addresses */ | |
| ret = oa_tc6_write_register(priv->tc6, LAN865X_REG_MAC_H_HASH, | |
| 0xffffffff); | |
| if (ret) { | |
| netdev_err(priv->netdev, "Failed to write reg_hashh: %d\n", | |
| ret); | |
| return ret; | |
| } | |
| ret = oa_tc6_write_register(priv->tc6, LAN865X_REG_MAC_L_HASH, | |
| 0xffffffff); | |
| if (ret) | |
| netdev_err(priv->netdev, "Failed to write reg_hashl: %d\n", | |
| ret); | |
| return ret; | |
| } | |
| static int lan865x_clear_all_multicast_addr(struct lan865x_priv *priv) | |
| { | |
| int ret; | |
| ret = oa_tc6_write_register(priv->tc6, LAN865X_REG_MAC_H_HASH, 0); | |
| if (ret) { | |
| netdev_err(priv->netdev, "Failed to write reg_hashh: %d\n", | |
| ret); | |
| return ret; | |
| } | |
| ret = oa_tc6_write_register(priv->tc6, LAN865X_REG_MAC_L_HASH, 0); | |
| if (ret) | |
| netdev_err(priv->netdev, "Failed to write reg_hashl: %d\n", | |
| ret); | |
| return ret; | |
| } | |
| static void lan865x_multicast_work_handler(struct work_struct *work) | |
| { | |
| struct lan865x_priv *priv = container_of(work, struct lan865x_priv, | |
| multicast_work); | |
| u32 regval = 0; | |
| int ret; | |
| if (priv->netdev->flags & IFF_PROMISC) { | |
| /* Enabling promiscuous mode */ | |
| regval |= MAC_NET_CFG_PROMISCUOUS_MODE; | |
| regval &= (~MAC_NET_CFG_MULTICAST_MODE); | |
| regval &= (~MAC_NET_CFG_UNICAST_MODE); | |
| } else if (priv->netdev->flags & IFF_ALLMULTI) { | |
| /* Enabling all multicast mode */ | |
| if (lan865x_set_all_multicast_addr(priv)) | |
| return; | |
| regval &= (~MAC_NET_CFG_PROMISCUOUS_MODE); | |
| regval |= MAC_NET_CFG_MULTICAST_MODE; | |
| regval &= (~MAC_NET_CFG_UNICAST_MODE); | |
| } else if (!netdev_mc_empty(priv->netdev)) { | |
| /* Enabling specific multicast mode */ | |
| if (lan865x_set_specific_multicast_addr(priv)) | |
| return; | |
| regval &= (~MAC_NET_CFG_PROMISCUOUS_MODE); | |
| regval |= MAC_NET_CFG_MULTICAST_MODE; | |
| regval &= (~MAC_NET_CFG_UNICAST_MODE); | |
| } else { | |
| /* Enabling local mac address only */ | |
| if (lan865x_clear_all_multicast_addr(priv)) | |
| return; | |
| } | |
| ret = oa_tc6_write_register(priv->tc6, LAN865X_REG_MAC_NET_CFG, regval); | |
| if (ret) | |
| netdev_err(priv->netdev, "Failed to enable promiscuous/multicast/normal mode: %d\n", | |
| ret); | |
| } | |
| static void lan865x_set_multicast_list(struct net_device *netdev) | |
| { | |
| struct lan865x_priv *priv = netdev_priv(netdev); | |
| schedule_work(&priv->multicast_work); | |
| } | |
| static netdev_tx_t lan865x_send_packet(struct sk_buff *skb, | |
| struct net_device *netdev) | |
| { | |
| struct lan865x_priv *priv = netdev_priv(netdev); | |
| return oa_tc6_start_xmit(priv->tc6, skb); | |
| } | |
| static int lan865x_hw_disable(struct lan865x_priv *priv) | |
| { | |
| u32 regval; | |
| if (oa_tc6_read_register(priv->tc6, LAN865X_REG_MAC_NET_CTL, ®val)) | |
| return -ENODEV; | |
| regval &= ~(MAC_NET_CTL_TXEN | MAC_NET_CTL_RXEN); | |
| if (oa_tc6_write_register(priv->tc6, LAN865X_REG_MAC_NET_CTL, regval)) | |
| return -ENODEV; | |
| return 0; | |
| } | |
| static int lan865x_net_close(struct net_device *netdev) | |
| { | |
| struct lan865x_priv *priv = netdev_priv(netdev); | |
| int ret; | |
| netif_stop_queue(netdev); | |
| phy_stop(netdev->phydev); | |
| ret = lan865x_hw_disable(priv); | |
| if (ret) { | |
| netdev_err(netdev, "Failed to disable the hardware: %d\n", ret); | |
| return ret; | |
| } | |
| return 0; | |
| } | |
| static int lan865x_hw_enable(struct lan865x_priv *priv) | |
| { | |
| u32 regval; | |
| if (oa_tc6_read_register(priv->tc6, LAN865X_REG_MAC_NET_CTL, ®val)) | |
| return -ENODEV; | |
| regval |= MAC_NET_CTL_TXEN | MAC_NET_CTL_RXEN; | |
| if (oa_tc6_write_register(priv->tc6, LAN865X_REG_MAC_NET_CTL, regval)) | |
| return -ENODEV; | |
| return 0; | |
| } | |
| static int lan865x_net_open(struct net_device *netdev) | |
| { | |
| struct lan865x_priv *priv = netdev_priv(netdev); | |
| int ret; | |
| ret = lan865x_hw_enable(priv); | |
| if (ret) { | |
| netdev_err(netdev, "Failed to enable hardware: %d\n", ret); | |
| return ret; | |
| } | |
| phy_start(netdev->phydev); | |
| netif_start_queue(netdev); | |
| return 0; | |
| } | |
| static const struct net_device_ops lan865x_netdev_ops = { | |
| .ndo_open = lan865x_net_open, | |
| .ndo_stop = lan865x_net_close, | |
| .ndo_start_xmit = lan865x_send_packet, | |
| .ndo_set_rx_mode = lan865x_set_multicast_list, | |
| .ndo_set_mac_address = lan865x_set_mac_address, | |
| .ndo_validate_addr = eth_validate_addr, | |
| .ndo_eth_ioctl = phy_do_ioctl_running, | |
| }; | |
| static int lan865x_probe(struct spi_device *spi) | |
| { | |
| struct net_device *netdev; | |
| struct lan865x_priv *priv; | |
| int ret; | |
| netdev = alloc_etherdev(sizeof(struct lan865x_priv)); | |
| if (!netdev) | |
| return -ENOMEM; | |
| priv = netdev_priv(netdev); | |
| priv->netdev = netdev; | |
| priv->spi = spi; | |
| spi_set_drvdata(spi, priv); | |
| INIT_WORK(&priv->multicast_work, lan865x_multicast_work_handler); | |
| netdev->netdev_ops = &lan865x_netdev_ops; | |
| priv->tc6 = oa_tc6_init(spi, netdev, NULL); | |
| if (!priv->tc6) { | |
| ret = -ENODEV; | |
| goto free_netdev; | |
| } | |
| /* LAN865x Rev.B0/B1 configuration parameters from AN1760 | |
| * As per the Configuration Application Note AN1760 published in the | |
| * link, https://www.microchip.com/en-us/application-notes/an1760 | |
| * Revision F (DS60001760G - June 2024), configure the MAC to set time | |
| * stamping at the end of the Start of Frame Delimiter (SFD) and set the | |
| * Timer Increment reg to 40 ns to be used as a 25 MHz internal clock. | |
| */ | |
| ret = oa_tc6_write_register(priv->tc6, LAN865X_REG_MAC_TSU_TIMER_INCR, | |
| MAC_TSU_TIMER_INCR_COUNT_NANOSECONDS); | |
| if (ret) { | |
| dev_err(&spi->dev, "Failed to config TSU Timer Incr reg: %d\n", | |
| ret); | |
| goto oa_tc6_exit; | |
| } | |
| /* As per the point s3 in the below errata, SPI receive Ethernet frame | |
| * transfer may halt when starting the next frame in the same data block | |
| * (chunk) as the end of a previous frame. The RFA field should be | |
| * configured to 01b or 10b for proper operation. In these modes, only | |
| * one receive Ethernet frame will be placed in a single data block. | |
| * When the RFA field is written to 01b, received frames will be forced | |
| * to only start in the first word of the data block payload (SWO=0). As | |
| * recommended, enable zero align receive frame feature for proper | |
| * operation. | |
| * | |
| * https://ww1.microchip.com/downloads/aemDocuments/documents/AIS/ProductDocuments/Errata/LAN8650-1-Errata-80001075.pdf | |
| */ | |
| ret = oa_tc6_zero_align_receive_frame_enable(priv->tc6); | |
| if (ret) { | |
| dev_err(&spi->dev, "Failed to set ZARFE: %d\n", ret); | |
| goto oa_tc6_exit; | |
| } | |
| /* Get the MAC address from the SPI device tree node */ | |
| if (device_get_ethdev_address(&spi->dev, netdev)) | |
| eth_hw_addr_random(netdev); | |
| ret = lan865x_set_hw_macaddr(priv, netdev->dev_addr); | |
| if (ret) { | |
| dev_err(&spi->dev, "Failed to configure MAC: %d\n", ret); | |
| goto oa_tc6_exit; | |
| } | |
| netdev->if_port = IF_PORT_10BASET; | |
| netdev->irq = spi->irq; | |
| netdev->ethtool_ops = &lan865x_ethtool_ops; | |
| ret = register_netdev(netdev); | |
| if (ret) { | |
| dev_err(&spi->dev, "Register netdev failed (ret = %d)", ret); | |
| goto oa_tc6_exit; | |
| } | |
| return 0; | |
| oa_tc6_exit: | |
| oa_tc6_exit(priv->tc6); | |
| free_netdev: | |
| free_netdev(priv->netdev); | |
| return ret; | |
| } | |
| static void lan865x_remove(struct spi_device *spi) | |
| { | |
| struct lan865x_priv *priv = spi_get_drvdata(spi); | |
| cancel_work_sync(&priv->multicast_work); | |
| unregister_netdev(priv->netdev); | |
| oa_tc6_exit(priv->tc6); | |
| free_netdev(priv->netdev); | |
| } | |
| static const struct spi_device_id lan865x_ids[] = { | |
| { .name = "lan8650" }, | |
| { .name = "lan8651" }, | |
| {}, | |
| }; | |
| MODULE_DEVICE_TABLE(spi, lan865x_ids); | |
| static const struct of_device_id lan865x_dt_ids[] = { | |
| { .compatible = "microchip,lan8650" }, | |
| { .compatible = "microchip,lan8651" }, | |
| { /* Sentinel */ } | |
| }; | |
| MODULE_DEVICE_TABLE(of, lan865x_dt_ids); | |
| static struct spi_driver lan865x_driver = { | |
| .driver = { | |
| .name = DRV_NAME, | |
| .of_match_table = lan865x_dt_ids, | |
| }, | |
| .probe = lan865x_probe, | |
| .remove = lan865x_remove, | |
| .id_table = lan865x_ids, | |
| }; | |
| module_spi_driver(lan865x_driver); | |
| MODULE_DESCRIPTION(DRV_NAME " 10Base-T1S MACPHY Ethernet Driver"); | |
| MODULE_AUTHOR("Parthiban Veerasooran <parthiban.veerasooran@microchip.com>"); | |
| MODULE_LICENSE("GPL"); |