[PATCH 2/2] firmware: zynqmp: Add dynamic CSU register discovery and sysfs interface

Thangaraj, Senthil Nathan SenthilNathan.Thangaraj at amd.com
Thu Apr 23 21:39:17 PDT 2026


[AMD Official Use Only - AMD Internal Distribution Only]

> -----Original Message-----
> From: Ronak Jain <ronak.jain at amd.com>
> Sent: Wednesday, April 8, 2026 4:43 AM
> To: Simek, Michal <michal.simek at amd.com>; Thangaraj, Senthil Nathan
> <SenthilNathan.Thangaraj at amd.com>
> Cc: linux-kernel at vger.kernel.org; linux-arm-kernel at lists.infradead.org; Jain,
> Ronak <ronak.jain at amd.com>
> Subject: [PATCH 2/2] firmware: zynqmp: Add dynamic CSU register discovery
> and sysfs interface
>
> Add support for dynamically discovering and exposing Configuration Security
> Unit (CSU) registers through sysfs. Leverage the existing PM_QUERY_DATA
> API to discover available registers at runtime, making the interface flexible and
> maintainable.
>
> Key features:
> - Dynamic register discovery using PM_QUERY_DATA API
>   * PM_QID_GET_NODE_COUNT: Query number of available registers
>   * PM_QID_GET_NODE_NAME: Query register names by index
> - Automatic sysfs attribute creation under csu_registers/ group
> - Read operations via existing IOCTL_READ_REG API
> - Write operations via existing IOCTL_MASK_WRITE_REG API
> - Firmware-enforced access control (read-only registers reject writes)
>
> The sysfs interface is created at:
>   /sys/devices/platform/firmware:zynqmp-firmware/csu_registers/
>
> Currently supported registers include:
>   - multiboot (CSU_MULTI_BOOT)
>   - idcode (CSU_IDCODE, read-only)
>   - pcap-status (CSU_PCAP_STATUS, read-only)
>
> The dynamic discovery approach allows firmware to control which registers are
> exposed without requiring kernel changes, improving maintainability and
> security.
>
> Signed-off-by: Ronak Jain <ronak.jain at amd.com>
> ---
>  MAINTAINERS                              |  10 +
>  drivers/firmware/xilinx/Makefile         |   2 +-
>  drivers/firmware/xilinx/zynqmp-csu-reg.c | 249 +++++++++++++++++++++++
> drivers/firmware/xilinx/zynqmp-csu-reg.h |  18 ++
>  drivers/firmware/xilinx/zynqmp.c         |   6 +
>  include/linux/firmware/xlnx-zynqmp.h     |   4 +-
>  6 files changed, 287 insertions(+), 2 deletions(-)  create mode 100644
> drivers/firmware/xilinx/zynqmp-csu-reg.c
>  create mode 100644 drivers/firmware/xilinx/zynqmp-csu-reg.h
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 10d12b51b1f6..37fe2b7e0ccf 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -29212,6 +29212,16 @@ F:   drivers/dma/xilinx/xdma.c
>  F:   include/linux/dma/amd_xdma.h
>  F:   include/linux/platform_data/amd_xdma.h
>
> +XILINX ZYNQMP CSU REGISTER DRIVER
> +M:   Senthil Nathan Thangaraj <senthilnathan.thangaraj at amd.com>
> +R:   Michal Simek <michal.simek at amd.com>
> +R:   Ronak Jain <ronak.jain at amd.com>
> +L:   linux-arm-kernel at lists.infradead.org (moderated for non-subscribers)
> +S:   Maintained
> +F:   Documentation/ABI/stable/sysfs-driver-firmware-zynqmp
> +F:   drivers/firmware/xilinx/zynqmp-csu-reg.c
> +F:   drivers/firmware/xilinx/zynqmp-csu-reg.h
> +
>  XILINX ZYNQMP DPDMA DRIVER
>  M:   Laurent Pinchart <laurent.pinchart at ideasonboard.com>
>  L:   dmaengine at vger.kernel.org
> diff --git a/drivers/firmware/xilinx/Makefile b/drivers/firmware/xilinx/Makefile
> index 8db0e66b6b7e..6203f41daaa6 100644
> --- a/drivers/firmware/xilinx/Makefile
> +++ b/drivers/firmware/xilinx/Makefile
> @@ -1,5 +1,5 @@
>  # SPDX-License-Identifier: GPL-2.0
>  # Makefile for Xilinx firmwares
>
> -obj-$(CONFIG_ZYNQMP_FIRMWARE) += zynqmp.o zynqmp-ufs.o zynqmp-
> crypto.o
> +obj-$(CONFIG_ZYNQMP_FIRMWARE) += zynqmp.o zynqmp-ufs.o zynqmp-
> crypto.o
> +zynqmp-csu-reg.o
>  obj-$(CONFIG_ZYNQMP_FIRMWARE_DEBUG) += zynqmp-debug.o diff --git
> a/drivers/firmware/xilinx/zynqmp-csu-reg.c b/drivers/firmware/xilinx/zynqmp-csu-
> reg.c
> new file mode 100644
> index 000000000000..1f304ce858b1
> --- /dev/null
> +++ b/drivers/firmware/xilinx/zynqmp-csu-reg.c
> @@ -0,0 +1,249 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Xilinx Zynq MPSoC CSU Register Access
> + *
> + * Copyright (C) 2026 Advanced Micro Devices, Inc.
> + *
> + *  Michal Simek <michal.simek at amd.com>
> + *  Ronak Jain <ronak.jain at amd.com>
> + */
> +
> +#include <linux/firmware/xlnx-zynqmp.h> #include
> +<linux/platform_device.h> #include <linux/slab.h> #include
> +<linux/string.h>
> +
> +#include "zynqmp-csu-reg.h"
> +
> +/* Node ID for CSU module in firmware */ #define CSU_NODE_ID 0
> +
> +/* Maximum number of CSU registers supported */ #define MAX_CSU_REGS
> 50
> +
> +/* Size of register name returned by firmware (3 u32 words = 12 bytes)
> +*/ #define CSU_REG_NAME_LEN 12
> +
> +/**
> + * struct zynqmp_csu_reg - CSU register information
> + * @id: Register index from firmware
> + * @name: Register name
> + * @attr: Device attribute for sysfs
> + */
> +struct zynqmp_csu_reg {
> +     u32 id;
> +     char name[CSU_REG_NAME_LEN];
> +     struct device_attribute attr;
> +};
> +
> +/**
> + * struct zynqmp_csu_data - Per-device CSU data
> + * @csu_regs: Array of CSU registers
> + * @csu_reg_count: Number of CSU registers
> + * @csu_attr_group: Attribute group for sysfs  */ struct
> +zynqmp_csu_data {
> +     struct zynqmp_csu_reg *csu_regs;
> +     int csu_reg_count;
> +     struct attribute_group csu_attr_group; };
> +
> +/**
> + * zynqmp_pm_get_node_count() - Get number of supported nodes via
> +QUERY_DATA
> + *
> + * Return: Number of nodes on success, or negative error code  */
> +static int zynqmp_pm_get_node_count(void) {
> +     struct zynqmp_pm_query_data qdata = {0};
> +     u32 ret_payload[PAYLOAD_ARG_CNT];
> +     int ret;
> +
> +     qdata.qid = PM_QID_GET_NODE_COUNT;
> +
> +     ret = zynqmp_pm_query_data(qdata, ret_payload);
> +     if (ret)
> +             return ret;
> +
> +     return ret_payload[1];
> +}
> +
> +/**
> + * zynqmp_pm_get_node_name() - Get node name via QUERY_DATA
> + * @index: Register index
> + * @name: Buffer to store register name
> + *
> + * Return: 0 on success, error code otherwise  */ static int
> +zynqmp_pm_get_node_name(u32 index, char *name) {
> +     struct zynqmp_pm_query_data qdata = {0};
> +     u32 ret_payload[PAYLOAD_ARG_CNT];
> +     int ret;
> +
> +     qdata.qid = PM_QID_GET_NODE_NAME;
> +     qdata.arg1 = index;
> +
> +     ret = zynqmp_pm_query_data(qdata, ret_payload);
> +     if (ret)
> +             return ret;
> +
> +     memcpy(name, &ret_payload[1], CSU_REG_NAME_LEN);
> +     name[CSU_REG_NAME_LEN - 1] = '\0';
> +
> +     return 0;
> +}
> +
> +/**
> + * zynqmp_csu_reg_show() - Generic show function for all registers
> + * @dev: Device pointer
> + * @attr: Device attribute
> + * @buf: Output buffer
> + *
> + * Return: Number of bytes written to buffer, or error code  */ static
> +ssize_t zynqmp_csu_reg_show(struct device *dev,
> +                                struct device_attribute *attr,
> +                                char *buf)
> +{
> +     struct zynqmp_csu_reg *reg;
> +     u32 value;
> +     int ret;
> +
> +     /* Use container_of to get register directly */
> +     reg = container_of(attr, struct zynqmp_csu_reg, attr);
> +
> +     ret = zynqmp_pm_sec_read_reg(CSU_NODE_ID, reg->id, &value);
> +     if (ret)
> +             return ret;
> +
> +     return sysfs_emit(buf, "0x%08x\n", value); }
> +
> +/**
> + * zynqmp_csu_reg_store() - Generic store function for writable
> +registers
> + * @dev: Device pointer
> + * @attr: Device attribute
> + * @buf: Input buffer
> + * @count: Buffer size
> + *
> + * Format: "mask value" - both mask and value required
> + * Example: echo "0xFFFFFFFF 0x12345678" > register
> + *
> + * Return: count on success, error code otherwise  */ static ssize_t
> +zynqmp_csu_reg_store(struct device *dev,
> +                                 struct device_attribute *attr,
> +                                 const char *buf, size_t count)
> +{
> +     struct zynqmp_csu_reg *reg;
> +     u32 mask, value;
> +     int ret;
> +
> +     reg = container_of(attr, struct zynqmp_csu_reg, attr);
> +
> +     if (sscanf(buf, "%x %x", &mask, &value) != 2)
> +             return -EINVAL;
> +
> +     ret = zynqmp_pm_sec_mask_write_reg(CSU_NODE_ID, reg->id, mask,
> value);
> +     if (ret)
> +             return ret;
> +
> +     return count;
> +}
> +
> +/**
> + * zynqmp_csu_discover_registers() - Discover CSU registers from
> +firmware
> + * @pdev: Platform device pointer
> + *
> + * This function uses PM_QUERY_DATA to discover all available CSU
> +registers
> + * and creates sysfs group under
> +/sys/devices/platform/firmware:zynqmp-firmware/
> + *
> + * Return: 0 on success, error code otherwise  */ int
> +zynqmp_csu_discover_registers(struct platform_device *pdev) {
> +     struct zynqmp_csu_data *csu_data;
> +     struct attribute **attrs;
> +     int count, ret, i;
> +
> +     ret = zynqmp_pm_is_function_supported(PM_QUERY_DATA,
> PM_QID_GET_NODE_COUNT);
> +     if (ret) {
> +             dev_dbg(&pdev->dev, "CSU register discovery not supported by
> current firmware\n");
> +             return 0;
> +     }

Should we also call zynqmp_pm_is_function_supported(PM_QUERY_DATA, PM_QID_GET_NODE_NAME) to verify firmware support before proceeding?

> +
> +     count = zynqmp_pm_get_node_count();
> +     if (count < 0)
> +             return count;
> +     if (count == 0) {
> +             dev_dbg(&pdev->dev, "No nodes available from firmware\n");
> +             return 0;
> +     }
> +
> +     /* Validate count to prevent excessive memory allocation */
> +     if (count > MAX_CSU_REGS) {
> +             dev_err(&pdev->dev, "Register count %d exceeds maximum %d\n",
> +                     count, MAX_CSU_REGS);
> +             return -EINVAL;
> +     }
> +
> +     dev_dbg(&pdev->dev, "Discovered %d nodes from firmware\n", count);
> +
> +     csu_data = devm_kzalloc(&pdev->dev, sizeof(*csu_data), GFP_KERNEL);
> +     if (!csu_data)
> +             return -ENOMEM;
> +
> +     csu_data->csu_reg_count = count;
> +
> +     csu_data->csu_regs = devm_kcalloc(&pdev->dev, count,
> sizeof(*csu_data->csu_regs),
> +                                       GFP_KERNEL);
> +     if (!csu_data->csu_regs) {
> +             devm_kfree(&pdev->dev, csu_data);
> +             return -ENOMEM;
> +     }
> +
> +     attrs = devm_kcalloc(&pdev->dev, count + 1, sizeof(*attrs), GFP_KERNEL);
> +     if (!attrs) {
> +             devm_kfree(&pdev->dev, csu_data->csu_regs);
> +             devm_kfree(&pdev->dev, csu_data);
> +             return -ENOMEM;
> +     }
> +
> +     for (i = 0; i < count; i++) {
> +             struct zynqmp_csu_reg *reg = &csu_data->csu_regs[i];
> +             struct device_attribute *dev_attr = &reg->attr;
> +
> +             reg->id = i;
> +
> +             ret = zynqmp_pm_get_node_name(i, reg->name);
> +             if (ret) {
> +                     dev_warn(&pdev->dev, "Failed to get name for register %d\n",
> i);
> +                     snprintf(reg->name, sizeof(reg->name), "csu_reg_%d", i);
> +             }
> +
> +             /* Create sysfs attribute - firmware enforces actual access control */
> +             sysfs_attr_init(&dev_attr->attr);
> +             dev_attr->attr.name = reg->name;
> +             dev_attr->attr.mode = 0644;
> +             dev_attr->show = zynqmp_csu_reg_show;
> +             dev_attr->store = zynqmp_csu_reg_store;
> +
> +             attrs[i] = &dev_attr->attr;
> +
> +             dev_dbg(&pdev->dev, "Register %d: id=%d name=%s\n", i, reg->id,
> reg->name);
> +     }
> +
> +     csu_data->csu_attr_group.name = "csu_registers";
> +     csu_data->csu_attr_group.attrs = attrs;
> +
> +     ret = devm_device_add_group(&pdev->dev, &csu_data->csu_attr_group);
> +     if (ret) {
> +             devm_kfree(&pdev->dev, attrs);
> +             devm_kfree(&pdev->dev, csu_data->csu_regs);
> +             devm_kfree(&pdev->dev, csu_data);
> +     }
> +
> +     return ret;
> +}
> +EXPORT_SYMBOL_GPL(zynqmp_csu_discover_registers);
> diff --git a/drivers/firmware/xilinx/zynqmp-csu-reg.h
> b/drivers/firmware/xilinx/zynqmp-csu-reg.h
> new file mode 100644
> index 000000000000..b12415db3496
> --- /dev/null
> +++ b/drivers/firmware/xilinx/zynqmp-csu-reg.h
> @@ -0,0 +1,18 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +/*
> + * Xilinx Zynq MPSoC CSU Register Access
> + *
> + * Copyright (C) 2026 Advanced Micro Devices, Inc.
> + *
> + *  Michal Simek <michal.simek at amd.com>
> + *  Ronak Jain <ronak.jain at amd.com>
> + */
> +
> +#ifndef __ZYNQMP_CSU_REG_H__
> +#define __ZYNQMP_CSU_REG_H__
> +
> +#include <linux/platform_device.h>
> +
> +int zynqmp_csu_discover_registers(struct platform_device *pdev);
> +
> +#endif /* __ZYNQMP_CSU_REG_H__ */
> diff --git a/drivers/firmware/xilinx/zynqmp.c b/drivers/firmware/xilinx/zynqmp.c
> index fbe8510f4927..b549d07f7497 100644
> --- a/drivers/firmware/xilinx/zynqmp.c
> +++ b/drivers/firmware/xilinx/zynqmp.c
> @@ -27,6 +27,7 @@
>
>  #include <linux/firmware/xlnx-zynqmp.h>  #include <linux/firmware/xlnx-event-
> manager.h>
> +#include "zynqmp-csu-reg.h"
>  #include "zynqmp-debug.h"
>
>  /* Max HashMap Order for PM API feature check (1<<7 = 128) */ @@ -2120,6
> +2121,11 @@ static int zynqmp_firmware_probe(struct platform_device *pdev)
>                       dev_err_probe(&pdev->dev, PTR_ERR(em_dev), "EM register
> fail with error\n");
>       }
>
> +     /* Discover CSU registers dynamically */
> +     ret = zynqmp_csu_discover_registers(pdev);
> +     if (ret)
> +             dev_warn(&pdev->dev, "CSU register discovery failed: %d\n", ret);
> +
>       return of_platform_populate(dev->of_node, NULL, NULL, dev);  }
>
> diff --git a/include/linux/firmware/xlnx-zynqmp.h b/include/linux/firmware/xlnx-
> zynqmp.h
> index d70dcd462b44..a4b293eb96ce 100644
> --- a/include/linux/firmware/xlnx-zynqmp.h
> +++ b/include/linux/firmware/xlnx-zynqmp.h
> @@ -3,7 +3,7 @@
>   * Xilinx Zynq MPSoC Firmware layer
>   *
>   *  Copyright (C) 2014-2021 Xilinx
> - *  Copyright (C) 2022 - 2025 Advanced Micro Devices, Inc.
> + *  Copyright (C) 2022 - 2026 Advanced Micro Devices, Inc.
>   *
>   *  Michal Simek <michal.simek at amd.com>
>   *  Davorin Mista <davorin.mista at aggios.com> @@ -262,6 +262,8 @@ enum
> pm_query_id {
>       PM_QID_CLOCK_GET_NUM_CLOCKS = 12,
>       PM_QID_CLOCK_GET_MAX_DIVISOR = 13,
>       PM_QID_PINCTRL_GET_ATTRIBUTES = 15,
> +     PM_QID_GET_NODE_NAME = 16,
> +     PM_QID_GET_NODE_COUNT = 17,
>  };
>
>  enum rpu_oper_mode {
> --
> 2.34.1

Thanks,
Senthil



More information about the linux-arm-kernel mailing list