0

Feather M0 Sine Wave generator using ZeroDMA
Moderators: adafruit_support_bill, adafruit

Please be positive and constructive with your questions and comments.

Re: Feather M0 Sine Wave generator using ZeroDMA

by MartinL2 on Wed Jul 03, 2019 8:18 am

Hi BDL,

Here's some code that performs a sine wave frequency sweep between 100Hz and 10kHz using 32 samples per cycle. A frequency of 10kHz gives a conversion rate of 10kHz * 32 = 320k samples per second. The limitation being the DAC's conversion rate at 350k samples per second:

Code: Select all | TOGGLE FULL SIZE
// Output sine wave with frequency sweep between 100Hz and 10kHz (32 samples) on analog pin A0 using the DAC and DMAC
volatile uint16_t sintable1[32];

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

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

void setup()
{
  for (uint16_t i = 0; i < 32; i++)                                // Calculate the sine table with 32 entries
  {
    sintable1[i] = (uint16_t)((sinf(2 * PI * (float)i / 32) * 511) + 512);
  }

  analogWriteResolution(10);                                        // Set the DAC's resolution to 10-bits
  analogWrite(A0, 0);                                               // Initialise the DAC
 
  DMAC->BASEADDR.reg = (uint32_t)descriptor_section;                // Set the descriptor section base address
  DMAC->WRBADDR.reg = (uint32_t)wrb;                                // Set the write-back descriptor base adddress
  DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf);      // Enable the DMAC and priority levels

  DMAC->CHID.reg = DMAC_CHID_ID(0);                                 // Select DMAC channel 0
  DMAC->CHINTENSET.reg = DMAC_CHINTENSET_SUSP;                      // Enable suspend channel interrupts on each channel
  DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(0) |                         // Set DMAC priority to level 0 (lowest)
                      DMAC_CHCTRLB_TRIGSRC(TCC0_DMAC_ID_OVF) |      // Trigger on timer TCC0 overflow
                      DMAC_CHCTRLB_TRIGACT_BEAT;                    // Trigger every beat
  descriptor.descaddr = (uint32_t)&descriptor_section[0];                   // Set up a circular descriptor
  descriptor.srcaddr = (uint32_t)&sintable1[0] + 32 * sizeof(uint16_t);    // Read the current value in the sine table
  descriptor.dstaddr = (uint32_t)&DAC->DATA.reg;                            // Copy it into the DAC data register
  descriptor.btcnt = 32;                                                   // This takes the number of sine table entries = 32 beats
  descriptor.btctrl = DMAC_BTCTRL_BLOCKACT_SUSPEND |                // Suspend DMAC channel at end of block transfer
                      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[0], &descriptor, sizeof(dmacdescriptor));  // Copy to the channel 0 descriptor 
 
  NVIC_SetPriority(DMAC_IRQn, 0);           // Set the Nested Vector Interrupt Controller (NVIC) priority for the DMAC to 0 (highest)
  NVIC_EnableIRQ(DMAC_IRQn);                // Connect the DMAC to the Nested Vector Interrupt Controller (NVIC)

  GCLK->GENDIV.reg = GCLK_GENDIV_DIV(1) |          // Divide the 48MHz clock source by divisor 1: 48MHz/1=48MHz
                     GCLK_GENDIV_ID(4);            // Select Generic Clock (GCLK) 4
  while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization

  GCLK->GENCTRL.reg = GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                      GCLK_GENCTRL_GENEN |         // Enable GCLK4
                      GCLK_GENCTRL_SRC_DFLL48M |   // Set the 48MHz clock source
                      GCLK_GENCTRL_ID(4);          // Select GCLK4
  while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization

  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TCC0 and TCC1
                      GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                      GCLK_CLKCTRL_ID_TCC0_TCC1;   // Feed GCLK4 to TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization

  TCC0->WAVE.reg = TCC_WAVE_WAVEGEN_NFRQ;          // Setup TCC0 in Normal Frequency (NFRQ) mode
  while (TCC0->SYNCBUSY.bit.WAVE);                 // Wait for synchronization
 
  TCC0->PER.reg = 14999;                           // 100Hz sine wave, 32 samples: 48MHz / (100 * 32) - 1
  while(TCC0->SYNCBUSY.bit.PER);                   // Wait for synchronization
 
  TCC0->CTRLA.reg = TCC_CTRLA_PRESCALER_DIV1;      // Set the TCC0 prescaler to 1 giving 48MHz (20.83ns) timer tick
  TCC0->CTRLA.bit.ENABLE = 1;                      // Enable the TCC0 output
  while (TCC0->SYNCBUSY.bit.ENABLE);               // Wait for synchronization
 
  DMAC->CHID.reg = DMAC_CHID_ID(0);                // Select DMAC channel
  DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE;        // Enable DMAC channel
}

