Very Stable DAC Output using PDB (Programmable Delay Block) and DAC Buffer

Status
Not open for further replies.

the_pman

New member
Hello to all.

This is my first post and I hope you'll go easy on me. I spent a lot of hours experimenting with the DAC and the PDB (Programmable Delay Block) to drive the DAC. Perhaps this will help someone else. The code below details the following:

-Generation of extremely stable DAC output. A sine wave in this example.
-Usage of the DAC and built-in DAC buffer.
-The PDB (Programmable Delay Block) to trigger the DAC, saving on usage of any other interval timers or clocks.

The DAC has a 16 word buffer and three interrupts that can be associated with the state of the buffer: top, watermark and bottom. There is a lot of flexiblity here. I programmed the DAC in such a way that I treat it as an A/B ping-pong buffer. There are different ways to program the buffer but this seemed the simplest to me. The buffer has 16 word locations (0-15) with the watermark pointing to 4 words before the bottom (15th) location. When I reach the watermark an interrupt occurs and I know I can refill the lower half of the buffer. Next, when I reach 'top' (0th position) I know I have wrapped around the end and the top half of the buffer can be refilled.

The 12-bit DAC can be triggered by the PDB. The PDB uses a 'MODULUS' which is some multiple of the bus clock (48Mhz on my system).
Further, the PDB can have an additional clock the subdivides the modulus even further. For example, one could fire off the main PDB
trigger every 1000 bus clocks. One could additionally divide this into 10 parts (with the PDB DAC interval register) so that the DAC could be triggered every
100 bus clocks. Since I'm only using the PDB to clock the DAC I'm just concerned with setting the modulus. That is why I set the PDB DAC interval register to the same value as the modulus.

This works as of r1.18. Please let me know if you have any questions.

Code:
/*
Pre-computing the sin wave into a lookup table (LUT).
Uses the PDB (Programmable Delay Buffer) and DAC buffer and interrupts

NB DAC_C1_DACBFEN is misdefined as 0x00 in mk20dx128.h (as of release 1.18). It should be 0x01!!!
*/

// hardware defines not found elsewhere
#define PDB0_DACINT0 *(volatile uint32_t *)0x40036154 // PDB DAC interval register
#define PDB0_DACINTC0 *(volatile uint32_t *)0x40036150 // PDB DAC interval control register
#define PDB_DACINTC0_TOE 0x01 // 0x01 -> PDB DAC interal trigger enabled

#define DAC0_SR_DACBFWMF 0x04 // buffer watermark flag
#define DAC0_SR_DACBFRTF 0x02 // buffer top flag
#define DAC0_SR_DACBFRBF 0x01 // buffer bottom flag (not used)
#define DAC0_DAT0 (uint16_t *)0x400CC000 // DAC word buffer base address
#define DAC0_DAT7 (uint16_t *)0x400CC010 // DAC word buffer top eight words base address.

#define audioRate 48000 // desired audio rate
#define BUS_CLOCK 48000000 // bus clock (1/2 system clock, on my sys anyway)
#define maxAmplitude 3500

#define PDB0_MOD_TIME ((int)(BUS_CLOCK/audioRate))  // modulus time for the DAC in PDB
// PDB0_MOD_TIME == 1000 in this case. This number will be the multiple of the
// bus clock (48000000 / 48000 = 1000). The PDB will trigger the DAC and output a
// value from the next element in the DAC buffer.

#define twopi (3.14159265359 * 2.0) // needed to fill the LUT with a sine wave

uint16_t *p1;

// an array to hold a sine wave
#define LUT_SIZE 1024
uint16_t lut[LUT_SIZE];
int lutndx = 0; // wave buffer pointer

// variables to copy the DAC interrupt flags
volatile uint32_t dacBufferWatermarkFlag = 0;
volatile uint32_t dacBufferTopFlag = 0;
volatile uint32_t dacBufferBottomFlag = 0; // not used

// my DAC interrupt service routine. Simply copying the state of two flags
void dac0_isr(void) {
  if ( DAC0_SR & DAC0_SR_DACBFWMF ) { // see if watermark flag is set        
    DAC0_SR &= ~(DAC0_SR_DACBFWMF); // clear the watermark flag
    dacBufferWatermarkFlag = 1; // copy it
  }
  
  if ( DAC0_SR & DAC0_SR_DACBFRTF ) { // see if top flag is set
    DAC0_SR &= ~(DAC0_SR_DACBFRTF); // clear the top flag
    dacBufferTopFlag = 1; // copy it
  }  
}

