[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 = ®->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