/*
 * Copyright (c) 2023 Intel Corporation.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT snps_designware_dma_axi

#include <zephyr/device.h>
#include <zephyr/drivers/dma.h>
#include <zephyr/logging/log.h>
#include <zephyr/drivers/reset.h>
#include <zephyr/cache.h>

LOG_MODULE_REGISTER(dma_designware_axi, CONFIG_DMA_LOG_LEVEL);

#define DEV_CFG(_dev)	((const struct dma_dw_axi_dev_cfg *)(_dev)->config)
#define DEV_DATA(_dev)		((struct dma_dw_axi_dev_data *const)(_dev)->data)

/* mask for block transfer size */
#define BLOCK_TS_MASK GENMASK(21, 0)

/* blen : number of data units
 * blen will always be in power of two
 *
 * when blen is 1 then set msize to zero otherwise find most significant bit set
 * and subtract two (as IP doesn't support number  of data items 2)
 */
#define DMA_DW_AXI_GET_MSIZE(blen) ((blen == 1) ? (0U) : (find_msb_set(blen) - 2U))

/* Common_Registers_Address_Block */
#define DMA_DW_AXI_IDREG                         0x0
#define DMA_DW_AXI_COMPVERREG                    0x08
#define DMA_DW_AXI_CFGREG                        0x10
#define DMA_DW_AXI_CHENREG                       0x18
#define DMA_DW_AXI_INTSTATUSREG                  0x30
#define DMA_DW_AXI_COMMONREG_INTCLEARREG         0x38
#define DMA_DW_AXI_COMMONREG_INTSTATUS_ENABLEREG 0x40
#define DMA_DW_AXI_COMMONREG_INTSIGNAL_ENABLEREG 0x48
#define DMA_DW_AXI_COMMONREG_INTSTATUSREG        0x50
#define DMA_DW_AXI_RESETREG                      0x58
#define DMA_DW_AXI_LOWPOWER_CFGREG               0x60

/* Channel enable by setting ch_en and ch_en_we */
#define CH_EN(chan)    (BIT64(8 + chan) | BIT64(chan))
/* Channel enable by setting ch_susp and ch_susp_we */
#define CH_SUSP(chan)  (BIT64(24 + chan) | BIT64(16 + chan))
/* Channel enable by setting ch_abort and ch_abort_we */
#define CH_ABORT(chan) (BIT64(40 + chan) | BIT64(32 + chan))

/* channel susp/resume write enable pos */
#define CH_RESUME_WE(chan) (BIT64(24 + chan))
/* channel resume bit pos */
#define CH_RESUME(chan)    (BIT64(16 + chan))

#define DMA_DW_AXI_CHAN_OFFSET(chan)             (0x100 * chan)

/* source address register for a channel */
#define DMA_DW_AXI_CH_SAR(chan)                  (0x100 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* destination address register for a channel */
#define DMA_DW_AXI_CH_DAR(chan)                  (0x108 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* block transfer size register for a channel */
#define DMA_DW_AXI_CH_BLOCK_TS(chan)             (0x110 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* channel control register */
#define DMA_DW_AXI_CH_CTL(chan)                  (0x118 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* channel configuration register */
#define DMA_DW_AXI_CH_CFG(chan)                  (0x120 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* linked list pointer register */
#define DMA_DW_AXI_CH_LLP(chan)                  (0x128 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* channel status register */
#define DMA_DW_AXI_CH_STATUSREG(chan)            (0x130 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* channel software handshake source register */
#define DMA_DW_AXI_CH_SWHSSRCREG(chan)           (0x138 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* channel software handshake destination register */
#define DMA_DW_AXI_CH_SWHSDSTREG(chan)           (0x140 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* channel block transfer resume request register */
#define DMA_DW_AXI_CH_BLK_TFR_RESUMEREQREG(chan) (0x148 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* channel AXI ID rester */
#define DMA_DW_AXI_CH_AXI_IDREG(chan)            (0x150 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* channel AXI QOS register */
#define DMA_DW_AXI_CH_AXI_QOSREG(chan)           (0x158 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* channel interrupt status enable register */
#define DMA_DW_AXI_CH_INTSTATUS_ENABLEREG(chan)  (0x180 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* channel interrupt status register */
#define DMA_DW_AXI_CH_INTSTATUS(chan)            (0x188 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* channel interrupt signal enable register */
#define DMA_DW_AXI_CH_INTSIGNAL_ENABLEREG(chan)  (0x190 + DMA_DW_AXI_CHAN_OFFSET(chan))
/* channel interrupt clear register */
#define DMA_DW_AXI_CH_INTCLEARREG(chan)          (0x198 + DMA_DW_AXI_CHAN_OFFSET(chan))

/* bitfield configuration for multi-block transfer */
#define DMA_DW_AXI_CFG_SRC_MULTBLK_TYPE(x)       FIELD_PREP(GENMASK64(1, 0), x)
#define DMA_DW_AXI_CFG_DST_MULTBLK_TYPE(x)       FIELD_PREP(GENMASK64(3, 2), x)

