[PATCH v2 2/3] ASoC: qcom: q6apm-dai: add VMID-based SCM assignment for mDSP buffers
Ajay Kumar Nandam <[email protected]>
| Newsgroups | gmane.linux.drivers.devicetree,gmane.linux.sound,gmane.linux.ports.arm.msm,gmane.linux.kernel |
|---|---|
| Message-ID | <[email protected]> |
On platforms such as Qualcomm Shikra, audio is served by the modem DSP (mDSP) which runs in a stage-2 protected context. Unlike ADSP targets where SMMU-mapped system RAM is directly accessible, the mDSP cannot reach the PCM buffers unless they are explicitly SCM-assigned to the appropriate Virtual Machine IDs (VMIDs). Without this assignment, audio does not function on these platforms. Parse the qcom,vmids DT property and, when present, SCM-assign all memory regions to HLOS (retained as source owner, RW) plus the listed consumer VMIDs (all RW). The two access models are mutually exclusive: a node uses the SMMU path (iommus) or the SCM path (qcom,vmids), never both. Memory assignment follows the upstream pattern used by rmtfs_mem and qcom_q6v5_pas: reserved-memory carveouts are SCM-assigned once at probe() time and restored to HLOS-only ownership via devm_add_action_or_reset() at device removal. For the non-reserved-mem path (qcom,vmids without memory-region), the fixed DMA buffer is assigned at pcm_new() time since its address is only known after allocation. When memory-region is present (has_reserved_mem), the data-path carveout is attached as a DMA pool via of_reserved_mem_device_init_by_idx() and PCM buffers allocate directly from the carveout instead of system RAM. Buffer constraints are capped at reserved_buf_size and snd_pcm_set_managed_buffer_all() replaces snd_pcm_set_fixed_buffer_all(). All new code paths are gated on use_scm_assign (false when qcom,vmids is absent), ensuring existing ADSP/iommus targets are completely unaffected. Co-developed-by: Mohit Sharma <[email protected]> Signed-off-by: Mohit Sharma <[email protected]> Signed-off-by: Ajay Kumar Nandam <[email protected]> --- sound/soc/qcom/Kconfig | 1 + sound/soc/qcom/qdsp6/q6apm-dai.c | 373 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 351 insertions(+), 23 deletions(-) diff --git a/sound/soc/qcom/Kconfig b/sound/soc/qcom/Kconfig index e6e24f3b9922..82f496e53acb 100644 --- a/sound/soc/qcom/Kconfig +++ b/sound/soc/qcom/Kconfig @@ -101,6 +101,7 @@ config SND_SOC_QDSP6_ASM_DAI config SND_SOC_QDSP6_APM_DAI tristate + select QCOM_SCM select SND_SOC_COMPRESS config SND_SOC_QDSP6_APM_LPASS_DAI diff --git a/sound/soc/qcom/qdsp6/q6apm-dai.c b/sound/soc/qcom/qdsp6/q6apm-dai.c index bf1f872a09f4..19a5c62cc4e1 100644 --- a/sound/soc/qcom/qdsp6/q6apm-dai.c +++ b/sound/soc/qcom/qdsp6/q6apm-dai.c @@ -1,20 +1,22 @@ // SPDX-License-Identifier: GPL-2.0 // Copyright (c) 2021, Linaro Limited -#include <linux/init.h> +#include <linux/dma-mapping.h> #include <linux/err.h> +#include <linux/firmware/qcom/qcom_scm.h> +#include <linux/init.h> #include <linux/module.h> #include <linux/of.h> +#include <linux/of_reserved_mem.h> #include <linux/platform_device.h> #include <linux/slab.h> -#include <sound/soc.h> -#include <sound/soc-dapm.h> #include <linux/spinlock.h> -#include <sound/pcm.h> #include <asm/div64.h> #include <asm/dma.h> -#include <linux/dma-mapping.h> +#include <sound/pcm.h> #include <sound/pcm_params.h> +#include <sound/soc.h> +#include <sound/soc-dapm.h> #include "q6apm.h" #define DRV_NAME "q6apm-dai" @@ -34,6 +36,9 @@ #define COMPR_PLAYBACK_MAX_NUM_FRAGMENTS (16 * 4) #define COMPR_PLAYBACK_MIN_FRAGMENT_SIZE (8 * 1024) #define COMPR_PLAYBACK_MIN_NUM_FRAGMENTS (4) +#define Q6APM_MAX_VMIDS 2 +#define Q6APM_MAX_CARVEOUTS 2 +#define Q6APM_POOL_MAX_STREAMS 8 #define SID_MASK_DEFAULT 0xF static const struct snd_compr_codec_caps q6apm_compr_caps = { @@ -57,6 +62,13 @@ enum stream_state { Q6APM_STREAM_RUNNING, }; +struct q6apm_scm_region { + phys_addr_t addr; + size_t size; + u64 src_perms; + bool assigned; +}; + struct q6apm_dai_rtd { struct snd_pcm_substream *substream; struct snd_compr_stream *cstream; @@ -84,9 +96,123 @@ struct q6apm_dai_rtd { }; struct q6apm_dai_data { + struct device *dev; long long sid; + int num_vmids; + u32 vmids[Q6APM_MAX_VMIDS]; + bool use_scm_assign; + bool has_reserved_mem; + size_t reserved_buf_size; + struct q6apm_scm_region carveout_regions[Q6APM_MAX_CARVEOUTS]; + int num_carveouts; }; +static int q6apm_dai_scm_assign(struct q6apm_scm_region *region, + const struct q6apm_dai_data *pdata) +{ + struct qcom_scm_vmperm *dst; + int dst_count = 0; + int ret, i; + + if (region->assigned) + return 0; + + dst = kcalloc(pdata->num_vmids + 1, sizeof(*dst), GFP_KERNEL); + if (!dst) + return -ENOMEM; + + dst[dst_count].vmid = QCOM_SCM_VMID_HLOS; + dst[dst_count].perm = QCOM_SCM_PERM_RW; + dst_count++; + + for (i = 0; i < pdata->num_vmids; i++) { + if (WARN_ON_ONCE(pdata->vmids[i] == QCOM_SCM_VMID_HLOS)) + continue; + dst[dst_count].vmid = pdata->vmids[i]; + dst[dst_count].perm = QCOM_SCM_PERM_RW; + dst_count++; + } + + if (dst_count == 1) { + kfree(dst); + return 0; + } + + ret = qcom_scm_assign_mem(region->addr, region->size, + ®ion->src_perms, dst, dst_count); + kfree(dst); + if (!ret) + region->assigned = true; + + return ret; +} + +static void q6apm_dai_scm_unassign(struct q6apm_scm_region *region, + const struct q6apm_dai_data *pdata) +{ + struct qcom_scm_vmperm hlos = { + .vmid = QCOM_SCM_VMID_HLOS, + .perm = QCOM_SCM_PERM_RW, + }; + int ret; + + if (!region->assigned) + return; + + ret = qcom_scm_assign_mem(region->addr, region->size, + ®ion->src_perms, &hlos, 1); + if (ret) { + /* + * A failed reclaim leaves the buffer owned by the DSP VMIDs + * instead of HLOS: it must not be reused. Warn loudly and keep + * it marked assigned so it is never handed back to the pool. + */ + dev_err(pdata->dev, + "SCM unassign failed for %pa (size %zu): %d\n", + ®ion->addr, region->size, ret); + return; + } + + region->assigned = false; + region->src_perms = BIT_ULL(QCOM_SCM_VMID_HLOS); +} + +static void q6apm_dai_unassign_all(void *data) +{ + struct q6apm_dai_data *pdata = data; + int i; + + for (i = 0; i < pdata->num_carveouts; i++) + q6apm_dai_scm_unassign(&pdata->carveout_regions[i], pdata); +} + +/* + * Reclaim and drop the runtime carveout tracking the buffer at @addr. + * Only per-PCM buffers (allocated in pcm_new when there is no reserved + * memory pool) are removed this way, so tearing one PCM down never + * disturbs regions still owned by other live PCMs. + */ +static void q6apm_dai_scm_unassign_addr(struct q6apm_dai_data *pdata, + phys_addr_t addr) +{ + int i; + + for (i = 0; i < pdata->num_carveouts; i++) { + if (pdata->carveout_regions[i].addr != addr) + continue; + + q6apm_dai_scm_unassign(&pdata->carveout_regions[i], pdata); + if (pdata->carveout_regions[i].assigned) + return; /* reclaim failed: keep tracking, warned already */ + + /* swap-remove the (now HLOS-owned) slot */ + pdata->num_carveouts--; + pdata->carveout_regions[i] = + pdata->carveout_regions[pdata->num_carveouts]; + return; + } +} + static const struct snd_pcm_hardware q6apm_dai_hardware_capture = { .info = (SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_MMAP_VALID | SNDRV_PCM_INFO_INTERLEAVED | @@ -409,8 +535,11 @@ static int q6apm_dai_open(struct snd_soc_component *component, } if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + size_t buf_max = pdata->has_reserved_mem ? + pdata->reserved_buf_size : BUFFER_BYTES_MAX; + ret = snd_pcm_hw_constraint_minmax(runtime, SNDRV_PCM_HW_PARAM_BUFFER_BYTES, - BUFFER_BYTES_MIN, BUFFER_BYTES_MAX); + BUFFER_BYTES_MIN, buf_max); if (ret < 0) { dev_err(dev, "constraint for buffer bytes min max ret = %d\n", ret); goto err; @@ -431,17 +560,18 @@ static int q6apm_dai_open(struct snd_soc_component *component, } runtime->private_data = prtd; - runtime->dma_bytes = BUFFER_BYTES_MAX; if (pdata->sid < 0) prtd->phys = substream->dma_buffer.addr; else prtd->phys = substream->dma_buffer.addr | (pdata->sid << 32); if (q6apm_is_graph_in_push_pull_mode(prtd->graph)) { + size_t buf_sz = pdata->has_reserved_mem ? + pdata->reserved_buf_size : BUFFER_BYTES_MAX; void *pos_buffer; - prtd->pos_phys = prtd->phys + BUFFER_BYTES_MAX; - pos_buffer = (void *)(substream->dma_buffer.area + BUFFER_BYTES_MAX); + prtd->pos_phys = prtd->phys + buf_sz; + pos_buffer = (void *)(substream->dma_buffer.area + buf_sz); prtd->pos_buffer = (struct sh_mem_pull_push_mode_position_buffer *)(pos_buffer); } @@ -529,6 +659,9 @@ static int q6apm_dai_hw_params(struct snd_soc_component *component, return 0; } +static void q6apm_dai_memory_unmap(struct snd_soc_component *component, + struct snd_pcm_substream *substream); + static int q6apm_dai_memory_map(struct snd_soc_component *component, struct snd_pcm_substream *substream, int graph_id, bool is_push_pull) @@ -549,15 +682,21 @@ static int q6apm_dai_memory_map(struct snd_soc_component *component, else phys = substream->dma_buffer.addr | (pdata->sid << 32); - ret = q6apm_map_memory_fixed_region(dev, graph_id, phys, BUFFER_BYTES_MAX); + ret = q6apm_map_memory_fixed_region(dev, graph_id, phys, + pdata->has_reserved_mem ? + pdata->reserved_buf_size : + BUFFER_BYTES_MAX); if (ret < 0) dev_err(dev, "Audio Start: Buffer Allocation failed rc = %d\n", ret); if (is_push_pull) { + size_t buf_sz = pdata->has_reserved_mem ? + pdata->reserved_buf_size : BUFFER_BYTES_MAX; + if (pdata->sid < 0) - phys = substream->dma_buffer.addr + BUFFER_BYTES_MAX; + phys = substream->dma_buffer.addr + buf_sz; else - phys = (substream->dma_buffer.addr + BUFFER_BYTES_MAX) | (pdata->sid << 32); + phys = (substream->dma_buffer.addr + buf_sz) | (pdata->sid << 32); ret = q6apm_map_pos_buffer(dev, graph_id, phys, POS_BUFFER_BYTES); if (ret < 0) @@ -572,6 +711,7 @@ static int q6apm_dai_memory_map(struct snd_soc_component *component, static int q6apm_dai_pcm_new(struct snd_soc_component *component, struct snd_soc_pcm_runtime *rtd) { struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(rtd, 0); + struct q6apm_dai_data *pdata = snd_soc_component_get_drvdata(component); struct snd_pcm *pcm = rtd->pcm; /* * Allocate one extra page as a workaround for a DSP bug where 32-bit @@ -583,15 +723,17 @@ static int q6apm_dai_pcm_new(struct snd_soc_component *component, struct snd_soc bool is_push_pull; struct snd_pcm_substream *substream = NULL; + if (!pdata) + return -EINVAL; + graph_id = cpu_dai->driver->id; /* Note: DSP backend dais are uni-directional ONLY(either playback or capture) */ if (pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream) substream = pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream; - else if (pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream) + else if (pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream) substream = pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream; - if (substream) { is_push_pull = q6apm_is_graph_in_push_pull_mode_from_id(component->dev, graph_id, @@ -599,13 +741,46 @@ static int q6apm_dai_pcm_new(struct snd_soc_component *component, struct snd_soc if (is_push_pull) size += POS_BUFFER_BYTES; - ret = snd_pcm_set_fixed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV, component->dev, size); + if (pdata->has_reserved_mem) + ret = snd_pcm_set_managed_buffer_all(pcm, + SNDRV_DMA_TYPE_DEV, component->dev, + pdata->reserved_buf_size, + pdata->reserved_buf_size); + else + ret = snd_pcm_set_fixed_buffer_all(pcm, + SNDRV_DMA_TYPE_DEV, component->dev, + size); if (ret) return ret; ret = q6apm_dai_memory_map(component, substream, graph_id, is_push_pull); if (ret) return ret; + + if (pdata->use_scm_assign && !pdata->has_reserved_mem) { + struct q6apm_scm_region *r; + + if (pdata->num_carveouts >= Q6APM_MAX_CARVEOUTS) { + dev_err(component->dev, + "too many SCM carveouts (max %d)\n", + Q6APM_MAX_CARVEOUTS); + q6apm_dai_memory_unmap(component, substream); + return -ENOSPC; + } + + r = &pdata->carveout_regions[pdata->num_carveouts]; + r->addr = substream->dma_buffer.addr; + r->size = ALIGN(size, PAGE_SIZE); + r->src_perms = BIT_ULL(QCOM_SCM_VMID_HLOS); + ret = q6apm_dai_scm_assign(r, pdata); + if (ret) { + dev_err(component->dev, + "SCM assign DMA buffer failed: %d\n", ret); + q6apm_dai_memory_unmap(component, substream); + return ret; + } + pdata->num_carveouts++; + } } return 0; @@ -635,15 +810,29 @@ static void q6apm_dai_memory_unmap(struct snd_soc_component *component, static void q6apm_dai_pcm_free(struct snd_soc_component *component, struct snd_pcm *pcm) { + struct q6apm_dai_data *pdata = snd_soc_component_get_drvdata(component); struct snd_pcm_substream *substream; + int i; - substream = pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream; - if (substream) - q6apm_dai_memory_unmap(component, substream); + if (!pdata) + return; + + /* + * Reverse of pcm_new: unmap the buffer from the DSP first, then + * reclaim only this PCM's carveout(s) back to HLOS. Regions owned + * by other live PCMs, and the reserved-memory pool, are untouched. + */ + for (i = 0; i < SNDRV_PCM_STREAM_LAST + 1; i++) { + substream = pcm->streams[i].substream; + if (!substream) + continue; - substream = pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream; - if (substream) q6apm_dai_memory_unmap(component, substream); + + if (pdata->use_scm_assign && !pdata->has_reserved_mem) + q6apm_dai_scm_unassign_addr(pdata, + substream->dma_buffer.addr); + } } static int q6apm_dai_compr_open(struct snd_soc_component *component, @@ -1017,23 +1206,161 @@ static const struct snd_soc_component_driver q6apm_fe_dai_component = { .remove_order = SND_SOC_COMP_ORDER_EARLY, }; +static void q6apm_dai_reserved_mem_release(void *data) +{ + of_reserved_mem_device_release(data); +} + static int q6apm_dai_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct device_node *node = dev->of_node; struct q6apm_dai_data *pdata; struct of_phandle_args args; - int rc; + int rc, vmids, i; pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL); if (!pdata) return -ENOMEM; + pdata->dev = dev; + rc = of_parse_phandle_with_fixed_args(node, "iommus", 1, 0, &args); - if (rc < 0) + if (rc < 0) { pdata->sid = -1; - else + } else { pdata->sid = args.args[0] & SID_MASK_DEFAULT; + of_node_put(args.np); + } + + vmids = of_property_count_u32_elems(node, "qcom,vmids"); + if (vmids == -EINVAL) { + /* no qcom,vmids: SCM assignment not used on this target */ + pdata->use_scm_assign = false; + } else if (vmids < 0) { + return vmids; + } else if (vmids == 0 || vmids > Q6APM_MAX_VMIDS) { + dev_err(dev, "qcom,vmids: invalid count %d (need 1..%d)\n", + vmids, Q6APM_MAX_VMIDS); + return -EINVAL; + } else if (pdata->sid >= 0) { + /* + * iommus (SMMU translation) and qcom,vmids (SCM VMID + * assignment) are mutually exclusive buffer-protection + * schemes; the binding forbids both, reject them here too. + */ + dev_err(dev, "qcom,vmids and iommus are mutually exclusive\n"); + return -EINVAL; + } + + if (vmids > 0) { + rc = of_property_read_u32_array(node, "qcom,vmids", + pdata->vmids, vmids); + if (rc) + return rc; + + for (i = 0; i < vmids; i++) { + if (pdata->vmids[i] == QCOM_SCM_VMID_HLOS) { + dev_err(dev, + "qcom,vmids must not include HLOS\n"); + return -EINVAL; + } + } + pdata->num_vmids = vmids; + pdata->use_scm_assign = true; + } + + if (pdata->use_scm_assign) { + struct device_node *mem_node; + struct reserved_mem *rmem; + + if (!qcom_scm_is_available()) + return -EPROBE_DEFER; + + mem_node = of_parse_phandle(node, "memory-region", 0); + if (mem_node) { + rmem = of_reserved_mem_lookup(mem_node); + of_node_put(mem_node); + if (rmem) { + struct q6apm_scm_region *r; + + r = &pdata->carveout_regions[pdata->num_carveouts++]; + r->addr = rmem->base; + r->size = ALIGN(rmem->size, PAGE_SIZE); + r->src_perms = BIT_ULL(QCOM_SCM_VMID_HLOS); + } + } + + mem_node = of_parse_phandle(node, "memory-region", 1); + if (mem_node) { + rmem = of_reserved_mem_lookup(mem_node); + of_node_put(mem_node); + if (rmem) { + struct q6apm_scm_region *r; + size_t per_stream; + + /* + * The data-path carveout is a shared DMA pool + * from which all PCM substreams pre-allocate. + * Divide evenly so concurrent streams fit, + * subtracting per-stream position-buffer + * overhead. Reject a pool too small to give + * each stream a usable buffer (unsigned + * arithmetic would otherwise wrap). + */ + per_stream = rmem->size / Q6APM_POOL_MAX_STREAMS; + if (per_stream <= POS_BUFFER_BYTES) { + dev_err(dev, + "reserved-memory pool too small: %llu bytes\n", + (u64)rmem->size); + return -EINVAL; + } + + rc = of_reserved_mem_device_init_by_idx(dev, node, 1); + if (rc) { + dev_err(dev, + "reserved-memory pool init failed: %d\n", + rc); + return rc; + } + rc = devm_add_action_or_reset(dev, + q6apm_dai_reserved_mem_release, + dev); + if (rc) + return rc; + + /* never exceed the per-substream buffer cap */ + pdata->reserved_buf_size = + min_t(size_t, + per_stream - POS_BUFFER_BYTES, + BUFFER_BYTES_MAX); + pdata->has_reserved_mem = true; + + r = &pdata->carveout_regions[pdata->num_carveouts++]; + r->addr = rmem->base; + r->size = ALIGN(rmem->size, PAGE_SIZE); + r->src_perms = BIT_ULL(QCOM_SCM_VMID_HLOS); + } + } + + if (pdata->num_carveouts) { + for (i = 0; i < pdata->num_carveouts; i++) { + rc = q6apm_dai_scm_assign(&pdata->carveout_regions[i], + pdata); + if (rc) { + dev_err(dev, + "SCM assign carveout[%d] failed: %d\n", + i, rc); + return rc; + } + } + rc = devm_add_action_or_reset(dev, + q6apm_dai_unassign_all, + pdata); + if (rc) + return rc; + } + } dev_set_drvdata(dev, pdata); -- 2.34.1