diff --git a/cpu/sam0_common/Kconfig b/cpu/sam0_common/Kconfig index 3443a363ab..fedf79cc31 100644 --- a/cpu/sam0_common/Kconfig +++ b/cpu/sam0_common/Kconfig @@ -8,6 +8,7 @@ config CPU_COMMON_SAM0 bool select HAS_PERIPH_CPUID + select HAS_PERIPH_DMA select HAS_PERIPH_FLASHPAGE select HAS_PERIPH_FLASHPAGE_RAW select HAS_PERIPH_FLASHPAGE_RWEE diff --git a/cpu/sam0_common/include/periph_cpu_common.h b/cpu/sam0_common/include/periph_cpu_common.h index e55e2a109b..182d67328b 100644 --- a/cpu/sam0_common/include/periph_cpu_common.h +++ b/cpu/sam0_common/include/periph_cpu_common.h @@ -670,6 +670,218 @@ typedef struct { */ #define WDT_HAS_INIT (1) +/** + * The sam0 DMA peripheral has a number of channels. Each channel is a separate + * data stream, triggered by a configurable trigger when enabled, or triggered + * by software (not yet supported). In theory each DMA channel is equal and can + * have a configurable priority and can be triggered by the full set of triggers + * available. + * + * DMA descriptors, specifying a single transfer with size, source and + * destination, are kept in RAM and are read when the channel is enabled and + * triggered. On the SAML21 platform, these descriptors must reside in the LP + * SRAM. + */ + +/** + * @brief Indicates that the peripheral doesn't utilize the DMA controller. + * Matches with the register configuration for software based triggers. + */ +#define DMA_TRIGGER_DISABLED 0 + +/** + * @brief Move the DMA descriptors to the LP SRAM. Required on the SAML21 + */ +#if defined(CPU_FAM_SAML21) +#define DMA_DESCRIPTOR_IN_LPSRAM +#endif + +#ifdef DMA_DESCRIPTOR_IN_LPSRAM +#define DMA_DESCRIPTOR_ATTRS __attribute__((section(".backup.bss"))) +#else +#define DMA_DESCRIPTOR_ATTRS +#endif + +/** + * @brief DMA channel type + */ +typedef unsigned dma_t; + +/** + * @brief Available DMA address increment modes + */ +typedef enum { + DMA_INCR_NONE = 0, /**< Don't increment any addresses after a beat */ + DMA_INCR_SRC = 1, /**< Increment the source address after a beat */ + DMA_INCR_DEST = 2, /**< Increment destination address after a beat */ + DMA_INCR_BOTH = 3, /**< Increment both addresses after a beat */ +} dma_incr_t; + +/** + * @brief Initialize DMA + */ +void dma_init(void); + +/** + * @brief Acquire a DMA channel. + * + * A free DMA channel is marked as allocated and a reference is returned. + * DMA channels can be acquired for long periods of time, e.g. from the start to + * end of a number of transfers or directly at boot and never released. + * + * @returns A reference to the DMA channel + * @returns UINT8_MAX when no DMA channel is available + */ +dma_t dma_acquire_channel(void); + +/** + * @brief Release a previously acquired DMA channel + * + * @param dma DMA channel to release + */ +void dma_release_channel(dma_t dma); + +/** + * @brief Initialize a previously allocated DMA channel with one-time settings + * + * @param dma DMA channel reference + * @param trigger Trigger to use for this DMA channel + * @param prio Channel priority + * @param irq Whether to enable the interrupt handler for this channel + */ +void dma_setup(dma_t dma, unsigned trigger, uint8_t prio, bool irq); + +/** + * @brief Prepare the DMA channel for an individual transfer. + * + * @param dma DMA channel reference + * @param width Transfer beat size to use + * @param src Source address for the transfer + * @param dst Destination address for the transfer + * @param len Number of beats to transfer + * @param incr Which of the addresses to increment after a beat + */ +void dma_prepare(dma_t dma, uint8_t width, void *src, void *dst, size_t len, + dma_incr_t incr); + +/** + * @brief Prepare a transfer without modifying the destination address + * settings. + * + * Can be used when repeatedly using a dma channel to transfer to the same + * peripheral address, leaving the destination address and related settings + * untouched + * + * @note This only touches the source address, length and source increment + * settings. Be sure to initialize the full descriptor beforehand with + * @ref dma_prepare + * + * @param dma DMA channel reference + * @param src Source address for the transfer + * @param len Number of beats to transfer + * @param incr Whether to increment the source address after a beat + */ +void dma_prepare_src(dma_t dma, void *src, size_t len, bool incr); + +/** + * @brief Prepare a transfer without modifying the source address + * settings. + * + * Can be used when repeatedly using a dma channel to transfer from the same + * peripheral address, leaving the source address and related settings + * untouched + * + * @note This only touches the destination address, length and destination + * increment settings. Be sure to initialize the full descriptor beforehand with + * @ref dma_prepare + * + * @param dma DMA channel reference + * @param dst Destination address for the transfer + * @param len Number of beats to transfer + * @param incr Whether to increment the destination address after a beat + */ +void dma_prepare_dst(dma_t dma, void *dst, size_t len, bool incr); + +/** + * @brief Append a second transfer descriptor after the default channel + * descriptor. + * + * @note Only a single extra transfer descriptor is supported for now. + * @note @p descriptor must remain valid throughout the full transfer duration + * + * @param dma DMA channel reference to add the descriptor to + * @param descriptor Extra transfer descriptor to append + * @param width Transfer beat size to use + * @param src Source address for the transfer + * @param dst Destination address for the transfer + * @param len Number of beats to transfer + * @param incr Which of the addresses to increment after a beat + */ +void dma_append(dma_t dma, DmacDescriptor *descriptor, uint8_t width, + void *src, void *dst, size_t len, dma_incr_t incr); + +/** + * @brief Append a second transfer descriptor after the default channel + * descriptor, copying destination and block size from the initial + * descriptor. + * + * @note Only a single extra transfer descriptor is supported for now. + * @note @p descriptor must remain valid throughout the full transfer duration + * + * @param dma DMA channel reference to add the descriptor to + * @param next Extra transfer descriptor to append + * @param src Source address for the transfer + * @param len Number of beats to transfer + * @param incr Whether to increment the source address after a beat + */ +void dma_append_src(dma_t dma, DmacDescriptor *next, void *src, size_t len, + bool incr); + +/** + * @brief Append a second transfer descriptor after the default channel + * descriptor, copying source and block size from the initial + * descriptor. + * + * @note Only a single extra transfer descriptor is supported for now. + * @note @p descriptor must remain valid throughout the full transfer duration + * + * @param dma DMA channel reference to add the descriptor to + * @param next Extra transfer descriptor to append + * @param dst Destination address for the transfer + * @param len Number of beats to transfer + * @param incr Whether to increment the source address after a beat + */ +void dma_append_dst(dma_t dma, DmacDescriptor *next, void *dst, size_t len, + bool incr); + +/** + * @brief Start a DMA transfer. + * + * @param dma DMA channel reference + */ +void dma_start(dma_t dma); + +/** + * @brief Wait for a DMA channel to finish the transfer. + * + * This function uses a blocking mutex to wait for the transfer to finish + * + * @note Use only with DMA channels of which the interrupt is enabled + * + * @param dma DMA channel reference + */ +void dma_wait(dma_t dma); + +/** + * @brief Cancel an active DMA transfer + * + * It is not harmful to call this on an inactive channel, but it will waste some + * processing time + * + * @param dma DMA channel reference + */ +void dma_cancel(dma_t dma); + #ifdef __cplusplus } #endif diff --git a/cpu/sam0_common/periph/dma.c b/cpu/sam0_common/periph/dma.c new file mode 100644 index 0000000000..86a2ed3503 --- /dev/null +++ b/cpu/sam0_common/periph/dma.c @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2020 Koen Zandberg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup cpu_sam0_common + * @{ + * + * @file + * @brief Low-level DMA driver implementation + * + * @author Koen Zandberg + * + * @} + */ + +#include "periph_cpu.h" +#include "periph_conf.h" +#include "mutex.h" +#include "assert.h" +#include "bitarithm.h" +#include "pm_layered.h" +#include "thread_flags.h" +#include "periph/gpio.h" + +#define ENABLE_DEBUG (0) +#include "debug.h" + +#ifndef CONFIG_DMA_NUMOF +#define CONFIG_DMA_NUMOF DMAC_CH_NUM +#endif + +/* In memory DMA transfer descriptors */ +static DmacDescriptor DMA_DESCRIPTOR_ATTRS descriptors[CONFIG_DMA_NUMOF]; +static DmacDescriptor DMA_DESCRIPTOR_ATTRS writeback[CONFIG_DMA_NUMOF]; + +/* Bitmap of dma channels available */ +static uint32_t channels_free = (1LLU << CONFIG_DMA_NUMOF) - 1; + +struct dma_ctx { + mutex_t sync_lock; +}; + +struct dma_ctx dma_ctx[CONFIG_DMA_NUMOF]; + +static void _poweron(void) +{ +#if defined(MCLK) + MCLK->AHBMASK.reg |= MCLK_AHBMASK_DMAC; +#else + PM->AHBMASK.reg |= PM_AHBMASK_DMAC; + PM->APBBMASK.reg |= PM_APBBMASK_DMAC; +#endif +} + +void dma_init(void) +{ + _poweron(); + for (unsigned i = 0; i < CONFIG_DMA_NUMOF; i++) { + mutex_init(&dma_ctx[i].sync_lock); + } + /* Enable all priorities with RR scheduling */ + DMAC->CTRL.reg = DMAC_CTRL_LVLEN0 | + DMAC_CTRL_LVLEN1 | + DMAC_CTRL_LVLEN2 | + DMAC_CTRL_LVLEN3; + DMAC->PRICTRL0.reg = DMAC_PRICTRL0_RRLVLEN0 | + DMAC_PRICTRL0_RRLVLEN1 | + DMAC_PRICTRL0_RRLVLEN2 | + DMAC_PRICTRL0_RRLVLEN3; + + DMAC->BASEADDR.reg = (uint32_t)descriptors; + DMAC->WRBADDR.reg = (uint32_t)writeback; + +#if defined(CPU_FAM_SAML11) || defined(CPU_FAM_SAML10) + NVIC_EnableIRQ(DMAC_0_IRQn); + NVIC_EnableIRQ(DMAC_1_IRQn); + NVIC_EnableIRQ(DMAC_2_IRQn); + NVIC_EnableIRQ(DMAC_3_IRQn); +#elif defined(CPU_FAM_SAMD5X) + NVIC_EnableIRQ(DMAC_0_IRQn); + NVIC_EnableIRQ(DMAC_1_IRQn); + NVIC_EnableIRQ(DMAC_2_IRQn); + NVIC_EnableIRQ(DMAC_3_IRQn); + NVIC_EnableIRQ(DMAC_4_IRQn); +#else + NVIC_EnableIRQ(DMAC_IRQn); +#endif + + DMAC->CTRL.bit.DMAENABLE = 1; +} + +dma_t dma_acquire_channel(void) +{ + dma_t channel = UINT8_MAX; + unsigned state = irq_disable(); + + if (channels_free) { + channel = bitarithm_lsb(channels_free); + /* Clear channel bit */ + channels_free &= ~(1 << channel); + /* ensure the sync lock is locked */ + mutex_trylock(&dma_ctx[channel].sync_lock); + } + irq_restore(state); + return channel; +} + +void dma_release_channel(dma_t dma) +{ + unsigned state = irq_disable(); +#ifdef REG_DMAC_CHID + DMAC->CHID.reg = dma; + /* Reset DMA channel */ + DMAC->CHCTRLA.reg = DMAC_CHCTRLA_SWRST; +#else + DMAC->Channel[dma].CHCTRLA.reg = DMAC_CHCTRLA_SWRST; +#endif + channels_free |= 1 << dma; + irq_restore(state); +} + +static inline void _set_source(DmacDescriptor *descr, void *src) +{ + descr->SRCADDR.reg = (uint32_t)src; +} + +static inline void _set_destination(DmacDescriptor *descr, void *dst) +{ + descr->DSTADDR.reg = (uint32_t)dst; +} + +static inline void _set_len(DmacDescriptor *descr, size_t len) +{ + descr->BTCNT.reg = len; +} + +static inline void _set_next_descriptor(DmacDescriptor *descr, void *next) +{ + descr->DESCADDR.reg = (uint32_t)next; +} + +void dma_setup(dma_t dma, unsigned trigger, uint8_t prio, bool irq) +{ +#ifdef REG_DMAC_CHID + /* Ensure that this set of register writes is atomic */ + unsigned state = irq_disable(); + DMAC->CHID.reg = dma; + DMAC->CHCTRLB.reg = DMAC_CHCTRLB_TRIGACT_BEAT | + (trigger << DMAC_CHCTRLB_TRIGSRC_Pos) | + (prio << DMAC_CHCTRLB_LVL_Pos); + /* Clear everything in case a previous user left it configured */ + DMAC->CHINTENCLR.reg = 0xFF; + if (irq) { + DMAC->CHINTENSET.reg = DMAC_CHINTENSET_TCMPL; + } + irq_restore(state); +#else + DMAC->Channel[dma].CHCTRLA.reg = DMAC_CHCTRLA_TRIGACT_BURST | + (trigger << DMAC_CHCTRLA_TRIGSRC_Pos); + DMAC->Channel[dma].CHPRILVL.reg = prio; + DMAC->Channel[dma].CHINTENCLR.reg = 0xFF; + if (irq) { + DMAC->Channel[dma].CHINTENSET.reg = DMAC_CHINTENSET_TCMPL; + } +#endif +} + +void dma_prepare(dma_t dma, uint8_t width, void *src, void *dst, size_t len, + uint8_t incr) +{ + DEBUG("[DMA]: Prepare %u, len: %u\n", dma, (unsigned)len); + DmacDescriptor *descr = &descriptors[dma]; + _set_len(descr, len); + _set_source(descr, src); + _set_destination(descr, dst); + descr->DESCADDR.reg = (uint32_t)NULL; + descr->BTCTRL.reg = width << DMAC_BTCTRL_BEATSIZE_Pos | + incr << DMAC_BTCTRL_SRCINC_Pos | + DMAC_BTCTRL_VALID; +} + +void dma_prepare_src(dma_t dma, void *src, size_t len, + bool incr) +{ + DEBUG("[dma]: %u: prep src %p, %u, %u\n", dma, src, (unsigned)len, incr); + DmacDescriptor *descr = &descriptors[dma]; + _set_len(descr, len); + _set_source(descr, src); + descr->BTCTRL.reg = (descr->BTCTRL.reg & ~DMAC_BTCTRL_SRCINC) | + (incr << DMAC_BTCTRL_SRCINC_Pos); + _set_next_descriptor(descr, NULL); +} + +void dma_prepare_dst(dma_t dma, void *dst, size_t len, + bool incr) +{ + DEBUG("[dma]: %u: prep dst %p, %u, %u\n", dma, dst, (unsigned)len, incr); + DmacDescriptor *descr = &descriptors[dma]; + _set_len(descr, len); + _set_destination(descr, dst); + descr->BTCTRL.reg = (descr->BTCTRL.reg & ~DMAC_BTCTRL_DSTINC) | + (incr << DMAC_BTCTRL_DSTINC_Pos); + _set_next_descriptor(descr, NULL); +} + +void _fmt_append(DmacDescriptor *descr, DmacDescriptor *next, + void *src, void *dst, size_t len) +{ + /* Configure the full descriptor besides the BTCTRL data */ + _set_next_descriptor(descr, next); + _set_next_descriptor(next, NULL); + _set_source(next, src); + _set_len(next, len); + _set_destination(next, dst); +} + +void dma_append(dma_t dma, DmacDescriptor *next, uint8_t width, + void *src, void *dst, size_t len, dma_incr_t incr) +{ + DmacDescriptor *descr = &descriptors[dma]; + + next->BTCTRL.reg = width << DMAC_BTCTRL_BEATSIZE_Pos | + incr << DMAC_BTCTRL_SRCINC_Pos | + DMAC_BTCTRL_VALID; + _fmt_append(descr, next, src, dst, len); +} + +void dma_append_src(dma_t dma, DmacDescriptor *next, void *src, size_t len, + bool incr) +{ + DmacDescriptor *descr = &descriptors[dma]; + + /* Copy the original descriptor config and modify the increment */ + next->BTCTRL.reg = (descr->BTCTRL.reg & ~DMAC_BTCTRL_SRCINC) | + (incr << DMAC_BTCTRL_SRCINC_Pos); + _fmt_append(descr, next, src, (void *)descr->DSTADDR.reg, len); +} + +void dma_append_dst(dma_t dma, DmacDescriptor *next, void *dst, size_t len, + bool incr) +{ + DmacDescriptor *descr = &descriptors[dma]; + + /* Copy the original descriptor config and modify the increment */ + next->BTCTRL.reg = (descr->BTCTRL.reg & ~DMAC_BTCTRL_DSTINC) | + (incr << DMAC_BTCTRL_DSTINC_Pos); + _fmt_append(descr, next, (void *)descr->SRCADDR.reg, dst, len); +} + +void dma_start(dma_t dma) +{ + DEBUG("[dma]: starting: %u\n", dma); + +#ifdef REG_DMAC_CHID + unsigned state = irq_disable(); + DMAC->CHID.bit.ID = dma; + DMAC->CHCTRLA.reg = DMAC_CHCTRLA_ENABLE; + irq_restore(state); +#else + DMAC->Channel[dma].CHCTRLA.bit.ENABLE = 1; +#endif +} + +void dma_wait(dma_t dma) +{ + DEBUG("[DMA]: mutex lock: %u\n", dma); + mutex_lock(&dma_ctx[dma].sync_lock); +} + +void dma_cancel(dma_t dma) +{ + DEBUG("[DMA]: Cancelling active transfer: %u\n", dma); +#ifdef REG_DMAC_CHID + unsigned state = irq_disable(); + DMAC->CHID.bit.ID = dma; + /* Write zero to the enable bit */ + DMAC->CHCTRLA.reg = 0; + /* Wait until the active beat is finished */ + while (DMAC->CHCTRLA.bit.ENABLE) {} + irq_restore(state); +#else + DMAC->Channel[dma].CHCTRLA.bit.ENABLE = 0; + while (DMAC->Channel[dma].CHCTRLA.bit.ENABLE) {} +#endif +} + +void isr_dmac(void) +{ + /* Always holds the interrupt status for the highest priority channel with + * pending interrupts */ + uint16_t status = DMAC->INTPEND.reg; + dma_t dma = status & DMAC_INTPEND_ID_Msk; + + /* Clear the pending interrupt flags for this channel by writing the + * channel ID together with the flags to clear */ + DMAC->INTPEND.reg = status; + if (status & DMAC_INTPEND_TCMPL) { + mutex_unlock(&dma_ctx[dma].sync_lock); + } + DEBUG("[DMA] IRQ: %u: %x\n", dma, status); + cortexm_isr_end(); +} + +void isr_dmac0(void) +{ + isr_dmac(); +} + +void isr_dmac1(void) +{ + isr_dmac(); +} + +void isr_dmac2(void) +{ + isr_dmac(); +} + +void isr_dmac3(void) +{ + isr_dmac(); +} + +void isr_dmac4(void) +{ + isr_dmac(); +}