/*
 * Copyright (c) 2020-2023 NXP
 *
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @brief Common part of DMA drivers for some NXP SoC.
 */
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <soc.h>
#include <zephyr/drivers/dma.h>
#include <fsl_dma.h>
#include <fsl_inputmux.h>
#if defined(FSL_FEATURE_MEMORY_HAS_ADDRESS_OFFSET) && (FSL_FEATURE_MEMORY_HAS_ADDRESS_OFFSET == 1)
#include "fsl_memory.h"
#endif
#include <zephyr/logging/log.h>
#include <zephyr/irq.h>
#include <zephyr/sys/barrier.h>
#include <zephyr/sys/util.h>
#include <zephyr/sys/util_macro.h>
#include <zephyr/drivers/dma/dma_mcux_lpc.h>
#include <zephyr/pm/device.h>
#include <zephyr/pm/policy.h>

#define DT_DRV_COMPAT nxp_lpc_dma

LOG_MODULE_REGISTER(dma_mcux_lpc, CONFIG_DMA_LOG_LEVEL);

#if CONFIG_PM_DEVICE
/*
 * Data structures to backup DMA registers when the
 * register content lost in low power modes.
 */
struct dma_backup_reg {
	/*
	 * Backup the control registers.
	 * Don't need to backup CTRL and SRAMBASE, they
	 * are configured in function DMA_Init.
	 */
	uint32_t enableset;	/* Register ENABLESET */
	uint32_t intenset;	/* Register INTENSET */
};

struct dma_ch_backup_reg {
	/* Don't need to backup status register CTLSTAT. */
	uint32_t cfg;		/* Register CFG */
	uint32_t xfercfg;	/* Register XFERCFG */
};
#endif /* CONFIG_PM_DEVICE */

struct dma_mcux_lpc_config {
	DMA_Type *base;
	uint32_t otrig_base_address;
	uint32_t itrig_base_address;
	uint8_t num_of_channels;
	uint8_t num_of_allocated_channels;
	uint8_t num_of_otrigs;
	void (*irq_config_func)(const struct device *dev);
};

struct channel_data {
	SDK_ALIGN(dma_descriptor_t dma_descriptor_table[CONFIG_DMA_MCUX_LPC_NUMBER_OF_DESCRIPTORS],
		  FSL_FEATURE_DMA_LINK_DESCRIPTOR_ALIGN_SIZE);
	dma_handle_t dma_handle;
	const struct device *dev;
	void *user_data;
	dma_callback_t dma_callback;
	dma_descriptor_t *curr_descriptor;
	uint32_t width;
	enum dma_channel_direction dir;
	uint8_t src_inc;
	uint8_t dst_inc;
	uint8_t num_of_descriptors;
	bool descriptors_queued;
	bool busy;
#if CONFIG_PM_DEVICE
	struct dma_ch_backup_reg backup_reg;
#endif /* CONFIG_PM_DEVICE */
};

struct dma_otrig {
	int8_t source_channel;
	int8_t linked_channel;
};

struct dma_mcux_lpc_dma_data {
	struct dma_context ctx;

	struct channel_data *channel_data;
	struct dma_otrig *otrig_array;
	int8_t *channel_index;
	uint8_t num_channels_used;
#if CONFIG_PM_DEVICE
	struct dma_backup_reg backup_reg;
#endif /* CONFIG_PM_DEVICE */
};

struct k_spinlock configuring_otrigs;

#define NXP_LPC_DMA_MAX_XFER ((DMA_CHANNEL_XFERCFG_XFERCOUNT_MASK >> \
			      DMA_CHANNEL_XFERCFG_XFERCOUNT_SHIFT) + 1)

#define DEV_BASE(dev) \
	((DMA_Type *)((const struct dma_mcux_lpc_config *const)(dev)->config)->base)

#define DEV_CHANNEL_DATA(dev, ch)                                              \
	((struct channel_data *)(&(((struct dma_mcux_lpc_dma_data *)dev->data)->channel_data[ch])))

#define DEV_DMA_HANDLE(dev, ch)                                               \
		((dma_handle_t *)(&(DEV_CHANNEL_DATA(dev, ch)->dma_handle)))

#define EMPTY_OTRIG -1

static void nxp_lpc_dma_callback(dma_handle_t *handle, void *param,
			      bool transferDone, uint32_t intmode)
{
	int ret = -EIO;
	struct channel_data *data = (struct channel_data *)param;
	uint32_t channel = handle->channel;

	if (intmode == kDMA_IntError) {
		DMA_AbortTransfer(handle);
		data->busy = false;
	} else if (intmode == kDMA_IntA) {
		/* this is interrupt for a block that is not the
		 * last so leave busy flag set.
		 */
		ret = DMA_STATUS_BLOCK;
	} else {
		/* this is interrupt for end of a block list
		 * or a single transfer.
		 */
		data->busy = false;
		ret = DMA_STATUS_COMPLETE;
	}

	if (!data->busy) {
		pm_policy_device_power_lock_put(data->dev);
	}

	if (data->dma_callback) {
		data->dma_callback(data->dev, data->user_data, channel, ret);
	}
}

/* Handles DMA interrupts and dispatches to the individual channel */
static void dma_mcux_lpc_irq_handler(const struct device *dev)
{
	DMA_IRQHandle(DEV_BASE(dev));
/*
 * Add for ARM errata 838869, affects Cortex-M4, Cortex-M4F Store
 * immediate overlapping exception return operation might vector
 * to incorrect interrupt
 */
#if defined __CORTEX_M && (__CORTEX_M == 4U)
	barrier_dsync_fence_full();
#endif
}

