mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v2 0/7] Add RISC-V RPMI performance service support
@ 2026-10-08  9:10 Joshua Yeong
  2026-10-08  9:10 ` [PATCH v2 1/7] dt-bindings: dvfs: Add RPMI performance service message proxy bindings Joshua Yeong
                   ` (6 more replies)
  0 siblings, 7 replies; 9+ messages in thread
From: Joshua Yeong @ 2026-10-08  9:10 UTC (permalink / raw)
  To: robh, krzk+dt, conor+dt, pjw, palmer, aou, rafael, viresh.kumar,
	ulfh, rahul, anup, lftan.linux
  Cc: alex, joshua.yeong, linux-riscv, devicetree, linux-pm, linux-kernel

The RISC-V Platform Management Interface (RPMI) specification defines a
modular and extensible messaging protocol between the supervisor software
and a platform microcontroller (PuC). Among the service groups it defines
is the performance service group (service group ID 0x0000A), which allows
the supervisor to enumerate the performance domains managed by the PuC,
query their attributes and supported levels, and get/set their
performance level and limits, optionally through fast channels in shared
memory.

The specification says the service group is primarily meant for devices
such as GPUs and accelerators, though it can also be used for application
processors. This series adds supervisor-side support for both:

 - DT bindings for the performance domain controller exposed to the
   supervisor ("riscv,rpmi-performance") and for the SBI MPXY channel
   that the SBI implementation uses to expose the service group to the
   supervisor ("riscv,rpmi-mpxy-performance"). A CPU names its domain
   through "performance-domains", documented for RISC-V CPUs. Any other
   device names it through "power-domains": the controller is also a
   power domain provider, with one power domain per performance domain
   and the levels of the domain as the performance states of that power
   domain.

 - A core for the service group, drivers/firmware/riscv/
   riscv-rpmi-performance.c, which owns the mailbox channel, since the
   channel cannot be shared between drivers. At probe it checks the
   RPMI and service group versions, queries PERF_GET_NUM_DOMAINS, then
   for each domain PERF_GET_ATTRIBUTES and PERF_GET_SUPPORTED_LEVELS, and
   sets up the level fast channels from PERF_GET_FAST_CHANNEL_REGION and
   PERF_GET_FAST_CHANNEL_ATTRIBUTES. It exports an interface to read and
   set the levels and limits of a domain, and creates the devices the
   front-ends below bind to.

 - A cpufreq driver, drivers/cpufreq/riscv-rpmi-cpufreq.c. CPUs that
   name the same domain share a policy, the levels of the domain become
   its frequency table, and the level is set through the domain's fast
   channel when it has one, so that the governor can switch frequency
   from the scheduler. An energy model is registered from the power cost
   of each level.

 - A power domain driver, drivers/pmdomain/riscv/riscv-rpmi-perf-domain.c,
   which registers each performance domain as a generic power domain
   with performance states. A device that attaches is handed an
   operating point per level of the domain, and its driver drives the
   domain through the OPP library, with devfreq on top if it wants a
   governor. Several devices can share a domain, and genpd runs it at the
   highest level any of them asks for. Performance state N is RPMI level
   index N - 1, since genpd keeps state 0 for "no request". A device
   that names a domain the CPUs use is refused, so that cpufreq and genpd
   never both set the level of one domain.

 - A MAINTAINERS update adding the drivers and their bindings to the
   RPMI device power entry, renamed "RISC-V RPMI DEVICE POWER AND
   PERFORMANCE DRIVERS".

