diff --git a/sound/soc/sof/imx/Kconfig b/sound/soc/sof/imx/Kconfig index 09d88ce5b9e6e5..1aa8d2c27a924f 100644 --- a/sound/soc/sof/imx/Kconfig +++ b/sound/soc/sof/imx/Kconfig @@ -22,11 +22,20 @@ config SND_SOC_SOF_IMX_COMMON This option is not user-selectable but automagically handled by 'select' statements at a higher level. +config SND_SOC_SOF_IMX_PROBES + tristate + select SND_SOC_SOF_DEBUG_PROBES + help + This option is not user-selectable but automagically handled by + 'select' statements at a higher level. It pulls in the generic SOF + probes client (snd-sof-probes) for i.MX platforms. + config SND_SOC_SOF_IMX8 tristate "SOF support for i.MX8" depends on IMX_SCU depends on IMX_DSP select SND_SOC_SOF_IMX_COMMON + select SND_SOC_SOF_IMX_PROBES help This adds support for Sound Open Firmware for NXP i.MX8 platforms. Say Y if you have such a device. diff --git a/sound/soc/sof/imx/Makefile b/sound/soc/sof/imx/Makefile index 74b5ecad8fe8ce..b8d43ac7535b18 100644 --- a/sound/soc/sof/imx/Makefile +++ b/sound/soc/sof/imx/Makefile @@ -1,5 +1,6 @@ # SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause) snd-sof-imx8-y := imx8.o +snd-sof-imx8-$(CONFIG_SND_SOC_SOF_IMX_PROBES) += imx-probes.o snd-sof-imx9-y := imx9.o snd-sof-imx-common-y := imx-common.o diff --git a/sound/soc/sof/imx/imx-common.h b/sound/soc/sof/imx/imx-common.h index 9bd711dbb5d03f..77d43550494c39 100644 --- a/sound/soc/sof/imx/imx-common.h +++ b/sound/soc/sof/imx/imx-common.h @@ -166,4 +166,7 @@ void imx8_dump(struct snd_sof_dev *sdev, u32 flags); extern const struct snd_sof_dsp_ops sof_imx_ops; +int imx_probes_register(struct snd_sof_dev *sdev); +void imx_probes_unregister(struct snd_sof_dev *sdev); + #endif diff --git a/sound/soc/sof/imx/imx-probes.c b/sound/soc/sof/imx/imx-probes.c new file mode 100644 index 00000000000000..c383a7cf7c76a9 --- /dev/null +++ b/sound/soc/sof/imx/imx-probes.c @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause) +// +// Copyright 2026 NXP +// +// SOF probes client for i.MX (IPC3). The DSP cannot use a host DMA engine, so +// the host allocates a coherent buffer (header page + ring), gives its bus +// address to the firmware in PROBE_INIT (as stream_tag) and the firmware +// writes the extracted packets into the ring and publishes a byte counter in +// the first word of the header page. A delayed work copies new data into the +// compress buffer and wakes up the reader. + +#include +#include +#include +#include +#include +#include +#include +#include "../sof-priv.h" +#include "../sof-client-probes.h" +#include "../sof-client.h" +#include "imx-common.h" + +/* must match PROBE_HOST_HDR_SIZE in the firmware */ +#define IMX_PROBES_HDR_SIZE 4096 +#define IMX_PROBES_POLL_MS 10 + +struct imx_probes_stream { + struct delayed_work work; + struct snd_compr_stream *cstream; + struct device *dev; + void *vaddr; + dma_addr_t paddr; + size_t ring_size; + u32 sampling_rate; + u64 dsp_wr; /* bytes written by the DSP (32-bit counter extended) */ + u64 dsp_rd; /* bytes consumed from the DSP ring */ + u64 produced; /* bytes copied to the compress buffer in this run */ +}; + +/* only one probes extraction stream exists */ +static struct imx_probes_stream imx_stream; + +static void imx_probes_sync_wr(struct imx_probes_stream *s) +{ + /* the DSP counter is 32-bit and wraps */ + s->dsp_wr += (u32)(readl(s->vaddr) - (u32)s->dsp_wr); +} + +static void imx_probes_work(struct work_struct *work) +{ + struct imx_probes_stream *s = &imx_stream; + struct snd_compr_runtime *rtd; + u8 *ring = s->vaddr + IMX_PROBES_HDR_SIZE; + u64 pending, used; + u32 n, src, dst; + + if (!s->cstream || !s->vaddr) + return; + + rtd = s->cstream->runtime; + imx_probes_sync_wr(s); + + pending = s->dsp_wr - s->dsp_rd; + if (pending > s->ring_size) { + /* the DSP overwrote data we did not read, skip to the oldest valid */ + dev_warn_ratelimited(s->dev, "probes ring overrun, dropped %llu bytes\n", + pending - s->ring_size); + s->dsp_rd = s->dsp_wr - s->ring_size; + pending = s->ring_size; + } + + /* never overwrite data the reader has not consumed yet */ + used = s->produced - rtd->total_bytes_transferred; + n = min_t(u64, pending, rtd->buffer_size - used); + if (n) { + bool copied = false; + + src = s->dsp_rd % s->ring_size; + dst = s->produced % rtd->buffer_size; + /* copy in chunks that do not wrap either buffer */ + while (n) { + u32 chunk = min3(n, (u32)(s->ring_size - src), + (u32)(rtd->buffer_size - dst)); + + memcpy(rtd->dma_area + dst, ring + src, chunk); + s->dsp_rd += chunk; + s->produced += chunk; + src = (src + chunk) % s->ring_size; + dst = (dst + chunk) % rtd->buffer_size; + n -= chunk; + copied = true; + } + if (copied) + snd_compr_fragment_elapsed(s->cstream); + } + + schedule_delayed_work(&s->work, msecs_to_jiffies(IMX_PROBES_POLL_MS)); +} + +static int imx_probes_compr_startup(struct sof_client_dev *cdev, + struct snd_compr_stream *cstream, + struct snd_soc_dai *dai, u32 *stream_id) +{ + struct imx_probes_stream *s = &imx_stream; + + /* one stream only: do not touch the active stream's tag */ + if (s->cstream) + return -EBUSY; + + s->cstream = cstream; + /* real value (buffer address) is set in set_params */ + *stream_id = 0; + return 0; +} + +static void imx_probes_free_buf(struct imx_probes_stream *s) +{ + cancel_delayed_work_sync(&s->work); + if (s->vaddr) + dma_free_coherent(s->dev, IMX_PROBES_HDR_SIZE + s->ring_size, + s->vaddr, s->paddr); + s->vaddr = NULL; +} + +static int imx_probes_compr_shutdown(struct sof_client_dev *cdev, + struct snd_compr_stream *cstream, + struct snd_soc_dai *dai) +{ + struct imx_probes_stream *s = &imx_stream; + + if (s->cstream != cstream) + return 0; + + imx_probes_free_buf(s); + s->cstream = NULL; + return 0; +} + +static int imx_probes_compr_set_params(struct sof_client_dev *cdev, + struct snd_compr_stream *cstream, + struct snd_compr_params *params, + struct snd_soc_dai *dai) +{ + struct sof_probes_priv *priv = cdev->data; + struct imx_probes_stream *s = &imx_stream; + struct snd_soc_pcm_stream *pstream = &dai->driver->capture; + + imx_probes_free_buf(s); + + s->dev = sof_client_get_dma_dev(cdev); + /* DSP can only address 32-bit physical addresses */ + if (dma_set_coherent_mask(s->dev, DMA_BIT_MASK(32))) + return -ENOMEM; + /* firmware needs a power-of-two ring of at least 8 KiB */ + if (cstream->runtime->dma_bytes < SZ_8K || + !is_power_of_2(cstream->runtime->dma_bytes)) + return -EINVAL; + s->ring_size = cstream->runtime->dma_bytes; + s->vaddr = dma_alloc_coherent(s->dev, IMX_PROBES_HDR_SIZE + s->ring_size, + &s->paddr, GFP_KERNEL); + if (!s->vaddr) + return -ENOMEM; + + s->dsp_wr = 0; + s->dsp_rd = 0; + s->produced = 0; + s->sampling_rate = snd_pcm_rate_bit_to_rate(pstream->rates); + + /* the firmware takes the buffer address from the stream_tag field */ + priv->extractor_stream_tag = (u32)s->paddr; + + return 0; +} + +static int imx_probes_compr_trigger(struct sof_client_dev *cdev, + struct snd_compr_stream *cstream, + int cmd, struct snd_soc_dai *dai) +{ + struct imx_probes_stream *s = &imx_stream; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + /* a new run: the core restarts its counters, skip stale data */ + s->produced = 0; + if (s->vaddr) { + imx_probes_sync_wr(s); + s->dsp_rd = s->dsp_wr; + } + schedule_delayed_work(&s->work, msecs_to_jiffies(IMX_PROBES_POLL_MS)); + break; + case SNDRV_PCM_TRIGGER_STOP: + cancel_delayed_work_sync(&s->work); + s->produced = 0; + break; + } + + return 0; +} + +static int imx_probes_compr_pointer(struct sof_client_dev *cdev, + struct snd_compr_stream *cstream, + struct snd_compr_tstamp64 *tstamp, + struct snd_soc_dai *dai) +{ + tstamp->copied_total = imx_stream.produced; + tstamp->sampling_rate = imx_stream.sampling_rate; + + return 0; +} + +static const struct sof_probes_host_ops imx_probes_ops = { + .startup = imx_probes_compr_startup, + .shutdown = imx_probes_compr_shutdown, + .set_params = imx_probes_compr_set_params, + .trigger = imx_probes_compr_trigger, + .pointer = imx_probes_compr_pointer, +}; + +int imx_probes_register(struct snd_sof_dev *sdev) +{ + INIT_DELAYED_WORK(&imx_stream.work, imx_probes_work); + + return sof_client_dev_register(sdev, "imx-probes", 0, &imx_probes_ops, + sizeof(imx_probes_ops)); +} + +void imx_probes_unregister(struct snd_sof_dev *sdev) +{ + sof_client_dev_unregister(sdev, "imx-probes", 0); +} + +MODULE_IMPORT_NS("SND_SOC_SOF_CLIENT"); diff --git a/sound/soc/sof/imx/imx8.c b/sound/soc/sof/imx/imx8.c index 0820724dbb3491..3ce1f4fa6de3f5 100644 --- a/sound/soc/sof/imx/imx8.c +++ b/sound/soc/sof/imx/imx8.c @@ -295,6 +295,11 @@ static int imx8_ops_init(struct snd_sof_dev *sdev) sof_imx8_ops.debugfs_add_region_item = snd_sof_debugfs_add_region_item_iomem; + if (IS_ENABLED(CONFIG_SND_SOC_SOF_IMX_PROBES)) { + sof_imx8_ops.register_ipc_clients = imx_probes_register; + sof_imx8_ops.unregister_ipc_clients = imx_probes_unregister; + } + /* ... and finally set DAI driver */ sof_imx8_ops.drv = get_chip_info(sdev)->drv; sof_imx8_ops.num_drv = get_chip_info(sdev)->num_drv; diff --git a/sound/soc/sof/sof-client-probes.c b/sound/soc/sof/sof-client-probes.c index 124f5550815927..a4604db16a1126 100644 --- a/sound/soc/sof/sof-client-probes.c +++ b/sound/soc/sof/sof-client-probes.c @@ -55,7 +55,9 @@ static int sof_probes_compr_startup(struct snd_compr_stream *cstream, ret = ops->startup(cdev, cstream, dai, &priv->extractor_stream_tag); if (ret) { dev_err(dai->dev, "Failed to startup probe stream: %d\n", ret); - priv->extractor_stream_tag = SOF_PROBES_INVALID_NODE_ID; + /* keep the tag of the stream that is already running */ + if (ret != -EBUSY) + priv->extractor_stream_tag = SOF_PROBES_INVALID_NODE_ID; sof_client_core_module_put(cdev); } @@ -582,6 +584,7 @@ static void sof_probes_client_remove(struct auxiliary_device *auxdev) static const struct auxiliary_device_id sof_probes_client_id_table[] = { { .name = "snd_sof.hda-probes", }, { .name = "snd_sof.acp-probes", }, + { .name = "snd_sof.imx-probes", }, {}, }; MODULE_DEVICE_TABLE(auxiliary, sof_probes_client_id_table);