#ifdef CONFIG_SOC_SERIES_RW6XX
static inline void rw6xx_dma_addr_fixup(struct dma_block_config *block)
{
	/* RW6xx AHB design does not route DMA engine through FlexSPI CACHE.
	 * Therefore, to use DMA from the FlexSPI space we must adjust the
	 * source address to use the non cached FlexSPI region.
	 * FlexSPI cached region is at 0x800_0000 (nonsecure) or 0x1800_0000
	 * (secure). We move the address into non cached region, which is at
	 * 0x4800_0000 or 0x5800_000.
	 */
	if (((block->source_address & 0xF8000000) == 0x18000000) ||
	  ((block->source_address & 0xF8000000) == 0x8000000)) {
		block->source_address = block->source_address + 0x40000000;
	}
	if (((block->dest_address & 0xF8000000) == 0x18000000) ||
	  ((block->dest_address & 0xF8000000) == 0x8000000)) {
		block->dest_address = block->dest_address + 0x40000000;
	}

}
#endif

static int dma_mcux_lpc_queue_descriptors(struct channel_data *data,
					   struct dma_block_config *block,
					   uint8_t src_inc,
					   uint8_t dest_inc,
					   bool callback_en)
{
	uint32_t xfer_config = 0U;
	dma_descriptor_t *next_descriptor = NULL;
	uint32_t width = data->width;
	uint32_t max_xfer_bytes = NXP_LPC_DMA_MAX_XFER * width;
	bool setup_extra_descriptor = false;
	/* intA is used to indicate transfer of a block */
	uint8_t enable_a_interrupt;
	/* intB is used to indicate complete transfer of the list of blocks */
	uint8_t enable_b_interrupt;
	uint8_t reload;
	struct dma_block_config local_block;
	bool last_block = false;

	memcpy(&local_block, block, sizeof(struct dma_block_config));

	do {
		/* Descriptors are queued during dma_configure, do not add more
		 * during dma_reload.
		 */
		if (!data->descriptors_queued) {
			/* Increase the number of descriptors queued */
			data->num_of_descriptors++;

			if (data->num_of_descriptors >= CONFIG_DMA_MCUX_LPC_NUMBER_OF_DESCRIPTORS) {
				return -ENOMEM;
			}
			/* Do we need to queue additional DMA descriptors for this block */
			if ((local_block.block_size > max_xfer_bytes) ||
			    (local_block.next_block != NULL)) {
				/* Allocate DMA descriptors */
				next_descriptor =
					&data->dma_descriptor_table[data->num_of_descriptors];
			} else {
				/* Check if this is the last block to transfer */
				if (local_block.next_block == NULL) {
					last_block = true;
					/* Last descriptor, check if we should setup a
					 * circular chain
					 */
					if (!local_block.source_reload_en) {
						/* No more descriptors */
						next_descriptor = NULL;
					} else if (data->num_of_descriptors == 1) {
						/* Allocate one more descriptors for
						 * ping-pong transfer
						 */
						next_descriptor = &data->dma_descriptor_table[
							data->num_of_descriptors];

						setup_extra_descriptor = true;
					} else {
						/* Loop back to the head */
						next_descriptor = data->dma_descriptor_table;
					}
				}
			}
		} else {
			/* Descriptors have already been allocated, reuse them as this
			 * is called from a reload function
			 */
			next_descriptor = data->curr_descriptor->linkToNextDesc;
			/* The SDK converts next descriptor addresses to DMA's
			 * address space when linking the descriptors, so
			 * convert it back.
			 */
#if defined(FSL_FEATURE_MEMORY_HAS_ADDRESS_OFFSET) && (FSL_FEATURE_MEMORY_HAS_ADDRESS_OFFSET == 1)
			next_descriptor = (void *)MEMORY_ConvertMemoryMapAddress(
				(uint32_t)next_descriptor, kMEMORY_DMA2Local);
#endif
		}

		/* SPI TX transfers need to queue a DMA descriptor to
		 * indicate an end of transfer. Source or destination
		 * address does not need to be change for these
		 * transactions and the transfer width is 4 bytes
		 */
		if ((data->dir == LPC_DMA_SPI_MCUX_FLEXCOMM_TX) &&
		    (local_block.block_size == sizeof(uint32_t)) &&
		    (local_block.next_block == NULL)) {
			src_inc = 0;
			dest_inc = 0;
			width = sizeof(uint32_t);
		}

		/* Fire an interrupt after the whole block has been transferred */
		if (local_block.block_size > max_xfer_bytes) {
			enable_a_interrupt = 0;
			enable_b_interrupt = 0;
		} else {
			/* Use intB when this is the end of the block list and transfer */
			if (last_block && !setup_extra_descriptor) {
				enable_a_interrupt = 0;
				enable_b_interrupt = 1;
			} else {
				/* Use intA when we need an interrupt per block
				 * Enable or disable intA based on user configuration
				 */
				enable_a_interrupt = callback_en;
				enable_b_interrupt = 0;
			}
		}

		/* Reload if we have more descriptors */
		if (next_descriptor) {
			reload = 1;
		} else {
			reload = 0;
		}

		/* Enable interrupt and reload for the descriptor */
		xfer_config = DMA_CHANNEL_XFER(reload, 0UL, enable_a_interrupt,
					enable_b_interrupt,
					width,
					src_inc,
					dest_inc,
					MIN(local_block.block_size, max_xfer_bytes));

#ifdef CONFIG_SOC_SERIES_RW6XX
		rw6xx_dma_addr_fixup(&local_block);
#endif
		DMA_SetupDescriptor(data->curr_descriptor,
				xfer_config,
				(void *)local_block.source_address,
				(void *)local_block.dest_address,
				(void *)next_descriptor);

		data->curr_descriptor = next_descriptor;

		if (local_block.block_size > max_xfer_bytes) {
			local_block.block_size -= max_xfer_bytes;
			if (src_inc) {
				local_block.source_address += max_xfer_bytes;
			}
			if (dest_inc) {
				local_block.dest_address += max_xfer_bytes;
			}
		} else {
			local_block.block_size = 0;
		}
	} while (local_block.block_size > 0);

	/* If an additional descriptor is queued for a certain case, set it up here.
	 */
	if (setup_extra_descriptor) {
		/* Increase the number of descriptors queued */
		data->num_of_descriptors++;

		/* Loop back to the head */
		next_descriptor = data->dma_descriptor_table;

		/* Leave curr pointer unchanged so we start queuing new data from
		 * this descriptor
		 */
		/* Enable or disable interrupt based on user request.
		 * Reload for the descriptor.
		 */
		xfer_config = DMA_CHANNEL_XFER(1UL, 0UL, callback_en, 0U,
					width,
					src_inc,
					dest_inc,
					MIN(local_block.block_size, max_xfer_bytes));
		/* Mark this as invalid */
		xfer_config &= ~DMA_CHANNEL_XFERCFG_CFGVALID_MASK;
#ifdef CONFIG_SOC_SERIES_RW6XX
		rw6xx_dma_addr_fixup(&local_block);
#endif
		DMA_SetupDescriptor(data->curr_descriptor,
				xfer_config,
				(void *)local_block.source_address,
				(void *)local_block.dest_address,
				(void *)next_descriptor);
	}

	return 0;
}