The series has a prerequisite: the RPMI device power series ("Add
RISC-V RPMI device power service support"), which has been applied for
next:

  https://lists.infradead.org/pipermail/linux-riscv/2026-September/099498.html

Changes in v2:
 - Split the driver. The RPMI protocol, the mailbox channel and the
   domain enumeration move into a core for the service group under
   drivers/firmware/riscv/, and the cpufreq driver becomes a front-end
   over it.
 - Add the power domain front-end, so that devices other than CPUs can
   use a performance domain. CPUs changes the performance through
   "performance-domains" and other devices through "power-domains".

v1: https://lore.kernel.org/r/20260106092117.3727152-1-joshua.yeong@starfivetech.com

Testing
=======

The series was tested under QEMU with the RPMI performance service
implemented in firmware.

Components:

 - OpenSBI: v1.9
   https://github.com/riscv-software-src/opensbi

 - QEMU: the RPMI-enabled tree at
   https://github.com/yeongjoshua/qemu/tree/rpmi-v11.1.0

Kernel config: enable CONFIG_RISCV_RPMI_PERFORMANCE,
CONFIG_RISCV_RPMI_CPUFREQ and CONFIG_RISCV_RPMI_PERF_DOMAIN (all default
y on RISC-V when MAILBOX is enabled) along with the SBI MPXY mailbox
driver.

Run with:

  qemu-system-riscv64 \
      -M virt -m 2G -smp 4 \
      -bios fw_dynamic.bin \
      -kernel Image \
      -M rpmi=true \
      -nographic \
      -initrd rootfs-busybox.cpio \
      -append "root=/dev/ram rw console=ttyS0,115200 no_console_suspend mem=2048M earlycon=uart8250,mmio,0x10000000"

The emulated PuC advertises seven performance domains. With -smp 4, cpu0
and cpu1 share one domain and one cpufreq policy, cpu2 has a domain of
its own, and cpu3 has none. A test device names one of the remaining
domains through "power-domains". Each policy's frequency table matches
the levels of its domain, and frequencies set through the userspace
governor read back from the PuC, with the level set through the fast
channel and its doorbell. A consumer test driver, kept out of this
series, attached the test device to its power domain, drove it through
the OPP library with devfreq on top, set every operating point and read
the level behind each one back from the PuC, checked that a level the
domain never advertised is refused, and ran alongside cpufreq without
either disturbing the other's domain. A device naming a CPU's domain was
refused, and so was a CPU naming another provider. With failures
simulated in the core (a domain that fails enumeration, a level table
returned in reverse order, a fast-channel region too small for its
channels), the affected domain or fast channel was left out or fell
back to the mailbox, as intended. All checks passed. The series also
builds for arm64 with COMPILE_TEST.

Joshua Yeong (7):
  dt-bindings: dvfs: Add RPMI performance service message proxy bindings
  dt-bindings: dvfs: Add RPMI performance service bindings
  dt-bindings: riscv: cpus: document performance-domains property
  firmware: riscv: Add RPMI performance service
  cpufreq: Add RISC-V RPMI cpufreq driver
  pmdomain: riscv: Add RPMI performance domains as power domains
  MAINTAINERS: Add RISC-V RPMI performance driver

 .../dvfs/riscv,rpmi-mpxy-performance.yaml     |   65 +
 .../bindings/dvfs/riscv,rpmi-performance.yaml |   88 ++
 .../devicetree/bindings/riscv/cpus.yaml       |    3 +
 MAINTAINERS                                   |    7 +-
 drivers/cpufreq/Kconfig                       |   16 +
 drivers/cpufreq/Makefile                      |    4 +
 drivers/cpufreq/riscv-rpmi-cpufreq.c          |  294 ++++
 drivers/firmware/Kconfig                      |    1 +
 drivers/firmware/Makefile                     |    1 +
 drivers/firmware/riscv/Kconfig                |   17 +
 drivers/firmware/riscv/Makefile               |    3 +
 .../firmware/riscv/riscv-rpmi-performance.c   | 1186 +++++++++++++++++
 drivers/pmdomain/riscv/Kconfig                |   16 +
 drivers/pmdomain/riscv/Makefile               |    1 +
 .../pmdomain/riscv/riscv-rpmi-perf-domain.c   |  265 ++++
 .../firmware/riscv/riscv-rpmi-performance.h   |  148 ++
 include/linux/mailbox/riscv-rpmi-message.h    |   16 +
 17 files changed, 2130 insertions(+), 1 deletion(-)
 create mode 100644 Documentation/devicetree/bindings/dvfs/riscv,rpmi-mpxy-performance.yaml
 create mode 100644 Documentation/devicetree/bindings/dvfs/riscv,rpmi-performance.yaml
 create mode 100644 drivers/cpufreq/riscv-rpmi-cpufreq.c
 create mode 100644 drivers/firmware/riscv/Kconfig
 create mode 100644 drivers/firmware/riscv/Makefile
 create mode 100644 drivers/firmware/riscv/riscv-rpmi-performance.c
 create mode 100644 drivers/pmdomain/riscv/riscv-rpmi-perf-domain.c
 create mode 100644 include/linux/firmware/riscv/riscv-rpmi-performance.h


base-commit: a90ee4305c4a5df72c11b31dacfdc76e00fcf78a
prerequisite-patch-id: 907ffadca74a65c93ad38f428e002d2b34341067
prerequisite-patch-id: 5a4df94e66de2f63697891a0f1382fabd9df7b6f
prerequisite-patch-id: 0827a7050d08fea7deaab8e82f2dd16acdca507d
--
2.43.0

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

* [PATCH v2 1/7] dt-bindings: dvfs: Add RPMI performance service message proxy bindings
  2026-10-08  9:10 [PATCH v2 0/7] Add RISC-V RPMI performance service support Joshua Yeong
@ 2026-10-08  9:10 ` Joshua Yeong
  2026-10-08  9:10 ` [PATCH v2 2/7] dt-bindings: dvfs: Add RPMI performance service bindings Joshua Yeong
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: Joshua Yeong @ 2026-10-08  9:10 UTC (permalink / raw)
  To: robh, krzk+dt, conor+dt, pjw, palmer, aou, rafael, viresh.kumar,
	ulfh, rahul, anup, lftan.linux
  Cc: alex, joshua.yeong, linux-riscv, devicetree, linux-pm, linux-kernel

Add device tree bindings for the RPMI performance service group based
message proxy implemented by the SBI implementation (machine mode
firmware or hypervisor).

The RPMI performance service group is defined by the RISC-V Platform
Management Interface (RPMI) specification.

Signed-off-by: Joshua Yeong <joshua.yeong@starfivetech.com>
---
 .../dvfs/riscv,rpmi-mpxy-performance.yaml     | 65 +++++++++++++++++++
 1 file changed, 65 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/dvfs/riscv,rpmi-mpxy-performance.yaml

diff --git a/Documentation/devicetree/bindings/dvfs/riscv,rpmi-mpxy-performance.yaml b/Documentation/devicetree/bindings/dvfs/riscv,rpmi-mpxy-performance.yaml
new file mode 100644
index 000000000000..4a05c48c1e12
--- /dev/null
+++ b/Documentation/devicetree/bindings/dvfs/riscv,rpmi-mpxy-performance.yaml
@@ -0,0 +1,65 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/dvfs/riscv,rpmi-mpxy-performance.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: RISC-V RPMI performance service group based message proxy
+
+maintainers:
+  - Joshua Yeong <joshua.yeong@starfivetech.com>
+
+description: |
+  The RISC-V Platform Management Interface (RPMI) [1] defines a modular and
+  extensible messaging protocol for platform management functions. The supervisor
+  software can send and receive RPMI messages via SBI MPXY extension [2]
+  or via a dedicated supervisor-mode RPMI transport.
+
+  The RPMI specification [1] defines performance service group (performance
+  domain) for accessing and controlling platform-managed performance-related
+  resources implemented by a platform microcontroller. The SBI implementation
+  (machine mode firmware or hypervisor) can implement an SBI MPXY channel
+  to allow supervisor software to access the RPMI performance service group.
+
+  ===========================================
+  References
+  ===========================================
+
+  [1] RISC-V Platform Management Interface (RPMI) v1.0 (or higher)
+      https://github.com/riscv-non-isa/riscv-rpmi/releases
+
+  [2] RISC-V Supervisor Binary Interface (SBI) v3.0 (or higher)
+      https://github.com/riscv-non-isa/riscv-sbi-doc/releases
+
+properties:
+  compatible:
+    description:
+      Intended for use by the SBI implementation.
+    const: riscv,rpmi-mpxy-performance
+
+  mboxes:
+    maxItems: 1
+    description:
+      Mailbox channel of the underlying RPMI transport.
+
+  riscv,sbi-mpxy-channel-id:
+    $ref: /schemas/types.yaml#/definitions/uint32
+    description:
+      The SBI MPXY channel id to be used for providing RPMI access to
+      the supervisor software.
+
+required:
+  - compatible
+  - mboxes
+  - riscv,sbi-mpxy-channel-id
+
+additionalProperties: false
+
+examples:
+  - |
+    performance {
+        compatible = "riscv,rpmi-mpxy-performance";
+        mboxes = <&rpmi_shmem_mbox 0x0a>;
+        riscv,sbi-mpxy-channel-id = <0x1003>;
+    };
+...
-- 
2.43.0


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

* [PATCH v2 2/7] dt-bindings: dvfs: Add RPMI performance service bindings
  2026-10-08  9:10 [PATCH v2 0/7] Add RISC-V RPMI performance service support Joshua Yeong
  2026-10-08  9:10 ` [PATCH v2 1/7] dt-bindings: dvfs: Add RPMI performance service message proxy bindings Joshua Yeong
@ 2026-10-08  9:10 ` Joshua Yeong
  2026-10-08 10:41   ` Conor Dooley
  2026-10-08  9:10 ` [PATCH v2 3/7] dt-bindings: riscv: cpus: document performance-domains property Joshua Yeong
                   ` (4 subsequent siblings)
  6 siblings, 1 reply; 9+ messages in thread
From: Joshua Yeong @ 2026-10-08  9:10 UTC (permalink / raw)
  To: robh, krzk+dt, conor+dt, pjw, palmer, aou, rafael, viresh.kumar,
	ulfh, rahul, anup, lftan.linux
  Cc: alex, joshua.yeong, linux-riscv, devicetree, linux-pm, linux-kernel

Add device tree bindings for the RPMI performance service group based
controller for the supervisor software.

A CPU names its performance domain through "performance-domains". Any
other device names it through "power-domains": the controller is also a
power domain provider, with one power domain per performance domain and
the levels the domain advertises as the performance states of that
power domain.

The RPMI performance service group is defined by the RISC-V Platform
Management Interface (RPMI) specification.

Signed-off-by: Joshua Yeong <joshua.yeong@starfivetech.com>
---
 .../bindings/dvfs/riscv,rpmi-performance.yaml | 88 +++++++++++++++++++
 1 file changed, 88 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/dvfs/riscv,rpmi-performance.yaml

diff --git a/Documentation/devicetree/bindings/dvfs/riscv,rpmi-performance.yaml b/Documentation/devicetree/bindings/dvfs/riscv,rpmi-performance.yaml
new file mode 100644
index 000000000000..ec7856cb7257
--- /dev/null
+++ b/Documentation/devicetree/bindings/dvfs/riscv,rpmi-performance.yaml
@@ -0,0 +1,88 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/dvfs/riscv,rpmi-performance.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: RISC-V RPMI performance service group
+
+maintainers:
+  - Joshua Yeong <joshua.yeong@starfivetech.com>
+
+description: |
+  The RISC-V Platform Management Interface (RPMI) [1] defines a modular and
+  extensible messaging protocol for platform management functions. The supervisor
+  software can send and receive RPMI messages via SBI MPXY extension [2]
+  or via a dedicated supervisor-mode RPMI transport.
+
+  The RPMI specification [1] defines performance service group (performance
+  domain) for accessing and controlling platform-managed performance-related
+  resources, as implemented by a platform microcontroller. Supervisor software
+  can interact with the RPMI performance service group through an SBI MPXY
+  channel or through a dedicated supervisor-mode RPMI transport.
+
+  The node is a performance domain provider. A CPU references its domain
+  through the generic "performance-domains" property described in
+  dvfs/performance-domain.yaml.
+
+  The node can also be a power domain provider, with one power domain for each
+  performance domain. Any other device that runs in one of the domains
+  references it through "power-domains", and each level the domain advertises
+  is a performance state of that power domain.
+
+  For example, with the provider labelled "performance", a CPU that runs in
+  domain 0 and a device that runs in domain 1 reference their domains as:
+
+    cpu@0 {
+        ...
+        performance-domains = <&performance 0>;
+    };
+
+    gpu@40000000 {
+        ...
+        power-domains = <&performance 1>;
+    };
+
+  ===========================================
+  References
+  ===========================================
+
+  [1] RISC-V Platform Management Interface (RPMI) v1.0 (or higher)
+      https://github.com/riscv-non-isa/riscv-rpmi/releases
+
+  [2] RISC-V Supervisor Binary Interface (SBI) v3.0 (or higher)
+      https://github.com/riscv-non-isa/riscv-sbi-doc/releases
+
+properties:
+  compatible:
+    description:
+      Intended for use by the supervisor software.
+    const: riscv,rpmi-performance
+
+  mboxes:
+    maxItems: 1
+    description:
+      Mailbox channel of the underlying RPMI transport or SBI message proxy channel.
+
+  "#performance-domain-cells":
+    const: 1
+
+  "#power-domain-cells":
+    const: 1
+
+required:
+  - compatible
+  - mboxes
+  - "#performance-domain-cells"
+
+additionalProperties: false
+
+examples:
+  - |
+    rpmi-performance {
+        compatible = "riscv,rpmi-performance";
+        mboxes = <&mpxy_mbox 0x1003 0x0>;
+        #performance-domain-cells = <0x01>;
+        #power-domain-cells = <0x01>;
+    };
+...
-- 
2.43.0


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

* [PATCH v2 3/7] dt-bindings: riscv: cpus: document performance-domains property
  2026-10-08  9:10 [PATCH v2 0/7] Add RISC-V RPMI performance service support Joshua Yeong
  2026-10-08  9:10 ` [PATCH v2 1/7] dt-bindings: dvfs: Add RPMI performance service message proxy bindings Joshua Yeong
  2026-10-08  9:10 ` [PATCH v2 2/7] dt-bindings: dvfs: Add RPMI performance service bindings Joshua Yeong
@ 2026-10-08  9:10 ` Joshua Yeong
  2026-10-08  9:10 ` [PATCH v2 4/7] firmware: riscv: Add RPMI performance service Joshua Yeong
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: Joshua Yeong @ 2026-10-08  9:10 UTC (permalink / raw)
  To: robh, krzk+dt, conor+dt, pjw, palmer, aou, rafael, viresh.kumar,
	ulfh, rahul, anup, lftan.linux
  Cc: alex, joshua.yeong, linux-riscv, devicetree, linux-pm, linux-kernel

A CPU names the performance domain it runs in through the generic
"performance-domains" property of dvfs/performance-domain.yaml, which
gives the property its type and serves any performance domain provider.
The RISC-V CPU binding only allows the properties it lists, so list it
there, with one domain per CPU as the Arm CPU binding does.

Signed-off-by: Joshua Yeong <joshua.yeong@starfivetech.com>
---
 Documentation/devicetree/bindings/riscv/cpus.yaml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/Documentation/devicetree/bindings/riscv/cpus.yaml b/Documentation/devicetree/bindings/riscv/cpus.yaml
index 0da219ae6769..f21f003575b6 100644
--- a/Documentation/devicetree/bindings/riscv/cpus.yaml
+++ b/Documentation/devicetree/bindings/riscv/cpus.yaml
@@ -144,6 +144,9 @@ properties:
       DMIPS/MHz, relative to highest capacity-dmips-mhz
       in the system.
 
+  performance-domains:
+    maxItems: 1
+
 anyOf:
   - required:
       - riscv,isa
-- 
2.43.0


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

* [PATCH v2 4/7] firmware: riscv: Add RPMI performance service
  2026-10-08  9:10 [PATCH v2 0/7] Add RISC-V RPMI performance service support Joshua Yeong
                   ` (2 preceding siblings ...)
  2026-10-08  9:10 ` [PATCH v2 3/7] dt-bindings: riscv: cpus: document performance-domains property Joshua Yeong
@ 2026-10-08  9:10 ` Joshua Yeong
  2026-10-08  9:10 ` [PATCH v2 5/7] cpufreq: Add RISC-V RPMI cpufreq driver Joshua Yeong
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: Joshua Yeong @ 2026-10-08  9:10 UTC (permalink / raw)
  To: robh, krzk+dt, conor+dt, pjw, palmer, aou, rafael, viresh.kumar,
	ulfh, rahul, anup, lftan.linux
  Cc: alex, joshua.yeong, linux-riscv, devicetree, linux-pm, linux-kernel

The RPMI specification defines a performance service group which can be
accessed via SBI MPXY extension or dedicated S-mode RPMI transport.

Add a mailbox client based core for the RISC-V RPMI performance service
group. It owns the mailbox channel, which cannot be shared, enumerates
the performance domains advertised by the platform microcontroller, and
exports an interface to read and set their levels and limits, through
the fast channel where a domain has one. The front-ends that drive the
domains build on it and are added separately.

Signed-off-by: Joshua Yeong <joshua.yeong@starfivetech.com>
---
 drivers/firmware/Kconfig                      |    1 +
 drivers/firmware/Makefile                     |    1 +
 drivers/firmware/riscv/Kconfig                |   17 +
 drivers/firmware/riscv/Makefile               |    3 +
 .../firmware/riscv/riscv-rpmi-performance.c   | 1105 +++++++++++++++++
 .../firmware/riscv/riscv-rpmi-performance.h   |  148 +++
 include/linux/mailbox/riscv-rpmi-message.h    |   16 +
 7 files changed, 1291 insertions(+)
 create mode 100644 drivers/firmware/riscv/Kconfig
 create mode 100644 drivers/firmware/riscv/Makefile
 create mode 100644 drivers/firmware/riscv/riscv-rpmi-performance.c
 create mode 100644 include/linux/firmware/riscv/riscv-rpmi-performance.h

diff --git a/drivers/firmware/Kconfig b/drivers/firmware/Kconfig
index b7cc11e4fbfa..cf0212576dfd 100644
--- a/drivers/firmware/Kconfig
+++ b/drivers/firmware/Kconfig
@@ -306,6 +306,7 @@ source "drivers/firmware/meson/Kconfig"
 source "drivers/firmware/microchip/Kconfig"
 source "drivers/firmware/psci/Kconfig"
 source "drivers/firmware/qcom/Kconfig"
+source "drivers/firmware/riscv/Kconfig"
 source "drivers/firmware/samsung/Kconfig"
 source "drivers/firmware/smccc/Kconfig"
 source "drivers/firmware/tegra/Kconfig"
diff --git a/drivers/firmware/Makefile b/drivers/firmware/Makefile
index be46f1e1dc77..879953c316dc 100644
--- a/drivers/firmware/Makefile
+++ b/drivers/firmware/Makefile
@@ -35,6 +35,7 @@ obj-y				+= efi/
 obj-y				+= imx/
 obj-y				+= psci/
 obj-y				+= qcom/
+obj-y				+= riscv/
 obj-y				+= samsung/
 obj-y				+= smccc/
 obj-y				+= tegra/
diff --git a/drivers/firmware/riscv/Kconfig b/drivers/firmware/riscv/Kconfig
new file mode 100644
index 000000000000..309f43bdb6d1
--- /dev/null
+++ b/drivers/firmware/riscv/Kconfig
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: GPL-2.0-only
+menu "RISC-V Firmware Drivers"
+	depends on RISCV || COMPILE_TEST
+
+config RISCV_RPMI_PERFORMANCE
+	tristate "RISC-V RPMI performance service group"
+	depends on RISCV || COMPILE_TEST
+	depends on MAILBOX
+	default RISCV
+	select PM_OPP
+	help
+	  Support for the performance service group defined by the RISC-V
+	  platform management interface (RPMI) specification. The performance
+	  domains advertised by the platform microcontroller are enumerated
+	  here and made available to the front-end drivers that control them.
+
+endmenu
diff --git a/drivers/firmware/riscv/Makefile b/drivers/firmware/riscv/Makefile
new file mode 100644
index 000000000000..4020f828704a
--- /dev/null
+++ b/drivers/firmware/riscv/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+obj-$(CONFIG_RISCV_RPMI_PERFORMANCE)	+= riscv-rpmi-performance.o
diff --git a/drivers/firmware/riscv/riscv-rpmi-performance.c b/drivers/firmware/riscv/riscv-rpmi-performance.c
new file mode 100644
index 000000000000..d44802f07ab0
--- /dev/null
+++ b/drivers/firmware/riscv/riscv-rpmi-performance.c
@@ -0,0 +1,1105 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * RISC-V RPMI performance service group core
+ *
+ * Copyright (C) 2026 Shanghai StarFive Technology Co., Ltd.
+ *
+ * Owns the MPXY mailbox channel of the RPMI performance service group and the
+ * enumeration of the performance domains behind it, and hands both to the
+ * front-ends that drive a domain. The channel cannot be shared, so it has a
+ * single owner here and the front-ends sit on top of it.
+ */
+
+#define pr_fmt(fmt) "riscv-rpmi-performance: " fmt
+
+#include <linux/bitfield.h>
+#include <linux/bsearch.h>
+#include <linux/firmware/riscv/riscv-rpmi-performance.h>
+#include <linux/io.h>
+#include <linux/mailbox/riscv-rpmi-message.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/platform_device.h>
+#include <linux/pm_opp.h>
+#include <linux/slab.h>
+#include <linux/sort.h>
+
+#define RPMI_PERF_DOMAIN_NAME_LEN	16
+
+struct rpmi_fc_db_info {
+	int width;
+	u32 set;
+	void __iomem *addr;
+};
+
+struct rpmi_fc_info {
+	void __iomem *set_addr;
+	void __iomem *get_addr;
+	struct rpmi_fc_db_info *set_db;
+};
+
+struct rpmi_ctx {
+	struct mbox_chan *chan;
+	struct mbox_client client;
+	/* serialises RPMI transactions on @chan */
+	struct mutex lock;
+	u32 max_msg_size;
+};
+
+struct rpmi_fast_channel {
+	u64 addr;
+	u64 size;
+};
+
+struct rpmi_perf_domain {
+	u32 id;
+	bool enumerated;
+	bool set_limit;
+	bool set_level;
+	bool has_fastchannels;
+	u32 level_count;
+	u32 rate_limit_us;
+	char name[RPMI_PERF_DOMAIN_NAME_LEN];
+	struct rpmi_perf_level *level;
+	struct rpmi_fc_info *fc_info;
+	struct rpmi_ctx *mpxy_ctx;
+	struct device *dev;
+	struct rpmi_perf *perf;
+};
+
+struct rpmi_perf {
+	u32 num_domains;
+	struct rpmi_fast_channel fast_channel;
+	struct device *dev;
+	struct rpmi_perf_domain *domain;
+};
+
+enum {
+	RPMI_PERF_FC_LEVEL,
+	RPMI_PERF_FC_MAX,
+};
+
+/* Service: ENABLE_NOTIFICATION */
+struct rpmi_perf_enable_notification_tx {
+	__le32 event_id;
+};
+
+struct rpmi_perf_enable_notification_rx {
+	__le32 status;
+};
+
+/* Service: GET_NUM_DOMAINS */
+struct rpmi_perf_get_num_domain_rx {
+	__le32 status;
+	__le32 num_domains;
+};
+
+/* Service: GET_ATTRS */
+struct rpmi_perf_get_attrs_tx {
+	__le32 domain_id;
+};
+
+struct rpmi_perf_get_attrs_rx {
+	__le32 status;
+	__le32 flags;
+#define PERF_LIMIT_SETTING(f)		(FIELD_GET(BIT(2), (f)))
+#define PERF_LEVEL_SETTING(f)		(FIELD_GET(BIT(1), (f)))
+#define FAST_CHANNEL_SUPPORT(f)		(FIELD_GET(BIT(0), (f)))
+	__le32 num_levels;
+	__le32 trans_latency_us;
+	char name[RPMI_PERF_DOMAIN_NAME_LEN];
+};
+
+/* Service: GET_SUPPORTED_LEVELS */
+struct rpmi_perf_get_supported_level_tx {
+	__le32 domain_id;
+	__le32 level_index;
+};
+
+struct rpmi_perf_supported_level {
+	__le32 index;
+	__le32 clock_freq;
+	__le32 power_cost;
+	__le32 trans_latency_us;
+};
+
+struct rpmi_perf_get_supported_level_rx {
+	__le32 status;
+	__le32 flags;
+	__le32 remaining;
+	__le32 returned;
+	struct rpmi_perf_supported_level level[];
+};
+
+/* Service: GET_LEVEL */
+struct rpmi_perf_get_level_tx {
+	__le32 domain_id;
+};
+
+struct rpmi_perf_get_level_rx {
+	__le32 status;
+	__le32 level_index;
+};
+
+/* Service: SET_LEVEL */
+struct rpmi_perf_set_level_tx {
+	__le32 domain_id;
+	__le32 level_index;
+};
+
+struct rpmi_perf_set_level_rx {
+	__le32 status;
+};
+
+/* Service: GET_LIMIT */
+struct rpmi_perf_get_limit_tx {
+	__le32 domain_id;
+};
+
+struct rpmi_perf_get_limit_rx {
+	__le32 status;
+	__le32 max_level;
+	__le32 min_level;
+};
+
+/* Service: SET_LIMIT */
+struct rpmi_perf_set_limit_tx {
+	__le32 domain_id;
+	__le32 max_level;
+	__le32 min_level;
+};
+
+struct rpmi_perf_set_limit_rx {
+	__le32 status;
+};
+
+/* Service: GET_FAST_CHANNEL_REGION */
+struct rpmi_perf_get_fast_channel_region_rx {
+	__le32 status;
+	__le32 addr_low;
+	__le32 addr_high;
+	__le32 size_low;
+	__le32 size_high;
+};
+
+/* Service: GET_FAST_CHANNEL_ATTRIBUTES */
+struct rpmi_perf_get_fast_channel_attributes_tx {
+	__le32 domain_id;
+	__le32 service_id;
+};
+
+struct rpmi_perf_get_fast_channel_attributes_rx {
+	__le32 status;
+	__le32 flags;
+#define DOORBELL_REG_WIDTH(f)		(FIELD_GET(GENMASK(2, 1), (f)))
+#define DOORBELL_REG_WIDTH_RESERVED	0x3
+#define SUPPORTS_DOORBELL(f)		(FIELD_GET(BIT(0), (f)))
+	__le32 offset_low;
+	__le32 offset_high;
+	__le32 size;
+	__le32 db_addr_low;
+	__le32 db_addr_high;
+	__le32 db_id;
+};
+
+static void rpmi_perf_fastchannel_db_ring(struct rpmi_fc_db_info *db)
+{
+	if (!db || !db->addr)
+		return;
+
+	switch (db->width) {
+	case 8:
+		iowrite8((u8)db->set, db->addr);
+		break;
+	case 16:
+		iowrite16((u16)db->set, db->addr);
+		break;
+	case 32:
+		iowrite32(db->set, db->addr);
+		break;
+	}
+}
+
+static int rpmi_perf_send_message(struct rpmi_ctx *mpxy_ctx,
+				  struct rpmi_mbox_message *msg)
+{
+	guard(mutex)(&mpxy_ctx->lock);
+
+	return rpmi_mbox_send_message(mpxy_ctx->chan, msg);
+}
+
+static int rpmi_perf_get_num_domains(struct rpmi_ctx *mpxy_ctx, u32 *domain)
+{
+	struct rpmi_perf_get_num_domain_rx rx = { };
+	struct rpmi_mbox_message msg;
+	int ret;
+
+	rpmi_mbox_init_send_with_response(&msg, RPMI_PERF_SRV_GET_NUM_DOMAINS,
+					  NULL, 0, &rx, sizeof(rx));
+	ret = rpmi_perf_send_message(mpxy_ctx, &msg);
+	if (ret)
+		return ret;
+
+	if (rx.status)
+		return rpmi_to_linux_error(le32_to_cpu(rx.status));
+
+	if (msg.data.out_response_len < sizeof(rx))
+		return -EPROTO;
+
+	*domain = le32_to_cpu(rx.num_domains);
+
+	return 0;
+}
+
+static int rpmi_perf_get_attrs(struct rpmi_perf *mpxy_perf, struct rpmi_perf_domain *perf_domain)
+{
+	struct rpmi_perf_get_attrs_tx tx;
+	struct rpmi_perf_get_attrs_rx rx = { };
+	struct rpmi_mbox_message msg;
+	u32 flags;
+	int ret;
+
+	tx.domain_id = cpu_to_le32(perf_domain->id);
+
+	rpmi_mbox_init_send_with_response(&msg, RPMI_PERF_SRV_GET_ATTRIBUTES,
+					  &tx, sizeof(tx), &rx, sizeof(rx));
+	ret = rpmi_perf_send_message(perf_domain->mpxy_ctx, &msg);
+	if (ret)
+		return ret;
+
+	if (rx.status)
+		return rpmi_to_linux_error(le32_to_cpu(rx.status));
+
+	if (msg.data.out_response_len < sizeof(rx))
+		return -EPROTO;
+
+	flags = le32_to_cpu(rx.flags);
+
+	perf_domain->set_limit = PERF_LIMIT_SETTING(flags);
+	perf_domain->set_level = PERF_LEVEL_SETTING(flags);
+	perf_domain->has_fastchannels = mpxy_perf->fast_channel.size &&
+					(perf_domain->set_limit || perf_domain->set_level) ?
+					FAST_CHANNEL_SUPPORT(flags) : false;
+	perf_domain->level_count = le32_to_cpu(rx.num_levels);
+	perf_domain->rate_limit_us = le32_to_cpu(rx.trans_latency_us);
+	strscpy(perf_domain->name, rx.name, RPMI_PERF_DOMAIN_NAME_LEN);
+
+	if (!perf_domain->level_count)
+		return -EINVAL;
+
+	perf_domain->level = devm_kcalloc(mpxy_perf->dev, perf_domain->level_count,
+					  sizeof(struct rpmi_perf_level), GFP_KERNEL);
+	if (!perf_domain->level)
+		return -ENOMEM;
+
+	return 0;
+}
+
+static int rpmi_perf_get_supported_levels(struct rpmi_perf_domain *perf_domain)
+{
+	struct rpmi_perf_get_supported_level_tx tx;
+	struct rpmi_perf_get_supported_level_rx *rx;
+	struct rpmi_mbox_message msg;
+	struct rpmi_perf_level *level;
+	u32 index = 0, remaining, returned;
+	int ret = 0;
+
+	/*
+	 * A reply carries as many levels as fit in one message, so size it
+	 * for the largest message the channel carries. max_msg_size is in
+	 * bytes, and was checked to hold at least one level.
+	 */
+	rx = kzalloc(perf_domain->mpxy_ctx->max_msg_size, GFP_KERNEL);
+	if (!rx)
+		return -ENOMEM;
+
+	do {
+		tx.domain_id = cpu_to_le32(perf_domain->id);
+		tx.level_index = cpu_to_le32(index);
+
+		rpmi_mbox_init_send_with_response(&msg, RPMI_PERF_SRV_GET_SUPPORTED_LEVELS,
+						  &tx, sizeof(tx), rx,
+						  perf_domain->mpxy_ctx->max_msg_size);
+		ret = rpmi_perf_send_message(perf_domain->mpxy_ctx, &msg);
+		if (ret)
+			goto exit;
+
+		if (rx->status) {
+			ret = rpmi_to_linux_error(le32_to_cpu(rx->status));
+			goto exit;
+		}
+
+		if (msg.data.out_response_len < sizeof(*rx)) {
+			ret = -EPROTO;
+			goto exit;
+		}
+
+		remaining = le32_to_cpu(rx->remaining);
+		returned = le32_to_cpu(rx->returned);
+
+		if (!returned || returned > perf_domain->level_count - index ||
+		    returned > (msg.data.out_response_len - sizeof(*rx)) /
+			       sizeof(rx->level[0]) ||
+		    remaining != perf_domain->level_count - index - returned) {
+			ret = -EPROTO;
+			goto exit;
+		}
+
+		for (u32 i = 0; i < returned; i++) {
+			level = &perf_domain->level[index + i];
+			level->index = le32_to_cpu(rx->level[i].index);
+			level->clock_freq = le32_to_cpu(rx->level[i].clock_freq);
+			level->power_cost = le32_to_cpu(rx->level[i].power_cost);
+			level->trans_latency_us = le32_to_cpu(rx->level[i].trans_latency_us);
+		}
+
+		index += returned;
+
+	} while (remaining);
+
+exit:
+	kfree(rx);
+
+	return ret;
+}
+
+static int rpmi_perf_get_fast_channel_regions(struct rpmi_ctx *mpxy_ctx, struct rpmi_perf *perf)
+{
+	struct rpmi_perf_get_fast_channel_region_rx rx = { };
+	struct rpmi_mbox_message msg;
+	int ret;
+
+	rpmi_mbox_init_send_with_response(&msg, RPMI_PERF_SRV_GET_FAST_CHANNEL_REGION,
+					  NULL, 0, &rx, sizeof(rx));
+	ret = rpmi_perf_send_message(mpxy_ctx, &msg);
+	if (ret)
+		return ret;
+
+	if (rx.status)
+		return rpmi_to_linux_error(le32_to_cpu(rx.status));
+
+	if (msg.data.out_response_len < sizeof(rx))
+		return -EPROTO;
+
+	perf->fast_channel.addr = le32_to_cpu(rx.addr_low) |
+				  ((u64)le32_to_cpu(rx.addr_high) << 32);
+	perf->fast_channel.size = le32_to_cpu(rx.size_low) |
+				  ((u64)le32_to_cpu(rx.size_high) << 32);
+
+	/* Every fast channel is checked against this region, so it must not wrap. */
+	if (perf->fast_channel.addr + perf->fast_channel.size < perf->fast_channel.addr) {
+		perf->fast_channel.size = 0;
+		return -EPROTO;
+	}
+
+	return 0;
+}
+
+static int rpmi_perf_extract_attributes(struct rpmi_perf_get_fast_channel_attributes_rx *rx,
+					struct rpmi_perf_domain *domain,
+					struct rpmi_fc_info *fc, u32 service_id)
+{
+	u64 region_size = domain->perf->fast_channel.size;
+	u64 phys_offset, phys_addr, db_phys_addr;
+	struct rpmi_fc_db_info *db = NULL;
+	void __iomem *addr, *db_addr;
+	u32 flags, size;
+	u8 db_addr_width;
+	int ret;
+
+	flags = le32_to_cpu(rx->flags);
+	phys_offset = le32_to_cpu(rx->offset_low);
+	phys_offset |= (u64)le32_to_cpu(rx->offset_high) << 32;
+	size = le32_to_cpu(rx->size);
+
+	/*
+	 * The fast channels all lie in the region PERF_GET_FAST_CHANNEL_REGION
+	 * describes, at an offset from its base, so refuse one that does not
+	 * fit in it or is too small for its payload, a 32-bit level.
+	 */
+	if (size < sizeof(u32) || phys_offset > region_size ||
+	    size > region_size - phys_offset)
+		return -EPROTO;
+
+	phys_addr = domain->perf->fast_channel.addr + phys_offset;
+
+	addr = devm_ioremap(domain->dev, phys_addr, size);
+	if (!addr) {
+		dev_err(domain->dev,
+			"failed to get fastchannel virtual addr in domain: %d\n",
+			domain->id);
+		return -EADDRNOTAVAIL;
+	}
+
+	switch (service_id) {
+	case RPMI_PERF_SRV_GET_LEVEL:
+		fc->get_addr = addr;
+		break;
+	case RPMI_PERF_SRV_SET_LEVEL:
+		if (SUPPORTS_DOORBELL(flags)) {
+			db = devm_kzalloc(domain->dev,
+					  sizeof(struct rpmi_fc_db_info),
+					  GFP_KERNEL);
+			if (!db) {
+				ret = -ENOMEM;
+				goto err_unmap;
+			}
+
+			db_addr_width = 1 << (DOORBELL_REG_WIDTH(flags) + 3);
+			db_phys_addr = le32_to_cpu(rx->db_addr_low);
+			db_phys_addr |= (u64)le32_to_cpu(rx->db_addr_high) << 32;
+
+			/* The doorbell can be any register; map only its width. */
+			db_addr = devm_ioremap(domain->dev, db_phys_addr,
+					       db_addr_width / BITS_PER_BYTE);
+			if (!db_addr) {
+				devm_kfree(domain->dev, db);
+				ret = -EADDRNOTAVAIL;
+				goto err_unmap;
+			}
+
+			db->addr = db_addr;
+			db->width = db_addr_width;
+			db->set = le32_to_cpu(rx->db_id);
+			fc->set_db = db;
+		}
+		fc->set_addr = addr;
+
+		break;
+	}
+
+	return 0;
+
+err_unmap:
+	/*
+	 * The platform asked for a doorbell, so without one a request would
+	 * never reach it. Leave the service on the mailbox.
+	 */
+	devm_iounmap(domain->dev, addr);
+
+	return ret;
+}
+
+static int rpmi_perf_get_fast_channel_attributes(struct rpmi_perf_domain *perf_domain,
+						 struct rpmi_fc_info *fc,
+						 u32 service_id)
+{
+	struct rpmi_perf_get_fast_channel_attributes_tx tx;
+	struct rpmi_perf_get_fast_channel_attributes_rx rx = { };
+	struct rpmi_mbox_message msg;
+	int ret;
+
+	tx.domain_id = cpu_to_le32(perf_domain->id);
+	tx.service_id = cpu_to_le32(service_id);
+
+	rpmi_mbox_init_send_with_response(&msg, RPMI_PERF_SRV_GET_FAST_CHANNEL_ATTRS,
+					  &tx, sizeof(tx), &rx, sizeof(rx));
+	ret = rpmi_perf_send_message(perf_domain->mpxy_ctx, &msg);
+	if (ret)
+		return ret;
+
+	if (rx.status)
+		return rpmi_to_linux_error(le32_to_cpu(rx.status));
+
+	/*
+	 * The doorbell words are unused without a doorbell, and a platform
+	 * may leave them out of the reply, so only require them with one.
+	 */
+	if (msg.data.out_response_len < offsetof(typeof(rx), db_addr_low) ||
+	    (SUPPORTS_DOORBELL(le32_to_cpu(rx.flags)) &&
+	     msg.data.out_response_len < sizeof(rx)))
+		return -EPROTO;
+
+	/* A doorbell of the reserved width could not be rung at all. */
+	if (SUPPORTS_DOORBELL(le32_to_cpu(rx.flags)) &&
+	    DOORBELL_REG_WIDTH(le32_to_cpu(rx.flags)) == DOORBELL_REG_WIDTH_RESERVED)
+		return -EPROTO;
+
+	ret = rpmi_perf_extract_attributes(&rx, perf_domain, fc, service_id);
+
+	return ret;
+}
+
+/*
+ * A service without a fast channel goes through the mailbox, so a fast channel
+ * that cannot be set up only costs speed. The platform may say it implements
+ * none; any other failure is reported, since nothing else would show it.
+ */
+static void rpmi_perf_setup_fast_channel(struct rpmi_perf_domain *domain,
+					 struct rpmi_fc_info *fc, u32 service_id)
+{
+	int ret;
+
+	ret = rpmi_perf_get_fast_channel_attributes(domain, fc, service_id);
+	if (ret && ret != -EOPNOTSUPP)
+		dev_warn(domain->dev,
+			 "perf domain #%u: no fast channel for service %#x: %d\n",
+			 domain->id, service_id, ret);
+}
+
+static int rpmi_perf_cmp_freq(const void *a, const void *b)
+{
+	const struct rpmi_perf_level *la = a, *lb = b;
+
+	return (la->clock_freq > lb->clock_freq) - (la->clock_freq < lb->clock_freq);
+}
+
+static int rpmi_perf_enumerate(struct rpmi_perf *mpxy_perf, struct rpmi_perf_domain *domain)
+{
+	struct rpmi_fc_info *fc;
+	int ret, id;
+
+	id = domain->id;
+
+	ret = rpmi_perf_get_attrs(mpxy_perf, domain);
+	if (ret) {
+		dev_err(domain->dev,
+			"Failed to get attributes of perf domain: #%u\n", id);
+		return ret;
+	}
+
+	ret = rpmi_perf_get_supported_levels(domain);
+	if (ret) {
+		dev_err(domain->dev,
+			"Failed to get supported level of perf domain: #%u\n", id);
+		return ret;
+	}
+
+	/*
+	 * The platform may return its levels in any order. A level index is a
+	 * point on the platform's performance scale, so a higher index is a
+	 * higher level, but the indices need not be contiguous. Keep the table
+	 * in ascending order of frequency, which the frequency lookup and every
+	 * front-end walking the levels rely on.
+	 */
+	sort(domain->level, domain->level_count, sizeof(domain->level[0]),
+	     rpmi_perf_cmp_freq, NULL);
+
+	if (domain->has_fastchannels) {
+		fc = devm_kcalloc(domain->dev, RPMI_PERF_FC_MAX, sizeof(*fc), GFP_KERNEL);
+		if (!fc)
+			return -ENOMEM;
+
+		if (domain->set_level)
+			rpmi_perf_setup_fast_channel(domain, &fc[RPMI_PERF_FC_LEVEL],
+						     RPMI_PERF_SRV_SET_LEVEL);
+
+		rpmi_perf_setup_fast_channel(domain, &fc[RPMI_PERF_FC_LEVEL],
+					     RPMI_PERF_SRV_GET_LEVEL);
+
+		/*
+		 * Limits always go through the mailbox, whose reply says
+		 * whether the platform took them, so their fast channels are
+		 * not set up.
+		 */
+
+		domain->fc_info = fc;
+	}
+
+	return 0;
+}
+
+/*
+ * Domain accessors. A consumer only ever sees an opaque handle, so the
+ * enumeration above stays private to this file.
+ */
+
+const char *rpmi_perf_domain_name(struct rpmi_perf_domain *pd)
+{
+	return pd->name;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_name);
+
+u32 rpmi_perf_domain_level_count(struct rpmi_perf_domain *pd)
+{
+	return pd->level_count;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_level_count);
+
+/**
+ * rpmi_perf_domain_level_info() - get one level of a performance domain
+ * @pd: performance domain
+ * @idx: position in the level table, from 0 to rpmi_perf_domain_level_count() - 1
+ * @level: filled in with the level
+ *
+ * The table is in ascending order of frequency. @idx is a position in it, not
+ * an RPMI level index, which is in @level->index.
+ *
+ * Return: 0 on success, -EINVAL if @idx is out of range.
+ */
+int rpmi_perf_domain_level_info(struct rpmi_perf_domain *pd, u32 idx,
+				struct rpmi_perf_level *level)
+{
+	if (idx >= pd->level_count)
+		return -EINVAL;
+
+	*level = pd->level[idx];
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_level_info);
+
+bool rpmi_perf_domain_can_set_level(struct rpmi_perf_domain *pd)
+{
+	return pd->set_level;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_can_set_level);
+
+bool rpmi_perf_domain_has_fast_channel(struct rpmi_perf_domain *pd)
+{
+	return pd->fc_info && pd->fc_info[RPMI_PERF_FC_LEVEL].set_addr;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_has_fast_channel);
+
+u32 rpmi_perf_domain_trans_latency_us(struct rpmi_perf_domain *pd)
+{
+	return pd->rate_limit_us;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_trans_latency_us);
+
+/**
+ * rpmi_perf_domain_by_id() - look up a performance domain
+ * @perf: performance service group
+ * @id: RPMI domain ID
+ *
+ * Return: the domain, or NULL if @id is out of range or the domain could not
+ * be enumerated. Such a domain has no level table to work from, so no
+ * consumer is ever handed one.
+ */
+struct rpmi_perf_domain *rpmi_perf_domain_by_id(struct rpmi_perf *perf, u32 id)
+{
+	if (!perf || id >= perf->num_domains || !perf->domain[id].enumerated)
+		return NULL;
+
+	return &perf->domain[id];
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_by_id);
+
+u32 rpmi_perf_num_domains(struct rpmi_perf *perf)
+{
+	return perf ? perf->num_domains : 0;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_num_domains);
+
+/**
+ * rpmi_perf_domain_get_level() - read the level a domain is running at
+ * @pd: performance domain
+ * @level: where the level index is stored
+ *
+ * Reads through the fast channel when the domain has one, and over the
+ * mailbox otherwise. Either way the value comes from the platform
+ * microcontroller rather than from a cached copy.
+ *
+ * Return: 0 on success, a negative errno otherwise.
+ */
+int rpmi_perf_domain_get_level(struct rpmi_perf_domain *pd, u32 *level)
+{
+	struct rpmi_perf_get_level_tx tx;
+	struct rpmi_perf_get_level_rx rx = { };
+	struct rpmi_mbox_message msg;
+	int ret;
+
+	if (pd->fc_info && pd->fc_info[RPMI_PERF_FC_LEVEL].get_addr) {
+		*level = ioread32(pd->fc_info[RPMI_PERF_FC_LEVEL].get_addr);
+		return 0;
+	}
+
+	tx.domain_id = cpu_to_le32(pd->id);
+
+	rpmi_mbox_init_send_with_response(&msg, RPMI_PERF_SRV_GET_LEVEL,
+					  &tx, sizeof(tx), &rx, sizeof(rx));
+	ret = rpmi_perf_send_message(pd->mpxy_ctx, &msg);
+	if (ret)
+		return ret;
+
+	if (rx.status)
+		return rpmi_to_linux_error(le32_to_cpu(rx.status));
+
+	if (msg.data.out_response_len < sizeof(rx))
+		return -EPROTO;
+
+	*level = le32_to_cpu(rx.level_index);
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_get_level);
+
+/**
+ * rpmi_perf_domain_set_level() - ask a domain to run at a level
+ * @pd: performance domain
+ * @level: level index, as advertised by the domain
+ *
+ * May sleep, because it goes over the mailbox. Use
+ * rpmi_perf_domain_set_level_fast() from a context that must not.
+ *
+ * Return: 0 on success, a negative errno otherwise.
+ */
+int rpmi_perf_domain_set_level(struct rpmi_perf_domain *pd, u32 level)
+{
+	struct rpmi_perf_set_level_tx tx;
+	struct rpmi_perf_set_level_rx rx = { };
+	struct rpmi_mbox_message msg;
+	int ret;
+
+	if (!pd->set_level)
+		return -EOPNOTSUPP;
+
+	tx.domain_id = cpu_to_le32(pd->id);
+	tx.level_index = cpu_to_le32(level);
+
+	rpmi_mbox_init_send_with_response(&msg, RPMI_PERF_SRV_SET_LEVEL,
+					  &tx, sizeof(tx), &rx, sizeof(rx));
+	ret = rpmi_perf_send_message(pd->mpxy_ctx, &msg);
+	if (ret)
+		return ret;
+
+	if (rx.status)
+		return rpmi_to_linux_error(le32_to_cpu(rx.status));
+
+	if (msg.data.out_response_len < sizeof(rx))
+		return -EPROTO;
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_set_level);
+
+/**
+ * rpmi_perf_domain_set_level_fast() - set a level without sleeping
+ * @pd: performance domain
+ * @level: level index, as advertised by the domain
+ *
+ * Writes the level through the domain's fast channel and rings its doorbell.
+ * The platform microcontroller applies it asynchronously and there is no
+ * status to read back, so this only reports whether the request was posted.
+ *
+ * Return: 0 on success, -EOPNOTSUPP if the domain has no fast channel.
+ */
+int rpmi_perf_domain_set_level_fast(struct rpmi_perf_domain *pd, u32 level)
+{
+	if (!rpmi_perf_domain_has_fast_channel(pd))
+		return -EOPNOTSUPP;
+
+	iowrite32(level, pd->fc_info[RPMI_PERF_FC_LEVEL].set_addr);
+	rpmi_perf_fastchannel_db_ring(pd->fc_info[RPMI_PERF_FC_LEVEL].set_db);
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_set_level_fast);
+
+int rpmi_perf_domain_get_limit(struct rpmi_perf_domain *pd, u32 *min, u32 *max)
+{
+	struct rpmi_perf_get_limit_tx tx;
+	struct rpmi_perf_get_limit_rx rx = { };
+	struct rpmi_mbox_message msg;
+	int ret;
+
+	tx.domain_id = cpu_to_le32(pd->id);
+
+	rpmi_mbox_init_send_with_response(&msg, RPMI_PERF_SRV_GET_LIMIT,
+					  &tx, sizeof(tx), &rx, sizeof(rx));
+	ret = rpmi_perf_send_message(pd->mpxy_ctx, &msg);
+	if (ret)
+		return ret;
+
+	if (rx.status)
+		return rpmi_to_linux_error(le32_to_cpu(rx.status));
+
+	if (msg.data.out_response_len < sizeof(rx))
+		return -EPROTO;
+
+	*max = le32_to_cpu(rx.max_level);
+	*min = le32_to_cpu(rx.min_level);
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_get_limit);
+
+int rpmi_perf_domain_set_limit(struct rpmi_perf_domain *pd, u32 min, u32 max)
+{
+	struct rpmi_perf_set_limit_tx tx;
+	struct rpmi_perf_set_limit_rx rx = { };
+	struct rpmi_mbox_message msg;
+	int ret;
+
+	if (!pd->set_limit)
+		return -EOPNOTSUPP;
+
+	tx.domain_id = cpu_to_le32(pd->id);
+	tx.max_level = cpu_to_le32(max);
+	tx.min_level = cpu_to_le32(min);
+
+	rpmi_mbox_init_send_with_response(&msg, RPMI_PERF_SRV_SET_LIMIT,
+					  &tx, sizeof(tx), &rx, sizeof(rx));
+	ret = rpmi_perf_send_message(pd->mpxy_ctx, &msg);
+	if (ret)
+		return ret;
+
+	if (rx.status)
+		return rpmi_to_linux_error(le32_to_cpu(rx.status));
+
+	if (msg.data.out_response_len < sizeof(rx))
+		return -EPROTO;
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_set_limit);
+
+static int rpmi_perf_search_freq(const void *id, const void *level)
+{
+	u32 khz = *(const u32 *)id;
+	u32 freq = ((const struct rpmi_perf_level *)level)->clock_freq;
+
+	return (khz > freq) - (khz < freq);
+}
+
+int rpmi_perf_domain_level_to_freq(struct rpmi_perf_domain *pd, u32 level, u32 *khz)
+{
+	/* The table is sorted by frequency, not by level index. */
+	for (u32 i = 0; i < pd->level_count; i++) {
+		if (pd->level[i].index == level) {
+			*khz = pd->level[i].clock_freq;
+			return 0;
+		}
+	}
+
+	return -EINVAL;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_level_to_freq);
+
+int rpmi_perf_domain_freq_to_level(struct rpmi_perf_domain *pd, u32 khz, u32 *level)
+{
+	struct rpmi_perf_level *found;
+
+	found = bsearch(&khz, pd->level, pd->level_count,
+			sizeof(pd->level[0]), rpmi_perf_search_freq);
+	if (!found)
+		return -EINVAL;
+
+	*level = found->index;
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_freq_to_level);
+
+/**
+ * rpmi_perf_domain_opps_add() - give @dev an OPP for every level of @pd
+ * @pd: performance domain
+ * @dev: device that runs in the domain
+ *
+ * The levels come from the platform microcontroller, so there is no
+ * operating-points-v2 table in the device tree to parse. The OPP frequency is
+ * the level's clock frequency in Hz and the OPP level is the RPMI level
+ * index, which is what the set and get services take.
+ *
+ * Return: 0 on success, a negative errno otherwise.
+ */
+int rpmi_perf_domain_opps_add(struct rpmi_perf_domain *pd, struct device *dev)
+{
+	struct dev_pm_opp_data data = {};
+	struct rpmi_perf_level *level;
+	int idx, ret;
+
+	for (level = pd->level, idx = 0; idx < pd->level_count; idx++, level++) {
+		/* Frequency from RPMI is in kHz */
+		data.freq = (unsigned long)level->clock_freq * 1000;
+		data.level = level->index;
+
+		ret = dev_pm_opp_add_dynamic(dev, &data);
+		if (ret) {
+			dev_warn(dev, "failed to add opp %luHz\n", data.freq);
+
+			while (idx-- > 0) {
+				/* Frequency from RPMI is in kHz */
+				dev_pm_opp_remove(dev,
+						  (unsigned long)(--level)->clock_freq * 1000);
+			}
+			return ret;
+		}
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(rpmi_perf_domain_opps_add);
+
+static int rpmi_perf_attr_setup(struct device *dev, struct rpmi_ctx *mpxy_ctx)
+{
+	struct rpmi_mbox_message msg;
+	int ret;
+
+	/* Validate RPMI specification version */
+	rpmi_mbox_init_get_attribute(&msg, RPMI_MBOX_ATTR_SPEC_VERSION);
+	ret = rpmi_perf_send_message(mpxy_ctx, &msg);
+	if (ret) {
+		dev_dbg(dev, "Failed to get spec version\n");
+		return ret;
+	}
+
+	if (msg.attr.value < RPMI_MKVER(1, 0)) {
+		dev_dbg(dev,
+			"msg protocol version mismatch, expected 0x%x, found 0x%x\n",
+			RPMI_MKVER(1, 0), msg.attr.value);
+		return -EINVAL;
+	}
+
+	/* Validate performance service group ID */
+	rpmi_mbox_init_get_attribute(&msg, RPMI_MBOX_ATTR_SERVICEGROUP_ID);
+	ret = rpmi_perf_send_message(mpxy_ctx, &msg);
+	if (ret) {
+		dev_dbg(dev, "Failed to get service group ID\n");
+		return ret;
+	}
+
+	if (msg.attr.value != RPMI_SRVGRP_PERFORMANCE) {
+		dev_dbg(dev,
+			"service group match failed, expected 0x%x, found 0x%x\n",
+			RPMI_SRVGRP_PERFORMANCE, msg.attr.value);
+		return -EINVAL;
+	}
+
+	/* Validate performance service group version */
+	rpmi_mbox_init_get_attribute(&msg, RPMI_MBOX_ATTR_SERVICEGROUP_VERSION);
+	ret = rpmi_perf_send_message(mpxy_ctx, &msg);
+	if (ret) {
+		dev_dbg(dev, "Failed to get service group version\n");
+		return ret;
+	}
+
+	if (msg.attr.value < RPMI_MKVER(1, 0)) {
+		dev_dbg(dev,
+			"service group version failed, expected 0x%x, found 0x%x\n",
+			RPMI_MKVER(1, 0), msg.attr.value);
+		return -EINVAL;
+	}
+
+	/* Get max message size */
+	rpmi_mbox_init_get_attribute(&msg, RPMI_MBOX_ATTR_MAX_MSG_DATA_SIZE);
+	ret = rpmi_perf_send_message(mpxy_ctx, &msg);
+	if (ret) {
+		dev_dbg(dev, "Failed to get max message data size\n");
+		return ret;
+	}
+
+	if (msg.attr.value < sizeof(struct rpmi_perf_get_supported_level_rx) +
+			     sizeof(struct rpmi_perf_supported_level)) {
+		dev_dbg(dev, "max message data size %u too small\n",
+			msg.attr.value);
+		return -EINVAL;
+	}
+	mpxy_ctx->max_msg_size = msg.attr.value;
+
+	return 0;
+}
+
+static void rpmi_perf_mbox_chan_release(void *data)
+{
+	mbox_free_channel((struct mbox_chan *)data);
+}
+
+static int rpmi_perf_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct rpmi_perf *mpxy_perf;
+	struct rpmi_ctx *mpxy_ctx;
+	u32 num_domains = 0;
+	int ret;
+	u32 i;
+
+	mpxy_ctx = devm_kzalloc(dev, sizeof(*mpxy_ctx), GFP_KERNEL);
+	if (!mpxy_ctx)
+		return -ENOMEM;
+
+	ret = devm_mutex_init(dev, &mpxy_ctx->lock);
+	if (ret)
+		return ret;
+
+	/* Setup mailbox client */
+	mpxy_ctx->client.dev		= dev;
+	mpxy_ctx->client.rx_callback	= NULL;
+	mpxy_ctx->client.tx_block	= false;
+	mpxy_ctx->client.knows_txdone	= true;
+	mpxy_ctx->client.tx_tout	= 0;
+
+	/* Request mailbox channel */
+	mpxy_ctx->chan = mbox_request_channel(&mpxy_ctx->client, 0);
+	if (IS_ERR(mpxy_ctx->chan))
+		return PTR_ERR(mpxy_ctx->chan);
+
+	ret = devm_add_action_or_reset(dev, rpmi_perf_mbox_chan_release,
+				       mpxy_ctx->chan);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to add rpmi mbox channel cleanup\n");
+
+	ret = rpmi_perf_attr_setup(dev, mpxy_ctx);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to verify RPMI attribute\n");
+
+	/* Get number of performance domain */
+	ret = rpmi_perf_get_num_domains(mpxy_ctx, &num_domains);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "invalid number of perf domains\n");
+
+	if (!num_domains)
+		return dev_err_probe(dev, -EINVAL, "No perf domains found!\n");
+
+	mpxy_perf = devm_kzalloc(dev, sizeof(*mpxy_perf), GFP_KERNEL);
+	if (!mpxy_perf)
+		return -ENOMEM;
+
+	mpxy_perf->domain = devm_kcalloc(dev, num_domains,
+					 sizeof(struct rpmi_perf_domain),
+					 GFP_KERNEL);
+	if (!mpxy_perf->domain)
+		return -ENOMEM;
+
+	mpxy_perf->num_domains = num_domains;
+	mpxy_perf->dev = dev;
+
+	/*
+	 * Without a fast-channel region every domain goes through the mailbox.
+	 * The platform may implement none; any other failure is reported.
+	 */
+	ret = rpmi_perf_get_fast_channel_regions(mpxy_ctx, mpxy_perf);
+	if (ret && ret != -EOPNOTSUPP)
+		dev_warn(dev, "no fast channel region, using the mailbox: %d\n", ret);
+
+	for (i = 0; i < num_domains; i++) {
+		struct rpmi_perf_domain *domain = &mpxy_perf->domain[i];
+
+		domain->id = i;
+		domain->mpxy_ctx = mpxy_ctx;
+		domain->dev = dev;
+		domain->perf = mpxy_perf;
+
+		domain->enumerated = !rpmi_perf_enumerate(mpxy_perf, domain);
+	}
+
+	dev_set_drvdata(dev, mpxy_perf);
+
+	dev_info(dev, "%d MPXY performance domains registered\n", num_domains);
+
+	return 0;
+}
+
+static const struct of_device_id rpmi_perf_of_match[] = {
+	{ .compatible = "riscv,rpmi-performance" },
+	{ },
+};
+
+MODULE_DEVICE_TABLE(of, rpmi_perf_of_match);
+
+static struct platform_driver rpmi_perf_platdrv = {
+	.driver = {
+		.name = "riscv-rpmi-performance",
+		.of_match_table = rpmi_perf_of_match,
+	},
+	.probe = rpmi_perf_probe,
+};
+
+module_platform_driver(rpmi_perf_platdrv);
+
+MODULE_AUTHOR("Joshua Yeong <joshua.yeong@starfivetech.com>");
+MODULE_DESCRIPTION("RISC-V RPMI performance service group core");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/firmware/riscv/riscv-rpmi-performance.h b/include/linux/firmware/riscv/riscv-rpmi-performance.h
new file mode 100644
index 000000000000..c65ea924b75d
--- /dev/null
+++ b/include/linux/firmware/riscv/riscv-rpmi-performance.h
@@ -0,0 +1,148 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * RISC-V RPMI performance service group interface
+ *
+ * Copyright (C) 2026 Shanghai StarFive Technology Co., Ltd.
+ *
+ * The RPMI performance service group is a provider of performance domains.
+ * The front-ends that drive a domain reach the platform microcontroller
+ * through this interface, so that the mailbox channel and the domain
+ * enumeration have a single owner.
+ */
+
+#ifndef _LINUX_RISCV_RPMI_PERFORMANCE_H_
+#define _LINUX_RISCV_RPMI_PERFORMANCE_H_
+
+#include <linux/types.h>
+
+struct device;
+struct rpmi_perf;
+struct rpmi_perf_domain;
+
+/**
+ * struct rpmi_perf_level - one level of a performance domain
+ *
+ * @index: level index, which is what the RPMI services take and return
+ * @clock_freq: clock frequency of the level, in kHz
+ * @power_cost: power cost of the level, in uW
+ * @trans_latency_us: worst case time to switch to this level, in us
+ */
+struct rpmi_perf_level {
+	u32 index;
+	u32 clock_freq;
+	u32 power_cost;
+	u32 trans_latency_us;
+};
+
+#if IS_ENABLED(CONFIG_RISCV_RPMI_PERFORMANCE)
+
+struct rpmi_perf_domain *rpmi_perf_domain_by_id(struct rpmi_perf *perf, u32 id);
+u32 rpmi_perf_num_domains(struct rpmi_perf *perf);
+
+const char *rpmi_perf_domain_name(struct rpmi_perf_domain *pd);
+u32 rpmi_perf_domain_level_count(struct rpmi_perf_domain *pd);
+int rpmi_perf_domain_level_info(struct rpmi_perf_domain *pd, u32 idx,
+				struct rpmi_perf_level *level);
+bool rpmi_perf_domain_can_set_level(struct rpmi_perf_domain *pd);
+bool rpmi_perf_domain_has_fast_channel(struct rpmi_perf_domain *pd);
+u32 rpmi_perf_domain_trans_latency_us(struct rpmi_perf_domain *pd);
+
+int rpmi_perf_domain_get_level(struct rpmi_perf_domain *pd, u32 *level);
+int rpmi_perf_domain_set_level(struct rpmi_perf_domain *pd, u32 level);
+int rpmi_perf_domain_set_level_fast(struct rpmi_perf_domain *pd, u32 level);
+int rpmi_perf_domain_get_limit(struct rpmi_perf_domain *pd, u32 *min, u32 *max);
+int rpmi_perf_domain_set_limit(struct rpmi_perf_domain *pd, u32 min, u32 max);
+
+int rpmi_perf_domain_level_to_freq(struct rpmi_perf_domain *pd, u32 level, u32 *khz);
+int rpmi_perf_domain_freq_to_level(struct rpmi_perf_domain *pd, u32 khz, u32 *level);
+int rpmi_perf_domain_opps_add(struct rpmi_perf_domain *pd, struct device *dev);
+
+#else
+
+static inline struct rpmi_perf_domain *
+rpmi_perf_domain_by_id(struct rpmi_perf *perf, u32 id)
+{
+	return NULL;
+}
+
+static inline u32 rpmi_perf_num_domains(struct rpmi_perf *perf)
+{
+	return 0;
+}
+
+static inline const char *rpmi_perf_domain_name(struct rpmi_perf_domain *pd)
+{
+	return NULL;
+}
+
+static inline u32 rpmi_perf_domain_level_count(struct rpmi_perf_domain *pd)
+{
+	return 0;
+}
+
+static inline int rpmi_perf_domain_level_info(struct rpmi_perf_domain *pd, u32 idx,
+					      struct rpmi_perf_level *level)
+{
+	return -EOPNOTSUPP;
+}
+
+static inline bool rpmi_perf_domain_can_set_level(struct rpmi_perf_domain *pd)
+{
+	return false;
+}
+
+static inline bool rpmi_perf_domain_has_fast_channel(struct rpmi_perf_domain *pd)
+{
+	return false;
+}
+
+static inline u32 rpmi_perf_domain_trans_latency_us(struct rpmi_perf_domain *pd)
+{
+	return 0;
+}
+
+static inline int rpmi_perf_domain_get_level(struct rpmi_perf_domain *pd, u32 *level)
+{
+	return -EOPNOTSUPP;
+}
+
+static inline int rpmi_perf_domain_set_level(struct rpmi_perf_domain *pd, u32 level)
+{
+	return -EOPNOTSUPP;
+}
+
+static inline int rpmi_perf_domain_set_level_fast(struct rpmi_perf_domain *pd, u32 level)
+{
+	return -EOPNOTSUPP;
+}
+
+static inline int rpmi_perf_domain_get_limit(struct rpmi_perf_domain *pd, u32 *min, u32 *max)
+{
+	return -EOPNOTSUPP;
+}
+
+static inline int rpmi_perf_domain_set_limit(struct rpmi_perf_domain *pd, u32 min, u32 max)
+{
+	return -EOPNOTSUPP;
+}
+
+static inline int rpmi_perf_domain_level_to_freq(struct rpmi_perf_domain *pd, u32 level,
+						 u32 *khz)
+{
+	return -EOPNOTSUPP;
+}
+
+static inline int rpmi_perf_domain_freq_to_level(struct rpmi_perf_domain *pd, u32 khz,
+						 u32 *level)
+{
+	return -EOPNOTSUPP;
+}
+
+static inline int rpmi_perf_domain_opps_add(struct rpmi_perf_domain *pd, struct device *dev)
+{
+	return -EOPNOTSUPP;
+}
+
+#endif /* CONFIG_RISCV_RPMI_PERFORMANCE */
+
+#endif /* _LINUX_RISCV_RPMI_PERFORMANCE_H_ */
diff --git a/include/linux/mailbox/riscv-rpmi-message.h b/include/linux/mailbox/riscv-rpmi-message.h
index d5362b5821f9..fbef6e721435 100644
--- a/include/linux/mailbox/riscv-rpmi-message.h
+++ b/include/linux/mailbox/riscv-rpmi-message.h
@@ -94,6 +94,7 @@ static inline int rpmi_to_linux_error(int rpmi_error)
 #define RPMI_SRVGRP_SYSTEM_MSI		0x00002
 #define RPMI_SRVGRP_CLOCK		0x00008
 #define RPMI_SRVGRP_DEVICE_POWER	0x00009
+#define RPMI_SRVGRP_PERFORMANCE		0x0000A
 
 /* RPMI clock service IDs */
 enum rpmi_clock_service_id {
@@ -130,6 +131,21 @@ enum rpmi_device_power_service_id {
 	RPMI_DP_SRV_ID_MAX_COUNT,
 };
 
+/* RPMI performances service IDs */
+enum rpmi_perf_service_id {
+	RPMI_PERF_SRV_ENABLE_NOTIFICATION = 0x01,
+	RPMI_PERF_SRV_GET_NUM_DOMAINS = 0x02,
+	RPMI_PERF_SRV_GET_ATTRIBUTES = 0x03,
+	RPMI_PERF_SRV_GET_SUPPORTED_LEVELS = 0x04,
+	RPMI_PERF_SRV_GET_LEVEL = 0x05,
+	RPMI_PERF_SRV_SET_LEVEL = 0x06,
+	RPMI_PERF_SRV_GET_LIMIT = 0x07,
+	RPMI_PERF_SRV_SET_LIMIT = 0x08,
+	RPMI_PERF_SRV_GET_FAST_CHANNEL_REGION = 0x09,
+	RPMI_PERF_SRV_GET_FAST_CHANNEL_ATTRS = 0x0a,
+	RPMI_PERF_SRV_ID_MAX_COUNT,
+};
+
 /* RPMI Linux mailbox attribute IDs */
 enum rpmi_mbox_attribute_id {
 	RPMI_MBOX_ATTR_SPEC_VERSION,
-- 
2.43.0


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

* [PATCH v2 5/7] cpufreq: Add RISC-V RPMI cpufreq driver
  2026-10-08  9:10 [PATCH v2 0/7] Add RISC-V RPMI performance service support Joshua Yeong
                   ` (3 preceding siblings ...)
  2026-10-08  9:10 ` [PATCH v2 4/7] firmware: riscv: Add RPMI performance service Joshua Yeong
@ 2026-10-08  9:10 ` Joshua Yeong
  2026-10-08  9:10 ` [PATCH v2 6/7] pmdomain: riscv: Add RPMI performance domains as power domains Joshua Yeong
  2026-10-08  9:10 ` [PATCH v2 7/7] MAINTAINERS: Add RISC-V RPMI performance driver Joshua Yeong
  6 siblings, 0 replies; 9+ messages in thread
From: Joshua Yeong @ 2026-10-08  9:10 UTC (permalink / raw)
  To: robh, krzk+dt, conor+dt, pjw, palmer, aou, rafael, viresh.kumar,
	ulfh, rahul, anup, lftan.linux
  Cc: alex, joshua.yeong, linux-riscv, devicetree, linux-pm, linux-kernel

Add a cpufreq driver for the RISC-V RPMI performance domains that CPUs
name through "performance-domains". The CPUs that name the same domain
share a policy, the levels the domain advertises become its frequency
table, and the level is set through the domain's fast channel when it
has one, so that the governor can switch frequency from the scheduler.
An energy model is registered from the power cost of each level.

The driver is a front-end over the RPMI performance service group core,
which creates the device it binds to when a CPU names the provider.

Signed-off-by: Joshua Yeong <joshua.yeong@starfivetech.com>
---
 drivers/cpufreq/Kconfig                       |  16 +
 drivers/cpufreq/Makefile                      |   4 +
 drivers/cpufreq/riscv-rpmi-cpufreq.c          | 294 ++++++++++++++++++
 .../firmware/riscv/riscv-rpmi-performance.c   |  55 ++++
 4 files changed, 369 insertions(+)
 create mode 100644 drivers/cpufreq/riscv-rpmi-cpufreq.c

diff --git a/drivers/cpufreq/Kconfig b/drivers/cpufreq/Kconfig
index db83f3365698..f012b8268b5e 100644
--- a/drivers/cpufreq/Kconfig
+++ b/drivers/cpufreq/Kconfig
@@ -364,6 +364,22 @@ config ACPI_CPPC_CPUFREQ_FIE
 
 	  If in doubt, say N.
 
+config RISCV_RPMI_CPUFREQ
+	tristate "RISC-V RPMI Based CPUFreq driver"
+	depends on RISCV_RPMI_PERFORMANCE
+	default RISCV
+	select PM_OPP
+	help
+	  This adds the CPUfreq driver support for RISC-V platforms whose CPU
+	  performance is managed through the RPMI performance service group.
+	  CPUs reference their domain through the "performance-domains"
+	  property. The performance domains it drives are enumerated by the
+	  RPMI performance service group core, which is what owns the mailbox
+	  channel.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called riscv-rpmi-cpufreq.
+
 endif # CPU_FREQ
 
 endmenu
diff --git a/drivers/cpufreq/Makefile b/drivers/cpufreq/Makefile
index 6c7a39b7f8d2..265a6bccef4b 100644
--- a/drivers/cpufreq/Makefile
+++ b/drivers/cpufreq/Makefile
@@ -96,6 +96,10 @@ obj-$(CONFIG_CPU_FREQ_PMAC64)		+= pmac64-cpufreq.o
 obj-$(CONFIG_PPC_PASEMI_CPUFREQ)	+= pasemi-cpufreq.o
 obj-$(CONFIG_POWERNV_CPUFREQ)		+= powernv-cpufreq.o
 
+##################################################################################
+# RISC-V platform drivers
+obj-$(CONFIG_RISCV_RPMI_CPUFREQ)	+= riscv-rpmi-cpufreq.o
+
 ##################################################################################
 # Other platform drivers
 obj-$(CONFIG_BMIPS_CPUFREQ)		+= bmips-cpufreq.o
diff --git a/drivers/cpufreq/riscv-rpmi-cpufreq.c b/drivers/cpufreq/riscv-rpmi-cpufreq.c
new file mode 100644
index 000000000000..1a5d0248f879
--- /dev/null
+++ b/drivers/cpufreq/riscv-rpmi-cpufreq.c
@@ -0,0 +1,294 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * RISC-V RPMI Based CPUFreq Driver
+ *
+ * Copyright (C) 2026 Shanghai StarFive Technology Co., Ltd.
+ *
+ * Drives the performance domains that CPUs share. The RPMI protocol and the
+ * domain enumeration live in the performance service group core, which owns
+ * the mailbox channel and creates the device this driver binds to.
+ */
+
+#define pr_fmt(fmt) "riscv-rpmi-cpufreq: " fmt
+
+#include <linux/cpufreq.h>
+#include <linux/energy_model.h>
+#include <linux/firmware/riscv/riscv-rpmi-performance.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/pm_opp.h>
+#include <linux/slab.h>
+
+struct rpmi_perf_cpufreq_data {
+	int nr_opp;
+	struct device *cpu_dev;
+	struct rpmi_perf_domain *domain;
+};
+
+static int rpmi_perf_set_target_index(struct cpufreq_policy *policy, unsigned int index)
+{
+	struct rpmi_perf_cpufreq_data *data = policy->driver_data;
+	u32 level;
+	int ret;
+
+	/*
+	 * cpufreq indexes its own frequency table, which is not the same thing
+	 * as an RPMI level index. Go through the frequency so that the two
+	 * only have to agree on what they mean, not on how they are numbered.
+	 */
+	ret = rpmi_perf_domain_freq_to_level(data->domain,
+					     policy->freq_table[index].frequency,
+					     &level);
+	if (ret)
+		return ret;
+
+	if (rpmi_perf_domain_has_fast_channel(data->domain))
+		return rpmi_perf_domain_set_level_fast(data->domain, level);
+
+	return rpmi_perf_domain_set_level(data->domain, level);
+}
+
+static unsigned int rpmi_perf_fast_switch(struct cpufreq_policy *policy,
+					  unsigned int target_freq)
+{
+	struct rpmi_perf_cpufreq_data *data = policy->driver_data;
+	u32 level;
+
+	if (rpmi_perf_domain_freq_to_level(data->domain, target_freq, &level))
+		return 0;
+
+	if (rpmi_perf_domain_set_level_fast(data->domain, level))
+		return 0;
+
+	return target_freq;
+}
+
+static unsigned int rpmi_perf_get_rate(unsigned int cpu)
+{
+	struct cpufreq_policy *policy = cpufreq_cpu_get_raw(cpu);
+	struct rpmi_perf_cpufreq_data *data;
+	u32 cpufreq, level;
+
+	if (!policy)
+		return 0;
+
+	data = policy->driver_data;
+
+	if (rpmi_perf_domain_get_level(data->domain, &level))
+		return 0;
+
+	if (rpmi_perf_domain_level_to_freq(data->domain, level, &cpufreq))
+		return 0;
+
+	return cpufreq;
+}
+
+static int rpmi_perf_init(struct cpufreq_policy *policy)
+{
+	struct cpufreq_frequency_table *freq_table;
+	struct rpmi_perf_cpufreq_data *data;
+	struct rpmi_perf_domain *domain;
+	struct platform_device *pdev = cpufreq_get_driver_data();
+	struct rpmi_perf **mpxy_perf = dev_get_platdata(&pdev->dev);
+	struct of_phandle_args args;
+	int ret, nr_opp;
+	struct device *cpu_dev;
+
+	cpu_dev = get_cpu_device(policy->cpu);
+	if (!cpu_dev) {
+		pr_err("failed to get cpu%d device\n", policy->cpu);
+		return -ENODEV;
+	}
+
+	data = kzalloc(sizeof(*data), GFP_KERNEL);
+	if (!data)
+		return -ENOMEM;
+
+	ret = of_perf_domain_get_sharing_cpumask(policy->cpu,
+						 "performance-domains",
+						 "#performance-domain-cells",
+						 policy->cpus, &args);
+	if (ret) {
+		dev_err(cpu_dev, "%s: failed to get performance domain info: %d\n",
+			__func__, ret);
+		goto out_free_priv;
+	}
+
+	/* A domain ID only means something to the provider the CPU names. */
+	if (args.np != dev_of_node(pdev->dev.parent)) {
+		dev_err(cpu_dev, "performance domain of %pOF, not of %pOF\n",
+			args.np, dev_of_node(pdev->dev.parent));
+		of_node_put(args.np);
+		ret = -ENODEV;
+		goto out_free_priv;
+	}
+
+	domain = rpmi_perf_domain_by_id(*mpxy_perf, args.args[0]);
+	of_node_put(args.np);
+	if (!domain) {
+		dev_err(cpu_dev, "performance domain %u is not usable\n",
+			args.args[0]);
+		ret = -EINVAL;
+		goto out_free_priv;
+	}
+
+	ret = rpmi_perf_domain_opps_add(domain, cpu_dev);
+	if (ret) {
+		dev_warn(cpu_dev, "failed to add opps to the device\n");
+		goto out_free_priv;
+	}
+
+	nr_opp = dev_pm_opp_get_opp_count(cpu_dev);
+	if (nr_opp <= 0) {
+		dev_err(cpu_dev, "performance domain has no operating points\n");
+		ret = -ENODEV;
+		goto out_free_opp;
+	}
+
+	ret = dev_pm_opp_init_cpufreq_table(cpu_dev, &freq_table);
+	if (ret) {
+		dev_err(cpu_dev, "failed to init cpufreq table: %d\n", ret);
+		goto out_free_opp;
+	}
+
+	data->cpu_dev = cpu_dev;
+	data->nr_opp = nr_opp;
+	data->domain = domain;
+
+	/* Allow DVFS request for any domain from any CPU */
+	policy->dvfs_possible_from_any_cpu = true;
+	policy->driver_data = data;
+	policy->freq_table = freq_table;
+
+	policy->cpuinfo.transition_latency =
+		rpmi_perf_domain_trans_latency_us(domain) * 1000;
+	policy->fast_switch_possible = rpmi_perf_domain_has_fast_channel(domain);
+
+	return 0;
+
+out_free_opp:
+	dev_pm_opp_remove_all_dynamic(cpu_dev);
+
+out_free_priv:
+	kfree(data);
+
+	return ret;
+}
+
+static void rpmi_perf_exit(struct cpufreq_policy *policy)
+{
+	struct rpmi_perf_cpufreq_data *data = policy->driver_data;
+
+	dev_pm_opp_free_cpufreq_table(data->cpu_dev, &policy->freq_table);
+	dev_pm_opp_remove_all_dynamic(data->cpu_dev);
+	kfree(data);
+}
+
+static int __maybe_unused
+rpmi_perf_get_cpu_power(struct device *cpu_dev, unsigned long *uW,
+			unsigned long *kHz)
+{
+	struct rpmi_perf_cpufreq_data *data;
+	struct rpmi_perf_level level;
+	struct cpufreq_policy *policy;
+	u32 idx, count;
+
+	policy = cpufreq_cpu_get_raw(cpu_dev->id);
+	if (!policy)
+		return -EINVAL;
+
+	data = policy->driver_data;
+	count = rpmi_perf_domain_level_count(data->domain);
+
+	/* The levels are in ascending order of frequency. */
+	for (idx = 0; idx < count; idx++) {
+		if (rpmi_perf_domain_level_info(data->domain, idx, &level))
+			return -EINVAL;
+
+		if (level.clock_freq < *kHz)
+			continue;
+
+		*uW = level.power_cost;
+		*kHz = level.clock_freq;
+		return 0;
+	}
+
+	/* No level at or above *kHz: there is no state to describe. */
+	return -EINVAL;
+}
+
+static void rpmi_perf_register_em(struct cpufreq_policy *policy)
+{
+	struct em_data_callback em_cb = EM_DATA_CB(rpmi_perf_get_cpu_power);
+	struct rpmi_perf_cpufreq_data *data = policy->driver_data;
+
+	em_dev_register_perf_domain(get_cpu_device(policy->cpu), data->nr_opp,
+				    &em_cb, policy->cpus, true);
+}
+
+static struct cpufreq_driver  rpmi_perf_cpufreq_driver = {
+	.name = "rpmi-cpufreq",
+	.flags = CPUFREQ_HAVE_GOVERNOR_PER_POLICY |
+		 CPUFREQ_NEED_INITIAL_FREQ_CHECK |
+		 CPUFREQ_IS_COOLING_DEV,
+	.verify = cpufreq_generic_frequency_table_verify,
+	.target_index = rpmi_perf_set_target_index,
+	.fast_switch = rpmi_perf_fast_switch,
+	.get = rpmi_perf_get_rate,
+	.init = rpmi_perf_init,
+	.exit = rpmi_perf_exit,
+	.register_em = rpmi_perf_register_em,
+};
+
+static int rpmi_cpufreq_probe(struct platform_device *pdev)
+{
+	struct rpmi_perf **mpxy_perf = dev_get_platdata(&pdev->dev);
+	struct device *dev = &pdev->dev;
+	int ret;
+
+	if (!mpxy_perf || !*mpxy_perf)
+		return -EINVAL;
+
+	/*
+	 * There is one cpufreq driver for the whole system, so only one
+	 * provider can drive the CPUs. Refuse another one rather than point
+	 * the registered driver at its domains.
+	 */
+	if (rpmi_perf_cpufreq_driver.driver_data)
+		return dev_err_probe(dev, -EBUSY,
+				     "CPUs are already driven by another provider\n");
+
+	rpmi_perf_cpufreq_driver.driver_data = pdev;
+
+	ret = cpufreq_register_driver(&rpmi_perf_cpufreq_driver);
+	if (ret) {
+		rpmi_perf_cpufreq_driver.driver_data = NULL;
+		return dev_err_probe(dev, ret, "registering cpufreq failed\n");
+	}
+
+	dev_info(dev, "%d MPXY cpufreq domains registered\n",
+		 rpmi_perf_num_domains(*mpxy_perf));
+
+	return 0;
+}
+
+static void rpmi_cpufreq_remove(struct platform_device *pdev)
+{
+	cpufreq_unregister_driver(&rpmi_perf_cpufreq_driver);
+	rpmi_perf_cpufreq_driver.driver_data = NULL;
+}
+
+static struct platform_driver rpmi_cpufreq_platdrv = {
+	.driver = {
+		.name = "riscv-rpmi-cpufreq",
+	},
+	.probe = rpmi_cpufreq_probe,
+	.remove = rpmi_cpufreq_remove,
+};
+
+module_platform_driver(rpmi_cpufreq_platdrv);
+
+MODULE_ALIAS("platform:riscv-rpmi-cpufreq");
+MODULE_AUTHOR("Joshua Yeong <joshua.yeong@starfivetech.com>");
+MODULE_DESCRIPTION("RISC-V RPMI performance cpufreq driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/firmware/riscv/riscv-rpmi-performance.c b/drivers/firmware/riscv/riscv-rpmi-performance.c
index d44802f07ab0..f91f2d861dc0 100644
--- a/drivers/firmware/riscv/riscv-rpmi-performance.c
+++ b/drivers/firmware/riscv/riscv-rpmi-performance.c
@@ -19,6 +19,7 @@
 #include <linux/mailbox/riscv-rpmi-message.h>
 #include <linux/module.h>
 #include <linux/mutex.h>
+#include <linux/of.h>
 #include <linux/platform_device.h>
 #include <linux/pm_opp.h>
 #include <linux/slab.h>
@@ -995,6 +996,56 @@ static void rpmi_perf_mbox_chan_release(void *data)
 	mbox_free_channel((struct mbox_chan *)data);
 }
 
+/*
+ * cpufreq has no device tree node of its own: a CPU names its domain through
+ * "performance-domains" on the CPU node. Create the device it binds to, but
+ * only when some CPU actually points back here.
+ */
+static bool rpmi_perf_cpus_present(struct device *dev)
+{
+	struct device_node *cpu_np;
+	struct of_phandle_args args;
+	int ret;
+
+	for_each_of_cpu_node(cpu_np) {
+		ret = of_parse_phandle_with_args(cpu_np, "performance-domains",
+						 "#performance-domain-cells", 0,
+						 &args);
+		if (ret)
+			continue;
+
+		if (args.np == dev_of_node(dev)) {
+			of_node_put(args.np);
+			of_node_put(cpu_np);
+			return true;
+		}
+		of_node_put(args.np);
+	}
+
+	return false;
+}
+
+static void rpmi_perf_frontend_unregister(void *data)
+{
+	platform_device_unregister((struct platform_device *)data);
+}
+
+static int rpmi_perf_cpufreq_register(struct device *dev, struct rpmi_perf *perf)
+{
+	struct platform_device *pdev;
+
+	if (!IS_ENABLED(CONFIG_RISCV_RPMI_CPUFREQ) || !rpmi_perf_cpus_present(dev))
+		return 0;
+
+	pdev = platform_device_register_data(dev, "riscv-rpmi-cpufreq",
+					     PLATFORM_DEVID_AUTO, &perf,
+					     sizeof(perf));
+	if (IS_ERR(pdev))
+		return PTR_ERR(pdev);
+
+	return devm_add_action_or_reset(dev, rpmi_perf_frontend_unregister, pdev);
+}
+
 static int rpmi_perf_probe(struct platform_device *pdev)
 {
 	struct device *dev = &pdev->dev;
@@ -1078,6 +1129,10 @@ static int rpmi_perf_probe(struct platform_device *pdev)
 
 	dev_set_drvdata(dev, mpxy_perf);
 
+	ret = rpmi_perf_cpufreq_register(dev, mpxy_perf);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to register cpufreq device\n");
+
 	dev_info(dev, "%d MPXY performance domains registered\n", num_domains);
 
 	return 0;
-- 
2.43.0


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

* [PATCH v2 6/7] pmdomain: riscv: Add RPMI performance domains as power domains
  2026-10-08  9:10 [PATCH v2 0/7] Add RISC-V RPMI performance service support Joshua Yeong
                   ` (4 preceding siblings ...)
  2026-10-08  9:10 ` [PATCH v2 5/7] cpufreq: Add RISC-V RPMI cpufreq driver Joshua Yeong
@ 2026-10-08  9:10 ` Joshua Yeong
  2026-10-08  9:10 ` [PATCH v2 7/7] MAINTAINERS: Add RISC-V RPMI performance driver Joshua Yeong
  6 siblings, 0 replies; 9+ messages in thread
From: Joshua Yeong @ 2026-10-08  9:10 UTC (permalink / raw)
  To: robh, krzk+dt, conor+dt, pjw, palmer, aou, rafael, viresh.kumar,
	ulfh, rahul, anup, lftan.linux
  Cc: alex, joshua.yeong, linux-riscv, devicetree, linux-pm, linux-kernel

A device that is not a CPU has no generic way to drive an RPMI
performance domain: "performance-domains" is only resolved by cpufreq,
which would leave a device driver to call into RPMI directly, with
nothing to arbitrate between devices that share a domain.

A device names its domain through "power-domains", is handed an
operating point per level when it attaches, and drives the domain
through the OPP library, while genpd settles on the highest state any
device in the domain asks for.

Like cpufreq, this is a front-end over the performance service group
core, which creates the device it binds to when the node carries
"#power-domain-cells".

Signed-off-by: Joshua Yeong <joshua.yeong@starfivetech.com>
---
 .../firmware/riscv/riscv-rpmi-performance.c   |  26 ++
 drivers/pmdomain/riscv/Kconfig                |  16 ++
 drivers/pmdomain/riscv/Makefile               |   1 +
 .../pmdomain/riscv/riscv-rpmi-perf-domain.c   | 265 ++++++++++++++++++
 4 files changed, 308 insertions(+)
 create mode 100644 drivers/pmdomain/riscv/riscv-rpmi-perf-domain.c

diff --git a/drivers/firmware/riscv/riscv-rpmi-performance.c b/drivers/firmware/riscv/riscv-rpmi-performance.c
index f91f2d861dc0..d532d41ecbba 100644
--- a/drivers/firmware/riscv/riscv-rpmi-performance.c
+++ b/drivers/firmware/riscv/riscv-rpmi-performance.c
@@ -1046,6 +1046,28 @@ static int rpmi_perf_cpufreq_register(struct device *dev, struct rpmi_perf *perf
 	return devm_add_action_or_reset(dev, rpmi_perf_frontend_unregister, pdev);
 }
 
+/*
+ * A device that is not a CPU names its domain through "power-domains", which
+ * the power domain front-end serves on this node. Create the device it binds
+ * to, but only when the node is a power domain provider at all.
+ */
+static int rpmi_perf_genpd_register(struct device *dev, struct rpmi_perf *perf)
+{
+	struct platform_device *pdev;
+
+	if (!IS_ENABLED(CONFIG_RISCV_RPMI_PERF_DOMAIN) ||
+	    !of_property_present(dev_of_node(dev), "#power-domain-cells"))
+		return 0;
+
+	pdev = platform_device_register_data(dev, "riscv-rpmi-perf-domain",
+					     PLATFORM_DEVID_AUTO, &perf,
+					     sizeof(perf));
+	if (IS_ERR(pdev))
+		return PTR_ERR(pdev);
+
+	return devm_add_action_or_reset(dev, rpmi_perf_frontend_unregister, pdev);
+}
+
 static int rpmi_perf_probe(struct platform_device *pdev)
 {
 	struct device *dev = &pdev->dev;
@@ -1133,6 +1155,10 @@ static int rpmi_perf_probe(struct platform_device *pdev)
 	if (ret)
 		return dev_err_probe(dev, ret, "failed to register cpufreq device\n");
 
+	ret = rpmi_perf_genpd_register(dev, mpxy_perf);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to register power domain device\n");
+
 	dev_info(dev, "%d MPXY performance domains registered\n", num_domains);
 
 	return 0;
diff --git a/drivers/pmdomain/riscv/Kconfig b/drivers/pmdomain/riscv/Kconfig
index 97232edc294a..6f3006d3220a 100644
--- a/drivers/pmdomain/riscv/Kconfig
+++ b/drivers/pmdomain/riscv/Kconfig
@@ -13,3 +13,19 @@ config RISCV_RPMI_DEVICE_POWER
 	  specification. The power domains advertised by the platform
 	  microcontroller are registered as generic power domains, so that
 	  devices can reference them through the "power-domains" property.
+
+config RISCV_RPMI_PERF_DOMAIN
+	tristate "RISC-V RPMI performance domains as power domains"
+	depends on RISCV_RPMI_PERFORMANCE
+	depends on PM
+	default RISCV
+	select PM_GENERIC_DOMAINS
+	help
+	  Register the performance domains of the RISC-V platform management
+	  interface (RPMI) performance service group as generic power domains,
+	  with a performance state for each level the platform microcontroller
+	  advertises. A device that is not a CPU references its domain through
+	  the "power-domains" property and drives it through the OPP library.
+
+	  To compile this driver as a module, choose M here: the module will
+	  be called riscv-rpmi-perf-domain.
diff --git a/drivers/pmdomain/riscv/Makefile b/drivers/pmdomain/riscv/Makefile
index 2cb0ee8ad413..32fd12ebb9cd 100644
--- a/drivers/pmdomain/riscv/Makefile
+++ b/drivers/pmdomain/riscv/Makefile
@@ -1,3 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 
 obj-$(CONFIG_RISCV_RPMI_DEVICE_POWER)	+= riscv-rpmi-device-power.o
+obj-$(CONFIG_RISCV_RPMI_PERF_DOMAIN)	+= riscv-rpmi-perf-domain.o
diff --git a/drivers/pmdomain/riscv/riscv-rpmi-perf-domain.c b/drivers/pmdomain/riscv/riscv-rpmi-perf-domain.c
new file mode 100644
index 000000000000..fc5c512fe069
--- /dev/null
+++ b/drivers/pmdomain/riscv/riscv-rpmi-perf-domain.c
@@ -0,0 +1,265 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * RISC-V RPMI performance domains as generic power domains
+ *
+ * Copyright (C) 2026 Shanghai StarFive Technology Co., Ltd.
+ *
+ * A device that is not a CPU names its RPMI performance domain through
+ * "power-domains". Each domain is registered here as a generic power domain
+ * whose performance states are the levels the platform microcontroller
+ * advertises, so the device's driver drives it through the OPP library like
+ * any other power domain with performance states, and genpd settles on the
+ * highest state any device in the domain asks for.
+ *
+ * Performance state 0 means that no device in the domain asks for anything,
+ * while 0 is a valid RPMI level index. State N is therefore level index
+ * N - 1, and state 0 leaves the domain at whatever level it is running.
+ *
+ * genpd runs the domain at the highest state asked for, which is the highest
+ * level index. That is the level to run at: the specification makes the
+ * level index the platform's performance scale, which its limit services
+ * also order levels by, so a higher index is a higher level whatever the
+ * indices in between.
+ *
+ * A domain that CPUs name through "performance-domains" belongs to cpufreq,
+ * which sets its level directly, so no device is attached to it as well.
+ *
+ * The RPMI protocol and the domain enumeration live in the performance
+ * service group core, which owns the mailbox channel and creates the device
+ * this driver binds to.
+ */
+
+#include <linux/firmware/riscv/riscv-rpmi-performance.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/pm_domain.h>
+#include <linux/pm_opp.h>
+#include <linux/units.h>
+
+struct rpmi_perf_genpd {
+	struct generic_pm_domain genpd;
+	struct rpmi_perf_domain *domain;
+	bool cpu_domain;
+};
+
+struct rpmi_perf_genpd_provider {
+	struct genpd_onecell_data data;
+	struct rpmi_perf_genpd *pds;
+};
+
+#define to_rpmi_perf_genpd(gpd) container_of(gpd, struct rpmi_perf_genpd, genpd)
+
+static int rpmi_perf_genpd_set_performance_state(struct generic_pm_domain *genpd,
+						 unsigned int state)
+{
+	struct rpmi_perf_genpd *pd = to_rpmi_perf_genpd(genpd);
+	int ret;
+
+	/* No device in the domain asks for a level, so leave it where it is. */
+	if (!state)
+		return 0;
+
+	ret = rpmi_perf_domain_set_level(pd->domain, state - 1);
+	if (ret)
+		dev_warn(&genpd->dev, "failed to set level %u: %d\n",
+			 state - 1, ret);
+
+	return ret;
+}
+
+/*
+ * The levels come from the platform microcontroller, so there is no
+ * operating-points-v2 table in the device tree. Hand each device that
+ * attaches an operating point per level instead, carrying the level's
+ * frequency and its performance state.
+ */
+static int rpmi_perf_genpd_attach_dev(struct generic_pm_domain *genpd,
+				      struct device *dev)
+{
+	struct rpmi_perf_genpd *pd = to_rpmi_perf_genpd(genpd);
+	struct dev_pm_opp_data data = {};
+	struct rpmi_perf_level level;
+	u32 idx;
+	int ret;
+
+	/* cpufreq and genpd would each set the level behind the other's back. */
+	if (pd->cpu_domain) {
+		dev_err(dev, "%s is the performance domain of a CPU\n", genpd->name);
+		return -EBUSY;
+	}
+
+	/* A domain whose level cannot be set has no state to offer. */
+	if (!rpmi_perf_domain_can_set_level(pd->domain))
+		return 0;
+
+	for (idx = 0; idx < rpmi_perf_domain_level_count(pd->domain); idx++) {
+		ret = rpmi_perf_domain_level_info(pd->domain, idx, &level);
+		if (ret)
+			goto err;
+
+		/* Frequency from RPMI is in kHz */
+		data.freq = (unsigned long)level.clock_freq * HZ_PER_KHZ;
+		data.level = level.index + 1;
+
+		ret = dev_pm_opp_add_dynamic(dev, &data);
+		if (ret)
+			goto err;
+	}
+
+	return 0;
+
+err:
+	dev_warn(dev, "failed to add operating points for %s: %d\n",
+		 genpd->name, ret);
+	dev_pm_opp_remove_all_dynamic(dev);
+
+	return ret;
+}
+
+static void rpmi_perf_genpd_detach_dev(struct generic_pm_domain *genpd,
+				       struct device *dev)
+{
+	struct rpmi_perf_genpd *pd = to_rpmi_perf_genpd(genpd);
+
+	if (rpmi_perf_domain_can_set_level(pd->domain))
+		dev_pm_opp_remove_all_dynamic(dev);
+}
+
+/* Mark the domains that CPUs name through "performance-domains". */
+static void rpmi_perf_genpd_mark_cpu_domains(struct device_node *np,
+					     struct rpmi_perf_genpd *pds,
+					     u32 num_domains)
+{
+	struct of_phandle_args args;
+	struct device_node *cpu_np;
+
+	for_each_of_cpu_node(cpu_np) {
+		if (of_parse_phandle_with_args(cpu_np, "performance-domains",
+					       "#performance-domain-cells", 0,
+					       &args))
+			continue;
+
+		if (args.np == np && args.args_count >= 1 &&
+		    args.args[0] < num_domains)
+			pds[args.args[0]].cpu_domain = true;
+
+		of_node_put(args.np);
+	}
+}
+
+static int rpmi_perf_genpd_probe(struct platform_device *pdev)
+{
+	struct rpmi_perf **mpxy_perf = dev_get_platdata(&pdev->dev);
+	struct device *dev = &pdev->dev;
+	struct rpmi_perf_genpd_provider *provider;
+	struct generic_pm_domain **domains;
+	u32 i, num_domains, registered = 0;
+	int ret;
+
+	if (!mpxy_perf || !*mpxy_perf)
+		return -EINVAL;
+
+	num_domains = rpmi_perf_num_domains(*mpxy_perf);
+
+	provider = devm_kzalloc(dev, sizeof(*provider), GFP_KERNEL);
+	if (!provider)
+		return -ENOMEM;
+
+	provider->pds = devm_kcalloc(dev, num_domains, sizeof(*provider->pds),
+				     GFP_KERNEL);
+	if (!provider->pds)
+		return -ENOMEM;
+
+	domains = devm_kcalloc(dev, num_domains, sizeof(*domains), GFP_KERNEL);
+	if (!domains)
+		return -ENOMEM;
+
+	rpmi_perf_genpd_mark_cpu_domains(dev_of_node(dev->parent), provider->pds,
+					 num_domains);
+
+	for (i = 0; i < num_domains; i++) {
+		struct rpmi_perf_genpd *pd = &provider->pds[i];
+
+		/*
+		 * A domain that could not be enumerated is left out, the way
+		 * the device power provider leaves out a domain it cannot use.
+		 * A device that names it then never gets its power domain.
+		 */
+		pd->domain = rpmi_perf_domain_by_id(*mpxy_perf, i);
+		if (!pd->domain)
+			continue;
+
+		pd->genpd.name = rpmi_perf_domain_name(pd->domain);
+		/*
+		 * A performance domain has no off state, its operating points
+		 * come from the platform microcontroller rather than from the
+		 * device tree, and its name is not guaranteed to be unique.
+		 */
+		pd->genpd.flags = GENPD_FLAG_ALWAYS_ON |
+				  GENPD_FLAG_OPP_TABLE_FW |
+				  GENPD_FLAG_DEV_NAME_FW;
+		pd->genpd.set_performance_state = rpmi_perf_genpd_set_performance_state;
+		pd->genpd.attach_dev = rpmi_perf_genpd_attach_dev;
+		pd->genpd.detach_dev = rpmi_perf_genpd_detach_dev;
+
+		ret = pm_genpd_init(&pd->genpd, NULL, false);
+		if (ret)
+			goto err;
+
+		domains[i] = &pd->genpd;
+		registered++;
+	}
+
+	provider->data.domains = domains;
+	provider->data.num_domains = num_domains;
+
+	/* The domains belong to the node the core was probed from. */
+	ret = of_genpd_add_provider_onecell(dev_of_node(dev->parent),
+					    &provider->data);
+	if (ret)
+		goto err;
+
+	platform_set_drvdata(pdev, provider);
+
+	dev_info(dev, "%u MPXY performance domains registered as power domains\n",
+		 registered);
+
+	return 0;
+
+err:
+	while (i--) {
+		if (domains[i])
+			pm_genpd_remove(domains[i]);
+	}
+
+	return dev_err_probe(dev, ret, "failed to register power domains\n");
+}
+
+static void rpmi_perf_genpd_remove(struct platform_device *pdev)
+{
+	struct rpmi_perf_genpd_provider *provider = platform_get_drvdata(pdev);
+	u32 i;
+
+	of_genpd_del_provider(dev_of_node(pdev->dev.parent));
+
+	for (i = 0; i < provider->data.num_domains; i++) {
+		if (provider->data.domains[i])
+			pm_genpd_remove(provider->data.domains[i]);
+	}
+}
+
+static struct platform_driver rpmi_perf_genpd_platdrv = {
+	.driver = {
+		.name = "riscv-rpmi-perf-domain",
+	},
+	.probe = rpmi_perf_genpd_probe,
+	.remove = rpmi_perf_genpd_remove,
+};
+
+module_platform_driver(rpmi_perf_genpd_platdrv);
+
+MODULE_ALIAS("platform:riscv-rpmi-perf-domain");
+MODULE_AUTHOR("Joshua Yeong <joshua.yeong@starfivetech.com>");
+MODULE_DESCRIPTION("Power domains for the RISC-V RPMI performance service");
+MODULE_LICENSE("GPL");
-- 
2.43.0


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

* [PATCH v2 7/7] MAINTAINERS: Add RISC-V RPMI performance driver
  2026-10-08  9:10 [PATCH v2 0/7] Add RISC-V RPMI performance service support Joshua Yeong
                   ` (5 preceding siblings ...)
  2026-10-08  9:10 ` [PATCH v2 6/7] pmdomain: riscv: Add RPMI performance domains as power domains Joshua Yeong
@ 2026-10-08  9:10 ` Joshua Yeong
  6 siblings, 0 replies; 9+ messages in thread
From: Joshua Yeong @ 2026-10-08  9:10 UTC (permalink / raw)
  To: robh, krzk+dt, conor+dt, pjw, palmer, aou, rafael, viresh.kumar,
	ulfh, rahul, anup, lftan.linux
  Cc: alex, joshua.yeong, linux-riscv, devicetree, linux-pm, linux-kernel

Add the RISC-V RPMI performance service group to the RPMI device power
entry, renamed to cover both service groups, so that its bindings, the
service group core and header, and the cpufreq and power domain drivers
built on it share one maintainer.

Signed-off-by: Joshua Yeong <joshua.yeong@starfivetech.com>
---
 MAINTAINERS | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/MAINTAINERS b/MAINTAINERS
index bdee5c7ee83d..b6bb3c6ea8aa 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -23600,13 +23600,18 @@ F:	drivers/irqchip/irq-riscv-rpmi-sysmsi.c
 F:	drivers/mailbox/riscv-sbi-mpxy-mbox.c
 F:	include/linux/mailbox/riscv-rpmi-message.h
 
-RISC-V RPMI DEVICE POWER DRIVER
+RISC-V RPMI DEVICE POWER AND PERFORMANCE DRIVERS
 M:	Joshua Yeong <joshua.yeong@starfivetech.com>
 L:	linux-riscv@lists.infradead.org
 S:	Maintained
+F:	Documentation/devicetree/bindings/dvfs/riscv,rpmi-mpxy-performance.yaml
+F:	Documentation/devicetree/bindings/dvfs/riscv,rpmi-performance.yaml
 F:	Documentation/devicetree/bindings/power/riscv,rpmi-device-power.yaml
 F:	Documentation/devicetree/bindings/power/riscv,rpmi-mpxy-device-power.yaml
+F:	drivers/cpufreq/riscv-rpmi-cpufreq.c
+F:	drivers/firmware/riscv/
 F:	drivers/pmdomain/riscv/
+F:	include/linux/firmware/riscv/riscv-rpmi-performance.h
 
 RISC-V SPACEMIT SoC Support
 M:	Yixun Lan <dlan@kernel.org>
-- 
2.43.0


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

* Re: [PATCH v2 2/7] dt-bindings: dvfs: Add RPMI performance service bindings
  2026-10-08  9:10 ` [PATCH v2 2/7] dt-bindings: dvfs: Add RPMI performance service bindings Joshua Yeong
@ 2026-10-08 10:41   ` Conor Dooley
  0 siblings, 0 replies; 9+ messages in thread
From: Conor Dooley @ 2026-10-08 10:41 UTC (permalink / raw)
  To: Joshua Yeong
  Cc: robh, krzk+dt, conor+dt, pjw, palmer, aou, rafael, viresh.kumar,
	ulfh, rahul, anup, lftan.linux, alex, linux-riscv, devicetree,
	linux-pm, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 4763 bytes --]

On Thu, Oct 08, 2026 at 05:10:26PM +0800, Joshua Yeong wrote:
> Add device tree bindings for the RPMI performance service group based
> controller for the supervisor software.
> 
> A CPU names its performance domain through "performance-domains". Any
> other device names it through "power-domains": the controller is also a
> power domain provider, with one power domain per performance domain and
> the levels the domain advertises as the performance states of that
> power domain.
> 
> The RPMI performance service group is defined by the RISC-V Platform
> Management Interface (RPMI) specification.
> 
> Signed-off-by: Joshua Yeong <joshua.yeong@starfivetech.com>
> ---
>  .../bindings/dvfs/riscv,rpmi-performance.yaml | 88 +++++++++++++++++++
>  1 file changed, 88 insertions(+)
>  create mode 100644 Documentation/devicetree/bindings/dvfs/riscv,rpmi-performance.yaml
> 
> diff --git a/Documentation/devicetree/bindings/dvfs/riscv,rpmi-performance.yaml b/Documentation/devicetree/bindings/dvfs/riscv,rpmi-performance.yaml
> new file mode 100644
> index 000000000000..ec7856cb7257
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/dvfs/riscv,rpmi-performance.yaml
> @@ -0,0 +1,88 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/dvfs/riscv,rpmi-performance.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: RISC-V RPMI performance service group
> +
> +maintainers:
> +  - Joshua Yeong <joshua.yeong@starfivetech.com>
> +
> +description: |
> +  The RISC-V Platform Management Interface (RPMI) [1] defines a modular and
> +  extensible messaging protocol for platform management functions. The supervisor
> +  software can send and receive RPMI messages via SBI MPXY extension [2]
> +  or via a dedicated supervisor-mode RPMI transport.
> +
> +  The RPMI specification [1] defines performance service group (performance
> +  domain) for accessing and controlling platform-managed performance-related
> +  resources, as implemented by a platform microcontroller. Supervisor software
> +  can interact with the RPMI performance service group through an SBI MPXY
> +  channel or through a dedicated supervisor-mode RPMI transport.
> +
> +  The node is a performance domain provider. A CPU references its domain
> +  through the generic "performance-domains" property described in
> +  dvfs/performance-domain.yaml.
> +
> +  The node can also be a power domain provider, with one power domain for each
> +  performance domain. Any other device that runs in one of the domains
> +  references it through "power-domains", and each level the domain advertises
> +  is a performance state of that power domain.
> +
> +  For example, with the provider labelled "performance", a CPU that runs in
> +  domain 0 and a device that runs in domain 1 reference their domains as:
> +
> +    cpu@0 {
> +        ...
> +        performance-domains = <&performance 0>;
> +    };
> +
> +    gpu@40000000 {
> +        ...
> +        power-domains = <&performance 1>;
> +    };
> +
> +  ===========================================
> +  References
> +  ===========================================
> +
> +  [1] RISC-V Platform Management Interface (RPMI) v1.0 (or higher)
> +      https://github.com/riscv-non-isa/riscv-rpmi/releases
> +
> +  [2] RISC-V Supervisor Binary Interface (SBI) v3.0 (or higher)
> +      https://github.com/riscv-non-isa/riscv-sbi-doc/releases
> +
> +properties:
> +  compatible:
> +    description:
> +      Intended for use by the supervisor software.
> +    const: riscv,rpmi-performance
> +
> +  mboxes:
> +    maxItems: 1
> +    description:
> +      Mailbox channel of the underlying RPMI transport or SBI message proxy channel.
> +
> +  "#performance-domain-cells":
> +    const: 1
> +
> +  "#power-domain-cells":
> +    const: 1
> +
> +required:
> +  - compatible
> +  - mboxes
> +  - "#performance-domain-cells"

Why is performance-domain-cells mandatory? Is it not possible for a
domain to only control non-cpu devices? If that's the case, this should
be relaxed to require that either cells property is present I think.

Also, all other rpmi/mpxy series have been written such that there's
both M and S mode bindings in a single file. Why has that approach not
been taken here?

Thanks,
Conor.

      
  -
> +
> +additionalProperties: false
> +
> +examples:
> +  - |
> +    rpmi-performance {
> +        compatible = "riscv,rpmi-performance";
> +        mboxes = <&mpxy_mbox 0x1003 0x0>;
> +        #performance-domain-cells = <0x01>;
> +        #power-domain-cells = <0x01>;
> +    };
> +...
> -- 
> 2.43.0
> 

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]

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

end of thread, other threads:[~2026-10-08 10:41 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-08  9:10 [PATCH v2 0/7] Add RISC-V RPMI performance service support Joshua Yeong
2026-10-08  9:10 ` [PATCH v2 1/7] dt-bindings: dvfs: Add RPMI performance service message proxy bindings Joshua Yeong
2026-10-08  9:10 ` [PATCH v2 2/7] dt-bindings: dvfs: Add RPMI performance service bindings Joshua Yeong
2026-10-08 10:41   ` Conor Dooley
2026-10-08  9:10 ` [PATCH v2 3/7] dt-bindings: riscv: cpus: document performance-domains property Joshua Yeong
2026-10-08  9:10 ` [PATCH v2 4/7] firmware: riscv: Add RPMI performance service Joshua Yeong
2026-10-08  9:10 ` [PATCH v2 5/7] cpufreq: Add RISC-V RPMI cpufreq driver Joshua Yeong
2026-10-08  9:10 ` [PATCH v2 6/7] pmdomain: riscv: Add RPMI performance domains as power domains Joshua Yeong
2026-10-08  9:10 ` [PATCH v2 7/7] MAINTAINERS: Add RISC-V RPMI performance driver Joshua Yeong

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

all inboxes | Powered by JetHome®