void setup() {
  
  //fill the LUT with a sine wave
  double midpoint = maxAmplitude / 2.0;
  for (int i=0; i<LUT_SIZE; i++) {
    lut[i] = (int)(sin(twopi*((double)i/(double)LUT_SIZE)) * midpoint + midpoint);
  }

  // reset everything in the DAC first  
  SIM_SCGC2 |= SIM_SCGC2_DAC0; // turn on clock to the DAC
  DAC0_C0 |= DAC_C0_DACEN; // enable the DAC; must do before any of the following
  
  /* Notes
  By default, the DAC trigger resets to 0 => hardware trigger. I want this
  By default, the DAC uses the high power mode. Good.
  By default, DMA is disabled. Good
  */

  DAC0_C0 |= DAC_C0_DACRFS; // 3.3V VDDA is DACREF_2  
  DAC0_C0 |= DAC_C0_DACBWIEN; // Enable interrupts on watermark reached
  DAC0_C0 |= DAC_C0_DACBTIEN; // Enable interrupts on top of buffer (zeroth position)  
  DAC0_C1 |= DAC_C1_DACBFWM(3); // set watermark 4 words away from upper limit
  DAC0_C2 |= DAC_C2_DACBFUP(15); // resets to 15 but setting it anyway...
   
  // clear the buffer flags
  DAC0_SR &= ~(DAC0_SR_DACBFWMF);
  DAC0_SR &= ~(DAC0_SR_DACBFRTF);
  DAC0_SR &= ~(DAC0_SR_DACBFRBF); // not used but clearing anyway
  
  NVIC_ENABLE_IRQ(IRQ_DAC0); // enable irq defined above
  
  DAC0_C1 |= (0x01) << 0; // 0x01 enables the DAC buffer! See note above
 
  // prefill the DAC buffer with first 16 values from the lut
  p1 = DAC0_DAT0;
  for (int i = 0; i < 16; i++) {
    *p1++ = lut[lutndx++];
    if (lutndx == (LUT_SIZE-1)) lutndx = 0;
  }

  // Setup the PDB
  /* PDB Notes
  -Setup the PDB to start on a software trigger. Run in continuous mode.
  -By default, the multiplier (MULT) value (prescaler multiplier value) is '1'.
  -By default, the prescaler is 1 * MULT = 1 * 1 which implies that the PDB
   is running at full bus clock speed. (Which is 48Mhz)
  -Every time the counter reaches the MOD value the DACINT0 counter is reset
   to 0. 
  */  
 
  SIM_SCGC6 |= SIM_SCGC6_PDB; // turn on the PDB clock  
  PDB0_SC |= PDB_SC_PDBEN; // enable the PDB  
  PDB0_SC |= PDB_SC_TRGSEL(15); // trigger the PDB on software start (SWTRIG)  
  PDB0_SC |= PDB_SC_CONT; // run in continuous mode  
  PDB0_MOD = PDB0_MOD_TIME; // modulus time for the PDB  
  PDB0_DACINT0 = (uint16_t)(1 * PDB0_MOD_TIME); // we won't subdivide the clock... 
  PDB0_DACINTC0 |= PDB_DACINTC0_TOE; // enable the DAC interval trigger  
  PDB0_SC |= PDB_SC_LDOK; // update pdb registers  

  PDB0_SC |= PDB_SC_SWTRIG; // ...and start the PDB
}

void loop() {  
  
  if ( dacBufferWatermarkFlag ) { // refill the lower half of circular DAC buffer
    dacBufferWatermarkFlag = 0;
    p1 = DAC0_DAT0;
    for (int i=0; i<8; i++) {
      *p1++ = lut[lutndx++];
      if (lutndx == (LUT_SIZE-1)) lutndx = 0;
    }    
  }
  
  if ( dacBufferTopFlag ) { // refill the upper half of circular DAC buffer
    dacBufferTopFlag = 0;
    p1 = DAC0_DAT7;
    for (int i=0; i<8; i++) {
      *p1++ = lut[lutndx++];
      if (lutndx == (LUT_SIZE-1)) lutndx = 0;
    }
  }  
}
 
Great job and thanks. I look forward to trying it.

