mirror of
https://github.com/torvalds/linux.git
synced 2026-10-17 06:20:02 +02:00
adin1140_probe() calls adin1140_phy_init(), which connects the PHY, before netdev->netdev_ops is assigned. phy_attach_direct() -> phy_link_topo_add_phy() reads dev->netdev_ops through netdev_need_ops_lock() since the commit in question, so probing crashes with a NULL pointer dereference when CONFIG_NET_SHAPER is enabled. Assign netdev_ops before the PHY attach, as was done for emac in commit7c9f391ec8("net: emac: move setting of netops to fix crash"). Fixes:ded86da4bb("net: ethtool: relax ethnl_req_get_phydev() locking assertion") Signed-off-by: David Yang <mmyangfl@gmail.com> Reviewed-by: Maxime Chevallier <maxime.chevallier@bootlin.com> Reviewed-by: Ciprian Regus <ciprian.regus@analog.com> Link: https://patch.msgid.link/20261008022656.3592191-5-mmyangfl@gmail.com Reviewed-by: Jijie Shao <shaojijie@huawei.com> Signed-off-by: Paolo Abeni <pabeni@redhat.com>
793 lines
21 KiB
C
793 lines
21 KiB
C
// SPDX-License-Identifier: GPL-2.0+
|
|
/*
|
|
* Driver for Analog Devices, Inc. ADIN1140 10BASE-T1S MAC-PHY
|
|
*
|
|
* Copyright 2026 Analog Devices Inc.
|
|
*/
|
|
|
|
#include <linux/cleanup.h>
|
|
#include <linux/etherdevice.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/mdio.h>
|
|
#include <linux/module.h>
|
|
#include <linux/oa_tc6.h>
|
|
#include <linux/phy.h>
|
|
|
|
#define ADIN1140_MAC_CONFIG2_STAT_CLR_RD BIT(6)
|
|
#define ADIN1140_MAC_CONFIG2_FWD_UNK2HOST BIT(2)
|
|
|
|
#define ADIN1140_MAC_P1_LOOP_ADDR_REG 0xC4
|
|
|
|
#define ADIN1140_MAC_ADDR_FILT_UPR_REG 0x50
|
|
#define ADIN1140_MAC_ADDR_FILT_APPLY2PORT1 BIT(30)
|
|
#define ADIN1140_MAC_ADDR_FILT_TO_HOST BIT(16)
|
|
|
|
#define ADIN1140_MAC_ADDR_FILT_LWR_REG 0x51
|
|
|
|
#define ADIN1140_MAC_ADDR_MASK_UPR_REG 0x70
|
|
#define ADIN1140_MAC_ADDR_MASK_LWR_REG 0x71
|
|
|
|
#define ADIN1140_MAC_FILT_MC_SLOT 0U
|
|
#define ADIN1140_MAC_FILT_BC_SLOT 1U
|
|
#define ADIN1140_MAC_FILT_UC_SLOT 2U
|
|
#define ADIN1140_MAC_FILT_MAX_SLOT 16U
|
|
#define ADIN1140_MAC_FILT_MASK_LIMIT 2U
|
|
|
|
#define ADIN1140_MAC_RX_FRAME_CNT 0xA1
|
|
#define ADIN1140_MAC_RX_BC_FRAME_CNT 0xA2
|
|
#define ADIN1140_MAC_RX_MC_FRAME_CNT 0xA3
|
|
#define ADIN1140_MAC_RX_CRC_ERR_CNT 0xA5
|
|
#define ADIN1140_MAC_RX_ALIGN_ERR_CNT 0xA6
|
|
#define ADIN1140_MAC_RX_PREAMBLE_ERR_CNT 0xA7
|
|
#define ADIN1140_MAC_RX_SHORT_ERR_CNT 0xA8
|
|
#define ADIN1140_MAC_RX_LONG_ERR_CNT 0xA9
|
|
#define ADIN1140_MAC_RX_PHY_ERR_CNT 0xAA
|
|
#define ADIN1140_MAC_RX_DRP_FULL_CNT 0xAB
|
|
#define ADIN1140_MAC_RX_DRP_FILTER_CNT 0xAD
|
|
#define ADIN1140_MAC_RX_IFG_ERR_CNT 0xAE
|
|
#define ADIN1140_MAC_TX_FRAME_CNT 0xB1
|
|
#define ADIN1140_MAC_TX_BC_FRAME_CNT 0xB2
|
|
#define ADIN1140_MAC_TX_MC_FRAME_CNT 0xB3
|
|
#define ADIN1140_MAC_TX_SINGLE_COL_CNT 0xB5
|
|
#define ADIN1140_MAC_TX_MULTI_COL_CNT 0xB6
|
|
#define ADIN1140_MAC_TX_DEFERRED_CNT 0xB7
|
|
#define ADIN1140_MAC_TX_LATE_COL_CNT 0xB8
|
|
#define ADIN1140_MAC_TX_EXCESS_COL_CNT 0xB9
|
|
#define ADIN1140_MAC_TX_UNDERRUN_CNT 0xBA
|
|
|
|
/* ADIN1140_MAC_FILT_MAX_SLOT - 3 (multicast, broadcast and unicast
|
|
* reserved slots)
|
|
*/
|
|
#define ADIN1140_MAC_FILT_AVAIL 13U
|
|
|
|
#define ADIN1140_PHY_CTRL_DEFAULT 0x1000
|
|
#define ADIN1140_PHY_STATUS_DEFAULT 0x082D
|
|
#define ADIN1140_PHY_ID1 0x0283
|
|
#define ADIN1140_PHY_ID2 0xBE00
|
|
|
|
#define ADIN1140_STATS_CHECK_DELAY (3 * HZ)
|
|
|
|
enum adin1140_statistics_entry {
|
|
rx_frames,
|
|
rx_bc_frames,
|
|
rx_mc_frames,
|
|
rx_crc_errors,
|
|
rx_align_errors,
|
|
rx_preamble_errors,
|
|
rx_short_frame_errors,
|
|
rx_long_frame_errors,
|
|
rx_phy_errors,
|
|
rx_fifo_full_dropped,
|
|
rx_addr_filter_dropped,
|
|
rx_ifg_errors,
|
|
tx_frames,
|
|
tx_bc_frames,
|
|
tx_mc_frames,
|
|
tx_single_collision,
|
|
tx_multi_collision,
|
|
tx_deferred,
|
|
tx_late_collision,
|
|
tx_excess_collision,
|
|
tx_underrun,
|
|
ADIN1140_STATS_CNT,
|
|
};
|
|
|
|
struct adin1140_statistics_reg {
|
|
const char *name;
|
|
enum adin1140_statistics_entry idx;
|
|
};
|
|
|
|
struct adin1140_priv {
|
|
struct net_device *netdev;
|
|
struct oa_tc6 *tc6;
|
|
struct mii_bus *mdiobus;
|
|
struct phy_device *phydev;
|
|
struct delayed_work stats_work;
|
|
|
|
/* Protects stats[] from concurrent updates in adin1140_stats_work
|
|
* and reads in the get_stats functions
|
|
*/
|
|
spinlock_t stat_lock;
|
|
u64 stats[ADIN1140_STATS_CNT];
|
|
};
|
|
|
|
static const u32 adin1140_stat_regs[] = {
|
|
[rx_frames] = ADIN1140_MAC_RX_FRAME_CNT,
|
|
[rx_bc_frames] = ADIN1140_MAC_RX_BC_FRAME_CNT,
|
|
[rx_mc_frames] = ADIN1140_MAC_RX_MC_FRAME_CNT,
|
|
[rx_crc_errors] = ADIN1140_MAC_RX_CRC_ERR_CNT,
|
|
[rx_align_errors] = ADIN1140_MAC_RX_ALIGN_ERR_CNT,
|
|
[rx_preamble_errors] = ADIN1140_MAC_RX_PREAMBLE_ERR_CNT,
|
|
[rx_short_frame_errors] = ADIN1140_MAC_RX_SHORT_ERR_CNT,
|
|
[rx_long_frame_errors] = ADIN1140_MAC_RX_LONG_ERR_CNT,
|
|
[rx_phy_errors] = ADIN1140_MAC_RX_PHY_ERR_CNT,
|
|
[rx_fifo_full_dropped] = ADIN1140_MAC_RX_DRP_FULL_CNT,
|
|
[rx_addr_filter_dropped] = ADIN1140_MAC_RX_DRP_FILTER_CNT,
|
|
[rx_ifg_errors] = ADIN1140_MAC_RX_IFG_ERR_CNT,
|
|
[tx_frames] = ADIN1140_MAC_TX_FRAME_CNT,
|
|
[tx_bc_frames] = ADIN1140_MAC_TX_BC_FRAME_CNT,
|
|
[tx_mc_frames] = ADIN1140_MAC_TX_MC_FRAME_CNT,
|
|
[tx_single_collision] = ADIN1140_MAC_TX_SINGLE_COL_CNT,
|
|
[tx_multi_collision] = ADIN1140_MAC_TX_MULTI_COL_CNT,
|
|
[tx_deferred] = ADIN1140_MAC_TX_DEFERRED_CNT,
|
|
[tx_late_collision] = ADIN1140_MAC_TX_LATE_COL_CNT,
|
|
[tx_excess_collision] = ADIN1140_MAC_TX_EXCESS_COL_CNT,
|
|
[tx_underrun] = ADIN1140_MAC_TX_UNDERRUN_CNT,
|
|
};
|
|
|
|
static const struct adin1140_statistics_reg adin1140_stats[] = {
|
|
{.name = "rx_preamble_errors", .idx = rx_preamble_errors},
|
|
{.name = "rx_ifg_errors", .idx = rx_ifg_errors},
|
|
};
|
|
|
|
static int adin1140_mac_filter_set(struct adin1140_priv *priv,
|
|
const u8 *addr, const u8 *mask,
|
|
u8 slot)
|
|
{
|
|
u32 reg_address;
|
|
u32 val;
|
|
int ret;
|
|
|
|
if (slot >= ADIN1140_MAC_FILT_MAX_SLOT)
|
|
return -ENOSPC;
|
|
|
|
reg_address = ADIN1140_MAC_ADDR_FILT_UPR_REG + 2 * slot;
|
|
|
|
ret = oa_tc6_write_register_mms(priv->tc6, OA_TC6_MAC_MMS1,
|
|
reg_address,
|
|
get_unaligned_be16(&addr[0]) |
|
|
ADIN1140_MAC_ADDR_FILT_APPLY2PORT1 |
|
|
ADIN1140_MAC_ADDR_FILT_TO_HOST);
|
|
if (ret)
|
|
return ret;
|
|
|
|
reg_address = ADIN1140_MAC_ADDR_FILT_LWR_REG + 2 * slot;
|
|
ret = oa_tc6_write_register_mms(priv->tc6, OA_TC6_MAC_MMS1,
|
|
reg_address,
|
|
get_unaligned_be32(&addr[2]));
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* Only the first 2 destination MAC filter slots support masking.
|
|
* For the other entries, the destination address in the received
|
|
* frame must match exactly.
|
|
*/
|
|
if (slot >= ADIN1140_MAC_FILT_MASK_LIMIT)
|
|
return 0;
|
|
|
|
val = get_unaligned_be16(&mask[0]);
|
|
reg_address = ADIN1140_MAC_ADDR_MASK_UPR_REG + (2 * slot);
|
|
|
|
ret = oa_tc6_write_register_mms(priv->tc6, OA_TC6_MAC_MMS1,
|
|
reg_address, val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
val = get_unaligned_be32(&mask[2]);
|
|
reg_address = ADIN1140_MAC_ADDR_MASK_LWR_REG + (2 * slot);
|
|
|
|
return oa_tc6_write_register_mms(priv->tc6, OA_TC6_MAC_MMS1,
|
|
reg_address, val);
|
|
}
|
|
|
|
static int adin1140_mac_filter_clear(struct adin1140_priv *priv, u8 slot)
|
|
{
|
|
u8 mask[ETH_ALEN];
|
|
u8 addr[ETH_ALEN];
|
|
|
|
memset(mask, 0xFF, ETH_ALEN);
|
|
memset(addr, 0x0, ETH_ALEN);
|
|
|
|
return adin1140_mac_filter_set(priv, addr, mask, slot);
|
|
}
|
|
|
|
static int adin1140_filter_unicast(struct adin1140_priv *priv)
|
|
{
|
|
/* Only the first 2 filter slots support masking, so no unicast
|
|
* address will ever need a mask. The first slots are used for the
|
|
* all multicast and broadcast filter.
|
|
*/
|
|
return adin1140_mac_filter_set(priv, priv->netdev->dev_addr, NULL,
|
|
ADIN1140_MAC_FILT_UC_SLOT);
|
|
}
|
|
|
|
static int adin1140_filter_all_multicast(struct adin1140_priv *priv, bool en)
|
|
{
|
|
u8 multicast_addr[ETH_ALEN] = {1, 0, 0, 0, 0, 0};
|
|
|
|
if (en)
|
|
return adin1140_mac_filter_set(priv, multicast_addr,
|
|
multicast_addr,
|
|
ADIN1140_MAC_FILT_MC_SLOT);
|
|
|
|
return adin1140_mac_filter_clear(priv, ADIN1140_MAC_FILT_MC_SLOT);
|
|
}
|
|
|
|
static int adin1140_filter_broadcast(struct adin1140_priv *priv, bool enabled)
|
|
{
|
|
u8 mask[ETH_ALEN];
|
|
|
|
if (enabled) {
|
|
memset(mask, 0xFF, ETH_ALEN);
|
|
return adin1140_mac_filter_set(priv, mask, mask,
|
|
ADIN1140_MAC_FILT_BC_SLOT);
|
|
}
|
|
|
|
return adin1140_mac_filter_clear(priv, ADIN1140_MAC_FILT_BC_SLOT);
|
|
}
|
|
|
|
static int adin1140_default_filter_config(struct adin1140_priv *priv)
|
|
{
|
|
int ret;
|
|
|
|
ret = adin1140_filter_broadcast(priv, true);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return adin1140_filter_unicast(priv);
|
|
}
|
|
|
|
static int adin1140_promiscuous_mode(struct adin1140_priv *priv, bool enabled)
|
|
{
|
|
int ret;
|
|
u32 val;
|
|
|
|
ret = oa_tc6_read_register(priv->tc6, OA_TC6_REG_CONFIG2, &val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (enabled)
|
|
val |= ADIN1140_MAC_CONFIG2_FWD_UNK2HOST;
|
|
else
|
|
val &= ~ADIN1140_MAC_CONFIG2_FWD_UNK2HOST;
|
|
|
|
return oa_tc6_write_register(priv->tc6, OA_TC6_REG_CONFIG2, val);
|
|
}
|
|
|
|
static int adin1140_rx_mode(struct net_device *dev,
|
|
struct netdev_hw_addr_list *uc,
|
|
struct netdev_hw_addr_list *mc)
|
|
{
|
|
struct adin1140_priv *priv = netdev_priv(dev);
|
|
struct netdev_hw_addr *ha;
|
|
bool all_multi, promisc;
|
|
u32 mac_addrs;
|
|
u8 slot, i;
|
|
int ret;
|
|
|
|
/* The ADIN1140 has 16 dest MAC address filter slots:
|
|
* 0 - reserved for all multicast filter.
|
|
* 1 - reserved for broadcast filter.
|
|
* 2 - reserved for the device's own unicast MAC.
|
|
* 3 -> 15 - available for other unicast/multicast filters.
|
|
*/
|
|
|
|
mac_addrs = netdev_hw_addr_list_count(uc);
|
|
all_multi = false;
|
|
promisc = false;
|
|
|
|
if (priv->netdev->flags & IFF_PROMISC)
|
|
promisc = true;
|
|
else if (priv->netdev->flags & IFF_ALLMULTI)
|
|
all_multi = true;
|
|
else
|
|
mac_addrs += netdev_hw_addr_list_count(mc);
|
|
|
|
if (mac_addrs > ADIN1140_MAC_FILT_AVAIL) {
|
|
/* The filter table is full. Enable promisc mode. */
|
|
promisc = true;
|
|
}
|
|
|
|
ret = adin1140_promiscuous_mode(priv, promisc);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = adin1140_filter_all_multicast(priv, all_multi);
|
|
if (ret)
|
|
return ret;
|
|
|
|
slot = ADIN1140_MAC_FILT_UC_SLOT + 1;
|
|
if (!promisc) {
|
|
netdev_hw_addr_list_for_each(ha, uc) {
|
|
ret = adin1140_mac_filter_set(priv, ha->addr, NULL,
|
|
slot);
|
|
if (ret)
|
|
return ret;
|
|
|
|
slot++;
|
|
}
|
|
|
|
if (!all_multi) {
|
|
netdev_hw_addr_list_for_each(ha, mc) {
|
|
ret = adin1140_mac_filter_set(priv, ha->addr,
|
|
NULL, slot);
|
|
if (ret)
|
|
return ret;
|
|
|
|
slot++;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i = slot; i < ADIN1140_MAC_FILT_MAX_SLOT; i++) {
|
|
ret = adin1140_mac_filter_clear(priv, i);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void adin1140_stats_work(struct work_struct *work)
|
|
{
|
|
struct delayed_work *dwork = to_delayed_work(work);
|
|
struct adin1140_priv *priv;
|
|
u32 reg_val;
|
|
int ret;
|
|
u32 i;
|
|
|
|
priv = container_of(dwork, struct adin1140_priv, stats_work);
|
|
|
|
for (i = 0; i < ARRAY_SIZE(adin1140_stat_regs); i++) {
|
|
ret = oa_tc6_read_register_mms(priv->tc6, OA_TC6_MAC_MMS1,
|
|
adin1140_stat_regs[i],
|
|
®_val);
|
|
if (ret)
|
|
goto out;
|
|
|
|
scoped_guard(spinlock, &priv->stat_lock)
|
|
priv->stats[i] += reg_val;
|
|
}
|
|
|
|
out:
|
|
schedule_delayed_work(dwork, ADIN1140_STATS_CHECK_DELAY);
|
|
}
|
|
|
|
static int adin1140_configure(struct adin1140_priv *priv)
|
|
{
|
|
u32 reg_val;
|
|
int ret;
|
|
|
|
ret = oa_tc6_zero_align_receive_frame_enable(priv->tc6);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* Disable MAC loopback */
|
|
ret = oa_tc6_write_register_mms(priv->tc6, OA_TC6_MAC_MMS1,
|
|
ADIN1140_MAC_P1_LOOP_ADDR_REG, 0x0);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = oa_tc6_read_register(priv->tc6, OA_TC6_REG_CONFIG2, ®_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
reg_val |= ADIN1140_MAC_CONFIG2_STAT_CLR_RD;
|
|
|
|
ret = oa_tc6_write_register(priv->tc6, OA_TC6_REG_CONFIG2, reg_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return adin1140_default_filter_config(priv);
|
|
}
|
|
|
|
static int adin1140_open(struct net_device *netdev)
|
|
{
|
|
struct adin1140_priv *priv = netdev_priv(netdev);
|
|
|
|
schedule_delayed_work(&priv->stats_work, ADIN1140_STATS_CHECK_DELAY);
|
|
|
|
phy_start(netdev->phydev);
|
|
netif_start_queue(netdev);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int adin1140_close(struct net_device *netdev)
|
|
{
|
|
struct adin1140_priv *priv = netdev_priv(netdev);
|
|
|
|
cancel_delayed_work_sync(&priv->stats_work);
|
|
|
|
netif_stop_queue(netdev);
|
|
phy_stop(netdev->phydev);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static netdev_tx_t adin1140_start_xmit(struct sk_buff *skb,
|
|
struct net_device *netdev)
|
|
{
|
|
struct adin1140_priv *priv = netdev_priv(netdev);
|
|
|
|
/* The MAC doesn't automatically pad the frame to a 60 byte minimum
|
|
* size in case the host sends a shorter skb, so we have to do it in
|
|
* the driver. The FCS will be added by the MAC.
|
|
*/
|
|
if (skb_put_padto(skb, ETH_ZLEN))
|
|
return NETDEV_TX_OK;
|
|
|
|
return oa_tc6_start_xmit(priv->tc6, skb);
|
|
}
|
|
|
|
static int adin1140_set_mac_address(struct net_device *netdev, void *addr)
|
|
{
|
|
struct adin1140_priv *priv = netdev_priv(netdev);
|
|
struct sockaddr *address = addr;
|
|
u8 mask[ETH_ALEN];
|
|
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;
|
|
|
|
memset(mask, 0xFF, ETH_ALEN);
|
|
ret = adin1140_mac_filter_set(priv, address->sa_data, mask,
|
|
ADIN1140_MAC_FILT_UC_SLOT);
|
|
if (ret)
|
|
return ret;
|
|
|
|
eth_commit_mac_addr_change(netdev, addr);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void __adin1140_ndo_get_stats64(struct adin1140_priv *priv,
|
|
struct rtnl_link_stats64 *storage)
|
|
{
|
|
storage->rx_errors = priv->stats[rx_crc_errors] +
|
|
priv->stats[rx_align_errors] +
|
|
priv->stats[rx_preamble_errors] +
|
|
priv->stats[rx_short_frame_errors] +
|
|
priv->stats[rx_long_frame_errors] +
|
|
priv->stats[rx_phy_errors] +
|
|
priv->stats[rx_ifg_errors];
|
|
|
|
storage->tx_errors = priv->stats[tx_excess_collision] +
|
|
priv->stats[tx_underrun];
|
|
|
|
storage->rx_dropped = priv->stats[rx_fifo_full_dropped] +
|
|
priv->stats[rx_addr_filter_dropped];
|
|
|
|
storage->multicast = priv->stats[rx_mc_frames];
|
|
|
|
storage->collisions = priv->stats[tx_single_collision] +
|
|
priv->stats[tx_multi_collision];
|
|
|
|
storage->rx_length_errors = priv->stats[rx_short_frame_errors] +
|
|
priv->stats[rx_long_frame_errors];
|
|
storage->rx_over_errors = priv->stats[rx_fifo_full_dropped];
|
|
storage->rx_crc_errors = priv->stats[rx_crc_errors];
|
|
storage->rx_frame_errors = priv->stats[rx_align_errors];
|
|
storage->rx_missed_errors = priv->stats[rx_fifo_full_dropped];
|
|
|
|
storage->tx_aborted_errors = priv->stats[tx_excess_collision];
|
|
storage->tx_fifo_errors = priv->stats[tx_underrun];
|
|
storage->tx_window_errors = priv->stats[tx_late_collision];
|
|
}
|
|
|
|
static void adin1140_ndo_get_stats64(struct net_device *dev,
|
|
struct rtnl_link_stats64 *storage)
|
|
{
|
|
struct adin1140_priv *priv = netdev_priv(dev);
|
|
|
|
storage->rx_packets = priv->netdev->stats.rx_packets;
|
|
storage->tx_packets = priv->netdev->stats.tx_packets;
|
|
|
|
storage->rx_bytes = priv->netdev->stats.rx_bytes;
|
|
storage->tx_bytes = priv->netdev->stats.tx_bytes;
|
|
|
|
scoped_guard(spinlock, &priv->stat_lock)
|
|
__adin1140_ndo_get_stats64(priv, storage);
|
|
}
|
|
|
|
static void adin1140_get_drvinfo(struct net_device *netdev,
|
|
struct ethtool_drvinfo *info)
|
|
{
|
|
strscpy(info->driver, "ADIN1140", sizeof(info->driver));
|
|
strscpy(info->bus_info, dev_name(netdev->dev.parent),
|
|
sizeof(info->bus_info));
|
|
}
|
|
|
|
static void adin1140_get_ethtool_stats(struct net_device *netdev,
|
|
struct ethtool_stats *stats, u64 *data)
|
|
{
|
|
struct adin1140_priv *priv = netdev_priv(netdev);
|
|
u32 i;
|
|
|
|
scoped_guard(spinlock, &priv->stat_lock) {
|
|
for (i = 0; i < ARRAY_SIZE(adin1140_stats); i++)
|
|
data[i] = priv->stats[adin1140_stats[i].idx];
|
|
}
|
|
}
|
|
|
|
static void adin1140_get_ethtool_strings(struct net_device *netdev, u32 sset,
|
|
u8 *p)
|
|
{
|
|
u32 i;
|
|
|
|
switch (sset) {
|
|
case ETH_SS_STATS:
|
|
for (i = 0; i < ARRAY_SIZE(adin1140_stats); i++)
|
|
ethtool_puts(&p, adin1140_stats[i].name);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int adin1140_get_sset_count(struct net_device *netdev, int sset)
|
|
{
|
|
switch (sset) {
|
|
case ETH_SS_STATS:
|
|
return ARRAY_SIZE(adin1140_stats);
|
|
default:
|
|
return -EOPNOTSUPP;
|
|
}
|
|
}
|
|
|
|
static void __adin1140_eth_mac_stats(struct adin1140_priv *priv,
|
|
struct ethtool_eth_mac_stats *mac_stats)
|
|
{
|
|
mac_stats->FramesReceivedOK = priv->stats[rx_frames];
|
|
mac_stats->BroadcastFramesReceivedOK = priv->stats[rx_bc_frames];
|
|
mac_stats->MulticastFramesReceivedOK = priv->stats[rx_mc_frames];
|
|
mac_stats->FrameCheckSequenceErrors = priv->stats[rx_crc_errors];
|
|
mac_stats->AlignmentErrors = priv->stats[rx_align_errors];
|
|
mac_stats->FrameTooLongErrors = priv->stats[rx_long_frame_errors];
|
|
mac_stats->FramesLostDueToIntMACRcvError =
|
|
priv->stats[rx_fifo_full_dropped];
|
|
mac_stats->FramesTransmittedOK = priv->stats[tx_frames];
|
|
mac_stats->BroadcastFramesXmittedOK = priv->stats[tx_bc_frames];
|
|
mac_stats->MulticastFramesXmittedOK = priv->stats[tx_mc_frames];
|
|
mac_stats->SingleCollisionFrames = priv->stats[tx_single_collision];
|
|
mac_stats->MultipleCollisionFrames = priv->stats[tx_multi_collision];
|
|
mac_stats->FramesWithDeferredXmissions = priv->stats[tx_deferred];
|
|
mac_stats->LateCollisions = priv->stats[tx_late_collision];
|
|
mac_stats->FramesAbortedDueToXSColls =
|
|
priv->stats[tx_excess_collision];
|
|
mac_stats->FramesLostDueToIntMACXmitError = priv->stats[tx_underrun];
|
|
}
|
|
|
|
static void adin1140_get_eth_mac_stats(struct net_device *netdev,
|
|
struct ethtool_eth_mac_stats *mac_stats)
|
|
{
|
|
struct adin1140_priv *priv = netdev_priv(netdev);
|
|
|
|
scoped_guard(spinlock, &priv->stat_lock)
|
|
__adin1140_eth_mac_stats(priv, mac_stats);
|
|
}
|
|
|
|
static int adin1140_mdiobus_read(struct mii_bus *bus, int addr, int regnum)
|
|
{
|
|
/* The ADIN1140's standard PHY C22 register map (OA TC6 0xFF00 -
|
|
* 0xFF1F), of which only 0xFF00 - 0xFF03 are implemented) cannot be
|
|
* accessed while frames are being received by the PHY. In case this
|
|
* happens the CONFIG0 and CONFIG2 register values will get corrupted,
|
|
* getting a random value. Both reads and writes cause the same
|
|
* behavior. This is a workaround that avoids MDIO accesses all
|
|
* together. Since this is a 10BASE-T1S PHY, only the loopback and
|
|
* reset (AN) bits in the control register (0x0) can be written.
|
|
* These functionalities have custom implementations in the PHY
|
|
* driver. C45 accesses do not cause this issue.
|
|
*/
|
|
|
|
switch (regnum) {
|
|
case MII_BMCR:
|
|
return ADIN1140_PHY_CTRL_DEFAULT;
|
|
case MII_BMSR:
|
|
return ADIN1140_PHY_STATUS_DEFAULT;
|
|
case MII_PHYSID1:
|
|
return ADIN1140_PHY_ID1;
|
|
case MII_PHYSID2:
|
|
return ADIN1140_PHY_ID2;
|
|
default:
|
|
return 0xFFFF;
|
|
}
|
|
}
|
|
|
|
static int adin1140_mdiobus_write(struct mii_bus *bus, int addr, int regnum,
|
|
u16 val)
|
|
{
|
|
return -EIO;
|
|
}
|
|
|
|
static int adin1140_mdio_register(struct adin1140_priv *priv,
|
|
struct spi_device *spidev)
|
|
{
|
|
priv->mdiobus = devm_mdiobus_alloc(&spidev->dev);
|
|
if (!priv->mdiobus)
|
|
return dev_err_probe(&spidev->dev, -ENOMEM,
|
|
"MDIO bus alloc failed\n");
|
|
|
|
priv->mdiobus->name = "adin1140-mdiobus";
|
|
priv->mdiobus->priv = priv->tc6;
|
|
priv->mdiobus->parent = &spidev->dev;
|
|
priv->mdiobus->phy_mask = GENMASK(31, 1);
|
|
priv->mdiobus->read = adin1140_mdiobus_read;
|
|
priv->mdiobus->write = adin1140_mdiobus_write;
|
|
priv->mdiobus->read_c45 = oa_tc6_mdiobus_read_c45;
|
|
priv->mdiobus->write_c45 = oa_tc6_mdiobus_write_c45;
|
|
|
|
snprintf(priv->mdiobus->id, MII_BUS_ID_SIZE, "adin1140-%s.%u",
|
|
dev_name(&spidev->dev), spi_get_chipselect(spidev, 0));
|
|
|
|
return devm_mdiobus_register(&spidev->dev, priv->mdiobus);
|
|
}
|
|
|
|
static void adin1140_handle_link_change(struct net_device *netdev)
|
|
{
|
|
phy_print_status(netdev->phydev);
|
|
}
|
|
|
|
static void adin1140_phy_remove(void *data)
|
|
{
|
|
phy_disconnect(data);
|
|
}
|
|
|
|
static int adin1140_phy_init(struct adin1140_priv *priv,
|
|
struct spi_device *spidev)
|
|
{
|
|
int ret;
|
|
|
|
ret = adin1140_mdio_register(priv, spidev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
priv->phydev = phy_find_first(priv->mdiobus);
|
|
if (!priv->phydev)
|
|
return dev_err_probe(&spidev->dev, -ENODEV, "No PHY found\n");
|
|
|
|
priv->phydev->is_internal = true;
|
|
ret = phy_connect_direct(priv->netdev, priv->phydev,
|
|
&adin1140_handle_link_change,
|
|
PHY_INTERFACE_MODE_INTERNAL);
|
|
if (ret)
|
|
return dev_err_probe(&spidev->dev, ret,
|
|
"Can't attach PHY to %s\n",
|
|
priv->mdiobus->id);
|
|
|
|
ret = devm_add_action_or_reset(&spidev->dev, adin1140_phy_remove,
|
|
priv->phydev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
phy_attached_info(priv->phydev);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct ethtool_ops adin1140_ethtool_ops = {
|
|
.get_drvinfo = adin1140_get_drvinfo,
|
|
.get_link = ethtool_op_get_link,
|
|
.get_ethtool_stats = adin1140_get_ethtool_stats,
|
|
.get_sset_count = adin1140_get_sset_count,
|
|
.get_strings = adin1140_get_ethtool_strings,
|
|
.get_link_ksettings = phy_ethtool_get_link_ksettings,
|
|
.set_link_ksettings = phy_ethtool_set_link_ksettings,
|
|
.get_eth_mac_stats = adin1140_get_eth_mac_stats,
|
|
};
|
|
|
|
static const struct net_device_ops adin1140_netdev_ops = {
|
|
.ndo_open = adin1140_open,
|
|
.ndo_stop = adin1140_close,
|
|
.ndo_start_xmit = adin1140_start_xmit,
|
|
.ndo_set_mac_address = adin1140_set_mac_address,
|
|
.ndo_validate_addr = eth_validate_addr,
|
|
.ndo_set_rx_mode_async = adin1140_rx_mode,
|
|
.ndo_eth_ioctl = phy_do_ioctl_running,
|
|
.ndo_get_stats64 = adin1140_ndo_get_stats64,
|
|
};
|
|
|
|
static void adin1140_oa_tc6_remove(void *data)
|
|
{
|
|
oa_tc6_exit(data);
|
|
}
|
|
|
|
static int adin1140_probe(struct spi_device *spi)
|
|
{
|
|
struct oa_tc6_quirks tc6_quirks = {};
|
|
struct net_device *netdev;
|
|
struct adin1140_priv *priv;
|
|
int ret;
|
|
|
|
netdev = devm_alloc_etherdev(&spi->dev, sizeof(struct adin1140_priv));
|
|
if (!netdev)
|
|
return -ENOMEM;
|
|
|
|
priv = netdev_priv(netdev);
|
|
priv->netdev = netdev;
|
|
spi_set_drvdata(spi, priv);
|
|
spin_lock_init(&priv->stat_lock);
|
|
|
|
tc6_quirks.quirk_flags = OA_TC6_BROKEN_PHY;
|
|
|
|
priv->tc6 = oa_tc6_init(spi, netdev, &tc6_quirks);
|
|
if (!priv->tc6)
|
|
return -ENODEV;
|
|
|
|
ret = devm_add_action_or_reset(&spi->dev, adin1140_oa_tc6_remove,
|
|
priv->tc6);
|
|
if (ret)
|
|
return ret;
|
|
|
|
netdev->netdev_ops = &adin1140_netdev_ops;
|
|
|
|
ret = adin1140_phy_init(priv, spi);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (device_get_ethdev_address(&spi->dev, netdev))
|
|
eth_hw_addr_random(netdev);
|
|
|
|
ret = adin1140_configure(priv);
|
|
if (ret)
|
|
return ret;
|
|
|
|
INIT_DELAYED_WORK(&priv->stats_work, adin1140_stats_work);
|
|
|
|
netdev->if_port = IF_PORT_10BASET;
|
|
netdev->irq = spi->irq;
|
|
netdev->ethtool_ops = &adin1140_ethtool_ops;
|
|
netdev->netns_immutable = true;
|
|
netdev->priv_flags |= IFF_LIVE_ADDR_CHANGE |
|
|
IFF_UNICAST_FLT;
|
|
|
|
ret = devm_register_netdev(&spi->dev, netdev);
|
|
if (ret)
|
|
return dev_err_probe(&spi->dev, ret,
|
|
"Failed to register netdev");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct spi_device_id adin1140_spi_id[] = {
|
|
{ .name = "ad3306" },
|
|
{ .name = "adin1140" },
|
|
{},
|
|
};
|
|
MODULE_DEVICE_TABLE(spi, adin1140_spi_id);
|
|
|
|
static const struct of_device_id adin1140_match_table[] = {
|
|
{ .compatible = "adi,ad3306" },
|
|
{ .compatible = "adi,adin1140" },
|
|
{ }
|
|
};
|
|
MODULE_DEVICE_TABLE(of, adin1140_match_table);
|
|
|
|
static struct spi_driver adin1140_driver = {
|
|
.driver = {
|
|
.name = "adin1140",
|
|
.of_match_table = adin1140_match_table,
|
|
},
|
|
.probe = adin1140_probe,
|
|
.id_table = adin1140_spi_id,
|
|
};
|
|
module_spi_driver(adin1140_driver);
|
|
|
|
MODULE_DESCRIPTION("Analog Devices, Inc. ADIN1140 10BASE-T1S MAC-PHY");
|
|
MODULE_AUTHOR("Ciprian Regus <ciprian.regus@analog.com>");
|
|
MODULE_LICENSE("GPL");
|