Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 13 of 13

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

  1. #1
    Junior Member
    Join Date
    Mar 2014
    Posts
    4

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

    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;
        }
      }  
    }

  2. #2
    Senior Member
    Join Date
    Jan 2014
    Posts
    156
    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

  3. #3
    Junior Member
    Join Date
    Mar 2014
    Posts
    4
    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.

  4. #4
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,530
    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.

  5. #5
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,530
    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.

  6. #6
    Senior Member xxxajk's Avatar
    Join Date
    Nov 2013
    Location
    Buffalo, NY USA
    Posts
    528
    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.

  7. #7
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,530
    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...

  8. #8
    Senior Member Jp3141's Avatar
    Join Date
    Nov 2012
    Posts
    458
    Quote Originally Posted by Theremingenieur View Post
    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:
    #define STEP 1 // tested up to 31, should go higher/*
    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 + 0.5);
    // 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++) {
        *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;   // 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++) {
        *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;   // 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++) {
        *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;   // effectively a modulo counter
        }
      }  
    }
    Last edited by Jp3141; 03-13-2014 at 03:40 AM. Reason: add 0.5

  9. #9
    Junior Member
    Join Date
    Mar 2014
    Posts
    4
    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!

  10. #10
    Junior Member
    Join Date
    Mar 2015
    Posts
    8
    Where abouts is the DACBFMD (DAC Buffer Work Mode Select) being set in DAC0_C1?
    Last edited by jpshea; 07-21-2015 at 02:16 AM.

  11. #11
    Junior Member
    Join Date
    Mar 2014
    Posts
    4
    Quote Originally Posted by jpshea View Post
    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.

  12. #12
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,530
    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)

  13. #13
    Senior Member+ manitou's Avatar
    Join Date
    Jan 2013
    Posts
    2,196
    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 by manitou; 04-13-2018 at 07:09 PM.

Tags for this Thread

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •