Triggered I2S

TeensyPhonon

Active member
Hi everyone,

Unfortunately, I require a very precise and repeatable method to initiate sound emission and reception on several Teensy 4.1 boards equipped with SPH0645 and MAX98357A I2S microphones and amplifiers.

Currently, using the existing audio library, I encounter approximately 3ms of jitter (random). This level of variation is far too high for my needs; I require an error margin of maximum ~20µs.

Therefore, I am considering reinventing the wheel by developing alternative code for I2S communication.

My initial thought is to create code that operates sample by sample (eliminating the need for DMA?), which could be triggered by one of the digital inputs.

What would be the most effective approach to achieve this? (I have reviewed various forum threads on I2S, but the topic remains somewhat cryptic to me.)
 
Interrupt driven code to pull samples from the I2S hardware. Reading up on the SAI units in the i.MX RT1060 manual (link in the T4 product pages). Some of the setup for these can probably be adapted from the AudioInputI2S class.
 
Last edited:
which could be triggered by one of the digital inputs.

This part of your goal simply is not realistic. These I2S audio parts require a continuous stream of audio data at a fixed sample rate. You can choose a different sample rate (within a range the hardware supports) and you can craft very low latency (but CPU inefficient) code on Teensy to handle each sample either by interrupt or polling, but you can't choose when to transmit or receive each individual sample.

The reason runs much deeper than merely the I2S protocol. If you look at the I2S timing diagrams, maybe you're imagining you could just stop BCLK and LRCLK when you don't want to send or receive data, and then give 16 or 32 or 64 clocks when you do want data. I'm here to tell you as clearly as possible, in the hope of saving you from wasting a lot of time, that definitely will not work.

The reason why involves the internal design of the ADC and DAC inside those parts. For many of these parts you get little or no info about how it's really designed internally. But you can be pretty sure virtually all of these parts are designed around sigma delta modulation. If you Google search, you can find an incredible amount of highly technical and usually very academic (heavy on equations, light on explanation) literature about sigma delta, noise shaping, modulator order vs oversampling ratio vs decimation factor vs filter requirements, and so on. It all depends on continuous (or steady sample rate) processing.

If you look at I2S and imagine "why not just transfer 1 sample when I want to", the reason is delta sigma modulation and digital filters.
 
Last edited:
So, the only way is to do continuous recording sample by sample and transfer them to some buffer when triggered? Or do i need to use analog stuff?
 
I don't understand "continuous recording sample by sample and transfer them to some buffer when triggered". Trying to answer your questions, but without some context I just don't know what you're asking by these words. In particular, a thing which is triggered is usually considered to be pretty much the opposite of continuous.
 
Sure, if you write code to transfer data from I2S using polling or interrupts (rather than DMA) you could pretty easily read any pin and use that result to discard the data.

With DMA, you can't do this as the data is collected. But after the fact, you could also just ignore and skip past data which did get written into the buffer.
 
Sorry for the beginner question, but I'm having trouble understanding how to retrieve data from I2S.

I noticed in the file i2s_input.cpp that data retrieval is accomplished with the line: dma.TCD->SADDR = (void *)((uint32_t)&I2S1_RDR0 + 2);

While I understand that this sets the origin of the DMA channel to the appropriate register, the right side of the expression is confusing to me, especially the "+2".

I attempted to read the data with the following code:

Code:
void Triggeredi2s::getData(uint32_t* ptrdata)
{
    ptrdata = (uint32_t*)((uint32_t)&I2S1_RDR0 + 2);
}

However, it seems that the value of ptrdata stay at 0...
 
Depending of the method I use, I obtain 0 or 65535 (=2^16-1) as result... I clearly don't really know what I am doing, however, I verified with my oscilloscope that the the i2s communication is active!

Header file :

C++:
#include <Arduino.h>
#include <imxrt.h>

#define IMXRT_CACHE_ENABLED 2 // 0=disabled, 1=WT, 2= WB

void audioClock(int nfact, int32_t nmult, uint32_t ndiv,  bool force = false);

