/*
 * Copyright 2020 Broadcom
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr/device.h>
#include <zephyr/drivers/dma.h>
#include <errno.h>
#include <zephyr/init.h>
#include <string.h>
#include <soc.h>
#include <zephyr/sys/__assert.h>
#include "dma_pl330.h"

#define LOG_LEVEL CONFIG_DMA_LOG_LEVEL
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(dma_pl330);

#define BYTE_WIDTH(burst_size) (1 << (burst_size))

static int dma_pl330_submit(const struct device *dev, uint64_t dst,
			    uint64_t src, uint32_t channel, uint32_t size);

static void dma_pl330_get_counter(struct dma_pl330_ch_internal *ch_handle,
				  uint32_t *psrc_byte_width,
				  uint32_t *pdst_byte_width,
				  uint32_t *ploop_counter,
				  uint32_t *presidue)
{
	uint32_t srcbytewidth, dstbytewidth;
	uint32_t loop_counter, residue;

	srcbytewidth = BYTE_WIDTH(ch_handle->src_burst_sz);
	dstbytewidth = BYTE_WIDTH(ch_handle->dst_burst_sz);

	loop_counter = ch_handle->trans_size /
		       (srcbytewidth * (ch_handle->src_burst_len + 1));

	residue = ch_handle->trans_size - loop_counter *
		  (srcbytewidth * (ch_handle->src_burst_len + 1));

	*psrc_byte_width = srcbytewidth;
	*pdst_byte_width = dstbytewidth;
	*ploop_counter = loop_counter;
	*presidue = residue;
}

static uint32_t dma_pl330_ch_ccr(struct dma_pl330_ch_internal *ch_handle)
{
	uint32_t ccr;
	int secure = ch_handle->nonsec_mode ? SRC_PRI_NONSEC_VALUE :
		     SRC_PRI_SEC_VALUE;

	ccr = ((ch_handle->dst_cache_ctrl & CC_SRCCCTRL_MASK) <<
		CC_DSTCCTRL_SHIFT) +
	       ((ch_handle->nonsec_mode) << CC_DSTNS_SHIFT) +
	       (ch_handle->dst_burst_len << CC_DSTBRSTLEN_SHIFT) +
	       (ch_handle->dst_burst_sz << CC_DSTBRSTSIZE_SHIFT) +
	       (ch_handle->dst_inc << CC_DSTINC_SHIFT) +
	       ((ch_handle->src_cache_ctrl & CC_SRCCCTRL_MASK) <<
		CC_SRCCCTRL_SHIFT) +
	       (secure << CC_SRCPRI_SHIFT) +
	       (ch_handle->src_burst_len << CC_SRCBRSTLEN_SHIFT)  +
	       (ch_handle->src_burst_sz << CC_SRCBRSTSIZE_SHIFT)  +
	       (ch_handle->src_inc << CC_SRCINC_SHIFT);

	return ccr;
}

static void dma_pl330_calc_burstsz_len(struct dma_pl330_ch_internal *ch_handle,
				       uint64_t dst, uint64_t src,
				       uint32_t size)
{
	uint32_t byte_width, burst_sz, burst_len;

	burst_sz = MAX_BURST_SIZE_LOG2;
	/* src, dst and size should be aligned to burst size in bytes */
	while ((src | dst | size) & ((BYTE_WIDTH(burst_sz)) - 1)) {
		burst_sz--;
	}

	byte_width = BYTE_WIDTH(burst_sz);

	burst_len = MAX_BURST_LEN;
	while (burst_len) {
		/* Choose burst length so that size is aligned */
		if (!(size % ((burst_len + 1) << byte_width))) {
			break;
		}

		burst_len--;
	}

	ch_handle->src_burst_len = burst_len;
	ch_handle->src_burst_sz = burst_sz;
	ch_handle->dst_burst_len = burst_len;
	ch_handle->dst_burst_sz = burst_sz;
}

#ifdef CONFIG_DMA_64BIT
static void dma_pl330_cfg_dmac_add_control(uint32_t control_reg_base,
					   uint64_t dst, uint64_t src, int ch)
{
	uint32_t src_h = src >> 32;
	uint32_t dst_h = dst >> 32;
	uint32_t dmac_higher_addr;

	dmac_higher_addr = ((dst_h & HIGHER_32_ADDR_MASK) << DST_ADDR_SHIFT) |
			   (src_h & HIGHER_32_ADDR_MASK);

	sys_write32(dmac_higher_addr,
		    control_reg_base +
		    (ch * CONTROL_OFFSET)
		   );
}
#endif

