| /* |
| * 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); |