/* bitfield configuration to assign handshaking interface to source and destination */
#define DMA_DW_AXI_CFG_SRC_PER(x)                FIELD_PREP(GENMASK64(9, 4), x)
#define DMA_DW_AXI_CFG_DST_PER(x)                FIELD_PREP(GENMASK64(16, 11), x)

/* bitfield configuration for transfer type and flow controller */
#define DMA_DW_AXI_CFG_TT_FC(x)                  FIELD_PREP(GENMASK64(34, 32), x)

#define DMA_DW_AXI_CFG_HW_HS_SRC_BIT_POS         35
#define DMA_DW_AXI_CFG_HW_HS_DST_BIT_POS         36

#define DMA_DW_AXI_CFG_PRIORITY(x)               FIELD_PREP(GENMASK64(51, 47), x)

/* descriptor valid or not */
#define DMA_DW_AXI_CTL_LLI_VALID                 BIT64(63)
/* descriptor is last or not in a link */
#define DMA_DW_AXI_CTL_LLI_LAST                  BIT64(62)
/* interrupt on completion of block transfer */
#define DMA_DW_AXI_CTL_IOC_BLK_TFR               BIT64(58)
/* source status enable bit */
#define DMA_DW_AXI_CTL_SRC_STAT_EN               BIT64(56)
/* destination status enable bit */
#define DMA_DW_AXI_CTL_DST_STAT_EN               BIT64(57)
/* source burst length enable */
#define DMA_DW_AXI_CTL_ARLEN_EN                  BIT64(38)
/* source burst length(considered when corresponding enable bit is set) */
#define DMA_DW_AXI_CTL_ARLEN(x)                  FIELD_PREP(GENMASK64(46, 39), x)
/* destination burst length enable */
#define DMA_DW_AXI_CTL_AWLEN_EN                  BIT64(47)
/* destination burst length(considered when corresponding enable bit is set) */
#define DMA_DW_AXI_CTL_AWLEN(x)                  FIELD_PREP(GENMASK64(55, 48), x)

/* source burst transaction length */
#define DMA_DW_AXI_CTL_SRC_MSIZE(x)              FIELD_PREP(GENMASK64(17, 14), x)
/* destination burst transaction length */
#define DMA_DW_AXI_CTL_DST_MSIZE(x)              FIELD_PREP(GENMASK64(21, 18), x)
/* source transfer width */
#define DMA_DW_AXI_CTL_SRC_WIDTH(x)              FIELD_PREP(GENMASK64(10, 8), x)
/* destination transfer width */
#define DMA_DW_AXI_CTL_DST_WIDTH(x)              FIELD_PREP(GENMASK64(13, 11), x)

/* mask all the interrupts */
#define DMA_DW_AXI_IRQ_NONE                      0
/* enable block completion transfer interrupt */
#define DMA_DW_AXI_IRQ_BLOCK_TFR                 BIT64(0)
/* enable transfer completion interrupt */
#define DMA_DW_AXI_IRQ_DMA_TFR                   BIT64(1)
/* enable interrupts on any dma transfer error */
#define DMA_DW_AXI_IRQ_ALL_ERR                   (GENMASK64(14, 5) | GENMASK64(21, 16))

/* global enable bit for dma controller */
#define DMA_DW_AXI_CFG_EN                        BIT64(0)
/* global enable bit for interrupt */
#define DMA_DW_AXI_CFG_INT_EN                    BIT64(1)

/* descriptor used by dw axi dma controller*/
struct dma_lli {
	uint64_t sar;
	uint64_t dar;
	uint32_t block_ts_lo;
	uint32_t reserved;
	uint64_t llp;
	uint64_t ctl;
	uint32_t sstat;
	uint32_t dstat;
	uint64_t llp_status;
	uint64_t reserved1;
} __aligned(64);

/* status of the channel */
enum dma_dw_axi_ch_state {
	DMA_DW_AXI_CH_IDLE,
	DMA_DW_AXI_CH_SUSPENDED,
	DMA_DW_AXI_CH_ACTIVE,
	DMA_DW_AXI_CH_PREPARED,
};

/* source and destination transfer width */
enum dma_dw_axi_ch_width {
	BITS_8,
	BITS_16,
	BITS_32,
	BITS_64,
	BITS_128,
	BITS_256,
	BITS_512,
};

/* transfer direction and flow controller */
enum dma_dw_axi_tt_fc {
	M2M_DMAC,
	M2P_DMAC,
	P2M_DMAC,
	P2P_DMAC,
	P2M_SRC,
	P2P_SRC,
	M2P_DST,
	P2P_DST,
};

/* type of multi-block transfer */
enum dma_dw_axi_multi_blk_type {
	MULTI_BLK_CONTIGUOUS,
	MULTI_BLK_RELOAD,
	MULTI_BLK_SHADOW_REG,
	MULTI_BLK_LLI,
};

