/*
 * \file
 *
 * \brief SAM Direct Memory Access Controller Driver
 *
 * Copyright (c) 2014-2018 Microchip Technology Inc. and its subsidiaries.
 *
 * \asf_license_start
 *
 * \page License
 *
 * Subject to your compliance with these terms, you may use Microchip
 * software and any derivatives exclusively with Microchip products.
 * It is your responsibility to comply with third party license terms applicable
 * to your use of third party software (including open source software) that
 * may accompany Microchip software.
 *
 * THIS SOFTWARE IS SUPPLIED BY MICROCHIP "AS IS". NO WARRANTIES,
 * WHETHER EXPRESS, IMPLIED OR STATUTORY, APPLY TO THIS SOFTWARE,
 * INCLUDING ANY IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY,
 * AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT WILL MICROCHIP BE
 * LIABLE FOR ANY INDIRECT, SPECIAL, PUNITIVE, INCIDENTAL OR CONSEQUENTIAL
 * LOSS, DAMAGE, COST OR EXPENSE OF ANY KIND WHATSOEVER RELATED TO THE
 * SOFTWARE, HOWEVER CAUSED, EVEN IF MICROCHIP HAS BEEN ADVISED OF THE
 * POSSIBILITY OR THE DAMAGES ARE FORESEEABLE.  TO THE FULLEST EXTENT
 * ALLOWED BY LAW, MICROCHIP'S TOTAL LIABILITY ON ALL CLAIMS IN ANY WAY
 * RELATED TO THIS SOFTWARE WILL NOT EXCEED THE AMOUNT OF FEES, IF ANY,
 * THAT YOU HAVE PAID DIRECTLY TO MICROCHIP FOR THIS SOFTWARE.
 *
 * \asf_license_stop
 *
 */
/*
 * Support and FAQ: visit <a href="https://www.microchip.com/support/">Microchip Support</a>
 */

#include <string.h>
#include "dma.h"
#include "clock.h"
#include "system_interrupt.h"

struct _dma_module {
	volatile bool _dma_init;
	volatile uint32_t allocated_channels;
	uint8_t free_channels;
};

struct _dma_module _dma_inst = {
	._dma_init = false,
	.allocated_channels = 0,
	.free_channels = CONF_MAX_USED_CHANNEL_NUM,
};

/** Maximum retry counter for resuming a job transfer. */
#define MAX_JOB_RESUME_COUNT    10000

/** DMA channel mask. */
#define DMA_CHANNEL_MASK   (0x1f)

COMPILER_ALIGNED(16)
DmacDescriptor descriptor_section[CONF_MAX_USED_CHANNEL_NUM] SECTION_DMAC_DESCRIPTOR;

/** Initial write back memory section. */
COMPILER_ALIGNED(16)
static DmacDescriptor _write_back_section[CONF_MAX_USED_CHANNEL_NUM] SECTION_DMAC_DESCRIPTOR;

/** Internal DMA resource pool. */
static struct dma_resource* _dma_active_resource[CONF_MAX_USED_CHANNEL_NUM];

/* DMA channel interrup flag. */
uint8_t g_chan_interrupt_flag[CONF_MAX_USED_CHANNEL_NUM]={0};

/**
 * \brief Find a free channel for a DMA resource.
 *
 * Find a channel for the requested DMA resource.
 *
 * \return Status of channel allocation.
 * \retval DMA_INVALID_CHANNEL  No channel available
 * \retval count          Allocated channel for the DMA resource
 */
static uint8_t _dma_find_first_free_channel_and_allocate(void)
{
	uint8_t count;
	uint32_t tmp;
	bool allocated = false;

	system_interrupt_enter_critical_section();

	tmp = _dma_inst.allocated_channels;

	for (count = 0; count < CONF_MAX_USED_CHANNEL_NUM; ++count) {
		if (!(tmp & 0x00000001)) {
			/* If free channel found, set as allocated and return
			 *number */

			_dma_inst.allocated_channels |= 1 << count;
			_dma_inst.free_channels--;
			allocated = true;

			break;
		}

		tmp = tmp >> 1;
	}

	system_interrupt_leave_critical_section();

	if (!allocated) {
		return DMA_INVALID_CHANNEL;
	} else {
		return count;
	}
}

/**
 * \brief Release an allocated DMA channel.
 *
 * \param[in]  channel  Channel id to be released
 *
 */
static void _dma_release_channel(uint8_t channel)
{
	_dma_inst.allocated_channels &= ~(1 << channel);
	_dma_inst.free_channels++;
}

