Linux Documentation
 help / color / mirror / Atom feed
From: Xilin Wu <sophon@radxa.com>
To: "Guenter Roeck" <linux@roeck-us.net>,
	"Jonathan Corbet" <corbet@lwn.net>,
	"Shuah Khan" <skhan@linuxfoundation.org>,
	"Randy Dunlap" <rdunlap@infradead.org>,
	"Hans de Goede" <hansg@kernel.org>,
	"Ilpo Järvinen" <ilpo.jarvinen@linux.intel.com>,
	"Bryan O'Donoghue" <bryan.odonoghue@linaro.org>
Cc: linux-hwmon@vger.kernel.org, linux-doc@vger.kernel.org,
	 linux-kernel@vger.kernel.org, linux-arm-msm@vger.kernel.org,
	 platform-driver-x86@vger.kernel.org, Xilin Wu <sophon@radxa.com>
Subject: [PATCH 1/2] platform: arm64: Add Radxa SVC GLINK driver
Date: Mon, 31 Aug 2026 23:09:32 +0800	[thread overview]
Message-ID: <20260831-radxa-svc-v1-1-7c028de6a387@radxa.com> (raw)
In-Reply-To: <20260831-radxa-svc-v1-0-7c028de6a387@radxa.com>

Add an rpmsg client for the RADXA_SVC_ADSP_APPS firmware service used
on supported Radxa boards with Qualcomm SoCs.

Expose fan control through the standard hwmon pwm1 and pwm1_enable
attributes. SET_PROFILE only changes the fan curve, so map its quiet and
performance curves to automatic modes instead of platform_profile.

Signed-off-by: Xilin Wu <sophon@radxa.com>
---
 Documentation/hwmon/index.rst            |   1 +
 Documentation/hwmon/radxa-svc-glink.rst  |  32 ++
 MAINTAINERS                              |   8 +
 drivers/platform/arm64/Kconfig           |  13 +
 drivers/platform/arm64/Makefile          |   1 +
 drivers/platform/arm64/radxa_svc_glink.c | 735 +++++++++++++++++++++++++++++++
 6 files changed, 790 insertions(+)

diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
index 9955a525436a..08a2062b4f84 100644
--- a/Documentation/hwmon/index.rst
+++ b/Documentation/hwmon/index.rst
@@ -234,6 +234,7 @@ Hardware Monitoring Kernel Drivers
    pwm-fan
    q54sj108a2
    qnap-mcu-hwmon
+   radxa-svc-glink
    raspberrypi-hwmon
    sbrmi
    sbtsi_temp
diff --git a/Documentation/hwmon/radxa-svc-glink.rst b/Documentation/hwmon/radxa-svc-glink.rst
new file mode 100644
index 000000000000..c464b130a4e4
--- /dev/null
+++ b/Documentation/hwmon/radxa-svc-glink.rst
@@ -0,0 +1,32 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+Kernel driver radxa-svc-glink
+=============================
+
+Description
+-----------
+
+The Radxa SVC GLINK driver communicates with the ``RADXA_SVC_ADSP_APPS``
+firmware service found on supported Radxa boards with Qualcomm SoCs. The
+firmware provides fan control.
+
+Fan control
+-----------
+
+The fan controller provides the following attributes:
+
+=============== ======= ======================================================
+``pwm1``        RW      Current fan PWM value. Writes are accepted in manual
+                        mode and use values from 0 to 255.
+``pwm1_enable`` RW      Fan control mode, as described below.
+=============== ======= ======================================================
+
+The supported ``pwm1_enable`` values are:
+
+  - 0: fan at full speed
+  - 1: manual control using ``pwm1``
+  - 2: automatic control using the quiet curve
+  - 3: automatic control using the performance curve
+
+When manual mode is selected, the driver starts with the current fan speed. If
+the current speed cannot be determined, it starts at full speed.
diff --git a/MAINTAINERS b/MAINTAINERS
index 3a19da74d00c..9e4685759194 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22719,6 +22719,14 @@ F:	Documentation/ABI/testing/sysfs-bus-rbd
 F:	drivers/block/rbd.c
 F:	drivers/block/rbd_types.h
 
