DAC free running mode

Hi all,

I'm trying to get the DAC on the due to run as fast as possible. Specifically I want to update the output as quickly as possible after a value is computed. The data-sheet for the SAM3X processor mentions a free running mode.

"In free running mode, conversion starts as soon as at least one channel is enabled and data is written in the DACC Conversion Data Register, then 25 DACC Clock periods later, the converted data is available at the corresponding analog output as stated above.
...Disabling the external trigger mode automatically sets the DACC in free running mode"

This seems to be what I'm looking for, but I can't get it to work.

Here is my setup code:

unsigned long cher_reg_val = 0;
unsigned long mr_reg_val =0;

void setup() {

  Serial.begin(9600);

  // enable dac clock
  PMC->PMC_PCER1 |= 1<<6;
  /*bit:function (desired value)
   * 0:freerun (0)
   * 1-3:trigger selection (shouldn't matter in free run?)
   * 4:word(0 half word)
   * 5:sleep(0)
   * 6:Fastwakeup(0)
   * 8-15:refresh
   * 16-17:channel selection (Dac0 0; Dac1 1)
   * 20:tag (0)
   * 21:max speed (1)
   * 24-29:Startup (0)
   */
  mr_reg_val += 16<<24; // 1024 dacc clock cycles startup
  mr_reg_val += 1<<21;
  mr_reg_val += 0<<16; // select dac pin
  mr_reg_val += 1<<11; // refresh rate this value is used in analog read
  DACC->DACC_MR = mr_reg_val; // write to register

  cher_reg_val += 1<<0; // enable DAC on pin DAC0 
  DACC->DACC_CHER = cher_reg_val; // write to channel enable register
}

However, when I try to write to the dac (using DACC->DACC_CDR = outputValue;) it stays at 0V (I'm aware it should not be able to go below 3.3/6 V).

I've verified write protect mode is not enabled by default.
Reading DACC->DACC_CHSR (channel status register) shows DAC0 as enabled.

Am I misunderstanding the free running mode?
Why am I getting 0V? It seems is the DAC not switched on?

PS. I'm sure my dac pin is ok, as using analogWrite still works.

You should avoid magic numbers to ease debugging, instead use constant names that you find in header files and the DAC section of Sam3x datasheet itself.

https://android.googlesource.com/platform/external/arduino-ide/+/f876b2abdebd02acfa4ba21e607327be4f9668d4/hardware/arduino/sam/system/CMSIS/Device/ATMEL/sam3xa/include/component

Be careful with your DACs, always connect at least a 1 or 2K resistor in serie, they are very sensitive.

An example sketch to ouput on DAC1 in free running mode and update DAC output inside DAC Handler:

void dac_setup ()
{

  PMC->PMC_PCER1 = PMC_PCER1_PID38;     // DACC power ON

  DACC->DACC_CR = DACC_CR_SWRST ;       // Reset DACC

  DACC->DACC_MR = DACC_MR_TRGEN_DIS                    // Free running mode
                  | DACC_MR_USER_SEL_CHANNEL1          // select channel 1
                  | DACC_MR_REFRESH (1)
                  | DACC_MR_STARTUP_8
                  | DACC_MR_MAXS;


  DACC->DACC_IER |= DACC_IER_EOC;

  NVIC_EnableIRQ(DACC_IRQn);               // Enable DACC interrupt
  DACC->DACC_CHER = DACC_CHER_CH1;      // enable channel 1 = DAC1

}
void DACC_Handler() {

  DACC->DACC_ISR;  // Read and clear status register
  DACC->DACC_CDR = 1000;  // or whatever you want between 0 and 4095

}

void setup() {
  dac_setup();
}


void loop() {
}

Thanks for that ard!

I tried finding some examples but couldn't find any. I've now got my code working (yes it was an incorrect register value) but will definitely take onboard your constant name suggestion.

BTW, for a full range DAC ( 0V----3.3V):

Why? You say you're calculating some output value. So what's wrong with analogWrite(calculatedValue)? How slow is it and how fast do you need it to be? Is that truly the bottleneck in the speed of your sketch?

The DAC can use the DMA direct memory access to play out a segment of memory at a defined rate, with no impact on the running speed of the main sketch. If you are able to pre-compute the waveform you need and save it in memory, then this will be faster than a lot of analogWrite()'s. I've never seen an Arduino sketch which uses any of the DMA features however.

I'm implementing a feedback loop on the due. This requires reading a voltage using the ADC. A new output voltage is then calculated based on each new ADC read. This must be done via the DAC as a PWM signal is not suited to my needs. Reducing the latency in this process directly translates to being able to remove higher frequency noise. (I'm targeting 100 kHz [or more] for latency to amount to a pi phase shift.) I'm therefore using neither analogRead nor analogWrite.

Manually setting up the ADC and DAC allows me to minimise both the in loop cycles of reading and writing, as well as the latency within the peripheral [Ex: the DAC takes 25 DAC clock cycles to actually change the output, but this is reduced by 2 cycles if DACC_MR_MAXS is set].

I'm currently using both the DAC and ADC in free running mode. With this I've achieved a latency of 5 us (between an external voltage being applied and the DAC voltage responding). Any suggestion that could speed this up further would be very welcome.

I didn't elaborate on this in my original post as I felt the DAC setup was quite independent from the rest of my project.

Post your full sketch between an ADC sampling and an DAC output.

Since the datasheet states that Max ADC sampling frequency = 1MHz and Max DAC output frequency = 2 MHz, the Max feedback loop frequency should be 1 MHz (divide by 5 your latency).

pathfinder_49:
Reducing the latency in this process directly translates to being able to remove higher frequency noise.

???

To remove higher frequencies, process a digital filtering in between ADC sampling and DAC output.

I've messed around a bit with my ADC setup and am now getting 1.5-3 us latency (as measured on a scope looking at the input and DAC response). When i include the feedback calculations the latency becomes 2-5.5 us. The delay seems to fluctuate randomly within these ranges between cycles. I'm surprised by the large range, especially when I include calculations. (3.5 us range is larger than maximum latency without calculations.)

Please find my full sketch below:

// PID feedback constants (interger division by this value)
// faster than floating point multiplication!
const int pFac = 100;
const int iFac = 10000; //12 bit resolution -> if >4096 same as no feedback
const int dFac = 10000;


// PID values (declared volatile so compiler doesn't remove these when testing timings)
volatile int pVal = 0;
volatile int iVal = 0;
volatile int dVal = 0;

// varible to contain value read from pin
unsigned int inVal = 0;
// store previously read value
unsigned int oldVal = 0;
// target voltage (12 bit int)
unsigned int targetVal = 2048;

// timing variables for testing only
unsigned long t0=0;
unsigned long t1=0;


void adc_setup(){
  // setup mode register
  ADC->ADC_MR = ADC_MR_FREERUN_ON | ADC_MR_LOWRES_BITS_12 | ADC_MR_SLEEP_NORMAL | ADC_MR_FWUP_OFF;
  ADC->ADC_MR |= ADC_MR_STARTUP_SUT512; // startup time as per default 
  ADC->ADC_MR |= ADC_MR_PRESCAL(0); // set clock prescalar to run as fast as possible
  ADC->ADC_MR |= ADC_MR_SETTLING_AST3; //settling time if the analog chanel is switched (dosn't mater if ANACH is not set)
  ADC->ADC_MR |= ADC_MR_ANACH_NONE;
  ADC->ADC_MR |= ADC_MR_TRACKTIM(0); // minimal tracking time
  ADC->ADC_MR |= ADC_MR_TRANSFER(0); // minimal transfer time

  // enable channel
  ADC->ADC_CHER = ADC_CHER_CH7; //enable ADC on pin A0 (channel 7)
}

void dac_setup(){
  /* Conversion Triggers
   * In free running mode, conversion starts as soon as at least one channel is enabled and data is written in the DACC
   * Conversion Data Register, then 25 DACC Clock periods later, the converted data is available at the corresponding
   * analog output as stated above.
   * In external trigger mode, the conversion waits for a rising edge on the selected trigger to begin.
   */
   
  analogWriteResolution(12);
  // enable dac clock
  PMC->PMC_PCER1 = PMC_PCER1_PID38;
  // set DAC to free running mode

  //setup dac mode register
  DACC->DACC_MR = DACC_MR_TRGEN_DIS; // enable free running mode
  DACC->DACC_MR |= DACC_MR_WORD_HALF | DACC_MR_REFRESH(0); // refresh of 8 used by default
  DACC->DACC_MR |= DACC_MR_USER_SEL_CHANNEL0 | DACC_MR_TAG_DIS | DACC_MR_MAXS_MAXIMUM;
  DACC->DACC_MR |= DACC_MR_STARTUP_1024; // startup clock cycles
  
  DACC->DACC_CHER =DACC_CHER_CH0; //enable DAC on pin DAC0 
}


void setup() {
  // initialize serial communications at 9600 bps:
  //Serial.begin(57600);
  //delay(200);
  //Serial.println(ADC->ADC_MR,BIN);
  adc_setup();
  dac_setup();
}

void loop() {
  oldVal = inVal;
  while(ADC->ADC_ISR == ADC_ISR_EOC7); // wait for conversion 
  inVal=ADC->ADC_CDR[7]; //get values
  // P part
  pVal = inVal/pFac; 
  // I part
  iVal += (inVal-targetVal)/iFac;
  // D part
  dVal = (inVal-oldVal)/dFac;
  // combine results
  // "AIUI, the Due's master clock frequency is 84 MHz, so DACC Clock frequency is 42 MHz, 
  // whence 25 cycles take 575 ns, which is an update frequency of about 1.74 MHz"
  //DACC->DACC_CDR = pVal+iVal+dVal; // write feedback value to DAC
  DACC->DACC_CDR = inVal; // for testing purposes (pval, etc. are declared volatile so they are not removed by compiler)
  
  
  // print the results to the Serial Monitor:
  //Serial.print("sensor = ");
  //Serial.println(inVal);
  /*
  Serial.print("\t output = ");
  Serial.println(outputValue);
  */
}

