Go Down

Topic: Using ADC with DMA (Read 2126 times) previous topic - next topic

sthudium

Dec 22, 2017, 06:43 pm Last Edit: Dec 22, 2017, 07:32 pm by sthudium
This is a continuation of the earlier post, speeding up analogread() at the Arduino Zero.

In that post, the author provides code that continuously executes ADC conversions, and places each sample in DMA, all at a 2 usec rate.  For this to be useful, three capabilities are needed:

1.  The main sketch is allowed to periodically extract a current sample, from one of the many in the DMA, and on demand, and while the DMA is filling with additional samples.
2.  The ADC routine does not compete with the main sketch for microprocessor cycles (i.e. the main sketch runs independently of the ADC).  This effectively means that the microprocessor executes two threads simultaneously and independently -- a multi-thread processor.
3.  Multiple analog inputs.

Is this true.  How is it setup to do so? 

If not, then the scheme is merely to fill a buffer with ADC samples, wait the time needed to fill the DMA, and THEN, process them in the main sketch.
I study the use of cognitive dissonance and confirmation bias to manipulate gullible people.

MartinL

#1
Dec 23, 2017, 11:41 am Last Edit: Dec 23, 2017, 11:19 pm by MartinL
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: https://github.com/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:

Code: [Select]
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.

Code: [Select]
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:

Code: [Select]
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:
 
Code: [Select]
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.

sthudium

Thanks, MArtinL... that's an elegant solution.

Also, could the original scheme work if the DMA address range is limited to only one address.  I think that would work, as long as the ADC-DMA operation runs parallel to the sketch code.
I study the use of cognitive dissonance and confirmation bias to manipulate gullible people.

MartinL

#3
Dec 24, 2017, 10:28 am Last Edit: Dec 24, 2017, 10:31 am by MartinL
Yes, by default the DMA address range is limited to one address. If for example you're logging data from the ADC RESULT register to the SAMD21's memory, you only need to increment the SAMD21's memory (destination) address each time the ADC is read.

If you take mantoui's "adcdma.ino" code:

Code: [Select]
DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(0) |
DMAC_CHCTRLB_TRIGSRC(ADC_DMAC_ID_RESRDY) | DMAC_CHCTRLB_TRIGACT_BEAT;
descriptor.descaddr = 0;
descriptor.srcaddr = (uint32_t) &ADC->RESULT.reg;
descriptor.btcnt =  hwords;
descriptor.dstaddr = (uint32_t)rxdata + hwords*2;   // end address
descriptor.btctrl =  DMAC_BTCTRL_BEATSIZE_HWORD | DMAC_BTCTRL_DSTINC | DMAC_BTCTRL_VALID;
memcpy(&descriptor_section[chnl],&descriptor, sizeof(dmacdescriptor));

Note that the data is read as a 16-bit HWORD (half word) from the ADCs result register (ADC->RESULT.reg). The DMAC copies the data from the ADC's result register to memory each time it receives a ADC_DMAC_ID_RESRDY (result ready) trigger and that the destination (memory) address is incremented each time (DMAC_BTCTRL_DSTINC).

So it's possible to specify addresses from one to one, one to many, many to one or many to many depending upon the situation.

Go Up