* [PATCH net-next v5 2/3] ethtool: add a PHY receive byte total
2026-11-15 7:45 [PATCH net-next v5 0/3] net: phy: report ACx00 receive statistics James Hilliard
2026-11-15 7:45 ` [PATCH net-next v5 1/3] docs: networking: correct the PHY statistics group name James Hilliard
@ 2026-11-15 7:45 ` James Hilliard
2026-11-15 15:25 ` Andrew Lunn
2026-11-15 7:45 ` [PATCH net-next v5 3/3] net: phy: acx00: report receive statistics James Hilliard
2 siblings, 1 reply; 6+ messages in thread
From: James Hilliard @ 2026-11-15 7:45 UTC (permalink / raw)
To: Andrew Lunn, Heiner Kallweit, Russell King, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
Jonathan Corbet, Shuah Khan, Randy Dunlap
Cc: Oleksij Rempel, netdev, linux-kernel, James Hilliard, linux-doc
The generic PHY receive-byte counter describes successfully received
traffic. Some PHYs instead count bytes in all received frames, including
frames with errors. Unlike frame counts, the successful-byte count cannot
be derived by subtracting a frame error count because damaged frames have
different lengths.
Add an optional rx_bytes_total counter to ethtool_phy_stats and the phydev
statistics group. Count frame bytes including the Ethernet header, padding
and FCS, and excluding the preamble and start frame delimiter. The counter
describes reception at the PHY, before MAC or host filtering. Clarify the
same byte boundaries for the successful-only receive counter.
Append the new attribute so existing statistic IDs and their meanings
remain unchanged. Publish its name through the existing statistics string
set and retain the unset-counter convention.
Signed-off-by: James Hilliard <james.hilliard1@gmail.com>
---
Documentation/networking/ethtool-netlink.rst | 13 +++++++++++++
include/linux/ethtool.h | 8 +++++++-
include/uapi/linux/ethtool_netlink.h | 1 +
net/ethtool/stats.c | 5 ++++-
4 files changed, 25 insertions(+), 2 deletions(-)
diff --git a/Documentation/networking/ethtool-netlink.rst b/Documentation/networking/ethtool-netlink.rst
index 8659761ebc69..1dad86561823 100644
--- a/Documentation/networking/ethtool-netlink.rst
+++ b/Documentation/networking/ethtool-netlink.rst
@@ -1683,6 +1683,19 @@ by ``ETHTOOL_A_STATS_GRP_SS_ID``. Complex statistics (such as RMON histogram
entries) are also listed inside ``ETHTOOL_A_STATS_GRP`` and do not have
a string defined in the string set.
+The ``phydev`` group provides separate counters for successfully received
+bytes and total received bytes. ``ETHTOOL_A_STATS_PHY_RX_BYTES`` (``RxOctets``)
+counts bytes in successfully received frames, while
+``ETHTOOL_A_STATS_PHY_RX_BYTES_TOTAL`` (``RxOctetsTotal``) also includes bytes
+in frames with errors. Both include the Ethernet header, padding and FCS,
+but exclude the preamble and start frame delimiter. These counters describe
+reception at the PHY, independently of subsequent filtering or drops at
+the MAC or host.
+
+Each counter is optional. Drivers leave unsupported counters at
+``ETHTOOL_STAT_NOT_SET``, and the kernel omits them from the reply. A PHY can
+report total received bytes even when it cannot separate successful bytes.
+
RMON "histogram" counters count number of packets within given size range.
Because RFC does not specify the ranges beyond the standard 1518 MTU devices
differ in definition of buckets. For this reason the definition of packet ranges
diff --git a/include/linux/ethtool.h b/include/linux/ethtool.h
index c3a41fbd5426..fedc127d9a8a 100644
--- a/include/linux/ethtool.h
+++ b/include/linux/ethtool.h
@@ -481,11 +481,16 @@ struct ethtool_eth_phy_stats {
/**
* struct ethtool_phy_stats - PHY-level statistics counters
* @rx_packets: Total successfully received frames
- * @rx_bytes: Total successfully received bytes
+ * @rx_bytes: Total successfully received frame bytes. Includes the Ethernet
+ * header, padding and FCS, but excludes the preamble and start frame
+ * delimiter.
* @rx_errors: Total received frames with errors (e.g., CRC errors)
* @tx_packets: Total successfully transmitted frames
* @tx_bytes: Total successfully transmitted bytes
* @tx_errors: Total transmitted frames with errors
+ * @rx_bytes_total: Total received frame bytes, including frames with errors.
+ * Includes the Ethernet header, padding and FCS, but excludes the preamble
+ * and start frame delimiter.
*
* This structure provides a standardized interface for reporting
* PHY-level statistics counters. It is designed to expose statistics
@@ -499,6 +504,7 @@ struct ethtool_phy_stats {
u64 tx_packets;
u64 tx_bytes;
u64 tx_errors;
+ u64 rx_bytes_total;
};
/* Basic IEEE 802.3 MAC Ctrl statistics (30.3.3.*), not otherwise exposed
diff --git a/include/uapi/linux/ethtool_netlink.h b/include/uapi/linux/ethtool_netlink.h
index fa5d645140a4..2e81f7345f72 100644
--- a/include/uapi/linux/ethtool_netlink.h
+++ b/include/uapi/linux/ethtool_netlink.h
@@ -202,6 +202,7 @@ enum {
ETHTOOL_A_STATS_PHY_TX_PKTS,
ETHTOOL_A_STATS_PHY_TX_BYTES,
ETHTOOL_A_STATS_PHY_TX_ERRORS,
+ ETHTOOL_A_STATS_PHY_RX_BYTES_TOTAL,
/* add new constants above here */
__ETHTOOL_A_STATS_PHY_CNT,
diff --git a/net/ethtool/stats.c b/net/ethtool/stats.c
index 9b0d8cb07675..9b9ada615ac2 100644
--- a/net/ethtool/stats.c
+++ b/net/ethtool/stats.c
@@ -88,6 +88,7 @@ const char stats_phy_names[__ETHTOOL_A_STATS_PHY_CNT][ETH_GSTRING_LEN] = {
[ETHTOOL_A_STATS_PHY_TX_PKTS] = "TxFrames",
[ETHTOOL_A_STATS_PHY_TX_BYTES] = "TxOctets",
[ETHTOOL_A_STATS_PHY_TX_ERRORS] = "TxErrors",
+ [ETHTOOL_A_STATS_PHY_RX_BYTES_TOTAL] = "RxOctetsTotal",
};
const struct nla_policy ethnl_stats_get_policy[ETHTOOL_A_STATS_SRC + 1] = {
@@ -295,7 +296,9 @@ static int stats_put_phydev_stats(struct sk_buff *skb,
stat_put(skb, ETHTOOL_A_STATS_PHY_TX_BYTES,
data->phydev_stats.tx_bytes) ||
stat_put(skb, ETHTOOL_A_STATS_PHY_TX_ERRORS,
- data->phydev_stats.tx_errors))
+ data->phydev_stats.tx_errors) ||
+ stat_put(skb, ETHTOOL_A_STATS_PHY_RX_BYTES_TOTAL,
+ data->phydev_stats.rx_bytes_total))
return -EMSGSIZE;
return 0;
}
--
2.53.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH net-next v5 3/3] net: phy: acx00: report receive statistics
2026-11-15 7:45 [PATCH net-next v5 0/3] net: phy: report ACx00 receive statistics James Hilliard
2026-11-15 7:45 ` [PATCH net-next v5 1/3] docs: networking: correct the PHY statistics group name James Hilliard
2026-11-15 7:45 ` [PATCH net-next v5 2/3] ethtool: add a PHY receive byte total James Hilliard
@ 2026-11-15 7:45 ` James Hilliard
2 siblings, 0 replies; 6+ messages in thread
From: James Hilliard @ 2026-11-15 7:45 UTC (permalink / raw)
To: Andrew Lunn, Heiner Kallweit, Russell King, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
Jonathan Corbet, Shuah Khan, Randy Dunlap
Cc: Oleksij Rempel, netdev, linux-kernel, James Hilliard, linux-doc
AC200 and AC300 count received frames including those with CRC errors.
Subtract the CRC-error count to report successful frames through the
existing PHY rx_packets statistic, and report CRC errors through
rx_errors. Report the hardware byte total through rx_bytes_total, leaving
the successful-only rx_bytes unset because damaged-frame lengths are not
available separately.
The free-running packet and CRC-error counters both wrap at 2^32.
Read each counter high/low/high to detect a carry, and bracket the packet
read with an unchanged CRC count before subtracting. Their difference
also wraps at 2^32. Accumulate its wrap-adjusted deltas in a 64-bit total.
Hardware measurements on AC200 and AC300 established a receive-byte
modulus of 4,000,000,000. The byte counts include Ethernet headers, padding
and FCS, but exclude preamble and SFD. Apply that modulus to byte deltas
and skip byte samples at or above it without changing their baseline.
Bound sampling retries to avoid retaining the PHY and MDIO locks
indefinitely. When CRC errors keep arriving, update rx_errors from the
latest stable error read without advancing the successful-frame baseline.
A later coherent pair accounts for the intervening successful frames.
Collect bytes independently of that pair. An unstable sample is not an
MDIO error: returning a synthetic error from update_stats would make
phylib take the link down. Real MDIO errors, including page-restoration
failures, still propagate.
Serialize sampling, reporting and power transitions with the package
mutex. Commit samples only after page restoration succeeds, and return
cached totals from get_phy_stats without further MDIO access.
Invalidate baselines before soft_reset or power-off, since phylib may
already have reset the counters. Take a best-effort snapshot before
suspend and start new intervals on resume, including when WoL retained
power while polling stopped. Keep a separate sampling time for each
counter and discard deltas after gaps of at least 320 seconds, including
system sleep and register-read time. At 100 Mb/s, that bounds the byte
counter and conservatively bounds the frame counters even at one frame
per received byte. Retain accumulated totals across these gaps and power
transitions, without inventing an unobservable number of counter cycles.
Signed-off-by: James Hilliard <james.hilliard1@gmail.com>
---
drivers/net/phy/xpowers/acx00.c | 223 +++++++++++++++++++++++++++++++++++++++-
1 file changed, 220 insertions(+), 3 deletions(-)
diff --git a/drivers/net/phy/xpowers/acx00.c b/drivers/net/phy/xpowers/acx00.c
index cee6c7ff0278..691f4931af46 100644
--- a/drivers/net/phy/xpowers/acx00.c
+++ b/drivers/net/phy/xpowers/acx00.c
@@ -8,12 +8,14 @@
#include <linux/bitfield.h>
#include <linux/device.h>
+#include <linux/ethtool.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/nvmem-consumer.h>
#include <linux/of.h>
#include <linux/phy.h>
#include <linux/slab.h>
+#include <linux/timekeeping.h>
#include "../phylib.h"
#include "acx00.h"
@@ -31,6 +33,7 @@
#define ACX00_PAGE_2 2
#define ACX00_PAGE_6 6
#define ACX00_PAGE_8 8
+#define ACX00_PAGE_9 9
#define ACX00_PAGE0_GLOBAL_CONFIG_REG 0x13
#define ACX00_PAGE0_XMII_RX_CLOCK_INVERT BIT(12)
@@ -76,6 +79,26 @@
(ACX00_PAGE8_AUTO_CAL_TX_LEVEL_ADJUST_BYPASS | \
ACX00_PAGE8_AUTO_CAL_LOW_CAL_OPAQUE_BITS)
+#define ACX00_PAGE9_RX_BYTES_HIGH_REG 0x19
+#define ACX00_PAGE9_RX_PACKETS_HIGH_REG 0x1b
+#define ACX00_PAGE9_RX_CRC_ERRORS_HIGH_REG 0x1d
+
+/*
+ * AC200 and AC300 hardware testing confirmed that the receive byte counter
+ * wraps at 4,000,000,000 rather than 2^32.
+ */
+#define ACX00_RX_BYTES_WRAP 4000000000ULL
+/* At 100 Mb/s, a full byte-counter cycle takes at least 320 seconds. */
+#define ACX00_STATS_MAX_INTERVAL_MS (ACX00_RX_BYTES_WRAP * 8 / (SPEED_100 * 1000))
+#define ACX00_COUNTER_READ_ATTEMPTS 3
+
+enum acx00_ephy_stat {
+ ACX00_STAT_RX_PACKETS,
+ ACX00_STAT_RX_CRC_ERRORS,
+ ACX00_STAT_RX_BYTES,
+ ACX00_NUM_STATS,
+};
+
/*
* Another integration of this exact-ID PHY documents its digital vendor
* register map, which also matches the observed ACx00 reset values. The ACx00
@@ -86,11 +109,16 @@
struct acx00_ephy_priv {
struct phy_device *phydev;
struct acx00_ephy_control *control;
- /* Serializes package state and multi-register power sequences. */
+ /* Serializes package state, power sequences and statistics. */
struct mutex state_lock;
bool is_ac300;
bool use_low_calibration_tuning;
bool xmii_rx_clock_inverted;
+ bool stats_active;
+ u8 stats_valid;
+ ktime_t stats_start[ACX00_NUM_STATS];
+ u32 stats_last[ACX00_NUM_STATS];
+ u64 stats[ACX00_NUM_STATS];
};
static int acx00_ephy_read_page(struct phy_device *phydev)
@@ -110,6 +138,169 @@ static int acx00_ephy_write_page(struct phy_device *phydev, int page)
FIELD_PREP(ACX00_PAGE_SELECT_MASK, page));
}
+/* Return 0 on success, 1 for an unstable sample, or a negative MDIO error. */
+static int acx00_ephy_read_counter(struct phy_device *phydev, u8 reg, u32 *count)
+{
+ int high, low, check;
+ int i;
+
+ /* The free-running counter can carry while its two halves are read. */
+ for (i = 0; i < ACX00_COUNTER_READ_ATTEMPTS; i++) {
+ high = __phy_read(phydev, reg);
+ if (high < 0)
+ return high;
+ low = __phy_read(phydev, reg + 1);
+ if (low < 0)
+ return low;
+ check = __phy_read(phydev, reg);
+ if (check < 0)
+ return check;
+ if (high != check)
+ continue;
+
+ *count = (u32)high << 16 | low;
+ return 0;
+ }
+
+ return 1;
+}
+
+/* Return a mask of usable counters, or a negative MDIO error. */
+static int acx00_ephy_read_counts(struct phy_device *phydev, u32 *counts)
+{
+ u32 packets, errors;
+ int ret;
+ int i;
+
+ ret = acx00_ephy_read_counter(phydev, ACX00_PAGE9_RX_CRC_ERRORS_HIGH_REG,
+ &counts[ACX00_STAT_RX_CRC_ERRORS]);
+ if (ret)
+ return ret < 0 ? ret : 0;
+
+ /* Bracket the packet counter with an unchanged CRC count. */
+ for (i = 0; i < ACX00_COUNTER_READ_ATTEMPTS; i++) {
+ ret = acx00_ephy_read_counter(phydev, ACX00_PAGE9_RX_PACKETS_HIGH_REG,
+ &packets);
+ if (ret < 0)
+ return ret;
+ if (ret)
+ break;
+ ret = acx00_ephy_read_counter(phydev, ACX00_PAGE9_RX_CRC_ERRORS_HIGH_REG,
+ &errors);
+ if (ret < 0)
+ return ret;
+ if (ret)
+ break;
+ if (errors == counts[ACX00_STAT_RX_CRC_ERRORS]) {
+ /* Both hardware counters and their difference wrap at 2^32. */
+ counts[ACX00_STAT_RX_PACKETS] = packets - errors;
+ return BIT(ACX00_STAT_RX_PACKETS) | BIT(ACX00_STAT_RX_CRC_ERRORS);
+ }
+ counts[ACX00_STAT_RX_CRC_ERRORS] = errors;
+ }
+
+ /* Keep reporting errors even when no coherent packet sample is possible. */
+ return BIT(ACX00_STAT_RX_CRC_ERRORS);
+}
+
+static int __acx00_ephy_update_stats(struct phy_device *phydev)
+{
+ struct acx00_ephy_priv *priv = phydev->priv;
+ u32 counts[ACX00_NUM_STATS];
+ ktime_t start;
+ int oldpage;
+ int valid;
+ int ret;
+ int i;
+
+ lockdep_assert_held(&priv->state_lock);
+
+ if (!priv->stats_active)
+ return 0;
+
+ start = ktime_get_boottime();
+ oldpage = phy_select_page(phydev, ACX00_PAGE_9);
+ if (oldpage < 0)
+ return phy_restore_page(phydev, oldpage, oldpage);
+
+ valid = acx00_ephy_read_counts(phydev, counts);
+ if (valid < 0) {
+ ret = valid;
+ goto out_restore_page;
+ }
+
+ ret = acx00_ephy_read_counter(phydev, ACX00_PAGE9_RX_BYTES_HIGH_REG,
+ &counts[ACX00_STAT_RX_BYTES]);
+ if (ret < 0)
+ goto out_restore_page;
+ if (!ret && counts[ACX00_STAT_RX_BYTES] < ACX00_RX_BYTES_WRAP)
+ valid |= BIT(ACX00_STAT_RX_BYTES);
+ ret = valid;
+
+out_restore_page:
+ ret = phy_restore_page(phydev, oldpage, ret);
+ if (ret < 0)
+ return ret;
+
+ for (i = 0; i < ACX00_NUM_STATS; i++) {
+ u64 count;
+
+ if (!(ret & BIT(i)))
+ continue;
+ count = counts[i];
+
+ /* Include sleep and read time; unseen cycles cannot be recovered. */
+ if ((priv->stats_valid & BIT(i)) &&
+ ktime_ms_delta(ktime_get_boottime(), priv->stats_start[i]) <
+ (s64)ACX00_STATS_MAX_INTERVAL_MS) {
+ if (count < priv->stats_last[i])
+ count += i == ACX00_STAT_RX_BYTES ?
+ ACX00_RX_BYTES_WRAP : BIT_ULL(32);
+ priv->stats[i] += count - priv->stats_last[i];
+ }
+ priv->stats_last[i] = counts[i];
+ priv->stats_start[i] = start;
+ priv->stats_valid |= BIT(i);
+ }
+
+ return 0;
+}
+
+static int acx00_ephy_update_stats(struct phy_device *phydev)
+{
+ struct acx00_ephy_priv *priv = phydev->priv;
+ int ret;
+
+ mutex_lock(&priv->state_lock);
+ ret = __acx00_ephy_update_stats(phydev);
+ mutex_unlock(&priv->state_lock);
+
+ return ret;
+}
+
+static void acx00_ephy_get_phy_stats(struct phy_device *phydev,
+ struct ethtool_eth_phy_stats *eth_stats,
+ struct ethtool_phy_stats *stats)
+{
+ struct acx00_ephy_priv *priv = phydev->priv;
+
+ mutex_lock(&priv->state_lock);
+ stats->rx_packets = priv->stats[ACX00_STAT_RX_PACKETS];
+ stats->rx_errors = priv->stats[ACX00_STAT_RX_CRC_ERRORS];
+ stats->rx_bytes_total = priv->stats[ACX00_STAT_RX_BYTES];
+ mutex_unlock(&priv->state_lock);
+}
+
+static void acx00_ephy_snapshot_stats(struct phy_device *phydev)
+{
+ int ret;
+
+ /* Statistics collection must not prevent reset or power management. */
+ ret = __acx00_ephy_update_stats(phydev);
+ if (ret)
+ phydev_dbg(phydev, "failed to snapshot statistics: %pe\n", ERR_PTR(ret));
+}
+
static int acx00_ephy_control_power_on(struct acx00_ephy_priv *priv)
{
lockdep_assert_held(&priv->state_lock);
@@ -122,6 +313,10 @@ static int acx00_ephy_control_power_off(struct acx00_ephy_priv *priv)
{
lockdep_assert_held(&priv->state_lock);
+ /* Even a failed power-off can have reset the hardware counter. */
+ priv->stats_active = false;
+ priv->stats_valid = 0;
+
return priv->control->power_off(priv->control);
}
@@ -350,6 +545,10 @@ static int acx00_ephy_soft_reset(struct phy_device *phydev)
mutex_lock(&priv->state_lock);
+ /* phylib may already have reset the counters before calling us. */
+ priv->stats_active = false;
+ priv->stats_valid = 0;
+
ret = acx00_ephy_set_interface(phydev);
if (ret)
goto out_power_off;
@@ -475,10 +674,21 @@ static int __acx00_ephy_config_init(struct phy_device *phydev)
/* Restore the standard EEE policy retained by phylib across resets. */
ret = genphy_c45_an_config_eee_aneg(phydev);
- if (ret <= 0)
+ if (ret < 0)
return ret;
+ if (ret > 0) {
+ ret = genphy_restart_aneg(phydev);
+ if (ret)
+ return ret;
+ }
+
+ if (!priv->stats_active) {
+ /* Establish a new baseline after reset, retaining the software total. */
+ priv->stats_active = true;
+ acx00_ephy_snapshot_stats(phydev);
+ }
- return genphy_restart_aneg(phydev);
+ return 0;
}
static int acx00_ephy_config_init(struct phy_device *phydev)
@@ -534,6 +744,9 @@ static int acx00_ephy_resume(struct phy_device *phydev)
int ret;
mutex_lock(&priv->state_lock);
+ /* WoL can keep the PHY counting while phylib polling is stopped. */
+ priv->stats_active = false;
+ priv->stats_valid = 0;
ret = __acx00_ephy_power_on_and_resume(phydev);
mutex_unlock(&priv->state_lock);
@@ -550,6 +763,8 @@ static int acx00_ephy_suspend(struct phy_device *phydev)
mutex_lock(&phydev->lock);
mutex_lock(&priv->state_lock);
+ acx00_ephy_snapshot_stats(phydev);
+
ret = genphy_suspend(phydev);
if (ret)
goto out_unlock;
@@ -616,6 +831,8 @@ static struct phy_driver acx00_ephy_driver[] = {
.soft_reset = acx00_ephy_soft_reset,
.config_init = acx00_ephy_config_init,
.disable_autonomous_eee = acx00_ephy_disable_autonomous_eee,
+ .get_phy_stats = acx00_ephy_get_phy_stats,
+ .update_stats = acx00_ephy_update_stats,
.suspend = acx00_ephy_suspend,
.resume = acx00_ephy_resume,
},
--
2.53.0
^ permalink raw reply [flat|nested] 6+ messages in thread