void loop() {}

void DMAC_Handler()
{
  static uint16_t period = 14999;                         // Initialise 100Hz sine wave, 32 samples: 48MHz / (100 * 32) - 1

  DMAC->CHID.reg = DMAC_CHID_ID(DMAC->INTPEND.bit.ID);    // Find the DMAC channel generating the interrupt
  descriptor_section[0].btctrl &= ~DMAC_BTCTRL_VALID;     // Disable the descriptor   
  TCC0->PERB.reg = period;                                // Change the sine wave's period
  while(TCC0->SYNCBUSY.bit.PERB);                         // Wait for synchronization
  period -= 100;                                          // Decrement the period counter
  period =  period < 149 ? 14999 : period;                // Reset the period a end of frequency sweep
  descriptor_section[0].btctrl |= DMAC_BTCTRL_VALID;      // Enable the descriptor                       
  DMAC->CHCTRLB.reg |= DMAC_CHCTRLB_CMD_RESUME;           // Resume the DMAC channel
  DMAC->CHINTFLAG.bit.SUSP = 1;                           // Clear the DMAC channel suspend (SUSP) interrupt flag 
}
Last edited by MartinL2 on Wed Jul 03, 2019 10:14 am, edited 1 time in total.

MartinL2
 
Posts: 34
Joined: Sun Mar 10, 2019 2:17 pm

Re: Feather M0 Sine Wave generator using ZeroDMA

by BDL on Wed Jul 03, 2019 9:31 am

Hi MartinL2,

Wow, thanks for these code samples! I'll study and run them. Hopefully this will get me on the right track. Appreciate your help very much!

BDL
 
Posts: 68
Joined: Wed Jan 09, 2019 10:45 pm

Re: Feather M0 Sine Wave generator using ZeroDMA

by adafruit2 on Wed Jul 03, 2019 12:48 pm


adafruit2
Site Admin
 
Posts: 18652
Joined: Fri Mar 11, 2005 7:36 pm

Re: Feather M0 Sine Wave generator using ZeroDMA

by BDL on Wed Jul 03, 2019 4:34 pm

Cool libraries. (from adafruit)

Found two problems in my code. First is I did a SW reset, midway through dma_init() which might have messed up some of the earlier configuration settings. I commented that out. Now the sinewave re-triggers. So at least I'm getting into the DMAC_Handler. Yay. Previous to that, I only output 1 cycle.

Another issue is the re-trigger time, I'm getting really long retriggers, like 5-40ms. The retrigger time is equal to the time to calculate out the new sine table. (Using floats on the poor M0) At the moment I'm doing that in the dmac handler... The short retrigger time is when the array is the smallest. The long retrigger time is obviously for the longest arrays. I was hoping to keep the timer period constant (DAC updates being constant) and just increase the number of points. Lower frequencies would have more points. I think this would give me better spectrum (not so many harmonics).

Third issue with my code is that it works for a while, but dies after updating the value. I suspect this is due to me not stopping the DMA or pausing it to update all the values. My primitive method only works some of the time.

MartinL2's code, changes the timer period and keeps the number of sine points the same. Seems MartinL2's code is better suited for the limited abilities of the M0. I'll play about with his version some more, to see if I can get something a little closer to what I was hoping for. MartinL2, your code has been a great help.

Hmm, wonder what this would be like with an M4 (SAMD51)? Faster clock rates, certainly, and a few more bits on the DAC. Pondering...

If anyone is still following along, here is my latest code.
Code: Select all | TOGGLE FULL SIZE
//  dacdma
//  TCC timer   OVF triggers DMA to write to DAC (A0)

// use pin 12 for PWM TCC0 channel 3
// Based on mantoui's code posted at https://github.com/manitou48/ZERO/blob/master/dacdma.ino'
// Slowly evolving to a sine wave generator, one baby step at a time
// BDL 070119

#define PWMPIN 12
#define TCCx TCC0
#define TCCchannel 3

#define syncTCC while (TCCx->SYNCBUSY.reg & TCC_SYNCBUSY_MASK)

// TCC freq = 48mhz/period
uint16_t PERIOD = 0x10;  // 48MHz/16 = 3 MHz, which is ok
uint16_t DUTY = PERIOD/2;
volatile bool first = true;

float twopi = 8.0f * atanf( 1.0f );  // why guess at 2*pi, 2pi = 8xpi/4
float dcoffset = 512.f;  // in counts
float scale    = 500.f;  // in counts, not volts (not quite fullscale)

// we can vary between 64 and 8192 to get the frequency we want
uint16_t HWORDS = 128;
uint16_t oldHWORDS = HWORDS;

