[PATCH v21 7/8] coresight: ctcu: enable byte-cntr for TMC ETR devices
Jie Gan
jie.gan at oss.qualcomm.com
Mon Nov 2 01:46:35 PST 2026
The byte-cntr function provided by the CTCU device is used to transfer data
from the ETR buffer to the userspace. An interrupt is triggered if the data
size exceeds the threshold set in the BYTECNTRVAL register. The interrupt
handler counts the number of triggered interruptions and the read function
will read the data from the synced ETR buffer.
When the current etr_buf is nearly full or the timeout is triggered, the
ETR is switched to the other etr_buf of the etr_buf_list and the synced
etr_buf becomes available for reading, while the ETR keeps tracing. Once
the trace has been stopped, the last etr_buf is read by the generic sysfs
read of the TMC.
The byte-cntr read is exclusive with the generic sysfs read of the TMC. It
is used only when it is enabled for the ETR, otherwise the TMC falls back
to its original read.
Byte-cntr workflow:
start -> ctcu_enable(ctcu_byte_cntr_start) -> tmc_enable_etr_sink ->
tmc_read_prepare_etr(jump to tmc_read_prepare_byte_cntr) ->
tmc_etr_get_sysfs_trace(jump to tmc_byte_cntr_get_data) ->
tmc_disable_etr_sink -> ctcu_disable(ctcu_byte_cntr_stop) ->
tmc_read_unprepare_etr(jump to tmc_read_unprepare_byte_cntr) -> finish
Signed-off-by: Jie Gan <jie.gan at oss.qualcomm.com>
---
.../ABI/testing/sysfs-bus-coresight-devices-ctcu | 9 +
drivers/hwtracing/coresight/Makefile | 2 +-
.../hwtracing/coresight/coresight-ctcu-byte-cntr.c | 400 +++++++++++++++++++++
drivers/hwtracing/coresight/coresight-ctcu-core.c | 163 ++++++++-
drivers/hwtracing/coresight/coresight-ctcu.h | 88 ++++-
drivers/hwtracing/coresight/coresight-tmc-core.c | 3 +-
drivers/hwtracing/coresight/coresight-tmc-etr.c | 87 ++++-
drivers/hwtracing/coresight/coresight-tmc.h | 5 +
8 files changed, 729 insertions(+), 28 deletions(-)
diff --git a/Documentation/ABI/testing/sysfs-bus-coresight-devices-ctcu b/Documentation/ABI/testing/sysfs-bus-coresight-devices-ctcu
new file mode 100644
index 000000000000..08dbc8e6b2ec
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-bus-coresight-devices-ctcu
@@ -0,0 +1,9 @@
+What: /sys/bus/coresight/devices/<ctcu-name>/irq_enabled[0:1]
+Date: October 2026
+KernelVersion: 7.5
+Contact: Tingwei Zhang <tingwei.zhang at oss.qualcomm.com>; Jinlong Mao <jinlong.mao at oss.qualcomm.com>; Jie Gan <jie.gan at oss.qualcomm.com>
+Description:
+ (RW) Configure the flag to enable interrupt to count data during CTCU enablement.
+ An interrupt is generated when the data size exceeds the value set in the IRQ register.
+ 0 : disable
+ 1 : enable
diff --git a/drivers/hwtracing/coresight/Makefile b/drivers/hwtracing/coresight/Makefile
index ab16d06783a5..821a1b06b20c 100644
--- a/drivers/hwtracing/coresight/Makefile
+++ b/drivers/hwtracing/coresight/Makefile
@@ -55,5 +55,5 @@ coresight-cti-y := coresight-cti-core.o coresight-cti-platform.o \
obj-$(CONFIG_ULTRASOC_SMB) += ultrasoc-smb.o
obj-$(CONFIG_CORESIGHT_DUMMY) += coresight-dummy.o
obj-$(CONFIG_CORESIGHT_CTCU) += coresight-ctcu.o
-coresight-ctcu-y := coresight-ctcu-core.o
+coresight-ctcu-y := coresight-ctcu-core.o coresight-ctcu-byte-cntr.o
obj-$(CONFIG_CORESIGHT_KUNIT_TESTS) += coresight-kunit-tests.o
diff --git a/drivers/hwtracing/coresight/coresight-ctcu-byte-cntr.c b/drivers/hwtracing/coresight/coresight-ctcu-byte-cntr.c
new file mode 100644
index 000000000000..bff87e776a75
--- /dev/null
+++ b/drivers/hwtracing/coresight/coresight-ctcu-byte-cntr.c
@@ -0,0 +1,400 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#include <linux/coresight.h>
+#include <linux/device.h>
+#include <linux/fs.h>
+#include <linux/interrupt.h>
+#include <linux/mutex.h>
+#include <linux/of_irq.h>
+#include <linux/uaccess.h>
+
+#include "coresight-ctcu.h"
+#include "coresight-priv.h"
+#include "coresight-tmc.h"
+
+/*
+ * The byte-cntr sysfs ops are shared by all the CTCU devices, so they are
+ * registered by the first CTCU with byte-cntr and removed with the last one.
+ */
+static DEFINE_MUTEX(byte_cntr_ops_lock);
+static int byte_cntr_ops_users;
+
+static void ctcu_byte_cntr_put_ops(void *data)
+{
+ guard(mutex)(&byte_cntr_ops_lock);
+ if (!--byte_cntr_ops_users)
+ tmc_etr_reset_byte_cntr_sysfs_ops();
+}
+
+static irqreturn_t byte_cntr_handler(int irq, void *data)
+{
+ struct ctcu_byte_cntr *byte_cntr_data = data;
+
+ atomic_inc(&byte_cntr_data->irq_cnt);
+ wake_up(&byte_cntr_data->wq);
+
+ return IRQ_HANDLED;
+}
+
+static void ctcu_cfg_byte_cntr_reg(struct ctcu_drvdata *drvdata, u32 val,
+ u32 offset)
+{
+ /* A one value for IRQCTRL register represents 8 bytes */
+ ctcu_program_register(drvdata, val / 8, offset);
+}
+
+static struct ctcu_byte_cntr *ctcu_get_byte_cntr(struct coresight_device *ctcu,
+ struct coresight_device *etr)
+{
+ struct ctcu_byte_cntr *byte_cntr_data;
+ struct ctcu_drvdata *drvdata;
+ int port;
+
+ /* The ETR may not be connected to a CTCU device */
+ if (!ctcu)
+ return NULL;
+
+ drvdata = dev_get_drvdata(ctcu->dev.parent);
+ port = coresight_get_in_port(etr, ctcu);
+ if (port < 0 || port >= ETR_MAX_NUM)
+ return NULL;
+
+ byte_cntr_data = &drvdata->byte_cntr_data[port];
+ /* The port is not initialized by ctcu_byte_cntr_init() */
+ if (!byte_cntr_data->ctcu_drvdata)
+ return NULL;
+
+ return byte_cntr_data;
+}
+
+/*
+ * ctcu_byte_cntr_get_data() - reads data from the deactivated and filled buffer.
+ * The byte-cntr reading work reads data from the deactivated and filled buffer.
+ * The read operation waits for a buffer to become available, either filled or
+ * upon timeout, and then reads trace data from the synced buffer.
+ */
+static ssize_t tmc_byte_cntr_get_data(struct tmc_drvdata *etr_drvdata, loff_t pos,
+ size_t len, char **bufpp)
+{
+ struct coresight_device *ctcu = tmc_etr_get_ctcu_device(etr_drvdata);
+ struct device *dev = &etr_drvdata->csdev->dev;
+ struct ctcu_byte_cntr *byte_cntr_data;
+ struct etr_buf *sysfs_buf;
+ atomic_t *irq_cnt;
+ ssize_t actual;
+ int ret;
+
+ /* No byte-cntr read session, let the caller use the generic read */
+ byte_cntr_data = ctcu_get_byte_cntr(ctcu, etr_drvdata->csdev);
+ if (!byte_cntr_data || !byte_cntr_data->irq_enabled ||
+ !byte_cntr_data->reading)
+ return -EINVAL;
+
+ irq_cnt = &byte_cntr_data->irq_cnt;
+
+wait_buffer:
+ if (!byte_cntr_data->buf_node) {
+ ret = wait_event_interruptible_timeout(byte_cntr_data->wq,
+ (atomic_read(irq_cnt) >= MAX_IRQ_CNT - 1) ||
+ !byte_cntr_data->enable,
+ BYTE_CNTR_TIMEOUT);
+ if (ret < 0)
+ return ret;
+ /*
+ * The current etr_buf is almost full or timeout is triggered,
+ * so switch the buffer and mark the switched buffer as reading.
+ */
+ if (byte_cntr_data->enable) {
+ byte_cntr_data->buf_node =
+ tmc_etr_switch_buf_node(etr_drvdata);
+ if (!byte_cntr_data->buf_node) {
+ /*
+ * The sink has stopped, let the generic read
+ * handle the last etr_buf.
+ */
+ if (coresight_get_mode(etr_drvdata->csdev) != CS_MODE_SYSFS)
+ return 0;
+
+ dev_err(dev, "Switch buffer failed for the byte-cntr\n");
+ return -ENOMEM;
+ }
+
+ /*
+ * Get the interrupt count of the switched out etr_buf
+ * and reset it for the next etr_buf.
+ */
+ if (atomic_xchg(irq_cnt, 0) >= MAX_IRQ_CNT)
+ dev_warn(dev, "Data overwrite happened\n");
+ } else {
+ /* Exit byte-cntr reading */
+ return 0;
+ }
+ }
+
+ pos = byte_cntr_data->buf_node->pos;
+ sysfs_buf = byte_cntr_data->buf_node->sysfs_buf;
+ actual = tmc_etr_read_sysfs_buf(sysfs_buf, pos, len, bufpp);
+ if (actual <= 0) {
+ /* Reset buf_node upon reading is finished or failed */
+ byte_cntr_data->buf_node->reading = false;
+ byte_cntr_data->buf_node = NULL;
+
+ /*
+ * Nothing in the buffer, waiting for the next buffer
+ * to be filled.
+ */
+ if (actual == 0)
+ goto wait_buffer;
+ }
+
+ return actual;
+}
+
+static int tmc_read_prepare_byte_cntr(struct tmc_drvdata *etr_drvdata)
+{
+ struct coresight_device *ctcu = tmc_etr_get_ctcu_device(etr_drvdata);
+ struct ctcu_byte_cntr *byte_cntr_data;
+ unsigned long flags;
+ int ret = 0;
+
+ /* byte-cntr is operating with SYSFS mode being enabled only */
+ if (coresight_get_mode(etr_drvdata->csdev) != CS_MODE_SYSFS)
+ return -EINVAL;
+
+ byte_cntr_data = ctcu_get_byte_cntr(ctcu, etr_drvdata->csdev);
+ if (!byte_cntr_data)
+ return -EINVAL;
+
+ raw_spin_lock_irqsave(&byte_cntr_data->spin_lock, flags);
+ /*
+ * irq_enabled can only be changed with the lock held and reading
+ * cleared, so check it here to keep it stable once reading is set.
+ */
+ if (!byte_cntr_data->irq_enabled) {
+ raw_spin_unlock_irqrestore(&byte_cntr_data->spin_lock, flags);
+ return -EINVAL;
+ }
+
+ if (byte_cntr_data->reading) {
+ raw_spin_unlock_irqrestore(&byte_cntr_data->spin_lock, flags);
+ return -EBUSY;
+ }
+
+ /* byte_cntr_data->enable may race with ctcu_platform_remove() */
+ if (!byte_cntr_data->enable) {
+ raw_spin_unlock_irqrestore(&byte_cntr_data->spin_lock, flags);
+ return -ENODEV;
+ }
+
+ /*
+ * Pin the module for the whole read session, so that rmmod fails with
+ * -EBUSY instead of blocking in ctcu_platform_remove() until the file
+ * is closed.
+ */
+ if (!try_module_get(THIS_MODULE)) {
+ raw_spin_unlock_irqrestore(&byte_cntr_data->spin_lock, flags);
+ return -ENODEV;
+ }
+
+ reinit_completion(&byte_cntr_data->session_done);
+ byte_cntr_data->reading = true;
+ raw_spin_unlock_irqrestore(&byte_cntr_data->spin_lock, flags);
+
+ /* Setup an available etr_buf_list for byte-cntr */
+ ret = tmc_create_etr_buf_list(etr_drvdata, 2);
+ if (ret)
+ goto err_reading;
+
+ /*
+ * The byte-cntr register is configured and the interrupts are counted
+ * since the path was enabled, so irq_cnt is not reset here.
+ */
+ scoped_guard(raw_spinlock_irqsave, &byte_cntr_data->spin_lock)
+ byte_cntr_data->buf_node = NULL;
+ /* enable_irq_wake() may sleep on slow-bus irqchips, call it unlocked */
+ enable_irq_wake(byte_cntr_data->irq);
+
+ return 0;
+
+err_reading:
+ /*
+ * reading was set before the sleeping allocations, so
+ * ctcu_platform_remove() may be waiting for the session to end. Complete
+ * the session with reading cleared in one critical section, so that a
+ * new session can't start in between.
+ */
+ scoped_guard(raw_spinlock_irqsave, &byte_cntr_data->spin_lock) {
+ byte_cntr_data->reading = false;
+ complete_all(&byte_cntr_data->session_done);
+ }
+
+ module_put(THIS_MODULE);
+ return ret;
+}
+
+static int tmc_read_unprepare_byte_cntr(struct tmc_drvdata *etr_drvdata)
+{
+ struct coresight_device *ctcu = tmc_etr_get_ctcu_device(etr_drvdata);
+ struct ctcu_byte_cntr *byte_cntr_data;
+
+ /*
+ * Do the unprepare operation only when the byte_cntr_data->reading
+ * is truly set
+ */
+ byte_cntr_data = ctcu_get_byte_cntr(ctcu, etr_drvdata->csdev);
+ if (!byte_cntr_data || !byte_cntr_data->irq_enabled ||
+ !byte_cntr_data->reading)
+ return -EINVAL;
+
+ tmc_clean_etr_buf_list(etr_drvdata);
+
+ /* disable_irq_wake() may sleep on slow-bus irqchips, call it unlocked */
+ disable_irq_wake(byte_cntr_data->irq);
+
+ /*
+ * The byte-cntr register is not accessed here: the trace path, which
+ * keeps the CTCU powered, may have been released already. It is reset
+ * when the path is disabled.
+ */
+ scoped_guard(raw_spinlock_irqsave, &byte_cntr_data->spin_lock) {
+ byte_cntr_data->buf_node = NULL;
+ byte_cntr_data->reading = false;
+ /*
+ * Complete the session with reading cleared in one critical
+ * section, so that a new session can't start in between.
+ * ctcu_platform_remove() may free byte_cntr_data after this,
+ * don't use it anymore.
+ */
+ complete_all(&byte_cntr_data->session_done);
+ }
+
+ module_put(THIS_MODULE);
+
+ return 0;
+}
+
+static const struct tmc_sysfs_ops byte_cntr_sysfs_ops = {
+ .read_prepare = tmc_read_prepare_byte_cntr,
+ .read_unprepare = tmc_read_unprepare_byte_cntr,
+ .get_trace_data = tmc_byte_cntr_get_data,
+};
+
+/* Start the byte-cntr function when the path is enabled. */
+void ctcu_byte_cntr_start(struct coresight_device *csdev, struct coresight_path *path)
+{
+ struct coresight_device *sink = coresight_get_sink(path);
+ struct tmc_drvdata *etr_drvdata;
+ struct ctcu_byte_cntr *byte_cntr_data;
+
+ byte_cntr_data = ctcu_get_byte_cntr(csdev, sink);
+ if (!byte_cntr_data)
+ return;
+
+ etr_drvdata = dev_get_drvdata(sink->dev.parent);
+ guard(raw_spinlock_irqsave)(&byte_cntr_data->spin_lock);
+ /*
+ * Don't start byte-cntr function when irq_enabled is not set, or when
+ * the CTCU device is being removed.
+ */
+ if (!byte_cntr_data->irq_enabled || byte_cntr_data->removing)
+ return;
+
+ /*
+ * A new trace starts with an empty etr_buf. Don't reset the count if
+ * the byte-cntr is already started by another path of the same sink.
+ */
+ if (!byte_cntr_data->enable)
+ atomic_set(&byte_cntr_data->irq_cnt, 0);
+
+ /*
+ * Configure the byte-cntr register to enable IRQ while the path is
+ * enabled, the CTCU is powered then. The configured size is 5% of the
+ * buffer_size.
+ */
+ ctcu_cfg_byte_cntr_reg(byte_cntr_data->ctcu_drvdata,
+ etr_drvdata->size / MAX_IRQ_CNT,
+ byte_cntr_data->irq_ctrl_offset);
+ byte_cntr_data->enable = true;
+}
+
+/* Stop the byte-cntr function when the path is disabled. */
+void ctcu_byte_cntr_stop(struct coresight_device *csdev, struct coresight_path *path)
+{
+ struct coresight_device *sink = coresight_get_sink(path);
+ struct ctcu_byte_cntr *byte_cntr_data;
+
+ if (coresight_get_mode(sink) == CS_MODE_SYSFS)
+ return;
+
+ byte_cntr_data = ctcu_get_byte_cntr(csdev, sink);
+ if (!byte_cntr_data)
+ return;
+
+ scoped_guard(raw_spinlock_irqsave, &byte_cntr_data->spin_lock) {
+ /*
+ * Configure the byte-cntr register to disable IRQ before the
+ * path is released, the CTCU is still powered.
+ */
+ if (byte_cntr_data->enable)
+ ctcu_cfg_byte_cntr_reg(byte_cntr_data->ctcu_drvdata, 0,
+ byte_cntr_data->irq_ctrl_offset);
+ byte_cntr_data->enable = false;
+ }
+
+ /* Wake up the blocked reader, which exits the byte-cntr reading */
+ wake_up(&byte_cntr_data->wq);
+}
+
+void ctcu_byte_cntr_init(struct device *dev, struct ctcu_drvdata *drvdata, int etr_num)
+{
+ struct ctcu_byte_cntr *byte_cntr_data;
+ struct device_node *nd = dev->of_node;
+ int irq_num, ret, i, irq_registered = 0;
+
+ for (i = 0; i < etr_num; i++) {
+ byte_cntr_data = &drvdata->byte_cntr_data[i];
+ /*
+ * Initialize the lock and the waitqueue before requesting the
+ * IRQ, and for every port as ctcu_platform_remove() uses them
+ * even if the IRQ of the port is not registered.
+ */
+ byte_cntr_data->ctcu_drvdata = drvdata;
+ init_waitqueue_head(&byte_cntr_data->wq);
+ raw_spin_lock_init(&byte_cntr_data->spin_lock);
+ init_completion(&byte_cntr_data->session_done);
+ /* No read session in progress */
+ complete_all(&byte_cntr_data->session_done);
+
+ irq_num = of_irq_get(nd, i);
+ if (irq_num <= 0) {
+ dev_err(dev, "Failed to get IRQ from DT for port%d\n", i);
+ continue;
+ }
+
+ ret = devm_request_irq(dev, irq_num, byte_cntr_handler,
+ IRQF_TRIGGER_RISING,
+ dev_name(dev), byte_cntr_data);
+ if (ret) {
+ dev_err(dev, "Failed to register IRQ for port%d\n", i);
+ continue;
+ }
+
+ byte_cntr_data->irq = irq_num;
+ irq_registered++;
+ }
+
+ if (!irq_registered)
+ return;
+
+ scoped_guard(mutex, &byte_cntr_ops_lock) {
+ tmc_etr_set_byte_cntr_sysfs_ops(&byte_cntr_sysfs_ops);
+ byte_cntr_ops_users++;
+ }
+
+ /* Drop the ops user when the CTCU device is removed or probe fails */
+ if (devm_add_action_or_reset(dev, ctcu_byte_cntr_put_ops, NULL))
+ dev_err(dev, "Failed to add the byte-cntr devm action\n");
+}
diff --git a/drivers/hwtracing/coresight/coresight-ctcu-core.c b/drivers/hwtracing/coresight/coresight-ctcu-core.c
index 739105acb0a1..cce4e4d76da0 100644
--- a/drivers/hwtracing/coresight/coresight-ctcu-core.c
+++ b/drivers/hwtracing/coresight/coresight-ctcu-core.c
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
- * Copyright (c) 2024-2025 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2024-2026 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
*/
#include <linux/clk.h>
@@ -18,6 +19,7 @@
#include "coresight-ctcu.h"
#include "coresight-priv.h"
+#include "coresight-tmc.h"
#define ctcu_writel(drvdata, val, offset) __raw_writel((val), drvdata->base + offset)
#define ctcu_readl(drvdata, offset) __raw_readl(drvdata->base + offset)
@@ -43,17 +45,21 @@
#define CTCU_ATID_REG_BIT(traceid) (traceid % 32)
#define CTCU_ATID_REG_SIZE 0x10
+#define CTCU_ETR0_IRQCTRL 0x6c
+#define CTCU_ETR1_IRQCTRL 0x70
#define CTCU_ETR0_ATID0 0xf8
#define CTCU_ETR1_ATID0 0x108
static const struct ctcu_etr_config sa8775p_etr_cfgs[] = {
{
- .atid_offset = CTCU_ETR0_ATID0,
- .port_num = 0,
+ .atid_offset = CTCU_ETR0_ATID0,
+ .irq_ctrl_offset = CTCU_ETR0_IRQCTRL,
+ .port_num = 0,
},
{
- .atid_offset = CTCU_ETR1_ATID0,
- .port_num = 1,
+ .atid_offset = CTCU_ETR1_ATID0,
+ .irq_ctrl_offset = CTCU_ETR1_IRQCTRL,
+ .port_num = 1,
},
};
@@ -62,6 +68,93 @@ static const struct ctcu_config sa8775p_cfgs = {
.num_etr_config = ARRAY_SIZE(sa8775p_etr_cfgs),
};
+/*
+ * The register access is unlocked and locked in the same way as the ATID
+ * register, so serialise them with drvdata->spin_lock. Otherwise one side may
+ * lock the registers between the unlock and the write of the other side.
+ */
+void ctcu_program_register(struct ctcu_drvdata *drvdata, u32 val, u32 offset)
+{
+ guard(raw_spinlock_irqsave)(&drvdata->spin_lock);
+ CS_UNLOCK(drvdata->base);
+ ctcu_writel(drvdata, val, offset);
+ CS_LOCK(drvdata->base);
+}
+
+static ssize_t irq_enabled_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct ctcu_byte_cntr_irq_attribute *irq_attr =
+ container_of(attr, struct ctcu_byte_cntr_irq_attribute, attr);
+ struct ctcu_drvdata *drvdata = dev_get_drvdata(dev->parent);
+ u8 port = irq_attr->port;
+
+ if (!drvdata->byte_cntr_data[port].irq_ctrl_offset)
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%u\n",
+ (unsigned int)drvdata->byte_cntr_data[port].irq_enabled);
+}
+
+static ssize_t irq_enabled_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf,
+ size_t size)
+{
+ struct ctcu_byte_cntr_irq_attribute *irq_attr =
+ container_of(attr, struct ctcu_byte_cntr_irq_attribute, attr);
+ struct ctcu_drvdata *drvdata = dev_get_drvdata(dev->parent);
+ u8 port = irq_attr->port;
+ unsigned long val;
+
+ if (kstrtoul(buf, 0, &val))
+ return -EINVAL;
+
+ guard(raw_spinlock_irqsave)(&drvdata->byte_cntr_data[port].spin_lock);
+ if (drvdata->byte_cntr_data[port].reading)
+ return -EBUSY;
+ else if (drvdata->byte_cntr_data[port].irq_ctrl_offset)
+ drvdata->byte_cntr_data[port].irq_enabled = !!val;
+
+ return size;
+}
+
+static umode_t irq_enabled_is_visible(struct kobject *kobj,
+ struct attribute *attr, int n)
+{
+ struct device_attribute *dev_attr =
+ container_of(attr, struct device_attribute, attr);
+ struct ctcu_byte_cntr_irq_attribute *irq_attr =
+ container_of(dev_attr, struct ctcu_byte_cntr_irq_attribute, attr);
+ struct device *dev = kobj_to_dev(kobj);
+ struct ctcu_drvdata *drvdata = dev_get_drvdata(dev->parent);
+ u8 port = irq_attr->port;
+
+ /* Expose irq_enabled only for the ports with a registered IRQ */
+ if (drvdata && drvdata->byte_cntr_data[port].irq_ctrl_offset &&
+ drvdata->byte_cntr_data[port].irq)
+ return attr->mode;
+
+ return 0;
+}
+
+static struct attribute *ctcu_attrs[] = {
+ ctcu_byte_cntr_irq_rw(0),
+ ctcu_byte_cntr_irq_rw(1),
+ NULL,
+};
+
+static struct attribute_group ctcu_attr_grp = {
+ .attrs = ctcu_attrs,
+ .is_visible = irq_enabled_is_visible,
+};
+
+static const struct attribute_group *ctcu_attr_grps[] = {
+ &ctcu_attr_grp,
+ NULL,
+};
+
static void ctcu_program_atid_register(struct ctcu_drvdata *drvdata, u32 reg_offset,
u8 bit, bool enable)
{
@@ -140,11 +233,23 @@ static int ctcu_set_etr_traceid(struct coresight_device *csdev, struct coresight
static int ctcu_enable(struct coresight_device *csdev, enum cs_mode mode,
struct coresight_path *path)
{
- return ctcu_set_etr_traceid(csdev, path, true);
+ int ret;
+
+ ret = ctcu_set_etr_traceid(csdev, path, true);
+ /*
+ * Byte-cntr works in sysfs mode only. Start it once the trace ID is set,
+ * as ctcu_disable() won't be called to stop it if the enabling failed.
+ */
+ if (!ret && mode == CS_MODE_SYSFS)
+ ctcu_byte_cntr_start(csdev, path);
+
+ return ret;
}
static int ctcu_disable(struct coresight_device *csdev, struct coresight_path *path)
{
+ ctcu_byte_cntr_stop(csdev, path);
+
return ctcu_set_etr_traceid(csdev, path, false);
}
@@ -195,7 +300,10 @@ static int ctcu_probe(struct platform_device *pdev)
for (i = 0; i < cfgs->num_etr_config; i++) {
etr_cfg = &cfgs->etr_cfgs[i];
drvdata->atid_offset[i] = etr_cfg->atid_offset;
+ drvdata->byte_cntr_data[i].irq_ctrl_offset =
+ etr_cfg->irq_ctrl_offset;
}
+ ctcu_byte_cntr_init(dev, drvdata, cfgs->num_etr_config);
}
}
@@ -209,6 +317,7 @@ static int ctcu_probe(struct platform_device *pdev)
desc.dev = dev;
desc.ops = &ctcu_ops;
desc.access = CSDEV_ACCESS_IOMEM(base);
+ desc.groups = ctcu_attr_grps;
raw_spin_lock_init(&drvdata->spin_lock);
drvdata->csdev = coresight_register(&desc);
@@ -244,10 +353,52 @@ static int ctcu_platform_probe(struct platform_device *pdev)
static void ctcu_platform_remove(struct platform_device *pdev)
{
struct ctcu_drvdata *drvdata = platform_get_drvdata(pdev);
+ struct ctcu_byte_cntr *byte_cntr_data;
+ unsigned long flags;
+ int i;
if (WARN_ON(!drvdata))
return;
+ /*
+ * Signal all active byte-cntr readers to exit, then wait for them to
+ * finish before resetting the ops pointer and freeing driver data.
+ * Without this, a reader blocked in wait_event_interruptible_timeout()
+ * would access the freed ctcu_drvdata wait-queue head (use-after-free).
+ *
+ * The wait ends only when the userspace closes the tmc_etr file, so an
+ * unbind with the file left open blocks until then. Unloading the module
+ * is rejected with -EBUSY while a read session is open, so only a manual
+ * unbind can block here.
+ */
+ for (i = 0; i < ETR_MAX_NUM; i++) {
+ byte_cntr_data = &drvdata->byte_cntr_data[i];
+ /* The port was not initialized by ctcu_byte_cntr_init() */
+ if (!byte_cntr_data->irq)
+ continue;
+
+ raw_spin_lock_irqsave(&byte_cntr_data->spin_lock, flags);
+ /*
+ * Set enable=false to signal teardown to racing readers, and
+ * removing=true to keep a new path from enabling byte-cntr again.
+ */
+ byte_cntr_data->enable = false;
+ byte_cntr_data->removing = true;
+ raw_spin_unlock_irqrestore(&byte_cntr_data->spin_lock, flags);
+
+ /* Wake up the blocked reader, then wait for the session to end */
+ wake_up_all(&byte_cntr_data->wq);
+ wait_for_completion(&byte_cntr_data->session_done);
+
+ /*
+ * The ending session completes it with the lock held. Take the
+ * lock once more, to be sure that it has released the lock
+ * before byte_cntr_data is freed.
+ */
+ raw_spin_lock_irqsave(&byte_cntr_data->spin_lock, flags);
+ raw_spin_unlock_irqrestore(&byte_cntr_data->spin_lock, flags);
+ }
+
ctcu_remove(pdev);
pm_runtime_disable(&pdev->dev);
}
diff --git a/drivers/hwtracing/coresight/coresight-ctcu.h b/drivers/hwtracing/coresight/coresight-ctcu.h
index e9594c38dd91..60dded125ece 100644
--- a/drivers/hwtracing/coresight/coresight-ctcu.h
+++ b/drivers/hwtracing/coresight/coresight-ctcu.h
@@ -1,23 +1,32 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
- * Copyright (c) 2024-2025 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2024-2026 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
*/
#ifndef _CORESIGHT_CTCU_H
#define _CORESIGHT_CTCU_H
+
+#include <linux/completion.h>
+#include <linux/time.h>
#include "coresight-trace-id.h"
/* Maximum number of supported ETR devices for a single CTCU. */
#define ETR_MAX_NUM 2
+#define BYTE_CNTR_TIMEOUT (3 * HZ)
+#define MAX_IRQ_CNT 20
+
/**
- * struct ctcu_etr_config
+ * struct ctcu_etr_config - ETR specific configuration of the CTCU device
* @atid_offset: offset to the ATID0 Register.
- * @port_num: in-port number of CTCU device that connected to ETR.
+ * @port_num: in-port number of the CTCU device that connected to ETR.
+ * @irq_ctrl_offset: offset to the BYTECNTRVAL register.
*/
struct ctcu_etr_config {
const u32 atid_offset;
const u32 port_num;
+ const u32 irq_ctrl_offset;
};
struct ctcu_config {
@@ -25,15 +34,72 @@ struct ctcu_config {
int num_etr_config;
};
-struct ctcu_drvdata {
- void __iomem *base;
- struct clk *apb_clk;
- struct device *dev;
- struct coresight_device *csdev;
+/**
+ * struct ctcu_byte_cntr - byte-cntr data of a port of the CTCU device
+ * @enable: indicates that byte_cntr function is enabled or not.
+ * @irq_enabled: indicates that the interruption is enabled.
+ * @reading: indicates that byte_cntr is reading.
+ * @removing: indicates that the CTCU device is being removed.
+ * @irq: allocated number of the IRQ.
+ * @irq_cnt: IRQ count number of the triggered interruptions.
+ * @wq: waitqueue for reading data from ETR buffer.
+ * @spin_lock: spinlock of the byte_cntr_data.
+ * @session_done: completed when there is no read session in progress.
+ * @irq_ctrl_offset: offset to the BYTECNTVAL Register.
+ * @ctcu_drvdata: drvdata of the CTCU device.
+ * @buf_node: etr_buf_node for reading.
+ */
+struct ctcu_byte_cntr {
+ bool enable;
+ bool irq_enabled;
+ bool reading;
+ bool removing;
+ int irq;
+ atomic_t irq_cnt;
+ wait_queue_head_t wq;
raw_spinlock_t spin_lock;
- u32 atid_offset[ETR_MAX_NUM];
- /* refcnt for each traceid of each sink */
- u8 traceid_refcnt[ETR_MAX_NUM][CORESIGHT_TRACE_ID_RES_TOP];
+ struct completion session_done;
+ u32 irq_ctrl_offset;
+ struct ctcu_drvdata *ctcu_drvdata;
+ struct etr_buf_node *buf_node;
};
+struct ctcu_drvdata {
+ void __iomem *base;
+ struct clk *apb_clk;
+ struct device *dev;
+ struct coresight_device *csdev;
+ struct ctcu_byte_cntr byte_cntr_data[ETR_MAX_NUM];
+ raw_spinlock_t spin_lock;
+ u32 atid_offset[ETR_MAX_NUM];
+ /* refcnt for each traceid of each sink */
+ u8 traceid_refcnt[ETR_MAX_NUM][CORESIGHT_TRACE_ID_RES_TOP];
+};
+
+/**
+ * struct ctcu_byte_cntr_irq_attribute - sysfs attribute of the byte-cntr IRQ
+ * @attr: The device attribute.
+ * @port: port number.
+ */
+struct ctcu_byte_cntr_irq_attribute {
+ struct device_attribute attr;
+ u8 port;
+};
+
+#define ctcu_byte_cntr_irq_rw(port) \
+ (&((struct ctcu_byte_cntr_irq_attribute[]) { \
+ { \
+ __ATTR(irq_enabled##port, 0644, irq_enabled_show, \
+ irq_enabled_store), \
+ port, \
+ } \
+ })[0].attr.attr)
+
+void ctcu_program_register(struct ctcu_drvdata *drvdata, u32 val, u32 offset);
+
+/* Byte-cntr functions */
+void ctcu_byte_cntr_start(struct coresight_device *csdev, struct coresight_path *path);
+void ctcu_byte_cntr_stop(struct coresight_device *csdev, struct coresight_path *path);
+void ctcu_byte_cntr_init(struct device *dev, struct ctcu_drvdata *drvdata, int port_num);
+
#endif
diff --git a/drivers/hwtracing/coresight/coresight-tmc-core.c b/drivers/hwtracing/coresight/coresight-tmc-core.c
index 4b40b692be4d..6ad09995ba87 100644
--- a/drivers/hwtracing/coresight/coresight-tmc-core.c
+++ b/drivers/hwtracing/coresight/coresight-tmc-core.c
@@ -293,7 +293,8 @@ static ssize_t tmc_read(struct file *file, char __user *data, size_t len,
return -EFAULT;
}
- *ppos += actual;
+ if (!tmc_etr_update_buf_node_pos(drvdata, actual))
+ *ppos += actual;
dev_dbg(&drvdata->csdev->dev, "%zu bytes copied\n", actual);
return actual;
diff --git a/drivers/hwtracing/coresight/coresight-tmc-etr.c b/drivers/hwtracing/coresight/coresight-tmc-etr.c
index bf4c1d8f7b2e..20d6bc60a446 100644
--- a/drivers/hwtracing/coresight/coresight-tmc-etr.c
+++ b/drivers/hwtracing/coresight/coresight-tmc-etr.c
@@ -1168,6 +1168,9 @@ static int tmc_etr_enable_hw(struct tmc_drvdata *drvdata,
return rc;
}
+/* Shared by the CTCU devices, set by the first one and reset by the last one. */
+static const struct tmc_sysfs_ops *byte_cntr_sysfs_ops;
+
/*
* Return the available trace data in the buffer (starts at etr_buf->offset,
* limited by etr_buf->len) from @pos, with a maximum limit of @len,
@@ -1178,23 +1181,44 @@ static int tmc_etr_enable_hw(struct tmc_drvdata *drvdata,
* We are protected here by drvdata->reading != 0, which ensures the
* sysfs_buf stays alive.
*/
-ssize_t tmc_etr_get_sysfs_trace(struct tmc_drvdata *drvdata,
- loff_t pos, size_t len, char **bufpp)
+ssize_t tmc_etr_read_sysfs_buf(struct etr_buf *sysfs_buf, loff_t pos,
+ size_t len, char **bufpp)
{
s64 offset;
ssize_t actual = len;
- struct etr_buf *etr_buf = drvdata->sysfs_buf;
- if (pos + actual > etr_buf->len)
- actual = etr_buf->len - pos;
+ if (pos + actual > sysfs_buf->len)
+ actual = sysfs_buf->len - pos;
if (actual <= 0)
return actual;
/* Compute the offset from which we read the data */
- offset = etr_buf->offset + pos;
- if (offset >= etr_buf->size)
- offset -= etr_buf->size;
- return tmc_etr_buf_get_data(etr_buf, offset, actual, bufpp);
+ offset = sysfs_buf->offset + pos;
+ if (offset >= sysfs_buf->size)
+ offset -= sysfs_buf->size;
+ return tmc_etr_buf_get_data(sysfs_buf, offset, actual, bufpp);
+}
+EXPORT_SYMBOL_GPL(tmc_etr_read_sysfs_buf);
+
+ssize_t tmc_etr_get_sysfs_trace(struct tmc_drvdata *drvdata,
+ loff_t pos, size_t len, char **bufpp)
+{
+ ssize_t ret;
+ const struct tmc_sysfs_ops *byte_cntr_ops = READ_ONCE(byte_cntr_sysfs_ops);
+
+ if (byte_cntr_ops) {
+ ret = byte_cntr_ops->get_trace_data(drvdata, pos, len, bufpp);
+ /*
+ * -EINVAL means byte-cntr has no active read session. 0 means
+ * byte-cntr has no more data because the sink has stopped, the
+ * last etr_buf is read by the generic read. Anything else is
+ * the result of the byte-cntr session.
+ */
+ if (ret != -EINVAL && ret != 0)
+ return ret;
+ }
+
+ return tmc_etr_read_sysfs_buf(drvdata->sysfs_buf, pos, len, bufpp);
}
static struct etr_buf *
@@ -2170,15 +2194,54 @@ int tmc_create_etr_buf_list(struct tmc_drvdata *drvdata, int num_nodes)
}
EXPORT_SYMBOL_GPL(tmc_create_etr_buf_list);
+void tmc_etr_set_byte_cntr_sysfs_ops(const struct tmc_sysfs_ops *sysfs_ops)
+{
+ WRITE_ONCE(byte_cntr_sysfs_ops, sysfs_ops);
+}
+EXPORT_SYMBOL_GPL(tmc_etr_set_byte_cntr_sysfs_ops);
+
+void tmc_etr_reset_byte_cntr_sysfs_ops(void)
+{
+ WRITE_ONCE(byte_cntr_sysfs_ops, NULL);
+}
+EXPORT_SYMBOL_GPL(tmc_etr_reset_byte_cntr_sysfs_ops);
+
+bool tmc_etr_update_buf_node_pos(struct tmc_drvdata *drvdata, ssize_t size)
+{
+ struct etr_buf_node *nd, *next;
+
+ if (drvdata->config_type != TMC_CONFIG_TYPE_ETR)
+ return false;
+
+ list_for_each_entry_safe(nd, next, &drvdata->etr_buf_list, link) {
+ if (nd && nd->reading) {
+ nd->pos += size;
+ return true;
+ }
+ }
+
+ return false;
+}
+
int tmc_read_prepare_etr(struct tmc_drvdata *drvdata)
{
int ret = 0;
unsigned long flags;
+ const struct tmc_sysfs_ops *byte_cntr_ops;
/* config types are set a boot time and never change */
if (WARN_ON_ONCE(drvdata->config_type != TMC_CONFIG_TYPE_ETR))
return -EINVAL;
+ byte_cntr_ops = READ_ONCE(byte_cntr_sysfs_ops);
+ if (byte_cntr_ops) {
+ ret = byte_cntr_ops->read_prepare(drvdata);
+ if (!ret || ret == -EBUSY)
+ return ret;
+
+ ret = 0;
+ }
+
raw_spin_lock_irqsave(&drvdata->spinlock, flags);
/* The sysfs_buf is owned by the etr_buf_list */
if (drvdata->reading || !list_empty(&drvdata->etr_buf_list)) {
@@ -2211,11 +2274,17 @@ int tmc_read_unprepare_etr(struct tmc_drvdata *drvdata)
{
unsigned long flags;
struct etr_buf *sysfs_buf = NULL;
+ const struct tmc_sysfs_ops *byte_cntr_ops;
/* config types are set a boot time and never change */
if (WARN_ON_ONCE(drvdata->config_type != TMC_CONFIG_TYPE_ETR))
return -EINVAL;
+ byte_cntr_ops = READ_ONCE(byte_cntr_sysfs_ops);
+ if (byte_cntr_ops)
+ if (!byte_cntr_ops->read_unprepare(drvdata))
+ return 0;
+
raw_spin_lock_irqsave(&drvdata->spinlock, flags);
/* RE-enable the TMC if need be */
diff --git a/drivers/hwtracing/coresight/coresight-tmc.h b/drivers/hwtracing/coresight/coresight-tmc.h
index 2345bdbb0b9d..af89825ec041 100644
--- a/drivers/hwtracing/coresight/coresight-tmc.h
+++ b/drivers/hwtracing/coresight/coresight-tmc.h
@@ -484,6 +484,11 @@ struct etr_buf *tmc_etr_get_buffer(struct coresight_device *csdev,
extern const struct attribute_group coresight_etr_group;
void tmc_clean_etr_buf_list(struct tmc_drvdata *drvdata);
int tmc_create_etr_buf_list(struct tmc_drvdata *drvdata, int num_nodes);
+void tmc_etr_set_byte_cntr_sysfs_ops(const struct tmc_sysfs_ops *sysfs_ops);
+void tmc_etr_reset_byte_cntr_sysfs_ops(void);
struct etr_buf_node *tmc_etr_switch_buf_node(struct tmc_drvdata *drvdata);
+bool tmc_etr_update_buf_node_pos(struct tmc_drvdata *drvdata, ssize_t size);
+ssize_t tmc_etr_read_sysfs_buf(struct etr_buf *sysfs_buf, loff_t pos,
+ size_t len, char **bufpp);
#endif
--
2.34.1
More information about the linux-arm-kernel
mailing list