/* dma driver channel specific information */
struct dma_dw_axi_ch_data {
	/* lli descriptor base */
	struct dma_lli *lli_desc_base;
	/* lli current descriptor */
	struct dma_lli *lli_desc_current;
	/* dma channel state */
	enum dma_dw_axi_ch_state ch_state;
	/* direction of transfer */
	uint32_t direction;
	/* number of descriptors */
	uint32_t lli_desc_count;
	/* cfg register configuration for dma transfer */
	uint64_t cfg;
	/* mask and unmask interrupts */
	uint64_t irq_unmask;
	/* user call back for dma transfer completion */
	dma_callback_t dma_xfer_callback;
	/* user data for dma callback for dma transfer completion */
	void *priv_data_xfer;
	/* user call back for dma block transfer completion */
	dma_callback_t dma_blk_xfer_callback;
	/* user data for dma callback for dma block transfer completion */
	void *priv_data_blk_tfr;
};

/* dma controller driver data structure */
struct dma_dw_axi_dev_data {
	/* dma context */
	struct dma_context dma_ctx;

	/* mmio address mapping info for dma controller */
	DEVICE_MMIO_NAMED_RAM(dma_mmio);
	/* pointer to store channel specific info */
	struct dma_dw_axi_ch_data *chan;
	/* pointer to hold descriptor base address */
	struct dma_lli *dma_desc_pool;
};

/* Device constant configuration parameters */
struct dma_dw_axi_dev_cfg {
	/* dma address space to map */
	DEVICE_MMIO_NAMED_ROM(dma_mmio);

#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(resets)
	/* Reset controller device configurations */
	const struct reset_dt_spec reset;
#endif
	/* dma controller interrupt configuration function pointer */
	void (*irq_config)(void);
};

/**
 * @brief get current status of the channel
 *
 * @param dev Pointer to the device structure for the driver instance
 * @param channel channel number
 *
 * @retval status of the channel
 */
static enum dma_dw_axi_ch_state dma_dw_axi_get_ch_status(const struct device *dev, uint32_t ch)
{
	uint32_t bit_status;
	uint64_t ch_status;
	uintptr_t reg_base = DEVICE_MMIO_NAMED_GET(dev, dma_mmio);

	ch_status = sys_read64(reg_base + DMA_DW_AXI_CHENREG);

	/* channel is active/busy in the dma transfer */
	bit_status = ((ch_status >> ch) & 1);
	if (bit_status) {
		return DMA_DW_AXI_CH_ACTIVE;
	}

	/* channel is currently suspended */
	bit_status = ((ch_status >> (16 + ch)) & 1);
	if (bit_status) {
		return DMA_DW_AXI_CH_SUSPENDED;
	}

	/* channel is idle */
	return DMA_DW_AXI_CH_IDLE;
}

static void dma_dw_axi_isr(const struct device *dev)
{
	unsigned int channel;
	uint64_t status, ch_status;
	int ret_status = 0;
	struct dma_dw_axi_ch_data *chan_data;
	uintptr_t reg_base = DEVICE_MMIO_NAMED_GET(dev, dma_mmio);
	struct dma_dw_axi_dev_data *const dw_dev_data = DEV_DATA(dev);

	/* read interrupt status register to find interrupt is for which channel */
	status = sys_read64(reg_base + DMA_DW_AXI_INTSTATUSREG);
	channel = find_lsb_set(status) - 1;
	if (channel < 0) {
		LOG_ERR("Spurious interrupt received channel:%u\n", channel);
		return;
	}

	if (channel > (dw_dev_data->dma_ctx.dma_channels - 1)) {
		LOG_ERR("Interrupt received on invalid channel:%d\n", channel);
		return;
	}

	/* retrieve channel specific data pointer for a channel */
	chan_data = &dw_dev_data->chan[channel];

	/* get dma transfer status */
	ch_status = sys_read64(reg_base + DMA_DW_AXI_CH_INTSTATUS(channel));
	if (!ch_status) {
		LOG_ERR("Spurious interrupt received ch_status:0x%llx\n", ch_status);
		return;
	}

	/* handle dma transfer errors if any */
	if (ch_status & DMA_DW_AXI_IRQ_ALL_ERR) {
		sys_write64(DMA_DW_AXI_IRQ_ALL_ERR,
			reg_base + DMA_DW_AXI_CH_INTCLEARREG(channel));
		LOG_ERR("DMA Error: Channel:%d Channel interrupt status:0x%llx\n",
				channel, ch_status);
		ret_status = -(ch_status & DMA_DW_AXI_IRQ_ALL_ERR);
	}

	/* handle block transfer completion */
	if (ch_status & DMA_DW_AXI_IRQ_BLOCK_TFR) {
		sys_write64(DMA_DW_AXI_IRQ_ALL_ERR | DMA_DW_AXI_IRQ_BLOCK_TFR,
				reg_base + DMA_DW_AXI_CH_INTCLEARREG(channel));

		if (chan_data->dma_blk_xfer_callback) {
			chan_data->dma_blk_xfer_callback(dev,
				chan_data->priv_data_blk_tfr, channel, ret_status);
		}
	}

	/* handle dma transfer completion */
	if (ch_status & DMA_DW_AXI_IRQ_DMA_TFR) {
		sys_write64(DMA_DW_AXI_IRQ_ALL_ERR | DMA_DW_AXI_IRQ_DMA_TFR,
				reg_base + DMA_DW_AXI_CH_INTCLEARREG(channel));

		if (chan_data->dma_xfer_callback) {
			chan_data->dma_xfer_callback(dev, chan_data->priv_data_xfer,
						channel, ret_status);
			chan_data->ch_state = dma_dw_axi_get_ch_status(dev, channel);
		}
	}
}

