mirror of
https://github.com/torvalds/linux.git
synced 2026-11-07 06:17:02 +01:00
bus: mhi: host: clients: Add loopback driver with sysfs interface
The MHI specification defines a LOOPBACK channel. The endpoint firmware echoes back whatever the host sends on this channel. Without a host-side driver, there is no way to exercise this channel to validate MHI data path integrity between host and endpoint. Add a host-side loopback driver that binds to the LOOPBACK channel and expose a sysfs interface for data path testing. The sysfs interface allows users to configure TRE buffer size and count, trigger a loopback test, and read the result. The new sysfs ABI is documented under Documentation/ABI/testing/, not stable/, since this is a new interface without established API guarantees yet. It is unrelated to the existing stable MHI sysfs ABI documented in Documentation/ABI/stable/sysfs-bus-mhi, despite both being covered by the same MAINTAINERS entry. Co-developed-by: Krishna Chaitanya Chundru <krishna.chundru@oss.qualcomm.com> Signed-off-by: Krishna Chaitanya Chundru <krishna.chundru@oss.qualcomm.com> Signed-off-by: Sumit Kumar <sumit.kumar@oss.qualcomm.com> Link: https://patch.msgid.link/20261005-lb-v7-1-36a268b6386f@oss.qualcomm.com Signed-off-by: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>
This commit is contained in:
parent
f6985f512f
commit
dd93cef28d
49
Documentation/ABI/testing/sysfs-bus-mhi-devices-loopback
Normal file
49
Documentation/ABI/testing/sysfs-bus-mhi-devices-loopback
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
What: /sys/bus/mhi/devices/mhi<N>_LOOPBACK/tre_size
|
||||
Date: August 2026
|
||||
KernelVersion: 7.4
|
||||
Contact: mhi@lists.linux.dev
|
||||
Description:
|
||||
(RW) Size of each Transfer Ring Element (TRE) buffer in bytes
|
||||
used for the loopback test. Valid range is 1 to the value
|
||||
reported by max_tre_size. Default value is 32 bytes.
|
||||
|
||||
What: /sys/bus/mhi/devices/mhi<N>_LOOPBACK/max_tre_size
|
||||
Date: August 2026
|
||||
KernelVersion: 7.4
|
||||
Contact: mhi@lists.linux.dev
|
||||
Description:
|
||||
(RO) Maximum allowed Transfer Ring Element (TRE) size in bytes.
|
||||
Reading this file returns the upper bound for the tre_size
|
||||
attribute.
|
||||
|
||||
What: /sys/bus/mhi/devices/mhi<N>_LOOPBACK/num_tre
|
||||
Date: August 2026
|
||||
KernelVersion: 7.4
|
||||
Contact: mhi@lists.linux.dev
|
||||
Description:
|
||||
(RW) Number of Transfer Ring Elements (TREs) to use per
|
||||
loopback test. Must be greater than zero and must not exceed
|
||||
the value reported by max_num_tre. Default value is 1.
|
||||
|
||||
What: /sys/bus/mhi/devices/mhi<N>_LOOPBACK/max_num_tre
|
||||
Date: August 2026
|
||||
KernelVersion: 7.4
|
||||
Contact: mhi@lists.linux.dev
|
||||
Description:
|
||||
(RO) Maximum number of Transfer Ring Elements (TREs) currently
|
||||
available on the channel ring. Reading this file returns the
|
||||
upper bound for the num_tre attribute.
|
||||
|
||||
What: /sys/bus/mhi/devices/mhi<N>_LOOPBACK/start
|
||||
Date: August 2026
|
||||
KernelVersion: 7.4
|
||||
Contact: mhi@lists.linux.dev
|
||||
Description:
|
||||
(WO) Write any value to trigger a loopback test. The driver
|
||||
sends random data to the endpoint using the configured tre_size
|
||||
and num_tre parameters, waits for the endpoint to echo it back,
|
||||
and verifies the received data matches what was sent.
|
||||
|
||||
This is a blocking write that returns when the test completes
|
||||
or times out after 5 seconds. The write returns an error code
|
||||
if the test fails or times out.
|
||||
|
|
@ -17640,6 +17640,7 @@ L: linux-arm-msm@vger.kernel.org
|
|||
S: Maintained
|
||||
T: git git://git.kernel.org/pub/scm/linux/kernel/git/mani/mhi.git
|
||||
F: Documentation/ABI/stable/sysfs-bus-mhi
|
||||
F: Documentation/ABI/testing/sysfs-bus-mhi-devices-loopback
|
||||
F: Documentation/mhi/
|
||||
F: drivers/bus/mhi/
|
||||
F: drivers/pci/endpoint/functions/pci-epf-mhi.c
|
||||
|
|
|
|||
|
|
@ -29,3 +29,4 @@ config MHI_BUS_PCI_GENERIC
|
|||
This driver provides MHI PCI controller driver for devices such as
|
||||
Qualcomm SDX55 based PCIe modems.
|
||||
|
||||
source "drivers/bus/mhi/host/clients/Kconfig"
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ mhi-$(CONFIG_MHI_BUS_DEBUG) += debugfs.o
|
|||
|
||||
obj-$(CONFIG_MHI_BUS_PCI_GENERIC) += mhi_pci_generic.o
|
||||
mhi_pci_generic-y += pci_generic.o
|
||||
obj-y += clients/
|
||||
|
|
|
|||
17
drivers/bus/mhi/host/clients/Kconfig
Normal file
17
drivers/bus/mhi/host/clients/Kconfig
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
config MHI_BUS_LOOPBACK
|
||||
tristate "MHI LOOPBACK client driver"
|
||||
depends on MHI_BUS
|
||||
help
|
||||
MHI LOOPBACK client driver that binds to the MHI LOOPBACK channel
|
||||
as defined in the MHI specification. The LOOPBACK channel is
|
||||
implemented by MHI-based devices (e.g. modems, WLAN) in the field,
|
||||
where the endpoint firmware echoes back whatever the host sends.
|
||||
|
||||
This driver exposes a sysfs interface for testing MHI data path
|
||||
integrity between host and endpoint. Users can configure the TRE
|
||||
size and count, and trigger a loopback test.
|
||||
|
||||
To compile this driver as a module, choose M here. The module
|
||||
will be called mhi_loopback.
|
||||
2
drivers/bus/mhi/host/clients/Makefile
Normal file
2
drivers/bus/mhi/host/clients/Makefile
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
obj-$(CONFIG_MHI_BUS_LOOPBACK) += mhi_loopback.o
|
||||
mhi_loopback-y += loopback.o
|
||||
312
drivers/bus/mhi/host/clients/loopback.c
Normal file
312
drivers/bus/mhi/host/clients/loopback.c
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
|
||||
*/
|
||||
|
||||
#include <linux/atomic.h>
|
||||
#include <linux/cleanup.h>
|
||||
#include <linux/completion.h>
|
||||
#include <linux/errno.h>
|
||||
#include <linux/mhi.h>
|
||||
#include <linux/module.h>
|
||||
#include <linux/mutex.h>
|
||||
#include <linux/random.h>
|
||||
#include <linux/sizes.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/string.h>
|
||||
#include <linux/sysfs.h>
|
||||
#include <linux/types.h>
|
||||
|
||||
#define MHI_LOOPBACK_DEFAULT_TRE_SIZE 32
|
||||
#define MHI_LOOPBACK_DEFAULT_NUM_TRE 1
|
||||
#define MHI_LOOPBACK_TIMEOUT_MS 5000
|
||||
#define MHI_LOOPBACK_MAX_TRE_SIZE (SZ_64K - 1)
|
||||
|
||||
struct mhi_loopback {
|
||||
struct mhi_device *mdev;
|
||||
/* Serializes the sysfs attributes against a running test */
|
||||
struct mutex lb_mutex;
|
||||
struct completion comp;
|
||||
/* Counts pending DL completions; comp fires when it hits zero */
|
||||
atomic_t tre_pending;
|
||||
u32 num_tre;
|
||||
u32 tre_size;
|
||||
};
|
||||
|
||||
static ssize_t tre_size_show(struct device *dev,
|
||||
struct device_attribute *attr, char *buf)
|
||||
{
|
||||
struct mhi_loopback *loopback = dev_get_drvdata(dev);
|
||||
|
||||
return sysfs_emit(buf, "%u\n", loopback->tre_size);
|
||||
}
|
||||
|
||||
static ssize_t tre_size_store(struct device *dev,
|
||||
struct device_attribute *attr,
|
||||
const char *buf, size_t count)
|
||||
{
|
||||
struct mhi_loopback *loopback = dev_get_drvdata(dev);
|
||||
u32 val;
|
||||
|
||||
if (kstrtou32(buf, 0, &val))
|
||||
return -EINVAL;
|
||||
|
||||
if (val == 0 || val > MHI_LOOPBACK_MAX_TRE_SIZE)
|
||||
return -EINVAL;
|
||||
|
||||
guard(mutex)(&loopback->lb_mutex);
|
||||
loopback->tre_size = val;
|
||||
|
||||
return count;
|
||||
}
|
||||
static DEVICE_ATTR_RW(tre_size);
|
||||
|
||||
static ssize_t max_tre_size_show(struct device *dev,
|
||||
struct device_attribute *attr, char *buf)
|
||||
{
|
||||
return sysfs_emit(buf, "%u\n", MHI_LOOPBACK_MAX_TRE_SIZE);
|
||||
}
|
||||
static DEVICE_ATTR_RO(max_tre_size);
|
||||
|
||||
static int mhi_loopback_ring_capacity(struct mhi_loopback *loopback)
|
||||
{
|
||||
return min(mhi_get_free_desc_count(loopback->mdev, DMA_TO_DEVICE),
|
||||
mhi_get_free_desc_count(loopback->mdev, DMA_FROM_DEVICE));
|
||||
}
|
||||
|
||||
static ssize_t max_num_tre_show(struct device *dev,
|
||||
struct device_attribute *attr, char *buf)
|
||||
{
|
||||
struct mhi_loopback *loopback = dev_get_drvdata(dev);
|
||||
|
||||
return sysfs_emit(buf, "%d\n", mhi_loopback_ring_capacity(loopback));
|
||||
}
|
||||
static DEVICE_ATTR_RO(max_num_tre);
|
||||
|
||||
static ssize_t num_tre_show(struct device *dev,
|
||||
struct device_attribute *attr, char *buf)
|
||||
{
|
||||
struct mhi_loopback *loopback = dev_get_drvdata(dev);
|
||||
|
||||
return sysfs_emit(buf, "%u\n", loopback->num_tre);
|
||||
}
|
||||
|
||||
static ssize_t num_tre_store(struct device *dev,
|
||||
struct device_attribute *attr,
|
||||
const char *buf, size_t count)
|
||||
{
|
||||
struct mhi_loopback *loopback = dev_get_drvdata(dev);
|
||||
u32 val;
|
||||
int el_num;
|
||||
|
||||
if (kstrtou32(buf, 0, &val))
|
||||
return -EINVAL;
|
||||
|
||||
if (val == 0)
|
||||
return -EINVAL;
|
||||
|
||||
guard(mutex)(&loopback->lb_mutex);
|
||||
|
||||
el_num = mhi_loopback_ring_capacity(loopback);
|
||||
if (val > el_num) {
|
||||
dev_err(dev, "num_tre (%u) exceeds ring capacity (%d)\n", val, el_num);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
loopback->num_tre = val;
|
||||
|
||||
return count;
|
||||
}
|
||||
static DEVICE_ATTR_RW(num_tre);
|
||||
|
||||
static void mhi_loopback_reset_chan(struct mhi_loopback *loopback)
|
||||
{
|
||||
struct device *dev = &loopback->mdev->dev;
|
||||
int ret;
|
||||
|
||||
mhi_unprepare_from_transfer(loopback->mdev);
|
||||
ret = mhi_prepare_for_transfer(loopback->mdev);
|
||||
if (ret)
|
||||
dev_err(dev, "Failed to re-prepare channel for transfers: %d\n", ret);
|
||||
}
|
||||
|
||||
static ssize_t start_store(struct device *dev,
|
||||
struct device_attribute *attr,
|
||||
const char *buf, size_t count)
|
||||
{
|
||||
struct mhi_loopback *loopback = dev_get_drvdata(dev);
|
||||
u32 total_size, tre_count, tre_size;
|
||||
int i, ret;
|
||||
|
||||
guard(mutex)(&loopback->lb_mutex);
|
||||
|
||||
tre_size = loopback->tre_size;
|
||||
tre_count = loopback->num_tre;
|
||||
total_size = size_mul(tre_count, tre_size);
|
||||
|
||||
if (total_size > KMALLOC_MAX_SIZE)
|
||||
return -EINVAL;
|
||||
|
||||
if (tre_count > mhi_loopback_ring_capacity(loopback)) {
|
||||
dev_err(dev, "Not enough ring space for %u TREs\n", tre_count);
|
||||
return -ENOSPC;
|
||||
}
|
||||
|
||||
void *recv_buf __free(kfree) = kzalloc(total_size, GFP_KERNEL);
|
||||
if (!recv_buf)
|
||||
return -ENOMEM;
|
||||
|
||||
void *send_buf __free(kfree) = kzalloc(total_size, GFP_KERNEL);
|
||||
if (!send_buf)
|
||||
return -ENOMEM;
|
||||
|
||||
get_random_bytes(send_buf, total_size);
|
||||
|
||||
atomic_set(&loopback->tre_pending, tre_count);
|
||||
reinit_completion(&loopback->comp);
|
||||
|
||||
for (i = 0; i < tre_count; i++) {
|
||||
ret = mhi_queue_buf(loopback->mdev, DMA_FROM_DEVICE,
|
||||
recv_buf + (i * tre_size), tre_size, MHI_EOT);
|
||||
if (ret) {
|
||||
dev_err(dev, "Unable to queue read TRE %d: %d\n", i, ret);
|
||||
/*
|
||||
* TREs already queued for this iteration still point at
|
||||
* recv_buf, which is about to be freed. Reset the channel
|
||||
* to reclaim them before returning.
|
||||
*/
|
||||
mhi_loopback_reset_chan(loopback);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < tre_count - 1; i++) {
|
||||
ret = mhi_queue_buf(loopback->mdev, DMA_TO_DEVICE,
|
||||
send_buf + (i * tre_size), tre_size, MHI_CHAIN);
|
||||
if (ret) {
|
||||
dev_err(dev, "Unable to queue send TRE %d: %d\n", i, ret);
|
||||
/* Reclaim the queued DL and UL TREs before recv_buf/send_buf are freed */
|
||||
mhi_loopback_reset_chan(loopback);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
ret = mhi_queue_buf(loopback->mdev, DMA_TO_DEVICE,
|
||||
send_buf + (i * tre_size), tre_size, MHI_EOT);
|
||||
if (ret) {
|
||||
dev_err(dev, "Unable to queue final TRE: %d\n", ret);
|
||||
/* Reclaim the queued DL and UL TREs before recv_buf/send_buf are freed */
|
||||
mhi_loopback_reset_chan(loopback);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (!wait_for_completion_timeout(&loopback->comp,
|
||||
msecs_to_jiffies(MHI_LOOPBACK_TIMEOUT_MS))) {
|
||||
dev_err(dev, "Loopback test timed out\n");
|
||||
mhi_loopback_reset_chan(loopback);
|
||||
return -ETIMEDOUT;
|
||||
}
|
||||
|
||||
if (memcmp(send_buf, recv_buf, total_size)) {
|
||||
dev_err(dev, "Loopback data mismatch\n");
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
static DEVICE_ATTR_WO(start);
|
||||
|
||||
static void mhi_loopback_dl_callback(struct mhi_device *mhi_dev,
|
||||
struct mhi_result *mhi_res)
|
||||
{
|
||||
struct mhi_loopback *loopback = dev_get_drvdata(&mhi_dev->dev);
|
||||
|
||||
if (mhi_res->transaction_status && mhi_res->transaction_status != -ENOTCONN)
|
||||
dev_err(&mhi_dev->dev, "DL callback error: status %d\n",
|
||||
mhi_res->transaction_status);
|
||||
|
||||
if (atomic_dec_and_test(&loopback->tre_pending))
|
||||
complete(&loopback->comp);
|
||||
}
|
||||
|
||||
static void mhi_loopback_ul_callback(struct mhi_device *mhi_dev,
|
||||
struct mhi_result *mhi_res)
|
||||
{
|
||||
if (mhi_res->transaction_status && mhi_res->transaction_status != -ENOTCONN)
|
||||
dev_err(&mhi_dev->dev, "UL callback error: status %d\n",
|
||||
mhi_res->transaction_status);
|
||||
}
|
||||
|
||||
static struct attribute *mhi_loopback_attrs[] = {
|
||||
&dev_attr_tre_size.attr,
|
||||
&dev_attr_max_tre_size.attr,
|
||||
&dev_attr_num_tre.attr,
|
||||
&dev_attr_max_num_tre.attr,
|
||||
&dev_attr_start.attr,
|
||||
NULL
|
||||
};
|
||||
|
||||
static const struct attribute_group mhi_loopback_group = {
|
||||
.attrs = mhi_loopback_attrs,
|
||||
};
|
||||
|
||||
static const struct attribute_group *mhi_loopback_groups[] = {
|
||||
&mhi_loopback_group,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static int mhi_loopback_probe(struct mhi_device *mhi_dev,
|
||||
const struct mhi_device_id *id)
|
||||
{
|
||||
struct mhi_loopback *loopback;
|
||||
int ret;
|
||||
|
||||
loopback = devm_kzalloc(&mhi_dev->dev, sizeof(*loopback), GFP_KERNEL);
|
||||
if (!loopback)
|
||||
return -ENOMEM;
|
||||
|
||||
loopback->mdev = mhi_dev;
|
||||
loopback->tre_size = MHI_LOOPBACK_DEFAULT_TRE_SIZE;
|
||||
loopback->num_tre = MHI_LOOPBACK_DEFAULT_NUM_TRE;
|
||||
|
||||
mutex_init(&loopback->lb_mutex);
|
||||
init_completion(&loopback->comp);
|
||||
|
||||
dev_set_drvdata(&mhi_dev->dev, loopback);
|
||||
|
||||
ret = mhi_prepare_for_transfer(mhi_dev);
|
||||
if (ret)
|
||||
return dev_err_probe(&mhi_dev->dev, ret, "Failed to prepare for transfers\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void mhi_loopback_remove(struct mhi_device *mhi_dev)
|
||||
{
|
||||
mhi_unprepare_from_transfer(mhi_dev);
|
||||
}
|
||||
|
||||
static const struct mhi_device_id mhi_loopback_id_table[] = {
|
||||
{ .chan = "LOOPBACK" },
|
||||
{ }
|
||||
};
|
||||
MODULE_DEVICE_TABLE(mhi, mhi_loopback_id_table);
|
||||
|
||||
static struct mhi_driver mhi_loopback_driver = {
|
||||
.probe = mhi_loopback_probe,
|
||||
.remove = mhi_loopback_remove,
|
||||
.dl_xfer_cb = mhi_loopback_dl_callback,
|
||||
.ul_xfer_cb = mhi_loopback_ul_callback,
|
||||
.id_table = mhi_loopback_id_table,
|
||||
.driver = {
|
||||
.name = "mhi_loopback",
|
||||
.dev_groups = mhi_loopback_groups,
|
||||
},
|
||||
};
|
||||
|
||||
module_mhi_driver(mhi_loopback_driver);
|
||||
|
||||
MODULE_AUTHOR("Krishna Chaitanya Chundru <krishna.chundru@oss.qualcomm.com>");
|
||||
MODULE_AUTHOR("Sumit Kumar <sumit.kumar@oss.qualcomm.com>");
|
||||
MODULE_DESCRIPTION("MHI Host Loopback Driver");
|
||||
MODULE_LICENSE("GPL");
|
||||
Loading…
Reference in New Issue
Block a user