With your level of understanding, perhaps you would be interested in taking on the two simultaneously sampling a/d converters? ;-)

thanks again,

Richard
 
Thanks Richard. At this time I have no need to use the ADC functionality of the chip (although I could see myself using it in the future). Just looking at the hardware registers and configuration for the ADC is pretty daunting though. There looks to be a lot of setup and calibration needed to be done before the ADC is ready to start converting. The ADC looks to easily be twice as complicated as the DAC. :)
 
Wow! Impressive work! Can't wait to try it out tonight when I'll come home from my ...ing day job. Made a first attempt to generate a wave table, too, and to output the values with the help of an IntervalTimer and analogWrite. But that was ways too slow and sloppy for good audio quality. Thus I'm very grateful for your approach which seems to make better use of the hardware.
 
Tested it today. Works like a charm! Just I have still a problem when I want to increase the lutndx pointer by more than 1 in order to output higher frequencies. That gives ugly spikes in the output signal because read operations beyond the lut table end may happen since there is actually the increment first and then a "end of table check" with a "==" comparison. I'll try to fix that in the next days and post a (hopefully) improved version here.
 
Very nice. I actually did something similar, but fully in software, with all IRQ disabled and a much more fine-grained stepping (360 steps, total). If you could mix the whole thing together with self-circular DMA, you could have a really nice inexpensive digital wave function generator.

I am curious on why you just don't do the buffer filling in the ISR. To me it seems like that is where it belongs.
 
My intention is in fact using it as a kind of function generator. I have tried first to write a class library but that did not work because the compiler doesn't like ISRs as class members. When I found the_pman's code above basically working I decided to use and extend it and I finally put my class library in the trash bin.
Actually, my day job does not leave me enough free time and power, thus I'm only going on very slowly...
 
Tested it today. Works like a charm! Just I have still a problem when I want to increase the lutndx pointer by more than 1 in order to output higher frequencies. That gives ugly spikes in the output signal because read operations beyond the lut table end may happen since there is actually the increment first and then a "end of table check" with a "==" comparison. I'll try to fix that in the next days and post a (hopefully) improved version here.

I quickly tested this update which generates multiples of the ~ 46.83 Hz signal. Also, I think the original was wrong (it skipped the last LUT value) when it used

if (lutndx == (LUT_SIZE-1)) lutndx = 0; I think this was wrong -- should be just LUT_SIZE

also, the line which computes the LUT should have a 0.5 added (because the int() rounds down) to get the nearest sine point

Code:
[B][I]#define STEP 1 // tested up to 31, should go higher[/I][/B]/*
Pre-computing the sin wave into a lookup table (LUT).
Uses the PDB (Programmable Delay Buffer) and DAC buffer and interrupts

NB DAC_C1_DACBFEN is misdefined as 0x00 in mk20dx128.h (as of release 1.18). It should be 0x01!!!
*/

// hardware defines not found elsewhere
#define PDB0_DACINT0 *(volatile uint32_t *)0x40036154 // PDB DAC interval register
#define PDB0_DACINTC0 *(volatile uint32_t *)0x40036150 // PDB DAC interval control register
#define PDB_DACINTC0_TOE 0x01 // 0x01 -> PDB DAC interal trigger enabled

#define DAC0_SR_DACBFWMF 0x04 // buffer watermark flag
#define DAC0_SR_DACBFRTF 0x02 // buffer top flag
#define DAC0_SR_DACBFRBF 0x01 // buffer bottom flag (not used)
#define DAC0_DAT0 (uint16_t *)0x400CC000 // DAC word buffer base address
#define DAC0_DAT7 (uint16_t *)0x400CC010 // DAC word buffer top eight words base address.

#define audioRate 48000 // desired audio rate
#define BUS_CLOCK 48000000 // bus clock (1/2 system clock, on my sys anyway)
#define maxAmplitude 3500

#define PDB0_MOD_TIME ((int)(BUS_CLOCK/audioRate))  // modulus time for the DAC in PDB
// PDB0_MOD_TIME == 1000 in this case. This number will be the multiple of the
// bus clock (48000000 / 48000 = 1000). The PDB will trigger the DAC and output a
// value from the next element in the DAC buffer.

#define twopi (3.14159265359 * 2.0) // needed to fill the LUT with a sine wave

uint16_t *p1;

