Writing Directly to SGTL5000 CODEC DACs

Neal

Well-known member
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
 
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.
 
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-Implementing-XY2-100-serial-protocol-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:
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 a moderator:
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.
 
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
 
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.
 
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
 
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.
 
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;
}
 
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.
 
Back
Top