/**
 * @brief set data source and destination data width
 *
 * @param lli_desc Pointer to the descriptor
 * @param src_data_width source data width
 * @param dest_data_width destination data width
 *
 * @retval 0 on success, -ENOTSUP if the data width is not supported
 */
static int dma_dw_axi_set_data_width(struct dma_lli *lli_desc,
				uint32_t src_data_width, uint32_t dest_data_width)
{
	if (src_data_width > CONFIG_DMA_DW_AXI_DATA_WIDTH ||
			dest_data_width > CONFIG_DMA_DW_AXI_DATA_WIDTH) {
		LOG_ERR("transfer width more than %u not supported", CONFIG_DMA_DW_AXI_DATA_WIDTH);
		return -ENOTSUP;
	}

	switch (src_data_width) {
	case 1:
		/* one byte transfer */
		lli_desc->ctl |= DMA_DW_AXI_CTL_SRC_WIDTH(BITS_8);
		break;
	case 2:
		/* 2-bytes transfer width */
		lli_desc->ctl |= DMA_DW_AXI_CTL_SRC_WIDTH(BITS_16);
		break;
	case 4:
		/* 4-bytes transfer width */
		lli_desc->ctl |= DMA_DW_AXI_CTL_SRC_WIDTH(BITS_32);
		break;
	case 8:
		/* 8-bytes transfer width */
		lli_desc->ctl |= DMA_DW_AXI_CTL_SRC_WIDTH(BITS_64);
		break;
	case 16:
		/* 16-bytes transfer width */
		lli_desc->ctl |= DMA_DW_AXI_CTL_SRC_WIDTH(BITS_128);
		break;
	case 32:
		/* 32-bytes transfer width */
		lli_desc->ctl |= DMA_DW_AXI_CTL_SRC_WIDTH(BITS_256);
		break;
	case 64:
		/* 64-bytes transfer width */
		lli_desc->ctl |= DMA_DW_AXI_CTL_SRC_WIDTH(BITS_512);
		break;
	default:
		LOG_ERR("Source transfer width not supported");
		return -ENOTSUP;
	}

	switch (dest_data_width) {
	case 1:
		/* one byte transfer */
		lli_desc->ctl |= DMA_DW_AXI_CTL_DST_WIDTH(BITS_8);
		break;
	case 2:
		/* 2-bytes transfer width */
		lli_desc->ctl |= DMA_DW_AXI_CTL_DST_WIDTH(BITS_16);
		break;
	case 4:
		/* 4-bytes transfer width */
		lli_desc->ctl |= DMA_DW_AXI_CTL_DST_WIDTH(BITS_32);
		break;
	case 8:
		/* 8-bytes transfer width */
		lli_desc->ctl |= DMA_DW_AXI_CTL_DST_WIDTH(BITS_64);
		break;
	case 16:
		/* 16-bytes transfer width */
		lli_desc->ctl |= DMA_DW_AXI_CTL_DST_WIDTH(BITS_128);
		break;
	case 32:
		/* 32-bytes transfer width */
		lli_desc->ctl |= DMA_DW_AXI_CTL_DST_WIDTH(BITS_256);
		break;
	case 64:
		/* 64-bytes transfer width */
		lli_desc->ctl |= DMA_DW_AXI_CTL_DST_WIDTH(BITS_512);
		break;
	default:
		LOG_ERR("Destination transfer width not supported");
		return -ENOTSUP;
	}

	return 0;
}

static int dma_dw_axi_config(const struct device *dev, uint32_t channel,
						 struct dma_config *cfg)
{
	int ret;
	uint32_t msize_src, msize_dst, i, ch_state;
	struct dma_dw_axi_ch_data *chan_data;
	struct dma_block_config *blk_cfg;
	struct dma_lli *lli_desc;
	struct dma_dw_axi_dev_data *const dw_dev_data = DEV_DATA(dev);

	/* check for invalid parameters before dereferencing them. */
	if (cfg == NULL) {
		LOG_ERR("invalid dma config :%p", cfg);
		return -ENODATA;
	}

	/* check if the channel is valid */
	if (channel > (dw_dev_data->dma_ctx.dma_channels - 1)) {
		LOG_ERR("invalid dma channel %d", channel);
		return -EINVAL;
	}

	/* return if the channel is not idle */
	ch_state = dma_dw_axi_get_ch_status(dev, channel);
	if (ch_state != DMA_DW_AXI_CH_IDLE) {
		LOG_ERR("DMA channel:%d is not idle(status:%d)", channel, ch_state);
		return -EBUSY;
	}

	if (!cfg->block_count) {
		LOG_ERR("no blocks to transfer");
		return -EINVAL;
	}

