SAMD51 DAC using DMA seems too fast?

Hello Everyone,
I'm working on implementing some code using the SAMD51 on the Adafruit Metro M4 express.
I create a single period sinusoid (4000 samples long) and block-write it using DMA to the DAC at what I thought was 1M Sample/second.
Problem is: the sinusoid when looked at on the scope has a period of 200uS - which would make it a whopping 20M Samples/second - seems incorrect....
the DMA is triggered every 800uS - which looks good on the scope (time between pulses).
Can anyone tell me what I'm missing here - I'm sure that I'll be kicking myself...
thank you in advance for any help!

Code using arduino IDE follows........

#define HWORDS 4000
uint16_t data[HWORDS];
float phase = 3.14159 * 2./HWORDS;
int i;
static DmacDescriptor descriptor1 attribute((aligned(16)));
void dma_init() {

static DmacDescriptor descriptor attribute((aligned(16)));
static DmacDescriptor descriptor1 attribute((aligned(16)));
static DmacDescriptor wrb attribute((aligned(16)));
static uint32_t chnl0 = 0; // DMA channel
DMAC->BASEADDR.reg = (uint32_t)&descriptor1;
DMAC->WRBADDR.reg = (uint32_t)&wrb;
DMAC->CTRL.bit.LVLEN0 =1 ;
DMAC->CTRL.bit.LVLEN1 =1 ;
DMAC->CTRL.bit.LVLEN2 =1 ;
DMAC->CTRL.bit.LVLEN3 =1 ;
DMAC->CTRL.bit.DMAENABLE = 1;
DMAC->Channel[0].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(0x0) | // Set DMAC t0 software trigger
DMAC_CHCTRLA_TRIGACT_BLOCK; // DMAC block transfer
descriptor1.DESCADDR.reg = (uint32_t) &descriptor1;
descriptor1.BTCTRL.bit.VALID = 0x1; //Its a valid channel
descriptor1.BTCTRL.bit.BEATSIZE = 0x1; // HWORD.
descriptor1.BTCTRL.bit.SRCINC = 0x1; //Source increment is enabled
descriptor1.BTCTRL.bit.DSTINC = 0x0; //Destination increment disabled
descriptor1.BTCNT.reg = HWORDS; //HWORDS points to send
descriptor1.SRCADDR.reg = (uint32_t)&data + 2*HWORDS; //send from the data vevtor
descriptor1.DSTADDR.reg = (uint32_t)&DAC->DATA[1].reg; //to the DAC output
// start channel
DMAC ->Channel[0].CHCTRLA.bit.ENABLE = 0x1; //OK
DMAC->CTRL.bit.DMAENABLE = 1;
}
void dac_init(){
GCLK->GENCTRL[7].reg = GCLK_GENCTRL_DIV(4) | // Divide the 48MHz clock source by divisor 4: 48MHz/4 = 12MHz (max for DAC)
GCLK_GENCTRL_GENEN | // Enable GCLK7
GCLK_GENCTRL_SRC_DFLL; // Select 48MHz DFLL clock source

while (GCLK->SYNCBUSY.bit.GENCTRL7);
GCLK->PCHCTRL[42].reg = GCLK_PCHCTRL_CHEN | // Enable the DAC peripheral channel
GCLK_PCHCTRL_GEN_GCLK7; // Connect generic clock 7 to DAC
MCLK->APBDMASK.bit.DAC_ = 1;
DAC->CTRLA.bit.SWRST = 1;
while(DAC->CTRLA.bit.SWRST);
DAC->DACCTRL[1].reg = DAC_DACCTRL_REFRESH(2) |
DAC_DACCTRL_CCTRL_CC12M |
DAC_DACCTRL_ENABLE;// |
DAC_DACCTRL_LEFTADJ;
DAC->CTRLA.reg = DAC_CTRLA_ENABLE;
while(DAC->SYNCBUSY.bit.ENABLE);
while(!DAC->STATUS.bit.READY1);
PORT->Group[0].DIRSET.reg = (1 << 2);
PORT->Group[0].PINCFG[5].bit.PMUXEN = 1;
PORT->Group[0].PMUX[1].bit.PMUXE = 1;
}
void setup() {
for (i=0;i<HWORDS;i++) data_= 4*(sinf(i*phase) * 510.0f + 512.0f); //Make a single-period sine wave._

  • dac_init();*
  • dma_init(); *
    }
    void loop() {
  • DMAC->SWTRIGCTRL.reg |= (1 << 0); //Software trigger to set off the DAC*
  • delayMicroseconds(800);*
    }


DAC_ONLY.ino (2.9 KB)

Hi dsp_mike,

The SAMD51's DAC is only capable of 1MSPS, therefore 4000 samples gives a maximum sine wave frequency of:

1000000 samples per second / 4000 = 250Hz

Also, I noticed that you've specifed a DMAC block transfer rather than a burst transfer. A burst transfer causes the DMAC to load a single sine wave table element into the DAC's DATA register.

Unless you have a particular reason to do so, it's also possible to trigger a sample using a timer rather than a software trigger in the loop().

To set the trigger to occur on a TC0 timer overflow (OVF):

DMAC->Channel[5].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(TC0_DMAC_ID_OVF) |  // Set DMAC to trigger when TC0 timer overflows
                                DMAC_CHCTRLA_TRIGACT_BURST;             // DMAC burst transfer

To set up the TC0 timer:

GCLK->PCHCTRL[TC0_GCLK_ID].reg = GCLK_PCHCTRL_CHEN |                    // Enable perhipheral channel for TC0
                                 GCLK_PCHCTRL_GEN_GCLK1;                // Connect generic clock 0 at 48MHz
 
TC0->COUNT16.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;                           // Set TC0 to Match Frequency (MFRQ) mode
TC0->COUNT16.CC[0].reg = 47;                                            // Set the sine wave frequency to 1kHz: 48MHz / (1000 * 1000) - 1
while (TC0->COUNT16.SYNCBUSY.bit.CC0);                                  // Wait for synchronization

TC0->COUNT16.CTRLA.bit.ENABLE = 1;                                      // Enable the TC0 timer
while (TC0->COUNT16.SYNCBUSY.bit.ENABLE);                               // Wait for synchronization

The TC0's CC0 register value is determined by the sine wave frequency and number of samples:

CC0 = 48MHz (timer clock) / (sine wave frequency * number of samples) - 1

Note that the (sine wave frequency * number of samples) value should not exceed the 1MSPS limit.

I've been contributing to this questions over on the adafruit forums.
https://forums.adafruit.com/viewtopic.php?f=63&t=170522

(I can't get the example to work, though.)

Hi westfw,

The 1 million samples per second steps that you measured at the DAC output is consistent with the SAMD51 datasheet, which means that it's not possible for dsp_mike to use 4000 samples to generate a 1kHz sine wave output.

Also, I agree with what you said over on the Adafruit forum that the DMA is essentially outputting the sine wave table at full speed. Well at least that seems what it's doing.

This is because the DMA has been configured to transfer a "block" of data, whereupon receipt of a single trigger will cause it to transfer the entire sine table array to the DAC at full speed. The software trigger in the loop() function causes the DMA to repeat this action every 800us.

Configuring the DMA for "burst" transfer instead, gets it to transfer only one element in the sine wave array table to the DAC upon receipt of a trigger. This issue then becomes how to time each burst trigger, to get the DAC to output each sine wave array element at the required sine wave period. This can be handled by using a timer to trigger the DMA at a specified interval.

If the DMA descriptor's "next descriptor address" references itself:

descriptor.descaddr = (uint32_t)&descriptor_section[5];                 // Set up a circular descriptor

... then the DMA will loopback can repeat the process indefinitely, generating a continous sine wave.

If on the other hand the next descriptor address is set to 0:

descriptor.descaddr = 0;

... then the DMA will only output a single sine wave period, each time the DMA channel is enabled.

Here's some code that uses the DMA in burst transfer mode in conjuction with the TC0 timer, to output a continous 1kHz sine wave, 0V to 3.3V amplitude with 1000 samples:

// Use SAMD51's DMAC to generate a 1kHz sine wave 0 to 3.3V amplitude on A0 using DAC0
#define SAMPLE_NO 1000

uint16_t sintable[SAMPLE_NO];                                             // Sine table

typedef struct                                                            // DMAC descriptor structure
{
  uint16_t btctrl;
  uint16_t btcnt;
  uint32_t srcaddr;
  uint32_t dstaddr;
  uint32_t descaddr;
} dmacdescriptor ;

volatile dmacdescriptor wrb[DMAC_CH_NUM] __attribute__ ((aligned (16)));          // Write-back DMAC descriptors
dmacdescriptor descriptor_section[DMAC_CH_NUM] __attribute__ ((aligned (16)));    // DMAC channel descriptors
dmacdescriptor descriptor __attribute__ ((aligned (16)));                         // Place holder descriptor

void setup() {
  DMAC->BASEADDR.reg = (uint32_t)descriptor_section;                      // Specify the location of the descriptors
  DMAC->WRBADDR.reg = (uint32_t)wrb;                                      // Specify the location of the write back descriptors
  DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf);            // Enable the DMAC peripheral

  for (uint16_t i = 0; i < SAMPLE_NO; i++)                                // Calculate the sine table with 1000 entries
  {
    sintable[i] = (uint16_t)((sinf(2 * PI * (float)i / SAMPLE_NO) * 2047.0f) + 2048.0f);    // 12-bit resolution with +1.63V offset
  }
  
  DAC->DACCTRL[0].bit.CCTRL = 1;                                          // Set the DAC's current control to allow output to operate at 1MSPS
  analogWriteResolution(12);                                              // Set the DAC's resolution to 12-bits
  analogWrite(A0, 0);                                                     // Initialise DAC0
 
  DMAC->Channel[5].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(TC0_DMAC_ID_OVF) |  // Set DMAC to trigger when TC0 timer overflows
                                 DMAC_CHCTRLA_TRIGACT_BURST;              // DMAC burst transfer
  descriptor.descaddr = (uint32_t)&descriptor_section[5];                 // Set up a circular descriptor
  descriptor.srcaddr = (uint32_t)&sintable[0] + SAMPLE_NO * sizeof(uint16_t);  // Read the current value in the sine table
  descriptor.dstaddr = (uint32_t)&DAC->DATA[0].reg;                       // Copy it into the DAC data register
  descriptor.btcnt = SAMPLE_NO;                                                // This takes the number of sine table entries = 1000 beats
  descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD |                        // Set the beat size to 16-bits (Half Word)
                      DMAC_BTCTRL_SRCINC |                                // Increment the source address every beat
                      DMAC_BTCTRL_VALID;                                  // Flag the descriptor as valid
  memcpy((void*)&descriptor_section[5], &descriptor, sizeof(dmacdescriptor));  // Copy to the channel 5 descriptor 

  GCLK->PCHCTRL[TC0_GCLK_ID].reg = GCLK_PCHCTRL_CHEN |                     // Enable perhipheral channel for TC0
                                   GCLK_PCHCTRL_GEN_GCLK1;                // Connect generic clock 1 at 48MHz
 
  TC0->COUNT16.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;                           // Set TC0 to Match Frequency (MFRQ) mode
  TC0->COUNT16.CC[0].reg = 47;                                            // Set the sine wave frequency to 1kHz: 48MHz / (1000 * 1000) - 1
  while (TC0->COUNT16.SYNCBUSY.bit.CC0);                                  // Wait for synchronization

  TC0->COUNT16.CTRLA.bit.ENABLE = 1;                                      // Enable the TC0 timer
  while (TC0->COUNT16.SYNCBUSY.bit.ENABLE);                               // Wait for synchronization
 
  DMAC->Channel[5].CHCTRLA.bit.ENABLE = 1;                                // Enable DMAC on channel 5
}

void loop(){}

Here's the output:

Sine.png

Sine.png

Configuring the DMA for "burst" transfer instead, gets it to transfer only one element in the sine wave array table to the DAC upon receipt of a trigger.

Ah hah! The DMA docs talk about "beats", "bursts", and "blocks", but the register settings for "Trigger Action" talk about "Blocks", "Bursts", and "Transactions." Since I was pretty sure I wanted one "beat" for each trigger, I was ... confused. Setting the action to "transaction" turns out to behave the same as "block", and I was SO SURE that I didn't want "bursts."
Sigh. I DID want bursts. Apparently the size of bursts is controllable separately, down to a single "beat."
So changing the code to:

     DMAC->Channel[0].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(0x49) |   // Set DMAC t0 software trigger
                                    DMAC_CHCTRLA_TRIGACT_BURST;

Seems to get a lot closer to the behavior I was looking for: self-clocked by the DAC itself, and resulting in a nice 250Hz waveform from the 4000-element table and 12MHz DAC Clock.
It doesn't stop after a single waveform; I need to do some more playing with that, I guess...

Ah hah! The DMA docs talk about "beats", "bursts", and "blocks", but the register settings for "Trigger Action" talk about "Blocks", "Bursts", and "Transactions." Since I was pretty sure I wanted one "beat" for each trigger, I was ... confused. Setting the action to "transaction" turns out to behave the same as "block", and I was SO SURE that I didn't want "bursts."

Yes, the SAMD51's datasheet is confusing, as there appears to be a mismatch in terminology between the DMA descriptors and the DMA peripheral registers. It turns out that a DMA descriptor "beat" transfer is actually "single beat burst", as far as the DMA peripheral registers are concerned.

Just to make things more confusing, the SAMD21's DMA peripheral registers only support "beat", "block" and "transaction" options, even though its datasheet contains the same descriptor explanation as the SAMD51's that describes "burst" transfers. It took me quite some time messing around with the DMA to find out what was actually going on and that burst transfers weren't an option.

Seems to get a lot closer to the behavior I was looking for: self-clocked by the DAC itself, and resulting in a nice 250Hz waveform from the 4000-element table and 12MHz DAC Clock.

That's interesting and looks like a more elegant solution than my suggestion, as it dispenses with the timer. So are you just letting the DAC run at full speed at 1MS/s and let it trigger the DMA transfer instead?

Yep. Here's the "final" experiment, which has both the fixed triggering, and uses Suspend/Resume to output one waveform at a time...

#define HWORDS 4000
uint16_t data[HWORDS];
float phase = 3.14159 * 2. / HWORDS;
int i;
static DmacDescriptor descriptor1 __attribute__((aligned(16)));
void dma_init() {

  static DmacDescriptor descriptor __attribute__((aligned(16)));
  static DmacDescriptor descriptor1 __attribute__((aligned(16)));
  static DmacDescriptor wrb __attribute__((aligned(16)));
  static uint32_t chnl0 = 0;  // DMA channel
  DMAC->BASEADDR.reg = (uint32_t)&descriptor1;
  DMAC->WRBADDR.reg = (uint32_t)&wrb;
  DMAC->CTRL.bit.LVLEN0 = 1 ;
  DMAC->CTRL.bit.LVLEN1 = 1 ;
  DMAC->CTRL.bit.LVLEN2 = 1 ;
  DMAC->CTRL.bit.LVLEN3 = 1 ;
  DMAC->CTRL.bit.DMAENABLE = 1;
  DMAC->Channel[0].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(0x49) |   // DAC DATA trigger
                                 DMAC_CHCTRLA_TRIGACT_BURST;    // Burst transfers
  descriptor1.DESCADDR.reg = (uint32_t) &descriptor1;
  descriptor1.BTCTRL.bit.VALID    = 0x1; //Its a valid channel
  descriptor1.BTCTRL.bit.BEATSIZE = 0x1;  // HWORD.
  descriptor1.BTCTRL.bit.SRCINC   = 0x1;   //Source increment is enabled
  descriptor1.BTCTRL.bit.DSTINC   = 0x0;   //Destination increment disabled
  descriptor1.BTCTRL.bit.BLOCKACT = 0x2;   //Suspend after block complete.
  // ("Burst" size will be 1 "beat" = 1 HWORD, by default)
  descriptor1.BTCNT.reg           = HWORDS;   //HWORDS points to send
  descriptor1.SRCADDR.reg         = (uint32_t)(&data[HWORDS]); //send from the data vevtor
  descriptor1.DSTADDR.reg         = (uint32_t)&DAC->DATA[1].reg;   //to the DAC output
  // start channel
  DMAC ->Channel[0].CHCTRLA.bit.ENABLE = 0x1;     //OK
  DMAC->CTRL.bit.DMAENABLE = 1;
}

void dac_init() {
 
 GCLK->GENCTRL[7].reg = GCLK_GENCTRL_DIV(4) |       // Divide the 
48MHz clock source by divisor 4: 48MHz/4 = 12MHz (max for DAC)
                         GCLK_GENCTRL_GENEN |        // Enable GCLK7
                         GCLK_GENCTRL_SRC_DFLL;      // Select 48MHz DFLL clock source

  while (GCLK->SYNCBUSY.bit.GENCTRL7);
  GCLK->PCHCTRL[42].reg = GCLK_PCHCTRL_CHEN |        // Enable the DAC peripheral channel
                          GCLK_PCHCTRL_GEN_GCLK7;    // Connect generic clock 7 to DAC
  MCLK->APBDMASK.bit.DAC_ = 1;
  DAC->CTRLA.bit.SWRST = 1;
  while (DAC->CTRLA.bit.SWRST);
  DAC->DACCTRL[1].reg = DAC_DACCTRL_REFRESH(2) |
                        DAC_DACCTRL_CCTRL_CC12M |
                        DAC_DACCTRL_ENABLE
                        //                        | DAC_DACCTRL_FEXT
                        ;
  DAC_DACCTRL_LEFTADJ;
  DAC->CTRLA.reg = DAC_CTRLA_ENABLE;
  while (DAC->SYNCBUSY.bit.ENABLE);
  while (!DAC->STATUS.bit.READY1);
  PORT->Group[0].DIRSET.reg = (1 << 2);
  PORT->Group[0].PINCFG[5].bit.PMUXEN = 1;
  PORT->Group[0].PMUX[1].bit.PMUXE = 1;
}

void setup() {
  for (i = 0; i < HWORDS; i++) {
    data[i] = 4 * (sinf(i * phase) * 510.0f + 512.0f); //Make a single-period sine wave.
  }
#ifdef ANALOGWRITESQUARE
  //  for (i = 0; i < 100000; i++) {
  while (0) {
    analogWrite(A0, 999);
    delayMicroseconds(100);
    analogWrite(A0, 0);
    delayMicroseconds(100);
  }
#endif


#ifdef ANALOGWRITERAMP
  while (1) {
    i++;
    analogWrite(A0, i & 0x3FF);
  }
#endif
  dac_init();
  dma_init();
}

//#define ANALOGWRITESINE 1
void loop() {
#ifdef ANALOGWRITESINE
  for (i = 0; i < HWORDS; i++) {
    analogWrite(A0, data[i]);
  }
#endif
  DMAC ->Channel[0].CHCTRLB.reg = 0x2;     // resume
  delayMicroseconds(8000);
}

Nice.

Thank you everyone for your help!
This thread definitely answered the question!
MartinL - thanks definitely appreciate all the help!