static void dma_mcux_lpc_clear_channel_data(struct channel_data *data)
{
	data->dma_callback = NULL;
	data->dir = 0;
	data->src_inc = 0;
	data->dst_inc = 0;
	data->descriptors_queued = false;
	data->num_of_descriptors = 0;
	data->curr_descriptor = NULL;
	data->width = 0;
}

static int get_block_increments(const struct dma_config *config,
				struct dma_block_config *block_config, uint8_t *src_inc_p,
				uint8_t *dst_inc_p)
{
	uint8_t src_inc = 1;
	uint8_t dst_inc = 1;

	uint8_t width = config->dest_data_size;

	if ((block_config->source_addr_adj == DMA_ADDR_ADJ_DECREMENT) ||
	    (block_config->dest_addr_adj == DMA_ADDR_ADJ_DECREMENT)) {
		LOG_ERR("DMA_ADDR_ADJ_DECREMENT not supported");
		return -EINVAL;
	}

	switch (config->channel_direction) {
	case MEMORY_TO_MEMORY:
	case HOST_TO_MEMORY:
	case MEMORY_TO_HOST:
		if (block_config->source_gather_en && (block_config->source_gather_interval != 0)) {
			src_inc = block_config->source_gather_interval / width;
			/* The current controller only supports incrementing the
			 * source and destination up to 4 time transfer width
			 */
			if ((src_inc > 4) || (src_inc == 3)) {
				return -EINVAL;
			}
		}

		if (block_config->dest_scatter_en && (block_config->dest_scatter_interval != 0)) {
			dst_inc = block_config->dest_scatter_interval / width;
			/* The current controller only supports incrementing the
			 * source and destination up to 4 time transfer width
			 */
			if ((dst_inc > 4) || (dst_inc == 3)) {
				return -EINVAL;
			}
		}
		break;
	case LPC_DMA_SPI_MCUX_FLEXCOMM_TX:
	case MEMORY_TO_PERIPHERAL:
		/* Set the source increment value */
		if (block_config->source_gather_en) {
			src_inc = block_config->source_gather_interval / width;
			/* The current controller only supports incrementing the
			 * source and destination up to 4 time transfer width
			 */
			if ((src_inc > 4) || (src_inc == 3)) {
				return -EINVAL;
			}
		}

		dst_inc = 0;
		if (block_config->dest_addr_adj != DMA_ADDR_ADJ_NO_CHANGE) {
			LOG_ERR("DMA to peripheral must set DMA_ADDR_ADJ_NO_CHANGE");
			return -EINVAL;
		}
		break;
	case PERIPHERAL_TO_MEMORY:
		src_inc = 0;
		if (block_config->source_addr_adj != DMA_ADDR_ADJ_NO_CHANGE) {
			LOG_ERR("DMA from peripheral must set DMA_ADDR_ADJ_NO_CHANGE");
			return -EINVAL;
		}

		/* Set the destination increment value */
		if (block_config->dest_scatter_en) {
			dst_inc = block_config->dest_scatter_interval / width;
			/* The current controller only supports incrementing the
			 * source and destination up to 4 time transfer width
			 */
			if ((dst_inc > 4) || (dst_inc == 3)) {
				return -EINVAL;
			}
		}
		break;
	default:
		LOG_ERR("not support transfer direction");
		return -EINVAL;
	}

	/* Check if user does not want to increment address */
	if (block_config->source_addr_adj == DMA_ADDR_ADJ_NO_CHANGE) {
		src_inc = 0;
	}
	if (block_config->dest_addr_adj == DMA_ADDR_ADJ_NO_CHANGE) {
		dst_inc = 0;
	}

	*src_inc_p = src_inc;
	*dst_inc_p = dst_inc;

	return 0;
}

