/*
 * Copyright (c) 2018 Google LLC.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT atmel_sam0_dmac

#include <zephyr/device.h>
#include <soc.h>
#include <zephyr/drivers/dma.h>

#include <zephyr/logging/log.h>
#include <zephyr/irq.h>
LOG_MODULE_REGISTER(dma_sam0, CONFIG_DMA_LOG_LEVEL);

#define DMA_REGS	((Dmac *)DT_INST_REG_ADDR(0))

struct dma_sam0_channel {
	dma_callback_t cb;
	void *user_data;
};

struct dma_sam0_data {
	__aligned(16) DmacDescriptor descriptors[DMAC_CH_NUM];
	__aligned(16) DmacDescriptor descriptors_wb[DMAC_CH_NUM];
	struct dma_sam0_channel channels[DMAC_CH_NUM];
};

/* Handles DMA interrupts and dispatches to the individual channel */
static void dma_sam0_isr(const struct device *dev)
{
	struct dma_sam0_data *data = dev->data;
	struct dma_sam0_channel *chdata;
	uint16_t pend = DMA_REGS->INTPEND.reg;
	uint32_t channel;

	/* Acknowledge all interrupts for the channel in pend */
	DMA_REGS->INTPEND.reg = pend;

	channel = (pend & DMAC_INTPEND_ID_Msk) >> DMAC_INTPEND_ID_Pos;
	chdata = &data->channels[channel];

	if (pend & DMAC_INTPEND_TERR) {
		if (chdata->cb) {
			chdata->cb(dev, chdata->user_data,
				   channel, -DMAC_INTPEND_TERR);
		}
	} else if (pend & DMAC_INTPEND_TCMPL) {
		if (chdata->cb) {
			chdata->cb(dev, chdata->user_data, channel, 0);
		}
	}

	/*
	 * If more than one channel is pending, we'll just immediately
	 * interrupt again and handle it through a different INTPEND value.
	 */
}

/* Configure a channel */
static int dma_sam0_config(const struct device *dev, uint32_t channel,
			   struct dma_config *config)
{
	struct dma_sam0_data *data = dev->data;
	DmacDescriptor *desc = &data->descriptors[channel];
	struct dma_block_config *block = config->head_block;
	struct dma_sam0_channel *channel_control;
	DMAC_BTCTRL_Type btctrl = { .reg = 0 };
	unsigned int key;

	if (channel >= DMAC_CH_NUM) {
		LOG_ERR("Unsupported channel");
		return -EINVAL;
	}

	if (config->block_count > 1) {
		LOG_ERR("Chained transfers not supported");
		/* TODO: add support for chained transfers. */
		return -ENOTSUP;
	}

	if (config->dma_slot >= DMAC_TRIG_NUM) {
		LOG_ERR("Invalid trigger number");
		return -EINVAL;
	}

	/* Lock and page in the channel configuration */
	key = irq_lock();

	/*
	 * The "bigger" DMAC on some SAM0 chips (e.g. SAMD5x) has
	 * independently accessible registers for each channel, while
	 * the other ones require an indirect channel selection before
	 * accessing shared registers.  The simplest way to detect the
	 * difference is the presence of the DMAC_CHID_ID macro from the
	 * ASF HAL (i.e. it's only defined if indirect access is required).
	 */
#ifdef DMAC_CHID_ID
	/* Select the channel for configuration */
	DMA_REGS->CHID.reg = DMAC_CHID_ID(channel);
	DMA_REGS->CHCTRLA.reg = 0;

	/* Connect the peripheral trigger */
	if (config->channel_direction == MEMORY_TO_MEMORY) {
		/*
		 * A single software trigger will start the
		 * transfer
		 */
		DMA_REGS->CHCTRLB.reg = DMAC_CHCTRLB_TRIGACT_TRANSACTION |
				    DMAC_CHCTRLB_TRIGSRC(config->dma_slot);
	} else {
		/* One peripheral trigger per beat */
		DMA_REGS->CHCTRLB.reg = DMAC_CHCTRLB_TRIGACT_BEAT |
				    DMAC_CHCTRLB_TRIGSRC(config->dma_slot);
	}

	/* Set the priority */
	if (config->channel_priority >= DMAC_LVL_NUM) {
		LOG_ERR("Invalid priority");
		goto inval;
	}

	DMA_REGS->CHCTRLB.bit.LVL = config->channel_priority;

	/* Enable the interrupts */
	DMA_REGS->CHINTENSET.reg = DMAC_CHINTENSET_TCMPL;
	if (!config->error_callback_en) {
		DMA_REGS->CHINTENSET.reg = DMAC_CHINTENSET_TERR;
	} else {
		DMA_REGS->CHINTENCLR.reg = DMAC_CHINTENSET_TERR;
	}

	DMA_REGS->CHINTFLAG.reg = DMAC_CHINTFLAG_TERR | DMAC_CHINTFLAG_TCMPL;
#else
	/* Channels have separate configuration registers */
	DmacChannel * chcfg = &DMA_REGS->Channel[channel];

	if (config->channel_direction == MEMORY_TO_MEMORY) {
		/*
		 * A single software trigger will start the
		 * transfer
		 */
		chcfg->CHCTRLA.reg = DMAC_CHCTRLA_TRIGACT_TRANSACTION |
				     DMAC_CHCTRLA_TRIGSRC(config->dma_slot);
	} else if ((config->channel_direction == MEMORY_TO_PERIPHERAL) ||
		(config->channel_direction == PERIPHERAL_TO_MEMORY)) {
		/* One peripheral trigger per beat */
		chcfg->CHCTRLA.reg = DMAC_CHCTRLA_TRIGACT_BURST |
				     DMAC_CHCTRLA_TRIGSRC(config->dma_slot);
	} else {
		LOG_ERR("Direction error. %d", config->channel_direction);
		goto inval;
	}

	/* Set the priority */
	if (config->channel_priority >= DMAC_LVL_NUM) {
		LOG_ERR("Invalid priority");
		goto inval;
	}

	chcfg->CHPRILVL.bit.PRILVL = config->channel_priority;

	/* Set the burst length */
	if (config->source_burst_length != config->dest_burst_length) {
		LOG_ERR("Source and destination burst lengths must be equal");
		goto inval;
	}

	if (config->source_burst_length > 16U) {
		LOG_ERR("Invalid burst length");
		goto inval;
	}

	if (config->source_burst_length > 0U) {
		chcfg->CHCTRLA.reg |= DMAC_CHCTRLA_BURSTLEN(
			config->source_burst_length - 1U);
	}

	/* Enable the interrupts */
	chcfg->CHINTENSET.reg = DMAC_CHINTENSET_TCMPL;
	if (!config->error_callback_en) {
		chcfg->CHINTENSET.reg = DMAC_CHINTENSET_TERR;
	} else {
		chcfg->CHINTENCLR.reg = DMAC_CHINTENSET_TERR;
	}

	chcfg->CHINTFLAG.reg = DMAC_CHINTFLAG_TERR | DMAC_CHINTFLAG_TCMPL;
#endif

	/* Set the beat (single transfer) size */
	if (config->source_data_size != config->dest_data_size) {
		LOG_ERR("Source and destination data sizes must be equal");
		goto inval;
	}

	switch (config->source_data_size) {
	case 1:
		btctrl.bit.BEATSIZE = DMAC_BTCTRL_BEATSIZE_BYTE_Val;
		break;
	case 2:
		btctrl.bit.BEATSIZE = DMAC_BTCTRL_BEATSIZE_HWORD_Val;
		break;
	case 4:
		btctrl.bit.BEATSIZE = DMAC_BTCTRL_BEATSIZE_WORD_Val;
		break;
	default:
		LOG_ERR("Invalid data size");
		goto inval;
	}

	/* Set up the one and only block */
	desc->BTCNT.reg = block->block_size / config->source_data_size;
	desc->DESCADDR.reg = 0;

