blob: d9fd653fd1ffc31600fd0221efc663c407f84611 [file] [log] [blame]
/*
* Copyright (c) 2014-2015,2017, The Linux Foundation. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 and
* only version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*/
/*
* Qualcomm technologies inc, DMA API for BAM (Bus Access Manager).
* This DMA driver uses sps-BAM API to access the HW, thus it is effectively a
* DMA engine wrapper of the sps-BAM API.
*
* Client channel configuration example:
* struct dma_slave_config config {
* .direction = DMA_MEM_TO_DEV;
* };
*
* chan = dma_request_slave_channel(client_dev, "rx");
* dmaengine_slave_config(chan, &config);
*/
#include <linux/kernel.h>
#include <linux/io.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/dma-mapping.h>
#include <linux/scatterlist.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_dma.h>
#include <linux/list.h>
#include <linux/msm-sps.h>
#include "dmaengine.h"
#define QBAM_OF_SLAVE_N_ARGS (4)
#define QBAM_OF_MANAGE_LOCAL "qcom,managed-locally"
#define QBAM_OF_SUM_THRESHOLD "qcom,summing-threshold"
#define QBAM_MAX_DESCRIPTORS (0x100)
#define QBAM_MAX_CHANNELS (32)
/*
* qbam_async_tx_descriptor - dma descriptor plus a list of xfer_bufs
*
* @sgl scatterlist of transfer buffers
* @sg_len size of that list
* @flags dma xfer flags
*/
struct qbam_async_tx_descriptor {
struct dma_async_tx_descriptor dma_desc;
struct scatterlist *sgl;
unsigned int sg_len;
unsigned long flags;
};
#define DMA_TO_QBAM_ASYNC_DESC(dma_async_desc) \
container_of(dma_async_desc, struct qbam_async_tx_descriptor, dma_desc)
struct qbam_channel;
/*
* qbam_device - top level device of current driver
* @handle bam sps handle.
* @regs bam register space virtual base address.
* @mem_resource bam register space resource.
* @deregister_required if bam is registered by this driver it need to be
* unregistered by this driver.
* @manage is bame managed locally or remotely,
* @summing_threshold event threshold.
* @irq bam interrupt line.
* @channels has the same channels as qbam_dev->dma_dev.channels but
* supports fast access by pipe index.
*/
struct qbam_device {
struct dma_device dma_dev;
void __iomem *regs;
struct resource *mem_resource;
ulong handle;
bool deregister_required;
u32 summing_threshold;
u32 manage;
int irq;
struct qbam_channel *channels[QBAM_MAX_CHANNELS];
};
/* qbam_pipe: aggregate of bam pipe related entries of qbam_channel */
struct qbam_pipe {
u32 index;
struct sps_pipe *handle;
struct sps_connect cfg;
u32 num_descriptors;
u32 sps_connect_flags;
u32 sps_register_event_flags;
};
/*
* qbam_channel - dma channel plus bam pipe info and current pending transfers
*
* @direction is a producer or consumer (MEM => DEV or DEV => MEM)
* @pending_desc next set of transfer to process
* @error last error that took place on the current pending_desc
*/
struct qbam_channel {
struct qbam_pipe bam_pipe;
struct dma_chan chan;
enum dma_transfer_direction direction;
struct qbam_async_tx_descriptor pending_desc;
struct qbam_device *qbam_dev;
struct mutex lock;
int error;
};
#define DMA_TO_QBAM_CHAN(dma_chan) \
container_of(dma_chan, struct qbam_channel, chan)
#define qbam_err(qbam_dev, fmt ...) dev_err(qbam_dev->dma_dev.dev, fmt)
/* qbam_disconnect_chan - disconnect a channel */
static int qbam_disconnect_chan(struct qbam_channel *qbam_chan)
{
struct qbam_device *qbam_dev = qbam_chan->qbam_dev;
struct sps_pipe *pipe_handle = qbam_chan->bam_pipe.handle;
struct sps_connect pipe_config_no_irq = {.options = SPS_O_POLL};
int ret;
/*
* SW workaround:
* When disconnecting BAM pipe a spurious interrupt sometimes appears.
* To avoid that, we change the pipe setting from interrupt (default)
* to polling (SPS_O_POLL) before diconnecting the pipe.
*/
ret = sps_set_config(pipe_handle, &pipe_config_no_irq);
if (ret)
qbam_err(qbam_dev,
"error:%d sps_set_config(pipe:%d) before disconnect\n",
ret, qbam_chan->bam_pipe.index);
ret = sps_disconnect(pipe_handle);
if (ret)
qbam_err(qbam_dev, "error:%d sps_disconnect(pipe:%d)\n",
ret, qbam_chan->bam_pipe.index);
return ret;
}
/* qbam_free_chan - disconnect channel and free its resources */
static void qbam_free_chan(struct dma_chan *chan)
{
struct qbam_channel *qbam_chan = DMA_TO_QBAM_CHAN(chan);
struct qbam_device *qbam_dev = qbam_chan->qbam_dev;
mutex_lock(&qbam_chan->lock);
if (qbam_disconnect_chan(qbam_chan))
qbam_err(qbam_dev,
"error free_chan() failed to disconnect(pipe:%d)\n",
qbam_chan->bam_pipe.index);
qbam_chan->pending_desc.sgl = NULL;
qbam_chan->pending_desc.sg_len = 0;
mutex_unlock(&qbam_chan->lock);
}
static struct dma_chan *qbam_dma_xlate(struct of_phandle_args *dma_spec,
struct of_dma *of)
{
struct qbam_device *qbam_dev = of->of_dma_data;
struct qbam_channel *qbam_chan;
u32 channel_index;
u32 num_descriptors;
if (dma_spec->args_count != QBAM_OF_SLAVE_N_ARGS) {
qbam_err(qbam_dev,
"invalid number of dma arguments, expect:%d got:%d\n",
QBAM_OF_SLAVE_N_ARGS, dma_spec->args_count);
return NULL;
};
channel_index = dma_spec->args[0];
if (channel_index >= QBAM_MAX_CHANNELS) {
qbam_err(qbam_dev,
"error: channel_index:%d out of bounds",
channel_index);
return NULL;
}
qbam_chan = qbam_dev->channels[channel_index];
/* return qbam_chan if exists, or create one */
if (qbam_chan) {
qbam_chan->chan.client_count = 1;
return &qbam_chan->chan;
}
num_descriptors = dma_spec->args[1];
if (!num_descriptors || (num_descriptors > QBAM_MAX_DESCRIPTORS)) {
qbam_err(qbam_dev,
"invalid number of descriptors, range[1..%d] got:%d\n",
QBAM_MAX_DESCRIPTORS, num_descriptors);
return NULL;
}
/* allocate a channel */
qbam_chan = kzalloc(sizeof(*qbam_chan), GFP_KERNEL);
if (!qbam_chan)
return NULL;
/* allocate BAM resources for that channel */
qbam_chan->bam_pipe.handle = sps_alloc_endpoint();
if (!qbam_chan->bam_pipe.handle) {
qbam_err(qbam_dev, "error: sps_alloc_endpoint() return NULL\n");
kfree(qbam_chan);
return NULL;
}
/* init dma_chan */
qbam_chan->chan.device = &qbam_dev->dma_dev;
dma_cookie_init(&qbam_chan->chan);
qbam_chan->chan.client_count = 1;
/* init qbam_chan */
qbam_chan->bam_pipe.index = channel_index;
qbam_chan->bam_pipe.num_descriptors = num_descriptors;
qbam_chan->bam_pipe.sps_connect_flags = dma_spec->args[2];
qbam_chan->bam_pipe.sps_register_event_flags = dma_spec->args[3];
qbam_chan->qbam_dev = qbam_dev;
mutex_init(&qbam_chan->lock);
/* add to dma_device list of channels */
list_add(&qbam_chan->chan.device_node, &qbam_dev->dma_dev.channels);
qbam_dev->channels[channel_index] = qbam_chan;
return &qbam_chan->chan;
}
static enum dma_status qbam_tx_status(struct dma_chan *chan,
dma_cookie_t cookie, struct dma_tx_state *state)
{
struct qbam_channel *qbam_chan = DMA_TO_QBAM_CHAN(chan);
struct qbam_async_tx_descriptor *qbam_desc = &qbam_chan->pending_desc;
enum dma_status ret;
mutex_lock(&qbam_chan->lock);
if (qbam_chan->error) {
mutex_unlock(&qbam_chan->lock);
return DMA_ERROR;
}
ret = dma_cookie_status(chan, cookie, state);
if (ret == DMA_IN_PROGRESS) {
struct scatterlist *sg;
int i;
u32 transfer_size = 0;
for_each_sg(qbam_desc->sgl, sg, qbam_desc->sg_len, i)
transfer_size += sg_dma_len(sg);
dma_set_residue(state, transfer_size);
}
mutex_unlock(&qbam_chan->lock);
return ret;
}
/*
* qbam_init_bam_handle - find or create bam handle.
*
* BAM device needs to be registered for each BLSP once and only once. if it
* was registered, then we find the handle to the registered bam and return
* it, otherwise we register it here.
* The module which registered BAM is responsible for deregistering it.
*/
static int qbam_init_bam_handle(struct qbam_device *qbam_dev)
{
int ret = 0;
struct sps_bam_props bam_props = {0};
/*
* Check if BAM is already registred with SPS on the current
* BLSP. If it isn't then go ahead and register it.
*/
ret = sps_phy2h(qbam_dev->mem_resource->start, &qbam_dev->handle);
if (qbam_dev->handle)
return 0;
qbam_dev->regs = devm_ioremap_resource(qbam_dev->dma_dev.dev,
qbam_dev->mem_resource);
if (IS_ERR(qbam_dev->regs)) {
qbam_err(qbam_dev, "error:%ld ioremap(phy:0x%lx len:0x%lx)\n",
PTR_ERR(qbam_dev->regs),
(ulong) qbam_dev->mem_resource->start,
(ulong) resource_size(qbam_dev->mem_resource));
return PTR_ERR(qbam_dev->regs);
};
bam_props.phys_addr = qbam_dev->mem_resource->start;
bam_props.virt_addr = qbam_dev->regs;
bam_props.summing_threshold = qbam_dev->summing_threshold;
bam_props.manage = qbam_dev->manage;
bam_props.irq = qbam_dev->irq;
ret = sps_register_bam_device(&bam_props, &qbam_dev->handle);
if (ret)
qbam_err(qbam_dev, "error:%d sps_register_bam_device\n"
"(phy:0x%lx virt:0x%lx irq:%d)\n",
ret, (ulong) bam_props.phys_addr,
(ulong) bam_props.virt_addr, qbam_dev->irq);
else
qbam_dev->deregister_required = true;
return ret;
}
static int qbam_alloc_chan(struct dma_chan *chan)
{
return 0;
}
static void qbam_eot_callback(struct sps_event_notify *notify)
{
struct qbam_async_tx_descriptor *qbam_desc = notify->data.transfer.user;
struct dma_async_tx_descriptor *dma_desc = &qbam_desc->dma_desc;
dma_async_tx_callback callback = dma_desc->callback;
void *param = dma_desc->callback_param;
if (callback)
callback(param);
}
static void qbam_error_callback(struct sps_event_notify *notify)
{
struct qbam_channel *qbam_chan = notify->user;
qbam_err(qbam_chan->qbam_dev, "error: qbam_error_callback(pipe:%d\n)",
qbam_chan->bam_pipe.index);
}
static int qbam_connect_chan(struct qbam_channel *qbam_chan)
{
int ret = 0;
struct qbam_device *qbam_dev = qbam_chan->qbam_dev;
struct sps_register_event bam_eot_event = {
.mode = SPS_TRIGGER_CALLBACK,
.options = qbam_chan->bam_pipe.sps_register_event_flags,
.callback = qbam_eot_callback,
};
struct sps_register_event bam_error_event = {
.mode = SPS_TRIGGER_CALLBACK,
.options = SPS_O_ERROR,
.callback = qbam_error_callback,
.user = qbam_chan,
};
ret = sps_connect(qbam_chan->bam_pipe.handle, &qbam_chan->bam_pipe.cfg);
if (ret) {
qbam_err(qbam_dev, "error:%d sps_connect(pipe:%d)\n", ret,
qbam_chan->bam_pipe.index);
return ret;
}
ret = sps_register_event(qbam_chan->bam_pipe.handle, &bam_eot_event);
if (ret) {
qbam_err(qbam_dev, "error:%d sps_register_event(eot@pipe:%d)\n",
ret, qbam_chan->bam_pipe.index);
goto need_disconnect;
}
ret = sps_register_event(qbam_chan->bam_pipe.handle, &bam_error_event);
if (ret) {
qbam_err(qbam_dev, "error:%d sps_register_event(err@pipe:%d)\n",
ret, qbam_chan->bam_pipe.index);
goto need_disconnect;
}
return 0;
need_disconnect:
ret = sps_disconnect(qbam_chan->bam_pipe.handle);
if (ret)
qbam_err(qbam_dev, "error:%d sps_disconnect(pipe:%d)\n", ret,
qbam_chan->bam_pipe.index);
return ret;
}
/*
* qbam_slave_cfg - configure and connect a BAM pipe
*
* @cfg only cares about cfg->direction
*/
static int qbam_slave_cfg(struct dma_chan *chan,
struct dma_slave_config *cfg)
{
int ret = 0;
struct qbam_channel *qbam_chan = DMA_TO_QBAM_CHAN(chan);
struct qbam_device *qbam_dev = qbam_chan->qbam_dev;
struct sps_connect *pipe_cfg = &qbam_chan->bam_pipe.cfg;
if (!qbam_dev->handle) {
ret = qbam_init_bam_handle(qbam_dev);
if (ret)
return ret;
}
if (qbam_chan->bam_pipe.cfg.desc.base)
goto cfg_done;
ret = sps_get_config(qbam_chan->bam_pipe.handle,
&qbam_chan->bam_pipe.cfg);
if (ret) {
qbam_err(qbam_dev, "error:%d sps_get_config(0x%p)\n",
ret, qbam_chan->bam_pipe.handle);
return ret;
}
qbam_chan->direction = cfg->direction;
if (cfg->direction == DMA_MEM_TO_DEV) {
pipe_cfg->source = SPS_DEV_HANDLE_MEM;
pipe_cfg->destination = qbam_dev->handle;
pipe_cfg->mode = SPS_MODE_DEST;
pipe_cfg->src_pipe_index = 0;
pipe_cfg->dest_pipe_index = qbam_chan->bam_pipe.index;
} else {
pipe_cfg->source = qbam_dev->handle;
pipe_cfg->destination = SPS_DEV_HANDLE_MEM;
pipe_cfg->mode = SPS_MODE_SRC;
pipe_cfg->src_pipe_index = qbam_chan->bam_pipe.index;
pipe_cfg->dest_pipe_index = 0;
}
pipe_cfg->options = qbam_chan->bam_pipe.sps_connect_flags;
pipe_cfg->desc.size = (qbam_chan->bam_pipe.num_descriptors + 1) *
sizeof(struct sps_iovec);
/* managed dma_alloc_coherent() */
pipe_cfg->desc.base = dmam_alloc_coherent(qbam_dev->dma_dev.dev,
pipe_cfg->desc.size,
&pipe_cfg->desc.phys_base,
GFP_KERNEL);
if (!pipe_cfg->desc.base) {
qbam_err(qbam_dev,
"error dma_alloc_coherent(desc-sz:%llu * n-descs:%d)\n",
(u64) sizeof(struct sps_iovec),
qbam_chan->bam_pipe.num_descriptors);
return -ENOMEM;
}
cfg_done:
ret = qbam_connect_chan(qbam_chan);
if (ret)
dmam_free_coherent(qbam_dev->dma_dev.dev, pipe_cfg->desc.size,
pipe_cfg->desc.base, pipe_cfg->desc.phys_base);
return ret;
}
static int qbam_flush_chan(struct dma_chan *chan)
{
struct qbam_channel *qbam_chan = DMA_TO_QBAM_CHAN(chan);
int ret = qbam_disconnect_chan(qbam_chan);
if (ret) {
qbam_err(qbam_chan->qbam_dev,
"error: disconnect flush(pipe:%d\n)",
qbam_chan->bam_pipe.index);
return ret;
}
ret = qbam_connect_chan(qbam_chan);
if (ret)
qbam_err(qbam_chan->qbam_dev,
"error: reconnect flush(pipe:%d\n)",
qbam_chan->bam_pipe.index);
return ret;
}
/* qbam_tx_submit - sets the descriptor as the next one to be executed */
static dma_cookie_t qbam_tx_submit(struct dma_async_tx_descriptor *dma_desc)
{
struct qbam_channel *qbam_chan = DMA_TO_QBAM_CHAN(dma_desc->chan);
dma_cookie_t ret;
mutex_lock(&qbam_chan->lock);
ret = dma_cookie_assign(dma_desc);
mutex_unlock(&qbam_chan->lock);
return ret;
}
/*
* qbam_prep_slave_sg - creates qbam_xfer_buf from a list of sg
*
* @chan: dma channel
* @sgl: scatter gather list
* @sg_len: length of sg
* @direction: DMA transfer direction
* @flags: DMA flags
* @context: transfer context (unused)
* @return the newly created descriptor or negative ERR_PTR() on error
*/
static struct dma_async_tx_descriptor *qbam_prep_slave_sg(struct dma_chan *chan,
struct scatterlist *sgl, unsigned int sg_len,
enum dma_transfer_direction direction, unsigned long flags,
void *context)
{
struct qbam_channel *qbam_chan = DMA_TO_QBAM_CHAN(chan);
struct qbam_device *qbam_dev = qbam_chan->qbam_dev;
struct qbam_async_tx_descriptor *qbam_desc = &qbam_chan->pending_desc;
if (qbam_chan->direction != direction) {
qbam_err(qbam_dev,
"invalid dma transfer direction expected:%d given:%d\n",
qbam_chan->direction, direction);
return ERR_PTR(-EINVAL);
}
qbam_desc->dma_desc.chan = &qbam_chan->chan;
qbam_desc->dma_desc.tx_submit = qbam_tx_submit;
qbam_desc->sgl = sgl;
qbam_desc->sg_len = sg_len;
qbam_desc->flags = flags;
return &qbam_desc->dma_desc;
}
/*
* qbam_issue_pending - queue pending descriptor to BAM
*
* Iterate over the transfers of the pending descriptor and push them to bam
*/
static void qbam_issue_pending(struct dma_chan *chan)
{
int i;
int ret = 0;
struct qbam_channel *qbam_chan = DMA_TO_QBAM_CHAN(chan);
struct qbam_device *qbam_dev = qbam_chan->qbam_dev;
struct qbam_async_tx_descriptor *qbam_desc = &qbam_chan->pending_desc;
struct scatterlist *sg;
mutex_lock(&qbam_chan->lock);
if (!qbam_chan->pending_desc.sgl) {
qbam_err(qbam_dev,
"error qbam_issue_pending() no pending descriptor pipe:%d\n",
qbam_chan->bam_pipe.index);
mutex_unlock(&qbam_chan->lock);
return;
}
for_each_sg(qbam_desc->sgl, sg, qbam_desc->sg_len, i) {
/* Add BAM flags only on the last buffer */
bool is_last_buf = (i == ((qbam_desc->sg_len) - 1));
ret = sps_transfer_one(qbam_chan->bam_pipe.handle,
sg_dma_address(sg), sg_dma_len(sg),
qbam_desc,
(is_last_buf ? qbam_desc->flags : 0));
if (ret < 0) {
qbam_chan->error = ret;
qbam_err(qbam_dev, "erorr:%d sps_transfer_one\n"
"(addr:0x%lx len:%d flags:0x%lx pipe:%d)\n",
ret, (ulong) sg_dma_address(sg), sg_dma_len(sg),
qbam_desc->flags, qbam_chan->bam_pipe.index);
break;
}
}
dma_cookie_complete(&qbam_desc->dma_desc);
qbam_chan->error = 0;
qbam_desc->sgl = NULL;
qbam_desc->sg_len = 0;
mutex_unlock(&qbam_chan->lock);
};
static int qbam_deregister_bam_dev(struct qbam_device *qbam_dev)
{
int ret;
if (!qbam_dev->handle)
return 0;
ret = sps_deregister_bam_device(qbam_dev->handle);
if (ret)
qbam_err(qbam_dev,
"error:%d sps_deregister_bam_device(hndl:0x%lx) failed",
ret, qbam_dev->handle);
return ret;
}
static void qbam_pipes_free(struct qbam_device *qbam_dev)
{
struct qbam_channel *qbam_chan_cur, *qbam_chan_next;
list_for_each_entry_safe(qbam_chan_cur, qbam_chan_next,
&qbam_dev->dma_dev.channels, chan.device_node) {
mutex_lock(&qbam_chan_cur->lock);
qbam_free_chan(&qbam_chan_cur->chan);
sps_free_endpoint(qbam_chan_cur->bam_pipe.handle);
list_del(&qbam_chan_cur->chan.device_node);
mutex_unlock(&qbam_chan_cur->lock);
kfree(qbam_chan_cur);
}
}
static int qbam_probe(struct platform_device *pdev)
{
struct qbam_device *qbam_dev;
int ret;
bool managed_locally;
struct device_node *of_node = pdev->dev.of_node;
qbam_dev = devm_kzalloc(&pdev->dev, sizeof(*qbam_dev), GFP_KERNEL);
if (!qbam_dev)
return -ENOMEM;
qbam_dev->dma_dev.dev = &pdev->dev;
platform_set_drvdata(pdev, qbam_dev);
qbam_dev->mem_resource = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!qbam_dev->mem_resource) {
qbam_err(qbam_dev, "missing 'reg' DT entry");
return -ENODEV;
}
qbam_dev->irq = platform_get_irq(pdev, 0);
if (qbam_dev->irq < 0) {
qbam_err(qbam_dev, "missing DT IRQ resource entry");
return -EINVAL;
}
ret = of_property_read_u32(of_node, QBAM_OF_SUM_THRESHOLD,
&qbam_dev->summing_threshold);
if (ret) {
qbam_err(qbam_dev, "missing '%s' DT entry",
QBAM_OF_SUM_THRESHOLD);
return ret;
}
/* read from DT and set sps_bam_props.manage */
managed_locally = of_property_read_bool(of_node, QBAM_OF_MANAGE_LOCAL);
qbam_dev->manage = managed_locally ? SPS_BAM_MGR_LOCAL :
SPS_BAM_MGR_DEVICE_REMOTE;
/* Init channels */
INIT_LIST_HEAD(&qbam_dev->dma_dev.channels);
/* Set capabilities */
dma_cap_zero(qbam_dev->dma_dev.cap_mask);
dma_cap_set(DMA_SLAVE, qbam_dev->dma_dev.cap_mask);
dma_cap_set(DMA_PRIVATE, qbam_dev->dma_dev.cap_mask);
/* Initialize dmaengine callback apis */
qbam_dev->dma_dev.device_alloc_chan_resources = qbam_alloc_chan;
qbam_dev->dma_dev.device_free_chan_resources = qbam_free_chan;
qbam_dev->dma_dev.device_prep_slave_sg = qbam_prep_slave_sg;
qbam_dev->dma_dev.device_terminate_all = qbam_flush_chan;
qbam_dev->dma_dev.device_config = qbam_slave_cfg;
qbam_dev->dma_dev.device_issue_pending = qbam_issue_pending;
qbam_dev->dma_dev.device_tx_status = qbam_tx_status;
/* Regiser to DMA framework */
dma_async_device_register(&qbam_dev->dma_dev);
/*
* Do not return error in order to not break the existing
* way of requesting channels.
*/
ret = of_dma_controller_register(of_node, qbam_dma_xlate, qbam_dev);
if (ret) {
qbam_err(qbam_dev, "error:%d of_dma_controller_register()\n",
ret);
goto err_unregister_dma;
}
return 0;
err_unregister_dma:
dma_async_device_unregister(&qbam_dev->dma_dev);
if (qbam_dev->deregister_required)
return qbam_deregister_bam_dev(qbam_dev);
return ret;
}
static int qbam_remove(struct platform_device *pdev)
{
struct qbam_device *qbam_dev = platform_get_drvdata(pdev);
dma_async_device_unregister(&qbam_dev->dma_dev);
/* free BAM pipes resources */
qbam_pipes_free(qbam_dev);
if (qbam_dev->deregister_required)
return qbam_deregister_bam_dev(qbam_dev);
return 0;
}
static const struct of_device_id qbam_of_match[] = {
{ .compatible = "qcom,sps-dma" },
{}
};
MODULE_DEVICE_TABLE(of, qbam_of_match);
static struct platform_driver qbam_driver = {
.probe = qbam_probe,
.remove = qbam_remove,
.driver = {
.name = "qcom-sps-dma",
.owner = THIS_MODULE,
.of_match_table = qbam_of_match,
},
};
module_platform_driver(qbam_driver);
MODULE_DESCRIPTION("DMA-API driver to qcom BAM");
MODULE_LICENSE("GPL v2");
MODULE_ALIAS("platform:qcom-sps-dma");