static void dma_pl330_config_channel(struct dma_pl330_ch_config *ch_cfg,
				     uint64_t dst, uint64_t src, uint32_t size)
{
	struct dma_pl330_ch_internal *ch_handle = &ch_cfg->internal;

	ch_handle->src_addr = src;
	ch_handle->dst_addr = dst;
	ch_handle->trans_size = size;

	if (ch_cfg->src_addr_adj == DMA_ADDR_ADJ_INCREMENT) {
		ch_handle->src_inc = 1;
	}

	if (ch_cfg->dst_addr_adj == DMA_ADDR_ADJ_INCREMENT) {
		ch_handle->dst_inc = 1;
	}
}

static inline uint32_t dma_pl330_gen_mov(mem_addr_t buf,
					 enum dmamov_type type,
					 uint32_t val)
{
	sys_write8(OP_DMA_MOV, buf);
	sys_write8(type, buf + 1);
	sys_write8(val, buf + 2);
	sys_write8(val >> 8, buf + 3);
	sys_write8(val >> 16, buf + 4);
	sys_write8(val >> 24, buf + 5);

	return SZ_CMD_DMAMOV;
}

static inline void dma_pl330_gen_op(uint8_t opcode, uint32_t addr, uint32_t val)
{
	sys_write8(opcode, addr);
	sys_write8(val, addr + 1);
}

static int dma_pl330_setup_ch(const struct device *dev,
			      struct dma_pl330_ch_internal *ch_dat,
			      int ch)
{
	mem_addr_t dma_exec_addr;
	uint32_t offset = 0, ccr;
	uint32_t lp0_start, lp1_start;
	uint32_t loop_counter0 = 0, loop_counter1 = 0;
	uint32_t srcbytewidth, dstbytewidth;
	uint32_t loop_counter, residue;
	struct dma_pl330_dev_data *const dev_data = dev->data;
	struct dma_pl330_ch_config *channel_cfg;
	int secure = ch_dat->nonsec_mode ? SRC_PRI_NONSEC_VALUE :
				SRC_PRI_SEC_VALUE;

	channel_cfg = &dev_data->channels[ch];
	dma_exec_addr = channel_cfg->dma_exec_addr;

	offset  += dma_pl330_gen_mov(dma_exec_addr,
				     SAR, ch_dat->src_addr);

	offset  += dma_pl330_gen_mov(dma_exec_addr + offset,
				     DAR, ch_dat->dst_addr);

	ccr = dma_pl330_ch_ccr(ch_dat);

	offset  += dma_pl330_gen_mov(dma_exec_addr + offset,
				     CCR, ccr);

	dma_pl330_get_counter(ch_dat, &srcbytewidth, &dstbytewidth,
			      &loop_counter, &residue);

	if (loop_counter >= PL330_LOOP_COUNTER0_MAX) {
		loop_counter0 = PL330_LOOP_COUNTER0_MAX - 1;
		loop_counter1 = loop_counter / PL330_LOOP_COUNTER0_MAX - 1;
		dma_pl330_gen_op(OP_DMA_LOOP_COUNT1, dma_exec_addr + offset,
				 loop_counter1 & 0xff);
		offset = offset + 2;
		dma_pl330_gen_op(OP_DMA_LOOP, dma_exec_addr + offset,
				 loop_counter0 & 0xff);
		offset = offset + 2;
		lp1_start = offset;
		lp0_start = offset;
		sys_write8(OP_DMA_LD, dma_exec_addr + offset);
		sys_write8(OP_DMA_ST, dma_exec_addr + offset + 1);
		offset = offset + 2;
		dma_pl330_gen_op(OP_DMA_LP_BK_JMP1, dma_exec_addr + offset,
				 ((offset - lp0_start) & 0xff));
		offset = offset + 2;
		dma_pl330_gen_op(OP_DMA_LOOP, dma_exec_addr + offset,
				 (loop_counter0 & 0xff));
		offset = offset + 2;
		loop_counter1--;
		dma_pl330_gen_op(OP_DMA_LP_BK_JMP2, dma_exec_addr + offset,
				 ((offset - lp1_start) & 0xff));
		offset = offset + 2;
	}

	if ((loop_counter % PL330_LOOP_COUNTER0_MAX) != 0) {
		loop_counter0 = (loop_counter % PL330_LOOP_COUNTER0_MAX) - 1;
		dma_pl330_gen_op(OP_DMA_LOOP, dma_exec_addr + offset,
				 (loop_counter0 & 0xff));
		offset = offset + 2;
		loop_counter1--;
		lp0_start = offset;
		sys_write8(OP_DMA_LD, dma_exec_addr + offset);
		sys_write8(OP_DMA_ST, dma_exec_addr + offset + 1);
		offset = offset + 2;
		dma_pl330_gen_op(OP_DMA_LP_BK_JMP1, dma_exec_addr + offset,
				 ((offset - lp0_start) & 0xff));
		offset = offset + 2;
	}

	if (residue != 0) {
		ccr = ((ch_dat->nonsec_mode) << CC_DSTNS_SHIFT) +
		       (0x0 << CC_DSTBRSTLEN_SHIFT) +
		       (0x0 << CC_DSTBRSTSIZE_SHIFT) +
		       (ch_dat->dst_inc << CC_DSTINC_SHIFT) +
		       (secure << CC_SRCPRI_SHIFT) +
		       (0x0 << CC_SRCBRSTLEN_SHIFT) +
		       (0x0 << CC_SRCBRSTSIZE_SHIFT) +
		       ch_dat->src_inc;
		offset += dma_pl330_gen_mov(dma_exec_addr + offset,
					    CCR, ccr);
		dma_pl330_gen_op(OP_DMA_LOOP, dma_exec_addr + offset,
				 ((residue - 1) & 0xff));
		offset = offset + 2;
		lp0_start = offset;
		sys_write8(OP_DMA_LD, dma_exec_addr + offset);
		sys_write8(OP_DMA_ST, dma_exec_addr + offset + 1);
		offset = offset + 2;
		dma_pl330_gen_op(OP_DMA_LP_BK_JMP1, dma_exec_addr + offset,
				 ((offset - lp0_start) & 0xff));
		offset = offset + 2;
	}

	sys_write8(OP_DMA_END, dma_exec_addr + offset);
	sys_write8(OP_DMA_END, dma_exec_addr + offset + 1);
	sys_write8(OP_DMA_END, dma_exec_addr + offset + 2);
	sys_write8(OP_DMA_END, dma_exec_addr + offset + 3);

	return 0;
}