/**
 * \brief Configure the DMA resource.
 *
 * \param[in]  dma_resource Pointer to a DMA resource instance
 * \param[out] resource_config Configurations of the DMA resource
 *
 */
static void _dma_set_config(struct dma_resource *resource,
		struct dma_resource_config *resource_config)
{
	Assert(resource);
	Assert(resource_config);
	uint32_t temp_CHCTRLB_reg;
	system_interrupt_enter_critical_section();

	/** Select the DMA channel and clear software trigger */
	DMAC->CHID.reg = DMAC_CHID_ID(resource->channel_id);
	DMAC->SWTRIGCTRL.reg &= (uint32_t)(~(1 << resource->channel_id));

	temp_CHCTRLB_reg = DMAC_CHCTRLB_LVL(resource_config->priority) | \
			DMAC_CHCTRLB_TRIGSRC(resource_config->peripheral_trigger) | \
			DMAC_CHCTRLB_TRIGACT(resource_config->trigger_action);


	if(resource_config->event_config.input_action){
	temp_CHCTRLB_reg |= DMAC_CHCTRLB_EVIE | DMAC_CHCTRLB_EVACT(
				resource_config->event_config.input_action);
	}

	/** Enable event output, the event output selection is configured in
	 * each transfer descriptor  */
	if (resource_config->event_config.event_output_enable) {
		temp_CHCTRLB_reg |= DMAC_CHCTRLB_EVOE;
	}

	/* Write config to CTRLB register */
	DMAC->CHCTRLB.reg = temp_CHCTRLB_reg;



	system_interrupt_leave_critical_section();
}

/**
 * \brief DMA interrupt service routine.
 *
 */
void DMAC_Handler( void )
{
	uint8_t active_channel;
	struct dma_resource *resource;
	uint8_t isr;
	uint32_t write_size;
	uint32_t total_size;

	system_interrupt_enter_critical_section();

	/* Get Pending channel */
	active_channel =  DMAC->INTPEND.reg & DMAC_INTPEND_ID_Msk;

	Assert(_dma_active_resource[active_channel]);

	/* Get active DMA resource based on channel */
	resource = _dma_active_resource[active_channel];

	/* Select the active channel */
	DMAC->CHID.reg = DMAC_CHID_ID(resource->channel_id);
	isr = DMAC->CHINTFLAG.reg;

	/* Calculate block transfer size of the DMA transfer */
	total_size = descriptor_section[resource->channel_id].BTCNT.reg;
	write_size = _write_back_section[resource->channel_id].BTCNT.reg;
	resource->transfered_size = total_size - write_size;

	/* DMA channel interrupt handler */
	if (isr & DMAC_CHINTENCLR_TERR) {
		/* Clear transfer error flag */
		DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_TERR;

		/* Set I/O ERROR status */
		resource->job_status = STATUS_ERR_IO;

		/* Execute the callback function */
		if ((resource->callback_enable & (1<<DMA_CALLBACK_TRANSFER_ERROR)) &&
				(resource->callback[DMA_CALLBACK_TRANSFER_ERROR])) {
			resource->callback[DMA_CALLBACK_TRANSFER_ERROR](resource);
		}
	} else if (isr & DMAC_CHINTENCLR_TCMPL) {
		/* Clear the transfer complete flag */
		DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_TCMPL;

		/* Set job status */
		resource->job_status = STATUS_OK;

		/* Execute the callback function */
		if ((resource->callback_enable & (1 << DMA_CALLBACK_TRANSFER_DONE)) &&
				(resource->callback[DMA_CALLBACK_TRANSFER_DONE])) {
			resource->callback[DMA_CALLBACK_TRANSFER_DONE](resource);
		}
	} else if (isr & DMAC_CHINTENCLR_SUSP) {
		/* Clear channel suspend flag */
		DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_SUSP;

		/* Set job status */
		resource->job_status = STATUS_SUSPEND;

		/* Execute the callback function */
		if ((resource->callback_enable & (1 << DMA_CALLBACK_CHANNEL_SUSPEND)) &&
			(resource->callback[DMA_CALLBACK_CHANNEL_SUSPEND])){
			resource->callback[DMA_CALLBACK_CHANNEL_SUSPEND](resource);
		}
	}

	system_interrupt_leave_critical_section();
}

