blob: 9ef59a33d27e4a45fcad9c80e035b0d10cda64ab [file] [log] [blame]
/*
* Copyright (c) 2016, Intel Corporation
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the Intel Corporation nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* Author: Liam Girdwood <liam.r.girdwood@linux.intel.com>
* Keyon Jie <yang.jie@linux.intel.com>
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <errno.h>
#include <sof/sof.h>
#include <sof/lock.h>
#include <sof/list.h>
#include <sof/stream.h>
#include <sof/alloc.h>
#include <sof/trace.h>
#include <sof/dma.h>
#include <sof/ipc.h>
#include <sof/wait.h>
#include <sof/audio/component.h>
#include <sof/audio/pipeline.h>
#include <platform/dma.h>
#include <arch/cache.h>
#include <uapi/ipc.h>
#define trace_host(__e) trace_event(TRACE_CLASS_HOST, __e)
#define tracev_host(__e) tracev_event(TRACE_CLASS_HOST, __e)
#define trace_host_error(__e) trace_error(TRACE_CLASS_HOST, __e)
struct hc_buf {
/* host buffer info */
struct list_item elem_list;
struct list_item *current;
uint32_t current_end;
};
/**
* \brief Host component data.
*
* Host reports local position in the host buffer every params.host_period_bytes
* if the latter is != 0. report_pos is used to track progress since the last
* multiple of host_period_bytes.
*
* host_size is the host buffer size (in bytes) specified in the IPC parameters.
*/
struct host_data {
/* local DMA config */
struct dma *dma;
int chan;
struct dma_sg_config config;
completion_t complete;
struct comp_buffer *dma_buffer;
uint32_t period_bytes; /**< Size of a single period (in bytes) */
uint32_t period_count; /**< Number of periods */
uint32_t pointer_init;
/* host position reporting related */
uint32_t host_size; /**< Host buffer size (in bytes) */
uint32_t report_pos; /**< Position in current report period */
uint32_t local_pos; /**< Local position in host buffer */
/* local and host DMA buffer info */
#if !defined CONFIG_DMA_GW
struct hc_buf host;
struct hc_buf local;
/* pointers set during params to host or local above */
struct hc_buf *source;
struct hc_buf *sink;
uint32_t split_remaining;
uint32_t next_inc;
#endif
/* stream info */
struct sof_ipc_stream_posn posn; /* TODO: update this */
};
static int host_stop(struct comp_dev *dev);
static int host_copy_int(struct comp_dev *dev, bool preload_run);
#if !defined CONFIG_DMA_GW
static inline struct dma_sg_elem *next_buffer(struct hc_buf *hc)
{
struct dma_sg_elem *elem;
if (list_item_is_last(hc->current, &hc->elem_list))
elem = list_first_item(&hc->elem_list, struct dma_sg_elem, list);
else
elem = list_first_item(hc->current, struct dma_sg_elem, list);
hc->current = &elem->list;
return elem;
}
#endif
/*
* Host period copy between DSP and host DMA completion.
* This is called by DMA driver every time when DMA completes its current
* transfer between host and DSP. The host memory is not guaranteed to be
* continuous and also not guaranteed to have a period/buffer size that is a
* multiple of the DSP period size. This means we must check we do not
* overflow host period/buffer/page boundaries on each transfer and split the
* DMA transfer if we do overflow.
*/
static void host_dma_cb(void *data, uint32_t type, struct dma_sg_elem *next)
{
struct comp_dev *dev = (struct comp_dev *)data;
struct host_data *hd = comp_get_drvdata(dev);
struct dma_sg_elem *local_elem;
#if !defined CONFIG_DMA_GW
struct dma_sg_elem *source_elem;
struct dma_sg_elem *sink_elem;
uint32_t next_size;
uint32_t need_copy = 0;
uint32_t period_bytes = hd->period_bytes;
#endif
local_elem = list_first_item(&hd->config.elem_list,
struct dma_sg_elem, list);
tracev_host("irq");
if (dev->params.direction == SOF_IPC_STREAM_PLAYBACK)
/* recalc available buffer space */
comp_update_buffer_produce(hd->dma_buffer, local_elem->size);
else
/* recalc available buffer space */
comp_update_buffer_consume(hd->dma_buffer, local_elem->size);
dev->position += local_elem->size;
/* new local period, update host buffer position blks
* local_pos is queried by the ops.potision() API
*/
hd->local_pos += local_elem->size;
/* buffer overlap, hard code host buffer size at the moment ? */
if (hd->local_pos >= hd->host_size)
hd->local_pos = 0;
/* NO_IRQ mode if host_period_size == 0 */
if (dev->params.host_period_bytes != 0) {
hd->report_pos += local_elem->size;
/* send IPC message to driver if needed */
if (hd->report_pos >= dev->params.host_period_bytes) {
hd->report_pos = 0;
/* send timestamped position to host
* (updates position first, by calling ops.position())
*/
pipeline_get_timestamp(dev->pipeline, dev, &hd->posn);
ipc_stream_send_position(dev, &hd->posn);
}
}
#if !defined CONFIG_DMA_GW
/* update src and dest positions and check for overflow */
local_elem->src += local_elem->size;
local_elem->dest += local_elem->size;
if (local_elem->src == hd->source->current_end) {
/* end of elem, so use next */
source_elem = next_buffer(hd->source);
hd->source->current_end = source_elem->src + source_elem->size;
local_elem->src = source_elem->src;
}
if (local_elem->dest == hd->sink->current_end) {
/* end of elem, so use next */
sink_elem = next_buffer(hd->sink);
hd->sink->current_end = sink_elem->dest + sink_elem->size;
local_elem->dest = sink_elem->dest;
}
/* calc size of next transfer */
next_size = period_bytes;
if (local_elem->src + next_size > hd->source->current_end)
next_size = hd->source->current_end - local_elem->src;
if (local_elem->dest + next_size > hd->sink->current_end)
next_size = hd->sink->current_end - local_elem->dest;
/* are we dealing with a split transfer ? */
if (!hd->split_remaining) {
/* no, is next transfer split ? */
if (next_size != period_bytes)
hd->split_remaining = period_bytes - next_size;
} else {
/* yes, than calc transfer size */
need_copy = 1;
next_size = next_size < hd->split_remaining ?
next_size : hd->split_remaining;
hd->split_remaining -= next_size;
}
local_elem->size = next_size;
/* schedule immediate split transfer if needed */
if (need_copy) {
next->src = local_elem->src;
next->dest = local_elem->dest;
next->size = local_elem->size;
return;
} else
next->size = DMA_RELOAD_END;
/* let any waiters know we have completed */
wait_completed(&hd->complete);
#endif
}
static int create_local_elems(struct comp_dev *dev)
{
struct host_data *hd = comp_get_drvdata(dev);
struct dma_sg_elem *e;
struct list_item *elist;
struct list_item *tlist;
int i;
/* TODO: simplify elem storage by using an array */
for (i = 0; i < hd->period_count; i++) {
/* allocate new host DMA elem and add it to our list */
e = rzalloc(RZONE_RUNTIME, SOF_MEM_CAPS_RAM, sizeof(*e));
if (e == NULL)
goto unwind;
if (dev->params.direction == SOF_IPC_STREAM_PLAYBACK)
e->dest = (uintptr_t)(hd->dma_buffer->addr) +
i * hd->period_bytes;
else
e->src = (uintptr_t)(hd->dma_buffer->addr) +
i * hd->period_bytes;
e->size = hd->period_bytes;
#if defined CONFIG_DMA_GW
list_item_append(&e->list, &hd->config.elem_list);
#else
list_item_append(&e->list, &hd->local.elem_list);
#endif
}
return 0;
unwind:
#if defined CONFIG_DMA_GW
list_for_item_safe(elist, tlist, &hd->config.elem_list) {
#else
list_for_item_safe(elist, tlist, &hd->local.elem_list) {
#endif
e = container_of(elist, struct dma_sg_elem, list);
list_item_del(&e->list);
rfree(e);
}
trace_host_error("el0");
return -ENOMEM;
}
/**
* \brief Command handler.
* \param[in,out] dev Device
* \param[in] cmd Command
* \return 0 if successful, error code otherwise.
*
* Used to pass standard and bespoke commands (with data) to component.
* This function is common for all dma types, with one exception:
* dw-dma is run on demand, so no start()/stop() is issued.
*/
static int host_trigger(struct comp_dev *dev, int cmd)
{
struct host_data *hd = comp_get_drvdata(dev);
int ret = 0;
trace_host("trg");
ret = comp_set_state(dev, cmd);
if (ret < 0)
goto out;
switch (cmd) {
case COMP_TRIGGER_STOP:
ret = host_stop(dev);
/* fall through */
case COMP_TRIGGER_XRUN:
/* TODO: add attribute to dma interface and do run-time if() here */
#if defined CONFIG_DMA_GW
ret = dma_stop(hd->dma, hd->chan);
#endif
break;
case COMP_TRIGGER_START:
#if defined CONFIG_DMA_GW
ret = dma_start(hd->dma, hd->chan);
if (ret < 0) {
trace_host_error("TsF");
trace_error_value(ret);
goto out;
}
#endif
/* preload first playback period for preloader task */
if (dev->params.direction == SOF_IPC_STREAM_PLAYBACK) {
if (!hd->pointer_init) {
ret = host_copy_int(dev, true);
if (ret == dev->frames)
ret = 0;
// wait for completion ?
hd->pointer_init = 1;
}
}
break;
default:
break;
}
out:
return ret;
}
static struct comp_dev *host_new(struct sof_ipc_comp *comp)
{
struct comp_dev *dev;
struct host_data *hd;
struct sof_ipc_comp_host *host;
struct sof_ipc_comp_host *ipc_host = (struct sof_ipc_comp_host *)comp;
uint32_t dir, caps, dma_dev;
#if !defined CONFIG_DMA_GW
struct dma_sg_elem *elem = NULL;
#endif
trace_host("new");
dev = rzalloc(RZONE_RUNTIME, SOF_MEM_CAPS_RAM,
COMP_SIZE(struct sof_ipc_comp_host));
if (dev == NULL)
return NULL;
host = (struct sof_ipc_comp_host *)&dev->comp;
memcpy(host, ipc_host, sizeof(struct sof_ipc_comp_host));
hd = rzalloc(RZONE_RUNTIME, SOF_MEM_CAPS_RAM, sizeof(*hd));
if (hd == NULL) {
rfree(dev);
return NULL;
}
comp_set_drvdata(dev, hd);
/* request HDA DMA with shared access privilege */
if (ipc_host->direction == SOF_IPC_STREAM_PLAYBACK)
dir = DMA_DIR_HMEM_TO_LMEM;
else
dir = DMA_DIR_LMEM_TO_HMEM;
caps = 0;
dma_dev = DMA_DEV_HOST;
hd->dma = dma_get(dir, caps, dma_dev, DMA_ACCESS_SHARED);
if (hd->dma == NULL) {
trace_host_error("eDM");
goto error;
}
/* init buffer elems */
list_init(&hd->config.elem_list);
#if !defined CONFIG_DMA_GW
list_init(&hd->host.elem_list);
list_init(&hd->local.elem_list);
elem = rzalloc(RZONE_RUNTIME, SOF_MEM_CAPS_RAM, sizeof(*elem));
if (!elem)
goto error;
list_item_prepend(&elem->list, &hd->config.elem_list);
#endif
/* init posn data. TODO: other fields */
hd->posn.comp_id = comp->id;
hd->pointer_init = 0;
dev->state = COMP_STATE_READY;
dev->is_dma_connected = 1;
return dev;
error:
#if !defined CONFIG_DMA_GW
rfree(elem);
#endif
rfree(hd);
rfree(dev);
return NULL;
}
static void host_free(struct comp_dev *dev)
{
struct host_data *hd = comp_get_drvdata(dev);
struct dma_sg_elem *elem;
trace_host("fre");
elem = list_first_item(&hd->config.elem_list,
struct dma_sg_elem, list);
#if !defined CONFIG_DMA_GW
dma_channel_put(hd->dma, hd->chan);
#endif
rfree(elem);
rfree(hd);
rfree(dev);
}
#if !defined CONFIG_DMA_GW
static int host_elements_reset(struct comp_dev *dev)
{
struct host_data *hd = comp_get_drvdata(dev);
struct dma_sg_elem *source_elem;
struct dma_sg_elem *sink_elem;
struct dma_sg_elem *local_elem;
/* setup elem to point to first source elem */
source_elem = list_first_item(&hd->source->elem_list,
struct dma_sg_elem, list);
hd->source->current = &source_elem->list;
hd->source->current_end = source_elem->src + source_elem->size;
/* setup elem to point to first sink elem */
sink_elem = list_first_item(&hd->sink->elem_list,
struct dma_sg_elem, list);
hd->sink->current = &sink_elem->list;
hd->sink->current_end = sink_elem->dest + sink_elem->size;
/* local element */
local_elem = list_first_item(&hd->config.elem_list,
struct dma_sg_elem, list);
local_elem->dest = sink_elem->dest;
local_elem->size = hd->period_bytes;
local_elem->src = source_elem->src;
hd->next_inc = hd->period_bytes;
return 0;
}
#endif
/* configure the DMA params and descriptors for host buffer IO */
static int host_params(struct comp_dev *dev)
{
struct host_data *hd = comp_get_drvdata(dev);
struct sof_ipc_comp_config *cconfig = COMP_GET_CONFIG(dev);
struct dma_sg_config *config = &hd->config;
uint32_t buffer_size;
int err;
trace_host("par");
/* host params always installed by pipeline IPC */
hd->host_size = dev->params.buffer.size;
/* determine source and sink buffer elems */
if (dev->params.direction == SOF_IPC_STREAM_PLAYBACK) {
#if !defined CONFIG_DMA_GW
hd->source = &hd->host;
hd->sink = &hd->local;
#endif
hd->dma_buffer = list_first_item(&dev->bsink_list,
struct comp_buffer, source_list);
config->direction = DMA_DIR_HMEM_TO_LMEM;
hd->period_count = cconfig->periods_sink;
} else {
#if !defined CONFIG_DMA_GW
hd->source = &hd->local;
hd->sink = &hd->host;
#endif
hd->dma_buffer = list_first_item(&dev->bsource_list,
struct comp_buffer, sink_list);
config->direction = DMA_DIR_LMEM_TO_HMEM;
hd->period_count = cconfig->periods_source;
}
/* validate period count */
if (hd->period_count == 0) {
trace_host_error("eS0");
return -EINVAL;
}
/* calculate period size based on config */
hd->period_bytes = dev->frames * comp_frame_bytes(dev);
if (hd->period_bytes == 0) {
trace_host_error("eS1");
return -EINVAL;
}
/* resize the buffer if space is available to align with period size */
buffer_size = hd->period_count * hd->period_bytes;
err = buffer_set_size(hd->dma_buffer, buffer_size);
if (err < 0) {
trace_host_error("eSz");
trace_error_value(buffer_size);
return err;
}
/* component buffer size must be divisor of host buffer size */
if (hd->host_size % hd->period_bytes) {
trace_comp_error("eHB");
trace_error_value(hd->host_size);
trace_error_value(hd->period_bytes);
return -EINVAL;
}
/* create SG DMA elems for local DMA buffer */
err = create_local_elems(dev);
if (err < 0)
return err;
/* set up DMA configuration - copy in sample bytes. */
config->src_width = comp_sample_bytes(dev);
config->dest_width = comp_sample_bytes(dev);
config->cyclic = 0;
#if !defined CONFIG_DMA_GW
host_elements_reset(dev);
#endif
dev->params.stream_tag -= 1;
/* get DMA channel from DMAC
* note: stream_tag is ignored by dw-dma
*/
hd->chan = dma_channel_get(hd->dma, dev->params.stream_tag);
if (hd->chan < 0) {
trace_host_error("eDC");
return -ENODEV;
}
#if defined CONFIG_DMA_GW
err = dma_set_config(hd->dma, hd->chan, &hd->config);
if (err < 0) {
trace_host_error("eDc");
dma_channel_put(hd->dma, hd->chan);
return err;
}
#endif
/* set up callback */
dma_set_cb(hd->dma, hd->chan, DMA_IRQ_TYPE_LLIST,
host_dma_cb,
dev);
return 0;
}
static int host_prepare(struct comp_dev *dev)
{
struct host_data *hd = comp_get_drvdata(dev);
int ret;
trace_host("pre");
ret = comp_set_state(dev, COMP_TRIGGER_PREPARE);
if (ret < 0)
return ret;
hd->local_pos = 0;
hd->report_pos = 0;
#if !defined CONFIG_DMA_GW
hd->split_remaining = 0;
#endif
hd->pointer_init = 0;
dev->position = 0;
return 0;
}
static int host_pointer_reset(struct comp_dev *dev)
{
struct host_data *hd = comp_get_drvdata(dev);
/* reset buffer pointers */
hd->local_pos = 0;
hd->report_pos = 0;
dev->position = 0;
return 0;
}
static int host_stop(struct comp_dev *dev)
{
return 0;
}
static int host_position(struct comp_dev *dev,
struct sof_ipc_stream_posn *posn)
{
struct host_data *hd = comp_get_drvdata(dev);
/* TODO: improve accuracy by adding current DMA position */
posn->host_posn = hd->local_pos;
return 0;
}
#if !defined CONFIG_DMA_GW
static int host_buffer(struct comp_dev *dev, struct dma_sg_elem *elem,
uint32_t host_size)
{
struct host_data *hd = comp_get_drvdata(dev);
struct dma_sg_elem *e;
/* allocate new host DMA elem and add it to our list */
e = rzalloc(RZONE_RUNTIME, SOF_MEM_CAPS_RAM, sizeof(*e));
if (e == NULL)
return -ENOMEM;
*e = *elem;
list_item_append(&e->list, &hd->host.elem_list);
return 0;
}
#endif
static int host_reset(struct comp_dev *dev)
{
struct host_data *hd = comp_get_drvdata(dev);
struct dma_sg_elem *e;
struct list_item *elist;
struct list_item *tlist;
trace_host("res");
#if !defined CONFIG_DMA_GW
/* free all host DMA elements */
list_for_item_safe(elist, tlist, &hd->host.elem_list) {
e = container_of(elist, struct dma_sg_elem, list);
list_item_del(&e->list);
rfree(e);
}
/* free all local DMA elements */
list_for_item_safe(elist, tlist, &hd->local.elem_list) {
e = container_of(elist, struct dma_sg_elem, list);
list_item_del(&e->list);
rfree(e);
}
#endif
#if defined CONFIG_DMA_GW
dma_stop(hd->dma, hd->chan);
dma_channel_put(hd->dma, hd->chan);
e = list_first_item(&hd->config.elem_list,
struct dma_sg_elem, list);
/*
* here free dma_sg_elem those allocated in create_local_elems(),
* we should keep header and the first local elem after reset (but only
* for dw-dma since hda-dma allocates the full list again)
*/
list_for_item_safe(elist, tlist, &e->list) {
e = container_of(elist, struct dma_sg_elem, list);
#if !defined CONFIG_DMA_GW
/* should not free the header, finished */
if (elist == &hd->config.elem_list)
break;
#endif
list_item_del(&e->list);
rfree(e);
}
#endif
host_pointer_reset(dev);
hd->pointer_init = 0;
#if !defined CONFIG_DMA_GW
hd->source = NULL;
hd->sink = NULL;
#endif
dev->state = COMP_STATE_READY;
return 0;
}
/* copy and process stream data from source to sink buffers */
static int host_copy(struct comp_dev *dev)
{
return host_copy_int(dev, false);
}
static int host_copy_int(struct comp_dev *dev, bool preload_run)
{
struct host_data *hd = comp_get_drvdata(dev);
struct dma_sg_elem *local_elem;
int ret;
tracev_host("cpy");
if (dev->state != COMP_STATE_ACTIVE)
return 0;
local_elem = list_first_item(&hd->config.elem_list,
struct dma_sg_elem, list);
/* enough free or avail to copy ? */
if (dev->params.direction == SOF_IPC_STREAM_PLAYBACK) {
if (hd->dma_buffer->free < local_elem->size) {
/* buffer is enough avail, just return. */
trace_host("Bea");
return 0;
}
} else {
if (hd->dma_buffer->avail < local_elem->size) {
/* buffer is enough empty, just return. */
trace_host("Bee");
return 0;
}
}
/* TODO: this could be run-time if() based on the same attribute
* as in the host_trigger().
*/
#if defined CONFIG_DMA_GW
/* tell gateway to copy another period */
ret = dma_copy(hd->dma, hd->chan, hd->period_bytes,
preload_run ? DMA_COPY_PRELOAD : 0);
if (ret < 0)
goto out;
/* note: update() moved to callback */
#else
/* do DMA transfer */
ret = dma_set_config(hd->dma, hd->chan, &hd->config);
if (ret < 0)
goto out;
ret = dma_start(hd->dma, hd->chan);
if (ret < 0)
goto out;
#endif
return dev->frames;
out:
trace_host_error("CpF");
trace_error_value(ret);
return ret;
}
struct comp_driver comp_host = {
.type = SOF_COMP_HOST,
.ops = {
.new = host_new,
.free = host_free,
.params = host_params,
.reset = host_reset,
.trigger = host_trigger,
.copy = host_copy,
.prepare = host_prepare,
#if !defined CONFIG_DMA_GW
.host_buffer = host_buffer,
#endif
.position = host_position,
},
};
void sys_comp_host_init(void)
{
comp_register(&comp_host);
}