+RADXA SVC GLINK DRIVER
+M:	Xilin Wu <sophon@radxa.com>
+L:	linux-arm-msm@vger.kernel.org
+L:	linux-hwmon@vger.kernel.org
+S:	Maintained
+F:	Documentation/hwmon/radxa-svc-glink.rst
+F:	drivers/platform/arm64/radxa_svc_glink.c
+
 RAGE128 FRAMEBUFFER DISPLAY DRIVER
 L:	linux-fbdev@vger.kernel.org
 S:	Orphan
diff --git a/drivers/platform/arm64/Kconfig b/drivers/platform/arm64/Kconfig
index e32e01b2a9bd..60c1e541fe89 100644
--- a/drivers/platform/arm64/Kconfig
+++ b/drivers/platform/arm64/Kconfig
@@ -103,4 +103,17 @@ config EC_QCOM_HAMOA
 
 	  This driver currently supports Hamoa/Purwa/Glymur reference devices.
 
+config RADXA_SVC_GLINK
+	tristate "Radxa SVC GLINK driver"
+	depends on ARCH_QCOM || COMPILE_TEST
+	depends on RPMSG
+	depends on HWMON
+	help
+	  Enable support for the Radxa SVC firmware service found on supported
+	  Radxa boards using Qualcomm SoCs. The driver communicates with the
+	  RADXA_SVC_ADSP_APPS service over rpmsg and exposes fan control through
+	  the standard hwmon interface.
+
+	  Say M or Y here to include this support.
+
 endif # ARM64_PLATFORM_DEVICES
diff --git a/drivers/platform/arm64/Makefile b/drivers/platform/arm64/Makefile
index 7681be4a46e9..327acb7b983c 100644
--- a/drivers/platform/arm64/Makefile
+++ b/drivers/platform/arm64/Makefile
@@ -10,3 +10,4 @@ obj-$(CONFIG_EC_HUAWEI_GAOKUN)	+= huawei-gaokun-ec.o
 obj-$(CONFIG_EC_LENOVO_YOGA_C630) += lenovo-yoga-c630.o
 obj-$(CONFIG_EC_LENOVO_THINKPAD_T14S) += lenovo-thinkpad-t14s.o
 obj-$(CONFIG_EC_QCOM_HAMOA) += qcom-hamoa-ec.o