/* Configure a channel */
static int dma_mcux_lpc_configure(const struct device *dev, uint32_t channel,
				  struct dma_config *config)
{
	const struct dma_mcux_lpc_config *dev_config;
	dma_handle_t *p_handle;
	uint32_t xfer_config = 0U;
	struct channel_data *data;
	struct dma_mcux_lpc_dma_data *dma_data;
	struct dma_block_config *block_config;
	uint32_t virtual_channel;
	uint8_t otrig_index;
	uint8_t src_inc, dst_inc;
	bool is_periph;
	uint8_t width;
	uint32_t max_xfer_bytes;
	uint8_t reload = 0;
	bool complete_callback;

	if (NULL == dev || NULL == config) {
		return -EINVAL;
	}

	dev_config = dev->config;
	dma_data = dev->data;
	block_config = config->head_block;
	/* The DMA controller deals with just one transfer
	 * size, though the API provides separate sizes
	 * for source and dest. So assert that the source
	 * and dest sizes are the same.
	 */
	assert(config->dest_data_size == config->source_data_size);
	width = config->dest_data_size;

	/* If skip is set on both source and destination
	 * then skip by the same amount on both sides
	 */
	if (block_config->source_gather_en && block_config->dest_scatter_en) {
		assert(block_config->source_gather_interval ==
		       block_config->dest_scatter_interval);
	}

	max_xfer_bytes = NXP_LPC_DMA_MAX_XFER * width;

	/*
	 * Check if circular mode is requested.
	 */
	if (config->head_block->source_reload_en ||
	    config->head_block->dest_reload_en) {
		reload = 1;
	}

	/* Check if have a free slot to store DMA channel data */
	if (dma_data->num_channels_used > dev_config->num_of_channels) {
		LOG_ERR("out of DMA channel %d", channel);
		return -EINVAL;
	}

	/* Check if the dma channel number is valid */
	if (channel >= dev_config->num_of_channels) {
		LOG_ERR("invalid DMA channel number %d", channel);
		return -EINVAL;
	}

	if (config->source_data_size != 4U &&
		config->source_data_size != 2U &&
		config->source_data_size != 1U) {
		LOG_ERR("Source unit size error, %d", config->source_data_size);
		return -EINVAL;
	}

	if (config->dest_data_size != 4U &&
		config->dest_data_size != 2U &&
		config->dest_data_size != 1U) {
		LOG_ERR("Dest unit size error, %d", config->dest_data_size);
		return -EINVAL;
	}

	if ((config->channel_direction == MEMORY_TO_PERIPHERAL) ||
	    (config->channel_direction == PERIPHERAL_TO_MEMORY) ||
	    (config->channel_direction == LPC_DMA_SPI_MCUX_FLEXCOMM_TX)) {
		is_periph = true;
	} else {
		is_periph = false;
	}

	if (get_block_increments(config, block_config, &src_inc, &dst_inc) != 0) {
		return -EINVAL;
	}

	/* If needed, allocate a slot to store dma channel data */
	if (dma_data->channel_index[channel] == -1) {
		/* Not enough items in channel data array */
		if (dma_data->num_channels_used >= dev_config->num_of_allocated_channels) {
			LOG_ERR("No free DMA channels available");
			return -ENOMEM;
		}

		dma_data->channel_index[channel] = dma_data->num_channels_used;
		dma_data->num_channels_used++;
		/* Get the slot number that has the dma channel data */
		virtual_channel = dma_data->channel_index[channel];
		/* dma channel data */
		p_handle = DEV_DMA_HANDLE(dev, virtual_channel);
		data = DEV_CHANNEL_DATA(dev, virtual_channel);

		DMA_CreateHandle(p_handle, DEV_BASE(dev), channel);
		DMA_SetCallback(p_handle, nxp_lpc_dma_callback, (void *)data);
	} else {
		/* Get the slot number that has the dma channel data */
		virtual_channel = dma_data->channel_index[channel];
		/* dma channel data */
		p_handle = DEV_DMA_HANDLE(dev, virtual_channel);
		data = DEV_CHANNEL_DATA(dev, virtual_channel);
	}

	dma_mcux_lpc_clear_channel_data(data);

	data->dir = config->channel_direction;
	/* Save the increment values for the reload function */
	data->src_inc = src_inc;
	data->dst_inc = dst_inc;

	if (data->busy) {
		DMA_AbortTransfer(p_handle);
		pm_policy_device_power_lock_put(dev);
	}

	LOG_DBG("channel is %d", p_handle->channel);

	k_spinlock_key_t otrigs_key = k_spin_lock(&configuring_otrigs);

	data->width = width;

