Documentation/ABI/testing/debugfs-spmi | 75 +++++ MAINTAINERS | 1 + drivers/spmi/Makefile | 1 + drivers/spmi/spmi-debugfs.c | 371 +++++++++++++++++++++++++ drivers/spmi/spmi.c | 7 + include/linux/spmi.h | 18 ++ 6 files changed, 473 insertions(+) create mode 100644 Documentation/ABI/testing/debugfs-spmi create mode 100644 drivers/spmi/spmi-debugfs.c
Introduce a generic, controller-agnostic DebugFS interface in the SPMI
core under /sys/kernel/debug/spmi/spmi-<nr>/:
- name: Displays the controller parent device name and OF node.
- detect: Probes the bus for active slave IDs (0x0 to 0xF).
- xfer: Transactional interface supporting register reads, writes,
and directed command opcodes with automatic opcode selection.
Write and command operations restrict access to root (mode 0600) and
mark the kernel tainted with TAINT_USER. Runtime PM is managed
automatically across transactions.
Detailed interface syntax and parameters are documented in
Documentation/ABI/testing/debugfs-spmi.
Co-developed-by: Jim Wylder <jwylder@google.com>
Signed-off-by: Jim Wylder <jwylder@google.com>
Signed-off-by: vamshi gajjela <vamshigajjela@google.com>
---
Documentation/ABI/testing/debugfs-spmi | 75 +++++
MAINTAINERS | 1 +
drivers/spmi/Makefile | 1 +
drivers/spmi/spmi-debugfs.c | 371 +++++++++++++++++++++++++
drivers/spmi/spmi.c | 7 +
include/linux/spmi.h | 18 ++
6 files changed, 473 insertions(+)
create mode 100644 Documentation/ABI/testing/debugfs-spmi
create mode 100644 drivers/spmi/spmi-debugfs.c
diff --git a/Documentation/ABI/testing/debugfs-spmi b/Documentation/ABI/testing/debugfs-spmi
new file mode 100644
index 000000000000..10ad075b8619
--- /dev/null
+++ b/Documentation/ABI/testing/debugfs-spmi
@@ -0,0 +1,75 @@
+What: /sys/kernel/debug/spmi/spmi-<nr>/name
+Date: September 2026
+KernelVersion: 7.0
+Contact: vamshi gajjela <vamshigajjela@google.com>
+Description:
+ Displays the SPMI controller parent device name and
+ associated Device Tree node.
+Users: Debugging, hardware diagnostics
+
+What: /sys/kernel/debug/spmi/spmi-<nr>/detect
+Date: September 2026
+KernelVersion: 7.0
+Contact: vamshi gajjela <vamshigajjela@google.com>
+Description:
+ Scans the SPMI bus for all 16 possible MIPI SPMI slave IDs
+ (0x0 to 0xF) using standard register reads at offset 0x0.
+ Active slaves are printed in two-digit hexadecimal notation
+ in an i2cdetect-style grid, while unresponsive slaves are
+ represented with "--".
+Users: Debugging, hardware diagnostics
+
+What: /sys/kernel/debug/spmi/spmi-<nr>/xfer
+Date: September 2026
+KernelVersion: 7.0
+Contact: vamshi gajjela <vamshigajjela@google.com>
+Description:
+ Transactional interface for raw SPMI bus operations:
+
+ - Register Read:
+ echo "r <sid> <addr> [len]" > xfer && cat xfer
+ Reads 1 to 16 bytes starting at <addr> from slave <sid>.
+ The optional [len] parameter defaults to 1 byte.
+ Output is formatted as:
+ 0x<addr>: <byte0> <byte1> ...
+ Reading the file consumes the response; subsequent reads
+ return EOF (0 bytes) to prevent reading stale data.
+
+ - Register Write:
+ echo "w <sid> <addr> <val...>" > xfer
+ Writes 1 to 16 bytes starting at <addr> to slave <sid>.
+ Values are separated by spaces (e.g. "w 0x4 0x8 0x12 0x34"
+ or "w 4 8 12 34").
+
+ - Command:
+ echo "c <sid> <opcode>" > xfer
+ Sends a single SPMI command opcode (e.g. RESET 0x10,
+ SLEEP 0x11, SHUTDOWN 0x12, WAKEUP 0x13) to slave <sid>.
+
+ Parameter Formatting & Validation:
+ - <sid>: 4-bit Slave Identifier (0x0 to 0xF, supports both
+ decimal "4" and hexadecimal "0x4").
+ - <addr>: 16-bit register address (0x0000 to 0xFFFF) parsed in
+ hexadecimal (supports both "0x12" and "12", as well as
+ "08", "09").
+ - [len]: Number of bytes to read (1 to 16 for 8-bit addresses,
+ up to 8 bytes for 16-bit extended addresses; default 1).
+ - <val...>: 1 to 16 hexadecimal byte values (0x00 to 0xFF,
+ up to 8 bytes for 16-bit extended addresses). Values
+ exceeding 0xFF or payloads exceeding 16 bytes are rejected
+ with -EINVAL.
+ - <opcode>: 1-byte command opcode parsed in hexadecimal
+ (0x00 to 0xFF, e.g. "0x10" or "10").
+ - Trailing unrecognized arguments return -EINVAL.
+
+ The interface automatically selects the optimal SPMI opcode
+ (Register Read/Write, Extended Register Read/Write, Extended
+ Register Read/Write Long, Zero Write) based on the address
+ and burst length.
+
+ All write and command operations mark the kernel as tainted
+ with TAINT_USER. On failure, the write system call returns a
+ negative errno (e.g. -EIO on bus NACK/timeout, -EINVAL on
+ syntax/range error, -EOPNOTSUPP if the controller lacks the
+ capability).
+Users: Debugging, hardware bringup, manufacturing test
diff --git a/MAINTAINERS b/MAINTAINERS
index 529c7d7ba113..6ba973aa98de 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -24946,6 +24946,7 @@ M: Stephen Boyd <sboyd@kernel.org>
L: linux-kernel@vger.kernel.org
S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/sboyd/spmi.git
+F: Documentation/ABI/testing/debugfs-spmi
F: Documentation/devicetree/bindings/spmi/
F: drivers/spmi/
F: include/dt-bindings/spmi/spmi.h
diff --git a/drivers/spmi/Makefile b/drivers/spmi/Makefile
index 38ac635645ba..23cd6036863b 100644
--- a/drivers/spmi/Makefile
+++ b/drivers/spmi/Makefile
@@ -3,6 +3,7 @@
# Makefile for kernel SPMI framework.
#
obj-$(CONFIG_SPMI) += spmi.o spmi-devres.o
+obj-$(CONFIG_DEBUG_FS) += spmi-debugfs.o
obj-$(CONFIG_SPMI_APPLE) += spmi-apple-controller.o
obj-$(CONFIG_SPMI_HISI3670) += hisi-spmi-controller.o
diff --git a/drivers/spmi/spmi-debugfs.c b/drivers/spmi/spmi-debugfs.c
new file mode 100644
index 000000000000..4b216a0f325e
--- /dev/null
+++ b/drivers/spmi/spmi-debugfs.c
@@ -0,0 +1,371 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * SPMI core DebugFS interface
+ *
+ * Copyright (C) 2026 Google LLC
+ */
+
+#include <linux/debugfs.h>
+#include <linux/seq_file.h>
+#include <linux/uaccess.h>
+#include <linux/slab.h>
+#include <linux/pm_runtime.h>
+#include <linux/spmi.h>
+#include <linux/mutex.h>
+#include <linux/ctype.h>
+#include <linux/string.h>
+#include <linux/of.h>
+#include <linux/panic.h>
+
+struct spmi_ctrl_debugfs {
+ struct dentry *dentry;
+ struct mutex lock; /* protects xfer_buf and xfer_len */
+ char xfer_buf[64];
+ size_t xfer_len;
+};
+
+/* Top-level SPMI debugfs directory (/sys/kernel/debug/spmi) */
+static struct dentry *spmi_debugfs_root;
+
+static int spmi_calc_read_opcode(u16 addr, size_t len, u8 *opc)
+{
+ if (len == 0)
+ return -EINVAL;
+
+ if (addr <= 0x1f && len == 1)
+ *opc = SPMI_CMD_READ;
+ else if (addr <= 0xff && len <= 16)
+ *opc = SPMI_CMD_EXT_READ;
+ else if (len <= 8)
+ *opc = SPMI_CMD_EXT_READL;
+ else
+ return -EINVAL;
+
+ return 0;
+}
+
+static int spmi_calc_write_opcode(u16 addr, size_t len, u8 *opc)
+{
+ if (len == 0)
+ return -EINVAL;
+
+ if (addr == 0 && len == 1)
+ *opc = SPMI_CMD_ZERO_WRITE;
+ else if (addr <= 0x1f && len == 1)
+ *opc = SPMI_CMD_WRITE;
+ else if (addr <= 0xff && len <= 16)
+ *opc = SPMI_CMD_EXT_WRITE;
+ else if (len <= 8)
+ *opc = SPMI_CMD_EXT_WRITEL;
+ else
+ return -EINVAL;
+
+ return 0;
+}
+
+static int spmi_debugfs_name_show(struct seq_file *s, void *data)
+{
+ struct spmi_controller *ctrl = s->private;
+ struct device *parent = ctrl->dev.parent;
+
+ if (!parent)
+ return -ENODEV;
+
+ if (dev_of_node(parent))
+ seq_printf(s, "%s (%pOF)\n", dev_name(parent), dev_of_node(parent));
+ else
+ seq_printf(s, "%s\n", dev_name(parent));
+
+ return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(spmi_debugfs_name);
+
+static int spmi_debugfs_detect_show(struct seq_file *s, void *data)
+{
+ struct spmi_controller *ctrl = s->private;
+ struct device *parent = ctrl->dev.parent;
+ int ret, sid;
+ u8 dummy;
+ bool rpm;
+
+ if (!ctrl->read_cmd)
+ return -EOPNOTSUPP;
+
+ rpm = parent ? pm_runtime_enabled(parent) : false;
+ if (rpm) {
+ ret = pm_runtime_resume_and_get(parent);
+ if (ret < 0)
+ return ret;
+ }
+
+ /*
+ * MIPI SPMI defines a 4-bit Slave Identifier (SID), allowing a maximum
+ * of 16 slave devices (0x0 to 0xF) per bus. Format as a single-row grid.
+ */
+ seq_puts(s, " 0 1 2 3 4 5 6 7 8 9 a b c d e f\n00: ");
+ for (sid = 0; sid < SPMI_MAX_SLAVE_ID; sid++) {
+ ret = ctrl->read_cmd(ctrl, SPMI_CMD_READ, sid, 0x0, &dummy, 1);
+ if (ret == 0)
+ seq_printf(s, "%02x ", sid);
+ else
+ seq_puts(s, "-- ");
+ }
+ seq_puts(s, "\n");
+
+ if (rpm) {
+ pm_runtime_mark_last_busy(parent);
+ pm_runtime_put_autosuspend(parent);
+ }
+
+ return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(spmi_debugfs_detect);
+
+static ssize_t spmi_debugfs_xfer_read(struct file *file, char __user *user_buf,
+ size_t count, loff_t *ppos)
+{
+ struct spmi_controller *ctrl = file->private_data;
+ struct spmi_ctrl_debugfs *dbg = ctrl->debugfs;
+ ssize_t ret;
+
+ if (!dbg)
+ return -ENODEV;
+
+ mutex_lock(&dbg->lock);
+ ret = simple_read_from_buffer(user_buf, count, ppos, dbg->xfer_buf, dbg->xfer_len);
+ if (ret > 0 && *ppos >= dbg->xfer_len)
+ dbg->xfer_len = 0;
+ mutex_unlock(&dbg->lock);
+
+ return ret;
+}
+
+static char *spmi_debugfs_next_tok(char **str)
+{
+ char *tok;
+
+ *str = skip_spaces(*str);
+ if (!**str)
+ return NULL;
+
+ tok = *str;
+ while (**str && !isspace(**str))
+ (*str)++;
+
+ if (**str) {
+ **str = '\0';
+ (*str)++;
+ }
+
+ return tok;
+}
+
+static ssize_t spmi_debugfs_xfer_write(struct file *file, const char __user *user_buf,
+ size_t count, loff_t *ppos)
+{
+ struct spmi_controller *ctrl = file->private_data;
+ struct spmi_ctrl_debugfs *dbg = ctrl->debugfs;
+ struct device *parent = ctrl->dev.parent;
+ unsigned int sid, addr, len;
+ unsigned int cmd_opc, val;
+ char *str, *tok;
+ char kbuf[256];
+ int ret = 0, i;
+ u8 data[16];
+ size_t off;
+ u8 opcode;
+ char type;
+ bool rpm;
+
+ if (!dbg)
+ return -ENODEV;
+
+ if (count == 0 || count >= sizeof(kbuf))
+ return -EINVAL;
+
+ if (copy_from_user(kbuf, user_buf, count))
+ return -EFAULT;
+ kbuf[count] = '\0';
+
+ str = strim(kbuf);
+ type = tolower(*str);
+ if (type != 'r' && type != 'w' && type != 'c')
+ return -EINVAL;
+ str = skip_spaces(str + 1);
+
+ tok = spmi_debugfs_next_tok(&str);
+ if (!tok || kstrtouint(tok, 0, &sid) || sid >= SPMI_MAX_SLAVE_ID)
+ return -EINVAL;
+
+ rpm = parent ? pm_runtime_enabled(parent) : false;
+ if (rpm) {
+ ret = pm_runtime_resume_and_get(parent);
+ if (ret < 0)
+ return ret;
+ }
+
+ mutex_lock(&dbg->lock);
+ dbg->xfer_len = 0;
+
+ if (type == 'r') {
+ tok = spmi_debugfs_next_tok(&str);
+ if (!tok || kstrtouint(tok, 16, &addr)) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ len = 1;
+ tok = spmi_debugfs_next_tok(&str);
+ if (tok) {
+ if (kstrtouint(tok, 0, &len) || len == 0 || len > sizeof(data)) {
+ ret = -EINVAL;
+ goto out;
+ }
+ if (spmi_debugfs_next_tok(&str)) {
+ ret = -EINVAL;
+ goto out;
+ }
+ }
+
+ if (!ctrl->read_cmd) {
+ ret = -EOPNOTSUPP;
+ goto out;
+ }
+
+ ret = spmi_calc_read_opcode(addr, len, &opcode);
+ if (ret)
+ goto out;
+
+ ret = ctrl->read_cmd(ctrl, opcode, sid, addr, data, len);
+ if (!ret) {
+ off = scnprintf(dbg->xfer_buf, sizeof(dbg->xfer_buf),
+ "0x%04x:", addr);
+
+ for (i = 0; i < len; i++)
+ off += scnprintf(dbg->xfer_buf + off, sizeof(dbg->xfer_buf) - off,
+ " %02x", data[i]);
+ off += scnprintf(dbg->xfer_buf + off, sizeof(dbg->xfer_buf) - off, "\n");
+ dbg->xfer_len = off;
+ }
+ } else if (type == 'w') {
+ tok = spmi_debugfs_next_tok(&str);
+ if (!tok || kstrtouint(tok, 16, &addr)) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ len = 0;
+ while ((tok = spmi_debugfs_next_tok(&str)) != NULL) {
+ if (len >= sizeof(data) || kstrtouint(tok, 16, &val) || val > 0xff) {
+ ret = -EINVAL;
+ goto out;
+ }
+ data[len++] = val;
+ }
+
+ if (len == 0) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ if (!ctrl->write_cmd) {
+ ret = -EOPNOTSUPP;
+ goto out;
+ }
+
+ ret = spmi_calc_write_opcode(addr, len, &opcode);
+ if (ret)
+ goto out;
+
+ add_taint(TAINT_USER, LOCKDEP_STILL_OK);
+
+ ret = ctrl->write_cmd(ctrl, opcode, sid, addr, data, len);
+ } else if (type == 'c') {
+ tok = spmi_debugfs_next_tok(&str);
+ if (!tok || kstrtouint(tok, 16, &cmd_opc) || cmd_opc > 0xff) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ if (spmi_debugfs_next_tok(&str)) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ if (!ctrl->cmd) {
+ ret = -EOPNOTSUPP;
+ goto out;
+ }
+
+ add_taint(TAINT_USER, LOCKDEP_STILL_OK);
+
+ ret = ctrl->cmd(ctrl, cmd_opc, sid);
+ }
+
+out:
+ mutex_unlock(&dbg->lock);
+
+ if (rpm) {
+ pm_runtime_mark_last_busy(parent);
+ pm_runtime_put_autosuspend(parent);
+ }
+
+ return ret ? ret : count;
+}
+
+static const struct file_operations spmi_debugfs_xfer_fops = {
+ .open = simple_open,
+ .read = spmi_debugfs_xfer_read,
+ .write = spmi_debugfs_xfer_write,
+ .llseek = default_llseek,
+};
+
+void spmi_controller_debugfs_init(struct spmi_controller *ctrl)
+{
+ struct spmi_ctrl_debugfs *dbg;
+ char name[32];
+
+ if (IS_ERR_OR_NULL(spmi_debugfs_root))
+ return;
+
+ dbg = kzalloc_obj(*dbg, GFP_KERNEL);
+ if (!dbg)
+ return;
+
+ mutex_init(&dbg->lock);
+
+ snprintf(name, sizeof(name), "spmi-%u", ctrl->nr);
+ dbg->dentry = debugfs_create_dir(name, spmi_debugfs_root);
+ if (IS_ERR_OR_NULL(dbg->dentry)) {
+ kfree(dbg);
+ return;
+ }
+
+ debugfs_create_file("name", 0444, dbg->dentry, ctrl, &spmi_debugfs_name_fops);
+ debugfs_create_file("detect", 0444, dbg->dentry, ctrl, &spmi_debugfs_detect_fops);
+ debugfs_create_file("xfer", 0600, dbg->dentry, ctrl, &spmi_debugfs_xfer_fops);
+
+ ctrl->debugfs = dbg;
+}
+
+void spmi_controller_debugfs_remove(struct spmi_controller *ctrl)
+{
+ if (!ctrl->debugfs)
+ return;
+
+ debugfs_remove(ctrl->debugfs->dentry);
+ kfree(ctrl->debugfs);
+ ctrl->debugfs = NULL;
+}
+
+void spmi_debugfs_init(void)
+{
+ if (!spmi_debugfs_root)
+ spmi_debugfs_root = debugfs_create_dir("spmi", NULL);
+}
+
+void spmi_debugfs_exit(void)
+{
+ debugfs_remove(spmi_debugfs_root);
+ spmi_debugfs_root = NULL;
+}
diff --git a/drivers/spmi/spmi.c b/drivers/spmi/spmi.c
index e889b129f3ac..7d2e9a762fb8 100644
--- a/drivers/spmi/spmi.c
+++ b/drivers/spmi/spmi.c
@@ -554,6 +554,8 @@ int spmi_controller_add(struct spmi_controller *ctrl)
dev_dbg(&ctrl->dev, "spmi-%d registered: dev:%p\n",
ctrl->nr, &ctrl->dev);
+ spmi_controller_debugfs_init(ctrl);
+
return 0;
};
EXPORT_SYMBOL_GPL(spmi_controller_add);
@@ -580,6 +582,8 @@ void spmi_controller_remove(struct spmi_controller *ctrl)
if (!ctrl)
return;
+ spmi_controller_debugfs_remove(ctrl);
+
device_for_each_child(&ctrl->dev, NULL, spmi_ctrl_remove_device);
device_del(&ctrl->dev);
}
@@ -603,6 +607,7 @@ EXPORT_SYMBOL_GPL(__spmi_driver_register);
static void __exit spmi_exit(void)
{
+ spmi_debugfs_exit();
bus_unregister(&spmi_bus_type);
}
module_exit(spmi_exit);
@@ -615,6 +620,8 @@ static int __init spmi_init(void)
if (ret)
return ret;
+ spmi_debugfs_init();
+
is_registered = true;
return 0;
}
diff --git a/include/linux/spmi.h b/include/linux/spmi.h
index 28e8c8bd3944..fcd8b9fc6cbf 100644
--- a/include/linux/spmi.h
+++ b/include/linux/spmi.h
@@ -76,7 +76,10 @@ void spmi_device_remove(struct spmi_device *sdev);
* @cmd: sends a non-data command sequence on the SPMI bus.
* @read_cmd: sends a register read command sequence on the SPMI bus.
* @write_cmd: sends a register write command sequence on the SPMI bus.
+ * @debugfs: debugfs private data for this controller.
*/
+struct spmi_ctrl_debugfs;
+
struct spmi_controller {
struct device dev;
unsigned int nr;
@@ -85,6 +88,9 @@ struct spmi_controller {
u8 sid, u16 addr, u8 *buf, size_t len);
int (*write_cmd)(struct spmi_controller *ctrl, u8 opcode,
u8 sid, u16 addr, const u8 *buf, size_t len);
+#if IS_ENABLED(CONFIG_DEBUG_FS)
+ struct spmi_ctrl_debugfs *debugfs;
+#endif
};
static inline struct spmi_controller *to_spmi_controller(struct device *d)
@@ -186,4 +192,16 @@ int spmi_command_sleep(struct spmi_device *sdev);
int spmi_command_wakeup(struct spmi_device *sdev);
int spmi_command_shutdown(struct spmi_device *sdev);
+#if IS_ENABLED(CONFIG_DEBUG_FS)
+void spmi_debugfs_init(void);
+void spmi_debugfs_exit(void);
+void spmi_controller_debugfs_init(struct spmi_controller *ctrl);
+void spmi_controller_debugfs_remove(struct spmi_controller *ctrl);
+#else
+static inline void spmi_debugfs_init(void) {}
+static inline void spmi_debugfs_exit(void) {}
+static inline void spmi_controller_debugfs_init(struct spmi_controller *ctrl) {}
+static inline void spmi_controller_debugfs_remove(struct spmi_controller *ctrl) {}
+#endif
+
#endif
--
2.56.0.rc1.310.g51773c2048-goog
© 2016 - 2026 Red Hat, Inc.