+obj-$(CONFIG_RADXA_SVC_GLINK)	+= radxa_svc_glink.o
diff --git a/drivers/platform/arm64/radxa_svc_glink.c b/drivers/platform/arm64/radxa_svc_glink.c
new file mode 100644
index 000000000000..1c91bfabcd12
--- /dev/null
+++ b/drivers/platform/arm64/radxa_svc_glink.c
@@ -0,0 +1,735 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Radxa SVC GLINK driver.
+ *
+ * Copyright (c) 2026 Radxa Computer (Shenzhen) Co., Ltd.
+ */
+
+#include <linux/completion.h>
+#include <linux/errno.h>
+#include <linux/hwmon.h>
+#include <linux/jiffies.h>
+#include <linux/kernel.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/rpmsg.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/string.h>
+
+#define RADXA_SVC_MAGIC		0x58444152 /* "RADX" */
+#define RADXA_SVC_VERSION	1
+#define RADXA_SVC_TIMEOUT	msecs_to_jiffies(5000)
+
+#define RADXA_SVC_MAX_TX_PAYLOAD	64
+#define RADXA_SVC_MAX_RX_PAYLOAD	2048
+
+#define RADXA_SVC_OP_GET_VERSION	0x02
+#define RADXA_SVC_OP_SET_PROFILE	0x10
+#define RADXA_SVC_OP_GET_PROFILE	0x11
+#define RADXA_SVC_OP_FAN_GET_STATE	0x50
+#define RADXA_SVC_OP_FAN_SET_CONTROL	0x51
+#define RADXA_SVC_OP_FAN_GET_CONTROL	0x52
+
+#define RADXA_SVC_PROFILE_QUIET		0
+#define RADXA_SVC_PROFILE_PERFORMANCE	1
+
+#define RADXA_SVC_FAN_CONTROL_FULL_SPEED	0
+#define RADXA_SVC_FAN_CONTROL_MANUAL		1
+#define RADXA_SVC_FAN_CONTROL_AUTO		2
+#define RADXA_SVC_FAN_PWM_MAX			255
+
+#define RADXA_SVC_PWM_MODE_FULL_SPEED		0
+#define RADXA_SVC_PWM_MODE_MANUAL		1
+#define RADXA_SVC_PWM_MODE_AUTO_QUIET		2
+#define RADXA_SVC_PWM_MODE_AUTO_PERFORMANCE	3
+
+#define RADXA_SVC_CAP_PROFILE		BIT(1)
+#define RADXA_SVC_CAP_FANCTL		BIT(5)
+#define RADXA_SVC_CAP_FANCTL_CTRL	BIT(6)
+
+#define RADXA_SVC_REQUIRED_CAPS		(RADXA_SVC_CAP_PROFILE | \
+					 RADXA_SVC_CAP_FANCTL | \
+					 RADXA_SVC_CAP_FANCTL_CTRL)
+
+struct radxa_svc_hdr {
+	__le32 magic;
+	__le16 version;
+	__le16 header_size;
+	__le32 opcode;
+	__le32 seq;
+	__le32 status;
+	__le32 payload_len;
+} __packed;
+
+struct radxa_svc_version_resp {
+	__le16 major;
+	__le16 minor;
+	__le32 caps;
+} __packed;
+
+struct radxa_svc_fan_state_resp {
+	__le32 profile;
+	__le32 running;
+	__le32 emergency;
+	__le32 cpu_valid;
+	__le32 cpu_stale_ticks;
+	__le32 cpu_temp_deci_c;
+	__le32 gpu_valid;
+	__le32 gpu_stale_ticks;
+	__le32 gpu_temp_deci_c;
+	__le32 current_duty_ns;
+	__le32 target_duty_ns;
+	__le32 pwm_channel;
+	__le32 pwm_period_ns;
+	__le32 loop_count;
+	__le32 fault_count;
+	__le32 last_status;
+	__le32 control_mode;
+	__le32 manual_pwm;
+} __packed;
+
+struct radxa_svc_fan_control_req {
+	__le32 control_mode;
+	__le32 manual_pwm;
+} __packed;
+
+struct radxa_svc_fan_control_resp {
+	__le32 control_mode;
+	__le32 manual_pwm;
+} __packed;
+
+struct radxa_svc_glink {
+	struct device *dev;
+	struct rpmsg_device *rpdev;
+	struct device *hwmon_dev;
+
+	struct mutex xfer_lock; /* serializes request/response transactions */
+	struct mutex fan_lock; /* serializes multi-request fan configuration */
+	spinlock_t rsp_lock;
+	struct completion rsp;
+	bool pending;
+	bool shutting_down;
+	u32 pending_seq;
+	u32 seq;
+
+	u32 rsp_opcode;
+	int rsp_status;
+	size_t rsp_len;
+	u8 rsp_payload[RADXA_SVC_MAX_RX_PAYLOAD];
+	u32 caps;
+};
+
+static int radxa_svc_request(struct radxa_svc_glink *svc, u32 opcode,
+			     const void *req_payload, size_t req_len,
+			     void *rsp_payload, size_t *rsp_len);
+
+static int radxa_svc_get_version(struct radxa_svc_glink *svc,
+				 struct radxa_svc_version_resp *resp)
+{
+	size_t len = sizeof(*resp);
+	int ret;
+
+	ret = radxa_svc_request(svc, RADXA_SVC_OP_GET_VERSION, NULL, 0,
+				resp, &len);
+	if (ret)
+		return ret;
+
+	if (len < sizeof(*resp))
+		return -EIO;
+
+	return 0;
+}
+
+static int radxa_svc_check_version(struct device *dev,
+				   const struct radxa_svc_version_resp *resp,
+				   u32 *caps)
+{
+	u32 missing_caps;
+	u16 major;
+
+	major = le16_to_cpu(resp->major);
+	*caps = le32_to_cpu(resp->caps);
+
+	if (major != RADXA_SVC_VERSION)
+		return dev_err_probe(dev, -EPROTONOSUPPORT,
+				     "unsupported service major version %u\n",
+				     major);
+
+	missing_caps = RADXA_SVC_REQUIRED_CAPS & ~*caps;
+	if (missing_caps)
+		return dev_err_probe(dev, -ENODEV,
+				     "service missing required caps 0x%08x\n",
+				     missing_caps);
+
+	return 0;
+}
+
+static int radxa_svc_get_profile(struct radxa_svc_glink *svc, u32 *profile)
+{
+	__le32 resp;
+	size_t len = sizeof(resp);
+	int ret;
+
+	ret = radxa_svc_request(svc, RADXA_SVC_OP_GET_PROFILE, NULL, 0,
+				&resp, &len);
+	if (ret)
+		return ret;
+
+	if (len < sizeof(resp))
+		return -EIO;
+
+	*profile = le32_to_cpu(resp);
+
+	return 0;
+}
+
+static int radxa_svc_set_profile(struct radxa_svc_glink *svc, u32 profile)
+{
+	__le32 payload;
+
+	switch (profile) {
+	case RADXA_SVC_PROFILE_QUIET:
+	case RADXA_SVC_PROFILE_PERFORMANCE:
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	payload = cpu_to_le32(profile);
+
+	return radxa_svc_request(svc, RADXA_SVC_OP_SET_PROFILE, &payload,
+				 sizeof(payload), NULL, NULL);
+}
+
+static int radxa_svc_fan_get_state(struct radxa_svc_glink *svc,
+				   struct radxa_svc_fan_state_resp *resp)
+{
+	size_t len = sizeof(*resp);
+	size_t base_len = offsetof(struct radxa_svc_fan_state_resp,
+				   control_mode);
+	int ret;
+
+	ret = radxa_svc_request(svc, RADXA_SVC_OP_FAN_GET_STATE, NULL, 0,
+				resp, &len);
+	if (ret)
+		return ret;
+
+	if (len < base_len)
+		return -EIO;
+
+	return 0;
+}
+
+static int radxa_svc_fan_get_control(struct radxa_svc_glink *svc,
+				     struct radxa_svc_fan_control_resp *resp)
+{
+	size_t len = sizeof(*resp);
+	u32 control_mode;
+	u32 manual_pwm;
+	int ret;
+
+	ret = radxa_svc_request(svc, RADXA_SVC_OP_FAN_GET_CONTROL, NULL, 0,
+				resp, &len);
+	if (ret)
+		return ret;
+
+	if (len < sizeof(*resp))
+		return -EIO;
+
+	control_mode = le32_to_cpu(resp->control_mode);
+	manual_pwm = le32_to_cpu(resp->manual_pwm);
+	if (control_mode > RADXA_SVC_FAN_CONTROL_AUTO ||
+	    manual_pwm > RADXA_SVC_FAN_PWM_MAX)
+		return -EIO;
+
+	return 0;
+}
+
+static int radxa_svc_fan_set_control(struct radxa_svc_glink *svc,
+				     u32 control_mode, u32 manual_pwm)
+{
+	struct radxa_svc_fan_control_req req;
+
+	if (control_mode > RADXA_SVC_FAN_CONTROL_AUTO ||
+	    manual_pwm > RADXA_SVC_FAN_PWM_MAX)
+		return -EINVAL;
+
+	req.control_mode = cpu_to_le32(control_mode);
+	req.manual_pwm = cpu_to_le32(manual_pwm);
+
+	return radxa_svc_request(svc, RADXA_SVC_OP_FAN_SET_CONTROL, &req,
+				 sizeof(req), NULL, NULL);
+}
+
+static int radxa_svc_fan_duty_to_pwm(u32 period_ns, u32 duty_ns, u32 *value)
+{
+	u64 pwm;
+
+	if (!period_ns)
+		return -ENODATA;
+
+	if (duty_ns > period_ns)
+		return -EPROTO;
+
+	pwm = (u64)(period_ns - duty_ns) * RADXA_SVC_FAN_PWM_MAX;
+	*value = min_t(u64, DIV_ROUND_CLOSEST_ULL(pwm, period_ns),
+		       RADXA_SVC_FAN_PWM_MAX);
+
+	return 0;
+}
+
+static int radxa_svc_request(struct radxa_svc_glink *svc, u32 opcode,
+			     const void *req_payload, size_t req_len,
+			     void *rsp_payload, size_t *rsp_len)
+{
+	struct radxa_svc_hdr *hdr;
+	unsigned long flags;
+	size_t tx_len;
+	u32 seq;
+	u8 *tx_buf;
+	int ret;
+
+	if (req_len > RADXA_SVC_MAX_TX_PAYLOAD)
+		return -EMSGSIZE;
+
+	tx_len = sizeof(*hdr) + req_len;
+	tx_buf = kzalloc(tx_len, GFP_KERNEL);
+	if (!tx_buf)
+		return -ENOMEM;
+
+	mutex_lock(&svc->xfer_lock);
+
+	if (!svc->rpdev || !svc->rpdev->ept) {
+		ret = -ENODEV;
+		goto out_unlock;
+	}
+
+	seq = ++svc->seq;
+	if (!seq)
+		seq = ++svc->seq;
+
+	hdr = (struct radxa_svc_hdr *)tx_buf;
+	hdr->magic = cpu_to_le32(RADXA_SVC_MAGIC);
+	hdr->version = cpu_to_le16(RADXA_SVC_VERSION);
+	hdr->header_size = cpu_to_le16(sizeof(*hdr));
+	hdr->opcode = cpu_to_le32(opcode);
+	hdr->seq = cpu_to_le32(seq);
+	hdr->status = cpu_to_le32(0);
+	hdr->payload_len = cpu_to_le32(req_len);
+
+	if (req_len)
+		memcpy(tx_buf + sizeof(*hdr), req_payload, req_len);
+
+	reinit_completion(&svc->rsp);
+
+	spin_lock_irqsave(&svc->rsp_lock, flags);
+	if (svc->shutting_down) {
+		spin_unlock_irqrestore(&svc->rsp_lock, flags);
+		ret = -ENODEV;
+		goto out_unlock;
+	}
+
+	svc->pending = true;
+	svc->pending_seq = seq;
+	svc->rsp_opcode = 0;
+	svc->rsp_status = 0;
+	svc->rsp_len = 0;
+	spin_unlock_irqrestore(&svc->rsp_lock, flags);
+
+	ret = rpmsg_send(svc->rpdev->ept, tx_buf, tx_len);
+	if (ret) {
+		spin_lock_irqsave(&svc->rsp_lock, flags);
+		svc->pending = false;
+		spin_unlock_irqrestore(&svc->rsp_lock, flags);
+		goto out_unlock;
+	}
+
+	if (!wait_for_completion_timeout(&svc->rsp, RADXA_SVC_TIMEOUT)) {
+		spin_lock_irqsave(&svc->rsp_lock, flags);
+		svc->pending = false;
+		spin_unlock_irqrestore(&svc->rsp_lock, flags);
+		ret = -ETIMEDOUT;
+		goto out_unlock;
+	}
+
+	spin_lock_irqsave(&svc->rsp_lock, flags);
+	if (svc->shutting_down) {
+		ret = -ENODEV;
+	} else if (svc->rsp_opcode != opcode) {
+		ret = -EIO;
+	} else {
+		ret = svc->rsp_status;
+		if (rsp_payload && rsp_len) {
+			size_t copy_len = min(*rsp_len, svc->rsp_len);
+
+			memcpy(rsp_payload, svc->rsp_payload, copy_len);
+			if (*rsp_len < svc->rsp_len && !ret)
+				ret = -EMSGSIZE;
+			*rsp_len = svc->rsp_len;
+		}
+	}
+	spin_unlock_irqrestore(&svc->rsp_lock, flags);
+
+out_unlock:
+	mutex_unlock(&svc->xfer_lock);
+	kfree(tx_buf);
+
+	return ret;
+}
+
+static int radxa_svc_rpmsg_callback(struct rpmsg_device *rpdev, void *data,
+				    int len, void *priv, u32 addr)
+{
+	struct radxa_svc_glink *svc = dev_get_drvdata(&rpdev->dev);
+	const struct radxa_svc_hdr *hdr = data;
+	unsigned long flags;
+	size_t header_size;
+	size_t payload_len;
+	bool do_complete = false;
+	u32 seq;
+
+	if (len < sizeof(*hdr))
+		goto bad_msg;
+
+	if (le32_to_cpu(hdr->magic) != RADXA_SVC_MAGIC ||
+	    le16_to_cpu(hdr->version) != RADXA_SVC_VERSION)
+		goto bad_msg;
+
+	header_size = le16_to_cpu(hdr->header_size);
+	if (header_size < sizeof(*hdr) || header_size > len)
+		goto bad_msg;
+
+	payload_len = le32_to_cpu(hdr->payload_len);
+	if (payload_len > len - header_size ||
+	    payload_len > RADXA_SVC_MAX_RX_PAYLOAD)
+		goto bad_msg;
+
+	seq = le32_to_cpu(hdr->seq);
+
+	spin_lock_irqsave(&svc->rsp_lock, flags);
+	if (svc->pending && seq == svc->pending_seq) {
+		svc->rsp_opcode = le32_to_cpu(hdr->opcode);
+		svc->rsp_status = (s32)le32_to_cpu(hdr->status);
+		svc->rsp_len = payload_len;
+		memcpy(svc->rsp_payload, data + header_size, payload_len);
+		svc->pending = false;
+		do_complete = true;
+	}
+	spin_unlock_irqrestore(&svc->rsp_lock, flags);
+
+	if (do_complete)
+		complete(&svc->rsp);
+
+	return 0;
+
+bad_msg:
+	return 0;
+}
+
+static int radxa_svc_fan_get_pwm_mode(struct radxa_svc_glink *svc,
+				      long *mode)
+{
+	struct radxa_svc_fan_control_resp control = {};
+	u32 control_mode;
+	u32 profile;
+	int ret;
+
+	ret = radxa_svc_fan_get_control(svc, &control);
+	if (ret)
+		return ret;
+
+	control_mode = le32_to_cpu(control.control_mode);
+
+	switch (control_mode) {
+	case RADXA_SVC_FAN_CONTROL_FULL_SPEED:
+		*mode = RADXA_SVC_PWM_MODE_FULL_SPEED;
+		return 0;
+	case RADXA_SVC_FAN_CONTROL_MANUAL:
+		*mode = RADXA_SVC_PWM_MODE_MANUAL;
+		return 0;
+	case RADXA_SVC_FAN_CONTROL_AUTO:
+		break;
+	default:
+		return -EIO;
+	}
+
+	ret = radxa_svc_get_profile(svc, &profile);
+	if (ret)
+		return ret;
+
+	switch (profile) {
+	case RADXA_SVC_PROFILE_QUIET:
+		*mode = RADXA_SVC_PWM_MODE_AUTO_QUIET;
+		return 0;
+	case RADXA_SVC_PROFILE_PERFORMANCE:
+		*mode = RADXA_SVC_PWM_MODE_AUTO_PERFORMANCE;
+		return 0;
+	default:
+		return -EIO;
+	}
+}
+
+static int radxa_svc_fan_set_auto_mode(struct radxa_svc_glink *svc,
+				       u32 profile)
+{
+	struct radxa_svc_fan_control_resp control = {};
+	int ret;
+
+	ret = radxa_svc_fan_get_control(svc, &control);
+	if (ret)
+		return ret;
+
+	ret = radxa_svc_set_profile(svc, profile);
+	if (ret)
+		return ret;
+
+	return radxa_svc_fan_set_control(svc, RADXA_SVC_FAN_CONTROL_AUTO,
+					 le32_to_cpu(control.manual_pwm));
+}
+
+static int radxa_svc_fan_set_pwm_mode(struct radxa_svc_glink *svc, long mode)
+{
+	struct radxa_svc_fan_state_resp state = {};
+	u32 duty_ns;
+	u32 manual_pwm;
+	u32 period_ns;
+	int ret;
+
+	switch (mode) {
+	case RADXA_SVC_PWM_MODE_FULL_SPEED:
+		return radxa_svc_fan_set_control(svc,
+						 RADXA_SVC_FAN_CONTROL_FULL_SPEED,
+						 RADXA_SVC_FAN_PWM_MAX);
+	case RADXA_SVC_PWM_MODE_MANUAL:
+		ret = radxa_svc_fan_get_state(svc, &state);
+		if (ret)
+			return ret;
+
+		period_ns = le32_to_cpu(state.pwm_period_ns);
+		duty_ns = le32_to_cpu(state.current_duty_ns);
+		if (!period_ns) {
+			manual_pwm = RADXA_SVC_FAN_PWM_MAX;
+		} else {
+			ret = radxa_svc_fan_duty_to_pwm(period_ns, duty_ns,
+							&manual_pwm);
+			if (ret)
+				return ret;
+		}
+
+		return radxa_svc_fan_set_control(svc,
+						 RADXA_SVC_FAN_CONTROL_MANUAL,
+						 manual_pwm);
+	case RADXA_SVC_PWM_MODE_AUTO_QUIET:
+		return radxa_svc_fan_set_auto_mode(svc,
+						RADXA_SVC_PROFILE_QUIET);
+	case RADXA_SVC_PWM_MODE_AUTO_PERFORMANCE:
+		return radxa_svc_fan_set_auto_mode(svc,
+						RADXA_SVC_PROFILE_PERFORMANCE);
+	default:
+		return -EINVAL;
+	}
+}
+
+static umode_t radxa_svc_hwmon_is_visible(const void *data,
+					  enum hwmon_sensor_types type,
+					  u32 attr, int channel)
+{
+	if (type != hwmon_pwm || channel)
+		return 0;
+
+	switch (attr) {
+	case hwmon_pwm_input:
+	case hwmon_pwm_enable:
+		return 0644;
+	default:
+		return 0;
+	}
+}
+
+static int radxa_svc_hwmon_read(struct device *dev,
+				enum hwmon_sensor_types type, u32 attr,
+				int channel, long *val)
+{
+	struct radxa_svc_glink *svc = dev_get_drvdata(dev);
+	struct radxa_svc_fan_state_resp state = {};
+	u32 duty_ns;
+	u32 period_ns;
+	u32 pwm;
+	int ret;
+
+	if (type != hwmon_pwm || channel)
+		return -EOPNOTSUPP;
+
+	switch (attr) {
+	case hwmon_pwm_input:
+		ret = radxa_svc_fan_get_state(svc, &state);
+		if (ret)
+			return ret;
+
+		period_ns = le32_to_cpu(state.pwm_period_ns);
+		duty_ns = le32_to_cpu(state.current_duty_ns);
+		ret = radxa_svc_fan_duty_to_pwm(period_ns, duty_ns, &pwm);
+		if (ret)
+			return ret;
+
+		*val = pwm;
+		return 0;
+	case hwmon_pwm_enable:
+		mutex_lock(&svc->fan_lock);
+		ret = radxa_svc_fan_get_pwm_mode(svc, val);
+		mutex_unlock(&svc->fan_lock);
+
+		return ret;
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static int radxa_svc_hwmon_write(struct device *dev,
+				 enum hwmon_sensor_types type, u32 attr,
+				 int channel, long val)
+{
+	struct radxa_svc_glink *svc = dev_get_drvdata(dev);
+	struct radxa_svc_fan_control_resp control = {};
+	int ret;
+
+	if (type != hwmon_pwm || channel)
+		return -EOPNOTSUPP;
+
+	mutex_lock(&svc->fan_lock);
+
+	switch (attr) {
+	case hwmon_pwm_input:
+		if (val < 0 || val > RADXA_SVC_FAN_PWM_MAX) {
+			ret = -EINVAL;
+			break;
+		}
+
+		ret = radxa_svc_fan_get_control(svc, &control);
+		if (ret)
+			break;
+
+		if (le32_to_cpu(control.control_mode) !=
+		    RADXA_SVC_FAN_CONTROL_MANUAL) {
+			ret = -EINVAL;
+			break;
+		}
+
+		ret = radxa_svc_fan_set_control(svc,
+						RADXA_SVC_FAN_CONTROL_MANUAL,
+						val);
+		break;
+	case hwmon_pwm_enable:
+		ret = radxa_svc_fan_set_pwm_mode(svc, val);
+		break;
+	default:
+		ret = -EOPNOTSUPP;
+		break;
+	}
+
+	mutex_unlock(&svc->fan_lock);
+
+	return ret;
+}
+
+static const struct hwmon_ops radxa_svc_hwmon_ops = {
+	.is_visible = radxa_svc_hwmon_is_visible,
+	.read = radxa_svc_hwmon_read,
+	.write = radxa_svc_hwmon_write,
+};
+
+static const struct hwmon_channel_info * const radxa_svc_hwmon_info[] = {
+	HWMON_CHANNEL_INFO(pwm, HWMON_PWM_INPUT | HWMON_PWM_ENABLE),
+	NULL
+};
+
+static const struct hwmon_chip_info radxa_svc_hwmon_chip_info = {
+	.ops = &radxa_svc_hwmon_ops,
+	.info = radxa_svc_hwmon_info,
+};
+
+static int radxa_svc_hwmon_init(struct radxa_svc_glink *svc)
+{
+	svc->hwmon_dev = devm_hwmon_device_register_with_info(svc->dev,
+							      "radxa_svc_glink",
+							      svc,
+							      &radxa_svc_hwmon_chip_info,
+							      NULL);
+
+	return PTR_ERR_OR_ZERO(svc->hwmon_dev);
+}
+
+static int radxa_svc_rpmsg_probe(struct rpmsg_device *rpdev)
+{
+	struct radxa_svc_version_resp version = {};
+	struct radxa_svc_glink *svc;
+	int ret;
+
+	svc = devm_kzalloc(&rpdev->dev, sizeof(*svc), GFP_KERNEL);
+	if (!svc)
+		return -ENOMEM;
+
+	svc->dev = &rpdev->dev;
+	svc->rpdev = rpdev;
+	mutex_init(&svc->xfer_lock);
+	mutex_init(&svc->fan_lock);
+	spin_lock_init(&svc->rsp_lock);
+	init_completion(&svc->rsp);
+
+	dev_set_drvdata(&rpdev->dev, svc);
+
+	ret = radxa_svc_get_version(svc, &version);
+	if (ret)
+		return dev_err_probe(&rpdev->dev, ret,
+				     "failed to read service version\n");
+
+	ret = radxa_svc_check_version(&rpdev->dev, &version, &svc->caps);
+	if (ret)
+		return ret;
+
+	ret = radxa_svc_hwmon_init(svc);
+	if (ret)
+		return dev_err_probe(&rpdev->dev, ret,
+				     "failed to register hwmon\n");
+
+	return 0;
+}
+
+static void radxa_svc_rpmsg_remove(struct rpmsg_device *rpdev)
+{
+	struct radxa_svc_glink *svc = dev_get_drvdata(&rpdev->dev);
+	unsigned long flags;
+
+	spin_lock_irqsave(&svc->rsp_lock, flags);
+	svc->shutting_down = true;
+	svc->pending = false;
+	spin_unlock_irqrestore(&svc->rsp_lock, flags);
+	complete_all(&svc->rsp);
+
+	mutex_lock(&svc->xfer_lock);
+	svc->rpdev = NULL;
+	mutex_unlock(&svc->xfer_lock);
+}
+
+static const struct rpmsg_device_id radxa_svc_rpmsg_id_match[] = {
+	{ "RADXA_SVC_ADSP_APPS" },
+	{}
+};
+MODULE_DEVICE_TABLE(rpmsg, radxa_svc_rpmsg_id_match);
+
+static struct rpmsg_driver radxa_svc_rpmsg_driver = {
+	.probe = radxa_svc_rpmsg_probe,
+	.remove = radxa_svc_rpmsg_remove,
+	.callback = radxa_svc_rpmsg_callback,
+	.id_table = radxa_svc_rpmsg_id_match,
+	.drv = {
+		.name = "radxa_svc_glink",
+	},
+};
+module_rpmsg_driver(radxa_svc_rpmsg_driver);
+
+MODULE_AUTHOR("Xilin Wu <sophon@radxa.com>");
+MODULE_DESCRIPTION("Radxa SVC GLINK driver");
+MODULE_LICENSE("GPL");

-- 
2.55.0


  reply	other threads:[~2026-08-31 15:11 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-31 15:09 [PATCH 0/2] platform: arm64: Add Radxa SVC GLINK support Xilin Wu
2026-08-31 15:09 ` Xilin Wu [this message]
2026-09-03 12:55   ` [PATCH 1/2] platform: arm64: Add Radxa SVC GLINK driver Ilpo Järvinen
2026-09-04  9:44   ` Konrad Dybcio
2026-08-31 15:09 ` [PATCH 2/2] platform: arm64: radxa-svc: Add hwmon sensors Xilin Wu
2026-09-03 13:06   ` Ilpo Järvinen

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260831-radxa-svc-v1-1-7c028de6a387@radxa.com \
    --to=sophon@radxa.com \
    --cc=bryan.odonoghue@linaro.org \
    --cc=corbet@lwn.net \
    --cc=hansg@kernel.org \
    --cc=ilpo.jarvinen@linux.intel.com \
    --cc=linux-arm-msm@vger.kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-hwmon@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux@roeck-us.net \
    --cc=platform-driver-x86@vger.kernel.org \
    --cc=rdunlap@infradead.org \
    --cc=skhan@linuxfoundation.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox