mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v1 0/1] fpga: m10bmc-sec: add image_load sysfs for N3000/D5005
@ 2026-07-15  7:38 Ionut Nechita (Wind River)
  2026-07-15  7:38 ` [PATCH v1 1/1] fpga: m10bmc-sec: add image_load sysfs for N3000 and D5005 Ionut Nechita (Wind River)
  0 siblings, 1 reply; 2+ messages in thread
From: Ionut Nechita (Wind River) @ 2026-07-15  7:38 UTC (permalink / raw)
  To: Xu Yilun
  Cc: Tom Rix, Moritz Fischer, Lee Jones, Ilpo Järvinen,
	Ionut Nechita, linux-fpga, mfd, linux-kernel

From: Ionut Nechita <ionut.nechita@windriver.com>

The mainline Intel MAX10 BMC secure-update driver can stage and flash a
new image through the firmware-upload ABI, but it has no way to trigger a
reload of an image that is already resident in FLASH or EEPROM. OPAE
userspace needs that trigger to switch between the BMC factory/user
images and to reload the PKVL retimer firmware without running a full
firmware-upload cycle.

This series adds the control/available_images and control/image_load
sysfs attributes for the N3000 and D5005 cards, restoring image-reload
parity with linux-dfl for those two boards:

  - N3000: bmc_factory, bmc_user, retimer_fw
  - D5005: bmc_factory, bmc_user (config-select polarity inverted vs
    N3000, hence a separate ops/handler table)

The control group is only visible when the device ops provide an
image_load table, so N6000 (.image_load = NULL) is unaffected. The N6000
PMCI and SDM image-load handlers are deliberately not included here; they
depend on the wider PMCI stack that has not been upstreamed yet.

The code is derived from the linux-dfl commits 604ad4f48cee and
e1b885714f68 by Russ Weight, Tianfei Zhang, Xu Yilun and Ilpo Järvinen,
adapted to the current mainline driver structure. New sysfs attributes
are documented under Documentation/ABI/testing/.

No functional change for N6000/PMCI/SDM devices. checkpatch --strict is
clean and the driver builds as a module on x86_64.

Ionut Nechita (1):
  fpga: m10bmc-sec: add image_load sysfs for N3000 and D5005

 .../sysfs-driver-intel-m10-bmc-sec-update     |  23 ++
 drivers/fpga/intel-m10-bmc-sec-update.c       | 286 +++++++++++++++++-
 include/linux/mfd/intel-m10-bmc.h             |  31 ++
 3 files changed, 339 insertions(+), 1 deletion(-)


base-commit: cc2b5f627e8ccbae1188ef2d8be3e451d7f933a5
-- 
2.55.0


^ permalink raw reply	[flat|nested] 2+ messages in thread

* [PATCH v1 1/1] fpga: m10bmc-sec: add image_load sysfs for N3000 and D5005
  2026-07-15  7:38 [PATCH v1 0/1] fpga: m10bmc-sec: add image_load sysfs for N3000/D5005 Ionut Nechita (Wind River)
@ 2026-07-15  7:38 ` Ionut Nechita (Wind River)
  0 siblings, 0 replies; 2+ messages in thread
From: Ionut Nechita (Wind River) @ 2026-07-15  7:38 UTC (permalink / raw)
  To: Xu Yilun
  Cc: Tom Rix, Moritz Fischer, Lee Jones, Ilpo Järvinen,
	Ionut Nechita, linux-fpga, mfd, linux-kernel, Russ Weight,
	Tianfei Zhang

From: Ionut Nechita <ionut.nechita@windriver.com>

The Intel MAX10 BMC secure-update driver in mainline exposes only the
firmware-upload ABI (/sys/class/firmware/secure-updateN) for staging and
flashing a new image. It has no way to trigger a reload of an image that
is already present in FLASH or EEPROM, which OPAE userspace relies on to
switch between the BMC factory/user images or to reload the PKVL retimer
firmware without a full firmware-upload cycle.