volatile uint16_t *data = new uint16_t[HWORDS];
volatile float phase = twopi /(float)HWORDS;  // phase increment

void timer_init() {
  uint32_t cc;
  analogWrite(PWMPIN,0);  // init clock,timer,pin, and zero TODO just timer
  TCCx->CTRLA.reg &= ~TCC_CTRLA_ENABLE;   // disable
  syncTCC;
  // set period and duty cycle
  TCCx->PER.reg = PERIOD;
  syncTCC;
  TCCx->CC[TCCchannel].reg = DUTY;   // initial duty cycle
  syncTCC;
  TCCx->CTRLA.reg |=  TCC_CTRLA_ENABLE ;  // start timer
  syncTCC;
}

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)));
volatile dmacdescriptor descriptor_section[12] __attribute__ ((aligned (16)));
dmacdescriptor descriptor __attribute__ ((aligned (16)));

static uint32_t chnl0 = 0;  // DMA channel
#define DMA_TRIGGER TCC0_DMAC_ID_OVF

// probably want to rewrite using ZeroDMA
void dma_init() {
  // trigger on TCC OVF, update TCC duty, circular
  uint32_t temp_CHCTRLB_reg;

  // probably on by default
  PM->AHBMASK.reg |= PM_AHBMASK_DMAC ;
  PM->APBBMASK.reg |= PM_APBBMASK_DMAC ;

  DMAC->BASEADDR.reg = (uint32_t)descriptor_section;
  DMAC->WRBADDR.reg  = (uint32_t)wrb;
  DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf);

  DMAC->CHID.reg = DMAC_CHID_ID(chnl0);
  DMAC->CHINTENSET.reg = DMAC_CHINTENSET_SUSP; // Enable suspend ch int on channel 0

  temp_CHCTRLB_reg =  DMAC_CHCTRLB_LVL(0) |
                      DMAC_CHCTRLB_TRIGSRC(DMA_TRIGGER) |
                      DMAC_CHCTRLB_TRIGACT_BEAT;
  DMAC->CHCTRLB.reg = temp_CHCTRLB_reg;
 
  descriptor.descaddr = (uint32_t) &descriptor_section[chnl0];   // circular
  descriptor.srcaddr = (uint32_t)&data[0] + oldHWORDS*sizeof(uint16_t);
  descriptor.dstaddr = (uint32_t)&DAC->DATA.reg;
  descriptor.btcnt   =  oldHWORDS;
  descriptor.btctrl  =  DMAC_BTCTRL_BLOCKACT_SUSPEND |
                        DMAC_BTCTRL_BEATSIZE_HWORD |
                        DMAC_BTCTRL_SRCINC |
                        DMAC_BTCTRL_VALID;
  //Serial.print("descriptor.btctrl:");  Serial.println(descriptor.btctrl, BIN);
  memcpy((void*)&descriptor_section[chnl0], &descriptor, sizeof(dmacdescriptor));
  NVIC_SetPriority(DMAC_IRQn, 0);   // Set the NVIC priority to 0 (highest)
  NVIC_EnableIRQ(DMAC_IRQn);        // Connect the DMAC to NVIC
  // start channel
  DMAC->CHID.reg = DMAC_CHID_ID(chnl0);
  DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE;
}

void DMAC_Handler() {
  //Serial.println("we got here for some reason");      // we called it from loop
  // shouldn't it also be from the DMA ending? Now it is
  DMAC->CHID.reg = DMAC_CHID_ID(DMAC->INTPEND.bit.ID);    // Determine the DMAC interrupt channel
  descriptor_section[0].btctrl &= ~DMAC_BTCTRL_VALID;     // Disable the descriptor
  //Serial.println( DMAC_CHID_ID(DMAC->INTPEND.bit.ID), HEX );
  //
  // Add your descriptor changes here...
  // ... like update HWORDS, and also change the length of the data array here
  // in accordance with the up or down button being pressed.
  //
  //When we get here, oldHWORDS has been changed by the loop, need to recalculate
 
  // Zero is a bit too slow to do this in the DMAC handler!!
  digitalWrite(11, LOW);                  // for debug
  for (uint16_t i=0;i<oldHWORDS;i++) {
    data[i]= (uint16_t)(( sinf( 2*PI*(float)i/(float)oldHWORDS ) * scale ) + dcoffset);
  }  // data is now changed
  digitalWrite(11, HIGH);                 // for debug
  descriptor.descaddr = (uint32_t) &descriptor_section[chnl0];   // circular
  descriptor.srcaddr  = (uint32_t) &data[0] + oldHWORDS*sizeof(uint16_t);
  descriptor.dstaddr  = (uint32_t) &DAC->DATA.reg;
  descriptor.btcnt    =  oldHWORDS;
  descriptor_section[0].btctrl |= DMAC_BTCTRL_VALID;  // Enable the descriptor
  memcpy((void*)&descriptor_section[chnl0], &descriptor, sizeof(dmacdescriptor));
  // copy descriptor into DMAC
 
  DMAC->CHCTRLB.reg |= DMAC_CHCTRLB_CMD_RESUME; // Resume the DMAC channel
  DMAC->CHINTFLAG.bit.SUSP = 1;  // Clear the DMAC chan suspend (SUSP) int flag 
}