	/* descriptor should be less than max configured descriptor */
	if (cfg->block_count > CONFIG_DMA_DW_AXI_MAX_DESC) {
		LOG_ERR("dma:%s channel %d descriptor block count: %d larger than"
			" max descriptors in pool: %d", dev->name, channel,
			cfg->block_count, CONFIG_DMA_DW_AXI_MAX_DESC);
		return -EINVAL;
	}

	if (cfg->source_burst_length > CONFIG_DMA_DW_AXI_MAX_BURST_TXN_LEN ||
			cfg->dest_burst_length > CONFIG_DMA_DW_AXI_MAX_BURST_TXN_LEN ||
			cfg->source_burst_length == 0 || cfg->dest_burst_length == 0) {
		LOG_ERR("dma:%s burst length not supported", dev->name);
		return -ENOTSUP;
	}

	/* get channel specific data pointer */
	chan_data = &dw_dev_data->chan[channel];

	/* check if the channel is currently idle */
	if (chan_data->ch_state != DMA_DW_AXI_CH_IDLE) {
		LOG_ERR("DMA channel:%d is busy", channel);
		return -EBUSY;
	}

	/* burst transaction length for source and destination */
	msize_src = DMA_DW_AXI_GET_MSIZE(cfg->source_burst_length);
	msize_dst = DMA_DW_AXI_GET_MSIZE(cfg->dest_burst_length);

	chan_data->cfg = 0;
	chan_data->irq_unmask = 0;

	chan_data->direction = cfg->channel_direction;

	chan_data->lli_desc_base =
			&dw_dev_data->dma_desc_pool[channel * CONFIG_DMA_DW_AXI_MAX_DESC];
	chan_data->lli_desc_count = cfg->block_count;
	memset(chan_data->lli_desc_base, 0,
			sizeof(struct dma_lli) * chan_data->lli_desc_count);

	lli_desc = chan_data->lli_desc_base;
	blk_cfg = cfg->head_block;

	/* max channel priority can be MAX_CHANNEL - 1 */
	if (cfg->channel_priority < dw_dev_data->dma_ctx.dma_channels) {
		chan_data->cfg |= DMA_DW_AXI_CFG_PRIORITY(cfg->channel_priority);
	}

	/* configure all the descriptors in a loop */
	for (i = 0; i < cfg->block_count; i++) {

		ret = dma_dw_axi_set_data_width(lli_desc, cfg->source_data_size,
				cfg->dest_data_size);
		if (ret) {
			return ret;
		}

		lli_desc->ctl |= DMA_DW_AXI_CTL_SRC_STAT_EN |
				DMA_DW_AXI_CTL_DST_STAT_EN | DMA_DW_AXI_CTL_IOC_BLK_TFR;

		lli_desc->sar = blk_cfg->source_address;
		lli_desc->dar = blk_cfg->dest_address;

		/* set block transfer size*/
		lli_desc->block_ts_lo = (blk_cfg->block_size / cfg->source_data_size) - 1;
		if (lli_desc->block_ts_lo > CONFIG_DMA_DW_AXI_MAX_BLOCK_TS) {
			LOG_ERR("block transfer size more than %u not supported",
				CONFIG_DMA_DW_AXI_MAX_BLOCK_TS);
			return -ENOTSUP;
		}

		/* configuration based on channel direction */
		if (cfg->channel_direction == MEMORY_TO_MEMORY) {
			chan_data->cfg |= DMA_DW_AXI_CFG_TT_FC(M2M_DMAC);

			lli_desc->ctl |= DMA_DW_AXI_CTL_SRC_MSIZE(msize_src) |
					DMA_DW_AXI_CTL_DST_MSIZE(msize_dst);

		} else if (cfg->channel_direction == MEMORY_TO_PERIPHERAL) {

			chan_data->cfg |= DMA_DW_AXI_CFG_TT_FC(M2P_DMAC);
			lli_desc->ctl |= DMA_DW_AXI_CTL_SRC_MSIZE(msize_src) |
					DMA_DW_AXI_CTL_DST_MSIZE(msize_dst);
			WRITE_BIT(chan_data->cfg, DMA_DW_AXI_CFG_HW_HS_DST_BIT_POS, 0);

			/* assign a hardware handshake interface */
			chan_data->cfg |= DMA_DW_AXI_CFG_DST_PER(cfg->dma_slot);

		} else if (cfg->channel_direction == PERIPHERAL_TO_MEMORY) {
			lli_desc->ctl |= DMA_DW_AXI_CTL_SRC_MSIZE(msize_src) |
					DMA_DW_AXI_CTL_DST_MSIZE(msize_dst);
			chan_data->cfg |= DMA_DW_AXI_CFG_TT_FC(P2M_DMAC);
			WRITE_BIT(chan_data->cfg, DMA_DW_AXI_CFG_HW_HS_SRC_BIT_POS, 0);

			/* assign a hardware handshake interface */
			chan_data->cfg |= DMA_DW_AXI_CFG_SRC_PER(cfg->dma_slot);

		} else {
			LOG_ERR("%s: dma %s channel %d invalid direction %d",
				__func__, dev->name, channel, cfg->channel_direction);

			return -EINVAL;
		}

		/* set pointer to the next descriptor */
		lli_desc->llp = ((uint64_t)(lli_desc + 1));

#if defined(CONFIG_DMA_DW_AXI_LLI_SUPPORT)
		/* configure multi block transfer size as linked list */
		chan_data->cfg |= DMA_DW_AXI_CFG_SRC_MULTBLK_TYPE(MULTI_BLK_LLI) |
				DMA_DW_AXI_CFG_DST_MULTBLK_TYPE(MULTI_BLK_LLI);

		lli_desc->ctl |= DMA_DW_AXI_CTL_LLI_VALID;
		/* last descriptor*/
		if ((i + 1) == chan_data->lli_desc_count) {
			lli_desc->ctl |= DMA_DW_AXI_CTL_LLI_LAST | DMA_DW_AXI_CTL_LLI_VALID;
			lli_desc->llp = 0;
		}
#else
		/* configure multi-block transfer as contiguous mode */
		chan_data->cfg |= DMA_DW_AXI_CFG_SRC_MULTBLK_TYPE(MULTI_BLK_CONTIGUOUS) |
				DMA_DW_AXI_CFG_DST_MULTBLK_TYPE(MULTI_BLK_CONTIGUOUS);
#endif

		/* next descriptor to configure*/
		lli_desc++;
		blk_cfg = blk_cfg->next_block;
	}

