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

Thread: Writing Directly to SGTL5000 CODEC DACs

  1. #1

    Writing Directly to SGTL5000 CODEC DACs

    I am new to the forum and in my application I am using a Teensy4.1 with the Audio Shield and attempting to do some digital signal processing for a class I am teaching.

    Has anyone come up with a simple way to write to the SGTL500 DAC outputs for testing purposes without using the full Audio Library functionality for passing data blocks? I know how to create an Audio Library elements that will do it, but for my application, I want to be able to update the DAC outputs at specific times that I control.

    What I would like to be able to do is this:

    AudioOutputI2S i2s1;
    AudioControlSGTL5000 sgtl5000_1;
    sgtl5000_1.enable();

    //////////////////////////
    //code to write to DACs
    //////////////////////////

    Any suggestions would be appreciated.

    Neal

  2. #2
    Senior Member
    Join Date
    Apr 2020
    Location
    Tucson
    Posts
    187
    The library isn't that big and the dac can't just be sent data whenever, in order for it to output a signal it has to have a constant flow of data. Why not make a minimal component using an output and two "DC" inputs? You can drive these at 5uS intervals (IOW way above sample rate) with hundreds of lines of code.

  3. #3
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    27,966
    Quote Originally Posted by Neal View Post
    Has anyone come up with a simple way to write to the SGTL500 DAC outputs for testing purposes
    That really depends upon what you consider "simple".

    Just yesterday I answered a question about transmitting a special 20 bit synchronous serial protocol by repurposing the digital audio hardware.

    https://forum.pjrc.com/threads/62819...-on-Teensy-4-1

    Maybe the code on msg #9 and #17 in that thread can help?

    To explain just briefly, the basic idea is to copy all the audio library code which configures the SAI port to transmit & receive in a particular format (and in that thread, bend it to achieve that special 20 bit protocol). But when you set up the SAI hardware, do *not* set the I2S_TCSR_FRDE bit which causes it to request DMA. See the "not using DMA" comment in that code on those 2 messages.

    Then when the SAI hardware is ready to go, instead of configuring a DMA channel, you use this:

    Code:
        attachInterruptVector(IRQ_SAI1, isr);
        NVIC_ENABLE_IRQ(IRQ_SAI1);
        I2S1_TCSR |= 1<<8;  // start generating TX FIFO interrupts
    Setting bit #8 in I2S1_TCSR causes the hardware to request interrupts when it wants more data. Your isr() function gets called, because of attachInterruptVector() and NVIC_ENABLE_IRQ().

    Inside isr(), you must write to I2S1_TDR0. If you've set up the hardware to transmit on multple pins, you must also write to I2S1_TDR1, I2S1_TDR2 or I2S1_TDR3 as needed. If you don't write to these TX FIFO registers as the hardware expects, it will keep asserting the interrupt request and your program will become an infinite loop of running only that interrupt function. For all practical purposes, your program crashes. Expect to need to press the pushbutton on your Teensy to initiate the next code upload, since that infinite looping will block servicing the USB interrupt. (if you're doing this as a class and expect students to implement it, be ready for this pain point....)

    Hopefully that answers your question, but I'm curious if you consider it "simple"?
    Last edited by PaulStoffregen; 09-23-2020 at 09:41 PM. Reason: typos

  4. #4
    Thanks for the replies. I think I should have explained better what I am doing better in my original post. I got something working last night that I will include for others to view.

    My goal is to have the DACs running continuously at the 44100kHz (will change later) rate. I want to have the DACs output continuously from a single 32bit memory location containing right/left data. The single output 32bit word in the code below is called myi2s_tx_buffer[0]. The output is continuous, and the value in myi2s_tx_buffer[0] can change asynchronously by other program elements. It needs more work, but it is functioning. The code I am using to control the i2s and DMA is extracted from the Audio Library output_i2s.cpp.

    The following are code excerpts from a simple stereo ramp generator I have running:
    Code:
    void setup()
    {
      Serial.begin(115200);
      CodecDAC_begin();
      sgtl5000_1.enable(); // sets up the CODEC chip
      sgtl5000_1.volume(.75); 
      while(1){
        myi2s_tx_buffer[0] += 0x01000200 ; // triangle ramp right and left; Dac currently outputs at 44.1 kHz update rate
        delayMicroseconds(10);
      }
    }
    
    void CodecDAC_config_i2s(void)
    {
        CCM_CCGR5 |= CCM_CCGR5_SAI1(CCM_CCGR_ON); //enables SAI1 clock in CCM_CCGR5 register
    
    //PLL:
      int fs = AUDIO_SAMPLE_RATE_EXACT;
        // PLL between 27*24 = 648MHz und 54*24=1296MHz
        int n1 = 4; //SAI prescaler 4 => (n1*n2) = multiple of 4
        int n2 = 1 + (24000000 * 27) / (fs * 256 * n1);
    
        double C = ((double)fs * 256 * n1 * n2) / 24000000;
        int c0 = C;
        int c2 = 10000;
        int c1 = C * c2 - (c0 * c2);
        set_audioClock(c0, c1, c2, false);
    
        // clear SAI1_CLK register locations
        CCM_CSCMR1 = (CCM_CSCMR1 & ~(CCM_CSCMR1_SAI1_CLK_SEL_MASK))
               | CCM_CSCMR1_SAI1_CLK_SEL(2); // &0x03 // (0,1,2): PLL3PFD0, PLL5, PLL4; first part clears all bits except SAI1_CLK_SEL; second part choosing PLL4 currently 
        CCM_CS1CDR = (CCM_CS1CDR & ~(CCM_CS1CDR_SAI1_CLK_PRED_MASK | CCM_CS1CDR_SAI1_CLK_PODF_MASK))
               | CCM_CS1CDR_SAI1_CLK_PRED(n1-1) // &0x07
               | CCM_CS1CDR_SAI1_CLK_PODF(n2-1); // &0x3f
    
        // Select MCLK
        IOMUXC_GPR_GPR1 = (IOMUXC_GPR_GPR1 // master clock is an output and something else?
            & ~(IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL_MASK))
            | (IOMUXC_GPR_GPR1_SAI1_MCLK_DIR | IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL(0));
    
        CORE_PIN23_CONFIG = 3;  //1:MCLK
        CORE_PIN21_CONFIG = 3;  //1:RX_BCLK
        CORE_PIN20_CONFIG = 3;  //1:RX_SYNC
    
        int rsync = 0;
        int tsync = 1;
    
        I2S1_TMR = 0; // no masking
        //I2S1_TCSR = (1<<25); //Reset
        I2S1_TCR1 = I2S_TCR1_RFW(1);
        I2S1_TCR2 = I2S_TCR2_SYNC(tsync) | I2S_TCR2_BCP // sync=0; tx is async;
                | (I2S_TCR2_BCD | I2S_TCR2_DIV((1)) | I2S_TCR2_MSEL(1));
        I2S1_TCR3 = I2S_TCR3_TCE;
        I2S1_TCR4 = I2S_TCR4_FRSZ((2-1)) | I2S_TCR4_SYWD((32-1)) | I2S_TCR4_MF
                | I2S_TCR4_FSD | I2S_TCR4_FSE | I2S_TCR4_FSP;
        I2S1_TCR5 = I2S_TCR5_WNW((32-1)) | I2S_TCR5_W0W((32-1)) | I2S_TCR5_FBT((32-1));
    
        I2S1_RMR = 0;
        //I2S1_RCSR = (1<<25); //Reset
        I2S1_RCR1 = I2S_RCR1_RFW(1);
        I2S1_RCR2 = I2S_RCR2_SYNC(rsync) | I2S_RCR2_BCP  // sync=0; rx is async;
                | (I2S_RCR2_BCD | I2S_RCR2_DIV((1)) | I2S_RCR2_MSEL(1));
        I2S1_RCR3 = I2S_RCR3_RCE;
        I2S1_RCR4 = I2S_RCR4_FRSZ((2-1)) | I2S_RCR4_SYWD((32-1)) | I2S_RCR4_MF
                | I2S_RCR4_FSE | I2S_RCR4_FSP | I2S_RCR4_FSD;
        I2S1_RCR5 = I2S_RCR5_WNW((32-1)) | I2S_RCR5_W0W((32-1)) | I2S_RCR5_FBT((32-1));
    }
    
    void CodecDAC_begin(void)
    {
        CodecDAC_dma.begin(true); // Allocate the DMA channel first
    
        CodecDAC_config_i2s();
    
        CORE_PIN7_CONFIG  = 3;  //1:TX_DATA0 pin 7 on uP
        CodecDAC_dma.TCD->SADDR = myi2s_tx_buffer; //source address
        CodecDAC_dma.TCD->SOFF = 2; // source buffer address increment per transfer in bytes
        CodecDAC_dma.TCD->ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // specifies 16 bit source and destination
        CodecDAC_dma.TCD->NBYTES_MLNO = 2; // bytes to transfer for each service request///////////////////////////////////////////////////////////////////
        CodecDAC_dma.TCD->SLAST = -sizeof(myi2s_tx_buffer); // last source address adjustment
        CodecDAC_dma.TCD->DOFF = 0; // increments at destination
        CodecDAC_dma.TCD->CITER_ELINKNO = sizeof(myi2s_tx_buffer) / 2;
        CodecDAC_dma.TCD->DLASTSGA = 0; // destination address offset
        CodecDAC_dma.TCD->BITER_ELINKNO = sizeof(myi2s_tx_buffer) / 2;
        CodecDAC_dma.TCD->CSR = DMA_TCD_CSR_INTHALF | DMA_TCD_CSR_INTMAJOR; // enables interrupt when transfers half and full complete 
        CodecDAC_dma.TCD->DADDR = (void *)((uint32_t)&I2S1_TDR0 + 2); // I2S1 register DMA writes to
        CodecDAC_dma.triggerAtHardwareEvent(DMAMUX_SOURCE_SAI1_TX); // i2s channel that will trigger the DMA transfer when ready for data
        CodecDAC_dma.enable();
    
        I2S1_RCSR |= I2S_RCSR_RE | I2S_RCSR_BCE;
        I2S1_TCSR = I2S_TCSR_TE | I2S_TCSR_BCE | I2S_TCSR_FRDE;
    
        CodecDAC_dma.attachInterrupt(CodecDAC_isr);
    }
    
    void CodecDAC_isr(void)
    {
        pinMode(3,OUTPUT);
        digitalToggleFast(3);
        
        CodecDAC_dma.clearInterrupt();
            delayMicroseconds(1); ///////////////////////need this but I don't know why yet, but problem without at least a 100 nsec delay?????
    }
    I need to spend more time digesting what is being proposed in your responses. Hopefully there is a even simpler solution. As I said, my approach needs more work.

    Cheers
    Neal
    Last edited by Frank B; 11-25-2020 at 07:25 PM. Reason: added code tags

  5. #5
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    27,966
    Quote Originally Posted by Neal View Post
    I want to have the DACs output continuously from a single 32bit memory location containing right/left data.
    First try a 16 bit location which will output both channels. To do that, just change SOFF and SLAST to zero, so the DMA doesn't ever change the source address. Then it will just read from the same 16 bits every time.

    Maybe also set CSR to zero rather than setting those 2 interrupt enable bits, if you want the DMA to just run without giving you any interrupts.

  6. #6
    Paul, both excellent suggestions. I left the SOFF and SLAST that way in case I wanted to use a larger buffer later. I left the completion interrupt in so I could monitor that things were running by toggling pin3 in the isr. That can easily go away now.

    Currently I am globally declaring the myi2s_tx_buffer[] like this:

    static volatile uint32_t myi2s_tx_buffer[1] = {0x00000000};

    I tried to do it the same as it is in the Audio Library like this:

    DMAMEM __attribute__((aligned(32))) static uint32_t myi2s_tx_buffer[1] = {0};

    It compiled OK, but myi2s_tx_buffer[0] never gets modified by myi2s_tx_buffer[0] += 0x01000200 instruction in the while loop.

    Do you have any idea why?

    Thanks for your help!
    Neal

  7. #7
    To answer my own question in my last post, I had to add a data cache management functions must be used to flush cached data after data after each output to the CODEC. The function I used I saw in the Audio Library output_i2s.cpp

    arm_dcache_flush_delete(myi2s_tx_buffer, sizeof(myi2s_tx_buffer));

    That allowed me to declare the tx buffer in DMAMEM.

  8. #8
    Hi Neal,

    Is your full code available somewhere?
    I'm familiarizing myself with with this level of T4's audio interface to achieve low latency i2s writes, and this looks like just the thing to start with.

    Thanks!!

    -Josh

  9. #9
    Not sure what you are asking for. The code in #4 above is the code required to set up the i2s communication with the audio adapter board. It configures the is2 port and the example sends a simple ramp signal to the left and right channel of the audio adapter board DAC.

  10. #10
    I figured it out. In the end, I had to add the set_audioClock() function and 1-2 other minor mods.
    Here's my complete sketch, tested on T4.1. I changed the sound to a square wave sweep, which is generated inside the ISR.

    Code:
    #include <Audio.h>
    #include <Wire.h>
    
    // Set up codec and DMA channel
    AudioControlSGTL5000 sgtl5000_1;
    DMAMEM __attribute__((aligned(32))) static uint16_t myi2s_tx_buffer[2] = {0};
    static DMAChannel CodecDAC_dma;
    
    // Audio square wave synth variables
    int8_t sign = 1; // Sign of current sample (+ or -)
    uint32_t signCounter = 0; // How many samples since last sign flip
    uint32_t Threshold = 441; // Sign flip threshold (at 44.1kHz, 441 = 50Hz square wave)
    boolean readyForNewSample = true; // Push new data on every second call to the interrupt service routine
    
    void setup()
    {
      CodecDAC_begin();
      sgtl5000_1.enable(); // sets up the CODEC chip
      sgtl5000_1.volume(.4); // Half volume for a waveform without saturation
    }
    
    void loop() {
    
    
    }
    
    FLASHMEM static void set_audioClock(int nfact, int32_t nmult, uint32_t ndiv, bool force) // sets PLL4
    {
      if (!force && (CCM_ANALOG_PLL_AUDIO & CCM_ANALOG_PLL_AUDIO_ENABLE)) return;
    
      CCM_ANALOG_PLL_AUDIO = CCM_ANALOG_PLL_AUDIO_BYPASS | CCM_ANALOG_PLL_AUDIO_ENABLE
               | CCM_ANALOG_PLL_AUDIO_POST_DIV_SELECT(2) // 2: 1/4; 1: 1/2; 0: 1/1
               | CCM_ANALOG_PLL_AUDIO_DIV_SELECT(nfact);
    
      CCM_ANALOG_PLL_AUDIO_NUM   = nmult & CCM_ANALOG_PLL_AUDIO_NUM_MASK;
      CCM_ANALOG_PLL_AUDIO_DENOM = ndiv & CCM_ANALOG_PLL_AUDIO_DENOM_MASK;
    
      CCM_ANALOG_PLL_AUDIO &= ~CCM_ANALOG_PLL_AUDIO_POWERDOWN;//Switch on PLL
      while (!(CCM_ANALOG_PLL_AUDIO & CCM_ANALOG_PLL_AUDIO_LOCK)) {}; //Wait for pll-lock
    
      const int div_post_pll = 1; // other values: 2,4
      CCM_ANALOG_MISC2 &= ~(CCM_ANALOG_MISC2_DIV_MSB | CCM_ANALOG_MISC2_DIV_LSB);
      if(div_post_pll>1) CCM_ANALOG_MISC2 |= CCM_ANALOG_MISC2_DIV_LSB;
      if(div_post_pll>3) CCM_ANALOG_MISC2 |= CCM_ANALOG_MISC2_DIV_MSB;
    
      CCM_ANALOG_PLL_AUDIO &= ~CCM_ANALOG_PLL_AUDIO_BYPASS;//Disable Bypass
    }
    
    void CodecDAC_config_i2s(void)
    {
        CCM_CCGR5 |= CCM_CCGR5_SAI1(CCM_CCGR_ON); //enables SAI1 clock in CCM_CCGR5 register
    
    //PLL:
      int fs = AUDIO_SAMPLE_RATE_EXACT;
        // PLL between 27*24 = 648MHz und 54*24=1296MHz
        int n1 = 4; //SAI prescaler 4 => (n1*n2) = multiple of 4
        int n2 = 1 + (24000000 * 27) / (fs * 256 * n1);
    
        double C = ((double)fs * 256 * n1 * n2) / 24000000;
        int c0 = C;
        int c2 = 10000;
        int c1 = C * c2 - (c0 * c2);
        set_audioClock(c0, c1, c2, false);
    
        // clear SAI1_CLK register locations
        CCM_CSCMR1 = (CCM_CSCMR1 & ~(CCM_CSCMR1_SAI1_CLK_SEL_MASK))
               | CCM_CSCMR1_SAI1_CLK_SEL(2); // &0x03 // (0,1,2): PLL3PFD0, PLL5, PLL4; first part clears all bits except SAI1_CLK_SEL; second part choosing PLL4 currently 
        CCM_CS1CDR = (CCM_CS1CDR & ~(CCM_CS1CDR_SAI1_CLK_PRED_MASK | CCM_CS1CDR_SAI1_CLK_PODF_MASK))
               | CCM_CS1CDR_SAI1_CLK_PRED(n1-1) // &0x07
               | CCM_CS1CDR_SAI1_CLK_PODF(n2-1); // &0x3f
    
        // Select MCLK
        IOMUXC_GPR_GPR1 = (IOMUXC_GPR_GPR1 // master clock is an output and something else?
            & ~(IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL_MASK))
            | (IOMUXC_GPR_GPR1_SAI1_MCLK_DIR | IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL(0));
    
        CORE_PIN23_CONFIG = 3;  //1:MCLK
        CORE_PIN21_CONFIG = 3;  //1:RX_BCLK
        CORE_PIN20_CONFIG = 3;  //1:RX_SYNC
    
        int rsync = 0;
        int tsync = 1;
    
        I2S1_TMR = 0; // no masking
        //I2S1_TCSR = (1<<25); //Reset
        I2S1_TCR1 = I2S_TCR1_RFW(1);
        I2S1_TCR2 = I2S_TCR2_SYNC(tsync) | I2S_TCR2_BCP // sync=0; tx is async;
                | (I2S_TCR2_BCD | I2S_TCR2_DIV((1)) | I2S_TCR2_MSEL(1));
        I2S1_TCR3 = I2S_TCR3_TCE;
        I2S1_TCR4 = I2S_TCR4_FRSZ((2-1)) | I2S_TCR4_SYWD((32-1)) | I2S_TCR4_MF
                | I2S_TCR4_FSD | I2S_TCR4_FSE | I2S_TCR4_FSP;
        I2S1_TCR5 = I2S_TCR5_WNW((32-1)) | I2S_TCR5_W0W((32-1)) | I2S_TCR5_FBT((32-1));
    
        I2S1_RMR = 0;
        //I2S1_RCSR = (1<<25); //Reset
        I2S1_RCR1 = I2S_RCR1_RFW(1);
        I2S1_RCR2 = I2S_RCR2_SYNC(rsync) | I2S_RCR2_BCP  // sync=0; rx is async;
                | (I2S_RCR2_BCD | I2S_RCR2_DIV((1)) | I2S_RCR2_MSEL(1));
        I2S1_RCR3 = I2S_RCR3_RCE;
        I2S1_RCR4 = I2S_RCR4_FRSZ((2-1)) | I2S_RCR4_SYWD((32-1)) | I2S_RCR4_MF
                | I2S_RCR4_FSE | I2S_RCR4_FSP | I2S_RCR4_FSD;
        I2S1_RCR5 = I2S_RCR5_WNW((32-1)) | I2S_RCR5_W0W((32-1)) | I2S_RCR5_FBT((32-1));
    }
    
    void CodecDAC_begin(void)
    {
        CodecDAC_dma.begin(true); // Allocate the DMA channel first
        CodecDAC_config_i2s();
        CORE_PIN7_CONFIG  = 3;  //1:TX_DATA0 pin 7 on uP
        CodecDAC_dma.TCD->SADDR = myi2s_tx_buffer; //source address
        CodecDAC_dma.TCD->SOFF = 2; // source buffer address increment per transfer in bytes
        CodecDAC_dma.TCD->ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // specifies 16 bit source and destination
        CodecDAC_dma.TCD->NBYTES_MLNO = 2; // bytes to transfer for each service request///////////////////////////////////////////////////////////////////
        CodecDAC_dma.TCD->SLAST = -sizeof(myi2s_tx_buffer); // last source address adjustment
        CodecDAC_dma.TCD->DOFF = 0; // increments at destination
        CodecDAC_dma.TCD->CITER_ELINKNO = sizeof(myi2s_tx_buffer) / 2;
        CodecDAC_dma.TCD->DLASTSGA = 0; // destination address offset
        CodecDAC_dma.TCD->BITER_ELINKNO = sizeof(myi2s_tx_buffer) / 2;
        CodecDAC_dma.TCD->CSR = DMA_TCD_CSR_INTHALF; //| DMA_TCD_CSR_INTMAJOR; // enables interrupt when transfers half complete. SET TO 0 to disable DMA interrupts
        CodecDAC_dma.TCD->DADDR = (void *)((uint32_t)&I2S1_TDR0 + 2); // I2S1 register DMA writes to
        CodecDAC_dma.triggerAtHardwareEvent(DMAMUX_SOURCE_SAI1_TX); // i2s channel that will trigger the DMA transfer when ready for data
        CodecDAC_dma.enable();
        I2S1_RCSR |= I2S_RCSR_RE | I2S_RCSR_BCE;
        I2S1_TCSR = I2S_TCSR_TE | I2S_TCSR_BCE | I2S_TCSR_FRDE;
        CodecDAC_dma.attachInterrupt(CodecDAC_isr);
    }
    
    void CodecDAC_isr(void) // DMA interrupt, called twice per sample. Audio data is pushed into the DMA channel source array on every second call.
    {
      if (readyForNewSample) {
        // Compute square waveform
        signCounter++;
        if (signCounter == Threshold) {
          sign *= -1;
          signCounter = 0;
          Threshold += 1;
        }
        if (Threshold == 441) {
          Threshold = 44;
        }
        int16_t sineVal = sign*32767;
      
        // Pass current sample to L+R audio buffers
        myi2s_tx_buffer[0] = sineVal; // Left Channel
        myi2s_tx_buffer[1] = sineVal; // Right Channel
      
        // Flush buffer and clear interrupt
        arm_dcache_flush_delete(myi2s_tx_buffer, sizeof(myi2s_tx_buffer));
        CodecDAC_dma.clearInterrupt();
      }
      readyForNewSample = 1-readyForNewSample;
    }

  11. #11
    Senior Member
    Join Date
    Feb 2021
    Posts
    172
    Thanks very much for the code examples in this thread, which I have used to send single L/R sample pairs from a T4.0 to the audio shield DAC in my application. Is it possible to modify this so that each time you send DAC samples out, you also read stereo input data from the ADC? If so, can anyone provide example code showing the needed changes? Thanks.

Posting Permissions

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