static int dma_pl330_start_dma_ch(const struct device *dev,
				  uint32_t reg_base, int ch, int secure)
{
	struct dma_pl330_dev_data *const dev_data = dev->data;
	struct dma_pl330_ch_config *channel_cfg;
	uint32_t count = 0U;
	uint32_t data;

	channel_cfg = &dev_data->channels[ch];
	do {
		data = sys_read32(reg_base + DMAC_PL330_DBGSTATUS);
		if (++count > DMA_TIMEOUT_US) {
			return -ETIMEDOUT;
		}
		k_busy_wait(1);
	} while ((data & DATA_MASK) != 0);

	sys_write32(((ch << DMA_INTSR1_SHIFT) +
		    (DMA_INTSR0 << DMA_INTSR0_SHIFT) +
		    (secure << DMA_SECURE_SHIFT) + (ch << DMA_CH_SHIFT)),
		    reg_base + DMAC_PL330_DBGINST0);

	sys_write32(channel_cfg->dma_exec_addr,
		    reg_base + DMAC_PL330_DBGINST1);

	sys_write32(0x0, reg_base + DMAC_PL330_DBGCMD);

	count = 0U;
	do {
		data = sys_read32(reg_base + DMAC_PL330_DBGCMD);
		if (++count > DMA_TIMEOUT_US) {
			return -ETIMEDOUT;
		}
		k_busy_wait(1);
	} while ((data & DATA_MASK) != 0);

	return 0;
}

static int dma_pl330_wait(uint32_t reg_base, int ch)
{
	int count = 0U;
	uint32_t cs0_reg = reg_base + DMAC_PL330_CS0;

	do {
		if (++count > DMA_TIMEOUT_US) {
			return -ETIMEDOUT;
		}
		k_busy_wait(1);
	} while (((sys_read32(cs0_reg + ch * 8)) & CH_STATUS_MASK) != 0);

	return 0;
}

static int dma_pl330_xfer(const struct device *dev, uint64_t dst,
			  uint64_t src, uint32_t size, uint32_t channel,
			  uint32_t *xfer_size)
{
	struct dma_pl330_dev_data *const dev_data = dev->data;
	const struct dma_pl330_config *const dev_cfg = dev->config;
	struct dma_pl330_ch_config *channel_cfg;
	struct dma_pl330_ch_internal *ch_handle;
	int ret;
	uint32_t max_size;

	channel_cfg = &dev_data->channels[channel];
	ch_handle = &channel_cfg->internal;

	dma_pl330_calc_burstsz_len(ch_handle, dst, src, size);

	max_size = GET_MAX_DMA_SIZE((1 << ch_handle->src_burst_sz),
				    ch_handle->src_burst_len);

	if (size > max_size) {
		size = max_size;
	}

	dma_pl330_config_channel(channel_cfg, dst, src, size);
#ifdef CONFIG_DMA_64BIT
	/*
	 * Pl330 supports only 4GB boundary, but boundary region can be
	 * configured.
	 * Support added for 36bit address, lower 32bit address are configured
	 * in pl330 registers and higher 4bit address are configured in
	 * LS_ICFG_DMAC_AXI_ADD_CONTROL registers.
	 * Each channel has 1 control register to configure higher 4bit address.
	 */

	dma_pl330_cfg_dmac_add_control(dev_cfg->control_reg_base,
				       dst, src, channel);
#endif
	ret = dma_pl330_setup_ch(dev, ch_handle, channel);
	if (ret) {
		LOG_ERR("Failed to setup channel for DMA PL330");
		goto err;
	}

	ret = dma_pl330_start_dma_ch(dev, dev_cfg->reg_base, channel,
				     ch_handle->nonsec_mode);
	if (ret) {
		LOG_ERR("Failed to start DMA PL330");
		goto err;
	}

	ret = dma_pl330_wait(dev_cfg->reg_base, channel);
	if (ret) {
		LOG_ERR("Failed waiting to finish DMA PL330");
		goto err;
	}

	*xfer_size = size;
err:
	return ret;
}

#if CONFIG_DMA_64BIT
static int dma_pl330_handle_boundary(const struct device *dev, uint64_t dst,
				     uint64_t src, uint32_t channel,
				     uint32_t size)
{
	uint32_t dst_low = (uint32_t)dst;
	uint32_t src_low = (uint32_t)src;
	uint32_t transfer_size;
	int ret;

	/*
	 * Pl330 has only 32bit registers and supports 4GB memory.
	 * 4GB memory window can be configured using DMAC_AXI_ADD_CONTROL
	 * registers.
	 * Divide the DMA operation in 2 parts, 1st DMA from given address
	 * to boundary (0xffffffff) and 2nd DMA on remaining size.
	 */

	if (size > (PL330_MAX_OFFSET - dst_low)) {
		transfer_size = PL330_MAX_OFFSET - dst_low;
		ret = dma_pl330_submit(dev, dst, src, channel,
				       transfer_size);
		if (ret < 0) {
			return ret;
		}

		dst += transfer_size;
		src += transfer_size;
		size -= transfer_size;
		return dma_pl330_submit(dev, dst, src, channel, size);
	}

	if (size > (PL330_MAX_OFFSET - src_low)) {
		transfer_size = PL330_MAX_OFFSET - src_low;
		ret = dma_pl330_submit(dev, dst, src, channel, transfer_size);
		if (ret < 0) {
			return ret;
		}

		src += transfer_size;
		dst += transfer_size;
		size -= transfer_size;
		return dma_pl330_submit(dev, dst, src, channel, size);
	}

	return 0;
}
#endif

static int dma_pl330_submit(const struct device *dev, uint64_t dst,
			    uint64_t src,
			    uint32_t channel, uint32_t size)
{
	int ret;
	uint32_t xfer_size;

#if CONFIG_DMA_64BIT
	/*
	 * Pl330 has only 32bit registers and supports 4GB memory.
	 * 4GB memory window can be configured using DMAC_AXI_ADD_CONTROL
	 * registers. 32bit boundary (0xffffffff) should be check.
	 * DMA on boundary condition is taken care in below function.
	 */

	if ((size > (PL330_MAX_OFFSET - (uint32_t)dst)) ||
	    (size > (PL330_MAX_OFFSET - (uint32_t)src))) {
		return dma_pl330_handle_boundary(dev, dst, src,
						 channel, size);
	}
#endif
	while (size) {
		xfer_size = 0;
		ret = dma_pl330_xfer(dev, dst, src, size,
				     channel, &xfer_size);
		if (ret) {
			return ret;
		}
		if (xfer_size > size) {
			return -EFAULT;
		}
		size -= xfer_size;
		dst += xfer_size;
		src += xfer_size;
	}

	return 0;
}

static int dma_pl330_configure(const struct device *dev, uint32_t channel,
			       struct dma_config *cfg)
{
	struct dma_pl330_dev_data *const dev_data = dev->data;
	struct dma_pl330_ch_config *channel_cfg;
	struct dma_pl330_ch_internal *ch_handle;

	if (channel >= MAX_DMA_CHANNELS) {
		return -EINVAL;
	}

	channel_cfg = &dev_data->channels[channel];
	k_mutex_lock(&channel_cfg->ch_mutex, K_FOREVER);
	if (channel_cfg->channel_active) {
		k_mutex_unlock(&channel_cfg->ch_mutex);
		return -EBUSY;
	}
	channel_cfg->channel_active = 1;
	k_mutex_unlock(&channel_cfg->ch_mutex);

	if (cfg->channel_direction != MEMORY_TO_MEMORY) {
		return -ENOTSUP;
	}

	ch_handle = &channel_cfg->internal;
	memset(ch_handle, 0, sizeof(*ch_handle));

	channel_cfg->direction = cfg->channel_direction;
	channel_cfg->dst_addr_adj = cfg->head_block->dest_addr_adj;

	channel_cfg->src_addr = cfg->head_block->source_address;
	channel_cfg->dst_addr = cfg->head_block->dest_address;
	channel_cfg->trans_size = cfg->head_block->block_size;

	channel_cfg->dma_callback = cfg->dma_callback;
	channel_cfg->user_data = cfg->user_data;

	if (cfg->head_block->source_addr_adj == DMA_ADDR_ADJ_INCREMENT ||
	    cfg->head_block->source_addr_adj == DMA_ADDR_ADJ_NO_CHANGE) {
		channel_cfg->src_addr_adj = cfg->head_block->source_addr_adj;
	} else {
		return -ENOTSUP;
	}

	if (cfg->head_block->dest_addr_adj == DMA_ADDR_ADJ_INCREMENT ||
	    cfg->head_block->dest_addr_adj == DMA_ADDR_ADJ_NO_CHANGE) {
		channel_cfg->dst_addr_adj = cfg->head_block->dest_addr_adj;
	} else {
		return -ENOTSUP;
	}

	return 0;
}

static int dma_pl330_transfer_start(const struct device *dev,
				    uint32_t channel)
{
	struct dma_pl330_dev_data *const dev_data = dev->data;
	struct dma_pl330_ch_config *channel_cfg;
	int ret;

	if (channel >= MAX_DMA_CHANNELS) {
		return -EINVAL;
	}

	channel_cfg = &dev_data->channels[channel];
	ret = dma_pl330_submit(dev, channel_cfg->dst_addr,
			       channel_cfg->src_addr, channel,
			       channel_cfg->trans_size);

	k_mutex_lock(&channel_cfg->ch_mutex, K_FOREVER);
	channel_cfg->channel_active = 0;
	k_mutex_unlock(&channel_cfg->ch_mutex);

	return ret;
}

static int dma_pl330_transfer_stop(const struct device *dev, uint32_t channel)
{
	if (channel >= MAX_DMA_CHANNELS) {
		return -EINVAL;
	}

	/* Nothing as of now */
	return 0;
}

static int dma_pl330_initialize(const struct device *dev)
{
	const struct dma_pl330_config *const dev_cfg = dev->config;
	struct dma_pl330_dev_data *const dev_data = dev->data;
	struct dma_pl330_ch_config *channel_cfg;

	for (int channel = 0; channel < MAX_DMA_CHANNELS; channel++) {
		channel_cfg = &dev_data->channels[channel];
		channel_cfg->dma_exec_addr = dev_cfg->mcode_base +
					(channel * MICROCODE_SIZE_MAX);
		k_mutex_init(&channel_cfg->ch_mutex);
	}

	LOG_INF("Device %s initialized", dev->name);
	return 0;
}

static DEVICE_API(dma, pl330_driver_api) = {
	.config = dma_pl330_configure,
	.start = dma_pl330_transfer_start,
	.stop = dma_pl330_transfer_stop,
};

static const struct dma_pl330_config pl330_config = {
	.reg_base = DT_INST_REG_ADDR(0),
#ifdef CONFIG_DMA_64BIT
	.control_reg_base = DT_INST_REG_ADDR_BY_NAME(0, control_regs),
#endif
	.mcode_base = DT_INST_PROP_BY_IDX(0, microcode, 0),
};

static struct dma_pl330_dev_data pl330_data;

DEVICE_DT_INST_DEFINE(0, dma_pl330_initialize, NULL,
		    &pl330_data, &pl330_config,
		    POST_KERNEL, CONFIG_DMA_INIT_PRIORITY,
		    &pl330_driver_api);