// an array to hold a sine wave
#define LUT_SIZE 1024
uint16_t lut[LUT_SIZE];
int lutndx = 0; // wave buffer pointer

// variables to copy the DAC interrupt flags
volatile uint32_t dacBufferWatermarkFlag = 0;
volatile uint32_t dacBufferTopFlag = 0;
volatile uint32_t dacBufferBottomFlag = 0; // not used

// my DAC interrupt service routine. Simply copying the state of two flags
void dac0_isr(void) {
  if ( DAC0_SR & DAC0_SR_DACBFWMF ) { // see if watermark flag is set        
    DAC0_SR &= ~(DAC0_SR_DACBFWMF); // clear the watermark flag
    dacBufferWatermarkFlag = 1; // copy it
  }
  
  if ( DAC0_SR & DAC0_SR_DACBFRTF ) { // see if top flag is set
    DAC0_SR &= ~(DAC0_SR_DACBFRTF); // clear the top flag
    dacBufferTopFlag = 1; // copy it
  }  
}

void setup() {
  
  //fill the LUT with a sine wave
  double midpoint = maxAmplitude / 2.0;
  for (int i=0; i<LUT_SIZE; i++) {
    lut[i] = (int)(sin(twopi*((double)i/(double)LUT_SIZE)) * midpoint + midpoint[B] + 0.5[/B]);
// added 0.5 here to make int() round to nearest (positive) integer
  }

  // reset everything in the DAC first  
  SIM_SCGC2 |= SIM_SCGC2_DAC0; // turn on clock to the DAC
  DAC0_C0 |= DAC_C0_DACEN; // enable the DAC; must do before any of the following
  
  /* Notes
  By default, the DAC trigger resets to 0 => hardware trigger. I want this
  By default, the DAC uses the high power mode. Good.
  By default, DMA is disabled. Good
  */

  DAC0_C0 |= DAC_C0_DACRFS; // 3.3V VDDA is DACREF_2  
  DAC0_C0 |= DAC_C0_DACBWIEN; // Enable interrupts on watermark reached
  DAC0_C0 |= DAC_C0_DACBTIEN; // Enable interrupts on top of buffer (zeroth position)  
  DAC0_C1 |= DAC_C1_DACBFWM(3); // set watermark 4 words away from upper limit
  DAC0_C2 |= DAC_C2_DACBFUP(15); // resets to 15 but setting it anyway...
   
  // clear the buffer flags
  DAC0_SR &= ~(DAC0_SR_DACBFWMF);
  DAC0_SR &= ~(DAC0_SR_DACBFRTF);
  DAC0_SR &= ~(DAC0_SR_DACBFRBF); // not used but clearing anyway
  
  NVIC_ENABLE_IRQ(IRQ_DAC0); // enable irq defined above
  
  DAC0_C1 |= (0x01) << 0; // 0x01 enables the DAC buffer! See note above
 
  // prefill the DAC buffer with first 16 values from the lut
  p1 = DAC0_DAT0;
  for (int i = 0; i < 16; i++) {
 [B][I]   *p1++ = lut[lutndx]; // remove ++
    lutndx += STEP;  // add step here
//    if (lutndx == (LUT_SIZE-1)) lutndx = 0; I think this was wrong -- should be just LUT_SIZE
    if (lutndx >= LUT_SIZE) lutndx -= 0;[/I][/B]   // effectively a modulo counter
  } 
  // Setup the PDB
  /* PDB Notes
  -Setup the PDB to start on a software trigger. Run in continuous mode.
  -By default, the multiplier (MULT) value (prescaler multiplier value) is '1'.
  -By default, the prescaler is 1 * MULT = 1 * 1 which implies that the PDB
   is running at full bus clock speed. (Which is 48Mhz)
  -Every time the counter reaches the MOD value the DACINT0 counter is reset
   to 0. 
  */  
 
  SIM_SCGC6 |= SIM_SCGC6_PDB; // turn on the PDB clock  
  PDB0_SC |= PDB_SC_PDBEN; // enable the PDB  
  PDB0_SC |= PDB_SC_TRGSEL(15); // trigger the PDB on software start (SWTRIG)  
  PDB0_SC |= PDB_SC_CONT; // run in continuous mode  
  PDB0_MOD = PDB0_MOD_TIME; // modulus time for the PDB  
  PDB0_DACINT0 = (uint16_t)(1 * PDB0_MOD_TIME); // we won't subdivide the clock... 
  PDB0_DACINTC0 |= PDB_DACINTC0_TOE; // enable the DAC interval trigger  
  PDB0_SC |= PDB_SC_LDOK; // update pdb registers  

  PDB0_SC |= PDB_SC_SWTRIG; // ...and start the PDB
}