	/* Set the automatic source / dest increment */
	switch (block->source_addr_adj) {
	case DMA_ADDR_ADJ_INCREMENT:
		desc->SRCADDR.reg = block->source_address + block->block_size;
		btctrl.bit.SRCINC = 1;
		break;
	case DMA_ADDR_ADJ_NO_CHANGE:
		desc->SRCADDR.reg = block->source_address;
		break;
	default:
		LOG_ERR("Invalid source increment");
		goto inval;
	}

	switch (block->dest_addr_adj) {
	case DMA_ADDR_ADJ_INCREMENT:
		desc->DSTADDR.reg = block->dest_address + block->block_size;
		btctrl.bit.DSTINC = 1;
		break;
	case DMA_ADDR_ADJ_NO_CHANGE:
		desc->DSTADDR.reg = block->dest_address;
		break;
	default:
		LOG_ERR("Invalid destination increment");
		goto inval;
	}

	btctrl.bit.VALID = 1;
	desc->BTCTRL = btctrl;

	channel_control = &data->channels[channel];
	channel_control->cb = config->dma_callback;
	channel_control->user_data = config->user_data;

	LOG_DBG("Configured channel %d for %08X to %08X (%u)",
		channel,
		block->source_address,
		block->dest_address,
		block->block_size);

	irq_unlock(key);
	return 0;

inval:
	irq_unlock(key);
	return -EINVAL;
}

static int dma_sam0_start(const struct device *dev, uint32_t channel)
{
	unsigned int key = irq_lock();

	ARG_UNUSED(dev);

#ifdef DMAC_CHID_ID
	DMA_REGS->CHID.reg = channel;
	DMA_REGS->CHCTRLA.reg = DMAC_CHCTRLA_ENABLE;

	if (DMA_REGS->CHCTRLB.bit.TRIGSRC == 0) {
		/* Trigger via software */
		DMA_REGS->SWTRIGCTRL.reg = 1U << channel;
	}

#else
	DmacChannel * chcfg = &DMA_REGS->Channel[channel];

	chcfg->CHCTRLA.bit.ENABLE = 1;

	if (chcfg->CHCTRLA.bit.TRIGSRC == 0) {
		/* Trigger via software */
		DMA_REGS->SWTRIGCTRL.reg = 1U << channel;
	}
#endif

	irq_unlock(key);

	return 0;
}

static int dma_sam0_stop(const struct device *dev, uint32_t channel)
{
	unsigned int key = irq_lock();

	ARG_UNUSED(dev);

#ifdef DMAC_CHID_ID
	DMA_REGS->CHID.reg = channel;
	DMA_REGS->CHCTRLA.reg = 0;
#else
	DmacChannel * chcfg = &DMA_REGS->Channel[channel];

	chcfg->CHCTRLA.bit.ENABLE = 0;
#endif

	irq_unlock(key);

	return 0;
}

static int dma_sam0_reload(const struct device *dev, uint32_t channel,
			   uint32_t src, uint32_t dst, size_t size)
{
	struct dma_sam0_data *data = dev->data;
	DmacDescriptor *desc = &data->descriptors[channel];
	unsigned int key = irq_lock();

	switch (desc->BTCTRL.bit.BEATSIZE) {
	case DMAC_BTCTRL_BEATSIZE_BYTE_Val:
		desc->BTCNT.reg = size;
		break;
	case DMAC_BTCTRL_BEATSIZE_HWORD_Val:
		desc->BTCNT.reg = size / 2U;
		break;
	case DMAC_BTCTRL_BEATSIZE_WORD_Val:
		desc->BTCNT.reg = size / 4U;
		break;
	default:
		goto inval;
	}

	if (desc->BTCTRL.bit.SRCINC) {
		desc->SRCADDR.reg = src + size;
	} else {
		desc->SRCADDR.reg = src;
	}

	if (desc->BTCTRL.bit.DSTINC) {
		desc->DSTADDR.reg = dst + size;
	} else {
		desc->DSTADDR.reg = dst;
	}

	LOG_DBG("Reloaded channel %d for %08X to %08X (%u)",
		channel, src, dst, size);

	irq_unlock(key);
	return 0;

inval:
	irq_unlock(key);
	return -EINVAL;
}