Add the control/available_images and control/image_load sysfs attributes
for the N3000 and D5005 cards. available_images returns the space
separated list of image key words accepted by the device, and writing
one of those key words to image_load triggers the corresponding reload:

 - bmc_factory / bmc_user reload a BMC image by writing DRBL_CONFIG_SEL
   and DRBL_REBOOT_REQ to the doorbell register. The config-select
   polarity is inverted between N3000 and D5005, so the two cards use
   separate handler tables and a separate m10sec_d5005_ops.

 - retimer_fw (N3000 only) reloads the PKVL retimer EEPROM by asserting
   DRBL_PKVL_EEPROM_LOAD_SEC, waiting for RSU_PROG_PKVL_PROM_DONE (or a
   RSU_STAT_PKVL_REJECT for a duplicate image), then polling
   M10BMC_PKVL_POLL_CTRL for preload completion.

The control group is hidden via its is_visible() callback whenever the
device ops do not provide an image_load table, so N6000 (which keeps
.image_load = NULL) is unaffected.

The M10BMC_PKVL_POLL_CTRL register and its bit definitions are added to
intel-m10-bmc.h for the retimer path.

This is a subset of the functionality carried in linux-dfl, ported to
the mainline driver structure. It is derived from the linux-dfl
commit 604ad4f48cee ("fpga: m10bmc-sec: add sysfs to load bmc images")
and commit e1b885714f68 ("fpga: m10bmc-sec: m10bmc_sec_retimer_load
callback") by Russ Weight, Tianfei Zhang, Xu Yilun and Ilpo Järvinen.
The N6000 PMCI and SDM image-load handlers are intentionally left out;
they depend on the wider PMCI stack that has not been upstreamed.

Cc: Russ Weight <russell.h.weight@intel.com>
Cc: Tianfei Zhang <tianfei.zhang@intel.com>
Cc: Xu Yilun <yilun.xu@intel.com>
Cc: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
Assisted-by: Claude:claude-opus-4-8 checkpatch
Signed-off-by: Ionut Nechita <ionut.nechita@windriver.com>
---
 .../sysfs-driver-intel-m10-bmc-sec-update     |  23 ++
 drivers/fpga/intel-m10-bmc-sec-update.c       | 286 +++++++++++++++++-
 include/linux/mfd/intel-m10-bmc.h             |  31 ++
 3 files changed, 339 insertions(+), 1 deletion(-)

diff --git a/Documentation/ABI/testing/sysfs-driver-intel-m10-bmc-sec-update b/Documentation/ABI/testing/sysfs-driver-intel-m10-bmc-sec-update
index 3a6ca780c75c..978ed68ff23e 100644
--- a/Documentation/ABI/testing/sysfs-driver-intel-m10-bmc-sec-update
+++ b/Documentation/ABI/testing/sysfs-driver-intel-m10-bmc-sec-update
@@ -59,3 +59,26 @@ Contact:	Matthew Gerlach <matthew.gerlach@altera.com>
 Description:	Read only. Returns number of times the secure update
 		staging area has been flashed.
 		Format: "%u".
+
+What:		/sys/bus/platform/drivers/intel-m10bmc-sec-update/.../control/available_images
+Date:		Jul 2026
+KernelVersion:	6.18
+Contact:	Ionut Nechita <ionut.nechita@windriver.com>
+Description:	Read only. Returns a space separated list of key words
+		that may be written into the image_load file described
+		below. These key words describe a BMC or retimer firmware
+		image in FLASH or EEPROM storage that may be reloaded.
+		This file is only visible if the underlying device
+		supports image reload.
+
+What:		/sys/bus/platform/drivers/intel-m10bmc-sec-update/.../control/image_load
+Date:		Jul 2026
+KernelVersion:	6.18
+Contact:	Ionut Nechita <ionut.nechita@windriver.com>
+Description:	Write only. A key word may be written to this file to
+		trigger a reload of a BMC or retimer firmware image from
+		FLASH or EEPROM. Refer to the available_images file for
+		the list of key words supported by the underlying device.
+		Writing an unsupported string to this file results in
+		-EINVAL being returned. This file is only visible if the
+		underlying device supports image reload.
diff --git a/drivers/fpga/intel-m10-bmc-sec-update.c b/drivers/fpga/intel-m10-bmc-sec-update.c
index 7d23d914df3f..7a390cd70516 100644
--- a/drivers/fpga/intel-m10-bmc-sec-update.c
+++ b/drivers/fpga/intel-m10-bmc-sec-update.c
@@ -15,8 +15,14 @@
 
 struct m10bmc_sec;
 
+struct image_load {
+	const char *name;
+	int (*load_image)(struct m10bmc_sec *sec);
+};
+
 struct m10bmc_sec_ops {
 	int (*rsu_status)(struct m10bmc_sec *sec);
+	struct image_load *image_load;		/* terminated with { } member */
 };
 
 struct m10bmc_sec {
@@ -253,8 +259,280 @@ static struct attribute_group m10bmc_security_attr_group = {
 	.attrs = m10bmc_security_attrs,
 };
 
+/* Image reload control (N3000 / D5005) */
+
+static int m10bmc_sec_bmc_image_load(struct m10bmc_sec *sec, unsigned int val)
+{
+	const struct m10bmc_csr_map *csr_map = sec->m10bmc->info->csr_map;
+	u32 doorbell;
+	int ret;
+
+	if (val > 1) {
+		dev_err(sec->dev, "%s invalid reload val = %d\n",
+			__func__, val);
+		return -EINVAL;
+	}
+
+	ret = m10bmc_sys_read(sec->m10bmc, csr_map->doorbell, &doorbell);
+	if (ret)
+		return ret;
+
+	if (doorbell & DRBL_REBOOT_DISABLED)
+		return -EBUSY;
+
+	return m10bmc_sys_update_bits(sec->m10bmc, csr_map->doorbell,
+				      DRBL_CONFIG_SEL | DRBL_REBOOT_REQ,
+				      FIELD_PREP(DRBL_CONFIG_SEL, val) |
+				      DRBL_REBOOT_REQ);
+}
+
+static int m10bmc_sec_bmc_image_load_0(struct m10bmc_sec *sec)
+{
+	return m10bmc_sec_bmc_image_load(sec, 0);
+}
+
+static int m10bmc_sec_bmc_image_load_1(struct m10bmc_sec *sec)
+{
+	return m10bmc_sec_bmc_image_load(sec, 1);
+}
+
+static int retimer_check_idle(struct m10bmc_sec *sec)
+{
+	const struct m10bmc_csr_map *csr_map = sec->m10bmc->info->csr_map;
+	u32 doorbell;
+	int ret;
+
+	ret = m10bmc_sys_read(sec->m10bmc, csr_map->doorbell, &doorbell);
+	if (ret)
+		return -EIO;
+
+	if (rsu_prog(doorbell) != RSU_PROG_IDLE &&
+	    rsu_prog(doorbell) != RSU_PROG_RSU_DONE &&
+	    rsu_prog(doorbell) != RSU_PROG_PKVL_PROM_DONE) {
+		dev_err(sec->dev, "Doorbell not idle: 0x%08x\n", doorbell);
+		return -EBUSY;
+	}
+
+	return 0;
+}
+
+static int trigger_retimer_eeprom_load(struct m10bmc_sec *sec)
+{
+	const struct m10bmc_csr_map *csr_map = sec->m10bmc->info->csr_map;
+	struct intel_m10bmc *m10bmc = sec->m10bmc;
+	unsigned int val;
+	int ret;
+
+	ret = m10bmc_sys_update_bits(m10bmc, csr_map->doorbell,
+				     DRBL_PKVL_EEPROM_LOAD_SEC,
+				     DRBL_PKVL_EEPROM_LOAD_SEC);
+	if (ret)
+		return ret;
+
+	/*
+	 * If the current NIOS FW is not bootloader, then the retimer load
+	 * is expected to trigger a NIOS reboot, so the DRBL_PKVL_EEPROM_LOAD
+	 * bit is cleared by the reboot. Otherwise wait for it to be cleared
+	 * by the NIOS FW.
+	 */
+	ret = regmap_read_poll_timeout(m10bmc->regmap,
+				       csr_map->base + csr_map->doorbell,
+				       val,
+				       (!(val & DRBL_PKVL_EEPROM_LOAD_SEC)),
+				       M10BMC_PKVL_LOAD_INTERVAL_US,
+				       M10BMC_PKVL_LOAD_TIMEOUT_US);
+	if (ret == -ETIMEDOUT) {
+		dev_err(sec->dev, "PKVL_EEPROM_LOAD clear timedout\n");
+		m10bmc_sys_update_bits(m10bmc, csr_map->doorbell,
+				       DRBL_PKVL_EEPROM_LOAD_SEC, 0);
+		ret = -ENODEV;
+	} else if (ret) {
+		dev_err(sec->dev, "poll EEPROM_LOAD error %d\n", ret);
+	}
+
+	return ret;
+}
+
+static int poll_retimer_eeprom_load_done(struct m10bmc_sec *sec)
+{
+	const struct m10bmc_csr_map *csr_map = sec->m10bmc->info->csr_map;
+	struct intel_m10bmc *m10bmc = sec->m10bmc;
+	unsigned int doorbell_reg;
+	int ret;
+
+	/*
+	 * RSU_STAT_PKVL_REJECT indicates that the current image is
+	 * already programmed. RSU_PROG_PKVL_PROM_DONE that the firmware
+	 * update process has finished, but does not necessarily indicate
+	 * a successful update.
+	 */
+	ret = regmap_read_poll_timeout(m10bmc->regmap,
+				       csr_map->base + csr_map->doorbell,
+				       doorbell_reg,
+				       (rsu_prog(doorbell_reg) ==
+					RSU_PROG_PKVL_PROM_DONE ||
+					FIELD_GET(DRBL_RSU_STATUS, doorbell_reg) ==
+					RSU_STAT_PKVL_REJECT),
+				       M10BMC_PKVL_PRELOAD_INTERVAL_US,
+				       M10BMC_PKVL_PRELOAD_TIMEOUT_US);
+	if (ret == -ETIMEDOUT) {
+		dev_err(sec->dev, "Doorbell check timedout: 0x%08x\n",
+			doorbell_reg);
+		return ret;
+	} else if (ret) {
+		dev_err(sec->dev, "poll Doorbell error %d\n", ret);
+		return ret;
+	}
+
+	if (FIELD_GET(DRBL_RSU_STATUS, doorbell_reg) == RSU_STAT_PKVL_REJECT) {
+		dev_err(sec->dev, "duplicate image rejected\n");
+		return -ECANCELED;
+	}
+
+	return 0;
+}
+
+static int poll_retimer_preload_done(struct m10bmc_sec *sec)
+{
+	const struct m10bmc_csr_map *csr_map = sec->m10bmc->info->csr_map;
+	struct intel_m10bmc *m10bmc = sec->m10bmc;
+	unsigned int val;
+	int ret;
+
+	/*
+	 * Wait for the updated firmware to be loaded by the PKVL device
+	 * and confirm that the applied version matches the expected value.
+	 */
+	ret = regmap_read_poll_timeout(m10bmc->regmap,
+				       csr_map->base + M10BMC_PKVL_POLL_CTRL,
+				       val,
+				       ((val & M10BMC_PKVL_PRELOAD) ==
+					M10BMC_PKVL_PRELOAD),
+				       M10BMC_PKVL_PRELOAD_INTERVAL_US,
+				       M10BMC_PKVL_PRELOAD_TIMEOUT_US);
+	if (ret) {
+		dev_err(sec->dev, "poll M10BMC_PKVL_PRELOAD error %d\n", ret);
+		return ret;
+	}
+
+	if ((val & M10BMC_PKVL_UPG_STATUS_MASK) != M10BMC_PKVL_UPG_STATUS_GOOD) {
+		dev_err(sec->dev, "error during M10BMC PKVL upgrade\n");
+		return -EIO;
+	}
+
+	return 0;
+}
+
+static int m10bmc_sec_retimer_eeprom_load(struct m10bmc_sec *sec)
+{
+	int ret;
+
+	ret = retimer_check_idle(sec);
+	if (ret)
+		return ret;
+
+	ret = trigger_retimer_eeprom_load(sec);
+	if (ret)
+		return ret;
+
+	ret = poll_retimer_eeprom_load_done(sec);
+	if (ret)
+		return ret;
+
+	return poll_retimer_preload_done(sec);
+}
+
+static struct image_load n3000_image_load_hndlrs[] = {
+	{
+		.name = "bmc_factory",
+		.load_image = m10bmc_sec_bmc_image_load_1,
+	},
+	{
+		.name = "bmc_user",
+		.load_image = m10bmc_sec_bmc_image_load_0,
+	},
+	{
+		.name = "retimer_fw",
+		.load_image = m10bmc_sec_retimer_eeprom_load,
+	},
+	{}
+};
+
+static struct image_load d5005_image_load_hndlrs[] = {
+	{
+		.name = "bmc_factory",
+		.load_image = m10bmc_sec_bmc_image_load_0,
+	},
+	{
+		.name = "bmc_user",
+		.load_image = m10bmc_sec_bmc_image_load_1,
+	},
+	{}
+};
+
+static ssize_t available_images_show(struct device *dev,
+				     struct device_attribute *attr, char *buf)
+{
+	struct m10bmc_sec *sec = dev_get_drvdata(dev);
+	const struct image_load *hndlr;
+	ssize_t count = 0;
+
+	for (hndlr = sec->ops->image_load; hndlr->name; hndlr++)
+		count += scnprintf(buf + count, PAGE_SIZE - count,
+				   "%s ", hndlr->name);
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+static DEVICE_ATTR_RO(available_images);
+
+static ssize_t image_load_store(struct device *dev,
+				struct device_attribute *attr,
+				const char *buf, size_t count)
+{
+	struct m10bmc_sec *sec = dev_get_drvdata(dev);
+	const struct image_load *hndlr;
+	int ret = -EINVAL;
+
+	for (hndlr = sec->ops->image_load; hndlr->name; hndlr++) {
+		if (sysfs_streq(buf, hndlr->name)) {
+			ret = hndlr->load_image(sec);
+			break;
+		}
+	}
+
+	return ret ? : count;
+}
+static DEVICE_ATTR_WO(image_load);
+
+static umode_t
+m10bmc_control_visible(struct kobject *kobj, struct attribute *attr, int n)
+{
+	struct m10bmc_sec *sec = dev_get_drvdata(kobj_to_dev(kobj));
+
+	if (!sec->ops->image_load)
+		return 0;
+
+	return attr->mode;
+}
+
+static struct attribute *m10bmc_control_attrs[] = {
+	&dev_attr_available_images.attr,
+	&dev_attr_image_load.attr,
+	NULL,
+};
+
+static struct attribute_group m10bmc_control_attr_group = {
+	.name = "control",
+	.attrs = m10bmc_control_attrs,
+	.is_visible = m10bmc_control_visible,
+};
+
 static const struct attribute_group *m10bmc_sec_attr_groups[] = {
 	&m10bmc_security_attr_group,
+	&m10bmc_control_attr_group,
 	NULL,
 };
 
@@ -675,6 +953,12 @@ static const struct fw_upload_ops m10bmc_ops = {
 
 static const struct m10bmc_sec_ops m10sec_n3000_ops = {
 	.rsu_status = m10bmc_sec_n3000_rsu_status,
+	.image_load = n3000_image_load_hndlrs,
+};
+
+static const struct m10bmc_sec_ops m10sec_d5005_ops = {
+	.rsu_status = m10bmc_sec_n3000_rsu_status,
+	.image_load = d5005_image_load_hndlrs,
 };
 
 static const struct m10bmc_sec_ops m10sec_n6000_ops = {
@@ -746,7 +1030,7 @@ static const struct platform_device_id intel_m10bmc_sec_ids[] = {
 	},
 	{
 		.name = "d5005bmc-sec-update",
-		.driver_data = (kernel_ulong_t)&m10sec_n3000_ops,
+		.driver_data = (kernel_ulong_t)&m10sec_d5005_ops,
 	},
 	{
 		.name = "n6000bmc-sec-update",
diff --git a/include/linux/mfd/intel-m10-bmc.h b/include/linux/mfd/intel-m10-bmc.h
index 988f1cd90032..bc9fafcbfe37 100644
--- a/include/linux/mfd/intel-m10-bmc.h
+++ b/include/linux/mfd/intel-m10-bmc.h
@@ -40,6 +40,37 @@
 #define M10BMC_N3000_VER_PCB_INFO_MSK	GENMASK(31, 24)
 #define M10BMC_N3000_VER_LEGACY_INVALID	0xffffffff
 
+/* PKVL (retimer) preload poll control register, in system register region */
+#define M10BMC_PKVL_POLL_CTRL		0x80
+#define M10BMC_PKVL_A_PRELOAD		BIT(16)
+#define M10BMC_PKVL_A_PRELOAD_TO	BIT(17)
+#define M10BMC_PKVL_A_DATA_TOO_BIG	BIT(18)
+#define M10BMC_PKVL_A_HDR_CKSUM		BIT(20)
+#define M10BMC_PKVL_B_PRELOAD		BIT(24)
+#define M10BMC_PKVL_B_PRELOAD_TO	BIT(25)
+#define M10BMC_PKVL_B_DATA_TOO_BIG	BIT(26)
+#define M10BMC_PKVL_B_HDR_CKSUM		BIT(28)
+
+#define M10BMC_PKVL_PRELOAD		(M10BMC_PKVL_A_PRELOAD | M10BMC_PKVL_B_PRELOAD)
+#define M10BMC_PKVL_PRELOAD_TIMEOUT	(M10BMC_PKVL_A_PRELOAD_TO | \
+					 M10BMC_PKVL_B_PRELOAD_TO)
+#define M10BMC_PKVL_DATA_TOO_BIG	(M10BMC_PKVL_A_DATA_TOO_BIG | \
+					 M10BMC_PKVL_B_DATA_TOO_BIG)
+#define M10BMC_PKVL_HDR_CHECKSUM	(M10BMC_PKVL_A_HDR_CKSUM | \
+					 M10BMC_PKVL_B_HDR_CKSUM)
+
+#define M10BMC_PKVL_UPG_STATUS_MASK	(M10BMC_PKVL_PRELOAD | M10BMC_PKVL_PRELOAD_TIMEOUT | \
+					 M10BMC_PKVL_DATA_TOO_BIG | M10BMC_PKVL_HDR_CHECKSUM)
+#define M10BMC_PKVL_UPG_STATUS_GOOD	(M10BMC_PKVL_PRELOAD | M10BMC_PKVL_HDR_CHECKSUM)
+
+/* interval 100ms, timeout 2s to trigger the PKVL EEPROM load */
+#define M10BMC_PKVL_LOAD_INTERVAL_US	(100 * 1000)
+#define M10BMC_PKVL_LOAD_TIMEOUT_US	(2 * 1000 * 1000)
+
+/* interval 100ms, timeout 30s for the PKVL preload to complete */
+#define M10BMC_PKVL_PRELOAD_INTERVAL_US	(100 * 1000)
+#define M10BMC_PKVL_PRELOAD_TIMEOUT_US	(30 * 1000 * 1000)
+
 /* Telemetry registers */
 #define M10BMC_N3000_TELEM_START	0x100
 #define M10BMC_N3000_TELEM_END		0x250
-- 
2.55.0


^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-07-15  7:39 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-07-15  7:38 [PATCH v1 0/1] fpga: m10bmc-sec: add image_load sysfs for N3000/D5005 Ionut Nechita (Wind River)
2026-07-15  7:38 ` [PATCH v1 1/1] fpga: m10bmc-sec: add image_load sysfs for N3000 and D5005 Ionut Nechita (Wind River)

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox