[PATCH] wifi: mt76: mt7925: add MT7935 virtual efuse (vefuse) support

JB Tsai <[email protected]> Wed, 5 Aug 2026 16:05:36 +0800
Newsgroups org.kernel.vger.linux-wireless,org.infradead.lists.linux-mediatek
Message-ID <[email protected]>
From: "tony-tc.lee" <[email protected]>

MT7935 can override eFuse/EEPROM values at runtime using a "virtual
efuse" (vefuse) payload embedded in the RAM firmware image as a
non-download region (FW_TYPE_EXTEND_BIN_EFUSE). The host driver locates
this region and pushes it to firmware, which applies it as an eFuse
override.

Firmware advertises vefuse capability via the NIC capability TLV
MT_NIC_CAP_VEFUSE_INFO (0x44), reporting whether vefuse is enabled and
the expected payload size. Parse it in mt7925_mcu_get_nic_capability()
and store it in mt792x_dev.vefuse_cap.

After the firmware is up (mt7925_run_firmware),
mt7925_load_vefuse_from_fw() finds the FW_TYPE_EXTEND_BIN_EFUSE / NON_DL
region in the RAM firmware image and downloads it via the UNI EFUSE_CTRL
buffer-mode command (source_mode = VEFUSE), paged in chunks of
MT7925_VEFUSE_PAGE_SIZE (1024).

Two non-obvious ABI details:
  - The EFUSE_CTRL command payload carries a 4-byte header before the
    buffer-mode TLV (as the efuse access/read commands do); omitting it
    makes firmware misparse the TLV and silently drop the write.
  - The packed FW region is larger than the vefuse payload (connac adds a
    per-region CRC and 16-byte alignment), so the region length is only
    sanity-checked as ">= vefuse_cap.size" and exactly vefuse_cap.size
    bytes are pushed, matching the firmware's expected size.

Applying the vefuse override is best-effort: any failure only warns and
is skipped, so it never fails firmware bring-up (the chip falls back to
its default eFuse values).

vefuse content is delivered inside the RAM firmware image and is not
separately verified by the driver (no MD5/hash/eFuse binding).

Also add a root-only, read-only debugfs "vefuse" node (created only when a
vefuse region is present in the RAM firmware image) that reads the vefuse
buffer back from firmware.

Signed-off-by: tony-tc.lee <[email protected]>
---
 .../wireless/mediatek/mt76/mt76_connac_mcu.h  |   2 +
 .../wireless/mediatek/mt76/mt7925/debugfs.c   |  52 ++++
 .../net/wireless/mediatek/mt76/mt7925/mcu.c   | 226 ++++++++++++++++++
 .../net/wireless/mediatek/mt76/mt7925/mcu.h   |  35 +++
 drivers/net/wireless/mediatek/mt76/mt792x.h   |   6 +
 5 files changed, 321 insertions(+)

diff --git a/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h b/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h
index 0ededa569e9a..9c844bef92c9 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h
+++ b/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h
@@ -44,6 +44,7 @@
 enum {
 	FW_TYPE_DEFAULT = 0,
 	FW_TYPE_CLC = 2,
+	FW_TYPE_EXTEND_BIN_EFUSE = 3,
 	FW_TYPE_MAX_NUM = 255
 };
 
@@ -1481,6 +1482,7 @@ enum {
 	MT_NIC_CAP_6G,
 	MT_NIC_CAP_CHIP_CAP = 0x20,
 	MT_NIC_CAP_EML_CAP = 0x22,
+	MT_NIC_CAP_VEFUSE_INFO = 0x44,
 };
 
 #define UNI_WOW_DETECT_TYPE_MAGIC		BIT(0)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/debugfs.c b/drivers/net/wireless/mediatek/mt76/mt7925/debugfs.c