	if (config->source_chaining_en || config->dest_chaining_en) {
		/* Chaining is enabled */
		if (!dev_config->otrig_base_address || !dev_config->itrig_base_address) {
			LOG_ERR("Calling function tried to setup up channel"
			" chaining but the current platform is missing"
			" the correct trigger base addresses.");
			k_spin_unlock(&configuring_otrigs, otrigs_key);
			return -ENXIO;
		}

		LOG_DBG("link dma 0 channel %d with channel %d",
			channel, config->linked_channel);
		uint8_t is_otrig_available = 0;

		for (otrig_index = 0; otrig_index < dev_config->num_of_otrigs;
			++otrig_index) {
			if (dma_data->otrig_array[otrig_index].linked_channel == EMPTY_OTRIG ||
			    dma_data->otrig_array[otrig_index].source_channel == channel) {
				if (dma_data->otrig_array[otrig_index].source_channel == channel) {
					int ChannelToDisable =
						dma_data->otrig_array[otrig_index].linked_channel;
					DMA_DisableChannel(DEV_BASE(dev), ChannelToDisable);
					DEV_BASE(dev)->CHANNEL[ChannelToDisable].CFG &=
						~DMA_CHANNEL_CFG_HWTRIGEN_MASK;
				}
				is_otrig_available = 1;
				break;
			}
		}
		if (!is_otrig_available) {
			LOG_ERR("Calling function tried to setup up multiple"
			" channels to be configured but the dma driver has"
			" run out of OTrig Muxes");
			k_spin_unlock(&configuring_otrigs, otrigs_key);
			return -EINVAL;
		}

		/* Since INPUTMUX handles the dma signals and
		 * must be hardware triggered via the INPUTMUX
		 * hardware.
		 */
		DEV_BASE(dev)->CHANNEL[config->linked_channel].CFG |=
			DMA_CHANNEL_CFG_HWTRIGEN_MASK;

		DMA_EnableChannel(DEV_BASE(dev), config->linked_channel);

		/* Link OTrig Muxes with passed-in channels */
		INPUTMUX_Init(INPUTMUX);
		INPUTMUX_AttachSignal(INPUTMUX, otrig_index,
			dev_config->otrig_base_address + channel);
		INPUTMUX_AttachSignal(INPUTMUX, config->linked_channel,
				dev_config->itrig_base_address + otrig_index);
		INPUTMUX_Deinit(INPUTMUX);

		/* Otrig is now connected with linked channel */
		dma_data->otrig_array[otrig_index].source_channel = channel;
		dma_data->otrig_array[otrig_index].linked_channel = config->linked_channel;
	} else {
		/* Chaining is _NOT_ enabled, Freeing connected OTrig */
		for (otrig_index = 0; otrig_index < dev_config->num_of_otrigs; otrig_index++) {
			if (dma_data->otrig_array[otrig_index].linked_channel != EMPTY_OTRIG &&
			   (channel == dma_data->otrig_array[otrig_index].source_channel)) {
				int ChannelToDisable =
					dma_data->otrig_array[otrig_index].linked_channel;
				DMA_DisableChannel(DEV_BASE(dev), ChannelToDisable);
				DEV_BASE(dev)->CHANNEL[ChannelToDisable].CFG &=
					~DMA_CHANNEL_CFG_HWTRIGEN_MASK;
				dma_data->otrig_array[otrig_index].linked_channel = EMPTY_OTRIG;
				dma_data->otrig_array[otrig_index].source_channel = EMPTY_OTRIG;
				break;
			}
		}
	}

	k_spin_unlock(&configuring_otrigs, otrigs_key);

	complete_callback = config->complete_callback_en;

	/* Check if we need to queue DMA descriptors */
	if ((block_config->block_size > max_xfer_bytes) ||
		(block_config->next_block != NULL)) {
		/* Allocate a DMA descriptor */
		data->curr_descriptor = data->dma_descriptor_table;

		if (block_config->block_size > max_xfer_bytes) {
			/* Disable interrupt as this is not the entire data.
			 * Reload for the descriptor
			 */
			xfer_config = DMA_CHANNEL_XFER(1UL, 0UL, 0UL, 0UL,
					width,
					src_inc,
					dst_inc,
					max_xfer_bytes);
		} else {
			/* Enable INTA interrupt if user requested DMA for each block.
			 * Reload for the descriptor.
			 */
			xfer_config = DMA_CHANNEL_XFER(1UL, 0UL, complete_callback, 0UL,
					width,
					src_inc,
					dst_inc,
					block_config->block_size);
		}
	} else {
		/* Enable interrupt for the descriptor. Use int_b */
		xfer_config = DMA_CHANNEL_XFER(0UL, 0UL, 0UL, 1UL,
				width,
				src_inc,
				dst_inc,
				block_config->block_size);
	}
	/* DMA controller requires that the address be aligned to transfer size */
	assert(block_config->source_address == ROUND_UP(block_config->source_address, width));
	assert(block_config->dest_address == ROUND_UP(block_config->dest_address, width));

#ifdef CONFIG_SOC_SERIES_RW6XX
	rw6xx_dma_addr_fixup(block_config);
#endif

	DMA_SubmitChannelTransferParameter(p_handle,
					xfer_config,
					(void *)block_config->source_address,
					(void *)block_config->dest_address,
					(void *)data->curr_descriptor);