/**
 * \brief Initializes config with predefined default values.
 *
 * This function will initialize a given DMA configuration structure to
 * a set of known default values. This function should be called on
 * any new instance of the configuration structure before being
 * modified by the user application.
 *
 * The default configuration is as follows:
 *  \li Software trigger is used as the transfer trigger
 *  \li Priority level 0
 *  \li Only software/event trigger
 *  \li Requires a trigger for each transaction
 *  \li No event input /output
 *  \li DMA channel is disabled during sleep mode (if has the feature)
 * \param[out] config Pointer to the configuration
 *
 */
void dma_get_config_defaults(struct dma_resource_config *config)
{
	Assert(config);
	/* Set as priority 0 */
	config->priority = DMA_PRIORITY_LEVEL_0;
	/* Only software/event trigger */
	config->peripheral_trigger = 0;
	/* Transaction trigger */
	config->trigger_action = DMA_TRIGGER_ACTION_TRANSACTION;

	/* Event configurations, no event input/output */
	config->event_config.input_action = DMA_EVENT_INPUT_NOACT;
	config->event_config.event_output_enable = false;
#ifdef FEATURE_DMA_CHANNEL_STANDBY
	config->run_in_standby = false;
#endif
}

/**
 * \brief Allocate a DMA with configurations.
 *
 * This function will allocate a proper channel for a DMA transfer request.
 *
 * \param[in,out]  dma_resource Pointer to a DMA resource instance
 * \param[in] transfer_config Configurations of the DMA transfer
 *
 * \return Status of the allocation procedure.
 *
 * \retval STATUS_OK The DMA resource was allocated successfully
 * \retval STATUS_ERR_NOT_FOUND DMA resource allocation failed
 */
enum status_code dma_allocate(struct dma_resource *resource,
		struct dma_resource_config *config)
{
	uint8_t new_channel;

	Assert(resource);

	system_interrupt_enter_critical_section();

	if (!_dma_inst._dma_init) {
		/* Initialize clocks for DMA */
#if (SAML21) || (SAML22) || (SAMC20) || (SAMC21) || (SAMR30) || (SAMR34) || (SAMR35)
		system_ahb_clock_set_mask(MCLK_AHBMASK_DMAC);
#else
		system_ahb_clock_set_mask(PM_AHBMASK_DMAC);
		system_apb_clock_set_mask(SYSTEM_CLOCK_APB_APBB,
				PM_APBBMASK_DMAC);
#endif

		/* Perform a software reset before enable DMA controller */
		DMAC->CTRL.reg &= ~DMAC_CTRL_DMAENABLE;
		DMAC->CTRL.reg = DMAC_CTRL_SWRST;

		/* Setup descriptor base address and write back section base
		 * address */
		DMAC->BASEADDR.reg = (uint32_t)descriptor_section;
		DMAC->WRBADDR.reg = (uint32_t)_write_back_section;

		/* Enable all priority level at the same time */
		DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf);

		_dma_inst._dma_init = true;
	}

	/* Find the proper channel */
	new_channel = _dma_find_first_free_channel_and_allocate();

	/* If no channel available, return not found */
	if (new_channel == DMA_INVALID_CHANNEL) {
		system_interrupt_leave_critical_section();

		return STATUS_ERR_NOT_FOUND;
	}

	/* Set the channel */
	resource->channel_id = new_channel;

	/** Perform a reset for the allocated channel */
	DMAC->CHID.reg = DMAC_CHID_ID(resource->channel_id);
	DMAC->CHCTRLA.reg &= ~DMAC_CHCTRLA_ENABLE;
	DMAC->CHCTRLA.reg = DMAC_CHCTRLA_SWRST;

#ifdef FEATURE_DMA_CHANNEL_STANDBY
	if(config->run_in_standby){
		DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_RUNSTDBY;
	}
#endif

	/** Configure the DMA control,channel registers and descriptors here */
	_dma_set_config(resource, config);

	resource->descriptor = NULL;

	/* Log the DMA resource into the internal DMA resource pool */
	_dma_active_resource[resource->channel_id] = resource;

	system_interrupt_leave_critical_section();

	return STATUS_OK;
}

/**
 * \brief Free an allocated DMA resource.
 *
 * This function will free an allocated DMA resource.
 *
 * \param[in,out] resource Pointer to the DMA resource
 *
 * \return Status of the free procedure.
 *
 * \retval STATUS_OK The DMA resource was freed successfully
 * \retval STATUS_BUSY The DMA resource was busy and can't be freed
 * \retval STATUS_ERR_NOT_INITIALIZED DMA resource was not initialized
 */
enum status_code dma_free(struct dma_resource *resource)
{
	Assert(resource);
	Assert(resource->channel_id != DMA_INVALID_CHANNEL);

