* [PATCH v2 0/3] Add SDIO userspace passthrough support
@ 2026-09-21 7:18 Shawn Lin
2026-09-21 7:18 ` [PATCH v2 1/3] mmc: core: Add SDIO userspace passthrough character device Shawn Lin
` (3 more replies)
0 siblings, 4 replies; 5+ messages in thread
From: Shawn Lin @ 2026-09-21 7:18 UTC (permalink / raw)
To: Ulf Hansson; +Cc: linux-mmc, linux-kernel, Shawn Lin, Shawn Lin
SDIO cards are normally driven by function drivers (brcmfmac,
mwifiex, rtw88_8723bs, ...), but bring-up, debugging and factory
test frequently need to poke the SDIO register space directly:
CCCR/FBR in function 0, or vendor registers before any function
driver is bound, and some deployments, notably lightweight IoT
boards, want to handle the SDIO function entirely from userspace.
Today the only way is to write a tiny kernel driver per device.
So this series adds raw userspace access to the SDIO register
space as well a small reference client. Meanwhile, adding a new
documentation about how to write a proper SDIO function driver,
because I have recently been working on SDIO function drivers
and kept referring to the existing drivers for guidance. It turns
out that the knowledge is scattered and there is no unified document
describing how an SDIO function driver is supposed to be written,
despite the MMC core having clear expectations about how the APIs
are used.
In particular, the interrupt model has many pitfalls:
sdio_claim_irq()/sdio_release_irq() must be called with the host
claimed, the handler runs with the host already claimed and must
not sleep or claim the bus itself, and sdio_release_irq() must be
called from ->remove() because it is the last chance before the
core removes the function devices from mmc_remove_host(). Recent
fixes around the SDIO IRQ work and host removal and the discussions
on the mailing list showed how easily these contracts get broken.
Also worth documenting is the buffer requirement of the CMD53 based
helpers: the core builds a scatterlist directly from the caller's
buffer and the host driver maps it for DMA, so vmalloc() or stack
allocated buffers are not acceptable, while the CMD52 byte accessors
are safe with stack buffers.
Changes-in-v2:
- drop RFC as I don't include mmc-utils to be sdio usersapce tool
- add sdio doc
Shawn Lin (3):
mmc: core: Add SDIO userspace passthrough character device
tools/sdio: add sdio-raw userspace client
docs: mmc: add introduction to writing SDIO function drivers
Documentation/driver-api/mmc/index.rst | 1 +
.../driver-api/mmc/sdio-function-drivers.rst | 211 ++++++
MAINTAINERS | 2 +
drivers/mmc/Kconfig | 11 +
drivers/mmc/core/Makefile | 1 +
drivers/mmc/core/sdio_bus.c | 12 +-
drivers/mmc/core/sdio_cdev.c | 189 ++++++
drivers/mmc/core/sdio_cdev.h | 21 +
include/linux/mmc/sdio_func.h | 4 +
include/uapi/linux/mmc/sdio_ioctl.h | 48 ++
tools/Makefile | 14 +-
tools/sdio/Build | 1 +
tools/sdio/Makefile | 66 ++
tools/sdio/gen-sdio-ids.sh | 59 ++
tools/sdio/sdio-raw.c | 714 +++++++++++++++++++++
15 files changed, 1346 insertions(+), 8 deletions(-)
create mode 100644 Documentation/driver-api/mmc/sdio-function-drivers.rst
create mode 100644 drivers/mmc/core/sdio_cdev.c
create mode 100644 drivers/mmc/core/sdio_cdev.h
create mode 100644 include/uapi/linux/mmc/sdio_ioctl.h
create mode 100644 tools/sdio/Build
create mode 100644 tools/sdio/Makefile
create mode 100644 tools/sdio/gen-sdio-ids.sh
create mode 100644 tools/sdio/sdio-raw.c
--
2.7.4
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2 1/3] mmc: core: Add SDIO userspace passthrough character device
2026-09-21 7:18 [PATCH v2 0/3] Add SDIO userspace passthrough support Shawn Lin
@ 2026-09-21 7:18 ` Shawn Lin
2026-09-21 7:18 ` [PATCH v2 2/3] tools/sdio: add sdio-raw userspace client Shawn Lin
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Shawn Lin @ 2026-09-21 7:18 UTC (permalink / raw)
To: Ulf Hansson; +Cc: linux-mmc, linux-kernel, Shawn Lin
From: Shawn Lin <shawn.lin@linux.dev>
Expose a character device (/dev/mmcX:YYYY:FN) for every SDIO
function, providing userspace access to the SDIO register space:
- SDIO_IOC_RW issues CMD52: single byte read/write. Function 0
addresses the card common area (CCCR/FBR); with the raw flag the
register is read back after being written.
- SDIO_IOC_RW_EXT issues CMD53 in byte mode for 1..512 bytes.
Intended for debugging and factory test tools on systems without a
function driver (the userspace counterpart is a new 'mmc sdio'
command in mmc-utils). The ioctls require CAP_SYS_ADMIN, are gated
behind CONFIG_MMC_SDIO_CDEV and take the host claim for the duration
of each command, since raw access can disturb the state a bound
function driver maintains.
Signed-off-by: Shawn Lin <shawn.lin@linux.dev>
---
drivers/mmc/Kconfig | 11 +++
drivers/mmc/core/Makefile | 1 +
drivers/mmc/core/sdio_bus.c | 12 ++-
drivers/mmc/core/sdio_cdev.c | 189 ++++++++++++++++++++++++++++++++++++
drivers/mmc/core/sdio_cdev.h | 21 ++++
include/linux/mmc/sdio_func.h | 4 +
include/uapi/linux/mmc/sdio_ioctl.h | 48 +++++++++
7 files changed, 284 insertions(+), 2 deletions(-)
create mode 100644 drivers/mmc/core/sdio_cdev.c
create mode 100644 drivers/mmc/core/sdio_cdev.h
create mode 100644 include/uapi/linux/mmc/sdio_ioctl.h
diff --git a/drivers/mmc/Kconfig b/drivers/mmc/Kconfig
index 2436eb4..5d0ddc5 100644
--- a/drivers/mmc/Kconfig
+++ b/drivers/mmc/Kconfig
@@ -13,6 +13,17 @@ menuconfig MMC
If you want MMC/SD/SDIO support, you should say Y here and
also to your specific host controller driver.
+config MMC_SDIO_CDEV
+ bool "SDIO userspace passthrough character device"
+ depends on MMC
+ default y
+ help
+ Expose a character device (/dev/mmcX:YYYY:FN) for every SDIO
+ function, providing CMD52/CMD53 register access to userspace
+ via ioctl. Intended for debugging and factory test tools;
+ raw access requires CAP_SYS_ADMIN and can disturb the state
+ a bound function driver maintains.
+
if MMC
source "drivers/mmc/core/Kconfig"
diff --git a/drivers/mmc/core/Makefile b/drivers/mmc/core/Makefile
index 15b067e..485b4f5 100644
--- a/drivers/mmc/core/Makefile
+++ b/drivers/mmc/core/Makefile
@@ -4,6 +4,7 @@
#
obj-$(CONFIG_MMC) += mmc_core.o
+obj-$(CONFIG_MMC_SDIO_CDEV) += sdio_cdev.o
mmc_core-y := core.o bus.o host.o \
mmc.o mmc_ops.o sd.o sd_ops.o \
sdio.o sdio_ops.o sdio_bus.o \
diff --git a/drivers/mmc/core/sdio_bus.c b/drivers/mmc/core/sdio_bus.c
index 4b07098..06fc26cb 100644
--- a/drivers/mmc/core/sdio_bus.c
+++ b/drivers/mmc/core/sdio_bus.c
@@ -23,6 +23,7 @@
#include "core.h"
#include "card.h"
+#include "sdio_cdev.h"
#include "sdio_cis.h"
#include "sdio_bus.h"
@@ -400,9 +401,12 @@ int sdio_add_func(struct sdio_func *func)
sdio_acpi_set_handle(func);
device_enable_async_suspend(&func->dev);
ret = device_add(&func->dev);
- if (ret == 0)
+ if (ret == 0) {
sdio_func_set_present(func);
+ sdio_cdev_create(func);
+ }
+
return ret;
}
@@ -414,8 +418,12 @@ int sdio_add_func(struct sdio_func *func)
*/
void sdio_remove_func(struct sdio_func *func)
{
- if (sdio_func_present(func))
+ if (sdio_func_present(func)) {
+ sdio_cdev_destroy(func);
+
device_del(&func->dev);
+ }
+
of_node_put(func->dev.of_node);
put_device(&func->dev);
diff --git a/drivers/mmc/core/sdio_cdev.c b/drivers/mmc/core/sdio_cdev.c
new file mode 100644
index 0000000..aa2f4cf
--- /dev/null
+++ b/drivers/mmc/core/sdio_cdev.c
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * SDIO userspace passthrough character device.
+ *
+ * Copyright (C) 2026 Shawn Lin <shawn.lin@linux.dev>
+ *
+ * Every SDIO function gets a /dev/<function name> node (e.g.
+ * /dev/mmc1:0001:1); ioctls on the node issue CMD52 (single register
+ * access, function 0 included for the CCCR/FBR) and CMD53 (extended
+ * multi-byte access) to the card.
+ *
+ * Raw access can disturb the state a bound function driver maintains,
+ * so the ioctls require CAP_SYS_ADMIN and take the host claim for the
+ * duration of each command.
+ */
+#include <linux/capability.h>
+#include <linux/fs.h>
+#include <linux/miscdevice.h>
+#include <linux/module.h>
+#include <linux/pm_runtime.h>
+#include <linux/slab.h>
+#include <linux/uaccess.h>
+
+#include <uapi/linux/mmc/sdio_ioctl.h>
+
+#include <linux/mmc/card.h>
+#include <linux/mmc/host.h>
+#include <linux/mmc/sdio_func.h>
+
+#include "sdio_cdev.h"
+#include "sdio_ops.h"
+
+static int sdio_cdev_rw(struct sdio_func *func,
+ struct sdio_ioc_rw __user *uarg)
+{
+ struct sdio_ioc_rw rw;
+ u8 data = 0;
+ int err, ret;
+
+ if (!capable(CAP_SYS_ADMIN))
+ return -EPERM;
+
+ if (copy_from_user(&rw, uarg, sizeof(rw)))
+ return -EFAULT;
+
+ if (rw.fn > 7)
+ return -EINVAL;
+
+ pm_runtime_get_sync(&func->dev);
+ sdio_claim_host(func);
+ if (rw.write && !rw.raw) {
+ /*
+ * Plain write: pass a NULL out so the kernel does not set
+ * the CMD52 RAW bit - out doubles as the RAW flag there.
+ */
+ err = mmc_io_rw_direct(func->card, 1, rw.fn, rw.addr,
+ rw.data, NULL);
+ } else {
+ err = mmc_io_rw_direct(func->card, rw.write, rw.fn, rw.addr,
+ rw.write ? rw.data : 0, &data);
+ }
+ sdio_release_host(func);
+ pm_runtime_put_autosuspend(&func->dev);
+
+ if (err) {
+ dev_err(&func->dev, "CMD52 fn %u addr 0x%05x failed: %d\n",
+ rw.fn, rw.addr, err);
+ return err;
+ }
+
+ if (!rw.write || rw.raw)
+ rw.data = data;
+ ret = copy_to_user(uarg, &rw, sizeof(rw));
+
+ return ret ? -EFAULT : 0;
+}
+
+static int sdio_cdev_rw_ext(struct sdio_func *func,
+ struct sdio_ioc_rw_ext __user *uarg)
+{
+ struct sdio_ioc_rw_ext rw;
+ u8 *buf;
+ int err;
+
+ if (!capable(CAP_SYS_ADMIN))
+ return -EPERM;
+
+ if (copy_from_user(&rw, uarg, sizeof(rw)))
+ return -EFAULT;
+
+ if (rw.fn == 0 || rw.fn > 7)
+ return -EINVAL;
+ if (!rw.count || rw.count > 512)
+ return -EINVAL;
+
+ pm_runtime_get_sync(&func->dev);
+
+ /*
+ * Stage through a kmalloc'd buffer: rw.data lives on the ioctl
+ * handler's stack, which is vmalloc'd with CONFIG_VMAP_STACK -
+ * virt_to_page() on a vmalloc address is invalid, and the PIO
+ * and DMA paths of the host drivers both derive struct pages
+ * from the data buffer.
+ */
+ buf = kmalloc(rw.count, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ if (rw.write)
+ memcpy(buf, rw.data, rw.count);
+
+ sdio_claim_host(func);
+ err = mmc_io_rw_extended(func->card, rw.write, rw.fn, rw.addr,
+ rw.incr_addr, buf, 0, rw.count);
+ sdio_release_host(func);
+ pm_runtime_put_autosuspend(&func->dev);
+
+ if (!err && !rw.write)
+ memcpy(rw.data, buf, rw.count);
+
+ kfree(buf);
+
+ if (err) {
+ dev_err(&func->dev, "CMD53 fn %u addr 0x%05x failed: %d\n",
+ rw.fn, rw.addr, err);
+ return err;
+ }
+
+ return 0;
+}
+
+static long sdio_cdev_ioctl(struct file *file, unsigned int cmd,
+ unsigned long arg)
+{
+ struct miscdevice *misc = file->private_data;
+ struct sdio_func *func = container_of(misc->parent,
+ struct sdio_func, dev);
+
+ switch (cmd) {
+ case SDIO_IOC_RW:
+ return sdio_cdev_rw(func, (struct sdio_ioc_rw __user *)arg);
+ case SDIO_IOC_RW_EXT:
+ return sdio_cdev_rw_ext(func,
+ (struct sdio_ioc_rw_ext __user *)arg);
+ default:
+ return -ENOTTY;
+ }
+}
+
+static const struct file_operations sdio_cdev_fops = {
+ .owner = THIS_MODULE,
+ .unlocked_ioctl = sdio_cdev_ioctl,
+};
+
+int sdio_cdev_create(struct sdio_func *func)
+{
+ struct miscdevice *misc;
+ int ret;
+
+ misc = kzalloc_obj(*misc);
+ if (!misc)
+ return -ENOMEM;
+
+ misc->minor = MISC_DYNAMIC_MINOR;
+ misc->name = dev_name(&func->dev);
+ misc->fops = &sdio_cdev_fops;
+ misc->parent = &func->dev;
+ func->miscdev = misc;
+
+ ret = misc_register(func->miscdev);
+ if (ret) {
+ dev_err(&func->dev, "failed to register /dev/%s: %d\n",
+ dev_name(&func->dev), ret);
+ kfree(misc);
+ func->miscdev = NULL;
+ }
+
+ return ret;
+}
+
+void sdio_cdev_destroy(struct sdio_func *func)
+{
+ if (!func->miscdev)
+ return;
+
+ misc_deregister(func->miscdev);
+ kfree(func->miscdev);
+ func->miscdev = NULL;
+}
diff --git a/drivers/mmc/core/sdio_cdev.h b/drivers/mmc/core/sdio_cdev.h
new file mode 100644
index 0000000..93bac8c
--- /dev/null
+++ b/drivers/mmc/core/sdio_cdev.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _MMC_CORE_SDIO_CDEV_H
+#define _MMC_CORE_SDIO_CDEV_H
+
+#include <linux/mmc/sdio_func.h>
+
+#ifdef CONFIG_MMC_SDIO_CDEV
+int sdio_cdev_create(struct sdio_func *func);
+void sdio_cdev_destroy(struct sdio_func *func);
+#else
+static inline int sdio_cdev_create(struct sdio_func *func)
+{
+ return 0;
+}
+
+static inline void sdio_cdev_destroy(struct sdio_func *func)
+{
+}
+#endif
+
+#endif /* _MMC_CORE_SDIO_CDEV_H */
diff --git a/include/linux/mmc/sdio_func.h b/include/linux/mmc/sdio_func.h
index 5d63a64..6d01579 100644
--- a/include/linux/mmc/sdio_func.h
+++ b/include/linux/mmc/sdio_func.h
@@ -57,6 +57,10 @@ struct sdio_func {
const char **info; /* info strings */
struct sdio_func_tuple *tuples;
+
+#ifdef CONFIG_MMC_SDIO_CDEV
+ struct miscdevice *miscdev; /* userspace passthrough node */
+#endif
};
#define sdio_func_present(f) ((f)->state & SDIO_STATE_PRESENT)
diff --git a/include/uapi/linux/mmc/sdio_ioctl.h b/include/uapi/linux/mmc/sdio_ioctl.h
new file mode 100644
index 0000000..d099bec8
--- /dev/null
+++ b/include/uapi/linux/mmc/sdio_ioctl.h
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef _UAPI_LINUX_MMC_SDIO_IOCTL_H
+#define _UAPI_LINUX_MMC_SDIO_IOCTL_H
+
+#include <linux/ioctl.h>
+#include <linux/types.h>
+
+/*
+ * SDIO userspace passthrough interface, issued on the character device
+ * exposed by each SDIO function (/dev/mmcX:YYYY:FN). Requires
+ * CAP_SYS_ADMIN. Raw access while a function driver is bound can
+ * disturb the state the driver maintains.
+ */
+
+/*
+ * CMD52: single byte read/write. fn 0 addresses the card common area
+ * (CCCR/FBR), functions 1-7 address the per-function registers. With
+ * raw set, the register is read back after being written and data
+ * carries the value actually read.
+ */
+struct sdio_ioc_rw {
+ __u8 write; /* 1 = write, 0 = read */
+ __u8 fn; /* function number, 0..7 */
+ __u8 raw; /* read back after write */
+ __u8 data; /* byte to write / value read */
+ __u32 addr; /* SDIO register address */
+};
+
+/*
+ * CMD53: extended (multiple byte) read/write in byte mode, 1..512
+ * bytes. Function 0 is not valid for CMD53.
+ */
+struct sdio_ioc_rw_ext {
+ __u8 write; /* 1 = write, 0 = read */
+ __u8 fn; /* function number, 1..7 */
+ __u8 incr_addr; /* 1 = incrementing address, 0 = fixed */
+ __u8 reserved1;
+ __u32 addr; /* SDIO register address */
+ __u16 count; /* number of bytes, 1..512 */
+ __u16 reserved2;
+ __u8 data[512]; /* data to write / buffer for read */
+};
+
+#define SDIO_IOC_MAGIC 'S'
+#define SDIO_IOC_RW _IOWR(SDIO_IOC_MAGIC, 0x52, struct sdio_ioc_rw)
+#define SDIO_IOC_RW_EXT _IOWR(SDIO_IOC_MAGIC, 0x53, struct sdio_ioc_rw_ext)
+
+#endif /* _UAPI_LINUX_MMC_SDIO_IOCTL_H */
--
2.7.4
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2 2/3] tools/sdio: add sdio-raw userspace client
2026-09-21 7:18 [PATCH v2 0/3] Add SDIO userspace passthrough support Shawn Lin
2026-09-21 7:18 ` [PATCH v2 1/3] mmc: core: Add SDIO userspace passthrough character device Shawn Lin
@ 2026-09-21 7:18 ` Shawn Lin
2026-09-21 7:18 ` [PATCH v2 3/3] docs: mmc: add introduction to writing SDIO function drivers Shawn Lin
2026-10-29 0:27 ` [PATCH v2 0/3] Add SDIO userspace passthrough support Shawn Lin
3 siblings, 0 replies; 5+ messages in thread
From: Shawn Lin @ 2026-09-21 7:18 UTC (permalink / raw)
To: Ulf Hansson; +Cc: linux-mmc, linux-kernel, Shawn Lin
From: Shawn Lin <shawn.lin@linux.dev>
Add a minimal reference/debug client for the SDIO userspace passthrough
character device (CONFIG_MMC_SDIO_CDEV). It issues CMD52/CMD53 through
the SDIO_IOC_RW and SDIO_IOC_RW_EXT ioctls of /dev/mmcX:YYYY:FN, which
covers the usual bring-up and factory-test needs:
- list: enumerate SDIO functions from sysfs; the VID:PID is looked
up against the id database of include/linux/mmc/sdio_ids.h and
shown with its name, or as Unlisted(0x<pid>); the CIS strings the
kernel parsed at enumeration (sysfs info1..4) are shown as well
- cmd52: single byte read/write, with read-back verification
- cmd53: byte mode read/write of 1..512 bytes, hexdump or file I/O
- cccr: dump the CCCR of function 0
- fbr: dump the FBR of the selected function, accessed through
function 0 like the kernel does (SDIO_FBR_BASE(), the CIS pointer
sits at offset 0x09 of the FBR)
- cis: walk the common (fn 0) or per-function CIS tuple chain the
same way drivers/mmc/core/sdio_cis.c does, decoding MANFID,
VERS_1 and FUNCID, hexdumping the rest
The name tables for 'list' are generated at build time from
sdio_ids.h, so no database is duplicated in the tree. The ioctls
require CAP_SYS_ADMIN. The target function is parsed from the device
node name and can be overridden with -f, so any function can be poked
through a single node.
Also add tools/sdio/ to the MMC subsystem entry in MAINTAINERS.
Tested on RK3576 with a Cypress 43752 SDIO WLAN card (both functions
report 0x02d0:0xaae8). The card's function CIS carries no VERS_1
tuple, so the INFO column of 'list' stays empty for it.
# ./sdio-raw -d /dev/mmc2:0001:1 cmd52 -a 0x0
fn1 addr 0x00000: 0xe8
# ./sdio-raw -d /dev/mmc2:0001:1 cmd53 -a 0x0 -n 8
0000: 00 00 00 00 00 00 00 00 ........
# ./sdio-raw -d /dev/mmc2:0001:1 cccr
CCCR (fn 0):
0x00 CCCR/SDIO revision 0x43
0x01 SD spec revision 0x03
0x02 I/O enable 0x06
0x03 I/O ready 0x06
0x04 Int enable 0x06
0x05 Int pending 0x00
0x06 I/O abort 0x00
0x07 Bus interface control 0x42
0x08 Card capability 0x02
0x09-0x0b Common CIS pointer 0x001070
0x0c Bus suspend 0x00
0x0d Function select 0x00
0x0e Exec flags 0x00
0x0f Ready flags 0x00
0x10-0x11 FN0 block size 0x0000
0x12 Power control 0x01
0x13 High-speed 0x07
# ./sdio-raw -d /dev/mmc2:0001:1 cis -f 1
CIS (fn 1) @ 0x01000:
0x01000 TPL 0x20 MANFID len=4
manufacturer 0x02d0, product 0xaae8
0x01006 TPL 0x21 FUNCID len=2
function code 0x0c
Signed-off-by: Shawn Lin <shawn.lin@linux.dev>
---
MAINTAINERS | 1 +
tools/Makefile | 14 +-
tools/sdio/Build | 1 +
tools/sdio/Makefile | 66 +++++
tools/sdio/gen-sdio-ids.sh | 59 ++++
tools/sdio/sdio-raw.c | 714 +++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 849 insertions(+), 6 deletions(-)
create mode 100644 tools/sdio/Build
create mode 100644 tools/sdio/Makefile
create mode 100644 tools/sdio/gen-sdio-ids.sh
create mode 100644 tools/sdio/sdio-raw.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 06760c4..a476c06 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18845,6 +18845,7 @@ F: Documentation/devicetree/bindings/mmc/
F: drivers/mmc/
F: include/linux/mmc/
F: include/uapi/linux/mmc/
+F: tools/sdio/
MULTIPLEXER SUBSYSTEM
M: Peter Rosin <peda@lysator.liu.se>
diff --git a/tools/Makefile b/tools/Makefile
index cb40961..10efd38 100644
--- a/tools/Makefile
+++ b/tools/Makefile
@@ -31,6 +31,7 @@ help:
@echo ' sched_ext - sched_ext example schedulers'
@echo ' bootconfig - boot config tool'
@echo ' spi - spi tools'
+ @echo ' sdio - sdio tools'
@echo ' tmon - thermal monitoring and tuning tool'
@echo ' thermometer - temperature capture tool'
@echo ' thermal-engine - thermal monitoring tool'
@@ -70,7 +71,7 @@ acpi: FORCE
cpupower: FORCE
$(call descend,power/$@)
-counter dma firewire hv guest bootconfig spi usb virtio mm bpf iio gpio objtool leds wmi firmware debugging tracing: FORCE
+counter dma firewire hv guest bootconfig spi sdio usb virtio mm bpf iio gpio objtool leds wmi firmware debugging tracing: FORCE
$(call descend,$@)
bpf/%: FORCE
@@ -124,7 +125,7 @@ ynl: FORCE
$(call descend,net/ynl)
all: acpi counter cpupower dma gpio hv firewire \
- perf selftests bootconfig spi turbostat usb \
+ perf selftests bootconfig spi sdio turbostat usb \
virtio mm bpf x86_energy_perf_policy \
tmon freefall iio objtool kvm_stat wmi \
debugging tracing thermal thermometer thermal-engine ynl
@@ -135,7 +136,7 @@ acpi_install:
cpupower_install:
$(call descend,power/$(@:_install=),install)
-counter_install dma_install firewire_install gpio_install hv_install iio_install perf_install bootconfig_install spi_install usb_install virtio_install mm_install bpf_install objtool_install wmi_install debugging_install tracing_install:
+counter_install dma_install firewire_install gpio_install hv_install iio_install perf_install bootconfig_install spi_install sdio_install usb_install virtio_install mm_install bpf_install objtool_install wmi_install debugging_install tracing_install:
$(call descend,$(@:_install=),install)
selftests_install:
@@ -171,7 +172,8 @@ install: acpi_install counter_install cpupower_install dma_install gpio_install
virtio_install mm_install bpf_install x86_energy_perf_policy_install \
tmon_install freefall_install objtool_install kvm_stat_install \
wmi_install debugging_install intel-speed-select_install \
- tracing_install thermometer_install thermal-engine_install ynl_install
+ tracing_install thermometer_install thermal-engine_install ynl_install \
+ sdio_install
acpi_clean:
$(call descend,power/acpi,clean)
@@ -179,7 +181,7 @@ acpi_clean:
cpupower_clean:
$(call descend,power/cpupower,clean)
-counter_clean dma_clean hv_clean firewire_clean bootconfig_clean spi_clean usb_clean virtio_clean mm_clean wmi_clean bpf_clean iio_clean gpio_clean objtool_clean leds_clean firmware_clean debugging_clean tracing_clean:
+counter_clean dma_clean hv_clean firewire_clean bootconfig_clean spi_clean sdio_clean usb_clean virtio_clean mm_clean wmi_clean bpf_clean iio_clean gpio_clean objtool_clean leds_clean firmware_clean debugging_clean tracing_clean:
$(call descend,$(@:_clean=),clean)
libapi_clean:
@@ -226,7 +228,7 @@ ynl_clean:
$(call descend,net/$(@:_clean=),clean)
clean: acpi_clean counter_clean cpupower_clean dma_clean hv_clean firewire_clean \
- perf_clean selftests_clean turbostat_clean bootconfig_clean spi_clean usb_clean virtio_clean \
+ perf_clean selftests_clean turbostat_clean bootconfig_clean spi_clean sdio_clean usb_clean virtio_clean \
mm_clean bpf_clean iio_clean x86_energy_perf_policy_clean tmon_clean \
freefall_clean build_clean libbpf_clean libsubcmd_clean \
gpio_clean objtool_clean leds_clean wmi_clean firmware_clean debugging_clean \
diff --git a/tools/sdio/Build b/tools/sdio/Build
new file mode 100644
index 0000000..582fecd
--- /dev/null
+++ b/tools/sdio/Build
@@ -0,0 +1 @@
+sdio-raw-y += sdio-raw.o
diff --git a/tools/sdio/Makefile b/tools/sdio/Makefile
new file mode 100644
index 0000000..229bab8
--- /dev/null
+++ b/tools/sdio/Makefile
@@ -0,0 +1,66 @@
+# SPDX-License-Identifier: GPL-2.0-only
+include ../scripts/Makefile.include
+
+bindir ?= /usr/bin
+
+ifeq ($(srctree),)
+srctree := $(patsubst %/,%,$(dir $(CURDIR)))
+srctree := $(patsubst %/,%,$(dir $(srctree)))
+endif
+
+# Do not use make's built-in rules
+# (this improves performance and avoids hard-to-debug behaviour);
+MAKEFLAGS += -r
+
+CFLAGS += -O2 -Wall -g -D_GNU_SOURCE -I$(OUTPUT)include -I$(OUTPUT).
+
+ALL_TARGETS := sdio-raw
+ALL_PROGRAMS := $(patsubst %,$(OUTPUT)%,$(ALL_TARGETS))
+
+all: $(ALL_PROGRAMS)
+
+export srctree OUTPUT CC LD CFLAGS
+include $(srctree)/tools/build/Makefile.include
+
+#
+# We need the SDIO UAPI header outside of the kernel tree
+#
+$(OUTPUT)include/linux/mmc/sdio_ioctl.h: FORCE
+ mkdir -p $(OUTPUT)include/linux/mmc 2>&1 || true
+ ln -sf $(CURDIR)/../../include/uapi/linux/mmc/sdio_ioctl.h $@
+
+prepare: $(OUTPUT)include/linux/mmc/sdio_ioctl.h $(OUTPUT)sdio_ids_gen.h
+
+#
+# Name tables for the 'list' command, generated from the kernel's
+# SDIO id database.
+#
+$(OUTPUT)sdio_ids_gen.h: gen-sdio-ids.sh $(srctree)/include/linux/mmc/sdio_ids.h
+ $(Q)$(SHELL) $< $(srctree)/include/linux/mmc/sdio_ids.h > $@
+
+#
+# sdio-raw
+#
+SDIO_RAW_IN := $(OUTPUT)sdio-raw-in.o
+$(SDIO_RAW_IN): prepare FORCE
+ $(Q)$(MAKE) $(build)=sdio-raw
+$(OUTPUT)sdio-raw: $(SDIO_RAW_IN) $(OUTPUT)sdio_ids_gen.h
+ $(QUIET_LINK)$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
+
+clean:
+ rm -f $(ALL_PROGRAMS)
+ rm -rf $(OUTPUT)include/
+ rm -f $(OUTPUT)sdio_ids_gen.h
+ find $(or $(OUTPUT),.) -name '*.o' -delete
+ find $(or $(OUTPUT),.) -name '\.*.o.d' -delete
+ find $(or $(OUTPUT),.) -name '\.*.o.cmd' -delete
+
+install: $(ALL_PROGRAMS)
+ install -d -m 755 $(DESTDIR)$(bindir); \
+ for program in $(ALL_PROGRAMS); do \
+ install $$program $(DESTDIR)$(bindir); \
+ done
+
+FORCE:
+
+.PHONY: all install clean FORCE prepare
diff --git a/tools/sdio/gen-sdio-ids.sh b/tools/sdio/gen-sdio-ids.sh
new file mode 100644
index 0000000..89a8489
--- /dev/null
+++ b/tools/sdio/gen-sdio-ids.sh
@@ -0,0 +1,59 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+#
+# Generate the SDIO name lookup tables for the sdio-raw 'list' command
+# from include/linux/mmc/sdio_ids.h. Devices and classes listed there
+# show up with their macro name, everything else is reported unlisted.
+
+set -e
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <path/to/sdio_ids.h>" >&2
+ exit 1
+fi
+
+src="$1"
+
+# The generated header carries the same license tag as this script.
+# It is assembled from parts so that checkpatch does not mistake the
+# tag inside the heredoc for a misplaced tag in this shell file.
+spdx='/* SPDX''-License-Identifier: GPL-2.0 */'
+
+cat <<EOF
+$spdx
+/*
+ * Auto-generated from include/linux/mmc/sdio_ids.h - do not edit.
+ * Regenerated by tools/sdio/Makefile at build time.
+ */
+#ifndef SDIO_IDS_GEN_H
+#define SDIO_IDS_GEN_H
+
+struct sdio_id_name {
+ unsigned int id;
+ const char *name;
+};
+
+static const struct sdio_id_name sdio_class_names[] = {
+EOF
+
+awk '
+/^#define[ \t]+SDIO_CLASS_[A-Za-z0-9_]+[ \t]+0[xX][0-9a-fA-F]+/ {
+ name = $2;
+ sub(/^SDIO_CLASS_/, "", name);
+ printf "\t{ %s, \"%s\" },\n", $3, name;
+}' "$src"
+
+echo "};"
+echo ""
+echo "static const struct sdio_id_name sdio_device_names[] = {"
+
+awk '
+/^#define[ \t]+SDIO_DEVICE_ID_[A-Za-z0-9_]+[ \t]+(0[xX][0-9a-fA-F]+|[0-9]+)/ {
+ name = $2;
+ sub(/^SDIO_DEVICE_ID_/, "", name);
+ printf "\t{ %s, \"%s\" },\n", $3, name;
+}' "$src"
+
+echo "};"
+echo ""
+echo "#endif /* SDIO_IDS_GEN_H */"
diff --git a/tools/sdio/sdio-raw.c b/tools/sdio/sdio-raw.c
new file mode 100644
index 0000000..ab8c0fb
--- /dev/null
+++ b/tools/sdio/sdio-raw.c
@@ -0,0 +1,714 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * sdio-raw - raw SDIO register access via the SDIO userspace
+ * passthrough character device (CONFIG_MMC_SDIO_CDEV).
+ *
+ * Reference/debug client for the SDIO_IOC_RW (CMD52) and
+ * SDIO_IOC_RW_EXT (CMD53) ioctls exposed by /dev/mmcX:YYYY:FN.
+ *
+ * The ioctls require CAP_SYS_ADMIN; raw access while a function
+ * driver is bound can disturb the state the driver maintains.
+ *
+ * Author: Shawn Lin <shawn.lin@linux.dev>
+ */
+
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+
+#include <linux/mmc/sdio_ioctl.h>
+
+#ifdef __has_include
+#if __has_include("sdio_ids_gen.h")
+#include "sdio_ids_gen.h"
+#define HAVE_SDIO_IDS 1
+#endif
+#endif
+
+#ifndef HAVE_SDIO_IDS
+/*
+ * No generated tables (e.g. manual build without the tools Makefile):
+ * everything is reported as unlisted.
+ */
+struct sdio_id_name {
+ unsigned int id;
+ const char *name;
+};
+static const struct sdio_id_name sdio_class_names[] = { { ~0U, "" } };
+static const struct sdio_id_name sdio_device_names[] = { { ~0U, "" } };
+#endif
+
+#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
+
+static const char *progname = "sdio-raw";
+
+static _Noreturn void usage(int rc)
+{
+ fprintf(rc ? stderr : stdout,
+"Usage: %s -d DEV COMMAND [ARGS]\n"
+"\n"
+"Raw SDIO register access through the SDIO passthrough character\n"
+"device. The ioctls require CAP_SYS_ADMIN (run as root).\n"
+"\n"
+"Options:\n"
+" -d, --device DEV SDIO function chardev, e.g. /dev/mmc1:0001:1\n"
+" -f, --func FN override function number (default: parsed from DEV)\n"
+" -h, --help show this help\n"
+"\n"
+"Commands:\n"
+" cmd52 -a ADDR [-w VAL] [-r] read one byte (-w: write it, -r: read back)\n"
+" cmd53 -a ADDR -n COUNT [-w FILE] [-o FILE] [--fixed]\n"
+" byte mode read/write, 1..512 bytes\n"
+" list enumerate SDIO functions (sysfs)\n"
+" cccr dump CCCR (function 0)\n"
+" fbr dump FBR of the selected function\n"
+" cis [-f FN] walk the common (fn 0) or function CIS\n"
+"\n"
+"Addresses accept 0x-hex or decimal. cmd53 read goes to stdout as a\n"
+"hexdump unless -o is given; cmd53 write reads from FILE ('-' = stdin).\n",
+ progname);
+ exit(rc);
+}
+
+static void die_errno(const char *what)
+{
+ fprintf(stderr, "%s: %s: %s", progname, what, strerror(errno));
+ if (errno == EPERM)
+ fprintf(stderr, " (CAP_SYS_ADMIN required, run as root)");
+ fputc('\n', stderr);
+ exit(1);
+}
+
+static unsigned long parse_num(const char *s, const char *what)
+{
+ char *end;
+ unsigned long v;
+
+ errno = 0;
+ v = strtoul(s, &end, 0);
+ if (errno || end == s || *end) {
+ fprintf(stderr, "%s: invalid %s: %s\n", progname, what, s);
+ exit(1);
+ }
+ return v;
+}
+
+/* /dev/mmc1:0001:1 -> function 1 */
+static unsigned int fn_from_dev(const char *dev)
+{
+ const char *p = strrchr(dev, ':');
+
+ if (p && p[1])
+ return strtoul(p + 1, NULL, 10);
+ return 0;
+}
+
+static int open_dev(const char *dev)
+{
+ int fd;
+
+ if (!dev) {
+ fprintf(stderr, "%s: no device given, use -d DEV\n", progname);
+ exit(1);
+ }
+
+ fd = open(dev, O_RDWR);
+ if (fd < 0) {
+ fprintf(stderr, "%s: open %s: %s\n", progname, dev,
+ strerror(errno));
+ exit(1);
+ }
+ return fd;
+}
+
+static int rd52(int fd, unsigned int fn, unsigned int addr,
+ unsigned char *val)
+{
+ struct sdio_ioc_rw rw = {
+ .fn = fn,
+ .addr = addr,
+ };
+
+ if (ioctl(fd, SDIO_IOC_RW, &rw) < 0)
+ return -1;
+ *val = rw.data;
+ return 0;
+}
+
+static void hexdump(const unsigned char *buf, unsigned int len)
+{
+ unsigned int i, j;
+
+ for (i = 0; i < len; i += 16) {
+ printf(" %04x:", i);
+ for (j = 0; j < 16; j++) {
+ if (i + j < len)
+ printf(" %02x", buf[i + j]);
+ else
+ printf(" ");
+ }
+ printf(" ");
+ for (j = 0; j < 16 && i + j < len; j++)
+ putchar(isprint(buf[i + j]) ? buf[i + j] : '.');
+ putchar('\n');
+ }
+}
+
+static void cmd52(int fd, unsigned int fn, unsigned long addr, int write,
+ unsigned long val, int raw)
+{
+ struct sdio_ioc_rw rw = {
+ .write = write,
+ .fn = fn,
+ .raw = raw,
+ .addr = addr,
+ .data = val,
+ };
+
+ if (ioctl(fd, SDIO_IOC_RW, &rw) < 0)
+ die_errno("CMD52 failed");
+
+ if (write && !raw)
+ printf("fn%u addr 0x%05lx: wrote 0x%02lx\n", rw.fn, addr, val);
+ else
+ printf("fn%u addr 0x%05lx: 0x%02x\n", rw.fn, addr, rw.data);
+}
+
+static void cmd53(int fd, unsigned int fn, unsigned long addr,
+ unsigned long count,
+ int write, int incr_addr, const char *infile,
+ const char *outfile)
+{
+ struct sdio_ioc_rw_ext rw;
+ FILE *f;
+
+ memset(&rw, 0, sizeof(rw));
+ rw.write = write;
+ rw.fn = fn;
+ rw.incr_addr = incr_addr;
+ rw.addr = addr;
+ rw.count = count;
+
+ if (write) {
+ f = (!infile || !strcmp(infile, "-")) ? stdin :
+ fopen(infile, "rb");
+ if (!f) {
+ fprintf(stderr, "%s: open %s: %s\n", progname, infile,
+ strerror(errno));
+ exit(1);
+ }
+ if (fread(rw.data, 1, count, f) != count) {
+ fprintf(stderr, "%s: short read from %s\n", progname,
+ infile ? infile : "stdin");
+ exit(1);
+ }
+ if (f != stdin)
+ fclose(f);
+ }
+
+ if (ioctl(fd, SDIO_IOC_RW_EXT, &rw) < 0)
+ die_errno("CMD53 failed");
+
+ if (write)
+ return;
+
+ if (outfile) {
+ f = fopen(outfile, "wb");
+ if (!f) {
+ fprintf(stderr, "%s: open %s: %s\n", progname, outfile,
+ strerror(errno));
+ exit(1);
+ }
+ fwrite(rw.data, 1, count, f);
+ fclose(f);
+ } else {
+ hexdump(rw.data, count);
+ }
+}
+
+static void cccr(int fd)
+{
+ static const struct {
+ unsigned char addr;
+ unsigned char len;
+ const char *name;
+ } fields[] = {
+ { 0x00, 1, "CCCR/SDIO revision" },
+ { 0x01, 1, "SD spec revision" },
+ { 0x02, 1, "I/O enable" },
+ { 0x03, 1, "I/O ready" },
+ { 0x04, 1, "Int enable" },
+ { 0x05, 1, "Int pending" },
+ { 0x06, 1, "I/O abort" },
+ { 0x07, 1, "Bus interface control" },
+ { 0x08, 1, "Card capability" },
+ { 0x09, 3, "Common CIS pointer" },
+ { 0x0c, 1, "Bus suspend" },
+ { 0x0d, 1, "Function select" },
+ { 0x0e, 1, "Exec flags" },
+ { 0x0f, 1, "Ready flags" },
+ { 0x10, 2, "FN0 block size" },
+ { 0x12, 1, "Power control" },
+ { 0x13, 1, "High-speed" },
+ };
+ unsigned char regs[0x18];
+ unsigned int i, j;
+ unsigned long v;
+
+ for (i = 0; i < sizeof(regs); i++)
+ if (rd52(fd, 0, i, ®s[i]))
+ die_errno("CMD52 (CCCR dump) failed");
+
+ printf("CCCR (fn 0):\n");
+ for (i = 0; i < ARRAY_SIZE(fields); i++) {
+ v = 0;
+ for (j = 0; j < fields[i].len; j++)
+ v |= (unsigned long)regs[fields[i].addr + j] << (8 * j);
+
+ if (fields[i].len == 1)
+ printf(" 0x%02x %-24s 0x%02lx\n", fields[i].addr,
+ fields[i].name, v);
+ else if (fields[i].len == 2)
+ printf(" 0x%02x-0x%02x %-21s 0x%04lx\n",
+ fields[i].addr, fields[i].addr + 1,
+ fields[i].name, v);
+ else
+ printf(" 0x%02x-0x%02x %-21s 0x%06lx\n",
+ fields[i].addr,
+ fields[i].addr + fields[i].len - 1,
+ fields[i].name, v);
+ }
+ printf(" raw 0x00-0x17:\n");
+ hexdump(regs, sizeof(regs));
+}
+
+static void fbr(int fd, unsigned int fn)
+{
+ unsigned char regs[0x20];
+ unsigned long cis, csa;
+ unsigned int i;
+
+ if (fn == 0) {
+ fprintf(stderr, "%s: %s needs a function 1..7, use -f\n",
+ progname, __func__);
+ exit(1);
+ }
+
+ /*
+ * The FBR of function fn lives at fn * 0x100 in the common
+ * register area and is always accessed through function 0, like
+ * the kernel does (SDIO_FBR_BASE(), drivers/mmc/core/sdio_cis.c).
+ */
+ for (i = 0; i < sizeof(regs); i++)
+ if (rd52(fd, 0, fn * 0x100 + i, ®s[i]))
+ die_errno("CMD52 (FBR dump) failed");
+
+ cis = regs[0x09] | regs[0x0a] << 8 | regs[0x0b] << 16;
+ csa = regs[0x0c] | regs[0x0d] << 8 | regs[0x0e] << 16;
+
+ printf("FBR (fn %u) @ 0x%03x:\n", fn, fn * 0x100);
+ printf(" 0x00 Standard interface code 0x%02x\n", regs[0x00]);
+ printf(" 0x01 Ext std interface code 0x%02x\n", regs[0x01]);
+ printf(" 0x02 Power selection 0x%02x\n", regs[0x02]);
+ printf(" 0x09-0x0b CIS pointer 0x%06lx\n", cis);
+ printf(" 0x0c-0x0e CSA pointer 0x%06lx\n", csa);
+ printf(" 0x10-0x11 Function block size 0x%04x\n",
+ regs[0x10] | regs[0x11] << 8);
+ printf(" raw 0x00-0x1f:\n");
+ hexdump(regs, sizeof(regs));
+}
+
+/*
+ * CIS walk, mirroring drivers/mmc/core/sdio_cis.c: the CIS pointer
+ * sits at offset 0x09 of the CCCR (fn 0) or of the FBR of the selected
+ * function, and both the pointer and the tuple chain are read through
+ * function 0. Function 0 dumps the common CIS, functions 1..7 their
+ * per-function CIS.
+ */
+#define CISTPL_END 0xff
+#define CISTPL_VERS_1 0x15
+#define CISTPL_MANFID 0x20
+#define CISTPL_FUNCID 0x21
+
+static const char *tpl_name(unsigned char code)
+{
+ switch (code) {
+ case CISTPL_VERS_1: return "VERS_1";
+ case CISTPL_MANFID: return "MANFID";
+ case CISTPL_FUNCID: return "FUNCID";
+ default: return "";
+ }
+}
+
+static void cis_dump(int fd, unsigned int fn)
+{
+ unsigned char b[3], body[256];
+ unsigned long addr;
+ unsigned int n;
+
+ if (rd52(fd, 0, fn * 0x100 + 0x09, &b[0]) ||
+ rd52(fd, 0, fn * 0x100 + 0x0a, &b[1]) ||
+ rd52(fd, 0, fn * 0x100 + 0x0b, &b[2]))
+ die_errno("CMD52 (CIS pointer) failed");
+
+ addr = b[0] | b[1] << 8 | b[2] << 16;
+ if (!addr) {
+ printf("CIS (fn %u): not present (CIS pointer is 0)\n", fn);
+ return;
+ }
+ addr &= 0x1ffff;
+
+ printf("CIS (fn %u) @ 0x%05lx:\n", fn, addr);
+
+ for (n = 0; n < 256; n++) {
+ unsigned char code, link;
+ unsigned int i;
+
+ if (rd52(fd, 0, addr, &code))
+ die_errno("CMD52 (CIS walk) failed");
+ if (code == CISTPL_END) {
+ printf(" 0x%05lx TPL 0xff END\n", addr);
+ break;
+ }
+ if (code == 0x00) { /* null tuple, has no link field */
+ printf(" 0x%05lx TPL 0x00 NULL\n", addr);
+ addr++;
+ continue;
+ }
+ if (rd52(fd, 0, addr + 1, &link))
+ die_errno("CMD52 (CIS walk) failed");
+ if (link == 0xff) {
+ printf(" 0x%05lx TPL 0x%02x link=0xff (undefined), stop\n",
+ addr, code);
+ break;
+ }
+
+ for (i = 0; i < link; i++)
+ if (rd52(fd, 0, addr + 2 + i, &body[i]))
+ die_errno("CMD52 (CIS walk) failed");
+
+ printf(" 0x%05lx TPL 0x%02x %-6s len=%u\n", addr, code,
+ tpl_name(code), link);
+
+ switch (code) {
+ case CISTPL_MANFID:
+ if (link >= 4) {
+ printf(" manufacturer 0x%04x, product 0x%04x\n",
+ body[0] | body[1] << 8,
+ body[2] | body[3] << 8);
+ break;
+ }
+ hexdump(body, link);
+ break;
+ case CISTPL_VERS_1: {
+ unsigned int j = 2;
+
+ printf(" rev %u.%u:", body[0], body[1]);
+ while (j < link && body[j] != 0xff) {
+ unsigned int start = j;
+
+ while (j < link && body[j])
+ j++;
+ printf(" \"%.*s\"", (int)(j - start),
+ (const char *)&body[start]);
+ j++; /* skip the NUL */
+ }
+ putchar('\n');
+ break;
+ }
+ case CISTPL_FUNCID:
+ if (link >= 1) {
+ printf(" function code 0x%02x\n", body[0]);
+ break;
+ }
+ hexdump(body, link);
+ break;
+ default:
+ hexdump(body, link);
+ break;
+ }
+
+ addr += 2 + link;
+ }
+}
+
+#ifndef SDIO_SYSFS_DEVICES
+#define SDIO_SYSFS_DEVICES "/sys/bus/sdio/devices"
+#endif
+
+static int read_attr(const char *dir, const char *attr, char *buf, size_t len)
+{
+ char path[PATH_MAX];
+ FILE *f;
+ size_t n;
+
+ snprintf(path, sizeof(path), "%s/%s", dir, attr);
+ f = fopen(path, "r");
+ if (!f)
+ return -1;
+ if (!fgets(buf, len, f)) {
+ fclose(f);
+ return -1;
+ }
+ fclose(f);
+ n = strlen(buf);
+ while (n && (buf[n - 1] == '\n' || buf[n - 1] == '\r'))
+ buf[--n] = '\0';
+ return 0;
+}
+
+static const char *lookup_name(const struct sdio_id_name *tbl,
+ unsigned int count, unsigned int id)
+{
+ unsigned int i;
+
+ for (i = 0; i < count; i++)
+ if (tbl[i].id == id)
+ return tbl[i].name;
+ return NULL;
+}
+
+/*
+ * Join the CIS strings the kernel parsed at enumeration (sysfs
+ * info1..info4, from CISTPL_VERS_1) into one identification string.
+ */
+static void read_info(const char *dir, char *buf, size_t len)
+{
+ static const char * const attrs[] = { "info1", "info2", "info3", "info4" };
+ char tmp[64];
+ size_t used = 0;
+ unsigned int i;
+
+ buf[0] = '\0';
+ for (i = 0; i < ARRAY_SIZE(attrs); i++) {
+ size_t slen;
+
+ if (read_attr(dir, attrs[i], tmp, sizeof(tmp)) || !tmp[0])
+ continue;
+
+ slen = strlen(tmp);
+ if (used + slen + 1 >= len)
+ break;
+ if (used)
+ buf[used++] = ' ';
+ memcpy(buf + used, tmp, slen + 1);
+ used += slen;
+ }
+}
+
+static void list_devices(void)
+{
+ struct dirent **entries;
+ int i, n, total = 0;
+
+ n = scandir(SDIO_SYSFS_DEVICES, &entries, NULL, alphasort);
+ if (n < 0) {
+ printf("no SDIO bus (%s: %s)\n", SDIO_SYSFS_DEVICES,
+ strerror(errno));
+ return;
+ }
+
+ printf("%-20s %-14s %-24s %-14s %-5s %s\n",
+ "NODE", "VID:PID", "NAME", "CLASS", "REV", "INFO");
+
+ for (i = 0; i < n; i++) {
+ const char *devname = entries[i]->d_name;
+ const char *dname, *cname;
+ char path[PATH_MAX];
+ char vend[16], devid[16], clsv[16], rev[32], info[160];
+ char namebuf[64], clsbuf[40], vidpid[16];
+ unsigned int vid, pid, c;
+
+ if (devname[0] == '.')
+ continue;
+
+ snprintf(path, sizeof(path), "%s/%s", SDIO_SYSFS_DEVICES,
+ devname);
+
+ if (read_attr(path, "vendor", vend, sizeof(vend)) ||
+ read_attr(path, "device", devid, sizeof(devid)) ||
+ read_attr(path, "class", clsv, sizeof(clsv)) ||
+ read_attr(path, "revision", rev, sizeof(rev)))
+ continue;
+
+ read_info(path, info, sizeof(info));
+
+ vid = strtoul(vend, NULL, 0);
+ pid = strtoul(devid, NULL, 0);
+ c = strtoul(clsv, NULL, 0);
+
+ snprintf(vidpid, sizeof(vidpid), "0x%04x:0x%04x", vid, pid);
+
+ dname = lookup_name(sdio_device_names,
+ ARRAY_SIZE(sdio_device_names), pid);
+ if (!dname) {
+ snprintf(namebuf, sizeof(namebuf), "Unlisted(0x%04x)",
+ pid);
+ dname = namebuf;
+ }
+
+ cname = lookup_name(sdio_class_names,
+ ARRAY_SIZE(sdio_class_names), c);
+ if (cname)
+ snprintf(clsbuf, sizeof(clsbuf), "0x%02x (%s)", c,
+ cname);
+ else
+ snprintf(clsbuf, sizeof(clsbuf), "0x%02x", c);
+
+ printf("/dev/%-15s %-14s %-24s %-14s %-5s %s\n",
+ devname, vidpid, dname, clsbuf, rev,
+ info[0] ? info : "-");
+ total++;
+ }
+
+ if (!total)
+ printf("no SDIO device found\n");
+
+ for (i = 0; i < n; i++)
+ free(entries[i]);
+ free(entries);
+}
+
+static unsigned int get_fn(const char *dev, int fn_override)
+{
+ unsigned int fn = fn_override >= 0 ? (unsigned int)fn_override :
+ fn_from_dev(dev);
+
+ if (fn > 7) {
+ fprintf(stderr, "%s: invalid function number %u\n", progname,
+ fn);
+ exit(1);
+ }
+ return fn;
+}
+
+static unsigned long get_addr(const char *s)
+{
+ unsigned long addr = parse_num(s, "address");
+
+ if (addr > 0x1ffff) {
+ fprintf(stderr, "%s: addr 0x%lx out of range\n", progname,
+ addr);
+ exit(1);
+ }
+ return addr;
+}
+
+int main(int argc, char **argv)
+{
+ const char *dev = NULL, *cmd;
+ const char *infile = NULL, *outfile = NULL;
+ unsigned long addr = 0, count = 0, val = 0;
+ int fn_override = -1, write = 0, raw = 0, incr_addr = 1;
+ unsigned int fn;
+ int i;
+
+ for (i = 1; i < argc; i++) {
+ const char *a = argv[i];
+
+ if (!strcmp(a, "-d") || !strcmp(a, "--device")) {
+ if (++i >= argc)
+ usage(1);
+ dev = argv[i];
+ } else if (!strcmp(a, "-f") || !strcmp(a, "--func")) {
+ if (++i >= argc)
+ usage(1);
+ fn_override = parse_num(argv[i], "function number");
+ } else if (!strcmp(a, "-h") || !strcmp(a, "--help")) {
+ usage(0);
+ } else {
+ break;
+ }
+ }
+
+ if (i >= argc)
+ usage(1);
+
+ cmd = argv[i++];
+
+ if (!strcmp(cmd, "cmd52")) {
+ for (; i < argc; i++) {
+ if (!strcmp(argv[i], "-a") && i + 1 < argc)
+ addr = get_addr(argv[++i]);
+ else if (!strcmp(argv[i], "-w") && i + 1 < argc)
+ write = 1, val = parse_num(argv[++i], "byte value");
+ else if (!strcmp(argv[i], "-r"))
+ raw = 1;
+ else if (!strcmp(argv[i], "-f") && i + 1 < argc)
+ fn_override = parse_num(argv[++i], "function number");
+ else
+ usage(1);
+ }
+ fn = get_fn(dev, fn_override);
+ cmd52(open_dev(dev), fn, addr, write, val, raw);
+ } else if (!strcmp(cmd, "cmd53")) {
+ for (; i < argc; i++) {
+ if (!strcmp(argv[i], "-a") && i + 1 < argc)
+ addr = get_addr(argv[++i]);
+ else if (!strcmp(argv[i], "-n") && i + 1 < argc)
+ count = parse_num(argv[++i], "count");
+ else if (!strcmp(argv[i], "-w") && i + 1 < argc)
+ write = 1, infile = argv[++i];
+ else if (!strcmp(argv[i], "-o") && i + 1 < argc)
+ outfile = argv[++i];
+ else if (!strcmp(argv[i], "--fixed"))
+ incr_addr = 0;
+ else if (!strcmp(argv[i], "-f") && i + 1 < argc)
+ fn_override = parse_num(argv[++i], "function number");
+ else
+ usage(1);
+ }
+ if (!addr && !count) {
+ fprintf(stderr, "%s: cmd53 needs -a ADDR and -n COUNT\n",
+ progname);
+ usage(1);
+ }
+ if (!count || count > 512) {
+ fprintf(stderr, "%s: count must be 1..512\n", progname);
+ exit(1);
+ }
+ fn = get_fn(dev, fn_override);
+ if (fn == 0) {
+ fprintf(stderr, "%s: cmd53 is not valid for fn 0\n",
+ progname);
+ exit(1);
+ }
+ cmd53(open_dev(dev), fn, addr, count, write, incr_addr,
+ infile, outfile);
+ } else if (!strcmp(cmd, "list")) {
+ list_devices();
+ } else if (!strcmp(cmd, "cccr")) {
+ cccr(open_dev(dev));
+ } else if (!strcmp(cmd, "cis")) {
+ for (; i < argc; i++) {
+ if (!strcmp(argv[i], "-f") && i + 1 < argc)
+ fn_override = parse_num(argv[++i], "function number");
+ else
+ usage(1);
+ }
+ fn = get_fn(dev, fn_override);
+ cis_dump(open_dev(dev), fn);
+ } else if (!strcmp(cmd, "fbr")) {
+ for (; i < argc; i++) {
+ if (!strcmp(argv[i], "-f") && i + 1 < argc)
+ fn_override = parse_num(argv[++i], "function number");
+ else
+ usage(1);
+ }
+ fn = get_fn(dev, fn_override);
+ fbr(open_dev(dev), fn);
+ } else {
+ fprintf(stderr, "%s: unknown command: %s\n", progname, cmd);
+ usage(1);
+ }
+
+ return 0;
+}
--
2.7.4
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2 3/3] docs: mmc: add introduction to writing SDIO function drivers
2026-09-21 7:18 [PATCH v2 0/3] Add SDIO userspace passthrough support Shawn Lin
2026-09-21 7:18 ` [PATCH v2 1/3] mmc: core: Add SDIO userspace passthrough character device Shawn Lin
2026-09-21 7:18 ` [PATCH v2 2/3] tools/sdio: add sdio-raw userspace client Shawn Lin
@ 2026-09-21 7:18 ` Shawn Lin
2026-10-29 0:27 ` [PATCH v2 0/3] Add SDIO userspace passthrough support Shawn Lin
3 siblings, 0 replies; 5+ messages in thread
From: Shawn Lin @ 2026-09-21 7:18 UTC (permalink / raw)
To: Ulf Hansson; +Cc: linux-mmc, linux-kernel, Shawn Lin
From: Shawn Lin <shawn.lin@linux.dev>
Add Documentation/driver-api/mmc/sdio-function-drivers.rst to describe
the bus model, host claiming, the probe/remove sequence, register and
data access with their buffer requirements, the interrupt model and
power management, along with references to existing drivers for further
reading.
Signed-off-by: Shawn Lin <shawn.lin@linux.dev>
---
Documentation/driver-api/mmc/index.rst | 1 +
.../driver-api/mmc/sdio-function-drivers.rst | 211 +++++++++++++++++++++
MAINTAINERS | 1 +
3 files changed, 213 insertions(+)
create mode 100644 Documentation/driver-api/mmc/sdio-function-drivers.rst
diff --git a/Documentation/driver-api/mmc/index.rst b/Documentation/driver-api/mmc/index.rst
index 8188863..d7ef417 100644
--- a/Documentation/driver-api/mmc/index.rst
+++ b/Documentation/driver-api/mmc/index.rst
@@ -12,3 +12,4 @@ MMC/SD/SDIO card support
mmc-async-req
mmc-test
mmc-tools
+ sdio-function-drivers
diff --git a/Documentation/driver-api/mmc/sdio-function-drivers.rst b/Documentation/driver-api/mmc/sdio-function-drivers.rst
new file mode 100644
index 0000000..39dd38e
--- /dev/null
+++ b/Documentation/driver-api/mmc/sdio-function-drivers.rst
@@ -0,0 +1,211 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+=================================
+SDIO function driver introduction
+=================================
+
+This document describes how to write a driver for an SDIO function
+(sometimes referred to as an SDIO client driver). It covers the driver
+skeleton, the bus access rules and the interrupt model that the MMC core
+expects drivers to follow.
+
+Bus model
+=========
+
+An SDIO card carries one or more functions. Function 0 is the card
+itself and is managed by the MMC core; the remaining functions (1-7) are
+the actual devices. For every present function the core registers a
+``struct sdio_func`` device on the SDIO bus, which is described by
+``include/linux/mmc/sdio_func.h``.
+
+Function drivers bind to these devices with ``sdio_register_driver()``
+using a ``struct sdio_driver``. Matching is done through the vendor and
+device id, or through the standard interface class::
+
+ static const struct sdio_device_id my_ids[] = {
+ { SDIO_DEVICE(0x1234, 0x5678) },
+ { SDIO_DEVICE_CLASS(SDIO_CLASS_WLAN) },
+ { }
+ };
+ MODULE_DEVICE_TABLE(sdio, my_ids);
+
+Use the ``SDIO_DEVICE()`` macro for specific devices and
+``SDIO_DEVICE_CLASS()`` for standard function classes. Device and
+vendor ids are defined in ``include/linux/mmc/sdio_ids.h`` and new ids
+should be added there. The ``class``, ``vendor``, ``device``,
+``max_blksize`` and ``cur_blksize`` fields of ``struct sdio_func``
+describe the bound function.
+
+Bus claiming
+============
+
+All register access on an SDIO function requires exclusive access to
+the bus. This is done with ``sdio_claim_host()`` and
+``sdio_release_host()``, which wrap ``mmc_claim_host()`` and may sleep.
+Every call to ``sdio_readb()``, ``sdio_writesb()``, ``sdio_enable_func()``,
+``sdio_claim_irq()`` and friends **must** happen while the host is
+claimed. Failure to do so triggers a ``WARN_ON(!host->claimed)`` from
+the core at best, and corrupted transfers at worst.
+
+.. note::
+
+ Never hold the claim for long periods of time, since this prevents
+ the core from servicing other functions on the same bus.
+
+Probing and removing
+====================
+
+A typical ``->probe()`` claims the bus, enables the function, sets the
+block size and registers the interrupt handler. ``->remove()`` must undo
+this in reverse order, and this includes releasing the interrupt (see
+`Interrupts`_). The following example shows the expected sequence::
+
+ static int my_probe(struct sdio_func *func,
+ const struct sdio_device_id *id)
+ {
+ struct my_dev *dev;
+ int ret;
+
+ dev = devm_kzalloc(&func->dev, sizeof(*dev), GFP_KERNEL);
+ if (!dev)
+ return -ENOMEM;
+
+ dev->func = func;
+
+ sdio_claim_host(func);
+
+ ret = sdio_enable_func(func);
+ if (ret)
+ goto err_release;
+
+ ret = sdio_set_block_size(func, 512);
+ if (ret)
+ goto err_disable;
+
+ ret = sdio_claim_irq(func, my_irq_handler);
+ if (ret)
+ goto err_disable;
+
+ sdio_release_host(func);
+
+ sdio_set_drvdata(func, dev);
+ return 0;
+
+ err_disable:
+ sdio_disable_func(func);
+ err_release:
+ sdio_release_host(func);
+ return ret;
+ }
+
+ static void my_remove(struct sdio_func *func)
+ {
+ struct my_dev *dev = sdio_get_drvdata(func);
+
+ sdio_claim_host(func);
+ sdio_release_irq(func);
+ sdio_disable_func(func);
+ sdio_release_host(func);
+ }
+
+``sdio_enable_func()`` polls the function ready status for up to
+``func->enable_timeout`` milliseconds, so it must not be called from an
+atomic context. Likewise, ``sdio_claim_host()`` can block, so the IRQ
+handler must not be used to claim the bus on its behalf (see below).
+
+Register and data access
+========================
+
+Two classes of access exist, and the buffer requirements differ.
+
+Single byte access (CMD52)
+--------------------------
+
+``sdio_readb()``, ``sdio_writeb()`` and ``sdio_writeb_readb()``
+transfer the data inside the command itself. No data phase is involved,
+so these calls are safe with stack allocated buffers.
+
+The ``sdio_f0_readb()`` and ``sdio_f0_writeb()`` variants address the
+common area (CCCR and FBR registers) instead of the function's own
+address space. Note that ``sdio_f0_writeb()`` only accepts addresses
+0xF0-0xFF unless the card advertises the lenient function 0 quirk.
+
+Word access is provided by ``sdio_readw()``, ``sdio_readl()``,
+``sdio_writew()`` and ``sdio_writel()``. These are implemented as small
+CMD53 transfers on a DMA-able scratch buffer owned by the core, so
+drivers can use them without allocating anything.
+
+Bulk access (CMD53)
+-------------------
+
+``sdio_memcpy_fromio()``, ``sdio_memcpy_toio()``, ``sdio_readsb()``,
+``sdio_writesb()`` transfer a data phase. The MMC core builds a
+scatterlist directly from the buffer supplied by the driver and the host
+controller driver maps it for DMA. The buffer must therefore be:
+
+- A single, contiguous, physically mappable allocation. Use
+ ``kmalloc()`` (or a similar DMA-able allocator, such as ``skb``
+ buffers).
+- Not memory obtained from ``vmalloc()`` and not memory on the stack.
+ Those buffers are not part of the kernel linear map, so
+ ``virt_to_page()`` on them yields invalid pages. With
+ ``CONFIG_DEBUG_SG`` enabled the kernel will BUG(), without it, DMA
+ silently corrupts memory.
+
+Buffer sizes should be aligned with ``sdio_align_size()`` for optimal
+performance. The core splits larger transfers internally, but only
+within the one linear buffer supplied by the driver.
+
+Interrupts
+==========
+
+An SDIO function signals interrupts with a dedicated DAT1 line. Function
+drivers register a handler with ``sdio_claim_irq()`` and release it with
+``sdio_release_irq()``. Both calls must be made while the bus is
+claimed.
+
+The handler is always called by the core with the bus already claimed,
+and it must not sleep. Consequently it must not call
+``sdio_claim_host()``, ``sdio_release_host()`` or any function that
+sleeps. If more work is needed, hand it over to a workqueue and signal
+it from the handler.
+
+Internally the core processes interrupts either from a kernel thread
+(``ksdioirqd``) or, if the host sets ``MMC_CAP2_SDIO_IRQ_NOTHREAD``,
+from a work item. Host drivers that handle their own interrupt line may
+call ``sdio_signal_irq()`` when they detect that the SDIO function
+asserted an interrupt. The core
+checks for enabled interrupts when the work runs, so work that races
+with ``->remove()`` simply returns without calling back into the
+driver. This is why ``sdio_release_irq()`` in ``->remove()`` is
+mandatory: the core removes the function devices from
+``mmc_remove_host()``, and the SDIO function driver ``->remove()``
+callback is the last chance to release the interrupts. The commit
+6feadbecdae6 ("mmc: core: Cancel SDIO IRQ work before freeing host") is
+an example of what goes wrong when this contract is broken.
+
+Power management
+================
+
+The core suspends and resumes the SDIO card as a whole. Function
+drivers may implement ``struct dev_pm_ops`` and assign it to the
+``drv.pm`` member of ``struct sdio_driver``. The bus uses the generic
+PM callbacks, so a driver only implements what it needs.
+
+To support wakeup from a suspended host controller, check
+``sdio_get_host_pm_caps()`` and call ``sdio_set_host_pm_flags()`` with
+``MMC_PM_KEEP_POWER`` or ``MMC_PM_WAKE_SDIO_IRQ`` as appropriate.
+
+Reference drivers
+=================
+
+These existing drivers are good reading material:
+
+ - ``drivers/bluetooth/btsdio.c`` - small, self-contained driver
+ showing probe/remove and CMD53 transfers.
+ - ``drivers/net/wireless/ti/wl1251/sdio.c`` - shows power management
+ and retuning helpers.
+ - ``drivers/net/wireless/marvell/libertas/if_sdio.c`` - interrupt
+ handling.
+ - ``drivers/net/wireless/broadcom/brcm80211/brcmfmac/bcmsdh.c`` - a
+ full featured driver with block mode transfers.
\ No newline at end of file
diff --git a/MAINTAINERS b/MAINTAINERS
index a476c06..b7e4ee9 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18842,6 +18842,7 @@ L: linux-mmc@vger.kernel.org
S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/ulfh/mmc.git
F: Documentation/devicetree/bindings/mmc/
+F: Documentation/driver-api/mmc/
F: drivers/mmc/
F: include/linux/mmc/
F: include/uapi/linux/mmc/
--
2.7.4
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2 0/3] Add SDIO userspace passthrough support
2026-09-21 7:18 [PATCH v2 0/3] Add SDIO userspace passthrough support Shawn Lin
` (2 preceding siblings ...)
2026-09-21 7:18 ` [PATCH v2 3/3] docs: mmc: add introduction to writing SDIO function drivers Shawn Lin
@ 2026-10-29 0:27 ` Shawn Lin
3 siblings, 0 replies; 5+ messages in thread
From: Shawn Lin @ 2026-10-29 0:27 UTC (permalink / raw)
To: Ulf Hansson; +Cc: shawn.lin, linux-mmc, linux-kernel, Shawn Lin
Hi Ulf
在 2026/09/21 星期一 15:18, Shawn Lin 写道:
> SDIO cards are normally driven by function drivers (brcmfmac,
> mwifiex, rtw88_8723bs, ...), but bring-up, debugging and factory
> test frequently need to poke the SDIO register space directly:
> CCCR/FBR in function 0, or vendor registers before any function
> driver is bound, and some deployments, notably lightweight IoT
> boards, want to handle the SDIO function entirely from userspace.
> Today the only way is to write a tiny kernel driver per device.
>
Gentle ping...
> So this series adds raw userspace access to the SDIO register
> space as well a small reference client. Meanwhile, adding a new
> documentation about how to write a proper SDIO function driver,
> because I have recently been working on SDIO function drivers
> and kept referring to the existing drivers for guidance. It turns
> out that the knowledge is scattered and there is no unified document
> describing how an SDIO function driver is supposed to be written,
> despite the MMC core having clear expectations about how the APIs
> are used.
>
> In particular, the interrupt model has many pitfalls:
> sdio_claim_irq()/sdio_release_irq() must be called with the host
> claimed, the handler runs with the host already claimed and must
> not sleep or claim the bus itself, and sdio_release_irq() must be
> called from ->remove() because it is the last chance before the
> core removes the function devices from mmc_remove_host(). Recent
> fixes around the SDIO IRQ work and host removal and the discussions
> on the mailing list showed how easily these contracts get broken.
>
> Also worth documenting is the buffer requirement of the CMD53 based
> helpers: the core builds a scatterlist directly from the caller's
> buffer and the host driver maps it for DMA, so vmalloc() or stack
> allocated buffers are not acceptable, while the CMD52 byte accessors
> are safe with stack buffers.
>
> Changes-in-v2:
> - drop RFC as I don't include mmc-utils to be sdio usersapce tool
> - add sdio doc
>
> Shawn Lin (3):
> mmc: core: Add SDIO userspace passthrough character device
> tools/sdio: add sdio-raw userspace client
> docs: mmc: add introduction to writing SDIO function drivers
>
> Documentation/driver-api/mmc/index.rst | 1 +
> .../driver-api/mmc/sdio-function-drivers.rst | 211 ++++++
> MAINTAINERS | 2 +
> drivers/mmc/Kconfig | 11 +
> drivers/mmc/core/Makefile | 1 +
> drivers/mmc/core/sdio_bus.c | 12 +-
> drivers/mmc/core/sdio_cdev.c | 189 ++++++
> drivers/mmc/core/sdio_cdev.h | 21 +
> include/linux/mmc/sdio_func.h | 4 +
> include/uapi/linux/mmc/sdio_ioctl.h | 48 ++
> tools/Makefile | 14 +-
> tools/sdio/Build | 1 +
> tools/sdio/Makefile | 66 ++
> tools/sdio/gen-sdio-ids.sh | 59 ++
> tools/sdio/sdio-raw.c | 714 +++++++++++++++++++++
> 15 files changed, 1346 insertions(+), 8 deletions(-)
> create mode 100644 Documentation/driver-api/mmc/sdio-function-drivers.rst
> create mode 100644 drivers/mmc/core/sdio_cdev.c
> create mode 100644 drivers/mmc/core/sdio_cdev.h
> create mode 100644 include/uapi/linux/mmc/sdio_ioctl.h
> create mode 100644 tools/sdio/Build
> create mode 100644 tools/sdio/Makefile
> create mode 100644 tools/sdio/gen-sdio-ids.sh
> create mode 100644 tools/sdio/sdio-raw.c
>
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-29 0:27 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-21 7:18 [PATCH v2 0/3] Add SDIO userspace passthrough support Shawn Lin
2026-09-21 7:18 ` [PATCH v2 1/3] mmc: core: Add SDIO userspace passthrough character device Shawn Lin
2026-09-21 7:18 ` [PATCH v2 2/3] tools/sdio: add sdio-raw userspace client Shawn Lin
2026-09-21 7:18 ` [PATCH v2 3/3] docs: mmc: add introduction to writing SDIO function drivers Shawn Lin
2026-10-29 0:27 ` [PATCH v2 0/3] Add SDIO userspace passthrough support Shawn Lin
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®