	/* Start queuing DMA descriptors */
	if (data->curr_descriptor) {
		if (block_config->block_size > max_xfer_bytes) {
			/* Queue additional DMA descriptors because the amount of data to
			 * be transferred is greater that the DMA descriptors max XFERCOUNT.
			 */
			struct dma_block_config local_block = { 0 };

			if (src_inc) {
				local_block.source_address = block_config->source_address
							     + max_xfer_bytes;
			} else {
				local_block.source_address = block_config->source_address;
			}
			if (dst_inc) {
				local_block.dest_address = block_config->dest_address
							     + max_xfer_bytes;
			} else {
				local_block.dest_address = block_config->dest_address;
			}
			local_block.block_size = block_config->block_size - max_xfer_bytes;
			local_block.next_block = block_config->next_block;
			local_block.source_reload_en = reload;

			if (block_config->next_block == NULL) {
				/* This is the last block, enable callback. */
				complete_callback = true;
			}

			if (dma_mcux_lpc_queue_descriptors(data, &local_block,
					src_inc, dst_inc, complete_callback)) {
				return -ENOMEM;
			}
		}
		/* Get the next block to transfer */
		block_config = block_config->next_block;

		while (block_config != NULL) {
			/* Each block can have a different configuration so update
			 * src_inc/dst_inc based on the block_config.
			 */
			if (get_block_increments(config, block_config, &src_inc, &dst_inc) != 0) {
				return -EINVAL;
			}
			block_config->source_reload_en = reload;

			/* DMA controller requires that the address be aligned to transfer size */
			assert(block_config->source_address ==
			       ROUND_UP(block_config->source_address, width));
			assert(block_config->dest_address ==
			       ROUND_UP(block_config->dest_address, width));

			if (block_config->next_block == NULL) {
				/* This is the last block. Enable callback if not enabled. */
				complete_callback = true;
			}
			if (dma_mcux_lpc_queue_descriptors(data, block_config,
				src_inc, dst_inc, complete_callback)) {
				return -ENOMEM;
			}

			/* Get the next block and start queuing descriptors */
			block_config = block_config->next_block;
		}
		/* We have finished queuing DMA descriptors */
		data->descriptors_queued = true;
	}

	if (config->dma_slot) {
		uint32_t cfg_reg = 0;

		/* User supplied manual trigger configuration */
		if (config->dma_slot & LPC_DMA_PERIPH_REQ_EN) {
			cfg_reg |= DMA_CHANNEL_CFG_PERIPHREQEN_MASK;
		}
		if (config->dma_slot & LPC_DMA_HWTRIG_EN) {
			/* Setup hardware trigger */
			cfg_reg |= DMA_CHANNEL_CFG_HWTRIGEN_MASK;
			if (config->dma_slot & LPC_DMA_TRIGTYPE_LEVEL) {
				cfg_reg |= DMA_CHANNEL_CFG_TRIGTYPE_MASK;
			}
			if (config->dma_slot & LPC_DMA_TRIGPOL_HIGH_RISING) {
				cfg_reg |= DMA_CHANNEL_CFG_TRIGPOL_MASK;
			}
			if (config->dma_slot & LPC_DMA_TRIGBURST) {
				cfg_reg |= DMA_CHANNEL_CFG_TRIGBURST_MASK;
				cfg_reg |= DMA_CHANNEL_CFG_BURSTPOWER(
					LPC_DMA_GET_BURSTPOWER(config->dma_slot));
			}
		}
		p_handle->base->CHANNEL[p_handle->channel].CFG = cfg_reg;
	} else if (is_periph) {
		DMA_EnableChannelPeriphRq(p_handle->base, p_handle->channel);
	} else {
		DMA_DisableChannelPeriphRq(p_handle->base, p_handle->channel);
	}
	DMA_SetChannelPriority(p_handle->base, p_handle->channel, config->channel_priority);

	data->busy = false;
	if (config->dma_callback) {
		LOG_DBG("INSTALL call back on channel %d", channel);
		data->user_data = config->user_data;
		data->dma_callback = config->dma_callback;
		data->dev = dev;
	}

	return 0;
}

static int dma_mcux_lpc_start(const struct device *dev, uint32_t channel)
{
	struct dma_mcux_lpc_dma_data *dev_data = dev->data;
	int8_t virtual_channel = dev_data->channel_index[channel];
	struct channel_data *data = DEV_CHANNEL_DATA(dev, virtual_channel);
	dma_handle_t *p_handle = DEV_DMA_HANDLE(dev, virtual_channel);

	LOG_DBG("START TRANSFER");
	LOG_DBG("DMA CTRL 0x%x", DEV_BASE(dev)->CTRL);
	data->busy = true;
	pm_policy_device_power_lock_get(dev);
	/* In case of a restart after a stop, reinstall the DMA callback
	 * that was removed by the stop.
	 */
	DMA_SetCallback(p_handle, nxp_lpc_dma_callback, (void *)data);
	DMA_StartTransfer(p_handle);
	return 0;
}

static int dma_mcux_lpc_stop(const struct device *dev, uint32_t channel)
{
	struct dma_mcux_lpc_dma_data *dev_data = dev->data;
	int8_t virtual_channel = dev_data->channel_index[channel];
	struct channel_data *data = DEV_CHANNEL_DATA(dev, virtual_channel);
	dma_handle_t *p_handle = DEV_DMA_HANDLE(dev, virtual_channel);

	/* Abort/disable even if not busy. it's safe and avoids
	 * any risk of data->busy not being accurate.
	 */
	DMA_AbortTransfer(p_handle);
	DMA_DisableChannel(DEV_BASE(dev), p_handle->channel);

	if (data->busy) {
		data->busy = false;
		pm_policy_device_power_lock_put(dev);
	}

	/* Handle race condition where if this is called from an ISR
	 * and the DMA channel completion interrupt becomes pending
	 * before we complete the abort and disable, then the DMA ISR
	 * may run and invoke the callback may run after we return,
	 * which is unexpected by user since from their perspective
	 * the stop was already completed. We cannot just checking
	 * and clear the pending IRQ since it is shared by other
	 * channels. This way, the pending DMA ISR will still run,
	 * but should just clear the interrupt and not invoke
	 * the callback.
	 */
	DMA_SetCallback(p_handle, NULL, NULL);
	return 0;
}

