Hi sthudium,
Yes, this is true.
The SAMD21 on the Arduino Zero has a 12 channel DMAC (Direct Memory Access Controller). The DMAC can be used to move data from memory to memory, peripheral to memory, memory to peripheral or peripheral to peripheral, independently of the CPU.
This means the DMAC can autonomously read and write data to/from the ADC, DAC, TCC/TC timers, serial ports, SPI and I2C peripherals to/from the SAMD21's memory. The DMAC is especially useful in situations that requires a large amount of data, such as a display's frame buffer, or for peripherals that require constant attention without constantly interrupting the CPU with interrupt service routines (ISR).
I found mantoui's excellent DMA code on github really helpful: GitHub - manitou48/ZERO.
At the heart of the DMAC is the descriptor. This is a data structure held in SRAM that the microcontroller uses to control the DMA transfer:
typedef struct {
uint16_t btctrl;
uint16_t btcnt;
uint32_t srcaddr;
uint32_t dstaddr;
uint32_t descaddr;
} dmacdescriptor ;
volatile dmacdescriptor wrb[12] __attribute__ ((aligned (16)));
dmacdescriptor descriptor_section[12] __attribute__ ((aligned (16)));
dmacdescriptor descriptor __attribute__ ((aligned (16)));
It describes:
btcntrl - (block transfer control) the type of data to be sent/received, be it BYTE, HWORD (half word) or WORD known as the beat size and whether the source or destrination address is incremented during each read, (useful for reading or writing from/to sequential memory locations)
btcnt - (block transfer count) the number of BYTEs, HWORDs or WORDs to transfer
srcaddr - the source address of the data to be transfered, (you actually enter the address the data at the end of your data block: source address + data size in bytes)
destaddr - the destination address of the data to be transfered, (you actually enter the address the data at the end of your data block: destination address + data size in bytes)
descaddr - the address of the next descriptor, which allows descriptors to be chained as a linked list so that they can be executed sequentially, the last descriptor in the list is loaded with address 0, (so if you're using only one descriptor for the transfer descaddr is 0)
In the code above the "descriptor" declaration holds the current transfer descriptor.
There are 12 "descriptor_section[]" array elements, one for each of the DMAC's 12 channels, but you can have many more than this by linking the descriptors together, hence why the they're stored in SRAM. This allows you to chain a number of different sequential transfers, perhaps reading then writing, all independent of the CPU.
The wrb[] (write back) descriptor array is used to hold a descriptor in the event that the current transfer is interrupted by a DMA transfer with a higher priory (see below). It's holds the descriptor pending the completion of a higher priorty transfer, whereupon the wrb is copied back to the descriptor to continue the transfer.
The other DMAC register worthy of note is the CHCTRLB (Channel Control B) register. This controls the channel's priority level, the trigger source and the trigger action.
DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(0) |
DMAC_CHCTRLB_TRIGSRC(ADC_DMAC_ID_RESRDY) | DMAC_CHCTRLB_TRIGACT_BEAT;
In terms of priority the DMAC channels go in ascending order from 0 the highest to 12 the lowest. However, in addition each channel can be assigned a priory level that goes (confusingly) in descending order from 3 the highest to 0 the lowest that overrides the channel number ordering. The channel selection is determined by the DMAC's arbiter.
The trigger source is the interrupt or event that causes a transfer to occur, this could be for example a timer overflow (OVF), or serial port's receive complete (RXC), or on the other hand it could be an event or software trigger from your loop().
The trigger action describes the block size of the data that's transfered for each trigger, either BEAT, BLOCK or TRANSACTION. If you're transfering to/from peripherals this is usually set to BEAT.
Optionally the DMAC's interrupt sevice routine can be called, should CPU intervention be required at any stage:
void DMAC_Handler() {}
This can be used to service for example the DMAC's interrupt flags.
Once the DMAC and its descriptors have been set-up, a transfer can be initiated by simply selecting the DMAC channel and enabling it in the CHCTRLA (Channel Control A) register:
DMAC->CHID.reg = DMAC_CHID_ID(0);
DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE;
These two lines can be used repeatedly to start the transfer, (without having to initialise the descriptor each time).
The SAMD21's datasheet provides a full description of the DMAC's operation.
All in all the DMAC can be a powerful tool that can significantly maximise your CPU's performance. I guess the only downside is that its implementation is tied to the microcontroller, making the code less portable between processors.