summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGreg Kroah-Hartman <gregkh@linuxfoundation.org>2026-06-12 11:44:24 +0300
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>2026-06-12 11:44:24 +0300
commitf176a7c3e44f80b4493d22aa12d02673243ec7d8 (patch)
tree8ca94801b69d603617663e13dfcc6432cdf5e4dc
parent511e746700ee6e93104d061a87743906128ab709 (diff)
parentc1bef05763c94ae284ee2881c03bf0753f8d213a (diff)
downloadlinux-f176a7c3e44f80b4493d22aa12d02673243ec7d8.tar.xz
Merge tag 'thunderbolt-for-v7.2-rc1' of ssh://gitolite.kernel.org/pub/scm/linux/kernel/git/westeri/thunderbolt into usb-next
Mika writes: thunderbolt: Changes for v7.2 merge window This includes following USB4/Thunderbolt changes for the v7.2 merge window: - Make the driver more compliant with the connection manager guide. - Improvements over Thunderbolt XDomain service handling. - USB4STREAM driver. - Split out PCIe bits into pci.c to allow the driver to work on non-PCIe hosts as well. - Various fixes and improvements. All these have been in linux-next with no reported issues. * tag 'thunderbolt-for-v7.2-rc1' of ssh://gitolite.kernel.org/pub/scm/linux/kernel/git/westeri/thunderbolt: (41 commits) thunderbolt: debugfs: Fix sideband write size check thunderbolt: debugfs: Fix margining error counter buffer leak thunderbolt: test: Release third DP tunnel thunderbolt: Prevent XDomain delayed work use-after-free on disconnect thunderbolt: test: Add KUnit tests for property parser bounds checks thunderbolt: Add some more descriptive probe error messages thunderbolt: Require nhi->ops be valid thunderbolt: Separate out common NHI bits thunderbolt: Move pci_device out of tb_nhi thunderbolt: Increase Notification Timeout to 255 ms for USB4 routers thunderbolt: Increase timeout for Configuration Ready bit thunderbolt: Verify Router Ready bit is set after router enumeration thunderbolt: Verify PCIe adapter in detect state before tunnel setup thunderbolt: Activate path hops from source to destination thunderbolt: Fix lane bonding log when bonding not possible thunderbolt: Don't access path config space on Lane 1 adapters in tb_switch_reset_host() thunderbolt: Improve multi-display DisplayPort tunnel allocation docs: admin-guide: thunderbolt: Add instructions how to use USB4STREAM thunderbolt: Add support for USB4STREAM thunderbolt: Add support for ConfigFS ...
-rw-r--r--Documentation/ABI/testing/configfs-thunderbolt_stream83
-rw-r--r--Documentation/admin-guide/thunderbolt.rst61
-rw-r--r--drivers/net/thunderbolt/main.c20
-rw-r--r--drivers/thunderbolt/Kconfig15
-rw-r--r--drivers/thunderbolt/Makefile6
-rw-r--r--drivers/thunderbolt/acpi.c14
-rw-r--r--drivers/thunderbolt/configfs.c61
-rw-r--r--drivers/thunderbolt/ctl.c16
-rw-r--r--drivers/thunderbolt/debugfs.c18
-rw-r--r--drivers/thunderbolt/dma_test.c25
-rw-r--r--drivers/thunderbolt/domain.c37
-rw-r--r--drivers/thunderbolt/eeprom.c2
-rw-r--r--drivers/thunderbolt/icm.c39
-rw-r--r--drivers/thunderbolt/nhi.c606
-rw-r--r--drivers/thunderbolt/nhi.h33
-rw-r--r--drivers/thunderbolt/nhi_ops.c185
-rw-r--r--drivers/thunderbolt/nhi_regs.h3
-rw-r--r--drivers/thunderbolt/path.c37
-rw-r--r--drivers/thunderbolt/pci.c622
-rw-r--r--drivers/thunderbolt/property.c154
-rw-r--r--drivers/thunderbolt/stream.c1698
-rw-r--r--drivers/thunderbolt/switch.c77
-rw-r--r--drivers/thunderbolt/tb.c86
-rw-r--r--drivers/thunderbolt/tb.h21
-rw-r--r--drivers/thunderbolt/tb_regs.h19
-rw-r--r--drivers/thunderbolt/test.c249
-rw-r--r--drivers/thunderbolt/tunnel.c35
-rw-r--r--drivers/thunderbolt/usb4.c35
-rw-r--r--drivers/thunderbolt/usb4_port.c2
-rw-r--r--drivers/thunderbolt/xdomain.c311
-rw-r--r--include/linux/thunderbolt.h59
31 files changed, 3710 insertions, 919 deletions
diff --git a/Documentation/ABI/testing/configfs-thunderbolt_stream b/Documentation/ABI/testing/configfs-thunderbolt_stream
new file mode 100644
index 000000000000..7abc6b73a1e4
--- /dev/null
+++ b/Documentation/ABI/testing/configfs-thunderbolt_stream
@@ -0,0 +1,83 @@
+What: /sys/kernel/config/thunderbolt/stream/<xdomain>.<service>
+Date: Sep 2026
+KernelVersion: v7.2
+Contact: Mika Westerberg <mika.westerberg@linux.intel.com>
+Description:
+ Configuration group for a stream Thunderbolt/USB4
+ service. It is possible to create groups even if there
+ is no connection yet to the other host. Once a
+ connection established and there is stream service on
+ the remote side that matches, this configuration is
+ applied to it.
+
+ To find the service name you can run tblist from tbtools [1]:
+
+ # tblist -A
+ ...
+ Domain 0 Route 3 Index 0: stream
+
+ [1] https://github.com/intel/tbtools
+
+What: /sys/kernel/config/thunderbolt/stream/<xdomain>.<service>/$name
+Date: Sep 2026
+KernelVersion: v7.2
+Contact: Mika Westerberg <mika.westerberg@linux.intel.com>
+Description:
+ Creates new stream with $name and fills it with the
+ default values. If there is an advertised remote stream
+ with the same name, uses its values as the default.
+
+What: /sys/kernel/config/thunderbolt/stream/<xdomain>.<service>/$name/index
+Date: Sep 2026
+KernelVersion: v7.2
+Contact: Mika Westerberg <mika.westerberg@linux.intel.com>
+Description:
+ This matches the X in /dev/tbstreamX and allows userspace
+ to map the configfs directory to the corresponding character
+ device.
+
+What: /sys/kernel/config/thunderbolt/stream/<xdomain>.<service>/$name/in_hopid
+Date: Sep 2026
+KernelVersion: v7.2
+Contact: Mika Westerberg <mika.westerberg@linux.intel.com>
+Description:
+ In HopID used with the read path of the tunnel. Available HopIDs
+ for tunneling start from 8. You can pass also -1 for automatic
+ allocation. The allocated value can be read here. Writing 0 will
+ de-allocate if the stream is not in use.
+
+ To figure out the maximum HopID you can run tbget from
+ tbtools for the lane adapter. For example below we check
+ for lane adapter number 1 (first USB4 port):
+
+ # tbget -r 0 -a 1 -D ADP_CS_5.Max\ Input\ HopID
+ 19
+
+ This allows to use anything between 8 and 19 inclusive.
+
+What: /sys/kernel/config/thunderbolt/stream/<xdomain>.<service>/$name/out_hopid
+Date: Sep 2026
+KernelVersion: v7.2
+Contact: Mika Westerberg <mika.westerberg@linux.intel.com>
+Description:
+ Out HopID used with the write path of the tunnel. Available HopIDs
+ for tunneling start from 8. You can pass also -1 for automatic
+ allocation. The allocated value can be read here. Writing 0 will
+ de-allocate if the stream is not in use. See @in_hopid
+ for how to figure out the maximum HopID.
+
+What: /sys/kernel/config/thunderbolt/stream/<xdomain>.<service>/$name/ring_size
+Date: Sep 2026
+KernelVersion: v7.2
+Contact: Mika Westerberg <mika.westerberg@linux.intel.com>
+Description:
+ Size of the TX/RX rings. Can be adjusted between 32 and
+ 4096. The default is 256.
+
+What: /sys/kernel/config/thunderbolt/stream/<xdomain>.<service>/$name/throttling
+Date: Sep 2026
+KernelVersion: v7.2
+Contact: Mika Westerberg <mika.westerberg@linux.intel.com>
+Description:
+ Interrupt throttling rate in ns. Lower values can give
+ better latency. The default is 8192 ns.
diff --git a/Documentation/admin-guide/thunderbolt.rst b/Documentation/admin-guide/thunderbolt.rst
index 89df26553aa0..91a6cb109988 100644
--- a/Documentation/admin-guide/thunderbolt.rst
+++ b/Documentation/admin-guide/thunderbolt.rst
@@ -373,6 +373,67 @@ port which are named like ``thunderbolt0`` and so on. From this point
you can either use standard userspace tools like ``ip`` to
configure the interface or let your GUI handle it automatically.
+Streaming data directly over Thunderbolt cable
+----------------------------------------------
+In addition to Thunderbolt networking (aka. USB4NET) Linux supports
+streaming data directly over a cable as well (aka. USB4STREAM). This is
+possible through ``thunderbolt-stream`` driver.
+
+Similarly to ``thunderbolt-net`` you load the driver first on one end::
+
+ host1 # modprobe thunderbolt-stream
+
+Then you configure it via ``ConfigFS``::
+
+ host1 # cd /sys/kernel/config/thunderbolt/stream
+ host1 # mkdir -p 0-1.0/data
+ host1 # cd 0-1.0
+ host1 # echo -1 > data/in_hopid
+ host1 # echo -1 > data/out_hopid
+
+This information is automatically announced to the other side via
+XDomain properties so if you have cable connected the other side knows
+that there is a stream named ``data`` available and can configure it for
+you automatically::
+
+ host2 # cd /sys/kernel/config/thunderbolt/stream
+ host2 # mkdir -p 0-3.0/data
+
+Here we used auto-configuration but you can configure it manually too.
+In that case you need to fill ``in_hopid`` and ``out_hopid`` accordingly.
+If you set them to ``-1`` the next available HopID is used which is
+typically what we want.
+
+Once they are configured you can use ``/dev/tbstreamX`` on both sides to
+transfer data::
+
+ host2 # cat /dev/tbstream0
+ host1 # dmesg > /dev/tbstream0
+
+Once you are done with the stream you can remove them::
+
+ host2 # cd /sys/kernel/config/thunderbolt/stream
+ host2 # rmdir -p 0-1.0/data
+ host1 # cd /sys/kernel/config/thunderbolt/stream
+ host1 # rmdir -p 0-3.0/data
+
+Since streams are essentially files you can use any existing application
+that supports ``read(2)`` and ``write(2)`` in some form.
+
+It is possible to have more than one stream and you can have both stream
+and ``thunderbolt-net`` in use simultaneously. For example we can create
+two streams with name ``control`` and ``data`` like this::
+
+ host1 # cd /sys/kernel/config/thunderbolt/stream
+ host1 # mkdir 0-1.0
+ host1 # cd 0-1.0
+ host1 # mkdir control
+ host1 # mkdir data
+
+Then you have ``/dev/tbstream0`` for ``control`` and ``/dev/tbstream1``
+for ``data``. Before you can use them you need to configure them as
+shown above for the one stream case.
+
Forcing power
-------------
Many OEMs include a method that can be used to force the power of a
diff --git a/drivers/net/thunderbolt/main.c b/drivers/net/thunderbolt/main.c
index 7aae5d915a1e..f8f97e8e2226 100644
--- a/drivers/net/thunderbolt/main.c
+++ b/drivers/net/thunderbolt/main.c
@@ -34,11 +34,12 @@
#define TBNET_RING_SIZE 256
#define TBNET_LOGIN_RETRIES 60
#define TBNET_LOGOUT_RETRIES 10
+#define TBNET_THROTTLING 128000
#define TBNET_E2E BIT(0)
#define TBNET_MATCH_FRAGS_ID BIT(1)
#define TBNET_64K_FRAMES BIT(2)
#define TBNET_MAX_MTU SZ_64K
-#define TBNET_FRAME_SIZE SZ_4K
+#define TBNET_FRAME_SIZE TB_MAX_FRAME_SIZE
#define TBNET_MAX_PAYLOAD_SIZE \
(TBNET_FRAME_SIZE - sizeof(struct thunderbolt_ip_frame_header))
/* Rx packets need to hold space for skb_shared_info */
@@ -327,11 +328,6 @@ static void stop_login(struct tbnet *net)
netdev_dbg(net->dev, "login stopped\n");
}
-static inline unsigned int tbnet_frame_size(const struct tbnet_frame *tf)
-{
- return tf->frame.size ? : TBNET_FRAME_SIZE;
-}
-
static void tbnet_free_buffers(struct tbnet_ring *ring)
{
unsigned int i;
@@ -562,7 +558,7 @@ static struct tbnet_frame *tbnet_get_tx_buffer(struct tbnet *net)
tf->frame.size = 0;
dma_sync_single_for_cpu(dma_dev, tf->frame.buffer_phy,
- tbnet_frame_size(tf), DMA_TO_DEVICE);
+ tb_ring_frame_size(&tf->frame), DMA_TO_DEVICE);
return tf;
}
@@ -744,7 +740,7 @@ static bool tbnet_check_frame(struct tbnet *net, const struct tbnet_frame *tf,
}
/* Should be greater than just header i.e. contains data */
- size = tbnet_frame_size(tf);
+ size = tb_ring_frame_size(&tf->frame);
if (size <= sizeof(*hdr)) {
net->stats.rx_length_errors++;
return false;
@@ -961,6 +957,9 @@ static int tbnet_open(struct net_device *dev)
}
net->rx_ring.ring = ring;
+ tb_ring_throttling(net->tx_ring.ring, TBNET_THROTTLING);
+ tb_ring_throttling(net->rx_ring.ring, TBNET_THROTTLING);
+
napi_enable(&net->napi);
start_login(net);
@@ -1011,7 +1010,8 @@ static bool tbnet_xmit_csum_and_map(struct tbnet *net, struct sk_buff *skb,
hdr->frame_index, hdr->frame_count);
dma_sync_single_for_device(dma_dev,
frames[i]->frame.buffer_phy,
- tbnet_frame_size(frames[i]), DMA_TO_DEVICE);
+ tb_ring_frame_size(&frames[i]->frame),
+ DMA_TO_DEVICE);
}
return true;
@@ -1085,7 +1085,7 @@ static bool tbnet_xmit_csum_and_map(struct tbnet *net, struct sk_buff *skb,
*/
for (i = 0; i < frame_count; i++) {
dma_sync_single_for_device(dma_dev, frames[i]->frame.buffer_phy,
- tbnet_frame_size(frames[i]), DMA_TO_DEVICE);
+ tb_ring_frame_size(&frames[i]->frame), DMA_TO_DEVICE);
}
return true;
diff --git a/drivers/thunderbolt/Kconfig b/drivers/thunderbolt/Kconfig
index db3b0bef48f4..294b3227a545 100644
--- a/drivers/thunderbolt/Kconfig
+++ b/drivers/thunderbolt/Kconfig
@@ -18,6 +18,10 @@ menuconfig USB4
if USB4
+config USB4_CONFIGFS
+ def_tristate USB4
+ depends on CONFIGFS_FS && !(USB4=y && CONFIGFS_FS=m)
+
config USB4_DEBUGFS_WRITE
bool "Enable write by debugfs to configuration spaces (DANGEROUS)"
help
@@ -60,4 +64,15 @@ config USB4_DMA_TEST
To compile this driver a module, choose M here. The module will be
called thunderbolt_dma_test.
+config USB4_STREAM
+ tristate "Stream data over Thunderbolt/USB4 cable"
+ depends on USB4_CONFIGFS
+ help
+ This adds support for USB4STREAM protocol that allows two
+ hosts to stream data directly over Thunderbolt/USB4 cable
+ through /dev/tbstreamX devices.
+
+ To compile this driver a module, choose M here. The module will be
+ called thunderbolt_stream.
+
endif # USB4
diff --git a/drivers/thunderbolt/Makefile b/drivers/thunderbolt/Makefile
index b44b32dcb832..beb054c3126b 100644
--- a/drivers/thunderbolt/Makefile
+++ b/drivers/thunderbolt/Makefile
@@ -1,14 +1,18 @@
# SPDX-License-Identifier: GPL-2.0-only
ccflags-y := -I$(src)
obj-${CONFIG_USB4} := thunderbolt.o
-thunderbolt-objs := nhi.o nhi_ops.o ctl.o tb.o switch.o cap.o path.o tunnel.o eeprom.o
+thunderbolt-objs := nhi.o ctl.o tb.o switch.o cap.o pci.o path.o tunnel.o eeprom.o
thunderbolt-objs += domain.o dma_port.o icm.o property.o xdomain.o lc.o tmu.o usb4.o
thunderbolt-objs += usb4_port.o nvm.o retimer.o quirks.o clx.o
thunderbolt-${CONFIG_ACPI} += acpi.o
thunderbolt-$(CONFIG_DEBUG_FS) += debugfs.o
+thunderbolt-$(CONFIG_USB4_CONFIGFS) += configfs.o
thunderbolt-${CONFIG_USB4_KUNIT_TEST} += test.o
CFLAGS_test.o += $(DISABLE_STRUCTLEAK_PLUGIN)
thunderbolt_dma_test-${CONFIG_USB4_DMA_TEST} += dma_test.o
obj-$(CONFIG_USB4_DMA_TEST) += thunderbolt_dma_test.o
+
+thunderbolt_stream-${CONFIG_USB4_STREAM} += stream.o
+obj-$(CONFIG_USB4_STREAM) += thunderbolt_stream.o
diff --git a/drivers/thunderbolt/acpi.c b/drivers/thunderbolt/acpi.c
index 45d1415871b4..53546bc477a5 100644
--- a/drivers/thunderbolt/acpi.c
+++ b/drivers/thunderbolt/acpi.c
@@ -28,7 +28,7 @@ static acpi_status tb_acpi_add_link(acpi_handle handle, u32 level, void *data,
return AE_OK;
/* It needs to reference this NHI */
- if (dev_fwnode(&nhi->pdev->dev) != fwnode)
+ if (dev_fwnode(nhi->dev) != fwnode)
goto out_put;
/*
@@ -57,16 +57,16 @@ static acpi_status tb_acpi_add_link(acpi_handle handle, u32 level, void *data,
*/
pm_runtime_get_sync(&pdev->dev);
- link = device_link_add(&pdev->dev, &nhi->pdev->dev,
+ link = device_link_add(&pdev->dev, nhi->dev,
DL_FLAG_AUTOREMOVE_SUPPLIER |
DL_FLAG_RPM_ACTIVE |
DL_FLAG_PM_RUNTIME);
if (link) {
- dev_dbg(&nhi->pdev->dev, "created link from %s\n",
+ dev_dbg(nhi->dev, "created link from %s\n",
dev_name(&pdev->dev));
*(bool *)ret = true;
} else {
- dev_warn(&nhi->pdev->dev, "device link creation from %s failed\n",
+ dev_warn(nhi->dev, "device link creation from %s failed\n",
dev_name(&pdev->dev));
}
@@ -93,7 +93,7 @@ bool tb_acpi_add_links(struct tb_nhi *nhi)
acpi_status status;
bool ret = false;
- if (!has_acpi_companion(&nhi->pdev->dev))
+ if (!has_acpi_companion(nhi->dev))
return false;
/*
@@ -103,7 +103,7 @@ bool tb_acpi_add_links(struct tb_nhi *nhi)
status = acpi_walk_namespace(ACPI_TYPE_DEVICE, ACPI_ROOT_OBJECT, 32,
tb_acpi_add_link, NULL, nhi, (void **)&ret);
if (ACPI_FAILURE(status)) {
- dev_warn(&nhi->pdev->dev, "failed to enumerate tunneled ports\n");
+ dev_warn(nhi->dev, "failed to enumerate tunneled ports\n");
return false;
}
@@ -305,7 +305,7 @@ static struct acpi_device *tb_acpi_switch_find_companion(struct tb_switch *sw)
struct tb_nhi *nhi = sw->tb->nhi;
struct acpi_device *parent_adev;
- parent_adev = ACPI_COMPANION(&nhi->pdev->dev);
+ parent_adev = ACPI_COMPANION(nhi->dev);
if (parent_adev)
adev = acpi_find_child_device(parent_adev, 0, false);
}
diff --git a/drivers/thunderbolt/configfs.c b/drivers/thunderbolt/configfs.c
new file mode 100644
index 000000000000..dc6bc363dfe8
--- /dev/null
+++ b/drivers/thunderbolt/configfs.c
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ConfigFS support
+ *
+ * Copyright (C) 2026, Intel Corporation
+ * Author: Mika Westerberg <mika.westerberg@linux.intel.com>
+ */
+
+#include <linux/configfs.h>
+#include <linux/export.h>
+
+#include "tb.h"
+
+static const struct config_item_type tb_root_group_type = {
+ .ct_owner = THIS_MODULE,
+};
+
+static struct configfs_subsystem tb_configfs = {
+ .su_group = {
+ .cg_item = {
+ .ci_namebuf = "thunderbolt",
+ .ci_type = &tb_root_group_type,
+ },
+ },
+};
+
+/**
+ * tb_configfs_register_group() - Register Thunderbolt ConfigFS group
+ * @group: Group to register.
+ *
+ * Registers the new @group under Thunderbolt subsystem ConfigFS.
+ *
+ * Return: 0% in case of success, negative errno otherwise.
+ */
+int tb_configfs_register_group(struct config_group *group)
+{
+ return configfs_register_group(&tb_configfs.su_group, group);
+}
+EXPORT_SYMBOL_GPL(tb_configfs_register_group);
+
+/**
+ * tb_configfs_unregister_group() - Unregister previously registered group
+ * @group: Group to unregister.
+ */
+void tb_configfs_unregister_group(struct config_group *group)
+{
+ configfs_unregister_group(group);
+}
+EXPORT_SYMBOL_GPL(tb_configfs_unregister_group);
+
+int tb_configfs_init(void)
+{
+ config_group_init(&tb_configfs.su_group);
+ mutex_init(&tb_configfs.su_mutex);
+ return configfs_register_subsystem(&tb_configfs);
+}
+
+void tb_configfs_exit(void)
+{
+ configfs_unregister_subsystem(&tb_configfs);
+}
diff --git a/drivers/thunderbolt/ctl.c b/drivers/thunderbolt/ctl.c
index b2fd60fc7bcc..cd47b627f97b 100644
--- a/drivers/thunderbolt/ctl.c
+++ b/drivers/thunderbolt/ctl.c
@@ -56,22 +56,22 @@ struct tb_ctl {
#define tb_ctl_WARN(ctl, format, arg...) \
- dev_WARN(&(ctl)->nhi->pdev->dev, format, ## arg)
+ dev_WARN((ctl)->nhi->dev, format, ## arg)
#define tb_ctl_err(ctl, format, arg...) \
- dev_err(&(ctl)->nhi->pdev->dev, format, ## arg)
+ dev_err((ctl)->nhi->dev, format, ## arg)
#define tb_ctl_warn(ctl, format, arg...) \
- dev_warn(&(ctl)->nhi->pdev->dev, format, ## arg)
+ dev_warn((ctl)->nhi->dev, format, ## arg)
#define tb_ctl_info(ctl, format, arg...) \
- dev_info(&(ctl)->nhi->pdev->dev, format, ## arg)
+ dev_info((ctl)->nhi->dev, format, ## arg)
#define tb_ctl_dbg(ctl, format, arg...) \
- dev_dbg(&(ctl)->nhi->pdev->dev, format, ## arg)
+ dev_dbg((ctl)->nhi->dev, format, ## arg)
#define tb_ctl_dbg_once(ctl, format, arg...) \
- dev_dbg_once(&(ctl)->nhi->pdev->dev, format, ## arg)
+ dev_dbg_once((ctl)->nhi->dev, format, ## arg)
static DECLARE_WAIT_QUEUE_HEAD(tb_cfg_request_cancel_queue);
/* Serializes access to request kref_get/put */
@@ -666,8 +666,8 @@ struct tb_ctl *tb_ctl_alloc(struct tb_nhi *nhi, int index, int timeout_msec,
mutex_init(&ctl->request_queue_lock);
INIT_LIST_HEAD(&ctl->request_queue);
- ctl->frame_pool = dma_pool_create("thunderbolt_ctl", &nhi->pdev->dev,
- TB_FRAME_SIZE, 4, 0);
+ ctl->frame_pool = dma_pool_create("thunderbolt_ctl", nhi->dev,
+ TB_FRAME_SIZE, 4, 0);
if (!ctl->frame_pool)
goto err;
diff --git a/drivers/thunderbolt/debugfs.c b/drivers/thunderbolt/debugfs.c
index 042f6a0d0f7f..f5cf0e177f40 100644
--- a/drivers/thunderbolt/debugfs.c
+++ b/drivers/thunderbolt/debugfs.c
@@ -366,7 +366,7 @@ static ssize_t sb_regs_write(struct tb_port *port, const struct sb_reg *sb_regs,
if (!sb_reg)
return -EINVAL;
- if (bytes_read > sb_regs->size)
+ if (bytes_read > sb_reg->size)
return -E2BIG;
ret = usb4_port_sb_write(port, target, index, sb_reg->reg, data,
@@ -956,7 +956,9 @@ margining_error_counter_write(struct file *file, const char __user *user_buf,
else if (!strcmp(buf, "stop"))
error_counter = USB4_MARGIN_SW_ERROR_COUNTER_STOP;
else
- return -EINVAL;
+ goto err_free;
+
+ free_page((unsigned long)buf);
scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &tb->lock) {
if (!margining->software)
@@ -966,6 +968,10 @@ margining_error_counter_write(struct file *file, const char __user *user_buf,
}
return count;
+
+err_free:
+ free_page((unsigned long)buf);
+ return -EINVAL;
}
static int margining_error_counter_show(struct seq_file *s, void *not_used)
@@ -1780,6 +1786,8 @@ static void margining_port_remove(struct tb_port *port)
if (!port->usb4)
return;
+ if (!port->usb4->margining)
+ return;
snprintf(dir_name, sizeof(dir_name), "port%d", port->port);
parent = debugfs_lookup(dir_name, port->sw->debugfs_dir);
@@ -2361,8 +2369,10 @@ static int sb_regs_show(struct tb_port *port, const struct sb_reg *sb_regs,
memset(data, 0, sizeof(data));
ret = usb4_port_sb_read(port, target, index, regs->reg, data,
regs->size);
- if (ret)
- return ret;
+ if (ret) {
+ seq_printf(s, "0x%02x <not accessible>\n", regs->reg);
+ continue;
+ }
seq_printf(s, "0x%02x", regs->reg);
for (j = 0; j < regs->size; j++)
diff --git a/drivers/thunderbolt/dma_test.c b/drivers/thunderbolt/dma_test.c
index b4aa79d482a0..7877319b1b03 100644
--- a/drivers/thunderbolt/dma_test.c
+++ b/drivers/thunderbolt/dma_test.c
@@ -87,7 +87,6 @@ static const char * const dma_test_result_names[] = {
* @error_code: Error code of the last run
* @complete: Used to wait for the Rx to complete
* @lock: Lock serializing access to this structure
- * @debugfs_dir: dentry of this dma_test
*/
struct dma_test {
const struct tb_service *svc;
@@ -108,7 +107,6 @@ struct dma_test {
enum dma_test_test_error error_code;
struct completion complete;
struct mutex lock;
- struct dentry *debugfs_dir;
};
/* DMA test property directory UUID: 3188cd10-6523-4a5a-a682-fdca07a248d8 */
@@ -157,6 +155,8 @@ static int dma_test_start_rings(struct dma_test *dt)
dt->tx_ring = ring;
e2e_tx_hop = ring->hop;
+ tb_ring_throttling(ring, 128000);
+
ret = tb_xdomain_alloc_out_hopid(xd, -1);
if (ret < 0) {
dma_test_free_rings(dt);
@@ -164,6 +164,7 @@ static int dma_test_start_rings(struct dma_test *dt)
}
dt->tx_hopid = ret;
+
}
if (dt->packets_to_receive) {
@@ -182,6 +183,8 @@ static int dma_test_start_rings(struct dma_test *dt)
dt->rx_ring = ring;
+ tb_ring_throttling(ring, 128000);
+
ret = tb_xdomain_alloc_in_hopid(xd, -1);
if (ret < 0) {
dma_test_free_rings(dt);
@@ -619,18 +622,18 @@ DEFINE_SHOW_ATTRIBUTE(status);
static void dma_test_debugfs_init(struct tb_service *svc)
{
- struct dma_test *dt = tb_service_get_drvdata(svc);
+ struct dentry *debugfs_dir;
- dt->debugfs_dir = debugfs_create_dir("dma_test", svc->debugfs_dir);
+ debugfs_dir = debugfs_create_dir("dma_test", svc->debugfs_dir);
- debugfs_create_file("lanes", 0600, dt->debugfs_dir, svc, &lanes_fops);
- debugfs_create_file("speed", 0600, dt->debugfs_dir, svc, &speed_fops);
- debugfs_create_file("packets_to_receive", 0600, dt->debugfs_dir, svc,
+ debugfs_create_file("lanes", 0600, debugfs_dir, svc, &lanes_fops);
+ debugfs_create_file("speed", 0600, debugfs_dir, svc, &speed_fops);
+ debugfs_create_file("packets_to_receive", 0600, debugfs_dir, svc,
&packets_to_receive_fops);
- debugfs_create_file("packets_to_send", 0600, dt->debugfs_dir, svc,
+ debugfs_create_file("packets_to_send", 0600, debugfs_dir, svc,
&packets_to_send_fops);
- debugfs_create_file("status", 0400, dt->debugfs_dir, svc, &status_fops);
- debugfs_create_file("test", 0200, dt->debugfs_dir, svc, &test_fops);
+ debugfs_create_file("status", 0400, debugfs_dir, svc, &status_fops);
+ debugfs_create_file("test", 0200, debugfs_dir, svc, &test_fops);
}
static int dma_test_probe(struct tb_service *svc, const struct tb_service_id *id)
@@ -658,7 +661,7 @@ static void dma_test_remove(struct tb_service *svc)
struct dma_test *dt = tb_service_get_drvdata(svc);
mutex_lock(&dt->lock);
- debugfs_remove_recursive(dt->debugfs_dir);
+ debugfs_lookup_and_remove("dma_test", svc->debugfs_dir);
mutex_unlock(&dt->lock);
}
diff --git a/drivers/thunderbolt/domain.c b/drivers/thunderbolt/domain.c
index 317780b99992..479fa4d265c2 100644
--- a/drivers/thunderbolt/domain.c
+++ b/drivers/thunderbolt/domain.c
@@ -319,12 +319,15 @@ const struct bus_type tb_bus_type = {
static void tb_domain_release(struct device *dev)
{
struct tb *tb = container_of(dev, struct tb, dev);
+ struct tb_nhi *nhi = tb->nhi;
tb_ctl_free(tb->ctl);
destroy_workqueue(tb->wq);
ida_free(&tb_domain_ida, tb->index);
mutex_destroy(&tb->lock);
kfree(tb);
+
+ complete(&nhi->domain_released);
}
const struct device_type tb_domain_type = {
@@ -402,7 +405,7 @@ struct tb *tb_domain_alloc(struct tb_nhi *nhi, int timeout_msec, size_t privsize
if (!tb->ctl)
goto err_destroy_wq;
- tb->dev.parent = &nhi->pdev->dev;
+ tb->dev.parent = nhi->dev;
tb->dev.bus = &tb_bus_type;
tb->dev.type = &tb_domain_type;
tb->dev.groups = domain_attr_groups;
@@ -850,10 +853,41 @@ int tb_domain_disconnect_all_paths(struct tb *tb)
return bus_for_each_dev(&tb_bus_type, NULL, tb, disconnect_xdomain);
}
+struct unregister_context {
+ const struct tb *tb;
+ int n;
+};
+
+static int unregister_unplugged_xdomain(struct device *dev, void *data)
+{
+ struct unregister_context *ctx = data;
+ struct tb_xdomain *xd;
+
+ xd = tb_to_xdomain(dev);
+ if (xd && xd->tb == ctx->tb && xd->is_unplugged) {
+ tb_xdomain_unregister(xd);
+ ctx->n++;
+ }
+ return 0;
+}
+
+int tb_domain_unregister_unplugged_xdomains(struct tb *tb)
+{
+ struct unregister_context ctx;
+
+ ctx.tb = tb_domain_get(tb);
+ ctx.n = 0;
+ bus_for_each_dev(&tb_bus_type, NULL, &ctx, unregister_unplugged_xdomain);
+ tb_domain_put(tb);
+
+ return ctx.n;
+}
+
int tb_domain_init(void)
{
int ret;
+ tb_configfs_init();
tb_debugfs_init();
tb_acpi_init();
@@ -883,4 +917,5 @@ void tb_domain_exit(void)
tb_xdomain_exit();
tb_acpi_exit();
tb_debugfs_exit();
+ tb_configfs_exit();
}
diff --git a/drivers/thunderbolt/eeprom.c b/drivers/thunderbolt/eeprom.c
index 5477b9437048..5681c17f82ec 100644
--- a/drivers/thunderbolt/eeprom.c
+++ b/drivers/thunderbolt/eeprom.c
@@ -465,7 +465,7 @@ static void tb_switch_drom_free(struct tb_switch *sw)
*/
static int tb_drom_copy_efi(struct tb_switch *sw, u16 *size)
{
- struct device *dev = &sw->tb->nhi->pdev->dev;
+ struct device *dev = sw->tb->nhi->dev;
int len, res;
len = device_property_count_u8(dev, "ThunderboltDROM");
diff --git a/drivers/thunderbolt/icm.c b/drivers/thunderbolt/icm.c
index 9d95bf3ab44c..10fefac3b1d9 100644
--- a/drivers/thunderbolt/icm.c
+++ b/drivers/thunderbolt/icm.c
@@ -587,6 +587,11 @@ static int icm_fr_approve_xdomain_paths(struct tb *tb, struct tb_xdomain *xd,
struct icm_fr_pkg_approve_xdomain request;
int ret;
+ if (atomic_read(&xd->ntunnels) >= 1) {
+ tb_warn(tb, "only one tunnel is supported by the firmware\n");
+ return -EOPNOTSUPP;
+ }
+
memset(&request, 0, sizeof(request));
request.hdr.code = ICM_APPROVE_XDOMAIN;
request.link_info = xd->depth << ICM_LINK_INFO_DEPTH_SHIFT | xd->link;
@@ -738,6 +743,7 @@ static void remove_xdomain(struct tb_xdomain *xd)
sw = tb_to_switch(xd->dev.parent);
tb_port_at(xd->route, sw)->xdomain = NULL;
+ xd->is_unplugged = true;
tb_xdomain_remove(xd);
}
@@ -1157,6 +1163,11 @@ static int icm_tr_approve_xdomain_paths(struct tb *tb, struct tb_xdomain *xd,
struct icm_tr_pkg_approve_xdomain request;
int ret;
+ if (atomic_read(&xd->ntunnels) >= 1) {
+ tb_warn(tb, "only one tunnel is supported by the firmware\n");
+ return -EOPNOTSUPP;
+ }
+
memset(&request, 0, sizeof(request));
request.hdr.code = ICM_APPROVE_XDOMAIN;
request.route_hi = upper_32_bits(xd->route);
@@ -1455,6 +1466,7 @@ static struct pci_dev *get_upstream_port(struct pci_dev *pdev)
static bool icm_ar_is_supported(struct tb *tb)
{
+ struct pci_dev *pdev = to_pci_dev(tb->nhi->dev);
struct pci_dev *upstream_port;
struct icm *icm = tb_priv(tb);
@@ -1472,7 +1484,7 @@ static bool icm_ar_is_supported(struct tb *tb)
* Find the upstream PCIe port in case we need to do reset
* through its vendor specific registers.
*/
- upstream_port = get_upstream_port(tb->nhi->pdev);
+ upstream_port = get_upstream_port(pdev);
if (upstream_port) {
int cap;
@@ -1508,7 +1520,7 @@ static int icm_ar_get_mode(struct tb *tb)
} while (--retries);
if (!retries) {
- dev_err(&nhi->pdev->dev, "ICM firmware not authenticated\n");
+ dev_err(nhi->dev, "ICM firmware not authenticated\n");
return -ENODEV;
}
@@ -1674,11 +1686,11 @@ icm_icl_driver_ready(struct tb *tb, enum tb_security_level *security_level,
static void icm_icl_set_uuid(struct tb *tb)
{
- struct tb_nhi *nhi = tb->nhi;
+ struct pci_dev *pdev = to_pci_dev(tb->nhi->dev);
u32 uuid[4];
- pci_read_config_dword(nhi->pdev, VS_CAP_10, &uuid[0]);
- pci_read_config_dword(nhi->pdev, VS_CAP_11, &uuid[1]);
+ pci_read_config_dword(pdev, VS_CAP_10, &uuid[0]);
+ pci_read_config_dword(pdev, VS_CAP_11, &uuid[1]);
uuid[2] = 0xffffffff;
uuid[3] = 0xffffffff;
@@ -1762,6 +1774,8 @@ static void icm_handle_notification(struct work_struct *work)
kfree(n->pkg);
kfree(n);
+
+ tb_domain_unregister_unplugged_xdomains(tb);
}
static void icm_handle_event(struct tb *tb, enum tb_cfg_pkg_type type,
@@ -1853,7 +1867,7 @@ static int icm_firmware_start(struct tb *tb, struct tb_nhi *nhi)
if (icm_firmware_running(nhi))
return 0;
- dev_dbg(&nhi->pdev->dev, "starting ICM firmware\n");
+ dev_dbg(nhi->dev, "starting ICM firmware\n");
ret = icm_firmware_reset(tb, nhi);
if (ret)
@@ -1948,7 +1962,7 @@ static int icm_firmware_init(struct tb *tb)
ret = icm_firmware_start(tb, nhi);
if (ret) {
- dev_err(&nhi->pdev->dev, "could not start ICM firmware\n");
+ dev_err(nhi->dev, "could not start ICM firmware\n");
return ret;
}
@@ -1980,10 +1994,10 @@ static int icm_firmware_init(struct tb *tb)
*/
ret = icm_reset_phy_port(tb, 0);
if (ret)
- dev_warn(&nhi->pdev->dev, "failed to reset links on port0\n");
+ dev_warn(nhi->dev, "failed to reset links on port0\n");
ret = icm_reset_phy_port(tb, 1);
if (ret)
- dev_warn(&nhi->pdev->dev, "failed to reset links on port1\n");
+ dev_warn(nhi->dev, "failed to reset links on port1\n");
return 0;
}
@@ -2112,6 +2126,8 @@ static void icm_rescan_work(struct work_struct *work)
if (tb->root_switch)
icm_free_unplugged_children(tb->root_switch);
mutex_unlock(&tb->lock);
+
+ tb_domain_unregister_unplugged_xdomains(tb);
}
static void icm_complete(struct tb *tb)
@@ -2462,6 +2478,7 @@ static const struct tb_cm_ops icm_icl_ops = {
struct tb *icm_probe(struct tb_nhi *nhi)
{
+ struct pci_dev *pdev = to_pci_dev(nhi->dev);
struct icm *icm;
struct tb *tb;
@@ -2473,7 +2490,7 @@ struct tb *icm_probe(struct tb_nhi *nhi)
INIT_DELAYED_WORK(&icm->rescan_work, icm_rescan_work);
mutex_init(&icm->request_lock);
- switch (nhi->pdev->device) {
+ switch (pdev->device) {
case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI:
case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_4C_NHI:
icm->can_upgrade_nvm = true;
@@ -2579,7 +2596,7 @@ struct tb *icm_probe(struct tb_nhi *nhi)
}
if (!icm->is_supported || !icm->is_supported(tb)) {
- dev_dbg(&nhi->pdev->dev, "ICM not supported on this controller\n");
+ dev_dbg(nhi->dev, "ICM not supported on this controller\n");
tb_domain_put(tb);
return NULL;
}
diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c
index 2bb2e79ca3cb..0f795ea58756 100644
--- a/drivers/thunderbolt/nhi.c
+++ b/drivers/thunderbolt/nhi.c
@@ -2,7 +2,7 @@
/*
* Thunderbolt driver - NHI driver
*
- * The NHI (native host interface) is the pci device that allows us to send and
+ * The NHI (native host interface) is the device that allows us to send and
* receive frames from the thunderbolt bus.
*
* Copyright (c) 2014 Andreas Noever <andreas.noever@gmail.com>
@@ -12,7 +12,6 @@
#include <linux/pm_runtime.h>
#include <linux/slab.h>
#include <linux/errno.h>
-#include <linux/pci.h>
#include <linux/dma-mapping.h>
#include <linux/interrupt.h>
#include <linux/iommu.h>
@@ -34,19 +33,9 @@
* transferred.
*/
#define RING_E2E_RESERVED_HOPID RING_FIRST_USABLE_HOPID
-/*
- * Minimal number of vectors when we use MSI-X. Two for control channel
- * Rx/Tx and the rest four are for cross domain DMA paths.
- */
-#define MSIX_MIN_VECS 6
-#define MSIX_MAX_VECS 16
#define NHI_MAILBOX_TIMEOUT 500 /* ms */
-/* Host interface quirks */
-#define QUIRK_AUTO_CLEAR_INT BIT(0)
-#define QUIRK_E2E BIT(1)
-
static bool host_reset = true;
module_param(host_reset, bool, 0444);
MODULE_PARM_DESC(host_reset, "reset USB4 host router (default: true)");
@@ -93,7 +82,7 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active)
u32 old, new;
if (ring->irq > 0) {
- u32 step, shift, ivr, misc;
+ u32 step, shift, ivr, misc, itr;
void __iomem *ivr_base;
int auto_clear_bit;
int index;
@@ -131,6 +120,12 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active)
if (active)
ivr |= ring->vector << shift;
iowrite32(ivr, ivr_base + step);
+
+ /* Throttling is specified in 256ns increments */
+ itr = DIV_ROUND_UP(ring->interval_nsec, 256);
+ itr &= REG_INT_THROTTLING_RATE_INTERVAL_MASK;
+ iowrite32(itr, ring->nhi->iobase + REG_INT_THROTTLING_RATE +
+ ring->vector * 4);
}
old = ioread32(ring->nhi->iobase + reg);
@@ -139,15 +134,14 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active)
else
new = old & ~mask;
- dev_dbg(&ring->nhi->pdev->dev,
+ dev_dbg(ring->nhi->dev,
"%s interrupt at register %#x bit %d (%#x -> %#x)\n",
active ? "enabling" : "disabling", reg, interrupt_bit, old, new);
if (new == old)
- dev_WARN(&ring->nhi->pdev->dev,
- "interrupt for %s %d is already %s\n",
- RING_TYPE(ring), ring->hop,
- str_enabled_disabled(active));
+ dev_WARN(ring->nhi->dev, "interrupt for %s %d is already %s\n",
+ RING_TYPE(ring), ring->hop,
+ str_enabled_disabled(active));
if (active)
iowrite32(new, ring->nhi->iobase + reg);
@@ -160,7 +154,7 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active)
*
* Use only during init and shutdown.
*/
-static void nhi_disable_interrupts(struct tb_nhi *nhi)
+void nhi_disable_interrupts(struct tb_nhi *nhi)
{
int i = 0;
/* disable interrupts */
@@ -319,6 +313,8 @@ invoke_callback:
if (frame->callback)
frame->callback(ring, frame, canceled);
}
+
+ wake_up(&ring->wait);
}
int __tb_ring_enqueue(struct tb_ring *ring, struct ring_frame *frame)
@@ -445,7 +441,7 @@ static void ring_clear_msix(const struct tb_ring *ring)
4 * (ring->nhi->hop_count / 32));
}
-static irqreturn_t ring_msix(int irq, void *data)
+irqreturn_t ring_msix(int irq, void *data)
{
struct tb_ring *ring = data;
@@ -459,51 +455,6 @@ static irqreturn_t ring_msix(int irq, void *data)
return IRQ_HANDLED;
}
-static int ring_request_msix(struct tb_ring *ring, bool no_suspend)
-{
- struct tb_nhi *nhi = ring->nhi;
- unsigned long irqflags;
- int ret;
-
- if (!nhi->pdev->msix_enabled)
- return 0;
-
- ret = ida_alloc_max(&nhi->msix_ida, MSIX_MAX_VECS - 1, GFP_KERNEL);
- if (ret < 0)
- return ret;
-
- ring->vector = ret;
-
- ret = pci_irq_vector(ring->nhi->pdev, ring->vector);
- if (ret < 0)
- goto err_ida_remove;
-
- ring->irq = ret;
-
- irqflags = no_suspend ? IRQF_NO_SUSPEND : 0;
- ret = request_irq(ring->irq, ring_msix, irqflags, "thunderbolt", ring);
- if (ret)
- goto err_ida_remove;
-
- return 0;
-
-err_ida_remove:
- ida_free(&nhi->msix_ida, ring->vector);
-
- return ret;
-}
-
-static void ring_release_msix(struct tb_ring *ring)
-{
- if (ring->irq <= 0)
- return;
-
- free_irq(ring->irq, ring);
- ida_free(&ring->nhi->msix_ida, ring->vector);
- ring->vector = 0;
- ring->irq = 0;
-}
-
static int nhi_alloc_hop(struct tb_nhi *nhi, struct tb_ring *ring)
{
unsigned int start_hop = RING_FIRST_USABLE_HOPID;
@@ -512,7 +463,7 @@ static int nhi_alloc_hop(struct tb_nhi *nhi, struct tb_ring *ring)
if (nhi->quirks & QUIRK_E2E) {
start_hop = RING_FIRST_USABLE_HOPID + 1;
if (ring->flags & RING_FLAG_E2E && !ring->is_tx) {
- dev_dbg(&nhi->pdev->dev, "quirking E2E TX HopID %u -> %u\n",
+ dev_dbg(nhi->dev, "quirking E2E TX HopID %u -> %u\n",
ring->e2e_tx_hop, RING_E2E_RESERVED_HOPID);
ring->e2e_tx_hop = RING_E2E_RESERVED_HOPID;
}
@@ -543,23 +494,23 @@ static int nhi_alloc_hop(struct tb_nhi *nhi, struct tb_ring *ring)
}
if (ring->hop > 0 && ring->hop < start_hop) {
- dev_warn(&nhi->pdev->dev, "invalid hop: %d\n", ring->hop);
+ dev_warn(nhi->dev, "invalid hop: %d\n", ring->hop);
ret = -EINVAL;
goto err_unlock;
}
if (ring->hop < 0 || ring->hop >= nhi->hop_count) {
- dev_warn(&nhi->pdev->dev, "invalid hop: %d\n", ring->hop);
+ dev_warn(nhi->dev, "invalid hop: %d\n", ring->hop);
ret = -EINVAL;
goto err_unlock;
}
if (ring->is_tx && nhi->tx_rings[ring->hop]) {
- dev_warn(&nhi->pdev->dev, "TX hop %d already allocated\n",
+ dev_warn(nhi->dev, "TX hop %d already allocated\n",
ring->hop);
ret = -EBUSY;
goto err_unlock;
}
if (!ring->is_tx && nhi->rx_rings[ring->hop]) {
- dev_warn(&nhi->pdev->dev, "RX hop %d already allocated\n",
+ dev_warn(nhi->dev, "RX hop %d already allocated\n",
ring->hop);
ret = -EBUSY;
goto err_unlock;
@@ -584,7 +535,7 @@ static struct tb_ring *tb_ring_alloc(struct tb_nhi *nhi, u32 hop, int size,
{
struct tb_ring *ring = NULL;
- dev_dbg(&nhi->pdev->dev, "allocating %s ring %d of size %d\n",
+ dev_dbg(nhi->dev, "allocating %s ring %d of size %d\n",
transmit ? "TX" : "RX", hop, size);
ring = kzalloc_obj(*ring);
@@ -595,6 +546,7 @@ static struct tb_ring *tb_ring_alloc(struct tb_nhi *nhi, u32 hop, int size,
INIT_LIST_HEAD(&ring->queue);
INIT_LIST_HEAD(&ring->in_flight);
INIT_WORK(&ring->work, ring_work);
+ init_waitqueue_head(&ring->wait);
ring->nhi = nhi;
ring->hop = hop;
@@ -610,14 +562,16 @@ static struct tb_ring *tb_ring_alloc(struct tb_nhi *nhi, u32 hop, int size,
ring->start_poll = start_poll;
ring->poll_data = poll_data;
- ring->descriptors = dma_alloc_coherent(&ring->nhi->pdev->dev,
- size * sizeof(*ring->descriptors),
- &ring->descriptors_dma, GFP_KERNEL | __GFP_ZERO);
+ ring->descriptors = dma_alloc_coherent(ring->nhi->dev,
+ size * sizeof(*ring->descriptors),
+ &ring->descriptors_dma, GFP_KERNEL | __GFP_ZERO);
if (!ring->descriptors)
goto err_free_ring;
- if (ring_request_msix(ring, flags & RING_FLAG_NO_SUSPEND))
- goto err_free_descs;
+ if (nhi->ops->request_ring_irq) {
+ if (nhi->ops->request_ring_irq(ring, flags & RING_FLAG_NO_SUSPEND))
+ goto err_free_descs;
+ }
if (nhi_alloc_hop(nhi, ring))
goto err_release_msix;
@@ -625,9 +579,10 @@ static struct tb_ring *tb_ring_alloc(struct tb_nhi *nhi, u32 hop, int size,
return ring;
err_release_msix:
- ring_release_msix(ring);
+ if (nhi->ops->release_ring_irq)
+ nhi->ops->release_ring_irq(ring);
err_free_descs:
- dma_free_coherent(&ring->nhi->pdev->dev,
+ dma_free_coherent(ring->nhi->dev,
ring->size * sizeof(*ring->descriptors),
ring->descriptors, ring->descriptors_dma);
err_free_ring:
@@ -694,10 +649,10 @@ void tb_ring_start(struct tb_ring *ring)
if (ring->nhi->going_away)
goto err;
if (ring->running) {
- dev_WARN(&ring->nhi->pdev->dev, "ring already started\n");
+ dev_WARN(ring->nhi->dev, "ring already started\n");
goto err;
}
- dev_dbg(&ring->nhi->pdev->dev, "starting %s %d\n",
+ dev_dbg(ring->nhi->dev, "starting %s %d\n",
RING_TYPE(ring), ring->hop);
if (ring->flags & RING_FLAG_FRAME) {
@@ -734,11 +689,11 @@ void tb_ring_start(struct tb_ring *ring)
hop &= REG_RX_OPTIONS_E2E_HOP_MASK;
flags |= hop;
- dev_dbg(&ring->nhi->pdev->dev,
+ dev_dbg(ring->nhi->dev,
"enabling E2E for %s %d with TX HopID %d\n",
RING_TYPE(ring), ring->hop, ring->e2e_tx_hop);
} else {
- dev_dbg(&ring->nhi->pdev->dev, "enabling E2E for %s %d\n",
+ dev_dbg(ring->nhi->dev, "enabling E2E for %s %d\n",
RING_TYPE(ring), ring->hop);
}
@@ -754,6 +709,31 @@ err:
}
EXPORT_SYMBOL_GPL(tb_ring_start);
+static bool tb_ring_empty(struct tb_ring *ring)
+{
+ guard(spinlock_irqsave)(&ring->lock);
+ return list_empty(&ring->in_flight);
+}
+
+/**
+ * tb_ring_flush() - Waits for a ring to be empty
+ * @ring: Ring to wait
+ * @timeout_msec: Timeout in ms how long to wait.
+ *
+ * This can be called before stopping a ring to make sure all the frames
+ * submitted prior have been completed.
+ *
+ * Return: %true if the ring is empty now, %false otherwise.
+ */
+bool tb_ring_flush(struct tb_ring *ring, unsigned int timeout_msec)
+{
+ if (!wait_event_timeout(ring->wait, tb_ring_empty(ring),
+ msecs_to_jiffies(timeout_msec)))
+ return false;
+ return tb_ring_empty(ring);
+}
+EXPORT_SYMBOL_GPL(tb_ring_flush);
+
/**
* tb_ring_stop() - shutdown a ring
* @ring: Ring to stop
@@ -772,12 +752,12 @@ void tb_ring_stop(struct tb_ring *ring)
{
spin_lock_irq(&ring->nhi->lock);
spin_lock(&ring->lock);
- dev_dbg(&ring->nhi->pdev->dev, "stopping %s %d\n",
+ dev_dbg(ring->nhi->dev, "stopping %s %d\n",
RING_TYPE(ring), ring->hop);
if (ring->nhi->going_away)
goto err;
if (!ring->running) {
- dev_WARN(&ring->nhi->pdev->dev, "%s %d already stopped\n",
+ dev_WARN(ring->nhi->dev, "%s %d already stopped\n",
RING_TYPE(ring), ring->hop);
goto err;
}
@@ -815,6 +795,8 @@ EXPORT_SYMBOL_GPL(tb_ring_stop);
*/
void tb_ring_free(struct tb_ring *ring)
{
+ struct tb_nhi *nhi = ring->nhi;
+
spin_lock_irq(&ring->nhi->lock);
/*
* Dissociate the ring from the NHI. This also ensures that
@@ -826,14 +808,15 @@ void tb_ring_free(struct tb_ring *ring)
ring->nhi->rx_rings[ring->hop] = NULL;
if (ring->running) {
- dev_WARN(&ring->nhi->pdev->dev, "%s %d still running\n",
+ dev_WARN(ring->nhi->dev, "%s %d still running\n",
RING_TYPE(ring), ring->hop);
}
spin_unlock_irq(&ring->nhi->lock);
- ring_release_msix(ring);
+ if (nhi->ops->release_ring_irq)
+ nhi->ops->release_ring_irq(ring);
- dma_free_coherent(&ring->nhi->pdev->dev,
+ dma_free_coherent(ring->nhi->dev,
ring->size * sizeof(*ring->descriptors),
ring->descriptors, ring->descriptors_dma);
@@ -841,7 +824,7 @@ void tb_ring_free(struct tb_ring *ring)
ring->descriptors_dma = 0;
- dev_dbg(&ring->nhi->pdev->dev, "freeing %s %d\n", RING_TYPE(ring),
+ dev_dbg(ring->nhi->dev, "freeing %s %d\n", RING_TYPE(ring),
ring->hop);
/*
@@ -855,6 +838,26 @@ void tb_ring_free(struct tb_ring *ring)
EXPORT_SYMBOL_GPL(tb_ring_free);
/**
+ * tb_ring_throttling() - Configure throttling for ring interrupt
+ * @ring: Ring to configure
+ * @interval_nsec: Interval counter for moderation (in ns), %0 disables
+ *
+ * Enables or disables ring interrupt throttling. The ring must be
+ * stopped for this to be called. Granularity is 256 ns.
+ *
+ * Return: %0 on success, negative errno otherwise.
+ */
+int tb_ring_throttling(struct tb_ring *ring, unsigned int interval_nsec)
+{
+ guard(spinlock_irqsave)(&ring->lock);
+ if (WARN_ON_ONCE(ring->running))
+ return -EBUSY;
+ ring->interval_nsec = interval_nsec;
+ return 0;
+}
+EXPORT_SYMBOL_GPL(tb_ring_throttling);
+
+/**
* nhi_mailbox_cmd() - Send a command through NHI mailbox
* @nhi: Pointer to the NHI structure
* @cmd: Command to send
@@ -912,7 +915,7 @@ enum nhi_fw_mode nhi_mailbox_mode(struct tb_nhi *nhi)
return (enum nhi_fw_mode)val;
}
-static void nhi_interrupt_work(struct work_struct *work)
+void nhi_interrupt_work(struct work_struct *work)
{
struct tb_nhi *nhi = container_of(work, typeof(*nhi), interrupt_work);
int value = 0; /* Suppress uninitialized usage warning. */
@@ -940,9 +943,7 @@ static void nhi_interrupt_work(struct work_struct *work)
if ((value & (1 << (bit % 32))) == 0)
continue;
if (type == 2) {
- dev_warn(&nhi->pdev->dev,
- "RX overflow for ring %d\n",
- hop);
+ dev_warn(nhi->dev, "RX overflow for ring %d\n", hop);
continue;
}
if (type == 0)
@@ -950,7 +951,7 @@ static void nhi_interrupt_work(struct work_struct *work)
else
ring = nhi->rx_rings[hop];
if (ring == NULL) {
- dev_warn(&nhi->pdev->dev,
+ dev_warn(nhi->dev,
"got interrupt for inactive %s ring %d\n",
type ? "RX" : "TX",
hop);
@@ -964,7 +965,7 @@ static void nhi_interrupt_work(struct work_struct *work)
spin_unlock_irq(&nhi->lock);
}
-static irqreturn_t nhi_msi(int irq, void *data)
+irqreturn_t nhi_msi(int irq, void *data)
{
struct tb_nhi *nhi = data;
schedule_work(&nhi->interrupt_work);
@@ -973,8 +974,7 @@ static irqreturn_t nhi_msi(int irq, void *data)
static int __nhi_suspend_noirq(struct device *dev, bool wakeup)
{
- struct pci_dev *pdev = to_pci_dev(dev);
- struct tb *tb = pci_get_drvdata(pdev);
+ struct tb *tb = dev_get_drvdata(dev);
struct tb_nhi *nhi = tb->nhi;
int ret;
@@ -982,7 +982,7 @@ static int __nhi_suspend_noirq(struct device *dev, bool wakeup)
if (ret)
return ret;
- if (nhi->ops && nhi->ops->suspend_noirq) {
+ if (nhi->ops->suspend_noirq) {
ret = nhi->ops->suspend_noirq(tb->nhi, wakeup);
if (ret)
return ret;
@@ -998,21 +998,19 @@ static int nhi_suspend_noirq(struct device *dev)
static int nhi_freeze_noirq(struct device *dev)
{
- struct pci_dev *pdev = to_pci_dev(dev);
- struct tb *tb = pci_get_drvdata(pdev);
+ struct tb *tb = dev_get_drvdata(dev);
return tb_domain_freeze_noirq(tb);
}
static int nhi_thaw_noirq(struct device *dev)
{
- struct pci_dev *pdev = to_pci_dev(dev);
- struct tb *tb = pci_get_drvdata(pdev);
+ struct tb *tb = dev_get_drvdata(dev);
return tb_domain_thaw_noirq(tb);
}
-static bool nhi_wake_supported(struct pci_dev *pdev)
+static bool nhi_wake_supported(struct device *dev)
{
u8 val;
@@ -1020,7 +1018,7 @@ static bool nhi_wake_supported(struct pci_dev *pdev)
* If power rails are sustainable for wakeup from S4 this
* property is set by the BIOS.
*/
- if (!device_property_read_u8(&pdev->dev, "WAKE_SUPPORTED", &val))
+ if (!device_property_read_u8(dev, "WAKE_SUPPORTED", &val))
return !!val;
return true;
@@ -1028,33 +1026,15 @@ static bool nhi_wake_supported(struct pci_dev *pdev)
static int nhi_poweroff_noirq(struct device *dev)
{
- struct pci_dev *pdev = to_pci_dev(dev);
bool wakeup;
- wakeup = device_may_wakeup(dev) && nhi_wake_supported(pdev);
+ wakeup = device_may_wakeup(dev) && nhi_wake_supported(dev);
return __nhi_suspend_noirq(dev, wakeup);
}
-static void nhi_enable_int_throttling(struct tb_nhi *nhi)
-{
- /* Throttling is specified in 256ns increments */
- u32 throttle = DIV_ROUND_UP(128 * NSEC_PER_USEC, 256);
- unsigned int i;
-
- /*
- * Configure interrupt throttling for all vectors even if we
- * only use few.
- */
- for (i = 0; i < MSIX_MAX_VECS; i++) {
- u32 reg = REG_INT_THROTTLING_RATE + i * 4;
- iowrite32(throttle, nhi->iobase + reg);
- }
-}
-
static int nhi_resume_noirq(struct device *dev)
{
- struct pci_dev *pdev = to_pci_dev(dev);
- struct tb *tb = pci_get_drvdata(pdev);
+ struct tb *tb = dev_get_drvdata(dev);
struct tb_nhi *nhi = tb->nhi;
int ret;
@@ -1063,15 +1043,12 @@ static int nhi_resume_noirq(struct device *dev)
* unplugged last device which causes the host controller to go
* away on PCs.
*/
- if (!pci_device_is_present(pdev)) {
+ if ((nhi->ops->is_present && !nhi->ops->is_present(nhi))) {
nhi->going_away = true;
- } else {
- if (nhi->ops && nhi->ops->resume_noirq) {
- ret = nhi->ops->resume_noirq(nhi);
- if (ret)
- return ret;
- }
- nhi_enable_int_throttling(tb->nhi);
+ } else if (nhi->ops->resume_noirq) {
+ ret = nhi->ops->resume_noirq(nhi);
+ if (ret)
+ return ret;
}
return tb_domain_resume_noirq(tb);
@@ -1079,32 +1056,29 @@ static int nhi_resume_noirq(struct device *dev)
static int nhi_suspend(struct device *dev)
{
- struct pci_dev *pdev = to_pci_dev(dev);
- struct tb *tb = pci_get_drvdata(pdev);
+ struct tb *tb = dev_get_drvdata(dev);
return tb_domain_suspend(tb);
}
static void nhi_complete(struct device *dev)
{
- struct pci_dev *pdev = to_pci_dev(dev);
- struct tb *tb = pci_get_drvdata(pdev);
+ struct tb *tb = dev_get_drvdata(dev);
/*
* If we were runtime suspended when system suspend started,
* schedule runtime resume now. It should bring the domain back
* to functional state.
*/
- if (pm_runtime_suspended(&pdev->dev))
- pm_runtime_resume(&pdev->dev);
+ if (pm_runtime_suspended(dev))
+ pm_runtime_resume(dev);
else
tb_domain_complete(tb);
}
static int nhi_runtime_suspend(struct device *dev)
{
- struct pci_dev *pdev = to_pci_dev(dev);
- struct tb *tb = pci_get_drvdata(pdev);
+ struct tb *tb = dev_get_drvdata(dev);
struct tb_nhi *nhi = tb->nhi;
int ret;
@@ -1112,7 +1086,7 @@ static int nhi_runtime_suspend(struct device *dev)
if (ret)
return ret;
- if (nhi->ops && nhi->ops->runtime_suspend) {
+ if (nhi->ops->runtime_suspend) {
ret = nhi->ops->runtime_suspend(tb->nhi);
if (ret)
return ret;
@@ -1122,115 +1096,39 @@ static int nhi_runtime_suspend(struct device *dev)
static int nhi_runtime_resume(struct device *dev)
{
- struct pci_dev *pdev = to_pci_dev(dev);
- struct tb *tb = pci_get_drvdata(pdev);
+ struct tb *tb = dev_get_drvdata(dev);
struct tb_nhi *nhi = tb->nhi;
int ret;
- if (nhi->ops && nhi->ops->runtime_resume) {
+ if (nhi->ops->runtime_resume) {
ret = nhi->ops->runtime_resume(nhi);
if (ret)
return ret;
}
- nhi_enable_int_throttling(nhi);
return tb_domain_runtime_resume(tb);
}
-static void nhi_shutdown(struct tb_nhi *nhi)
+void nhi_shutdown(struct tb_nhi *nhi)
{
int i;
- dev_dbg(&nhi->pdev->dev, "shutdown\n");
+ dev_dbg(nhi->dev, "shutdown\n");
for (i = 0; i < nhi->hop_count; i++) {
if (nhi->tx_rings[i])
- dev_WARN(&nhi->pdev->dev,
+ dev_WARN(nhi->dev,
"TX ring %d is still active\n", i);
if (nhi->rx_rings[i])
- dev_WARN(&nhi->pdev->dev,
+ dev_WARN(nhi->dev,
"RX ring %d is still active\n", i);
}
nhi_disable_interrupts(nhi);
- /*
- * We have to release the irq before calling flush_work. Otherwise an
- * already executing IRQ handler could call schedule_work again.
- */
- if (!nhi->pdev->msix_enabled) {
- devm_free_irq(&nhi->pdev->dev, nhi->pdev->irq, nhi);
- flush_work(&nhi->interrupt_work);
- }
- ida_destroy(&nhi->msix_ida);
- if (nhi->ops && nhi->ops->shutdown)
+ if (nhi->ops->shutdown)
nhi->ops->shutdown(nhi);
}
-static void nhi_check_quirks(struct tb_nhi *nhi)
-{
- if (nhi->pdev->vendor == PCI_VENDOR_ID_INTEL) {
- /*
- * Intel hardware supports auto clear of the interrupt
- * status register right after interrupt is being
- * issued.
- */
- nhi->quirks |= QUIRK_AUTO_CLEAR_INT;
-
- switch (nhi->pdev->device) {
- case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI:
- case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_4C_NHI:
- /*
- * Falcon Ridge controller needs the end-to-end
- * flow control workaround to avoid losing Rx
- * packets when RING_FLAG_E2E is set.
- */
- nhi->quirks |= QUIRK_E2E;
- break;
- }
- }
-}
-
-static int nhi_check_iommu_pdev(struct pci_dev *pdev, void *data)
-{
- if (!pdev->external_facing ||
- !device_iommu_capable(&pdev->dev, IOMMU_CAP_PRE_BOOT_PROTECTION))
- return 0;
- *(bool *)data = true;
- return 1; /* Stop walking */
-}
-
-static void nhi_check_iommu(struct tb_nhi *nhi)
-{
- struct pci_bus *bus = nhi->pdev->bus;
- bool port_ok = false;
-
- /*
- * Ideally what we'd do here is grab every PCI device that
- * represents a tunnelling adapter for this NHI and check their
- * status directly, but unfortunately USB4 seems to make it
- * obnoxiously difficult to reliably make any correlation.
- *
- * So for now we'll have to bodge it... Hoping that the system
- * is at least sane enough that an adapter is in the same PCI
- * segment as its NHI, if we can find *something* on that segment
- * which meets the requirements for Kernel DMA Protection, we'll
- * take that to imply that firmware is aware and has (hopefully)
- * done the right thing in general. We need to know that the PCI
- * layer has seen the ExternalFacingPort property which will then
- * inform the IOMMU layer to enforce the complete "untrusted DMA"
- * flow, but also that the IOMMU driver itself can be trusted not
- * to have been subverted by a pre-boot DMA attack.
- */
- while (bus->parent)
- bus = bus->parent;
-
- pci_walk_bus(bus, nhi_check_iommu_pdev, &port_ok);
-
- nhi->iommu_dma_protection = port_ok;
- dev_dbg(&nhi->pdev->dev, "IOMMU DMA protection is %s\n",
- str_enabled_disabled(port_ok));
-}
-
static void nhi_reset(struct tb_nhi *nhi)
{
ktime_t timeout;
@@ -1242,7 +1140,7 @@ static void nhi_reset(struct tb_nhi *nhi)
return;
if (!host_reset) {
- dev_dbg(&nhi->pdev->dev, "skipping host router reset\n");
+ dev_dbg(nhi->dev, "skipping host router reset\n");
return;
}
@@ -1253,64 +1151,13 @@ static void nhi_reset(struct tb_nhi *nhi)
do {
val = ioread32(nhi->iobase + REG_RESET);
if (!(val & REG_RESET_HRR)) {
- dev_warn(&nhi->pdev->dev, "host router reset successful\n");
+ dev_warn(nhi->dev, "host router reset successful\n");
return;
}
usleep_range(10, 20);
} while (ktime_before(ktime_get(), timeout));
- dev_warn(&nhi->pdev->dev, "timeout resetting host router\n");
-}
-
-static int nhi_init_msi(struct tb_nhi *nhi)
-{
- struct pci_dev *pdev = nhi->pdev;
- struct device *dev = &pdev->dev;
- int res, irq, nvec;
-
- /* In case someone left them on. */
- nhi_disable_interrupts(nhi);
-
- nhi_enable_int_throttling(nhi);
-
- ida_init(&nhi->msix_ida);
-
- /*
- * The NHI has 16 MSI-X vectors or a single MSI. We first try to
- * get all MSI-X vectors and if we succeed, each ring will have
- * one MSI-X. If for some reason that does not work out, we
- * fallback to a single MSI.
- */
- nvec = pci_alloc_irq_vectors(pdev, MSIX_MIN_VECS, MSIX_MAX_VECS,
- PCI_IRQ_MSIX);
- if (nvec < 0) {
- nvec = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
- if (nvec < 0)
- return nvec;
-
- INIT_WORK(&nhi->interrupt_work, nhi_interrupt_work);
-
- irq = pci_irq_vector(nhi->pdev, 0);
- if (irq < 0)
- return irq;
-
- res = devm_request_irq(&pdev->dev, irq, nhi_msi,
- IRQF_NO_SUSPEND, "thunderbolt", nhi);
- if (res)
- return dev_err_probe(dev, res, "request_irq failed, aborting\n");
- }
-
- return 0;
-}
-
-static bool nhi_imr_valid(struct pci_dev *pdev)
-{
- u8 val;
-
- if (!device_property_read_u8(&pdev->dev, "IMR_VALID", &val))
- return !!val;
-
- return true;
+ dev_warn(nhi->dev, "timeout resetting host router\n");
}
static struct tb *nhi_select_cm(struct tb_nhi *nhi)
@@ -1336,62 +1183,47 @@ static struct tb *nhi_select_cm(struct tb_nhi *nhi)
return tb;
}
-static int nhi_probe(struct pci_dev *pdev, const struct pci_device_id *id)
+int nhi_probe(struct tb_nhi *nhi)
{
- struct device *dev = &pdev->dev;
- struct tb_nhi *nhi;
+ struct device *dev = nhi->dev;
struct tb *tb;
int res;
- if (!nhi_imr_valid(pdev))
- return dev_err_probe(dev, -ENODEV, "firmware image not valid, aborting\n");
+ if (!nhi->ops)
+ return dev_err_probe(dev, -EINVAL, "NHI ops not set\n");
- res = pcim_enable_device(pdev);
- if (res)
- return dev_err_probe(dev, res, "cannot enable PCI device, aborting\n");
-
- nhi = devm_kzalloc(&pdev->dev, sizeof(*nhi), GFP_KERNEL);
- if (!nhi)
- return -ENOMEM;
-
- nhi->pdev = pdev;
- nhi->ops = (const struct tb_nhi_ops *)id->driver_data;
-
- nhi->iobase = pcim_iomap_region(pdev, 0, "thunderbolt");
- res = PTR_ERR_OR_ZERO(nhi->iobase);
- if (res)
- return dev_err_probe(dev, res, "cannot obtain PCI resources, aborting\n");
+ if (!nhi->ops->init_interrupts)
+ return dev_err_probe(dev, -EINVAL, "missing required NHI ops\n");
nhi->hop_count = ioread32(nhi->iobase + REG_CAPS) & 0x3ff;
dev_dbg(dev, "total paths: %d\n", nhi->hop_count);
- nhi->tx_rings = devm_kcalloc(&pdev->dev, nhi->hop_count,
+ nhi->tx_rings = devm_kcalloc(dev, nhi->hop_count,
sizeof(*nhi->tx_rings), GFP_KERNEL);
- nhi->rx_rings = devm_kcalloc(&pdev->dev, nhi->hop_count,
+ nhi->rx_rings = devm_kcalloc(dev, nhi->hop_count,
sizeof(*nhi->rx_rings), GFP_KERNEL);
if (!nhi->tx_rings || !nhi->rx_rings)
return -ENOMEM;
- nhi_check_quirks(nhi);
- nhi_check_iommu(nhi);
nhi_reset(nhi);
- res = nhi_init_msi(nhi);
+ /* In case someone left them on. */
+ nhi_disable_interrupts(nhi);
+
+ res = nhi->ops->init_interrupts(nhi);
if (res)
- return dev_err_probe(dev, res, "cannot enable MSI, aborting\n");
+ return dev_err_probe(dev, res, "cannot enable interrupts, aborting\n");
spin_lock_init(&nhi->lock);
- res = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
+ res = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
if (res)
return dev_err_probe(dev, res, "failed to set DMA mask\n");
- pci_set_master(pdev);
-
- if (nhi->ops && nhi->ops->init) {
+ if (nhi->ops->init) {
res = nhi->ops->init(nhi);
if (res)
- return res;
+ return dev_err_probe(dev, res, "NHI specific init failed\n");
}
tb = nhi_select_cm(nhi);
@@ -1401,6 +1233,8 @@ static int nhi_probe(struct pci_dev *pdev, const struct pci_device_id *id)
dev_dbg(dev, "NHI initialized, starting thunderbolt\n");
+ init_completion(&nhi->domain_released);
+
res = tb_domain_add(tb, host_reset);
if (res) {
/*
@@ -1408,40 +1242,28 @@ static int nhi_probe(struct pci_dev *pdev, const struct pci_device_id *id)
* activated. Do a proper shutdown.
*/
tb_domain_put(tb);
+ wait_for_completion(&nhi->domain_released);
nhi_shutdown(nhi);
- return res;
+ return dev_err_probe(dev, res, "failed to add domain\n");
}
- pci_set_drvdata(pdev, tb);
+ dev_set_drvdata(dev, tb);
- device_wakeup_enable(&pdev->dev);
+ device_wakeup_enable(dev);
- pm_runtime_allow(&pdev->dev);
- pm_runtime_set_autosuspend_delay(&pdev->dev, TB_AUTOSUSPEND_DELAY);
- pm_runtime_use_autosuspend(&pdev->dev);
- pm_runtime_put_autosuspend(&pdev->dev);
+ pm_runtime_allow(dev);
+ pm_runtime_set_autosuspend_delay(dev, TB_AUTOSUSPEND_DELAY);
+ pm_runtime_use_autosuspend(dev);
+ pm_runtime_put_autosuspend(dev);
return 0;
}
-static void nhi_remove(struct pci_dev *pdev)
-{
- struct tb *tb = pci_get_drvdata(pdev);
- struct tb_nhi *nhi = tb->nhi;
-
- pm_runtime_get_sync(&pdev->dev);
- pm_runtime_dont_use_autosuspend(&pdev->dev);
- pm_runtime_forbid(&pdev->dev);
-
- tb_domain_remove(tb);
- nhi_shutdown(nhi);
-}
-
/*
* The tunneled pci bridges are siblings of us. Use resume_noirq to reenable
* the tunnels asap. A corresponding pci quirk blocks the downstream bridges
* resume_noirq until we are done.
*/
-static const struct dev_pm_ops nhi_pm_ops = {
+const struct dev_pm_ops nhi_pm_ops = {
.suspend_noirq = nhi_suspend_noirq,
.resume_noirq = nhi_resume_noirq,
.freeze_noirq = nhi_freeze_noirq, /*
@@ -1457,129 +1279,3 @@ static const struct dev_pm_ops nhi_pm_ops = {
.runtime_suspend = nhi_runtime_suspend,
.runtime_resume = nhi_runtime_resume,
};
-
-static struct pci_device_id nhi_ids[] = {
- /*
- * We have to specify class, the TB bridges use the same device and
- * vendor (sub)id on gen 1 and gen 2 controllers.
- */
- {
- .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0,
- .vendor = PCI_VENDOR_ID_INTEL,
- .device = PCI_DEVICE_ID_INTEL_LIGHT_RIDGE,
- .subvendor = 0x2222, .subdevice = 0x1111,
- },
- {
- .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0,
- .vendor = PCI_VENDOR_ID_INTEL,
- .device = PCI_DEVICE_ID_INTEL_CACTUS_RIDGE_4C,
- .subvendor = 0x2222, .subdevice = 0x1111,
- },
- {
- .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0,
- .vendor = PCI_VENDOR_ID_INTEL,
- .device = PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI,
- .subvendor = PCI_ANY_ID, .subdevice = PCI_ANY_ID,
- },
- {
- .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0,
- .vendor = PCI_VENDOR_ID_INTEL,
- .device = PCI_DEVICE_ID_INTEL_FALCON_RIDGE_4C_NHI,
- .subvendor = PCI_ANY_ID, .subdevice = PCI_ANY_ID,
- },
-
- /* Thunderbolt 3 */
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_2C_NHI) },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_4C_NHI) },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_USBONLY_NHI) },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_LP_NHI) },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_LP_USBONLY_NHI) },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_2C_NHI) },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_4C_NHI) },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_USBONLY_NHI) },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TITAN_RIDGE_2C_NHI) },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TITAN_RIDGE_4C_NHI) },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ICL_NHI0),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ICL_NHI1),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- /* Thunderbolt 4 */
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_NHI0),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_NHI1),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_H_NHI0),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_H_NHI1),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ADL_NHI0),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ADL_NHI1),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_RPL_NHI0),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_RPL_NHI1),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_MTL_M_NHI0),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_MTL_P_NHI0),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_MTL_P_NHI1),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_LNL_NHI0),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_LNL_NHI1),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_M_NHI0),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_M_NHI1),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_P_NHI0),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_P_NHI1),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_WCL_NHI0),
- .driver_data = (kernel_ulong_t)&icl_nhi_ops },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_BARLOW_RIDGE_HOST_80G_NHI) },
- { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_BARLOW_RIDGE_HOST_40G_NHI) },
-
- /* Any USB4 compliant host */
- { PCI_DEVICE_CLASS(PCI_CLASS_SERIAL_USB_USB4, ~0) },
-
- { 0,}
-};
-
-MODULE_DEVICE_TABLE(pci, nhi_ids);
-MODULE_DESCRIPTION("Thunderbolt/USB4 core driver");
-MODULE_LICENSE("GPL");
-
-static struct pci_driver nhi_driver = {
- .name = "thunderbolt",
- .id_table = nhi_ids,
- .probe = nhi_probe,
- .remove = nhi_remove,
- .shutdown = nhi_remove,
- .driver.pm = &nhi_pm_ops,
-};
-
-static int __init nhi_init(void)
-{
- int ret;
-
- ret = tb_domain_init();
- if (ret)
- return ret;
- ret = pci_register_driver(&nhi_driver);
- if (ret)
- tb_domain_exit();
- return ret;
-}
-
-static void __exit nhi_unload(void)
-{
- pci_unregister_driver(&nhi_driver);
- tb_domain_exit();
-}
-
-rootfs_initcall(nhi_init);
-module_exit(nhi_unload);
diff --git a/drivers/thunderbolt/nhi.h b/drivers/thunderbolt/nhi.h
index 24ac4246d0ca..d488eadadfce 100644
--- a/drivers/thunderbolt/nhi.h
+++ b/drivers/thunderbolt/nhi.h
@@ -29,6 +29,14 @@ enum nhi_mailbox_cmd {
int nhi_mailbox_cmd(struct tb_nhi *nhi, enum nhi_mailbox_cmd cmd, u32 data);
enum nhi_fw_mode nhi_mailbox_mode(struct tb_nhi *nhi);
+void nhi_enable_int_throttling(struct tb_nhi *nhi);
+void nhi_disable_interrupts(struct tb_nhi *nhi);
+void nhi_interrupt_work(struct work_struct *work);
+irqreturn_t nhi_msi(int irq, void *data);
+irqreturn_t ring_msix(int irq, void *data);
+int nhi_probe(struct tb_nhi *nhi);
+void nhi_shutdown(struct tb_nhi *nhi);
+extern const struct dev_pm_ops nhi_pm_ops;
/**
* struct tb_nhi_ops - NHI specific optional operations
@@ -38,6 +46,12 @@ enum nhi_fw_mode nhi_mailbox_mode(struct tb_nhi *nhi);
* @runtime_suspend: NHI specific runtime_suspend hook
* @runtime_resume: NHI specific runtime_resume hook
* @shutdown: NHI specific shutdown
+ * @pre_nvm_auth: hook to run before Thunderbolt 3 NVM authentication
+ * @post_nvm_auth: hook to run after Thunderbolt 3 NVM authentication
+ * @request_ring_irq: NHI specific interrupt retrieval hook
+ * @release_ring_irq: NHI specific interrupt release hook
+ * @is_present: Whether the device is currently present on the parent bus
+ * @init_interrupts: NHI specific interrupt initialization hook
*/
struct tb_nhi_ops {
int (*init)(struct tb_nhi *nhi);
@@ -46,10 +60,14 @@ struct tb_nhi_ops {
int (*runtime_suspend)(struct tb_nhi *nhi);
int (*runtime_resume)(struct tb_nhi *nhi);
void (*shutdown)(struct tb_nhi *nhi);
+ void (*pre_nvm_auth)(struct tb_nhi *nhi);
+ void (*post_nvm_auth)(struct tb_nhi *nhi);
+ int (*request_ring_irq)(struct tb_ring *ring, bool no_suspend);
+ void (*release_ring_irq)(struct tb_ring *ring);
+ bool (*is_present)(struct tb_nhi *nhi);
+ int (*init_interrupts)(struct tb_nhi *nhi);
};
-extern const struct tb_nhi_ops icl_nhi_ops;
-
/*
* PCI IDs used in this driver from Win Ridge forward. There is no
* need for the PCI quirk anymore as we will use ICM also on Apple
@@ -100,4 +118,15 @@ extern const struct tb_nhi_ops icl_nhi_ops;
#define PCI_CLASS_SERIAL_USB_USB4 0x0c0340
+/* Host interface quirks */
+#define QUIRK_AUTO_CLEAR_INT BIT(0)
+#define QUIRK_E2E BIT(1)
+
+/*
+ * Minimal number of vectors when we use MSI-X. Two for control channel
+ * Rx/Tx and the rest four are for cross domain DMA paths.
+ */
+#define MSIX_MIN_VECS 6
+#define MSIX_MAX_VECS 16
+
#endif
diff --git a/drivers/thunderbolt/nhi_ops.c b/drivers/thunderbolt/nhi_ops.c
deleted file mode 100644
index 96da07e88c52..000000000000
--- a/drivers/thunderbolt/nhi_ops.c
+++ /dev/null
@@ -1,185 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0
-/*
- * NHI specific operations
- *
- * Copyright (C) 2019, Intel Corporation
- * Author: Mika Westerberg <mika.westerberg@linux.intel.com>
- */
-
-#include <linux/delay.h>
-#include <linux/suspend.h>
-
-#include "nhi.h"
-#include "nhi_regs.h"
-#include "tb.h"
-
-/* Ice Lake specific NHI operations */
-
-#define ICL_LC_MAILBOX_TIMEOUT 500 /* ms */
-
-static int check_for_device(struct device *dev, void *data)
-{
- return tb_is_switch(dev);
-}
-
-static bool icl_nhi_is_device_connected(struct tb_nhi *nhi)
-{
- struct tb *tb = pci_get_drvdata(nhi->pdev);
- int ret;
-
- ret = device_for_each_child(&tb->root_switch->dev, NULL,
- check_for_device);
- return ret > 0;
-}
-
-static int icl_nhi_force_power(struct tb_nhi *nhi, bool power)
-{
- u32 vs_cap;
-
- /*
- * The Thunderbolt host controller is present always in Ice Lake
- * but the firmware may not be loaded and running (depending
- * whether there is device connected and so on). Each time the
- * controller is used we need to "Force Power" it first and wait
- * for the firmware to indicate it is up and running. This "Force
- * Power" is really not about actually powering on/off the
- * controller so it is accessible even if "Force Power" is off.
- *
- * The actual power management happens inside shared ACPI power
- * resources using standard ACPI methods.
- */
- pci_read_config_dword(nhi->pdev, VS_CAP_22, &vs_cap);
- if (power) {
- vs_cap &= ~VS_CAP_22_DMA_DELAY_MASK;
- vs_cap |= 0x22 << VS_CAP_22_DMA_DELAY_SHIFT;
- vs_cap |= VS_CAP_22_FORCE_POWER;
- } else {
- vs_cap &= ~VS_CAP_22_FORCE_POWER;
- }
- pci_write_config_dword(nhi->pdev, VS_CAP_22, vs_cap);
-
- if (power) {
- unsigned int retries = 350;
- u32 val;
-
- /* Wait until the firmware tells it is up and running */
- do {
- pci_read_config_dword(nhi->pdev, VS_CAP_9, &val);
- if (val & VS_CAP_9_FW_READY)
- return 0;
- usleep_range(3000, 3100);
- } while (--retries);
-
- return -ETIMEDOUT;
- }
-
- return 0;
-}
-
-static void icl_nhi_lc_mailbox_cmd(struct tb_nhi *nhi, enum icl_lc_mailbox_cmd cmd)
-{
- u32 data;
-
- data = (cmd << VS_CAP_19_CMD_SHIFT) & VS_CAP_19_CMD_MASK;
- pci_write_config_dword(nhi->pdev, VS_CAP_19, data | VS_CAP_19_VALID);
-}
-
-static int icl_nhi_lc_mailbox_cmd_complete(struct tb_nhi *nhi, int timeout)
-{
- unsigned long end;
- u32 data;
-
- if (!timeout)
- goto clear;
-
- end = jiffies + msecs_to_jiffies(timeout);
- do {
- pci_read_config_dword(nhi->pdev, VS_CAP_18, &data);
- if (data & VS_CAP_18_DONE)
- goto clear;
- usleep_range(1000, 1100);
- } while (time_before(jiffies, end));
-
- return -ETIMEDOUT;
-
-clear:
- /* Clear the valid bit */
- pci_write_config_dword(nhi->pdev, VS_CAP_19, 0);
- return 0;
-}
-
-static void icl_nhi_set_ltr(struct tb_nhi *nhi)
-{
- u32 max_ltr, ltr;
-
- pci_read_config_dword(nhi->pdev, VS_CAP_16, &max_ltr);
- max_ltr &= 0xffff;
- /* Program the same value for both snoop and no-snoop */
- ltr = max_ltr << 16 | max_ltr;
- pci_write_config_dword(nhi->pdev, VS_CAP_15, ltr);
-}
-
-static int icl_nhi_suspend(struct tb_nhi *nhi)
-{
- struct tb *tb = pci_get_drvdata(nhi->pdev);
- int ret;
-
- if (icl_nhi_is_device_connected(nhi))
- return 0;
-
- if (tb_switch_is_icm(tb->root_switch)) {
- /*
- * If there is no device connected we need to perform
- * both: a handshake through LC mailbox and force power
- * down before entering D3.
- */
- icl_nhi_lc_mailbox_cmd(nhi, ICL_LC_PREPARE_FOR_RESET);
- ret = icl_nhi_lc_mailbox_cmd_complete(nhi, ICL_LC_MAILBOX_TIMEOUT);
- if (ret)
- return ret;
- }
-
- return icl_nhi_force_power(nhi, false);
-}
-
-static int icl_nhi_suspend_noirq(struct tb_nhi *nhi, bool wakeup)
-{
- struct tb *tb = pci_get_drvdata(nhi->pdev);
- enum icl_lc_mailbox_cmd cmd;
-
- if (!pm_suspend_via_firmware())
- return icl_nhi_suspend(nhi);
-
- if (!tb_switch_is_icm(tb->root_switch))
- return 0;
-
- cmd = wakeup ? ICL_LC_GO2SX : ICL_LC_GO2SX_NO_WAKE;
- icl_nhi_lc_mailbox_cmd(nhi, cmd);
- return icl_nhi_lc_mailbox_cmd_complete(nhi, ICL_LC_MAILBOX_TIMEOUT);
-}
-
-static int icl_nhi_resume(struct tb_nhi *nhi)
-{
- int ret;
-
- ret = icl_nhi_force_power(nhi, true);
- if (ret)
- return ret;
-
- icl_nhi_set_ltr(nhi);
- return 0;
-}
-
-static void icl_nhi_shutdown(struct tb_nhi *nhi)
-{
- icl_nhi_force_power(nhi, false);
-}
-
-const struct tb_nhi_ops icl_nhi_ops = {
- .init = icl_nhi_resume,
- .suspend_noirq = icl_nhi_suspend_noirq,
- .resume_noirq = icl_nhi_resume,
- .runtime_suspend = icl_nhi_suspend,
- .runtime_resume = icl_nhi_resume,
- .shutdown = icl_nhi_shutdown,
-};
diff --git a/drivers/thunderbolt/nhi_regs.h b/drivers/thunderbolt/nhi_regs.h
index cf5222bee971..d6a197fabc74 100644
--- a/drivers/thunderbolt/nhi_regs.h
+++ b/drivers/thunderbolt/nhi_regs.h
@@ -101,7 +101,8 @@ struct ring_desc {
#define REG_RING_INTERRUPT_MASK_CLEAR_BASE 0x38208
-#define REG_INT_THROTTLING_RATE 0x38c00
+#define REG_INT_THROTTLING_RATE 0x38c00
+#define REG_INT_THROTTLING_RATE_INTERVAL_MASK GENMASK(15, 0)
/* Interrupt Vector Allocation */
#define REG_INT_VEC_ALLOC_BASE 0x38c40
diff --git a/drivers/thunderbolt/path.c b/drivers/thunderbolt/path.c
index 8713ea0f47c1..b2c322e76b8a 100644
--- a/drivers/thunderbolt/path.c
+++ b/drivers/thunderbolt/path.c
@@ -412,7 +412,8 @@ static int __tb_path_deactivate_hop(struct tb_port *port, int hop_index,
* in the USB4 spec so we clear them
* only for pre-USB4 adapters.
*/
- if (!tb_switch_is_usb4(port->sw)) {
+ if (tb_port_is_null(port) ||
+ !tb_switch_is_usb4(port->sw)) {
hop.ingress_fc = 0;
hop.ingress_shared_buffer = 0;
}
@@ -483,7 +484,7 @@ void tb_path_deactivate(struct tb_path *path)
* tb_path_activate() - activate a path
* @path: Path to activate
*
- * Activate a path starting with the last hop and iterating backwards. The
+ * Activate a path starting with the first hop and ending on the last hop. The
* caller must fill path->hops before calling tb_path_activate().
*
* Return: %0 on success, negative errno otherwise.
@@ -525,22 +526,25 @@ int tb_path_activate(struct tb_path *path)
}
/* Activate hops. */
- for (i = path->path_length - 1; i >= 0; i--) {
+ for (i = 0; i < path->path_length; i++) {
struct tb_regs_hop hop = { 0 };
/* If it is left active deactivate it first */
__tb_path_deactivate_hop(path->hops[i].in_port,
path->hops[i].in_hop_index, path->clear_fc);
- /* dword 0 */
+ /* Needed for USB4 routers, read path config space before write */
+ res = tb_port_read(path->hops[i].in_port, &hop, TB_CFG_HOPS,
+ 2 * path->hops[i].in_hop_index, 2);
+ if (res)
+ goto err;
+
hop.next_hop = path->hops[i].next_hop_index;
hop.out_port = path->hops[i].out_port->port;
- hop.initial_credits = path->hops[i].initial_credits;
hop.pmps = path->hops[i].pm_support;
hop.unknown1 = 0;
hop.enable = 1;
- /* dword 1 */
out_mask = (i == path->path_length - 1) ?
TB_PATH_DESTINATION : TB_PATH_INTERNAL;
in_mask = (i == 0) ? TB_PATH_SOURCE : TB_PATH_INTERNAL;
@@ -550,12 +554,21 @@ int tb_path_activate(struct tb_path *path)
hop.drop_packages = path->drop_packages;
hop.counter = path->hops[i].in_counter_index;
hop.counter_enable = path->hops[i].in_counter_index != -1;
- hop.ingress_fc = path->ingress_fc_enable & in_mask;
hop.egress_fc = path->egress_fc_enable & out_mask;
- hop.ingress_shared_buffer = path->ingress_shared_buffer
- & in_mask;
- hop.egress_shared_buffer = path->egress_shared_buffer
- & out_mask;
+ hop.egress_shared_buffer = path->egress_shared_buffer & out_mask;
+ /*
+ * Protocol adapters IFC and ISE bits, and Path Credits
+ * Allocated are vendor defined in the USB4 spec so we
+ * program them only for pre-USB4 and lane adapters.
+ */
+ if (tb_port_is_null(path->hops[i].in_port) ||
+ !tb_switch_is_usb4(path->hops[i].in_port->sw)) {
+ hop.initial_credits = path->hops[i].initial_credits;
+ hop.ingress_fc = path->ingress_fc_enable & in_mask;
+ hop.ingress_shared_buffer =
+ path->ingress_shared_buffer & in_mask;
+ }
+
hop.unknown3 = 0;
tb_port_dbg(path->hops[i].in_port, "Writing hop %d\n", i);
@@ -563,7 +576,7 @@ int tb_path_activate(struct tb_path *path)
res = tb_port_write(path->hops[i].in_port, &hop, TB_CFG_HOPS,
2 * path->hops[i].in_hop_index, 2);
if (res) {
- __tb_path_deactivate_hops(path, i);
+ __tb_path_deactivate_hops(path, 0);
__tb_path_deallocate_nfc(path, 0);
goto err;
}
diff --git a/drivers/thunderbolt/pci.c b/drivers/thunderbolt/pci.c
new file mode 100644
index 000000000000..bbd186c29ef7
--- /dev/null
+++ b/drivers/thunderbolt/pci.c
@@ -0,0 +1,622 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Thunderbolt driver - PCI NHI driver
+ *
+ * Copyright (c) 2014 Andreas Noever <andreas.noever@gmail.com>
+ * Copyright (C) 2018, Intel Corporation
+ */
+
+#include <linux/pm_runtime.h>
+#include <linux/slab.h>
+#include <linux/errno.h>
+#include <linux/pci.h>
+#include <linux/dma-mapping.h>
+#include <linux/interrupt.h>
+#include <linux/iommu.h>
+#include <linux/module.h>
+#include <linux/delay.h>
+#include <linux/property.h>
+#include <linux/string_helpers.h>
+#include <linux/suspend.h>
+
+#include "nhi.h"
+#include "nhi_regs.h"
+#include "tb.h"
+
+/**
+ * struct tb_nhi_pci - NHI device connected over PCIe
+ * @nhi: NHI device
+ * @msix_ida: Used to allocate MSI-X vectors for rings
+ */
+struct tb_nhi_pci {
+ struct tb_nhi nhi;
+ struct ida msix_ida;
+};
+
+static inline struct tb_nhi_pci *nhi_to_pci(struct tb_nhi *nhi)
+{
+ return container_of(nhi, struct tb_nhi_pci, nhi);
+}
+
+static void nhi_pci_check_quirks(struct tb_nhi_pci *nhi_pci)
+{
+ struct tb_nhi *nhi = &nhi_pci->nhi;
+ struct pci_dev *pdev = to_pci_dev(nhi->dev);
+
+ if (pdev->vendor == PCI_VENDOR_ID_INTEL) {
+ /*
+ * Intel hardware supports auto clear of the interrupt
+ * status register right after interrupt is being
+ * issued.
+ */
+ nhi->quirks |= QUIRK_AUTO_CLEAR_INT;
+
+ switch (pdev->device) {
+ case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI:
+ case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_4C_NHI:
+ /*
+ * Falcon Ridge controller needs the end-to-end
+ * flow control workaround to avoid losing Rx
+ * packets when RING_FLAG_E2E is set.
+ */
+ nhi->quirks |= QUIRK_E2E;
+ break;
+ }
+ }
+}
+
+static int nhi_pci_check_iommu_pdev(struct pci_dev *pdev, void *data)
+{
+ if (!pdev->external_facing ||
+ !device_iommu_capable(&pdev->dev, IOMMU_CAP_PRE_BOOT_PROTECTION))
+ return 0;
+ *(bool *)data = true;
+ return 1; /* Stop walking */
+}
+
+static void nhi_pci_check_iommu(struct tb_nhi_pci *nhi_pci)
+{
+ struct tb_nhi *nhi = &nhi_pci->nhi;
+ struct pci_dev *pdev = to_pci_dev(nhi->dev);
+ struct pci_bus *bus = pdev->bus;
+ bool port_ok = false;
+
+ /*
+ * Ideally what we'd do here is grab every PCI device that
+ * represents a tunnelling adapter for this NHI and check their
+ * status directly, but unfortunately USB4 seems to make it
+ * obnoxiously difficult to reliably make any correlation.
+ *
+ * So for now we'll have to bodge it... Hoping that the system
+ * is at least sane enough that an adapter is in the same PCI
+ * segment as its NHI, if we can find *something* on that segment
+ * which meets the requirements for Kernel DMA Protection, we'll
+ * take that to imply that firmware is aware and has (hopefully)
+ * done the right thing in general. We need to know that the PCI
+ * layer has seen the ExternalFacingPort property which will then
+ * inform the IOMMU layer to enforce the complete "untrusted DMA"
+ * flow, but also that the IOMMU driver itself can be trusted not
+ * to have been subverted by a pre-boot DMA attack.
+ */
+ while (bus->parent)
+ bus = bus->parent;
+
+ pci_walk_bus(bus, nhi_pci_check_iommu_pdev, &port_ok);
+
+ nhi->iommu_dma_protection = port_ok;
+ dev_dbg(nhi->dev, "IOMMU DMA protection is %s\n",
+ str_enabled_disabled(port_ok));
+}
+
+static int nhi_pci_init_msi(struct tb_nhi *nhi)
+{
+ struct tb_nhi_pci *nhi_pci = nhi_to_pci(nhi);
+ struct pci_dev *pdev = to_pci_dev(nhi->dev);
+ struct device *dev = &pdev->dev;
+ int res, irq, nvec;
+
+ ida_init(&nhi_pci->msix_ida);
+
+ /*
+ * The NHI has 16 MSI-X vectors or a single MSI. We first try to
+ * get all MSI-X vectors and if we succeed, each ring will have
+ * one MSI-X. If for some reason that does not work out, we
+ * fallback to a single MSI.
+ */
+ nvec = pci_alloc_irq_vectors(pdev, MSIX_MIN_VECS, MSIX_MAX_VECS,
+ PCI_IRQ_MSIX);
+ if (nvec < 0) {
+ nvec = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
+ if (nvec < 0)
+ return nvec;
+
+ INIT_WORK(&nhi->interrupt_work, nhi_interrupt_work);
+
+ irq = pci_irq_vector(pdev, 0);
+ if (irq < 0)
+ return irq;
+
+ res = devm_request_irq(&pdev->dev, irq, nhi_msi,
+ IRQF_NO_SUSPEND, "thunderbolt", nhi);
+ if (res)
+ return dev_err_probe(dev, res, "request_irq failed, aborting\n");
+ }
+
+ return 0;
+}
+
+static bool nhi_pci_imr_valid(struct pci_dev *pdev)
+{
+ u8 val;
+
+ if (!device_property_read_u8(&pdev->dev, "IMR_VALID", &val))
+ return !!val;
+
+ return true;
+}
+
+static void nhi_pci_start_dma_port(struct tb_nhi *nhi)
+{
+ struct pci_dev *pdev = to_pci_dev(nhi->dev);
+ struct pci_dev *root_port;
+
+ /*
+ * During host router NVM upgrade we should not allow root port to
+ * go into D3cold because some root ports cannot trigger PME
+ * itself. To be on the safe side keep the root port in D0 during
+ * the whole upgrade process.
+ */
+ root_port = pcie_find_root_port(pdev);
+ if (root_port)
+ pm_runtime_get_noresume(&root_port->dev);
+}
+
+static void nhi_pci_complete_dma_port(struct tb_nhi *nhi)
+{
+ struct pci_dev *pdev = to_pci_dev(nhi->dev);
+ struct pci_dev *root_port;
+
+ root_port = pcie_find_root_port(pdev);
+ if (root_port)
+ pm_runtime_put(&root_port->dev);
+}
+
+static int nhi_pci_ring_request_msix(struct tb_ring *ring, bool no_suspend)
+{
+ struct tb_nhi *nhi = ring->nhi;
+ struct tb_nhi_pci *nhi_pci = nhi_to_pci(nhi);
+ struct pci_dev *pdev = to_pci_dev(nhi->dev);
+ unsigned long irqflags;
+ int ret;
+
+ if (!pdev->msix_enabled)
+ return 0;
+
+ ret = ida_alloc_max(&nhi_pci->msix_ida, MSIX_MAX_VECS - 1, GFP_KERNEL);
+ if (ret < 0)
+ return ret;
+
+ ring->vector = ret;
+
+ ret = pci_irq_vector(pdev, ring->vector);
+ if (ret < 0)
+ goto err_ida_remove;
+
+ ring->irq = ret;
+
+ irqflags = no_suspend ? IRQF_NO_SUSPEND : 0;
+ ret = request_irq(ring->irq, ring_msix, irqflags, "thunderbolt", ring);
+ if (ret)
+ goto err_ida_remove;
+
+ return 0;
+
+err_ida_remove:
+ ida_free(&nhi_pci->msix_ida, ring->vector);
+
+ return ret;
+}
+
+static void nhi_pci_ring_release_msix(struct tb_ring *ring)
+{
+ struct tb_nhi_pci *nhi_pci = nhi_to_pci(ring->nhi);
+
+ if (ring->irq <= 0)
+ return;
+
+ free_irq(ring->irq, ring);
+ ida_free(&nhi_pci->msix_ida, ring->vector);
+ ring->vector = 0;
+ ring->irq = 0;
+}
+
+static void nhi_pci_shutdown(struct tb_nhi *nhi)
+{
+ struct tb_nhi_pci *nhi_pci = nhi_to_pci(nhi);
+ struct pci_dev *pdev = to_pci_dev(nhi->dev);
+
+ /*
+ * We have to release the irq before calling flush_work. Otherwise an
+ * already executing IRQ handler could call schedule_work again.
+ */
+ if (!pdev->msix_enabled) {
+ devm_free_irq(nhi->dev, pdev->irq, nhi);
+ flush_work(&nhi->interrupt_work);
+ }
+ ida_destroy(&nhi_pci->msix_ida);
+}
+
+static bool nhi_pci_is_present(struct tb_nhi *nhi)
+{
+ return pci_device_is_present(to_pci_dev(nhi->dev));
+}
+
+static const struct tb_nhi_ops pci_nhi_default_ops = {
+ .pre_nvm_auth = nhi_pci_start_dma_port,
+ .post_nvm_auth = nhi_pci_complete_dma_port,
+ .request_ring_irq = nhi_pci_ring_request_msix,
+ .release_ring_irq = nhi_pci_ring_release_msix,
+ .shutdown = nhi_pci_shutdown,
+ .is_present = nhi_pci_is_present,
+ .init_interrupts = nhi_pci_init_msi,
+};
+
+/* Ice Lake specific NHI operations */
+
+#define ICL_LC_MAILBOX_TIMEOUT 500 /* ms */
+
+static int check_for_device(struct device *dev, void *data)
+{
+ return tb_is_switch(dev);
+}
+
+static bool icl_nhi_is_device_connected(struct tb_nhi *nhi)
+{
+ struct tb *tb = dev_get_drvdata(nhi->dev);
+ int ret;
+
+ ret = device_for_each_child(&tb->root_switch->dev, NULL,
+ check_for_device);
+ return ret > 0;
+}
+
+static int icl_nhi_force_power(struct tb_nhi *nhi, bool power)
+{
+ struct pci_dev *pdev = to_pci_dev(nhi->dev);
+ u32 vs_cap;
+
+ /*
+ * The Thunderbolt host controller is present always in Ice Lake
+ * but the firmware may not be loaded and running (depending
+ * whether there is device connected and so on). Each time the
+ * controller is used we need to "Force Power" it first and wait
+ * for the firmware to indicate it is up and running. This "Force
+ * Power" is really not about actually powering on/off the
+ * controller so it is accessible even if "Force Power" is off.
+ *
+ * The actual power management happens inside shared ACPI power
+ * resources using standard ACPI methods.
+ */
+ pci_read_config_dword(pdev, VS_CAP_22, &vs_cap);
+ if (power) {
+ vs_cap &= ~VS_CAP_22_DMA_DELAY_MASK;
+ vs_cap |= 0x22 << VS_CAP_22_DMA_DELAY_SHIFT;
+ vs_cap |= VS_CAP_22_FORCE_POWER;
+ } else {
+ vs_cap &= ~VS_CAP_22_FORCE_POWER;
+ }
+ pci_write_config_dword(pdev, VS_CAP_22, vs_cap);
+
+ if (power) {
+ unsigned int retries = 350;
+ u32 val;
+
+ /* Wait until the firmware tells it is up and running */
+ do {
+ pci_read_config_dword(pdev, VS_CAP_9, &val);
+ if (val & VS_CAP_9_FW_READY)
+ return 0;
+ usleep_range(3000, 3100);
+ } while (--retries);
+
+ return -ETIMEDOUT;
+ }
+
+ return 0;
+}
+
+static void icl_nhi_lc_mailbox_cmd(struct tb_nhi *nhi, enum icl_lc_mailbox_cmd cmd)
+{
+ struct pci_dev *pdev = to_pci_dev(nhi->dev);
+ u32 data;
+
+ data = (cmd << VS_CAP_19_CMD_SHIFT) & VS_CAP_19_CMD_MASK;
+ pci_write_config_dword(pdev, VS_CAP_19, data | VS_CAP_19_VALID);
+}
+
+static int icl_nhi_lc_mailbox_cmd_complete(struct tb_nhi *nhi, int timeout)
+{
+ struct pci_dev *pdev = to_pci_dev(nhi->dev);
+ unsigned long end;
+ u32 data;
+
+ if (!timeout)
+ goto clear;
+
+ end = jiffies + msecs_to_jiffies(timeout);
+ do {
+ pci_read_config_dword(pdev, VS_CAP_18, &data);
+ if (data & VS_CAP_18_DONE)
+ goto clear;
+ usleep_range(1000, 1100);
+ } while (time_before(jiffies, end));
+
+ return -ETIMEDOUT;
+
+clear:
+ /* Clear the valid bit */
+ pci_write_config_dword(pdev, VS_CAP_19, 0);
+ return 0;
+}
+
+static void icl_nhi_set_ltr(struct tb_nhi *nhi)
+{
+ struct pci_dev *pdev = to_pci_dev(nhi->dev);
+ u32 max_ltr, ltr;
+
+ pci_read_config_dword(pdev, VS_CAP_16, &max_ltr);
+ max_ltr &= 0xffff;
+ /* Program the same value for both snoop and no-snoop */
+ ltr = max_ltr << 16 | max_ltr;
+ pci_write_config_dword(pdev, VS_CAP_15, ltr);
+}
+
+static int icl_nhi_suspend(struct tb_nhi *nhi)
+{
+ struct tb *tb = dev_get_drvdata(nhi->dev);
+ int ret;
+
+ if (icl_nhi_is_device_connected(nhi))
+ return 0;
+
+ if (tb_switch_is_icm(tb->root_switch)) {
+ /*
+ * If there is no device connected we need to perform
+ * both: a handshake through LC mailbox and force power
+ * down before entering D3.
+ */
+ icl_nhi_lc_mailbox_cmd(nhi, ICL_LC_PREPARE_FOR_RESET);
+ ret = icl_nhi_lc_mailbox_cmd_complete(nhi, ICL_LC_MAILBOX_TIMEOUT);
+ if (ret)
+ return ret;
+ }
+
+ return icl_nhi_force_power(nhi, false);
+}
+
+static int icl_nhi_suspend_noirq(struct tb_nhi *nhi, bool wakeup)
+{
+ struct tb *tb = dev_get_drvdata(nhi->dev);
+ enum icl_lc_mailbox_cmd cmd;
+
+ if (!pm_suspend_via_firmware())
+ return icl_nhi_suspend(nhi);
+
+ if (!tb_switch_is_icm(tb->root_switch))
+ return 0;
+
+ cmd = wakeup ? ICL_LC_GO2SX : ICL_LC_GO2SX_NO_WAKE;
+ icl_nhi_lc_mailbox_cmd(nhi, cmd);
+ return icl_nhi_lc_mailbox_cmd_complete(nhi, ICL_LC_MAILBOX_TIMEOUT);
+}
+
+static int icl_nhi_resume(struct tb_nhi *nhi)
+{
+ int ret;
+
+ ret = icl_nhi_force_power(nhi, true);
+ if (ret)
+ return ret;
+
+ icl_nhi_set_ltr(nhi);
+ return 0;
+}
+
+static void icl_nhi_shutdown(struct tb_nhi *nhi)
+{
+ nhi_pci_shutdown(nhi);
+
+ icl_nhi_force_power(nhi, false);
+}
+
+static const struct tb_nhi_ops icl_nhi_ops = {
+ .init = icl_nhi_resume,
+ .suspend_noirq = icl_nhi_suspend_noirq,
+ .resume_noirq = icl_nhi_resume,
+ .runtime_suspend = icl_nhi_suspend,
+ .runtime_resume = icl_nhi_resume,
+ .shutdown = icl_nhi_shutdown,
+ .pre_nvm_auth = nhi_pci_start_dma_port,
+ .post_nvm_auth = nhi_pci_complete_dma_port,
+ .request_ring_irq = nhi_pci_ring_request_msix,
+ .release_ring_irq = nhi_pci_ring_release_msix,
+ .is_present = nhi_pci_is_present,
+ .init_interrupts = nhi_pci_init_msi,
+};
+
+static int nhi_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
+{
+ struct device *dev = &pdev->dev;
+ struct tb_nhi_pci *nhi_pci;
+ struct tb_nhi *nhi;
+ int res;
+
+ if (!nhi_pci_imr_valid(pdev))
+ return dev_err_probe(dev, -ENODEV, "firmware image not valid, aborting\n");
+
+ res = pcim_enable_device(pdev);
+ if (res)
+ return dev_err_probe(dev, res, "cannot enable PCI device, aborting\n");
+
+ nhi_pci = devm_kzalloc(dev, sizeof(*nhi_pci), GFP_KERNEL);
+ if (!nhi_pci)
+ return -ENOMEM;
+
+ nhi = &nhi_pci->nhi;
+ nhi->dev = dev;
+ nhi->ops = (const struct tb_nhi_ops *)id->driver_data ?: &pci_nhi_default_ops;
+
+ nhi->iobase = pcim_iomap_region(pdev, 0, "thunderbolt");
+ res = PTR_ERR_OR_ZERO(nhi->iobase);
+ if (res)
+ return dev_err_probe(dev, res, "cannot obtain PCI resources, aborting\n");
+
+ nhi_pci_check_quirks(nhi_pci);
+ nhi_pci_check_iommu(nhi_pci);
+
+ pci_set_master(pdev);
+
+ return nhi_probe(&nhi_pci->nhi);
+}
+
+static void nhi_pci_remove(struct pci_dev *pdev)
+{
+ struct tb *tb = pci_get_drvdata(pdev);
+ struct tb_nhi *nhi = tb->nhi;
+
+ pm_runtime_get_sync(&pdev->dev);
+ pm_runtime_dont_use_autosuspend(&pdev->dev);
+ pm_runtime_forbid(&pdev->dev);
+
+ tb_domain_remove(tb);
+ wait_for_completion(&nhi->domain_released);
+ nhi_shutdown(nhi);
+}
+
+static struct pci_device_id nhi_ids[] = {
+ /*
+ * We have to specify class, the TB bridges use the same device and
+ * vendor (sub)id on gen 1 and gen 2 controllers.
+ */
+ {
+ .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0,
+ .vendor = PCI_VENDOR_ID_INTEL,
+ .device = PCI_DEVICE_ID_INTEL_LIGHT_RIDGE,
+ .subvendor = 0x2222, .subdevice = 0x1111,
+ },
+ {
+ .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0,
+ .vendor = PCI_VENDOR_ID_INTEL,
+ .device = PCI_DEVICE_ID_INTEL_CACTUS_RIDGE_4C,
+ .subvendor = 0x2222, .subdevice = 0x1111,
+ },
+ {
+ .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0,
+ .vendor = PCI_VENDOR_ID_INTEL,
+ .device = PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI,
+ .subvendor = PCI_ANY_ID, .subdevice = PCI_ANY_ID,
+ },
+ {
+ .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0,
+ .vendor = PCI_VENDOR_ID_INTEL,
+ .device = PCI_DEVICE_ID_INTEL_FALCON_RIDGE_4C_NHI,
+ .subvendor = PCI_ANY_ID, .subdevice = PCI_ANY_ID,
+ },
+
+ /* Thunderbolt 3 */
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_2C_NHI) },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_4C_NHI) },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_USBONLY_NHI) },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_LP_NHI) },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_LP_USBONLY_NHI) },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_2C_NHI) },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_4C_NHI) },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_USBONLY_NHI) },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TITAN_RIDGE_2C_NHI) },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TITAN_RIDGE_4C_NHI) },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ICL_NHI0),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ICL_NHI1),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ /* Thunderbolt 4 */
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_NHI0),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_NHI1),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_H_NHI0),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_H_NHI1),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ADL_NHI0),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ADL_NHI1),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_RPL_NHI0),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_RPL_NHI1),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_MTL_M_NHI0),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_MTL_P_NHI0),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_MTL_P_NHI1),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_LNL_NHI0),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_LNL_NHI1),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_M_NHI0),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_M_NHI1),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_P_NHI0),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_P_NHI1),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_WCL_NHI0),
+ .driver_data = (kernel_ulong_t)&icl_nhi_ops },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_BARLOW_RIDGE_HOST_80G_NHI) },
+ { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_BARLOW_RIDGE_HOST_40G_NHI) },
+
+ /* Any USB4 compliant host */
+ { PCI_DEVICE_CLASS(PCI_CLASS_SERIAL_USB_USB4, ~0) },
+
+ { 0,}
+};
+
+MODULE_DEVICE_TABLE(pci, nhi_ids);
+MODULE_DESCRIPTION("Thunderbolt/USB4 core driver");
+MODULE_LICENSE("GPL");
+
+static struct pci_driver nhi_driver = {
+ .name = "thunderbolt",
+ .id_table = nhi_ids,
+ .probe = nhi_pci_probe,
+ .remove = nhi_pci_remove,
+ .shutdown = nhi_pci_remove,
+ .driver.pm = &nhi_pm_ops,
+};
+
+static int __init nhi_init(void)
+{
+ int ret;
+
+ ret = tb_domain_init();
+ if (ret)
+ return ret;
+
+ ret = pci_register_driver(&nhi_driver);
+ if (ret)
+ tb_domain_exit();
+
+ return ret;
+}
+
+static void __exit nhi_unload(void)
+{
+ pci_unregister_driver(&nhi_driver);
+ tb_domain_exit();
+}
+
+rootfs_initcall(nhi_init);
+module_exit(nhi_unload);
diff --git a/drivers/thunderbolt/property.c b/drivers/thunderbolt/property.c
index da2c59a17db5..fb19df8fca4e 100644
--- a/drivers/thunderbolt/property.c
+++ b/drivers/thunderbolt/property.c
@@ -40,6 +40,7 @@ struct tb_property_dir_entry {
static struct tb_property_dir *__tb_property_parse_dir(const u32 *block,
size_t block_len, unsigned int dir_offset, size_t dir_len,
bool is_root, unsigned int depth);
+static struct tb_property *tb_property_copy(const struct tb_property *property);
static inline void parse_dwdata(void *dst, const void *src, size_t dwords)
{
@@ -521,17 +522,9 @@ ssize_t tb_property_format_dir(const struct tb_property_dir *dir, u32 *block,
return ret < 0 ? ret : 0;
}
-/**
- * tb_property_copy_dir() - Take a deep copy of directory
- * @dir: Directory to copy
- *
- * The resulting directory needs to be released by calling tb_property_free_dir().
- *
- * Return: Pointer to &struct tb_property_dir, %NULL in case of failure.
- */
-struct tb_property_dir *tb_property_copy_dir(const struct tb_property_dir *dir)
+static struct tb_property_dir *copy_dir(const struct tb_property_dir *dir)
{
- struct tb_property *property, *p = NULL;
+ struct tb_property *property, *p;
struct tb_property_dir *d;
if (!dir)
@@ -542,57 +535,132 @@ struct tb_property_dir *tb_property_copy_dir(const struct tb_property_dir *dir)
return NULL;
list_for_each_entry(property, &dir->properties, list) {
- struct tb_property *p;
-
- p = tb_property_alloc(property->key, property->type);
+ p = tb_property_copy(property);
if (!p)
goto err_free;
+ list_add_tail(&p->list, &d->properties);
+ }
- p->length = property->length;
+ return d;
- switch (property->type) {
- case TB_PROPERTY_TYPE_DIRECTORY:
- p->value.dir = tb_property_copy_dir(property->value.dir);
- if (!p->value.dir)
- goto err_free;
- break;
+err_free:
+ tb_property_free_dir(d);
+ return NULL;
+}
- case TB_PROPERTY_TYPE_DATA:
- p->value.data = kmemdup(property->value.data,
- property->length * 4,
- GFP_KERNEL);
- if (!p->value.data)
- goto err_free;
- break;
+static struct tb_property *tb_property_copy(const struct tb_property *property)
+{
+ struct tb_property *p;
- case TB_PROPERTY_TYPE_TEXT:
- p->value.text = kzalloc(p->length * 4, GFP_KERNEL);
- if (!p->value.text)
- goto err_free;
- strcpy(p->value.text, property->value.text);
- break;
+ p = tb_property_alloc(property->key, property->type);
+ if (!p)
+ return NULL;
- case TB_PROPERTY_TYPE_VALUE:
- p->value.immediate = property->value.immediate;
- break;
+ p->length = property->length;
+ switch (property->type) {
+ case TB_PROPERTY_TYPE_DIRECTORY:
+ p->value.dir = copy_dir(property->value.dir);
+ if (!p->value.dir)
+ goto err_free;
+ break;
- default:
- break;
- }
+ case TB_PROPERTY_TYPE_DATA:
+ p->value.data = kmemdup(property->value.data,
+ property->length * 4,
+ GFP_KERNEL);
+ if (!p->value.data)
+ goto err_free;
+ break;
- list_add_tail(&p->list, &d->properties);
+ case TB_PROPERTY_TYPE_TEXT:
+ p->value.text = kzalloc(p->length * 4, GFP_KERNEL);
+ if (!p->value.text)
+ goto err_free;
+ strcpy(p->value.text, property->value.text);
+ break;
+
+ case TB_PROPERTY_TYPE_VALUE:
+ p->value.immediate = property->value.immediate;
+ break;
+
+ default:
+ break;
}
- return d;
+ return p;
err_free:
kfree(p);
- tb_property_free_dir(d);
-
return NULL;
}
/**
+ * tb_property_copy_dir() - Take a deep copy of directory
+ * @dir: Directory to copy
+ *
+ * The resulting directory needs to be released by calling tb_property_free_dir().
+ *
+ * Return: Pointer to &struct tb_property_dir, %NULL in case of failure.
+ */
+struct tb_property_dir *tb_property_copy_dir(const struct tb_property_dir *dir)
+{
+ return copy_dir(dir);
+}
+EXPORT_SYMBOL_GPL(tb_property_copy_dir);
+
+/**
+ * tb_property_merge_dir() - Merges directory into parent
+ * @parent: Directory to merge @dir
+ * @dir: Directory that is merged
+ * @replace: Replace existing entries
+ *
+ * This will merge @dir into @parent. Both must have same UUID. The
+ * properties in @dir will overwrite overlapping properties in @parent
+ * if @replace is %true. Contents of @dir is copied (so if it is not
+ * needed afterwards it needs to relesed by calling tb_property_free_dir()).
+ */
+int tb_property_merge_dir(struct tb_property_dir *parent,
+ const struct tb_property_dir *dir,
+ bool replace)
+{
+ const struct tb_property *property;
+
+ if (WARN_ON(parent == dir))
+ return -EINVAL;
+
+ if (!uuid_equal(parent->uuid, dir->uuid))
+ return -EINVAL;
+
+ list_for_each_entry(property, &dir->properties, list) {
+ struct tb_property *p, *tmp;
+
+ tmp = tb_property_copy(property);
+ if (!tmp)
+ return -ENOMEM;
+
+ p = tb_property_find(parent, property->key, property->type);
+ if (p) {
+ if (replace) {
+ /*
+ * Found existing property in parent so
+ * replace with the new one.
+ */
+ list_replace(&p->list, &tmp->list);
+ tb_property_free(p);
+ } else {
+ tb_property_free(tmp);
+ continue;
+ }
+ } else {
+ list_add_tail(&tmp->list, &parent->properties);
+ }
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(tb_property_merge_dir);
+
+/**
* tb_property_add_immediate() - Add immediate property to directory
* @parent: Directory to add the property
* @key: Key for the property
diff --git a/drivers/thunderbolt/stream.c b/drivers/thunderbolt/stream.c
new file mode 100644
index 000000000000..c1f5c55583d0
--- /dev/null
+++ b/drivers/thunderbolt/stream.c
@@ -0,0 +1,1698 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Stream data over Thunderbolt/USB4 cable
+ *
+ * Copyright (C) 2026, Intel Corporation
+ * Authors: Alan Borzeszkowski <alan.borzeszkowski@linux.intel.com>
+ * Mika Westerberg <mika.westerberg@linux.intel.com>
+ */
+
+#define pr_fmt(fmt) "tbstream: " fmt
+
+#include <linux/configfs.h>
+#include <linux/file.h>
+#include <linux/fs.h>
+#include <linux/idr.h>
+#include <linux/miscdevice.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/poll.h>
+#include <linux/sizes.h>
+#include <linux/thunderbolt.h>
+#include <linux/uaccess.h>
+#include <linux/uio.h>
+#include <linux/uuid.h>
+#include <linux/wait.h>
+
+/*
+ * USB4STREAM - Stream data directly over Thunderbolt/USB4 cable
+ *
+ * HopIDs are configured by the user. In Linux this is done through
+ * ConfigFS. Once that is done paths are be established the first time
+ * the stream is opened. Typically the read side is opened first to make
+ * sure all the data will be received.
+ *
+ * End-to-end flow control is mandatory on both sides.
+ *
+ * Data is sent to the other side as tunneled DATA packets. All the data
+ * is owned by the user and passed as-is from the writer to the reader.
+ *
+ * Once the stream device is closed, a CLOSE packet is sent to the peer
+ * so it can take the necessary action. On Linux this typically results
+ * in EOF being returned to the reader.
+ *
+ * Tunneled packet types:
+ *
+ * +-------+---------+------------------+
+ * | PDF | Type | Payload size |
+ * +-------+---------+------------------+
+ * | 2 | DATA | up to 4 KiB |
+ * | 3 | CLOSE | up to 256 bytes |
+ * +-------+---------+------------------+
+ *
+ * Each stream can optionally publish configuration values under its own
+ * XDomain property directory. The name of the directory is the name of
+ * the stream in question and the UUID is up to the stream. For example
+ * if the stream exposes video output then the directory name could be
+ * "video".
+ *
+ * Below values are reserved and can be used by the stream:
+ *
+ * +----------+-----------+-------------------------+
+ * | Key | Type | Contents |
+ * +----------+-----------+-------------------------+
+ * | inhopid | IMMEDIATE | Configured input HopID |
+ * | outhopid | IMMEDIATE | Configured output HopID |
+ * +----------+-----------+-------------------------+
+ *
+ * It is allowed to add more stream specific properties as well if the
+ * above are not enough.
+ */
+
+#define TBSTREAM_DEV_RING_SIZE 256
+#define TBSTREAM_DEV_MIN_RING_SIZE 32
+#define TBSTREAM_DEV_MAX_RING_SIZE 4096
+#define TBSTREAM_DEV_THROTTLING 8192
+#define TBSTREAM_DEV_MAX_THROTTLING 16776960
+
+/**
+ * enum tbstream_frame_pdf - PDF numbers for tunneled frames
+ * @TBSTREAM_FRAME_START: PDF of the start of the frame
+ * @TBSTREAM_DATA: PDF of the DATA frame
+ * @TBSTREAM_CLOSE: PDF of the CLOSE frame
+ */
+enum tbstream_frame_pdf {
+ TBSTREAM_FRAME_START = 1,
+ TBSTREAM_DATA,
+ TBSTREAM_CLOSE,
+};
+
+/**
+ * struct tbstream_frame - Frame submitted to/from the rings
+ * @sdev: Pointer to the stream device
+ * @page: Page holding the packet
+ * @offset: Offset inside @page if partial read is done
+ * @completed: %true if the RX frame is completed
+ * @frame: Underlying frame structure
+ */
+struct tbstream_frame {
+ struct tbstream_dev *sdev;
+ struct page *page;
+ unsigned int offset;
+ bool completed;
+ struct ring_frame frame;
+};
+
+/**
+ * struct tbstream_ring - Stream RX/TX ring structure
+ * @ring: Pointer to the API ring
+ * @prod: Current value of producer
+ * @cons: Current value of consumer
+ * @frames: Holds the ring frames
+ */
+struct tbstream_ring {
+ struct tb_ring *ring;
+ unsigned long prod;
+ unsigned long cons;
+ struct tbstream_frame *frames;
+};
+
+/**
+ * struct tbstream_dev - Stream character device
+ * @group: ConfigFS group for this device
+ * @stream: Pointer to the stream if it is attached (%NULL otherwise)
+ * @misc: Character device used for tunneling
+ * @kref: Reference count
+ * @index: Unique identifier for the character device
+ * @in_hopid: In HopID
+ * @out_hopid: Out HopID
+ * @ring_size: Size of the rings
+ * @throttling: Interrupt throttling rate in ns
+ * @users: Number of times @cdev has been opened
+ * @closed: CLOSE packet was received
+ * @removed: Userspace removed the ConfigFS group underneath.
+ * @wait: Waitqueue for open, read and write
+ * @lock: Lock protecting this structure
+ * @tx_ring: Transmit ring
+ * @rx_ring: Receive ring
+ * @list: Stream devices are linked through this
+ */
+struct tbstream_dev {
+ struct config_group group;
+ struct tbstream *stream;
+ struct miscdevice misc;
+ struct kref kref;
+ int index;
+ int in_hopid;
+ int out_hopid;
+ unsigned int ring_size;
+ unsigned int throttling;
+ int users;
+ bool closed;
+ bool removed;
+ wait_queue_head_t wait;
+ struct mutex lock;
+ struct tbstream_ring tx_ring;
+ struct tbstream_ring rx_ring;
+ struct list_head list;
+};
+
+/**
+ * struct tbstream_group - Config group for stream
+ * @group: ConfigFS group for @stream
+ * @stream: Stream the ConfigFS group is attached to. %NULL if there is
+ * no stream attached.
+ * @lock: Lock protecting this structure
+ * @dev_list: List of stream devices
+ *
+ * This is the ConfigFS directory for one connection to another host.
+ * There can be several &struct stream_dev linked through @dev_list of
+ * this structure. Reference count managed through @group.
+ */
+struct tbstream_group {
+ struct config_group group;
+ struct tbstream *stream;
+ struct mutex lock;
+ struct list_head dev_list;
+};
+
+/**
+ * struct tbstream - Stream service private data
+ * @kref: Reference count
+ * @svc: Pointer to the service device
+ * @list: Streams are linked through this in @stream_list
+ *
+ * This represents the actual physical connection between two hosts.
+ */
+struct tbstream {
+ struct kref kref;
+ struct tb_service *svc;
+ struct list_head list;
+};
+
+static DEFINE_IDA(tbstream_indices);
+
+/* Protects tbstream_list */
+static DEFINE_MUTEX(tbstream_lock);
+static LIST_HEAD(tbstream_list);
+
+/* Serializes tbstream_get()/put() */
+static DEFINE_MUTEX(tbstream_kref_lock);
+
+/* Serializes tbstream_dev_get()/put() */
+static DEFINE_MUTEX(tbstream_dev_kref_lock);
+
+/* Stream property directory UUID: 3a1cb984-c4d9-4469-a277-ce2fdfd11f0d */
+static const uuid_t tbstream_dir_uuid =
+ UUID_INIT(0x3a1cb984, 0xc4d9, 0x4469,
+ 0xa2, 0x77, 0xce, 0x2f, 0xdf, 0xd1, 0x1f, 0x0d);
+
+static struct tb_property_dir *tbstream_dir;
+
+static void tbstream_release(struct kref *kref)
+{
+ struct tbstream *stream = container_of(kref, typeof(*stream), kref);
+
+ tb_service_put(stream->svc);
+ kfree(stream);
+}
+
+static void tbstream_put(struct tbstream *stream)
+{
+ if (stream) {
+ guard(mutex)(&tbstream_kref_lock);
+ kref_put(&stream->kref, tbstream_release);
+ }
+}
+
+static struct tbstream *tbstream_get(struct tbstream *stream)
+{
+ if (stream) {
+ guard(mutex)(&tbstream_kref_lock);
+ kref_get(&stream->kref);
+ }
+ return stream;
+}
+
+static inline bool tbstream_valid(const struct tbstream *stream)
+{
+ if (stream)
+ return !tb_service_parent(stream->svc)->is_unplugged;
+ return false;
+}
+
+static void tbstream_ring_free(struct tbstream_ring *ring)
+{
+ struct device *dma_dev = tb_ring_dma_device(ring->ring);
+ enum dma_data_direction dir;
+ int i;
+
+ if (ring->ring->is_tx)
+ dir = DMA_TO_DEVICE;
+ else
+ dir = DMA_FROM_DEVICE;
+
+ for (i = 0; i < tb_ring_size(ring->ring); i++) {
+ struct tbstream_frame *sf = &ring->frames[i];
+
+ if (sf->frame.buffer_phy)
+ dma_unmap_page(dma_dev, sf->frame.buffer_phy,
+ tb_ring_frame_size(&sf->frame), dir);
+ sf->frame.buffer_phy = 0;
+ if (sf->page)
+ __free_page(sf->page);
+ sf->page = NULL;
+ }
+
+ ring->prod = 0;
+ ring->cons = 0;
+ kfree(ring->frames);
+}
+
+static inline bool tbstream_ring_available(const struct tbstream_ring *ring)
+{
+ return ring->prod > ring->cons;
+}
+
+static inline struct tb_xdomain *tbstream_dev_xdomain(struct tbstream_dev *sdev)
+{
+ if (sdev->stream)
+ return tb_service_parent(sdev->stream->svc);
+ return NULL;
+}
+
+static void tbstream_dev_release(struct kref *kref)
+{
+ struct tbstream_dev *sdev = container_of(kref, struct tbstream_dev, kref);
+
+ if (sdev->stream) {
+ struct tb_xdomain *xd = tbstream_dev_xdomain(sdev);
+
+ if (sdev->out_hopid > 0)
+ tb_xdomain_release_out_hopid(xd, sdev->out_hopid);
+ if (sdev->in_hopid > 0)
+ tb_xdomain_release_in_hopid(xd, sdev->in_hopid);
+
+ tbstream_put(sdev->stream);
+ }
+ ida_free(&tbstream_indices, sdev->index);
+ kfree(sdev->misc.name);
+ kfree(sdev);
+}
+
+static inline void tbstream_dev_put(struct tbstream_dev *sdev)
+{
+ guard(mutex)(&tbstream_dev_kref_lock);
+ kref_put(&sdev->kref, tbstream_dev_release);
+}
+
+static inline struct tbstream_dev *tbstream_dev_get(struct tbstream_dev *sdev)
+{
+ guard(mutex)(&tbstream_dev_kref_lock);
+ kref_get(&sdev->kref);
+ return sdev;
+}
+
+static inline struct tbstream_dev *to_tbstream_dev(struct miscdevice *misc)
+{
+ return container_of(misc, struct tbstream_dev, misc);
+}
+
+static inline int tbstream_dev_valid(const struct tbstream_dev *sdev)
+{
+ const struct tbstream *stream = sdev->stream;
+
+ if (!tbstream_valid(stream))
+ return -ENXIO;
+ if (sdev->in_hopid <= 0 || sdev->out_hopid <= 0)
+ return -EINVAL;
+ return 0;
+}
+
+static inline bool tbstream_dev_removed(const struct tbstream_dev *sdev)
+{
+ return sdev->removed;
+}
+
+static inline bool tbstream_dev_closed(const struct tbstream_dev *sdev)
+{
+ return sdev->closed;
+}
+
+static void
+tbstream_dev_rx_callback(struct tb_ring *ring, struct ring_frame *frame,
+ bool canceled)
+{
+ struct tbstream_frame *sf = container_of(frame, typeof(*sf), frame);
+ struct tbstream_dev *sdev = sf->sdev;
+
+ if (canceled)
+ return;
+
+ sf->completed = true;
+ sdev->rx_ring.prod++;
+
+ if (sf->frame.flags & RING_DESC_CRC_ERROR)
+ pr_warn("RX CRC error\n");
+ else if (sf->frame.flags & RING_DESC_BUFFER_OVERRUN)
+ pr_warn("RX buffer overrun\n");
+ else
+ wake_up_interruptible_poll(&sdev->wait, EPOLLIN | EPOLLRDNORM);
+}
+
+static struct tbstream_frame *
+tbstream_dev_completed_rx(struct tbstream_dev *sdev)
+{
+ struct device *dma_dev = tb_ring_dma_device(sdev->rx_ring.ring);
+ struct tbstream_frame *sf;
+ int index;
+
+ index = sdev->rx_ring.cons % tb_ring_size(sdev->rx_ring.ring);
+ sf = &sdev->rx_ring.frames[index];
+ if (!sf->completed)
+ return NULL;
+
+ dma_sync_single_for_cpu(dma_dev, sf->frame.buffer_phy,
+ tb_ring_frame_size(&sf->frame),
+ DMA_FROM_DEVICE);
+ return sf;
+}
+
+static int tbstream_dev_consume_rx(struct tbstream_dev *sdev)
+{
+ struct device *dma_dev = tb_ring_dma_device(sdev->rx_ring.ring);
+ struct tbstream_frame *sf;
+ int index;
+
+ index = sdev->rx_ring.cons % tb_ring_size(sdev->rx_ring.ring);
+ sdev->rx_ring.cons++;
+
+ sf = &sdev->rx_ring.frames[index];
+ sf->completed = false;
+ sf->offset = 0;
+ sf->frame.size = 0;
+
+ dma_sync_single_for_device(dma_dev, sf->frame.buffer_phy,
+ tb_ring_frame_size(&sf->frame),
+ DMA_FROM_DEVICE);
+
+ return tb_ring_rx(sdev->rx_ring.ring, &sf->frame);
+}
+
+static int tbstream_dev_alloc_rx_buffers(struct tbstream_dev *sdev)
+{
+ size_t ring_size = tb_ring_size(sdev->rx_ring.ring);
+ int i;
+
+ sdev->rx_ring.frames = kcalloc(ring_size, sizeof(struct tbstream_frame),
+ GFP_KERNEL);
+ if (!sdev->rx_ring.frames)
+ return -ENOMEM;
+
+ for (i = 0; i < ring_size; i++) {
+ struct device *dma_dev = tb_ring_dma_device(sdev->rx_ring.ring);
+ struct tbstream_frame *sf = &sdev->rx_ring.frames[i];
+ dma_addr_t dma_addr;
+
+ sf->page = alloc_page(GFP_KERNEL);
+ if (!sf->page)
+ return -ENOMEM;
+
+ dma_addr = dma_map_page(dma_dev, sf->page, 0, TB_MAX_FRAME_SIZE,
+ DMA_FROM_DEVICE);
+ if (dma_mapping_error(dma_dev, dma_addr)) {
+ __free_page(sf->page);
+ sf->page = NULL;
+ return -ENOMEM;
+ }
+
+ sf->sdev = sdev;
+ sf->frame.callback = tbstream_dev_rx_callback;
+ sf->frame.buffer_phy = dma_addr;
+
+ tb_ring_rx(sdev->rx_ring.ring, &sf->frame);
+ }
+
+ sdev->rx_ring.cons = 0;
+ sdev->rx_ring.prod = 0;
+ return 0;
+}
+
+static void
+tbstream_dev_tx_callback(struct tb_ring *ring, struct ring_frame *frame,
+ bool canceled)
+{
+ struct tbstream_frame *sf = container_of(frame, typeof(*sf), frame);
+ struct tbstream_dev *sdev = sf->sdev;
+
+ if (canceled)
+ return;
+
+ sdev->tx_ring.prod++;
+ if (sf->frame.eof == TBSTREAM_DATA)
+ wake_up_interruptible_poll(&sdev->wait, EPOLLOUT | EPOLLWRNORM);
+}
+
+static int tbstream_dev_alloc_tx_buffers(struct tbstream_dev *sdev)
+{
+ struct device *dma_dev = tb_ring_dma_device(sdev->tx_ring.ring);
+ size_t ring_size = tb_ring_size(sdev->tx_ring.ring);
+ int i;
+
+ sdev->tx_ring.frames = kcalloc(ring_size, sizeof(struct tbstream_frame),
+ GFP_KERNEL);
+ if (!sdev->tx_ring.frames)
+ return -ENOMEM;
+
+ for (i = 0; i < ring_size; i++) {
+ struct tbstream_frame *sf = &sdev->tx_ring.frames[i];
+ dma_addr_t dma_addr;
+
+ sf->page = alloc_page(GFP_KERNEL);
+ if (!sf->page)
+ return -ENOMEM;
+
+ dma_addr = dma_map_page(dma_dev, sf->page, 0, TB_MAX_FRAME_SIZE,
+ DMA_TO_DEVICE);
+ if (dma_mapping_error(dma_dev, dma_addr)) {
+ __free_page(sf->page);
+ sf->page = NULL;
+ return -ENOMEM;
+ }
+
+ sf->sdev = sdev;
+ sf->frame.callback = tbstream_dev_tx_callback;
+ sf->frame.buffer_phy = dma_addr;
+ sf->frame.sof = TBSTREAM_FRAME_START;
+ }
+
+ sdev->tx_ring.cons = 0;
+ sdev->tx_ring.prod = ring_size - 1;
+ return 0;
+}
+
+static struct tbstream_frame *
+tbstream_dev_alloc_tx(struct tbstream_dev *sdev, enum tbstream_frame_pdf pdf,
+ struct iov_iter *from, size_t size)
+{
+ struct device *dma_dev = tb_ring_dma_device(sdev->tx_ring.ring);
+ struct tbstream_frame *sf;
+ int index;
+
+ if (!tbstream_ring_available(&sdev->tx_ring))
+ return ERR_PTR(-ENOBUFS);
+
+ index = sdev->tx_ring.cons % tb_ring_size(sdev->tx_ring.ring);
+ sdev->tx_ring.cons++;
+
+ sf = &sdev->tx_ring.frames[index];
+ sf->frame.size = size < TB_MAX_FRAME_SIZE ? size : 0;
+ sf->frame.eof = pdf;
+
+ dma_sync_single_for_cpu(dma_dev, sf->frame.buffer_phy, size,
+ DMA_TO_DEVICE);
+ if (pdf == TBSTREAM_DATA) {
+ if (copy_page_from_iter(sf->page, 0, size, from) != size)
+ return ERR_PTR(-EFAULT);
+ } else {
+ memset(page_address(sf->page), 0, size);
+ }
+ dma_sync_single_for_device(dma_dev, sf->frame.buffer_phy, size,
+ DMA_TO_DEVICE);
+ return sf;
+}
+
+static int
+tbstream_dev_send_data(struct tbstream_dev *sdev, struct iov_iter *from,
+ size_t size)
+{
+ struct tbstream_frame *sf;
+
+ sf = tbstream_dev_alloc_tx(sdev, TBSTREAM_DATA, from, size);
+ if (IS_ERR(sf))
+ return PTR_ERR(sf);
+ return tb_ring_tx(sdev->tx_ring.ring, &sf->frame);
+}
+
+static int tbstream_dev_send_close(struct tbstream_dev *sdev)
+{
+ struct tbstream_frame *sf;
+
+ sf = tbstream_dev_alloc_tx(sdev, TBSTREAM_CLOSE, NULL, SZ_256);
+ if (IS_ERR(sf))
+ return PTR_ERR(sf);
+ return tb_ring_tx(sdev->tx_ring.ring, &sf->frame);
+}
+
+static int tbstream_dev_start(struct tbstream_dev *sdev)
+{
+ struct tb_xdomain *xd = tbstream_dev_xdomain(sdev);
+ u16 sof_mask, eof_mask;
+ struct tb_ring *ring;
+ int ret, e2e_tx_hop;
+
+ ring = tb_ring_alloc_tx(xd->tb->nhi, -1, sdev->ring_size,
+ RING_FLAG_FRAME | RING_FLAG_E2E);
+ if (!ring)
+ return -ENOMEM;
+ sdev->tx_ring.ring = ring;
+
+ ret = tbstream_dev_alloc_tx_buffers(sdev);
+ if (ret)
+ goto err_free_tx;
+
+ e2e_tx_hop = ring->hop;
+ sof_mask = BIT(TBSTREAM_FRAME_START);
+ eof_mask = BIT(TBSTREAM_DATA) | BIT(TBSTREAM_CLOSE);
+
+ ring = tb_ring_alloc_rx(xd->tb->nhi, -1, sdev->ring_size,
+ RING_FLAG_FRAME | RING_FLAG_E2E, e2e_tx_hop,
+ sof_mask, eof_mask, NULL, NULL);
+ if (!ring) {
+ ret = -ENOMEM;
+ goto err_free_tx_buffers;
+ }
+ sdev->rx_ring.ring = ring;
+
+ ret = tb_xdomain_enable_paths(xd, sdev->out_hopid,
+ sdev->tx_ring.ring->hop,
+ sdev->in_hopid,
+ sdev->rx_ring.ring->hop);
+ if (ret)
+ goto err_free_rx;
+
+ tb_ring_throttling(sdev->tx_ring.ring, sdev->throttling);
+ tb_ring_throttling(sdev->rx_ring.ring, sdev->throttling);
+
+ tb_ring_start(sdev->tx_ring.ring);
+ tb_ring_start(sdev->rx_ring.ring);
+
+ ret = tbstream_dev_alloc_rx_buffers(sdev);
+ if (ret)
+ goto err_stop;
+ return 0;
+
+err_stop:
+ tb_ring_stop(sdev->rx_ring.ring);
+ tb_ring_stop(sdev->tx_ring.ring);
+err_free_rx:
+ tb_ring_free(sdev->rx_ring.ring);
+err_free_tx_buffers:
+ tbstream_ring_free(&sdev->tx_ring);
+err_free_tx:
+ tb_ring_free(sdev->tx_ring.ring);
+
+ return ret;
+}
+
+static void tbstream_dev_stop(struct tbstream_dev *sdev)
+{
+ struct tb_xdomain *xd;
+
+ /* Wait for the ring to complete any outstanding frames */
+ tb_ring_flush(sdev->tx_ring.ring, 500);
+ tb_ring_stop(sdev->tx_ring.ring);
+ tb_ring_flush(sdev->rx_ring.ring, 500);
+ tb_ring_stop(sdev->rx_ring.ring);
+
+ xd = tbstream_dev_xdomain(sdev);
+ if (xd) {
+ tb_xdomain_disable_paths(xd, sdev->out_hopid,
+ sdev->tx_ring.ring->hop,
+ sdev->in_hopid,
+ sdev->rx_ring.ring->hop);
+ }
+
+ tbstream_ring_free(&sdev->rx_ring);
+ tb_ring_free(sdev->rx_ring.ring);
+ sdev->rx_ring.ring = NULL;
+ tbstream_ring_free(&sdev->tx_ring);
+ tb_ring_free(sdev->tx_ring.ring);
+ sdev->tx_ring.ring = NULL;
+}
+
+static ssize_t
+tbstream_dev_fops_read_iter(struct kiocb *kiocb, struct iov_iter *to)
+{
+ struct file *file = kiocb->ki_filp;
+ struct tbstream_dev *sdev = to_tbstream_dev(file->private_data);
+ size_t nbytes;
+ int ret;
+
+ ret = tbstream_dev_valid(sdev);
+ if (ret)
+ return ret;
+
+ if (mutex_lock_interruptible(&sdev->lock))
+ return -ERESTARTSYS;
+
+ while (!tbstream_ring_available(&sdev->rx_ring)) {
+ mutex_unlock(&sdev->lock);
+
+ if (file->f_flags & O_NONBLOCK)
+ return -EAGAIN;
+ ret = wait_event_interruptible(sdev->wait,
+ tbstream_ring_available(&sdev->rx_ring) ||
+ tbstream_dev_valid(sdev) != 0 ||
+ tbstream_dev_closed(sdev) ||
+ tbstream_dev_removed(sdev));
+ if (ret)
+ return ret;
+
+ ret = tbstream_dev_valid(sdev);
+ if (ret)
+ return ret;
+
+ if (tbstream_dev_closed(sdev) || tbstream_dev_removed(sdev))
+ return 0;
+
+ if (mutex_lock_interruptible(&sdev->lock))
+ return -ERESTARTSYS;
+ }
+
+ nbytes = 0;
+ while (nbytes < iov_iter_count(to)) {
+ struct tbstream_frame *sf;
+ size_t size, sf_size;
+
+ sf = tbstream_dev_completed_rx(sdev);
+ if (!sf)
+ break;
+ /*
+ * CLOSE tunneled packet. If userspace already read
+ * something then we stop processing now and return
+ * those bytes. Next time the first frame will be CLOSE
+ * in which case we return EOF to the user.
+ */
+ if (sf->frame.eof == TBSTREAM_CLOSE) {
+ if (!nbytes) {
+ tbstream_dev_consume_rx(sdev);
+ sdev->closed = true;
+ }
+ break;
+ }
+
+ sf_size = tb_ring_frame_size(&sf->frame);
+ size = min(iov_iter_count(to) - nbytes, sf_size);
+
+ if (copy_page_to_iter(sf->page, sf->offset, size, to) != size) {
+ ret = -EFAULT;
+ break;
+ }
+
+ /*
+ * If not all data from the frame is read so leave it in
+ * place and update the offset accordingly so next read
+ * gets the rest.
+ */
+ if (size < sf_size) {
+ sf->offset += size;
+ sf->frame.size = sf_size - size;
+ } else {
+ ret = tbstream_dev_consume_rx(sdev);
+ if (ret)
+ break;
+ }
+
+ nbytes += size;
+ }
+
+ mutex_unlock(&sdev->lock);
+ if (ret)
+ return ret;
+ return nbytes;
+}
+
+static ssize_t
+tbstream_dev_fops_write_iter(struct kiocb *kiocb, struct iov_iter *from)
+{
+ struct file *file = kiocb->ki_filp;
+ struct tbstream_dev *sdev = to_tbstream_dev(file->private_data);
+ size_t nbytes;
+ int ret;
+
+ ret = tbstream_dev_valid(sdev);
+ if (ret)
+ return ret;
+
+ if (mutex_lock_interruptible(&sdev->lock))
+ return -ERESTARTSYS;
+
+ while (!tbstream_ring_available(&sdev->tx_ring)) {
+ mutex_unlock(&sdev->lock);
+
+ if (file->f_flags & O_NONBLOCK)
+ return -EAGAIN;
+ ret = wait_event_interruptible(sdev->wait,
+ tbstream_ring_available(&sdev->tx_ring) ||
+ tbstream_dev_valid(sdev) != 0 ||
+ tbstream_dev_closed(sdev) ||
+ tbstream_dev_removed(sdev));
+ if (ret)
+ return ret;
+
+ ret = tbstream_dev_valid(sdev);
+ if (ret)
+ return ret;
+
+ if (tbstream_dev_closed(sdev) || tbstream_dev_removed(sdev))
+ return -ENXIO;
+
+ if (mutex_lock_interruptible(&sdev->lock))
+ return -ERESTARTSYS;
+ }
+
+ nbytes = 0;
+ while (nbytes < iov_iter_count(from)) {
+ size_t size;
+
+ size = min(iov_iter_count(from) - nbytes, TB_MAX_FRAME_SIZE);
+ ret = tbstream_dev_send_data(sdev, from, size);
+ if (ret) {
+ /*
+ * If there are no more buffers we are done for
+ * this write.
+ */
+ if (ret == -ENOBUFS)
+ ret = 0;
+ break;
+ }
+
+ nbytes += size;
+ }
+
+ mutex_unlock(&sdev->lock);
+ if (ret)
+ return ret;
+ return nbytes;
+}
+
+static __poll_t
+tbstream_dev_fops_poll(struct file *file, struct poll_table_struct *wait)
+{
+ struct tbstream_dev *sdev = to_tbstream_dev(file->private_data);
+ __poll_t mask = 0;
+
+ poll_wait(file, &sdev->wait, wait);
+ guard(mutex)(&sdev->lock);
+ if (tbstream_dev_valid(sdev) != 0) {
+ mask |= EPOLLHUP | EPOLLERR;
+ } else {
+ if (tbstream_ring_available(&sdev->tx_ring))
+ mask |= EPOLLOUT | EPOLLWRNORM;
+ if (tbstream_ring_available(&sdev->rx_ring))
+ mask |= EPOLLIN | EPOLLRDNORM;
+ }
+ return mask;
+}
+
+static int tbstream_dev_fops_open(struct inode *inode, struct file *file)
+{
+ struct tbstream_dev *sdev = to_tbstream_dev(file->private_data);
+ int ret;
+
+ tbstream_dev_get(sdev);
+
+ if (mutex_lock_interruptible(&sdev->lock)) {
+ tbstream_dev_put(sdev);
+ return -ERESTARTSYS;
+ }
+
+ /*
+ * If there is no stream attached yet, block until it appears
+ * unless this is opened in non-blocking mode.
+ */
+ while ((ret = tbstream_dev_valid(sdev))) {
+ mutex_unlock(&sdev->lock);
+
+ if (ret != -ENXIO || (file->f_flags & O_NONBLOCK))
+ goto err_put;
+
+ ret = wait_event_interruptible(sdev->wait,
+ tbstream_dev_valid(sdev) == 0 ||
+ tbstream_dev_removed(sdev));
+ if (ret)
+ goto err_put;
+
+ if (tbstream_dev_removed(sdev)) {
+ ret = -ENXIO;
+ goto err_put;
+ }
+
+ if (mutex_lock_interruptible(&sdev->lock)) {
+ ret = -ERESTARTSYS;
+ goto err_put;
+ }
+ }
+
+ /* Only on first open we allocate rings and enable paths */
+ if (!sdev->users++) {
+ ret = tbstream_dev_start(sdev);
+ if (ret) {
+ sdev->users--;
+ goto err_unlock;
+ }
+ sdev->closed = false;
+ }
+
+ mutex_unlock(&sdev->lock);
+ return 0;
+
+err_unlock:
+ mutex_unlock(&sdev->lock);
+err_put:
+ tbstream_dev_put(sdev);
+
+ return ret;
+}
+
+static int tbstream_dev_fops_release(struct inode *inode, struct file *file)
+{
+ struct tbstream_dev *sdev = to_tbstream_dev(file->private_data);
+
+ mutex_lock(&sdev->lock);
+ if (--sdev->users == 0) {
+ /*
+ * Send CLOSE tunneled packet to notify the other end
+ * that we are closing the file. We do this twice if the
+ * first one fails.
+ */
+ tbstream_dev_send_close(sdev);
+ tbstream_dev_stop(sdev);
+ }
+ mutex_unlock(&sdev->lock);
+
+ tbstream_dev_put(sdev);
+ return 0;
+}
+
+static const struct file_operations tbstream_dev_fops = {
+ .owner = THIS_MODULE,
+ .llseek = noop_llseek,
+ .read_iter = tbstream_dev_fops_read_iter,
+ .write_iter = tbstream_dev_fops_write_iter,
+ .poll = tbstream_dev_fops_poll,
+ .open = tbstream_dev_fops_open,
+ .release = tbstream_dev_fops_release,
+};
+
+static inline struct tbstream_dev *
+tbstream_dev_from_group(struct config_group *group)
+{
+ return container_of(group, struct tbstream_dev, group);
+}
+
+static ssize_t tbstream_dev_index_show(struct config_item *item, char *buf)
+{
+ struct config_group *group = to_config_group(item);
+ struct tbstream_dev *sdev = tbstream_dev_from_group(group);
+
+ return sysfs_emit(buf, "%d\n", sdev->index);
+}
+CONFIGFS_ATTR_RO(tbstream_dev_, index);
+
+static ssize_t tbstream_dev_in_hopid_show(struct config_item *item, char *buf)
+{
+ struct config_group *group = to_config_group(item);
+ struct tbstream_dev *sdev = tbstream_dev_from_group(group);
+
+ return sysfs_emit(buf, "%d\n", sdev->in_hopid);
+}
+
+/* svc->lock must be held */
+static void service_remove_properties(struct tb_service *svc, const char *name)
+{
+ struct tb_property *p;
+
+ if (!svc->local_properties)
+ return;
+
+ p = tb_property_find(svc->local_properties, name,
+ TB_PROPERTY_TYPE_DIRECTORY);
+ if (p) {
+ tb_property_free_dir(p->value.dir);
+ tb_property_remove(p);
+
+ dev_dbg(&svc->dev, "removed local directory %s\n", name);
+
+ /*
+ * Is the service directory empty already? If it is then
+ * we can release it as well.
+ */
+ tb_property_for_each(svc->local_properties, p) {
+ if (p->type == TB_PROPERTY_TYPE_DIRECTORY)
+ return;
+ }
+
+ tb_property_free_dir(svc->local_properties);
+ svc->local_properties = NULL;
+ }
+}
+
+static int service_update_properties(struct tb_service *svc, const char *name,
+ int in_hopid, int out_hopid)
+{
+ struct tb_property_dir *dir;
+ struct tb_property *p;
+
+ guard(mutex)(&svc->lock);
+
+ if (in_hopid < 8 || out_hopid < 8) {
+ service_remove_properties(svc, name);
+ return 0;
+ }
+
+ if (!svc->local_properties) {
+ /*
+ * Add the service directory first time we
+ * populate the entries.
+ */
+ svc->local_properties = tb_property_copy_dir(tbstream_dir);
+ if (!svc->local_properties)
+ return -ENOMEM;
+ }
+
+ p = tb_property_find(svc->local_properties, name,
+ TB_PROPERTY_TYPE_DIRECTORY);
+ if (p) {
+ dir = p->value.dir;
+
+ p = tb_property_find(dir, "inhopid", TB_PROPERTY_TYPE_VALUE);
+ if (p && p->value.immediate != in_hopid)
+ p->value.immediate = in_hopid;
+ p = tb_property_find(dir, "outhopid", TB_PROPERTY_TYPE_VALUE);
+ if (p && p->value.immediate != out_hopid)
+ p->value.immediate = out_hopid;
+
+ dev_dbg(&svc->dev,
+ "updated local directory %s: in HopID %d, out HopID %d\n",
+ name, in_hopid, out_hopid);
+ } else {
+ uuid_t uuid;
+ int ret;
+
+ uuid_gen(&uuid);
+ dir = tb_property_create_dir(&uuid);
+ if (!dir)
+ return -ENOMEM;
+
+ tb_property_add_immediate(dir, "inhopid", in_hopid);
+ tb_property_add_immediate(dir, "outhopid", out_hopid);
+
+ ret = tb_property_add_dir(svc->local_properties, name, dir);
+ if (ret) {
+ tb_property_free_dir(dir);
+ return ret;
+ }
+
+ dev_dbg(&svc->dev,
+ "added local directory %s: in HopID %d, out HopID %d\n",
+ name, in_hopid, out_hopid);
+ }
+
+ return 0;
+}
+
+static int tbstream_dev_update_properties(struct tbstream_dev *sdev)
+{
+ struct tbstream *stream;
+ int ret;
+
+ stream = tbstream_get(sdev->stream);
+ if (!stream)
+ return 0;
+
+ ret = service_update_properties(stream->svc,
+ config_item_name(&sdev->group.cg_item),
+ sdev->in_hopid, sdev->out_hopid);
+ if (!ret)
+ tb_service_properties_changed(stream->svc);
+
+ tbstream_put(stream);
+ return ret;
+}
+
+static int tbstream_dev_alloc_in_hopid(struct tbstream_dev *sdev, int hopid)
+{
+ struct tb_xdomain *xd = tbstream_dev_xdomain(sdev);
+ int ret;
+
+ if (sdev->in_hopid > 0 && sdev->in_hopid != hopid)
+ tb_xdomain_release_in_hopid(xd, sdev->in_hopid);
+ if (!hopid) {
+ sdev->in_hopid = hopid;
+ return 0;
+ }
+ ret = tb_xdomain_alloc_in_hopid(xd, hopid);
+ if (ret < 0)
+ return ret;
+ /*
+ * If specific HopID was asked by the user and we did not get
+ * that one then release and return error instead.
+ */
+ if (hopid > 0 && hopid != ret) {
+ tb_xdomain_release_in_hopid(xd, ret);
+ return -EBUSY;
+ }
+ sdev->in_hopid = ret;
+ return 0;
+}
+
+static int tbstream_dev_alloc_out_hopid(struct tbstream_dev *sdev, int hopid)
+{
+ struct tb_xdomain *xd = tbstream_dev_xdomain(sdev);
+ int ret;
+
+ if (sdev->out_hopid > 0 && sdev->out_hopid != hopid)
+ tb_xdomain_release_out_hopid(xd, sdev->out_hopid);
+ if (!hopid) {
+ sdev->out_hopid = hopid;
+ return 0;
+ }
+ ret = tb_xdomain_alloc_out_hopid(xd, hopid);
+ if (ret < 0)
+ return ret;
+ if (hopid > 0 && hopid != ret) {
+ tb_xdomain_release_out_hopid(xd, ret);
+ return -EBUSY;
+ }
+ sdev->out_hopid = ret;
+ return 0;
+}
+
+static ssize_t
+tbstream_dev_in_hopid_store(struct config_item *item, const char *buf,
+ size_t count)
+{
+ struct config_group *group = to_config_group(item);
+ struct tbstream_dev *sdev = tbstream_dev_from_group(group);
+ int ret, in_hopid;
+
+ ret = kstrtoint(buf, 0, &in_hopid);
+ if (ret)
+ return ret;
+
+ guard(mutex)(&sdev->lock);
+ if (sdev->users)
+ return -EBUSY;
+ if (sdev->stream) {
+ ret = tbstream_dev_alloc_in_hopid(sdev, in_hopid);
+ if (ret)
+ return ret;
+ ret = tbstream_dev_update_properties(sdev);
+ } else {
+ sdev->in_hopid = in_hopid;
+ }
+ return ret ? ret : count;
+}
+CONFIGFS_ATTR(tbstream_dev_, in_hopid);
+
+static ssize_t tbstream_dev_out_hopid_show(struct config_item *item, char *buf)
+{
+ struct config_group *group = to_config_group(item);
+ struct tbstream_dev *sdev = tbstream_dev_from_group(group);
+
+ return sysfs_emit(buf, "%d\n", sdev->out_hopid);
+}
+
+static ssize_t
+tbstream_dev_out_hopid_store(struct config_item *item, const char *buf,
+ size_t count)
+{
+ struct config_group *group = to_config_group(item);
+ struct tbstream_dev *sdev = tbstream_dev_from_group(group);
+ int ret, out_hopid;
+
+ ret = kstrtoint(buf, 0, &out_hopid);
+ if (ret)
+ return ret;
+
+ guard(mutex)(&sdev->lock);
+ if (sdev->users)
+ return -EBUSY;
+ if (sdev->stream) {
+ ret = tbstream_dev_alloc_out_hopid(sdev, out_hopid);
+ if (ret)
+ return ret;
+ ret = tbstream_dev_update_properties(sdev);
+ } else {
+ sdev->out_hopid = out_hopid;
+ }
+ return ret ? ret : count;
+}
+CONFIGFS_ATTR(tbstream_dev_, out_hopid);
+
+static ssize_t tbstream_dev_ring_size_show(struct config_item *item, char *buf)
+{
+ struct config_group *group = to_config_group(item);
+ struct tbstream_dev *sdev = tbstream_dev_from_group(group);
+
+ return sysfs_emit(buf, "%u\n", sdev->ring_size);
+}
+
+static ssize_t
+tbstream_dev_ring_size_store(struct config_item *item, const char *buf,
+ size_t count)
+{
+ struct config_group *group = to_config_group(item);
+ struct tbstream_dev *sdev = tbstream_dev_from_group(group);
+ unsigned int ring_size;
+ int ret;
+
+ ret = kstrtouint(buf, 0, &ring_size);
+ if (ret)
+ return ret;
+
+ if (ring_size < TBSTREAM_DEV_MIN_RING_SIZE ||
+ ring_size > TBSTREAM_DEV_MAX_RING_SIZE)
+ return -EINVAL;
+
+ guard(mutex)(&sdev->lock);
+ if (sdev->users)
+ return -EBUSY;
+ sdev->ring_size = ring_size;
+ return count;
+}
+CONFIGFS_ATTR(tbstream_dev_, ring_size);
+
+static ssize_t tbstream_dev_throttling_show(struct config_item *item, char *buf)
+{
+ struct config_group *group = to_config_group(item);
+ struct tbstream_dev *sdev = tbstream_dev_from_group(group);
+
+ return sysfs_emit(buf, "%u\n", sdev->throttling);
+}
+
+static ssize_t
+tbstream_dev_throttling_store(struct config_item *item, const char *buf,
+ size_t count)
+{
+ struct config_group *group = to_config_group(item);
+ struct tbstream_dev *sdev = tbstream_dev_from_group(group);
+ unsigned int throttling;
+ int ret;
+
+ ret = kstrtouint(buf, 0, &throttling);
+ if (ret)
+ return ret;
+
+ if (throttling > TBSTREAM_DEV_MAX_THROTTLING)
+ return -EINVAL;
+
+ guard(mutex)(&sdev->lock);
+ if (sdev->users)
+ return -EBUSY;
+ sdev->throttling = throttling;
+ return count;
+}
+CONFIGFS_ATTR(tbstream_dev_, throttling);
+
+static struct configfs_attribute *tbstream_dev_attrs[] = {
+ &tbstream_dev_attr_index,
+ &tbstream_dev_attr_in_hopid,
+ &tbstream_dev_attr_out_hopid,
+ &tbstream_dev_attr_ring_size,
+ &tbstream_dev_attr_throttling,
+ NULL,
+};
+
+static void tbstream_dev_item_release(struct config_item *item)
+{
+ struct config_group *group = to_config_group(item);
+ struct tbstream_dev *sdev = tbstream_dev_from_group(group);
+
+ misc_deregister(&sdev->misc);
+ tbstream_dev_put(sdev);
+}
+
+static struct configfs_item_operations tbstream_dev_item_ops = {
+ .release = tbstream_dev_item_release,
+};
+
+static const struct config_item_type tbstream_dev_type = {
+ .ct_owner = THIS_MODULE,
+ .ct_item_ops = &tbstream_dev_item_ops,
+ .ct_attrs = tbstream_dev_attrs,
+};
+
+static void service_get_hopids(struct tb_service *svc, const char *name,
+ int *in_hopid, int *out_hopid)
+{
+ struct tb_property_dir *dir;
+ struct tb_property *p;
+
+ guard(mutex)(&svc->lock);
+
+ /* See if we have directory entry with the matching name */
+ p = tb_property_find(svc->remote_properties, name,
+ TB_PROPERTY_TYPE_DIRECTORY);
+ if (!p)
+ return;
+
+ dir = p->value.dir;
+
+ /*
+ * We need to reverse the HopIDs on our end so that in becomes
+ * out and vice versa.
+ */
+ p = tb_property_find(dir, "inhopid", TB_PROPERTY_TYPE_VALUE);
+ if (p && p->value.immediate >= 8)
+ *out_hopid = p->value.immediate;
+ p = tb_property_find(dir, "outhopid", TB_PROPERTY_TYPE_VALUE);
+ if (p && p->value.immediate >= 8)
+ *in_hopid = p->value.immediate;
+}
+
+static void
+tbstream_dev_attach_stream(struct tbstream_dev *sdev, struct tbstream_group *sg)
+{
+ const char *name = config_item_name(&sdev->group.cg_item);
+ struct tbstream *stream;
+
+ stream = tbstream_get(sg->stream);
+ if (!stream)
+ return;
+
+ scoped_guard(mutex, &sdev->lock) {
+ sdev->stream = stream;
+ /*
+ * If there is no existing configuration (or automatic
+ * configuration is being used) check if the other side
+ * has configuration for this and use it.
+ */
+ if (sdev->in_hopid <= 0 && sdev->out_hopid <= 0)
+ service_get_hopids(stream->svc, name, &sdev->in_hopid,
+ &sdev->out_hopid);
+ if (sdev->in_hopid)
+ tbstream_dev_alloc_in_hopid(sdev, sdev->in_hopid);
+ if (sdev->out_hopid)
+ tbstream_dev_alloc_out_hopid(sdev, sdev->out_hopid);
+ }
+
+ service_update_properties(stream->svc, name, sdev->in_hopid,
+ sdev->out_hopid);
+ tb_service_properties_changed(stream->svc);
+
+ /* Notify any openerers that the stream is now attached */
+ wake_up_interruptible(&sdev->wait);
+}
+
+static void tbstream_dev_detach_stream(struct tbstream_dev *sdev)
+{
+ const char *name = config_item_name(&sdev->group.cg_item);
+ struct tbstream *stream;
+ struct tb_xdomain *xd;
+
+ scoped_guard(mutex, &sdev->lock) {
+ stream = sdev->stream;
+ if (!stream)
+ return;
+ sdev->stream = NULL;
+ xd = tb_service_parent(stream->svc);
+ if (sdev->out_hopid > 0)
+ tb_xdomain_release_out_hopid(xd, sdev->out_hopid);
+ if (sdev->in_hopid > 0)
+ tb_xdomain_release_in_hopid(xd, sdev->in_hopid);
+ }
+
+ service_update_properties(stream->svc, name, 0, 0);
+ tb_service_properties_changed(stream->svc);
+
+ tbstream_put(stream);
+
+ /* Notify any task that the stream is not valid anymore */
+ wake_up_interruptible_poll(&sdev->wait, EPOLLHUP | EPOLLERR);
+}
+
+static inline struct tbstream_group *
+to_tbstream_group(struct config_group *group)
+{
+ return container_of(group, struct tbstream_group, group);
+}
+
+static struct config_group *
+tbstream_dev_make_group(struct config_group *group, const char *name)
+{
+ struct tbstream_group *sg = to_tbstream_group(group);
+ struct tbstream_dev *sdev;
+ int ret, index;
+
+ /*
+ * We want the names to be suitable for passing as property
+ * directory names.
+ */
+ if (strlen(name) > TB_PROPERTY_KEY_SIZE)
+ return ERR_PTR(-ENAMETOOLONG);
+
+ sdev = kzalloc_obj(*sdev, GFP_KERNEL);
+ if (!sdev)
+ return ERR_PTR(-ENOMEM);
+
+ index = ida_alloc(&tbstream_indices, GFP_KERNEL);
+ if (index < 0) {
+ kfree(sdev);
+ return ERR_PTR(index);
+ }
+
+ sdev->index = index;
+ sdev->ring_size = TBSTREAM_DEV_RING_SIZE;
+ sdev->throttling = TBSTREAM_DEV_THROTTLING;
+ mutex_init(&sdev->lock);
+ init_waitqueue_head(&sdev->wait);
+ INIT_LIST_HEAD(&sdev->list);
+ /* This point forward tbstream_dev_put() must be used to release sdev */
+ kref_init(&sdev->kref);
+
+ config_group_init_type_name(&sdev->group, name, &tbstream_dev_type);
+
+ scoped_guard(mutex, &sg->lock)
+ list_add_tail(&sdev->list, &sg->dev_list);
+
+ tbstream_dev_attach_stream(sdev, sg);
+
+ sdev->misc.name = kasprintf(GFP_KERNEL, "tbstream%d", index);
+ sdev->misc.minor = MISC_DYNAMIC_MINOR;
+ sdev->misc.fops = &tbstream_dev_fops;
+
+ ret = misc_register(&sdev->misc);
+ if (ret) {
+ tbstream_dev_detach_stream(sdev);
+ scoped_guard(mutex, &sg->lock)
+ list_del(&sdev->list);
+ /* Calls tbstream_dev_put() */
+ config_group_put(&sdev->group);
+ return ERR_PTR(ret);
+ }
+
+ return &sdev->group;
+}
+
+static void
+tbstream_dev_drop_item(struct config_group *group, struct config_item *item)
+{
+ struct config_group *sdev_group = to_config_group(item);
+ struct tbstream_dev *sdev = tbstream_dev_from_group(sdev_group);
+ struct tbstream_group *sg = to_tbstream_group(group);
+
+ scoped_guard(mutex, &sg->lock)
+ list_del(&sdev->list);
+ /* Notify any task that the underlying group was removed */
+ sdev->removed = true;
+ wake_up_interruptible_poll(&sdev->wait, EPOLLHUP | EPOLLERR);
+ config_item_put(item);
+}
+
+static struct configfs_group_operations tbstream_dev_group_ops = {
+ .make_group = tbstream_dev_make_group,
+ .drop_item = tbstream_dev_drop_item,
+};
+
+static void tbstream_item_release(struct config_item *item)
+{
+ struct config_group *group = to_config_group(item);
+ struct tbstream_group *sg = to_tbstream_group(group);
+
+ tbstream_put(sg->stream);
+ kfree(sg);
+}
+
+static struct configfs_item_operations tbstream_item_ops = {
+ .release = tbstream_item_release,
+};
+
+static const struct config_item_type tbstream_dev_group_type = {
+ .ct_owner = THIS_MODULE,
+ .ct_group_ops = &tbstream_dev_group_ops,
+ .ct_item_ops = &tbstream_item_ops,
+};
+
+static struct config_group *
+tbstream_make_group(struct config_group *group, const char *name)
+{
+ struct tbstream_group *sg;
+ struct tbstream *stream;
+ int domain, index;
+ u64 route;
+
+ /* Make sure the format is correct */
+ if (sscanf(name, "%u-%llx.%u", &domain, &route, &index) != 3)
+ return ERR_PTR(-EINVAL);
+
+ sg = kzalloc_obj(*sg, GFP_KERNEL);
+ if (!sg)
+ return ERR_PTR(-ENOMEM);
+
+ mutex_init(&sg->lock);
+ INIT_LIST_HEAD(&sg->dev_list);
+
+ guard(mutex)(&tbstream_lock);
+ list_for_each_entry(stream, &tbstream_list, list) {
+ tbstream_get(stream);
+ if (sysfs_streq(name, dev_name(&stream->svc->dev))) {
+ sg->stream = stream;
+ break;
+ }
+ tbstream_put(stream);
+ }
+
+ config_group_init_type_name(&sg->group, name, &tbstream_dev_group_type);
+ return &sg->group;
+}
+
+static struct configfs_group_operations tbstream_group_ops = {
+ .make_group = tbstream_make_group,
+};
+
+static const struct config_item_type tbstream_group_type = {
+ .ct_owner = THIS_MODULE,
+ .ct_group_ops = &tbstream_group_ops,
+};
+
+static struct config_group tbstream_group = {
+ .cg_item = {
+ .ci_namebuf = "stream",
+ .ci_type = &tbstream_group_type,
+ },
+};
+
+/* Returns reference count increased */
+static struct tbstream_group *tbstream_group_find(struct tbstream *stream)
+{
+ const char *name = dev_name(&stream->svc->dev);
+ struct config_item *item;
+
+ guard(mutex)(&tbstream_group.cg_subsys->su_mutex);
+ item = config_group_find_item(&tbstream_group, name);
+ if (!item)
+ return NULL;
+ return to_tbstream_group(to_config_group(item));
+}
+
+static void tbstream_group_attach_stream(struct tbstream *stream)
+{
+ struct tbstream_group *sg;
+ struct tbstream_dev *sdev;
+
+ sg = tbstream_group_find(stream);
+ if (!sg)
+ return;
+
+ guard(mutex)(&sg->lock);
+ if (WARN_ON(sg->stream)) {
+ config_group_put(&sg->group);
+ return;
+ }
+ sg->stream = tbstream_get(stream);
+ /*
+ * If there are existing stream devices, attach the stream to
+ * them now.
+ */
+ list_for_each_entry(sdev, &sg->dev_list, list) {
+ tbstream_dev_get(sdev);
+ tbstream_dev_attach_stream(sdev, sg);
+ tbstream_dev_put(sdev);
+ }
+
+ config_group_put(&sg->group);
+}
+
+static void tbstream_group_detach_stream(struct tbstream *stream)
+{
+ struct tbstream_group *sg;
+ struct tbstream_dev *sdev;
+
+ sg = tbstream_group_find(stream);
+ if (!sg)
+ return;
+
+ guard(mutex)(&sg->lock);
+ if (sg->stream) {
+ /* Detach this stream from the stream devices */
+ list_for_each_entry_reverse(sdev, &sg->dev_list, list) {
+ tbstream_dev_get(sdev);
+ tbstream_dev_detach_stream(sdev);
+ tbstream_dev_put(sdev);
+ }
+ tbstream_put(sg->stream);
+ sg->stream = NULL;
+ }
+
+ config_group_put(&sg->group);
+}
+
+static int tbstream_probe(struct tb_service *svc, const struct tb_service_id *id)
+{
+ struct tbstream *stream;
+
+ stream = kzalloc_obj(*stream, GFP_KERNEL);
+ if (!stream)
+ return -ENOMEM;
+
+ /* After this point, release stream by calling tbstream_put() */
+ kref_init(&stream->kref);
+ stream->svc = tb_service_get(svc);
+ INIT_LIST_HEAD(&stream->list);
+
+ scoped_guard(mutex, &tbstream_lock)
+ list_add_tail(&stream->list, &tbstream_list);
+
+ tbstream_group_attach_stream(stream);
+ tb_service_set_drvdata(svc, stream);
+ return 0;
+}
+
+static void tbstream_remove(struct tb_service *svc)
+{
+ struct tbstream *stream = tb_service_get_drvdata(svc);
+
+ tbstream_group_detach_stream(stream);
+ scoped_guard(mutex, &tbstream_lock)
+ list_del(&stream->list);
+ tbstream_put(stream);
+}
+
+static int __maybe_unused tbstream_suspend(struct device *dev)
+{
+ struct tb_service *svc = tb_to_service(dev);
+ struct tbstream *stream = tb_service_get_drvdata(svc);
+ struct tbstream_group *sg;
+ struct tbstream_dev *sdev;
+
+ sg = tbstream_group_find(stream);
+ if (!sg)
+ return 0;
+
+ list_for_each_entry_reverse(sdev, &sg->dev_list, list) {
+ tbstream_dev_get(sdev);
+ /* Stop the stream (if it was open) */
+ if (sdev->users)
+ tbstream_dev_stop(sdev);
+ tbstream_dev_put(sdev);
+ }
+
+ config_group_put(&sg->group);
+ return 0;
+}
+
+static int __maybe_unused tbstream_resume(struct device *dev)
+{
+ struct tb_service *svc = tb_to_service(dev);
+ struct tbstream *stream = tb_service_get_drvdata(svc);
+ struct tbstream_group *sg;
+ struct tbstream_dev *sdev;
+
+ sg = tbstream_group_find(stream);
+ if (!sg)
+ return 0;
+
+ list_for_each_entry(sdev, &sg->dev_list, list) {
+ tbstream_dev_get(sdev);
+ if (sdev->users) {
+ int ret;
+
+ ret = tbstream_dev_start(sdev);
+ if (ret) {
+ tbstream_dev_put(sdev);
+ config_group_put(&sg->group);
+ return ret;
+ }
+ }
+ tbstream_dev_put(sdev);
+ }
+
+ config_group_put(&sg->group);
+ return 0;
+}
+
+static const struct dev_pm_ops tbstream_pm_ops = {
+ SET_SYSTEM_SLEEP_PM_OPS(tbstream_suspend, tbstream_resume)
+};
+
+static const struct tb_service_id tbstream_ids[] = {
+ { TB_SERVICE("stream", 1) },
+ { },
+};
+MODULE_DEVICE_TABLE(tbsvc, tbstream_ids);
+
+static struct tb_service_driver tbstream_driver = {
+ .driver = {
+ .owner = THIS_MODULE,
+ .name = "thunderbolt_stream",
+ .pm = &tbstream_pm_ops,
+ },
+ .probe = tbstream_probe,
+ .remove = tbstream_remove,
+ .id_table = tbstream_ids,
+};
+
+static int __init tbstream_init(void)
+{
+ int ret;
+
+ tbstream_dir = tb_property_create_dir(&tbstream_dir_uuid);
+ if (!tbstream_dir)
+ return -ENOMEM;
+
+ tb_property_add_immediate(tbstream_dir, "prtcid", 1);
+ tb_property_add_immediate(tbstream_dir, "prtcvers", 1);
+ tb_property_add_immediate(tbstream_dir, "prtcrevs", 0);
+ tb_property_add_immediate(tbstream_dir, "prtcstns", 0);
+
+ ret = tb_register_property_dir("stream", tbstream_dir);
+ if (ret)
+ goto err_free_dir;
+
+ config_group_init(&tbstream_group);
+ ret = tb_configfs_register_group(&tbstream_group);
+ if (ret)
+ goto err_unregister_dir;
+
+ ret = tb_register_service_driver(&tbstream_driver);
+ if (ret)
+ goto err_unregister_group;
+ return 0;
+
+err_unregister_group:
+ tb_configfs_unregister_group(&tbstream_group);
+err_unregister_dir:
+ tb_unregister_property_dir("stream", tbstream_dir);
+err_free_dir:
+ tb_property_free_dir(tbstream_dir);
+ return ret;
+}
+module_init(tbstream_init);
+
+static void __exit tbstream_exit(void)
+{
+ tb_unregister_service_driver(&tbstream_driver);
+ tb_configfs_unregister_group(&tbstream_group);
+ tb_unregister_property_dir("stream", tbstream_dir);
+ tb_property_free_dir(tbstream_dir);
+ ida_destroy(&tbstream_indices);
+}
+module_exit(tbstream_exit);
+
+MODULE_AUTHOR("Alan Borzeszkowski <alan.borzeszkowski@linux.intel.com>");
+MODULE_AUTHOR("Mika Westerberg <mika.westerberg@linux.intel.com>");
+MODULE_DESCRIPTION("Stream data over Thunderbolt/USB4 cable");
+MODULE_LICENSE("GPL");
diff --git a/drivers/thunderbolt/switch.c b/drivers/thunderbolt/switch.c
index c2ad58b19e7b..a830c82bb905 100644
--- a/drivers/thunderbolt/switch.c
+++ b/drivers/thunderbolt/switch.c
@@ -209,30 +209,6 @@ static int nvm_authenticate_device_dma_port(struct tb_switch *sw)
return -ETIMEDOUT;
}
-static void nvm_authenticate_start_dma_port(struct tb_switch *sw)
-{
- struct pci_dev *root_port;
-
- /*
- * During host router NVM upgrade we should not allow root port to
- * go into D3cold because some root ports cannot trigger PME
- * itself. To be on the safe side keep the root port in D0 during
- * the whole upgrade process.
- */
- root_port = pcie_find_root_port(sw->tb->nhi->pdev);
- if (root_port)
- pm_runtime_get_noresume(&root_port->dev);
-}
-
-static void nvm_authenticate_complete_dma_port(struct tb_switch *sw)
-{
- struct pci_dev *root_port;
-
- root_port = pcie_find_root_port(sw->tb->nhi->pdev);
- if (root_port)
- pm_runtime_put(&root_port->dev);
-}
-
static inline bool nvm_readable(struct tb_switch *sw)
{
if (tb_switch_is_usb4(sw)) {
@@ -258,6 +234,7 @@ static inline bool nvm_upgradeable(struct tb_switch *sw)
static int nvm_authenticate(struct tb_switch *sw, bool auth_only)
{
+ struct tb_nhi *nhi = sw->tb->nhi;
int ret;
if (tb_switch_is_usb4(sw)) {
@@ -274,7 +251,8 @@ static int nvm_authenticate(struct tb_switch *sw, bool auth_only)
sw->nvm->authenticating = true;
if (!tb_route(sw)) {
- nvm_authenticate_start_dma_port(sw);
+ if (nhi->ops->pre_nvm_auth)
+ nhi->ops->pre_nvm_auth(nhi);
ret = nvm_authenticate_host_dma_port(sw);
} else {
ret = nvm_authenticate_device_dma_port(sw);
@@ -1620,6 +1598,12 @@ static int tb_switch_reset_host(struct tb_switch *sw)
ret = tb_port_reset(port);
if (ret)
return ret;
+ /*
+ * USB4 Lane 1 adapters do not have accessible
+ * path config space.
+ */
+ if (tb_switch_is_usb4(sw) && !port->usb4)
+ continue;
} else if (tb_port_is_usb3_down(port) ||
tb_port_is_usb3_up(port)) {
tb_usb3_port_enable(port, false);
@@ -1761,8 +1745,6 @@ int tb_switch_wait_for_bit(struct tb_switch *sw, u32 offset, u32 bit,
/*
* tb_plug_events_active() - enable/disable plug events on a switch
*
- * Also configures a sane plug_events_delay of 255ms.
- *
* Return: %0 on success, negative errno otherwise.
*/
static int tb_plug_events_active(struct tb_switch *sw, bool active)
@@ -1773,11 +1755,6 @@ static int tb_plug_events_active(struct tb_switch *sw, bool active)
if (tb_switch_is_icm(sw) || tb_switch_is_usb4(sw))
return 0;
- sw->config.plug_events_delay = 0xff;
- res = tb_sw_write(sw, ((u32 *) &sw->config) + 4, TB_CFG_SWITCH, 4, 1);
- if (res)
- return res;
-
res = tb_sw_read(sw, &data, TB_CFG_SWITCH, sw->cap_plug_events + 1, 1);
if (res)
return res;
@@ -2639,6 +2616,8 @@ int tb_switch_configure(struct tb_switch *sw)
sw->config.enabled = 1;
+ /* Set Notification Timeout to 255 ms for all routers */
+ sw->config.plug_events_delay = 0xff;
if (tb_switch_is_usb4(sw)) {
/*
* For USB4 devices, we need to program the CM version
@@ -2650,7 +2629,6 @@ int tb_switch_configure(struct tb_switch *sw)
sw->config.cmuv = ROUTER_CS_4_CMUV_V1;
else
sw->config.cmuv = ROUTER_CS_4_CMUV_V2;
- sw->config.plug_events_delay = 0xa;
/* Enumerate the switch */
ret = tb_sw_write(sw, (u32 *)&sw->config + 1, TB_CFG_SWITCH,
@@ -2671,7 +2649,7 @@ int tb_switch_configure(struct tb_switch *sw)
/* Enumerate the switch */
ret = tb_sw_write(sw, (u32 *)&sw->config + 1, TB_CFG_SWITCH,
- ROUTER_CS_1, 3);
+ ROUTER_CS_1, 4);
}
if (ret)
return ret;
@@ -2743,6 +2721,7 @@ static int tb_switch_set_uuid(struct tb_switch *sw)
static int tb_switch_add_dma_port(struct tb_switch *sw)
{
+ struct tb_nhi *nhi = sw->tb->nhi;
u32 status;
int ret;
@@ -2802,8 +2781,10 @@ static int tb_switch_add_dma_port(struct tb_switch *sw)
*/
nvm_get_auth_status(sw, &status);
if (status) {
- if (!tb_route(sw))
- nvm_authenticate_complete_dma_port(sw);
+ if (!tb_route(sw)) {
+ if (nhi->ops->post_nvm_auth)
+ nhi->ops->post_nvm_auth(nhi);
+ }
return 0;
}
@@ -2817,8 +2798,10 @@ static int tb_switch_add_dma_port(struct tb_switch *sw)
return ret;
/* Now we can allow root port to suspend again */
- if (!tb_route(sw))
- nvm_authenticate_complete_dma_port(sw);
+ if (!tb_route(sw)) {
+ if (nhi->ops->post_nvm_auth)
+ nhi->ops->post_nvm_auth(nhi);
+ }
if (status) {
tb_sw_info(sw, "switch flash authentication failed\n");
@@ -2971,14 +2954,14 @@ static int tb_switch_lane_bonding_enable(struct tb_switch *sw)
int ret;
if (!tb_switch_lane_bonding_possible(sw))
- return 0;
+ return -EOPNOTSUPP;
up = tb_upstream_port(sw);
down = tb_switch_downstream_port(sw);
if (!tb_port_width_supported(up, TB_LINK_WIDTH_DUAL) ||
!tb_port_width_supported(down, TB_LINK_WIDTH_DUAL))
- return 0;
+ return -EOPNOTSUPP;
/*
* Both lanes need to be in CL0. Here we assume lane 0 already be in
@@ -3625,6 +3608,20 @@ int tb_switch_resume(struct tb_switch *sw, bool runtime)
tb_port_warn(port,
"lost during suspend, disconnecting\n");
tb_sw_set_unplugged(port->remote->sw);
+ } else if (port->xdomain) {
+ /*
+ * If the user replaced the XDomain with
+ * another router, this will succeed in
+ * which case we must remove the XDomain
+ * before adding the new router.
+ */
+ err = tb_cfg_get_upstream_port(sw->tb->ctl,
+ port->xdomain->route);
+ if (err > 0) {
+ tb_port_warn(port,
+ "XDomain was disconnected\n");
+ port->xdomain->is_unplugged = true;
+ }
}
}
}
diff --git a/drivers/thunderbolt/tb.c b/drivers/thunderbolt/tb.c
index c69c323e6952..76323255439a 100644
--- a/drivers/thunderbolt/tb.c
+++ b/drivers/thunderbolt/tb.c
@@ -98,7 +98,7 @@ static void tb_queue_hotplug(struct tb *tb, u64 route, u8 port, bool unplug)
if (!ev)
return;
- ev->tb = tb;
+ ev->tb = tb_domain_get(tb);
ev->route = route;
ev->port = port;
ev->unplug = unplug;
@@ -2524,9 +2524,14 @@ put_sw:
out:
mutex_unlock(&tb->lock);
+ tb_domain_unregister_unplugged_xdomains(tb);
+
pm_runtime_mark_last_busy(&tb->dev);
pm_runtime_put_autosuspend(&tb->dev);
+ /* Undo the refcount increased in tb_queue_hotplug() */
+ tb_domain_put(tb);
+
kfree(ev);
}
@@ -2844,6 +2849,9 @@ static void tb_handle_dp_bandwidth_request(struct work_struct *work)
/* Update other clients about the allocation change */
tb_recalc_estimated_bandwidth(tb);
+
+ tb_dbg(tb, "checking if more DP tunnels can be established now\n");
+ tb_tunnel_dp(tb);
}
put_sw:
@@ -2949,6 +2957,7 @@ static void tb_stop(struct tb *tb)
tb_tunnel_put(tunnel);
}
tb_switch_remove(tb->root_switch);
+ tb->root_switch = NULL;
tcm->hotplug_active = false; /* signal tb_handle_hotplug to quit */
}
@@ -2991,7 +3000,8 @@ static int tb_start(struct tb *tb, bool reset)
tb->root_switch = tb_switch_alloc(tb, &tb->dev, 0);
if (IS_ERR(tb->root_switch))
- return PTR_ERR(tb->root_switch);
+ return dev_err_probe(tb->nhi->dev, PTR_ERR(tb->root_switch),
+ "failed to allocate host router\n");
/*
* ICM firmware upgrade needs running firmware and in native
@@ -3008,14 +3018,14 @@ static int tb_start(struct tb *tb, bool reset)
ret = tb_switch_configure(tb->root_switch);
if (ret) {
tb_switch_put(tb->root_switch);
- return ret;
+ return dev_err_probe(tb->nhi->dev, ret, "failed to configure host router\n");
}
/* Announce the switch to the world */
ret = tb_switch_add(tb->root_switch);
if (ret) {
tb_switch_put(tb->root_switch);
- return ret;
+ return dev_err_probe(tb->nhi->dev, ret, "failed to add host router\n");
}
/*
@@ -3110,6 +3120,24 @@ static void tb_restore_children(struct tb_switch *sw)
}
}
+static void tb_free_unplugged_xdomains(struct tb_switch *sw)
+{
+ struct tb_port *port;
+
+ tb_switch_for_each_port(sw, port) {
+ if (tb_is_upstream_port(port))
+ continue;
+ if (port->xdomain && port->xdomain->is_unplugged) {
+ tb_retimer_remove_all(port);
+ tb_xdomain_remove(port->xdomain);
+ tb_port_unconfigure_xdomain(port);
+ port->xdomain = NULL;
+ } else if (port->remote) {
+ tb_free_unplugged_xdomains(port->remote->sw);
+ }
+ }
+}
+
static int tb_resume_noirq(struct tb *tb)
{
struct tb_cm *tcm = tb_priv(tb);
@@ -3129,6 +3157,7 @@ static int tb_resume_noirq(struct tb *tb)
tb_switch_resume(tb->root_switch, false);
tb_free_invalid_tunnels(tb);
tb_free_unplugged_children(tb->root_switch);
+ tb_free_unplugged_xdomains(tb->root_switch);
tb_restore_children(tb->root_switch);
/*
@@ -3171,28 +3200,6 @@ static int tb_resume_noirq(struct tb *tb)
return 0;
}
-static int tb_free_unplugged_xdomains(struct tb_switch *sw)
-{
- struct tb_port *port;
- int ret = 0;
-
- tb_switch_for_each_port(sw, port) {
- if (tb_is_upstream_port(port))
- continue;
- if (port->xdomain && port->xdomain->is_unplugged) {
- tb_retimer_remove_all(port);
- tb_xdomain_remove(port->xdomain);
- tb_port_unconfigure_xdomain(port);
- port->xdomain = NULL;
- ret++;
- } else if (port->remote) {
- ret += tb_free_unplugged_xdomains(port->remote->sw);
- }
- }
-
- return ret;
-}
-
static int tb_freeze_noirq(struct tb *tb)
{
struct tb_cm *tcm = tb_priv(tb);
@@ -3212,14 +3219,14 @@ static int tb_thaw_noirq(struct tb *tb)
static void tb_complete(struct tb *tb)
{
/*
- * Release any unplugged XDomains and if there is a case where
+ * Unregister unplugged XDomains and if there is a case where
* another domain is swapped in place of unplugged XDomain we
* need to run another rescan.
*/
- mutex_lock(&tb->lock);
- if (tb_free_unplugged_xdomains(tb->root_switch))
- tb_scan_switch(tb->root_switch);
- mutex_unlock(&tb->lock);
+ if (tb_domain_unregister_unplugged_xdomains(tb)) {
+ scoped_guard(mutex, &tb->lock)
+ tb_scan_switch(tb->root_switch);
+ }
}
static int tb_runtime_suspend(struct tb *tb)
@@ -3246,11 +3253,11 @@ static void tb_remove_work(struct work_struct *work)
struct tb *tb = tcm_to_tb(tcm);
mutex_lock(&tb->lock);
- if (tb->root_switch) {
+ if (tb->root_switch)
tb_free_unplugged_children(tb->root_switch);
- tb_free_unplugged_xdomains(tb->root_switch);
- }
mutex_unlock(&tb->lock);
+
+ tb_free_unplugged_xdomains(tb->root_switch);
}
static int tb_runtime_resume(struct tb *tb)
@@ -3304,13 +3311,14 @@ static const struct tb_cm_ops tb_cm_ops = {
*/
static bool tb_apple_add_links(struct tb_nhi *nhi)
{
+ struct pci_dev *nhi_pdev = to_pci_dev(nhi->dev);
struct pci_dev *upstream, *pdev;
bool ret;
if (!x86_apple_machine)
return false;
- switch (nhi->pdev->device) {
+ switch (nhi_pdev->device) {
case PCI_DEVICE_ID_INTEL_LIGHT_RIDGE:
case PCI_DEVICE_ID_INTEL_CACTUS_RIDGE_4C:
case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI:
@@ -3320,7 +3328,7 @@ static bool tb_apple_add_links(struct tb_nhi *nhi)
return false;
}
- upstream = pci_upstream_bridge(nhi->pdev);
+ upstream = pci_upstream_bridge(nhi_pdev);
while (upstream) {
if (!pci_is_pcie(upstream))
return false;
@@ -3347,15 +3355,15 @@ static bool tb_apple_add_links(struct tb_nhi *nhi)
!pdev->is_pciehp)
continue;
- link = device_link_add(&pdev->dev, &nhi->pdev->dev,
+ link = device_link_add(&pdev->dev, nhi->dev,
DL_FLAG_AUTOREMOVE_SUPPLIER |
DL_FLAG_PM_RUNTIME);
if (link) {
- dev_dbg(&nhi->pdev->dev, "created link from %s\n",
+ dev_dbg(nhi->dev, "created link from %s\n",
dev_name(&pdev->dev));
ret = true;
} else {
- dev_warn(&nhi->pdev->dev, "device link creation from %s failed\n",
+ dev_warn(nhi->dev, "device link creation from %s failed\n",
dev_name(&pdev->dev));
}
}
diff --git a/drivers/thunderbolt/tb.h b/drivers/thunderbolt/tb.h
index 217c3114bec8..ec9192b61bc0 100644
--- a/drivers/thunderbolt/tb.h
+++ b/drivers/thunderbolt/tb.h
@@ -725,11 +725,11 @@ static inline int tb_port_write(struct tb_port *port, const void *buffer,
length);
}
-#define tb_err(tb, fmt, arg...) dev_err(&(tb)->nhi->pdev->dev, fmt, ## arg)
-#define tb_WARN(tb, fmt, arg...) dev_WARN(&(tb)->nhi->pdev->dev, fmt, ## arg)
-#define tb_warn(tb, fmt, arg...) dev_warn(&(tb)->nhi->pdev->dev, fmt, ## arg)
-#define tb_info(tb, fmt, arg...) dev_info(&(tb)->nhi->pdev->dev, fmt, ## arg)
-#define tb_dbg(tb, fmt, arg...) dev_dbg(&(tb)->nhi->pdev->dev, fmt, ## arg)
+#define tb_err(tb, fmt, arg...) dev_err((tb)->nhi->dev, fmt, ## arg)
+#define tb_WARN(tb, fmt, arg...) dev_WARN((tb)->nhi->dev, fmt, ## arg)
+#define tb_warn(tb, fmt, arg...) dev_warn((tb)->nhi->dev, fmt, ## arg)
+#define tb_info(tb, fmt, arg...) dev_info((tb)->nhi->dev, fmt, ## arg)
+#define tb_dbg(tb, fmt, arg...) dev_dbg((tb)->nhi->dev, fmt, ## arg)
#define __TB_SW_PRINT(level, sw, fmt, arg...) \
do { \
@@ -793,6 +793,7 @@ int tb_domain_disconnect_xdomain_paths(struct tb *tb, struct tb_xdomain *xd,
int transmit_path, int transmit_ring,
int receive_path, int receive_ring);
int tb_domain_disconnect_all_paths(struct tb *tb);
+int tb_domain_unregister_unplugged_xdomains(struct tb *tb);
static inline struct tb *tb_domain_get(struct tb *tb)
{
@@ -1263,6 +1264,7 @@ struct tb_xdomain *tb_xdomain_alloc(struct tb *tb, struct device *parent,
const uuid_t *remote_uuid);
void tb_xdomain_add(struct tb_xdomain *xd);
void tb_xdomain_remove(struct tb_xdomain *xd);
+void tb_xdomain_unregister(struct tb_xdomain *xd);
struct tb_xdomain *tb_xdomain_find_by_link_depth(struct tb *tb, u8 link,
u8 depth);
@@ -1479,6 +1481,7 @@ int usb4_dp_port_allocate_bandwidth(struct tb_port *port, int bw);
int usb4_dp_port_requested_bandwidth(struct tb_port *port);
int usb4_pci_port_set_ext_encapsulation(struct tb_port *port, bool enable);
+int usb4_pci_port_ltssm_state(struct tb_port *port);
static inline bool tb_is_usb4_port_device(const struct device *dev)
{
@@ -1556,4 +1559,12 @@ static inline void tb_retimer_debugfs_init(struct tb_retimer *rt) { }
static inline void tb_retimer_debugfs_remove(struct tb_retimer *rt) { }
#endif
+#if IS_REACHABLE(CONFIG_CONFIGFS_FS)
+int tb_configfs_init(void);
+void tb_configfs_exit(void);
+#else
+static inline int tb_configfs_init(void) { return 0; }
+static inline void tb_configfs_exit(void) { }
+#endif
+
#endif
diff --git a/drivers/thunderbolt/tb_regs.h b/drivers/thunderbolt/tb_regs.h
index c0bf136236e6..92d893634d2b 100644
--- a/drivers/thunderbolt/tb_regs.h
+++ b/drivers/thunderbolt/tb_regs.h
@@ -182,8 +182,7 @@ struct tb_regs_switch_header {
/* DWORD 4 */
u32 plug_events_delay:8; /*
* RW, pause between plug events in
- * milliseconds. Writing 0x00 is interpreted
- * as 255ms.
+ * milliseconds.
*/
u32 cmuv:8;
u32 __unknown4:8;
@@ -216,6 +215,7 @@ struct tb_regs_switch_header {
#define ROUTER_CS_6_WOPS BIT(2)
#define ROUTER_CS_6_WOUS BIT(3)
#define ROUTER_CS_6_HCI BIT(18)
+#define ROUTER_CS_6_RR BIT(24)
#define ROUTER_CS_6_CR BIT(25)
#define ROUTER_CS_7 0x07
#define ROUTER_CS_9 0x09
@@ -473,10 +473,25 @@ struct tb_regs_port_header {
/* PCIe adapter registers */
#define ADP_PCIE_CS_0 0x00
+#define ADP_PCIE_CS_0_LTSSM_MASK GENMASK(28, 25)
#define ADP_PCIE_CS_0_PE BIT(31)
#define ADP_PCIE_CS_1 0x01
#define ADP_PCIE_CS_1_EE BIT(0)
+enum tb_pcie_ltssm_state {
+ USB4_PCIE_LTSSM_DETECT,
+ USB4_PCIE_LTSSM_POLLING,
+ USB4_PCIE_LTSSM_CONFIG,
+ USB4_PCIE_LTSSM_CONFIG_IDLE,
+ USB4_PCIE_LTSSM_RECOVERY,
+ USB4_PCIE_LTSSM_RECOVERY_IDLE,
+ USB4_PCIE_LTSSM_L0,
+ USB4_PCIE_LTSSM_L1,
+ USB4_PCIE_LTSSM_L2,
+ USB4_PCIE_LTSSM_DISABLED,
+ USB4_PCIE_LTSSM_HOT_RESET,
+};
+
/* USB adapter registers */
#define ADP_USB3_CS_0 0x00
#define ADP_USB3_CS_0_V BIT(30)
diff --git a/drivers/thunderbolt/test.c b/drivers/thunderbolt/test.c
index 1f4318249c22..05652ee82fbf 100644
--- a/drivers/thunderbolt/test.c
+++ b/drivers/thunderbolt/test.c
@@ -1661,6 +1661,7 @@ static void tb_test_tunnel_3dp(struct kunit *test)
KUNIT_ASSERT_EQ(test, tunnel3->npaths, 3);
KUNIT_ASSERT_EQ(test, tunnel3->paths[0]->path_length, 3);
+ tb_tunnel_put(tunnel3);
tb_tunnel_put(tunnel2);
tb_tunnel_put(tunnel1);
}
@@ -2852,7 +2853,252 @@ static void tb_test_property_copy(struct kunit *test)
tb_property_free_dir(src);
}
+/*
+ * Reproducers for three memory-safety defects in
+ * drivers/thunderbolt/property.c reached from a crafted XDomain
+ * PROPERTIES_RESPONSE payload. Without the fix these trip KASAN or
+ * smash the kernel stack; with the fix each returns NULL cleanly.
+ *
+ * The on-wire entry layout matches struct tb_property_entry in
+ * property.c (private to that translation unit): u32 key_hi, u32
+ * key_lo, then a packed u32 = (type << 24) | (reserved << 16) |
+ * length, then u32 value. Each entry is 4 dwords.
+ */
+
+static void tb_test_property_parse_u32_wrap(struct kunit *test)
+{
+ /*
+ * 0x102 dwords: enough for the entry's length field (0x100) to
+ * pass the "entry->length > block_len" gate so the wrap check
+ * is actually exercised. parse_dwdata's downstream OOB read
+ * lands ~16 GiB past the allocation regardless.
+ */
+ u32 *block = kunit_kzalloc(test, 0x102 * sizeof(u32), GFP_KERNEL);
+ struct tb_property_dir *dir;
+
+ KUNIT_ASSERT_NOT_NULL(test, block);
+
+ block[0] = 0x55584401; /* "UXD" v1 magic */
+ block[1] = 0x00000004; /* Root directory length: one entry */
+
+ /*
+ * DATA entry whose value 0xffffff00 + length 0x100 wrap to 0
+ * in u32, passing the sum <= block_len guard even though the
+ * real offset is far past the allocation.
+ */
+ block[2] = 0x61616161; /* key_hi */
+ block[3] = 0x61616161; /* key_lo */
+ block[4] = 0x64000100; /* type=DATA, reserved=0, length=0x100 */
+ block[5] = 0xffffff00; /* value */
+
+ dir = tb_property_parse_dir(block, 0x102);
+ KUNIT_EXPECT_NULL(test, dir);
+ tb_property_free_dir(dir);
+}
+
+static void tb_test_property_parse_recursion(struct kunit *test)
+{
+ /*
+ * 10 dwords: rootdir header (2) + parent DIRECTORY entry (4) +
+ * the child entry that lives at dir_offset(2) + UUID(4) = 6,
+ * occupying block[6..9]. Each recursive level re-reads the
+ * same block[6..9] as its first child entry, which is itself
+ * a DIRECTORY pointing at offset 2.
+ */
+ u32 *block = kunit_kzalloc(test, 10 * sizeof(u32), GFP_KERNEL);
+ struct tb_property_dir *dir;
+
+ KUNIT_ASSERT_NOT_NULL(test, block);
+
+ block[0] = 0x55584401; /* "UXD" v1 magic */
+ block[1] = 0x00000004; /* Root directory length: one entry */
+
+ /*
+ * DIRECTORY entry pointing at dir_offset = 2 with length = 8.
+ * Non-root parse derives content_offset = 6, content_len = 4,
+ * nentries = 1. block[6..9] is read both as the parent's UUID
+ * (kmemdup'd into dir->uuid) and as the single child entry --
+ * which is itself a DIRECTORY pointing at offset 2, so the
+ * recursion never terminates and the kernel stack is exhausted.
+ */
+ block[2] = 0x61616161; /* key_hi */
+ block[3] = 0x61616161; /* key_lo */
+ block[4] = 0x44000008; /* type=DIRECTORY, reserved=0, length=8 */
+ block[5] = 0x00000002; /* value = dir_offset */
+
+ block[6] = 0x62626262; /* doubles as UUID dword 0 / child key_hi */
+ block[7] = 0x62626262; /* doubles as UUID dword 1 / child key_lo */
+ block[8] = 0x44000008; /* type=DIRECTORY, reserved=0, length=8 */
+ block[9] = 0x00000002; /* value = dir_offset (back at parent) */
+
+ dir = tb_property_parse_dir(block, 10);
+ KUNIT_EXPECT_NULL(test, dir);
+ tb_property_free_dir(dir);
+}
+
+static void tb_test_property_parse_dir_len_underflow(struct kunit *test)
+{
+ /*
+ * Allocate exactly 7 dwords (28 bytes) so the kmalloc-32 chunk
+ * leaves a 4-byte slab redzone tail that KASAN-Generic can flag.
+ * With block_len = 7, dir_offset = 4, dir_len = 3, the non-root
+ * UUID kmemdup reads 16 bytes from byte 16, so bytes 28..31 fall
+ * in the redzone and trip a KASAN slab-out-of-bounds report on
+ * the pre-fix kernel. Sizing the buffer at a power of two (32,
+ * 64, ...) puts the over-read into the slab cache tail where
+ * KASAN's generic shadow does not flag it, and the test reduces
+ * to the downstream content_len = dir_len - 4 underflow path
+ * which also returns NULL.
+ */
+ u32 *block = kunit_kzalloc(test, 7 * sizeof(u32), GFP_KERNEL);
+ struct tb_property_dir *dir;
+
+ KUNIT_ASSERT_NOT_NULL(test, block);
+
+ block[0] = 0x55584401; /* "UXD" v1 magic */
+ block[1] = 0x00000004; /* Root directory length: one entry */
+
+ /*
+ * DIRECTORY entry with length = 3 pointing at dir_offset = 4.
+ * tb_property_entry_valid() permits value(4) + length(3) <=
+ * block_len(7). Non-root parse begins with a kmemdup of 4
+ * dwords from dir_offset for the UUID; that read runs past the
+ * 28-byte allocation before the dir_len < 4 reject would fire.
+ */
+ block[2] = 0x61616161; /* key_hi */
+ block[3] = 0x61616161; /* key_lo */
+ block[4] = 0x44000003; /* type=DIRECTORY, reserved=0, length=3 */
+ block[5] = 0x00000004; /* value = dir_offset */
+ /* block[6] is the start of the four UUID dwords; block[7..] is OOB. */
+
+ dir = tb_property_parse_dir(block, 7);
+ KUNIT_EXPECT_NULL(test, dir);
+ tb_property_free_dir(dir);
+}
+
+static void tb_test_property_parse_zero_length(struct kunit *test)
+{
+ u32 *block = kunit_kzalloc(test, 6 * sizeof(u32), GFP_KERNEL);
+ struct tb_property_dir *dir;
+
+ KUNIT_ASSERT_NOT_NULL(test, block);
+
+ block[0] = 0x55584401; /* rootdir magic */
+ block[1] = 0x00000004; /* root length: one entry */
+
+ block[2] = 0x61616161; /* key_hi */
+ block[3] = 0x61616161; /* key_lo */
+ block[4] = 0x74000000; /* type=TEXT, reserved=0, length=0 */
+ block[5] = 0x00000000; /* value */
+
+ dir = tb_property_parse_dir(block, 6);
+ KUNIT_EXPECT_NULL(test, dir);
+ tb_property_free_dir(dir);
+}
+
+static void tb_test_property_parse_rootdir_overflow(struct kunit *test)
+{
+ u32 *block = kunit_kzalloc(test, 4 * sizeof(u32), GFP_KERNEL);
+ struct tb_property_dir *dir;
+
+ KUNIT_ASSERT_NOT_NULL(test, block);
+
+ block[0] = 0x55584401; /* rootdir magic */
+ block[1] = 0x00000004; /* root length claims 4 dwords of content */
+ block[2] = 0x61616161;
+ block[3] = 0x61616161;
+
+ /* content_offset(2) + content_len(4) = 6 > block_len(4) */
+ dir = tb_property_parse_dir(block, 4);
+ KUNIT_EXPECT_NULL(test, dir);
+ tb_property_free_dir(dir);
+}
+
+static void tb_test_property_merge(struct kunit *test)
+{
+ struct tb_property_dir *dir1, *dir2, *dir3;
+ struct tb_property *p;
+ uuid_t uuid;
+ int ret;
+
+ dir1 = tb_property_create_dir(&network_dir_uuid);
+ KUNIT_ASSERT_NOT_NULL(test, dir1);
+ ret = tb_property_add_immediate(dir1, "prtcid", 1);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ ret = tb_property_add_immediate(dir1, "prtcvers", 1);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ ret = tb_property_add_immediate(dir1, "prtcrevs", 0);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ ret = tb_property_add_immediate(dir1, "prtcstns", 0);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ dir2 = tb_property_create_dir(&network_dir_uuid);
+ KUNIT_ASSERT_NOT_NULL(test, dir2);
+ ret = tb_property_add_text(dir2, "descr", "This is text");
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ /* This replaces the value in dir1 */
+ ret = tb_property_add_immediate(dir2, "prtcvers", 0x1234);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ uuid_gen(&uuid);
+ dir3 = tb_property_create_dir(&uuid);
+ KUNIT_ASSERT_NOT_NULL(test, dir3);
+ ret = tb_property_add_immediate(dir3, "value0", 0);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ ret = tb_property_add_text(dir3, "value1", "Text value");
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ ret = tb_property_add_dir(dir2, "my", dir3);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ ret = tb_property_merge_dir(dir1, dir2, true);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ p = tb_property_get_next(dir1, NULL);
+ KUNIT_ASSERT_NOT_NULL(test, p);
+ KUNIT_ASSERT_STREQ(test, &p->key[0], "prtcid");
+ KUNIT_ASSERT_EQ(test, p->type, TB_PROPERTY_TYPE_VALUE);
+ KUNIT_ASSERT_EQ(test, p->length, 1);
+ KUNIT_ASSERT_EQ(test, p->value.immediate, 1);
+ p = tb_property_get_next(dir1, p);
+ KUNIT_ASSERT_NOT_NULL(test, p);
+ KUNIT_ASSERT_STREQ(test, &p->key[0], "prtcvers");
+ KUNIT_ASSERT_EQ(test, p->type, TB_PROPERTY_TYPE_VALUE);
+ KUNIT_ASSERT_EQ(test, p->length, 1);
+ KUNIT_ASSERT_EQ(test, p->value.immediate, 0x1234);
+ p = tb_property_get_next(dir1, p);
+ KUNIT_ASSERT_NOT_NULL(test, p);
+ KUNIT_ASSERT_STREQ(test, &p->key[0], "prtcrevs");
+ KUNIT_ASSERT_EQ(test, p->type, TB_PROPERTY_TYPE_VALUE);
+ KUNIT_ASSERT_EQ(test, p->length, 1);
+ KUNIT_ASSERT_EQ(test, p->value.immediate, 0);
+ p = tb_property_get_next(dir1, p);
+ KUNIT_ASSERT_NOT_NULL(test, p);
+ KUNIT_ASSERT_STREQ(test, &p->key[0], "prtcstns");
+ KUNIT_ASSERT_EQ(test, p->type, TB_PROPERTY_TYPE_VALUE);
+ KUNIT_ASSERT_EQ(test, p->length, 1);
+ KUNIT_ASSERT_EQ(test, p->value.immediate, 0);
+ p = tb_property_get_next(dir1, p);
+ KUNIT_ASSERT_NOT_NULL(test, p);
+ KUNIT_ASSERT_STREQ(test, &p->key[0], "descr");
+ KUNIT_ASSERT_EQ(test, p->type, TB_PROPERTY_TYPE_TEXT);
+ KUNIT_ASSERT_EQ(test, p->length, 4);
+ KUNIT_ASSERT_STREQ(test, p->value.text, "This is text");
+ p = tb_property_get_next(dir1, p);
+ KUNIT_ASSERT_NOT_NULL(test, p);
+ KUNIT_ASSERT_STREQ(test, &p->key[0], "my");
+ KUNIT_ASSERT_EQ(test, p->type, TB_PROPERTY_TYPE_DIRECTORY);
+ compare_dirs(test, p->value.dir, dir3);
+ p = tb_property_get_next(dir1, p);
+ KUNIT_ASSERT_NULL(test, p);
+
+ tb_property_free_dir(dir2);
+ tb_property_free_dir(dir1);
+}
+
static struct kunit_case tb_test_cases[] = {
+ KUNIT_CASE(tb_test_property_parse_u32_wrap),
+ KUNIT_CASE(tb_test_property_parse_recursion),
+ KUNIT_CASE(tb_test_property_parse_dir_len_underflow),
KUNIT_CASE(tb_test_path_basic),
KUNIT_CASE(tb_test_path_not_connected_walk),
KUNIT_CASE(tb_test_path_single_hop_walk),
@@ -2892,6 +3138,9 @@ static struct kunit_case tb_test_cases[] = {
KUNIT_CASE(tb_test_property_parse),
KUNIT_CASE(tb_test_property_format),
KUNIT_CASE(tb_test_property_copy),
+ KUNIT_CASE(tb_test_property_parse_zero_length),
+ KUNIT_CASE(tb_test_property_parse_rootdir_overflow),
+ KUNIT_CASE(tb_test_property_merge),
{ }
};
diff --git a/drivers/thunderbolt/tunnel.c b/drivers/thunderbolt/tunnel.c
index f38f7753b6e4..b7f32305f14a 100644
--- a/drivers/thunderbolt/tunnel.c
+++ b/drivers/thunderbolt/tunnel.c
@@ -290,6 +290,40 @@ static inline void tb_tunnel_changed(struct tb_tunnel *tunnel)
tunnel->src_port, tunnel->dst_port);
}
+static int tb_pci_port_ltssm_state_detect(struct tb_port *port)
+{
+ ktime_t timeout = ktime_add_ms(ktime_get(), 500);
+
+ do {
+ int ret;
+
+ ret = usb4_pci_port_ltssm_state(port);
+ if (ret < 0)
+ return ret;
+ if (ret == USB4_PCIE_LTSSM_DETECT)
+ return 0;
+
+ fsleep(50);
+ } while (ktime_before(ktime_get(), timeout));
+
+ return -ETIMEDOUT;
+}
+
+static int tb_pci_pre_activate(struct tb_tunnel *tunnel)
+{
+ struct tb_port *down = tunnel->src_port;
+ struct tb_port *up = tunnel->dst_port;
+ int ret;
+
+ ret = tb_switch_is_usb4(down->sw) ?
+ tb_pci_port_ltssm_state_detect(down) : 0;
+ if (ret)
+ return ret;
+
+ return tb_switch_is_usb4(up->sw) ?
+ tb_pci_port_ltssm_state_detect(up) : 0;
+}
+
static int tb_pci_set_ext_encapsulation(struct tb_tunnel *tunnel, bool enable)
{
struct tb_port *port = tb_upstream_port(tunnel->dst_port->sw);
@@ -505,6 +539,7 @@ struct tb_tunnel *tb_tunnel_alloc_pci(struct tb *tb, struct tb_port *up,
if (!tunnel)
return NULL;
+ tunnel->pre_activate = tb_pci_pre_activate;
tunnel->activate = tb_pci_activate;
tunnel->src_port = down;
tunnel->dst_port = up;
diff --git a/drivers/thunderbolt/usb4.c b/drivers/thunderbolt/usb4.c
index 9e810b2ae0b5..5fd4fe070f25 100644
--- a/drivers/thunderbolt/usb4.c
+++ b/drivers/thunderbolt/usb4.c
@@ -294,7 +294,12 @@ int usb4_switch_setup(struct tb_switch *sw)
/* TBT3 supported by the CM */
val &= ~ROUTER_CS_5_CNS;
- return tb_sw_write(sw, &val, TB_CFG_SWITCH, ROUTER_CS_5, 1);
+ ret = tb_sw_write(sw, &val, TB_CFG_SWITCH, ROUTER_CS_5, 1);
+ if (ret)
+ return ret;
+
+ return tb_switch_wait_for_bit(sw, ROUTER_CS_6, ROUTER_CS_6_RR,
+ ROUTER_CS_6_RR, 500);
}
/**
@@ -304,7 +309,7 @@ int usb4_switch_setup(struct tb_switch *sw)
* Sets configuration valid bit for the router. Must be called before
* any tunnels can be set through the router and after
* usb4_switch_setup() has been called. Can be called to host and device
- * routers (does nothing for the latter).
+ * routers (does nothing for the former).
*
* Return: %0 on success, negative errno otherwise.
*/
@@ -327,7 +332,7 @@ int usb4_switch_configuration_valid(struct tb_switch *sw)
return ret;
return tb_switch_wait_for_bit(sw, ROUTER_CS_6, ROUTER_CS_6_CR,
- ROUTER_CS_6_CR, 50);
+ ROUTER_CS_6_CR, 500);
}
/**
@@ -3145,3 +3150,27 @@ int usb4_pci_port_set_ext_encapsulation(struct tb_port *port, bool enable)
return tb_port_write(port, &val, TB_CFG_PORT,
port->cap_adap + ADP_PCIE_CS_1, 1);
}
+
+/**
+ * usb4_pci_port_ltssm_state() - Read PCIe adapter LTSSM state
+ * @port: PCIe adapter
+ *
+ * Return:
+ * * LTSSM state of @port.
+ * * Negative errno - On failure.
+ */
+int usb4_pci_port_ltssm_state(struct tb_port *port)
+{
+ u32 val;
+ int ret;
+
+ if (!tb_port_is_pcie_down(port) && !tb_port_is_pcie_up(port))
+ return -EINVAL;
+
+ ret = tb_port_read(port, &val, TB_CFG_PORT,
+ port->cap_adap + ADP_PCIE_CS_0, 1);
+ if (ret)
+ return ret;
+
+ return FIELD_GET(ADP_PCIE_CS_0_LTSSM_MASK, val);
+}
diff --git a/drivers/thunderbolt/usb4_port.c b/drivers/thunderbolt/usb4_port.c
index c32d3516e780..890de530debc 100644
--- a/drivers/thunderbolt/usb4_port.c
+++ b/drivers/thunderbolt/usb4_port.c
@@ -138,7 +138,7 @@ bool usb4_usb3_port_match(struct device *usb4_port_dev,
return false;
/* Check if USB3 fwnode references same NHI where USB4 port resides */
- if (!device_match_fwnode(&nhi->pdev->dev, nhi_fwnode))
+ if (!device_match_fwnode(nhi->dev, nhi_fwnode))
return false;
if (fwnode_property_read_u8(usb3_port_fwnode, "usb4-port-number", &usb4_port_num))
diff --git a/drivers/thunderbolt/xdomain.c b/drivers/thunderbolt/xdomain.c
index 754808c43f00..fe6c5ac703f4 100644
--- a/drivers/thunderbolt/xdomain.c
+++ b/drivers/thunderbolt/xdomain.c
@@ -136,6 +136,7 @@ static int __tb_xdomain_response(struct tb_ctl *ctl, const void *response,
size_t size, enum tb_cfg_pkg_type type)
{
struct tb_cfg_request *req;
+ int ret;
req = tb_cfg_request_alloc();
if (!req)
@@ -147,7 +148,11 @@ static int __tb_xdomain_response(struct tb_ctl *ctl, const void *response,
req->request_size = size;
req->request_type = type;
- return tb_cfg_request(ctl, req, response_ready, req);
+ ret = tb_cfg_request(ctl, req, response_ready, req);
+ if (ret)
+ tb_cfg_request_put(req);
+
+ return ret;
}
/**
@@ -535,29 +540,19 @@ static int tb_xdp_link_state_status_request(struct tb_ctl *ctl, u64 route,
}
static int tb_xdp_link_state_status_response(struct tb *tb, struct tb_ctl *ctl,
- struct tb_xdomain *xd, u8 sequence)
+ struct tb_xdomain *xd, u8 sequence,
+ u8 slw, u8 sls, u8 tls, u8 tlw)
{
struct tb_xdp_link_state_status_response res;
- struct tb_port *port = tb_xdomain_downstream_port(xd);
- u32 val[2];
- int ret;
memset(&res, 0, sizeof(res));
tb_xdp_fill_header(&res.hdr, xd->route, sequence,
LINK_STATE_STATUS_RESPONSE, sizeof(res));
- ret = tb_port_read(port, val, TB_CFG_PORT,
- port->cap_phy + LANE_ADP_CS_0, ARRAY_SIZE(val));
- if (ret)
- return ret;
-
- res.slw = (val[0] & LANE_ADP_CS_0_SUPPORTED_WIDTH_MASK) >>
- LANE_ADP_CS_0_SUPPORTED_WIDTH_SHIFT;
- res.sls = (val[0] & LANE_ADP_CS_0_SUPPORTED_SPEED_MASK) >>
- LANE_ADP_CS_0_SUPPORTED_SPEED_SHIFT;
- res.tls = val[1] & LANE_ADP_CS_1_TARGET_SPEED_MASK;
- res.tlw = (val[1] & LANE_ADP_CS_1_TARGET_WIDTH_MASK) >>
- LANE_ADP_CS_1_TARGET_WIDTH_SHIFT;
+ res.slw = slw;
+ res.sls = sls;
+ res.tls = tls;
+ res.tlw = tlw;
return __tb_xdomain_response(ctl, &res, sizeof(res),
TB_CFG_PKG_XDOMAIN_RESP);
@@ -645,6 +640,32 @@ void tb_unregister_protocol_handler(struct tb_protocol_handler *handler)
}
EXPORT_SYMBOL_GPL(tb_unregister_protocol_handler);
+static int update_service_properties(struct device *dev, void *data)
+{
+ struct tb_property_dir *root = data;
+ struct tb_service *svc;
+ struct tb_property *p;
+
+ svc = tb_to_service(dev);
+ if (!svc)
+ return 0;
+
+ guard(mutex)(&svc->lock);
+
+ /*
+ * Replace the static service properties with the dynamic one.
+ * Typically this is the same but service drivers can add their
+ * own dynamic properties here too.
+ */
+ p = tb_property_find(root, svc->key, TB_PROPERTY_TYPE_DIRECTORY);
+ if (!p)
+ return 0;
+ if (svc->local_properties)
+ return tb_property_merge_dir(p->value.dir,
+ svc->local_properties, false);
+ return 0;
+}
+
static void update_property_block(struct tb_xdomain *xd)
{
mutex_lock(&xdomain_lock);
@@ -669,6 +690,9 @@ static void update_property_block(struct tb_xdomain *xd)
tb_property_add_text(dir, "deviceid", utsname()->nodename);
tb_property_add_immediate(dir, "maxhopid", xd->local_max_hopid);
+ /* Add service specific dynamic properties */
+ device_for_each_child(&xd->dev, dir, update_service_properties);
+
ret = tb_property_format_dir(dir, NULL, 0);
if (ret < 0) {
dev_warn(&xd->dev, "local property block creation failed\n");
@@ -745,7 +769,7 @@ static void tb_xdp_handle_request(struct work_struct *work)
mutex_lock(&tb->lock);
if (tb->root_switch)
- uuid = tb->root_switch->uuid;
+ uuid = kmemdup(tb->root_switch->uuid, sizeof(*uuid), GFP_KERNEL);
else
uuid = NULL;
mutex_unlock(&tb->lock);
@@ -779,9 +803,13 @@ static void tb_xdp_handle_request(struct work_struct *work)
* the xdomain related to this connection as well in
* case there is a change in services it offers.
*/
- if (xd && device_is_registered(&xd->dev))
- queue_delayed_work(tb->wq, &xd->state_work,
- msecs_to_jiffies(XDOMAIN_SHORT_TIMEOUT));
+ if (xd) {
+ mutex_lock(&xd->lock);
+ if (!xd->removing && device_is_registered(&xd->dev))
+ queue_delayed_work(tb->wq, &xd->state_work,
+ msecs_to_jiffies(XDOMAIN_SHORT_TIMEOUT));
+ mutex_unlock(&xd->lock);
+ }
break;
case UUID_REQUEST_OLD:
@@ -794,8 +822,12 @@ static void tb_xdp_handle_request(struct work_struct *work)
* received UUID request from the remote host.
*/
if (!ret && xd && xd->state == XDOMAIN_STATE_ERROR) {
- dev_dbg(&xd->dev, "restarting handshake\n");
- start_handshake(xd);
+ mutex_lock(&xd->lock);
+ if (!xd->removing) {
+ dev_dbg(&xd->dev, "restarting handshake\n");
+ start_handshake(xd);
+ }
+ mutex_unlock(&xd->lock);
}
break;
@@ -804,8 +836,47 @@ static void tb_xdp_handle_request(struct work_struct *work)
route);
if (xd) {
- ret = tb_xdp_link_state_status_response(tb, ctl, xd,
- sequence);
+ struct tb_port *port = tb_xdomain_downstream_port(xd);
+ u8 slw, sls, tls, tlw;
+ u32 val[2];
+
+ /*
+ * Read the adapter supported and target widths
+ * and speeds.
+ */
+ ret = tb_port_read(port, val, TB_CFG_PORT,
+ port->cap_phy + LANE_ADP_CS_0,
+ ARRAY_SIZE(val));
+ if (ret)
+ break;
+
+ slw = (val[0] & LANE_ADP_CS_0_SUPPORTED_WIDTH_MASK) >>
+ LANE_ADP_CS_0_SUPPORTED_WIDTH_SHIFT;
+ sls = (val[0] & LANE_ADP_CS_0_SUPPORTED_SPEED_MASK) >>
+ LANE_ADP_CS_0_SUPPORTED_SPEED_SHIFT;
+ tls = val[1] & LANE_ADP_CS_1_TARGET_SPEED_MASK;
+ tlw = (val[1] & LANE_ADP_CS_1_TARGET_WIDTH_MASK) >>
+ LANE_ADP_CS_1_TARGET_WIDTH_SHIFT;
+
+ /*
+ * When we have higher UUID, we are supposed to
+ * return ERROR_NOT_READY if the tlw is not yet
+ * set according to the Inter-Domain spec for
+ * USB4 v2.
+ */
+ if (xd->state == XDOMAIN_STATE_BONDING_UUID_HIGH &&
+ xd->target_link_width &&
+ xd->target_link_width != tlw) {
+ tb_dbg(tb, "%llx: target link width not yet set %#x != %#x\n",
+ route, tlw, xd->target_link_width);
+ tb_xdp_error_response(ctl, route, sequence,
+ ERROR_NOT_READY);
+ } else {
+ tb_dbg(tb, "%llx: replying with target link width set to %#x\n",
+ route, tlw);
+ ret = tb_xdp_link_state_status_response(tb, ctl,
+ xd, sequence, slw, sls, tls, tlw);
+ }
} else {
tb_xdp_error_response(ctl, route, sequence,
ERROR_NOT_READY);
@@ -822,9 +893,13 @@ static void tb_xdp_handle_request(struct work_struct *work)
ret = tb_xdp_link_state_change_response(ctl, route,
sequence, 0);
- xd->target_link_width = lsc->tlw;
- queue_delayed_work(tb->wq, &xd->state_work,
- msecs_to_jiffies(XDOMAIN_SHORT_TIMEOUT));
+ mutex_lock(&xd->lock);
+ if (!xd->removing) {
+ xd->target_link_width = lsc->tlw;
+ queue_delayed_work(tb->wq, &xd->state_work,
+ msecs_to_jiffies(XDOMAIN_SHORT_TIMEOUT));
+ }
+ mutex_unlock(&xd->lock);
} else {
tb_xdp_error_response(ctl, route, sequence,
ERROR_NOT_READY);
@@ -846,6 +921,7 @@ static void tb_xdp_handle_request(struct work_struct *work)
}
out:
+ kfree(uuid);
kfree(xw->pkg);
kfree(xw);
@@ -901,6 +977,44 @@ void tb_unregister_service_driver(struct tb_service_driver *drv)
}
EXPORT_SYMBOL_GPL(tb_unregister_service_driver);
+static int update_xdomain(struct device *dev, void *data)
+{
+ struct tb_xdomain *xd;
+
+ xd = tb_to_xdomain(dev);
+ if (xd) {
+ mutex_lock(&xd->lock);
+ if (!xd->removing)
+ queue_delayed_work(xd->tb->wq,
+ &xd->properties_changed_work,
+ msecs_to_jiffies(50));
+ mutex_unlock(&xd->lock);
+ }
+
+ return 0;
+}
+
+/**
+ * tb_service_properties_changed() - Notify the other host about changes
+ * @svc: Service whose properties changed
+ *
+ * Notifies the other host that service properties may have been
+ * changed. This should be called whenever @svc->local_properties is
+ * updated.
+ */
+void tb_service_properties_changed(struct tb_service *svc)
+{
+ struct tb_xdomain *xd = tb_service_parent(svc);
+
+ if (xd->is_unplugged)
+ return;
+
+ scoped_guard(mutex, &xdomain_lock)
+ xdomain_property_block_gen++;
+ update_xdomain(&xd->dev, NULL);
+}
+EXPORT_SYMBOL_GPL(tb_service_properties_changed);
+
static ssize_t key_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
@@ -1000,10 +1114,11 @@ static void tb_service_release(struct device *dev)
struct tb_service *svc = container_of(dev, struct tb_service, dev);
struct tb_xdomain *xd = tb_service_parent(svc);
- tb_service_debugfs_remove(svc);
+ tb_property_free_dir(svc->remote_properties);
ida_free(&xd->service_ids, svc->id);
kfree(svc->key);
kfree(svc);
+ tb_xdomain_put(xd);
}
const struct device_type tb_service_type = {
@@ -1014,6 +1129,24 @@ const struct device_type tb_service_type = {
};
EXPORT_SYMBOL_GPL(tb_service_type);
+static void update_service(struct tb_service *svc, struct tb_property *property)
+{
+ struct tb_property_dir *dir = property->value.dir;
+
+ guard(mutex)(&svc->lock);
+ tb_property_free_dir(svc->remote_properties);
+ svc->remote_properties = tb_property_copy_dir(dir);
+ kobject_uevent(&svc->dev.kobj, KOBJ_CHANGE);
+}
+
+static void __unregister_service(struct device *dev)
+{
+ struct tb_service *svc = tb_to_service(dev);
+
+ tb_service_debugfs_remove(svc);
+ device_unregister(&svc->dev);
+}
+
static int remove_missing_service(struct device *dev, void *data)
{
struct tb_xdomain *xd = data;
@@ -1025,7 +1158,7 @@ static int remove_missing_service(struct device *dev, void *data)
if (!tb_property_find(xd->remote_properties, svc->key,
TB_PROPERTY_TYPE_DIRECTORY))
- device_unregister(dev);
+ __unregister_service(dev);
return 0;
}
@@ -1066,6 +1199,12 @@ static int populate_service(struct tb_service *svc,
if (!svc->key)
return -ENOMEM;
+ svc->remote_properties = tb_property_copy_dir(dir);
+ if (!svc->remote_properties) {
+ kfree(svc->key);
+ return -ENOMEM;
+ }
+
return 0;
}
@@ -1090,6 +1229,7 @@ static void enumerate_services(struct tb_xdomain *xd)
/* If the service exists already we are fine */
dev = device_find_child(&xd->dev, p, find_service);
if (dev) {
+ update_service(tb_to_service(dev), p);
put_device(dev);
continue;
}
@@ -1112,12 +1252,14 @@ static void enumerate_services(struct tb_xdomain *xd)
svc->id = id;
svc->dev.bus = &tb_bus_type;
svc->dev.type = &tb_service_type;
- svc->dev.parent = &xd->dev;
+ svc->dev.parent = get_device(&xd->dev);
+ mutex_init(&svc->lock);
dev_set_name(&svc->dev, "%s.%d", dev_name(&xd->dev), svc->id);
tb_service_debugfs_init(svc);
if (device_register(&svc->dev)) {
+ tb_service_debugfs_remove(svc);
put_device(&svc->dev);
break;
}
@@ -1931,7 +2073,13 @@ static void tb_xdomain_link_exit(struct tb_xdomain *xd)
if (tb_port_get_link_generation(down) >= 4) {
down->bonded = false;
down->dual_link_port->bonded = false;
- } else if (xd->link_width > TB_LINK_WIDTH_SINGLE) {
+ return;
+ }
+
+ if (!xd->bonding_possible)
+ return;
+
+ if (xd->link_width > TB_LINK_WIDTH_SINGLE) {
/*
* Just return port structures back to way they were and
* update credits. No need to update userspace because
@@ -1988,6 +2136,7 @@ struct tb_xdomain *tb_xdomain_alloc(struct tb *tb, struct device *parent,
INIT_DELAYED_WORK(&xd->state_work, tb_xdomain_state_work);
INIT_DELAYED_WORK(&xd->properties_changed_work,
tb_xdomain_properties_changed);
+ atomic_set(&xd->ntunnels, 0);
xd->local_uuid = kmemdup(local_uuid, sizeof(uuid_t), GFP_KERNEL);
if (!xd->local_uuid)
@@ -2050,46 +2199,64 @@ void tb_xdomain_add(struct tb_xdomain *xd)
static int unregister_service(struct device *dev, void *data)
{
- device_unregister(dev);
+ __unregister_service(dev);
return 0;
}
/**
- * tb_xdomain_remove() - Remove XDomain from the bus
+ * tb_xdomain_remove() - Remove XDomain
* @xd: XDomain to remove
*
- * This will stop all ongoing configuration work and remove the XDomain
- * along with any services from the bus. When the last reference to @xd
- * is released the object will be released as well.
+ * This will stop all ongoing configuration work. XDomain is not removed
+ * from the bus if it was added. That needs to be done separately by
+ * calling tb_xdomain_unregister().
+ *
+ * Called with @tb->lock held.
*/
void tb_xdomain_remove(struct tb_xdomain *xd)
{
tb_xdomain_debugfs_remove(xd);
- stop_handshake(xd);
-
- device_for_each_child_reverse(&xd->dev, xd, unregister_service);
+ mutex_lock(&xd->lock);
+ xd->removing = true;
+ mutex_unlock(&xd->lock);
+ stop_handshake(xd);
tb_xdomain_link_exit(xd);
- /*
- * Undo runtime PM here explicitly because it is possible that
- * the XDomain was never added to the bus and thus device_del()
- * is not called for it (device_del() would handle this otherwise).
- */
- pm_runtime_disable(&xd->dev);
- pm_runtime_put_noidle(&xd->dev);
- pm_runtime_set_suspended(&xd->dev);
-
if (!device_is_registered(&xd->dev)) {
+ /*
+ * Undo runtime PM here explicitly because it is
+ * possible that the XDomain was never added to the bus
+ * and thus device_del() is not called for it
+ * (device_del() would handle this otherwise).
+ */
+ pm_runtime_disable(&xd->dev);
+ pm_runtime_put_noidle(&xd->dev);
+ pm_runtime_set_suspended(&xd->dev);
put_device(&xd->dev);
- } else {
- dev_info(&xd->dev, "host disconnected\n");
- device_unregister(&xd->dev);
}
}
/**
+ * tb_xdomain_unregister() - Unregister XDomain
+ * @xd: XDomain to unregister
+ *
+ * This will unregister the XDomain along with any services from the
+ * bus. When the last reference to @xd is released the object will be
+ * released as well.
+ */
+void tb_xdomain_unregister(struct tb_xdomain *xd)
+{
+ lockdep_assert_not_held(&xd->tb->lock);
+
+ device_for_each_child_reverse(&xd->dev, xd, unregister_service);
+
+ dev_info(&xd->dev, "host disconnected\n");
+ device_unregister(&xd->dev);
+}
+
+/**
* tb_xdomain_lane_bonding_enable() - Enable lane bonding on XDomain
* @xd: XDomain connection
*
@@ -2265,9 +2432,15 @@ int tb_xdomain_enable_paths(struct tb_xdomain *xd, int transmit_path,
int transmit_ring, int receive_path,
int receive_ring)
{
- return tb_domain_approve_xdomain_paths(xd->tb, xd, transmit_path,
- transmit_ring, receive_path,
- receive_ring);
+ int ret;
+
+ ret = tb_domain_approve_xdomain_paths(xd->tb, xd, transmit_path,
+ transmit_ring, receive_path,
+ receive_ring);
+ if (ret)
+ return ret;
+ atomic_inc(&xd->ntunnels);
+ return 0;
}
EXPORT_SYMBOL_GPL(tb_xdomain_enable_paths);
@@ -2290,9 +2463,15 @@ int tb_xdomain_disable_paths(struct tb_xdomain *xd, int transmit_path,
int transmit_ring, int receive_path,
int receive_ring)
{
- return tb_domain_disconnect_xdomain_paths(xd->tb, xd, transmit_path,
- transmit_ring, receive_path,
- receive_ring);
+ int ret;
+
+ ret = tb_domain_disconnect_xdomain_paths(xd->tb, xd, transmit_path,
+ transmit_ring, receive_path,
+ receive_ring);
+ if (ret)
+ return ret;
+ atomic_dec(&xd->ntunnels);
+ return 0;
}
EXPORT_SYMBOL_GPL(tb_xdomain_disable_paths);
@@ -2308,6 +2487,9 @@ static struct tb_xdomain *switch_find_xdomain(struct tb_switch *sw,
{
struct tb_port *port;
+ if (!sw)
+ return NULL;
+
tb_switch_for_each_port(sw, port) {
struct tb_xdomain *xd;
@@ -2470,19 +2652,6 @@ bool tb_xdomain_handle_request(struct tb *tb, enum tb_cfg_pkg_type type,
return ret > 0;
}
-static int update_xdomain(struct device *dev, void *data)
-{
- struct tb_xdomain *xd;
-
- xd = tb_to_xdomain(dev);
- if (xd) {
- queue_delayed_work(xd->tb->wq, &xd->properties_changed_work,
- msecs_to_jiffies(50));
- }
-
- return 0;
-}
-
static void update_all_xdomains(void)
{
bus_for_each_dev(&tb_bus_type, NULL, NULL, update_xdomain);
diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h
index 0ba112175bb3..feb1af175cfd 100644
--- a/include/linux/thunderbolt.h
+++ b/include/linux/thunderbolt.h
@@ -13,6 +13,7 @@
#include <linux/types.h>
+struct config_group;
struct fwnode_handle;
struct device;
@@ -153,6 +154,9 @@ struct tb_property_dir *tb_property_parse_dir(const u32 *block,
ssize_t tb_property_format_dir(const struct tb_property_dir *dir, u32 *block,
size_t block_len);
struct tb_property_dir *tb_property_copy_dir(const struct tb_property_dir *dir);
+int tb_property_merge_dir(struct tb_property_dir *parent,
+ const struct tb_property_dir *dir,
+ bool replace);
struct tb_property_dir *tb_property_create_dir(const uuid_t *uuid);
void tb_property_free_dir(struct tb_property_dir *dir);
int tb_property_add_immediate(struct tb_property_dir *parent, const char *key,
@@ -209,6 +213,8 @@ enum tb_link_width {
* @link_width: Width of the downstream facing link
* @link_usb4: Downstream link is USB4
* @is_unplugged: The XDomain is unplugged
+ * @removing: Set by tb_xdomain_remove() under @lock to prevent
+ * concurrent delayed work queueing
* @needs_uuid: If the XDomain does not have @remote_uuid it will be
* queried first
* @service_ids: Used to generate IDs for the services
@@ -228,6 +234,7 @@ enum tb_link_width {
* changed notification
* @bonding_possible: True if lane bonding is possible on local side
* @target_link_width: Target link width from the remote host
+ * @ntunnels: Keeps track of how many tunnels go through this XDomain
* @link: Root switch link the remote domain is connected (ICM only)
* @depth: Depth in the chain the remote domain is connected (ICM only)
*
@@ -257,6 +264,7 @@ struct tb_xdomain {
enum tb_link_width link_width;
bool link_usb4;
bool is_unplugged;
+ bool removing;
bool needs_uuid;
struct ida service_ids;
struct ida in_hopids;
@@ -273,6 +281,7 @@ struct tb_xdomain {
int properties_changed_retries;
bool bonding_possible;
u8 target_link_width;
+ atomic_t ntunnels;
u8 link;
u8 depth;
};
@@ -392,6 +401,10 @@ void tb_unregister_protocol_handler(struct tb_protocol_handler *handler);
* @prtcvers: Protocol version from the properties directory
* @prtcrevs: Protocol software revision from the properties directory
* @prtcstns: Protocol settings mask from the properties directory
+ * @lock: Protects this structure
+ * @local_properties: Properties owned by the service driver
+ * @remote_properties: Properties read from the remote service. These
+ * are read-only.
* @debugfs_dir: Pointer to the service debugfs directory. Always created
* when debugfs is enabled. Can be used by service drivers to
* add their own entries under the service.
@@ -399,6 +412,9 @@ void tb_unregister_protocol_handler(struct tb_protocol_handler *handler);
* Each domain exposes set of services it supports as collection of
* properties. For each service there will be one corresponding
* &struct tb_service. Service drivers are bound to these.
+ *
+ * Service drivers can add their own dynamic properties to
+ * @local_properties but whenever they do so @lock must be held.
*/
struct tb_service {
struct device dev;
@@ -408,6 +424,9 @@ struct tb_service {
u32 prtcvers;
u32 prtcrevs;
u32 prtcstns;
+ struct mutex lock;
+ struct tb_property_dir *local_properties;
+ struct tb_property_dir *remote_properties;
struct dentry *debugfs_dir;
};
@@ -476,16 +495,17 @@ static inline struct tb_xdomain *tb_service_parent(struct tb_service *svc)
return tb_to_xdomain(svc->dev.parent);
}
+void tb_service_properties_changed(struct tb_service *svc);
+
/**
* struct tb_nhi - thunderbolt native host interface
* @lock: Must be held during ring creation/destruction. Is acquired by
* interrupt_work when dispatching interrupts to individual rings.
- * @pdev: Pointer to the PCI device
+ * @dev: Device associated with this NHI instance
* @ops: NHI specific optional ops
* @iobase: MMIO space of the NHI
* @tx_rings: All Tx rings available on this host controller
* @rx_rings: All Rx rings available on this host controller
- * @msix_ida: Used to allocate MSI-X vectors for rings
* @going_away: The host controller device is about to disappear so when
* this flag is set, avoid touching the hardware anymore.
* @iommu_dma_protection: An IOMMU will isolate external-facing ports.
@@ -493,20 +513,21 @@ static inline struct tb_xdomain *tb_service_parent(struct tb_service *svc)
* MSI-X is used.
* @hop_count: Number of rings (end point hops) supported by NHI.
* @quirks: NHI specific quirks if any
+ * @domain_released: Completed when domain has been fully released
*/
struct tb_nhi {
spinlock_t lock;
- struct pci_dev *pdev;
+ struct device *dev;
const struct tb_nhi_ops *ops;
void __iomem *iobase;
struct tb_ring **tx_rings;
struct tb_ring **rx_rings;
- struct ida msix_ida;
bool going_away;
bool iommu_dma_protection;
struct work_struct interrupt_work;
u32 hop_count;
unsigned long quirks;
+ struct completion domain_released;
};
/**
@@ -535,6 +556,9 @@ struct tb_nhi {
* @start_poll: Called when ring interrupt is triggered to start
* polling. Passing %NULL keeps the ring in interrupt mode.
* @poll_data: Data passed to @start_poll
+ * @interval_nsec: Interval counter if interrupt throttling is to be
+ * used with this ring (in ns)
+ * @wait: Used to signal that the ring may be empty now
*/
struct tb_ring {
spinlock_t lock;
@@ -558,6 +582,8 @@ struct tb_ring {
u16 eof_mask;
void (*start_poll)(void *data);
void *poll_data;
+ unsigned int interval_nsec;
+ wait_queue_head_t wait;
};
/* Leave ring interrupt enabled on suspend */
@@ -609,7 +635,20 @@ struct ring_frame {
};
/* Minimum size for ring_rx */
-#define TB_FRAME_SIZE 0x100
+#define TB_FRAME_SIZE 256
+#define TB_MAX_FRAME_SIZE 4096
+
+static inline size_t tb_ring_frame_size(const struct ring_frame *frame)
+{
+ if (frame->size)
+ return frame->size;
+ return TB_MAX_FRAME_SIZE;
+}
+
+static inline size_t tb_ring_size(const struct tb_ring *ring)
+{
+ return ring->size;
+}
struct tb_ring *tb_ring_alloc_tx(struct tb_nhi *nhi, int hop, int size,
unsigned int flags);
@@ -618,6 +657,7 @@ struct tb_ring *tb_ring_alloc_rx(struct tb_nhi *nhi, int hop, int size,
u16 sof_mask, u16 eof_mask,
void (*start_poll)(void *), void *poll_data);
void tb_ring_start(struct tb_ring *ring);
+bool tb_ring_flush(struct tb_ring *ring, unsigned int timeout_msec);
void tb_ring_stop(struct tb_ring *ring);
void tb_ring_free(struct tb_ring *ring);
@@ -670,6 +710,8 @@ static inline int tb_ring_tx(struct tb_ring *ring, struct ring_frame *frame)
struct ring_frame *tb_ring_poll(struct tb_ring *ring);
void tb_ring_poll_complete(struct tb_ring *ring);
+int tb_ring_throttling(struct tb_ring *ring, unsigned int interval_nsec);
+
/**
* tb_ring_dma_device() - Return device used for DMA mapping
* @ring: Ring whose DMA device is retrieved
@@ -681,12 +723,17 @@ void tb_ring_poll_complete(struct tb_ring *ring);
*/
static inline struct device *tb_ring_dma_device(struct tb_ring *ring)
{
- return &ring->nhi->pdev->dev;
+ return ring->nhi->dev;
}
bool usb4_usb3_port_match(struct device *usb4_port_dev,
const struct fwnode_handle *usb3_port_fwnode);
+#if IS_REACHABLE(CONFIG_CONFIGFS_FS)
+int tb_configfs_register_group(struct config_group *group);
+void tb_configfs_unregister_group(struct config_group *group);
+#endif
+
#else /* CONFIG_USB4 */
static inline bool usb4_usb3_port_match(struct device *usb4_port_dev,
const struct fwnode_handle *usb3_port_fwnode)