	arch_dcache_flush_range((void *)chan_data->lli_desc_base,
				sizeof(struct dma_lli) * cfg->block_count);

	chan_data->lli_desc_current = chan_data->lli_desc_base;

	/* enable an interrupt depending on whether the callback is requested after dma transfer
	 * completion or dma block transfer completion
	 *
	 * disable an interrupt if callback is not requested
	 */
	if (cfg->dma_callback && cfg->complete_callback_en) {
		chan_data->dma_blk_xfer_callback = cfg->dma_callback;
		chan_data->priv_data_blk_tfr = cfg->user_data;

		chan_data->irq_unmask = DMA_DW_AXI_IRQ_BLOCK_TFR | DMA_DW_AXI_IRQ_DMA_TFR;
	} else if (cfg->dma_callback && !cfg->complete_callback_en) {
		chan_data->dma_xfer_callback = cfg->dma_callback;
		chan_data->priv_data_xfer = cfg->user_data;

		chan_data->irq_unmask = DMA_DW_AXI_IRQ_DMA_TFR;
	} else {
		chan_data->irq_unmask = DMA_DW_AXI_IRQ_NONE;
	}

	/* unmask error interrupts when error_callback_dis is 0 */
	if (!cfg->error_callback_dis) {
		chan_data->irq_unmask |= DMA_DW_AXI_IRQ_ALL_ERR;
	}

	/* dma descriptors are configured, ready to start dma transfer */
	chan_data->ch_state = DMA_DW_AXI_CH_PREPARED;

	return 0;
}

static int dma_dw_axi_start(const struct device *dev, uint32_t channel)
{
	uint32_t ch_state;
	struct dma_dw_axi_ch_data *chan_data;
	struct dma_lli *lli_desc;
	struct dma_dw_axi_dev_data *const dw_dev_data = DEV_DATA(dev);
	uintptr_t reg_base = DEVICE_MMIO_NAMED_GET(dev, dma_mmio);

	/* validate channel number */
	if (channel > (dw_dev_data->dma_ctx.dma_channels - 1)) {
		LOG_ERR("invalid dma channel %d", channel);
		return -EINVAL;
	}

	/* check whether channel is idle before initiating DMA transfer */
	ch_state = dma_dw_axi_get_ch_status(dev, channel);
	if (ch_state != DMA_DW_AXI_CH_IDLE) {
		LOG_ERR("DMA channel:%d is not idle", channel);
		return -EBUSY;
	}

	/* get channel specific data pointer */
	chan_data = &dw_dev_data->chan[channel];

	if (chan_data->ch_state != DMA_DW_AXI_CH_PREPARED) {
		LOG_ERR("DMA descriptors not configured");
		return -EINVAL;
	}

	/* enable dma controller and global interrupt bit */
	sys_write64(DMA_DW_AXI_CFG_INT_EN | DMA_DW_AXI_CFG_EN, reg_base + DMA_DW_AXI_CFGREG);

	sys_write64(chan_data->cfg, reg_base + DMA_DW_AXI_CH_CFG(channel));

	sys_write64(chan_data->irq_unmask,
				reg_base + DMA_DW_AXI_CH_INTSTATUS_ENABLEREG(channel));
	sys_write64(chan_data->irq_unmask,
				reg_base + DMA_DW_AXI_CH_INTSIGNAL_ENABLEREG(channel));

	lli_desc = chan_data->lli_desc_current;

#if defined(CONFIG_DMA_DW_AXI_LLI_SUPPORT)
	sys_write64(((uint64_t)lli_desc), reg_base + DMA_DW_AXI_CH_LLP(channel));
#else
	/* Program Source and Destination addresses */
	sys_write64(lli_desc->sar, reg_base + DMA_DW_AXI_CH_SAR(channel));
	sys_write64(lli_desc->dar, reg_base + DMA_DW_AXI_CH_DAR(channel));

	sys_write64(lli_desc->block_ts_lo & BLOCK_TS_MASK,
			reg_base + DMA_DW_AXI_CH_BLOCK_TS(channel));

	/* Program CH.CTL register */
	sys_write64(lli_desc->ctl, reg_base + DMA_DW_AXI_CH_CTL(channel));
#endif

	/* Enable the channel which will initiate DMA transfer */
	sys_write64(CH_EN(channel), reg_base + DMA_DW_AXI_CHENREG);

	chan_data->ch_state = dma_dw_axi_get_ch_status(dev, channel);

	return 0;
}

