[<prev] [next>] [day] [month] [year] [list]
Message-ID: <20260113142031.555787-1-o.rempel@pengutronix.de>
Date: Tue, 13 Jan 2026 15:20:31 +0100
From: Oleksij Rempel <o.rempel@...gutronix.de>
To: Michal Kubecek <mkubecek@...e.cz>
Cc: Oleksij Rempel <o.rempel@...gutronix.de>,
kernel@...gutronix.de,
netdev@...r.kernel.org
Subject: [PATCH ethtool v1] ethtool: add --show-mse netlink support
Add support for querying PHY Mean Square Error (MSE) diagnostics via
ETHTOOL_MSG_MSE_GET. This exposes MSE capability information and the
latest available snapshots for supported PHYs.
Signed-off-by: Oleksij Rempel <o.rempel@...gutronix.de>
---
Makefile.am | 1 +
ethtool.8.in | 46 ++++++
ethtool.c | 7 +
netlink/extapi.h | 2 +
netlink/mse.c | 388 +++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 444 insertions(+)
create mode 100644 netlink/mse.c
diff --git a/Makefile.am b/Makefile.am
index c58325c71a82..524b0922fd62 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -55,6 +55,7 @@ ethtool_SOURCES += \
netlink/pse-pd.c \
netlink/phy.c \
netlink/tsconfig.c \
+ netlink/mse.c \
uapi/linux/ethtool_netlink.h \
uapi/linux/ethtool_netlink_generated.h \
uapi/linux/netlink.h uapi/linux/genetlink.h \
diff --git a/ethtool.8.in b/ethtool.8.in
index 0aafb4b0321b..e73f8e7cfcca 100644
--- a/ethtool.8.in
+++ b/ethtool.8.in
@@ -639,6 +639,7 @@ lB l.
\-\-get\-plca\-status
\-\-show-pse
\-\-set-pse
+\-\-show-mse
.TE
.TP
.B \-a \-\-show\-pause
@@ -1977,6 +1978,51 @@ When a single domain exceeds its budget, ports in that domain are
powered up/down by priority (highest first for power-up; lowest shed
first).
+.RE
+.TP
+.B \-\-show\-mse
+Show PHY Mean Square Error (MSE) diagnostics.
+.RS 4
+.P
+Metrics follow the OPEN Alliance PHY diagnostics model (used by automotive and
+industrial PHYs). Numeric scaling, sampling windows, and update intervals are
+vendor specific and reported by the capability block.
+.P
+Values are raw snapshots from the PHY DSP. Lower values generally indicate
+better signal quality; 0 is ideal. Interpret values relative to the reported
+max-* scales for this PHY and link mode.
+.P
+The set of returned channels depends on PHY support. When per-channel data is
+available, Channel A/B/C/D nests are returned. Otherwise, the kernel may return
+a single WORST or LINK aggregate snapshot.
+.TP
+.B Capabilities
+Driver-provided scale and timing:
+.RS 4
+.TP
+.B max\-average\-mse
+Scale for average MSE values.
+.TP
+.B max\-peak\-mse
+Scale for peak MSE values. Present only if the PHY reports peak-mse and/or
+worst-peak-mse.
+.TP
+.B refresh\-rate\-ps
+Typical hardware update interval in picoseconds.
+.TP
+.B symbols\-per\-sample
+Number of symbols collected per hardware sample.
+.RE
+.TP
+.B Snapshots
+One nest per selector (Channel A/B/C/D, WORST, or LINK). Each contains the
+metrics supported by the PHY: average-mse and optionally peak-mse and/or
+worst-peak-mse. Values are raw and must be interpreted using the capability
+scales above.
+.P
+Short windows (low refresh-rate-ps or few symbols-per-sample) can yield high
+variance; userspace can improve stability by polling and averaging over time.
+
.RE
.TP
.B \-\-flash\-module\-firmware
diff --git a/ethtool.c b/ethtool.c
index 86214581e55f..c9c15023f8b2 100644
--- a/ethtool.c
+++ b/ethtool.c
@@ -6371,6 +6371,13 @@ static const struct option args[] = {
.nlfunc = nl_get_phy,
.help = "List PHYs"
},
+ {
+ .opts = "--show-mse",
+ .targets_phy = true,
+ .json = true,
+ .nlfunc = nl_gmse,
+ .help = "Show Mean Square Error (MSE) diagnostics",
+ },
{
.opts = "-h|--help",
.no_dev = true,
diff --git a/netlink/extapi.h b/netlink/extapi.h
index f2bf422a3f2d..b0e458804297 100644
--- a/netlink/extapi.h
+++ b/netlink/extapi.h
@@ -59,6 +59,7 @@ int nl_gpse(struct cmd_context *ctx);
int nl_spse(struct cmd_context *ctx);
int nl_flash_module_fw(struct cmd_context *ctx);
int nl_get_phy(struct cmd_context *ctx);
+int nl_gmse(struct cmd_context *ctx);
void nl_monitor_usage(void);
@@ -138,6 +139,7 @@ nl_get_eeprom_page(struct cmd_context *ctx __maybe_unused,
#define nl_spse NULL
#define nl_flash_module_fw NULL
#define nl_get_phy NULL
+#define nl_gmse NULL
#endif /* ETHTOOL_ENABLE_NETLINK */
diff --git a/netlink/mse.c b/netlink/mse.c
new file mode 100644
index 000000000000..cca0c1f272f5
--- /dev/null
+++ b/netlink/mse.c
@@ -0,0 +1,388 @@
+/*
+ * Implementation of "ethtool --show-mse <dev>"
+ *
+ * Background:
+ * - Kernel MSE GET is defined in Documentation/netlink/specs/ethtool.yaml
+ * and implemented in net/ethtool/mse.c.
+ * - Capabilities describe scale and timing for MSE readings:
+ * max-average-mse / max-peak-mse : scale
+ * refresh-rate-ps : nominal update interval (picoseconds)
+ * num-symbols : symbols per sample window
+ * These two timing fields are mandatory in the kernel reply; limits are
+ * present only when the corresponding metrics are supported.
+ * - Metrics originate from OPEN Alliance PHY diagnostics (100/1000BASE-T1),
+ * but scaling, windows, and refresh reate are vendor-specific; the
+ * capability block reports the implementation details provided by the PHY
+ * driver.
+ * - Snapshots carry per-channel values (A-D, WORST, LINK) chosen by the
+ * kernel in priority order (per-channel first, else WORST, else LINK).
+ */
+
+#include <errno.h>
+#include <inttypes.h>
+#include <string.h>
+#include <stdio.h>
+
+#include "../internal.h"
+#include "../common.h"
+#include "netlink.h"
+#include "parser.h"
+
+enum mse_attr_kind {
+ MSE_ATTR_HEADER,
+ MSE_ATTR_CAPS,
+ MSE_ATTR_SNAPSHOT,
+ MSE_ATTR_UNKNOWN,
+};
+
+struct mse_field_desc {
+ uint16_t attr;
+ const char *json_key;
+ const char *plain_fmt;
+ bool required;
+};
+
+static const struct mse_field_desc mse_cap_fields[] = {
+ {
+ .attr = ETHTOOL_A_MSE_CAPABILITIES_REFRESH_RATE_PS,
+ .json_key = "refresh-rate-ps",
+ .plain_fmt = "\tRefresh Rate: %" PRIu64 " ps\n",
+ .required = true,
+ },
+ {
+ .attr = ETHTOOL_A_MSE_CAPABILITIES_NUM_SYMBOLS,
+ .json_key = "symbols-per-sample",
+ .plain_fmt = "\tSymbols per Sample: %" PRIu64 "\n",
+ .required = true,
+ },
+ {
+ .attr = ETHTOOL_A_MSE_CAPABILITIES_MAX_AVERAGE_MSE,
+ .json_key = "max-average-mse",
+ .plain_fmt = "\tMax Average MSE: %" PRIu64 "\n",
+ .required = false,
+ },
+ {
+ .attr = ETHTOOL_A_MSE_CAPABILITIES_MAX_PEAK_MSE,
+ .json_key = "max-peak-mse",
+ .plain_fmt = "\tMax Peak MSE: %" PRIu64 "\n",
+ .required = false,
+ },
+};
+
+static const struct mse_field_desc mse_snapshot_fields[] = {
+ {
+ .attr = ETHTOOL_A_MSE_SNAPSHOT_AVERAGE_MSE,
+ .json_key = "average-mse",
+ .plain_fmt = "\tAverage MSE: %" PRIu64 "\n",
+ .required = false,
+ },
+ {
+ .attr = ETHTOOL_A_MSE_SNAPSHOT_PEAK_MSE,
+ .json_key = "peak-mse",
+ .plain_fmt = "\tPeak MSE: %" PRIu64 "\n",
+ .required = false,
+ },
+ {
+ .attr = ETHTOOL_A_MSE_SNAPSHOT_WORST_PEAK_MSE,
+ .json_key = "worst-peak-mse",
+ .plain_fmt = "\tWorst-Peak MSE: %" PRIu64 "\n",
+ .required = false,
+ },
+};
+
+static enum mse_attr_kind mse_classify_attr(uint16_t at)
+{
+ switch (at) {
+ case ETHTOOL_A_MSE_HEADER:
+ return MSE_ATTR_HEADER;
+ case ETHTOOL_A_MSE_CAPABILITIES:
+ return MSE_ATTR_CAPS;
+ case ETHTOOL_A_MSE_CHANNEL_A:
+ case ETHTOOL_A_MSE_CHANNEL_B:
+ case ETHTOOL_A_MSE_CHANNEL_C:
+ case ETHTOOL_A_MSE_CHANNEL_D:
+ case ETHTOOL_A_MSE_WORST_CHANNEL:
+ case ETHTOOL_A_MSE_LINK:
+ return MSE_ATTR_SNAPSHOT;
+ default:
+ return MSE_ATTR_UNKNOWN;
+ }
+}
+
+/* Validate presence (if required) and width of integer attrs, then fetch the
+ * value. The kernel uses nla_put_uint(), which may encode values in
+ * 8/16/32/64-bit payloads; rely on attr_get_uint() for size handling.
+ * @present reports whether the attribute was found.
+ *
+ * Return: 0 on success, -EINVAL/-EMSGSIZE on malformed attributes.
+ */
+static int mse_validate_get_u64_attr(const struct nlattr *attr, const char *name,
+ bool required, u64 *val, bool *present)
+{
+ if (present)
+ *present = false;
+ if (!attr) {
+ if (required)
+ fprintf(stderr, "warning: missing %s attribute in MSE reply; skipping\n",
+ name);
+ if (val)
+ *val = 0;
+ return 0;
+ }
+
+ *val = attr_get_uint(attr);
+ if (*val == UINT64_MAX) {
+ fprintf(stderr, "invalid %s attribute size in MSE reply\n", name);
+ return -EMSGSIZE;
+ }
+ if (present)
+ *present = true;
+
+ return 0;
+}
+
+static int mse_print_fields(const struct nlattr **tb,
+ const struct mse_field_desc *fields, size_t n,
+ bool *has_value)
+{
+ const struct mse_field_desc *f;
+ bool present;
+ u64 val;
+ int ret;
+
+ for (f = fields; f < fields + n; f++) {
+ ret = mse_validate_get_u64_attr(tb[f->attr], f->json_key,
+ f->required, &val, &present);
+ if (ret < 0)
+ return ret;
+ if (present) {
+ print_u64(PRINT_ANY, f->json_key, f->plain_fmt, val);
+ if (has_value)
+ *has_value = true;
+ }
+ }
+
+ return 0;
+}
+
+static const char *mse_get_channel_name(uint16_t channel)
+{
+ switch (channel) {
+ case ETHTOOL_A_MSE_CHANNEL_A:
+ return "a";
+ case ETHTOOL_A_MSE_CHANNEL_B:
+ return "b";
+ case ETHTOOL_A_MSE_CHANNEL_C:
+ return "c";
+ case ETHTOOL_A_MSE_CHANNEL_D:
+ return "d";
+ case ETHTOOL_A_MSE_WORST_CHANNEL:
+ return "worst";
+ case ETHTOOL_A_MSE_LINK:
+ return "link";
+ default:
+ return "unknown";
+ }
+}
+
+static int mse_dump_capabilities(const struct nlattr *cap_attr)
+{
+ const struct nlattr *tb[ETHTOOL_A_MSE_CAPABILITIES_MAX + 1] = {};
+ DECLARE_ATTR_TB_INFO(tb);
+ bool has_value = false;
+ int ret;
+
+ ret = mnl_attr_parse_nested(cap_attr, attr_cb, &tb_info);
+ if (ret != MNL_CB_OK) {
+ fprintf(stderr, "malformed netlink message (capabilities)\n");
+ return -EINVAL;
+ }
+
+ open_json_object("mse-capabilities");
+ if (!is_json_context())
+ printf("MSE Capabilities:\n");
+
+ /* Kernel sends max-average/peak only if corresponding PHY_MSE_CAP_* bits
+ * are set; refresh-rate-ps and num-symbols are always present.
+ */
+ ret = mse_print_fields(tb, mse_cap_fields, ARRAY_SIZE(mse_cap_fields),
+ &has_value);
+
+ if (!has_value)
+ fprintf(stderr, "warning: kernel returned empty MSE capability block\n");
+
+ close_json_object();
+
+ return ret;
+}
+
+static int mse_dump_snapshot(const struct nlattr *snapshot_attr,
+ uint16_t channel)
+{
+ const struct nlattr *tb[ETHTOOL_A_MSE_SNAPSHOT_MAX + 1] = {};
+ DECLARE_ATTR_TB_INFO(tb);
+ const char *channel_name;
+ bool has_value = false;
+ int ret;
+
+ ret = mnl_attr_parse_nested(snapshot_attr, attr_cb, &tb_info);
+ if (ret != MNL_CB_OK) {
+ fprintf(stderr, "malformed netlink message (snapshot)\n");
+ return -EINVAL;
+ }
+
+ channel_name = mse_get_channel_name(channel);
+ print_string(PRINT_ANY, "channel", "\nMSE Snapshot (Channel: %s):\n",
+ channel_name);
+
+ ret = mse_print_fields(tb, mse_snapshot_fields,
+ ARRAY_SIZE(mse_snapshot_fields), &has_value);
+ if (ret < 0)
+ return ret;
+
+ if (!has_value)
+ fprintf(stderr, "warning: kernel returned empty MSE snapshot for channel %s\n",
+ channel_name);
+
+ return 0;
+}
+
+static int mse_process_snapshot_attr(const struct nlattr *attr)
+{
+ uint16_t channel = mnl_attr_get_type(attr);
+ int ret;
+
+ open_json_object(NULL);
+
+ ret = mse_dump_snapshot(attr, channel);
+
+ close_json_object();
+
+ return ret;
+}
+
+static int mse_dump_snapshots(const struct nlmsghdr *nlhdr)
+{
+ bool snapshots_started = false;
+ unsigned int unknown_cnt = 0;
+ const struct nlattr *attr;
+ int ret = 0;
+
+ /*
+ * If the kernel provides no per-channel snapshot nests, still emit an
+ * empty "mse-snapshots" array in JSON mode. This keeps the JSON schema
+ * stable for consumers (always an array, possibly empty).
+ */
+ if (is_json_context())
+ open_json_array("mse-snapshots", NULL);
+
+ /* Kernel already picks per-channel over WORST over LINK; we just dump
+ * whatever nests are present.
+ */
+ mnl_attr_for_each(attr, nlhdr, GENL_HDRLEN) {
+ uint16_t at = mnl_attr_get_type(attr);
+
+ switch (mse_classify_attr(at)) {
+ case MSE_ATTR_SNAPSHOT:
+ ret = mse_process_snapshot_attr(attr);
+ if (ret < 0)
+ goto out;
+
+ snapshots_started = true;
+
+ break;
+ case MSE_ATTR_UNKNOWN:
+ unknown_cnt++;
+ break;
+ case MSE_ATTR_HEADER:
+ case MSE_ATTR_CAPS:
+ default:
+ break;
+ }
+ }
+
+ if (!snapshots_started)
+ fprintf(stderr, "warning: no MSE snapshot data available from kernel\n");
+
+ if (unknown_cnt)
+ fprintf(stderr, "warning: %u unknown MSE attribute(s) ignored\n",
+ unknown_cnt);
+out:
+ if (is_json_context())
+ close_json_array(NULL);
+
+ return ret;
+}
+
+int mse_reply_cb(const struct nlmsghdr *nlhdr, void *data)
+{
+ const struct nlattr *tb[ETHTOOL_A_MSE_MAX + 1] = {};
+ struct nl_context *nlctx = data;
+ DECLARE_ATTR_TB_INFO(tb);
+ int ret;
+
+ ret = mnl_attr_parse(nlhdr, GENL_HDRLEN, attr_cb, &tb_info);
+ if (ret != MNL_CB_OK)
+ return -EINVAL;
+
+ nlctx->devname = get_dev_name(tb[ETHTOOL_A_MSE_HEADER]);
+ if (!dev_ok(nlctx))
+ return 0;
+
+ open_json_object(NULL);
+ print_string(PRINT_ANY, "ifname", "MSE diagnostics for %s:\n",
+ nlctx->devname);
+
+ if (tb[ETHTOOL_A_MSE_CAPABILITIES]) {
+ ret = mse_dump_capabilities(tb[ETHTOOL_A_MSE_CAPABILITIES]);
+ if (ret < 0)
+ goto out;
+ } else {
+ fprintf(stderr, "warning: missing MSE capabilities; continuing with snapshots\n");
+ }
+
+ ret = mse_dump_snapshots(nlhdr);
+
+ print_nl();
+out:
+ close_json_object();
+
+ return ret;
+}
+
+int nl_gmse(struct cmd_context *ctx)
+{
+ struct nl_context *nlctx = ctx->nlctx;
+ struct nl_msg_buff *msgbuff;
+ struct nl_socket *nlsk;
+ int ret;
+
+ if (netlink_cmd_check(ctx, ETHTOOL_MSG_MSE_GET, true))
+ return -EOPNOTSUPP;
+
+ nlctx->cmd = "--show-mse";
+ nlctx->argp = ctx->argp;
+ nlctx->argc = ctx->argc;
+ nlctx->devname = ctx->devname;
+ nlsk = nlctx->ethnl_socket;
+ msgbuff = &nlsk->msgbuff;
+
+ ret = msg_init(nlctx, msgbuff, ETHTOOL_MSG_MSE_GET,
+ NLM_F_REQUEST | NLM_F_ACK);
+ if (ret < 0)
+ return ret;
+ ret = ethnla_fill_header_phy(msgbuff, ETHTOOL_A_MSE_HEADER,
+ ctx->devname, ctx->phy_index, 0);
+ if (ret < 0)
+ return ret;
+
+ new_json_obj(ctx->json);
+ ret = nlsock_sendmsg(nlsk, NULL);
+ if (ret < 0)
+ goto out;
+ ret = nlsock_process_reply(nlsk, mse_reply_cb, nlctx);
+
+out:
+ delete_json_obj();
+ return ret;
+}
--
2.47.3
Powered by blists - more mailing lists