index d01ff49de47a..34e261e247fc 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/debugfs.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/debugfs.c
@@ -286,6 +286,51 @@ static int mt7925_chip_reset(void *data, u64 val)
 
 DEFINE_DEBUGFS_ATTRIBUTE(fops_reset, NULL, mt7925_chip_reset, "%lld\n");
 
+static int
+mt7925_vefuse_read(struct seq_file *s, void *data)
+{
+	struct mt792x_dev *dev = dev_get_drvdata(s->private);
+	u16 size = dev->vefuse_cap.size;
+	int off = 0;
+	u8 *buf;
+	int ret = 0;
+
+	if (!dev->vefuse_cap.present || !size) {
+		seq_puts(s, "vefuse: no region in FW image\n");
+		return 0;
+	}
+
+	buf = kzalloc(size, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	mt792x_mutex_acquire(dev);
+	while (off < (int)size) {
+		u16 chunk = min_t(u16, 512, size - off);
+
+		ret = mt7925_mcu_read_vefuse(dev, off, chunk, buf + off);
+		if (ret)
+			break;
+		off += chunk;
+	}
+	mt792x_mutex_release(dev);
+
+	if (ret) {
+		seq_printf(s, "vefuse: read failed at 0x%04x (%d)\n", off, ret);
+		kfree(buf);
+		return 0;
+	}
+
+	seq_printf(s, "vefuse content: %u bytes\n", size);
+	for (off = 0; off < (int)size; off += 16)
+		seq_printf(s, "%04x: %*ph\n", off,
+			   (int)min_t(u16, 16, size - off), buf + off);
+
+	kfree(buf);
+
+	return 0;
+}
+
 int mt7925_init_debugfs(struct mt792x_dev *dev)
 {
 	struct dentry *dir;
@@ -309,6 +354,13 @@ int mt7925_init_debugfs(struct mt792x_dev *dev)
 	debugfs_create_file("idle-timeout", 0600, dir, dev,
 			    &fops_pm_idle_timeout);
 	debugfs_create_file("chip_reset", 0600, dir, dev, &fops_reset);
+	/* only expose the vefuse read node when a vefuse region was found in the
+	 * FW image (FW reports the capability unconditionally, so gate on the
+	 * actual region presence instead)
+	 */
+	if (dev->vefuse_cap.present)
+		debugfs_create_devm_seqfile(dev->mt76.dev, "vefuse", dir,
+					    mt7925_vefuse_read);
 	debugfs_create_devm_seqfile(dev->mt76.dev, "runtime_pm_stats", dir,
 				    mt792x_pm_stats);
 	debugfs_create_file("deep-sleep", 0600, dir, dev, &fops_ds);
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7925/mcu.c
index fa29c486a455..800d48bee110 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/mcu.c
@@ -871,6 +871,214 @@ static int mt7925_mcu_read_eeprom(struct mt792x_dev *dev, u32 offset, u8 *val)
 	return 0;
 }
 
+static int
+mt7925_mcu_send_vefuse_page(struct mt792x_dev *dev,
+			    const u8 *data, u32 total_len,
+			    int page_idx, int total_pages)
+{
+	struct mt7925_vefuse_page_cmd *cmd;
+	size_t offset = (size_t)page_idx * MT7925_VEFUSE_PAGE_SIZE;
+	u16 page_len = (u16)min_t(size_t,
+				  MT7925_VEFUSE_PAGE_SIZE,
+				  total_len - offset);
+	int ret;
+
+	cmd = kzalloc(sizeof(*cmd), GFP_KERNEL);
+	if (!cmd)
+		return -ENOMEM;
+
+	cmd->tag            = cpu_to_le16(UNI_EFUSE_BUFFER_MODE);
+	cmd->len            = cpu_to_le16(sizeof(*cmd) - sizeof(cmd->rsv));
+	cmd->source_mode    = UNI_EFUSE_SOURCE_VEFUSE;
+	cmd->content_format = FIELD_PREP(MT7925_VEFUSE_TOTAL_PAGE, total_pages) |
+			      FIELD_PREP(MT7925_VEFUSE_PAGE_INDEX, page_idx);
+	cmd->count          = cpu_to_le16(page_len);
+	memcpy(cmd->bin_content, data + offset, page_len);
+
+	ret = mt76_mcu_send_msg(&dev->mt76, MCU_UNI_CMD(EFUSE_CTRL),
+				cmd, sizeof(*cmd), true);
+	kfree(cmd);
+	return ret;
+}
+
+/* Read back vefuse content from FW (UNI EFUSE_CTRL query, BUFFER_RD tag).
+ * offset is the raw 0-based index into the FW vefuse buffer; source_mode
+ * selects the VEFUSE region. Exposed for the debugfs "vefuse" node.
+ */
+int mt7925_mcu_read_vefuse(struct mt792x_dev *dev, u16 offset, u16 count,
+			   u8 *out)
+{
+	struct {
+		u8 rsv[4];
+
+		__le16 tag;
+		__le16 len;
+
+		u8 source_mode;
+		u8 content_format;
+		__le16 offset;
+		__le16 count;
+		__le16 rsv1;
+	} __packed req = {
+		.tag = cpu_to_le16(UNI_EFUSE_BUFFER_RD),
+		.len = cpu_to_le16(sizeof(req) - sizeof(req.rsv)),
+		.source_mode = UNI_EFUSE_SOURCE_VEFUSE,
+		.offset = cpu_to_le16(offset),
+		.count = cpu_to_le16(count),
+	};
+	struct {
+		u8 rsv[4];
+
+		__le16 tag;
+		__le16 len;
+
+		u8 source_mode;
+		u8 content_format;
+		__le16 offset;
+		__le16 count;
+		u8 rsv1[2];
+		u8 data[];
+	} __packed * res;
+	struct sk_buff *skb;
+	int ret;
+
+	ret = mt76_mcu_send_and_get_msg(&dev->mt76, MCU_WM_UNI_CMD_QUERY(EFUSE_CTRL),
+					&req, sizeof(req), true, &skb);
+	if (ret)
+		return ret;
+
+	res = (void *)skb->data;
+	if (skb->len < sizeof(*res) + count) {
+		dev_kfree_skb(skb);
+		return -EINVAL;
+	}
+
+	memcpy(out, res->data, count);
+	dev_kfree_skb(skb);
+
+	return 0;
+}
+
+static int
+mt7925_load_vefuse_from_fw(struct mt792x_dev *dev, const char *fw_name)
+{
+	const struct mt76_connac2_fw_trailer *hdr;
+	const struct mt76_connac2_fw_region *region;
+	struct mt76_dev *mdev = &dev->mt76;
+	const struct firmware *fw = NULL;
+	const u8 *vefuse_base = NULL;
+	u32 offset = 0, len = 0, payload;
+	int i, total_pages, ret = 0;
+
+	if (!dev->vefuse_cap.enabled)
+		return 0;
+
+	/* recomputed on every FW load; set true only once a vefuse region is
+	 * found in the image below (the debugfs node is gated on this).
+	 */
+	dev->vefuse_cap.present = false;
+
+	ret = request_firmware(&fw, fw_name, mdev->dev);
+	if (ret) {
+		dev_warn(mdev->dev,
+			 "vefuse: firmware %s not available (%d), skipping\n",
+			 fw_name, ret);
+		ret = 0;
+		goto out;
+	}
+
+	if (!fw || !fw->data || fw->size < sizeof(*hdr)) {
+		dev_warn(mdev->dev, "vefuse: invalid firmware, skipping\n");
+		goto out;
+	}
+
+	hdr = (const void *)(fw->data + fw->size - sizeof(*hdr));
+
+	/* Region descriptors sit right before the trailer. Make sure the whole
+	 * descriptor array is within the image before dereferencing them, so a
+	 * corrupt n_region cannot walk the region pointer before fw->data.
+	 */
+	if (fw->size < sizeof(*hdr) + (size_t)hdr->n_region * sizeof(*region)) {
+		dev_warn(mdev->dev,
+			 "vefuse: firmware too small for %u region descriptors, skipping\n",
+			 hdr->n_region);
+		goto out;
+	}
+
+	for (i = 0; i < hdr->n_region; i++) {
+		region = (const void *)((const u8 *)hdr -
+					(hdr->n_region - i) * sizeof(*region));
+		len = le32_to_cpu(region->len);
+
+		if (len > fw->size - offset) {
+			dev_warn(mdev->dev, "vefuse: invalid firmware region, skipping\n");
+			goto out;
+		}
+
+		if ((region->feature_set & FW_FEATURE_NON_DL) &&
+		    region->type == FW_TYPE_EXTEND_BIN_EFUSE) {
+			vefuse_base = fw->data + offset;
+			dev->vefuse_cap.present = true;
+			break;
+		}
+		offset += len;
+	}
+
+	if (!vefuse_base) {
+		dev_warn(mdev->dev,
+			 "FW_TYPE_EXTEND_BIN_EFUSE chunk not found in FW image, virtual efuse skipped\n");
+		/* vefuse_cap.present stays false -> debugfs node not exposed on a
+		 * WF without a virtual efuse. vefuse_cap.enabled (the FW-reported
+		 * capability) is left untouched.
+		 */
+		goto out;
+	}
+
+	/* The packed FW region carries the vefuse payload plus connac
+	 * per-region framing (4-byte CRC + 16-byte alignment), so region
+	 * length is larger than the FW-reported payload. Only the leading
+	 * vefuse_cap.size bytes are the actual vefuse content to push.
+	 */
+	payload = dev->vefuse_cap.size;
+
+	/* virtual efuse is an optional override: if it cannot be applied the
+	 * chip still works on its default efuse values, so the checks below
+	 * only warn and skip - they must not fail firmware bring-up.
+	 */
+
+	/* [Check #1] region must hold at least the FW-reported payload */
+	if (len < payload) {
+		dev_warn(mdev->dev,
+			 "vefuse: region size %u smaller than FW capability %u, skipping\n",
+			 len, payload);
+		goto out;
+	}
+
+	total_pages = DIV_ROUND_UP(payload, MT7925_VEFUSE_PAGE_SIZE);
+	if (total_pages > MT7925_VEFUSE_MAX_PAGES) {
+		dev_warn(mdev->dev,
+			 "vefuse: payload too large: %u bytes (%d pages), max %d pages supported, skipping\n",
+			 payload, total_pages, MT7925_VEFUSE_MAX_PAGES);
+		goto out;
+	}
+
+	for (i = 0; i < total_pages; i++) {
+		ret = mt7925_mcu_send_vefuse_page(dev, vefuse_base, payload,
+						  i, total_pages);
+		if (ret) {
+			dev_warn(mdev->dev,
+				 "vefuse page %d/%d send failed: %d, skipping\n",
+				 i + 1, total_pages, ret);
+			ret = 0;
+			goto out;
+		}
+	}
+
+out:
+	release_firmware(fw);
+	return ret;
+}
+
 static int mt7925_load_clc(struct mt792x_dev *dev, const char *fw_name)
 {
 	const struct mt76_connac2_fw_trailer *hdr;
@@ -1126,6 +1334,17 @@ mt7925_mcu_get_nic_capability(struct mt792x_dev *dev)
 		case MT_NIC_CAP_EML_CAP:
 			mt7925_mcu_parse_eml_cap(dev, tlv->data);
 			break;
+		case MT_NIC_CAP_VEFUSE_INFO: {
+			struct mt7925_vefuse_cap *cap;
+
+			if (len < sizeof(*tlv) + sizeof(*cap))
+				break;
+
+			cap = (struct mt7925_vefuse_cap *)tlv->data;
+			dev->vefuse_cap.enabled   = cap->enabled;
+			dev->vefuse_cap.size      = le16_to_cpu(cap->size);
+			break;
+		}
 		default:
 			break;
 		}
@@ -1199,6 +1418,13 @@ int mt7925_run_firmware(struct mt792x_dev *dev)
 	if (err)
 		return err;
 
+	/* Virtual efuse is best-effort and currently never fails bring-up, but
+	 * keep the error propagation for a possible future fatal vefuse error.
+	 */
+	err = mt7925_load_vefuse_from_fw(dev, mt792x_ram_name(dev));
+	if (err)
+		return err;
+
 	err = mt7925_load_clc(dev, mt792x_ram_name(dev));
 	if (err)
 		return err;
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/mcu.h b/drivers/net/wireless/mediatek/mt76/mt7925/mcu.h
index 11f9eac13ffc..a0eb30d2570d 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/mcu.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/mcu.h
@@ -137,6 +137,41 @@ enum {
 	UNI_EFUSE_BUFFER_RD,
 };
 
+/* page size and content_format layout follow the FW buffer-mode ABI */
+#define MT7925_VEFUSE_PAGE_SIZE		1024	/* FW BUFFER_BIN_PAGE_SIZE (0x400) */
+#define MT7925_VEFUSE_MAX_PAGES		7	/* content_format page fields are 3-bit */
+
+#define MT7925_VEFUSE_TOTAL_PAGE	GENMASK(7, 5)	/* FW BUFFER_BIN_TOTAL_PAGE */
+#define MT7925_VEFUSE_PAGE_INDEX	GENMASK(4, 2)	/* FW BUFFER_BIN_PAGE_INDEX */
+
+/* buffer mode content source, aligns with FW BUFFER_SOURCE_MODE_INFO_* */
+enum {
+	UNI_EFUSE_SOURCE_EFUSE = 0,
+	UNI_EFUSE_SOURCE_EEPROM = 1,
+	UNI_EFUSE_SOURCE_VEFUSE = 2,
+};
+
+struct mt7925_vefuse_cap {
+	u8 version;
+	u8 enabled;
+	__le16 size;
+	__le32 hash_addr;
+} __packed;
+
+struct mt7925_vefuse_page_cmd {
+	u8 rsv[4];		/* UNI_CMD_EFUSE_CONTROL header before TLV */
+
+	__le16 tag;		/* UNI_EFUSE_BUFFER_MODE */
+	__le16 len;
+	u8 source_mode;		/* UNI_EFUSE_SOURCE_VEFUSE */
+	u8 content_format;	/* [7:5]=total_pages [4:2]=page_idx */
+	__le16 count;		/* valid bytes in this page */
+	u8 bin_content[MT7925_VEFUSE_PAGE_SIZE];
+} __packed;
+
+int mt7925_mcu_read_vefuse(struct mt792x_dev *dev, u16 offset, u16 count,
+			   u8 *out);
+
 enum {
 	UNI_CMD_ACCESS_REG_BASIC = 0x0,
 	UNI_CMD_ACCESS_RF_REG_BASIC,
diff --git a/drivers/net/wireless/mediatek/mt76/mt792x.h b/drivers/net/wireless/mediatek/mt76/mt792x.h
index 9efc251cb745..70a74e071b17 100644
--- a/drivers/net/wireless/mediatek/mt76/mt792x.h
+++ b/drivers/net/wireless/mediatek/mt76/mt792x.h
@@ -342,6 +342,12 @@ struct mt792x_dev {
 	struct ieee80211_vif *nan_vif;
 	const struct ieee80211_iface_combination *iface_combinations;
 	int n_iface_combinations;
+
+	struct {
+		bool enabled;	/* FW reports vefuse capability (unconditional) */
+		bool present;	/* a vefuse region was found in the FW image */
+		u16 size;
+	} vefuse_cap;
 };
 
 static inline struct mt792x_bss_conf *
-- 
2.45.2