class Triggeredi2s
{
public:
    Triggeredi2s(void) { begin(); }
    void begin(void);
  uint16_t getData(void);

private:
  static void config_i2s(bool only_bclk = false);
  static void isr(void);
  static uint16_t data_input;
};

cpp file :

C++:
#include "I2Sone_input.h"

uint16_t Triggeredi2s::data_input;

void Triggeredi2s::begin(void)
{
    config_i2s();
    CORE_PIN8_CONFIG  = 3;  //1:RX_DATA0
    IOMUXC_SAI1_RX_DATA0_SELECT_INPUT = 2;
    I2S1_RCSR = I2S_RCSR_RE | I2S_RCSR_BCE | I2S_RCSR_FRDE | I2S_RCSR_FR;
  attachInterruptVector(IRQ_SAI1, isr);
  NVIC_ENABLE_IRQ(IRQ_SAI1);
}



void Triggeredi2s::isr()
{
  memcpy(&data_input,(void*)((uint32_t)&I2S1_RDR0 + 2),2);
}



uint16_t Triggeredi2s::getData()
{
  return data_input;
}



void Triggeredi2s::config_i2s(bool only_bclk)
{

    CCM_CCGR5 |= CCM_CCGR5_SAI1(CCM_CCGR_ON);

    // if either transmitter or receiver is enabled, do nothing
    if ((I2S1_TCSR & I2S_TCSR_TE) != 0 || (I2S1_RCSR & I2S_RCSR_RE) != 0)
    {
      if (!only_bclk) // if previous transmitter/receiver only activated BCLK, activate the other clock pins now
      {
        CORE_PIN23_CONFIG = 3;  //1:MCLK
        CORE_PIN20_CONFIG = 3;  //1:RX_SYNC (LRCLK)
      }
      return ;
    }

    //PLL:
    int fs = 44100;
    // 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);
    audioClock(c0, c1, c2);

    // 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
    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
        & ~(IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL_MASK))
        | (IOMUXC_GPR_GPR1_SAI1_MCLK_DIR | IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL(0));

    if (!only_bclk)
    {
      CORE_PIN23_CONFIG = 3;  //1:MCLK
      CORE_PIN20_CONFIG = 3;  //1:RX_SYNC  (LRCLK)
    }
    CORE_PIN21_CONFIG = 3;  //1:RX_BCLK

    int rsync = 0;
    int tsync = 1;

    I2S1_TMR = 0;
    //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));

}


FLASHMEM
void 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
}

Arduino code :

C-like:
#include "I2Sone_input.h"

uint16_t data;
Triggeredi2s i2scom;

void setup() {
  Serial.begin(115200);
  i2scom.begin();
  pinMode(13,OUTPUT);
}

void loop() {
  digitalWrite(13,HIGH);
  data = i2scom.getData();
  digitalWrite(13,LOW);

  Serial.println(data);
}
 
Add volatile to "uint16_t Triggeredi2s::data_input;"

Without volatile, the compiler believes it is an ordinary variable in RAM. The compiler applies a lot of optimizations that assume data in RAM doesn't just magically change, because that's what RAM does... reliably store data. But when an interrupt writes to RAM, the contents do change all one their own. The volatile keyword is how to tell the compiler to always actually read rather than trying to be smart about optimizing away reads that it knows will always give the same data.
 
I corrected the mistake with volatile. (I also forgot to activate the interrupt...). However, I'm still obtaining 0:

C++:
#include "I2Sone_input.h"

volatile uint16_t data_input;

void Triggeredi2s::begin(void)
{
    config_i2s(true);
    CORE_PIN8_CONFIG  = 3;  //1:RX_DATA0
    IOMUXC_SAI1_RX_DATA0_SELECT_INPUT = 2;
    I2S1_RCSR = I2S_RCSR_RE | I2S_RCSR_BCE | I2S_RCSR_FRDE | I2S_RCSR_FR;
  attachInterruptVector(IRQ_SAI1, isr);
  NVIC_ENABLE_IRQ(IRQ_SAI1);
  I2S1_RCSR |= 1<<8;
}