Since you want a precise timing, you can't rely on Freerun Mode, but instead trigger a conversion every 1 us.

You can synchro an ADC conversion with a DAC conversion using Timer Counter 0 channel 2. Note that trigger is done internally between a rising edge of TIOA2 and ADC and TIOA2 and DACC.

Test this first sketch without any digital filtering before adding a filtering inside ADC_Handler().

(BTW, in your sketch, you forgot to power the ADC peripheral.)

/****************************************************************************************************/
/*  1 MHz ADC conversions of 1 analog input (A0) triggered by Timer Counter 0 channel 2 TIOA2       */
/*  1 MHz DAC output on channel 1 (DAC1) triggered by Timer Counter 0 channel 2 TIOA2               */
/****************************************************************************************************/

void setup()
{

  adc_setup();
  dac_setup();
  tc_setup();
}

void loop()
{

}

/*************  Configure adc_setup function  *******************/
void adc_setup() {

  PMC->PMC_PCER1 |= PMC_PCER1_PID37;                    // ADC power ON

  ADC->ADC_CR = ADC_CR_SWRST;                           // Reset ADC
  ADC->ADC_MR |=  ADC_MR_TRGEN_EN                       // Hardware trigger select
                  | ADC_MR_TRGSEL_ADC_TRIG3             // Trigger by TIOA2
                  | ADC_MR_PRESCAL(1);

  ADC->ADC_ACR = ADC_ACR_IBCTL(0b01);                   // For frequencies > 500 KHz

  ADC->ADC_IER = ADC_IER_EOC7;                          // End Of Conversion interrupt enable for channel 7
  NVIC_EnableIRQ(ADC_IRQn);                                  // Enable ADC interrupt
  ADC->ADC_CHER = ADC_CHER_CH7;                         // Enable Channel 7 = A0
}

void ADC_Handler () {

  /* Todo : Apply any digital filtering before DAC output  */
  /* Beware : Stay in ADC_Handler much less than 1 us  !!! */
  DACC->DACC_CDR = ADC->ADC_CDR[7];                    // Reading ADC->ADC_CDR[i] clears EOCi bit

}

/*************  Configure dacc_setup function  *******************/
void dac_setup ()
{

  PMC->PMC_PCER1 = PMC_PCER1_PID38;                   // DACC power ON

  DACC->DACC_CR = DACC_CR_SWRST ;                     // Reset DACC
  DACC->DACC_MR = DACC_MR_TRGEN_EN                    // Hardware trigger select
                  | DACC_MR_TRGSEL(0b011)             // Trigger by TIOA2
                  | DACC_MR_USER_SEL_CHANNEL1         // select channel 1
                  | DACC_MR_REFRESH (1)
                  | DACC_MR_STARTUP_8
                  | DACC_MR_MAXS;

  DACC->DACC_CHER = DACC_CHER_CH1;                   // enable channel 1 = DAC1

}

/*************  Timer Counter 0 Channel 2 to generate PWM pulses thru TIOA2  ************/
void tc_setup() {

  PMC->PMC_PCER0 |= PMC_PCER0_PID29;                      // TC2 power ON : Timer Counter 0 channel 2 IS TC2

  TC0->TC_CHANNEL[2].TC_CMR = TC_CMR_TCCLKS_TIMER_CLOCK1  // MCK/2, clk on rising edge
                              | TC_CMR_WAVE               // Waveform mode
                              | TC_CMR_WAVSEL_UP_RC       // UP mode with automatic trigger on RC Compare
                              | TC_CMR_ACPA_CLEAR         // Clear TIOA2 on RA compare match
                              | TC_CMR_ACPC_SET;          // Set TIOA2 on RC compare match


  TC0->TC_CHANNEL[2].TC_RC = 42;  //<*********************  Frequency = (Mck/2)/TC_RC  Hz = 1 MHz
  TC0->TC_CHANNEL[2].TC_RA = 20;  //<********************   Any Duty cycle in between 1 and TC_RC

  TC0->TC_CHANNEL[2].TC_CCR = TC_CCR_SWTRG | TC_CCR_CLKEN;// Software trigger TC2 counter and enable

}

Thanks again ard_newbie. With a few tweaks from your code the latency is now 1.5-2.5 us without any processing of the input. Performing the necessary calculations for PI-feedback increases latency to 2-3 us. However, including the differential calculation (least important to me) increases the latency range from 1 to 2 us. I would attribute this to the ADC_Handler() taking more than 1 us to execute when performing the full PID feedback calculation.

My modifications were as follows:

  • ADC_MR_PRESCAL(0) needs to be used for maximum ADC performance
  • The DAC should be set to free running mode. (This way DAC conversion is started as soon as the write in the ADC interrupt occurs. Triggering off the clock introduces an unnecessary delay when waiting for the conversion trigger. In both cases the latency variation remains 1 us and is caused by incoherence between my function generator and the ADC conversion.)

I forgot to mention that your code in loop() in reply #7 is wrong. You poll incorrectly ADC_ISR, this should be:

 // wait until EOC7 bit is high = end of conversion
while(!(ADC->ADC_ISR & ADC_ISR_EOC7));