	system_interrupt_enter_critical_section();

	/* Check if channel is busy */
	if (dma_is_busy(resource)) {
		system_interrupt_leave_critical_section();
		return STATUS_BUSY;
	}

	/* Check if DMA resource was not allocated */
	if (!(_dma_inst.allocated_channels & (1 << resource->channel_id))) {
		system_interrupt_leave_critical_section();
		return STATUS_ERR_NOT_INITIALIZED;
	}

	/* Release the DMA resource */
	_dma_release_channel(resource->channel_id);

	/* Reset the item in the DMA resource pool */
	_dma_active_resource[resource->channel_id] = NULL;

	system_interrupt_leave_critical_section();

	return STATUS_OK;
}

/**
 * \brief Start a DMA transfer.
 *
 * This function will start a DMA transfer through an allocated DMA resource.
 *
 * \param[in,out] resource Pointer to the DMA resource
 *
 * \return Status of the transfer start procedure.
 *
 * \retval STATUS_OK The transfer was started successfully
 * \retval STATUS_BUSY The DMA resource was busy and the transfer was not started
 * \retval STATUS_ERR_INVALID_ARG Transfer size is 0 and transfer was not started
 */
enum status_code dma_start_transfer_job(struct dma_resource *resource)
{
	Assert(resource);
	Assert(resource->channel_id != DMA_INVALID_CHANNEL);

	system_interrupt_enter_critical_section();

	/* Check if resource was busy */
	if (resource->job_status == STATUS_BUSY) {
		system_interrupt_leave_critical_section();
		return STATUS_BUSY;
	}

	/* Check if transfer size is valid */
	if (resource->descriptor->BTCNT.reg == 0) {
		system_interrupt_leave_critical_section();
		return STATUS_ERR_INVALID_ARG;
	}

	/* Enable DMA interrupt */
	system_interrupt_enable(SYSTEM_INTERRUPT_MODULE_DMA);

	/* Set the interrupt flag */
	DMAC->CHID.reg = DMAC_CHID_ID(resource->channel_id);
	DMAC->CHINTENSET.reg = (DMAC_CHINTENSET_MASK & g_chan_interrupt_flag[resource->channel_id]);
	/* Set job status */
	resource->job_status = STATUS_BUSY;

	/* Set channel x descriptor 0 to the descriptor base address */
	memcpy(&descriptor_section[resource->channel_id], resource->descriptor,
						sizeof(DmacDescriptor));

	/* Enable the transfer channel */
	DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE;

	system_interrupt_leave_critical_section();

	return STATUS_OK;
}

/**
 * \brief Abort a DMA transfer.
 *
 * This function will abort a DMA transfer. The DMA channel used for the DMA
 * resource will be disabled.
 * The block transfer count will also be calculated and written to the DMA
 * resource structure.
 *
 * \note The DMA resource will not be freed after calling this function.
 *       The function \ref dma_free() can be used to free an allocated resource.
 *
 * \param[in,out] resource Pointer to the DMA resource
 *
 */
void dma_abort_job(struct dma_resource *resource)
{
	uint32_t write_size;
	uint32_t total_size;

	Assert(resource);
	Assert(resource->channel_id != DMA_INVALID_CHANNEL);

	system_interrupt_enter_critical_section();

	DMAC->CHID.reg = DMAC_CHID_ID(resource->channel_id);
	DMAC->CHCTRLA.reg = 0;

	system_interrupt_leave_critical_section();

	/* Get transferred size */
	total_size = descriptor_section[resource->channel_id].BTCNT.reg;
	write_size = _write_back_section[resource->channel_id].BTCNT.reg;
	resource->transfered_size = total_size - write_size;

	resource->job_status = STATUS_ABORTED;
}

/**
 * \brief Suspend a DMA transfer.
 *
 * This function will request to suspend the transfer of the DMA resource.
 * The channel is kept enabled, can receive transfer triggers (the transfer
 * pending bit will be set), but will be removed from the arbitration scheme.
 * The channel operation can be resumed by calling \ref dma_resume_job().
 *
 * \note This function sets the command to suspend the DMA channel
 * associated with a DMA resource. The channel suspend interrupt flag
 * indicates whether the transfer is truly suspended.
 *
 * \param[in] resource Pointer to the DMA resource
 *
 */