void Triggeredi2s::isr()
{
  volatile byte data1 = *(byte*)((uint32_t)&I2S1_RDR0 + 2);
  volatile byte data2 = *(byte*)((uint32_t)&I2S1_RDR0 + 3);

  data_input = ((uint16_t)data2 << 8) | data1;
}



uint16_t Triggeredi2s::getData()
{
  return data_input;
}



void Triggeredi2s::config_i2s(bool only_bclk)
{

    CCM_CCGR5 |= CCM_CCGR5_SAI1(CCM_CCGR_ON);

    // if either transmitter or receiver is enabled, do nothing
    if ((I2S1_TCSR & I2S_TCSR_TE) != 0 || (I2S1_RCSR & I2S_RCSR_RE) != 0)
    {
      if (!only_bclk) // if previous transmitter/receiver only activated BCLK, activate the other clock pins now
      {
        CORE_PIN23_CONFIG = 3;  //1:MCLK
        CORE_PIN20_CONFIG = 3;  //1:RX_SYNC (LRCLK)
      }
      return ;
    }

    //PLL:
    int fs = 44100;
    // 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);
    audioClock(c0, c1, c2);

    // 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
    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
        & ~(IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL_MASK))
        | (IOMUXC_GPR_GPR1_SAI1_MCLK_DIR | IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL(0));

    if (!only_bclk)
    {
      CORE_PIN23_CONFIG = 3;  //1:MCLK
      CORE_PIN20_CONFIG = 3;  //1:RX_SYNC  (LRCLK)
    }
    CORE_PIN21_CONFIG = 3;  //1:RX_BCLK

    int rsync = 0;
    int tsync = 1;

    I2S1_TMR = 0;
    //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));

}


FLASHMEM
void 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
}

If i replace "data_input = ((uint16_t)data2 << 8) | data1;" by "data_input = rand();" it is working as expected though...
 
It works very well now!

But... I still have a doubt: I don't fully understand how stereo works in the I2S audio library. To clarify, I don't see where the two channels are represented in the I2S registers (I2S1_RDR0 and I2S1_TDR0).

Since I only want mono, I'm uncertain if the data is truly mono...

C++:
#include <Arduino.h>
#include <imxrt.h>
#include <arm_const_structs.h>

#define IMXRT_CACHE_ENABLED 2 // 0=disabled, 1=WT, 2= WB

void audioClock(int nfact, int32_t nmult, uint32_t ndiv,  bool force = false);

class Triggeredi2s
{
public:
    Triggeredi2s(void) { begin(); }
    void begin(void);
  float32_t* getData(void);
  void playData(float32_t* signal);

private:
  static void config_i2s(bool only_bclk = false);
  static void isr(void);
};

C++:
#include "I2Sone_input.h"

const uint32_t N = 10000;
uint32_t buffer_ind;
volatile uint16_t data_input;
float32_t input_buffer[N];
q15_t output_buffer[N];

void Triggeredi2s::begin(void)
{
    config_i2s(false);

  //input
    CORE_PIN8_CONFIG  = 3;  //1:RX_DATA0
    IOMUXC_SAI1_RX_DATA0_SELECT_INPUT = 2;
    I2S1_RCSR = I2S_RCSR_RE | I2S_RCSR_BCE | I2S_RCSR_FRDE | I2S_RCSR_FR;
  I2S1_RCSR |= 1<<8;

  //output
  CORE_PIN7_CONFIG  = 3;  //1:TX_DATA0
  I2S1_TCSR = I2S_TCSR_TE | I2S_TCSR_BCE | I2S_TCSR_FRDE;
  I2S1_TCSR |= 1<<8;

  attachInterruptVector(IRQ_SAI1, isr);
  NVIC_ENABLE_IRQ(IRQ_SAI1);
}



