/* * Copyright 2024 NXP * * SPDX-License-Identifier: Apache-2.0 */ #include "dma_nxp_edma.h" /* TODO list: * 1) Support for requesting a specific channel. * 2) Support for checking if DMA transfer is pending when attempting config. (?) * 3) Support for error interrupt. * 4) Support for error if buffer overflow/underrun. * 5) Ideally, HALFMAJOR should be set on a per-channel basis not through a * config. If not possible, this should be done through a DTS property. Also, * maybe do the same for INTMAJOR IRQ. */ static void edma_isr(const void *parameter) { const struct edma_config *cfg; struct edma_data *data; struct edma_channel *chan; int ret; uint32_t update_size; chan = (struct edma_channel *)parameter; cfg = chan->dev->config; data = chan->dev->data; if (!EDMA_ChannelRegRead(data->hal_cfg, chan->id, EDMA_TCD_CH_INT)) { /* skip, interrupt was probably triggered by another channel */ return; } /* clear interrupt */ EDMA_ChannelRegUpdate(data->hal_cfg, chan->id, EDMA_TCD_CH_INT, EDMA_TCD_CH_INT_MASK, 0); if (chan->cyclic_buffer) { update_size = chan->bsize; if (IS_ENABLED(CONFIG_DMA_NXP_EDMA_ENABLE_HALFMAJOR_IRQ)) { update_size = chan->bsize / 2; } else { update_size = chan->bsize; } /* TODO: add support for error handling here */ ret = EDMA_CHAN_PRODUCE_CONSUME_A(chan, update_size); if (ret < 0) { LOG_ERR("chan %d buffer overflow/underrun", chan->id); } } /* TODO: are there any sanity checks we have to perform before invoking * the registered callback? */ if (chan->cb) { chan->cb(chan->dev, chan->arg, chan->id, DMA_STATUS_COMPLETE); } } static struct edma_channel *lookup_channel(const struct device *dev, uint32_t chan_id) { struct edma_data *data; const struct edma_config *cfg; int i; data = dev->data; cfg = dev->config; /* optimization: if dma-channels property is present then * the channel data associated with the passed channel ID * can be found at index chan_id in the array of channels. */ if (cfg->contiguous_channels) { /* check for index out of bounds */ if (chan_id >= data->ctx.dma_channels) { return NULL; } return &data->channels[chan_id]; } /* channels are passed through the valid-channels property. * As such, since some channels may be missing we need to * look through the entire channels array for an ID match. */ for (i = 0; i < data->ctx.dma_channels; i++) { if (data->channels[i].id == chan_id) { return &data->channels[i]; } } return NULL; } static int edma_config(const struct device *dev, uint32_t chan_id, struct dma_config *dma_cfg) { struct edma_data *data; const struct edma_config *cfg; struct edma_channel *chan; uint32_t transfer_type; int ret; data = dev->data; cfg = dev->config; if (!dma_cfg->head_block) { LOG_ERR("head block shouldn't be NULL"); return -EINVAL; } /* validate source data size (SSIZE) */ if (!EDMA_TransferWidthIsValid(data->hal_cfg, dma_cfg->source_data_size)) { LOG_ERR("invalid source data size: %d", dma_cfg->source_data_size); return -EINVAL; } /* validate destination data size (DSIZE) */ if (!EDMA_TransferWidthIsValid(data->hal_cfg, dma_cfg->dest_data_size)) { LOG_ERR("invalid destination data size: %d", dma_cfg->dest_data_size); return -EINVAL; } /* validate configured alignment */ if (!EDMA_TransferWidthIsValid(data->hal_cfg, CONFIG_DMA_NXP_EDMA_ALIGN)) { LOG_ERR("configured alignment %d is invalid", CONFIG_DMA_NXP_EDMA_ALIGN); return -EINVAL; } /* Scatter-Gather configurations currently not supported */ if (dma_cfg->block_count != 1) { LOG_ERR("number of blocks %d not supported", dma_cfg->block_count); return -ENOTSUP; } /* source address shouldn't be NULL */ if (!dma_cfg->head_block->source_address) { LOG_ERR("source address cannot be NULL"); return -EINVAL; } /* destination address shouldn't be NULL */ if (!dma_cfg->head_block->dest_address) { LOG_ERR("destination address cannot be NULL"); return -EINVAL; } /* check source address's (SADDR) alignment with respect to the data size (SSIZE) * * Failing to meet this condition will lead to the assertion of the SAE * bit (see CHn_ES register). * * TODO: this will also restrict scenarios such as the following: * SADDR is 8B aligned and SSIZE is 16B. I've tested this * scenario and seems to raise no hardware errors (I'm assuming * because this doesn't break the 8B boundary of the 64-bit system * I tested it on). Is there a need to allow such a scenario? */ if (dma_cfg->head_block->source_address % dma_cfg->source_data_size) { LOG_ERR("source address 0x%x alignment doesn't match data size %d", dma_cfg->head_block->source_address, dma_cfg->source_data_size); return -EINVAL; } /* check destination address's (DADDR) alignment with respect to the data size (DSIZE) * Failing to meet this condition will lead to the assertion of the DAE * bit (see CHn_ES register). */ if (dma_cfg->head_block->dest_address % dma_cfg->dest_data_size) { LOG_ERR("destination address 0x%x alignment doesn't match data size %d", dma_cfg->head_block->dest_address, dma_cfg->dest_data_size); return -EINVAL; } /* source burst length should match destination burst length. * This is because the burst length is the equivalent of NBYTES which * is used for both the destination and the source. */ if (dma_cfg->source_burst_length != dma_cfg->dest_burst_length) { LOG_ERR("source burst length %d doesn't match destination burst length %d", dma_cfg->source_burst_length, dma_cfg->dest_burst_length); return -EINVAL; } /* total number of bytes should be a multiple of NBYTES. * * This is needed because the EDMA engine performs transfers based * on CITER (integer value) and NBYTES, thus it has no knowledge of * the total transfer size. If the total transfer size is not a * multiple of NBYTES then we'll end up with copying a wrong number * of bytes (CITER = TOTAL_SIZE / BITER). This, of course, raises * no error in the hardware but it's still wrong. */ if (dma_cfg->head_block->block_size % dma_cfg->source_burst_length) { LOG_ERR("block size %d should be a multiple of NBYTES %d", dma_cfg->head_block->block_size, dma_cfg->source_burst_length); return -EINVAL; } /* check if NBYTES is a multiple of MAX(SSIZE, DSIZE). * * This stems from the fact that NBYTES needs to be a multiple * of SSIZE AND DSIZE. If NBYTES is a multiple of MAX(SSIZE, DSIZE) * then it will for sure satisfy the aforementioned condition (since * SSIZE and DSIZE are powers of 2). * * Failing to meet this condition will lead to the assertion of the * NCE bit (see CHn_ES register). */ if (dma_cfg->source_burst_length % MAX(dma_cfg->source_data_size, dma_cfg->dest_data_size)) { LOG_ERR("NBYTES %d should be a multiple of MAX(SSIZE(%d), DSIZE(%d))", dma_cfg->source_burst_length, dma_cfg->source_data_size, dma_cfg->dest_data_size); return -EINVAL; } /* fetch channel data */ chan = lookup_channel(dev, chan_id); if (!chan) { LOG_ERR("channel ID %u is not valid", chan_id); return -EINVAL; } /* save the block size for later usage in edma_reload */ chan->bsize = dma_cfg->head_block->block_size; if (dma_cfg->cyclic) { chan->cyclic_buffer = true; chan->stat.read_position = 0; chan->stat.write_position = 0; /* ASSUMPTION: for CONSUMER-type channels, the buffer from * which the engine consumes should be full, while in the * case of PRODUCER-type channels it should be empty. */ switch (dma_cfg->channel_direction) { case MEMORY_TO_PERIPHERAL: chan->type = CHAN_TYPE_CONSUMER; chan->stat.free = 0; chan->stat.pending_length = chan->bsize; break; case PERIPHERAL_TO_MEMORY: chan->type = CHAN_TYPE_PRODUCER; chan->stat.pending_length = 0; chan->stat.free = chan->bsize; break; default: LOG_ERR("unsupported transfer dir %d for cyclic mode", dma_cfg->channel_direction); return -ENOTSUP; } } else { chan->cyclic_buffer = false; } /* change channel's state to CONFIGURED */ ret = channel_change_state(chan, CHAN_STATE_CONFIGURED); if (ret < 0) { LOG_ERR("failed to change channel %d state to CONFIGURED", chan_id); return ret; } ret = get_transfer_type(dma_cfg->channel_direction, &transfer_type); if (ret < 0) { return ret; } chan->cb = dma_cfg->dma_callback; chan->arg = dma_cfg->user_data; /* warning: this sets SOFF and DOFF to SSIZE and DSIZE which are POSITIVE. */ ret = EDMA_ConfigureTransfer(data->hal_cfg, chan_id, dma_cfg->head_block->source_address, dma_cfg->head_block->dest_address, dma_cfg->source_data_size, dma_cfg->dest_data_size, dma_cfg->source_burst_length, dma_cfg->head_block->block_size, transfer_type); if (ret < 0) { LOG_ERR("failed to configure transfer"); return to_std_error(ret); } /* TODO: channel MUX should be forced to 0 based on the previous state */ if (EDMA_HAS_MUX(data->hal_cfg)) { ret = EDMA_SetChannelMux(data->hal_cfg, chan_id, dma_cfg->dma_slot); if (ret < 0) { LOG_ERR("failed to set channel MUX"); return to_std_error(ret); } } /* set SLAST and DLAST */ ret = set_slast_dlast(dma_cfg, transfer_type, data, chan_id); if (ret < 0) { return ret; } /* allow interrupting the CPU when a major cycle is completed. * * interesting note: only 1 major loop is performed per slave peripheral * DMA request. For instance, if block_size = 768 and burst_size = 192 * we're going to get 4 transfers of 192 bytes. Each of these transfers * translates to a DMA request made by the slave peripheral. */ EDMA_ChannelRegUpdate(data->hal_cfg, chan_id, EDMA_TCD_CSR, EDMA_TCD_CSR_INTMAJOR_MASK, 0); if (IS_ENABLED(CONFIG_DMA_NXP_EDMA_ENABLE_HALFMAJOR_IRQ)) { /* if enabled through the above configuration, also * allow the CPU to be interrupted when CITER = BITER / 2. */ EDMA_ChannelRegUpdate(data->hal_cfg, chan_id, EDMA_TCD_CSR, EDMA_TCD_CSR_INTHALF_MASK, 0); } /* enable channel interrupt */ irq_enable(chan->irq); /* dump register status - for debugging purposes */ edma_dump_channel_registers(data, chan_id); return 0; } static int edma_get_status(const struct device *dev, uint32_t chan_id, struct dma_status *stat) { struct edma_data *data; struct edma_channel *chan; uint32_t citer, biter, done; unsigned int key; data = dev->data; /* fetch channel data */ chan = lookup_channel(dev, chan_id); if (!chan) { LOG_ERR("channel ID %u is not valid", chan_id); return -EINVAL; } if (chan->cyclic_buffer) { key = irq_lock(); stat->free = chan->stat.free; stat->pending_length = chan->stat.pending_length; irq_unlock(key); } else { /* note: no locking required here. The DMA interrupts * have no effect over CITER and BITER. */ citer = EDMA_ChannelRegRead(data->hal_cfg, chan_id, EDMA_TCD_CITER); biter = EDMA_ChannelRegRead(data->hal_cfg, chan_id, EDMA_TCD_BITER); done = EDMA_ChannelRegRead(data->hal_cfg, chan_id, EDMA_TCD_CH_CSR) & EDMA_TCD_CH_CSR_DONE_MASK; if (done) { stat->free = chan->bsize; stat->pending_length = 0; } else { stat->free = (biter - citer) * (chan->bsize / biter); stat->pending_length = chan->bsize - stat->free; } } LOG_DBG("free: %d, pending: %d", stat->free, stat->pending_length); return 0; } static int edma_suspend(const struct device *dev, uint32_t chan_id) { struct edma_data *data; const struct edma_config *cfg; struct edma_channel *chan; int ret; data = dev->data; cfg = dev->config; /* fetch channel data */ chan = lookup_channel(dev, chan_id); if (!chan) { LOG_ERR("channel ID %u is not valid", chan_id); return -EINVAL; } edma_dump_channel_registers(data, chan_id); /* change channel's state to SUSPENDED */ ret = channel_change_state(chan, CHAN_STATE_SUSPENDED); if (ret < 0) { LOG_ERR("failed to change channel %d state to SUSPENDED", chan_id); return ret; } LOG_DBG("suspending channel %u", chan_id); /* disable HW requests */ EDMA_ChannelRegUpdate(data->hal_cfg, chan_id, EDMA_TCD_CH_CSR, 0, EDMA_TCD_CH_CSR_ERQ_MASK); return 0; } static int edma_stop(const struct device *dev, uint32_t chan_id) { struct edma_data *data; const struct edma_config *cfg; struct edma_channel *chan; enum channel_state prev_state; int ret; data = dev->data; cfg = dev->config; /* fetch channel data */ chan = lookup_channel(dev, chan_id); if (!chan) { LOG_ERR("channel ID %u is not valid", chan_id); return -EINVAL; } prev_state = chan->state; /* change channel's state to STOPPED */ ret = channel_change_state(chan, CHAN_STATE_STOPPED); if (ret < 0) { LOG_ERR("failed to change channel %d state to STOPPED", chan_id); return ret; } LOG_DBG("stopping channel %u", chan_id); if (prev_state == CHAN_STATE_SUSPENDED) { /* if the channel has been suspended then there's * no point in disabling the HW requests again. Just * jump to the channel release operation. */ goto out_release_channel; } /* disable HW requests */ EDMA_ChannelRegUpdate(data->hal_cfg, chan_id, EDMA_TCD_CH_CSR, 0, EDMA_TCD_CH_CSR_ERQ_MASK); out_release_channel: /* clear the channel MUX so that it can used by a different peripheral. * * note: because the channel is released during dma_stop() that means * dma_start() can no longer be immediately called. This is because * one needs to re-configure the channel MUX which can only be done * through dma_config(). As such, if one intends to reuse the current * configuration then please call dma_suspend() instead of dma_stop(). */ if (EDMA_HAS_MUX(data->hal_cfg)) { ret = EDMA_SetChannelMux(data->hal_cfg, chan_id, 0); if (ret < 0) { LOG_ERR("failed to set channel MUX"); return to_std_error(ret); } } edma_dump_channel_registers(data, chan_id); return 0; } static int edma_start(const struct device *dev, uint32_t chan_id) { struct edma_data *data; const struct edma_config *cfg; struct edma_channel *chan; int ret; data = dev->data; cfg = dev->config; /* fetch channel data */ chan = lookup_channel(dev, chan_id); if (!chan) { LOG_ERR("channel ID %u is not valid", chan_id); return -EINVAL; } /* change channel's state to STARTED */ ret = channel_change_state(chan, CHAN_STATE_STARTED); if (ret < 0) { LOG_ERR("failed to change channel %d state to STARTED", chan_id); return ret; } LOG_DBG("starting channel %u", chan_id); /* enable HW requests */ EDMA_ChannelRegUpdate(data->hal_cfg, chan_id, EDMA_TCD_CH_CSR, EDMA_TCD_CH_CSR_ERQ_MASK, 0); return 0; } static int edma_reload(const struct device *dev, uint32_t chan_id, uint32_t src, uint32_t dst, size_t size) { struct edma_data *data; struct edma_channel *chan; int ret; unsigned int key; data = dev->data; /* fetch channel data */ chan = lookup_channel(dev, chan_id); if (!chan) { LOG_ERR("channel ID %u is not valid", chan_id); return -EINVAL; } /* channel needs to be started to allow reloading */ if (chan->state != CHAN_STATE_STARTED) { LOG_ERR("reload is only supported on started channels"); return -EINVAL; } if (chan->cyclic_buffer) { key = irq_lock(); ret = EDMA_CHAN_PRODUCE_CONSUME_B(chan, size); irq_unlock(key); if (ret < 0) { LOG_ERR("chan %d buffer overflow/underrun", chan_id); return ret; } } return 0; } static int edma_get_attribute(const struct device *dev, uint32_t type, uint32_t *val) { switch (type) { case DMA_ATTR_BUFFER_SIZE_ALIGNMENT: case DMA_ATTR_BUFFER_ADDRESS_ALIGNMENT: *val = CONFIG_DMA_NXP_EDMA_ALIGN; break; case DMA_ATTR_MAX_BLOCK_COUNT: /* this is restricted to 1 because SG configurations are not supported */ *val = 1; break; default: LOG_ERR("invalid attribute type: %d", type); return -EINVAL; } return 0; } static bool edma_channel_filter(const struct device *dev, int chan_id, void *param) { int *requested_channel; if (!param) { return false; } requested_channel = param; if (*requested_channel == chan_id && lookup_channel(dev, chan_id)) { return true; } return false; } static const struct dma_driver_api edma_api = { .reload = edma_reload, .config = edma_config, .start = edma_start, .stop = edma_stop, .suspend = edma_suspend, .resume = edma_start, .get_status = edma_get_status, .get_attribute = edma_get_attribute, .chan_filter = edma_channel_filter, }; static int edma_init(const struct device *dev) { const struct edma_config *cfg; struct edma_data *data; mm_reg_t regmap; data = dev->data; cfg = dev->config; /* map instance MMIO */ device_map(®map, cfg->regmap_phys, cfg->regmap_size, K_MEM_CACHE_NONE); /* overwrite physical address set in the HAL configuration. * We can down-cast the virtual address to a 32-bit address because * we know we're working with 32-bit addresses only. */ data->hal_cfg->regmap = (uint32_t)POINTER_TO_UINT(regmap); cfg->irq_config(); /* dma_request_channel() uses this variable to keep track of the * available channels. As such, it needs to be initialized with NULL * which signifies that all channels are initially available. */ data->channel_flags = ATOMIC_INIT(0); data->ctx.atomic = &data->channel_flags; data->ctx.dma_channels = data->hal_cfg->channels; return 0; } /* a few comments about the BUILD_ASSERT statements: * 1) dma-channels and valid-channels should be mutually exclusive. * This means that you specify the one or the other. There's no real * need to have both of them. * 2) Number of channels should match the number of interrupts for * said channels (TODO: what about error interrupts?) * 3) The channel-mux property shouldn't be specified unless * the eDMA is MUX-capable (signaled via the EDMA_HAS_CHAN_MUX * configuration). */ #define EDMA_INIT(inst) \ \ BUILD_ASSERT(!DT_NODE_HAS_PROP(DT_INST(inst, DT_DRV_COMPAT), dma_channels) || \ !DT_NODE_HAS_PROP(DT_INST(inst, DT_DRV_COMPAT), valid_channels), \ "dma_channels and valid_channels are mutually exclusive"); \ \ BUILD_ASSERT(DT_INST_PROP_OR(inst, dma_channels, 0) == \ DT_NUM_IRQS(DT_INST(inst, DT_DRV_COMPAT)) || \ DT_INST_PROP_LEN_OR(inst, valid_channels, 0) == \ DT_NUM_IRQS(DT_INST(inst, DT_DRV_COMPAT)), \ "number of interrupts needs to match number of channels"); \ \ BUILD_ASSERT(DT_PROP_OR(DT_INST(inst, DT_DRV_COMPAT), hal_cfg_index, 0) < \ ARRAY_SIZE(s_edmaConfigs), \ "HAL configuration index out of bounds"); \ \ static struct edma_channel channels_##inst[] = EDMA_CHANNEL_ARRAY_GET(inst); \ \ static void interrupt_config_function_##inst(void) \ { \ EDMA_CONNECT_INTERRUPTS(inst); \ } \ \ static struct edma_config edma_config_##inst = { \ .regmap_phys = DT_INST_REG_ADDR(inst), \ .regmap_size = DT_INST_REG_SIZE(inst), \ .irq_config = interrupt_config_function_##inst, \ .contiguous_channels = EDMA_CHANS_ARE_CONTIGUOUS(inst), \ }; \ \ static struct edma_data edma_data_##inst = { \ .channels = channels_##inst, \ .ctx.magic = DMA_MAGIC, \ .hal_cfg = &EDMA_HAL_CFG_GET(inst), \ }; \ \ DEVICE_DT_INST_DEFINE(inst, &edma_init, NULL, \ &edma_data_##inst, &edma_config_##inst, \ PRE_KERNEL_1, CONFIG_DMA_INIT_PRIORITY, \ &edma_api); \ DT_INST_FOREACH_STATUS_OKAY(EDMA_INIT);