static int dma_mcux_lpc_reload(const struct device *dev, uint32_t channel,
			       uint32_t src, uint32_t dst, size_t size)
{
	struct dma_mcux_lpc_dma_data *dev_data = dev->data;
	int8_t virtual_channel = dev_data->channel_index[channel];
	struct channel_data *data = DEV_CHANNEL_DATA(dev, virtual_channel);
	uint32_t xfer_config = 0U;

	/* DMA controller requires that the address be aligned to transfer size */
	assert(src == ROUND_UP(src, data->width));
	assert(dst == ROUND_UP(dst, data->width));

	if (!data->descriptors_queued) {
		dma_handle_t *p_handle;

		p_handle = DEV_DMA_HANDLE(dev, virtual_channel);

		/* Only one buffer, enable interrupt b */
		xfer_config = DMA_CHANNEL_XFER(0UL, 0UL, 0UL, 1UL,
					data->width,
					data->src_inc,
					data->dst_inc,
					size);
		DMA_SubmitChannelTransferParameter(p_handle,
						xfer_config,
						(void *)src,
						(void *)dst,
						NULL);
	} else {
		struct dma_block_config local_block = { 0 };

		local_block.source_address = src;
		local_block.dest_address = dst;
		local_block.block_size = size;
		local_block.source_reload_en = 1;
		dma_mcux_lpc_queue_descriptors(data, &local_block,
					       data->src_inc, data->dst_inc, true);
	}

	return 0;
}

static int dma_mcux_lpc_get_status(const struct device *dev, uint32_t channel,
				   struct dma_status *status)
{
	const struct dma_mcux_lpc_config *config = dev->config;
	struct dma_mcux_lpc_dma_data *dev_data = dev->data;
	int8_t virtual_channel = dev_data->channel_index[channel];
	struct channel_data *data = DEV_CHANNEL_DATA(dev, virtual_channel);

	if (channel > config->num_of_channels) {
		return -EINVAL;
	}

	/* If channel is actually busy or the virtual channel is just not set up */
	if (data->busy && (virtual_channel != -1)) {
		status->busy = true;
		status->pending_length = DMA_GetRemainingBytes(DEV_BASE(dev), channel);
	} else {
		status->busy = false;
		status->pending_length = 0;
	}
	status->dir = data->dir;
	LOG_DBG("DMA CR 0x%x", DEV_BASE(dev)->CTRL);
	LOG_DBG("DMA INT 0x%x", DEV_BASE(dev)->INTSTAT);

	return 0;
}

static int dma_mcux_lpc_get_attribute(const struct device *dev, uint32_t type, uint32_t *value)
{
	switch (type) {
	case DMA_ATTR_BUFFER_ADDRESS_ALIGNMENT:
	case DMA_ATTR_BUFFER_SIZE_ALIGNMENT:
	case DMA_ATTR_COPY_ALIGNMENT:
		*value = 4;
		break;

	default:
		return -EINVAL;
	}

	return 0;
}

#if CONFIG_PM_DEVICE
static void dma_mcux_lpc_backup_reg(const struct device *dev)
{
	struct dma_mcux_lpc_dma_data *dma_data = dev->data;
	const struct dma_mcux_lpc_config *config = dev->config;
	struct channel_data *p_channel_data;
	uint32_t virtual_channel;
	DMA_Type *dma_base = DEV_BASE(dev);

	dma_data->backup_reg.enableset = dma_base->COMMON[0].ENABLESET;
	dma_data->backup_reg.intenset  = dma_base->COMMON[0].INTENSET;

	/* Only backup the used channels */
	virtual_channel = 0;
	for (uint32_t channel = 0; channel < config->num_of_channels; channel++) {
		if (dma_data->channel_index[channel] != -1) {
			p_channel_data = &dma_data->channel_data[virtual_channel];

			p_channel_data->backup_reg.xfercfg = dma_base->CHANNEL[channel].XFERCFG;
			p_channel_data->backup_reg.cfg     = dma_base->CHANNEL[channel].CFG;
			virtual_channel++;
		}
	}
}

static void dma_mcux_lpc_restore_reg(const struct device *dev)
{
	struct dma_mcux_lpc_dma_data *dma_data = dev->data;
	const struct dma_mcux_lpc_config *config = dev->config;
	struct channel_data *p_channel_data;
	uint32_t virtual_channel;
	DMA_Type *dma_base = DEV_BASE(dev);

	dma_base->COMMON[0].ENABLESET = dma_data->backup_reg.enableset;
	dma_base->COMMON[0].INTENSET  = dma_data->backup_reg.intenset;

	/* Only backup the used channels */
	virtual_channel = 0;
	for (uint32_t channel = 0; channel < config->num_of_channels; channel++) {
		if (dma_data->channel_index[channel] != -1) {
			p_channel_data = &dma_data->channel_data[virtual_channel];

			dma_base->CHANNEL[channel].XFERCFG = p_channel_data->backup_reg.xfercfg;
			dma_base->CHANNEL[channel].CFG     = p_channel_data->backup_reg.cfg;
			virtual_channel++;
		}
	}
}

#else /* !CONFIG_PM_DEVICE */

static inline void dma_mcux_lpc_backup_reg(const struct device *dev)
{
	ARG_UNUSED(dev);
}

static inline void dma_mcux_lpc_restore_reg(const struct device *dev)
{
	ARG_UNUSED(dev);
}

#endif /* CONFIG_PM_DEVICE */

static int dma_mcux_lpc_pm_action(const struct device *dev, enum pm_device_action action)
{
	switch (action) {
	case PM_DEVICE_ACTION_RESUME:
		break;
	case PM_DEVICE_ACTION_SUSPEND:
		break;
	case PM_DEVICE_ACTION_TURN_OFF:
		dma_mcux_lpc_backup_reg(dev);
		break;
	case PM_DEVICE_ACTION_TURN_ON:
		DMA_Init(DEV_BASE(dev));
		dma_mcux_lpc_restore_reg(dev);
		break;
	default:
		return -ENOTSUP;
	}

	return 0;
}