static int dma_sam0_get_status(const struct device *dev, uint32_t channel,
			       struct dma_status *stat)
{
	struct dma_sam0_data *data = dev->data;
	uint32_t act;

	if (channel >= DMAC_CH_NUM || stat == NULL) {
		return -EINVAL;
	}

	act = DMA_REGS->ACTIVE.reg;
	if ((act & DMAC_ACTIVE_ABUSY) &&
	    ((act & DMAC_ACTIVE_ID_Msk) >> DMAC_ACTIVE_ID_Pos) == channel) {
		stat->busy = true;
		stat->pending_length = (act & DMAC_ACTIVE_BTCNT_Msk) >>
				       DMAC_ACTIVE_BTCNT_Pos;
	} else {
		stat->busy = false;
		stat->pending_length = data->descriptors_wb[channel].BTCNT.reg;
	}

	switch (data->descriptors[channel].BTCTRL.bit.BEATSIZE) {
	case DMAC_BTCTRL_BEATSIZE_BYTE_Val:
		break;
	case DMAC_BTCTRL_BEATSIZE_HWORD_Val:
		stat->pending_length *= 2U;
		break;
	case DMAC_BTCTRL_BEATSIZE_WORD_Val:
		stat->pending_length *= 4U;
		break;
	default:
		return -EINVAL;
	}

	return 0;
}

#define DMA_SAM0_IRQ_CONNECT(n)						 \
	do {								 \
		IRQ_CONNECT(DT_INST_IRQ_BY_IDX(0, n, irq),		 \
			    DT_INST_IRQ_BY_IDX(0, n, priority),		 \
			    dma_sam0_isr, DEVICE_DT_INST_GET(0), 0);	 \
		irq_enable(DT_INST_IRQ_BY_IDX(0, n, irq));		 \
	} while (false)

static int dma_sam0_init(const struct device *dev)
{
	struct dma_sam0_data *data = dev->data;

	/* Enable clocks. */
#ifdef MCLK
	MCLK->AHBMASK.bit.DMAC_ = 1;
#else
	PM->AHBMASK.bit.DMAC_ = 1;
	PM->APBBMASK.bit.DMAC_ = 1;
#endif

	/* Set up the descriptor and write back addresses */
	DMA_REGS->BASEADDR.reg = (uintptr_t)&data->descriptors;
	DMA_REGS->WRBADDR.reg = (uintptr_t)&data->descriptors_wb;

	/* Statically map each level to the same numeric priority */
	DMA_REGS->PRICTRL0.reg =
		DMAC_PRICTRL0_LVLPRI0(0) | DMAC_PRICTRL0_LVLPRI1(1) |
		DMAC_PRICTRL0_LVLPRI2(2) | DMAC_PRICTRL0_LVLPRI3(3);

	/* Enable the unit and enable all priorities */
	DMA_REGS->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0x0F);

#if DT_INST_IRQ_HAS_CELL(0, irq)
	DMA_SAM0_IRQ_CONNECT(0);
#endif
#if DT_INST_IRQ_HAS_IDX(0, 1)
	DMA_SAM0_IRQ_CONNECT(1);
#endif
#if DT_INST_IRQ_HAS_IDX(0, 2)
	DMA_SAM0_IRQ_CONNECT(2);
#endif
#if DT_INST_IRQ_HAS_IDX(0, 3)
	DMA_SAM0_IRQ_CONNECT(3);
#endif
#if DT_INST_IRQ_HAS_IDX(0, 4)
	DMA_SAM0_IRQ_CONNECT(4);
#endif

	return 0;
}

static struct dma_sam0_data dmac_data;

static const struct dma_driver_api dma_sam0_api = {
	.config = dma_sam0_config,
	.start = dma_sam0_start,
	.stop = dma_sam0_stop,
	.reload = dma_sam0_reload,
	.get_status = dma_sam0_get_status,
};

DEVICE_DT_INST_DEFINE(0, &dma_sam0_init, NULL,
		    &dmac_data, NULL, PRE_KERNEL_1,
		    CONFIG_DMA_INIT_PRIORITY, &dma_sam0_api);
