You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
428 lines
11 KiB
428 lines
11 KiB
/* |
|
* Copyright (c) 2018 Intel Corporation. |
|
* |
|
* SPDX-License-Identifier: Apache-2.0 |
|
*/ |
|
|
|
#define DT_DRV_COMPAT snps_designware_dma |
|
|
|
#include <errno.h> |
|
|
|
#include <stdio.h> |
|
#include <string.h> |
|
#include <kernel.h> |
|
#include <device.h> |
|
#include <init.h> |
|
#include <drivers/dma.h> |
|
#include <soc.h> |
|
#include "dma_dw.h" |
|
|
|
#define LOG_LEVEL CONFIG_DMA_LOG_LEVEL |
|
#include <logging/log.h> |
|
LOG_MODULE_REGISTER(dma_dw); |
|
|
|
#define BYTE (1) |
|
#define WORD (2) |
|
#define DWORD (4) |
|
|
|
/* CFG_LO */ |
|
#define DW_CFG_CLASS(x) (x << 29) |
|
/* CFG_HI */ |
|
#define DW_CFGH_SRC_PER(x) ((x & 0xf) | ((x & 0x30) << 24)) |
|
#define DW_CFGH_DST_PER(x) (((x & 0xf) << 4) | ((x & 0x30) << 26)) |
|
|
|
/* default initial setup register values */ |
|
#define DW_CFG_LOW_DEF 0x0 |
|
|
|
#define DEV_NAME(dev) ((dev)->name) |
|
#define DEV_DATA(dev) ((struct dw_dma_dev_data *const)(dev)->data) |
|
#define DEV_CFG(dev) \ |
|
((const struct dw_dma_dev_cfg *const)(dev)->config) |
|
|
|
/* number of tries to wait for reset */ |
|
#define DW_DMA_CFG_TRIES 10000 |
|
#define INT_MASK_ALL 0xFF00 |
|
|
|
static ALWAYS_INLINE void dw_write(uint32_t dma_base, uint32_t reg, uint32_t value) |
|
{ |
|
*((volatile uint32_t*)(dma_base + reg)) = value; |
|
} |
|
|
|
static ALWAYS_INLINE uint32_t dw_read(uint32_t dma_base, uint32_t reg) |
|
{ |
|
return *((volatile uint32_t*)(dma_base + reg)); |
|
} |
|
|
|
static void dw_dma_isr(const struct device *dev) |
|
{ |
|
const struct dw_dma_dev_cfg *const dev_cfg = DEV_CFG(dev); |
|
struct dw_dma_dev_data *const dev_data = DEV_DATA(dev); |
|
struct dma_chan_data *chan_data; |
|
|
|
uint32_t status_tfr = 0U; |
|
uint32_t status_block = 0U; |
|
uint32_t status_err = 0U; |
|
uint32_t status_intr; |
|
uint32_t channel; |
|
|
|
status_intr = dw_read(dev_cfg->base, DW_INTR_STATUS); |
|
if (!status_intr) { |
|
LOG_ERR("status_intr = %d", status_intr); |
|
} |
|
|
|
/* get the source of our IRQ. */ |
|
status_block = dw_read(dev_cfg->base, DW_STATUS_BLOCK); |
|
status_tfr = dw_read(dev_cfg->base, DW_STATUS_TFR); |
|
|
|
/* TODO: handle errors, just clear them atm */ |
|
status_err = dw_read(dev_cfg->base, DW_STATUS_ERR); |
|
if (status_err) { |
|
LOG_ERR("status_err = %d\n", status_err); |
|
dw_write(dev_cfg->base, DW_CLEAR_ERR, status_err); |
|
} |
|
|
|
/* clear interrupts */ |
|
dw_write(dev_cfg->base, DW_CLEAR_BLOCK, status_block); |
|
dw_write(dev_cfg->base, DW_CLEAR_TFR, status_tfr); |
|
|
|
/* Dispatch callbacks for channels depending upon the bit set */ |
|
while (status_block) { |
|
channel = find_lsb_set(status_block) - 1; |
|
status_block &= ~(1 << channel); |
|
chan_data = &dev_data->chan[channel]; |
|
|
|
if (chan_data->dma_blkcallback) { |
|
|
|
/* Ensure the linked list (chan_data->lli) is |
|
* freed in the user callback function once |
|
* all the blocks are transferred. |
|
*/ |
|
chan_data->dma_blkcallback(dev, |
|
chan_data->blkuser_data, |
|
channel, 0); |
|
} |
|
} |
|
|
|
while (status_tfr) { |
|
channel = find_lsb_set(status_tfr) - 1; |
|
status_tfr &= ~(1 << channel); |
|
chan_data = &dev_data->chan[channel]; |
|
if (chan_data->dma_tfrcallback) { |
|
chan_data->dma_tfrcallback(dev, |
|
chan_data->tfruser_data, |
|
channel, 0); |
|
} |
|
} |
|
} |
|
|
|
static int dw_dma_config(const struct device *dev, uint32_t channel, |
|
struct dma_config *cfg) |
|
{ |
|
struct dw_dma_dev_data *const dev_data = DEV_DATA(dev); |
|
const struct dw_dma_dev_cfg *const dev_cfg = DEV_CFG(dev); |
|
struct dma_chan_data *chan_data; |
|
struct dma_block_config *cfg_blocks; |
|
uint32_t m_size; |
|
uint32_t tr_width; |
|
uint32_t ctrl_lo; |
|
|
|
if (channel >= DW_MAX_CHAN) { |
|
return -EINVAL; |
|
} |
|
|
|
__ASSERT_NO_MSG(cfg->source_data_size == cfg->dest_data_size); |
|
__ASSERT_NO_MSG(cfg->source_burst_length == cfg->dest_burst_length); |
|
|
|
if (cfg->source_data_size != BYTE && cfg->source_data_size != WORD && |
|
cfg->source_data_size != DWORD) { |
|
LOG_ERR("Invalid 'source_data_size' value"); |
|
return -EINVAL; |
|
} |
|
|
|
cfg_blocks = cfg->head_block; |
|
|
|
if ((cfg_blocks->next_block) || (cfg->block_count > 1)) { |
|
/* |
|
* return error since the application may have allocated |
|
* memory for the buffers that may be lost when the DMA |
|
* driver discards the buffers provided in the linked blocks |
|
*/ |
|
LOG_ERR("block_count > 1 not supported"); |
|
return -EINVAL; |
|
} |
|
|
|
chan_data = &dev_data->chan[channel]; |
|
|
|
/* default channel config */ |
|
chan_data->direction = cfg->channel_direction; |
|
|
|
/* data_size = (2 ^ tr_width) */ |
|
tr_width = find_msb_set(cfg->source_data_size) - 1; |
|
LOG_DBG("Ch%u: tr_width=%d", channel, tr_width); |
|
|
|
/* burst_size = (2 ^ msize) */ |
|
m_size = find_msb_set(cfg->source_burst_length) - 1; |
|
LOG_DBG("Ch%u: m_size=%d", channel, m_size); |
|
|
|
ctrl_lo = DW_CTLL_SRC_WIDTH(tr_width) | DW_CTLL_DST_WIDTH(tr_width); |
|
ctrl_lo |= DW_CTLL_SRC_MSIZE(m_size) | DW_CTLL_DST_MSIZE(m_size); |
|
|
|
/* enable interrupt */ |
|
ctrl_lo |= DW_CTLL_INT_EN; |
|
|
|
switch (cfg->channel_direction) { |
|
|
|
case MEMORY_TO_MEMORY: |
|
ctrl_lo |= DW_CTLL_FC_M2M; |
|
ctrl_lo |= DW_CTLL_SRC_INC | DW_CTLL_DST_INC; |
|
break; |
|
|
|
case MEMORY_TO_PERIPHERAL: |
|
ctrl_lo |= DW_CTLL_FC_M2P; |
|
ctrl_lo |= DW_CTLL_SRC_INC | DW_CTLL_DST_FIX; |
|
|
|
/* Assign a hardware handshaking interface (0-15) to the |
|
* destination of channel |
|
*/ |
|
dw_write(dev_cfg->base, DW_CFG_HIGH(channel), |
|
DW_CFGH_DST_PER(cfg->dma_slot)); |
|
break; |
|
|
|
case PERIPHERAL_TO_MEMORY: |
|
ctrl_lo |= DW_CTLL_FC_P2M; |
|
ctrl_lo |= DW_CTLL_SRC_FIX | DW_CTLL_DST_INC; |
|
|
|
/* Assign a hardware handshaking interface (0-15) to the |
|
* source of channel |
|
*/ |
|
dw_write(dev_cfg->base, DW_CFG_HIGH(channel), |
|
DW_CFGH_SRC_PER(cfg->dma_slot)); |
|
break; |
|
|
|
default: |
|
LOG_ERR("channel_direction %d is not supported", |
|
cfg->channel_direction); |
|
return -EINVAL; |
|
} |
|
|
|
/* channel needs started from scratch, so write SARn, DARn */ |
|
dw_write(dev_cfg->base, DW_SAR(channel), cfg_blocks->source_address); |
|
dw_write(dev_cfg->base, DW_DAR(channel), cfg_blocks->dest_address); |
|
|
|
/* Configure a callback appropriately depending on whether the |
|
* interrupt is requested at the end of transaction completion or |
|
* at the end of each block. |
|
*/ |
|
if (cfg->complete_callback_en) { |
|
chan_data->dma_blkcallback = cfg->dma_callback; |
|
chan_data->blkuser_data = cfg->user_data; |
|
dw_write(dev_cfg->base, DW_MASK_BLOCK, INT_UNMASK(channel)); |
|
} else { |
|
chan_data->dma_tfrcallback = cfg->dma_callback; |
|
chan_data->tfruser_data = cfg->user_data; |
|
dw_write(dev_cfg->base, DW_MASK_TFR, INT_UNMASK(channel)); |
|
} |
|
|
|
dw_write(dev_cfg->base, DW_MASK_ERR, INT_UNMASK(channel)); |
|
|
|
/* write interrupt clear registers for the channel |
|
* ClearTfr, ClearBlock, ClearSrcTran, ClearDstTran, ClearErr |
|
*/ |
|
dw_write(dev_cfg->base, DW_CLEAR_TFR, 0x1 << channel); |
|
dw_write(dev_cfg->base, DW_CLEAR_BLOCK, 0x1 << channel); |
|
dw_write(dev_cfg->base, DW_CLEAR_SRC_TRAN, 0x1 << channel); |
|
dw_write(dev_cfg->base, DW_CLEAR_DST_TRAN, 0x1 << channel); |
|
dw_write(dev_cfg->base, DW_CLEAR_ERR, 0x1 << channel); |
|
|
|
/* single transfer, must set zero */ |
|
dw_write(dev_cfg->base, DW_LLP(channel), 0); |
|
|
|
/* program CTLn */ |
|
dw_write(dev_cfg->base, DW_CTRL_LOW(channel), ctrl_lo); |
|
dw_write(dev_cfg->base, DW_CTRL_HIGH(channel), |
|
DW_CFG_CLASS(dev_data->channel_data->chan[channel].class) | |
|
cfg_blocks->block_size); |
|
|
|
/* write channel config */ |
|
dw_write(dev_cfg->base, DW_CFG_LOW(channel), DW_CFG_LOW_DEF); |
|
|
|
return 0; |
|
} |
|
|
|
static int dw_dma_reload(const struct device *dev, uint32_t channel, |
|
uint32_t src, uint32_t dst, size_t size) |
|
{ |
|
struct dw_dma_dev_data *const dev_data = DEV_DATA(dev); |
|
const struct dw_dma_dev_cfg *const dev_cfg = DEV_CFG(dev); |
|
|
|
if (channel >= DW_MAX_CHAN) { |
|
return -EINVAL; |
|
} |
|
|
|
dw_write(dev_cfg->base, DW_SAR(channel), src); |
|
dw_write(dev_cfg->base, DW_DAR(channel), dst); |
|
dw_write(dev_cfg->base, DW_CTRL_HIGH(channel), |
|
DW_CFG_CLASS(dev_data->channel_data->chan[channel].class) | |
|
size); |
|
|
|
return 0; |
|
} |
|
|
|
static int dw_dma_transfer_start(const struct device *dev, uint32_t channel) |
|
{ |
|
const struct dw_dma_dev_cfg *const dev_cfg = DEV_CFG(dev); |
|
|
|
if (channel >= DW_MAX_CHAN) { |
|
return -EINVAL; |
|
} |
|
|
|
/* enable the channel */ |
|
dw_write(dev_cfg->base, DW_DMA_CHAN_EN, CHAN_ENABLE(channel)); |
|
|
|
return 0; |
|
} |
|
|
|
static int dw_dma_transfer_stop(const struct device *dev, uint32_t channel) |
|
{ |
|
const struct dw_dma_dev_cfg *const dev_cfg = DEV_CFG(dev); |
|
|
|
if (channel >= DW_MAX_CHAN) { |
|
return -EINVAL; |
|
} |
|
|
|
/* disable the channel */ |
|
dw_write(dev_cfg->base, DW_DMA_CHAN_EN, CHAN_DISABLE(channel)); |
|
return 0; |
|
} |
|
|
|
static void dw_dma_setup(const struct device *dev) |
|
{ |
|
const struct dw_dma_dev_cfg *const dev_cfg = DEV_CFG(dev); |
|
struct dw_dma_dev_data *const dev_data = DEV_DATA(dev); |
|
struct dw_drv_plat_data *dp = dev_data->channel_data; |
|
int i; |
|
|
|
/* we cannot config DMAC if DMAC has been already enabled by host */ |
|
if (dw_read(dev_cfg->base, DW_DMA_CFG) != 0) { |
|
dw_write(dev_cfg->base, DW_DMA_CFG, 0x0); |
|
} |
|
|
|
/* now check that it's 0 */ |
|
for (i = DW_DMA_CFG_TRIES; i > 0; i--) { |
|
if (dw_read(dev_cfg->base, DW_DMA_CFG) == 0) { |
|
goto found; |
|
} |
|
} |
|
LOG_ERR("DW_DMA_CFG is non-zero\n"); |
|
return; |
|
|
|
found: |
|
for (i = 0; i < DW_MAX_CHAN; i++) { |
|
dw_read(dev_cfg->base, DW_DMA_CHAN_EN); |
|
} |
|
|
|
/* enable the DMA controller */ |
|
dw_write(dev_cfg->base, DW_DMA_CFG, 1); |
|
|
|
/* mask all interrupts for all 8 channels */ |
|
dw_write(dev_cfg->base, DW_MASK_TFR, INT_MASK_ALL); |
|
dw_write(dev_cfg->base, DW_MASK_BLOCK, INT_MASK_ALL); |
|
dw_write(dev_cfg->base, DW_MASK_SRC_TRAN, INT_MASK_ALL); |
|
dw_write(dev_cfg->base, DW_MASK_DST_TRAN, INT_MASK_ALL); |
|
dw_write(dev_cfg->base, DW_MASK_ERR, INT_MASK_ALL); |
|
|
|
/* set channel priorities */ |
|
for (i = 0; i < DW_MAX_CHAN; i++) { |
|
dw_write(dev_cfg->base, DW_CTRL_HIGH(i), |
|
DW_CFG_CLASS(dp->chan[i].class)); |
|
} |
|
} |
|
|
|
static int dw_dma_init(const struct device *dev) |
|
{ |
|
const struct dw_dma_dev_cfg *const dev_cfg = DEV_CFG(dev); |
|
|
|
/* Disable all channels and Channel interrupts */ |
|
dw_dma_setup(dev); |
|
|
|
/* Configure interrupts */ |
|
dev_cfg->irq_config(); |
|
|
|
LOG_INF("Device %s initialized", DEV_NAME(dev)); |
|
|
|
return 0; |
|
} |
|
|
|
static const struct dma_driver_api dw_dma_driver_api = { |
|
.config = dw_dma_config, |
|
.reload = dw_dma_reload, |
|
.start = dw_dma_transfer_start, |
|
.stop = dw_dma_transfer_stop, |
|
}; |
|
|
|
#define DW_DMAC_INIT(inst) \ |
|
\ |
|
static struct dw_drv_plat_data dmac##inst = { \ |
|
.chan[0] = { \ |
|
.class = 6, \ |
|
.weight = 0, \ |
|
}, \ |
|
.chan[1] = { \ |
|
.class = 6, \ |
|
.weight = 0, \ |
|
}, \ |
|
.chan[2] = { \ |
|
.class = 6, \ |
|
.weight = 0, \ |
|
}, \ |
|
.chan[3] = { \ |
|
.class = 6, \ |
|
.weight = 0, \ |
|
}, \ |
|
.chan[4] = { \ |
|
.class = 6, \ |
|
.weight = 0, \ |
|
}, \ |
|
.chan[5] = { \ |
|
.class = 6, \ |
|
.weight = 0, \ |
|
}, \ |
|
.chan[6] = { \ |
|
.class = 6, \ |
|
.weight = 0, \ |
|
}, \ |
|
.chan[7] = { \ |
|
.class = 6, \ |
|
.weight = 0, \ |
|
}, \ |
|
}; \ |
|
\ |
|
static void dw_dma##inst##_irq_config(void); \ |
|
\ |
|
static const struct dw_dma_dev_cfg dw_dma##inst##_config = { \ |
|
.base = DT_INST_REG_ADDR(inst), \ |
|
.irq_config = dw_dma##inst##_irq_config \ |
|
}; \ |
|
\ |
|
static struct dw_dma_dev_data dw_dma##inst##_data = { \ |
|
.channel_data = &dmac##inst, \ |
|
}; \ |
|
\ |
|
DEVICE_DT_INST_DEFINE(inst, \ |
|
&dw_dma_init, \ |
|
NULL, \ |
|
&dw_dma##inst##_data, \ |
|
&dw_dma##inst##_config, POST_KERNEL, \ |
|
CONFIG_DMA_INIT_PRIORITY, \ |
|
&dw_dma_driver_api); \ |
|
\ |
|
static void dw_dma##inst##_irq_config(void) \ |
|
{ \ |
|
IRQ_CONNECT(DT_INST_IRQN(inst), \ |
|
DT_INST_IRQ(inst, priority), dw_dma_isr, \ |
|
DEVICE_DT_INST_GET(inst), \ |
|
DT_INST_IRQ(inst, sense)); \ |
|
irq_enable(DT_INST_IRQN(inst)); \ |
|
} |
|
|
|
DT_INST_FOREACH_STATUS_OKAY(DW_DMAC_INIT)
|
|
|