From: Paolo Abeni <pabeni@redhat.com>
To: ciprian.regus@analog.com
Cc: parthiban.veerasooran@microchip.com, andrew+netdev@lunn.ch,
davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
pabeni@redhat.com, horms@kernel.org, corbet@lwn.net,
skhan@linuxfoundation.org, andrew@lunn.ch, hkallweit1@gmail.com,
linux@armlinux.org.uk, robh@kernel.org, krzk+dt@kernel.org,
conor+dt@kernel.org, netdev@vger.kernel.org,
linux-kernel@vger.kernel.org, linux-doc@vger.kernel.org,
devicetree@vger.kernel.org
Subject: Re: [PATCH net-next v4 13/13] net: ethernet: adi: Add a driver for the ADIN1140 MACPHY
Date: Sat, 13 Jun 2026 11:26:45 +0200 [thread overview]
Message-ID: <20260613092645.123432-1-pabeni@redhat.com> (raw)
In-Reply-To: <20260609-adin1140-driver-v4-13-0753e28ee004@analog.com>
From: AI Reviewer <ai@example.com>
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
Full review at: https://netdev-ai.bots.linux.dev/sashiko/
---
net: ethernet: adi: Add a driver for the ADIN1140 MACPHY
This patch adds a driver for the ADIN1140 10BASE-T1S MAC-PHY which talks
to the host over SPI using the OA TC6 protocol. It introduces a custom
mii_bus to work around two hardware issues, and exposes both rtnl link
stats and ethtool MAC stats from periodically polled hardware counters.
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 50f795dfa880..c3cab8f7b9ef 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -1858,6 +1858,14 @@ S: Supported
> W: https://ez.analog.com/linux-software-drivers
> F: drivers/dma/dma-axi-dmac.c
>
> +ANALOG DEVICES INC ETHERNET DRIVERS
> +M: Ciprian Regus <ciprian.regus@analog.com>
> +L: netdev@vger.kernel.org
> +S: Maintained
> +W: https://ez.analog.com/linux-software-drivers
> +F: Documentation/devicetree/bindings/net/adi,ad3306.yaml
> +F: drivers/net/ethernet/adi/adin1140.c
[Medium]
The MAINTAINERS entry references
Documentation/devicetree/bindings/net/adi,ad3306.yaml, but that file is
not added by this patch and does not appear elsewhere in the tree.
The driver also introduces two new of compatibles:
{ .compatible = "adi,ad3306" },
{ .compatible = "adi,adin1140" },
with no YAML schema. Should the binding document be added in this same
series so that the F: line resolves and dt_binding_check can validate
the new compatibles?
> diff --git a/drivers/net/ethernet/adi/adin1140.c b/drivers/net/ethernet/adi/adin1140.c
> new file mode 100644
> index 000000000000..306e5de6faa5
> --- /dev/null
> +++ b/drivers/net/ethernet/adi/adin1140.c
[ ... ]
> +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);
> +}
[Medium]
adin1140_mac_filter_set() performs up to four sequential
oa_tc6_write_register_mms() calls (filter upper, filter lower, mask
upper, mask lower) and returns at the first error without rolling back
the writes that already succeeded.
If the upper-half write of a new MAC succeeds and the lower-half write
fails, the slot is left programmed with (new high | old low). Callers
treat this as single-shot:
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);
so adin1140_set_mac_address() leaves netdev->dev_addr at the old value
while the hardware filter now matches a third, hybrid address. The same
applies to adin1140_rx_mode_work() which just returns on the first
error and leaves the dynamic filter table inconsistent with
netdev->uc/mc.
Should this function stage the writes (or perform a rollback / clear of
the affected slot) so a partial failure cannot leave the hardware
filter in a state that does not match the driver's view?
[ ... ]
> +static void adin1140_rx_mode_work(struct work_struct *work)
> +{
> + struct adin1140_priv *priv = container_of(work, struct adin1140_priv,
> + rx_mode_work);
> + struct netdev_hw_addr *ha;
> + bool all_multi, promisc;
> + u8 mask[ETH_ALEN];
> + u8 start, end;
> + 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_uc_count(priv->netdev) +
> + netdev_mc_count(priv->netdev);
> +
> + if (priv->netdev->flags & IFF_PROMISC) {
> + promisc = true;
> + all_multi = false;
> + } else if (priv->netdev->flags & IFF_ALLMULTI) {
> + promisc = false;
> + all_multi = true;
> + } else if (mac_addrs <= ADIN1140_MAC_FILT_AVAIL) {
> + promisc = false;
> + all_multi = false;
> +
> + slot = ADIN1140_MAC_FILT_UC_SLOT + 1;
> + memset(mask, 0xFF, ETH_ALEN);
> +
> + netdev_for_each_uc_addr(ha, priv->netdev) {
> + ret = adin1140_mac_filter_set(priv, ha->addr, mask,
> + slot);
> + if (ret)
> + return;
> +
> + slot++;
> + }
> +
> + netdev_for_each_mc_addr(ha, priv->netdev) {
> + ret = adin1140_mac_filter_set(priv, ha->addr, mask,
> + slot);
> + if (ret)
> + return;
> +
> + slot++;
> + }
> +
> + for (i = slot; i < ADIN1140_MAC_FILT_MAX_SLOT; i++) {
> + ret = adin1140_mac_filter_clear(priv, i);
> + if (ret)
> + return;
> + }
> +
> + } else {
> + /* The filter table is full. Enable promisc mode. */
> + promisc = true;
> + all_multi = false;
> +
> + start = ADIN1140_MAC_FILT_UC_SLOT + 1;
> + end = ADIN1140_MAC_FILT_MAX_SLOT;
> + for (i = start; i < end; i++) {
> + ret = adin1140_mac_filter_clear(priv, i);
> + if (ret)
> + return;
> + }
> + }
> +
> + ret = adin1140_promiscuous_mode(priv, promisc);
> + if (ret)
> + return;
> +
> + adin1140_filter_all_multicast(priv, all_multi);
> +}
[High]
This work iterates priv->netdev->uc and ->mc with no synchronization:
netdev_for_each_uc_addr(ha, priv->netdev) {
ret = adin1140_mac_filter_set(priv, ha->addr, mask, slot);
...
}
netdev_for_each_mc_addr(ha, priv->netdev) {
...
}
netdev_for_each_uc_addr() and netdev_for_each_mc_addr() expand to
plain list_for_each_entry() over dev->uc.list / dev->mc.list. Callers
are required to hold netif_addr_lock_bh() (or rtnl) because dev_uc_add
/ dev_uc_del / dev_mc_add / dev_mc_del can mutate those lists from
user-space ip address/maddr changes.
Inside the loop ha->addr is then handed to adin1140_mac_filter_set(),
which performs SPI register writes that can sleep, so taking
netif_addr_lock_bh() across the iteration is not directly viable.
Can this race with a concurrent dev_uc_del() / dev_mc_del() and
dereference freed list entries (or skip / duplicate filter slots)?
Other ethernet drivers that need to issue sleeping ops while syncing
the address lists use __dev_uc_sync() / __dev_mc_sync() which copy
state under netif_addr_lock; would that pattern be more appropriate
here?
[Medium]
In the IFF_PROMISC and IFF_ALLMULTI branches, slots 3..15 are not
cleared:
if (priv->netdev->flags & IFF_PROMISC) {
promisc = true;
all_multi = false;
} else if (priv->netdev->flags & IFF_ALLMULTI) {
promisc = false;
all_multi = true;
} else if (mac_addrs <= ADIN1140_MAC_FILT_AVAIL) {
...
for (i = slot; i < ADIN1140_MAC_FILT_MAX_SLOT; i++) {
ret = adin1140_mac_filter_clear(priv, i);
...
}
} else {
...
for (i = start; i < end; i++) {
ret = adin1140_mac_filter_clear(priv, i);
...
}
}
If a previous invocation populated slots 3..N with extra unicast or
multicast filters and the user then enables IFF_ALLMULTI, those
previously installed unicast filters remain active in hardware
(carrying APPLY2PORT1 | TO_HOST). Should the IFF_ALLMULTI path also
clear slots 3..15 so traffic for addresses no longer in netdev->uc/mc
is not still forwarded to the host?
[ ... ]
> +static void adin1140_stats_work(struct work_struct *work)
> +{
> + struct delayed_work *dwork = to_delayed_work(work);
> + u64 stat_buff[ADIN1140_STATS_CNT] = {};
> + 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)
> + break;
> +
> + stat_buff[i] = reg_val;
> + }
> +
> + scoped_guard(spinlock, &priv->stat_lock)
> + memcpy(&priv->stats, stat_buff, sizeof(priv->stats));
> +
> + schedule_delayed_work(dwork, ADIN1140_STATS_CHECK_DELAY);
> +}
[Medium]
stat_buff is zero-initialized and the loop break on a read error is
followed unconditionally by:
scoped_guard(spinlock, &priv->stat_lock)
memcpy(&priv->stats, stat_buff, sizeof(priv->stats));
If oa_tc6_read_register_mms() fails partway through, every entry from
the failing index onwards is still zero, and that all-zero tail is
copied over the previously valid u64 counters. Since priv->stats[] is
read directly by adin1140_ndo_get_stats64(),
adin1140_get_eth_mac_stats() and adin1140_get_ethtool_stats(), a
single transient SPI/TC6 hiccup makes the rtnl_link_stats64 and
ethtool_eth_mac_stats counters drop to zero before climbing again.
Should the memcpy be skipped on error, or should partial updates be
merged into priv->stats[] only for the indices that were read
successfully?
[Medium]
The hardware counters at MMS1 0xA1..0xBA are read into u32 reg_val and
then stored as-is into the u64 stat_buff[i]:
ret = oa_tc6_read_register_mms(priv->tc6, OA_TC6_MAC_MMS1,
adin1140_stat_regs[i],
®_val);
...
stat_buff[i] = reg_val;
priv->stats[] feeds rtnl_link_stats64 and ethtool_eth_mac_stats, both
of which document monotonic u64 counters. On 32-bit hardware register
wrap (0xFFFFFFFF -> 0) the user-visible u64 value will go backwards,
and with a 3-second poll interval a fast counter (e.g.
FramesReceivedOK on a busy 10BASE-T1S segment) can wrap entirely
between samples. Should the driver compute deltas with 32-bit wrap
handling and accumulate them into a software u64 counter rather than
overwriting?
[ ... ]
> +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;
> + }
> +}
[Medium]
The driver implements both .ndo_get_stats64 and .get_eth_mac_stats,
yet adin1140_stats[] adds private ethtool -S strings whose values are
already exposed (or trivially derivable) through those standard uAPIs:
{.name = "rx_unicast_frames", .idx = rx_uc_frames},
{.name = "rx_preamble_errors", .idx = rx_preamble_errors},
{.name = "rx_ifg_errors", .idx = rx_ifg_errors},
{.name = "rx_addr_filter_dropped", .idx = rx_addr_filter_dropped},
{.name = "tx_unicast_frames", .idx = tx_uc_frames},
rx_addr_filter_dropped is already aggregated into
rtnl_link_stats64.rx_dropped by this same driver:
storage->rx_dropped = priv->stats[rx_fifo_full_dropped] +
priv->stats[rx_addr_filter_dropped];
rx_unicast_frames and tx_unicast_frames are derivable from
ethtool_eth_mac_stats (FramesReceivedOK minus broadcast minus
multicast, and the Xmitted equivalents) which are populated in
__adin1140_eth_mac_stats.
Per Documentation/networking/statistics.rst, should these counters be
left out of ethtool -S since standard uAPIs already cover them?
--
This is an AI-generated review.
prev parent reply other threads:[~2026-06-13 9:27 UTC|newest]
Thread overview: 15+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-09 15:27 [PATCH net-next v4 00/13] net: Add ADIN1140 support Ciprian Regus via B4 Relay
2026-06-09 15:28 ` [PATCH net-next v4 01/13] dt-bindings: net: Add ADIN1140 Ciprian Regus via B4 Relay
2026-06-09 15:28 ` [PATCH net-next v4 02/13] net: ethernet: oa_tc6: Handle the OA TC6 SPI protected mode Ciprian Regus via B4 Relay
2026-06-09 15:28 ` [PATCH net-next v4 03/13] net: ethernet: oa_tc6: add OA_TC6_BROKEN_PHY quirk flag Ciprian Regus via B4 Relay
2026-06-09 15:28 ` [PATCH net-next v4 04/13] net: ethernet: oa_tc6: Export the C45 access functions Ciprian Regus via B4 Relay
2026-06-09 15:28 ` [PATCH net-next v4 05/13] net: ethernet: oa_tc6: Export standard defined registers Ciprian Regus via B4 Relay
2026-06-09 15:28 ` [PATCH net-next v4 06/13] net: ethernet: oa_tc6: Add the OA_TC6_ prefix to standard registers Ciprian Regus via B4 Relay
2026-06-09 15:28 ` [PATCH net-next v4 07/13] net: ethernet: oa_tc6: Add read_mms/write_mms register access functions Ciprian Regus via B4 Relay
2026-06-09 15:28 ` [PATCH net-next v4 08/13] net: ethernet: oa_tc6: Use the read_mms/write_mms functions for C45 Ciprian Regus via B4 Relay
2026-06-09 15:28 ` [PATCH net-next v4 09/13] net: ethernet: oa_tc6: Add new register address defines Ciprian Regus via B4 Relay
2026-06-09 15:28 ` [PATCH net-next v4 10/13] net: phy: add generic helpers for direct C45 MMD access Ciprian Regus via B4 Relay
2026-06-09 15:28 ` [PATCH net-next v4 11/13] net: phy: microchip-t1s: use generic C45 MMD access helpers Ciprian Regus via B4 Relay
2026-06-09 15:28 ` [PATCH net-next v4 12/13] net: phy: Add support for the ADIN1140 PHY Ciprian Regus via B4 Relay
2026-06-09 15:28 ` [PATCH net-next v4 13/13] net: ethernet: adi: Add a driver for the ADIN1140 MACPHY Ciprian Regus via B4 Relay
2026-06-13 9:26 ` Paolo Abeni [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260613092645.123432-1-pabeni@redhat.com \
--to=pabeni@redhat.com \
--cc=andrew+netdev@lunn.ch \
--cc=andrew@lunn.ch \
--cc=ciprian.regus@analog.com \
--cc=conor+dt@kernel.org \
--cc=corbet@lwn.net \
--cc=davem@davemloft.net \
--cc=devicetree@vger.kernel.org \
--cc=edumazet@google.com \
--cc=hkallweit1@gmail.com \
--cc=horms@kernel.org \
--cc=krzk+dt@kernel.org \
--cc=kuba@kernel.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux@armlinux.org.uk \
--cc=netdev@vger.kernel.org \
--cc=parthiban.veerasooran@microchip.com \
--cc=robh@kernel.org \
--cc=skhan@linuxfoundation.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox