From nobody Thu Sep 24 20:36:59 2026 Received: from mail-m1973178.qiye.163.com (mail-m1973178.qiye.163.com [220.197.31.78]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id D00803BBA0E; Mon, 21 Sep 2026 07:54:11 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=220.197.31.78 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789977256; cv=none; b=C0p7JWFSLa1BSp9ITMhlHzI9KNHvcPauBNOkKozsUuCdH42UYZLjhgs17RebRkJH8Ygm0zoM9Sa7UddPRXP1nY0+j59o5bPv26uYtL/j/T6j01suvDRpKIRZKpVxVDm0lSaFTBVeodI8PlrdR3JJyTsvTA4WvVdIuGAuhad2Xuk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789977256; c=relaxed/simple; bh=um64cN9AuPQatfJGrRtfe+eWMczsnRIKXJR2SrRDIew=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References; b=hUhIOpym1FXRjD8ywyWTCfKwqNpHblmm1ZtwG2TIvitwqnSUuRrmwJuwd/y+iU5P2GOW0MIuxla2QrfMFYCE63c9mePDUq5JYq8bq0rmnRcHSZ9SObrda3/U2qb3/1cg2iEK7KycBfzHlP9191JmFppazFAL9l5uINT5K5xjXUM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=rock-chips.com; spf=pass smtp.mailfrom=rock-chips.com; dkim=pass (1024-bit key) header.d=rock-chips.com header.i=@rock-chips.com header.b=UIB2VaTJ; arc=none smtp.client-ip=220.197.31.78 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=rock-chips.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=rock-chips.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=rock-chips.com header.i=@rock-chips.com header.b="UIB2VaTJ" Received: from localhost.localdomain (unknown [58.22.7.114]) by smtp.qiye.163.com (Hmail) with ESMTP id 4e89bec6b; Mon, 21 Sep 2026 15:18:32 +0800 (GMT+08:00) From: Shawn Lin To: Ulf Hansson Cc: linux-mmc@vger.kernel.org, linux-kernel@vger.kernel.org, Shawn Lin Subject: [PATCH v2 1/3] mmc: core: Add SDIO userspace passthrough character device Date: Mon, 21 Sep 2026 15:18:17 +0800 Message-Id: <1789975099-157594-2-git-send-email-shawn.lin@rock-chips.com> X-Mailer: git-send-email 2.7.4 In-Reply-To: <1789975099-157594-1-git-send-email-shawn.lin@rock-chips.com> References: <1789975099-157594-1-git-send-email-shawn.lin@rock-chips.com> X-HM-Tid: 0aa0c2d4acaa03a4kunm73dcdfcc310941 X-HM-MType: 1 X-HM-Spam-Status: e1kfGhgUHx5ZQUpXWQgPGg8OCBgUHx5ZQUlOS1dZFg8aDwILHllBWSg2Ly tZV1koWUFDSUNOT01LS0k3V1kYFggdWUFKV1ktWUFJV1kPCRoVCBIfWUFZQ0hNSlYaHU9DSx1CQh 4eGEtWFRQJFhoXVRMBExYaEhckFA4PWVdZGBILWUFZTkNVSUlVTFVKSk9ZV1kWGg8SFR0UWUFZT0 tIVUpLSU9PT0hVSktLVUpCS0tZBg++ DKIM-Signature: a=rsa-sha256; b=UIB2VaTJ5SnKJZgw+jY/L703eIWR2PNRf6h61rcmcZFjvczpsdXeL2s6BabX/9hCnOWIopwZBSAWpNiC20V2A3yYpcggWE7I/F460bTJ22QG9p8ioNekz0DozvW7kjHM4Sa/jeI+slpDukL6W47ZhfbeJ72LpCdL9A0GFsXk1sU=; s=default; c=relaxed/relaxed; d=rock-chips.com; v=1; bh=KhnAGT/Dfq8pZkZgWXeHzWdSd+3LTAybCNYn2XO/n8w=; h=date:mime-version:subject:message-id:from; Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: Content-Transfer-Encoding: quoted-printable MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" From: Shawn Lin 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 --- 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. =20 +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 =20 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 @@ # =20 obj-$(CONFIG_MMC) +=3D mmc_core.o +obj-$(CONFIG_MMC_SDIO_CDEV) +=3D sdio_cdev.o mmc_core-y :=3D 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 @@ =20 #include "core.h" #include "card.h" +#include "sdio_cdev.h" #include "sdio_cis.h" #include "sdio_bus.h" =20 @@ -400,9 +401,12 @@ int sdio_add_func(struct sdio_func *func) sdio_acpi_set_handle(func); device_enable_async_suspend(&func->dev); ret =3D device_add(&func->dev); - if (ret =3D=3D 0) + if (ret =3D=3D 0) { sdio_func_set_present(func); =20 + sdio_cdev_create(func); + } + return ret; } =20 @@ -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); + } + =20 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 + * + * Every SDIO function gets a /dev/ 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 +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#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 =3D 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 =3D mmc_io_rw_direct(func->card, 1, rw.fn, rw.addr, + rw.data, NULL); + } else { + err =3D 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 =3D data; + ret =3D 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 =3D=3D 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 =3D kmalloc(rw.count, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + if (rw.write) + memcpy(buf, rw.data, rw.count); + + sdio_claim_host(func); + err =3D 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 =3D file->private_data; + struct sdio_func *func =3D 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 =3D { + .owner =3D THIS_MODULE, + .unlocked_ioctl =3D sdio_cdev_ioctl, +}; + +int sdio_cdev_create(struct sdio_func *func) +{ + struct miscdevice *misc; + int ret; + + misc =3D kzalloc_obj(*misc); + if (!misc) + return -ENOMEM; + + misc->minor =3D MISC_DYNAMIC_MINOR; + misc->name =3D dev_name(&func->dev); + misc->fops =3D &sdio_cdev_fops; + misc->parent =3D &func->dev; + func->miscdev =3D misc; + + ret =3D 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 =3D NULL; + } + + return ret; +} + +void sdio_cdev_destroy(struct sdio_func *func) +{ + if (!func->miscdev) + return; + + misc_deregister(func->miscdev); + kfree(func->miscdev); + func->miscdev =3D 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 + +#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 */ =20 struct sdio_func_tuple *tuples; + +#ifdef CONFIG_MMC_SDIO_CDEV + struct miscdevice *miscdev; /* userspace passthrough node */ +#endif }; =20 #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/s= dio_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 +#include + +/* + * 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 =3D write, 0 =3D 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 =3D write, 0 =3D read */ + __u8 fn; /* function number, 1..7 */ + __u8 incr_addr; /* 1 =3D incrementing address, 0 =3D 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_ex= t) + +#endif /* _UAPI_LINUX_MMC_SDIO_IOCTL_H */ --=20 2.7.4 From nobody Thu Sep 24 20:36:59 2026 Received: from mail-m1973175.qiye.163.com (mail-m1973175.qiye.163.com [220.197.31.75]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C738343B3DA; Mon, 21 Sep 2026 07:54:25 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=220.197.31.75 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789977270; cv=none; b=Vz4eOpR5r0DFHaHPu4M/+R4wjuUf5GWrIROwS5UTrmB8VFEavKW6m7iR/ipTxnsdJBA2LrM7lI4PF7goQDjqjvKuV3aMbRKABAj4nXPV/hwfP9tEI2WNaSJ1itQ4X+T6vq7K36HRx9iEGQjgD+xG4IUPTawhHC3OUd1634F5JFs= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789977270; c=relaxed/simple; bh=GH4pmSqkXu9vOdwhG/pmIybrWx8obcjcGkqlzB4ba+U=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References; b=r7pF163S02ZnowBU9Bq9DOvo/Q5aCKUDq6quFuQMo3AbbJpFenfkfKMpMgWaGBF4aIKPh0nnEz9cbyWmQ82zPStzP2OYI3XkmioYSa+uh4y5Fnp2JQyh3y1p/77k3t8zDM7ZAjDGXqfgyu15sXUJ4Nyfms9SiVR7c9VJ5u9yMko= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=rock-chips.com; spf=pass smtp.mailfrom=rock-chips.com; dkim=pass (1024-bit key) header.d=rock-chips.com header.i=@rock-chips.com header.b=Eyk6LxSo; arc=none smtp.client-ip=220.197.31.75 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=rock-chips.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=rock-chips.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=rock-chips.com header.i=@rock-chips.com header.b="Eyk6LxSo" Received: from localhost.localdomain (unknown [58.22.7.114]) by smtp.qiye.163.com (Hmail) with ESMTP id 4e89beca3; Mon, 21 Sep 2026 15:18:44 +0800 (GMT+08:00) From: Shawn Lin To: Ulf Hansson Cc: linux-mmc@vger.kernel.org, linux-kernel@vger.kernel.org, Shawn Lin Subject: [PATCH v2 2/3] tools/sdio: add sdio-raw userspace client Date: Mon, 21 Sep 2026 15:18:18 +0800 Message-Id: <1789975099-157594-3-git-send-email-shawn.lin@rock-chips.com> X-Mailer: git-send-email 2.7.4 In-Reply-To: <1789975099-157594-1-git-send-email-shawn.lin@rock-chips.com> References: <1789975099-157594-1-git-send-email-shawn.lin@rock-chips.com> X-HM-Tid: 0aa0c2d4da4d03a4kunm73dcdfcc3109b9 X-HM-MType: 1 X-HM-Spam-Status: e1kfGhgUHx5ZQUpXWQgPGg8OCBgUHx5ZQUlOS1dZFg8aDwILHllBWSg2Ly tZV1koWUFDSUNOT01LS0k3V1kYFggdWUFKV1ktWUFJV1kPCRoVCBIfWUFZQkJCGVZNSx5LT08fTh hOTUxWFRQJFhoXVRMBExYaEhckFA4PWVdZGBILWUFZTkNVSUlVTFVKSk9ZV1kWGg8SFR0UWUFZT0 tIVUpLSU9PT0hVSktLVUpCS0tZBg++ DKIM-Signature: a=rsa-sha256; b=Eyk6LxSoUD3OTumiORl0C6682RC2/knXaXbQ/C96yv7yior96zlxaGuvCmEFXrZnRsK0Y/3jk7tk21K2SjvYRhBuDXFNGb8gjUo/L8JwLoJPYN/i0Vx5cX/fKfhpGSuOSXeLDN5t5Lu5do9bv+zxl5V1cAma6UTDQ5K3y4t2VQI=; s=default; c=relaxed/relaxed; d=rock-chips.com; v=1; bh=jOC6qHiBgYi8dSnviZJ+OcXTR0ZqOxJlfo/1KFxPbIY=; h=date:mime-version:subject:message-id:from; Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: Content-Transfer-Encoding: quoted-printable MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" From: Shawn Lin 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); 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=3D4 manufacturer 0x02d0, product 0xaae8 0x01006 TPL 0x21 FUNCID len=3D2 function code 0x0c Signed-off-by: Shawn Lin --- 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/ =20 MULTIPLEXER SUBSYSTEM M: Peter Rosin 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/$@) =20 -counter dma firewire hv guest bootconfig spi usb virtio mm bpf iio gpio ob= jtool leds wmi firmware debugging tracing: FORCE +counter dma firewire hv guest bootconfig spi sdio usb virtio mm bpf iio gp= io objtool leds wmi firmware debugging tracing: FORCE $(call descend,$@) =20 bpf/%: FORCE @@ -124,7 +125,7 @@ ynl: FORCE $(call descend,net/ynl) =20 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=3D),install) =20 -counter_install dma_install firewire_install gpio_install hv_install iio_i= nstall perf_install bootconfig_install spi_install usb_install virtio_insta= ll mm_install bpf_install objtool_install wmi_install debugging_install tra= cing_install: +counter_install dma_install firewire_install gpio_install hv_install iio_i= nstall perf_install bootconfig_install spi_install sdio_install usb_install= virtio_install mm_install bpf_install objtool_install wmi_install debuggin= g_install tracing_install: $(call descend,$(@:_install=3D),install) =20 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 =20 acpi_clean: $(call descend,power/acpi,clean) @@ -179,7 +181,7 @@ acpi_clean: cpupower_clean: $(call descend,power/cpupower,clean) =20 -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 o= bjtool_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 g= pio_clean objtool_clean leds_clean firmware_clean debugging_clean tracing_c= lean: $(call descend,$(@:_clean=3D),clean) =20 libapi_clean: @@ -226,7 +228,7 @@ ynl_clean: $(call descend,net/$(@:_clean=3D),clean) =20 clean: acpi_clean counter_clean cpupower_clean dma_clean hv_clean firewire= _clean \ - perf_clean selftests_clean turbostat_clean bootconfig_clean spi_clean us= b_clean virtio_clean \ + perf_clean selftests_clean turbostat_clean bootconfig_clean spi_clean sd= io_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_c= lean \ 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 +=3D 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 ?=3D /usr/bin + +ifeq ($(srctree),) +srctree :=3D $(patsubst %/,%,$(dir $(CURDIR))) +srctree :=3D $(patsubst %/,%,$(dir $(srctree))) +endif + +# Do not use make's built-in rules +# (this improves performance and avoids hard-to-debug behaviour); +MAKEFLAGS +=3D -r + +CFLAGS +=3D -O2 -Wall -g -D_GNU_SOURCE -I$(OUTPUT)include -I$(OUTPUT). + +ALL_TARGETS :=3D sdio-raw +ALL_PROGRAMS :=3D $(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 :=3D $(OUTPUT)sdio-raw-in.o +$(SDIO_RAW_IN): prepare FORCE + $(Q)$(MAKE) $(build)=3Dsdio-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 " >&2 + exit 1 +fi + +src=3D"$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=3D'/* SPDX''-License-Identifier: GPL-2.0 */' + +cat < + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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[] =3D { { ~0U, "" } }; +static const struct sdio_id_name sdio_device_names[] =3D { { ~0U, "" } }; +#endif + +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) + +static const char *progname =3D "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 ('-' =3D stdin).\= n", + progname); + exit(rc); +} + +static void die_errno(const char *what) +{ + fprintf(stderr, "%s: %s: %s", progname, what, strerror(errno)); + if (errno =3D=3D 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 =3D 0; + v =3D strtoul(s, &end, 0); + if (errno || end =3D=3D 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 =3D 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 =3D 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 =3D { + .fn =3D fn, + .addr =3D addr, + }; + + if (ioctl(fd, SDIO_IOC_RW, &rw) < 0) + return -1; + *val =3D rw.data; + return 0; +} + +static void hexdump(const unsigned char *buf, unsigned int len) +{ + unsigned int i, j; + + for (i =3D 0; i < len; i +=3D 16) { + printf(" %04x:", i); + for (j =3D 0; j < 16; j++) { + if (i + j < len) + printf(" %02x", buf[i + j]); + else + printf(" "); + } + printf(" "); + for (j =3D 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 =3D { + .write =3D write, + .fn =3D fn, + .raw =3D raw, + .addr =3D addr, + .data =3D 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 =3D write; + rw.fn =3D fn; + rw.incr_addr =3D incr_addr; + rw.addr =3D addr; + rw.count =3D count; + + if (write) { + f =3D (!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) !=3D count) { + fprintf(stderr, "%s: short read from %s\n", progname, + infile ? infile : "stdin"); + exit(1); + } + if (f !=3D stdin) + fclose(f); + } + + if (ioctl(fd, SDIO_IOC_RW_EXT, &rw) < 0) + die_errno("CMD53 failed"); + + if (write) + return; + + if (outfile) { + f =3D 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[] =3D { + { 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 =3D 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 =3D 0; i < ARRAY_SIZE(fields); i++) { + v =3D 0; + for (j =3D 0; j < fields[i].len; j++) + v |=3D (unsigned long)regs[fields[i].addr + j] << (8 * j); + + if (fields[i].len =3D=3D 1) + printf(" 0x%02x %-24s 0x%02lx\n", fields[i].addr, + fields[i].name, v); + else if (fields[i].len =3D=3D 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 =3D=3D 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 =3D 0; i < sizeof(regs); i++) + if (rd52(fd, 0, fn * 0x100 + i, ®s[i])) + die_errno("CMD52 (FBR dump) failed"); + + cis =3D regs[0x09] | regs[0x0a] << 8 | regs[0x0b] << 16; + csa =3D 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 =3D b[0] | b[1] << 8 | b[2] << 16; + if (!addr) { + printf("CIS (fn %u): not present (CIS pointer is 0)\n", fn); + return; + } + addr &=3D 0x1ffff; + + printf("CIS (fn %u) @ 0x%05lx:\n", fn, addr); + + for (n =3D 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 =3D=3D CISTPL_END) { + printf(" 0x%05lx TPL 0xff END\n", addr); + break; + } + if (code =3D=3D 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 =3D=3D 0xff) { + printf(" 0x%05lx TPL 0x%02x link=3D0xff (undefined), stop\n", + addr, code); + break; + } + + for (i =3D 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=3D%u\n", addr, code, + tpl_name(code), link); + + switch (code) { + case CISTPL_MANFID: + if (link >=3D 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 =3D 2; + + printf(" rev %u.%u:", body[0], body[1]); + while (j < link && body[j] !=3D 0xff) { + unsigned int start =3D 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 >=3D 1) { + printf(" function code 0x%02x\n", body[0]); + break; + } + hexdump(body, link); + break; + default: + hexdump(body, link); + break; + } + + addr +=3D 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 =3D fopen(path, "r"); + if (!f) + return -1; + if (!fgets(buf, len, f)) { + fclose(f); + return -1; + } + fclose(f); + n =3D strlen(buf); + while (n && (buf[n - 1] =3D=3D '\n' || buf[n - 1] =3D=3D '\r')) + buf[--n] =3D '\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 =3D 0; i < count; i++) + if (tbl[i].id =3D=3D 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[] =3D { "info1", "info2", "info3", "info4= " }; + char tmp[64]; + size_t used =3D 0; + unsigned int i; + + buf[0] =3D '\0'; + for (i =3D 0; i < ARRAY_SIZE(attrs); i++) { + size_t slen; + + if (read_attr(dir, attrs[i], tmp, sizeof(tmp)) || !tmp[0]) + continue; + + slen =3D strlen(tmp); + if (used + slen + 1 >=3D len) + break; + if (used) + buf[used++] =3D ' '; + memcpy(buf + used, tmp, slen + 1); + used +=3D slen; + } +} + +static void list_devices(void) +{ + struct dirent **entries; + int i, n, total =3D 0; + + n =3D 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 =3D 0; i < n; i++) { + const char *devname =3D 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] =3D=3D '.') + 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 =3D strtoul(vend, NULL, 0); + pid =3D strtoul(devid, NULL, 0); + c =3D strtoul(clsv, NULL, 0); + + snprintf(vidpid, sizeof(vidpid), "0x%04x:0x%04x", vid, pid); + + dname =3D lookup_name(sdio_device_names, + ARRAY_SIZE(sdio_device_names), pid); + if (!dname) { + snprintf(namebuf, sizeof(namebuf), "Unlisted(0x%04x)", + pid); + dname =3D namebuf; + } + + cname =3D 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 =3D 0; i < n; i++) + free(entries[i]); + free(entries); +} + +static unsigned int get_fn(const char *dev, int fn_override) +{ + unsigned int fn =3D fn_override >=3D 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 =3D 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 =3D NULL, *cmd; + const char *infile =3D NULL, *outfile =3D NULL; + unsigned long addr =3D 0, count =3D 0, val =3D 0; + int fn_override =3D -1, write =3D 0, raw =3D 0, incr_addr =3D 1; + unsigned int fn; + int i; + + for (i =3D 1; i < argc; i++) { + const char *a =3D argv[i]; + + if (!strcmp(a, "-d") || !strcmp(a, "--device")) { + if (++i >=3D argc) + usage(1); + dev =3D argv[i]; + } else if (!strcmp(a, "-f") || !strcmp(a, "--func")) { + if (++i >=3D argc) + usage(1); + fn_override =3D parse_num(argv[i], "function number"); + } else if (!strcmp(a, "-h") || !strcmp(a, "--help")) { + usage(0); + } else { + break; + } + } + + if (i >=3D argc) + usage(1); + + cmd =3D argv[i++]; + + if (!strcmp(cmd, "cmd52")) { + for (; i < argc; i++) { + if (!strcmp(argv[i], "-a") && i + 1 < argc) + addr =3D get_addr(argv[++i]); + else if (!strcmp(argv[i], "-w") && i + 1 < argc) + write =3D 1, val =3D parse_num(argv[++i], "byte value"); + else if (!strcmp(argv[i], "-r")) + raw =3D 1; + else if (!strcmp(argv[i], "-f") && i + 1 < argc) + fn_override =3D parse_num(argv[++i], "function number"); + else + usage(1); + } + fn =3D 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 =3D get_addr(argv[++i]); + else if (!strcmp(argv[i], "-n") && i + 1 < argc) + count =3D parse_num(argv[++i], "count"); + else if (!strcmp(argv[i], "-w") && i + 1 < argc) + write =3D 1, infile =3D argv[++i]; + else if (!strcmp(argv[i], "-o") && i + 1 < argc) + outfile =3D argv[++i]; + else if (!strcmp(argv[i], "--fixed")) + incr_addr =3D 0; + else if (!strcmp(argv[i], "-f") && i + 1 < argc) + fn_override =3D 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 =3D get_fn(dev, fn_override); + if (fn =3D=3D 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 =3D parse_num(argv[++i], "function number"); + else + usage(1); + } + fn =3D 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 =3D parse_num(argv[++i], "function number"); + else + usage(1); + } + fn =3D get_fn(dev, fn_override); + fbr(open_dev(dev), fn); + } else { + fprintf(stderr, "%s: unknown command: %s\n", progname, cmd); + usage(1); + } + + return 0; +} --=20 2.7.4 From nobody Thu Sep 24 20:36:59 2026 Received: from mail-m32113.qiye.163.com (mail-m32113.qiye.163.com [220.197.32.113]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8F2933C819B; Mon, 21 Sep 2026 07:54:26 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=220.197.32.113 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789977277; cv=none; b=uVdZdSG8kU6ayWqsM2fgXOl4eZivzJcxNokyIIMws1u11tPlakPtRop/lchO/1HnF4Z3jpJYma2gnHAc/neRVybTVHSxPPdTK3M0M6rA0/UzilwRIw0dwrZ4ysRSYajc79xV2pdHKc2haO5hot3K/enyMT3WPG8/T++l8j2zI9Y= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789977277; c=relaxed/simple; bh=/9B46h7KGDGcvcMCet0lJXnFROLWwbfOPW13N2o3KOM=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References; b=d9fESceX1FMDcX028Eqd3BKjrimil5dAFnEakWD0YSrB3d+1Jje8hLTu9ShYkK2JnDq8+2AtylLr8+Cgn4Ev6CekfePlKEc5PYHbIicteVHHiO3aTTR7l811KaYz2bCf5fsXrYVvaSEJNdY6Obj9Qnhdk4tZjqUc25UmeV7OksM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=rock-chips.com; spf=pass smtp.mailfrom=rock-chips.com; dkim=pass (1024-bit key) header.d=rock-chips.com header.i=@rock-chips.com header.b=kI4t9j7S; arc=none smtp.client-ip=220.197.32.113 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=rock-chips.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=rock-chips.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=rock-chips.com header.i=@rock-chips.com header.b="kI4t9j7S" Received: from localhost.localdomain (unknown [58.22.7.114]) by smtp.qiye.163.com (Hmail) with ESMTP id 4e89becbb; Mon, 21 Sep 2026 15:18:47 +0800 (GMT+08:00) From: Shawn Lin To: Ulf Hansson Cc: linux-mmc@vger.kernel.org, linux-kernel@vger.kernel.org, Shawn Lin Subject: [PATCH v2 3/3] docs: mmc: add introduction to writing SDIO function drivers Date: Mon, 21 Sep 2026 15:18:19 +0800 Message-Id: <1789975099-157594-4-git-send-email-shawn.lin@rock-chips.com> X-Mailer: git-send-email 2.7.4 In-Reply-To: <1789975099-157594-1-git-send-email-shawn.lin@rock-chips.com> References: <1789975099-157594-1-git-send-email-shawn.lin@rock-chips.com> X-HM-Tid: 0aa0c2d4e64203a4kunm73dcdfcc3109da X-HM-MType: 1 X-HM-Spam-Status: e1kfGhgUHx5ZQUpXWQgPGg8OCBgUHx5ZQUlOS1dZFg8aDwILHllBWSg2Ly tZV1koWUFDSUNOT01LS0k3V1kYFggdWUFKV1ktWUFJV1kPCRoVCBIfWUFZQh4aQ1ZOTU9CSk9DHk lLTxpWFRQJFhoXVRMBExYaEhckFA4PWVdZGBILWUFZTkNVSUlVTFVKSk9ZV1kWGg8SFR0UWUFZT0 tIVUpLSEpOTE5VSktLVUpCS0tZBg++ DKIM-Signature: a=rsa-sha256; b=kI4t9j7SCtc1g9BAXjzu2T72MqCBMCaEwSfFJdg4q+kp3Ize2OmkCz7WplgjavMNFm1QyfDKLHgtWXE8xYjCboimY+9hoLP+GwUYW8NKsb9meedc4BdtvamYBEVQVWrClHHaO99HhgFnFJU+IfFq6JZJ1w/yYR57bP/yurjISI0=; s=default; c=relaxed/relaxed; d=rock-chips.com; v=1; bh=EufASKu8Cxl+AKOE1zCMWWItiVY529YYoQW6PH/HxC4=; h=date:mime-version:subject:message-id:from; Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: Content-Transfer-Encoding: quoted-printable MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" From: Shawn Lin 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 --- 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/Docum= entation/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 + +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D +SDIO function driver introduction +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D + +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 +=3D=3D=3D=3D=3D=3D=3D=3D=3D + +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[] =3D { + { 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 +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +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 +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +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 =3D devm_kzalloc(&func->dev, sizeof(*dev), GFP_KERNEL); + if (!dev) + return -ENOMEM; + + dev->func =3D func; + + sdio_claim_host(func); + + ret =3D sdio_enable_func(func); + if (ret) + goto err_release; + + ret =3D sdio_set_block_size(func, 512); + if (ret) + goto err_disable; + + ret =3D 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 =3D 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 +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +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 +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +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 +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +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 +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +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/ --=20 2.7.4