void dma_suspend_job(struct dma_resource *resource)
{
	Assert(resource);
	Assert(resource->channel_id != DMA_INVALID_CHANNEL);

	system_interrupt_enter_critical_section();

	/* Select the channel */
	DMAC->CHID.reg = DMAC_CHID_ID(resource->channel_id);

	/* Send the suspend request */
	DMAC->CHCTRLB.reg |= DMAC_CHCTRLB_CMD_SUSPEND;

	system_interrupt_leave_critical_section();
}

/**
 * \brief Resume a suspended DMA transfer.
 *
 * This function try to resume a suspended transfer of a DMA resource.
 *
 * \param[in] resource Pointer to the DMA resource
 *
 */
void dma_resume_job(struct dma_resource *resource)
{
	uint32_t bitmap_channel;
	uint32_t count = 0;

	Assert(resource);
	Assert(resource->channel_id != DMA_INVALID_CHANNEL);

	/* Get bitmap of the allocated DMA channel */
	bitmap_channel = (1 << resource->channel_id);

	/* Check if channel was suspended */
	if (resource->job_status != STATUS_SUSPEND) {
		return;
	}

	system_interrupt_enter_critical_section();

	/* Send resume request */
	DMAC->CHID.reg = DMAC_CHID_ID(resource->channel_id);
	DMAC->CHCTRLB.reg |= DMAC_CHCTRLB_CMD_RESUME;

	system_interrupt_leave_critical_section();

	/* Check if transfer job resumed */
	for (count = 0; count < MAX_JOB_RESUME_COUNT; count++) {
		if ((DMAC->BUSYCH.reg & bitmap_channel) == bitmap_channel) {
			break;
		}
	}

	if (count < MAX_JOB_RESUME_COUNT) {
		/* Job resumed */
		resource->job_status = STATUS_BUSY;
	} else {
		/* Job resume timeout */
		resource->job_status = STATUS_ERR_TIMEOUT;
	}
}

/**
 * \brief Create a DMA transfer descriptor with configurations.
 *
 * This function will set the transfer configurations to the DMA transfer
 * descriptor.
 *
 * \param[in] descriptor Pointer to the DMA transfer descriptor
 * \param[in] config Pointer to the descriptor configuration structure
 *
 */
void dma_descriptor_create(DmacDescriptor* descriptor,
	struct dma_descriptor_config *config)
{
	/* Set block transfer control */
	descriptor->BTCTRL.bit.VALID = config->descriptor_valid;
	descriptor->BTCTRL.bit.EVOSEL = config->event_output_selection;
	descriptor->BTCTRL.bit.BLOCKACT = config->block_action;
	descriptor->BTCTRL.bit.BEATSIZE = config->beat_size;
	descriptor->BTCTRL.bit.SRCINC = config->src_increment_enable;
	descriptor->BTCTRL.bit.DSTINC = config->dst_increment_enable;
	descriptor->BTCTRL.bit.STEPSEL = config->step_selection;
	descriptor->BTCTRL.bit.STEPSIZE = config->step_size;

	/* Set transfer size, source address and destination address */
	descriptor->BTCNT.reg = config->block_transfer_count;
	descriptor->SRCADDR.reg = config->source_address;
	descriptor->DSTADDR.reg = config->destination_address;

	/* Set next transfer descriptor address */
	descriptor->DESCADDR.reg = config->next_descriptor_address;
}

/**
 * \brief Add a DMA transfer descriptor to a DMA resource.
 *
 * This function will add a DMA transfer descriptor to a DMA resource.
 * If there was a transfer descriptor already allocated to the DMA resource,
 * the descriptor will be linked to the next descriptor address.
 *
 * \param[in] resource Pointer to the DMA resource
 * \param[in] descriptor Pointer to the transfer descriptor
 *
 * \retval STATUS_OK The descriptor is added to the DMA resource
 * \retval STATUS_BUSY The DMA resource was busy and the descriptor is not added
 */
enum status_code dma_add_descriptor(struct dma_resource *resource,
		DmacDescriptor* descriptor)
{
	DmacDescriptor* desc = resource->descriptor;

	if (resource->job_status == STATUS_BUSY) {
		return STATUS_BUSY;
	}

	/* Look up for an empty space for the descriptor */
	if (desc == NULL) {
		resource->descriptor = descriptor;
	} else {
		/* Looking for end of descriptor link */
		while(desc->DESCADDR.reg != 0) {
			desc = (DmacDescriptor*)(desc->DESCADDR.reg);
		}

		/* Set to the end of descriptor list */
		desc->DESCADDR.reg = (uint32_t)descriptor;
	}

	return STATUS_OK;
}
