[PATCH v1 1/1] fpga: m10bmc-sec: add image_load sysfs for N3000 and D5005
"Ionut Nechita (Wind River)" <[email protected]> Wed, 15 Jul 2026 10:38:54 +0300
| Newsgroups | org.kernel.vger.linux-fpga,dev.linux.lists.mfd,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
From: Ionut Nechita <[email protected]> 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 <[email protected]> Cc: Tianfei Zhang <[email protected]> Cc: Xu Yilun <[email protected]> Cc: Ilpo Järvinen <[email protected]> Assisted-by: Claude:claude-opus-4-8 checkpatch Signed-off-by: Ionut Nechita <[email protected]> --- .../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 <[email protected]> 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 <[email protected]> +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 <[email protected]> +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