void Triggeredi2s::isr()
{
  //input
  volatile char data1 = *(char*)((uint32_t)&I2S1_RDR0 + 2);
  volatile char data2 = *(char*)((uint32_t)&I2S1_RDR0 + 3);

  data_input = ((uint16_t)data2 << 8) | data1;
  input_buffer[buffer_ind] = 2*(float32_t)data_input/65535-1;

  //output
  char highByte = (output_buffer[buffer_ind] >> 8) & 0xFF;
  char lowByte = output_buffer[buffer_ind] & 0xFF;
  *(char*)((uint32_t)&I2S1_TDR0 + 2) = lowByte;
  *(char*)((uint32_t)&I2S1_TDR0 + 3) = highByte;
 
  buffer_ind++;
  buffer_ind %= N;
 
}



float32_t* Triggeredi2s::getData()
{
  return input_buffer;
}

void Triggeredi2s::playData(float32_t* signal)
{
  arm_float_to_q15(signal,output_buffer,N);
}



void Triggeredi2s::config_i2s(bool only_bclk)
{

    CCM_CCGR5 |= CCM_CCGR5_SAI1(CCM_CCGR_ON);

    // if either transmitter or receiver is enabled, do nothing
    if ((I2S1_TCSR & I2S_TCSR_TE) != 0 || (I2S1_RCSR & I2S_RCSR_RE) != 0)
    {
      if (!only_bclk) // if previous transmitter/receiver only activated BCLK, activate the other clock pins now
      {
        CORE_PIN23_CONFIG = 3;  //1:MCLK
        CORE_PIN20_CONFIG = 3;  //1:RX_SYNC (LRCLK)
      }
      return ;
    }

    //PLL:
    int fs = 44100;
    // 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);
    audioClock(c0, c1, c2);

    // 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
    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
        & ~(IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL_MASK))
        | (IOMUXC_GPR_GPR1_SAI1_MCLK_DIR | IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL(0));

    if (!only_bclk)
    {
      CORE_PIN23_CONFIG = 3;  //1:MCLK
      CORE_PIN20_CONFIG = 3;  //1:RX_SYNC  (LRCLK)
    }
    CORE_PIN21_CONFIG = 3;  //1:RX_BCLK

    int rsync = 0;
    int tsync = 1;

    I2S1_TMR = 0;
    //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));

}


FLASHMEM
void 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
}
 
Alright, I've implemented the triggering parts, but I'm getting some very strange results... The values I'm seeing in the isr function are different from the values I obtain from the getData function!

New code:

C++:
#include "I2Sone_input.h"

volatile uint32_t buffer_ind;
volatile uint32_t data_input;
volatile float32_t input_buffer[DATA_SIZE];
uint16_t output_buffer[DATA_SIZE];
int fs = 44100;
volatile bool triggered = false;
volatile bool data_complete = false;

void Triggeredi2s::begin(void)
{
    config_i2s(false);

  //input
    CORE_PIN8_CONFIG  = 3;  //1:RX_DATA0
    IOMUXC_SAI1_RX_DATA0_SELECT_INPUT = 2;
    I2S1_RCSR = I2S_RCSR_RE | I2S_RCSR_BCE | I2S_RCSR_FRDE | I2S_RCSR_FR;
  I2S1_RCSR |= 1<<8;
  /*
  //output
  CORE_PIN7_CONFIG  = 3;  //1:TX_DATA0
  I2S1_TCSR = I2S_TCSR_TE | I2S_TCSR_BCE | I2S_TCSR_FRDE;
  I2S1_TCSR |= 1<<8;
  */

  attachInterruptVector(IRQ_SAI1, isr);
  NVIC_ENABLE_IRQ(IRQ_SAI1);

  attachInterrupt(digitalPinToInterrupt(0), trigISR, CHANGE);
}



void Triggeredi2s::isr()
{
  //input
  volatile char data1 = *(char*)((uint32_t)&I2S1_RDR0 + 0);
  volatile char data2 = *(char*)((uint32_t)&I2S1_RDR0 + 1);
  volatile char data3 = *(char*)((uint32_t)&I2S1_RDR0 + 2);
  volatile char data4 = *(char*)((uint32_t)&I2S1_RDR0 + 3);

  data_input = ((uint32_t)data4 << 24)|((uint32_t)data3 << 16)|((uint32_t)data2 << 8)|(uint32_t)data1;
  //Serial.println(input_buffer[buffer_ind]);
  //output
  I2S1_TDR0 = (uint32_t)output_buffer[buffer_ind]*0xffff;
 
  buffer_ind++;
  buffer_ind %= DATA_SIZE;
 
  if(triggered && (buffer_ind==DATA_SIZE-1)){data_complete = true;}
  else if(triggered){input_buffer[buffer_ind] = 2*(float32_t)data_input/0xffffffff-1;}
 
}



volatile float32_t* Triggeredi2s::getData()
{
  while(!triggered);
  while(!data_complete);
  triggered = false;
  data_complete = false;
  return input_buffer;
}

void Triggeredi2s::playData(uint16_t* signal)
{
  while(!triggered);

  for(int i=0;i<DATA_SIZE;i++){
    output_buffer[i] = signal[i];
  }

  while(buffer_ind<DATA_SIZE-1);

  for(int i=0;i<DATA_SIZE;i++){
    output_buffer[i] = 0;
  }

  triggered = false;
}


void Triggeredi2s::set_SampligFreq(int freq)
{
  fs = freq;
}


void Triggeredi2s::trigISR()
{
  triggered = true;
  buffer_ind = 0;
}


void Triggeredi2s::config_i2s(bool only_bclk)
{

    CCM_CCGR5 |= CCM_CCGR5_SAI1(CCM_CCGR_ON);

    // if either transmitter or receiver is enabled, do nothing
    if ((I2S1_TCSR & I2S_TCSR_TE) != 0 || (I2S1_RCSR & I2S_RCSR_RE) != 0)
    {
      if (!only_bclk) // if previous transmitter/receiver only activated BCLK, activate the other clock pins now
      {
        CORE_PIN23_CONFIG = 3;  //1:MCLK
        CORE_PIN20_CONFIG = 3;  //1:RX_SYNC (LRCLK)
      }
      return ;
    }

    //PLL:
    // PLL between 27*24 = 648MHz und 54*24=1296MHz
  int fs2 = fs/2;
    int n1 = 4; //SAI prescaler 4 => (n1*n2) = multiple of 4
    int n2 = 1 + (24000000 * 27) / (fs2 * 256 * n1);

    double C = ((double)fs2 * 256 * n1 * n2) / 24000000;
    int c0 = C;
    int c2 = 10000;
    int c1 = C * c2 - (c0 * c2);
    audioClock(c0, c1, c2);

    // 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
    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
        & ~(IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL_MASK))
        | (IOMUXC_GPR_GPR1_SAI1_MCLK_DIR | IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL(0));

    if (!only_bclk)
    {
      CORE_PIN23_CONFIG = 3;  //1:MCLK
      CORE_PIN20_CONFIG = 3;  //1:RX_SYNC  (LRCLK)
    }
    CORE_PIN21_CONFIG = 3;  //1:RX_BCLK

    int rsync = 0;
    int tsync = 1;

    I2S1_TMR = 0;
    //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));

}


FLASHMEM
void 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
}
 
Yes, I know... but this seems to be the only way it even works... At least the values obtained with Serial.print() in the ISR seem correct (I tested it by emitting a sine wave with another speaker).

Also, I didn't mention this earlier, but the values returned by getData represent a sawtooth wave (ranging between -1 and 1).
Currently, I suspect that something strange might be happening with stereo (when I use mono speakers).

Nevertheless, the triggering functionality is working!
 
I just tested something, and it's very strange: if I place "Serial.println(input_buffer[buffer_ind]);" before the line "I2S1_TDR0 = (uint32_t)output_buffer[buffer_ind]*0xffff;", the data are correct. However, if I place it after, I obtain the sawtooth pattern again.

This doesn't make any sense to me...
 
I understand that you mentioned it's impossible to trigger I2S due to sigma-delta modulation, but I'm unsure if I really understood what I want:

I'm not aiming to trigger I2S for every sample; rather, I only want the initiation of data transmission to be triggered. To clarify, I want the Teensy to commence recording/transmiting upon receiving the trigger and capture/transmit, for example, 12800 points at a fixed sample rate.

Is there really no method to achieve this using the audio library?
 
The audio library is designed for continuous streaming. The existing software simply doesn't have the feature you want (or at least my not-so-clear understanding of what you want).

if I place "Serial.println(input_buffer[buffer_ind]);" before the line "I2S1_TDR0 = (uint32_t)output_buffer[buffer_ind]*0xffff;", the data are correct. However, if I place it after, I obtain the sawtooth pattern again.

This doesn't make any sense to me...

I could speculate about what might be wrong, but I doubt it will help. Here goes anyway...

There's a reason for use of DMA and buffers to collect many samples. When you try to interrupt on each individual sample, you're asking the CPU to run your function every ~21 microseconds. If you just put the data into a buffer, maybe check a pin to decide whether to do so, you'll probably always complete the work soon enough that you're ready for the next interrupt. But doing something like Serial.print() takes time, and the time taken can vary depending on the state of the USB buffers.

Remember, it's not just your code that matters. Any timers or communication (like USB) or libraries using interrupts also eat into your timing margin.

With DMA collecting 128 samples, an interrupt is needed every 2.9ms. That's (usually) plenty of time. Even if other interrupts and libraries eat up 100us or more, you've still got plenty of timing margin before you've waited 2.9ms and getting to the point where the next interrupt would occur.

Working with code that requires interrupt latency always well under 20us is tough. Not impossible, but certainly quite difficult.
 
Also, is it possible to stop i2s/dma and restart it when needed, so everything start at the same time?

I tried to manipulate I2S1_RCSR and I2S1_TCSR but it seems to break everything.

Actually, my problem is a little bit like trying to measure the speed of sound very precisely.
 
Last edited:
I found something!

I modified the I2S audio classes by removing the call to begin() in the lines:

Code:
AudioInputI2S(void) : AudioStream(0, NULL) { begin(); }
AudioOutputputI2S(void) : AudioStream(0, NULL) { begin(); }

And called begin only after triggered with:

Code:
digitalWrite(1,LOW);
        if(i2s_begin){
          i2s_in.begin();
          i2s_out.begin();
          i2s_begin = false;
        }

        Q_in_L.clear();
        
        for (i = 0; i < nfor; i++) {
          while(Q_in_L.available() < 1);
          sp_L = Q_in_L.readBuffer();
          arm_q15_to_float (sp_L, &float_buffer_L[partitionsize * i], partitionsize);
          Q_in_L.freeBuffer();
        }

For the "master" teensy.

Code:
while(digitalRead(0) == HIGH);
        if(i2s_begin){
          i2s_in.begin();
          i2s_out.begin();
          i2s_begin = false;
        }
        
        for (i = 0; i < nfor; i++) {
          sp_L = Q_out_L.getBuffer();
          arm_float_to_q15 (&chirp[partitionsize * i], sp_L, partitionsize);
          Q_out_L.playBuffer();
        }

For the "slave" teensy. The i2s_begin variable prevents the begin() function from being called twice.

As you see on this graphic, it is almost there:

almost_trig.png


The latest issue is an occasional shift of 128 samples...
 
This time, I think it is good enough!

triggered.png


To do this, I a added a delay of 1/4 of buffer :

Code:
delay(100);
digitalWrite(1,LOW);
i2s_in.begin();
i2s_out.begin();
delay(100);
digitalWrite(1,HIGH);

Code:
while(digitalRead(0) == HIGH);
delayMicroseconds(partitionsize/4/SAMPLE_RATE*1e6);
i2s_in.begin();
i2s_out.begin();

The only problem is that it seems to move a little bit over time (not really a problem for me).

It might be cool to modify the audio library so we have the option to not start I2S when the AudioInputI2S object is created.
 
Back
Top