static int dma_dw_axi_stop(const struct device *dev, uint32_t channel)
{
	bool is_channel_busy;
	uint32_t ch_state;
	struct dma_dw_axi_dev_data *const dw_dev_data = DEV_DATA(dev);
	uintptr_t reg_base = DEVICE_MMIO_NAMED_GET(dev, dma_mmio);

	/* channel should be valid */
	if (channel > (dw_dev_data->dma_ctx.dma_channels - 1)) {
		LOG_ERR("invalid dma channel %d", channel);
		return -EINVAL;
	}

	/* return if the channel is idle as there is nothing to stop */
	ch_state = dma_dw_axi_get_ch_status(dev, channel);
	if (ch_state == DMA_DW_AXI_CH_IDLE) {
		/* channel is already idle */
		return 0;
	}

	/* To stop transfer or abort the channel in case of abnormal state:
	 * 1. To disable channel, first suspend channel and drain the FIFO
	 * 2. Disable the channel. Channel may get hung and can't be disabled
	 * if there is no response from peripheral
	 * 3. If channel is not disabled, Abort the channel. Aborting channel will
	 * Flush out FIFO and data will be lost. Then corresponding interrupt will
	 * be raised for abort and CH_EN bit will be cleared from CHENREG register
	 */
	sys_write64(CH_SUSP(channel), reg_base + DMA_DW_AXI_CHENREG);

	/* Try to disable the channel */
	sys_clear_bit(reg_base + DMA_DW_AXI_CHENREG, channel);

	is_channel_busy = WAIT_FOR((sys_read64(reg_base + DMA_DW_AXI_CHENREG)) & (BIT(channel)),
						CONFIG_DMA_CHANNEL_STATUS_TIMEOUT, k_busy_wait(10));
	if (is_channel_busy) {
		LOG_WRN("No response from handshaking interface... Aborting a channel...");
		sys_write64(CH_ABORT(channel), reg_base + DMA_DW_AXI_CHENREG);

		is_channel_busy = WAIT_FOR((sys_read64(reg_base + DMA_DW_AXI_CHENREG)) &
				(BIT(channel)), CONFIG_DMA_CHANNEL_STATUS_TIMEOUT,
				k_busy_wait(10));
		if (is_channel_busy) {
			LOG_ERR("Channel abort failed");
			return -EBUSY;
		}
	}

	return 0;
}

static int dma_dw_axi_resume(const struct device *dev, uint32_t channel)
{
	uint32_t reg;
	uintptr_t reg_base = DEVICE_MMIO_NAMED_GET(dev, dma_mmio);
	struct dma_dw_axi_dev_data *const dw_dev_data = DEV_DATA(dev);
	uint32_t ch_state;

	/* channel should be valid */
	if (channel > (dw_dev_data->dma_ctx.dma_channels - 1)) {
		LOG_ERR("invalid dma channel %d", channel);
		return -EINVAL;
	}

	ch_state = dma_dw_axi_get_ch_status(dev, channel);
	if (ch_state != DMA_DW_AXI_CH_SUSPENDED) {
		LOG_INF("channel %u is not in suspended state so cannot resume channel", channel);
		return 0;
	}

	reg = sys_read64(reg_base + DMA_DW_AXI_CHENREG);
	/* channel susp write enable bit has to be asserted */
	WRITE_BIT(reg, CH_RESUME_WE(channel), 1);
	/* channel susp bit must be cleared to resume a channel*/
	WRITE_BIT(reg, CH_RESUME(channel), 0);
	/* resume a channel by writing 0: ch_susp and 1: ch_susp_we */
	sys_write64(reg, reg_base + DMA_DW_AXI_CHENREG);

	return 0;
}

/* suspend a dma channel */
static int dma_dw_axi_suspend(const struct device *dev, uint32_t channel)
{
	int ret;
	uintptr_t reg_base = DEVICE_MMIO_NAMED_GET(dev, dma_mmio);
	struct dma_dw_axi_dev_data *const dw_dev_data = DEV_DATA(dev);
	uint32_t ch_state;

	/* channel should be valid */
	if (channel > (dw_dev_data->dma_ctx.dma_channels - 1)) {
		LOG_ERR("invalid dma channel %u", channel);
		return -EINVAL;
	}

	ch_state = dma_dw_axi_get_ch_status(dev, channel);
	if (ch_state != DMA_DW_AXI_CH_ACTIVE) {
		LOG_INF("nothing to suspend as dma channel %u is not busy", channel);
		return 0;
	}

	/* suspend dma transfer */
	sys_write64(CH_SUSP(channel), reg_base + DMA_DW_AXI_CHENREG);

	ret = WAIT_FOR(dma_dw_axi_get_ch_status(dev, channel) &
			DMA_DW_AXI_CH_SUSPENDED, CONFIG_DMA_CHANNEL_STATUS_TIMEOUT,
			k_busy_wait(10));
	if (ret == 0) {
		LOG_ERR("channel suspend failed");
		return ret;
	}

	return 0;
}