static int dma_mcux_lpc_init(const struct device *dev)
{
	const struct dma_mcux_lpc_config *config = dev->config;
	struct dma_mcux_lpc_dma_data *data = dev->data;

	data->ctx.magic = DMA_MAGIC;
	data->ctx.dma_channels = config->num_of_channels;

	/* Indicate that the Otrig Muxes are not connected */
	for (int i = 0; i < config->num_of_otrigs; i++) {
		data->otrig_array[i].source_channel = EMPTY_OTRIG;
		data->otrig_array[i].linked_channel = EMPTY_OTRIG;
	}

	/*
	 * Initialize to -1 to indicate dma channel does not have a slot
	 * assigned to store dma channel data
	 */
	for (int i = 0; i < config->num_of_channels; i++) {
		data->channel_index[i] = -1;
	}

	data->num_channels_used = 0;

	config->irq_config_func(dev);

	/* Complete the remaining hardware specific init in the TURN_ON action
	 * of the power management handler.
	 */
	return pm_device_driver_init(dev, dma_mcux_lpc_pm_action);
}

static DEVICE_API(dma, dma_mcux_lpc_api) = {
	.config = dma_mcux_lpc_configure,
	.start = dma_mcux_lpc_start,
	.stop = dma_mcux_lpc_stop,
	.reload = dma_mcux_lpc_reload,
	.get_status = dma_mcux_lpc_get_status,
	.get_attribute = dma_mcux_lpc_get_attribute
};

#define DMA_MCUX_LPC_CONFIG_FUNC(n)					\
	static void dma_mcux_lpc_config_func_##n(const struct device *dev)	\
	{								\
		IRQ_CONNECT(DT_INST_IRQN(n),				\
			    DT_INST_IRQ(n, priority),			\
			    dma_mcux_lpc_irq_handler, DEVICE_DT_INST_GET(n), 0);\
									\
		irq_enable(DT_INST_IRQN(n));				\
	}
#define DMA_MCUX_LPC_IRQ_CFG_FUNC_INIT(n)				\
	.irq_config_func = dma_mcux_lpc_config_func_##n
#define DMA_MCUX_LPC_INIT_CFG(n)					\
	DMA_MCUX_LPC_DECLARE_CFG(n,					\
				 DMA_MCUX_LPC_IRQ_CFG_FUNC_INIT(n))

#define DMA_MCUX_LPC_NUM_USED_CHANNELS(n)				\
	COND_CODE_0(CONFIG_DMA_MCUX_LPC_NUMBER_OF_CHANNELS_ALLOCATED,	\
		    (DT_INST_PROP(n, dma_channels)),			\
		    (MIN(CONFIG_DMA_MCUX_LPC_NUMBER_OF_CHANNELS_ALLOCATED,	\
			DT_INST_PROP(n, dma_channels))))

#define DMA_MCUX_LPC_DECLARE_CFG(n, IRQ_FUNC_INIT)			\
static const struct dma_mcux_lpc_config dma_##n##_config = {		\
	.base = (DMA_Type *)DT_INST_REG_ADDR(n),			\
	.num_of_channels = DT_INST_PROP(n, dma_channels),		\
	.num_of_allocated_channels = DMA_MCUX_LPC_NUM_USED_CHANNELS(n),	\
	.num_of_otrigs = DT_INST_PROP_OR(n, nxp_dma_num_of_otrigs, 0),			\
	.otrig_base_address = DT_INST_PROP_OR(n, nxp_dma_otrig_base_address, 0x0),	\
	.itrig_base_address = DT_INST_PROP_OR(n, nxp_dma_itrig_base_address, 0x0),	\
	IRQ_FUNC_INIT							\
}

#define DMA_INIT(n) \
									\
	static const struct dma_mcux_lpc_config dma_##n##_config;	\
									\
	static struct channel_data dma_##n##_channel_data_arr		\
			[DMA_MCUX_LPC_NUM_USED_CHANNELS(n)] = {0};	\
									\
	static struct dma_otrig dma_##n##_otrig_arr			\
			[DT_INST_PROP_OR(n, nxp_dma_num_of_otrigs, 0)]; \
									\
	static int8_t							\
		dma_##n##_channel_index_arr				\
				[DT_INST_PROP(n, dma_channels)] = {0};	\
									\
	static struct dma_mcux_lpc_dma_data dma_data_##n = {		\
		.channel_data = dma_##n##_channel_data_arr,		\
		.channel_index = dma_##n##_channel_index_arr,		\
		.otrig_array = dma_##n##_otrig_arr,			\
	};								\
									\
	PM_DEVICE_DT_INST_DEFINE(n, dma_mcux_lpc_pm_action);		\
									\
	DEVICE_DT_INST_DEFINE(n,					\
			    dma_mcux_lpc_init,				\
			    PM_DEVICE_DT_INST_GET(n),			\
			    &dma_data_##n, &dma_##n##_config,		\
			    PRE_KERNEL_1, CONFIG_DMA_INIT_PRIORITY,	\
			    &dma_mcux_lpc_api);				\
									\
	DMA_MCUX_LPC_CONFIG_FUNC(n)					\
	DMA_MCUX_LPC_INIT_CFG(n);

DT_INST_FOREACH_STATUS_OKAY(DMA_INIT)