void loop() {  
  
  if ( dacBufferWatermarkFlag ) { // refill the lower half of circular DAC buffer
    dacBufferWatermarkFlag = 0;
    p1 = DAC0_DAT0;
    for (int i=0; i<8; i++) {
 [B][I]   *p1++ = lut[lutndx]; // remove ++
    lutndx += STEP;  // add step here
//    if (lutndx == (LUT_SIZE-1)) lutndx = 0; I think this was wrong -- should be just LUT_SIZE
    if (lutndx >= LUT_SIZE) lutndx -= 0;[/I][/B]   // effectively a modulo counter
    }    
  }
  
  if ( dacBufferTopFlag ) { // refill the upper half of circular DAC buffer
    dacBufferTopFlag = 0;
    p1 = DAC0_DAT7;
    for (int i=0; i<8; i++) {
 [B][I]   *p1++ = lut[lutndx]; // remove ++
    lutndx += STEP;  // add step here
//    if (lutndx == (LUT_SIZE-1)) lutndx = 0; I think this was wrong -- should be just LUT_SIZE
    if (lutndx >= LUT_SIZE) lutndx -= 0;[/I][/B]   // effectively a modulo counter
    }
  }  
}
 
Last edited:
Thanks Jp3141. You are so right. The old off-by-one error. I believe the original code would have worked with "if (lutndx == LUT_SIZE) lutndx = 0;" (with the lutndx++ left in place).
Also, I didn't realize the cast to int would round down. I learned something!
 
Where abouts is the DACBFMD (DAC Buffer Work Mode Select) being set in DAC0_C1?
Hi jpshea. On reset the DAC0_C1 BFMD field is set to 0 => Normal Mode => circular buffer mode. The descriptions of the modes are on page 736 of the manual.
 
Found another bug in that example code (and lost a few hours yesterday with that because I didn't read well the comment):
Code:
DAC0_C2 |= DAC_C2_DACBFUP(15); // resets to 15 but setting it anyway...
If you want to set a different (smaller) ring buffer size, the |= operator with the existing 0xF default value in DAC0_C2 will always return 0xF and the buffer size will not be changed. Solved it that way:
Code:
	uint8_t dacBufUp = (2 * overSample) -1;
	uint8_t dacBufWm = (overSample >= 4 ? 3 : overSample - 1);
	DAC0_C1 |= DAC_C1_DACBFWM(dacBufWm); 	// set water mark dacBufWm+1 words away from upper limit
	DAC0_C2 &= 0xF0 | DAC_C2_DACBFUP(dacBufUp); 	// set buffer size to 2 * overSample
In my project, I have audio data arriving at sample rates of either 32kHz or 48kHz. To improve audio quality (pseudo 14bit from 12bit) and to ease the analog output filter design, I decided to add a 2nd Order Butterworth digital low pass filter to obtain the 4 or 6 interpolated values and come out to the DAC with a fixed 192kHz sample rate. The DAC ring buffer will always hold 2 sets of these "over-samples" and must thus be set to sizes of 8 or 12.
(Just to prevent others from loosing time to find out why you can't reduce the buffer size with the original code)
 
I was messing with PDB and DAC buffer and revisited this old thread and thought I'd note some corrections to the sketches posted.

To be more flexible, get rid of BUS_CLOCK define and correct the PDB0_MOD calculation with
#define PDB0_MOD_TIME ((int)(F_BUS/audioRate) - 1) // modulus time for the DAC in PDB

if you want full amplitude with 12-bit DAC use
#define maxAmplitude 4095 // 12-bit DAC

I also observed that the ISR overhead limits how fast/accurate the desired frequency can be. For example, with T3.2@96mhz and testing with audioRate of 3MHz, the sine frequency is slightly slower than expected. No problems with rates of 1MHz or less, and 3MHz is OK with T3.2@120mhz. With rates greater than 1MHz, one needs to worry about DAC settle time.
 
Last edited:
Status
Not open for further replies.
Back
Top