void setup() {
  pinMode(11, OUTPUT);
  digitalWrite(11, HIGH);
   int i;
   Serial.begin(500000);
   while(!Serial);   // added only to see the serial output
  // for (i=0;i<HWORDS/2;i++) data[i]=data[HWORDS-1-i] = 2*1024*i/HWORDS; 
  // sawtooth
   
   for (i=0;i<HWORDS;i++) {
     data[i]= (uint16_t)(( sinf((float)i*phase) * scale ) + dcoffset);
    //Serial.println(data[i]);
  }
   analogWriteResolution(10);
   analogWrite(A0,0);   // DAC init setup DAC pin and zero it
  //   while(1) {for (i=0;i<HWORDS;i++){ analogWrite(A0,data[i]); delay(1);}} // scope 50ms
  //Serial.end(); // end serial to avoid timing errors
   dma_init();   // do me first
   timer_init();   // startup timer
}

void loop() {
   // could jumper DAC to A1 and sample it
  // Serial.println(analogRead(A1));  // running serial messes with the timing!
 
  if(Serial.available()) {
    int inByte = Serial.read();
    char c = (char) inByte;
    if ((c != '\n') & (c != '\r')){
      if (c == '+') {
        HWORDS -= 8;   // freq will go up
        if (HWORDS < 16) HWORDS = 16;
      }
      if (c == '-') {
        HWORDS += 8;   // freq will go down
        if (HWORDS > 8192) HWORDS = 8192;
      }
    }
  }  // will replace above with button press or linear ramp later
  if (HWORDS != oldHWORDS) {
    //Serial.println("We will attempt to change the frequency here");
    Serial.print("Previous HWORD was:"); Serial.println(oldHWORDS);
    Serial.print("New HWORD is      :"); Serial.println(HWORDS);
    oldHWORDS = HWORDS;
    //DMAC_Handler();
  }
}
Strange, the code tags don't like C comments /* */. Had to take them out of the file.

BDL
 
Posts: 68
Joined: Wed Jan 09, 2019 10:45 pm

Re: Feather M0 Sine Wave generator using ZeroDMA

by BDL on Wed Jul 17, 2019 10:10 am

SDS00003.png
Frequency chirp ndef const_pwr
SDS00003.png (42.53 KiB) Viewed 142 times
Following up on this thread. I have modified MartinL2's code to perform either a chirp or or a swept sine wave with "constant power" per frequency. The former is easier to see on a scope, but does not have constant power per frequency. This is true because each frequency (single cycle of a sine wave) is shorter and shorter as the frequency increases. If one observes this on an FFT, one sees the power per bin decreases as the frequency increases. The constant power mode emits a frequency for a unit time before changing. In this way the power as observed by an FFT should be constant. This mode is useful for characterizing filter frequency responses.
SDS00002.png
Constant power, def const_pwr
SDS00002.png (48.48 KiB) Viewed 142 times

I hope this is useful to the community. I am seeing 2nd harmonics of the DAC, which are only -25dBc. I was hoping for better than that. Strangely, the harmonics are that high starting at 37KHz. I am using a Blackman window for the FFT.

BDL
 
Posts: 68
Joined: Wed Jan 09, 2019 10:45 pm

Re: Feather M0 Sine Wave generator using ZeroDMA

by BDL on Wed Jul 17, 2019 10:34 am

sweptsine.ino
Updated code: Sine Wave generator using DMA
(8.91 KiB) Downloaded 27 times
Seems that I forgot to upload the code! Here it is. Comments welcome.

BDL
 
Posts: 68
Joined: Wed Jan 09, 2019 10:45 pm

Re: Feather M0 Sine Wave generator using ZeroDMA

by adafruit2 on Wed Jul 17, 2019 12:12 pm

nice work, we'll blog this up!

adafruit2
Site Admin
 
Posts: 18652
Joined: Fri Mar 11, 2005 7:36 pm

Re: Feather M0 Sine Wave generator using ZeroDMA

by BDL on Wed Jul 17, 2019 12:26 pm

Thanks :)

BDL
 
Posts: 68
Joined: Wed Jan 09, 2019 10:45 pm

Please be positive and constructive with your questions and comments.


cron