static int dma_dw_axi_init(const struct device *dev)
{
	DEVICE_MMIO_NAMED_MAP(dev, dma_mmio, K_MEM_CACHE_NONE);
	int i, ret;
	struct dma_dw_axi_ch_data *chan_data;
	const struct dma_dw_axi_dev_cfg *dw_dma_config = DEV_CFG(dev);
	struct dma_dw_axi_dev_data *const dw_dev_data = DEV_DATA(dev);

#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(resets)

	if (dw_dma_config->reset.dev != NULL) {
	/* check if reset manager is in ready state */
		if (!device_is_ready(dw_dma_config->reset.dev)) {
			LOG_ERR("reset controller device not found");
			return -ENODEV;
		}

		/* assert and de-assert dma controller */
		ret = reset_line_toggle(dw_dma_config->reset.dev, dw_dma_config->reset.id);
		if (ret != 0) {
			LOG_ERR("failed to reset dma controller");
			return ret;
		}
	}
#endif

	/* initialize channel state variable */
	for (i = 0; i < dw_dev_data->dma_ctx.dma_channels; i++) {
		chan_data = &dw_dev_data->chan[i];
		/* initialize channel state */
		chan_data->ch_state = DMA_DW_AXI_CH_IDLE;
	}

	/* configure and enable interrupt lines */
	dw_dma_config->irq_config();

	return 0;
}

static DEVICE_API(dma, dma_dw_axi_driver_api) = {
	.config = dma_dw_axi_config,
	.start = dma_dw_axi_start,
	.stop = dma_dw_axi_stop,
	.suspend = dma_dw_axi_suspend,
	.resume = dma_dw_axi_resume,
};

/* enable irq lines */
#define CONFIGURE_DMA_IRQ(idx, inst) \
	IF_ENABLED(DT_INST_IRQ_HAS_IDX(inst, idx), ( \
		IRQ_CONNECT(DT_INST_IRQ_BY_IDX(inst, idx, irq), \
			DT_INST_IRQ_BY_IDX(inst, idx, priority), \
			dma_dw_axi_isr, \
			DEVICE_DT_INST_GET(inst), 0); \
			irq_enable(DT_INST_IRQ_BY_IDX(inst, idx, irq)); \
	))

#define DW_AXI_DMA_RESET_SPEC_INIT(inst) \
	.reset = RESET_DT_SPEC_INST_GET(inst), \

#define DW_AXI_DMAC_INIT(inst)								\
	static struct dma_dw_axi_ch_data chan_##inst[DT_INST_PROP(inst, dma_channels)];	\
	static struct dma_lli								\
		dma_desc_pool_##inst[DT_INST_PROP(inst, dma_channels) *			\
			CONFIG_DMA_DW_AXI_MAX_DESC];					\
	ATOMIC_DEFINE(dma_dw_axi_atomic##inst,						\
		      DT_INST_PROP(inst, dma_channels));				\
	static struct dma_dw_axi_dev_data dma_dw_axi_data_##inst = {			\
		.dma_ctx = {								\
			.magic = DMA_MAGIC,						\
			.atomic = dma_dw_axi_atomic##inst,				\
			.dma_channels = DT_INST_PROP(inst, dma_channels),		\
		},									\
		.chan = chan_##inst,							\
		.dma_desc_pool = dma_desc_pool_##inst,					\
		};									\
	static void dw_dma_irq_config_##inst(void);					\
	static const struct dma_dw_axi_dev_cfg dma_dw_axi_config_##inst = {		\
		DEVICE_MMIO_NAMED_ROM_INIT(dma_mmio, DT_DRV_INST(inst)),		\
		IF_ENABLED(DT_INST_NODE_HAS_PROP(inst, resets),				\
			(DW_AXI_DMA_RESET_SPEC_INIT(inst)))				\
		.irq_config = dw_dma_irq_config_##inst,					\
	};										\
											\
	DEVICE_DT_INST_DEFINE(inst,							\
				dma_dw_axi_init,					\
				NULL,							\
				&dma_dw_axi_data_##inst,				\
				&dma_dw_axi_config_##inst, POST_KERNEL,			\
				CONFIG_DMA_INIT_PRIORITY,				\
				&dma_dw_axi_driver_api);				\
											\
	static void dw_dma_irq_config_##inst(void)					\
	{										\
		LISTIFY(DT_NUM_IRQS(DT_DRV_INST(inst)), CONFIGURE_DMA_IRQ, (), inst)	\
	}

DT_INST_FOREACH_STATUS_OKAY(DW_AXI_DMAC_INIT)
