SOF is designed with the idea that the data transfer between the host memory and local memory is performed via DMA. This is not true for imx platform in which the DSP performs this operation. Because of the aforementioned design, SOF performs a cache invalidation operation on the destination memory region before data is copied further down the pipeline. For some imx platforms, the destination memory region is cacheable. As such, some of the data transferred from the host memory region is held entirely in cache, which means the RAM and the cache have different data for the same memory region. In such cases, performing cache invalidation forces the DSP to fetch data from RAM instead of the cache, which means the DSP will read stale data and propagate it further down the pipeline. Although not optimal and mostly as a workaround to this issue, perform a cache WB operation on the destination memory region to make sure that the cache and the RAM hold the same data. During the data read operation (to copy it further down the pipeline) after the aforemention cache invalidation, the DSP will no longer end up reading stale data. Signed-off-by: Laurentiu Mihalcea <laurentiu.mihalcea@nxp.com>
310 lines
7.8 KiB
C
310 lines
7.8 KiB
C
/*
|
|
* Copyright 2023 NXP
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include <zephyr/drivers/dma.h>
|
|
#include <zephyr/logging/log.h>
|
|
#include <zephyr/cache.h>
|
|
|
|
/* used for driver binding */
|
|
#define DT_DRV_COMPAT nxp_sof_host_dma
|
|
|
|
/* macros used to parse DTS properties */
|
|
#define IDENTITY_VARGS(V, ...) IDENTITY(V)
|
|
|
|
#define _SOF_HOST_DMA_CHANNEL_INDEX_ARRAY(inst)\
|
|
LISTIFY(DT_INST_PROP_OR(inst, dma_channels, 0), IDENTITY_VARGS, (,))
|
|
|
|
#define _SOF_HOST_DMA_CHANNEL_DECLARE(idx) {}
|
|
|
|
#define SOF_HOST_DMA_CHANNELS_DECLARE(inst)\
|
|
FOR_EACH(_SOF_HOST_DMA_CHANNEL_DECLARE,\
|
|
(,), _SOF_HOST_DMA_CHANNEL_INDEX_ARRAY(inst))
|
|
|
|
LOG_MODULE_REGISTER(nxp_sof_host_dma);
|
|
|
|
/* note: This driver doesn't attempt to provide
|
|
* a generic software-based DMA engine implementation.
|
|
* As its name suggests, its only usage is in SOF
|
|
* (Sound Open Firmware) for NXP plaforms which are
|
|
* able to access the host memory directly from the
|
|
* core on which the firmware is running.
|
|
*/
|
|
|
|
enum channel_state {
|
|
CHAN_STATE_INIT = 0,
|
|
CHAN_STATE_CONFIGURED,
|
|
};
|
|
|
|
struct sof_host_dma_channel {
|
|
uint32_t src;
|
|
uint32_t dest;
|
|
uint32_t size;
|
|
uint32_t direction;
|
|
enum channel_state state;
|
|
};
|
|
|
|
struct sof_host_dma_data {
|
|
/* this needs to be first */
|
|
struct dma_context ctx;
|
|
atomic_t channel_flags;
|
|
struct sof_host_dma_channel *channels;
|
|
};
|
|
|
|
static int channel_change_state(struct sof_host_dma_channel *chan,
|
|
enum channel_state next)
|
|
{
|
|
enum channel_state prev = chan->state;
|
|
|
|
/* validate transition */
|
|
switch (prev) {
|
|
case CHAN_STATE_INIT:
|
|
case CHAN_STATE_CONFIGURED:
|
|
if (next != CHAN_STATE_CONFIGURED) {
|
|
return -EPERM;
|
|
}
|
|
break;
|
|
default:
|
|
LOG_ERR("invalid channel previous state: %d", prev);
|
|
return -EINVAL;
|
|
}
|
|
|
|
chan->state = next;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int sof_host_dma_reload(const struct device *dev, uint32_t chan_id,
|
|
uint32_t src, uint32_t dst, size_t size)
|
|
{
|
|
ARG_UNUSED(src);
|
|
ARG_UNUSED(dst);
|
|
ARG_UNUSED(size);
|
|
|
|
struct sof_host_dma_data *data;
|
|
struct sof_host_dma_channel *chan;
|
|
int ret;
|
|
|
|
data = dev->data;
|
|
|
|
if (chan_id >= data->ctx.dma_channels) {
|
|
LOG_ERR("channel %d is not a valid channel ID", chan_id);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* fetch channel data */
|
|
chan = &data->channels[chan_id];
|
|
|
|
/* validate state */
|
|
if (chan->state != CHAN_STATE_CONFIGURED) {
|
|
LOG_ERR("attempting to reload unconfigured DMA channel %d", chan_id);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (chan->direction == HOST_TO_MEMORY) {
|
|
/* the host may have modified the region we're about to copy
|
|
* to local memory. In this case, the data cache holds stale
|
|
* data so invalidate it to force a read from the main memory.
|
|
*/
|
|
ret = sys_cache_data_invd_range(UINT_TO_POINTER(chan->src),
|
|
chan->size);
|
|
if (ret < 0) {
|
|
LOG_ERR("failed to invalidate data cache range");
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
memcpy(UINT_TO_POINTER(chan->dest), UINT_TO_POINTER(chan->src), chan->size);
|
|
|
|
/*
|
|
* MEMORY_TO_HOST transfer: force range to main memory so that
|
|
* the host doesn't read any stale data.
|
|
*
|
|
* HOST_TO_MEMORY transfer:
|
|
* SOF assumes that data is copied from host to local memory via
|
|
* DMA, which is not the case for imx platforms. For these
|
|
* platforms, the DSP is in charge of copying the data from host to
|
|
* local memory.
|
|
*
|
|
* Additionally, because of the aforementioned assumption,
|
|
* SOF performs a cache invalidation on the destination
|
|
* memory chunk before data is copied further down the
|
|
* pipeline.
|
|
*
|
|
* If the destination memory chunk is cacheable what seems
|
|
* to happen is that the invalidation operation forces the
|
|
* DSP to fetch the data from RAM instead of the cache.
|
|
* Since a writeback was never performed on the destination
|
|
* memory chunk, the RAM will contain stale data.
|
|
*
|
|
* With this in mind, the writeback should also be
|
|
* performed in HOST_TO_MEMORY transfers (aka playback)
|
|
* to keep the cache and RAM in sync. This way, the DSP
|
|
* will read the correct data from RAM (when forced to do
|
|
* so by the cache invalidation operation).
|
|
*
|
|
* TODO: this is NOT optimal since we perform two unneeded
|
|
* cache management operations and should be addressed in
|
|
* SOF at some point.
|
|
*/
|
|
ret = sys_cache_data_flush_range(UINT_TO_POINTER(chan->dest), chan->size);
|
|
if (ret < 0) {
|
|
LOG_ERR("failed to flush data cache range");
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int sof_host_dma_config(const struct device *dev, uint32_t chan_id,
|
|
struct dma_config *config)
|
|
{
|
|
struct sof_host_dma_data *data;
|
|
struct sof_host_dma_channel *chan;
|
|
int ret;
|
|
|
|
data = dev->data;
|
|
|
|
if (chan_id >= data->ctx.dma_channels) {
|
|
LOG_ERR("channel %d is not a valid channel ID", chan_id);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* fetch channel data */
|
|
chan = &data->channels[chan_id];
|
|
|
|
/* attempt a state transition */
|
|
ret = channel_change_state(chan, CHAN_STATE_CONFIGURED);
|
|
if (ret < 0) {
|
|
LOG_ERR("failed to change channel %d's state to CONFIGURED", chan_id);
|
|
return ret;
|
|
}
|
|
|
|
/* SG configurations are not currently supported */
|
|
if (config->block_count != 1) {
|
|
LOG_ERR("invalid number of blocks: %d", config->block_count);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!config->head_block->source_address) {
|
|
LOG_ERR("got NULL source address");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!config->head_block->dest_address) {
|
|
LOG_ERR("got NULL destination address");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!config->head_block->block_size) {
|
|
LOG_ERR("got 0 bytes to copy");
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* for now, only H2M and M2H transfers are supported */
|
|
if (config->channel_direction != HOST_TO_MEMORY &&
|
|
config->channel_direction != MEMORY_TO_HOST) {
|
|
LOG_ERR("invalid channel direction: %d",
|
|
config->channel_direction);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* latch onto the passed configuration */
|
|
chan->src = config->head_block->source_address;
|
|
chan->dest = config->head_block->dest_address;
|
|
chan->size = config->head_block->block_size;
|
|
chan->direction = config->channel_direction;
|
|
|
|
LOG_DBG("configured channel %d with SRC 0x%x DST 0x%x SIZE 0x%x",
|
|
chan_id, chan->src, chan->dest, chan->size);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int sof_host_dma_start(const struct device *dev, uint32_t chan_id)
|
|
{
|
|
/* nothing to be done here */
|
|
return 0;
|
|
}
|
|
|
|
static int sof_host_dma_stop(const struct device *dev, uint32_t chan_id)
|
|
{
|
|
/* nothing to be done here */
|
|
return 0;
|
|
}
|
|
|
|
static int sof_host_dma_suspend(const struct device *dev, uint32_t chan_id)
|
|
{
|
|
/* nothing to be done here */
|
|
return 0;
|
|
}
|
|
|
|
static int sof_host_dma_resume(const struct device *dev, uint32_t chan_id)
|
|
{
|
|
/* nothing to be done here */
|
|
return 0;
|
|
}
|
|
|
|
static int sof_host_dma_get_status(const struct device *dev,
|
|
uint32_t chan_id, struct dma_status *stat)
|
|
{
|
|
/* nothing to be done here */
|
|
return 0;
|
|
}
|
|
|
|
static int sof_host_dma_get_attribute(const struct device *dev, uint32_t type, uint32_t *val)
|
|
{
|
|
switch (type) {
|
|
case DMA_ATTR_COPY_ALIGNMENT:
|
|
case DMA_ATTR_BUFFER_SIZE_ALIGNMENT:
|
|
case DMA_ATTR_BUFFER_ADDRESS_ALIGNMENT:
|
|
*val = CONFIG_DMA_NXP_SOF_HOST_DMA_ALIGN;
|
|
break;
|
|
default:
|
|
LOG_ERR("invalid attribute type: %d", type);
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct dma_driver_api sof_host_dma_api = {
|
|
.reload = sof_host_dma_reload,
|
|
.config = sof_host_dma_config,
|
|
.start = sof_host_dma_start,
|
|
.stop = sof_host_dma_stop,
|
|
.suspend = sof_host_dma_suspend,
|
|
.resume = sof_host_dma_resume,
|
|
.get_status = sof_host_dma_get_status,
|
|
.get_attribute = sof_host_dma_get_attribute,
|
|
};
|
|
|
|
static int sof_host_dma_init(const struct device *dev)
|
|
{
|
|
struct sof_host_dma_data *data = dev->data;
|
|
|
|
data->channel_flags = ATOMIC_INIT(0);
|
|
data->ctx.atomic = &data->channel_flags;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct sof_host_dma_channel channels[] = {
|
|
SOF_HOST_DMA_CHANNELS_DECLARE(0),
|
|
};
|
|
|
|
static struct sof_host_dma_data sof_host_dma_data = {
|
|
.ctx.magic = DMA_MAGIC,
|
|
.ctx.dma_channels = ARRAY_SIZE(channels),
|
|
.channels = channels,
|
|
};
|
|
|
|
/* assumption: only 1 SOF_HOST_DMA instance */
|
|
DEVICE_DT_INST_DEFINE(0, sof_host_dma_init, NULL,
|
|
&sof_host_dma_data, NULL,
|
|
PRE_KERNEL_1, CONFIG_DMA_INIT_PRIORITY,
|
|
&sof_host_dma_api);
|